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:
@@ -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