frontend-app-api: move root extension to be an input on core instead

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-10 13:43:51 +02:00
parent d2b73acde3
commit d7c5d80c57
9 changed files with 81 additions and 112 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': minor
---
The hidden `'root'` extension has been removed and has instead been made an input of the `'core'` extension. The checks for rejecting configuration of the `'root'` extension to rejects configuration of the `'core'` extension instead.
@@ -30,7 +30,19 @@ export const Core = createExtension({
themes: createExtensionInput({
theme: coreExtensionData.theme,
}),
root: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true },
),
},
output: {
root: coreExtensionData.reactElement,
},
factory({ bind, inputs }) {
bind({
root: inputs.root.element,
});
},
output: {},
factory() {},
});
@@ -24,7 +24,7 @@ import { SidebarPage } from '@backstage/core-components';
export const CoreLayout = createExtension({
id: 'core.layout',
attachTo: { id: 'root', input: 'default' },
attachTo: { id: 'core', input: 'root' },
inputs: {
nav: createExtensionInput(
{
@@ -74,12 +74,12 @@ function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
id: 'test',
extensions,
});
const { rootInstances } = createInstances({
const { coreInstance } = createInstances({
config: new MockConfigApi({}),
features: [plugin],
});
return extractRouteInfoFromInstanceTree(rootInstances);
return extractRouteInfoFromInstanceTree(coreInstance);
}
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
@@ -42,7 +42,7 @@ export function joinPaths(...paths: string[]): string {
return normalized;
}
export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
export function extractRouteInfoFromInstanceTree(core: ExtensionInstance): {
routePaths: Map<RouteRef, string>;
routeParents: Map<RouteRef, RouteRef | undefined>;
routeObjects: BackstageRouteObject[];
@@ -151,9 +151,7 @@ export function extractRouteInfoFromInstanceTree(roots: ExtensionInstance[]): {
}
}
for (const root of roots) {
visit(root);
}
visit(core);
return { routePaths, routeParents, routeObjects };
}
@@ -16,7 +16,6 @@
import {
createExtension,
createExtensionInput,
createExtensionOverrides,
createPageExtension,
createPlugin,
@@ -27,7 +26,6 @@ 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';
const extBaseConfig = {
id: 'test',
@@ -37,12 +35,12 @@ const extBaseConfig = {
};
describe('createInstances', () => {
it('throws an error when a root extension is parametrized', () => {
it('throws an error when a core extension is parametrized', () => {
const config = new MockConfigApi({
app: {
extensions: [
{
root: {},
core: {},
},
],
},
@@ -54,18 +52,18 @@ describe('createInstances', () => {
}),
];
expect(() => createInstances({ config, features })).toThrow(
"A 'root' extension configuration was detected, but the root extension is not configurable",
"A 'core' extension configuration was detected, but the core extension is not configurable",
);
});
it('throws an error when a root extension is overridden', () => {
it('throws an error when a core extension is overridden', () => {
const config = new MockConfigApi({});
const features = [
createPlugin({
id: 'plugin',
extensions: [
createExtension({
id: 'root',
id: 'core',
attachTo: { id: 'core.routes', input: 'route' },
inputs: {},
output: {},
@@ -75,7 +73,7 @@ describe('createInstances', () => {
}),
];
expect(() => createInstances({ config, features })).toThrow(
"The following plugin(s) are overriding the 'root' extension which is forbidden: plugin",
"The following plugin(s) are overriding the 'core' extension which is forbidden: plugin",
);
});
@@ -164,33 +162,14 @@ describe('createApp', () => {
});
it('should log an app', () => {
const { rootInstances } = createInstances({
const { coreInstance } = createInstances({
config: new MockConfigApi({}),
features: [],
});
const root = createExtensionInstance({
extension: createExtension({
id: 'root',
attachTo: { id: '', input: '' },
inputs: {
children: createExtensionInput({}),
},
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>
expect(String(coreInstance)).toMatchInlineSnapshot(`
"<core out=[core.reactElement]>
root [
<core.layout out=[core.reactElement]>
content [
<core.routes out=[core.reactElement] />
@@ -200,52 +179,24 @@ describe('createApp', () => {
]
</core.layout>
]
</root>"
themes [
<themes.light out=[core.theme] />
<themes.dark out=[core.theme] />
]
</core>"
`);
});
it('should serialize an app as JSON', () => {
const { rootInstances } = createInstances({
const { coreInstance } = createInstances({
config: new MockConfigApi({}),
features: [],
});
const root = createExtensionInstance({
extension: createExtension({
id: 'root',
attachTo: { id: '', input: '' },
inputs: {
children: createExtensionInput({}),
},
output: {},
factory() {},
}),
config: undefined,
attachments: new Map([['children', rootInstances]]),
});
expect(JSON.parse(JSON.stringify(root))).toMatchInlineSnapshot(`
expect(JSON.parse(JSON.stringify(coreInstance))).toMatchInlineSnapshot(`
{
"attachments": {
"children": [
{
"attachments": {
"themes": [
{
"id": "themes.light",
"output": [
"core.theme",
],
},
{
"id": "themes.dark",
"output": [
"core.theme",
],
},
],
},
"id": "core",
},
"root": [
{
"attachments": {
"content": [
@@ -271,8 +222,25 @@ describe('createApp', () => {
],
},
],
"themes": [
{
"id": "themes.light",
"output": [
"core.theme",
],
},
{
"id": "themes.dark",
"output": [
"core.theme",
],
},
],
},
"id": "root",
"id": "core",
"output": [
"core.reactElement",
],
}
`);
});
@@ -250,13 +250,11 @@ export function createInstances(options: {
return newInstance;
}
const rootConfigs = attachmentMap.get('root')?.get('default') ?? [];
const rootInstances = rootConfigs.map(instanceParams =>
createInstance(instanceParams),
const coreInstance = createInstance(
extensionParams.find(p => p.extension.id === 'core')!,
);
return { instances, rootInstances };
return { coreInstance, instances };
}
/** @public */
@@ -286,41 +284,29 @@ export function createApp(options: {
]),
);
const { rootInstances } = createInstances({
const { coreInstance } = createInstances({
features: allFeatures,
config,
});
const routeInfo = extractRouteInfoFromInstanceTree(rootInstances);
const coreInstance = rootInstances.find(({ id }) => id === 'core');
if (!coreInstance) {
throw Error('Unable to find core extension instance');
}
const apiHolder = createApiHolder(coreInstance, config);
const appContext = createLegacyAppContext(
allFeatures.filter(
(f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
),
);
const rootElements = rootInstances
.map(e => (
<React.Fragment key={e.id}>
{e.getData(coreExtensionData.reactElement)}
</React.Fragment>
))
.filter((x): x is JSX.Element => !!x);
const App = () => (
<ApiProvider apis={apiHolder}>
<ApiProvider apis={createApiHolder(coreInstance, config)}>
<AppContextProvider appContext={appContext}>
<AppThemeProvider>
<RoutingProvider {...routeInfo} routeBindings={new Map(/* TODO */)}>
<RoutingProvider
{...extractRouteInfoFromInstanceTree(coreInstance)}
routeBindings={new Map(/* TODO */)}
>
{/* TODO: set base path using the logic from AppRouter */}
<BrowserRouter>{rootElements}</BrowserRouter>
<BrowserRouter>
{coreInstance.getData(coreExtensionData.reactElement)}
</BrowserRouter>
</RoutingProvider>
</AppThemeProvider>
</AppContextProvider>
@@ -235,13 +235,13 @@ export function mergeExtensionParameters(options: {
override => toInternalExtensionOverrides(override).extensions,
);
// Prevent root override
if (pluginExtensions.some(({ id }) => id === 'root')) {
const rootPluginIds = pluginExtensions
.filter(({ id }) => id === 'root')
// Prevent core override
if (pluginExtensions.some(({ id }) => id === 'core')) {
const pluginIds = pluginExtensions
.filter(({ id }) => id === 'core')
.map(({ source }) => source.id);
throw new Error(
`The following plugin(s) are overriding the 'root' extension which is forbidden: ${rootPluginIds.join(
`The following plugin(s) are overriding the 'core' extension which is forbidden: ${pluginIds.join(
',',
)}`,
);
@@ -352,10 +352,10 @@ export function mergeExtensionParameters(options: {
for (const overrideParam of parameters) {
const extensionId = overrideParam.id;
// Prevent root parametrization
if (extensionId === 'root') {
// Prevent core parametrization
if (extensionId === 'core') {
throw new Error(
"A 'root' extension configuration was detected, but the root extension is not configurable",
"A 'core' extension configuration was detected, but the core extension is not configurable",
);
}
@@ -85,7 +85,7 @@ const TechDocsPage = createExtension({
const outputExtension = createExtension({
id: 'test.output',
attachTo: { id: 'root', input: 'default' },
attachTo: { id: 'core', input: 'root' },
inputs: {
names: createExtensionInput({
name: nameExtensionDataRef,