diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
index 463113fd55..49a2c9681c 100644
--- a/packages/app-next/app-config.yaml
+++ b/packages/app-next/app-config.yaml
@@ -1,7 +1,6 @@
app:
packages: 'all' # ✨
extensions:
- - core.router
- graphiql.page:
config:
path: /
diff --git a/packages/app-next/src/examples/graphiqlPlugin.tsx b/packages/app-next/src/examples/graphiqlPlugin.tsx
index d6c0d71fe7..2984b522dc 100644
--- a/packages/app-next/src/examples/graphiqlPlugin.tsx
+++ b/packages/app-next/src/examples/graphiqlPlugin.tsx
@@ -21,6 +21,7 @@ import {
import React from 'react';
export const GraphiqlPage = createPageExtension({
+ id: 'graphiql.page',
defaultPath: '/graphiql',
component: () =>
import('@backstage/plugin-graphiql').then(({ Router }) => ),
@@ -28,11 +29,5 @@ export const GraphiqlPage = createPageExtension({
export const graphiqlPlugin = createPlugin({
id: 'graphiql',
- defaultExtensionInstances: [
- {
- id: 'graphiql.page',
- at: 'core.router/routes',
- extension: GraphiqlPage,
- },
- ],
+ defaultExtensions: [GraphiqlPage],
});
diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx
index 031ad031bd..ee4130380f 100644
--- a/packages/frontend-app-api/src/createApp.tsx
+++ b/packages/frontend-app-api/src/createApp.tsx
@@ -17,7 +17,6 @@
import React from 'react';
import { ConfigReader } from '@backstage/config';
import {
- ExtensionInstanceParameters,
BackstagePlugin,
coreExtensionData,
} from '@backstage/frontend-plugin-api';
@@ -27,6 +26,7 @@ import {
ExtensionInstance,
} from './createExtensionInstance';
import {
+ ExtensionInstanceParameters,
mergeExtensionParameters,
readAppExtensionParameters,
} from './wiring/parameters';
@@ -37,21 +37,14 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
} {
const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
- const builtinExtensionInstanceParams = [
- {
- id: 'core.router',
- at: 'root/default',
- extension: RouteExtension,
- config: undefined,
- },
- ];
+ const builtinExtensions = [RouteExtension];
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
- const extensionInstanceParams = mergeExtensionParameters(
+ const extensionParams = mergeExtensionParameters(
[
- ...options.plugins.flatMap(plugin => plugin.defaultExtensionInstances),
- ...builtinExtensionInstanceParams,
+ ...options.plugins.flatMap(plugin => plugin.defaultExtensions),
+ ...builtinExtensions,
],
readAppExtensionParameters(appConfig),
);
@@ -64,7 +57,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
string,
Map
>();
- for (const instanceParams of extensionInstanceParams) {
+ for (const instanceParams of extensionParams) {
const [extensionId, pointId = 'default'] = instanceParams.at.split('/');
let pointMap = attachmentMap.get(extensionId);
@@ -87,24 +80,23 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
function createInstance(
instanceParams: ExtensionInstanceParameters,
): ExtensionInstance {
- const existingInstance = instances.get(instanceParams.id);
+ const existingInstance = instances.get(instanceParams.extension.id);
if (existingInstance) {
return existingInstance;
}
const attachments = Object.fromEntries(
- Array.from(attachmentMap.get(instanceParams.id)?.entries() ?? []).map(
- ([inputName, attachmentConfigs]) => [
- inputName,
- attachmentConfigs.map(createInstance),
- ],
- ),
+ Array.from(
+ attachmentMap.get(instanceParams.extension.id)?.entries() ?? [],
+ ).map(([inputName, attachmentConfigs]) => [
+ inputName,
+ attachmentConfigs.map(createInstance),
+ ]),
);
return createExtensionInstance({
- id: instanceParams.id,
- config: instanceParams.config,
extension: instanceParams.extension,
+ config: instanceParams.config,
attachments,
});
}
diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts
index de9a41f49f..633baf6919 100644
--- a/packages/frontend-app-api/src/createExtensionInstance.ts
+++ b/packages/frontend-app-api/src/createExtensionInstance.ts
@@ -26,12 +26,11 @@ export interface ExtensionInstance {
/** @internal */
export function createExtensionInstance(options: {
- id: string;
extension: Extension;
config: unknown;
attachments: Record;
}): ExtensionInstance {
- const { id, extension, config, attachments } = options;
+ const { extension, config, attachments } = options;
const extensionData = new Map();
let parsedConfig: unknown;
@@ -39,7 +38,7 @@ export function createExtensionInstance(options: {
parsedConfig = extension.configSchema?.parse(config ?? {});
} catch (e) {
throw new Error(
- `Invalid configuration for extension instance '${id}', ${e}`,
+ `Invalid configuration for extension instance '${extension.id}', ${e}`,
);
}
@@ -60,11 +59,13 @@ export function createExtensionInstance(options: {
),
});
} catch (e) {
- throw new Error(`Failed to instantiate extension instance '${id}', ${e}`);
+ throw new Error(
+ `Failed to instantiate extension instance '${extension.id}', ${e}`,
+ );
}
return {
- id: options.id,
+ id: options.extension.id,
data: extensionData,
$$type: 'extension-instance',
};
diff --git a/packages/frontend-app-api/src/extensions/RouteExtension.tsx b/packages/frontend-app-api/src/extensions/RouteExtension.tsx
index ef7e34ea30..dfa5874d70 100644
--- a/packages/frontend-app-api/src/extensions/RouteExtension.tsx
+++ b/packages/frontend-app-api/src/extensions/RouteExtension.tsx
@@ -22,6 +22,8 @@ import {
import { BrowserRouter, useRoutes } from 'react-router-dom';
export const RouteExtension = createExtension({
+ id: 'core.router',
+ at: 'root',
inputs: {
routes: {
extensionData: {
diff --git a/packages/frontend-app-api/src/wiring/parameters.test.ts b/packages/frontend-app-api/src/wiring/parameters.test.ts
index b47109ed67..f4d31b70c9 100644
--- a/packages/frontend-app-api/src/wiring/parameters.test.ts
+++ b/packages/frontend-app-api/src/wiring/parameters.test.ts
@@ -26,10 +26,9 @@ import {
function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') {
return {
id,
- at: 'foo/bar',
- extension: {} as Extension,
+ at: 'root',
disabled: status === 'disabled',
- };
+ } as Extension;
}
describe('mergeExtensionParameters', () => {
@@ -40,30 +39,20 @@ describe('mergeExtensionParameters', () => {
});
it('should pass through extension instances', () => {
- expect(mergeExtensionParameters([makeExt('a'), makeExt('b')], [])).toEqual([
- makeExt('a'),
- makeExt('b'),
+ const a = makeExt('a');
+ const b = makeExt('b');
+ expect(mergeExtensionParameters([a, b], [])).toEqual([
+ { extension: a, at: 'root' },
+ { extension: b, at: 'root' },
]);
});
- it('should override extension instances', () => {
- expect(
- mergeExtensionParameters(
- [makeExt('a'), makeExt('b')],
- [
- {
- id: 'b',
- extension: { ext: 'other' } as unknown as Extension,
- },
- ],
- ),
- ).toEqual([makeExt('a'), { ...makeExt('b'), extension: { ext: 'other' } }]);
- });
-
it('should override attachment points', () => {
+ const a = makeExt('a');
+ const b = makeExt('b');
expect(
mergeExtensionParameters(
- [makeExt('a'), makeExt('b')],
+ [a, b],
[
{
id: 'b',
@@ -71,17 +60,27 @@ describe('mergeExtensionParameters', () => {
},
],
),
- ).toEqual([makeExt('a'), { ...makeExt('b'), at: 'derp' }]);
+ ).toEqual([
+ { extension: a, at: 'root' },
+ { extension: b, at: 'derp' },
+ ]);
});
- it('should fully override configuration', () => {
+ it('should fully override configuration and duplicate', () => {
+ const a = makeExt('a');
+ const b = makeExt('b');
expect(
mergeExtensionParameters(
+ [a, b],
[
- { ...makeExt('a'), config: { foo: { bar: 1 } } },
- { ...makeExt('b'), config: { foo: { bar: 2 } } },
- ],
- [
+ {
+ id: 'a',
+ config: { foo: { bar: 1 } },
+ },
+ {
+ id: 'b',
+ config: { foo: { bar: 2 } },
+ },
{
id: 'b',
config: { foo: { qux: 3 } },
@@ -89,15 +88,17 @@ describe('mergeExtensionParameters', () => {
],
),
).toEqual([
- { ...makeExt('a'), config: { foo: { bar: 1 } } },
- { ...makeExt('b'), config: { foo: { qux: 3 } } },
+ { extension: a, at: 'root', config: { foo: { bar: 1 } } },
+ { extension: b, at: 'root', config: { foo: { qux: 3 } } },
]);
});
it('should place enabled instances in the order that they were enabled', () => {
+ const a = makeExt('a', 'disabled');
+ const b = makeExt('b', 'disabled');
expect(
mergeExtensionParameters(
- [makeExt('a', 'disabled'), makeExt('b', 'disabled')],
+ [a, b],
[
{
id: 'b',
@@ -109,7 +110,10 @@ describe('mergeExtensionParameters', () => {
},
],
),
- ).toEqual([makeExt('b'), makeExt('a')]);
+ ).toEqual([
+ { extension: b, at: 'root' },
+ { extension: a, at: 'root' },
+ ]);
});
});
@@ -149,6 +153,7 @@ describe('readAppExtensionParameters', () => {
).toEqual([
{
id: 'core.router',
+ disabled: false,
},
]);
expect(
@@ -175,26 +180,22 @@ describe('readAppExtensionParameters', () => {
]);
});
- it('should support extension implementation shorthand', () => {
- expect(
+ it('should not allow string keys', () => {
+ expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
- extensions: [{ 'core.router': 'example-package#CustomRouter' }],
+ extensions: [{ 'core.router': 'some-string' }],
},
}),
- ref => ({ ref } as unknown as Extension),
),
- ).toEqual([
- {
- id: 'core.router',
- extension: { ref: 'example-package#CustomRouter' },
- },
- ]);
+ ).toThrow(
+ 'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object',
+ );
});
- it('should support attachment shorthand', () => {
- expect(
+ it('should not allow invalid keys', () => {
+ expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
@@ -208,70 +209,16 @@ describe('readAppExtensionParameters', () => {
],
},
}),
- ref => ({ ref } as unknown as Extension),
- ),
- ).toEqual([
- {
- id: 'generated.1',
- at: 'core.router/routes',
- extension: { ref: 'example-package#MyPage' },
- config: { foo: 'bar' },
- },
- ]);
- });
-
- it('should support attachment with extension shorthand', () => {
- expect(
- readAppExtensionParameters(
- new ConfigReader({
- app: {
- extensions: [{ 'core.router/routes': 'example-package#MyPage' }],
- },
- }),
- ref => ({ ref } as unknown as Extension),
- ),
- ).toEqual([
- {
- id: 'generated.1',
- at: 'core.router/routes',
- extension: { ref: 'example-package#MyPage' },
- },
- ]);
- });
-
- it('should reject attachment shorthand with explicit attachment', () => {
- expect(() =>
- readAppExtensionParameters(
- new ConfigReader({
- app: {
- extensions: [
- {
- 'core.router/routes': {
- at: 'other/input',
- },
- },
- ],
- },
- }),
),
).toThrow(
- `Invalid extension configuration at app.extensions[0][core.router/routes], must not redundantly specify 'at' when the extension input ID form of the key is used (with a slash); the 'at' is already implicitly 'core.router/routes'`,
+ 'Invalid extension configuration at app.extensions[0], key must only contain letters, numbers and dots, got core.router/routes',
);
});
});
describe('expandShorthandExtensionParameters', () => {
- const resolveExtensionRef = jest.fn(
- ref => ({ ref } as unknown as Extension),
- );
- const generateExtensionId = jest.fn(() => 'generated.1');
const run = (value: JsonValue) => {
- return expandShorthandExtensionParameters(
- value,
- 1,
- resolveExtensionRef,
- generateExtensionId,
- );
+ return expandShorthandExtensionParameters(value, 1);
};
it('rejects unknown keys', () => {
@@ -297,19 +244,20 @@ describe('expandShorthandExtensionParameters', () => {
it('rejects unknown values', () => {
expect(() => run({ a: null })).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][a], value must be a boolean, string, or object"`,
+ `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
expect(() => run({ a: 1 })).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][a], value must be a boolean, string, or object"`,
+ `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
expect(() => run({ a: [] })).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][a], value must be a boolean, string, or object"`,
+ `"Invalid extension configuration at app.extensions[1][a], value must be a boolean or object"`,
);
});
it('supports string key', () => {
expect(run('core.router')).toEqual({
id: 'core.router',
+ disabled: false,
});
expect(() => run('')).toThrowErrorMatchingInlineSnapshot(
`"Invalid extension configuration at app.extensions[1], string shorthand cannot be the empty string"`,
@@ -328,40 +276,21 @@ describe('expandShorthandExtensionParameters', () => {
id: 'core.router',
disabled: true,
});
- expect(() =>
- run({ 'core.router/routes': false }),
- ).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router/routes], cannot target an extension instance input (key cannot contain slashes; did you mean 'core.router'?)"`,
- );
});
- it('supports string values', () => {
- expect(run({ 'core.router': 'example-package#MyRouter' })).toEqual({
- id: 'core.router',
- extension: { ref: 'example-package#MyRouter' },
- });
- expect(run({ 'core.router/routes': 'example-package#MyRouter' })).toEqual({
- id: 'generated.1',
- at: 'core.router/routes',
- extension: { ref: 'example-package#MyRouter' },
- });
+ it('should not support string values', () => {
+ expect(() =>
+ run({ 'core.router': 'example-package#MyRouter' }),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"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/routes': { id: 'some.id' } }),
+ run({ 'core.router': { id: 'some.id' } }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router/routes].id, must not specify 'id' when the extension input ID form of the key is used (with a slash); please replace the key 'core.router/routes' with the id instead, and put that key in the 'at' field"`,
- );
- expect(() =>
- run({
- 'core.router/routes': {
- id: 'example-package#MyRouter',
- at: 'core.router/routes',
- },
- }),
- ).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router/routes], must not redundantly specify 'at' when the extension input ID form of the key is used (with a slash); the 'at' is already implicitly 'core.router/routes'"`,
+ `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`,
);
});
@@ -377,7 +306,7 @@ describe('expandShorthandExtensionParameters', () => {
},
}),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router], must not redundantly specify 'id' when the extension instance ID form of the key is used (without a slash); the 'id' is already implicitly 'core.router'"`,
+ `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`,
);
});
@@ -397,21 +326,6 @@ describe('expandShorthandExtensionParameters', () => {
);
});
- it('supports object extension', () => {
- expect(
- run({ 'core.router/routes': { extension: 'example-package#MyRouter' } }),
- ).toEqual({
- id: 'generated.1',
- at: 'core.router/routes',
- extension: { ref: 'example-package#MyRouter' },
- });
- expect(() =>
- run({ 'core.router/routes': { extension: 0 } }),
- ).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router/routes].extension, must be a string"`,
- );
- });
-
it('supports object config', () => {
expect(
run({ 'core.router': { config: { disableRedirects: true } } }),
@@ -430,7 +344,7 @@ describe('expandShorthandExtensionParameters', () => {
expect(() =>
run({ 'core.router': { foo: { settings: true } } }),
).toThrowErrorMatchingInlineSnapshot(
- `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'at', 'disabled', 'extension', 'config'"`,
+ `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'at', 'disabled', 'config'"`,
);
});
});
diff --git a/packages/frontend-app-api/src/wiring/parameters.ts b/packages/frontend-app-api/src/wiring/parameters.ts
index ba04d6c9ee..d749808a24 100644
--- a/packages/frontend-app-api/src/wiring/parameters.ts
+++ b/packages/frontend-app-api/src/wiring/parameters.ts
@@ -15,19 +15,17 @@
*/
import { Config } from '@backstage/config';
-import {
- Extension,
- ExtensionInstanceParameters,
-} from '@backstage/frontend-plugin-api';
+import { Extension } from '@backstage/frontend-plugin-api';
import { JsonValue } from '@backstage/types';
-import omitBy from 'lodash/omitBy';
-const knownExtensionInstanceParameters = [
- 'at',
- 'disabled',
- 'extension',
- 'config',
-];
+export interface ExtensionParameters {
+ id: string;
+ at?: string;
+ disabled?: boolean;
+ config?: unknown;
+}
+
+const knownExtensionParameters = ['at', 'disabled', 'config'];
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
@@ -35,10 +33,7 @@ const knownExtensionInstanceParameters = [
/** @internal */
export function readAppExtensionParameters(
rootConfig: Config,
- resolveExtensionRef: (ref: string) => Extension = () => {
- throw new Error('Not Implemented');
- },
-): Partial[] {
+): ExtensionParameters[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
if (arr === undefined) {
@@ -49,18 +44,8 @@ export function readAppExtensionParameters(
return [];
}
- const generateExtensionId = (() => {
- let index = 1;
- return () => `generated.${index++}`;
- })();
-
return arr.map((arrayEntry, arrayIndex) =>
- expandShorthandExtensionParameters(
- arrayEntry,
- arrayIndex,
- resolveExtensionRef,
- generateExtensionId,
- ),
+ expandShorthandExtensionParameters(arrayEntry, arrayIndex),
);
}
@@ -68,9 +53,7 @@ export function readAppExtensionParameters(
export function expandShorthandExtensionParameters(
arrayEntry: JsonValue,
arrayIndex: number,
- resolveExtensionRef: (ref: string) => Extension,
- generateExtensionId: () => string,
-): Partial {
+): ExtensionParameters {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${arrayIndex}]${
key ? `[${key}]` : ''
@@ -93,6 +76,7 @@ export function expandShorthandExtensionParameters(
}
return {
id: arrayEntry,
+ disabled: false,
};
}
@@ -110,111 +94,43 @@ export function expandShorthandExtensionParameters(
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
- const key = keys[0];
+ const key = String(keys[0]);
const value = arrayEntry[key];
+ if (!key.match(/^[\.a-zA-Z0-9]+$/)) {
+ throw new Error(
+ errorMsg(`key must only contain letters, numbers and dots, got ${key}`),
+ );
+ }
// Example YAML:
// - catalog.page.cicd: false
if (typeof value === 'boolean') {
- if (key.includes('/')) {
- const suggestion = key.split('/')[0];
- throw new Error(
- errorMsg(
- `cannot target an extension instance input (key cannot contain slashes; did you mean '${suggestion}'?)`,
- key,
- ),
- );
- }
-
return {
id: key,
disabled: !value,
};
}
- // Example YAML:
- // - core.router/routes: '@backstage/plugin-some-plugin#MyPage'
- // - some-plugin.page: '@internal/frontend-customizations#MyModifiedPage'
- if (typeof value === 'string') {
- if (key.includes('/')) {
- return {
- id: generateExtensionId(),
- at: key,
- extension: resolveExtensionRef(value),
- };
- }
-
- return {
- id: key,
- extension: resolveExtensionRef(value),
- };
- }
-
// The remaining case is the generic object. Example YAML:
- // - core.router/routes:
- // extension: '@backstage/core-app-api#Redirect'
- // config:
- // path: /
- // to: /catalog
// - tech-radar.page:
// at: core.router/routes
- // extension: '@backstage/plugin-tech-radar#TechRadarPage'
+ // disabled: false
// config:
// path: /tech-radar
// width: 1500
// height: 800
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
- throw new Error(
- errorMsg('value must be a boolean, string, or object', key),
- );
+ throw new Error(errorMsg('value must be a boolean or object', key));
}
- let id = value.id;
- let at = value.at;
+ const at = value.at;
const disabled = value.disabled;
- const extensionRef = value.extension;
const config = value.config;
- if (key.includes('/')) {
- if (at !== undefined) {
- throw new Error(
- errorMsg(
- `must not redundantly specify 'at' when the extension input ID form of the key is used (with a slash); the 'at' is already implicitly '${key}'`,
- key,
- ),
- );
- }
- if (id !== undefined) {
- throw new Error(
- errorMsg(
- `must not specify 'id' when the extension input ID form of the key is used (with a slash); please replace the key '${key}' with the id instead, and put that key in the 'at' field`,
- key,
- 'id',
- ),
- );
- }
- id = generateExtensionId();
- at = key;
- } else {
- if (id !== undefined) {
- throw new Error(
- errorMsg(
- `must not redundantly specify 'id' when the extension instance ID form of the key is used (without a slash); the 'id' is already implicitly '${key}'`,
- key,
- ),
- );
- }
- id = key;
- }
-
- if (id !== undefined && typeof id !== 'string') {
- throw new Error(errorMsg('must be a string', key, 'id'));
- } else if (at !== undefined && typeof at !== 'string') {
+ if (at !== undefined && typeof at !== 'string') {
throw new Error(errorMsg('must be a string', key, 'at'));
} else if (disabled !== undefined && typeof disabled !== 'boolean') {
throw new Error(errorMsg('must be a boolean', key, 'disabled'));
- } else if (extensionRef !== undefined && typeof extensionRef !== 'string') {
- throw new Error(errorMsg('must be a string', key, 'extension'));
} else if (
config !== undefined &&
(typeof config !== 'object' || config === null || Array.isArray(config))
@@ -223,12 +139,12 @@ export function expandShorthandExtensionParameters(
}
const unknownKeys = Object.keys(value).filter(
- k => !knownExtensionInstanceParameters.includes(k),
+ k => !knownExtensionParameters.includes(k),
);
if (unknownKeys.length > 0) {
throw new Error(
errorMsg(
- `unknown parameter; expected one of '${knownExtensionInstanceParameters.join(
+ `unknown parameter; expected one of '${knownExtensionParameters.join(
"', '",
)}'`,
key,
@@ -237,63 +153,67 @@ export function expandShorthandExtensionParameters(
);
}
- const extension: Extension | undefined = extensionRef
- ? resolveExtensionRef(extensionRef)
- : undefined;
+ return {
+ id: key,
+ at,
+ disabled,
+ config,
+ };
+}
- return omitBy(
- {
- id,
- at,
- disabled,
- extension,
- config,
- },
- v => v === undefined,
- );
+export interface ExtensionInstanceParameters {
+ extension: Extension;
+ at: string;
+ config?: unknown;
}
/** @internal */
export function mergeExtensionParameters(
- base: ExtensionInstanceParameters[],
- overrides: Array>,
+ base: Extension[],
+ parameters: Array,
): ExtensionInstanceParameters[] {
- const extensionInstanceParams = base.slice();
+ const overrides = base.map(extension => ({
+ extension,
+ params: {
+ at: extension.at,
+ disabled: extension.disabled,
+ config: undefined as unknown,
+ },
+ }));
- for (const overrideParam of overrides) {
- const existingParamIndex = extensionInstanceParams.findIndex(
- e => e.id === overrideParam.id,
+ for (const overrideParam of parameters) {
+ const existingIndex = overrides.findIndex(
+ e => e.extension.id === overrideParam.id,
);
- if (existingParamIndex !== -1) {
- const existingParam = extensionInstanceParams[existingParamIndex];
+ if (existingIndex !== -1) {
+ const existing = overrides[existingIndex];
if (overrideParam.at) {
- existingParam.at = overrideParam.at;
- }
- if (overrideParam.extension) {
- // TODO: do we want to reset config here? it might be completely
- // unrelated to the previous one
- existingParam.extension = overrideParam.extension;
+ existing.params.at = overrideParam.at;
}
if (overrideParam.config) {
// TODO: merge config?
- existingParam.config = overrideParam.config;
+ existing.params.config = overrideParam.config;
}
- if (Boolean(existingParam.disabled) !== Boolean(overrideParam.disabled)) {
- existingParam.disabled = Boolean(overrideParam.disabled);
- if (!existingParam.disabled) {
+ if (
+ Boolean(existing.params.disabled) !== Boolean(overrideParam.disabled)
+ ) {
+ existing.params.disabled = Boolean(overrideParam.disabled);
+ if (!existing.params.disabled) {
// bump
- extensionInstanceParams.splice(existingParamIndex, 1);
- extensionInstanceParams.push(existingParam);
+ overrides.splice(existingIndex, 1);
+ overrides.push(existing);
}
}
- } else if (overrideParam.id) {
- const { id, at, extension, config } = overrideParam;
- if (!at || !extension) {
- throw new Error(`Extension ${overrideParam.id} is incomplete`);
- }
- extensionInstanceParams.push({ id, at, extension, config });
+ } else {
+ throw new Error(`Extension ${overrideParam.id} does not exist`);
}
}
- return extensionInstanceParams.filter(param => !param.disabled);
+ return overrides
+ .filter(override => !override.params.disabled)
+ .map(param => ({
+ extension: param.extension,
+ at: param.params.at,
+ config: param.params.config,
+ }));
}
diff --git a/packages/frontend-plugin-api/src/createPageExtension.tsx b/packages/frontend-plugin-api/src/createPageExtension.tsx
index 78674282a3..de2ca9f343 100644
--- a/packages/frontend-plugin-api/src/createPageExtension.tsx
+++ b/packages/frontend-plugin-api/src/createPageExtension.tsx
@@ -41,6 +41,9 @@ export function createPageExtension<
configSchema: PortableSchema;
}
) & {
+ id: string;
+ at?: string;
+ disabled?: boolean;
inputs?: TInputs;
component: (props: {
config: TConfig;
@@ -60,6 +63,9 @@ export function createPageExtension<
) as PortableSchema);
return createExtension({
+ id: options.id,
+ at: options.at ?? 'core.router/routes',
+ disabled: options.disabled,
output: {
component: coreExtensionData.reactComponent,
path: coreExtensionData.routePath,
diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts
index 46ff503121..df30cc813d 100644
--- a/packages/frontend-plugin-api/src/index.ts
+++ b/packages/frontend-plugin-api/src/index.ts
@@ -37,5 +37,4 @@ export {
type ExtensionDataBind,
type ExtensionDataRef,
type ExtensionDataValue,
- type ExtensionInstanceParameters,
} from './types';
diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts
index 8822cf044d..d634dc3c5d 100644
--- a/packages/frontend-plugin-api/src/types.ts
+++ b/packages/frontend-plugin-api/src/types.ts
@@ -54,6 +54,9 @@ export interface CreateExtensionOptions<
TPoint extends Record,
TConfig,
> {
+ id: string;
+ at: string;
+ disabled?: boolean;
inputs?: TPoint;
output: TData;
configSchema?: PortableSchema;
@@ -71,7 +74,9 @@ export interface CreateExtensionOptions<
/** @public */
export interface Extension {
$$type: 'extension';
- // TODO: will extensions have a default "at" as part of their contract, making it optional in the instance config?
+ id: string;
+ at: string;
+ disabled: boolean;
inputs: Record;
output: AnyExtensionDataMap;
configSchema?: PortableSchema;
@@ -88,29 +93,25 @@ export function createExtension<
TPoint extends Record,
TConfig = never,
>(options: CreateExtensionOptions): Extension {
- return { ...options, $$type: 'extension', inputs: options.inputs ?? {} };
-}
-
-/** @public */
-export interface ExtensionInstanceParameters {
- id: string;
- at: string;
- extension: Extension;
- config?: unknown;
- disabled?: boolean;
+ return {
+ ...options,
+ disabled: options.disabled ?? false,
+ $$type: 'extension',
+ inputs: options.inputs ?? {},
+ };
}
/** @public */
export interface BackstagePluginOptions {
id: string;
- defaultExtensionInstances?: ExtensionInstanceParameters[];
+ defaultExtensions?: Extension[];
}
/** @public */
export interface BackstagePlugin {
$$type: 'backstage-plugin';
id: string;
- defaultExtensionInstances: ExtensionInstanceParameters[];
+ defaultExtensions: Extension[];
}
/** @public */
@@ -118,6 +119,6 @@ export function createPlugin(options: BackstagePluginOptions): BackstagePlugin {
return {
...options,
$$type: 'backstage-plugin',
- defaultExtensionInstances: options.defaultExtensionInstances ?? [],
+ defaultExtensions: options.defaultExtensions ?? [],
};
}