diff --git a/packages/frontend-app-api/src/createExtensionInstance.ts b/packages/frontend-app-api/src/createExtensionInstance.ts index a7c49d48cb..d767f21d58 100644 --- a/packages/frontend-app-api/src/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/createExtensionInstance.ts @@ -54,9 +54,17 @@ export function createExtensionInstance(options: { extension.factory({ source, config: parsedConfig, - bind: mapValues(extension.output, ref => { - return (value: unknown) => extensionData.set(ref.id, value); - }), + bind: namedOutputs => { + for (const [name, output] of Object.entries(namedOutputs)) { + const ref = extension.output[name]; + if (!ref) { + throw new Error( + `Extension instance '${extension.id}' tried to bind unknown output '${name}'`, + ); + } + extensionData.set(ref.id, output); + } + }, inputs: mapValues( extension.inputs, ({ extensionData: pointData }, inputName) => { diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx index f2a760d25e..05f84aba6d 100644 --- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx @@ -47,10 +47,12 @@ export const CoreRouter = createExtension({ return element; }; - bind.component(() => ( - - - - )); + bind({ + component: () => ( + + + + ), + }); }, }); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index a27875bcf8..2959dcb92e 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -17,7 +17,9 @@ import { ZodSchema } from 'zod'; import { ZodTypeDef } from 'zod'; // @public (undocumented) -export type AnyExtensionDataMap = Record>; +export type AnyExtensionDataMap = { + [name in string]: ExtensionDataRef; +}; // @public (undocumented) export interface BackstagePlugin { @@ -29,12 +31,28 @@ export interface BackstagePlugin { id: string; } +// @public (undocumented) +export interface ConfigurableExtensionDataRef< + TData, + TConfig extends { + optional?: true; + } = {}, +> extends ExtensionDataRef { + // (undocumented) + optional(): ConfigurableExtensionDataRef< + TData, + TData & { + optional: true; + } + >; +} + // @public (undocumented) export const coreExtensionData: { - reactComponent: ExtensionDataRef>; - routePath: ExtensionDataRef; - apiFactory: ExtensionDataRef; - routeRef: ExtensionDataRef>; + reactComponent: ConfigurableExtensionDataRef, {}>; + routePath: ConfigurableExtensionDataRef; + apiFactory: ConfigurableExtensionDataRef; + routeRef: ConfigurableExtensionDataRef, {}>; }; // @public (undocumented) @@ -52,11 +70,7 @@ export function createApiExtension< api: AnyApiRef; factory: (options: { config: TConfig; - inputs: { - [pointName in keyof TInputs]: ExtensionDataValue< - TInputs[pointName]['extensionData'] - >[]; - }; + inputs: ExtensionDataInputValues; }) => AnyApiFactory; } | { @@ -70,20 +84,27 @@ export function createApiExtension< // @public (undocumented) export function createExtension< - TData extends AnyExtensionDataMap, - TPoint extends Record< + TOutput extends AnyExtensionDataMap, + TInputs extends Record< string, { extensionData: AnyExtensionDataMap; } >, TConfig = never, ->(options: CreateExtensionOptions): Extension; +>( + options: CreateExtensionOptions, +): Extension; + +// @public (undocumented) +export function createExtensionDataRef( + id: string, +): ConfigurableExtensionDataRef; // @public (undocumented) export interface CreateExtensionOptions< - TData extends AnyExtensionDataMap, - TPoint extends Record< + TOutput extends AnyExtensionDataMap, + TInputs extends Record< string, { extensionData: AnyExtensionDataMap; @@ -100,20 +121,16 @@ export interface CreateExtensionOptions< // (undocumented) factory(options: { source?: BackstagePlugin; - bind: ExtensionDataBind; + bind: ExtensionDataBind; config: TConfig; - inputs: { - [pointName in keyof TPoint]: ExtensionDataValue< - TPoint[pointName]['extensionData'] - >[]; - }; + inputs: ExtensionDataInputValues; }): void; // (undocumented) id: string; // (undocumented) - inputs?: TPoint; + inputs?: TInputs; // (undocumented) - output: TData; + output: TOutput; } // @public @@ -143,11 +160,7 @@ export function createPageExtension< routeRef?: RouteRef; component: (props: { config: TConfig; - inputs: { - [pointName in keyof TInputs]: ExtensionDataValue< - TInputs[pointName]['extensionData'] - >[]; - }; + inputs: ExtensionDataInputValues; }) => Promise; }, ): Extension; @@ -204,22 +217,60 @@ export interface ExtensionBoundaryProps { } // @public (undocumented) -export type ExtensionDataBind = { - [K in keyof TData]: (value: TData[K]['T']) => void; +export type ExtensionDataBind = ( + values: { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TMap[DataName]['T']; + } & { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TMap[DataName]['T']; + }, +) => void; + +// @public (undocumented) +export type ExtensionDataInputValues< + TInputs extends { + [name in string]: { + extensionData: AnyExtensionDataMap; + }; + }, +> = { + [InputName in keyof TInputs]: Array< + { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TInputs[InputName]['extensionData'][DataName]['T']; + } & { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TInputs[InputName]['extensionData'][DataName]['T']; + } + >; }; // @public (undocumented) -export type ExtensionDataRef = { +export type ExtensionDataRef< + TData, + TConfig extends { + optional?: true; + } = {}, +> = { id: string; - T: T; + T: TData; + config: TConfig; $$type: 'extension-data'; }; -// @public (undocumented) -export type ExtensionDataValue = { - [K in keyof TData]: TData[K]['T']; -}; - // @public (undocumented) export interface PluginOptions { // (undocumented) diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index a3662e5ef1..fadb814d4f 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -38,10 +38,11 @@ describe('createApiExtension', () => { configSchema: undefined, inputs: {}, output: { - api: { + api: expect.objectContaining({ $$type: 'extension-data', id: 'core.api.factory', - }, + config: {}, + }), }, factory: expect.any(Function), }); @@ -71,10 +72,11 @@ describe('createApiExtension', () => { configSchema: undefined, inputs: {}, output: { - api: { + api: expect.objectContaining({ $$type: 'extension-data', id: 'core.api.factory', - }, + config: {}, + }), }, factory: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 5dab38aec8..f81f07ca72 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -15,13 +15,13 @@ */ import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; -import { PortableSchema } from '../createSchemaFromZod'; +import { PortableSchema } from '../schema'; import { AnyExtensionDataMap, - coreExtensionData, + ExtensionDataInputValues, createExtension, - ExtensionDataValue, -} from '../types'; + coreExtensionData, +} from '../wiring'; /** @public */ export function createApiExtension< @@ -33,11 +33,7 @@ export function createApiExtension< api: AnyApiRef; factory: (options: { config: TConfig; - inputs: { - [pointName in keyof TInputs]: ExtensionDataValue< - TInputs[pointName]['extensionData'] - >[]; - }; + inputs: ExtensionDataInputValues; }) => AnyApiFactory; } | { @@ -63,9 +59,9 @@ export function createApiExtension< }, factory({ bind, config, inputs }) { if (typeof factory === 'function') { - bind.api(factory({ config, inputs })); + bind({ api: factory({ config, inputs }) }); } else { - bind.api(factory); + bind({ api: factory }); } }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index f1caaf7fdf..db389ae4b1 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -15,8 +15,8 @@ */ import React from 'react'; -import { PortableSchema } from '../createSchemaFromZod'; -import { coreExtensionData } from '../types'; +import { PortableSchema } from '../schema'; +import { coreExtensionData } from '../wiring'; import { createPageExtension } from './createPageExtension'; describe('createPageExtension', () => { @@ -42,6 +42,7 @@ describe('createPageExtension', () => { output: { component: expect.anything(), path: expect.anything(), + routeRef: expect.anything(), }, factory: expect.any(Function), }); @@ -73,6 +74,7 @@ describe('createPageExtension', () => { output: { component: expect.anything(), path: expect.anything(), + routeRef: expect.anything(), }, factory: expect.any(Function), }); @@ -93,6 +95,7 @@ describe('createPageExtension', () => { output: { component: expect.anything(), path: expect.anything(), + routeRef: expect.anything(), }, factory: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index fb2d930844..167dd397dd 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -17,14 +17,14 @@ import { RouteRef } from '@backstage/core-plugin-api'; import React from 'react'; import { ExtensionBoundary } from '../components'; -import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod'; +import { createSchemaFromZod, PortableSchema } from '../schema'; import { AnyExtensionDataMap, coreExtensionData, createExtension, Extension, - ExtensionDataValue, -} from '../types'; + ExtensionDataInputValues, +} from '../wiring'; /** * Helper for creating extensions for a routable React page component. @@ -50,11 +50,7 @@ export function createPageExtension< routeRef?: RouteRef; component: (props: { config: TConfig; - inputs: { - [pointName in keyof TInputs]: ExtensionDataValue< - TInputs[pointName]['extensionData'] - >[]; - }; + inputs: ExtensionDataInputValues; }) => Promise; }, ): Extension { @@ -72,7 +68,7 @@ export function createPageExtension< output: { component: coreExtensionData.reactComponent, path: coreExtensionData.routePath, - ...(options.routeRef && { routeRef: coreExtensionData.routeRef }), + routeRef: coreExtensionData.routeRef.optional(), }, inputs: options.inputs, configSchema, @@ -82,17 +78,18 @@ export function createPageExtension< .component({ config, inputs }) .then(element => ({ default: () => element })), ); - bind.path(config.path); - bind.component(() => ( - - - - - - )); - if (options.routeRef) { - bind.routeRef!(options.routeRef); - } + + bind({ + path: config.path, + component: () => ( + + + + + + ), + routeRef: options.routeRef, + }); }, }); } diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 16f226c483..4e1a383899 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -20,21 +20,8 @@ * @packageDocumentation */ -export { - createSchemaFromZod, - type PortableSchema, -} from './createSchemaFromZod'; export * from './components'; export * from './extensions'; -export { - coreExtensionData, - createExtension, - type AnyExtensionDataMap, - type CreateExtensionOptions, - type Extension, - type ExtensionDataBind, - type ExtensionDataRef, - type ExtensionDataValue, -} from './types'; +export * from './schema'; export * from './wiring'; export * from './routing'; diff --git a/packages/frontend-plugin-api/src/createSchemaFromZod.test.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/createSchemaFromZod.test.ts rename to packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts diff --git a/packages/frontend-plugin-api/src/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts similarity index 93% rename from packages/frontend-plugin-api/src/createSchemaFromZod.ts rename to packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts index 447cea8bbd..bfaf57f522 100644 --- a/packages/frontend-plugin-api/src/createSchemaFromZod.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -17,12 +17,7 @@ import { JsonObject } from '@backstage/types'; import { z, ZodSchema, ZodTypeDef } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; - -/** @public */ -export type PortableSchema = { - parse: (input: unknown) => TOutput; - schema: JsonObject; -}; +import { PortableSchema } from './types'; /** @public */ export function createSchemaFromZod( diff --git a/packages/frontend-plugin-api/src/schema/index.ts b/packages/frontend-plugin-api/src/schema/index.ts new file mode 100644 index 0000000000..7f21c4e07d --- /dev/null +++ b/packages/frontend-plugin-api/src/schema/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createSchemaFromZod } from './createSchemaFromZod'; +export { type PortableSchema } from './types'; diff --git a/packages/frontend-plugin-api/src/schema/types.ts b/packages/frontend-plugin-api/src/schema/types.ts new file mode 100644 index 0000000000..9636d3b896 --- /dev/null +++ b/packages/frontend-plugin-api/src/schema/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonObject } from '@backstage/types'; + +/** @public */ +export type PortableSchema = { + parse: (input: unknown) => TOutput; + schema: JsonObject; +}; diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts deleted file mode 100644 index 940e821af5..0000000000 --- a/packages/frontend-plugin-api/src/types.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AnyApiFactory } from '@backstage/core-plugin-api'; -import { RouteRef } from '@backstage/core-plugin-api'; -import { ComponentType } from 'react'; -import { PortableSchema } from './createSchemaFromZod'; -import { BackstagePlugin } from './wiring'; - -/** @public */ -export type ExtensionDataRef = { - id: string; - T: T; - $$type: 'extension-data'; -}; - -/** @public */ -// TODO: change to options object with ID. -export function createExtensionDataRef(id: string): ExtensionDataRef { - return { id, $$type: 'extension-data' } as ExtensionDataRef; -} - -/** @public */ -export const coreExtensionData = { - reactComponent: createExtensionDataRef('core.reactComponent'), - routePath: createExtensionDataRef('core.routing.path'), - apiFactory: createExtensionDataRef('core.api.factory'), - routeRef: createExtensionDataRef('core.routing.ref'), -}; - -/** @public */ -export type AnyExtensionDataMap = Record>; - -/** @public */ -export type ExtensionDataBind = { - [K in keyof TData]: (value: TData[K]['T']) => void; -}; - -/** @public */ -export type ExtensionDataValue = { - [K in keyof TData]: TData[K]['T']; -}; - -/** @public */ -export interface CreateExtensionOptions< - TData extends AnyExtensionDataMap, - TPoint extends Record, - TConfig, -> { - id: string; - at: string; - disabled?: boolean; - inputs?: TPoint; - output: TData; - configSchema?: PortableSchema; - factory(options: { - source?: BackstagePlugin; - bind: ExtensionDataBind; - config: TConfig; - inputs: { - [pointName in keyof TPoint]: ExtensionDataValue< - TPoint[pointName]['extensionData'] - >[]; - }; - }): void; -} - -/** @public */ -export interface Extension { - $$type: 'extension'; - id: string; - at: string; - disabled: boolean; - inputs: Record; - output: AnyExtensionDataMap; - configSchema?: PortableSchema; - factory(options: { - source?: BackstagePlugin; - bind: ExtensionDataBind; - config: TConfig; - inputs: Record>>; - }): void; -} - -/** @public */ -export function createExtension< - TData extends AnyExtensionDataMap, - TPoint extends Record, - TConfig = never, ->(options: CreateExtensionOptions): Extension { - return { - ...options, - disabled: options.disabled ?? false, - $$type: 'extension', - inputs: options.inputs ?? {}, - }; -} diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts new file mode 100644 index 0000000000..1f4e4370c9 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AnyApiFactory, RouteRef } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; +import { createExtensionDataRef } from './createExtensionDataRef'; + +/** @public */ +export const coreExtensionData = { + reactComponent: createExtensionDataRef('core.reactComponent'), + routePath: createExtensionDataRef('core.routing.path'), + apiFactory: createExtensionDataRef('core.api.factory'), + routeRef: createExtensionDataRef('core.routing.ref'), +}; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts new file mode 100644 index 0000000000..a992642d61 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createExtension } from './createExtension'; +import { createExtensionDataRef } from './createExtensionDataRef'; + +const stringData = createExtensionDataRef('string'); + +function unused(..._any: any[]) {} + +describe('createExtension', () => { + it('should create an extension with a simple output', () => { + const extension = createExtension({ + id: 'test', + at: 'root', + output: { + foo: stringData, + }, + factory({ bind }) { + bind({ + foo: 'bar', + }); + bind({ + // @ts-expect-error + foo: 3, + }); + bind({ + // @ts-expect-error + bar: 'bar', + }); + // @ts-expect-error + bind({}); + // @ts-expect-error + bind(); + // @ts-expect-error + bind('bar'); + }, + }); + expect(extension.id).toBe('test'); + }); + + it('should create an extension with a some optional output', () => { + const extension = createExtension({ + id: 'test', + at: 'root', + output: { + foo: stringData, + bar: stringData.optional(), + }, + factory({ bind }) { + bind({ + foo: 'bar', + }); + bind({ + foo: 'bar', + bar: 'baz', + }); + bind({ + // @ts-expect-error + foo: 3, + }); + bind({ + foo: 'bar', + // @ts-expect-error + bar: 3, + }); + // @ts-expect-error + bind({ bar: 'bar' }); + // @ts-expect-error + bind({}); + // @ts-expect-error + bind(); + // @ts-expect-error + bind('bar'); + }, + }); + expect(extension.id).toBe('test'); + }); + + it('should create an extension with input', () => { + const extension = createExtension({ + id: 'test', + at: 'root', + inputs: { + mixed: { + extensionData: { + required: stringData, + optional: stringData.optional(), + }, + }, + onlyRequired: { + extensionData: { + required: stringData, + }, + }, + onlyOptional: { + extensionData: { + optional: stringData.optional(), + }, + }, + }, + output: { + foo: stringData, + }, + factory({ bind, inputs }) { + const a1: string = inputs.mixed?.[0].required; + // @ts-expect-error + const a2: number = inputs.mixed?.[0].required; + // @ts-expect-error + const a3: any = inputs.mixed?.[0].nonExistent; + unused(a1, a2, a3); + + const b1: string | undefined = inputs.mixed?.[0].optional; + // @ts-expect-error + const b2: string = inputs.mixed?.[0].optional; + // @ts-expect-error + const b3: number = inputs.mixed?.[0].optional; + // @ts-expect-error + const b4: number | undefined = inputs.mixed?.[0].optional; + unused(b1, b2, b3, b4); + + const c1: string = inputs.onlyRequired?.[0].required; + // @ts-expect-error + const c2: number = inputs.onlyRequired?.[0].required; + unused(c1, c2); + + const d1: string | undefined = inputs.onlyOptional?.[0].optional; + // @ts-expect-error + const d2: string = inputs.onlyOptional?.[0].optional; + // @ts-expect-error + const d3: number = inputs.onlyOptional?.[0].optional; + // @ts-expect-error + const d4: number | undefined = inputs.onlyOptional?.[0].optional; + unused(d1, d2, d3, d4); + + bind({ + foo: 'bar', + }); + }, + }); + expect(extension.id).toBe('test'); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts new file mode 100644 index 0000000000..bcea03595b --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PortableSchema } from '../schema'; +import { BackstagePlugin } from './createPlugin'; +import { AnyExtensionDataMap, Extension } from './types'; + +/** @public */ +export type ExtensionDataInputValues< + TInputs extends { [name in string]: { extensionData: AnyExtensionDataMap } }, +> = { + [InputName in keyof TInputs]: Array< + { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TInputs[InputName]['extensionData'][DataName]['T']; + } & { + [DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TInputs[InputName]['extensionData'][DataName]['T']; + } + >; +}; + +/** @public */ +export type ExtensionDataBind = ( + values: { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? never + : DataName]: TMap[DataName]['T']; + } & { + [DataName in keyof TMap as TMap[DataName]['config'] extends { + optional: true; + } + ? DataName + : never]?: TMap[DataName]['T']; + }, +) => void; + +/** @public */ +export interface CreateExtensionOptions< + TOutput extends AnyExtensionDataMap, + TInputs extends Record, + TConfig, +> { + id: string; + at: string; + disabled?: boolean; + inputs?: TInputs; + output: TOutput; + configSchema?: PortableSchema; + factory(options: { + source?: BackstagePlugin; + bind: ExtensionDataBind; + config: TConfig; + inputs: ExtensionDataInputValues; + }): void; +} + +/** @public */ +export function createExtension< + TOutput extends AnyExtensionDataMap, + TInputs extends Record, + TConfig = never, +>( + options: CreateExtensionOptions, +): Extension { + return { + ...options, + disabled: options.disabled ?? false, + $$type: 'extension', + inputs: options.inputs ?? {}, + factory({ bind, config, inputs }) { + // TODO: Simplify this, but TS wouldn't infer the input type for some reason + return options.factory({ + bind, + config, + inputs: inputs as ExtensionDataInputValues, + }); + }, + }; +} diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts new file mode 100644 index 0000000000..6d6309bee4 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export type ExtensionDataRef< + TData, + TConfig extends { optional?: true } = {}, +> = { + id: string; + T: TData; + config: TConfig; + $$type: 'extension-data'; +}; + +/** @public */ +export interface ConfigurableExtensionDataRef< + TData, + TConfig extends { optional?: true } = {}, +> extends ExtensionDataRef { + optional(): ConfigurableExtensionDataRef; +} + +// TODO: change to options object with ID. +/** @public */ +export function createExtensionDataRef( + id: string, +): ConfigurableExtensionDataRef { + return { + id, + $$type: 'extension-data', + config: {}, + optional() { + return { ...this, config: { ...this.config, optional: true } }; + }, + } as ConfigurableExtensionDataRef; +} diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index 36389d7e63..724dad5143 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -17,14 +17,12 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { render, screen } from '@testing-library/react'; -import { createSchemaFromZod } from '../createSchemaFromZod'; -import { - createExtension, - coreExtensionData, - createExtensionDataRef, -} from '../types'; +import { createSchemaFromZod } from '../schema/createSchemaFromZod'; import { createPlugin, BackstagePlugin } from './createPlugin'; import { JsonObject } from '@backstage/types'; +import { createExtension } from './createExtension'; +import { createExtensionDataRef } from './createExtensionDataRef'; +import { coreExtensionData } from './coreExtensionData'; const nameExtensionDataRef = createExtensionDataRef('name'); @@ -35,7 +33,7 @@ const TechRadarPage = createExtension({ name: nameExtensionDataRef, }, factory({ bind }) { - bind.name('TechRadar'); + bind({ name: 'TechRadar' }); }, }); @@ -49,7 +47,7 @@ const CatalogPage = createExtension({ z.object({ name: z.string().default('Catalog') }), ), factory({ bind, config }) { - bind.name(config.name); + bind({ name: config.name }); }, }); @@ -63,7 +61,7 @@ const TechDocsAddon = createExtension({ z.object({ name: z.string().default('TechDocsAddon') }), ), factory({ bind, config }) { - bind.name(config.name); + bind({ name: config.name }); }, }); @@ -81,7 +79,7 @@ const TechDocsPage = createExtension({ name: nameExtensionDataRef, }, factory({ bind, inputs }) { - bind.name(`TechDocs-${inputs.addons.map(n => n.name).join('-')}`); + bind({ name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` }); }, }); @@ -99,11 +97,12 @@ const outputExtension = createExtension({ component: coreExtensionData.reactComponent, }, factory({ bind, inputs }) { - bind.component(() => - React.createElement('span', {}, [ - `Names: ${inputs.names.map(n => n.name).join(', ')}`, - ]), - ); + bind({ + component: () => + React.createElement('span', {}, [ + `Names: ${inputs.names.map(n => n.name).join(', ')}`, + ]), + }); }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index 3e15fb0b19..7f7e44ff03 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Extension } from '../types'; +import { Extension } from './types'; /** @public */ export interface PluginOptions { diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index d96ba09971..175200cf0c 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -14,8 +14,21 @@ * limitations under the License. */ +export { coreExtensionData } from './coreExtensionData'; +export { + createExtension, + type CreateExtensionOptions, + type ExtensionDataBind, + type ExtensionDataInputValues, +} from './createExtension'; +export { + createExtensionDataRef, + type ExtensionDataRef, + type ConfigurableExtensionDataRef, +} from './createExtensionDataRef'; export { createPlugin, type BackstagePlugin, type PluginOptions, } from './createPlugin'; +export type { AnyExtensionDataMap, Extension } from './types'; diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts new file mode 100644 index 0000000000..dbe4633ff5 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PortableSchema } from '../schema'; +import { ExtensionDataBind } from './createExtension'; +import { ExtensionDataRef } from './createExtensionDataRef'; +import { BackstagePlugin } from './createPlugin'; + +/** @public */ +export type AnyExtensionDataMap = { + [name in string]: ExtensionDataRef; +}; + +/** @public */ +export interface Extension { + $$type: 'extension'; + id: string; + at: string; + disabled: boolean; + inputs: Record; + output: AnyExtensionDataMap; + configSchema?: PortableSchema; + factory(options: { + source?: BackstagePlugin; + bind: ExtensionDataBind; + config: TConfig; + inputs: Record>>; + }): void; +}