From 68ffb9e67dad51d82321226c6ef4aad7ad708ac7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Oct 2023 10:10:27 +0200 Subject: [PATCH 1/3] frontend-app-api: reject undeclared attachments Signed-off-by: Patrik Oldsberg --- .changeset/five-hornets-provide.md | 5 +++ .../wiring/createExtensionInstance.test.ts | 43 +++++++++++++++++++ .../src/wiring/createExtensionInstance.ts | 9 ++++ 3 files changed, 57 insertions(+) create mode 100644 .changeset/five-hornets-provide.md diff --git a/.changeset/five-hornets-provide.md b/.changeset/five-hornets-provide.md new file mode 100644 index 0000000000..a422c5fbc8 --- /dev/null +++ b/.changeset/five-hornets-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +The app will now reject any extensions that attach to nonexistent inputs. diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts index 6550d76608..70f70368d3 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts @@ -250,6 +250,49 @@ describe('createExtensionInstance', () => { ); }); + it('should refuse to create an instance with undeclared inputs', () => { + expect(() => + createExtensionInstance({ + attachments: new Map([ + [ + 'declared', + [ + createExtensionInstance({ + attachments: new Map(), + config: { output: 'many1' }, + extension: simpleExtension, + }), + ], + ], + [ + 'undeclared', + [ + createExtensionInstance({ + attachments: new Map(), + config: { output: 'many1' }, + extension: simpleExtension, + }), + ], + ], + ]), + config: undefined, + extension: createExtension({ + id: 'core.test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + declared: createExtensionInput({ + test: testDataRef, + }), + }, + output: {}, + factory() {}, + }), + }), + ).toThrow( + "Failed to instantiate extension 'core.test', received undeclared input(s) 'undeclared'", + ); + }); + it('should refuse to create an instance with multiple inputs for required singleton', () => { expect(() => createExtensionInstance({ diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index 9aec3078bc..ad3b858a60 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -60,6 +60,15 @@ function resolveInputs( inputMap: AnyExtensionInputMap, attachments: Map, ) { + const undeclaredAttachments = Array.from(attachments.keys()).filter( + name => inputMap[name] === undefined, + ); + if (undeclaredAttachments.length > 0) { + throw new Error( + `received undeclared input(s) '${undeclaredAttachments.join("', '")}'`, + ); + } + return mapValues(inputMap, (input, inputName) => { const attachedInstances = attachments.get(inputName) ?? []; if (input.config.singleton) { From 0472294b0c1a2c09f59b89f97da4b57859b3d4e2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Oct 2023 10:15:00 +0200 Subject: [PATCH 2/3] frontend-app-api: fixes to make sure all inputs are declared Signed-off-by: Patrik Oldsberg --- packages/frontend-app-api/src/extensions/Core.tsx | 3 +++ .../src/routing/extractRouteInfoFromInstanceTree.test.ts | 3 ++- packages/frontend-app-api/src/wiring/createApp.test.tsx | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 4a66afe38d..ec00059bf8 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -27,6 +27,9 @@ export const Core = createExtension({ apis: createExtensionInput({ api: coreExtensionData.apiFactory, }), + themes: createExtensionInput({ + theme: coreExtensionData.theme, + }), }, output: {}, factory() {}, diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts index a510adf1b7..b833bda1b5 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts @@ -48,7 +48,7 @@ function createTestExtension(options: { id: options.id, attachTo: options.parent ? { id: options.parent, input: 'children' } - : { id: 'core.routes', input: 'children' }, + : { id: 'core.routes', input: 'routes' }, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath.optional(), @@ -305,6 +305,7 @@ describe('discovery', () => { }), createTestExtension({ id: 'fooEmpty', + parent: 'foo', }), createTestExtension({ id: 'page3', diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index abd0fe9b00..600901f96e 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -16,6 +16,7 @@ import { createExtension, + createExtensionInput, createPageExtension, createPlugin, createThemeExtension, @@ -143,6 +144,9 @@ describe('createApp', () => { extension: createExtension({ id: 'root', attachTo: { id: '', input: '' }, + inputs: { + children: createExtensionInput({}), + }, output: {}, factory() {}, }), @@ -181,6 +185,9 @@ describe('createApp', () => { extension: createExtension({ id: 'root', attachTo: { id: '', input: '' }, + inputs: { + children: createExtensionInput({}), + }, output: {}, factory() {}, }), From ac0e281977dcc0abec9ef9f3e162aa4c5cd08f48 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Oct 2023 13:01:43 +0200 Subject: [PATCH 3/3] frontend-app-api: tweak error messaging to include more information about where undeclared inputs are from Signed-off-by: Patrik Oldsberg --- .../wiring/createExtensionInstance.test.ts | 45 ++++++++++++++++++- .../src/wiring/createExtensionInstance.ts | 15 +++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts index 70f70368d3..6e69e99cb7 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts @@ -289,7 +289,50 @@ describe('createExtensionInstance', () => { }), }), ).toThrow( - "Failed to instantiate extension 'core.test', received undeclared input(s) 'undeclared'", + "Failed to instantiate extension 'core.test', received undeclared input 'undeclared' from extension 'core.test'", + ); + }); + + it('should refuse to create an instance with multiple undeclared inputs', () => { + expect(() => + createExtensionInstance({ + attachments: new Map([ + [ + 'undeclared1', + [ + createExtensionInstance({ + attachments: new Map(), + config: { output: 'many1' }, + extension: simpleExtension, + }), + ], + ], + [ + 'undeclared2', + [ + createExtensionInstance({ + attachments: new Map(), + config: { output: 'many1' }, + extension: simpleExtension, + }), + createExtensionInstance({ + attachments: new Map(), + config: { output: 'many1' }, + extension: simpleExtension, + }), + ], + ], + ]), + config: undefined, + extension: createExtension({ + id: 'core.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'", ); }); diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index ad3b858a60..5a325dc0a0 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -60,12 +60,21 @@ function resolveInputs( inputMap: AnyExtensionInputMap, attachments: Map, ) { - const undeclaredAttachments = Array.from(attachments.keys()).filter( - name => inputMap[name] === undefined, + const undeclaredAttachments = Array.from(attachments.entries()).filter( + ([inputName]) => inputMap[inputName] === undefined, ); if (undeclaredAttachments.length > 0) { throw new Error( - `received undeclared input(s) '${undeclaredAttachments.join("', '")}'`, + `received undeclared input${ + undeclaredAttachments.length > 1 ? 's' : '' + } ${undeclaredAttachments + .map( + ([k, exts]) => + `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts + .map(e => e.id) + .join("', '")}'`, + ) + .join(' and ')}`, ); }