Merge pull request #26138 from backstage/mob/move-2-root
NFS: Introduce the `Root` extension
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Internal update to add support for passing an `ApiRegistry` when creating the node tree
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
A new `apis` parameter has been added to `factory` for extensions. This is a way to access utility APIs without being coupled to the React context.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Added the `root` extension the replace the `app` extension as the root of the app.
|
||||
@@ -74,7 +74,7 @@ describe('collectLegacyRoutes', () => {
|
||||
},
|
||||
{
|
||||
id: 'api:plugin.scoringdata.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
@@ -90,7 +90,7 @@ describe('collectLegacyRoutes', () => {
|
||||
},
|
||||
{
|
||||
id: 'api:plugin.stackstorm.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
@@ -112,7 +112,7 @@ describe('collectLegacyRoutes', () => {
|
||||
},
|
||||
{
|
||||
id: 'api:plugin.puppetdb.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
@@ -211,7 +211,7 @@ describe('collectLegacyRoutes', () => {
|
||||
{
|
||||
id: 'api:plugin.catalog.service',
|
||||
attachTo: {
|
||||
id: 'app',
|
||||
id: 'root',
|
||||
input: 'apis',
|
||||
},
|
||||
defaultConfig: undefined,
|
||||
@@ -220,7 +220,7 @@ describe('collectLegacyRoutes', () => {
|
||||
{
|
||||
id: 'api:catalog-react.starred-entities',
|
||||
attachTo: {
|
||||
id: 'app',
|
||||
id: 'root',
|
||||
input: 'apis',
|
||||
},
|
||||
defaultConfig: undefined,
|
||||
@@ -229,7 +229,7 @@ describe('collectLegacyRoutes', () => {
|
||||
{
|
||||
id: 'api:plugin.catalog.entity-presentation',
|
||||
attachTo: {
|
||||
id: 'app',
|
||||
id: 'root',
|
||||
input: 'apis',
|
||||
},
|
||||
defaultConfig: undefined,
|
||||
@@ -242,7 +242,7 @@ describe('collectLegacyRoutes', () => {
|
||||
extensions: [
|
||||
{
|
||||
id: 'api:plugin.scoringdata.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('convertLegacyApp', () => {
|
||||
},
|
||||
{
|
||||
id: 'api:plugin.scoringdata.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
@@ -82,7 +82,7 @@ describe('convertLegacyApp', () => {
|
||||
},
|
||||
{
|
||||
id: 'api:plugin.stackstorm.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
@@ -104,7 +104,7 @@ describe('convertLegacyApp', () => {
|
||||
},
|
||||
{
|
||||
id: 'api:plugin.puppetdb.service',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ApiBlueprint,
|
||||
ExtensionBoundary,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
@@ -25,9 +24,8 @@ import {
|
||||
|
||||
export const App = createExtension({
|
||||
namespace: 'app',
|
||||
attachTo: { id: 'root', input: 'default' }, // ignored
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
inputs: {
|
||||
apis: createExtensionInput([ApiBlueprint.dataRefs.factory]),
|
||||
root: createExtensionInput([coreExtensionData.reactElement], {
|
||||
singleton: true,
|
||||
}),
|
||||
|
||||
+20
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
* Copyright 2024 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.
|
||||
@@ -14,4 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createAppTree } from './createAppTree';
|
||||
import {
|
||||
ApiBlueprint,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const Root = createExtension({
|
||||
namespace: 'root',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
app: createExtensionInput([coreExtensionData.reactElement], {
|
||||
singleton: true,
|
||||
}),
|
||||
apis: createExtensionInput([ApiBlueprint.dataRefs.factory]),
|
||||
},
|
||||
output: [],
|
||||
factory: () => [],
|
||||
});
|
||||
@@ -28,9 +28,13 @@ import {
|
||||
createFrontendPlugin,
|
||||
createRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { createAppTree } from '../tree';
|
||||
import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils';
|
||||
|
||||
import { builtinExtensions } from '../wiring/createApp';
|
||||
import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig';
|
||||
import { resolveAppNodeSpecs } from '../tree/resolveAppNodeSpecs';
|
||||
import { resolveAppTree } from '../tree/resolveAppTree';
|
||||
import { instantiateAppNodeTree } from '../tree/instantiateAppNodeTree';
|
||||
|
||||
const ref1 = createRouteRef();
|
||||
const ref2 = createRouteRef();
|
||||
@@ -77,11 +81,18 @@ function routeInfoFromExtensions(extensions: ExtensionDefinition<any, any>[]) {
|
||||
id: 'test',
|
||||
extensions,
|
||||
});
|
||||
const tree = createAppTree({
|
||||
config: new MockConfigApi({}),
|
||||
builtinExtensions,
|
||||
features: [plugin],
|
||||
});
|
||||
|
||||
const tree = resolveAppTree(
|
||||
'app',
|
||||
resolveAppNodeSpecs({
|
||||
features: [plugin],
|
||||
builtinExtensions,
|
||||
parameters: readAppExtensionsConfig(new MockConfigApi({})),
|
||||
forbidden: new Set(['app']),
|
||||
}),
|
||||
);
|
||||
|
||||
instantiateAppNodeTree(tree.root, TestApiRegistry.from());
|
||||
|
||||
return extractRouteInfoFromAppNode(tree.root);
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
createExtension,
|
||||
createExtensionOverrides,
|
||||
createFrontendPlugin,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { createAppTree } from './createAppTree';
|
||||
|
||||
const extBase = {
|
||||
id: 'test',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: [],
|
||||
factory: () => [],
|
||||
};
|
||||
|
||||
describe('createAppTree', () => {
|
||||
it('throws an error when a app extension is parametrized', () => {
|
||||
const config = new MockConfigApi({
|
||||
app: {
|
||||
extensions: [
|
||||
{
|
||||
app: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const features = [
|
||||
createFrontendPlugin({
|
||||
id: 'plugin',
|
||||
extensions: [],
|
||||
}),
|
||||
];
|
||||
expect(() =>
|
||||
createAppTree({ features, config, builtinExtensions: [] }),
|
||||
).toThrow("Configuration of the 'app' extension is forbidden");
|
||||
});
|
||||
|
||||
it('throws an error when a app extension is overridden', () => {
|
||||
const config = new MockConfigApi({});
|
||||
const features = [
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'app',
|
||||
attachTo: { id: 'app/routes', input: 'route' },
|
||||
inputs: {},
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
expect(() =>
|
||||
createAppTree({ features, config, builtinExtensions: [] }),
|
||||
).toThrow(
|
||||
"It is forbidden to override the following extension(s): 'app', which is done by one or more extension overrides",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when duplicated extension overrides are detected', () => {
|
||||
expect(() =>
|
||||
createAppTree({
|
||||
features: [
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({ ...extBase, name: 'a' }),
|
||||
createExtension({ ...extBase, name: 'a' }),
|
||||
createExtension({ ...extBase, name: 'b' }),
|
||||
],
|
||||
}),
|
||||
createExtensionOverrides({
|
||||
extensions: [createExtension({ ...extBase, name: 'b' })],
|
||||
}),
|
||||
],
|
||||
config: new MockConfigApi({}),
|
||||
builtinExtensions: [],
|
||||
}),
|
||||
).toThrow('The following extensions had duplicate overrides: a, b');
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* 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 { Extension, FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { readAppExtensionsConfig } from './readAppExtensionsConfig';
|
||||
import { resolveAppTree } from './resolveAppTree';
|
||||
import { resolveAppNodeSpecs } from './resolveAppNodeSpecs';
|
||||
import { AppTree } from '@backstage/frontend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { instantiateAppNodeTree } from './instantiateAppNodeTree';
|
||||
|
||||
/** @internal */
|
||||
export interface CreateAppTreeOptions {
|
||||
features: FrontendFeature[];
|
||||
builtinExtensions: Extension<any, any>[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createAppTree(options: CreateAppTreeOptions): AppTree {
|
||||
const tree = resolveAppTree(
|
||||
'app',
|
||||
resolveAppNodeSpecs({
|
||||
features: options.features,
|
||||
builtinExtensions: options.builtinExtensions,
|
||||
parameters: readAppExtensionsConfig(options.config),
|
||||
forbidden: new Set(['app']),
|
||||
}),
|
||||
);
|
||||
instantiateAppNodeTree(tree.root);
|
||||
return tree;
|
||||
}
|
||||
@@ -38,8 +38,9 @@ import {
|
||||
} from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { createSchemaFromZod } from '../../../frontend-plugin-api/src/schema/createSchemaFromZod';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import { TestApiRegistry, withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
const testApis = TestApiRegistry.from();
|
||||
const testDataRef = createExtensionDataRef<string>().with({ id: 'test' });
|
||||
const otherDataRef = createExtensionDataRef<number>().with({ id: 'other' });
|
||||
const inputMirrorDataRef = createExtensionDataRef<unknown>().with({
|
||||
@@ -82,6 +83,7 @@ function makeInstanceWithId<TConfig, TConfigInput>(
|
||||
instance: createAppNodeInstance({
|
||||
node,
|
||||
attachments: new Map(),
|
||||
apis: testApis,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -149,12 +151,12 @@ describe('instantiateAppNodeTree', () => {
|
||||
makeSpec(simpleExtension, { id: 'root-node' }),
|
||||
]);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(tree.root.instance?.getData(testDataRef)).toBe('test');
|
||||
|
||||
// Multiple calls should have no effect
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -163,7 +165,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
makeSpec(simpleExtension, { id: 'root-node', disabled: true }),
|
||||
]);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
});
|
||||
|
||||
@@ -195,7 +197,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({
|
||||
@@ -205,7 +207,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
});
|
||||
|
||||
// Multiple calls should have no effect
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
});
|
||||
@@ -243,7 +245,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({
|
||||
@@ -257,6 +259,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
const instance = createAppNodeInstance({
|
||||
node: makeNode(simpleExtension),
|
||||
attachments,
|
||||
apis: testApis,
|
||||
});
|
||||
|
||||
expect(Array.from(instance.getDataRefs())).toEqual([
|
||||
@@ -297,6 +300,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
],
|
||||
]);
|
||||
const instance = createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments,
|
||||
node: makeNode(
|
||||
createV1Extension({
|
||||
@@ -365,6 +369,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an extension with invalid config', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(simpleExtension, {
|
||||
config: { other: 'not-a-number' },
|
||||
}),
|
||||
@@ -378,6 +383,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should forward extension factory errors', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
@@ -401,6 +407,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with duplicate output', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
@@ -425,6 +432,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with disconnected output data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
@@ -448,6 +456,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with missing required input', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
@@ -475,6 +484,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should warn when creating an instance with undeclared inputs', () => {
|
||||
const { warn } = withLogCollector(['warn'], () =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'declared',
|
||||
@@ -518,6 +528,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple undeclared inputs', () => {
|
||||
const { warn } = withLogCollector(['warn'], () =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'undeclared1',
|
||||
@@ -552,6 +563,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple inputs for required singleton', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
@@ -587,6 +599,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple inputs for optional singleton', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
@@ -622,6 +635,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple inputs that did not provide required data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
@@ -713,12 +727,12 @@ describe('instantiateAppNodeTree', () => {
|
||||
makeSpec(simpleExtension, { id: 'root-node' }),
|
||||
]);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(tree.root.instance?.getData(testDataRef)).toBe('test');
|
||||
|
||||
// Multiple calls should have no effect
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -727,7 +741,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
makeSpec(simpleExtension, { id: 'root-node', disabled: true }),
|
||||
]);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
});
|
||||
|
||||
@@ -757,7 +771,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({
|
||||
@@ -765,7 +779,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
});
|
||||
|
||||
// Multiple calls should have no effect
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).toBeDefined();
|
||||
});
|
||||
@@ -801,7 +815,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
expect(tree.root.instance).not.toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, testApis);
|
||||
expect(tree.root.instance).toBeDefined();
|
||||
expect(childNode?.instance).not.toBeDefined();
|
||||
expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({
|
||||
@@ -815,6 +829,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
const instance = createAppNodeInstance({
|
||||
node: makeNode(simpleExtension),
|
||||
attachments,
|
||||
apis: testApis,
|
||||
});
|
||||
|
||||
expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]);
|
||||
@@ -854,6 +869,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
|
||||
const instance = createAppNodeInstance({
|
||||
attachments,
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
@@ -912,6 +928,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an extension with invalid config', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(simpleExtension, {
|
||||
config: { other: 'not-a-number' },
|
||||
}),
|
||||
@@ -925,6 +942,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should forward extension factory errors', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
@@ -950,6 +968,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with duplicate output', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
@@ -973,6 +992,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance without required', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
@@ -996,6 +1016,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with unknown output data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
// @ts-expect-error
|
||||
@@ -1020,6 +1041,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with missing required input', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
@@ -1046,6 +1068,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should warn when creating an instance with undeclared inputs', () => {
|
||||
const { warn } = withLogCollector(['warn'], () =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'declared',
|
||||
@@ -1089,6 +1112,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple undeclared inputs', () => {
|
||||
const { warn } = withLogCollector(['warn'], () =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'undeclared1',
|
||||
@@ -1125,6 +1149,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple inputs for required singleton', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
@@ -1159,6 +1184,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple inputs for optional singleton', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
@@ -1194,6 +1220,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should refuse to create an instance with multiple inputs that did not provide required data', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import {
|
||||
AnyExtensionDataRef,
|
||||
ApiHolder,
|
||||
ExtensionDataContainer,
|
||||
ExtensionDataRef,
|
||||
ExtensionInput,
|
||||
@@ -242,9 +243,10 @@ function resolveV2Inputs(
|
||||
/** @internal */
|
||||
export function createAppNodeInstance(options: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
attachments: ReadonlyMap<string, AppNode[]>;
|
||||
}): AppNodeInstance {
|
||||
const { node, attachments } = options;
|
||||
const { node, apis, attachments } = options;
|
||||
const { id, extension, config } = node.spec;
|
||||
const extensionData = new Map<string, unknown>();
|
||||
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
|
||||
@@ -268,6 +270,7 @@ export function createAppNodeInstance(options: {
|
||||
if (internalExtension.version === 'v1') {
|
||||
const namedOutputs = internalExtension.factory({
|
||||
node,
|
||||
apis,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV1Inputs(internalExtension.inputs, attachments),
|
||||
});
|
||||
@@ -288,6 +291,7 @@ export function createAppNodeInstance(options: {
|
||||
} else if (internalExtension.version === 'v2') {
|
||||
const outputDataValues = internalExtension.factory({
|
||||
node,
|
||||
apis,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV2Inputs(internalExtension.inputs, attachments),
|
||||
});
|
||||
@@ -349,7 +353,10 @@ export function createAppNodeInstance(options: {
|
||||
* Starting at the provided node, instantiate all reachable nodes in the tree that have not been disabled.
|
||||
* @internal
|
||||
*/
|
||||
export function instantiateAppNodeTree(rootNode: AppNode): void {
|
||||
export function instantiateAppNodeTree(
|
||||
rootNode: AppNode,
|
||||
apis: ApiHolder,
|
||||
): void {
|
||||
function createInstance(node: AppNode): AppNodeInstance | undefined {
|
||||
if (node.instance) {
|
||||
return node.instance;
|
||||
@@ -375,6 +382,7 @@ export function instantiateAppNodeTree(rootNode: AppNode): void {
|
||||
|
||||
(node as Mutable<AppNode>).instance = createAppNodeInstance({
|
||||
node,
|
||||
apis,
|
||||
attachments: instantiatedAttachments,
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/res
|
||||
/** @internal */
|
||||
export function resolveAppNodeSpecs(options: {
|
||||
features?: FrontendFeature[];
|
||||
builtinExtensions?: Extension<unknown>[];
|
||||
builtinExtensions?: Extension<any, any>[];
|
||||
parameters?: Array<ExtensionParameters>;
|
||||
forbidden?: Set<string>;
|
||||
}): AppNodeSpec[] {
|
||||
|
||||
@@ -255,28 +255,32 @@ describe('createApp', () => {
|
||||
const { tree } = appTreeApi!.getTree();
|
||||
|
||||
expect(String(tree.root)).toMatchInlineSnapshot(`
|
||||
"<app out=[core.reactElement]>
|
||||
root [
|
||||
<app/root out=[core.reactElement]>
|
||||
children [
|
||||
<app/layout out=[core.reactElement]>
|
||||
content [
|
||||
<app/routes out=[core.reactElement]>
|
||||
routes [
|
||||
<page:my-plugin out=[core.routing.path, core.reactElement] />
|
||||
"<root>
|
||||
app [
|
||||
<app out=[core.reactElement]>
|
||||
root [
|
||||
<app/root out=[core.reactElement]>
|
||||
children [
|
||||
<app/layout out=[core.reactElement]>
|
||||
content [
|
||||
<app/routes out=[core.reactElement]>
|
||||
routes [
|
||||
<page:my-plugin out=[core.routing.path, core.reactElement] />
|
||||
]
|
||||
</app/routes>
|
||||
]
|
||||
</app/routes>
|
||||
nav [
|
||||
<app/nav out=[core.reactElement] />
|
||||
]
|
||||
</app/layout>
|
||||
]
|
||||
nav [
|
||||
<app/nav out=[core.reactElement] />
|
||||
elements [
|
||||
<app-root-element:app/oauth-request-dialog out=[core.reactElement] />
|
||||
<app-root-element:app/alert-display out=[core.reactElement] />
|
||||
]
|
||||
</app/layout>
|
||||
</app/root>
|
||||
]
|
||||
elements [
|
||||
<app-root-element:app/oauth-request-dialog out=[core.reactElement] />
|
||||
<app-root-element:app/alert-display out=[core.reactElement] />
|
||||
]
|
||||
</app/root>
|
||||
</app>
|
||||
]
|
||||
apis [
|
||||
<api:app-theme out=[core.api.factory]>
|
||||
@@ -315,7 +319,7 @@ describe('createApp', () => {
|
||||
<api:core.auth.vmware-cloud out=[core.api.factory] />
|
||||
<api:plugin.permission.api out=[core.api.factory] />
|
||||
]
|
||||
</app>"
|
||||
</root>"
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@@ -19,10 +19,18 @@ import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ApiBlueprint,
|
||||
AppTree,
|
||||
AppTreeApi,
|
||||
appTreeApiRef,
|
||||
coreExtensionData,
|
||||
FrontendFeature,
|
||||
RouteRef,
|
||||
ExternalRouteRef,
|
||||
SubRouteRef,
|
||||
AnyRouteRefParams,
|
||||
RouteFunc,
|
||||
RouteResolutionApiResolveOptions,
|
||||
RouteResolutionApi,
|
||||
createApiFactory,
|
||||
routeResolutionApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { App } from '../extensions/App';
|
||||
@@ -73,7 +81,6 @@ import { CreateAppRouteBinder } from '../routing';
|
||||
import { RouteResolver } from '../routing/RouteResolver';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
import { collectRouteIds } from '../routing/collectRouteIds';
|
||||
import { createAppTree } from '../tree';
|
||||
import {
|
||||
DefaultProgressComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
@@ -93,12 +100,20 @@ import { TranslationsApi } from '../extensions/TranslationsApi';
|
||||
import { ComponentsApi } from '../extensions/ComponentsApi';
|
||||
import { AppLanguageApi } from '../extensions/AppLanguageApi';
|
||||
import { FeatureFlagsApi } from '../extensions/FeatureFlagsApi';
|
||||
import { Root } from '../extensions/Root';
|
||||
import { resolveAppTree } from '../tree/resolveAppTree';
|
||||
import { resolveAppNodeSpecs } from '../tree/resolveAppNodeSpecs';
|
||||
import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig';
|
||||
import { instantiateAppNodeTree } from '../tree/instantiateAppNodeTree';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry';
|
||||
|
||||
const DefaultApis = defaultApis.map(factory =>
|
||||
ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }),
|
||||
);
|
||||
|
||||
export const builtinExtensions = [
|
||||
Root,
|
||||
App,
|
||||
AppRoot,
|
||||
AppRoutes,
|
||||
@@ -230,6 +245,70 @@ export function createApp(options?: {
|
||||
};
|
||||
}
|
||||
|
||||
// Helps delay callers from reaching out to the API before the app tree has been materialized
|
||||
class AppTreeApiProxy implements AppTreeApi {
|
||||
#safeToUse: boolean = false;
|
||||
|
||||
constructor(private readonly tree: AppTree) {}
|
||||
|
||||
getTree() {
|
||||
if (!this.#safeToUse) {
|
||||
throw new Error(
|
||||
`You can't access the AppTreeApi during initialization of the app tree. Please move occurrences of this out of the initialization of the factory`,
|
||||
);
|
||||
}
|
||||
return { tree: this.tree };
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.#safeToUse = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Helps delay callers from reaching out to the API before the app tree has been materialized
|
||||
class RouteResolutionApiProxy implements RouteResolutionApi {
|
||||
#delegate: RouteResolutionApi | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly tree: AppTree,
|
||||
private readonly routeBindings: Map<
|
||||
ExternalRouteRef,
|
||||
RouteRef | SubRouteRef
|
||||
>,
|
||||
private readonly basePath: string,
|
||||
) {}
|
||||
|
||||
resolve<TParams extends AnyRouteRefParams>(
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams>,
|
||||
options?: RouteResolutionApiResolveOptions,
|
||||
): RouteFunc<TParams> | undefined {
|
||||
if (!this.#delegate) {
|
||||
throw new Error(
|
||||
`You can't access the RouteResolver during initialization of the app tree. Please move occurrences of this out of the initialization of the factory`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.#delegate.resolve(anyRouteRef, options);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
const routeInfo = extractRouteInfoFromAppNode(this.tree.root);
|
||||
|
||||
this.#delegate = new RouteResolver(
|
||||
routeInfo.routePaths,
|
||||
routeInfo.routeParents,
|
||||
routeInfo.routeObjects,
|
||||
this.routeBindings,
|
||||
this.basePath,
|
||||
);
|
||||
|
||||
return routeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous version of {@link createApp}, expecting all features and
|
||||
* config to have been loaded already.
|
||||
@@ -248,32 +327,46 @@ export function createSpecializedApp(options?: {
|
||||
|
||||
const features = deduplicateFeatures(duplicatedFeatures);
|
||||
|
||||
const tree = createAppTree({
|
||||
features,
|
||||
builtinExtensions,
|
||||
config,
|
||||
});
|
||||
const tree = resolveAppTree(
|
||||
'root',
|
||||
resolveAppNodeSpecs({
|
||||
features,
|
||||
builtinExtensions,
|
||||
parameters: readAppExtensionsConfig(config),
|
||||
forbidden: new Set(['root']),
|
||||
}),
|
||||
);
|
||||
|
||||
const routeInfo = extractRouteInfoFromAppNode(tree.root);
|
||||
const routeBindings = resolveRouteBindings(
|
||||
options?.bindRoutes,
|
||||
config,
|
||||
collectRouteIds(features),
|
||||
const factories = createApiFactories({ tree });
|
||||
|
||||
const appTreeApi = new AppTreeApiProxy(tree);
|
||||
const routeResolutionApi = new RouteResolutionApiProxy(
|
||||
tree,
|
||||
resolveRouteBindings(
|
||||
options?.bindRoutes,
|
||||
config,
|
||||
collectRouteIds(features),
|
||||
),
|
||||
getBasePath(config),
|
||||
);
|
||||
|
||||
const appIdentityProxy = new AppIdentityProxy();
|
||||
const apiHolder = createApiHolder(
|
||||
tree,
|
||||
config,
|
||||
appIdentityProxy,
|
||||
new RouteResolver(
|
||||
routeInfo.routePaths,
|
||||
routeInfo.routeParents,
|
||||
routeInfo.routeObjects,
|
||||
routeBindings,
|
||||
getBasePath(config),
|
||||
),
|
||||
);
|
||||
const apiHolder = createApiHolder({
|
||||
factories,
|
||||
staticFactories: [
|
||||
createApiFactory(appTreeApiRef, appTreeApi),
|
||||
createApiFactory(configApiRef, config),
|
||||
createApiFactory(routeResolutionApiRef, routeResolutionApi),
|
||||
createApiFactory(identityApiRef, appIdentityProxy),
|
||||
],
|
||||
});
|
||||
|
||||
for (const appNode of tree.root.edges.attachments.get('app') ?? []) {
|
||||
instantiateAppNodeTree(appNode, apiHolder);
|
||||
}
|
||||
|
||||
const routeInfo = routeResolutionApi.initialize();
|
||||
appTreeApi.initialize();
|
||||
|
||||
if (isProtectedApp()) {
|
||||
const discoveryApi = apiHolder.get(discoveryApiRef);
|
||||
@@ -310,7 +403,9 @@ export function createSpecializedApp(options?: {
|
||||
}
|
||||
}
|
||||
|
||||
const rootEl = tree.root.instance!.getData(coreExtensionData.reactElement);
|
||||
const rootEl = tree.root.edges.attachments
|
||||
.get('app')![0]
|
||||
.instance!.getData(coreExtensionData.reactElement);
|
||||
|
||||
const AppComponent = () => (
|
||||
<ApiProvider apis={apiHolder}>
|
||||
@@ -331,49 +426,37 @@ export function createSpecializedApp(options?: {
|
||||
};
|
||||
}
|
||||
|
||||
function createApiHolder(
|
||||
tree: AppTree,
|
||||
configApi: ConfigApi,
|
||||
appIdentityProxy: AppIdentityProxy,
|
||||
routeResolutionApi: RouteResolutionApi,
|
||||
): ApiHolder {
|
||||
function createApiFactories(options: { tree: AppTree }): AnyApiFactory[] {
|
||||
const emptyApiHolder = ApiRegistry.from([]);
|
||||
const factories = new Array<AnyApiFactory>();
|
||||
|
||||
for (const apiNode of options.tree.root.edges.attachments.get('apis') ?? []) {
|
||||
instantiateAppNodeTree(apiNode, emptyApiHolder);
|
||||
const apiFactory = apiNode.instance?.getData(ApiBlueprint.dataRefs.factory);
|
||||
if (!apiFactory) {
|
||||
throw new Error(
|
||||
`No API factory found in for extension ${apiNode.spec.id}`,
|
||||
);
|
||||
}
|
||||
factories.push(apiFactory);
|
||||
}
|
||||
|
||||
return factories;
|
||||
}
|
||||
|
||||
function createApiHolder(options: {
|
||||
factories: AnyApiFactory[];
|
||||
staticFactories: AnyApiFactory[];
|
||||
}): ApiHolder {
|
||||
const factoryRegistry = new ApiFactoryRegistry();
|
||||
|
||||
const pluginApis =
|
||||
tree.root.edges.attachments
|
||||
.get('apis')
|
||||
?.map(e => e.instance?.getData(ApiBlueprint.dataRefs.factory))
|
||||
.filter((x): x is AnyApiFactory => !!x) ?? [];
|
||||
|
||||
for (const factory of pluginApis) {
|
||||
for (const factory of options.factories) {
|
||||
factoryRegistry.register('default', factory);
|
||||
}
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: identityApiRef,
|
||||
deps: {},
|
||||
factory: () => appIdentityProxy,
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: appTreeApiRef,
|
||||
deps: {},
|
||||
factory: () => ({
|
||||
getTree: () => ({ tree }),
|
||||
}),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: routeResolutionApiRef,
|
||||
deps: {},
|
||||
factory: () => routeResolutionApi,
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: configApiRef,
|
||||
deps: {},
|
||||
factory: () => configApi,
|
||||
});
|
||||
for (const factory of options.staticFactories) {
|
||||
factoryRegistry.register('static', factory);
|
||||
}
|
||||
|
||||
ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis());
|
||||
|
||||
|
||||
@@ -650,6 +650,7 @@ export type CreateExtensionBlueprintOptions<
|
||||
params: TParams,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
};
|
||||
@@ -730,6 +731,7 @@ export type CreateExtensionOptions<
|
||||
};
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
};
|
||||
@@ -953,6 +955,7 @@ export interface ExtensionBlueprint<
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: TConfig & {
|
||||
[key in keyof TExtensionConfigSchema]: z.infer<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
@@ -1145,6 +1148,7 @@ export interface ExtensionDefinition<
|
||||
}) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: TConfig & {
|
||||
[key in keyof TExtensionConfigSchema]: z.infer<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('ApiBlueprint', () => {
|
||||
{
|
||||
"$$type": "@backstage/ExtensionDefinition",
|
||||
"attachTo": {
|
||||
"id": "app",
|
||||
"id": "root",
|
||||
"input": "apis",
|
||||
},
|
||||
"configSchema": undefined,
|
||||
@@ -87,7 +87,7 @@ describe('ApiBlueprint', () => {
|
||||
{
|
||||
"$$type": "@backstage/ExtensionDefinition",
|
||||
"attachTo": {
|
||||
"id": "app",
|
||||
"id": "root",
|
||||
"input": "apis",
|
||||
},
|
||||
"configSchema": {
|
||||
|
||||
@@ -28,7 +28,7 @@ const factoryDataRef = createExtensionDataRef<AnyApiFactory>().with({
|
||||
*/
|
||||
export const ApiBlueprint = createExtensionBlueprint({
|
||||
kind: 'api',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
attachTo: { id: 'root', input: 'apis' },
|
||||
output: [factoryDataRef],
|
||||
dataRefs: {
|
||||
factory: factoryDataRef,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppNode } from '../apis';
|
||||
import { ApiHolder, AppNode } from '../apis';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { Expand } from '../types';
|
||||
import {
|
||||
@@ -133,6 +133,7 @@ export type CreateExtensionOptions<
|
||||
};
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
};
|
||||
@@ -203,6 +204,7 @@ export interface ExtensionDefinition<
|
||||
}) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: TConfig & {
|
||||
[key in keyof TExtensionConfigSchema]: z.infer<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
@@ -273,6 +275,7 @@ export type InternalExtensionDefinition<
|
||||
};
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: TConfig;
|
||||
inputs: {
|
||||
[inputName in string]: unknown;
|
||||
@@ -292,6 +295,7 @@ export type InternalExtensionDefinition<
|
||||
readonly output: Array<AnyExtensionDataRef>;
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: TConfig;
|
||||
inputs: ResolvedExtensionInputs<{
|
||||
[inputName in string]: ExtensionInput<
|
||||
@@ -437,10 +441,11 @@ export function createExtension<
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
factory: ({ node, config, inputs }) => {
|
||||
factory: ({ node, apis, config, inputs }) => {
|
||||
if (!overrideOptions.factory) {
|
||||
return newOptions.factory({
|
||||
node,
|
||||
apis,
|
||||
config: config as any,
|
||||
inputs: inputs as any,
|
||||
});
|
||||
@@ -450,6 +455,7 @@ export function createExtension<
|
||||
return createExtensionDataContainer<UOutput>(
|
||||
newOptions.factory({
|
||||
node,
|
||||
apis,
|
||||
config: (innerContext?.config ?? config) as any,
|
||||
inputs: resolveInputOverrides(
|
||||
newOptions.inputs,
|
||||
@@ -462,6 +468,7 @@ export function createExtension<
|
||||
},
|
||||
{
|
||||
node,
|
||||
apis,
|
||||
config: config as any,
|
||||
inputs: inputs as any,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppNode } from '../apis';
|
||||
import { ApiHolder, AppNode } from '../apis';
|
||||
import { Expand } from '../types';
|
||||
import {
|
||||
ExtensionDefinition,
|
||||
@@ -70,6 +70,7 @@ export type CreateExtensionBlueprintOptions<
|
||||
params: TParams,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
};
|
||||
@@ -172,6 +173,7 @@ export interface ExtensionBlueprint<
|
||||
) => ExtensionDataContainer<UOutput>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config: TConfig & {
|
||||
[key in keyof TExtensionConfigSchema]: z.infer<
|
||||
ReturnType<TExtensionConfigSchema[key]>
|
||||
@@ -300,11 +302,12 @@ export function createExtensionBlueprint<
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
factory: ({ node, config, inputs }) => {
|
||||
factory: ({ node, config, inputs, apis }) => {
|
||||
return args.factory(
|
||||
(innerParams, innerContext) => {
|
||||
return createExtensionDataContainer<UOutput>(
|
||||
options.factory(innerParams, {
|
||||
apis,
|
||||
node,
|
||||
config: (innerContext?.config ?? config) as any,
|
||||
inputs: resolveInputOverrides(
|
||||
@@ -317,6 +320,7 @@ export function createExtensionBlueprint<
|
||||
);
|
||||
},
|
||||
{
|
||||
apis,
|
||||
node,
|
||||
config: config as any,
|
||||
inputs: inputs as any,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppNode } from '../apis';
|
||||
import { ApiHolder, AppNode } from '../apis';
|
||||
import {
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInputs,
|
||||
@@ -57,6 +57,7 @@ export type InternalExtension<TConfig, TConfigInput> = Extension<
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
factory(context: {
|
||||
apis: ApiHolder;
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: {
|
||||
@@ -76,6 +77,7 @@ export type InternalExtension<TConfig, TConfigInput> = Extension<
|
||||
};
|
||||
readonly output: Array<AnyExtensionDataRef>;
|
||||
factory(options: {
|
||||
apis: ApiHolder;
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: ResolvedExtensionInputs<{
|
||||
|
||||
@@ -49,6 +49,7 @@ import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveA
|
||||
import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
|
||||
const NavItem = (props: {
|
||||
routeRef: RouteRef<undefined>;
|
||||
@@ -320,7 +321,7 @@ export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
}),
|
||||
);
|
||||
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, TestApiRegistry.from());
|
||||
|
||||
this.#tree = tree;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user