chore: move to having a root extension with apis and app inputs
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Co-authored-by: Fredrik Adelöw <freben@users.noreply.github.com> Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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 {
|
||||
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: () => [],
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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 RouteResolverProxy 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 routeResolver = new RouteResolverProxy(
|
||||
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, routeResolver),
|
||||
createApiFactory(identityApiRef, appIdentityProxy),
|
||||
],
|
||||
});
|
||||
|
||||
for (const appNode of tree.root.edges.attachments.get('app') ?? []) {
|
||||
instantiateAppNodeTree(appNode, apiHolder);
|
||||
}
|
||||
|
||||
const routeInfo = routeResolver.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());
|
||||
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -50,6 +50,9 @@ import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/insta
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig';
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry';
|
||||
|
||||
const NavItem = (props: {
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
@@ -320,7 +323,7 @@ export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
}),
|
||||
);
|
||||
|
||||
instantiateAppNodeTree(tree.root);
|
||||
instantiateAppNodeTree(tree.root, ApiRegistry.from([]));
|
||||
|
||||
this.#tree = tree;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user