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:
blam
2024-08-27 15:43:13 +02:00
parent c1ef48f4fb
commit 860f6e7a3f
12 changed files with 307 additions and 64 deletions
@@ -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;