Initial implementation of an extension factory middleware
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
'@backstage/frontend-defaults': patch
|
||||
---
|
||||
|
||||
It's now possible to provide a middleware that wraps all extension factories by passing an `extensionFactoryMiddleware` to either `createApp()` or `createSpecializedApp()`.
|
||||
@@ -3,7 +3,10 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { AppTree } from '@backstage/frontend-plugin-api';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendModule } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
@@ -29,10 +32,19 @@ export function createSpecializedApp(options?: {
|
||||
features?: FrontendFeature[];
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
apis?: ApiHolder;
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
}): {
|
||||
apis: ApiHolder;
|
||||
createRoot(): JSX_2.Element;
|
||||
tree: AppTree;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ExtensionFactoryMiddleware = Parameters<
|
||||
ExtensionDefinition['override']
|
||||
>[0]['factory'];
|
||||
|
||||
// @public (undocumented)
|
||||
export type FrontendFeature =
|
||||
| FrontendPlugin
|
||||
|
||||
@@ -26,6 +26,8 @@ import mapValues from 'lodash/mapValues';
|
||||
import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
import { ExtensionFactoryMiddleware } from '../wiring';
|
||||
import { createExtensionDataContainer } from '@internal/frontend';
|
||||
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
@@ -242,6 +244,7 @@ function resolveV2Inputs(
|
||||
|
||||
/** @internal */
|
||||
export function createAppNodeInstance(options: {
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
attachments: ReadonlyMap<string, AppNode[]>;
|
||||
@@ -251,9 +254,11 @@ export function createAppNodeInstance(options: {
|
||||
const extensionData = new Map<string, unknown>();
|
||||
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
|
||||
|
||||
let parsedConfig: unknown;
|
||||
let parsedConfig: { [x: string]: any };
|
||||
try {
|
||||
parsedConfig = extension.configSchema?.parse(config ?? {});
|
||||
parsedConfig = extension.configSchema?.parse(config ?? {}) as {
|
||||
[x: string]: any;
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid configuration for extension '${id}'; caused by ${e}`,
|
||||
@@ -289,12 +294,30 @@ export function createAppNodeInstance(options: {
|
||||
extensionDataRefs.add(ref);
|
||||
}
|
||||
} else if (internalExtension.version === 'v2') {
|
||||
const outputDataValues = internalExtension.factory({
|
||||
const context = {
|
||||
node,
|
||||
apis,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV2Inputs(internalExtension.inputs, attachments),
|
||||
});
|
||||
};
|
||||
const outputDataValues = options.extensionFactoryMiddleware
|
||||
? createExtensionDataContainer(
|
||||
options.extensionFactoryMiddleware(
|
||||
({ config: configOverride } = {}) =>
|
||||
createExtensionDataContainer(
|
||||
internalExtension.factory(
|
||||
configOverride
|
||||
? {
|
||||
...context,
|
||||
config: configOverride,
|
||||
}
|
||||
: context,
|
||||
),
|
||||
),
|
||||
context,
|
||||
),
|
||||
)
|
||||
: internalExtension.factory(context);
|
||||
|
||||
const outputDataMap = new Map<string, unknown>();
|
||||
for (const value of outputDataValues) {
|
||||
@@ -356,6 +379,7 @@ export function createAppNodeInstance(options: {
|
||||
export function instantiateAppNodeTree(
|
||||
rootNode: AppNode,
|
||||
apis: ApiHolder,
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware,
|
||||
): void {
|
||||
function createInstance(node: AppNode): AppNodeInstance | undefined {
|
||||
if (node.instance) {
|
||||
@@ -381,6 +405,7 @@ export function instantiateAppNodeTree(
|
||||
}
|
||||
|
||||
(node as Mutable<AppNode>).instance = createAppNodeInstance({
|
||||
extensionFactoryMiddleware,
|
||||
node,
|
||||
apis,
|
||||
attachments: instantiatedAttachments,
|
||||
|
||||
@@ -69,7 +69,11 @@ import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
|
||||
import { BackstageRouteObject } from '../routing/types';
|
||||
import { FrontendFeature, RouteInfo } from './types';
|
||||
import {
|
||||
ExtensionFactoryMiddleware,
|
||||
FrontendFeature,
|
||||
RouteInfo,
|
||||
} from './types';
|
||||
import { matchRoutes } from 'react-router-dom';
|
||||
|
||||
function deduplicateFeatures(
|
||||
@@ -198,7 +202,9 @@ export function createSpecializedApp(options?: {
|
||||
features?: FrontendFeature[];
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
}): { createRoot(): JSX.Element } {
|
||||
apis?: ApiHolder;
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
}): { apis: ApiHolder; createRoot(): JSX.Element; tree: AppTree } {
|
||||
const config = options?.config ?? new ConfigReader({}, 'empty-config');
|
||||
const features = deduplicateFeatures(options?.features ?? []);
|
||||
|
||||
@@ -227,15 +233,17 @@ export function createSpecializedApp(options?: {
|
||||
);
|
||||
|
||||
const appIdentityProxy = new AppIdentityProxy();
|
||||
const apiHolder = createApiHolder({
|
||||
factories,
|
||||
staticFactories: [
|
||||
createApiFactory(appTreeApiRef, appTreeApi),
|
||||
createApiFactory(configApiRef, config),
|
||||
createApiFactory(routeResolutionApiRef, routeResolutionApi),
|
||||
createApiFactory(identityApiRef, appIdentityProxy),
|
||||
],
|
||||
});
|
||||
const apiHolder =
|
||||
options?.apis ??
|
||||
createApiHolder({
|
||||
factories,
|
||||
staticFactories: [
|
||||
createApiFactory(appTreeApiRef, appTreeApi),
|
||||
createApiFactory(configApiRef, config),
|
||||
createApiFactory(routeResolutionApiRef, routeResolutionApi),
|
||||
createApiFactory(identityApiRef, appIdentityProxy),
|
||||
],
|
||||
});
|
||||
|
||||
const featureFlagApi = apiHolder.get(featureFlagsApiRef);
|
||||
if (featureFlagApi) {
|
||||
@@ -260,7 +268,11 @@ export function createSpecializedApp(options?: {
|
||||
}
|
||||
|
||||
// Now instantiate the entire tree, which will skip anything that's already been instantiated
|
||||
instantiateAppNodeTree(tree.root, apiHolder);
|
||||
instantiateAppNodeTree(
|
||||
tree.root,
|
||||
apiHolder,
|
||||
options?.extensionFactoryMiddleware,
|
||||
);
|
||||
|
||||
const routeInfo = extractRouteInfoFromAppNode(tree.root);
|
||||
|
||||
@@ -272,6 +284,8 @@ export function createSpecializedApp(options?: {
|
||||
const AppComponent = () => rootEl;
|
||||
|
||||
return {
|
||||
apis: apiHolder,
|
||||
tree,
|
||||
createRoot() {
|
||||
return <AppComponent />;
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition, RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendModule, FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { BackstageRouteObject } from '../routing/types';
|
||||
|
||||
@@ -31,3 +31,8 @@ export type RouteInfo = {
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
routeObjects: BackstageRouteObject[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ExtensionFactoryMiddleware = Parameters<
|
||||
ExtensionDefinition['override']
|
||||
>[0]['factory'];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
import { ConfigApi } from '@backstage/frontend-plugin-api';
|
||||
import { CreateAppRouteBinder } from '@backstage/frontend-app-api';
|
||||
import { ExtensionFactoryMiddleware } from '@backstage/frontend-app-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-app-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
@@ -32,6 +33,8 @@ export interface CreateAppOptions {
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
// (undocumented)
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
// (undocumented)
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
loadingComponent?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ConfigReader } from '@backstage/config';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
import {
|
||||
CreateAppRouteBinder,
|
||||
ExtensionFactoryMiddleware,
|
||||
FrontendFeature,
|
||||
createSpecializedApp,
|
||||
} from '@backstage/frontend-app-api';
|
||||
@@ -65,6 +66,7 @@ export interface CreateAppOptions {
|
||||
* If set to "null" then no loading fallback component is rendered. *
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,6 +113,7 @@ export function createApp(options?: CreateAppOptions): {
|
||||
config,
|
||||
features: [appPlugin, ...discoveredFeatures, ...providedFeatures],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
extensionFactoryMiddleware: options?.extensionFactoryMiddleware,
|
||||
}).createRoot();
|
||||
|
||||
return { default: () => app };
|
||||
|
||||
Reference in New Issue
Block a user