Merge pull request #26245 from backstage/blam/namespace-be-gone

NFS: Introduce `createFrontendModule` as a replacement for `createExtensionOverrides`
This commit is contained in:
Patrik Oldsberg
2024-08-28 15:04:39 +02:00
committed by GitHub
42 changed files with 526 additions and 131 deletions
+58 -22
View File
@@ -320,29 +320,14 @@ export { BackstageIdentityApi };
export { BackstageIdentityResponse };
// @public (undocumented)
export interface BackstagePlugin<
// @public @deprecated (undocumented)
export type BackstagePlugin<
TRoutes extends AnyRoutes = AnyRoutes,
TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
TExtensionMap extends {
[id in string]: ExtensionDefinition;
} = {},
> {
// (undocumented)
readonly $$type: '@backstage/BackstagePlugin';
// (undocumented)
readonly externalRoutes: TExternalRoutes;
// (undocumented)
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
// (undocumented)
readonly id: string;
// (undocumented)
readonly routes: TRoutes;
// (undocumented)
withOverrides(options: {
extensions: Array<ExtensionDefinition>;
}): BackstagePlugin<TRoutes, TExternalRoutes, TExtensionMap>;
}
> = FrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;
export { BackstageUserIdentity };
@@ -732,7 +717,7 @@ export type CreateExtensionOptions<
}): Iterable<UFactoryOutput>;
} & VerifyExtensionFactoryOutput<UOutput, UFactoryOutput>;
// @public (undocumented)
// @public @deprecated (undocumented)
export function createExtensionOverrides(
options: ExtensionOverridesOptions,
): ExtensionOverrides;
@@ -760,6 +745,25 @@ export function createExternalRouteRef<
}
>;
// @public (undocumented)
export function createFrontendModule<
TId extends string,
TExtensions extends readonly ExtensionDefinition[] = [],
>(options: CreateFrontendModuleOptions<TId, TExtensions>): FrontendModule;
// @public (undocumented)
export interface CreateFrontendModuleOptions<
TPluginId extends string,
TExtensions extends readonly ExtensionDefinition[],
> {
// (undocumented)
extensions?: TExtensions;
// (undocumented)
featureFlags?: FeatureFlagConfig[];
// (undocumented)
pluginId: TPluginId;
}
// @public (undocumented)
export function createFrontendPlugin<
TId extends string,
@@ -768,7 +772,7 @@ export function createFrontendPlugin<
TExtensions extends readonly ExtensionDefinition[] = [],
>(
options: PluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>,
): BackstagePlugin<
): FrontendPlugin<
TRoutes,
TExternalRoutes,
{
@@ -1204,7 +1208,7 @@ export interface ExtensionOverrides {
readonly $$type: '@backstage/ExtensionOverrides';
}
// @public (undocumented)
// @public @deprecated (undocumented)
export interface ExtensionOverridesOptions {
// (undocumented)
extensions: ExtensionDefinition[];
@@ -1241,8 +1245,40 @@ export { FetchApi };
export { fetchApiRef };
// @public @deprecated (undocumented)
export type FrontendFeature = FrontendPlugin | ExtensionOverrides;
// @public (undocumented)
export type FrontendFeature = BackstagePlugin | ExtensionOverrides;
export interface FrontendModule {
// (undocumented)
readonly $$type: '@backstage/FrontendModule';
// (undocumented)
readonly pluginId: string;
}
// @public (undocumented)
export interface FrontendPlugin<
TRoutes extends AnyRoutes = AnyRoutes,
TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes,
TExtensionMap extends {
[id in string]: ExtensionDefinition;
} = {},
> {
// (undocumented)
readonly $$type: '@backstage/FrontendPlugin';
// (undocumented)
readonly externalRoutes: TExternalRoutes;
// (undocumented)
getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];
// (undocumented)
readonly id: string;
// (undocumented)
readonly routes: TRoutes;
// (undocumented)
withOverrides(options: {
extensions: Array<ExtensionDefinition>;
}): FrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;
}
export { githubAuthApiRef };
@@ -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,62 @@
/*
* Copyright 2024 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 { createExtension } from './createExtension';
import { createFrontendModule } from './createFrontendModule';
describe('createFrontendModule', () => {
it('should create a frontend module', () => {
expect(
createFrontendModule({
pluginId: 'test',
extensions: [
createExtension({
kind: 'route',
name: 'test',
output: [],
attachTo: { id: 'ignored', input: 'ignored' },
factory: () => [],
}),
],
}),
).toMatchInlineSnapshot(`
{
"$$type": "@backstage/FrontendModule",
"extensions": [
{
"$$type": "@backstage/Extension",
"T": undefined,
"attachTo": {
"id": "ignored",
"input": "ignored",
},
"configSchema": undefined,
"disabled": false,
"factory": [Function],
"id": "route:test/test",
"inputs": {},
"output": [],
"toString": [Function],
"version": "v2",
},
],
"featureFlags": [],
"pluginId": "test",
"toString": [Function],
"version": "v1",
}
`);
});
});
@@ -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 CreateFrontendModuleOptions<
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: CreateFrontendModuleOptions<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,13 +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 id: 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<
TId extends string,
@@ -46,10 +66,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[];
@@ -63,7 +83,7 @@ export function createFrontendPlugin<
TExtensions extends readonly ExtensionDefinition[] = [],
>(
options: PluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>,
): BackstagePlugin<
): FrontendPlugin<
TRoutes,
TExternalRoutes,
{
@@ -105,7 +125,7 @@ export function createFrontendPlugin<
}
return {
$$type: '@backstage/BackstagePlugin',
$$type: '@backstage/FrontendPlugin',
version: 'v1',
id: options.id,
routes: options.routes ?? ({} as TRoutes),
@@ -135,15 +155,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'
) {
// Make sure we throw if invalid
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') {
@@ -39,8 +39,15 @@ export {
export {
createPlugin,
createFrontendPlugin,
type FrontendPlugin,
type BackstagePlugin,
type PluginOptions,
} from './createFrontendPlugin';
export {
createFrontendModule,
type FrontendModule,
type CreateFrontendModuleOptions,
} 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,7 @@
import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing';
import { ExtensionDefinition } from './createExtension';
import { FrontendPlugin } from './createFrontendPlugin';
/**
* Feature flag configuration.
@@ -40,26 +41,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;
/**
* @public
* @deprecated import from {@link @backstage/frontend-app-api#FrontendFeature} instead
*/
export type FrontendFeature = FrontendPlugin | ExtensionOverrides;