frontend-app-api: add support for installing extension overrides
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Added support for installing `ExtensionOverrides` via `createApp` options. As part of this change the `plugins` option has been renamed to `features`, and the `pluginLoader` has been renamed to `featureLoader`.
|
||||
@@ -49,7 +49,7 @@ TODO:
|
||||
/* app.tsx */
|
||||
|
||||
const app = createApp({
|
||||
plugins: [graphiqlPlugin, pagesPlugin, techRadarPlugin],
|
||||
features: [graphiqlPlugin, pagesPlugin, techRadarPlugin],
|
||||
// bindRoutes({ bind }) {
|
||||
// bind(catalogPlugin.externalRoutes, {
|
||||
// createComponent: scaffolderPlugin.routes.root,
|
||||
|
||||
@@ -7,13 +7,16 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createApp(options: {
|
||||
plugins: BackstagePlugin[];
|
||||
features?: (BackstagePlugin | ExtensionOverrides)[];
|
||||
configLoader?: () => Promise<ConfigApi>;
|
||||
pluginLoader?: (ctx: { config: ConfigApi }) => Promise<BackstagePlugin[]>;
|
||||
featureLoader?: (ctx: {
|
||||
config: ConfigApi;
|
||||
}) => Promise<(BackstagePlugin | ExtensionOverrides)[]>;
|
||||
}): {
|
||||
createRoot(): JSX_2.Element;
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
|
||||
});
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [plugin],
|
||||
features: [plugin],
|
||||
});
|
||||
|
||||
return extractRouteInfoFromInstanceTree(rootInstances);
|
||||
|
||||
@@ -38,20 +38,20 @@ describe('createInstances', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
const plugins = [
|
||||
const features = [
|
||||
createPlugin({
|
||||
id: 'plugin',
|
||||
extensions: [],
|
||||
}),
|
||||
];
|
||||
expect(() => createInstances({ config, plugins })).toThrow(
|
||||
expect(() => createInstances({ config, features })).toThrow(
|
||||
"A 'root' extension configuration was detected, but the root extension is not configurable",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when a root extension is overridden', () => {
|
||||
const config = new MockConfigApi({});
|
||||
const plugins = [
|
||||
const features = [
|
||||
createPlugin({
|
||||
id: 'plugin',
|
||||
extensions: [
|
||||
@@ -65,7 +65,7 @@ describe('createInstances', () => {
|
||||
],
|
||||
}),
|
||||
];
|
||||
expect(() => createInstances({ config, plugins })).toThrow(
|
||||
expect(() => createInstances({ config, features })).toThrow(
|
||||
"The following plugin(s) are overriding the 'root' extension which is forbidden: plugin",
|
||||
);
|
||||
});
|
||||
@@ -97,9 +97,9 @@ describe('createInstances', () => {
|
||||
extensions: [ExtensionA, ExtensionB, ExtensionB],
|
||||
});
|
||||
|
||||
const plugins = [PluginA, PluginB];
|
||||
const features = [PluginA, PluginB];
|
||||
|
||||
expect(() => createInstances({ config, plugins })).toThrow(
|
||||
expect(() => createInstances({ config, features })).toThrow(
|
||||
"The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'",
|
||||
);
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe('createApp', () => {
|
||||
extensions: [{ 'themes.light': false }, { 'themes.dark': false }],
|
||||
},
|
||||
}),
|
||||
plugins: [
|
||||
features: [
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
@@ -137,7 +137,7 @@ describe('createApp', () => {
|
||||
it('should log an app', () => {
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [],
|
||||
features: [],
|
||||
});
|
||||
const root = createExtensionInstance({
|
||||
extension: createExtension({
|
||||
@@ -175,7 +175,7 @@ describe('createApp', () => {
|
||||
it('should serialize an app as JSON', () => {
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [],
|
||||
features: [],
|
||||
});
|
||||
const root = createExtensionInstance({
|
||||
extension: createExtension({
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
BackstagePlugin,
|
||||
coreExtensionData,
|
||||
ExtensionDataRef,
|
||||
ExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Core } from '../extensions/Core';
|
||||
import { CoreRoutes } from '../extensions/CoreRoutes';
|
||||
@@ -51,7 +52,7 @@ import {
|
||||
identityApiRef,
|
||||
AppTheme,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { getAvailablePlugins } from './discovery';
|
||||
import { getAvailableFeatures } from './discovery';
|
||||
import {
|
||||
ApiFactoryRegistry,
|
||||
ApiProvider,
|
||||
@@ -104,9 +105,9 @@ export interface ExtensionTree {
|
||||
export function createExtensionTree(options: {
|
||||
config: Config;
|
||||
}): ExtensionTree {
|
||||
const plugins = getAvailablePlugins();
|
||||
const features = getAvailableFeatures();
|
||||
const { instances } = createInstances({
|
||||
plugins,
|
||||
features,
|
||||
config: options.config,
|
||||
});
|
||||
|
||||
@@ -172,7 +173,7 @@ export function createExtensionTree(options: {
|
||||
* @internal
|
||||
*/
|
||||
export function createInstances(options: {
|
||||
plugins: BackstagePlugin[];
|
||||
features: (BackstagePlugin | ExtensionOverrides)[];
|
||||
config: Config;
|
||||
}) {
|
||||
const builtinExtensions = [
|
||||
@@ -187,7 +188,7 @@ export function createInstances(options: {
|
||||
// pull in default extension instance from discovered packages
|
||||
// apply config to adjust default extension instances and add more
|
||||
const extensionParams = mergeExtensionParameters({
|
||||
sources: options.plugins,
|
||||
features: options.features,
|
||||
builtinExtensions,
|
||||
parameters: readAppExtensionParameters(options.config),
|
||||
});
|
||||
@@ -260,9 +261,11 @@ export function createInstances(options: {
|
||||
|
||||
/** @public */
|
||||
export function createApp(options: {
|
||||
plugins: BackstagePlugin[];
|
||||
features?: (BackstagePlugin | ExtensionOverrides)[];
|
||||
configLoader?: () => Promise<ConfigApi>;
|
||||
pluginLoader?: (ctx: { config: ConfigApi }) => Promise<BackstagePlugin[]>;
|
||||
featureLoader?: (ctx: {
|
||||
config: ConfigApi;
|
||||
}) => Promise<(BackstagePlugin | ExtensionOverrides)[]>;
|
||||
}): {
|
||||
createRoot(): JSX.Element;
|
||||
} {
|
||||
@@ -273,14 +276,18 @@ export function createApp(options: {
|
||||
overrideBaseUrlConfigs(defaultConfigLoaderSync()),
|
||||
);
|
||||
|
||||
const discoveredPlugins = getAvailablePlugins();
|
||||
const loadedPlugins = (await options.pluginLoader?.({ config })) ?? [];
|
||||
const allPlugins = Array.from(
|
||||
new Set([...discoveredPlugins, ...options.plugins, ...loadedPlugins]),
|
||||
const discoveredFeatures = getAvailableFeatures();
|
||||
const loadedFeatures = (await options.featureLoader?.({ config })) ?? [];
|
||||
const allFeatures = Array.from(
|
||||
new Set([
|
||||
...discoveredFeatures,
|
||||
...(options.features ?? []),
|
||||
...loadedFeatures,
|
||||
]),
|
||||
);
|
||||
|
||||
const { rootInstances } = createInstances({
|
||||
plugins: allPlugins,
|
||||
features: allFeatures,
|
||||
config,
|
||||
});
|
||||
|
||||
@@ -293,7 +300,11 @@ export function createApp(options: {
|
||||
|
||||
const apiHolder = createApiHolder(coreInstance, config);
|
||||
|
||||
const appContext = createLegacyAppContext(allPlugins);
|
||||
const appContext = createLegacyAppContext(
|
||||
allFeatures.filter(
|
||||
(f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
|
||||
),
|
||||
);
|
||||
|
||||
const rootElements = rootInstances
|
||||
.map(e => (
|
||||
|
||||
@@ -15,25 +15,25 @@
|
||||
*/
|
||||
|
||||
import { createPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { getAvailablePlugins } from './discovery';
|
||||
import { getAvailableFeatures } from './discovery';
|
||||
|
||||
const globalSpy = jest.fn();
|
||||
Object.defineProperty(global, '__@backstage/discovered__', {
|
||||
get: globalSpy,
|
||||
});
|
||||
|
||||
describe('getAvailablePlugins', () => {
|
||||
describe('getAvailableFeatures', () => {
|
||||
afterEach(jest.resetAllMocks);
|
||||
|
||||
it('should discover nothing with undefined global', () => {
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should discover nothing with empty global', () => {
|
||||
globalSpy.mockReturnValue({
|
||||
modules: [],
|
||||
});
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should discover a plugin', () => {
|
||||
@@ -41,24 +41,24 @@ describe('getAvailablePlugins', () => {
|
||||
globalSpy.mockReturnValue({
|
||||
modules: [{ default: testPlugin }],
|
||||
});
|
||||
expect(getAvailablePlugins()).toEqual([testPlugin]);
|
||||
expect(getAvailableFeatures()).toEqual([testPlugin]);
|
||||
});
|
||||
|
||||
it('should ignore garbage', () => {
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: null }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: undefined }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: Symbol() }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: () => {} }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: 0 }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: false }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
globalSpy.mockReturnValueOnce({ modules: [{ default: true }] });
|
||||
expect(getAvailablePlugins()).toEqual([]);
|
||||
expect(getAvailableFeatures()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should discover multiple plugins', () => {
|
||||
@@ -72,7 +72,7 @@ describe('getAvailablePlugins', () => {
|
||||
{ default: test3Plugin },
|
||||
],
|
||||
});
|
||||
expect(getAvailablePlugins()).toEqual([
|
||||
expect(getAvailableFeatures()).toEqual([
|
||||
test1Plugin,
|
||||
test2Plugin,
|
||||
test3Plugin,
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
ExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
interface DiscoveryGlobal {
|
||||
modules: Array<{ name: string; default: unknown }>;
|
||||
@@ -23,19 +26,27 @@ interface DiscoveryGlobal {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getAvailablePlugins(): BackstagePlugin[] {
|
||||
export function getAvailableFeatures(): (
|
||||
| BackstagePlugin
|
||||
| ExtensionOverrides
|
||||
)[] {
|
||||
const discovered = (
|
||||
window as { '__@backstage/discovered__'?: DiscoveryGlobal }
|
||||
)['__@backstage/discovered__'];
|
||||
|
||||
return (
|
||||
discovered?.modules.map(m => m.default).filter(isBackstagePlugin) ?? []
|
||||
discovered?.modules.map(m => m.default).filter(isBackstageFeature) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function isBackstagePlugin(obj: unknown): obj is BackstagePlugin {
|
||||
function isBackstageFeature(
|
||||
obj: unknown,
|
||||
): obj is BackstagePlugin | ExtensionOverrides {
|
||||
if (obj !== null && typeof obj === 'object' && '$$type' in obj) {
|
||||
return obj.$$type === '@backstage/BackstagePlugin';
|
||||
return (
|
||||
obj.$$type === '@backstage/BackstagePlugin' ||
|
||||
obj.$$type === '@backstage/ExtensionOverrides'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('mergeExtensionParameters', () => {
|
||||
it('should filter out disabled extension instances', () => {
|
||||
expect(
|
||||
mergeExtensionParameters({
|
||||
sources: [],
|
||||
features: [],
|
||||
builtinExtensions: [makeExt('a', 'disabled')],
|
||||
parameters: [],
|
||||
}),
|
||||
@@ -47,7 +47,7 @@ describe('mergeExtensionParameters', () => {
|
||||
const b = makeExt('b');
|
||||
expect(
|
||||
mergeExtensionParameters({
|
||||
sources: [],
|
||||
features: [],
|
||||
builtinExtensions: [a, b],
|
||||
parameters: [],
|
||||
}),
|
||||
@@ -63,7 +63,7 @@ describe('mergeExtensionParameters', () => {
|
||||
const pluginA = createPlugin({ id: 'test', extensions: [a] });
|
||||
expect(
|
||||
mergeExtensionParameters({
|
||||
sources: [pluginA],
|
||||
features: [pluginA],
|
||||
builtinExtensions: [b],
|
||||
parameters: [
|
||||
{
|
||||
@@ -88,7 +88,7 @@ describe('mergeExtensionParameters', () => {
|
||||
const plugin = createPlugin({ id: 'test', extensions: [a, b] });
|
||||
expect(
|
||||
mergeExtensionParameters({
|
||||
sources: [plugin],
|
||||
features: [plugin],
|
||||
builtinExtensions: [],
|
||||
parameters: [
|
||||
{
|
||||
@@ -126,7 +126,7 @@ describe('mergeExtensionParameters', () => {
|
||||
const b = makeExt('b', 'disabled');
|
||||
expect(
|
||||
mergeExtensionParameters({
|
||||
sources: [createPlugin({ id: 'empty', extensions: [] })],
|
||||
features: [createPlugin({ id: 'empty', extensions: [] })],
|
||||
builtinExtensions: [a, b],
|
||||
parameters: [
|
||||
{
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
|
||||
export interface ExtensionParameters {
|
||||
@@ -208,15 +214,26 @@ export interface ExtensionInstanceParameters {
|
||||
|
||||
/** @internal */
|
||||
export function mergeExtensionParameters(options: {
|
||||
sources: BackstagePlugin[];
|
||||
features: (BackstagePlugin | ExtensionOverrides)[];
|
||||
builtinExtensions: Extension<unknown>[];
|
||||
parameters: Array<ExtensionParameters>;
|
||||
}): ExtensionInstanceParameters[] {
|
||||
const { sources, builtinExtensions, parameters } = options;
|
||||
const { builtinExtensions, parameters } = options;
|
||||
|
||||
const pluginExtensions = sources.flatMap(source => {
|
||||
const plugins = options.features.filter(
|
||||
(f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
|
||||
);
|
||||
const overrides = options.features.filter(
|
||||
(f): f is ExtensionOverrides =>
|
||||
f.$$type === '@backstage/ExtensionOverrides',
|
||||
);
|
||||
|
||||
const pluginExtensions = plugins.flatMap(source => {
|
||||
return source.extensions.map(extension => ({ ...extension, source }));
|
||||
});
|
||||
const overrideExtensions = overrides.flatMap(
|
||||
override => toInternalExtensionOverrides(override).extensions,
|
||||
);
|
||||
|
||||
// Prevent root override
|
||||
if (pluginExtensions.some(({ id }) => id === 'root')) {
|
||||
@@ -230,7 +247,28 @@ export function mergeExtensionParameters(options: {
|
||||
);
|
||||
}
|
||||
|
||||
const overrides = [
|
||||
if (overrideExtensions.some(({ id }) => id === 'root')) {
|
||||
throw new Error(
|
||||
`An extension override is overriding the 'root' extension which is forbidden`,
|
||||
);
|
||||
}
|
||||
const overrideExtensionIds = overrideExtensions.map(({ id }) => id);
|
||||
if (overrideExtensionIds.length !== new Set(overrideExtensionIds).size) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const id of overrideExtensionIds) {
|
||||
counts.set(id, (counts.get(id) ?? 0) + 1);
|
||||
}
|
||||
const duplicated = Array.from(counts.entries())
|
||||
.filter(([, count]) => count > 1)
|
||||
.map(([id]) => id);
|
||||
throw new Error(
|
||||
`The following extensions had duplicate overrides: ${duplicated.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const configuredExtensions = [
|
||||
...pluginExtensions.map(({ source, ...extension }) => ({
|
||||
extension,
|
||||
params: {
|
||||
@@ -251,8 +289,33 @@ export function mergeExtensionParameters(options: {
|
||||
})),
|
||||
];
|
||||
|
||||
// Install all extension overrides
|
||||
for (const extension of overrideExtensions) {
|
||||
// Check if our override is overriding an extension that already exists
|
||||
const index = configuredExtensions.findIndex(
|
||||
e => e.extension.id === extension.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
// Only implementation, attachment point and default disabled status are overridden, the source is kept
|
||||
configuredExtensions[index].extension = extension;
|
||||
configuredExtensions[index].params.attachTo = extension.attachTo;
|
||||
configuredExtensions[index].params.disabled = extension.disabled;
|
||||
} else {
|
||||
// Add the extension as a new one when not overriding an existing one
|
||||
configuredExtensions.push({
|
||||
extension,
|
||||
params: {
|
||||
source: undefined,
|
||||
attachTo: extension.attachTo,
|
||||
disabled: extension.disabled,
|
||||
config: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const duplicatedExtensionIds = new Set<string>();
|
||||
const duplicatedExtensionData = overrides.reduce<
|
||||
const duplicatedExtensionData = configuredExtensions.reduce<
|
||||
Record<string, Record<string, number>>
|
||||
>((data, { extension, params }) => {
|
||||
const extensionId = extension.id;
|
||||
@@ -296,11 +359,11 @@ export function mergeExtensionParameters(options: {
|
||||
);
|
||||
}
|
||||
|
||||
const existingIndex = overrides.findIndex(
|
||||
const existingIndex = configuredExtensions.findIndex(
|
||||
e => e.extension.id === extensionId,
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
const existing = overrides[existingIndex];
|
||||
const existing = configuredExtensions[existingIndex];
|
||||
if (overrideParam.attachTo) {
|
||||
existing.params.attachTo = overrideParam.attachTo;
|
||||
}
|
||||
@@ -314,8 +377,8 @@ export function mergeExtensionParameters(options: {
|
||||
existing.params.disabled = Boolean(overrideParam.disabled);
|
||||
if (!existing.params.disabled) {
|
||||
// bump
|
||||
overrides.splice(existingIndex, 1);
|
||||
overrides.push(existing);
|
||||
configuredExtensions.splice(existingIndex, 1);
|
||||
configuredExtensions.push(existing);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -323,7 +386,7 @@ export function mergeExtensionParameters(options: {
|
||||
}
|
||||
}
|
||||
|
||||
return overrides
|
||||
return configuredExtensions
|
||||
.filter(override => !override.params.disabled)
|
||||
.map(param => ({
|
||||
extension: param.extension,
|
||||
|
||||
@@ -104,14 +104,14 @@ const outputExtension = createExtension({
|
||||
});
|
||||
|
||||
function createTestAppRoot({
|
||||
plugins,
|
||||
features,
|
||||
config = {},
|
||||
}: {
|
||||
plugins: BackstagePlugin[];
|
||||
features: BackstagePlugin[];
|
||||
config: JsonObject;
|
||||
}) {
|
||||
return createApp({
|
||||
plugins: plugins,
|
||||
features,
|
||||
configLoader: async () => new MockConfigApi(config),
|
||||
}).createRoot();
|
||||
}
|
||||
@@ -132,7 +132,7 @@ describe('createPlugin', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
createTestAppRoot({
|
||||
plugins: [plugin],
|
||||
features: [plugin],
|
||||
config: { app: { extensions: [{ 'core.layout': false }] } },
|
||||
}),
|
||||
);
|
||||
@@ -157,7 +157,7 @@ describe('createPlugin', () => {
|
||||
|
||||
await renderWithEffects(
|
||||
createTestAppRoot({
|
||||
plugins: [plugin],
|
||||
features: [plugin],
|
||||
config: {
|
||||
app: {
|
||||
extensions: [
|
||||
|
||||
@@ -168,7 +168,7 @@ describe('createSearchResultListItemExtension', () => {
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
plugins: [SearchPlugin],
|
||||
features: [SearchPlugin],
|
||||
configLoader: async () =>
|
||||
new MockConfigApi({
|
||||
app: {
|
||||
|
||||
Reference in New Issue
Block a user