@backstage/frontend-plugin-api 0.12.2-next.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/apis/definitions/AppTreeApi.esm.js.map +1 -1
- package/dist/components/createSwappableComponent.esm.js +1 -0
- package/dist/components/createSwappableComponent.esm.js.map +1 -1
- package/dist/frontend-internal/src/routing/OpaqueExternalRouteRef.esm.js +9 -0
- package/dist/frontend-internal/src/routing/OpaqueExternalRouteRef.esm.js.map +1 -0
- package/dist/frontend-internal/src/routing/OpaqueRouteRef.esm.js +9 -0
- package/dist/frontend-internal/src/routing/OpaqueRouteRef.esm.js.map +1 -0
- package/dist/frontend-internal/src/routing/OpaqueSubRouteRef.esm.js +9 -0
- package/dist/frontend-internal/src/routing/OpaqueSubRouteRef.esm.js.map +1 -0
- package/dist/frontend-internal/src/wiring/InternalExtensionDefinition.esm.js.map +1 -1
- package/dist/frontend-internal/src/wiring/InternalExtensionInput.esm.js +9 -0
- package/dist/frontend-internal/src/wiring/InternalExtensionInput.esm.js.map +1 -0
- package/dist/index.d.ts +107 -32
- package/dist/routing/ExternalRouteRef.esm.js +36 -20
- package/dist/routing/ExternalRouteRef.esm.js.map +1 -1
- package/dist/routing/RouteRef.esm.js +34 -56
- package/dist/routing/RouteRef.esm.js.map +1 -1
- package/dist/routing/SubRouteRef.esm.js +21 -32
- package/dist/routing/SubRouteRef.esm.js.map +1 -1
- package/dist/wiring/createExtension.esm.js +58 -6
- package/dist/wiring/createExtension.esm.js.map +1 -1
- package/dist/wiring/createExtensionBlueprint.esm.js.map +1 -1
- package/dist/wiring/createExtensionInput.esm.js +11 -2
- package/dist/wiring/createExtensionInput.esm.js.map +1 -1
- package/dist/wiring/createFrontendPlugin.esm.js +2 -1
- package/dist/wiring/createFrontendPlugin.esm.js.map +1 -1
- package/dist/wiring/resolveExtensionDefinition.esm.js +49 -9
- package/dist/wiring/resolveExtensionDefinition.esm.js.map +1 -1
- package/dist/wiring/resolveInputOverrides.esm.js +2 -1
- package/dist/wiring/resolveInputOverrides.esm.js.map +1 -1
- package/package.json +9 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createExtensionInput.esm.js","sources":["../../src/wiring/createExtensionInput.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ExtensionDataRef } from './createExtensionDataRef';\n\n/** @public */\nexport interface ExtensionInput<\n UExtensionData extends ExtensionDataRef<\n unknown,\n string,\n { optional?: true }\n > = ExtensionDataRef,\n TConfig extends { singleton: boolean; optional: boolean } = {\n singleton: boolean;\n optional: boolean;\n },\n> {\n $$type: '@backstage/ExtensionInput';\n extensionData: Array<UExtensionData>;\n config: TConfig;\n replaces?: Array<{ id: string; input: string }>;\n}\n\n/** @public */\nexport function createExtensionInput<\n UExtensionData extends ExtensionDataRef<unknown, string, { optional?: true }>,\n TConfig extends { singleton?: boolean; optional?: boolean },\n>(\n extensionData: Array<UExtensionData>,\n config?: TConfig & { replaces?: Array<{ id: string; input: string }> },\n): ExtensionInput<\n UExtensionData,\n {\n singleton: TConfig['singleton'] extends true ? true : false;\n optional: TConfig['optional'] extends true ? true : false;\n }\n> {\n if (process.env.NODE_ENV !== 'production') {\n if (Array.isArray(extensionData)) {\n const seen = new Set();\n const duplicates = [];\n for (const dataRef of extensionData) {\n if (seen.has(dataRef.id)) {\n duplicates.push(dataRef.id);\n } else {\n seen.add(dataRef.id);\n }\n }\n if (duplicates.length > 0) {\n throw new Error(\n `ExtensionInput may not have duplicate data refs: '${duplicates.join(\n \"', '\",\n )}'`,\n );\n }\n }\n }\n
|
|
1
|
+
{"version":3,"file":"createExtensionInput.esm.js","sources":["../../src/wiring/createExtensionInput.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ExtensionInputContext,\n OpaqueExtensionInput,\n} from '@internal/frontend';\nimport { ExtensionDataRef } from './createExtensionDataRef';\n\n/** @public */\nexport interface ExtensionInput<\n UExtensionData extends ExtensionDataRef<\n unknown,\n string,\n { optional?: true }\n > = ExtensionDataRef,\n TConfig extends { singleton: boolean; optional: boolean } = {\n singleton: boolean;\n optional: boolean;\n },\n> {\n readonly $$type: '@backstage/ExtensionInput';\n readonly extensionData: Array<UExtensionData>;\n readonly config: TConfig;\n readonly replaces?: Array<{ id: string; input: string }>;\n}\n\n/** @public */\nexport function createExtensionInput<\n UExtensionData extends ExtensionDataRef<unknown, string, { optional?: true }>,\n TConfig extends { singleton?: boolean; optional?: boolean },\n>(\n extensionData: Array<UExtensionData>,\n config?: TConfig & { replaces?: Array<{ id: string; input: string }> },\n): ExtensionInput<\n UExtensionData,\n {\n singleton: TConfig['singleton'] extends true ? true : false;\n optional: TConfig['optional'] extends true ? true : false;\n }\n> {\n if (process.env.NODE_ENV !== 'production') {\n if (Array.isArray(extensionData)) {\n const seen = new Set();\n const duplicates = [];\n for (const dataRef of extensionData) {\n if (seen.has(dataRef.id)) {\n duplicates.push(dataRef.id);\n } else {\n seen.add(dataRef.id);\n }\n }\n if (duplicates.length > 0) {\n throw new Error(\n `ExtensionInput may not have duplicate data refs: '${duplicates.join(\n \"', '\",\n )}'`,\n );\n }\n }\n }\n const baseOptions = {\n extensionData,\n config: {\n singleton: Boolean(config?.singleton) as TConfig['singleton'] extends true\n ? true\n : false,\n optional: Boolean(config?.optional) as TConfig['optional'] extends true\n ? true\n : false,\n },\n replaces: config?.replaces,\n };\n\n function createInstance(parent?: ExtensionInputContext): ExtensionInput<\n UExtensionData,\n {\n singleton: TConfig['singleton'] extends true ? true : false;\n optional: TConfig['optional'] extends true ? true : false;\n }\n > {\n return OpaqueExtensionInput.createInstance(undefined, {\n ...baseOptions,\n context: parent,\n withContext: createInstance,\n });\n }\n\n return createInstance();\n}\n"],"names":[],"mappings":";;AAyCO,SAAS,oBAAA,CAId,eACA,MAAA,EAOA;AACA,EAAA,IAAI,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,YAAA,EAAc;AACzC,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,aAAa,CAAA,EAAG;AAChC,MAAA,MAAM,IAAA,uBAAW,GAAA,EAAI;AACrB,MAAA,MAAM,aAAa,EAAC;AACpB,MAAA,KAAA,MAAW,WAAW,aAAA,EAAe;AACnC,QAAA,IAAI,IAAA,CAAK,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA,EAAG;AACxB,UAAA,UAAA,CAAW,IAAA,CAAK,QAAQ,EAAE,CAAA;AAAA,QAC5B,CAAA,MAAO;AACL,UAAA,IAAA,CAAK,GAAA,CAAI,QAAQ,EAAE,CAAA;AAAA,QACrB;AAAA,MACF;AACA,MAAA,IAAI,UAAA,CAAW,SAAS,CAAA,EAAG;AACzB,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,qDAAqD,UAAA,CAAW,IAAA;AAAA,YAC9D;AAAA,WACD,CAAA,CAAA;AAAA,SACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,EAAA,MAAM,WAAA,GAAc;AAAA,IAClB,aAAA;AAAA,IACA,MAAA,EAAQ;AAAA,MACN,SAAA,EAAW,OAAA,CAAQ,MAAA,EAAQ,SAAS,CAAA;AAAA,MAGpC,QAAA,EAAU,OAAA,CAAQ,MAAA,EAAQ,QAAQ;AAAA,KAGpC;AAAA,IACA,UAAU,MAAA,EAAQ;AAAA,GACpB;AAEA,EAAA,SAAS,eAAe,MAAA,EAMtB;AACA,IAAA,OAAO,oBAAA,CAAqB,eAAe,MAAA,EAAW;AAAA,MACpD,GAAG,WAAA;AAAA,MACH,OAAA,EAAS,MAAA;AAAA,MACT,WAAA,EAAa;AAAA,KACd,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,cAAA,EAAe;AACxB;;;;"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveExtensionDefinition } from './resolveExtensionDefinition.esm.js';
|
|
2
2
|
import { OpaqueFrontendPlugin } from '../frontend-internal/src/wiring/InternalFrontendPlugin.esm.js';
|
|
3
|
-
import '../frontend-internal/src/wiring/InternalSwappableComponentRef.esm.js';
|
|
4
3
|
import { OpaqueExtensionDefinition } from '../frontend-internal/src/wiring/InternalExtensionDefinition.esm.js';
|
|
4
|
+
import '../frontend-internal/src/wiring/InternalExtensionInput.esm.js';
|
|
5
|
+
import '../frontend-internal/src/wiring/InternalSwappableComponentRef.esm.js';
|
|
5
6
|
|
|
6
7
|
function createFrontendPlugin(options) {
|
|
7
8
|
const pluginId = options.pluginId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createFrontendPlugin.esm.js","sources":["../../src/wiring/createFrontendPlugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n OpaqueExtensionDefinition,\n OpaqueFrontendPlugin,\n} from '@internal/frontend';\nimport { ExtensionDefinition } from './createExtension';\nimport {\n Extension,\n resolveExtensionDefinition,\n} from './resolveExtensionDefinition';\nimport { FeatureFlagConfig } from './types';\nimport { MakeSortedExtensionsMap } from './MakeSortedExtensionsMap';\nimport { JsonObject } from '@backstage/types';\nimport { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing';\n\n/**\n * Information about the plugin.\n *\n * @public\n * @remarks\n *\n * This interface is intended to be extended via [module\n * augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)\n * in order to add fields that are specific to each project.\n *\n * For example, one might add a `slackChannel` field that is read from the\n * opaque manifest file.\n *\n * See the options for `createApp` for more information about how to\n * customize the parsing of manifest files.\n */\nexport interface FrontendPluginInfo {\n /**\n * The name of the package that implements the plugin.\n */\n packageName?: string;\n\n /**\n * The version of the plugin, typically the version of the package.json file.\n */\n version?: string;\n\n /**\n * As short description of the plugin, typically the description field in\n * package.json.\n */\n description?: string;\n\n /**\n * The owner entity references of the plugin.\n */\n ownerEntityRefs?: string[];\n\n /**\n * Links related to the plugin.\n */\n links?: Array<{ title: string; url: string }>;\n}\n\n/**\n * Options for providing information for a plugin.\n *\n * @public\n */\nexport type FrontendPluginInfoOptions = {\n /**\n * A loader function for the package.json file for the plugin.\n */\n packageJson?: () => Promise<{ name: string } & JsonObject>;\n /**\n * A loader function for an opaque manifest file for the plugin.\n */\n manifest?: () => Promise<JsonObject>;\n};\n\n/**\n * A variant of the {@link FrontendPlugin} interface that can also be used to install overrides for the plugin.\n *\n * @public\n */\nexport interface OverridableFrontendPlugin<\n TRoutes extends { [name in string]: RouteRef | SubRouteRef } = {\n [name in string]: RouteRef | SubRouteRef;\n },\n TExternalRoutes extends { [name in string]: ExternalRouteRef } = {\n [name in string]: ExternalRouteRef;\n },\n TExtensionMap extends { [id in string]: ExtensionDefinition } = {\n [id in string]: ExtensionDefinition;\n },\n> extends FrontendPlugin<TRoutes, TExternalRoutes> {\n getExtension<TId extends keyof TExtensionMap>(id: TId): TExtensionMap[TId];\n withOverrides(options: {\n extensions: Array<ExtensionDefinition>;\n\n /**\n * Overrides the original info loaders of the plugin one by one.\n */\n info?: FrontendPluginInfoOptions;\n }): OverridableFrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;\n}\n\n/** @public */\nexport interface FrontendPlugin<\n TRoutes extends { [name in string]: RouteRef | SubRouteRef } = {\n [name in string]: RouteRef | SubRouteRef;\n },\n TExternalRoutes extends { [name in string]: ExternalRouteRef } = {\n [name in string]: ExternalRouteRef;\n },\n> {\n readonly $$type: '@backstage/FrontendPlugin';\n readonly id: string;\n readonly routes: TRoutes;\n readonly externalRoutes: TExternalRoutes;\n\n /**\n * Loads the plugin info.\n */\n info(): Promise<FrontendPluginInfo>;\n}\n\n/** @public */\nexport interface PluginOptions<\n TId extends string,\n TRoutes extends { [name in string]: RouteRef | SubRouteRef },\n TExternalRoutes extends { [name in string]: ExternalRouteRef },\n TExtensions extends readonly ExtensionDefinition[],\n> {\n pluginId: TId;\n routes?: TRoutes;\n externalRoutes?: TExternalRoutes;\n extensions?: TExtensions;\n featureFlags?: FeatureFlagConfig[];\n info?: FrontendPluginInfoOptions;\n}\n\n/**\n * Creates a new plugin that can be installed in a Backstage app.\n *\n * @remarks\n *\n * Every plugin is created with a unique ID and a set of extensions\n * that are installed as part of the plugin.\n *\n * For more information on how plugins work, see the\n * {@link https://backstage.io/docs/frontend-system/building-plugins/index | documentation for plugins}\n * in the frontend system documentation.\n *\n * @example\n *\n * ```tsx\n * import { createFrontendPlugin } from '@backstage/frontend-plugin-api';\n *\n * export const examplePlugin = createFrontendPlugin({\n * pluginId: 'example',\n * extensions: [\n * PageBlueprint.make({\n * path: '/example',\n * loader: () => import('./ExamplePage').then(m => <m.ExamplePage />),\n * }),\n * ],\n * });\n * ```\n *\n * @public\n */\nexport function createFrontendPlugin<\n TId extends string,\n TExtensions extends readonly ExtensionDefinition[],\n TRoutes extends { [name in string]: RouteRef | SubRouteRef } = {},\n TExternalRoutes extends { [name in string]: ExternalRouteRef } = {},\n>(\n options: PluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>,\n): OverridableFrontendPlugin<\n TRoutes,\n TExternalRoutes,\n MakeSortedExtensionsMap<TExtensions[number], TId>\n> {\n const pluginId = options.pluginId;\n\n const extensions = new Array<Extension<any>>();\n const extensionDefinitionsById = new Map<\n string,\n typeof OpaqueExtensionDefinition.TInternal\n >();\n\n for (const def of options.extensions ?? []) {\n const internal = OpaqueExtensionDefinition.toInternal(def);\n const ext = resolveExtensionDefinition(def, { namespace: pluginId });\n extensions.push(ext);\n extensionDefinitionsById.set(ext.id, {\n ...internal,\n namespace: pluginId,\n });\n }\n\n if (extensions.length !== extensionDefinitionsById.size) {\n const extensionIds = extensions.map(e => e.id);\n const duplicates = Array.from(\n new Set(\n extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index),\n ),\n );\n // TODO(Rugvip): This could provide some more information about the kind + name of the extensions\n throw new Error(\n `Plugin '${pluginId}' provided duplicate extensions: ${duplicates.join(\n ', ',\n )}`,\n );\n }\n\n return OpaqueFrontendPlugin.createInstance('v1', {\n id: pluginId,\n routes: options.routes ?? ({} as TRoutes),\n externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),\n featureFlags: options.featureFlags ?? [],\n extensions: extensions,\n infoOptions: options.info,\n\n // This method is overridden when the plugin instance is installed in an app\n async info() {\n throw new Error(\n `Attempted to load plugin info for plugin '${pluginId}', but the plugin instance is not installed in an app`,\n );\n },\n getExtension(id) {\n const ext = extensionDefinitionsById.get(id);\n if (!ext) {\n throw new Error(\n `Attempted to get non-existent extension '${id}' from plugin '${pluginId}'`,\n );\n }\n return ext;\n },\n toString() {\n return `Plugin{id=${pluginId}}`;\n },\n withOverrides(overrides) {\n const overriddenExtensionIds = new Set(\n overrides.extensions.map(\n e => resolveExtensionDefinition(e, { namespace: pluginId }).id,\n ),\n );\n const nonOverriddenExtensions = (options.extensions ?? []).filter(\n e =>\n !overriddenExtensionIds.has(\n resolveExtensionDefinition(e, { namespace: pluginId }).id,\n ),\n );\n return createFrontendPlugin({\n ...options,\n pluginId,\n extensions: [...nonOverriddenExtensions, ...overrides.extensions],\n info: {\n ...options.info,\n ...overrides.info,\n },\n });\n },\n });\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createFrontendPlugin.esm.js","sources":["../../src/wiring/createFrontendPlugin.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n OpaqueExtensionDefinition,\n OpaqueFrontendPlugin,\n} from '@internal/frontend';\nimport {\n ExtensionDefinition,\n OverridableExtensionDefinition,\n} from './createExtension';\nimport {\n Extension,\n resolveExtensionDefinition,\n} from './resolveExtensionDefinition';\nimport { FeatureFlagConfig } from './types';\nimport { MakeSortedExtensionsMap } from './MakeSortedExtensionsMap';\nimport { JsonObject } from '@backstage/types';\nimport { RouteRef, SubRouteRef, ExternalRouteRef } from '../routing';\n\n/**\n * Information about the plugin.\n *\n * @public\n * @remarks\n *\n * This interface is intended to be extended via [module\n * augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)\n * in order to add fields that are specific to each project.\n *\n * For example, one might add a `slackChannel` field that is read from the\n * opaque manifest file.\n *\n * See the options for `createApp` for more information about how to\n * customize the parsing of manifest files.\n */\nexport interface FrontendPluginInfo {\n /**\n * The name of the package that implements the plugin.\n */\n packageName?: string;\n\n /**\n * The version of the plugin, typically the version of the package.json file.\n */\n version?: string;\n\n /**\n * As short description of the plugin, typically the description field in\n * package.json.\n */\n description?: string;\n\n /**\n * The owner entity references of the plugin.\n */\n ownerEntityRefs?: string[];\n\n /**\n * Links related to the plugin.\n */\n links?: Array<{ title: string; url: string }>;\n}\n\n/**\n * Options for providing information for a plugin.\n *\n * @public\n */\nexport type FrontendPluginInfoOptions = {\n /**\n * A loader function for the package.json file for the plugin.\n */\n packageJson?: () => Promise<{ name: string } & JsonObject>;\n /**\n * A loader function for an opaque manifest file for the plugin.\n */\n manifest?: () => Promise<JsonObject>;\n};\n\n/**\n * A variant of the {@link FrontendPlugin} interface that can also be used to install overrides for the plugin.\n *\n * @public\n */\nexport interface OverridableFrontendPlugin<\n TRoutes extends { [name in string]: RouteRef | SubRouteRef } = {\n [name in string]: RouteRef | SubRouteRef;\n },\n TExternalRoutes extends { [name in string]: ExternalRouteRef } = {\n [name in string]: ExternalRouteRef;\n },\n TExtensionMap extends { [id in string]: ExtensionDefinition } = {\n [id in string]: ExtensionDefinition;\n },\n> extends FrontendPlugin<TRoutes, TExternalRoutes> {\n getExtension<TId extends keyof TExtensionMap>(\n id: TId,\n ): OverridableExtensionDefinition<TExtensionMap[TId]['T']>;\n withOverrides(options: {\n extensions: Array<ExtensionDefinition>;\n\n /**\n * Overrides the original info loaders of the plugin one by one.\n */\n info?: FrontendPluginInfoOptions;\n }): OverridableFrontendPlugin<TRoutes, TExternalRoutes, TExtensionMap>;\n}\n\n/** @public */\nexport interface FrontendPlugin<\n TRoutes extends { [name in string]: RouteRef | SubRouteRef } = {\n [name in string]: RouteRef | SubRouteRef;\n },\n TExternalRoutes extends { [name in string]: ExternalRouteRef } = {\n [name in string]: ExternalRouteRef;\n },\n> {\n readonly $$type: '@backstage/FrontendPlugin';\n readonly id: string;\n readonly routes: TRoutes;\n readonly externalRoutes: TExternalRoutes;\n\n /**\n * Loads the plugin info.\n */\n info(): Promise<FrontendPluginInfo>;\n}\n\n/** @public */\nexport interface PluginOptions<\n TId extends string,\n TRoutes extends { [name in string]: RouteRef | SubRouteRef },\n TExternalRoutes extends { [name in string]: ExternalRouteRef },\n TExtensions extends readonly ExtensionDefinition[],\n> {\n pluginId: TId;\n routes?: TRoutes;\n externalRoutes?: TExternalRoutes;\n extensions?: TExtensions;\n featureFlags?: FeatureFlagConfig[];\n info?: FrontendPluginInfoOptions;\n}\n\n/**\n * Creates a new plugin that can be installed in a Backstage app.\n *\n * @remarks\n *\n * Every plugin is created with a unique ID and a set of extensions\n * that are installed as part of the plugin.\n *\n * For more information on how plugins work, see the\n * {@link https://backstage.io/docs/frontend-system/building-plugins/index | documentation for plugins}\n * in the frontend system documentation.\n *\n * @example\n *\n * ```tsx\n * import { createFrontendPlugin } from '@backstage/frontend-plugin-api';\n *\n * export const examplePlugin = createFrontendPlugin({\n * pluginId: 'example',\n * extensions: [\n * PageBlueprint.make({\n * path: '/example',\n * loader: () => import('./ExamplePage').then(m => <m.ExamplePage />),\n * }),\n * ],\n * });\n * ```\n *\n * @public\n */\nexport function createFrontendPlugin<\n TId extends string,\n TExtensions extends readonly ExtensionDefinition[],\n TRoutes extends { [name in string]: RouteRef | SubRouteRef } = {},\n TExternalRoutes extends { [name in string]: ExternalRouteRef } = {},\n>(\n options: PluginOptions<TId, TRoutes, TExternalRoutes, TExtensions>,\n): OverridableFrontendPlugin<\n TRoutes,\n TExternalRoutes,\n MakeSortedExtensionsMap<TExtensions[number], TId>\n> {\n const pluginId = options.pluginId;\n\n const extensions = new Array<Extension<any>>();\n const extensionDefinitionsById = new Map<\n string,\n typeof OpaqueExtensionDefinition.TInternal\n >();\n\n for (const def of options.extensions ?? []) {\n const internal = OpaqueExtensionDefinition.toInternal(def);\n const ext = resolveExtensionDefinition(def, { namespace: pluginId });\n extensions.push(ext);\n extensionDefinitionsById.set(ext.id, {\n ...internal,\n namespace: pluginId,\n });\n }\n\n if (extensions.length !== extensionDefinitionsById.size) {\n const extensionIds = extensions.map(e => e.id);\n const duplicates = Array.from(\n new Set(\n extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index),\n ),\n );\n // TODO(Rugvip): This could provide some more information about the kind + name of the extensions\n throw new Error(\n `Plugin '${pluginId}' provided duplicate extensions: ${duplicates.join(\n ', ',\n )}`,\n );\n }\n\n return OpaqueFrontendPlugin.createInstance('v1', {\n id: pluginId,\n routes: options.routes ?? ({} as TRoutes),\n externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes),\n featureFlags: options.featureFlags ?? [],\n extensions: extensions,\n infoOptions: options.info,\n\n // This method is overridden when the plugin instance is installed in an app\n async info() {\n throw new Error(\n `Attempted to load plugin info for plugin '${pluginId}', but the plugin instance is not installed in an app`,\n );\n },\n getExtension(id) {\n const ext = extensionDefinitionsById.get(id);\n if (!ext) {\n throw new Error(\n `Attempted to get non-existent extension '${id}' from plugin '${pluginId}'`,\n );\n }\n return ext;\n },\n toString() {\n return `Plugin{id=${pluginId}}`;\n },\n withOverrides(overrides) {\n const overriddenExtensionIds = new Set(\n overrides.extensions.map(\n e => resolveExtensionDefinition(e, { namespace: pluginId }).id,\n ),\n );\n const nonOverriddenExtensions = (options.extensions ?? []).filter(\n e =>\n !overriddenExtensionIds.has(\n resolveExtensionDefinition(e, { namespace: pluginId }).id,\n ),\n );\n return createFrontendPlugin({\n ...options,\n pluginId,\n extensions: [...nonOverriddenExtensions, ...overrides.extensions],\n info: {\n ...options.info,\n ...overrides.info,\n },\n });\n },\n });\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,24 +1,64 @@
|
|
|
1
1
|
import { OpaqueExtensionDefinition } from '../frontend-internal/src/wiring/InternalExtensionDefinition.esm.js';
|
|
2
|
+
import { OpaqueExtensionInput } from '../frontend-internal/src/wiring/InternalExtensionInput.esm.js';
|
|
2
3
|
|
|
4
|
+
function resolveExtensionId(kind, namespace, name) {
|
|
5
|
+
const namePart = name && namespace ? `${namespace}/${name}` : namespace || name;
|
|
6
|
+
if (!namePart) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
`Extension must declare an explicit namespace or name as it could not be resolved from context, kind=${kind} namespace=${namespace} name=${name}`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return kind ? `${kind}:${namePart}` : namePart;
|
|
12
|
+
}
|
|
13
|
+
function resolveAttachTo(attachTo, namespace) {
|
|
14
|
+
const resolveSpec = (spec) => {
|
|
15
|
+
if (OpaqueExtensionInput.isType(spec)) {
|
|
16
|
+
const { context } = OpaqueExtensionInput.toInternal(spec);
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Invalid input object without a parent extension used as attachment point"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
id: resolveExtensionId(context.kind, namespace, context.name),
|
|
24
|
+
input: context.input
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if ("relative" in spec && spec.relative) {
|
|
28
|
+
return {
|
|
29
|
+
id: resolveExtensionId(
|
|
30
|
+
spec.relative.kind,
|
|
31
|
+
namespace,
|
|
32
|
+
spec.relative.name
|
|
33
|
+
),
|
|
34
|
+
input: spec.input
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if ("id" in spec) {
|
|
38
|
+
return { id: spec.id, input: spec.input };
|
|
39
|
+
}
|
|
40
|
+
throw new Error("Invalid attachment point specification");
|
|
41
|
+
};
|
|
42
|
+
if (Array.isArray(attachTo)) {
|
|
43
|
+
return attachTo.map(resolveSpec);
|
|
44
|
+
}
|
|
45
|
+
return resolveSpec(attachTo);
|
|
46
|
+
}
|
|
3
47
|
function resolveExtensionDefinition(definition, context) {
|
|
4
48
|
const internalDefinition = OpaqueExtensionDefinition.toInternal(definition);
|
|
5
49
|
const {
|
|
6
50
|
name,
|
|
7
51
|
kind,
|
|
8
|
-
namespace:
|
|
52
|
+
namespace: internalNamespace,
|
|
9
53
|
override: _skip2,
|
|
54
|
+
attachTo,
|
|
10
55
|
...rest
|
|
11
56
|
} = internalDefinition;
|
|
12
|
-
const namespace =
|
|
13
|
-
const
|
|
14
|
-
if (!namePart) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
`Extension must declare an explicit namespace or name as it could not be resolved from context, kind=${kind} namespace=${namespace} name=${name}`
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
const id = kind ? `${kind}:${namePart}` : namePart;
|
|
57
|
+
const namespace = internalNamespace ?? context?.namespace;
|
|
58
|
+
const id = resolveExtensionId(kind, namespace, name);
|
|
20
59
|
return {
|
|
21
60
|
...rest,
|
|
61
|
+
attachTo: resolveAttachTo(attachTo, namespace),
|
|
22
62
|
$$type: "@backstage/Extension",
|
|
23
63
|
version: internalDefinition.version,
|
|
24
64
|
id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveExtensionDefinition.esm.js","sources":["../../src/wiring/resolveExtensionDefinition.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ApiHolder, AppNode } from '../apis';\nimport {\n
|
|
1
|
+
{"version":3,"file":"resolveExtensionDefinition.esm.js","sources":["../../src/wiring/resolveExtensionDefinition.ts"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ApiHolder, AppNode } from '../apis';\nimport {\n ExtensionDefinitionAttachTo,\n ExtensionDefinition,\n ExtensionDefinitionParameters,\n ResolvedExtensionInputs,\n} from './createExtension';\nimport { PortableSchema } from '../schema';\nimport { ExtensionInput } from './createExtensionInput';\nimport { ExtensionDataRef, ExtensionDataValue } from './createExtensionDataRef';\nimport {\n OpaqueExtensionDefinition,\n OpaqueExtensionInput,\n} from '@internal/frontend';\n\n/** @public */\nexport type ExtensionAttachTo =\n | { id: string; input: string }\n | Array<{ id: string; input: string }>;\n\n/**\n * @deprecated Use {@link ExtensionAttachTo} instead.\n * @public\n */\nexport type ExtensionAttachToSpec = ExtensionAttachTo;\n\n/** @public */\nexport interface Extension<TConfig, TConfigInput = TConfig> {\n $$type: '@backstage/Extension';\n readonly id: string;\n readonly attachTo: ExtensionAttachToSpec;\n readonly disabled: boolean;\n readonly configSchema?: PortableSchema<TConfig, TConfigInput>;\n}\n\n/** @internal */\nexport type InternalExtension<TConfig, TConfigInput> = Extension<\n TConfig,\n TConfigInput\n> &\n (\n | {\n readonly version: 'v1';\n readonly inputs: {\n [inputName in string]: {\n $$type: '@backstage/ExtensionInput';\n extensionData: {\n [name in string]: ExtensionDataRef;\n };\n config: { optional: boolean; singleton: boolean };\n };\n };\n readonly output: {\n [name in string]: ExtensionDataRef;\n };\n factory(context: {\n apis: ApiHolder;\n node: AppNode;\n config: TConfig;\n inputs: {\n [inputName in string]: unknown;\n };\n }): {\n [inputName in string]: unknown;\n };\n }\n | {\n readonly version: 'v2';\n readonly inputs: { [inputName in string]: ExtensionInput };\n readonly output: Array<ExtensionDataRef>;\n factory(options: {\n apis: ApiHolder;\n node: AppNode;\n config: TConfig;\n inputs: ResolvedExtensionInputs<{\n [inputName in string]: ExtensionInput;\n }>;\n }): Iterable<ExtensionDataValue<any, any>>;\n }\n );\n\n/** @internal */\nexport function toInternalExtension<TConfig, TConfigInput>(\n overrides: Extension<TConfig, TConfigInput>,\n): InternalExtension<TConfig, TConfigInput> {\n const internal = overrides as InternalExtension<TConfig, TConfigInput>;\n if (internal.$$type !== '@backstage/Extension') {\n throw new Error(\n `Invalid extension instance, bad type '${internal.$$type}'`,\n );\n }\n const version = internal.version;\n if (version !== 'v1' && version !== 'v2') {\n throw new Error(`Invalid extension instance, bad version '${version}'`);\n }\n return internal;\n}\n\n/** @ignore */\nexport type ResolveExtensionId<\n TExtension extends ExtensionDefinition,\n TNamespace extends string,\n> = TExtension extends ExtensionDefinition<{\n kind: infer IKind extends string | undefined;\n name: infer IName extends string | undefined;\n params: any;\n}>\n ? [string] extends [IKind | IName]\n ? never\n : (\n undefined extends IName ? TNamespace : `${TNamespace}/${IName}`\n ) extends infer INamePart extends string\n ? IKind extends string\n ? `${IKind}:${INamePart}`\n : INamePart\n : never\n : never;\n\nfunction resolveExtensionId(\n kind?: string,\n namespace?: string,\n name?: string,\n): string {\n const namePart =\n name && namespace ? `${namespace}/${name}` : namespace || name;\n if (!namePart) {\n throw new Error(\n `Extension must declare an explicit namespace or name as it could not be resolved from context, kind=${kind} namespace=${namespace} name=${name}`,\n );\n }\n\n return kind ? `${kind}:${namePart}` : namePart;\n}\n\nfunction resolveAttachTo(\n attachTo: ExtensionDefinitionAttachTo,\n namespace?: string,\n): ExtensionAttachToSpec {\n const resolveSpec = (\n spec: Exclude<ExtensionDefinitionAttachTo, Array<any>>,\n ): { id: string; input: string } => {\n if (OpaqueExtensionInput.isType(spec)) {\n const { context } = OpaqueExtensionInput.toInternal(spec);\n if (!context) {\n throw new Error(\n 'Invalid input object without a parent extension used as attachment point',\n );\n }\n return {\n id: resolveExtensionId(context.kind, namespace, context.name),\n input: context.input,\n };\n }\n if ('relative' in spec && spec.relative) {\n return {\n id: resolveExtensionId(\n spec.relative.kind,\n namespace,\n spec.relative.name,\n ),\n input: spec.input,\n };\n }\n if ('id' in spec) {\n return { id: spec.id, input: spec.input };\n }\n throw new Error('Invalid attachment point specification');\n };\n\n if (Array.isArray(attachTo)) {\n return attachTo.map(resolveSpec);\n }\n\n return resolveSpec(attachTo);\n}\n\n/** @internal */\nexport function resolveExtensionDefinition<\n T extends ExtensionDefinitionParameters,\n>(\n definition: ExtensionDefinition<T>,\n context?: { namespace?: string },\n): Extension<T['config'], T['configInput']> {\n const internalDefinition = OpaqueExtensionDefinition.toInternal(definition);\n\n const {\n name,\n kind,\n namespace: internalNamespace,\n override: _skip2,\n attachTo,\n ...rest\n } = internalDefinition;\n\n const namespace = internalNamespace ?? context?.namespace;\n const id = resolveExtensionId(kind, namespace, name);\n\n return {\n ...rest,\n attachTo: resolveAttachTo(attachTo, namespace),\n $$type: '@backstage/Extension',\n version: internalDefinition.version,\n id,\n toString() {\n return `Extension{id=${id}}`;\n },\n } as InternalExtension<T['config'], T['configInput']> & Object;\n}\n"],"names":[],"mappings":";;;AAsIA,SAAS,kBAAA,CACP,IAAA,EACA,SAAA,EACA,IAAA,EACQ;AACR,EAAA,MAAM,QAAA,GACJ,QAAQ,SAAA,GAAY,CAAA,EAAG,SAAS,CAAA,CAAA,EAAI,IAAI,KAAK,SAAA,IAAa,IAAA;AAC5D,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,oGAAA,EAAuG,IAAI,CAAA,WAAA,EAAc,SAAS,SAAS,IAAI,CAAA;AAAA,KACjJ;AAAA,EACF;AAEA,EAAA,OAAO,IAAA,GAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA,GAAK,QAAA;AACxC;AAEA,SAAS,eAAA,CACP,UACA,SAAA,EACuB;AACvB,EAAA,MAAM,WAAA,GAAc,CAClB,IAAA,KACkC;AAClC,IAAA,IAAI,oBAAA,CAAqB,MAAA,CAAO,IAAI,CAAA,EAAG;AACrC,MAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,oBAAA,CAAqB,WAAW,IAAI,CAAA;AACxD,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,MAAM,IAAI,KAAA;AAAA,UACR;AAAA,SACF;AAAA,MACF;AACA,MAAA,OAAO;AAAA,QACL,IAAI,kBAAA,CAAmB,OAAA,CAAQ,IAAA,EAAM,SAAA,EAAW,QAAQ,IAAI,CAAA;AAAA,QAC5D,OAAO,OAAA,CAAQ;AAAA,OACjB;AAAA,IACF;AACA,IAAA,IAAI,UAAA,IAAc,IAAA,IAAQ,IAAA,CAAK,QAAA,EAAU;AACvC,MAAA,OAAO;AAAA,QACL,EAAA,EAAI,kBAAA;AAAA,UACF,KAAK,QAAA,CAAS,IAAA;AAAA,UACd,SAAA;AAAA,UACA,KAAK,QAAA,CAAS;AAAA,SAChB;AAAA,QACA,OAAO,IAAA,CAAK;AAAA,OACd;AAAA,IACF;AACA,IAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,MAAA,OAAO,EAAE,EAAA,EAAI,IAAA,CAAK,EAAA,EAAI,KAAA,EAAO,KAAK,KAAA,EAAM;AAAA,IAC1C;AACA,IAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,EAC1D,CAAA;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC3B,IAAA,OAAO,QAAA,CAAS,IAAI,WAAW,CAAA;AAAA,EACjC;AAEA,EAAA,OAAO,YAAY,QAAQ,CAAA;AAC7B;AAGO,SAAS,0BAAA,CAGd,YACA,OAAA,EAC0C;AAC1C,EAAA,MAAM,kBAAA,GAAqB,yBAAA,CAA0B,UAAA,CAAW,UAAU,CAAA;AAE1E,EAAA,MAAM;AAAA,IACJ,IAAA;AAAA,IACA,IAAA;AAAA,IACA,SAAA,EAAW,iBAAA;AAAA,IACX,QAAA,EAAU,MAAA;AAAA,IACV,QAAA;AAAA,IACA,GAAG;AAAA,GACL,GAAI,kBAAA;AAEJ,EAAA,MAAM,SAAA,GAAY,qBAAqB,OAAA,EAAS,SAAA;AAChD,EAAA,MAAM,EAAA,GAAK,kBAAA,CAAmB,IAAA,EAAM,SAAA,EAAW,IAAI,CAAA;AAEnD,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,QAAA,EAAU,eAAA,CAAgB,QAAA,EAAU,SAAS,CAAA;AAAA,IAC7C,MAAA,EAAQ,sBAAA;AAAA,IACR,SAAS,kBAAA,CAAmB,OAAA;AAAA,IAC5B,EAAA;AAAA,IACA,QAAA,GAAW;AACT,MAAA,OAAO,gBAAgB,EAAE,CAAA,CAAA,CAAA;AAAA,IAC3B;AAAA,GACF;AACF;;;;"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createExtensionDataContainer } from '../frontend-internal/src/wiring/createExtensionDataContainer.esm.js';
|
|
2
|
-
import '../frontend-internal/src/wiring/InternalSwappableComponentRef.esm.js';
|
|
3
2
|
import '../frontend-internal/src/wiring/InternalExtensionDefinition.esm.js';
|
|
3
|
+
import '../frontend-internal/src/wiring/InternalExtensionInput.esm.js';
|
|
4
4
|
import '../frontend-internal/src/wiring/InternalFrontendPlugin.esm.js';
|
|
5
|
+
import '../frontend-internal/src/wiring/InternalSwappableComponentRef.esm.js';
|
|
5
6
|
|
|
6
7
|
function expectArray(value) {
|
|
7
8
|
return value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveInputOverrides.esm.js","sources":["../../src/wiring/resolveInputOverrides.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AppNode } from '../apis';\nimport { Expand } from '@backstage/types';\nimport { ResolvedExtensionInput } from './createExtension';\nimport { createExtensionDataContainer } from '@internal/frontend';\nimport {\n ExtensionDataRefToValue,\n ExtensionDataValue,\n} from './createExtensionDataRef';\nimport { ExtensionInput } from './createExtensionInput';\nimport { ExtensionDataContainer } from './types';\n\n/** @ignore */\nexport type ResolvedInputValueOverrides<\n TInputs extends { [inputName in string]: ExtensionInput } = {\n [inputName in string]: ExtensionInput;\n },\n> = Expand<\n {\n [KName in keyof TInputs as TInputs[KName] extends ExtensionInput<\n any,\n {\n optional: infer IOptional extends boolean;\n singleton: boolean;\n }\n >\n ? IOptional extends true\n ? never\n : KName\n : never]: TInputs[KName] extends ExtensionInput<\n infer IDataRefs,\n { optional: boolean; singleton: infer ISingleton extends boolean }\n >\n ? ISingleton extends true\n ? Iterable<ExtensionDataRefToValue<IDataRefs>>\n : Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>\n : never;\n } & {\n [KName in keyof TInputs as TInputs[KName] extends ExtensionInput<\n any,\n {\n optional: infer IOptional extends boolean;\n singleton: boolean;\n }\n >\n ? IOptional extends true\n ? KName\n : never\n : never]?: TInputs[KName] extends ExtensionInput<\n infer IDataRefs,\n { optional: boolean; singleton: infer ISingleton extends boolean }\n >\n ? ISingleton extends true\n ? Iterable<ExtensionDataRefToValue<IDataRefs>>\n : Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>\n : never;\n }\n>;\n\nfunction expectArray<T>(value: T | T[]): T[] {\n return value as T[];\n}\nfunction expectItem<T>(value: T | T[]): T {\n return value as T;\n}\n\n/** @internal */\nexport function resolveInputOverrides(\n declaredInputs?: { [inputName in string]: ExtensionInput },\n inputs?: {\n [KName in string]?:\n | ({ node: AppNode } & ExtensionDataContainer<any>)\n | Array<{ node: AppNode } & ExtensionDataContainer<any>>;\n },\n inputOverrides?: ResolvedInputValueOverrides,\n) {\n if (!declaredInputs || !inputs || !inputOverrides) {\n return inputs;\n }\n\n const newInputs: typeof inputs = {};\n for (const name in declaredInputs) {\n if (!Object.hasOwn(declaredInputs, name)) {\n continue;\n }\n const declaredInput = declaredInputs[name];\n const providedData = inputOverrides[name];\n if (declaredInput.config.singleton) {\n const originalInput = expectItem(inputs[name]);\n if (providedData) {\n const providedContainer = createExtensionDataContainer(\n providedData as Iterable<ExtensionDataValue<any, any>>,\n 'extension input override',\n declaredInput.extensionData,\n );\n if (!originalInput) {\n throw new Error(\n `attempted to override data of input '${name}' but it is not present in the original inputs`,\n );\n }\n newInputs[name] = Object.assign(providedContainer, {\n node: (originalInput as ResolvedExtensionInput<any>).node,\n }) as any;\n }\n } else {\n const originalInput = expectArray(inputs[name]);\n if (!Array.isArray(providedData)) {\n throw new Error(\n `override data provided for input '${name}' must be an array`,\n );\n }\n\n // Regular inputs can be overridden in two different ways:\n // 1) Forward a subset of the original inputs in a new order\n // 2) Provide new data for each original input\n\n // First check if all inputs are being removed\n if (providedData.length === 0) {\n newInputs[name] = [];\n } else {\n // Check how many of the provided data items have a node property, i.e. is a forwarded input\n const withNodesCount = providedData.filter(d => 'node' in d).length;\n if (withNodesCount === 0) {\n if (originalInput.length !== providedData.length) {\n throw new Error(\n `override data provided for input '${name}' must match the length of the original inputs`,\n );\n }\n newInputs[name] = providedData.map((data, i) => {\n const providedContainer = createExtensionDataContainer(\n data as Iterable<ExtensionDataValue<any, any>>,\n 'extension input override',\n declaredInput.extensionData,\n );\n return Object.assign(providedContainer, {\n node: (originalInput[i] as ResolvedExtensionInput<any>).node,\n }) as any;\n });\n } else if (withNodesCount === providedData.length) {\n newInputs[name] = providedData as any;\n } else {\n throw new Error(\n `override data for input '${name}' may not mix forwarded inputs with data overrides`,\n );\n }\n }\n }\n }\n return newInputs;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resolveInputOverrides.esm.js","sources":["../../src/wiring/resolveInputOverrides.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AppNode } from '../apis';\nimport { Expand } from '@backstage/types';\nimport { ResolvedExtensionInput } from './createExtension';\nimport { createExtensionDataContainer } from '@internal/frontend';\nimport {\n ExtensionDataRefToValue,\n ExtensionDataValue,\n} from './createExtensionDataRef';\nimport { ExtensionInput } from './createExtensionInput';\nimport { ExtensionDataContainer } from './types';\n\n/** @ignore */\nexport type ResolvedInputValueOverrides<\n TInputs extends { [inputName in string]: ExtensionInput } = {\n [inputName in string]: ExtensionInput;\n },\n> = Expand<\n {\n [KName in keyof TInputs as TInputs[KName] extends ExtensionInput<\n any,\n {\n optional: infer IOptional extends boolean;\n singleton: boolean;\n }\n >\n ? IOptional extends true\n ? never\n : KName\n : never]: TInputs[KName] extends ExtensionInput<\n infer IDataRefs,\n { optional: boolean; singleton: infer ISingleton extends boolean }\n >\n ? ISingleton extends true\n ? Iterable<ExtensionDataRefToValue<IDataRefs>>\n : Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>\n : never;\n } & {\n [KName in keyof TInputs as TInputs[KName] extends ExtensionInput<\n any,\n {\n optional: infer IOptional extends boolean;\n singleton: boolean;\n }\n >\n ? IOptional extends true\n ? KName\n : never\n : never]?: TInputs[KName] extends ExtensionInput<\n infer IDataRefs,\n { optional: boolean; singleton: infer ISingleton extends boolean }\n >\n ? ISingleton extends true\n ? Iterable<ExtensionDataRefToValue<IDataRefs>>\n : Array<Iterable<ExtensionDataRefToValue<IDataRefs>>>\n : never;\n }\n>;\n\nfunction expectArray<T>(value: T | T[]): T[] {\n return value as T[];\n}\nfunction expectItem<T>(value: T | T[]): T {\n return value as T;\n}\n\n/** @internal */\nexport function resolveInputOverrides(\n declaredInputs?: { [inputName in string]: ExtensionInput },\n inputs?: {\n [KName in string]?:\n | ({ node: AppNode } & ExtensionDataContainer<any>)\n | Array<{ node: AppNode } & ExtensionDataContainer<any>>;\n },\n inputOverrides?: ResolvedInputValueOverrides,\n) {\n if (!declaredInputs || !inputs || !inputOverrides) {\n return inputs;\n }\n\n const newInputs: typeof inputs = {};\n for (const name in declaredInputs) {\n if (!Object.hasOwn(declaredInputs, name)) {\n continue;\n }\n const declaredInput = declaredInputs[name];\n const providedData = inputOverrides[name];\n if (declaredInput.config.singleton) {\n const originalInput = expectItem(inputs[name]);\n if (providedData) {\n const providedContainer = createExtensionDataContainer(\n providedData as Iterable<ExtensionDataValue<any, any>>,\n 'extension input override',\n declaredInput.extensionData,\n );\n if (!originalInput) {\n throw new Error(\n `attempted to override data of input '${name}' but it is not present in the original inputs`,\n );\n }\n newInputs[name] = Object.assign(providedContainer, {\n node: (originalInput as ResolvedExtensionInput<any>).node,\n }) as any;\n }\n } else {\n const originalInput = expectArray(inputs[name]);\n if (!Array.isArray(providedData)) {\n throw new Error(\n `override data provided for input '${name}' must be an array`,\n );\n }\n\n // Regular inputs can be overridden in two different ways:\n // 1) Forward a subset of the original inputs in a new order\n // 2) Provide new data for each original input\n\n // First check if all inputs are being removed\n if (providedData.length === 0) {\n newInputs[name] = [];\n } else {\n // Check how many of the provided data items have a node property, i.e. is a forwarded input\n const withNodesCount = providedData.filter(d => 'node' in d).length;\n if (withNodesCount === 0) {\n if (originalInput.length !== providedData.length) {\n throw new Error(\n `override data provided for input '${name}' must match the length of the original inputs`,\n );\n }\n newInputs[name] = providedData.map((data, i) => {\n const providedContainer = createExtensionDataContainer(\n data as Iterable<ExtensionDataValue<any, any>>,\n 'extension input override',\n declaredInput.extensionData,\n );\n return Object.assign(providedContainer, {\n node: (originalInput[i] as ResolvedExtensionInput<any>).node,\n }) as any;\n });\n } else if (withNodesCount === providedData.length) {\n newInputs[name] = providedData as any;\n } else {\n throw new Error(\n `override data for input '${name}' may not mix forwarded inputs with data overrides`,\n );\n }\n }\n }\n }\n return newInputs;\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/frontend-plugin-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"backstage": {
|
|
5
5
|
"role": "web-library"
|
|
6
6
|
},
|
|
@@ -31,20 +31,20 @@
|
|
|
31
31
|
"test": "backstage-cli package test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@backstage/core-components": "0.18.3
|
|
35
|
-
"@backstage/core-plugin-api": "1.
|
|
36
|
-
"@backstage/types": "1.2.2",
|
|
37
|
-
"@backstage/version-bridge": "1.0.11",
|
|
34
|
+
"@backstage/core-components": "^0.18.3",
|
|
35
|
+
"@backstage/core-plugin-api": "^1.12.0",
|
|
36
|
+
"@backstage/types": "^1.2.2",
|
|
37
|
+
"@backstage/version-bridge": "^1.0.11",
|
|
38
38
|
"@material-ui/core": "^4.12.4",
|
|
39
39
|
"lodash": "^4.17.21",
|
|
40
40
|
"zod": "^3.22.4",
|
|
41
41
|
"zod-to-json-schema": "^3.21.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@backstage/cli": "0.34.5
|
|
45
|
-
"@backstage/frontend-app-api": "0.13.2
|
|
46
|
-
"@backstage/frontend-test-utils": "0.4.1
|
|
47
|
-
"@backstage/test-utils": "1.7.13
|
|
44
|
+
"@backstage/cli": "^0.34.5",
|
|
45
|
+
"@backstage/frontend-app-api": "^0.13.2",
|
|
46
|
+
"@backstage/frontend-test-utils": "^0.4.1",
|
|
47
|
+
"@backstage/test-utils": "^1.7.13",
|
|
48
48
|
"@testing-library/jest-dom": "^6.0.0",
|
|
49
49
|
"@testing-library/react": "^16.0.0",
|
|
50
50
|
"@types/react": "^18.0.0",
|