diff --git a/.changeset/healthy-laws-divide.md b/.changeset/healthy-laws-divide.md new file mode 100644 index 0000000000..4ab29e6426 --- /dev/null +++ b/.changeset/healthy-laws-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Implement `toString()` and `toJSON()` for extension instances. diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 14e52af469..abd0fe9b00 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -25,6 +25,7 @@ import { screen } from '@testing-library/react'; import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; import { createRouteRef } from '@backstage/core-plugin-api'; +import { createExtensionInstance } from './createExtensionInstance'; describe('createInstances', () => { it('throws an error when a root extension is parametrized', () => { @@ -132,4 +133,112 @@ describe('createApp', () => { await expect(screen.findByText('Derp')).resolves.toBeInTheDocument(); }); + + it('should log an app', () => { + const { rootInstances } = createInstances({ + config: new MockConfigApi({}), + plugins: [], + }); + const root = createExtensionInstance({ + extension: createExtension({ + id: 'root', + attachTo: { id: '', input: '' }, + output: {}, + factory() {}, + }), + config: undefined, + attachments: new Map([['children', rootInstances]]), + }); + + expect(String(root)).toMatchInlineSnapshot(` + " + children [ + + themes [ + + + ] + + + content [ + + ] + nav [ + + ] + + ] + " + `); + }); + + it('should serialize an app as JSON', () => { + const { rootInstances } = createInstances({ + config: new MockConfigApi({}), + plugins: [], + }); + const root = createExtensionInstance({ + extension: createExtension({ + id: 'root', + attachTo: { id: '', input: '' }, + output: {}, + factory() {}, + }), + config: undefined, + attachments: new Map([['children', rootInstances]]), + }); + + expect(JSON.parse(JSON.stringify(root))).toMatchInlineSnapshot(` + { + "attachments": { + "children": [ + { + "attachments": { + "themes": [ + { + "id": "themes.light", + "output": [ + "core.theme", + ], + }, + { + "id": "themes.dark", + "output": [ + "core.theme", + ], + }, + ], + }, + "id": "core", + }, + { + "attachments": { + "content": [ + { + "id": "core.routes", + "output": [ + "core.reactElement", + ], + }, + ], + "nav": [ + { + "id": "core.nav", + "output": [ + "core.reactElement", + ], + }, + ], + }, + "id": "core.layout", + "output": [ + "core.reactElement", + ], + }, + ], + }, + "id": "root", + } + `); + }); }); diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index 18551eb8da..9aec3078bc 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -90,6 +90,68 @@ function resolveInputs( }); } +function indent(str: string) { + return str.replace(/^/gm, ' '); +} + +class ExtensionInstanceImpl implements ExtensionInstance { + readonly $$type = '@backstage/ExtensionInstance'; + + readonly id: string; + readonly #extensionData: Map; + readonly attachments: Map; + readonly source?: BackstagePlugin; + + constructor( + id: string, + extensionData: Map, + attachments: Map, + source: BackstagePlugin | undefined, + ) { + this.id = id; + this.#extensionData = extensionData; + this.attachments = attachments; + this.source = source; + } + + getData(ref: ExtensionDataRef): T | undefined { + return this.#extensionData.get(ref.id) as T | undefined; + } + + toJSON() { + return { + id: this.id, + output: + this.#extensionData.size > 0 + ? [...this.#extensionData.keys()] + : undefined, + attachments: + this.attachments.size > 0 + ? Object.fromEntries(this.attachments) + : undefined, + }; + } + + toString() { + const out = + this.#extensionData.size > 0 + ? ` out=[${[...this.#extensionData.keys()].join(', ')}]` + : ''; + + if (this.attachments.size === 0) { + return `<${this.id}${out} />`; + } + + return [ + `<${this.id}${out}>`, + ...[...this.attachments.entries()].map(([k, v]) => + indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')), + ), + ``, + ].join('\n'); + } +} + /** @internal */ export function createExtensionInstance(options: { extension: Extension; @@ -137,13 +199,10 @@ export function createExtensionInstance(options: { ); } - return { - $$type: '@backstage/ExtensionInstance', - id: options.extension.id, - getData(ref: ExtensionDataRef): T | undefined { - return extensionData.get(ref.id) as T | undefined; - }, - source, + return new ExtensionInstanceImpl( + options.extension.id, + extensionData, attachments, - }; + source, + ); }