diff --git a/.changeset/old-tools-smell.md b/.changeset/old-tools-smell.md new file mode 100644 index 0000000000..4dbd65e779 --- /dev/null +++ b/.changeset/old-tools-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Add support for accessing extensions definitions provided by a plugin via `plugin.getExtension(...)`. For this to work the extensions must be defined using the v2 format, typically using an extension blueprint. diff --git a/packages/app-next-example-plugin/api-report.md b/packages/app-next-example-plugin/api-report.md index c89a7b599b..962b1473a8 100644 --- a/packages/app-next-example-plugin/api-report.md +++ b/packages/app-next-example-plugin/api-report.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { default as React_2 } from 'react'; // @public (undocumented) -const examplePlugin: BackstagePlugin<{}, {}>; +const examplePlugin: BackstagePlugin<{}, {}, {}>; export default examplePlugin; // @public (undocumented) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 325dbd193e..a8f8ce591b 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -275,17 +275,22 @@ export { BackstageIdentityResponse }; // @public (undocumented) export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { + [id in string]: ExtensionDefinition; + } = {}, > { // (undocumented) readonly $$type: '@backstage/BackstagePlugin'; // (undocumented) - readonly externalRoutes: ExternalRoutes; + readonly externalRoutes: TExternalRoutes; + // (undocumented) + getExtension(id: TId): TExtensionMap[TId]; // (undocumented) readonly id: string; // (undocumented) - readonly routes: Routes; + readonly routes: TRoutes; } export { BackstageUserIdentity }; @@ -396,7 +401,15 @@ export function createApiExtension< configSchema?: PortableSchema; inputs?: TInputs; }, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createApiExtension { @@ -492,7 +505,15 @@ export function createComponentExtension< inputs: Expand>; }) => ComponentType; }; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createComponentExtension { @@ -528,8 +549,14 @@ export function createExtension< [key: string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, >( options: CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, TConfigSchema, @@ -545,7 +572,10 @@ export function createExtension< }> >, UOutput, - TInputs + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName >; // @public @deprecated (undocumented) @@ -580,11 +610,17 @@ export function createExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, TDataRefs extends { [name in string]: AnyExtensionDataRef; } = never, >( options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -593,6 +629,9 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, string extends keyof TInputs ? {} : TInputs, @@ -613,6 +652,9 @@ export function createExtensionBlueprint< // @public (undocumented) export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -632,8 +674,8 @@ export type CreateExtensionBlueprintOptions< [name in string]: AnyExtensionDataRef; }, > = { - kind: string; - namespace?: string | ((params: TParams) => string); + kind: TKind; + namespace?: TNamespace | ((params: TParams) => TNamespace); attachTo: { id: string; input: string; @@ -641,7 +683,7 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; - name?: string | ((params: TParams) => string); + name?: TName | ((params: TParams) => TName); config?: { schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; @@ -714,6 +756,9 @@ export function createExtensionInput< // @public (undocumented) export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -729,9 +774,9 @@ export type CreateExtensionOptions< }, UFactoryOutput extends ExtensionDataValue, > = { - kind?: string; - namespace?: string; - name?: string; + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string; @@ -794,7 +839,10 @@ export function createNavItemExtension(options: { title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @public (undocumented) @@ -817,7 +865,15 @@ export function createNavLogoExtension(options: { namespace?: string; logoIcon: JSX.Element; logoFull: JSX.Element; -}): ExtensionDefinition; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createNavLogoExtension { @@ -865,11 +921,22 @@ export function createPageExtension< // @public (undocumented) export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin; + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +>; // @public export function createRouteRef< @@ -972,7 +1039,15 @@ export function createSubRouteRef< // @public (undocumented) export function createThemeExtension( theme: AppTheme, -): ExtensionDefinition; +): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createThemeExtension { @@ -988,7 +1063,15 @@ export namespace createThemeExtension { export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; -}): ExtensionDefinition; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createTranslationExtension { @@ -1044,6 +1127,9 @@ export interface Extension { // @public (undocumented) export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -1068,6 +1154,8 @@ export interface ExtensionBlueprint< // (undocumented) dataRefs: TDataRefs; make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -1084,8 +1172,8 @@ export interface ExtensionBlueprint< }, >( args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string; @@ -1143,7 +1231,12 @@ export interface ExtensionBlueprint< >; }> > & - TConfigInput + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName >; } @@ -1239,6 +1332,9 @@ export interface ExtensionDefinition< } >; } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, > { // (undocumented) $$type: '@backstage/ExtensionDefinition'; @@ -1252,11 +1348,11 @@ export interface ExtensionDefinition< // (undocumented) readonly disabled: boolean; // (undocumented) - readonly kind?: string; + readonly kind?: TKind; // (undocumented) - readonly name?: string; + readonly name?: TName; // (undocumented) - readonly namespace?: string; + readonly namespace?: TNamespace; // (undocumented) override< TExtensionConfigSchema extends { @@ -1325,7 +1421,10 @@ export interface ExtensionDefinition< > & TConfigInput, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - TInputs & TExtraInputs + TInputs & TExtraInputs, + TKind, + TNamespace, + TName >; } @@ -1405,6 +1504,9 @@ export { googleAuthApiRef }; // @public (undocumented) export const IconBundleBlueprint: ExtensionBlueprint< + 'icon-bundle', + 'app', + undefined, { icons: { [x: string]: IconComponent; @@ -1536,19 +1638,21 @@ export { PendingOAuthRequest }; // @public (undocumented) export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { // (undocumented) - extensions?: ExtensionDefinition[]; + extensions?: TExtensions; // (undocumented) - externalRoutes?: ExternalRoutes; + externalRoutes?: TExternalRoutes; // (undocumented) featureFlags?: FeatureFlagConfig[]; // (undocumented) - id: string; + id: TId; // (undocumented) - routes?: Routes; + routes?: TRoutes; } // @public (undocumented) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 600b96ebe0..f2d3d906ab 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -174,6 +174,9 @@ export type VerifyExtensionFactoryOutput< /** @public */ export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -184,9 +187,9 @@ export type CreateExtensionOptions< TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, > = { - kind?: string; - namespace?: string; - name?: string; + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; @@ -214,11 +217,14 @@ export interface ExtensionDefinition< { optional: boolean; singleton: boolean } >; } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, > { $$type: '@backstage/ExtensionDefinition'; - readonly kind?: string; - readonly namespace?: string; - readonly name?: string; + readonly kind?: TKind; + readonly namespace?: TNamespace; + readonly name?: TName; readonly attachTo: { id: string; input: string }; readonly disabled: boolean; readonly configSchema?: PortableSchema; @@ -284,45 +290,68 @@ export interface ExtensionDefinition< > & TConfigInput, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - TInputs & TExtraInputs + TInputs & TExtraInputs, + TKind, + TNamespace, + TName >; } /** @internal */ -export type InternalExtensionDefinition = - ExtensionDefinition & - ( - | { - readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; - readonly output: AnyExtensionDataMap; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs; - }): ExtensionDataValues; - } - | { - readonly version: 'v2'; - readonly inputs: { +export type InternalExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> = ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + TNamespace, + TName +> & + ( + | { + readonly version: 'v1'; + readonly inputs: AnyExtensionInputMap; + readonly output: AnyExtensionDataMap; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs; + }): ExtensionDataValues; + } + | { + readonly version: 'v2'; + readonly inputs: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }; + readonly output: Array; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput< AnyExtensionDataRef, { optional: boolean; singleton: boolean } >; - }; - readonly output: Array; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs<{ - [inputName in string]: ExtensionInput< - AnyExtensionDataRef, - { optional: boolean; singleton: boolean } - >; - }>; - }): Iterable>; - } - ); + }>; + }): Iterable>; + } + ); /** @internal */ export function toInternalExtensionDefinition( @@ -357,8 +386,14 @@ export function createExtension< }, TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, >( options: CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, TConfigSchema, @@ -374,7 +409,10 @@ export function createExtension< }> >, UOutput, - TInputs + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName >; /** * @public @@ -394,6 +432,9 @@ export function createExtension< >, ): ExtensionDefinition; export function createExtension< + const TKind extends string | undefined, + const TNamespace extends string | undefined, + const TName extends string | undefined, UOutput extends AnyExtensionDataRef, TInputs extends { [inputName in string]: ExtensionInput< @@ -408,7 +449,15 @@ export function createExtension< UFactoryOutput extends ExtensionDataValue, >( options: - | CreateExtensionOptions + | CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + > | LegacyCreateExtensionOptions< AnyExtensionDataMap, TLegacyInputs, @@ -431,7 +480,10 @@ export function createExtension< }> >), UOutput, - TInputs + TInputs, + TKind, + TNamespace, + TName > { if ('configSchema' in options && 'config' in options) { throw new Error(`Cannot provide both configSchema and config.schema`); @@ -542,7 +594,10 @@ export function createExtension< > >, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - TInputs & TExtraInputs + TInputs & TExtraInputs, + TKind, + TNamespace, + TName > => { if (!Array.isArray(options.output)) { throw new Error( @@ -550,6 +605,9 @@ export function createExtension< ); } const newOptions = options as CreateExtensionOptions< + TKind, + TNamespace, + TName, UOutput, TInputs, TConfigSchema, @@ -612,7 +670,7 @@ export function createExtension< return deduplicatedResult.values() as Iterable; }, - } as CreateExtensionOptions); + } as CreateExtensionOptions); }, } as InternalExtensionDefinition< TConfig & @@ -630,6 +688,11 @@ export function createExtension< z.ZodObject<{ [key in keyof TConfigSchema]: ReturnType; }> - >) + >), + UOutput, + TInputs, + TKind, + TNamespace, + TName >; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index a8bf42c7f8..81137f47e2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -36,6 +36,9 @@ import { * @public */ export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -48,13 +51,13 @@ export type CreateExtensionBlueprintOptions< UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { - kind: string; - namespace?: string | ((params: TParams) => string); + kind: TKind; + namespace?: TNamespace | ((params: TParams) => TNamespace); attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; - name?: string | ((params: TParams) => string); + name?: TName | ((params: TParams) => TName); config?: { schema: TConfigSchema | ((params: TParams) => TConfigSchema); }; @@ -76,6 +79,9 @@ export type CreateExtensionBlueprintOptions< * @public */ export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -97,6 +103,8 @@ export interface ExtensionBlueprint< * optionally call the original factory with the same params. */ make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -110,8 +118,8 @@ export interface ExtensionBlueprint< }, >( args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TExtraInputs & { @@ -166,7 +174,12 @@ export interface ExtensionBlueprint< >; }> > & - TConfigInput + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName >; } @@ -198,6 +211,9 @@ export function createDataContainer( * @internal */ class ExtensionBlueprintImpl< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, TParams, UOutput extends AnyExtensionDataRef, TInputs extends { @@ -211,6 +227,9 @@ class ExtensionBlueprintImpl< > { constructor( private readonly options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -236,9 +255,11 @@ class ExtensionBlueprintImpl< { optional: boolean; singleton: boolean } >; }, + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, >(args: { - namespace?: string; - name?: string; + namespace?: TNewNamespace; + name?: TNewName; attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TExtraInputs; @@ -360,26 +381,7 @@ class ExtensionBlueprintImpl< } throw new Error('Either params or factory must be provided'); }, - } as CreateExtensionOptions) as ExtensionDefinition< - { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - } & { - [key in keyof TConfigSchema]: z.infer>; - }, - z.input< - z.ZodObject< - { - [key in keyof TExtensionConfigSchema]: ReturnType< - TExtensionConfigSchema[key] - >; - } & { - [key in keyof TConfigSchema]: ReturnType; - } - > - > - >; + } as CreateExtensionOptions); } } @@ -400,9 +402,15 @@ export function createExtensionBlueprint< }, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, >( options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, TParams, UOutput, TInputs, @@ -411,6 +419,9 @@ export function createExtensionBlueprint< TDataRefs >, ): ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, string extends keyof TInputs ? {} : TInputs, @@ -427,6 +438,9 @@ export function createExtensionBlueprint< TDataRefs > { return new ExtensionBlueprintImpl(options) as ExtensionBlueprint< + TKind, + TNamespace, + TName, TParams, UOutput, string extends keyof TInputs ? {} : TInputs, diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index bb9bdc2107..0242e2961a 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -34,11 +34,9 @@ const nameExtensionDataRef = createExtensionDataRef().with({ const Extension1 = createExtension({ name: '1', attachTo: { id: 'test/output', input: 'names' }, - output: { - name: nameExtensionDataRef, - }, + output: [nameExtensionDataRef], factory() { - return { name: 'extension-1' }; + return [nameExtensionDataRef('extension-1')]; }, }); @@ -150,6 +148,8 @@ describe('createPlugin', () => { }); expect(plugin).toBeDefined(); + expect(plugin.getExtension('test/1')).toBe(Extension1); + await renderWithEffects( createTestAppRoot({ features: [plugin], diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index b671d95d2f..1d2a12ffe2 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -17,6 +17,7 @@ import { ExtensionDefinition } from './createExtension'; import { Extension, + ResolveExtensionId, resolveExtensionDefinition, } from './resolveExtensionDefinition'; import { @@ -28,21 +29,23 @@ import { /** @public */ export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { - id: string; - routes?: Routes; - externalRoutes?: ExternalRoutes; - extensions?: ExtensionDefinition[]; + id: TId; + routes?: TRoutes; + externalRoutes?: TExternalRoutes; + extensions?: TExtensions; featureFlags?: FeatureFlagConfig[]; } /** @public */ export interface InternalBackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, -> extends BackstagePlugin { + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, +> extends BackstagePlugin { readonly version: 'v1'; readonly extensions: Extension[]; readonly featureFlags: FeatureFlagConfig[]; @@ -50,17 +53,36 @@ export interface InternalBackstagePlugin< /** @public */ export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin { - const extensions = (options.extensions ?? []).map(def => - resolveExtensionDefinition(def, { namespace: options.id }), - ); + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +> { + const extensions = new Array>(); + const extensionDefinitionsById = new Map< + string, + ExtensionDefinition + >(); - const extensionIds = extensions.map(e => e.id); - if (extensionIds.length !== new Set(extensionIds).size) { + for (const def of options.extensions ?? []) { + const ext = resolveExtensionDefinition(def, { namespace: options.id }); + extensions.push(ext); + extensionDefinitionsById.set(ext.id, def); + } + + if (extensions.length !== extensionDefinitionsById.size) { + const extensionIds = extensions.map(e => e.id); const duplicates = Array.from( new Set( extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index), @@ -78,14 +100,17 @@ export function createPlugin< $$type: '@backstage/BackstagePlugin', version: 'v1', id: options.id, - routes: options.routes ?? ({} as Routes), - externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes), + routes: options.routes ?? ({} as TRoutes), + externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes), featureFlags: options.featureFlags ?? [], extensions, + getExtension(id) { + return extensionDefinitionsById.get(id); + }, toString() { return `Plugin{id=${options.id}}`; }, - } as InternalBackstagePlugin; + } as InternalBackstagePlugin; } /** @internal */ diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index 74d081e95d..5a42e9100e 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -15,7 +15,10 @@ */ import { ExtensionDefinition } from './createExtension'; -import { resolveExtensionDefinition } from './resolveExtensionDefinition'; +import { + ResolveExtensionId, + resolveExtensionDefinition, +} from './resolveExtensionDefinition'; describe('resolveExtensionDefinition', () => { const baseDef = { @@ -98,3 +101,103 @@ describe('old resolveExtensionDefinition', () => { ); }); }); + +describe('ResolveExtensionId', () => { + it('should resolve extension IDs correctly', () => { + type NamedExtension< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, + > = ExtensionDefinition; + + const id1: 'k:ns' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + undefined + >; + const id2: 'k:n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + undefined + >; + const id3: 'ns/n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id4: never = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + undefined + >; + const id5: 'ns' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id6: 'n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id7: 'k:ns/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + undefined + >; + const id8: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + 'ns2' + >; + const id9: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + 'ns2' + >; + const ida: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idb: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + 'ns2' + >; + const idc: 'ns2' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idd: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const ide: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + 'ns2' + >; + + const invalid1: never = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const invalid2: never = {} as ResolveExtensionId< + NamedExtension<'k', string | undefined, 'n'>, + undefined + >; + const invalid3: never = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', string | undefined>, + undefined + >; + + expect([ + id1, + id2, + id3, + id4, + id5, + id6, + id7, + id8, + id9, + ida, + idb, + idc, + idd, + ide, + invalid1, + invalid2, + invalid3, + ]).toBeDefined(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 0c8a7959f7..35bf9c6b05 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -94,6 +94,36 @@ export function toInternalExtension( return internal; } +/** @ignore */ +export type ResolveExtensionId< + TExtension extends ExtensionDefinition, + TDefaultNamespace extends string | undefined, +> = TExtension extends ExtensionDefinition< + any, + any, + any, + any, + infer IKind, + infer INamespace, + infer IName +> + ? [string | undefined] extends [IKind | INamespace | IName] + ? never + : ( + ( + undefined extends TDefaultNamespace ? INamespace : TDefaultNamespace + ) extends infer ISelectedNamespace extends string + ? undefined extends IName + ? ISelectedNamespace + : `${ISelectedNamespace}/${IName}` + : IName + ) extends infer INamePart extends string + ? IKind extends string + ? `${IKind}:${INamePart}` + : INamePart + : never + : never; + /** @internal */ export function resolveExtensionDefinition( definition: ExtensionDefinition, diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index 7e0650e06c..b1afd4e7ea 100644 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -15,6 +15,7 @@ */ import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing'; +import { ExtensionDefinition } from './createExtension'; /** * Feature flag configuration. @@ -32,15 +33,24 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef }; /** @public */ export type AnyExternalRoutes = { [name in string]: ExternalRouteRef }; +/** @public */ +export type ExtensionMap< + TExtensionMap extends { [id in string]: ExtensionDefinition }, +> = { + get(id: TId): TExtensionMap[TId]; +}; + /** @public */ export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { [id in string]: ExtensionDefinition } = {}, > { readonly $$type: '@backstage/BackstagePlugin'; readonly id: string; - readonly routes: Routes; - readonly externalRoutes: ExternalRoutes; + readonly routes: TRoutes; + readonly externalRoutes: TExternalRoutes; + getExtension(id: TId): TExtensionMap[TId]; } /** @public */ diff --git a/plugins/api-docs/api-report-alpha.md b/plugins/api-docs/api-report-alpha.md index f17ba0fa3c..6003056cb3 100644 --- a/plugins/api-docs/api-report-alpha.md +++ b/plugins/api-docs/api-report-alpha.md @@ -14,7 +14,8 @@ const _default: BackstagePlugin< }, { registerApi: ExternalRouteRef; - } + }, + {} >; export default _default; diff --git a/plugins/app-visualizer/api-report.md b/plugins/app-visualizer/api-report.md index 764caa7190..fd3749b159 100644 --- a/plugins/app-visualizer/api-report.md +++ b/plugins/app-visualizer/api-report.md @@ -6,7 +6,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @public (undocumented) -const visualizerPlugin: BackstagePlugin<{}, {}>; +const visualizerPlugin: BackstagePlugin<{}, {}, {}>; export default visualizerPlugin; // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-graph/api-report-alpha.md b/plugins/catalog-graph/api-report-alpha.md index 88b3a38b76..e651946463 100644 --- a/plugins/catalog-graph/api-report-alpha.md +++ b/plugins/catalog-graph/api-report-alpha.md @@ -18,7 +18,8 @@ const _default: BackstagePlugin< kind: string; namespace: string; }>; - } + }, + {} >; export default _default; diff --git a/plugins/catalog-import/api-report-alpha.md b/plugins/catalog-import/api-report-alpha.md index 63e2cda339..51e3d99fd7 100644 --- a/plugins/catalog-import/api-report-alpha.md +++ b/plugins/catalog-import/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { importPage: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index 3ff0788b04..edfa183903 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -115,7 +115,15 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export function createEntityContentExtension< @@ -150,7 +158,10 @@ export function createEntityContentExtension< path?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 726ee80573..5019fc25ea 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -108,7 +108,15 @@ export function createCatalogFilterExtension< inputs?: TInputs; configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) const _default: BackstagePlugin< @@ -132,7 +140,8 @@ const _default: BackstagePlugin< templateName: string; }>; unregisterRedirect: ExternalRouteRef; - } + }, + {} >; export default _default; diff --git a/plugins/devtools/api-report-alpha.md b/plugins/devtools/api-report-alpha.md index 59aefd3698..23ede8144c 100644 --- a/plugins/devtools/api-report-alpha.md +++ b/plugins/devtools/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/home/api-report-alpha.md b/plugins/home/api-report-alpha.md index eb693a8e6f..0f9b980a78 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin<{}, {}>; +const _default: BackstagePlugin<{}, {}, {}>; export default _default; // @alpha (undocumented) diff --git a/plugins/kubernetes/api-report-alpha.md b/plugins/kubernetes/api-report-alpha.md index 8bd6e98baa..befbcf379a 100644 --- a/plugins/kubernetes/api-report-alpha.md +++ b/plugins/kubernetes/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { kubernetes: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md index e7c4db2c6d..be74cde141 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -11,7 +11,8 @@ const _default: BackstagePlugin< {}, { catalogIndex: ExternalRouteRef; - } + }, + {} >; export default _default; diff --git a/plugins/scaffolder/api-report-alpha.md b/plugins/scaffolder/api-report-alpha.md index 4628563be2..de0fc463a9 100644 --- a/plugins/scaffolder/api-report-alpha.md +++ b/plugins/scaffolder/api-report-alpha.md @@ -36,7 +36,8 @@ const _default: BackstagePlugin< kind: string; namespace: string; }>; - } + }, + {} >; export default _default; diff --git a/plugins/search-react/api-report-alpha.md b/plugins/search-react/api-report-alpha.md index 56c0b6eab9..4bcdbb3f78 100644 --- a/plugins/search-react/api-report-alpha.md +++ b/plugins/search-react/api-report-alpha.md @@ -25,7 +25,15 @@ export function createSearchResultListItemExtension< }, >( options: SearchResultItemExtensionOptions, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export namespace createSearchResultListItemExtension { diff --git a/plugins/search/api-report-alpha.md b/plugins/search/api-report-alpha.md index bb670522e4..e6f91d6d19 100644 --- a/plugins/search/api-report-alpha.md +++ b/plugins/search/api-report-alpha.md @@ -13,12 +13,21 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; // @alpha (undocumented) -export const searchApi: ExtensionDefinition<{}, {}, never, never>; +export const searchApi: ExtensionDefinition< + {}, + {}, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export const searchNavItem: ExtensionDefinition< @@ -29,7 +38,10 @@ export const searchNavItem: ExtensionDefinition< title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha (undocumented) @@ -43,7 +55,10 @@ export const searchPage: ExtensionDefinition< noTrack: boolean; }, AnyExtensionDataRef, - {} + {}, + string | undefined, + string | undefined, + string | undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/techdocs/api-report-alpha.md b/plugins/techdocs/api-report-alpha.md index e3c48ba214..f7c6679139 100644 --- a/plugins/techdocs/api-report-alpha.md +++ b/plugins/techdocs/api-report-alpha.md @@ -18,6 +18,7 @@ const _default: BackstagePlugin< }>; entityContent: RouteRef; }, + {}, {} >; export default _default; @@ -39,7 +40,10 @@ export const techDocsSearchResultListItemExtension: ExtensionDefinition< title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // (No @packageDocumentation comment for this package) diff --git a/plugins/user-settings/api-report-alpha.md b/plugins/user-settings/api-report-alpha.md index d0fad90e08..700e06a7ad 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -13,6 +13,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; @@ -26,7 +27,10 @@ export const settingsNavItem: ExtensionDefinition< title?: string | undefined; }, never, - never + never, + string | undefined, + string | undefined, + string | undefined >; // @alpha (undocumented)