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 ?? [], }; }