From e2eeb1dde10b99cc53696ab69157be205379f01a Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 12 Aug 2024 14:21:23 +0200 Subject: [PATCH 1/5] chore: wip some things Signed-off-by: blam --- .../src/blueprints/PageBlueprint.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index b224ea491e..89ac938505 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -64,3 +64,68 @@ export const PageBlueprint = createExtensionBlueprint({ } }, }); + +const homePage = PageBlueprint.make({ + params: { + defaultPath: '/home', + loader: async () =>

Home

, + }, +}); + +const homePlugin = createPlugin({ + id: 'asd', + extensions: [homePage], +}); + +// overrides-package locally + +/* + +# TODO +- [ ] Decorate getExtension(...) extension definitions with a namespace +- [ ] Implement plugin.override([...]) + +*/ + +// import plugin from '@backstage/plugin-home'; + +// export const exp1 = plugin.override([ +// plugin.getExtension('page:home').override({ +// *factory(originalFactory) { +// yield* originalFactory(); +// yield coreExtensionData.routePath('/backstage'); +// // return [...originalFactory(), coreExtensionData.routePath('/backstage')]; +// // return originalFactory({ +// // defaultPath: '/backstage', +// // }); +// }, +// }), +// PageBlueprint.make({ +// name: 'home', +// params: { +// defaultPath: '/home', +// loader: async () =>

Home

, +// }, +// }) +// ]); + +// // kind:namespace/name +// // entity-card:github/issues +// // entity-card:github/pull-requests +// // page:catalog + +// export const exp2 = createExtensionOverrides({ +// extensions: [ +// homePlugin.getExtension('page:home').override({ +// // namespace: 'home', +// *factory(originalFactory) { +// yield* originalFactory(); +// yield coreExtensionData.routePath('/backstage'); +// // return [...originalFactory(), coreExtensionData.routePath('/backstage')]; +// // return originalFactory({ +// // defaultPath: '/backstage', +// // }); +// }, +// }) +// ] +// }) From d0ab4d33566a6fc27a83d7c71d1a1cb083450728 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 12 Aug 2024 17:15:30 +0200 Subject: [PATCH 2/5] feat: implementing plugin overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Co-authored-by: Johan Haals Signed-off-by: blam --- .../src/blueprints/PageBlueprint.tsx | 65 ------------------- .../src/wiring/createPlugin.test.ts | 61 ++++++++++++++++- .../src/wiring/createPlugin.ts | 19 +++++- .../frontend-plugin-api/src/wiring/types.ts | 3 + 4 files changed, 81 insertions(+), 67 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 89ac938505..b224ea491e 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -64,68 +64,3 @@ export const PageBlueprint = createExtensionBlueprint({ } }, }); - -const homePage = PageBlueprint.make({ - params: { - defaultPath: '/home', - loader: async () =>

Home

, - }, -}); - -const homePlugin = createPlugin({ - id: 'asd', - extensions: [homePage], -}); - -// overrides-package locally - -/* - -# TODO -- [ ] Decorate getExtension(...) extension definitions with a namespace -- [ ] Implement plugin.override([...]) - -*/ - -// import plugin from '@backstage/plugin-home'; - -// export const exp1 = plugin.override([ -// plugin.getExtension('page:home').override({ -// *factory(originalFactory) { -// yield* originalFactory(); -// yield coreExtensionData.routePath('/backstage'); -// // return [...originalFactory(), coreExtensionData.routePath('/backstage')]; -// // return originalFactory({ -// // defaultPath: '/backstage', -// // }); -// }, -// }), -// PageBlueprint.make({ -// name: 'home', -// params: { -// defaultPath: '/home', -// loader: async () =>

Home

