Merge pull request #25981 from backstage/mob/nfs/plugin-overrides
NFS: Support `plugin.withOverrides` to override extensions from existing plugins
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Support overriding of plugin extensions using the new `plugin.withOverrides` method.
|
||||
|
||||
```tsx
|
||||
import homePlugin from '@backstage/plugin-home';
|
||||
|
||||
export default homePlugin.withOverrides({
|
||||
extensions: [
|
||||
homePage.getExtension('page:home').override({
|
||||
*factory(originalFactory) {
|
||||
yield* originalFactory();
|
||||
yield coreExtensionData.reactElement(<h1>My custom home page</h1>);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -356,6 +356,10 @@ export interface BackstagePlugin<
|
||||
readonly id: string;
|
||||
// (undocumented)
|
||||
readonly routes: TRoutes;
|
||||
// (undocumented)
|
||||
withOverrides(options: {
|
||||
extensions: Array<ExtensionDefinition<any, any>>;
|
||||
}): BackstagePlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
}
|
||||
|
||||
export { BackstageUserIdentity };
|
||||
|
||||
@@ -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,68 @@ describe('createPlugin', () => {
|
||||
}),
|
||||
).toThrow("Plugin 'test' provided duplicate extensions: test/2, test/3");
|
||||
});
|
||||
|
||||
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],
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TRoutes, TExternalRoutes>;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ export interface BackstagePlugin<
|
||||
readonly routes: TRoutes;
|
||||
readonly externalRoutes: TExternalRoutes;
|
||||
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
|
||||
withOverrides(options: {
|
||||
extensions: Array<ExtensionDefinition<any, any>>;
|
||||
}): BackstagePlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
Reference in New Issue
Block a user