diff --git a/.changeset/fuzzy-shrimps-refuse.md b/.changeset/fuzzy-shrimps-refuse.md new file mode 100644 index 0000000000..c2814da631 --- /dev/null +++ b/.changeset/fuzzy-shrimps-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Implemented support for the `internal` extension input option. diff --git a/.changeset/green-llamas-wink.md b/.changeset/green-llamas-wink.md new file mode 100644 index 0000000000..0590b59ef3 --- /dev/null +++ b/.changeset/green-llamas-wink.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-compat-api': patch +'@backstage/plugin-app-visualizer': patch +--- + +Internal updates for blueprint moves to `@backstage/plugin-app-react`. diff --git a/.changeset/lemon-eyes-grin.md b/.changeset/lemon-eyes-grin.md new file mode 100644 index 0000000000..7cbd4b29f1 --- /dev/null +++ b/.changeset/lemon-eyes-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Switched `next-app` template to use blueprint from `@backstage/plugin-app-react`. diff --git a/.changeset/many-bags-brake.md b/.changeset/many-bags-brake.md new file mode 100644 index 0000000000..91b48d63f4 --- /dev/null +++ b/.changeset/many-bags-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added a new `internal` option to `createExtensionInput` that marks the input as only allowing attachments from the same plugin. diff --git a/.changeset/polite-glasses-throw.md b/.changeset/polite-glasses-throw.md new file mode 100644 index 0000000000..67f554591a --- /dev/null +++ b/.changeset/polite-glasses-throw.md @@ -0,0 +1,13 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +**BREAKING**: The following blueprints have been removed and are now only available from `@backstage/plugin-app-react`: + +- `IconBundleBlueprint` +- `NavContentBlueprint` +- `RouterBlueprint` +- `SignInPageBlueprint` +- `SwappableComponentBlueprint` +- `ThemeBlueprint` +- `TranslationBlueprint` diff --git a/.changeset/quiet-humans-hammer.md b/.changeset/quiet-humans-hammer.md new file mode 100644 index 0000000000..d47fd647f1 --- /dev/null +++ b/.changeset/quiet-humans-hammer.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-app': minor +--- + +**BREAKING**: Extensions created with the following blueprints must now be provided via an override or a module for the `app` plugin. Extensions from other plugins will now trigger a warning in the app and be ignored. + +- `IconBundleBlueprint` +- `NavContentBlueprint` +- `RouterBlueprint` +- `SignInPageBlueprint` +- `SwappableComponentBlueprint` +- `ThemeBlueprint` +- `TranslationBlueprint` diff --git a/.changeset/slimy-dots-cross.md b/.changeset/slimy-dots-cross.md new file mode 100644 index 0000000000..fa1cf18aa2 --- /dev/null +++ b/.changeset/slimy-dots-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-defaults': patch +--- + +Dependency update for tests. diff --git a/.changeset/tired-sides-share.md b/.changeset/tired-sides-share.md new file mode 100644 index 0000000000..661a7aee50 --- /dev/null +++ b/.changeset/tired-sides-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-react': patch +--- + +Internal refactor to move implementation of blueprints from `@backstage/frontend-plugin-api` to this package. diff --git a/docs/frontend-system/architecture/20-extensions.md b/docs/frontend-system/architecture/20-extensions.md index a6d00a3131..c13a54427a 100644 --- a/docs/frontend-system/architecture/20-extensions.md +++ b/docs/frontend-system/architecture/20-extensions.md @@ -174,6 +174,8 @@ const navigationExtension = createExtension({ The input (see [1] above) is an object that we create using `createExtensionInput`. The first argument is the set of extension data that we accept via this input, and works just like the `output` option. The second argument is optional, and it allows us to put constraints on the extensions that are attached to our input. If the `singleton: true` option is set, only a single extension can be attached at a time, and unless the `optional: true` option is set it will also be required that there is exactly one attached extension. +Another option that can be used when creating an extension input is the `internal: true` option, which restricts the input to only accept extensions from the same plugin as the extension defining the input. Extensions from other plugins that attempt to attach to an internal input will be ignored, and a warning will be reported. This is useful when you want to limit extensibility to overrides and modules of your plugin, rather than letting it be open to any plugin. + So how can we now attach the output to the parent extension's input? If we think about a navigation component, like the Sidebar in Backstage, there might be plugins that want to attach a link to their plugin to this navigation component. In this case the plugin only needs to know the extension `id` and the name of the extension `input` to attach the extension `output` returned by the `factory` to the specified extension: ```tsx diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 46fcabbf5a..866979f4a1 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -48,6 +48,7 @@ "@backstage/integration-react": "workspace:^", "@backstage/plugin-api-docs": "workspace:^", "@backstage/plugin-app": "workspace:^", + "@backstage/plugin-app-react": "workspace:^", "@backstage/plugin-app-visualizer": "workspace:^", "@backstage/plugin-auth": "workspace:^", "@backstage/plugin-auth-react": "workspace:^", diff --git a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx index 7f690f90cb..965fe92eb8 100644 --- a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx +++ b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx @@ -14,10 +14,8 @@ * limitations under the License. */ -import { - SwappableComponentBlueprint, - NotFoundErrorPage, -} from '@backstage/frontend-plugin-api'; +import { NotFoundErrorPage } from '@backstage/frontend-plugin-api'; +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import Box from '@material-ui/core/Box'; import Typography from '@material-ui/core/Typography'; import { Button } from '@backstage/core-components'; diff --git a/packages/app-next/src/modules/appModuleNav.tsx b/packages/app-next/src/modules/appModuleNav.tsx index ff89b2d1de..9da6358dd3 100644 --- a/packages/app-next/src/modules/appModuleNav.tsx +++ b/packages/app-next/src/modules/appModuleNav.tsx @@ -28,10 +28,8 @@ import { import SearchIcon from '@material-ui/icons/Search'; import MenuIcon from '@material-ui/icons/Menu'; import BuildIcon from '@material-ui/icons/Build'; -import { - createFrontendModule, - NavContentBlueprint, -} from '@backstage/frontend-plugin-api'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { NavContentBlueprint } from '@backstage/plugin-app-react'; import { SidebarSearchModal } from '@backstage/plugin-search'; import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; import { diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 7262eb20bd..eb34da3d34 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -33,6 +33,7 @@ "dependencies": { "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/plugin-app-react": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", diff --git a/packages/core-compat-api/src/convertLegacyAppOptions.tsx b/packages/core-compat-api/src/convertLegacyAppOptions.tsx index 650ea9cd58..09b8329112 100644 --- a/packages/core-compat-api/src/convertLegacyAppOptions.tsx +++ b/packages/core-compat-api/src/convertLegacyAppOptions.tsx @@ -16,21 +16,23 @@ import { ComponentType } from 'react'; import { - SwappableComponentBlueprint, ApiBlueprint, ErrorDisplayProps, createExtension, createFrontendModule, ExtensionDefinition, FrontendModule, - IconBundleBlueprint, - RouterBlueprint, - SignInPageBlueprint, - ThemeBlueprint, ErrorDisplay as SwappableErrorDisplay, NotFoundErrorPage as SwappableNotFoundErrorPage, Progress as SwappableProgress, } from '@backstage/frontend-plugin-api'; +import { + IconBundleBlueprint, + RouterBlueprint, + SignInPageBlueprint, + SwappableComponentBlueprint, + ThemeBlueprint, +} from '@backstage/plugin-app-react'; import { AnyApiFactory, AppComponents, diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 51fa8dff84..db865a2db5 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -55,6 +55,7 @@ import { version as ui } from '../../../ui/package.json'; import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.json'; import { version as pluginAppVisualizer } from '../../../../plugins/app-visualizer/package.json'; import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json'; +import { version as pluginAppReact } from '../../../../plugins/app-react/package.json'; import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json'; import { version as pluginAuthBackendModuleGithubProvider } from '../../../../plugins/auth-backend-module-github-provider/package.json'; import { version as pluginAuthBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json'; @@ -118,6 +119,7 @@ export const packageVersions = { '@backstage/repo-tools': repoTools, '@backstage/plugin-api-docs': pluginApiDocs, '@backstage/plugin-app-backend': pluginAppBackend, + '@backstage/plugin-app-react': pluginAppReact, '@backstage/plugin-app-visualizer': pluginAppVisualizer, '@backstage/plugin-auth-backend': pluginAuthBackend, '@backstage/plugin-auth-backend-module-github-provider': diff --git a/packages/create-app/templates/next-app/packages/app/package.json.hbs b/packages/create-app/templates/next-app/packages/app/package.json.hbs index 726983fd69..bda3b32007 100644 --- a/packages/create-app/templates/next-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/next-app/packages/app/package.json.hbs @@ -21,6 +21,7 @@ "@backstage/frontend-defaults": "^{{ version '@backstage/frontend-defaults'}}", "@backstage/frontend-plugin-api": "^{{ version '@backstage/frontend-plugin-api'}}", "@backstage/integration-react": "^{{ version '@backstage/integration-react'}}", + "@backstage/plugin-app-react": "^{{ version '@backstage/plugin-app-react'}}", "@backstage/plugin-app-visualizer": "^{{ version '@backstage/plugin-app-visualizer'}}", "@backstage/plugin-catalog": "^{{ version '@backstage/plugin-catalog'}}", "@backstage/plugin-notifications": "^{{ version '@backstage/plugin-notifications'}}", diff --git a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx index ddfdde64d0..4d229714fa 100644 --- a/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx +++ b/packages/create-app/templates/next-app/packages/app/src/modules/nav/Sidebar.tsx @@ -7,7 +7,7 @@ import { } from '@backstage/core-components'; import { compatWrapper } from '@backstage/core-compat-api'; import { Sidebar } from '@backstage/core-components'; -import { NavContentBlueprint } from '@backstage/frontend-plugin-api'; +import { NavContentBlueprint } from '@backstage/plugin-app-react'; import { SidebarLogo } from './SidebarLogo'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import HomeIcon from '@material-ui/icons/Home'; diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 06b5e55278..e8fc019007 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -59,6 +59,14 @@ export type AppErrorTypes = { inputName: string; }; }; + EXTENSION_INPUT_INTERNAL_IGNORED: { + context: { + node: AppNode; + inputName: string; + extensionId: string; + plugin: FrontendPlugin; + }; + }; EXTENSION_ATTACHMENT_CONFLICT: { context: { node: AppNode; diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 485e9d95bd..b1da35313e 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -142,6 +142,36 @@ function createV1Extension(opts: { return ext; } +function mirrorInputs(ctx: { + inputs: { + [name in string]: + | undefined + | ResolvedExtensionInput + | Array>; + }; +}) { + return [ + inputMirrorDataRef( + Object.fromEntries( + Object.entries(ctx.inputs).map(([k, v]) => [ + k, + Array.isArray(v) + ? v.map(vi => ({ + node: vi.node, + test: vi.get(testDataRef), + other: vi.get(otherDataRef), + })) + : { + node: v?.node, + test: v?.get(testDataRef), + other: v?.get(otherDataRef), + }, + ]), + ), + ), + ]; +} + describe('instantiateAppNodeTree', () => { describe('v1', () => { const simpleExtension = createV1Extension({ @@ -237,6 +267,60 @@ describe('instantiateAppNodeTree', () => { expect(childNode?.instance).toBeDefined(); }); + it('should ignore non-matching plugin attachments for internal inputs', () => { + const otherPlugin = createFrontendPlugin({ pluginId: 'other' }); + const tree = resolveAppTree( + 'root-node', + [ + makeSpec( + resolveExtensionDefinition( + createExtension({ + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput([testDataRef], { + singleton: true, + internal: true, + }), + }, + output: [inputMirrorDataRef], + factory: mirrorInputs, + }), + { namespace: 'root-node' }, + ), + ), + makeSpec(simpleExtension, { + id: 'child-node-app', + attachTo: { id: 'root-node', input: 'test' }, + }), + makeSpec(simpleExtension, { + id: 'child-node-other', + attachTo: { id: 'root-node', input: 'test' }, + plugin: otherPlugin, + }), + ], + collector, + ); + + instantiateAppNodeTree(tree.root, testApis, collector); + + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: { node: { spec: { id: 'child-node-app' } }, test: 'test' }, + }); + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_INPUT_INTERNAL_IGNORED', + message: + "extension 'child-node-other' from plugin 'other' attached to input 'test' on 'root-node' was ignored, the input is marked as internal and attached extensions must therefore be provided via an override or a module for the 'app' plugin, not the 'other' plugin", + context: { + node: tree.root, + inputName: 'test', + extensionId: 'child-node-other', + plugin: otherPlugin, + }, + }, + ]); + }); + it('should not instantiate disabled attachments', () => { const tree = resolveAppTree( 'root-node', @@ -783,36 +867,6 @@ describe('instantiateAppNodeTree', () => { { namespace: 'app' }, ); - function mirrorInputs(ctx: { - inputs: { - [name in string]: - | undefined - | ResolvedExtensionInput - | Array>; - }; - }) { - return [ - inputMirrorDataRef( - Object.fromEntries( - Object.entries(ctx.inputs).map(([k, v]) => [ - k, - Array.isArray(v) - ? v.map(vi => ({ - node: vi.node, - test: vi.get(testDataRef), - other: vi.get(otherDataRef), - })) - : { - node: v?.node, - test: v?.get(testDataRef), - other: v?.get(otherDataRef), - }, - ]), - ), - ), - ]; - } - it('should instantiate a single node', () => { const tree = resolveAppTree( 'root-node', diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index ca0d785fee..0f641d020b 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -247,10 +247,32 @@ function resolveV2Inputs( inputMap: { [inputName in string]: ExtensionInput }, attachments: ReadonlyMap, parentCollector: ErrorCollector<{ node: AppNode }>, + node: AppNode, ): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }> { return mapValues(inputMap, (input, inputName) => { - const attachedNodes = attachments.get(inputName) ?? []; + const allAttachedNodes = attachments.get(inputName) ?? []; const collector = parentCollector.child({ inputName }); + const inputPluginId = node.spec.plugin.id; + + const attachedNodes = input.config.internal + ? allAttachedNodes.filter(attachment => { + const attachmentPluginId = attachment.spec.plugin.id; + if (attachmentPluginId !== inputPluginId) { + collector.report({ + code: 'EXTENSION_INPUT_INTERNAL_IGNORED', + message: + `extension '${attachment.spec.id}' from plugin '${attachmentPluginId}' attached to input '${inputName}' on '${node.spec.id}' was ignored, ` + + `the input is marked as internal and attached extensions must therefore be provided via an override or a module for the '${inputPluginId}' plugin, not the '${attachmentPluginId}' plugin`, + context: { + extensionId: attachment.spec.id, + plugin: attachment.spec.plugin, + }, + }); + return false; + } + return true; + }) + : allAttachedNodes; if (input.config.singleton) { if (attachedNodes.length > 1) { @@ -371,6 +393,7 @@ export function createAppNodeInstance(options: { internalExtension.inputs, attachments, collector, + node, ), }; const outputDataValues = options.extensionFactoryMiddleware diff --git a/packages/frontend-app-api/src/wiring/createErrorCollector.ts b/packages/frontend-app-api/src/wiring/createErrorCollector.ts index dab69ba19e..5f081bc092 100644 --- a/packages/frontend-app-api/src/wiring/createErrorCollector.ts +++ b/packages/frontend-app-api/src/wiring/createErrorCollector.ts @@ -38,6 +38,14 @@ export type AppErrorTypes = { EXTENSION_INPUT_DATA_MISSING: { context: { node: AppNode; inputName: string }; }; + EXTENSION_INPUT_INTERNAL_IGNORED: { + context: { + node: AppNode; + inputName: string; + extensionId: string; + plugin: FrontendPlugin; + }; + }; EXTENSION_ATTACHMENT_CONFLICT: { context: { node: AppNode; inputName: string }; }; diff --git a/packages/frontend-defaults/package.json b/packages/frontend-defaults/package.json index c47987083d..2a69357878 100644 --- a/packages/frontend-defaults/package.json +++ b/packages/frontend-defaults/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-app-react": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index c0928e7815..3a9ea91bec 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -22,11 +22,11 @@ import { PageBlueprint, createFrontendPlugin, createFrontendFeatureLoader, - ThemeBlueprint, createFrontendModule, useAppNode, FrontendPluginInfo, } from '@backstage/frontend-plugin-api'; +import { ThemeBlueprint } from '@backstage/plugin-app-react'; import { screen, waitFor } from '@testing-library/react'; import { createApp } from './createApp'; import { mockApis, renderWithEffects } from '@backstage/test-utils'; @@ -60,8 +60,8 @@ describe('createApp', () => { }), }, features: [ - createFrontendPlugin({ - pluginId: 'test', + createFrontendModule({ + pluginId: 'app', extensions: [ ThemeBlueprint.make({ name: 'derp', diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx index e80d058140..398dbcbf4a 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -14,10 +14,8 @@ * limitations under the License. */ -import { - SignInPageBlueprint, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; import { render, screen, waitFor } from '@testing-library/react'; import { useEffect } from 'react'; import { createPublicSignInApp } from './createPublicSignInApp'; diff --git a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx index 6bad97f666..c0fb807deb 100644 --- a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx +++ b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx @@ -22,6 +22,7 @@ const DEFAULT_WARNING_CODES: Array = [ 'EXTENSION_IGNORED', 'INVALID_EXTENSION_CONFIG_KEY', 'EXTENSION_INPUT_DATA_IGNORED', + 'EXTENSION_INPUT_INTERNAL_IGNORED', 'EXTENSION_OUTPUT_IGNORED', ]; diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 1a7f2e3c7a..76ca364952 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -13,7 +13,6 @@ import { ExpandRecursive } from '@backstage/types'; import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams as ExtensionBlueprintParams_2 } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef as ExtensionDataRef_2 } from '@backstage/frontend-plugin-api'; -import { IconComponent as IconComponent_2 } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react/jsx-runtime'; @@ -21,7 +20,6 @@ import { JSX as JSX_3 } from 'react'; import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; -import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api'; import type { z } from 'zod'; @@ -271,30 +269,6 @@ export const AppRootElementBlueprint: ExtensionBlueprint_2<{ dataRefs: never; }>; -// @public @deprecated -export const AppRootWrapperBlueprint: ExtensionBlueprint_2<{ - kind: 'app-root-wrapper'; - params: { - Component?: [error: 'Use the `component` parameter instead']; - component: (props: { children: ReactNode }) => JSX.Element | null; - }; - output: ExtensionDataRef_2< - (props: { children: ReactNode }) => JSX.Element | null, - 'app.root.wrapper', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - component: ConfigurableExtensionDataRef_2< - (props: { children: ReactNode }) => JSX.Element | null, - 'app.root.wrapper', - {} - >; - }; -}>; - // @public export type AppTheme = { id: string; @@ -604,7 +578,7 @@ export function createExtensionDataRef(): { }): ConfigurableExtensionDataRef; }; -// @public (undocumented) +// @public export function createExtensionInput< UExtensionData extends ExtensionDataRef< unknown, @@ -616,6 +590,7 @@ export function createExtensionInput< TConfig extends { singleton?: boolean; optional?: boolean; + internal?: boolean; }, >( extensionData: Array, @@ -630,6 +605,7 @@ export function createExtensionInput< { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; + internal: TConfig['internal'] extends true ? true : false; } >; @@ -1288,9 +1264,11 @@ export interface ExtensionInput< TConfig extends { singleton: boolean; optional: boolean; + internal?: boolean; } = { singleton: boolean; optional: boolean; + internal?: boolean; }, > { // (undocumented) @@ -1457,33 +1435,6 @@ export const googleAuthApiRef: ApiRef< SessionApi >; -// @public @deprecated (undocumented) -export const IconBundleBlueprint: ExtensionBlueprint_2<{ - kind: 'icon-bundle'; - params: { - icons: { [key in string]: IconComponent }; - }; - output: ExtensionDataRef_2< - { - [x: string]: IconComponent; - }, - 'core.icons', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - icons: ConfigurableExtensionDataRef_2< - { - [x: string]: IconComponent; - }, - 'core.icons', - {} - >; - }; -}>; - // @public export type IconComponent = ComponentType<{ fontSize?: 'medium' | 'large' | 'small' | 'inherit'; @@ -1522,45 +1473,6 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public @deprecated -export const NavContentBlueprint: ExtensionBlueprint_2<{ - kind: 'nav-content'; - params: { - component: NavContentComponent; - }; - output: ExtensionDataRef_2< - NavContentComponent, - 'core.nav-content.component', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - component: ConfigurableExtensionDataRef_2< - NavContentComponent, - 'core.nav-content.component', - {} - >; - }; -}>; - -// @public -export type NavContentComponent = ( - props: NavContentComponentProps, -) => JSX.Element | null; - -// @public -export interface NavContentComponentProps { - items: Array<{ - icon: IconComponent_2; - title: string; - routeRef: RouteRef_2; - to: string; - text: string; - }>; -} - // @public export const NavItemBlueprint: ExtensionBlueprint_2<{ kind: 'nav-item'; @@ -1923,30 +1835,6 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; -// @public @deprecated (undocumented) -export const RouterBlueprint: ExtensionBlueprint_2<{ - kind: 'app-router-component'; - params: { - Component?: [error: 'Use the `component` parameter instead']; - component: (props: { children: ReactNode }) => JSX.Element | null; - }; - output: ExtensionDataRef_2< - (props: { children: ReactNode }) => JSX.Element | null, - 'app.router.wrapper', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - component: ConfigurableExtensionDataRef_2< - (props: { children: ReactNode }) => JSX.Element | null, - 'app.router.wrapper', - {} - >; - }; -}>; - // @public export interface RouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -1998,35 +1886,6 @@ export namespace SessionState { export type SignedOut = typeof SessionState.SignedOut; } -// @public @deprecated -export const SignInPageBlueprint: ExtensionBlueprint_2<{ - kind: 'sign-in-page'; - params: { - loader: () => Promise>; - }; - output: ExtensionDataRef_2< - ComponentType, - 'core.sign-in-page.component', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - component: ConfigurableExtensionDataRef_2< - ComponentType, - 'core.sign-in-page.component', - {} - >; - }; -}>; - -// @public -export type SignInPageProps = { - onSignInSuccess(identityApi: IdentityApi): void; - children?: ReactNode; -}; - // @public export interface StorageApi { forBucket(name: string): StorageApi; @@ -2066,65 +1925,6 @@ export interface SubRouteRef< readonly T: TParams; } -// @public @deprecated -export const SwappableComponentBlueprint: ExtensionBlueprint_2<{ - 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_2<{ - 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_2< - { - ref: SwappableComponentRef; - loader: - | (() => (props: {}) => JSX.Element | null) - | (() => Promise<(props: {}) => JSX.Element | null>); - }, - 'core.swappableComponent', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - component: ConfigurableExtensionDataRef_2< - { - ref: SwappableComponentRef; - loader: - | (() => (props: {}) => JSX.Element | null) - | (() => Promise<(props: {}) => JSX.Element | null>); - }, - 'core.swappableComponent', - {} - >; - }; -}>; - // @public (undocumented) export type SwappableComponentRef< TInnerComponentProps extends {} = {}, @@ -2150,21 +1950,6 @@ export interface SwappableComponentsApi { // @public export const swappableComponentsApiRef: ApiRef_2; -// @public @deprecated -export const ThemeBlueprint: ExtensionBlueprint_2<{ - kind: 'theme'; - params: { - theme: AppTheme; - }; - output: ExtensionDataRef_2; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - theme: ConfigurableExtensionDataRef_2; - }; -}>; - // @public (undocumented) export type TranslationApi = { getTranslation< @@ -2186,43 +1971,6 @@ export type TranslationApi = { // @public (undocumented) export const translationApiRef: ApiRef; -// @public @deprecated -export const TranslationBlueprint: ExtensionBlueprint_2<{ - kind: 'translation'; - params: { - resource: TranslationResource | TranslationMessages; - }; - output: ExtensionDataRef_2< - | TranslationResource - | TranslationMessages< - string, - { - [x: string]: string; - }, - boolean - >, - 'core.translation.translation', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - translation: ConfigurableExtensionDataRef_2< - | TranslationResource - | TranslationMessages< - string, - { - [x: string]: string; - }, - boolean - >, - 'core.translation.translation', - {} - >; - }; -}>; - // @public (undocumented) export type TranslationFunction< TMessages extends { diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts index 6d16a215e1..8043752687 100644 --- a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -200,6 +200,7 @@ describe('ApiBlueprint', () => { "test": { "$$type": "@backstage/ExtensionInput", "config": { + "internal": false, "optional": false, "singleton": false, }, diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index 379c8d237b..344b44dbff 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -20,20 +20,5 @@ export { } from './AnalyticsImplementationBlueprint'; export { ApiBlueprint } from './ApiBlueprint'; export { AppRootElementBlueprint } from './AppRootElementBlueprint'; -export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; -export { IconBundleBlueprint } from './IconBundleBlueprint'; -export { - NavContentBlueprint, - type NavContentComponent, - type NavContentComponentProps, -} from './NavContentBlueprint'; export { NavItemBlueprint } from './NavItemBlueprint'; export { PageBlueprint } from './PageBlueprint'; -export { RouterBlueprint } from './RouterBlueprint'; -export { - type SignInPageProps, - SignInPageBlueprint, -} from './SignInPageBlueprint'; -export { ThemeBlueprint } from './ThemeBlueprint'; -export { TranslationBlueprint } from './TranslationBlueprint'; -export { SwappableComponentBlueprint } from './SwappableComponentBlueprint'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts index 279878542a..9b32ca947f 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts @@ -29,28 +29,28 @@ describe('createExtensionInput', () => { expect(input).toEqual({ $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], - config: { singleton: false, optional: false }, + config: { singleton: false, optional: false, internal: false }, withContext: expect.any(Function), }); const x1: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: false; optional: false } + { singleton: false; optional: false; internal: false } > = input; // @ts-expect-error const x2: ExtensionInput< typeof stringDataRef, - { singleton: false; optional: false } + { singleton: false; optional: false; internal: false } > = input; // @ts-expect-error const x3: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: true; optional: false } + { singleton: true; optional: false; internal: false } > = input; // @ts-expect-error const x4: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: false; optional: true } + { singleton: false; optional: true; internal: false } > = input; unused(x1, x2, x3, x4); @@ -64,7 +64,7 @@ describe('createExtensionInput', () => { expect(inputWithContext).toEqual({ $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], - config: { singleton: false, optional: false }, + config: { singleton: false, optional: false, internal: false }, withContext: expect.any(Function), context, }); @@ -77,28 +77,28 @@ describe('createExtensionInput', () => { expect(input).toEqual({ $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], - config: { singleton: true, optional: false }, + config: { singleton: true, optional: false, internal: false }, withContext: expect.any(Function), }); const x1: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: true; optional: false } + { singleton: true; optional: false; internal: false } > = input; // @ts-expect-error const x2: ExtensionInput< typeof stringDataRef, - { singleton: true; optional: false } + { singleton: true; optional: false; internal: false } > = input; // @ts-expect-error const x3: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: false; optional: false } + { singleton: false; optional: false; internal: false } > = input; // @ts-expect-error const x4: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: false; optional: true } + { singleton: false; optional: true; internal: false } > = input; unused(x1, x2, x3, x4); @@ -112,28 +112,28 @@ describe('createExtensionInput', () => { expect(input).toEqual({ $$type: '@backstage/ExtensionInput', extensionData: [stringDataRef, numberDataRef], - config: { singleton: true, optional: true }, + config: { singleton: true, optional: true, internal: false }, withContext: expect.any(Function), }); const x1: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: true; optional: true } + { singleton: true; optional: true; internal: false } > = input; // @ts-expect-error const x2: ExtensionInput< typeof stringDataRef, - { singleton: true; optional: true } + { singleton: true; optional: true; internal: false } > = input; // @ts-expect-error const x3: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: false; optional: false } + { singleton: false; optional: false; internal: false } > = input; // @ts-expect-error const x4: ExtensionInput< typeof stringDataRef | typeof numberDataRef, - { singleton: false; optional: true } + { singleton: false; optional: true; internal: false } > = input; unused(x1, x2, x3, x4); @@ -144,4 +144,14 @@ describe('createExtensionInput', () => { createExtensionInput([stringDataRef, stringDataRef], { singleton: true }), ).toThrow("ExtensionInput may not have duplicate data refs: 'str'"); }); + + it('should create an internal input', () => { + const input = createExtensionInput([stringDataRef], { internal: true }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef], + config: { singleton: false, optional: false, internal: true }, + withContext: expect.any(Function), + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts index 57040c8adf..760bd4b984 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts @@ -27,9 +27,14 @@ export interface ExtensionInput< string, { optional?: true } > = ExtensionDataRef, - TConfig extends { singleton: boolean; optional: boolean } = { + TConfig extends { singleton: boolean; optional: boolean; + internal?: boolean; + } = { + singleton: boolean; + optional: boolean; + internal?: boolean; }, > { readonly $$type: '@backstage/ExtensionInput'; @@ -38,10 +43,60 @@ export interface ExtensionInput< readonly replaces?: Array<{ id: string; input: string }>; } -/** @public */ +/** + * Creates a new extension input to be passed to the input map of an extension. + * + * @remarks + * + * Extension inputs created with this function can be passed to any `inputs` map + * as part of creating or overriding an extension. + * + * The array of extension data references defines the data this input expects. + * If the required data is not provided by the attached extension, the + * attachment will fail. + * + * The `config` object can be used to restrict the behavior and shape of the + * input. By default an input will accept zero or more extensions from any + * plugin. The following options are available: + * + * - `singleton`: If set to `true`, only one extension can be attached to the + * input at a time. Additional extensions will trigger an app error and be + * ignored. + * - `optional`: If set to `true`, the input is optional and can be omitted, + * this only has an effect if the `singleton` is set to `true`. + * - `internal`: If set to `true`, only extensions from the same plugin will be + * allowed to attach to this input. Other extensions will trigger an app error + * and be ignored. + * + * @param extensionData - The array of extension data references that this input + * expects. + * @param config - The configuration object for the input. + * @returns An extension input declaration. + * @example + * ```ts + * const extension = createExtension({ + * attachTo: { id: 'example-parent', input: 'example-input' }, + * inputs: { + * content: createExtensionInput([coreExtensionData.reactElement], { + * singleton: true, + * }), + * }, + * output: [coreExtensionData.reactElement], + * *factory({ inputs }) { + * const content = inputs.content?.get(coreExtensionData.reactElement); + * yield coreExtensionData.reactElement({content}); + * }, + * }); + * ``` + * @public + */ export function createExtensionInput< UExtensionData extends ExtensionDataRef, - TConfig extends { singleton?: boolean; optional?: boolean }, + TConfig extends { + singleton?: boolean; + optional?: boolean; + internal?: boolean; + }, >( extensionData: Array, config?: TConfig & { replaces?: Array<{ id: string; input: string }> }, @@ -50,6 +105,7 @@ export function createExtensionInput< { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; + internal: TConfig['internal'] extends true ? true : false; } > { if (process.env.NODE_ENV !== 'production') { @@ -81,6 +137,9 @@ export function createExtensionInput< optional: Boolean(config?.optional) as TConfig['optional'] extends true ? true : false, + internal: Boolean(config?.internal) as TConfig['internal'] extends true + ? true + : false, }, replaces: config?.replaces, }; @@ -90,6 +149,7 @@ export function createExtensionInput< { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; + internal: TConfig['internal'] extends true ? true : false; } > { return OpaqueExtensionInput.createInstance(undefined, { diff --git a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts index b46bb426fa..b9f239842d 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts @@ -37,6 +37,7 @@ export type ResolvedInputValueOverrides< { optional: infer IOptional extends boolean; singleton: boolean; + internal?: boolean; } > ? IOptional extends true @@ -44,7 +45,11 @@ export type ResolvedInputValueOverrides< : KName : never]: TInputs[KName] extends ExtensionInput< infer IDataRefs, - { optional: boolean; singleton: infer ISingleton extends boolean } + { + optional: boolean; + singleton: infer ISingleton extends boolean; + internal?: boolean; + } > ? ISingleton extends true ? Iterable> @@ -56,6 +61,7 @@ export type ResolvedInputValueOverrides< { optional: infer IOptional extends boolean; singleton: boolean; + internal?: boolean; } > ? IOptional extends true @@ -63,7 +69,11 @@ export type ResolvedInputValueOverrides< : never : never]?: TInputs[KName] extends ExtensionInput< infer IDataRefs, - { optional: boolean; singleton: infer ISingleton extends boolean } + { + optional: boolean; + singleton: infer ISingleton extends boolean; + internal?: boolean; + } > ? ISingleton extends true ? Iterable> diff --git a/plugins/app-react/report.api.md b/plugins/app-react/report.api.md index 251152a9e6..78f4bd3bfb 100644 --- a/plugins/app-react/report.api.md +++ b/plugins/app-react/report.api.md @@ -10,10 +10,9 @@ 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 { IdentityApi } from '@backstage/frontend-plugin-api'; import { ReactNode } from 'react'; -import { SignInPageProps } from '@backstage/frontend-plugin-api'; +import { RouteRef } 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'; @@ -92,9 +91,21 @@ export const NavContentBlueprint: ExtensionBlueprint<{ }; }>; -export { NavContentComponent }; +// @public +export type NavContentComponent = ( + props: NavContentComponentProps, +) => JSX.Element | null; -export { NavContentComponentProps }; +// @public +export interface NavContentComponentProps { + items: Array<{ + icon: IconComponent; + title: string; + routeRef: RouteRef; + to: string; + text: string; + }>; +} // @public export const RouterBlueprint: ExtensionBlueprint<{ @@ -143,7 +154,11 @@ export const SignInPageBlueprint: ExtensionBlueprint<{ }; }>; -export { SignInPageProps }; +// @public +export type SignInPageProps = { + onSignInSuccess(identityApi: IdentityApi): void; + children?: ReactNode; +}; // @public export const SwappableComponentBlueprint: ExtensionBlueprint<{ @@ -160,14 +175,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ loader: Ref extends SwappableComponentRef ? | (() => (props: IInnerComponentProps) => JSX.Element | null) - | (() => Promise< - (props: IInnerComponentProps) => JSX.Element - /** - * Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin. - * - * @public - */ | null - >) + | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never; }) => ExtensionBlueprintParams<{ component: Ref extends SwappableComponentRef< @@ -181,14 +189,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ loader: Ref extends SwappableComponentRef ? | (() => (props: IInnerComponentProps) => JSX.Element | null) - | (() => Promise< - (props: IInnerComponentProps) => JSX.Element - /** - * Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin. - * - * @public - */ | null - >) + | (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>) : never; }>; output: ExtensionDataRef< diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx b/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.test.tsx similarity index 80% rename from packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx rename to plugins/app-react/src/blueprints/AppRootWrapperBlueprint.test.tsx index 0f26af3243..a61f583ca8 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx +++ b/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -20,7 +20,8 @@ import { coreExtensionData, createExtension, createExtensionInput, -} from '../wiring'; + createFrontendModule, +} from '@backstage/frontend-plugin-api'; import { renderTestApp } from '@backstage/frontend-test-utils'; describe('AppRootWrapperBlueprint', () => { @@ -63,7 +64,11 @@ describe('AppRootWrapperBlueprint', () => { }, }); - renderTestApp({ extensions: [extension] }); + renderTestApp({ + features: [ + createFrontendModule({ pluginId: 'app', extensions: [extension] }), + ], + }); await waitFor(() => expect(screen.getByText('Hello')).toBeInTheDocument()); }); @@ -95,20 +100,28 @@ describe('AppRootWrapperBlueprint', () => { }); renderTestApp({ - extensions: [ - extension, - createExtension({ - name: 'test-child', - attachTo: { id: 'app-root-wrapper:test', input: 'children' }, - output: [coreExtensionData.reactElement], - factory: () => [coreExtensionData.reactElement(
Its Me
)], + extensions: [], + features: [ + createFrontendModule({ + pluginId: 'app', + extensions: [ + extension, + createExtension({ + name: 'test-child', + attachTo: extension.inputs.children, + output: [coreExtensionData.reactElement], + factory: () => [ + coreExtensionData.reactElement(
Its Me
), + ], + }), + ], }), ], config: { app: { extensions: [ { - 'app-root-wrapper:test': { config: { name: 'Robin' } }, + 'app-root-wrapper:app': { config: { name: 'Robin' } }, }, ], }, diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.tsx similarity index 79% rename from packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx rename to plugins/app-react/src/blueprints/AppRootWrapperBlueprint.tsx index 184a8c3327..bfeef61e1d 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx +++ b/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -15,7 +15,10 @@ */ import { ReactNode } from 'react'; -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; const componentDataRef = createExtensionDataRef< (props: { children: ReactNode }) => JSX.Element | null @@ -24,12 +27,9 @@ const componentDataRef = createExtensionDataRef< /** * Creates a extensions that render a React wrapper at the app root, enclosing * the app layout. This is useful for example for adding global React contexts - * and similar. + * and similar. This blueprint is limited to use by the app plugin. * * @public - * @deprecated Use {@link @backstage/plugin-app-react#AppRootWrapperBlueprint} instead. - * If you were using this blueprint to provide a context for your plugin, - * use `PluginWrapperBlueprint` from `@backstage/frontend-plugin-api/alpha` instead. */ export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', diff --git a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts b/plugins/app-react/src/blueprints/IconBundleBlueprint.ts similarity index 75% rename from packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts rename to plugins/app-react/src/blueprints/IconBundleBlueprint.ts index 2f52f7590a..ce340c2aeb 100644 --- a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts +++ b/plugins/app-react/src/blueprints/IconBundleBlueprint.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -14,16 +14,20 @@ * limitations under the License. */ -import { IconComponent } from '../icons'; -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; +import { IconComponent } from '@backstage/frontend-plugin-api'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; const iconsDataRef = createExtensionDataRef<{ [key in string]: IconComponent; }>().with({ id: 'core.icons' }); /** + * Creates an extension that adds icon bundles to your app. This blueprint is limited to use by the app plugin. + * * @public - * @deprecated Use {@link @backstage/plugin-app-react#IconBundleBlueprint} instead. */ export const IconBundleBlueprint = createExtensionBlueprint({ kind: 'icon-bundle', diff --git a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.test.tsx b/plugins/app-react/src/blueprints/NavContentBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.test.tsx rename to plugins/app-react/src/blueprints/NavContentBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts b/plugins/app-react/src/blueprints/NavContentBlueprint.ts similarity index 87% rename from packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts rename to plugins/app-react/src/blueprints/NavContentBlueprint.ts index f56de2032b..0f21ed832a 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavContentBlueprint.ts +++ b/plugins/app-react/src/blueprints/NavContentBlueprint.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -15,7 +15,10 @@ */ import { IconComponent, RouteRef } from '@backstage/frontend-plugin-api'; -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; /** * The props for the {@link NavContentComponent}. @@ -25,7 +28,7 @@ import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; export interface NavContentComponentProps { /** * The nav items available to the component. These are all the items created - * with the {@link NavItemBlueprint} in the app. + * with the {@link @backstage/frontend-plugin-api#NavItemBlueprint} in the app. * * In addition to the original properties from the nav items, these also * include a resolved route path as `to`, and duplicated `title` as `text` to @@ -57,10 +60,9 @@ const componentDataRef = createExtensionDataRef().with({ }); /** - * Creates an extension that replaces the entire nav bar with your own component. + * Creates an extension that replaces the entire nav bar with your own component. This blueprint is limited to use by the app plugin. * * @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.test.tsx b/plugins/app-react/src/blueprints/RouterBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx rename to plugins/app-react/src/blueprints/RouterBlueprint.test.tsx index f2e5199124..0371794fff 100644 --- a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx +++ b/plugins/app-react/src/blueprints/RouterBlueprint.test.tsx @@ -20,7 +20,7 @@ import { coreExtensionData, createExtension, createExtensionInput, -} from '../wiring'; +} from '@backstage/frontend-plugin-api'; import { createExtensionTester } from '@backstage/frontend-test-utils'; describe('RouterBlueprint', () => { diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx b/plugins/app-react/src/blueprints/RouterBlueprint.tsx similarity index 83% rename from packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx rename to plugins/app-react/src/blueprints/RouterBlueprint.tsx index 88096cf16a..dde2bc83e8 100644 --- a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx +++ b/plugins/app-react/src/blueprints/RouterBlueprint.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -15,15 +15,19 @@ */ import { ReactNode } from 'react'; -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; const componentDataRef = createExtensionDataRef< (props: { children: ReactNode }) => JSX.Element | null >().with({ id: 'app.router.wrapper' }); /** + * Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin. + * * @public - * @deprecated Use {@link @backstage/plugin-app-react#RouterBlueprint} instead. */ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/plugins/app-react/src/blueprints/SignInPageBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx rename to plugins/app-react/src/blueprints/SignInPageBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/plugins/app-react/src/blueprints/SignInPageBlueprint.tsx similarity index 83% rename from packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx rename to plugins/app-react/src/blueprints/SignInPageBlueprint.tsx index 9f51e8f666..fd60947b0a 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx +++ b/plugins/app-react/src/blueprints/SignInPageBlueprint.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -15,9 +15,12 @@ */ import { ComponentType, lazy, ReactNode } from 'react'; -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; -import { ExtensionBoundary } from '../components'; -import { IdentityApi } from '../apis'; +import { + createExtensionBlueprint, + createExtensionDataRef, + ExtensionBoundary, + IdentityApi, +} from '@backstage/frontend-plugin-api'; /** * Props for the `SignInPage` component. @@ -41,10 +44,9 @@ const componentDataRef = createExtensionDataRef< >().with({ id: 'core.sign-in-page.component' }); /** - * Creates an extension that replaces the sign in page. + * Creates an extension that replaces the sign in page. This blueprint is limited to use by the app plugin. * * @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.test.tsx b/plugins/app-react/src/blueprints/SwappableComponentBlueprint.test.tsx similarity index 97% rename from packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx rename to plugins/app-react/src/blueprints/SwappableComponentBlueprint.test.tsx index 63436308ed..8d9f886c58 100644 --- a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx +++ b/plugins/app-react/src/blueprints/SwappableComponentBlueprint.test.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ import { renderTestApp } from '@backstage/frontend-test-utils'; -import { createSwappableComponent } from '../components'; +import { + createSwappableComponent, + PageBlueprint, +} from '@backstage/frontend-plugin-api'; import { SwappableComponentBlueprint } from './SwappableComponentBlueprint'; -import { PageBlueprint } from './PageBlueprint'; import { screen } from '@testing-library/react'; describe('SwappableComponentBlueprint', () => { diff --git a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts b/plugins/app-react/src/blueprints/SwappableComponentBlueprint.ts similarity index 89% rename from packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts rename to plugins/app-react/src/blueprints/SwappableComponentBlueprint.ts index 38e333826a..801e258367 100644 --- a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.ts +++ b/plugins/app-react/src/blueprints/SwappableComponentBlueprint.ts @@ -1,5 +1,5 @@ /* - * Copyright 2025 The Backstage Authors + * 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. @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SwappableComponentRef } from '../components'; + +import { SwappableComponentRef } from '@backstage/frontend-plugin-api'; import { createExtensionBlueprint, createExtensionBlueprintParams, createExtensionDataRef, -} from '../wiring'; +} from '@backstage/frontend-plugin-api'; export const componentDataRef = createExtensionDataRef<{ ref: SwappableComponentRef; @@ -28,10 +29,9 @@ export const componentDataRef = createExtensionDataRef<{ }>().with({ id: 'core.swappableComponent' }); /** - * Blueprint for creating swappable components from a SwappableComponentRef and a loader + * Blueprint for creating swappable components from a SwappableComponentRef and a loader. This blueprint is limited to use by the app plugin. * * @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.test.ts b/plugins/app-react/src/blueprints/ThemeBlueprint.test.ts similarity index 96% rename from packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts rename to plugins/app-react/src/blueprints/ThemeBlueprint.test.ts index 7a53f28fa3..5ee66ece67 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts +++ b/plugins/app-react/src/blueprints/ThemeBlueprint.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AppTheme } from '../apis/definitions/AppThemeApi'; +import { AppTheme } from '@backstage/frontend-plugin-api'; import { ThemeBlueprint } from './ThemeBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/plugins/app-react/src/blueprints/ThemeBlueprint.ts similarity index 75% rename from packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts rename to plugins/app-react/src/blueprints/ThemeBlueprint.ts index f93075181e..559b442f4c 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts +++ b/plugins/app-react/src/blueprints/ThemeBlueprint.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -14,18 +14,20 @@ * limitations under the License. */ -import { AppTheme } from '../apis/definitions/AppThemeApi'; -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; +import { AppTheme } from '@backstage/frontend-plugin-api'; +import { + createExtensionBlueprint, + createExtensionDataRef, +} from '@backstage/frontend-plugin-api'; const themeDataRef = createExtensionDataRef().with({ id: 'core.theme.theme', }); /** - * Creates an extension that adds/replaces an app theme. + * Creates an extension that adds/replaces an app theme. This blueprint is limited to use by the app plugin. * * @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.test.ts b/plugins/app-react/src/blueprints/TranslationBlueprint.test.ts similarity index 98% rename from packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts rename to plugins/app-react/src/blueprints/TranslationBlueprint.test.ts index 07e02c1562..bf291526ca 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts +++ b/plugins/app-react/src/blueprints/TranslationBlueprint.test.ts @@ -17,7 +17,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createTranslationMessages, createTranslationRef, -} from '../translation'; +} from '@backstage/frontend-plugin-api'; import { TranslationBlueprint } from './TranslationBlueprint'; describe('TranslationBlueprint', () => { diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/plugins/app-react/src/blueprints/TranslationBlueprint.ts similarity index 76% rename from packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts rename to plugins/app-react/src/blueprints/TranslationBlueprint.ts index 491053d763..a45fb26b49 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts +++ b/plugins/app-react/src/blueprints/TranslationBlueprint.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. @@ -14,18 +14,21 @@ * limitations under the License. */ -import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; -import { TranslationMessages, TranslationResource } from '../translation'; +import { + createExtensionBlueprint, + createExtensionDataRef, + TranslationMessages, + TranslationResource, +} from '@backstage/frontend-plugin-api'; const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages >().with({ id: 'core.translation.translation' }); /** - * Creates an extension that adds translations to your app. + * Creates an extension that adds translations to your app. This blueprint is limited to use by the app plugin. * * @public - * @deprecated Use {@link @backstage/plugin-app-react#TranslationBlueprint} instead. */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', diff --git a/plugins/app-react/src/blueprints/index.ts b/plugins/app-react/src/blueprints/index.ts index f5e4d7ac4a..15a0fdcd47 100644 --- a/plugins/app-react/src/blueprints/index.ts +++ b/plugins/app-react/src/blueprints/index.ts @@ -14,94 +14,16 @@ * limitations under the License. */ -import { - AppRootWrapperBlueprint as _AppRootWrapperBlueprint, - 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 renders a React wrapper at the app root, enclosing - * the app layout. This blueprint is limited to use by the app plugin. - * - * @public - */ -export const AppRootWrapperBlueprint = _AppRootWrapperBlueprint; - -/** - * 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; +export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; +export { NavContentBlueprint } from './NavContentBlueprint'; +export type { + NavContentComponent, + NavContentComponentProps, +} from './NavContentBlueprint'; +export { RouterBlueprint } from './RouterBlueprint'; +export { SignInPageBlueprint } from './SignInPageBlueprint'; +export type { SignInPageProps } from './SignInPageBlueprint'; +export { SwappableComponentBlueprint } from './SwappableComponentBlueprint'; +export { ThemeBlueprint } from './ThemeBlueprint'; +export { TranslationBlueprint } from './TranslationBlueprint'; diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 259ee143c9..ef0a329b8c 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -21,7 +21,6 @@ import { coreExtensionData, ApiBlueprint, NavItemBlueprint, - ThemeBlueprint, useApi, routeResolutionApiRef, } from '@backstage/frontend-plugin-api'; @@ -79,7 +78,6 @@ const getOutputColor = createOutputColorGenerator( [coreExtensionData.routePath.id]: '#ffeb3b', [coreExtensionData.routeRef.id]: '#9c27b0', [ApiBlueprint.dataRefs.factory.id]: '#2196f3', - [ThemeBlueprint.dataRefs.theme.id]: '#cddc39', [NavItemBlueprint.dataRefs.target.id]: '#ff9800', }, @@ -330,7 +328,6 @@ const legendMap = { 'Route Path': coreExtensionData.routePath, 'Route Ref': coreExtensionData.routeRef, 'Nav Target': NavItemBlueprint.dataRefs.target, - Theme: ThemeBlueprint.dataRefs.theme, }; function Legend() { diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index c5ee77ef01..256517b86f 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -15,12 +15,12 @@ import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; -import { NavContentComponent } from '@backstage/frontend-plugin-api'; +import { NavContentComponent } from '@backstage/plugin-app-react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; -import { SignInPageProps } from '@backstage/frontend-plugin-api'; +import { SignInPageProps } from '@backstage/plugin-app-react'; import { SwappableComponentRef } from '@backstage/frontend-plugin-api'; import { TranslationMessages } from '@backstage/frontend-plugin-api'; import { TranslationResource } from '@backstage/frontend-plugin-api'; @@ -40,6 +40,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: false; + internal: false; } >; }; @@ -57,6 +58,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: false; + internal: false; } >; content: ExtensionInput< @@ -64,6 +66,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: false; + internal: false; } >; }; @@ -89,6 +92,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; content: ExtensionInput< @@ -100,6 +104,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: true; + internal: true; } >; }; @@ -121,6 +126,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: true; + internal: true; } >; signInPage: ExtensionInput< @@ -132,6 +138,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: true; + internal: true; } >; children: ExtensionInput< @@ -139,6 +146,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: true; optional: false; + internal: false; } >; elements: ExtensionInput< @@ -146,6 +154,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; wrappers: ExtensionInput< @@ -157,6 +166,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: true; } >; }; @@ -182,6 +192,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -218,6 +229,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -262,6 +274,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: true; } >; }; @@ -471,6 +484,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: true; } >; }; @@ -592,6 +606,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -669,6 +684,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: true; } >; }; @@ -703,6 +719,7 @@ const appPlugin: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: true; } >; }; diff --git a/plugins/app/src/alpha/appModulePublicSignIn.test.tsx b/plugins/app/src/alpha/appModulePublicSignIn.test.tsx index 787b8949e5..c7f1e5a68d 100644 --- a/plugins/app/src/alpha/appModulePublicSignIn.test.tsx +++ b/plugins/app/src/alpha/appModulePublicSignIn.test.tsx @@ -14,10 +14,8 @@ * limitations under the License. */ -import { - SignInPageBlueprint, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; import { render, screen, waitFor } from '@testing-library/react'; import { useEffect } from 'react'; import { appModulePublicSignIn } from './appModulePublicSignIn'; diff --git a/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx b/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx index bdd81f338c..9d3483319f 100644 --- a/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx +++ b/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx @@ -20,9 +20,9 @@ import { createExtensionInput, createFrontendModule, createSwappableComponent, - SwappableComponentBlueprint, swappableComponentsApiRef, } from '@backstage/frontend-plugin-api'; +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { DefaultSwappableComponentsApi } from './DefaultSwappableComponentsApi'; import { render, screen } from '@testing-library/react'; import { renderInTestApp, renderTestApp } from '@backstage/frontend-test-utils'; diff --git a/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.tsx b/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.tsx index 593fff24ff..b7c496b3d1 100644 --- a/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.tsx +++ b/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.tsx @@ -17,8 +17,8 @@ import { SwappableComponentRef, SwappableComponentsApi, - SwappableComponentBlueprint, } from '@backstage/frontend-plugin-api'; +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { OpaqueSwappableComponentRef } from '@internal/frontend'; import { lazy } from 'react'; diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index 8e76a16781..12e0c1a5c8 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -19,14 +19,16 @@ import { coreExtensionData, createExtensionInput, NavItemBlueprint, - NavContentBlueprint, - NavContentComponentProps, routeResolutionApiRef, IconComponent, RouteRef, useApi, - NavContentComponent, } from '@backstage/frontend-plugin-api'; +import { + NavContentBlueprint, + NavContentComponent, + NavContentComponentProps, +} from '@backstage/plugin-app-react'; import { Sidebar, SidebarItem } from '@backstage/core-components'; import { useMemo } from 'react'; @@ -90,18 +92,11 @@ export const AppNav = createExtension({ content: createExtensionInput([NavContentBlueprint.dataRefs.component], { singleton: true, optional: true, + internal: true, }), }, 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 c9f3035e5f..6237643056 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -22,9 +22,6 @@ import { JSX, } from 'react'; import { - AppRootWrapperBlueprint, - RouterBlueprint, - SignInPageBlueprint, coreExtensionData, discoveryApiRef, fetchApiRef, @@ -33,6 +30,11 @@ import { createExtensionInput, routeResolutionApiRef, } from '@backstage/frontend-plugin-api'; +import { + AppRootWrapperBlueprint, + RouterBlueprint, + SignInPageBlueprint, +} from '@backstage/plugin-app-react'; import { DiscoveryApi, ErrorApi, @@ -59,37 +61,26 @@ export const AppRoot = createExtension({ router: createExtensionInput([RouterBlueprint.dataRefs.component], { singleton: true, optional: true, + internal: true, }), signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], { singleton: true, optional: true, + internal: true, }), children: createExtensionInput([coreExtensionData.reactElement], { singleton: true, }), elements: createExtensionInput([coreExtensionData.reactElement]), - wrappers: createExtensionInput([ - AppRootWrapperBlueprint.dataRefs.component, - ]), + wrappers: createExtensionInput( + [AppRootWrapperBlueprint.dataRefs.component], + { + internal: true, + }, + ), }, 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) { @@ -117,16 +108,8 @@ export const AppRoot = createExtension({ for (const wrapper of inputs.wrappers) { const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component); - const pluginId = wrapper.node.spec.plugin.id; if (Component) { content = {content}; - if (pluginId !== 'app') { - // eslint-disable-next-line no-console - console.warn( - `DEPRECATION WARNING: AppRootWrappers 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: ${wrapper.node.spec.id}`, - ); - } } } diff --git a/plugins/app/src/extensions/AppThemeApi.tsx b/plugins/app/src/extensions/AppThemeApi.tsx index 2b265ddb53..e9fa23b574 100644 --- a/plugins/app/src/extensions/AppThemeApi.tsx +++ b/plugins/app/src/extensions/AppThemeApi.tsx @@ -22,10 +22,10 @@ import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; import { createExtensionInput, - ThemeBlueprint, ApiBlueprint, appThemeApiRef, } from '@backstage/frontend-plugin-api'; +import { ThemeBlueprint } from '@backstage/plugin-app-react'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppThemeSelector } from '../../../../packages/core-app-api/src/apis/implementations'; @@ -37,6 +37,7 @@ export const AppThemeApi = ApiBlueprint.makeWithOverrides({ inputs: { themes: createExtensionInput([ThemeBlueprint.dataRefs.theme], { replaces: [{ id: 'app', input: 'themes' }], + internal: true, }), }, factory: (originalFactory, { inputs }) => { @@ -45,19 +46,6 @@ export const AppThemeApi = ApiBlueprint.makeWithOverrides({ api: appThemeApiRef, deps: {}, 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/DefaultSignInPage.tsx b/plugins/app/src/extensions/DefaultSignInPage.tsx index 10ad638b00..48d34c09c2 100644 --- a/plugins/app/src/extensions/DefaultSignInPage.tsx +++ b/plugins/app/src/extensions/DefaultSignInPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SignInPageBlueprint } from '@backstage/frontend-plugin-api'; +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; import { SignInPage } from '@backstage/core-components'; export const DefaultSignInPage = SignInPageBlueprint.make({ diff --git a/plugins/app/src/extensions/IconsApi.ts b/plugins/app/src/extensions/IconsApi.ts index f10e927161..d8a97eb7c7 100644 --- a/plugins/app/src/extensions/IconsApi.ts +++ b/plugins/app/src/extensions/IconsApi.ts @@ -16,10 +16,10 @@ import { createExtensionInput, - IconBundleBlueprint, ApiBlueprint, iconsApiRef, } from '@backstage/frontend-plugin-api'; +import { IconBundleBlueprint } from '@backstage/plugin-app-react'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { DefaultIconsApi } from '../../../../packages/frontend-app-api/src/apis/implementations/IconsApi'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports @@ -33,6 +33,7 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ inputs: { icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons], { replaces: [{ id: 'app', input: 'icons' }], + internal: true, }), }, factory: (originalFactory, { inputs }) => { @@ -41,19 +42,6 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({ api: iconsApiRef, deps: {}, 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)) diff --git a/plugins/app/src/extensions/SwappableComponentsApi.ts b/plugins/app/src/extensions/SwappableComponentsApi.ts index f261d999c0..838b80e942 100644 --- a/plugins/app/src/extensions/SwappableComponentsApi.ts +++ b/plugins/app/src/extensions/SwappableComponentsApi.ts @@ -15,11 +15,11 @@ */ import { - SwappableComponentBlueprint, createExtensionInput, ApiBlueprint, swappableComponentsApiRef, } from '@backstage/frontend-plugin-api'; +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { DefaultSwappableComponentsApi } from '../apis/SwappableComponentsApi'; /** @@ -28,9 +28,12 @@ import { DefaultSwappableComponentsApi } from '../apis/SwappableComponentsApi'; export const SwappableComponentsApi = ApiBlueprint.makeWithOverrides({ name: 'swappable-components', inputs: { - components: createExtensionInput([ - SwappableComponentBlueprint.dataRefs.component, - ]), + components: createExtensionInput( + [SwappableComponentBlueprint.dataRefs.component], + { + internal: true, + }, + ), }, factory: (originalFactory, { inputs }) => { return originalFactory(defineParams => @@ -38,25 +41,8 @@ export const SwappableComponentsApi = ApiBlueprint.makeWithOverrides({ api: swappableComponentsApiRef, deps: {}, factory: () => { - const nonAppExtensions = inputs.components.filter( - i => i.node.spec.plugin?.id !== 'app', - ); - - if (nonAppExtensions.length > 0) { - // eslint-disable-next-line no-console - console.warn( - `SwappableComponents should only be installed as an extension in the app plugin. You can either use appPlugin.override(), or provide a module for the app-plugin with the extension there instead. Invalid extensions: ${nonAppExtensions - .map(i => i.node.spec.id) - .join(', ')}`, - ); - } - - const appExtensions = inputs.components.filter( - i => i.node.spec.plugin?.id === 'app', - ); - return DefaultSwappableComponentsApi.fromComponents( - appExtensions.map(i => + inputs.components.map(i => i.get(SwappableComponentBlueprint.dataRefs.component), ), ); diff --git a/plugins/app/src/extensions/TranslationsApi.tsx b/plugins/app/src/extensions/TranslationsApi.tsx index 2a6017af4b..a327aba299 100644 --- a/plugins/app/src/extensions/TranslationsApi.tsx +++ b/plugins/app/src/extensions/TranslationsApi.tsx @@ -15,9 +15,9 @@ */ import { ApiBlueprint, - TranslationBlueprint, createExtensionInput, } from '@backstage/frontend-plugin-api'; +import { TranslationBlueprint } from '@backstage/plugin-app-react'; import { appLanguageApiRef, translationApiRef, @@ -34,7 +34,7 @@ export const TranslationsApi = ApiBlueprint.makeWithOverrides({ inputs: { translations: createExtensionInput( [TranslationBlueprint.dataRefs.translation], - { replaces: [{ id: 'app', input: 'translations' }] }, + { replaces: [{ id: 'app', input: 'translations' }], internal: true }, ), }, factory: (originalFactory, { inputs }) => { @@ -43,19 +43,6 @@ export const TranslationsApi = ApiBlueprint.makeWithOverrides({ api: translationApiRef, deps: { languageApi: appLanguageApiRef }, 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 => diff --git a/plugins/app/src/extensions/components.tsx b/plugins/app/src/extensions/components.tsx index 99f6291e87..72a77e2446 100644 --- a/plugins/app/src/extensions/components.tsx +++ b/plugins/app/src/extensions/components.tsx @@ -17,9 +17,8 @@ import { NotFoundErrorPage as SwappableNotFoundErrorPage, Progress as SwappableProgress, ErrorDisplay as SwappableErrorDisplay, - SwappableComponentBlueprint, } from '@backstage/frontend-plugin-api'; - +import { SwappableComponentBlueprint } from '@backstage/plugin-app-react'; import { ErrorPage, ErrorPanel, diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index f70572e371..e83089b611 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -345,6 +345,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -794,6 +795,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; cards: ExtensionInput< @@ -822,6 +824,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -1000,6 +1003,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -1064,6 +1068,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; contents: ExtensionInput< @@ -1105,6 +1110,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; contextMenuItems: ExtensionInput< @@ -1119,6 +1125,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; diff --git a/plugins/devtools/report-alpha.api.md b/plugins/devtools/report-alpha.api.md index 3f78a90587..8f3e24f7f3 100644 --- a/plugins/devtools/report-alpha.api.md +++ b/plugins/devtools/report-alpha.api.md @@ -92,6 +92,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: true; + internal: false; } >; }; diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 6344805372..99e9b07463 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -85,6 +85,7 @@ const _default: OverridableFrontendPlugin< { singleton: true; optional: true; + internal: false; } >; }; diff --git a/plugins/scaffolder-react/report-alpha.api.md b/plugins/scaffolder-react/report-alpha.api.md index 2839851521..1befe9ca46 100644 --- a/plugins/scaffolder-react/report-alpha.api.md +++ b/plugins/scaffolder-react/report-alpha.api.md @@ -219,6 +219,7 @@ export const formFieldsApi: OverridableExtensionDefinition<{ { singleton: false; optional: false; + internal: false; } >; }; diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 7ffe367327..b9aa82782f 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -91,6 +91,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -118,6 +119,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -218,6 +220,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -411,6 +414,7 @@ export const formDecoratorsApi: OverridableExtensionDefinition<{ { singleton: false; optional: false; + internal: false; } >; }; diff --git a/plugins/search/report-alpha.api.md b/plugins/search/report-alpha.api.md index b129d4ab6c..2e82c77416 100644 --- a/plugins/search/report-alpha.api.md +++ b/plugins/search/report-alpha.api.md @@ -97,6 +97,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; resultTypes: ExtensionInput< @@ -112,6 +113,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; searchFilters: ExtensionInput< @@ -125,6 +127,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; @@ -215,6 +218,7 @@ export const searchPage: OverridableExtensionDefinition<{ { singleton: false; optional: false; + internal: false; } >; resultTypes: ExtensionInput< @@ -230,6 +234,7 @@ export const searchPage: OverridableExtensionDefinition<{ { singleton: false; optional: false; + internal: false; } >; searchFilters: ExtensionInput< @@ -243,6 +248,7 @@ export const searchPage: OverridableExtensionDefinition<{ { singleton: false; optional: false; + internal: false; } >; }; diff --git a/plugins/techdocs/report-alpha.api.md b/plugins/techdocs/report-alpha.api.md index 96c778b09b..cbdb912938 100644 --- a/plugins/techdocs/report-alpha.api.md +++ b/plugins/techdocs/report-alpha.api.md @@ -84,6 +84,7 @@ const _default: OverridableFrontendPlugin< { singleton: boolean; optional: boolean; + internal?: boolean; } >; }; @@ -146,6 +147,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; emptyState: ExtensionInput< @@ -159,6 +161,7 @@ const _default: OverridableFrontendPlugin< { singleton: true; optional: true; + internal: false; } >; }; @@ -290,6 +293,7 @@ const _default: OverridableFrontendPlugin< { singleton: false; optional: false; + internal: false; } >; }; diff --git a/plugins/user-settings/report-alpha.api.md b/plugins/user-settings/report-alpha.api.md index 73ef5cfa3d..7ff63e2403 100644 --- a/plugins/user-settings/report-alpha.api.md +++ b/plugins/user-settings/report-alpha.api.md @@ -66,6 +66,7 @@ const _default: OverridableFrontendPlugin< { singleton: true; optional: true; + internal: false; } >; }; diff --git a/yarn.lock b/yarn.lock index 97185a87a4..ee0d51463d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3571,6 +3571,7 @@ __metadata: "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" + "@backstage/plugin-app-react": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -3861,6 +3862,7 @@ __metadata: "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-app": "workspace:^" + "@backstage/plugin-app-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@react-hookz/web": "npm:^24.0.0" "@testing-library/jest-dom": "npm:^6.0.0" @@ -30697,6 +30699,7 @@ __metadata: "@backstage/integration-react": "workspace:^" "@backstage/plugin-api-docs": "workspace:^" "@backstage/plugin-app": "workspace:^" + "@backstage/plugin-app-react": "workspace:^" "@backstage/plugin-app-visualizer": "workspace:^" "@backstage/plugin-auth": "workspace:^" "@backstage/plugin-auth-react": "workspace:^"