frontend-app-api: extension instance string and JSON serialization

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-04 13:29:12 +02:00
parent cbed529916
commit 5072824817
3 changed files with 181 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Implement `toString()` and `toJSON()` for extension instances.
@@ -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(`
"<root>
children [
<core>
themes [
<themes.light out=[core.theme] />
<themes.dark out=[core.theme] />
]
</core>
<core.layout out=[core.reactElement]>
content [
<core.routes out=[core.reactElement] />
]
nav [
<core.nav out=[core.reactElement] />
]
</core.layout>
]
</root>"
`);
});
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",
}
`);
});
});
@@ -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<string, unknown>;
readonly attachments: Map<string, ExtensionInstance[]>;
readonly source?: BackstagePlugin;
constructor(
id: string,
extensionData: Map<string, unknown>,
attachments: Map<string, ExtensionInstance[]>,
source: BackstagePlugin | undefined,
) {
this.id = id;
this.#extensionData = extensionData;
this.attachments = attachments;
this.source = source;
}
getData<T>(ref: ExtensionDataRef<T>): 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')),
),
`</${this.id}>`,
].join('\n');
}
}
/** @internal */
export function createExtensionInstance(options: {
extension: Extension<unknown>;
@@ -137,13 +199,10 @@ export function createExtensionInstance(options: {
);
}
return {
$$type: '@backstage/ExtensionInstance',
id: options.extension.id,
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
return extensionData.get(ref.id) as T | undefined;
},
source,
return new ExtensionInstanceImpl(
options.extension.id,
extensionData,
attachments,
};
source,
);
}