diff --git a/.changeset/calm-grapes-remember.md b/.changeset/calm-grapes-remember.md new file mode 100644 index 0000000000..f21a074c74 --- /dev/null +++ b/.changeset/calm-grapes-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Exposed `createComponentRef`, and ensured that produced refs and feature bits have a `toString` for easier debugging diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 8dfa7aeea9..82f274b960 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -33,6 +33,7 @@ function makeExt( id, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + toString: expect.any(Function), } as Extension; } diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 46599bd6c4..a292634d9a 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -428,6 +428,11 @@ export namespace createComponentExtension { >; } +// @public (undocumented) +export function createComponentRef(options: { + id: string; +}): ComponentRef; + // @public (undocumented) export function createExtension< TOutput extends AnyExtensionDataMap, diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index d9bd41f2a9..9a8eb5b750 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -25,7 +25,7 @@ import { ErrorBoundary } from './ErrorBoundary'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; import { AppNode, useComponentRef } from '../apis'; -import { coreComponentRefs } from './ComponentRef'; +import { coreComponentRefs } from './coreComponentRefs'; type RouteTrackerProps = PropsWithChildren<{ disableTracking?: boolean; diff --git a/packages/frontend-plugin-api/src/components/ComponentRef.tsx b/packages/frontend-plugin-api/src/components/coreComponentRefs.ts similarity index 82% rename from packages/frontend-plugin-api/src/components/ComponentRef.tsx rename to packages/frontend-plugin-api/src/components/coreComponentRefs.ts index 50df9552ec..64412b8710 100644 --- a/packages/frontend-plugin-api/src/components/ComponentRef.tsx +++ b/packages/frontend-plugin-api/src/components/coreComponentRefs.ts @@ -19,22 +19,7 @@ import { CoreNotFoundErrorPageProps, CoreProgressProps, } from '../types'; - -/** @public */ -export type ComponentRef = { - id: string; - T: T; -}; - -/** @public */ -export function createComponentRef(options: { - id: string; -}): ComponentRef { - const { id } = options; - return { - id, - } as ComponentRef; -} +import { createComponentRef } from './createComponentRef'; const coreProgressComponentRef = createComponentRef({ id: 'core.components.progress', diff --git a/packages/frontend-plugin-api/src/components/createComponentRef.test.tsx b/packages/frontend-plugin-api/src/components/createComponentRef.test.tsx new file mode 100644 index 0000000000..9a84fe08fa --- /dev/null +++ b/packages/frontend-plugin-api/src/components/createComponentRef.test.tsx @@ -0,0 +1,25 @@ +/* + * 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 { createComponentRef } from './createComponentRef'; + +describe('createComponentRef', () => { + it('can be created and read', () => { + const ref = createComponentRef({ id: 'foo' }); + expect(ref.id).toBe('foo'); + expect(String(ref)).toBe('ComponentRef{id=foo}'); + }); +}); diff --git a/packages/frontend-plugin-api/src/components/createComponentRef.tsx b/packages/frontend-plugin-api/src/components/createComponentRef.tsx new file mode 100644 index 0000000000..88181dd079 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/createComponentRef.tsx @@ -0,0 +1,34 @@ +/* + * 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 ComponentRef = { + id: string; + T: T; +}; + +/** @public */ +export function createComponentRef(options: { + id: string; +}): ComponentRef { + const { id } = options; + return { + id, + toString() { + return `ComponentRef{id=${id}}`; + }, + } as ComponentRef; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 893918059b..5066144000 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -export { coreComponentRefs, type ComponentRef } from './ComponentRef'; - export { ExtensionBoundary, type ExtensionBoundaryProps, } from './ExtensionBoundary'; +export { coreComponentRefs } from './coreComponentRefs'; +export { createComponentRef, type ComponentRef } from './createComponentRef'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index 0636d0d79d..c4d0239094 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -47,6 +47,7 @@ describe('createApiExtension', () => { }), }, factory: expect.any(Function), + toString: expect.any(Function), }); }); @@ -83,6 +84,7 @@ describe('createApiExtension', () => { }), }, factory: expect.any(Function), + toString: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx index 90e1448448..8c54743f1d 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx @@ -41,6 +41,7 @@ describe('createNavLogoExtension', () => { logos: expect.anything(), }, factory: expect.any(Function), + toString: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 2de72296ac..374400fce8 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -55,6 +55,7 @@ describe('createPageExtension', () => { routeRef: expect.anything(), }, factory: expect.any(Function), + toString: expect.any(Function), }); expect( @@ -89,6 +90,7 @@ describe('createPageExtension', () => { routeRef: expect.anything(), }, factory: expect.any(Function), + toString: expect.any(Function), }); expect( @@ -112,6 +114,7 @@ describe('createPageExtension', () => { routeRef: expect.anything(), }, factory: expect.any(Function), + toString: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts index 3712da7185..975901b8e5 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts @@ -51,6 +51,7 @@ describe('createTranslationExtension', () => { resource: createTranslationExtension.translationDataRef, }, factory: expect.any(Function), + toString: expect.any(Function), }); expect((extension as any).factory({} as any)).toEqual({ @@ -88,6 +89,7 @@ describe('createTranslationExtension', () => { resource: createTranslationExtension.translationDataRef, }, factory: expect.any(Function), + toString: expect.any(Function), }); expect((extension as any).factory({} as any)).toEqual({ resource }); @@ -126,6 +128,7 @@ describe('createTranslationExtension', () => { resource: createTranslationExtension.translationDataRef, }, factory: expect.any(Function), + toString: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 22db95082a..42ab21db80 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -287,5 +287,8 @@ describe('createExtension', () => { }, }); expect(extension.namespace).toBe('test'); + expect(String(extension)).toBe( + 'ExtensionDefinition{namespace=test,attachTo=root@default}', + ); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 0d6806fe49..be76fbc069 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -166,5 +166,19 @@ export function createExtension< ...rest, }); }, + toString() { + const parts: string[] = []; + if (options.kind) { + parts.push(`kind=${options.kind}`); + } + if (options.namespace) { + parts.push(`namespace=${options.namespace}`); + } + if (options.name) { + parts.push(`name=${options.name}`); + } + parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); + return `ExtensionDefinition{${parts.join(',')}}`; + }, } as InternalExtensionDefinition; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts new file mode 100644 index 0000000000..f7a0dc82d0 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { createExtensionDataRef } from './createExtensionDataRef'; + +describe('createExtensionDataRef', () => { + it('can be created and read', () => { + const ref = createExtensionDataRef('foo'); + expect(ref.id).toBe('foo'); + expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); + const refOptional = ref.optional(); + expect(refOptional.id).toBe('foo'); + expect(String(refOptional)).toBe('ExtensionDataRef{id=foo,optional=true}'); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index d0d96e85f9..a7f4e4374e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -43,7 +43,14 @@ export function createExtensionDataRef( $$type: '@backstage/ExtensionDataRef', config: {}, optional() { - return { ...this, config: { ...this.config, optional: true } }; + return { + ...this, + config: { ...this.config, optional: true }, + }; }, - } as ConfigurableExtensionDataRef; + toString() { + const optional = Boolean(this.config.optional); + return `ExtensionDataRef{id=${id},optional=${optional}}`; + }, + } as ConfigurableExtensionDataRef; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts index de2d2f9a92..aa00247222 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.test.ts @@ -27,6 +27,7 @@ describe('createExtensionOverrides', () => { "$$type": "@backstage/ExtensionOverrides", "extensions": [], "featureFlags": [], + "toString": [Function], "version": "v1", } `); @@ -74,6 +75,7 @@ describe('createExtensionOverrides', () => { "id": "a", "inputs": {}, "output": {}, + "toString": [Function], "version": "v1", }, { @@ -88,6 +90,7 @@ describe('createExtensionOverrides', () => { "id": "b", "inputs": {}, "output": {}, + "toString": [Function], "version": "v1", }, { @@ -102,10 +105,12 @@ describe('createExtensionOverrides', () => { "id": "k:c/n", "inputs": {}, "output": {}, + "toString": [Function], "version": "v1", }, ], "featureFlags": [], + "toString": [Function], "version": "v1", } `); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts index 3c14d285b3..1f88038944 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts @@ -38,11 +38,20 @@ export interface InternalExtensionOverrides extends ExtensionOverrides { export function createExtensionOverrides( options: ExtensionOverridesOptions, ): ExtensionOverrides { + const extensions = options.extensions.map(def => + resolveExtensionDefinition(def), + ); + const featureFlags = options.featureFlags ?? []; return { $$type: '@backstage/ExtensionOverrides', version: 'v1', - extensions: options.extensions.map(def => resolveExtensionDefinition(def)), - featureFlags: options.featureFlags ?? [], + extensions, + featureFlags, + toString() { + const ex = extensions.map(String).join(','); + const ff = featureFlags.map(f => f.name).join(','); + return `ExtensionOverrides{extensions=[${ex}],featureFlags=[${ff}]}`; + }, } as InternalExtensionOverrides; } diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index d2f78122d3..c180dc9719 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -138,6 +138,7 @@ describe('createPlugin', () => { const plugin = createPlugin({ id: 'test' }); expect(plugin).toBeDefined(); + expect(String(plugin)).toBe('Plugin{id=test}'); }); it('should create a plugin with extension instances', async () => { diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index 67907e188e..797c46e58e 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -82,6 +82,9 @@ export function createPlugin< externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes), featureFlags: options.featureFlags ?? [], extensions, + toString() { + return `Plugin{id=${options.id}}`; + }, } as InternalBackstagePlugin; } diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index 9db9539ebb..8970cfe168 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -37,6 +37,7 @@ describe('resolveExtensionDefinition', () => { ...definition, } as ExtensionDefinition); expect(resolved.id).toBe(expected); + expect(String(resolved)).toBe(`Extension{id=${expected}}`); }); it('should fail to resolve extension ID without namespace', () => { diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index d702120447..0a44de4ab4 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -81,10 +81,15 @@ export function resolveExtensionDefinition( ); } + const id = kind ? `${kind}:${namePart}` : namePart; + return { ...rest, $$type: '@backstage/Extension', version: 'v1', - id: kind ? `${kind}:${namePart}` : namePart, - } as InternalExtension; + id, + toString() { + return `Extension{id=${id}}`; + }, + } as Extension; }