frontend-app-api: support array of extension factory middlewares
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added new `ExtensionMiddlewareFactory` type.
|
||||
@@ -6,7 +6,7 @@
|
||||
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 { ExtensionFactoryMiddleware } 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';
|
||||
@@ -32,17 +32,14 @@ export function createSpecializedApp(options?: {
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
apis?: ApiHolder;
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
}): {
|
||||
apis: ApiHolder;
|
||||
tree: AppTree;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ExtensionFactoryMiddleware = Parameters<
|
||||
ExtensionDefinition['override']
|
||||
>[0]['factory'];
|
||||
|
||||
// @public (undocumented)
|
||||
export type FrontendFeature =
|
||||
| FrontendPlugin
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ApiHolder,
|
||||
ExtensionDataContainer,
|
||||
ExtensionDataRef,
|
||||
ExtensionFactoryMiddleware,
|
||||
ExtensionInput,
|
||||
ResolvedExtensionInputs,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
@@ -26,7 +27,6 @@ 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> = {
|
||||
@@ -302,20 +302,16 @@ export function createAppNodeInstance(options: {
|
||||
};
|
||||
const outputDataValues = options.extensionFactoryMiddleware
|
||||
? createExtensionDataContainer(
|
||||
options.extensionFactoryMiddleware(
|
||||
({ config: configOverride } = {}) =>
|
||||
createExtensionDataContainer(
|
||||
internalExtension.factory(
|
||||
configOverride
|
||||
? {
|
||||
...context,
|
||||
config: configOverride,
|
||||
}
|
||||
: context,
|
||||
),
|
||||
),
|
||||
context,
|
||||
),
|
||||
options.extensionFactoryMiddleware(overrideContext => {
|
||||
return createExtensionDataContainer(
|
||||
internalExtension.factory({
|
||||
node: context.node,
|
||||
apis: context.apis,
|
||||
inputs: context.inputs,
|
||||
config: overrideContext?.config ?? context.config,
|
||||
}),
|
||||
);
|
||||
}, context),
|
||||
)
|
||||
: internalExtension.factory(context);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
createExtensionInput,
|
||||
useRouteRef,
|
||||
analyticsApiRef,
|
||||
createExtensionDataRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { createSpecializedApp } from './createSpecializedApp';
|
||||
@@ -603,4 +604,71 @@ describe('createSpecializedApp', () => {
|
||||
</root>"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should apply multiple middlewares in order', () => {
|
||||
const textDataRef = createExtensionDataRef<string>().with({ id: 'text' });
|
||||
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createFrontendPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createExtension({
|
||||
attachTo: { id: 'root', input: 'app' },
|
||||
inputs: {
|
||||
text: createExtensionInput([textDataRef], {
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: ({ inputs }) => [
|
||||
coreExtensionData.reactElement(
|
||||
<>{inputs.text.get(textDataRef)}</>,
|
||||
),
|
||||
],
|
||||
}),
|
||||
createExtension({
|
||||
name: 'child',
|
||||
attachTo: { id: 'test', input: 'text' },
|
||||
config: {
|
||||
schema: {
|
||||
text: z => z.string().default('test'),
|
||||
},
|
||||
},
|
||||
output: [textDataRef],
|
||||
factory: ({ config }) => [textDataRef(config.text)],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
extensionFactoryMiddleware: [
|
||||
function* middleware(originalFactory, { config }) {
|
||||
const result = originalFactory({
|
||||
config: config && { text: `1-${config.text}` },
|
||||
});
|
||||
yield* result;
|
||||
const el = result.get(textDataRef);
|
||||
if (el) {
|
||||
yield textDataRef(`${el}-1`);
|
||||
}
|
||||
},
|
||||
function* middleware(originalFactory, { config }) {
|
||||
const result = originalFactory({
|
||||
config: config && { text: `2-${config.text}` },
|
||||
});
|
||||
yield* result;
|
||||
const el = result.get(textDataRef);
|
||||
if (el) {
|
||||
yield textDataRef(`${el}-2`);
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const root = app.tree.root.instance!.getData(
|
||||
coreExtensionData.reactElement,
|
||||
);
|
||||
|
||||
expect(render(root).container.textContent).toBe('1-2-test-1-2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
createApiFactory,
|
||||
routeResolutionApiRef,
|
||||
AppNode,
|
||||
ExtensionFactoryMiddleware,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
@@ -40,10 +41,16 @@ import {
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ApiFactoryRegistry, ApiResolver } from '@backstage/core-app-api';
|
||||
import { OpaqueFrontendPlugin } from '@internal/frontend';
|
||||
import {
|
||||
createExtensionDataContainer,
|
||||
OpaqueFrontendPlugin,
|
||||
} from '@internal/frontend';
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
import {
|
||||
resolveExtensionDefinition,
|
||||
toInternalExtension,
|
||||
} from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
|
||||
import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode';
|
||||
|
||||
@@ -67,11 +74,7 @@ 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 {
|
||||
ExtensionFactoryMiddleware,
|
||||
FrontendFeature,
|
||||
RouteInfo,
|
||||
} from './types';
|
||||
import { FrontendFeature, RouteInfo } from './types';
|
||||
import { matchRoutes } from 'react-router-dom';
|
||||
|
||||
function deduplicateFeatures(
|
||||
@@ -202,7 +205,9 @@ export function createSpecializedApp(options?: {
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
apis?: ApiHolder;
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
}): { apis: ApiHolder; tree: AppTree } {
|
||||
const config = options?.config ?? new ConfigReader({}, 'empty-config');
|
||||
const features = deduplicateFeatures(options?.features ?? []);
|
||||
@@ -267,7 +272,11 @@ export function createSpecializedApp(options?: {
|
||||
}
|
||||
|
||||
// Now instantiate the entire tree, which will skip anything that's already been instantiated
|
||||
instantiateAppNodeTree(tree.root, apis, options?.extensionFactoryMiddleware);
|
||||
instantiateAppNodeTree(
|
||||
tree.root,
|
||||
apis,
|
||||
mergeExtensionFactoryMiddleware(options?.extensionFactoryMiddleware),
|
||||
);
|
||||
|
||||
const routeInfo = extractRouteInfoFromAppNode(tree.root);
|
||||
|
||||
@@ -313,3 +322,37 @@ function createApiHolder(options: {
|
||||
|
||||
return new ApiResolver(factoryRegistry);
|
||||
}
|
||||
|
||||
function mergeExtensionFactoryMiddleware(
|
||||
middlewares?: ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[],
|
||||
): ExtensionFactoryMiddleware | undefined {
|
||||
if (!middlewares) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Array.isArray(middlewares)) {
|
||||
return middlewares;
|
||||
}
|
||||
if (middlewares.length <= 1) {
|
||||
return middlewares[0];
|
||||
}
|
||||
return middlewares.reduce((prev, next) => {
|
||||
if (!prev || !next) {
|
||||
return prev ?? next;
|
||||
}
|
||||
return (orig, ctx) => {
|
||||
const internalExt = toInternalExtension(ctx.node.spec.extension);
|
||||
if (internalExt.version !== 'v2') {
|
||||
return orig();
|
||||
}
|
||||
return next(ctxOverrides => {
|
||||
return createExtensionDataContainer(
|
||||
prev(orig, {
|
||||
node: ctx.node,
|
||||
apis: ctx.apis,
|
||||
config: ctxOverrides?.config ?? ctx.config,
|
||||
}),
|
||||
);
|
||||
}, ctx);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ExtensionDefinition, RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendModule, FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { BackstageRouteObject } from '../routing/types';
|
||||
|
||||
@@ -31,8 +31,3 @@ export type RouteInfo = {
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
routeObjects: BackstageRouteObject[];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ExtensionFactoryMiddleware = Parameters<
|
||||
ExtensionDefinition['override']
|
||||
>[0]['factory'];
|
||||
|
||||
@@ -5,7 +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 { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-app-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
@@ -33,7 +33,9 @@ export interface CreateAppOptions {
|
||||
config: ConfigApi;
|
||||
}>;
|
||||
// (undocumented)
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
// (undocumented)
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
loadingComponent?: ReactNode;
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
|
||||
import React, { JSX, ReactNode } from 'react';
|
||||
import { ConfigApi, coreExtensionData } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
ConfigApi,
|
||||
coreExtensionData,
|
||||
ExtensionFactoryMiddleware,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader';
|
||||
@@ -26,7 +30,6 @@ import { ConfigReader } from '@backstage/config';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
import {
|
||||
CreateAppRouteBinder,
|
||||
ExtensionFactoryMiddleware,
|
||||
FrontendFeature,
|
||||
createSpecializedApp,
|
||||
} from '@backstage/frontend-app-api';
|
||||
@@ -66,7 +69,9 @@ export interface CreateAppOptions {
|
||||
* If set to "null" then no loading fallback component is rendered. *
|
||||
*/
|
||||
loadingComponent?: ReactNode;
|
||||
extensionFactoryMiddleware?: ExtensionFactoryMiddleware;
|
||||
extensionFactoryMiddleware?:
|
||||
| ExtensionFactoryMiddleware
|
||||
| ExtensionFactoryMiddleware[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1175,6 +1175,18 @@ export type ExtensionDefinitionParameters = {
|
||||
params?: object;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ExtensionFactoryMiddleware = (
|
||||
originalFactory: (contextOverrides?: {
|
||||
config?: JsonObject;
|
||||
}) => ExtensionDataContainer<AnyExtensionDataRef>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config?: JsonObject;
|
||||
},
|
||||
) => Iterable<ExtensionDataValue<any, any>>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ExtensionInput<
|
||||
UExtensionData extends ExtensionDataRef<
|
||||
|
||||
@@ -54,6 +54,7 @@ export {
|
||||
type ExtensionOverrides,
|
||||
type FeatureFlagConfig,
|
||||
type FrontendFeature,
|
||||
type ExtensionFactoryMiddleware,
|
||||
} from './types';
|
||||
export {
|
||||
type CreateExtensionBlueprintOptions,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing';
|
||||
import { ExtensionDefinition } from './createExtension';
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
ExtensionDataValue,
|
||||
} from './createExtensionDataRef';
|
||||
import { FrontendPlugin } from './createFrontendPlugin';
|
||||
import { ApiHolder, AppNode } from '../apis';
|
||||
|
||||
/**
|
||||
* Feature flag configuration.
|
||||
@@ -78,3 +80,15 @@ export type ExtensionDataContainer<UExtensionData extends AnyExtensionDataRef> =
|
||||
: IData
|
||||
: never;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type ExtensionFactoryMiddleware = (
|
||||
originalFactory: (contextOverrides?: {
|
||||
config?: JsonObject;
|
||||
}) => ExtensionDataContainer<AnyExtensionDataRef>,
|
||||
context: {
|
||||
node: AppNode;
|
||||
apis: ApiHolder;
|
||||
config?: JsonObject;
|
||||
},
|
||||
) => Iterable<ExtensionDataValue<any, any>>;
|
||||
|
||||
Reference in New Issue
Block a user