diff --git a/.changeset/forty-insects-drop.md b/.changeset/forty-insects-drop.md
new file mode 100644
index 0000000000..e2a94662ed
--- /dev/null
+++ b/.changeset/forty-insects-drop.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-test-utils': patch
+---
+
+Updates for compatibility with the new extension IDs.
diff --git a/.changeset/rich-bees-breathe.md b/.changeset/rich-bees-breathe.md
new file mode 100644
index 0000000000..7a40e3df2f
--- /dev/null
+++ b/.changeset/rich-bees-breathe.md
@@ -0,0 +1,17 @@
+---
+'@backstage/plugin-catalog-import': patch
+'@backstage/plugin-stack-overflow': patch
+'@backstage/plugin-catalog-react': patch
+'@backstage/plugin-user-settings': patch
+'@backstage/plugin-search-react': patch
+'@backstage/plugin-tech-radar': patch
+'@backstage/plugin-graphiql': patch
+'@backstage/plugin-techdocs': patch
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-explore': patch
+'@backstage/plugin-search': patch
+'@backstage/plugin-home': patch
+'@backstage/plugin-adr': patch
+---
+
+Refactor of the alpha exports due to API change in how extension IDs are constructed.
diff --git a/.changeset/stale-frogs-agree.md b/.changeset/stale-frogs-agree.md
new file mode 100644
index 0000000000..84052bded8
--- /dev/null
+++ b/.changeset/stale-frogs-agree.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-app-api': patch
+---
+
+Updates to match the introduction of `ExtensionDefinition` and new extension ID naming patterns.
diff --git a/.changeset/stupid-wasps-fold.md b/.changeset/stupid-wasps-fold.md
new file mode 100644
index 0000000000..260dd9f5f1
--- /dev/null
+++ b/.changeset/stupid-wasps-fold.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-compat-api': patch
+---
+
+Updates for compatibility with the new extension IDs.
diff --git a/.changeset/ten-snakes-wait.md b/.changeset/ten-snakes-wait.md
new file mode 100644
index 0000000000..1c15cb8d33
--- /dev/null
+++ b/.changeset/ten-snakes-wait.md
@@ -0,0 +1,7 @@
+---
+'@backstage/frontend-plugin-api': minor
+---
+
+**BREAKING**: This version changes how extensions are created and how their IDs are determined. The `createExtension` function now accepts `kind`, `namespace` and `name` instead of `id`. All of the new options are optional, and are used to construct the final extension ID. By convention extension creators should set the `kind` to match their own name, for example `createNavItemExtension` sets the kind `nav-item`.
+
+The `createExtension` function as well as all extension creators now also return an `ExtensionDefinition` rather than an `Extension`, which in turn needs to be passed to `createPlugin` or `createExtensionOverrides` to be used.
diff --git a/packages/app-next-example-plugin/src/plugin.tsx b/packages/app-next-example-plugin/src/plugin.tsx
index a3c95e900c..99c377b7e5 100644
--- a/packages/app-next-example-plugin/src/plugin.tsx
+++ b/packages/app-next-example-plugin/src/plugin.tsx
@@ -21,7 +21,6 @@ import {
} from '@backstage/frontend-plugin-api';
export const ExamplePage = createPageExtension({
- id: 'example.page',
defaultPath: '/example',
loader: () => import('./Component').then(m => ),
});
diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
index 34cd78d480..3eaab2fa1a 100644
--- a/packages/app-next/app-config.yaml
+++ b/packages/app-next/app-config.yaml
@@ -7,17 +7,17 @@ app:
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
extensions:
- - apis.plugin.graphiql.browse.gitlab: true
+ # - apis.plugin.graphiql.browse.gitlab: true
+ - graphiql-endpoint:graphiql/gitlab: true
- # Entity page cards
- - entity.cards.about
- - entity.cards.labels
- - entity.cards.links:
+ - entity-card:catalog/about
+ - entity-card:catalog/labels
+ - entity-card:catalog/links:
config:
filter: kind:component has:links
# Entity page content
- - entity.content.techdocs
+ - entity-content:techdocs
# scmAuthExtension: >-
# createScmAuthExtension({
diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
index 791d2b38fa..ccfeb61b58 100644
--- a/packages/app-next/src/App.tsx
+++ b/packages/app-next/src/App.tsx
@@ -83,7 +83,7 @@ TODO:
/* app.tsx */
const homePageExtension = createExtension({
- id: 'myhomepage',
+ name: 'myhomepage',
attachTo: { id: 'home', input: 'props' },
output: {
children: coreExtensionData.reactElement,
@@ -95,7 +95,7 @@ const homePageExtension = createExtension({
});
const signInPage = createSignInPageExtension({
- id: 'signInPage',
+ name: 'guest',
loader: async () => (props: SignInPageProps) =>
,
});
diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx
index 1e9409a5e4..c5069f99c3 100644
--- a/packages/app-next/src/examples/pagesPlugin.tsx
+++ b/packages/app-next/src/examples/pagesPlugin.tsx
@@ -36,7 +36,7 @@ export const pageXRouteRef = createRouteRef();
// });
const IndexPage = createPageExtension({
- id: 'index',
+ name: 'index',
defaultPath: '/',
routeRef: indexRouteRef,
loader: async () => {
@@ -68,7 +68,7 @@ const IndexPage = createPageExtension({
});
const Page1 = createPageExtension({
- id: 'page1',
+ name: 'page1',
defaultPath: '/page1',
routeRef: page1RouteRef,
loader: async () => {
@@ -102,7 +102,7 @@ const Page1 = createPageExtension({
});
const ExternalPage = createPageExtension({
- id: 'pageX',
+ name: 'pageX',
defaultPath: '/pageX',
routeRef: pageXRouteRef,
loader: async () => {
diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md
index 866517b5c8..93a2fa8cf1 100644
--- a/packages/core-compat-api/api-report.md
+++ b/packages/core-compat-api/api-report.md
@@ -8,7 +8,7 @@
import { AnyRouteRefParams } from '@backstage/core-plugin-api';
import { AppComponents } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api';
@@ -21,7 +21,7 @@ import { SubRouteRef as SubRouteRef_2 } from '@backstage/frontend-plugin-api';
// @public (undocumented)
export function collectLegacyComponents(
components: Partial,
-): Extension[];
+): ExtensionDefinition[];
// @public (undocumented)
export function collectLegacyRoutes(
diff --git a/packages/core-compat-api/src/collectLegacyComponents.test.tsx b/packages/core-compat-api/src/collectLegacyComponents.test.tsx
index 0600f58cf8..7ca11a777e 100644
--- a/packages/core-compat-api/src/collectLegacyComponents.test.tsx
+++ b/packages/core-compat-api/src/collectLegacyComponents.test.tsx
@@ -31,28 +31,33 @@ describe('collectLegacyComponents', () => {
expect(
collected.map(p => ({
- id: p.id,
+ kind: p.kind,
+ namespace: p.namespace,
attachTo: p.attachTo,
disabled: p.disabled,
})),
).toEqual([
{
- id: 'core.components.progress',
+ kind: 'component',
+ namespace: 'core.components.progress',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
{
- id: 'core.components.bootErrorPage',
+ kind: 'component',
+ namespace: 'core.components.bootErrorPage',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
{
- id: 'core.components.notFoundErrorPage',
+ kind: 'component',
+ namespace: 'core.components.notFoundErrorPage',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
{
- id: 'core.components.errorBoundaryFallback',
+ kind: 'component',
+ namespace: 'core.components.errorBoundaryFallback',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
diff --git a/packages/core-compat-api/src/collectLegacyComponents.tsx b/packages/core-compat-api/src/collectLegacyComponents.tsx
index 6092ca1be7..6ae6df7cff 100644
--- a/packages/core-compat-api/src/collectLegacyComponents.tsx
+++ b/packages/core-compat-api/src/collectLegacyComponents.tsx
@@ -15,10 +15,10 @@
*/
import {
- Extension,
ComponentRef,
createComponentExtension,
coreComponentsRefs,
+ ExtensionDefinition,
} from '@backstage/frontend-plugin-api';
import { AppComponents } from '@backstage/core-plugin-api';
@@ -33,7 +33,7 @@ const refs: Record> = {
/** @public */
export function collectLegacyComponents(components: Partial) {
- return Object.entries(components).reduce[]>(
+ return Object.entries(components).reduce[]>(
(extensions, [name, component]) => {
const ref = refs[name];
return ref
diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
index d09911b39d..3dd9f330cc 100644
--- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
+++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx
@@ -51,13 +51,13 @@ describe('collectLegacyRoutes', () => {
id: 'score-card',
extensions: [
{
- id: 'plugin.score-card.page',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:score-card',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'score-board' },
},
{
- id: 'apis.plugin.scoringdata.service',
+ id: 'api:plugin.scoringdata.service',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
},
@@ -67,13 +67,13 @@ describe('collectLegacyRoutes', () => {
id: 'stackstorm',
extensions: [
{
- id: 'plugin.stackstorm.page',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:stackstorm',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'stackstorm' },
},
{
- id: 'apis.plugin.stackstorm.service',
+ id: 'api:plugin.stackstorm.service',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
},
@@ -83,19 +83,19 @@ describe('collectLegacyRoutes', () => {
id: 'puppetDb',
extensions: [
{
- id: 'plugin.puppetDb.page',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:puppetDb',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'puppetdb' },
},
{
- id: 'plugin.puppetDb.page2',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:puppetDb/2',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'puppetdb' },
},
{
- id: 'apis.plugin.puppetdb.service',
+ id: 'api:plugin.puppetdb.service',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
},
diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx
index 037107bfda..92bced29a2 100644
--- a/packages/core-compat-api/src/collectLegacyRoutes.tsx
+++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx
@@ -16,11 +16,11 @@
import React, { ReactNode } from 'react';
import {
- Extension,
createApiExtension,
createPageExtension,
createPlugin,
BackstagePlugin,
+ ExtensionDefinition,
} from '@backstage/frontend-plugin-api';
import { Route, Routes } from 'react-router-dom';
import {
@@ -64,7 +64,7 @@ export function collectLegacyRoutes(
): BackstagePlugin[] {
const createdPluginIds = new Map<
LegacyBackstagePlugin,
- Extension[]
+ ExtensionDefinition[]
>();
React.Children.forEach(
@@ -95,19 +95,18 @@ export function collectLegacyRoutes(
'core.mountPoint',
);
- const pluginId = plugin.getId();
-
const detectedExtensions =
- createdPluginIds.get(plugin) ?? new Array>();
+ createdPluginIds.get(plugin) ??
+ new Array>();
createdPluginIds.set(plugin, detectedExtensions);
const path: string = route.props.path;
detectedExtensions.push(
createPageExtension({
- id: `plugin.${pluginId}.page${
- detectedExtensions.length ? detectedExtensions.length + 1 : ''
- }`,
+ name: detectedExtensions.length
+ ? String(detectedExtensions.length + 1)
+ : undefined,
defaultPath: path[0] === '/' ? path.slice(1) : path,
routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined,
@@ -132,9 +131,7 @@ export function collectLegacyRoutes(
extensions: [
...extensions,
...Array.from(plugin.getApis()).map(factory =>
- createApiExtension({
- factory,
- }),
+ createApiExtension({ factory }),
),
],
}),
diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx
index 0f1f6145c9..4a5be57bc8 100644
--- a/packages/core-compat-api/src/convertLegacyApp.test.tsx
+++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx
@@ -59,13 +59,13 @@ describe('convertLegacyApp', () => {
id: 'score-card',
extensions: [
{
- id: 'plugin.score-card.page',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:score-card',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'score-board' },
},
{
- id: 'apis.plugin.scoringdata.service',
+ id: 'api:plugin.scoringdata.service',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
},
@@ -75,13 +75,13 @@ describe('convertLegacyApp', () => {
id: 'stackstorm',
extensions: [
{
- id: 'plugin.stackstorm.page',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:stackstorm',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'stackstorm' },
},
{
- id: 'apis.plugin.stackstorm.service',
+ id: 'api:plugin.stackstorm.service',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
},
@@ -91,19 +91,19 @@ describe('convertLegacyApp', () => {
id: 'puppetDb',
extensions: [
{
- id: 'plugin.puppetDb.page',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:puppetDb',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'puppetdb' },
},
{
- id: 'plugin.puppetDb.page2',
- attachTo: { id: 'core.routes', input: 'routes' },
+ id: 'page:puppetDb/2',
+ attachTo: { id: 'core/routes', input: 'routes' },
disabled: false,
defaultConfig: { path: 'puppetdb' },
},
{
- id: 'apis.plugin.puppetdb.service',
+ id: 'api:plugin.puppetdb.service',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
},
@@ -113,13 +113,13 @@ describe('convertLegacyApp', () => {
id: undefined,
extensions: [
{
- id: 'core.layout',
+ id: 'core/layout',
attachTo: { id: 'core', input: 'root' },
disabled: false,
},
{
- id: 'core.nav',
- attachTo: { id: 'core.layout', input: 'nav' },
+ id: 'core/nav',
+ attachTo: { id: 'core/layout', input: 'nav' },
disabled: true,
},
],
diff --git a/packages/core-compat-api/src/convertLegacyApp.ts b/packages/core-compat-api/src/convertLegacyApp.ts
index 93521b3955..3b12f0e1fd 100644
--- a/packages/core-compat-api/src/convertLegacyApp.ts
+++ b/packages/core-compat-api/src/convertLegacyApp.ts
@@ -100,7 +100,8 @@ export function convertLegacyApp(
const [routesEl] = routesEls;
const CoreLayoutOverride = createExtension({
- id: 'core.layout',
+ namespace: 'core',
+ name: 'layout',
attachTo: { id: 'core', input: 'root' },
inputs: {
content: createExtensionInput(
@@ -121,8 +122,9 @@ export function convertLegacyApp(
},
});
const CoreNavOverride = createExtension({
- id: 'core.nav',
- attachTo: { id: 'core.layout', input: 'nav' },
+ namespace: 'core',
+ name: 'nav',
+ attachTo: { id: 'core/layout', input: 'nav' },
output: {},
factory: () => ({}),
disabled: true,
diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx
index 441cb5bcf0..472ed4905a 100644
--- a/packages/frontend-app-api/src/extensions/Core.tsx
+++ b/packages/frontend-app-api/src/extensions/Core.tsx
@@ -21,7 +21,7 @@ import {
} from '@backstage/frontend-plugin-api';
export const Core = createExtension({
- id: 'core',
+ namespace: 'core',
attachTo: { id: 'root', input: 'default' }, // ignored
inputs: {
apis: createExtensionInput({
diff --git a/packages/frontend-app-api/src/extensions/CoreLayout.tsx b/packages/frontend-app-api/src/extensions/CoreLayout.tsx
index 0099b67751..94681120db 100644
--- a/packages/frontend-app-api/src/extensions/CoreLayout.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreLayout.tsx
@@ -23,8 +23,9 @@ import {
import { SidebarPage } from '@backstage/core-components';
export const CoreLayout = createExtension({
- id: 'core.layout',
- attachTo: { id: 'core.router', input: 'children' },
+ namespace: 'core',
+ name: 'layout',
+ attachTo: { id: 'core/router', input: 'children' },
inputs: {
nav: createExtensionInput(
{
diff --git a/packages/frontend-app-api/src/extensions/CoreNav.tsx b/packages/frontend-app-api/src/extensions/CoreNav.tsx
index 8d0d4ea5de..21d113d3d6 100644
--- a/packages/frontend-app-api/src/extensions/CoreNav.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreNav.tsx
@@ -75,8 +75,9 @@ const SidebarNavItem = (props: NavTarget) => {
};
export const CoreNav = createExtension({
- id: 'core.nav',
- attachTo: { id: 'core.layout', input: 'nav' },
+ namespace: 'core',
+ name: 'nav',
+ attachTo: { id: 'core/layout', input: 'nav' },
inputs: {
items: createExtensionInput({
target: coreExtensionData.navTarget,
diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx
index 510af04b51..088fd25ae7 100644
--- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx
@@ -36,7 +36,8 @@ import { signInPageComponentDataRef } from '../../../frontend-plugin-api/src/ext
import { RouteTracker } from '../routing/RouteTracker';
export const CoreRouter = createExtension({
- id: 'core.router',
+ namespace: 'core',
+ name: 'router',
attachTo: { id: 'core', input: 'root' },
inputs: {
signInPage: createExtensionInput(
diff --git a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx
index 9acd27b6bf..d77839f7b3 100644
--- a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx
@@ -26,8 +26,9 @@ import {
import { useRoutes } from 'react-router-dom';
export const CoreRoutes = createExtension({
- id: 'core.routes',
- attachTo: { id: 'core.layout', input: 'content' },
+ namespace: 'core',
+ name: 'routes',
+ attachTo: { id: 'core/layout', input: 'content' },
inputs: {
routes: createExtensionInput({
path: coreExtensionData.routePath,
diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts
index a06d236b77..f0ce6ac590 100644
--- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts
+++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts
@@ -20,7 +20,7 @@ import { extractRouteInfoFromAppNode } from './extractRouteInfoFromAppNode';
import {
AnyRouteRefParams,
AppNode,
- Extension,
+ ExtensionDefinition,
RouteRef,
coreExtensionData,
createExtension,
@@ -40,16 +40,16 @@ const ref5 = createRouteRef();
const refOrder: RouteRef[] = [ref1, ref2, ref3, ref4, ref5];
function createTestExtension(options: {
- id: string;
+ name: string;
parent?: string;
path?: string;
routeRef?: RouteRef;
}) {
return createExtension({
- id: options.id,
+ name: options.name,
attachTo: options.parent
- ? { id: options.parent, input: 'children' }
- : { id: 'core.routes', input: 'routes' },
+ ? { id: `test/${options.parent}`, input: 'children' }
+ : { id: 'core/routes', input: 'routes' },
output: {
element: coreExtensionData.reactElement,
path: coreExtensionData.routePath.optional(),
@@ -70,14 +70,14 @@ function createTestExtension(options: {
});
}
-function routeInfoFromExtensions(extensions: Extension[]) {
+function routeInfoFromExtensions(extensions: ExtensionDefinition[]) {
const plugin = createPlugin({
id: 'test',
extensions,
});
const tree = createAppTree({
- builtinExtensions,
config: new MockConfigApi({}),
+ builtinExtensions,
features: [plugin],
});
@@ -122,33 +122,33 @@ describe('discovery', () => {
it('should collect routes', () => {
const info = routeInfoFromExtensions([
createTestExtension({
- id: 'nothing',
+ name: 'nothing',
path: 'nothing',
}),
createTestExtension({
- id: 'page1',
+ name: 'page1',
path: 'foo',
routeRef: ref1,
}),
createTestExtension({
- id: 'page2',
+ name: 'page2',
parent: 'page1',
path: 'bar/:id',
routeRef: ref2,
}),
createTestExtension({
- id: 'page3',
+ name: 'page3',
parent: 'page2',
path: 'baz',
routeRef: ref3,
}),
createTestExtension({
- id: 'page4',
+ name: 'page4',
path: 'divsoup',
routeRef: ref4,
}),
createTestExtension({
- id: 'page5',
+ name: 'page5',
parent: 'page1',
path: 'blop',
routeRef: ref5,
@@ -226,29 +226,29 @@ describe('discovery', () => {
it('should handle all react router Route patterns', () => {
const info = routeInfoFromExtensions([
createTestExtension({
- id: 'page1',
+ name: 'page1',
path: 'foo',
routeRef: ref1,
}),
createTestExtension({
- id: 'page2',
+ name: 'page2',
parent: 'page1',
path: 'bar/:id',
routeRef: ref2,
}),
createTestExtension({
- id: 'page3',
+ name: 'page3',
path: 'baz',
routeRef: ref3,
}),
createTestExtension({
- id: 'page4',
+ name: 'page4',
parent: 'page3',
path: 'divsoup',
routeRef: ref4,
}),
createTestExtension({
- id: 'page5',
+ name: 'page5',
parent: 'page3',
path: 'blop',
routeRef: ref5,
@@ -274,29 +274,29 @@ describe('discovery', () => {
it('should strip leading slashes in route paths', () => {
const info = routeInfoFromExtensions([
createTestExtension({
- id: 'page1',
+ name: 'page1',
path: '/foo',
routeRef: ref1,
}),
createTestExtension({
- id: 'page2',
+ name: 'page2',
parent: 'page1',
path: '/bar/:id',
routeRef: ref2,
}),
createTestExtension({
- id: 'page3',
+ name: 'page3',
path: '/baz',
routeRef: ref3,
}),
createTestExtension({
- id: 'page4',
+ name: 'page4',
parent: 'page3',
path: '/divsoup',
routeRef: ref4,
}),
createTestExtension({
- id: 'page5',
+ name: 'page5',
parent: 'page3',
path: '/blop',
routeRef: ref5,
@@ -322,44 +322,44 @@ describe('discovery', () => {
it('should use the route aggregator key to bind child routes to the same path', () => {
const info = routeInfoFromExtensions([
createTestExtension({
- id: 'foo',
+ name: 'foo',
path: 'foo',
}),
createTestExtension({
- id: 'page1',
+ name: 'page1',
parent: 'foo',
routeRef: ref1,
}),
createTestExtension({
- id: 'fooChild',
+ name: 'fooChild',
parent: 'foo',
}),
createTestExtension({
- id: 'page2',
+ name: 'page2',
parent: 'fooChild',
routeRef: ref2,
}),
createTestExtension({
- id: 'fooEmpty',
+ name: 'fooEmpty',
parent: 'foo',
}),
createTestExtension({
- id: 'page3',
+ name: 'page3',
path: 'bar',
routeRef: ref3,
}),
createTestExtension({
- id: 'page3Child',
+ name: 'page3Child',
parent: 'page3',
path: '',
}),
createTestExtension({
- id: 'page4',
+ name: 'page4',
parent: 'page3Child',
routeRef: ref4,
}),
createTestExtension({
- id: 'page5',
+ name: 'page5',
parent: 'page4',
routeRef: ref5,
}),
@@ -411,34 +411,34 @@ describe('discovery', () => {
it('should use the route aggregator but stop when encountering explicit path', () => {
const info = routeInfoFromExtensions([
createTestExtension({
- id: 'page1',
+ name: 'page1',
path: 'foo',
routeRef: ref1,
}),
createTestExtension({
- id: 'page1Child',
+ name: 'page1Child',
parent: 'page1',
path: 'bar',
}),
createTestExtension({
- id: 'page2',
+ name: 'page2',
parent: 'page1Child',
routeRef: ref2,
}),
createTestExtension({
- id: 'page3',
+ name: 'page3',
parent: 'page2',
path: 'baz',
routeRef: ref3,
}),
createTestExtension({
- id: 'page4',
+ name: 'page4',
parent: 'page3',
path: '/blop',
routeRef: ref4,
}),
createTestExtension({
- id: 'page5',
+ name: 'page5',
parent: 'page2',
routeRef: ref5,
}),
@@ -500,34 +500,34 @@ describe('discovery', () => {
it('should account for loose route paths', () => {
const info = routeInfoFromExtensions([
createTestExtension({
- id: 'r',
+ name: 'r',
path: 'r',
}),
createTestExtension({
- id: 'page1',
+ name: 'page1',
parent: 'r',
path: 'x',
routeRef: ref1,
}),
createTestExtension({
- id: 'y',
+ name: 'y',
path: 'y',
parent: 'r',
}),
createTestExtension({
- id: 'page2',
+ name: 'page2',
parent: 'y',
path: '1',
routeRef: ref2,
}),
createTestExtension({
- id: 'page3',
+ name: 'page3',
parent: 'page2',
path: 'a',
routeRef: ref3,
}),
createTestExtension({
- id: 'page4',
+ name: 'page4',
parent: 'page2',
path: 'b',
routeRef: ref4,
diff --git a/packages/frontend-app-api/src/tree/createAppTree.test.ts b/packages/frontend-app-api/src/tree/createAppTree.test.ts
index 667e3a6fc3..15c8ab4750 100644
--- a/packages/frontend-app-api/src/tree/createAppTree.test.ts
+++ b/packages/frontend-app-api/src/tree/createAppTree.test.ts
@@ -54,12 +54,11 @@ describe('createAppTree', () => {
it('throws an error when a core extension is overridden', () => {
const config = new MockConfigApi({});
const features = [
- createPlugin({
- id: 'plugin',
+ createExtensionOverrides({
extensions: [
createExtension({
- id: 'core',
- attachTo: { id: 'core.routes', input: 'route' },
+ name: 'core',
+ attachTo: { id: 'core/routes', input: 'route' },
inputs: {},
output: {},
factory: () => ({}),
@@ -70,33 +69,7 @@ describe('createAppTree', () => {
expect(() =>
createAppTree({ features, config, builtinExtensions: [] }),
).toThrow(
- "It is forbidden to override the following extension(s): 'core', which is done by the following plugin(s): 'plugin'",
- );
- });
-
- it('throws an error when duplicated extensions are detected', () => {
- const config = new MockConfigApi({});
-
- const ExtensionA = createExtension({ ...extBase, id: 'A' });
-
- const ExtensionB = createExtension({ ...extBase, id: 'B' });
-
- const PluginA = createPlugin({
- id: 'A',
- extensions: [ExtensionA, ExtensionA],
- });
-
- const PluginB = createPlugin({
- id: 'B',
- extensions: [ExtensionA, ExtensionB, ExtensionB],
- });
-
- const features = [PluginA, PluginB];
-
- expect(() =>
- createAppTree({ features, config, builtinExtensions: [] }),
- ).toThrow(
- "The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'",
+ "It is forbidden to override the following extension(s): 'core', which is done by one or more extension overrides",
);
});
@@ -106,13 +79,13 @@ describe('createAppTree', () => {
features: [
createExtensionOverrides({
extensions: [
- createExtension({ ...extBase, id: 'a' }),
- createExtension({ ...extBase, id: 'a' }),
- createExtension({ ...extBase, id: 'b' }),
+ createExtension({ ...extBase, name: 'a' }),
+ createExtension({ ...extBase, name: 'a' }),
+ createExtension({ ...extBase, name: 'b' }),
],
}),
createExtensionOverrides({
- extensions: [createExtension({ ...extBase, id: 'b' })],
+ extensions: [createExtension({ ...extBase, name: 'b' })],
}),
],
config: new MockConfigApi({}),
diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts
index 802c71f729..7167038184 100644
--- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts
+++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts
@@ -28,28 +28,33 @@ import {
} from './instantiateAppNodeTree';
import { AppNodeInstance, AppNodeSpec } from '@backstage/frontend-plugin-api';
import { resolveAppTree } from './resolveAppTree';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
const testDataRef = createExtensionDataRef('test');
const otherDataRef = createExtensionDataRef('other');
const inputMirrorDataRef = createExtensionDataRef('mirror');
-const simpleExtension = createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- output: {
- test: testDataRef,
- other: otherDataRef.optional(),
- },
- configSchema: createSchemaFromZod(z =>
- z.object({
- output: z.string().default('test'),
- other: z.number().optional(),
- }),
- ),
- factory({ config }) {
- return { test: config.output, other: config.other };
- },
-});
+const simpleExtension = resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ output: {
+ test: testDataRef,
+ other: otherDataRef.optional(),
+ },
+ configSchema: createSchemaFromZod(z =>
+ z.object({
+ output: z.string().default('test'),
+ other: z.number().optional(),
+ }),
+ ),
+ factory({ config }) {
+ return { test: config.output, other: config.other };
+ },
+ }),
+);
function makeSpec(
extension: Extension,
@@ -117,19 +122,21 @@ describe('instantiateAppNodeTree', () => {
it('should instantiate a node with attachments', () => {
const tree = resolveAppTree('root-node', [
makeSpec(
- createExtension({
- id: 'root-node',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- test: createExtensionInput({ test: testDataRef }),
- },
- output: {
- inputMirror: inputMirrorDataRef,
- },
- factory({ inputs }) {
- return { inputMirror: inputs };
- },
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'root-node',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ test: createExtensionInput({ test: testDataRef }),
+ },
+ output: {
+ inputMirror: inputMirrorDataRef,
+ },
+ factory({ inputs }) {
+ return { inputMirror: inputs };
+ },
+ }),
+ ),
),
makeSpec(simpleExtension, {
id: 'child-node',
@@ -159,19 +166,21 @@ describe('instantiateAppNodeTree', () => {
const tree = resolveAppTree('root-node', [
{
...makeSpec(
- createExtension({
- id: 'root-node',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- test: createExtensionInput({ test: testDataRef }),
- },
- output: {
- inputMirror: inputMirrorDataRef,
- },
- factory({ inputs }) {
- return { inputMirror: inputs };
- },
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'root-node',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ test: createExtensionInput({ test: testDataRef }),
+ },
+ output: {
+ inputMirror: inputMirrorDataRef,
+ },
+ factory({ inputs }) {
+ return { inputMirror: inputs };
+ },
+ }),
+ ),
),
},
{
@@ -242,43 +251,46 @@ describe('createAppNodeInstance', () => {
const instance = createAppNodeInstance({
attachments,
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- optionalSingletonPresent: createExtensionInput(
- {
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ optionalSingletonPresent: createExtensionInput(
+ {
+ test: testDataRef,
+ other: otherDataRef.optional(),
+ },
+ { singleton: true, optional: true },
+ ),
+ optionalSingletonMissing: createExtensionInput(
+ {
+ test: testDataRef,
+ other: otherDataRef.optional(),
+ },
+ { singleton: true, optional: true },
+ ),
+ singleton: createExtensionInput(
+ {
+ test: testDataRef,
+ other: otherDataRef.optional(),
+ },
+ { singleton: true },
+ ),
+ many: createExtensionInput({
test: testDataRef,
other: otherDataRef.optional(),
- },
- { singleton: true, optional: true },
- ),
- optionalSingletonMissing: createExtensionInput(
- {
- test: testDataRef,
- other: otherDataRef.optional(),
- },
- { singleton: true, optional: true },
- ),
- singleton: createExtensionInput(
- {
- test: testDataRef,
- other: otherDataRef.optional(),
- },
- { singleton: true },
- ),
- many: createExtensionInput({
- test: testDataRef,
- other: otherDataRef.optional(),
- }),
- },
- output: {
- inputMirror: inputMirrorDataRef,
- },
- factory({ inputs }) {
- return { inputMirror: inputs };
- },
- }),
+ }),
+ },
+ output: {
+ inputMirror: inputMirrorDataRef,
+ },
+ factory({ inputs }) {
+ return { inputMirror: inputs };
+ },
+ }),
+ ),
),
});
@@ -297,7 +309,7 @@ describe('createAppNodeInstance', () => {
attachments: new Map(),
}),
).toThrow(
- "Invalid configuration for extension 'core.test'; caused by Error: Expected number, received string at 'other'",
+ "Invalid configuration for extension 'core/test'; caused by Error: Expected number, received string at 'other'",
);
});
@@ -305,21 +317,24 @@ describe('createAppNodeInstance', () => {
expect(() =>
createAppNodeInstance({
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- output: {},
- factory() {
- const error = new Error('NOPE');
- error.name = 'NopeError';
- throw error;
- },
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ output: {},
+ factory() {
+ const error = new Error('NOPE');
+ error.name = 'NopeError';
+ throw error;
+ },
+ }),
+ ),
),
attachments: new Map(),
}),
).toThrow(
- "Failed to instantiate extension 'core.test'; caused by NopeError: NOPE",
+ "Failed to instantiate extension 'core/test'; caused by NopeError: NOPE",
);
});
@@ -327,22 +342,25 @@ describe('createAppNodeInstance', () => {
expect(() =>
createAppNodeInstance({
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- output: {
- test1: testDataRef,
- test2: testDataRef,
- },
- factory({}) {
- return { test1: 'test', test2: 'test2' };
- },
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ output: {
+ test1: testDataRef,
+ test2: testDataRef,
+ },
+ factory({}) {
+ return { test1: 'test', test2: 'test2' };
+ },
+ }),
+ ),
),
attachments: new Map(),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', duplicate extension data 'test' received via output 'test2'",
+ "Failed to instantiate extension 'core/test', duplicate extension data 'test' received via output 'test2'",
);
});
@@ -350,21 +368,24 @@ describe('createAppNodeInstance', () => {
expect(() =>
createAppNodeInstance({
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- output: {
- test: testDataRef,
- },
- factory({}) {
- return { nonexistent: 'test' } as any;
- },
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ output: {
+ test: testDataRef,
+ },
+ factory({}) {
+ return { nonexistent: 'test' } as any;
+ },
+ }),
+ ),
),
attachments: new Map(),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', unknown output provided via 'nonexistent'",
+ "Failed to instantiate extension 'core/test', unknown output provided via 'nonexistent'",
);
});
@@ -372,25 +393,28 @@ describe('createAppNodeInstance', () => {
expect(() =>
createAppNodeInstance({
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- singleton: createExtensionInput(
- {
- test: testDataRef,
- },
- { singleton: true },
- ),
- },
- output: {},
- factory: () => ({}),
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ singleton: createExtensionInput(
+ {
+ test: testDataRef,
+ },
+ { singleton: true },
+ ),
+ },
+ output: {},
+ factory: () => ({}),
+ }),
+ ),
),
attachments: new Map(),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', input 'singleton' is required but was not received",
+ "Failed to instantiate extension 'core/test', input 'singleton' is required but was not received",
);
});
@@ -416,21 +440,24 @@ describe('createAppNodeInstance', () => {
],
]),
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- declared: createExtensionInput({
- test: testDataRef,
- }),
- },
- output: {},
- factory: () => ({}),
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ declared: createExtensionInput({
+ test: testDataRef,
+ }),
+ },
+ output: {},
+ factory: () => ({}),
+ }),
+ ),
),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', received undeclared input 'undeclared' from extension 'core.test'",
+ "Failed to instantiate extension 'core/test', received undeclared input 'undeclared' from extension 'core/test'",
);
});
@@ -451,16 +478,19 @@ describe('createAppNodeInstance', () => {
],
]),
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- output: {},
- factory: () => ({}),
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ output: {},
+ factory: () => ({}),
+ }),
+ ),
),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', received undeclared inputs 'undeclared1' from extension 'core.test' and 'undeclared2' from extensions 'core.test', 'core.test'",
+ "Failed to instantiate extension 'core/test', received undeclared inputs 'undeclared1' from extension 'core/test' and 'undeclared2' from extensions 'core/test', 'core/test'",
);
});
@@ -477,24 +507,27 @@ describe('createAppNodeInstance', () => {
],
]),
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- singleton: createExtensionInput(
- {
- test: testDataRef,
- },
- { singleton: true },
- ),
- },
- output: {},
- factory: () => ({}),
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ singleton: createExtensionInput(
+ {
+ test: testDataRef,
+ },
+ { singleton: true },
+ ),
+ },
+ output: {},
+ factory: () => ({}),
+ }),
+ ),
),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', expected exactly one 'singleton' input but received multiple: 'core.test', 'core.test'",
+ "Failed to instantiate extension 'core/test', expected exactly one 'singleton' input but received multiple: 'core/test', 'core/test'",
);
});
@@ -511,24 +544,27 @@ describe('createAppNodeInstance', () => {
],
]),
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- singleton: createExtensionInput(
- {
- test: testDataRef,
- },
- { singleton: true, optional: true },
- ),
- },
- output: {},
- factory: () => ({}),
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ singleton: createExtensionInput(
+ {
+ test: testDataRef,
+ },
+ { singleton: true, optional: true },
+ ),
+ },
+ output: {},
+ factory: () => ({}),
+ }),
+ ),
),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', expected at most one 'singleton' input but received multiple: 'core.test', 'core.test'",
+ "Failed to instantiate extension 'core/test', expected at most one 'singleton' input but received multiple: 'core/test', 'core/test'",
);
});
@@ -539,24 +575,27 @@ describe('createAppNodeInstance', () => {
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
]),
node: makeNode(
- createExtension({
- id: 'core.test',
- attachTo: { id: 'ignored', input: 'ignored' },
- inputs: {
- singleton: createExtensionInput(
- {
- other: otherDataRef,
- },
- { singleton: true },
- ),
- },
- output: {},
- factory: () => ({}),
- }),
+ resolveExtensionDefinition(
+ createExtension({
+ namespace: 'core',
+ name: 'test',
+ attachTo: { id: 'ignored', input: 'ignored' },
+ inputs: {
+ singleton: createExtensionInput(
+ {
+ other: otherDataRef,
+ },
+ { singleton: true },
+ ),
+ },
+ output: {},
+ factory: () => ({}),
+ }),
+ ),
),
}),
).toThrow(
- "Failed to instantiate extension 'core.test', input 'singleton' did not receive required extension data 'other' from extension 'core.test'",
+ "Failed to instantiate extension 'core/test', input 'singleton' did not receive required extension data 'other' from extension 'core/test'",
);
});
});
diff --git a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.test.ts b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.test.ts
index 91f6992f29..504d5a6197 100644
--- a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.test.ts
+++ b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.test.ts
@@ -25,18 +25,18 @@ describe('readAppExtensionsConfig', () => {
it('should disable extension with shorthand notation', () => {
expect(
readAppExtensionsConfig(
- new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }),
+ new ConfigReader({ app: { extensions: [{ 'core/router': false }] } }),
),
).toEqual([
{
- id: 'core.router',
+ id: 'core/router',
disabled: true,
},
]);
expect(
readAppExtensionsConfig(
new ConfigReader({
- app: { extensions: [{ 'core.router': { disabled: true } }] },
+ app: { extensions: [{ 'core/router': { disabled: true } }] },
}),
),
).toEqual([
@@ -44,7 +44,7 @@ describe('readAppExtensionsConfig', () => {
at: undefined,
config: undefined,
disabled: true,
- id: 'core.router',
+ id: 'core/router',
},
]);
});
@@ -52,33 +52,33 @@ describe('readAppExtensionsConfig', () => {
it('should enable extension with shorthand notation', () => {
expect(
readAppExtensionsConfig(
- new ConfigReader({ app: { extensions: ['core.router'] } }),
+ new ConfigReader({ app: { extensions: ['core/router'] } }),
),
).toEqual([
{
- id: 'core.router',
+ id: 'core/router',
disabled: false,
},
]);
expect(
readAppExtensionsConfig(
- new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }),
+ new ConfigReader({ app: { extensions: [{ 'core/router': true }] } }),
),
).toEqual([
{
- id: 'core.router',
+ id: 'core/router',
disabled: false,
},
]);
expect(
readAppExtensionsConfig(
new ConfigReader({
- app: { extensions: [{ 'core.router': { disabled: false } }] },
+ app: { extensions: [{ 'core/router': { disabled: false } }] },
}),
),
).toEqual([
{
- id: 'core.router',
+ id: 'core/router',
disabled: false,
},
]);
@@ -89,12 +89,12 @@ describe('readAppExtensionsConfig', () => {
readAppExtensionsConfig(
new ConfigReader({
app: {
- extensions: [{ 'core.router': 'some-string' }],
+ extensions: [{ 'core/router': 'some-string' }],
},
}),
),
).toThrow(
- 'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object',
+ 'Invalid extension configuration at app.extensions[0][core/router], value must be a boolean or object',
);
});
@@ -105,7 +105,7 @@ describe('readAppExtensionsConfig', () => {
app: {
extensions: [
{
- 'core.router/routes': {
+ '': {
extension: 'example-package#MyPage',
config: { foo: 'bar' },
},
@@ -115,7 +115,7 @@ describe('readAppExtensionsConfig', () => {
}),
),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
+ `"Invalid extension configuration at app.extensions[0], extension ID must not be empty or contain whitespace"`,
);
});
});
@@ -156,8 +156,8 @@ describe('expandShorthandExtensionParameters', () => {
});
it('supports string key', () => {
- expect(run('core.router')).toEqual({
- id: 'core.router',
+ expect(run('core/router')).toEqual({
+ id: 'core/router',
disabled: false,
});
expect(() => run('')).toThrowErrorMatchingInlineSnapshot(
@@ -166,103 +166,100 @@ describe('expandShorthandExtensionParameters', () => {
expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`,
);
- expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
- );
});
it('supports null value', () => {
// this is the result of typing:
- // - core.router:
+ // - core/router:
// The missing value is interpreted as null by the yaml parser so we deal with that
- expect(run({ 'core.router': null })).toEqual({
- id: 'core.router',
+ expect(run({ 'core/router': null })).toEqual({
+ id: 'core/router',
disabled: false,
});
});
it('supports boolean value', () => {
- expect(run({ 'core.router': true })).toEqual({
- id: 'core.router',
+ expect(run({ 'core/router': true })).toEqual({
+ id: 'core/router',
disabled: false,
});
- expect(run({ 'core.router': false })).toEqual({
- id: 'core.router',
+ expect(run({ 'core/router': false })).toEqual({
+ id: 'core/router',
disabled: true,
});
});
it('should not support string values', () => {
expect(() =>
- run({ 'core.router': 'example-package#MyRouter' }),
+ run({ 'core/router': 'example-package#MyRouter' }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`,
+ `"Invalid extension configuration at app.extensions[1][core/router], value must be a boolean or object"`,
);
});
it('supports object id only in the key', () => {
expect(() =>
- run({ 'core.router': { id: 'some.id' } }),
+ run({ 'core/router': { id: 'some.id' } }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
+ `"Invalid extension configuration at app.extensions[1][core/router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
it('supports object attachTo', () => {
expect(
run({
- 'core.router': { attachTo: { id: 'other.root', input: 'inputs' } },
+ 'core/router': { attachTo: { id: 'other.root', input: 'inputs' } },
}),
).toEqual({
- id: 'core.router',
+ id: 'core/router',
attachTo: { id: 'other.root', input: 'inputs' },
});
expect(() =>
run({
- 'core.router': {
+ 'core/router': {
id: 'other-id',
},
}),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
+ `"Invalid extension configuration at app.extensions[1][core/router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
it('supports object disabled', () => {
- expect(run({ 'core.router': { disabled: true } })).toEqual({
- id: 'core.router',
+ expect(run({ 'core/router': { disabled: true } })).toEqual({
+ id: 'core/router',
disabled: true,
});
- expect(run({ 'core.router': { disabled: false } })).toEqual({
- id: 'core.router',
+ expect(run({ 'core/router': { disabled: false } })).toEqual({
+ id: 'core/router',
disabled: false,
});
expect(() =>
- run({ 'core.router': { disabled: 0 } }),
+ run({ 'core/router': { disabled: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`,
+ `"Invalid extension configuration at app.extensions[1][core/router].disabled, must be a boolean"`,
);
});
it('supports object config', () => {
expect(
- run({ 'core.router': { config: { disableRedirects: true } } }),
+ run({ 'core/router': { config: { disableRedirects: true } } }),
).toEqual({
- id: 'core.router',
+ id: 'core/router',
config: { disableRedirects: true },
});
expect(() =>
- run({ 'core.router': { config: 0 } }),
+ run({ 'core/router': { config: 0 } }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`,
+ `"Invalid extension configuration at app.extensions[1][core/router].config, must be an object"`,
);
});
it('rejects unknown object keys', () => {
expect(() =>
- run({ 'core.router': { foo: { settings: true } } }),
+ run({ 'core/router': { foo: { settings: true } } }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
+ `"Invalid extension configuration at app.extensions[1][core/router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
);
});
});
diff --git a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts
index 2b5b51ce92..cca7064add 100644
--- a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts
+++ b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts
@@ -70,15 +70,6 @@ export function expandShorthandExtensionParameters(
errorMsg('extension ID must not be empty or contain whitespace'),
);
}
-
- if (id.includes('/')) {
- let message = `extension ID must not contain slashes; got '${id}'`;
- const good = id.split('/')[0];
- if (good) {
- message += `, did you mean '${good}'?`;
- }
- throw new Error(errorMsg(message));
- }
}
// Example YAML:
diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts
index 39eceaaf28..2e9f73d87b 100644
--- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts
+++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts
@@ -18,6 +18,7 @@ import {
createExtensionOverrides,
createPlugin,
Extension,
+ ExtensionDefinition,
} from '@backstage/frontend-plugin-api';
import { resolveAppNodeSpecs } from './resolveAppNodeSpecs';
@@ -27,12 +28,26 @@ function makeExt(
attachId: string = 'root',
) {
return {
+ $$type: '@backstage/Extension',
id,
attachTo: { id: attachId, input: 'default' },
disabled: status === 'disabled',
} as Extension;
}
+function makeExtDef(
+ name: string,
+ status: 'disabled' | 'enabled' = 'enabled',
+ attachId: string = 'root',
+) {
+ return {
+ $$type: '@backstage/ExtensionDefinition',
+ name,
+ attachTo: { id: attachId, input: 'default' },
+ disabled: status === 'disabled',
+ } as ExtensionDefinition;
+}
+
describe('resolveAppNodeSpecs', () => {
it('should not filter out disabled extension instances', () => {
const a = makeExt('a', 'disabled');
@@ -78,9 +93,8 @@ describe('resolveAppNodeSpecs', () => {
});
it('should override attachment points', () => {
- const a = makeExt('a');
const b = makeExt('b');
- const pluginA = createPlugin({ id: 'test', extensions: [a] });
+ const pluginA = createPlugin({ id: 'test', extensions: [makeExtDef('a')] });
expect(
resolveAppNodeSpecs({
features: [pluginA],
@@ -94,8 +108,8 @@ describe('resolveAppNodeSpecs', () => {
}),
).toEqual([
{
- id: 'a',
- extension: a,
+ id: 'test/a',
+ extension: makeExt('test/a'),
attachTo: { id: 'root', input: 'default' },
source: pluginA,
disabled: false,
@@ -110,31 +124,34 @@ describe('resolveAppNodeSpecs', () => {
});
it('should fully override configuration and duplicate', () => {
- const a = makeExt('a');
- const b = makeExt('b');
- const plugin = createPlugin({ id: 'test', extensions: [a, b] });
+ const a = makeExt('test/a');
+ const b = makeExt('test/b');
+ const plugin = createPlugin({
+ id: 'test',
+ extensions: [makeExtDef('a'), makeExtDef('b')],
+ });
expect(
resolveAppNodeSpecs({
features: [plugin],
builtinExtensions: [],
parameters: [
{
- id: 'a',
+ id: 'test/a',
config: { foo: { bar: 1 } },
},
{
- id: 'b',
+ id: 'test/b',
config: { foo: { bar: 2 } },
},
{
- id: 'b',
+ id: 'test/b',
config: { foo: { qux: 3 } },
},
],
}),
).toEqual([
{
- id: 'a',
+ id: 'test/a',
extension: a,
attachTo: { id: 'root', input: 'default' },
source: plugin,
@@ -142,7 +159,7 @@ describe('resolveAppNodeSpecs', () => {
disabled: false,
},
{
- id: 'b',
+ id: 'test/b',
extension: b,
attachTo: { id: 'root', input: 'default' },
source: plugin,
@@ -187,11 +204,12 @@ describe('resolveAppNodeSpecs', () => {
});
it('should apply extension overrides', () => {
- const a = makeExt('a');
- const b = makeExt('b');
- const plugin = createPlugin({ id: 'test', extensions: [a, b] });
- const aOverride = makeExt('a', 'enabled', 'other');
- const bOverride = makeExt('b', 'disabled', 'other');
+ const plugin = createPlugin({
+ id: 'test',
+ extensions: [makeExtDef('a'), makeExtDef('b')],
+ });
+ const aOverride = makeExt('test/a', 'enabled', 'other');
+ const bOverride = makeExt('test/b', 'disabled', 'other');
const cOverride = makeExt('c');
expect(
@@ -199,7 +217,11 @@ describe('resolveAppNodeSpecs', () => {
features: [
plugin,
createExtensionOverrides({
- extensions: [aOverride, bOverride, cOverride],
+ extensions: [
+ makeExtDef('test/a', 'enabled', 'other'),
+ makeExtDef('test/b', 'disabled', 'other'),
+ makeExtDef('c'),
+ ],
}),
],
builtinExtensions: [],
@@ -207,14 +229,14 @@ describe('resolveAppNodeSpecs', () => {
}),
).toEqual([
{
- id: 'a',
+ id: 'test/a',
extension: aOverride,
attachTo: { id: 'other', input: 'default' },
source: plugin,
disabled: false,
},
{
- id: 'b',
+ id: 'test/b',
extension: bOverride,
attachTo: { id: 'other', input: 'default' },
source: plugin,
@@ -231,39 +253,50 @@ describe('resolveAppNodeSpecs', () => {
});
it('should use order from configuration when rather than overrides', () => {
- const a = makeExt('a', 'disabled');
- const b = makeExt('b', 'disabled');
- const c = makeExt('c', 'disabled');
- const aOverride = makeExt('c', 'disabled');
- const bOverride = makeExt('b', 'disabled');
- const cOverride = makeExt('a', 'disabled');
-
const result = resolveAppNodeSpecs({
features: [
- createPlugin({ id: 'test', extensions: [a, b, c] }),
+ createPlugin({
+ id: 'test',
+ extensions: [
+ makeExtDef('a', 'disabled'),
+ makeExtDef('b', 'disabled'),
+ makeExtDef('c', 'disabled'),
+ ],
+ }),
createExtensionOverrides({
- extensions: [cOverride, bOverride, aOverride],
+ extensions: [
+ makeExtDef('test/c', 'disabled'),
+ makeExtDef('test/b', 'disabled'),
+ makeExtDef('test/a', 'disabled'),
+ ],
}),
],
builtinExtensions: [],
- parameters: ['b', 'c', 'a'].map(id => ({ id, disabled: false })),
+ parameters: ['test/b', 'test/c', 'test/a'].map(id => ({
+ id,
+ disabled: false,
+ })),
});
- expect(result.map(r => r.extension.id)).toEqual(['b', 'c', 'a']);
+ expect(result.map(r => r.extension.id)).toEqual([
+ 'test/b',
+ 'test/c',
+ 'test/a',
+ ]);
});
it('throws an error when a forbidden extension is overridden by a plugin', () => {
expect(() =>
resolveAppNodeSpecs({
features: [
- createPlugin({ id: 'test', extensions: [makeExt('forbidden')] }),
+ createPlugin({ id: 'test', extensions: [makeExtDef('forbidden')] }),
],
builtinExtensions: [],
parameters: [],
- forbidden: new Set(['forbidden']),
+ forbidden: new Set(['test/forbidden']),
}),
).toThrow(
- "It is forbidden to override the following extension(s): 'forbidden', which is done by the following plugin(s): 'test'",
+ "It is forbidden to override the following extension(s): 'test/forbidden', which is done by the following plugin(s): 'test'",
);
});
@@ -271,7 +304,7 @@ describe('resolveAppNodeSpecs', () => {
expect(() =>
resolveAppNodeSpecs({
features: [
- createExtensionOverrides({ extensions: [makeExt('forbidden')] }),
+ createExtensionOverrides({ extensions: [makeExtDef('forbidden')] }),
],
builtinExtensions: [],
parameters: [],
diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts
index d84064b26a..4fca671554 100644
--- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts
+++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts
@@ -16,15 +16,17 @@
import { createExtension } from '@backstage/frontend-plugin-api';
import { resolveAppTree } from './resolveAppTree';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
-const extBaseConfig = {
- id: 'test',
- attachTo: { id: 'nonexistent', input: 'nonexistent' },
- output: {},
- factory: () => ({}),
-};
-
-const extension = createExtension(extBaseConfig);
+const extension = resolveExtensionDefinition(
+ createExtension({
+ name: 'test',
+ attachTo: { id: 'nonexistent', input: 'nonexistent' },
+ output: {},
+ factory: () => ({}),
+ }),
+);
const baseSpec = {
extension,
diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx
index 232de74de4..2713a9743e 100644
--- a/packages/frontend-app-api/src/wiring/createApp.test.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx
@@ -36,7 +36,10 @@ describe('createApp', () => {
configLoader: async () =>
new MockConfigApi({
app: {
- extensions: [{ 'themes.light': false }, { 'themes.dark': false }],
+ extensions: [
+ { 'theme:app/light': false },
+ { 'theme:app/dark': false },
+ ],
},
}),
features: [
@@ -68,7 +71,6 @@ describe('createApp', () => {
id: duplicatedFeatureId,
extensions: [
createPageExtension({
- id: 'test.page.first',
defaultPath: '/',
loader: async () => First Page
,
}),
@@ -78,7 +80,6 @@ describe('createApp', () => {
id: duplicatedFeatureId,
extensions: [
createPageExtension({
- id: 'test.page.last',
defaultPath: '/',
loader: async () => Last Page
,
}),
@@ -106,7 +107,7 @@ describe('createApp', () => {
featureFlags: [{ name: 'test-1' }],
extensions: [
createExtension({
- id: 'test.page.first',
+ name: 'first',
attachTo: { id: 'core', input: 'root' },
output: { element: coreExtensionData.reactElement },
factory() {
@@ -131,7 +132,8 @@ describe('createApp', () => {
featureFlags: [{ name: 'test-2' }],
extensions: [
createExtension({
- id: 'core.router',
+ namespace: 'core',
+ name: 'router',
attachTo: { id: 'core', input: 'root' },
disabled: true,
output: {},
@@ -159,7 +161,6 @@ describe('createApp', () => {
id: 'my-plugin',
extensions: [
createPageExtension({
- id: 'plugin.my-plugin.page',
defaultPath: '/',
loader: async () => {
const Component = () => {
@@ -182,32 +183,32 @@ describe('createApp', () => {
expect(String(tree.root)).toMatchInlineSnapshot(`
"
root [
-
+
children [
-
+
content [
-
+
routes [
-
+
]
-
+
]
nav [
-
+
]
-
+
]
-
+
]
components [
-
-
-
-
+
+
+
+
]
themes [
-
-
+
+
]
"
`);
diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx
index 4e463fb526..f58dfb3159 100644
--- a/packages/frontend-app-api/src/wiring/createApp.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.tsx
@@ -73,6 +73,8 @@ import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementati
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import {
apis as defaultApis,
components as defaultComponents,
@@ -119,7 +121,7 @@ export const builtinExtensions = [
DefaultNotFoundErrorPageComponent,
LightTheme,
DarkTheme,
-];
+].map(def => resolveExtensionDefinition(def));
/** @public */
export interface ExtensionTreeNode {
@@ -174,7 +176,7 @@ export function createExtensionTree(options: {
);
},
getRootRoutes(): JSX.Element[] {
- return this.getExtensionAttachments('core.routes', 'routes').map(node => {
+ return this.getExtensionAttachments('core/routes', 'routes').map(node => {
const path = node.getData(coreExtensionData.routePath);
const element = node.getData(coreExtensionData.reactElement);
const routeRef = node.getData(coreExtensionData.routeRef);
@@ -201,7 +203,7 @@ export function createExtensionTree(options: {
);
};
- return this.getExtensionAttachments('core.nav', 'items')
+ return this.getExtensionAttachments('core/nav', 'items')
.map((node, index) => {
const target = node.getData(coreExtensionData.navTarget);
if (!target) {
diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md
index e1ed76d82c..c491579eaf 100644
--- a/packages/frontend-plugin-api/api-report.md
+++ b/packages/frontend-plugin-api/api-report.md
@@ -392,7 +392,7 @@ export function createApiExtension<
configSchema?: PortableSchema;
inputs?: TInputs;
},
-): Extension;
+): ExtensionDefinition;
export { createApiFactory };
@@ -405,6 +405,7 @@ export function createComponentExtension<
TInputs extends AnyExtensionInputMap,
>(options: {
ref: TRef;
+ name?: string;
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema;
@@ -421,7 +422,7 @@ export function createComponentExtension<
inputs: Expand>;
}) => TRef['T'];
};
-}): Extension;
+}): ExtensionDefinition;
// @public (undocumented)
export function createExtension<
@@ -430,7 +431,7 @@ export function createExtension<
TConfig = never,
>(
options: CreateExtensionOptions,
-): Extension;
+): ExtensionDefinition;
// @public (undocumented)
export function createExtensionDataRef(
@@ -477,10 +478,14 @@ export interface CreateExtensionOptions<
inputs: Expand>;
}): Expand>;
// (undocumented)
- id: string;
- // (undocumented)
inputs?: TInputs;
// (undocumented)
+ kind?: string;
+ // (undocumented)
+ name?: string;
+ // (undocumented)
+ namespace?: string;
+ // (undocumented)
output: TOutput;
}
@@ -516,11 +521,12 @@ export function createExternalRouteRef<
// @public
export function createNavItemExtension(options: {
- id: string;
+ namespace?: string;
+ name?: string;
routeRef: RouteRef;
title: string;
icon: IconComponent_2;
-}): Extension<{
+}): ExtensionDefinition<{
title: string;
}>;
@@ -539,7 +545,8 @@ export function createPageExtension<
configSchema: PortableSchema;
}
) & {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: {
id: string;
input: string;
@@ -552,7 +559,7 @@ export function createPageExtension<
inputs: Expand>;
}) => Promise;
},
-): Extension;
+): ExtensionDefinition;
// @public (undocumented)
export function createPlugin<
@@ -592,7 +599,8 @@ export function createSignInPageExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: {
id: string;
input: string;
@@ -604,7 +612,7 @@ export function createSignInPageExtension<
config: TConfig;
inputs: Expand>;
}) => Promise>;
-}): Extension;
+}): ExtensionDefinition;
// @public
export function createSubRouteRef<
@@ -616,7 +624,9 @@ export function createSubRouteRef<
}): MakeSubRouteRef, ParentParams>;
// @public (undocumented)
-export function createThemeExtension(theme: AppTheme): Extension;
+export function createThemeExtension(
+ theme: AppTheme,
+): ExtensionDefinition;
export { DiscoveryApi };
@@ -703,6 +713,40 @@ export type ExtensionDataValues = {
: never]?: TExtensionData[DataName]['T'];
};
+// @public (undocumented)
+export interface ExtensionDefinition {
+ // (undocumented)
+ $$type: '@backstage/ExtensionDefinition';
+ // (undocumented)
+ attachTo: {
+ id: string;
+ input: string;
+ };
+ // (undocumented)
+ configSchema?: PortableSchema;
+ // (undocumented)
+ disabled: boolean;
+ // (undocumented)
+ factory(options: {
+ node: AppNode;
+ config: TConfig;
+ inputs: Record<
+ string,
+ undefined | Record | Array>
+ >;
+ }): ExtensionDataValues;
+ // (undocumented)
+ inputs: AnyExtensionInputMap;
+ // (undocumented)
+ kind?: string;
+ // (undocumented)
+ name?: string;
+ // (undocumented)
+ namespace?: string;
+ // (undocumented)
+ output: AnyExtensionDataMap;
+}
+
// @public (undocumented)
export interface ExtensionInput<
TExtensionData extends AnyExtensionDataMap,
@@ -743,7 +787,7 @@ export interface ExtensionOverrides {
// @public (undocumented)
export interface ExtensionOverridesOptions {
// (undocumented)
- extensions: Extension[];
+ extensions: ExtensionDefinition[];
// (undocumented)
featureFlags?: FeatureFlagConfig[];
}
@@ -841,7 +885,7 @@ export interface PluginOptions<
ExternalRoutes extends AnyExternalRoutes,
> {
// (undocumented)
- extensions?: Extension[];
+ extensions?: ExtensionDefinition[];
// (undocumented)
externalRoutes?: ExternalRoutes;
// (undocumented)
diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx
index 49a9f95ed2..669335d577 100644
--- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx
+++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx
@@ -24,11 +24,10 @@ import { createRouteRef } from '../routing';
import { createExtensionTester } from '@backstage/frontend-test-utils';
const wrapInBoundaryExtension = (element: JSX.Element) => {
- const id = 'plugin.extension';
const routeRef = createRouteRef();
return createExtension({
- id,
- attachTo: { id: 'core.routes', input: 'routes' },
+ name: 'test',
+ attachTo: { id: 'core/routes', input: 'routes' },
output: {
element: coreExtensionData.reactElement,
path: coreExtensionData.routePath,
@@ -84,15 +83,18 @@ describe('ExtensionBoundary', () => {
),
).render();
- await waitFor(() =>
- expect(analyticsApiMock.getEvents()[0]).toMatchObject({
+ await waitFor(() => {
+ const event = analyticsApiMock
+ .getEvents()
+ .find(e => e.subject === subject);
+
+ expect(event).toMatchObject({
action,
subject,
context: {
- extension: 'plugin.extension',
- routeRef: 'unknown',
+ extensionId: 'test',
},
- }),
- );
+ });
+ });
});
});
diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx
index c9fa75c7fa..623c875837 100644
--- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx
+++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx
@@ -59,7 +59,7 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) {
// Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight
const attributes = {
- extension: node.spec.id,
+ extensionId: node.spec.id,
pluginId: node.spec.source?.id,
};
diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
index e90c0b6b85..2585b0d694 100644
--- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
+++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts
@@ -26,13 +26,14 @@ describe('createApiExtension', () => {
factory: () => ({ foo: 'bar' }),
});
- const extension = createApiExtension({
- factory,
- });
-
- expect(extension).toEqual({
- $$type: '@backstage/Extension',
- id: 'apis.test',
+ expect(
+ createApiExtension({
+ factory,
+ }),
+ ).toEqual({
+ $$type: '@backstage/ExtensionDefinition',
+ kind: 'api',
+ namespace: 'test',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
configSchema: undefined,
@@ -65,8 +66,9 @@ describe('createApiExtension', () => {
});
// boo
expect(extension).toEqual({
- $$type: '@backstage/Extension',
- id: 'apis.test',
+ $$type: '@backstage/ExtensionDefinition',
+ kind: 'api',
+ namespace: 'test',
attachTo: { id: 'core', input: 'apis' },
disabled: false,
configSchema: undefined,
diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
index 7b6dded022..0f4f721530 100644
--- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
+++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts
@@ -51,7 +51,10 @@ export function createApiExtension<
'api' in options ? options.api : (factory as { api: AnyApiRef }).api;
return createExtension({
- id: `apis.${apiRef.id}`,
+ kind: 'api',
+ // Since ApiRef IDs use a global namespace we use the namespace here in order to override
+ // potential plugin IDs and always end up with the format `api:`
+ namespace: apiRef.id,
attachTo: { id: 'core', input: 'apis' },
inputs: extensionInputs,
configSchema,
diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx
index a20faa0b3a..c9780574b6 100644
--- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx
@@ -32,6 +32,7 @@ export function createComponentExtension<
TInputs extends AnyExtensionInputMap,
>(options: {
ref: TRef;
+ name?: string;
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema;
@@ -49,9 +50,10 @@ export function createComponentExtension<
}) => TRef['T'];
};
}) {
- const id = options.ref.id;
return createExtension({
- id,
+ kind: 'component',
+ namespace: options.ref.id,
+ name: options.name,
attachTo: { id: 'core', input: 'components' },
inputs: options.inputs,
disabled: options.disabled,
diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx
index 84fbe5ffee..207778fbd7 100644
--- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx
@@ -24,15 +24,18 @@ import { RouteRef } from '../routing';
* @public
*/
export function createNavItemExtension(options: {
- id: string;
+ namespace?: string;
+ name?: string;
routeRef: RouteRef;
title: string;
icon: IconComponent;
}) {
- const { id, routeRef, title, icon } = options;
+ const { routeRef, title, icon, namespace, name } = options;
return createExtension({
- id,
- attachTo: { id: 'core.nav', input: 'items' },
+ namespace,
+ name,
+ kind: 'nav-item',
+ attachTo: { id: 'core/nav', input: 'items' },
configSchema: createSchemaFromZod(z =>
z.object({
title: z.string().default(title),
diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx
index 93efadb0e1..ca95fad7bc 100644
--- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx
@@ -25,14 +25,15 @@ describe('createNavLogoExtension', () => {
it('creates the extension properly', () => {
expect(
createNavLogoExtension({
- id: 'test',
+ name: 'test',
logoFull: Logo Full
,
logoIcon: Logo Icon
,
}),
).toEqual({
- $$type: '@backstage/Extension',
- id: 'test',
- attachTo: { id: 'core.nav', input: 'logos' },
+ $$type: '@backstage/ExtensionDefinition',
+ kind: 'nav-logo',
+ name: 'test',
+ attachTo: { id: 'core/nav', input: 'logos' },
disabled: false,
inputs: {},
output: {
diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx
index 5cfff81a9d..878764bad7 100644
--- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx
@@ -21,14 +21,17 @@ import { coreExtensionData, createExtension } from '../wiring';
* @public
*/
export function createNavLogoExtension(options: {
- id: string;
+ name?: string;
+ namespace?: string;
logoIcon: JSX.Element;
logoFull: JSX.Element;
}) {
- const { id, logoIcon, logoFull } = options;
+ const { logoIcon, logoFull } = options;
return createExtension({
- id,
- attachTo: { id: 'core.nav', input: 'logos' },
+ kind: 'nav-logo',
+ name: options?.name,
+ namespace: options?.namespace,
+ attachTo: { id: 'core/nav', input: 'logos' },
output: {
logos: coreExtensionData.logoElements,
},
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
index f75821944a..525f10ee61 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
@@ -36,14 +36,15 @@ describe('createPageExtension', () => {
expect(
createPageExtension({
- id: 'test',
+ name: 'test',
configSchema,
loader: async () => ,
}),
).toEqual({
- $$type: '@backstage/Extension',
- id: 'test',
- attachTo: { id: 'core.routes', input: 'routes' },
+ $$type: '@backstage/ExtensionDefinition',
+ name: 'test',
+ kind: 'page',
+ attachTo: { id: 'core/routes', input: 'routes' },
configSchema: expect.anything(),
disabled: false,
inputs: {},
@@ -57,7 +58,7 @@ describe('createPageExtension', () => {
expect(
createPageExtension({
- id: 'test',
+ name: 'test',
attachTo: { id: 'other', input: 'place' },
disabled: true,
configSchema,
@@ -69,8 +70,9 @@ describe('createPageExtension', () => {
loader: async () => ,
}),
).toEqual({
- $$type: '@backstage/Extension',
- id: 'test',
+ $$type: '@backstage/ExtensionDefinition',
+ name: 'test',
+ kind: 'page',
attachTo: { id: 'other', input: 'place' },
configSchema: expect.anything(),
disabled: true,
@@ -89,14 +91,15 @@ describe('createPageExtension', () => {
expect(
createPageExtension({
- id: 'test',
+ name: 'test',
defaultPath: '/here',
loader: async () => ,
}),
).toEqual({
- $$type: '@backstage/Extension',
- id: 'test',
- attachTo: { id: 'core.routes', input: 'routes' },
+ $$type: '@backstage/ExtensionDefinition',
+ name: 'test',
+ kind: 'page',
+ attachTo: { id: 'core/routes', input: 'routes' },
configSchema: expect.anything(),
disabled: false,
inputs: {},
@@ -118,7 +121,6 @@ describe('createPageExtension', () => {
createExtensionTester(
createPageExtension({
- id: 'plugin.page',
defaultPath: '/',
loader: async () => Component
,
}),
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
index 99733ba367..6796fe884d 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
@@ -20,12 +20,12 @@ import { createSchemaFromZod, PortableSchema } from '../schema';
import {
coreExtensionData,
createExtension,
- Extension,
ExtensionInputValues,
AnyExtensionInputMap,
} from '../wiring';
import { RouteRef } from '../routing';
import { Expand } from '../types';
+import { ExtensionDefinition } from '../wiring/createExtension';
/**
* Helper for creating extensions for a routable React page component.
@@ -44,7 +44,8 @@ export function createPageExtension<
configSchema: PortableSchema;
}
) & {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: { id: string; input: string };
disabled?: boolean;
inputs?: TInputs;
@@ -54,9 +55,7 @@ export function createPageExtension<
inputs: Expand>;
}) => Promise;
},
-): Extension {
- const { id } = options;
-
+): ExtensionDefinition {
const configSchema =
'configSchema' in options
? options.configSchema
@@ -65,8 +64,10 @@ export function createPageExtension<
) as PortableSchema);
return createExtension({
- id,
- attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' },
+ kind: 'page',
+ namespace: options.namespace,
+ name: options.name,
+ attachTo: options.attachTo ?? { id: 'core/routes', input: 'routes' },
configSchema,
inputs: options.inputs,
disabled: options.disabled,
diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx
index 964941e213..d8835ae788 100644
--- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.test.tsx
@@ -23,13 +23,13 @@ import { coreExtensionData, createExtension } from '../wiring';
describe('createSignInPageExtension', () => {
it('renders a sign-in page', async () => {
const SignInPage = createSignInPageExtension({
- id: 'test',
+ name: 'test',
loader: async () => () => ,
});
createExtensionTester(
createExtension({
- id: 'dummy',
+ name: 'dummy',
attachTo: { id: 'ignored', input: 'ignored' },
output: {
element: coreExtensionData.reactElement,
diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx
index f869ed8f13..8709403407 100644
--- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx
@@ -19,10 +19,10 @@ import { ExtensionBoundary } from '../components';
import { PortableSchema } from '../schema';
import {
createExtension,
- Extension,
ExtensionInputValues,
AnyExtensionInputMap,
createExtensionDataRef,
+ ExtensionDefinition,
} from '../wiring';
import { Expand } from '../types';
import { SignInPageProps } from '@backstage/core-plugin-api';
@@ -39,7 +39,8 @@ export function createSignInPageExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: { id: string; input: string };
configSchema?: PortableSchema;
disabled?: boolean;
@@ -48,12 +49,12 @@ export function createSignInPageExtension<
config: TConfig;
inputs: Expand>;
}) => Promise>;
-}): Extension {
- const { id } = options;
-
+}): ExtensionDefinition {
return createExtension({
- id,
- attachTo: options.attachTo ?? { id: 'core.router', input: 'signInPage' },
+ kind: 'sign-in-page',
+ namespace: options?.namespace,
+ name: options?.name,
+ attachTo: options.attachTo ?? { id: 'core/router', input: 'signInPage' },
configSchema: options.configSchema,
inputs: options.inputs,
disabled: options.disabled,
diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts
index d41121d51c..5554a08874 100644
--- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts
+++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts
@@ -20,7 +20,9 @@ import { AppTheme } from '@backstage/core-plugin-api';
/** @public */
export function createThemeExtension(theme: AppTheme) {
return createExtension({
- id: `themes.${theme.id}`,
+ kind: 'theme',
+ namespace: 'app',
+ name: theme.id,
attachTo: { id: 'core', input: 'themes' },
output: {
theme: coreExtensionData.theme,
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts
index 071272d58f..07d52f5bb4 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts
@@ -25,7 +25,7 @@ function unused(..._any: any[]) {}
describe('createExtension', () => {
it('should create an extension with a simple output', () => {
const baseConfig = {
- id: 'test',
+ namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: {
foo: stringData,
@@ -39,7 +39,7 @@ describe('createExtension', () => {
};
},
});
- expect(extension.id).toBe('test');
+ expect(extension.namespace).toBe('test');
// When declared as an error function without a block the TypeScript errors
// are a more specific and will point at the property that is problematic.
@@ -163,7 +163,7 @@ describe('createExtension', () => {
it('should create an extension with a some optional output', () => {
const baseConfig = {
- id: 'test',
+ namespace: 'test',
attachTo: { id: 'root', input: 'default' },
output: {
foo: stringData,
@@ -176,7 +176,7 @@ describe('createExtension', () => {
foo: 'bar',
}),
});
- expect(extension.id).toBe('test');
+ expect(extension.namespace).toBe('test');
createExtension({
...baseConfig,
@@ -233,7 +233,7 @@ describe('createExtension', () => {
it('should create an extension with input', () => {
const extension = createExtension({
- id: 'test',
+ namespace: 'test',
attachTo: { id: 'root', input: 'default' },
inputs: {
mixed: createExtensionInput({
@@ -286,6 +286,6 @@ describe('createExtension', () => {
};
},
});
- expect(extension.id).toBe('test');
+ expect(extension.namespace).toBe('test');
});
});
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts
index 9443caeb66..e4c79a925b 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtension.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts
@@ -73,7 +73,9 @@ export interface CreateExtensionOptions<
TInputs extends AnyExtensionInputMap,
TConfig,
> {
- id: string;
+ kind?: string;
+ namespace?: string;
+ name?: string;
attachTo: { id: string; input: string };
disabled?: boolean;
inputs?: TInputs;
@@ -86,6 +88,27 @@ export interface CreateExtensionOptions<
}): Expand>;
}
+/** @public */
+export interface ExtensionDefinition {
+ $$type: '@backstage/ExtensionDefinition';
+ kind?: string;
+ namespace?: string;
+ name?: string;
+ attachTo: { id: string; input: string };
+ disabled: boolean;
+ inputs: AnyExtensionInputMap;
+ output: AnyExtensionDataMap;
+ configSchema?: PortableSchema;
+ factory(options: {
+ node: AppNode;
+ config: TConfig;
+ inputs: Record<
+ string,
+ undefined | Record | Array>
+ >;
+ }): ExtensionDataValues;
+}
+
/** @public */
export interface Extension {
$$type: '@backstage/Extension';
@@ -112,12 +135,17 @@ export function createExtension<
TConfig = never,
>(
options: CreateExtensionOptions,
-): Extension {
+): ExtensionDefinition {
return {
- ...options,
+ $$type: '@backstage/ExtensionDefinition',
+ kind: options.kind,
+ namespace: options.namespace,
+ name: options.name,
+ attachTo: options.attachTo,
disabled: options.disabled ?? false,
- $$type: '@backstage/Extension',
inputs: options.inputs ?? {},
+ output: options.output,
+ configSchema: options.configSchema,
factory({ inputs, ...rest }) {
// TODO: Simplify this, but TS wouldn't infer the input type for some reason
return options.factory({
diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts
index 90f878ceaa..772a214490 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts
@@ -37,7 +37,21 @@ describe('createExtensionOverrides', () => {
createExtensionOverrides({
extensions: [
createExtension({
- id: 'a',
+ name: 'a',
+ attachTo: { id: 'core', input: 'apis' },
+ output: {},
+ factory: () => ({}),
+ }),
+ createExtension({
+ namespace: 'b',
+ attachTo: { id: 'core', input: 'apis' },
+ output: {},
+ factory: () => ({}),
+ }),
+ createExtension({
+ kind: 'k',
+ namespace: 'c',
+ name: 'n',
attachTo: { id: 'core', input: 'apis' },
output: {},
factory: () => ({}),
@@ -54,12 +68,39 @@ describe('createExtensionOverrides', () => {
"id": "core",
"input": "apis",
},
+ "configSchema": undefined,
"disabled": false,
"factory": [Function],
"id": "a",
"inputs": {},
"output": {},
},
+ {
+ "$$type": "@backstage/Extension",
+ "attachTo": {
+ "id": "core",
+ "input": "apis",
+ },
+ "configSchema": undefined,
+ "disabled": false,
+ "factory": [Function],
+ "id": "b",
+ "inputs": {},
+ "output": {},
+ },
+ {
+ "$$type": "@backstage/Extension",
+ "attachTo": {
+ "id": "core",
+ "input": "apis",
+ },
+ "configSchema": undefined,
+ "disabled": false,
+ "factory": [Function],
+ "id": "k:c/n",
+ "inputs": {},
+ "output": {},
+ },
],
"featureFlags": [],
"version": "v1",
@@ -71,7 +112,7 @@ describe('createExtensionOverrides', () => {
const overrides = createExtensionOverrides({
extensions: [
createExtension({
- id: 'a',
+ namespace: 'a',
attachTo: { id: 'core', input: 'apis' },
output: {},
factory: () => ({}),
diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts
index 3902654db7..6c368a0e8b 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-import { Extension } from './createExtension';
+import { Extension, ExtensionDefinition } from './createExtension';
+import { resolveExtensionDefinition } from './resolveExtensionDefinition';
import { FeatureFlagConfig } from './types';
/** @public */
export interface ExtensionOverridesOptions {
- extensions: Extension[];
+ extensions: ExtensionDefinition[];
featureFlags?: FeatureFlagConfig[];
}
@@ -42,7 +43,7 @@ export function createExtensionOverrides(
return {
$$type: '@backstage/ExtensionOverrides',
version: 'v1',
- extensions: options.extensions,
+ extensions: options.extensions.map(def => resolveExtensionDefinition(def)),
featureFlags: options.featureFlags ?? [],
} as InternalExtensionOverrides;
}
diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
index f69b54aa42..0653c42d7c 100644
--- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
+++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
@@ -28,48 +28,34 @@ import { createExtensionInput } from './createExtensionInput';
const nameExtensionDataRef = createExtensionDataRef('name');
-const TechRadarPage = createExtension({
- id: 'plugin.techradar.page',
- attachTo: { id: 'test.output', input: 'names' },
+const Extension1 = createExtension({
+ name: '1',
+ attachTo: { id: 'test/output', input: 'names' },
output: {
name: nameExtensionDataRef,
},
factory() {
- return { name: 'TechRadar' };
+ return { name: 'extension-1' };
},
});
-const CatalogPage = createExtension({
- id: 'plugin.catalog.page',
- attachTo: { id: 'test.output', input: 'names' },
+const Extension2 = createExtension({
+ name: '2',
+ attachTo: { id: 'test/output', input: 'names' },
output: {
name: nameExtensionDataRef,
},
configSchema: createSchemaFromZod(z =>
- z.object({ name: z.string().default('Catalog') }),
+ z.object({ name: z.string().default('extension-2') }),
),
factory({ config }) {
return { name: config.name };
},
});
-const TechDocsAddon = createExtension({
- id: 'plugin.techdocs.addon.example',
- attachTo: { id: 'plugin.techdocs.page', input: 'addons' },
- output: {
- name: nameExtensionDataRef,
- },
- configSchema: createSchemaFromZod(z =>
- z.object({ name: z.string().default('TechDocsAddon') }),
- ),
- factory({ config }) {
- return { name: config.name };
- },
-});
-
-const TechDocsPage = createExtension({
- id: 'plugin.techdocs.page',
- attachTo: { id: 'test.output', input: 'names' },
+const Extension3 = createExtension({
+ name: '3',
+ attachTo: { id: 'test/output', input: 'names' },
inputs: {
addons: createExtensionInput({
name: nameExtensionDataRef,
@@ -79,12 +65,40 @@ const TechDocsPage = createExtension({
name: nameExtensionDataRef,
},
factory({ inputs }) {
- return { name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` };
+ return { name: `extension-3:${inputs.addons.map(n => n.name).join('-')}` };
+ },
+});
+
+const Child = createExtension({
+ name: 'child',
+ attachTo: { id: 'test/3', input: 'addons' },
+ output: {
+ name: nameExtensionDataRef,
+ },
+ configSchema: createSchemaFromZod(z =>
+ z.object({ name: z.string().default('child') }),
+ ),
+ factory({ config }) {
+ return { name: config.name };
+ },
+});
+
+const Child2 = createExtension({
+ name: 'child2',
+ attachTo: { id: 'test/3', input: 'addons' },
+ output: {
+ name: nameExtensionDataRef,
+ },
+ configSchema: createSchemaFromZod(z =>
+ z.object({ name: z.string().default('child2') }),
+ ),
+ factory({ config }) {
+ return { name: config.name };
},
});
const outputExtension = createExtension({
- id: 'test.output',
+ name: 'output',
attachTo: { id: 'core', input: 'root' },
inputs: {
names: createExtensionInput({
@@ -118,40 +132,34 @@ function createTestAppRoot({
describe('createPlugin', () => {
it('should create an empty plugin', () => {
- const plugin = createPlugin({ id: 'empty' });
+ const plugin = createPlugin({ id: 'test' });
expect(plugin).toBeDefined();
});
it('should create a plugin with extension instances', async () => {
const plugin = createPlugin({
- id: 'empty',
- extensions: [TechRadarPage, CatalogPage, outputExtension],
+ id: 'test',
+ extensions: [Extension1, Extension2, outputExtension],
});
expect(plugin).toBeDefined();
await renderWithEffects(
createTestAppRoot({
features: [plugin],
- config: { app: { extensions: [{ 'core.router': false }] } },
+ config: { app: { extensions: [{ 'core/router': false }] } },
}),
);
await expect(
- screen.findByText('Names: TechRadar, Catalog'),
+ screen.findByText('Names: extension-1, extension-2'),
).resolves.toBeInTheDocument();
});
it('should create a plugin with nested extension instances', async () => {
const plugin = createPlugin({
- id: 'empty',
- extensions: [
- TechRadarPage,
- CatalogPage,
- TechDocsPage,
- TechDocsAddon,
- outputExtension,
- ],
+ id: 'test',
+ extensions: [Extension1, Extension2, Extension3, Child, outputExtension],
});
expect(plugin).toBeDefined();
@@ -161,10 +169,10 @@ describe('createPlugin', () => {
config: {
app: {
extensions: [
- { 'core.router': false },
+ { 'core/router': false },
{
- 'plugin.catalog.page': {
- config: { name: 'CatalogRenamed' },
+ 'test/2': {
+ config: { name: 'extension-2-renamed' },
},
},
],
@@ -175,8 +183,63 @@ describe('createPlugin', () => {
await expect(
screen.findByText(
- 'Names: TechRadar, CatalogRenamed, TechDocs-TechDocsAddon',
+ 'Names: extension-1, extension-2-renamed, extension-3:child',
),
).resolves.toBeInTheDocument();
});
+
+ it('should create a plugin with nested extension instances and multiple children', async () => {
+ const plugin = createPlugin({
+ id: 'test',
+ extensions: [
+ Extension1,
+ Extension2,
+ Extension3,
+ Child,
+ Child2,
+ outputExtension,
+ ],
+ });
+ expect(plugin).toBeDefined();
+
+ await renderWithEffects(
+ createTestAppRoot({
+ features: [plugin],
+ config: {
+ app: {
+ extensions: [{ 'core/router': false }],
+ },
+ },
+ }),
+ );
+
+ await expect(
+ screen.findByText(
+ 'Names: extension-1, extension-2, extension-3:child-child2',
+ ),
+ ).resolves.toBeInTheDocument();
+ });
+
+ it('should throw on duplicate extensions', async () => {
+ expect(() =>
+ createPlugin({
+ id: 'test',
+ extensions: [Extension1, Extension1],
+ }),
+ ).toThrow("Plugin 'test' provided duplicate extensions: test/1");
+
+ expect(() =>
+ createPlugin({
+ id: 'test',
+ extensions: [
+ Extension1,
+ Extension2,
+ Extension2,
+ Extension3,
+ Extension3,
+ Extension3,
+ ],
+ }),
+ ).toThrow("Plugin 'test' provided duplicate extensions: test/2, test/3");
+ });
});
diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts
index 5e2a55ab88..ecc4d08f9a 100644
--- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts
+++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-import { Extension } from './createExtension';
+import { Extension, ExtensionDefinition } from './createExtension';
import { ExternalRouteRef, RouteRef } from '../routing';
import { FeatureFlagConfig } from './types';
+import { resolveExtensionDefinition } from './resolveExtensionDefinition';
/** @public */
export type AnyRoutes = { [name in string]: RouteRef };
@@ -32,7 +33,7 @@ export interface PluginOptions<
id: string;
routes?: Routes;
externalRoutes?: ExternalRoutes;
- extensions?: Extension[];
+ extensions?: ExtensionDefinition[];
featureFlags?: FeatureFlagConfig[];
}
@@ -64,14 +65,33 @@ export function createPlugin<
>(
options: PluginOptions,
): BackstagePlugin {
+ const extensions = (options.extensions ?? []).map(def =>
+ resolveExtensionDefinition(def, { namespace: options.id }),
+ );
+
+ const extensionIds = extensions.map(e => e.id);
+ if (extensionIds.length !== new Set(extensionIds).size) {
+ const duplicates = Array.from(
+ new Set(
+ extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index),
+ ),
+ );
+ // TODO(Rugvip): This could provide some more information about the kind + name of the extensions
+ throw new Error(
+ `Plugin '${options.id}' provided duplicate extensions: ${duplicates.join(
+ ', ',
+ )}`,
+ );
+ }
+
return {
$$type: '@backstage/BackstagePlugin',
version: 'v1',
id: options.id,
routes: options.routes ?? ({} as Routes),
externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes),
- extensions: options.extensions ?? [],
featureFlags: options.featureFlags ?? [],
+ extensions,
} as InternalBackstagePlugin;
}
diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts
index 191307ce1a..24f83be6e5 100644
--- a/packages/frontend-plugin-api/src/wiring/index.ts
+++ b/packages/frontend-plugin-api/src/wiring/index.ts
@@ -22,6 +22,7 @@ export {
export {
createExtension,
type Extension,
+ type ExtensionDefinition,
type CreateExtensionOptions,
type ExtensionDataValues,
type ExtensionInputValues,
diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts
new file mode 100644
index 0000000000..8f3755ac5d
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ExtensionDefinition } from './createExtension';
+import { resolveExtensionDefinition } from './resolveExtensionDefinition';
+
+describe('resolveExtensionDefinition', () => {
+ it.each([
+ [{ namespace: 'ns' }, 'ns'],
+ [{ namespace: 'n' }, 'n'],
+ [{ namespace: 'ns', name: 'n' }, 'ns/n'],
+ [{ kind: 'k', namespace: 'ns' }, 'k:ns'],
+ [{ kind: 'k', namespace: 'ns', name: 'n' }, 'k:ns/n'],
+ ])(`should resolve extension IDs %s`, (definition, expected) => {
+ const resolved = resolveExtensionDefinition(
+ definition as ExtensionDefinition,
+ );
+ expect(resolved.id).toBe(expected);
+ });
+
+ it('should fail to resolve extension ID without namespace', () => {
+ expect(() =>
+ resolveExtensionDefinition({
+ kind: 'k',
+ } as ExtensionDefinition),
+ ).toThrow(
+ 'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=k namespace=undefined name=undefined',
+ );
+ expect(() =>
+ resolveExtensionDefinition({} as ExtensionDefinition),
+ ).toThrow(
+ 'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=undefined namespace=undefined name=undefined',
+ );
+ });
+});
diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
new file mode 100644
index 0000000000..42f3a75246
--- /dev/null
+++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Extension, ExtensionDefinition } from './createExtension';
+
+/** @internal */
+export function resolveExtensionDefinition(
+ definition: ExtensionDefinition,
+ context?: { namespace?: string },
+): Extension {
+ const { name, kind, namespace: _, ...rest } = definition;
+ const namespace = definition.namespace ?? context?.namespace;
+
+ const namePart =
+ name && namespace ? `${namespace}/${name}` : namespace || name;
+ if (!namePart) {
+ throw new Error(
+ `Extension must declare an explicit namespace or name as it could not be resolved from context, kind=${kind} namespace=${namespace} name=${name}`,
+ );
+ }
+
+ return {
+ ...rest,
+ id: kind ? `${kind}:${namePart}` : namePart,
+ $$type: '@backstage/Extension',
+ };
+}
diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md
index 59f5696ddc..14e1c68c4c 100644
--- a/packages/frontend-test-utils/api-report.md
+++ b/packages/frontend-test-utils/api-report.md
@@ -4,7 +4,7 @@
```ts
import { ErrorWithContext } from '@backstage/test-utils';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { JsonObject } from '@backstage/types';
import { MockAnalyticsApi } from '@backstage/test-utils';
import { MockConfigApi } from '@backstage/test-utils';
@@ -24,7 +24,7 @@ import { withLogCollector } from '@backstage/test-utils';
// @public (undocumented)
export function createExtensionTester(
- subject: Extension,
+ subject: ExtensionDefinition,
options?: {
config?: TConfig;
},
@@ -36,7 +36,7 @@ export { ErrorWithContext };
export class ExtensionTester {
// (undocumented)
add(
- extension: Extension,
+ extension: ExtensionDefinition,
options?: {
config?: TConfig;
},
diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx
index 398acf4c74..e738bee7d1 100644
--- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx
+++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx
@@ -26,7 +26,7 @@ describe('createExtensionTester', () => {
it('should render a simple extension', async () => {
createExtensionTester(
createExtension({
- id: 'test',
+ namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
output: { element: coreExtensionData.reactElement },
factory: () => ({ element: test
}),
@@ -39,7 +39,7 @@ describe('createExtensionTester', () => {
it('should render an extension even if disabled by default', async () => {
createExtensionTester(
createExtension({
- id: 'test',
+ namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
disabled: true,
output: { element: coreExtensionData.reactElement },
@@ -54,7 +54,7 @@ describe('createExtensionTester', () => {
expect(() =>
createExtensionTester(
createExtension({
- id: 'test',
+ namespace: 'test',
attachTo: { id: 'ignored', input: 'ignored' },
disabled: true,
output: { path: coreExtensionData.routePath },
@@ -62,7 +62,7 @@ describe('createExtensionTester', () => {
}),
).render(),
).toThrow(
- "Failed to instantiate extension 'core.router', input 'children' did not receive required extension data 'core.reactElement' from extension 'test'",
+ "Failed to instantiate extension 'core/router', input 'children' did not receive required extension data 'core.reactElement' from extension 'test'",
);
});
});
diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.ts b/packages/frontend-test-utils/src/app/createExtensionTester.ts
index c95ece342d..257a47439c 100644
--- a/packages/frontend-test-utils/src/app/createExtensionTester.ts
+++ b/packages/frontend-test-utils/src/app/createExtensionTester.ts
@@ -15,7 +15,12 @@
*/
import { createSpecializedApp } from '@backstage/frontend-app-api';
-import { Extension, createPlugin } from '@backstage/frontend-plugin-api';
+import {
+ ExtensionDefinition,
+ createExtensionOverrides,
+} from '@backstage/frontend-plugin-api';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
import { MockConfigApi } from '@backstage/test-utils';
import { JsonArray, JsonObject, JsonValue } from '@backstage/types';
import { RenderResult, render } from '@testing-library/react';
@@ -24,7 +29,7 @@ import { RenderResult, render } from '@testing-library/react';
export class ExtensionTester {
/** @internal */
static forSubject(
- subject: Extension,
+ subject: ExtensionDefinition,
options?: { config?: TConfig },
): ExtensionTester {
const tester = new ExtensionTester();
@@ -33,16 +38,22 @@ export class ExtensionTester {
}
readonly #extensions = new Array<{
- extension: Extension;
+ id: string;
+ extension: ExtensionDefinition;
config?: JsonValue;
}>();
add(
- extension: Extension,
+ extension: ExtensionDefinition,
options?: { config?: TConfig },
): ExtensionTester {
+ const withNamespace = {
+ ...extension,
+ name: !extension.namespace && !extension.name ? 'test' : extension.name,
+ };
this.#extensions.push({
- extension,
+ id: resolveExtensionDefinition(withNamespace).id,
+ extension: withNamespace,
config: options?.config as JsonValue,
});
@@ -61,25 +72,25 @@ export class ExtensionTester {
const extensionsConfig: JsonArray = [
...rest.map(entry => ({
- [entry.extension.id]: {
+ [entry.id]: {
config: entry.config,
},
})),
{
- [subject.extension.id]: {
- attachTo: { id: 'core.router', input: 'children' },
+ [subject.id]: {
+ attachTo: { id: 'core/router', input: 'children' },
config: subject.config,
disabled: false,
},
},
{
- 'core.layout': false,
+ 'core/layout': false,
},
{
- 'core.nav': false,
+ 'core/nav': false,
},
{
- 'core.routes': false,
+ 'core/routes': false,
},
];
@@ -93,8 +104,7 @@ export class ExtensionTester {
const app = createSpecializedApp({
features: [
- createPlugin({
- id: 'test',
+ createExtensionOverrides({
extensions: this.#extensions.map(entry => entry.extension),
}),
],
@@ -107,7 +117,7 @@ export class ExtensionTester {
/** @public */
export function createExtensionTester(
- subject: Extension,
+ subject: ExtensionDefinition,
options?: { config?: TConfig },
): ExtensionTester {
return ExtensionTester.forSubject(subject, options);
diff --git a/plugins/adr/api-report-alpha.md b/plugins/adr/api-report-alpha.md
index 3864c85697..4c9ac1d17e 100644
--- a/plugins/adr/api-report-alpha.md
+++ b/plugins/adr/api-report-alpha.md
@@ -4,11 +4,11 @@
```ts
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
-export const AdrSearchResultListItemExtension: Extension<{
+export const AdrSearchResultListItemExtension: ExtensionDefinition<{
lineClamp: number;
noTrack: boolean;
}>;
diff --git a/plugins/adr/src/alpha.tsx b/plugins/adr/src/alpha.tsx
index 1f6fa0711d..ea4d32e0da 100644
--- a/plugins/adr/src/alpha.tsx
+++ b/plugins/adr/src/alpha.tsx
@@ -31,7 +31,6 @@ function isAdrDocument(result: any): result is AdrDocument {
/** @alpha */
export const AdrSearchResultListItemExtension =
createSearchResultListItemExtension({
- id: 'adr',
configSchema: createSchemaFromZod(z =>
z.object({
// TODO: Define how the icon can be configurable
diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx
index 18ddda2ceb..7ecbac5346 100644
--- a/plugins/catalog-import/src/alpha.tsx
+++ b/plugins/catalog-import/src/alpha.tsx
@@ -38,7 +38,6 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
// TODO: It's currently possible to override the import page with a custom one. We need to decide
// whether this type of override is typically done with an input or by overriding the entire extension.
const CatalogImportPageExtension = createPageExtension({
- id: 'plugin.catalog-import.page',
defaultPath: '/catalog-import',
routeRef: convertLegacyRouteRef(rootRouteRef),
loader: () => import('./components/ImportPage').then(m => ),
diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md
index 913055f8b0..1be108f695 100644
--- a/plugins/catalog-react/api-report-alpha.md
+++ b/plugins/catalog-react/api-report-alpha.md
@@ -8,7 +8,7 @@
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { ExtensionInputValues } from '@backstage/frontend-plugin-api';
import { ResourcePermission } from '@backstage/plugin-permission-common';
import { RouteRef } from '@backstage/frontend-plugin-api';
@@ -17,7 +17,8 @@ import { RouteRef } from '@backstage/frontend-plugin-api';
export function createEntityCardExtension<
TInputs extends AnyExtensionInputMap,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: {
id: string;
input: string;
@@ -30,7 +31,7 @@ export function createEntityCardExtension<
loader: (options: {
inputs: Expand>;
}) => Promise;
-}): Extension<{
+}): ExtensionDefinition<{
filter?: string | undefined;
}>;
@@ -38,7 +39,8 @@ export function createEntityCardExtension<
export function createEntityContentExtension<
TInputs extends AnyExtensionInputMap,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: {
id: string;
input: string;
@@ -54,7 +56,7 @@ export function createEntityContentExtension<
loader: (options: {
inputs: Expand>;
}) => Promise;
-}): Extension<{
+}): ExtensionDefinition<{
title: string;
path: string;
filter?: string | undefined;
diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx
index 43b7156b34..ccb0efbe6d 100644
--- a/plugins/catalog-react/src/alpha.tsx
+++ b/plugins/catalog-react/src/alpha.tsx
@@ -50,7 +50,8 @@ export const entityFilterExpressionExtensionDataRef =
export function createEntityCardExtension<
TInputs extends AnyExtensionInputMap,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: { id: string; input: string };
disabled?: boolean;
inputs?: TInputs;
@@ -61,12 +62,12 @@ export function createEntityCardExtension<
inputs: Expand>;
}) => Promise;
}) {
- const id = `entity.cards.${options.id}`;
-
return createExtension({
- id,
+ kind: 'entity-card',
+ namespace: options.namespace,
+ name: options.name,
attachTo: options.attachTo ?? {
- id: 'entity.content.overview',
+ id: 'entity-content:catalog/overview',
input: 'cards',
},
disabled: options.disabled ?? true,
@@ -104,7 +105,8 @@ export function createEntityCardExtension<
export function createEntityContentExtension<
TInputs extends AnyExtensionInputMap,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: { id: string; input: string };
disabled?: boolean;
inputs?: TInputs;
@@ -118,12 +120,12 @@ export function createEntityContentExtension<
inputs: Expand>;
}) => Promise;
}) {
- const id = `entity.content.${options.id}`;
-
return createExtension({
- id,
+ kind: 'entity-content',
+ namespace: options.namespace,
+ name: options.name,
attachTo: options.attachTo ?? {
- id: 'plugin.catalog.page.entity',
+ id: 'page:catalog/entity',
input: 'contents',
},
disabled: options.disabled ?? true,
diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md
index 52afb0894a..3226d10270 100644
--- a/plugins/catalog/api-report-alpha.md
+++ b/plugins/catalog/api-report-alpha.md
@@ -7,7 +7,7 @@
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
import { PortableSchema } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
@@ -17,11 +17,12 @@ export function createCatalogFilterExtension<
TInputs extends AnyExtensionInputMap,
TConfig = never,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
inputs?: TInputs;
configSchema?: PortableSchema;
loader: (options: { config: TConfig }) => Promise;
-}): Extension;
+}): ExtensionDefinition;
// @alpha (undocumented)
const _default: BackstagePlugin<
diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx
index 7dae30c1fa..48ee647b81 100644
--- a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx
+++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx
@@ -28,16 +28,17 @@ export function createCatalogFilterExtension<
TInputs extends AnyExtensionInputMap,
TConfig = never,
>(options: {
- id: string;
+ namespace?: string;
+ name?: string;
inputs?: TInputs;
configSchema?: PortableSchema;
loader: (options: { config: TConfig }) => Promise;
}) {
- const id = `catalog.filter.${options.id}`;
-
return createExtension({
- id,
- attachTo: { id: 'plugin.catalog.page.index', input: 'filters' },
+ kind: 'catalog-filter',
+ namespace: options.namespace,
+ name: options.name,
+ attachTo: { id: 'page:catalog', input: 'filters' },
inputs: options.inputs ?? {},
configSchema: options.configSchema,
output: {
diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx
index 572e2caa3c..b6a804a576 100644
--- a/plugins/catalog/src/alpha/entityCards.tsx
+++ b/plugins/catalog/src/alpha/entityCards.tsx
@@ -18,7 +18,7 @@ import React from 'react';
import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha';
export const EntityAboutCard = createEntityCardExtension({
- id: 'about',
+ name: 'about',
loader: async () =>
import('../components/AboutCard').then(m => (
@@ -26,7 +26,7 @@ export const EntityAboutCard = createEntityCardExtension({
});
export const EntityLinksCard = createEntityCardExtension({
- id: 'links',
+ name: 'links',
filter: 'has:links',
loader: async () =>
import('../components/EntityLinksCard').then(m => {
@@ -35,7 +35,7 @@ export const EntityLinksCard = createEntityCardExtension({
});
export const EntityLabelsCard = createEntityCardExtension({
- id: 'labels',
+ name: 'labels',
filter: 'has:labels',
loader: async () =>
import('../components/EntityLabelsCard').then(m => (
@@ -44,7 +44,7 @@ export const EntityLabelsCard = createEntityCardExtension({
});
export const EntityDependsOnComponentsCard = createEntityCardExtension({
- id: 'dependsOn.components',
+ name: 'depends-on-components',
loader: async () =>
import('../components/DependsOnComponentsCard').then(m => (
@@ -52,7 +52,7 @@ export const EntityDependsOnComponentsCard = createEntityCardExtension({
});
export const EntityDependsOnResourcesCard = createEntityCardExtension({
- id: 'dependsOn.resources',
+ name: 'depends-on-resources',
loader: async () =>
import('../components/DependsOnResourcesCard').then(m => (
@@ -60,7 +60,7 @@ export const EntityDependsOnResourcesCard = createEntityCardExtension({
});
export const EntityHasComponentsCard = createEntityCardExtension({
- id: 'has.components',
+ name: 'has-components',
loader: async () =>
import('../components/HasComponentsCard').then(m => (
@@ -68,7 +68,7 @@ export const EntityHasComponentsCard = createEntityCardExtension({
});
export const EntityHasResourcesCard = createEntityCardExtension({
- id: 'has.resources',
+ name: 'has-resources',
loader: async () =>
import('../components/HasResourcesCard').then(m => (
@@ -76,7 +76,7 @@ export const EntityHasResourcesCard = createEntityCardExtension({
});
export const EntityHasSubcomponentsCard = createEntityCardExtension({
- id: 'has.subcomponents',
+ name: 'has-subcomponents',
loader: async () =>
import('../components/HasSubcomponentsCard').then(m => (
@@ -84,7 +84,7 @@ export const EntityHasSubcomponentsCard = createEntityCardExtension({
});
export const EntityHasSystemsCard = createEntityCardExtension({
- id: 'has.systems',
+ name: 'has-systems',
loader: async () =>
import('../components/HasSystemsCard').then(m => (
diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx
index e280326b02..72adf4ec60 100644
--- a/plugins/catalog/src/alpha/entityContents.tsx
+++ b/plugins/catalog/src/alpha/entityContents.tsx
@@ -26,7 +26,7 @@ import {
} from '@backstage/plugin-catalog-react/alpha';
export const OverviewEntityContent = createEntityContentExtension({
- id: 'overview',
+ name: 'overview',
defaultPath: '/',
defaultTitle: 'Overview',
disabled: false,
diff --git a/plugins/catalog/src/alpha/filters.tsx b/plugins/catalog/src/alpha/filters.tsx
index 7b73c2d14b..be06e70611 100644
--- a/plugins/catalog/src/alpha/filters.tsx
+++ b/plugins/catalog/src/alpha/filters.tsx
@@ -19,7 +19,7 @@ import { createCatalogFilterExtension } from './createCatalogFilterExtension';
import { createSchemaFromZod } from '@backstage/frontend-plugin-api';
const CatalogEntityTagFilter = createCatalogFilterExtension({
- id: 'entity.tag',
+ name: 'tag',
loader: async () => {
const { EntityTagPicker } = await import('@backstage/plugin-catalog-react');
return ;
@@ -27,7 +27,7 @@ const CatalogEntityTagFilter = createCatalogFilterExtension({
});
const CatalogEntityKindFilter = createCatalogFilterExtension({
- id: 'entity.kind',
+ name: 'kind',
configSchema: createSchemaFromZod(z =>
z.object({
initialFilter: z.string().default('component'),
@@ -42,7 +42,7 @@ const CatalogEntityKindFilter = createCatalogFilterExtension({
});
const CatalogEntityTypeFilter = createCatalogFilterExtension({
- id: 'entity.type',
+ name: 'type',
loader: async () => {
const { EntityTypePicker } = await import(
'@backstage/plugin-catalog-react'
@@ -52,7 +52,7 @@ const CatalogEntityTypeFilter = createCatalogFilterExtension({
});
const CatalogEntityOwnerFilter = createCatalogFilterExtension({
- id: 'entity.mode',
+ name: 'mode',
configSchema: createSchemaFromZod(z =>
z.object({
mode: z.enum(['owners-only', 'all']).optional(),
@@ -67,7 +67,7 @@ const CatalogEntityOwnerFilter = createCatalogFilterExtension({
});
const CatalogEntityNamespaceFilter = createCatalogFilterExtension({
- id: 'entity.namespace',
+ name: 'namespace',
loader: async () => {
const { EntityNamespacePicker } = await import(
'@backstage/plugin-catalog-react'
@@ -77,7 +77,7 @@ const CatalogEntityNamespaceFilter = createCatalogFilterExtension({
});
const CatalogEntityLifecycleFilter = createCatalogFilterExtension({
- id: 'entity.lifecycle',
+ name: 'lifecycle',
loader: async () => {
const { EntityLifecyclePicker } = await import(
'@backstage/plugin-catalog-react'
@@ -87,7 +87,7 @@ const CatalogEntityLifecycleFilter = createCatalogFilterExtension({
});
const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({
- id: 'entity.processing.status',
+ name: 'processing.status',
loader: async () => {
const { EntityProcessingStatusPicker } = await import(
'@backstage/plugin-catalog-react'
@@ -97,7 +97,7 @@ const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({
});
const CatalogUserListFilter = createCatalogFilterExtension({
- id: 'user.list',
+ name: 'list',
configSchema: createSchemaFromZod(z =>
z.object({
initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'),
diff --git a/plugins/catalog/src/alpha/navItems.tsx b/plugins/catalog/src/alpha/navItems.tsx
index 77ea394a97..8dde27b95a 100644
--- a/plugins/catalog/src/alpha/navItems.tsx
+++ b/plugins/catalog/src/alpha/navItems.tsx
@@ -20,7 +20,6 @@ import { createNavItemExtension } from '@backstage/frontend-plugin-api';
import { rootRouteRef } from '../routes';
export const CatalogIndexNavItem = createNavItemExtension({
- id: 'catalog.nav.index',
routeRef: convertLegacyRouteRef(rootRouteRef),
title: 'Catalog',
icon: HomeIcon,
diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx
index 6324f53083..7dd32f7748 100644
--- a/plugins/catalog/src/alpha/pages.tsx
+++ b/plugins/catalog/src/alpha/pages.tsx
@@ -30,7 +30,6 @@ import { rootRouteRef } from '../routes';
import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl';
export const CatalogIndexPage = createPageExtension({
- id: 'plugin.catalog.page.index',
defaultPath: '/catalog',
routeRef: convertLegacyRouteRef(rootRouteRef),
inputs: {
@@ -46,7 +45,7 @@ export const CatalogIndexPage = createPageExtension({
});
export const CatalogEntityPage = createPageExtension({
- id: 'plugin.catalog.page.entity',
+ name: 'entity',
defaultPath: '/catalog/:namespace/:kind/:name',
routeRef: convertLegacyRouteRef(entityRouteRef),
inputs: {
diff --git a/plugins/catalog/src/alpha/searchResultItems.tsx b/plugins/catalog/src/alpha/searchResultItems.tsx
index f677869136..c84aabf300 100644
--- a/plugins/catalog/src/alpha/searchResultItems.tsx
+++ b/plugins/catalog/src/alpha/searchResultItems.tsx
@@ -18,7 +18,6 @@ import { createSearchResultListItemExtension } from '@backstage/plugin-search-re
export const CatalogSearchResultListItemExtension =
createSearchResultListItemExtension({
- id: 'catalog',
predicate: result => result.type === 'software-catalog',
component: () =>
import('../components/CatalogSearchResultListItem').then(
diff --git a/plugins/explore/api-report-alpha.md b/plugins/explore/api-report-alpha.md
index 8670338f3e..4d1afa89bf 100644
--- a/plugins/explore/api-report-alpha.md
+++ b/plugins/explore/api-report-alpha.md
@@ -4,14 +4,14 @@
```ts
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
const _default: BackstagePlugin<{}, {}>;
export default _default;
// @alpha (undocumented)
-export const ExploreSearchResultListItemExtension: Extension<{
+export const ExploreSearchResultListItemExtension: ExtensionDefinition<{
noTrack?: boolean | undefined;
}>;
diff --git a/plugins/explore/src/alpha.tsx b/plugins/explore/src/alpha.tsx
index 1825af5ddb..f06379c84b 100644
--- a/plugins/explore/src/alpha.tsx
+++ b/plugins/explore/src/alpha.tsx
@@ -20,7 +20,6 @@ import { createSearchResultListItemExtension } from '@backstage/plugin-search-re
/** @alpha */
export const ExploreSearchResultListItemExtension =
createSearchResultListItemExtension({
- id: 'explore',
predicate: result => result.type === 'tools',
component: () =>
import('./components/ToolSearchResultListItem').then(
diff --git a/plugins/graphiql/api-report-alpha.md b/plugins/graphiql/api-report-alpha.md
index 3362cf0a59..c5895067df 100644
--- a/plugins/graphiql/api-report-alpha.md
+++ b/plugins/graphiql/api-report-alpha.md
@@ -4,20 +4,21 @@
```ts
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { GraphQLEndpoint } from '@backstage/plugin-graphiql';
import { PortableSchema } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
-export function createEndpointExtension(options: {
- id: string;
+export function createGraphiQLEndpointExtension(options: {
+ namespace?: string;
+ name?: string;
configSchema?: PortableSchema;
disabled?: boolean;
factory: (options: { config: TConfig }) => {
endpoint: GraphQLEndpoint;
};
-}): Extension;
+}): ExtensionDefinition;
// @alpha (undocumented)
const _default: BackstagePlugin<
@@ -29,15 +30,15 @@ const _default: BackstagePlugin<
export default _default;
// @alpha (undocumented)
-export const graphiqlBrowseApi: Extension<{}>;
+export const graphiqlBrowseApi: ExtensionDefinition<{}>;
// @alpha (undocumented)
-export const GraphiqlPage: Extension<{
+export const GraphiqlPage: ExtensionDefinition<{
path: string;
}>;
// @alpha (undocumented)
-export const graphiqlPageSidebarItem: Extension<{
+export const graphiqlPageSidebarItem: ExtensionDefinition<{
title: string;
}>;
diff --git a/plugins/graphiql/src/alpha.tsx b/plugins/graphiql/src/alpha.tsx
index 760bc77d8e..d9470b7d46 100644
--- a/plugins/graphiql/src/alpha.tsx
+++ b/plugins/graphiql/src/alpha.tsx
@@ -38,7 +38,6 @@ import { convertLegacyRouteRef } from '@backstage/core-compat-api';
/** @alpha */
export const GraphiqlPage = createPageExtension({
- id: 'plugin.graphiql.page',
defaultPath: '/graphiql',
routeRef: convertLegacyRouteRef(graphiQLRouteRef),
loader: () => import('./components').then(m => ),
@@ -46,7 +45,6 @@ export const GraphiqlPage = createPageExtension({
/** @alpha */
export const graphiqlPageSidebarItem = createNavItemExtension({
- id: 'plugin.graphiql.nav.index',
title: 'GraphiQL',
icon: GraphiQLIcon as IconComponent,
routeRef: convertLegacyRouteRef(graphiQLRouteRef),
@@ -59,7 +57,7 @@ const endpointDataRef = createExtensionDataRef(
/** @alpha */
export const graphiqlBrowseApi = createApiExtension({
- api: graphQlBrowseApiRef, // apis.plugin.graphiql.browse
+ api: graphQlBrowseApiRef,
inputs: {
endpoints: createExtensionInput({
endpoint: endpointDataRef,
@@ -74,15 +72,18 @@ export const graphiqlBrowseApi = createApiExtension({
});
/** @alpha */
-export function createEndpointExtension(options: {
- id: string;
+export function createGraphiQLEndpointExtension(options: {
+ namespace?: string;
+ name?: string;
configSchema?: PortableSchema;
disabled?: boolean;
factory: (options: { config: TConfig }) => { endpoint: GraphQLEndpoint };
}) {
return createExtension({
- id: `apis.plugin.graphiql.browse.${options.id}`,
- attachTo: { id: 'apis.plugin.graphiql.browse', input: 'endpoints' },
+ kind: 'graphiql-endpoint',
+ namespace: options.namespace,
+ name: options.name,
+ attachTo: { id: 'api:graphiql/browse', input: 'endpoints' },
configSchema: options.configSchema,
disabled: options.disabled ?? false,
output: {
@@ -97,8 +98,8 @@ export function createEndpointExtension(options: {
}
/** @alpha */
-const gitlabGraphiQLBrowseExtension = createEndpointExtension({
- id: 'gitlab',
+const gitlabGraphiQLBrowseExtension = createGraphiQLEndpointExtension({
+ name: 'gitlab',
disabled: true,
configSchema: createSchemaFromZod(z =>
z
diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx
index c8308f7c0a..9ccb4c8ed6 100644
--- a/plugins/home/src/alpha.tsx
+++ b/plugins/home/src/alpha.tsx
@@ -33,7 +33,6 @@ const rootRouteRef = createRouteRef();
export const titleExtensionDataRef = createExtensionDataRef('title');
const HomepageCompositionRootExtension = createPageExtension({
- id: 'home',
defaultPath: '/home',
routeRef: rootRouteRef,
inputs: {
diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md
index 9c1248b536..285f1232c2 100644
--- a/plugins/search-react/api-report-alpha.md
+++ b/plugins/search-react/api-report-alpha.md
@@ -6,7 +6,7 @@
///
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { ListItemProps } from '@material-ui/core';
import { PortableSchema } from '@backstage/frontend-plugin-api';
import { SearchDocument } from '@backstage/plugin-search-common';
@@ -23,7 +23,9 @@ export function createSearchResultListItemExtension<
TConfig extends {
noTrack?: boolean;
},
->(options: SearchResultItemExtensionOptions): Extension;
+>(
+ options: SearchResultItemExtensionOptions,
+): ExtensionDefinition;
// @alpha (undocumented)
export type SearchResultItemExtensionComponent = <
@@ -47,7 +49,8 @@ export type SearchResultItemExtensionOptions<
noTrack?: boolean;
},
> = {
- id: string;
+ namespace?: string;
+ name?: string;
attachTo?: {
id: string;
input: string;
diff --git a/plugins/search-react/src/alpha.test.tsx b/plugins/search-react/src/alpha.test.tsx
index e1a8166390..6950dec664 100644
--- a/plugins/search-react/src/alpha.test.tsx
+++ b/plugins/search-react/src/alpha.test.tsx
@@ -44,8 +44,7 @@ describe('createSearchResultListItemExtension', () => {
const TechDocsSearchResultItemExtension =
createSearchResultListItemExtension({
- id: 'techdocs',
- attachTo: { id: 'plugin.search.page', input: 'items' },
+ namespace: 'techdocs',
configSchema: createSchemaFromZod(z =>
z
.object({
@@ -67,14 +66,13 @@ describe('createSearchResultListItemExtension', () => {
const ExploreSearchResultItemExtension =
createSearchResultListItemExtension({
- id: 'explore',
- attachTo: { id: 'plugin.search.page', input: 'items' },
+ namespace: 'explore',
predicate: result => result.type === 'explore',
component: async () => ExploreSearchResultItemComponent,
});
const SearchPageExtension = createPageExtension({
- id: 'plugin.search.page',
+ namespace: 'search',
defaultPath: '/',
inputs: {
items: createExtensionInput({
diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx
index bb738a2b08..c733e2ee9f 100644
--- a/plugins/search-react/src/alpha.tsx
+++ b/plugins/search-react/src/alpha.tsx
@@ -58,9 +58,13 @@ export type SearchResultItemExtensionOptions<
TConfig extends { noTrack?: boolean },
> = {
/**
- * The extension id.
+ * The extension namespace.
*/
- id: string;
+ namespace?: string;
+ /**
+ * The extension name.
+ */
+ name?: string;
/**
* The extension attachment point (e.g., search modal or page).
*/
@@ -86,8 +90,6 @@ export type SearchResultItemExtensionOptions<
export function createSearchResultListItemExtension<
TConfig extends { noTrack?: boolean },
>(options: SearchResultItemExtensionOptions) {
- const id = `plugin.search.result.item.${options.id}`;
-
const configSchema =
'configSchema' in options
? options.configSchema
@@ -98,9 +100,11 @@ export function createSearchResultListItemExtension<
) as PortableSchema);
return createExtension({
- id,
+ kind: 'search-result-list-item',
+ namespace: options.namespace,
+ name: options.name,
attachTo: options.attachTo ?? {
- id: 'plugin.search.page',
+ id: 'page:search',
input: 'items',
},
configSchema,
diff --git a/plugins/search/api-report-alpha.md b/plugins/search/api-report-alpha.md
index 99f3d07b57..533734f8e9 100644
--- a/plugins/search/api-report-alpha.md
+++ b/plugins/search/api-report-alpha.md
@@ -4,7 +4,7 @@
```ts
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
@@ -17,15 +17,15 @@ const _default: BackstagePlugin<
export default _default;
// @alpha (undocumented)
-export const SearchApi: Extension<{}>;
+export const SearchApi: ExtensionDefinition<{}>;
// @alpha (undocumented)
-export const SearchNavItem: Extension<{
+export const SearchNavItem: ExtensionDefinition<{
title: string;
}>;
// @alpha (undocumented)
-export const SearchPage: Extension<{
+export const SearchPage: ExtensionDefinition<{
path: string;
noTrack: boolean;
}>;
diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx
index ef11828c71..3f1d307237 100644
--- a/plugins/search/src/alpha.tsx
+++ b/plugins/search/src/alpha.tsx
@@ -98,7 +98,6 @@ const useSearchPageStyles = makeStyles((theme: Theme) => ({
/** @alpha */
export const SearchPage = createPageExtension({
- id: 'plugin.search.page',
routeRef: convertLegacyRouteRef(rootRouteRef),
configSchema: createSchemaFromZod(z =>
z.object({
@@ -235,7 +234,6 @@ export const SearchPage = createPageExtension({
/** @alpha */
export const SearchNavItem = createNavItemExtension({
- id: 'plugin.search.nav.index',
routeRef: convertLegacyRouteRef(rootRouteRef),
title: 'Search',
icon: SearchIcon,
diff --git a/plugins/stack-overflow/src/alpha.tsx b/plugins/stack-overflow/src/alpha.tsx
index 3708271ef1..f34c91f78b 100644
--- a/plugins/stack-overflow/src/alpha.tsx
+++ b/plugins/stack-overflow/src/alpha.tsx
@@ -33,7 +33,6 @@ const StackOverflowApi = createApiExtension({
/** @alpha */
const StackOverflowSearchResultListItem = createSearchResultListItemExtension({
- id: 'stack-overflow',
predicate: result => result.type === 'stack-overflow',
component: () =>
import('./search/StackOverflowSearchResultListItem').then(
diff --git a/plugins/tech-radar/api-report-alpha.md b/plugins/tech-radar/api-report-alpha.md
index da8f12cf0e..bcc9142223 100644
--- a/plugins/tech-radar/api-report-alpha.md
+++ b/plugins/tech-radar/api-report-alpha.md
@@ -4,7 +4,7 @@
```ts
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
@@ -17,7 +17,10 @@ const _default: BackstagePlugin<
export default _default;
// @alpha (undocumented)
-export const TechRadarPage: Extension<{
+export const sampleTechRadarApi: ExtensionDefinition<{}>;
+
+// @alpha (undocumented)
+export const TechRadarPage: ExtensionDefinition<{
height: number;
width: number;
title: string;
diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json
index bcfcc7c583..2fba4121c7 100644
--- a/plugins/tech-radar/package.json
+++ b/plugins/tech-radar/package.json
@@ -68,6 +68,7 @@
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
+ "@backstage/frontend-test-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
diff --git a/plugins/tech-radar/src/alpha.test.tsx b/plugins/tech-radar/src/alpha.test.tsx
new file mode 100644
index 0000000000..37d39c412b
--- /dev/null
+++ b/plugins/tech-radar/src/alpha.test.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createExtensionTester } from '@backstage/frontend-test-utils';
+import { screen } from '@testing-library/react';
+import { TechRadarPage, sampleTechRadarApi } from './alpha';
+
+describe('TechRadarPage', () => {
+ beforeAll(() => {
+ Object.defineProperty(window.SVGElement.prototype, 'getBBox', {
+ value: () => ({ width: 100, height: 100 }),
+ configurable: true,
+ });
+ });
+
+ it('renders without exploding', async () => {
+ createExtensionTester(TechRadarPage).add(sampleTechRadarApi).render();
+
+ await expect(screen.findByText('Tech Radar')).resolves.toBeInTheDocument();
+ });
+});
diff --git a/plugins/tech-radar/src/alpha.tsx b/plugins/tech-radar/src/alpha.tsx
index f9a34e2158..2d15e92537 100644
--- a/plugins/tech-radar/src/alpha.tsx
+++ b/plugins/tech-radar/src/alpha.tsx
@@ -29,7 +29,6 @@ import { rootRouteRef } from './plugin';
/** @alpha */
export const TechRadarPage = createPageExtension({
- id: 'plugin.techradar.page',
defaultPath: '/tech-radar',
routeRef: convertLegacyRouteRef(rootRouteRef),
configSchema: createSchemaFromZod(z =>
@@ -48,11 +47,9 @@ export const TechRadarPage = createPageExtension({
import('./components').then(m => ),
});
-const sampleTechRadarApi = createApiExtension({
- api: techRadarApiRef,
- factory() {
- return createApiFactory(techRadarApiRef, new SampleTechRadarApi());
- },
+/** @alpha */
+export const sampleTechRadarApi = createApiExtension({
+ factory: createApiFactory(techRadarApiRef, new SampleTechRadarApi()),
});
/** @alpha */
diff --git a/plugins/techdocs/api-report-alpha.md b/plugins/techdocs/api-report-alpha.md
index 2b63d6e691..afc3202a43 100644
--- a/plugins/techdocs/api-report-alpha.md
+++ b/plugins/techdocs/api-report-alpha.md
@@ -4,7 +4,7 @@
```ts
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
-import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
@@ -23,7 +23,7 @@ const _default: BackstagePlugin<
export default _default;
// @alpha (undocumented)
-export const TechDocsSearchResultListItemExtension: Extension<{
+export const TechDocsSearchResultListItemExtension: ExtensionDefinition<{
lineClamp: number;
noTrack: boolean;
asListItem: boolean;
diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx
index 533b4cb524..82bda828e2 100644
--- a/plugins/techdocs/src/alpha.tsx
+++ b/plugins/techdocs/src/alpha.tsx
@@ -46,53 +46,45 @@ import { createEntityContentExtension } from '@backstage/plugin-catalog-react/al
/** @alpha */
const techDocsStorage = createApiExtension({
- api: techdocsStorageApiRef,
-
- factory() {
- return createApiFactory({
- api: techdocsStorageApiRef,
- deps: {
- configApi: configApiRef,
- discoveryApi: discoveryApiRef,
- identityApi: identityApiRef,
- fetchApi: fetchApiRef,
- },
- factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
- new TechDocsStorageClient({
- configApi,
- discoveryApi,
- identityApi,
- fetchApi,
- }),
- });
- },
+ factory: createApiFactory({
+ api: techdocsStorageApiRef,
+ deps: {
+ configApi: configApiRef,
+ discoveryApi: discoveryApiRef,
+ identityApi: identityApiRef,
+ fetchApi: fetchApiRef,
+ },
+ factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
+ new TechDocsStorageClient({
+ configApi,
+ discoveryApi,
+ identityApi,
+ fetchApi,
+ }),
+ }),
});
/** @alpha */
const techDocsClient = createApiExtension({
- api: techdocsApiRef,
- factory() {
- return createApiFactory({
- api: techdocsApiRef,
- deps: {
- configApi: configApiRef,
- discoveryApi: discoveryApiRef,
- fetchApi: fetchApiRef,
- },
- factory: ({ configApi, discoveryApi, fetchApi }) =>
- new TechDocsClient({
- configApi,
- discoveryApi,
- fetchApi,
- }),
- });
- },
+ factory: createApiFactory({
+ api: techdocsApiRef,
+ deps: {
+ configApi: configApiRef,
+ discoveryApi: discoveryApiRef,
+ fetchApi: fetchApiRef,
+ },
+ factory: ({ configApi, discoveryApi, fetchApi }) =>
+ new TechDocsClient({
+ configApi,
+ discoveryApi,
+ fetchApi,
+ }),
+ }),
});
/** @alpha */
export const TechDocsSearchResultListItemExtension =
createSearchResultListItemExtension({
- id: 'techdocs',
configSchema: createSchemaFromZod(z =>
z.object({
// TODO: Define how the icon can be configurable
@@ -118,7 +110,6 @@ export const TechDocsSearchResultListItemExtension =
* @alpha
*/
const TechDocsIndexPage = createPageExtension({
- id: 'plugin.techdocs.indexPage',
defaultPath: '/docs',
routeRef: convertLegacyRouteRef(rootRouteRef),
loader: () =>
@@ -133,7 +124,7 @@ const TechDocsIndexPage = createPageExtension({
* @alpha
*/
const TechDocsReaderPage = createPageExtension({
- id: 'plugin.techdocs.readerPage',
+ name: 'reader',
defaultPath: '/docs/:namespace/:kind/:name',
routeRef: convertLegacyRouteRef(rootDocsRouteRef),
loader: () =>
@@ -148,7 +139,6 @@ const TechDocsReaderPage = createPageExtension({
* @alpha
*/
const TechDocsEntityContent = createEntityContentExtension({
- id: 'techdocs',
defaultPath: 'docs',
defaultTitle: 'TechDocs',
loader: () => import('./Router').then(m => ),
@@ -156,7 +146,6 @@ const TechDocsEntityContent = createEntityContentExtension({
/** @alpha */
const TechDocsNavItem = createNavItemExtension({
- id: 'plugin.techdocs.nav.index',
icon: LibraryBooks,
title: 'Docs',
routeRef: convertLegacyRouteRef(rootRouteRef),
diff --git a/plugins/user-settings/src/alpha.tsx b/plugins/user-settings/src/alpha.tsx
index 419ee2808d..dfcd1d7e2e 100644
--- a/plugins/user-settings/src/alpha.tsx
+++ b/plugins/user-settings/src/alpha.tsx
@@ -27,7 +27,6 @@ import React from 'react';
export * from './translation';
const UserSettingsPage = createPageExtension({
- id: 'plugin.user-settings.page',
defaultPath: '/settings',
routeRef: convertLegacyRouteRef(settingsRouteRef),
inputs: {
diff --git a/yarn.lock b/yarn.lock
index 45e22286fa..aac0b29773 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9587,6 +9587,7 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
+ "@backstage/frontend-test-utils": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.12.2