Merge pull request #20494 from backstage/rugvip/undeclared
frontend-app-api: reject attachments to undeclared inputs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
The app will now reject any extensions that attach to nonexistent inputs.
|
||||
@@ -27,6 +27,9 @@ export const Core = createExtension({
|
||||
apis: createExtensionInput({
|
||||
api: coreExtensionData.apiFactory,
|
||||
}),
|
||||
themes: createExtensionInput({
|
||||
theme: coreExtensionData.theme,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {},
|
||||
}),
|
||||
|
||||
@@ -250,6 +250,92 @@ 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 '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'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs for required singleton', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
|
||||
@@ -60,6 +60,24 @@ function resolveInputs(
|
||||
inputMap: AnyExtensionInputMap,
|
||||
attachments: Map<string, ExtensionInstance[]>,
|
||||
) {
|
||||
const undeclaredAttachments = Array.from(attachments.entries()).filter(
|
||||
([inputName]) => inputMap[inputName] === undefined,
|
||||
);
|
||||
if (undeclaredAttachments.length > 0) {
|
||||
throw new Error(
|
||||
`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 ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedInstances = attachments.get(inputName) ?? [];
|
||||
if (input.config.singleton) {
|
||||
|
||||
Reference in New Issue
Block a user