Remove the concept of extension instances on the surface

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2023-08-30 11:59:37 +02:00
parent bb126d2626
commit c93e9d5bf7
10 changed files with 174 additions and 345 deletions
-1
View File
@@ -1,7 +1,6 @@
app:
packages: 'all' # ✨
extensions:
- core.router
- graphiql.page:
config:
path: /
@@ -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 }) => <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],
});
+14 -22
View File
@@ -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<string, ExtensionInstanceParameters[]>
>();
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,
});
}
@@ -26,12 +26,11 @@ export interface ExtensionInstance {
/** @internal */
export function createExtensionInstance(options: {
id: string;
extension: Extension<unknown>;
config: unknown;
attachments: Record<string, ExtensionInstance[]>;
}): ExtensionInstance {
const { id, extension, config, attachments } = options;
const { extension, config, attachments } = options;
const extensionData = new Map<string, unknown>();
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',
};
@@ -22,6 +22,8 @@ import {
import { BrowserRouter, useRoutes } from 'react-router-dom';
export const RouteExtension = createExtension({
id: 'core.router',
at: 'root',
inputs: {
routes: {
extensionData: {
@@ -26,10 +26,9 @@ import {
function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') {
return {
id,
at: 'foo/bar',
extension: {} as Extension<unknown>,
at: 'root',
disabled: status === 'disabled',
};
} as Extension<unknown>;
}
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<unknown>,
},
],
),
).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<unknown>),
),
).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<unknown>),
),
).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<unknown>),
),
).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<unknown>),
);
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'"`,
);
});
});
@@ -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<unknown> = () => {
throw new Error('Not Implemented');
},
): Partial<ExtensionInstanceParameters>[] {
): 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<unknown>,
generateExtensionId: () => string,
): Partial<ExtensionInstanceParameters> {
): 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<unknown> | 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<unknown>;
at: string;
config?: unknown;
}
/** @internal */
export function mergeExtensionParameters(
base: ExtensionInstanceParameters[],
overrides: Array<Partial<ExtensionInstanceParameters>>,
base: Extension<unknown>[],
parameters: Array<ExtensionParameters>,
): 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,
}));
}
@@ -41,6 +41,9 @@ export function createPageExtension<
configSchema: PortableSchema<TConfig>;
}
) & {
id: string;
at?: string;
disabled?: boolean;
inputs?: TInputs;
component: (props: {
config: TConfig;
@@ -60,6 +63,9 @@ export function createPageExtension<
) as PortableSchema<TConfig>);
return createExtension({
id: options.id,
at: options.at ?? 'core.router/routes',
disabled: options.disabled,
output: {
component: coreExtensionData.reactComponent,
path: coreExtensionData.routePath,
@@ -37,5 +37,4 @@ export {
type ExtensionDataBind,
type ExtensionDataRef,
type ExtensionDataValue,
type ExtensionInstanceParameters,
} from './types';
+15 -14
View File
@@ -54,6 +54,9 @@ export interface CreateExtensionOptions<
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig,
> {
id: string;
at: string;
disabled?: boolean;
inputs?: TPoint;
output: TData;
configSchema?: PortableSchema<TConfig>;
@@ -71,7 +74,9 @@ export interface CreateExtensionOptions<
/** @public */
export interface Extension<TConfig> {
$$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<string, { extensionData: AnyExtensionDataMap }>;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
@@ -88,29 +93,25 @@ export function createExtension<
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig> {
return { ...options, $$type: 'extension', inputs: options.inputs ?? {} };
}
/** @public */
export interface ExtensionInstanceParameters {
id: string;
at: string;
extension: Extension<unknown>;
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<unknown>[];
}
/** @public */
export interface BackstagePlugin {
$$type: 'backstage-plugin';
id: string;
defaultExtensionInstances: ExtensionInstanceParameters[];
defaultExtensions: Extension<unknown>[];
}
/** @public */
@@ -118,6 +119,6 @@ export function createPlugin(options: BackstagePluginOptions): BackstagePlugin {
return {
...options,
$$type: 'backstage-plugin',
defaultExtensionInstances: options.defaultExtensionInstances ?? [],
defaultExtensions: options.defaultExtensions ?? [],
};
}