From 9ccf84e219cb2a36834e597bae1675b1bb224e12 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Jan 2026 10:58:04 +0100 Subject: [PATCH] frontend-plugin-api: move app blueprints to new app-react package Signed-off-by: Patrik Oldsberg --- .changeset/bright-pumas-cut.md | 6 + .changeset/common-heads-start.md | 13 + .changeset/curvy-zoos-drop.md | 5 + .changeset/slick-beans-relax.md | 13 + .changeset/warm-pets-accept.md | 13 + .../building-apps/08-migrating.md | 14 +- .../03-common-extension-blueprints.md | 10 + .../06-swappable-components.md | 7 +- packages/frontend-plugin-api/report.api.md | 14 +- .../src/blueprints/IconBundleBlueprint.ts | 5 +- .../src/blueprints/NavContentBlueprint.ts | 1 + .../src/blueprints/RouterBlueprint.tsx | 5 +- .../src/blueprints/SignInPageBlueprint.tsx | 1 + .../blueprints/SwappableComponentBlueprint.ts | 1 + .../src/blueprints/ThemeBlueprint.ts | 1 + .../src/blueprints/TranslationBlueprint.ts | 1 + plugins/app-backend/package.json | 3 +- plugins/app-node/package.json | 3 +- plugins/app-react/.eslintrc.js | 1 + plugins/app-react/README.md | 5 + plugins/app-react/catalog-info.yaml | 10 + plugins/app-react/package.json | 68 +++++ plugins/app-react/report.api.md | 234 ++++++++++++++++++ plugins/app-react/src/blueprints/index.ts | 98 ++++++++ plugins/app-react/src/index.ts | 23 ++ plugins/app-react/src/setupTests.ts | 16 ++ plugins/app/package.json | 4 +- plugins/app/src/extensions/AppNav.tsx | 8 + plugins/app/src/extensions/AppRoot.tsx | 16 ++ plugins/app/src/extensions/AppThemeApi.tsx | 20 +- plugins/app/src/extensions/IconsApi.ts | 20 +- .../app/src/extensions/TranslationsApi.tsx | 20 +- yarn.lock | 28 +++ 33 files changed, 653 insertions(+), 34 deletions(-) create mode 100644 .changeset/bright-pumas-cut.md create mode 100644 .changeset/common-heads-start.md create mode 100644 .changeset/curvy-zoos-drop.md create mode 100644 .changeset/slick-beans-relax.md create mode 100644 .changeset/warm-pets-accept.md create mode 100644 plugins/app-react/.eslintrc.js create mode 100644 plugins/app-react/README.md create mode 100644 plugins/app-react/catalog-info.yaml create mode 100644 plugins/app-react/package.json create mode 100644 plugins/app-react/report.api.md create mode 100644 plugins/app-react/src/blueprints/index.ts create mode 100644 plugins/app-react/src/index.ts create mode 100644 plugins/app-react/src/setupTests.ts diff --git a/.changeset/bright-pumas-cut.md b/.changeset/bright-pumas-cut.md new file mode 100644 index 0000000000..3cb4d7d0b5 --- /dev/null +++ b/.changeset/bright-pumas-cut.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-app-backend': patch +'@backstage/plugin-app-node': patch +--- + +Updated plugin metadata. diff --git a/.changeset/common-heads-start.md b/.changeset/common-heads-start.md new file mode 100644 index 0000000000..15ef218b12 --- /dev/null +++ b/.changeset/common-heads-start.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-app-react': patch +--- + +Moved the following blueprints from `@backstage/frontend-plugin-api`: + +- `IconBundleBlueprint` +- `NavContentBlueprint` +- `RouterBlueprint` +- `SignInPageBlueprint` +- `SwappableComponentBlueprint` +- `ThemeBlueprint` +- `TranslationBlueprint` diff --git a/.changeset/curvy-zoos-drop.md b/.changeset/curvy-zoos-drop.md new file mode 100644 index 0000000000..b58eff1065 --- /dev/null +++ b/.changeset/curvy-zoos-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-react': minor +--- + +Initial release of this web library for `@backstage/plugin-app`. diff --git a/.changeset/slick-beans-relax.md b/.changeset/slick-beans-relax.md new file mode 100644 index 0000000000..b4fc3c3175 --- /dev/null +++ b/.changeset/slick-beans-relax.md @@ -0,0 +1,13 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +The following blueprints are being restricted to only be used in app plugin overrides and modules. They are being moved to the `@backstage/plugin-app-react` package and have been deprecated: + +- `IconBundleBlueprint` +- `NavContentBlueprint` +- `RouterBlueprint` +- `SignInPageBlueprint` +- `SwappableComponentBlueprint` +- `ThemeBlueprint` +- `TranslationBlueprint` diff --git a/.changeset/warm-pets-accept.md b/.changeset/warm-pets-accept.md new file mode 100644 index 0000000000..5bf62cbb1a --- /dev/null +++ b/.changeset/warm-pets-accept.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-app': patch +--- + +The following blueprints are being restricted to only be used in app plugin overrides and modules. They will now produce a deprecation warning when used outside of the app plugin: + +- `IconBundleBlueprint` +- `NavContentBlueprint` +- `RouterBlueprint` +- `SignInPageBlueprint` +- `SwappableComponentBlueprint` +- `ThemeBlueprint` +- `TranslationBlueprint` diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 88dff704c4..26dc3d085c 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -444,7 +444,7 @@ const app = createApp({ Can be converted to the following extension: ```tsx -import { SignInPageBlueprint } from '@backstage/frontend-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; const signInPage = SignInPageBlueprint.make({ params: { @@ -492,7 +492,7 @@ const app = createApp({ Can be converted to the following extension: ```tsx -import { ThemeBlueprint } from '@backstage/frontend-plugin-api'; +import { ThemeBlueprint } from '@backstage/plugin-app-react'; const customLightThemeExtension = ThemeBlueprint.make({ name: 'custom-light', @@ -535,7 +535,7 @@ const app = createApp({ Icons are now installed as extensions, using the `IconBundleBlueprint` to make new instances which can be added to the app. ```ts -import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; +import { IconBundleBlueprint } from '@backstage/plugin-app-react'; const exampleIconBundle = IconBundleBlueprint.make({ name: 'example-bundle', @@ -586,10 +586,8 @@ Can be converted to the following extension: ```tsx import { catalogTranslationRef } from '@backstage/plugin-catalog/alpha'; -import { - createTranslationMessages, - TranslationBlueprint, -} from '@backstage/frontend-plugin-api'; +import { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; const catalogTranslations = TranslationBlueprint.make({ name: 'catalog-overrides', @@ -705,7 +703,7 @@ export const navModule = createFrontendModule({ Then in the actual implementation for the `SidebarContent` extension, you can provide something like the following, where you implement the entire `Sidebar` component. ```tsx title="in packages/app/src/modules/nav/Sidebar.tsx" -import { NavContentBlueprint } from '@backstage/frontend-plugin-api'; +import { NavContentBlueprint } from '@backstage/plugin-app-react'; export const SidebarContent = NavContentBlueprint.make({ params: { diff --git a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md index abac1f03f5..df83c696f6 100644 --- a/docs/frontend-system/building-plugins/03-common-extension-blueprints.md +++ b/docs/frontend-system/building-plugins/03-common-extension-blueprints.md @@ -23,6 +23,8 @@ Navigation item extensions are used to provide menu items that link to different Page extensions provide content for a particular route in the app. By default pages are attached to the app routes extensions, which renders the root routes. +## Extension blueprints in `@backstage/plugin-app-react` + ### SignInPage - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.SignInPageBlueprint.html) Sign-in page extension have a single purpose - to implement a custom sign-in page. They are always attached to the app root extension and are rendered before the rest of the app until the user is signed in. @@ -43,6 +45,14 @@ Icon bundle extensions provide the ability to replace or provide new icons to th Translation extension provide custom translation messages for the app. They can be used both to override the default english messages to custom ones, as well as provide translations for additional languages. +### NavContent - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.NavContentBlueprint.html) + +Nav content extensions allow you to replace the entire navbar with your own component. They are always attached to the app nav extension. + +### Router - [Reference](https://backstage.io/api/stable/variables/_backstage_frontend-plugin-api.RouterBlueprint.html) + +Router extensions allow you to replace the router component used by the app. They are always attached to the app root extension. + ## Extension blueprints in `@backstage/plugin-catalog-react/alpha` These are the [extension blueprints](../architecture/23-extension-blueprints.md) provided by the Catalog plugin. diff --git a/docs/frontend-system/building-plugins/06-swappable-components.md b/docs/frontend-system/building-plugins/06-swappable-components.md index a26396bbff..786388b3c8 100644 --- a/docs/frontend-system/building-plugins/06-swappable-components.md +++ b/docs/frontend-system/building-plugins/06-swappable-components.md @@ -52,11 +52,8 @@ In order to override a Swappable Component, you need to create a `SwappableCompo There are two different ways to add extensions to the `app` plugin, both are documented below in an example of overriding the `Progress` Swappable Component. ```tsx title="in packages/app/src/App.tsx" -import { - Progress, - SwappableComponentBlueprint, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; +import { Progress, createFrontendModule } from '@backstage/frontend-plugin-api'; +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { MyCustomProgress } from './CustomProgress'; import { createApp } from '@backstage/frontend-defaults'; import appPlugin from '@backstage/plugin-app'; diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index fbe7110d80..ceba08d7c3 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -1457,7 +1457,7 @@ export const googleAuthApiRef: ApiRef< SessionApi >; -// @public (undocumented) +// @public @deprecated (undocumented) export const IconBundleBlueprint: ExtensionBlueprint_2<{ kind: 'icon-bundle'; params: { @@ -1522,7 +1522,7 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public +// @public @deprecated export const NavContentBlueprint: ExtensionBlueprint_2<{ kind: 'nav-content'; params: { @@ -1923,7 +1923,7 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; -// @public (undocumented) +// @public @deprecated (undocumented) export const RouterBlueprint: ExtensionBlueprint_2<{ kind: 'app-router-component'; params: { @@ -1998,7 +1998,7 @@ export namespace SessionState { export type SignedOut = typeof SessionState.SignedOut; } -// @public +// @public @deprecated export const SignInPageBlueprint: ExtensionBlueprint_2<{ kind: 'sign-in-page'; params: { @@ -2066,7 +2066,7 @@ export interface SubRouteRef< readonly T: TParams; } -// @public +// @public @deprecated export const SwappableComponentBlueprint: ExtensionBlueprint_2<{ kind: 'component'; params: >(params: { @@ -2150,7 +2150,7 @@ export interface SwappableComponentsApi { // @public export const swappableComponentsApiRef: ApiRef_2; -// @public +// @public @deprecated export const ThemeBlueprint: ExtensionBlueprint_2<{ kind: 'theme'; params: { @@ -2186,7 +2186,7 @@ export type TranslationApi = { // @public (undocumented) export const translationApiRef: ApiRef; -// @public +// @public @deprecated export const TranslationBlueprint: ExtensionBlueprint_2<{ kind: 'translation'; params: { diff --git a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts index 303d1a3fd2..2f52f7590a 100644 --- a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts @@ -21,7 +21,10 @@ const iconsDataRef = createExtensionDataRef<{ [key in string]: IconComponent; }>().with({ id: 'core.icons' }); -/** @public */ +/** + * @public + * @deprecated Use {@link @backstage/plugin-app-react#IconBundleBlueprint} instead. + */ export const IconBundleBlueprint = createExtensionBlueprint({ kind: 'icon-bundle', attachTo: { id: 'api:app/icons', input: 'icons' }, diff --git a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts index a6524ea49e..f56de2032b 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts @@ -60,6 +60,7 @@ const componentDataRef = createExtensionDataRef().with({ * Creates an extension that replaces the entire nav bar with your own component. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#NavContentBlueprint} instead. */ export const NavContentBlueprint = createExtensionBlueprint({ kind: 'nav-content', diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx index c4b7c16bd6..88096cf16a 100644 --- a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx @@ -21,7 +21,10 @@ const componentDataRef = createExtensionDataRef< (props: { children: ReactNode }) => JSX.Element | null >().with({ id: 'app.router.wrapper' }); -/** @public */ +/** + * @public + * @deprecated Use {@link @backstage/plugin-app-react#RouterBlueprint} instead. + */ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', attachTo: { id: 'app/root', input: 'router' }, diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx index b28a0108ca..9f51e8f666 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -44,6 +44,7 @@ const componentDataRef = createExtensionDataRef< * Creates an extension that replaces the sign in page. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#SignInPageBlueprint} instead. */ export const SignInPageBlueprint = createExtensionBlueprint({ kind: 'sign-in-page', diff --git a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts index b72d14d87f..38e333826a 100644 --- a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts @@ -31,6 +31,7 @@ export const componentDataRef = createExtensionDataRef<{ * Blueprint for creating swappable components from a SwappableComponentRef and a loader * * @public + * @deprecated Use {@link @backstage/plugin-app-react#SwappableComponentBlueprint} instead. */ export const SwappableComponentBlueprint = createExtensionBlueprint({ kind: 'component', diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index aaf4f32608..f93075181e 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -25,6 +25,7 @@ const themeDataRef = createExtensionDataRef().with({ * Creates an extension that adds/replaces an app theme. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#ThemeBlueprint} instead. */ export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index dd4a9ddc09..491053d763 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -25,6 +25,7 @@ const translationDataRef = createExtensionDataRef< * Creates an extension that adds translations to your app. * * @public + * @deprecated Use {@link @backstage/plugin-app-react#TranslationBlueprint} instead. */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 9ab956713f..73ff162919 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -8,7 +8,8 @@ "pluginPackages": [ "@backstage/plugin-app", "@backstage/plugin-app-backend", - "@backstage/plugin-app-node" + "@backstage/plugin-app-node", + "@backstage/plugin-app-react" ] }, "publishConfig": { diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index c41733eb04..3641fbade9 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -8,7 +8,8 @@ "pluginPackages": [ "@backstage/plugin-app", "@backstage/plugin-app-backend", - "@backstage/plugin-app-node" + "@backstage/plugin-app-node", + "@backstage/plugin-app-react" ] }, "publishConfig": { diff --git a/plugins/app-react/.eslintrc.js b/plugins/app-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/app-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/app-react/README.md b/plugins/app-react/README.md new file mode 100644 index 0000000000..c7e7fb2823 --- /dev/null +++ b/plugins/app-react/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-app-react + +Welcome to the web library package for the app plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/app-react/catalog-info.yaml b/plugins/app-react/catalog-info.yaml new file mode 100644 index 0000000000..1ef48d3b2d --- /dev/null +++ b/plugins/app-react/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-app-react + title: '@backstage/plugin-app-react' + description: Web library for the app plugin +spec: + lifecycle: experimental + type: backstage-web-library + owner: framework-maintainers diff --git a/plugins/app-react/package.json b/plugins/app-react/package.json new file mode 100644 index 0000000000..e27c95d7bf --- /dev/null +++ b/plugins/app-react/package.json @@ -0,0 +1,68 @@ +{ + "name": "@backstage/plugin-app-react", + "version": "0.0.0", + "description": "Web library for the app plugin", + "backstage": { + "role": "web-library", + "pluginId": "app", + "pluginPackages": [ + "@backstage/plugin-app", + "@backstage/plugin-app-backend", + "@backstage/plugin-app-node", + "@backstage/plugin-app-react" + ] + }, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/app-react" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@material-ui/core": "^4.9.13" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^16.0.0", + "@types/react": "^18.0.0", + "react": "^18.0.2", + "react-dom": "^18.0.2", + "react-router-dom": "^6.3.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0", + "react-router-dom": "^6.3.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } +} diff --git a/plugins/app-react/report.api.md b/plugins/app-react/report.api.md new file mode 100644 index 0000000000..48ec50749d --- /dev/null +++ b/plugins/app-react/report.api.md @@ -0,0 +1,234 @@ +## API Report File for "@backstage/plugin-app-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AppTheme } from '@backstage/frontend-plugin-api'; +import { ComponentType } from 'react'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/frontend-plugin-api'; +import { NavContentComponent } from '@backstage/frontend-plugin-api'; +import { NavContentComponentProps } from '@backstage/frontend-plugin-api'; +import { ReactNode } from 'react'; +import { SignInPageProps } from '@backstage/frontend-plugin-api'; +import { SwappableComponentRef } from '@backstage/frontend-plugin-api'; +import { TranslationMessages } from '@backstage/frontend-plugin-api'; +import { TranslationResource } from '@backstage/frontend-plugin-api'; + +// @public +export const IconBundleBlueprint: ExtensionBlueprint<{ + kind: 'icon-bundle'; + params: { + icons: { [key in string]: IconComponent }; + }; + output: ExtensionDataRef< + { + [x: string]: IconComponent; + }, + 'core.icons', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + icons: ConfigurableExtensionDataRef< + { + [x: string]: IconComponent; + }, + 'core.icons', + {} + >; + }; +}>; + +// @public +export const NavContentBlueprint: ExtensionBlueprint<{ + kind: 'nav-content'; + params: { + component: NavContentComponent; + }; + output: ExtensionDataRef< + NavContentComponent, + 'core.nav-content.component', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + component: ConfigurableExtensionDataRef< + NavContentComponent, + 'core.nav-content.component', + {} + >; + }; +}>; + +export { NavContentComponent }; + +export { NavContentComponentProps }; + +// @public +export const RouterBlueprint: ExtensionBlueprint<{ + kind: 'app-router-component'; + params: { + Component?: [error: 'Use the `component` parameter instead']; + component: (props: { children: ReactNode }) => JSX.Element | null; + }; + output: ExtensionDataRef< + (props: { children: ReactNode }) => JSX.Element | null, + 'app.router.wrapper', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + component: ConfigurableExtensionDataRef< + (props: { children: ReactNode }) => JSX.Element | null, + 'app.router.wrapper', + {} + >; + }; +}>; + +// @public +export const SignInPageBlueprint: ExtensionBlueprint<{ + kind: 'sign-in-page'; + params: { + loader: () => Promise>; + }; + output: ExtensionDataRef< + ComponentType, + 'core.sign-in-page.component', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + component: ConfigurableExtensionDataRef< + ComponentType, + 'core.sign-in-page.component', + {} + >; + }; +}>; + +export { SignInPageProps }; + +// @public +export const SwappableComponentBlueprint: ExtensionBlueprint<{ + kind: 'component'; + params: >(params: { + component: Ref extends SwappableComponentRef< + any, + infer IExternalComponentProps + > + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element | null) + : never; + loader: Ref extends SwappableComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) + : never; + }) => ExtensionBlueprintParams<{ + component: Ref extends SwappableComponentRef< + any, + infer IExternalComponentProps + > + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element | null) + : never; + loader: Ref extends SwappableComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) + : never; + }>; + output: ExtensionDataRef< + { + ref: SwappableComponentRef; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + }, + 'core.swappableComponent', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + component: ConfigurableExtensionDataRef< + { + ref: SwappableComponentRef; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + }, + 'core.swappableComponent', + {} + >; + }; +}>; + +// @public +export const ThemeBlueprint: ExtensionBlueprint<{ + kind: 'theme'; + params: { + theme: AppTheme; + }; + output: ExtensionDataRef; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + theme: ConfigurableExtensionDataRef; + }; +}>; + +// @public +export const TranslationBlueprint: ExtensionBlueprint<{ + kind: 'translation'; + params: { + resource: TranslationResource | TranslationMessages; + }; + output: ExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + translation: ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >; + }; +}>; +``` diff --git a/plugins/app-react/src/blueprints/index.ts b/plugins/app-react/src/blueprints/index.ts new file mode 100644 index 0000000000..09d05b71ed --- /dev/null +++ b/plugins/app-react/src/blueprints/index.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + IconBundleBlueprint as _IconBundleBlueprint, + NavContentBlueprint as _NavContentBlueprint, + type NavContentComponent, + type NavContentComponentProps, + RouterBlueprint as _RouterBlueprint, + SignInPageBlueprint as _SignInPageBlueprint, + type SignInPageProps, + SwappableComponentBlueprint as _SwappableComponentBlueprint, + ThemeBlueprint as _ThemeBlueprint, + TranslationBlueprint as _TranslationBlueprint, +} from '@backstage/frontend-plugin-api'; + +/** + * Creates an extension that adds/replaces an app theme. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const ThemeBlueprint = _ThemeBlueprint; + +/** + * Blueprint for creating swappable components from a SwappableComponentRef and a loader. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const SwappableComponentBlueprint = _SwappableComponentBlueprint; + +/** + * Creates an extension that replaces the sign in page. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const SignInPageBlueprint = _SignInPageBlueprint; + +/** + * Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const RouterBlueprint = _RouterBlueprint; + +/** + * Creates an extension that replaces the entire nav bar with your own component. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const NavContentBlueprint = _NavContentBlueprint; + +/** + * Creates an extension that adds icon bundles to your app. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const IconBundleBlueprint = _IconBundleBlueprint; + +/** + * Props for the `SignInPage` component. + * + * @public + */ +export type { SignInPageProps }; + +/** + * The props for the {@link NavContentComponent}. + * + * @public + */ +export type { NavContentComponentProps }; + +/** + * A component that renders the nav bar content, to be passed to the {@link NavContentBlueprint}. + * + * @public + */ +export type { NavContentComponent }; + +/** + * Creates an extension that adds translations to your app. This blueprint is limited to use by the app plugin. + * + * @public + */ +export const TranslationBlueprint = _TranslationBlueprint; diff --git a/plugins/app-react/src/index.ts b/plugins/app-react/src/index.ts new file mode 100644 index 0000000000..bcdb919775 --- /dev/null +++ b/plugins/app-react/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Web library for the app plugin. + * + * @packageDocumentation + */ + +export * from './blueprints'; diff --git a/plugins/app-react/src/setupTests.ts b/plugins/app-react/src/setupTests.ts new file mode 100644 index 0000000000..5f11bab1c2 --- /dev/null +++ b/plugins/app-react/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/plugins/app/package.json b/plugins/app/package.json index 1d277a30e3..db77cee7cc 100644 --- a/plugins/app/package.json +++ b/plugins/app/package.json @@ -7,7 +7,8 @@ "pluginPackages": [ "@backstage/plugin-app", "@backstage/plugin-app-backend", - "@backstage/plugin-app-node" + "@backstage/plugin-app-node", + "@backstage/plugin-app-react" ] }, "publishConfig": { @@ -54,6 +55,7 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/integration-react": "workspace:^", + "@backstage/plugin-app-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/types": "workspace:^", diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index aa030300fe..8e76a16781 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -94,6 +94,14 @@ export const AppNav = createExtension({ }, output: [coreExtensionData.reactElement], *factory({ inputs }) { + if (inputs.content && inputs.content.node.spec.plugin?.id !== 'app') { + // eslint-disable-next-line no-console + console.warn( + `DEPRECATION WARNING: NavContent should only be installed as an extension in the app plugin. ` + + `You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${inputs.content.node.spec.id}`, + ); + } + const Content = inputs.content?.get(NavContentBlueprint.dataRefs.component) ?? DefaultNavContent; diff --git a/plugins/app/src/extensions/AppRoot.tsx b/plugins/app/src/extensions/AppRoot.tsx index 9ce2b055dd..47ea50a641 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -74,6 +74,22 @@ export const AppRoot = createExtension({ }, output: [coreExtensionData.reactElement], factory({ inputs, apis }) { + if (inputs.router && inputs.router.node.spec.plugin?.id !== 'app') { + // eslint-disable-next-line no-console + console.warn( + `DEPRECATION WARNING: Router should only be installed as an extension in the app plugin. ` + + `You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${inputs.router.node.spec.id}`, + ); + } + + if (inputs.signInPage && inputs.signInPage.node.spec.plugin?.id !== 'app') { + // eslint-disable-next-line no-console + console.warn( + `DEPRECATION WARNING: SignInPage should only be installed as an extension in the app plugin. ` + + `You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${inputs.signInPage.node.spec.id}`, + ); + } + if (isProtectedApp()) { const identityApi = apis.get(identityApiRef); if (!identityApi) { diff --git a/plugins/app/src/extensions/AppThemeApi.tsx b/plugins/app/src/extensions/AppThemeApi.tsx index e3c516ca69..2b265ddb53 100644 --- a/plugins/app/src/extensions/AppThemeApi.tsx +++ b/plugins/app/src/extensions/AppThemeApi.tsx @@ -44,10 +44,24 @@ export const AppThemeApi = ApiBlueprint.makeWithOverrides({ defineParams({ api: appThemeApiRef, deps: {}, - factory: () => - AppThemeSelector.createWithStorage( + factory: () => { + const nonAppExtensions = inputs.themes.filter( + i => i.node.spec.plugin?.id !== 'app', + ); + + if (nonAppExtensions.length > 0) { + const list = nonAppExtensions.map(i => i.node.spec.id).join(', '); + // eslint-disable-next-line no-console + console.warn( + `DEPRECATION WARNING: Theme should only be installed as an extension in the app plugin. ` + + `You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${list}`, + ); + } + + return AppThemeSelector.createWithStorage( inputs.themes.map(i => i.get(ThemeBlueprint.dataRefs.theme)), - ), + ); + }, }), ); }, diff --git a/plugins/app/src/extensions/IconsApi.ts b/plugins/app/src/extensions/IconsApi.ts index fcce9ea718..f10e927161 100644 --- a/plugins/app/src/extensions/IconsApi.ts +++ b/plugins/app/src/extensions/IconsApi.ts @@ -40,12 +40,26 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ defineParams({ api: iconsApiRef, deps: {}, - factory: () => - new DefaultIconsApi( + factory: () => { + const nonAppExtensions = inputs.icons.filter( + i => i.node.spec.plugin?.id !== 'app', + ); + + if (nonAppExtensions.length > 0) { + const list = nonAppExtensions.map(i => i.node.spec.id).join(', '); + // eslint-disable-next-line no-console + console.warn( + `DEPRECATION WARNING: IconBundle should only be installed as an extension in the app plugin. ` + + `You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${list}`, + ); + } + + return new DefaultIconsApi( inputs.icons .map(i => i.get(IconBundleBlueprint.dataRefs.icons)) .reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons), - ), + ); + }, }), ); }, diff --git a/plugins/app/src/extensions/TranslationsApi.tsx b/plugins/app/src/extensions/TranslationsApi.tsx index 31d89a5acb..2a6017af4b 100644 --- a/plugins/app/src/extensions/TranslationsApi.tsx +++ b/plugins/app/src/extensions/TranslationsApi.tsx @@ -42,13 +42,27 @@ export const TranslationsApi = ApiBlueprint.makeWithOverrides({ defineParams({ api: translationApiRef, deps: { languageApi: appLanguageApiRef }, - factory: ({ languageApi }) => - I18nextTranslationApi.create({ + factory: ({ languageApi }) => { + const nonAppExtensions = inputs.translations.filter( + i => i.node.spec.plugin?.id !== 'app', + ); + + if (nonAppExtensions.length > 0) { + const list = nonAppExtensions.map(i => i.node.spec.id).join(', '); + // eslint-disable-next-line no-console + console.warn( + `DEPRECATION WARNING: Translations should only be installed as an extension in the app plugin. ` + + `You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${list}`, + ); + } + + return I18nextTranslationApi.create({ languageApi, resources: inputs.translations.map(i => i.get(TranslationBlueprint.dataRefs.translation), ), - }), + }); + }, }), ); }, diff --git a/yarn.lock b/yarn.lock index 92dfaa0796..0f57d51fa7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4155,6 +4155,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-app-react@workspace:^, @backstage/plugin-app-react@workspace:plugins/app-react": + version: 0.0.0-use.local + resolution: "@backstage/plugin-app-react@workspace:plugins/app-react" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@material-ui/core": "npm:^4.9.13" + "@testing-library/jest-dom": "npm:^6.0.0" + "@testing-library/react": "npm:^16.0.0" + "@types/react": "npm:^18.0.0" + react: "npm:^18.0.2" + react-dom: "npm:^18.0.2" + react-router-dom: "npm:^6.3.0" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + react-router-dom: ^6.3.0 + peerDependenciesMeta: + "@types/react": + optional: true + languageName: unknown + linkType: soft + "@backstage/plugin-app-visualizer@workspace:^, @backstage/plugin-app-visualizer@workspace:plugins/app-visualizer": version: 0.0.0-use.local resolution: "@backstage/plugin-app-visualizer@workspace:plugins/app-visualizer" @@ -4194,6 +4221,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/integration-react": "workspace:^" + "@backstage/plugin-app-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^"