, -// }, -// }) -// ]); - -// // kind:namespace/name -// // entity-card:github/issues -// // entity-card:github/pull-requests -// // page:catalog - -// export const exp2 = createExtensionOverrides({ -// extensions: [ -// homePlugin.getExtension('page:home').override({ -// // namespace: 'home', -// *factory(originalFactory) { -// yield* originalFactory(); -// yield coreExtensionData.routePath('/backstage'); -// // return [...originalFactory(), coreExtensionData.routePath('/backstage')]; -// // return originalFactory({ -// // defaultPath: '/backstage', -// // }); -// }, -// }) -// ] -// }) diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index 76cc030be3..f1a0817c80 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -148,7 +148,28 @@ describe('createPlugin', () => { }); expect(plugin).toBeDefined(); - expect(plugin.getExtension('test/1')).toBe(Extension1); + expect(plugin.getExtension('test/1')).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "test/output", + "input": "names", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": undefined, + "name": "1", + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); // @ts-expect-error expect(plugin.getExtension('nonexistent')).toBeUndefined(); @@ -250,4 +271,42 @@ describe('createPlugin', () => { }), ).toThrow("Plugin 'test' provided duplicate extensions: test/2, test/3"); }); + + describe('overrides', () => { + it('should allow overriding extensions that have a matching ID, while keeping old extensions that do not have overlapping IDs', async () => { + const plugin = createPlugin({ + id: 'test', + extensions: [Extension1, Extension2, outputExtension], + }); + + expect(plugin.getExtension('test/1')).toMatchObject({ + namespace: 'test', + }); + + await renderWithEffects( + createTestAppRoot({ + features: [ + plugin.withOverrides({ + extensions: [ + plugin.getExtension('test/1').override({ + factory() { + return [nameExtensionDataRef('overridden')]; + }, + }), + ], + }), + ], + config: { + app: { + extensions: [{ 'app/root': false }], + }, + }, + }), + ); + + await expect( + screen.findByText('Names: extension-2, overridden'), + ).resolves.toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index 1d2a12ffe2..2f7a813c90 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -78,7 +78,7 @@ export function createPlugin< for (const def of options.extensions ?? []) { const ext = resolveExtensionDefinition(def, { namespace: options.id }); extensions.push(ext); - extensionDefinitionsById.set(ext.id, def); + extensionDefinitionsById.set(ext.id, { ...def, namespace: options.id }); } if (extensions.length !== extensionDefinitionsById.size) { @@ -110,6 +110,23 @@ export function createPlugin< toString() { return `Plugin{id=${options.id}}`; }, + withOverrides(overrides) { + const overrideExtensionIds = new Set( + overrides.extensions.map( + e => resolveExtensionDefinition(e, { namespace: options.id }).id, + ), + ); + const nonOverridenExtensions = (options.extensions ?? []).filter( + e => + !overrideExtensionIds.has( + resolveExtensionDefinition(e, { namespace: options.id }).id, + ), + ); + return createPlugin({ + ...options, + extensions: [...nonOverridenExtensions, ...overrides.extensions], + }); + }, } as InternalBackstagePlugin; } diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index b1afd4e7ea..4a7d972b37 100644 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -51,6 +51,9 @@ export interface BackstagePlugin< readonly routes: TRoutes; readonly externalRoutes: TExternalRoutes; getExtension(id: TId): TExtensionMap[TId]; + withOverrides(options: { + extensions: Array>; + }): BackstagePlugin; } /** @public */ From b0ef2dc0ba5b604bdbf7a87f4026b9975cfd05ba Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 09:12:21 +0200 Subject: [PATCH 3/5] chore: restructure tests and fix api-report Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 4 +++ .../src/wiring/createPlugin.test.ts | 34 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 6d548e3c26..f841fc2ce2 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -356,6 +356,10 @@ export interface BackstagePlugin< readonly id: string; // (undocumented) readonly routes: TRoutes; + // (undocumented) + withOverrides(options: { + extensions: Array>; + }): BackstagePlugin; } export { BackstageUserIdentity }; diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index f1a0817c80..f76e4ec9a5 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -273,16 +273,42 @@ describe('createPlugin', () => { }); describe('overrides', () => { + it('should return a plugin instance with the correct namespace', () => { + const plugin = createPlugin({ + id: 'test', + extensions: [Extension1, Extension2], + }); + + expect(plugin.getExtension('test/1')).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "test/output", + "input": "names", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": undefined, + "name": "1", + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + it('should allow overriding extensions that have a matching ID, while keeping old extensions that do not have overlapping IDs', async () => { const plugin = createPlugin({ id: 'test', extensions: [Extension1, Extension2, outputExtension], }); - expect(plugin.getExtension('test/1')).toMatchObject({ - namespace: 'test', - }); - await renderWithEffects( createTestAppRoot({ features: [ From 99abb6bf299366bd311ec6afdf9bba6c1e2be309 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 09:16:07 +0200 Subject: [PATCH 4/5] chore: added a simple changeset Signed-off-by: blam --- .changeset/fresh-timers-walk.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .changeset/fresh-timers-walk.md diff --git a/.changeset/fresh-timers-walk.md b/.changeset/fresh-timers-walk.md new file mode 100644 index 0000000000..647e72830a --- /dev/null +++ b/.changeset/fresh-timers-walk.md @@ -0,0 +1,20 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Support overriding of plugin extensions using the new `plugin.override` method. + +```tsx +import homePlugin from '@backstage/plugin-home'; + +export default homePlugin.override({ + extensions: [ + homePage.getExtension('page:home').override({ + *factory(originalFactory) { + yield* originalFactory(); + yield coreExtensionData.reactElement(

My custom home page

); + }, + }), + ], +}); +``` From aa11af2499cdf3de2f891d0020e9bddfb547ad58 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 09:19:15 +0200 Subject: [PATCH 5/5] chore: updating changeset Signed-off-by: blam --- .changeset/fresh-timers-walk.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fresh-timers-walk.md b/.changeset/fresh-timers-walk.md index 647e72830a..a187bede83 100644 --- a/.changeset/fresh-timers-walk.md +++ b/.changeset/fresh-timers-walk.md @@ -2,12 +2,12 @@ '@backstage/frontend-plugin-api': patch --- -Support overriding of plugin extensions using the new `plugin.override` method. +Support overriding of plugin extensions using the new `plugin.withOverrides` method. ```tsx import homePlugin from '@backstage/plugin-home'; -export default homePlugin.override({ +export default homePlugin.withOverrides({ extensions: [ homePage.getExtension('page:home').override({ *factory(originalFactory) {