feat: introduced createFrontendModule and deprecating createExtensionOverrides
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
@@ -31,7 +31,7 @@ import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { collectLegacyRoutes } from './collectLegacyRoutes';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalBackstagePlugin } from '../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
import { toInternalFrontendPlugin } from '../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
import {
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
@@ -55,7 +55,7 @@ describe('collectLegacyRoutes', () => {
|
||||
expect(
|
||||
collected.map(p => ({
|
||||
id: p.id,
|
||||
extensions: toInternalBackstagePlugin(p).extensions.map(e => ({
|
||||
extensions: toInternalFrontendPlugin(p).extensions.map(e => ({
|
||||
id: e.id,
|
||||
attachTo: e.attachTo,
|
||||
disabled: e.disabled,
|
||||
@@ -158,7 +158,7 @@ describe('collectLegacyRoutes', () => {
|
||||
expect(
|
||||
collected.map(p => ({
|
||||
id: p.id,
|
||||
extensions: toInternalBackstagePlugin(p).extensions.map(e => ({
|
||||
extensions: toInternalFrontendPlugin(p).extensions.map(e => ({
|
||||
id: e.id,
|
||||
attachTo: e.attachTo,
|
||||
disabled: e.disabled,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import { convertLegacyPlugin } from './convertLegacyPlugin';
|
||||
import { PageBlueprint } from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalBackstagePlugin } from '../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
import { toInternalFrontendPlugin } from '../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
|
||||
describe('convertLegacyPlugin', () => {
|
||||
it('should convert a plain legacy plugin to a new plugin', () => {
|
||||
@@ -34,12 +34,13 @@ describe('convertLegacyPlugin', () => {
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"$$type": "@backstage/BackstagePlugin",
|
||||
"$$type": "@backstage/FrontendPlugin",
|
||||
"extensions": [],
|
||||
"externalRoutes": {},
|
||||
"featureFlags": [],
|
||||
"getExtension": [Function],
|
||||
"id": "test",
|
||||
"pluginId": "test",
|
||||
"routes": {},
|
||||
"toString": [Function],
|
||||
"version": "v1",
|
||||
@@ -73,7 +74,7 @@ describe('convertLegacyPlugin', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const internalConverted = toInternalBackstagePlugin(converted);
|
||||
const internalConverted = toInternalFrontendPlugin(converted);
|
||||
|
||||
expect(internalConverted.id).toBe('test');
|
||||
expect(internalConverted.routes).toEqual({
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalSubRouteRef } from '../../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { isInternalFrontendPlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
|
||||
/** @internal */
|
||||
export interface RouteRefsById {
|
||||
@@ -42,12 +44,12 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
|
||||
const externalRoutesById = new Map<string, ExternalRouteRef>();
|
||||
|
||||
for (const feature of features) {
|
||||
if (feature.$$type !== '@backstage/BackstagePlugin') {
|
||||
if (!isInternalFrontendPlugin(feature)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [name, ref] of Object.entries(feature.routes)) {
|
||||
const refId = `${feature.id}.${name}`;
|
||||
const refId = `${feature.pluginId}.${name}`;
|
||||
if (routesById.has(refId)) {
|
||||
throw new Error(`Unexpected duplicate route '${refId}'`);
|
||||
}
|
||||
@@ -62,7 +64,7 @@ export function collectRouteIds(features: FrontendFeature[]): RouteRefsById {
|
||||
}
|
||||
}
|
||||
for (const [name, ref] of Object.entries(feature.externalRoutes)) {
|
||||
const refId = `${feature.id}.${name}`;
|
||||
const refId = `${feature.pluginId}.${name}`;
|
||||
if (externalRoutesById.has(refId)) {
|
||||
throw new Error(`Unexpected duplicate external route '${refId}'`);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionOverrides,
|
||||
FrontendFeature,
|
||||
@@ -25,7 +24,10 @@ import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/w
|
||||
import { ExtensionParameters } from './readAppExtensionsConfig';
|
||||
import { AppNodeSpec } from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
import {
|
||||
isInternalFrontendPlugin,
|
||||
toInternalFrontendPlugin,
|
||||
} from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
|
||||
@@ -43,16 +45,14 @@ export function resolveAppNodeSpecs(options: {
|
||||
features = [],
|
||||
} = options;
|
||||
|
||||
const plugins = features.filter(
|
||||
(f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
|
||||
);
|
||||
const plugins = features.filter(isInternalFrontendPlugin);
|
||||
const overrides = features.filter(
|
||||
(f): f is ExtensionOverrides =>
|
||||
f.$$type === '@backstage/ExtensionOverrides',
|
||||
);
|
||||
|
||||
const pluginExtensions = plugins.flatMap(source => {
|
||||
return toInternalBackstagePlugin(source).extensions.map(extension => ({
|
||||
return toInternalFrontendPlugin(source).extensions.map(extension => ({
|
||||
...extension,
|
||||
source,
|
||||
}));
|
||||
@@ -65,7 +65,7 @@ export function resolveAppNodeSpecs(options: {
|
||||
if (pluginExtensions.some(({ id }) => forbidden.has(id))) {
|
||||
const pluginsStr = pluginExtensions
|
||||
.filter(({ id }) => forbidden.has(id))
|
||||
.map(({ source }) => `'${source.id}'`)
|
||||
.map(({ source }) => `'${source.pluginId}'`)
|
||||
.join(', ');
|
||||
const forbiddenStr = [...forbidden].map(id => `'${id}'`).join(', ');
|
||||
throw new Error(
|
||||
@@ -156,7 +156,7 @@ export function resolveAppNodeSpecs(options: {
|
||||
const extensionId = extension.id;
|
||||
const extensionData = data?.[extensionId];
|
||||
if (extensionData) duplicatedExtensionIds.add(extensionId);
|
||||
const pluginId = params.source?.id ?? 'internal';
|
||||
const pluginId = params.source?.pluginId ?? 'internal';
|
||||
const pluginCount = extensionData?.[pluginId] ?? 0;
|
||||
return {
|
||||
...data,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
PageBlueprint,
|
||||
createFrontendPlugin,
|
||||
ThemeBlueprint,
|
||||
createFrontendModule,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { CreateAppFeatureLoader, createApp } from './createApp';
|
||||
@@ -379,4 +380,57 @@ describe('createApp', () => {
|
||||
|
||||
expect(screen.queryByText('Custom app root element')).toBeNull();
|
||||
});
|
||||
|
||||
describe('modules', () => {
|
||||
it('should be able to override extensions with a plugin extension override', async () => {
|
||||
const mod = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
appPlugin.getExtension('app/root').override({
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(
|
||||
<div>Custom app root element</div>,
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
features: [mod],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
expect(screen.queryByText('Custom app root element')).toBeNull();
|
||||
});
|
||||
|
||||
it('should be able to override extensions with a standalone extension override', async () => {
|
||||
const mod = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'root',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [
|
||||
coreExtensionData.reactElement(
|
||||
<div>Custom app root element</div>,
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
configLoader: () => new Promise(() => {}),
|
||||
features: [mod],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
expect(screen.queryByText('Custom app root element')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +61,15 @@ import { RouteResolver } from '../routing/RouteResolver';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
import { collectRouteIds } from '../routing/collectRouteIds';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
import {
|
||||
toInternalFrontendPlugin,
|
||||
isInternalFrontendPlugin,
|
||||
} from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
toInternalFrontendModule,
|
||||
isInternalFrontendModule,
|
||||
} from '../../../frontend-plugin-api/src/wiring/createFrontendModule';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
@@ -89,13 +97,13 @@ function deduplicateFeatures(
|
||||
return features
|
||||
.reverse()
|
||||
.filter(feature => {
|
||||
if (feature.$$type !== '@backstage/BackstagePlugin') {
|
||||
if (!isInternalFrontendPlugin(feature)) {
|
||||
return true;
|
||||
}
|
||||
if (seenIds.has(feature.id)) {
|
||||
if (seenIds.has(feature.pluginId)) {
|
||||
return false;
|
||||
}
|
||||
seenIds.add(feature.id);
|
||||
seenIds.add(feature.pluginId);
|
||||
return true;
|
||||
})
|
||||
.reverse();
|
||||
@@ -318,11 +326,19 @@ export function createSpecializedApp(options?: {
|
||||
const featureFlagApi = apiHolder.get(featureFlagsApiRef);
|
||||
if (featureFlagApi) {
|
||||
for (const feature of features) {
|
||||
if (feature.$$type === '@backstage/BackstagePlugin') {
|
||||
toInternalBackstagePlugin(feature).featureFlags.forEach(flag =>
|
||||
if (isInternalFrontendPlugin(feature)) {
|
||||
toInternalFrontendPlugin(feature).featureFlags.forEach(flag =>
|
||||
featureFlagApi.registerFlag({
|
||||
name: flag.name,
|
||||
pluginId: feature.id,
|
||||
pluginId: feature.pluginId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (isInternalFrontendModule(feature)) {
|
||||
toInternalFrontendModule(feature).featureFlags.forEach(flag =>
|
||||
featureFlagApi.registerFlag({
|
||||
name: flag.name,
|
||||
pluginId: feature.pluginId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
} from './resolveExtensionDefinition';
|
||||
import { ExtensionOverrides, FeatureFlagConfig } from './types';
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @deprecated Use {@link createFrontendModule} instead.
|
||||
* @public
|
||||
*/
|
||||
export interface ExtensionOverridesOptions {
|
||||
extensions: ExtensionDefinition[];
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
@@ -34,7 +37,10 @@ export interface InternalExtensionOverrides extends ExtensionOverrides {
|
||||
readonly featureFlags: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @deprecated Use {@link createFrontendModule} instead.
|
||||
* @public
|
||||
*/
|
||||
export function createExtensionOverrides(
|
||||
options: ExtensionOverridesOptions,
|
||||
): ExtensionOverrides {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 {
|
||||
ExtensionDefinition,
|
||||
InternalExtensionDefinition,
|
||||
toInternalExtensionDefinition,
|
||||
} from './createExtension';
|
||||
import {
|
||||
Extension,
|
||||
resolveExtensionDefinition,
|
||||
} from './resolveExtensionDefinition';
|
||||
import { FeatureFlagConfig } from './types';
|
||||
|
||||
/** @public */
|
||||
export interface ModuleOptions<
|
||||
TPluginId extends string,
|
||||
TExtensions extends readonly ExtensionDefinition[],
|
||||
> {
|
||||
pluginId: TPluginId;
|
||||
extensions?: TExtensions;
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface FrontendModule {
|
||||
readonly $$type: '@backstage/FrontendModule';
|
||||
readonly pluginId: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalFrontendModule extends FrontendModule {
|
||||
readonly version: 'v1';
|
||||
readonly extensions: Extension<unknown>[];
|
||||
readonly featureFlags: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createFrontendModule<
|
||||
TId extends string,
|
||||
TExtensions extends readonly ExtensionDefinition[] = [],
|
||||
>(options: ModuleOptions<TId, TExtensions>): FrontendModule {
|
||||
const { pluginId } = options;
|
||||
|
||||
const extensions = new Array<Extension<any>>();
|
||||
const extensionDefinitionsById = new Map<
|
||||
string,
|
||||
InternalExtensionDefinition
|
||||
>();
|
||||
|
||||
for (const def of options.extensions ?? []) {
|
||||
const internal = toInternalExtensionDefinition(def);
|
||||
const ext = resolveExtensionDefinition(def, { namespace: pluginId });
|
||||
extensions.push(ext);
|
||||
extensionDefinitionsById.set(ext.id, {
|
||||
...internal,
|
||||
namespace: pluginId,
|
||||
});
|
||||
}
|
||||
|
||||
if (extensions.length !== extensionDefinitionsById.size) {
|
||||
const extensionIds = extensions.map(e => e.id);
|
||||
const duplicates = Array.from(
|
||||
new Set(
|
||||
extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index),
|
||||
),
|
||||
);
|
||||
// TODO(Rugvip): This could provide some more information about the kind + name of the extensions
|
||||
throw new Error(
|
||||
`Plugin '${pluginId}' provided duplicate extensions: ${duplicates.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
$$type: '@backstage/FrontendModule',
|
||||
version: 'v1',
|
||||
pluginId,
|
||||
featureFlags: options.featureFlags ?? [],
|
||||
extensions,
|
||||
toString() {
|
||||
return `Module{pluginId=${pluginId}}`;
|
||||
},
|
||||
} as InternalFrontendModule;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isInternalFrontendModule(opaque: {
|
||||
$$type: string;
|
||||
}): opaque is InternalFrontendModule {
|
||||
if (opaque.$$type === '@backstage/FrontendModule') {
|
||||
// Make sure we throw if invalid
|
||||
toInternalFrontendModule(opaque as FrontendModule);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalFrontendModule(
|
||||
plugin: FrontendModule,
|
||||
): InternalFrontendModule {
|
||||
const internal = plugin as InternalFrontendModule;
|
||||
if (internal.$$type !== '@backstage/FrontendModule') {
|
||||
throw new Error(`Invalid plugin instance, bad type '${internal.$$type}'`);
|
||||
}
|
||||
if (internal.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Invalid plugin instance, bad version '${internal.version}'`,
|
||||
);
|
||||
}
|
||||
return internal;
|
||||
}
|
||||
@@ -17,14 +17,13 @@
|
||||
import React from 'react';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { createFrontendPlugin } from './createFrontendPlugin';
|
||||
import { FrontendPlugin, createFrontendPlugin } from './createFrontendPlugin';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { createExtension } from './createExtension';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import { coreExtensionData } from './coreExtensionData';
|
||||
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
|
||||
import { createExtensionInput } from './createExtensionInput';
|
||||
import { BackstagePlugin } from './types';
|
||||
|
||||
const nameExtensionDataRef = createExtensionDataRef<string>().with({
|
||||
id: 'name',
|
||||
@@ -123,7 +122,7 @@ function createTestAppRoot({
|
||||
features,
|
||||
config = {},
|
||||
}: {
|
||||
features: BackstagePlugin[];
|
||||
features: FrontendPlugin[];
|
||||
config: JsonObject;
|
||||
}) {
|
||||
return createApp({
|
||||
|
||||
@@ -24,12 +24,33 @@ import {
|
||||
ResolveExtensionId,
|
||||
resolveExtensionDefinition,
|
||||
} from './resolveExtensionDefinition';
|
||||
import {
|
||||
AnyExternalRoutes,
|
||||
AnyRoutes,
|
||||
BackstagePlugin,
|
||||
FeatureFlagConfig,
|
||||
} from './types';
|
||||
import { AnyExternalRoutes, AnyRoutes, FeatureFlagConfig } from './types';
|
||||
|
||||
/** @public */
|
||||
export interface FrontendPlugin<
|
||||
TRoutes extends AnyRoutes = AnyRoutes,
|
||||
TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
TExtensionMap extends { [id in string]: ExtensionDefinition } = {},
|
||||
> {
|
||||
readonly $$type: '@backstage/FrontendPlugin';
|
||||
readonly pluginId: string;
|
||||
readonly routes: TRoutes;
|
||||
readonly externalRoutes: TExternalRoutes;
|
||||
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
|
||||
withOverrides(options: {
|
||||
extensions: Array<ExtensionDefinition>;
|
||||
}): FrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link FrontendPlugin} instead.
|
||||
*/
|
||||
export type BackstagePlugin<
|
||||
TRoutes extends AnyRoutes = AnyRoutes,
|
||||
TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
TExtensionMap extends { [id in string]: ExtensionDefinition } = {},
|
||||
> = FrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
|
||||
/** @public */
|
||||
export interface PluginOptions<
|
||||
@@ -48,10 +69,10 @@ export interface PluginOptions<
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface InternalBackstagePlugin<
|
||||
export interface InternalFrontendPlugin<
|
||||
TRoutes extends AnyRoutes = AnyRoutes,
|
||||
TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
> extends BackstagePlugin<TRoutes, TExternalRoutes> {
|
||||
> extends FrontendPlugin<TRoutes, TExternalRoutes> {
|
||||
readonly version: 'v1';
|
||||
readonly extensions: Extension<unknown>[];
|
||||
readonly featureFlags: FeatureFlagConfig[];
|
||||
@@ -65,7 +86,7 @@ export function createFrontendPlugin<
|
||||
TExtensions extends readonly ExtensionDefinition[] = [],
|
||||
>(
|
||||
options: PluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>,
|
||||
): BackstagePlugin<
|
||||
): FrontendPlugin<
|
||||
TRoutes,
|
||||
TExternalRoutes,
|
||||
{
|
||||
@@ -112,9 +133,9 @@ export function createFrontendPlugin<
|
||||
}
|
||||
|
||||
return {
|
||||
$$type: '@backstage/BackstagePlugin',
|
||||
$$type: '@backstage/FrontendPlugin',
|
||||
version: 'v1',
|
||||
id: pluginId,
|
||||
id: pluginId, // TODO: Backwards compat to be able to install in older apps, remove after 1.31
|
||||
pluginId,
|
||||
routes: options.routes ?? ({} as TRoutes),
|
||||
externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),
|
||||
@@ -143,15 +164,33 @@ export function createFrontendPlugin<
|
||||
extensions: [...nonOverriddenExtensions, ...overrides.extensions],
|
||||
});
|
||||
},
|
||||
} as InternalBackstagePlugin<TRoutes, TExternalRoutes>;
|
||||
} as InternalFrontendPlugin<TRoutes, TExternalRoutes>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalBackstagePlugin(
|
||||
plugin: BackstagePlugin,
|
||||
): InternalBackstagePlugin {
|
||||
const internal = plugin as InternalBackstagePlugin;
|
||||
if (internal.$$type !== '@backstage/BackstagePlugin') {
|
||||
export function isInternalFrontendPlugin(opaque: {
|
||||
$$type: string;
|
||||
}): opaque is InternalFrontendPlugin {
|
||||
if (
|
||||
opaque.$$type === '@backstage/FrontendPlugin' ||
|
||||
opaque.$$type === '@backstage/BackstagePlugin'
|
||||
) {
|
||||
// Throw if invalid + mutate
|
||||
toInternalFrontendPlugin(opaque as FrontendPlugin);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalFrontendPlugin(
|
||||
plugin: FrontendPlugin,
|
||||
): InternalFrontendPlugin {
|
||||
const internal = plugin as InternalFrontendPlugin;
|
||||
if (
|
||||
internal.$$type !== '@backstage/FrontendPlugin' &&
|
||||
internal.$$type !== '@backstage/BackstagePlugin'
|
||||
) {
|
||||
throw new Error(`Invalid plugin instance, bad type '${internal.$$type}'`);
|
||||
}
|
||||
if (internal.version !== 'v1') {
|
||||
@@ -159,6 +198,10 @@ export function toInternalBackstagePlugin(
|
||||
`Invalid plugin instance, bad version '${internal.version}'`,
|
||||
);
|
||||
}
|
||||
// Backwards compatibility to support old plugins defined with just an .id
|
||||
if (!internal.pluginId) {
|
||||
(internal as any).pluginId = (internal as any).id;
|
||||
}
|
||||
return internal;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,15 @@ export {
|
||||
export {
|
||||
createPlugin,
|
||||
createFrontendPlugin,
|
||||
type FrontendPlugin,
|
||||
type BackstagePlugin,
|
||||
type PluginOptions,
|
||||
} from './createFrontendPlugin';
|
||||
export {
|
||||
createFrontendModule,
|
||||
type FrontendModule,
|
||||
type ModuleOptions,
|
||||
} from './createFrontendModule';
|
||||
export {
|
||||
createExtensionOverrides,
|
||||
type ExtensionOverridesOptions,
|
||||
@@ -49,7 +56,6 @@ export { type Extension } from './resolveExtensionDefinition';
|
||||
export {
|
||||
type AnyRoutes,
|
||||
type AnyExternalRoutes,
|
||||
type BackstagePlugin,
|
||||
type ExtensionOverrides,
|
||||
type FeatureFlagConfig,
|
||||
type FrontendFeature,
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing';
|
||||
import { ExtensionDefinition } from './createExtension';
|
||||
import { FrontendModule } from './createFrontendModule';
|
||||
import { FrontendPlugin } from './createFrontendPlugin';
|
||||
|
||||
/**
|
||||
* Feature flag configuration.
|
||||
@@ -40,26 +42,13 @@ export type ExtensionMap<
|
||||
get<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface BackstagePlugin<
|
||||
TRoutes extends AnyRoutes = AnyRoutes,
|
||||
TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
|
||||
TExtensionMap extends { [id in string]: ExtensionDefinition } = {},
|
||||
> {
|
||||
readonly $$type: '@backstage/BackstagePlugin';
|
||||
readonly id: string;
|
||||
readonly routes: TRoutes;
|
||||
readonly externalRoutes: TExternalRoutes;
|
||||
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
|
||||
withOverrides(options: {
|
||||
extensions: Array<ExtensionDefinition>;
|
||||
}): BackstagePlugin<TRoutes, TExternalRoutes, TExtensionMap>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionOverrides {
|
||||
readonly $$type: '@backstage/ExtensionOverrides';
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type FrontendFeature = BackstagePlugin | ExtensionOverrides;
|
||||
export type FrontendFeature =
|
||||
| FrontendPlugin
|
||||
| FrontendModule
|
||||
| ExtensionOverrides;
|
||||
|
||||
Reference in New Issue
Block a user