Merge pull request #26927 from davidfestal/refactor-dynamic-plugins

Refactor dynamic plugins
This commit is contained in:
Fredrik Adelöw
2024-10-14 16:28:13 +02:00
committed by GitHub
34 changed files with 1330 additions and 311 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/backend-dynamic-feature-service': patch
---
Enhance and simplify the activation of the dynamic plugins feature:
- The dynamic plugins service (which implements the `DynamicPluginsProvider`) is restored, since it is required for plugins to depend on it in order to get the details of loaded dynamic plugins (possibly with loading errors to be surfaced in some UI).
- A new all-in-one feature loader (`dynamicPluginsFeatureLoader`) is provided that allows a 1-liner activation of both the dynamic features and additional services or plugins required to have the dynamic plugins work correctly with dynamic plugins config schemas. Previous service factories or feature loaders are deprecated.
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/backend-dynamic-feature-service': patch
---
Enhance the API of the `DynamicPluginProvider` (available as a service) to:
- expose the new `getScannedPackage()` method that returns the `ScannedPluginPackage` from which a given plugin has been loaded,
- add an optional `includeFailed` argument in the plugins list retrieval methods, to include the plugins that could be successfully loaded (`false` by default).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-dynamic-feature-service': patch
---
Allow passing an async module loader in the `DynamicPluginsFeatureLoaderOptions`.
@@ -15,8 +15,7 @@ In the `backend` application, it can be enabled by adding the `backend-dynamic-f
```ts
const backend = createBackend();
+
+ backend.add(dynamicPluginsFeatureDiscoveryServiceFactory) // overridden version of the FeatureDiscoveryService which provides features loaded by dynamic plugins
+ backend.add(dynamicPluginsServiceFactory)
+ backend.add(dynamicPluginsFeatureLoader) // provides features loaded by dynamic plugins
+
```
@@ -76,6 +76,8 @@
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/plugin-app-backend": "workspace:^",
"triple-beam": "^1.4.1",
"wait-for-expect": "^3.0.2"
}
}
@@ -33,11 +33,12 @@ import { ServiceRef } from '@backstage/backend-plugin-api';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { TokenManager } from '@backstage/backend-common';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { WinstonLoggerOptions } from '@backstage/backend-defaults/rootLogger';
// @public (undocumented)
export interface BackendDynamicPlugin extends BaseDynamicPlugin {
// (undocumented)
installer: BackendDynamicPluginInstaller;
installer?: BackendDynamicPluginInstaller;
// (undocumented)
platform: 'node';
}
@@ -50,11 +51,13 @@ export type BackendDynamicPluginInstaller =
// @public (undocumented)
export interface BackendPluginProvider {
// (undocumented)
backendPlugins(): BackendDynamicPlugin[];
backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[];
}
// @public (undocumented)
export interface BaseDynamicPlugin {
// (undocumented)
failure?: string;
// (undocumented)
name: string;
// (undocumented)
@@ -75,15 +78,19 @@ export class DynamicPluginManager implements DynamicPluginProvider {
// (undocumented)
get availablePackages(): ScannedPluginPackage[];
// (undocumented)
backendPlugins(): BackendDynamicPlugin[];
backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[];
// (undocumented)
static create(
options: DynamicPluginManagerOptions,
): Promise<DynamicPluginManager>;
// (undocumented)
frontendPlugins(): FrontendDynamicPlugin[];
frontendPlugins(options?: {
includeFailed?: boolean;
}): FrontendDynamicPlugin[];
// (undocumented)
plugins(): DynamicPlugin[];
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
// (undocumented)
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[];
}
// @public (undocumented)
@@ -103,20 +110,19 @@ export interface DynamicPluginProvider
extends FrontendPluginProvider,
BackendPluginProvider {
// (undocumented)
plugins(): DynamicPlugin[];
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
// (undocumented)
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[];
}
// @public (undocumented)
export interface DynamicPluginsFactoryOptions {
// (undocumented)
moduleLoader?(logger: LoggerService): ModuleLoader;
moduleLoader?(logger: LoggerService): ModuleLoader | Promise<ModuleLoader>;
}
// @public
export const dynamicPluginsFeatureDiscoveryLoader: ((
options?: DynamicPluginsFactoryOptions,
) => BackendFeature) &
BackendFeature;
// @public @deprecated (undocumented)
export const dynamicPluginsFeatureDiscoveryLoader: BackendFeature;
// @public @deprecated (undocumented)
export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory<
@@ -125,16 +131,32 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactory<
'singleton'
>;
// @public
export const dynamicPluginsFeatureLoader: ((
options?: DynamicPluginsFeatureLoaderOptions,
) => BackendFeature) &
BackendFeature;
// @public (undocumented)
export type DynamicPluginsFeatureLoaderOptions = DynamicPluginsFactoryOptions &
DynamicPluginsSchemasOptions &
DynamicPluginsRootLoggerFactoryOptions;
// @public @deprecated (undocumented)
export const dynamicPluginsFrontendSchemas: BackendFeature;
// @public (undocumented)
export const dynamicPluginsRootLoggerServiceFactory: ServiceFactory<
RootLoggerService,
'root',
'singleton'
export type DynamicPluginsRootLoggerFactoryOptions = Omit<
WinstonLoggerOptions,
'meta'
>;
// @public @deprecated (undocumented)
export const dynamicPluginsRootLoggerServiceFactory: ((
options?: DynamicPluginsRootLoggerFactoryOptions,
) => ServiceFactory<RootLoggerService, 'root', 'singleton'>) &
ServiceFactory<RootLoggerService, 'root', 'singleton'>;
// @public (undocumented)
export interface DynamicPluginsSchemasOptions {
schemaLocator?: (pluginPackage: ScannedPluginPackage) => string;
@@ -148,31 +170,24 @@ export interface DynamicPluginsSchemasService {
}>;
}
// @public (undocumented)
export const dynamicPluginsSchemasServiceFactory: ServiceFactory<
DynamicPluginsSchemasService,
'root',
'singleton'
>;
// @public (undocumented)
export const dynamicPluginsSchemasServiceFactoryWithOptions: (
// @public @deprecated (undocumented)
export const dynamicPluginsSchemasServiceFactory: ((
options?: DynamicPluginsSchemasOptions,
) => ServiceFactory<DynamicPluginsSchemasService, 'root', 'singleton'>;
) => ServiceFactory<DynamicPluginsSchemasService, 'root', 'singleton'>) &
ServiceFactory<DynamicPluginsSchemasService, 'root', 'singleton'>;
// @public @deprecated (undocumented)
export const dynamicPluginsServiceFactory: ServiceFactory<
DynamicPluginProvider,
'root',
'singleton'
>;
export const dynamicPluginsServiceFactory: ((
options?: DynamicPluginsFactoryOptions,
) => ServiceFactory<DynamicPluginProvider, 'root', 'singleton'>) &
ServiceFactory<DynamicPluginProvider, 'root', 'singleton'>;
// @public @deprecated (undocumented)
export const dynamicPluginsServiceFactoryWithOptions: (
options?: DynamicPluginsFactoryOptions,
) => ServiceFactory<DynamicPluginProvider, 'root', 'singleton'>;
// @public @deprecated (undocumented)
// @public (undocumented)
export const dynamicPluginsServiceRef: ServiceRef<
DynamicPluginProvider,
'root',
@@ -188,7 +203,9 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin {
// @public (undocumented)
export interface FrontendPluginProvider {
// (undocumented)
frontendPlugins(): FrontendDynamicPlugin[];
frontendPlugins(options?: {
includeFailed?: boolean;
}): FrontendDynamicPlugin[];
}
// @public (undocumented)
@@ -278,6 +295,7 @@ export interface ScannedPluginPackage {
// Warnings were encountered during analysis:
//
// src/features/features.d.ts:7:1 - (ae-undocumented) Missing documentation for "DynamicPluginsFeatureLoaderOptions".
// src/loader/types.d.ts:4:1 - (ae-undocumented) Missing documentation for "ModuleLoader".
// src/loader/types.d.ts:5:5 - (ae-undocumented) Missing documentation for "bootstrap".
// src/loader/types.d.ts:6:5 - (ae-undocumented) Missing documentation for "load".
@@ -291,56 +309,60 @@ export interface ScannedPluginPackage {
// src/manager/plugin-manager.d.ts:27:5 - (ae-undocumented) Missing documentation for "availablePackages".
// src/manager/plugin-manager.d.ts:28:5 - (ae-undocumented) Missing documentation for "addBackendPlugin".
// src/manager/plugin-manager.d.ts:31:5 - (ae-undocumented) Missing documentation for "backendPlugins".
// src/manager/plugin-manager.d.ts:32:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
// src/manager/plugin-manager.d.ts:33:5 - (ae-undocumented) Missing documentation for "plugins".
// src/manager/plugin-manager.d.ts:39:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceRef".
// src/manager/plugin-manager.d.ts:43:1 - (ae-undocumented) Missing documentation for "DynamicPluginsFactoryOptions".
// src/manager/plugin-manager.d.ts:44:5 - (ae-undocumented) Missing documentation for "moduleLoader".
// src/manager/plugin-manager.d.ts:50:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactoryWithOptions".
// src/manager/plugin-manager.d.ts:55:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactory".
// src/manager/plugin-manager.d.ts:60:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryServiceFactory".
// src/manager/types.d.ts:27:1 - (ae-undocumented) Missing documentation for "LegacyPluginEnvironment".
// src/manager/types.d.ts:45:1 - (ae-undocumented) Missing documentation for "DynamicPluginProvider".
// src/manager/types.d.ts:46:5 - (ae-undocumented) Missing documentation for "plugins".
// src/manager/types.d.ts:51:1 - (ae-undocumented) Missing documentation for "BackendPluginProvider".
// src/manager/types.d.ts:52:5 - (ae-undocumented) Missing documentation for "backendPlugins".
// src/manager/types.d.ts:57:1 - (ae-undocumented) Missing documentation for "FrontendPluginProvider".
// src/manager/types.d.ts:58:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
// src/manager/types.d.ts:63:1 - (ae-undocumented) Missing documentation for "BaseDynamicPlugin".
// src/manager/types.d.ts:64:5 - (ae-undocumented) Missing documentation for "name".
// src/manager/types.d.ts:65:5 - (ae-undocumented) Missing documentation for "version".
// src/manager/types.d.ts:66:5 - (ae-undocumented) Missing documentation for "role".
// src/manager/types.d.ts:67:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:72:1 - (ae-undocumented) Missing documentation for "DynamicPlugin".
// src/manager/types.d.ts:76:1 - (ae-undocumented) Missing documentation for "FrontendDynamicPlugin".
// src/manager/types.d.ts:77:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:82:1 - (ae-undocumented) Missing documentation for "BackendDynamicPlugin".
// src/manager/types.d.ts:83:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:84:5 - (ae-undocumented) Missing documentation for "installer".
// src/manager/types.d.ts:89:1 - (ae-undocumented) Missing documentation for "BackendDynamicPluginInstaller".
// src/manager/types.d.ts:93:1 - (ae-undocumented) Missing documentation for "NewBackendPluginInstaller".
// src/manager/types.d.ts:94:5 - (ae-undocumented) Missing documentation for "kind".
// src/manager/types.d.ts:95:5 - (ae-undocumented) Missing documentation for "install".
// src/manager/types.d.ts:108:1 - (ae-undocumented) Missing documentation for "LegacyBackendPluginInstaller".
// src/manager/types.d.ts:109:5 - (ae-undocumented) Missing documentation for "kind".
// src/manager/types.d.ts:110:5 - (ae-undocumented) Missing documentation for "router".
// src/manager/types.d.ts:114:5 - (ae-undocumented) Missing documentation for "catalog".
// src/manager/types.d.ts:115:5 - (ae-undocumented) Missing documentation for "scaffolder".
// src/manager/types.d.ts:116:5 - (ae-undocumented) Missing documentation for "search".
// src/manager/types.d.ts:117:5 - (ae-undocumented) Missing documentation for "events".
// src/manager/types.d.ts:118:5 - (ae-undocumented) Missing documentation for "permissions".
// src/manager/types.d.ts:125:1 - (ae-undocumented) Missing documentation for "isBackendDynamicPluginInstaller".
// src/manager/plugin-manager.d.ts:34:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
// src/manager/plugin-manager.d.ts:37:5 - (ae-undocumented) Missing documentation for "plugins".
// src/manager/plugin-manager.d.ts:40:5 - (ae-undocumented) Missing documentation for "getScannedPackage".
// src/manager/plugin-manager.d.ts:45:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceRef".
// src/manager/plugin-manager.d.ts:49:1 - (ae-undocumented) Missing documentation for "DynamicPluginsFactoryOptions".
// src/manager/plugin-manager.d.ts:50:5 - (ae-undocumented) Missing documentation for "moduleLoader".
// src/manager/plugin-manager.d.ts:56:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactoryWithOptions".
// src/manager/plugin-manager.d.ts:61:22 - (ae-undocumented) Missing documentation for "dynamicPluginsServiceFactory".
// src/manager/plugin-manager.d.ts:66:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryServiceFactory".
// src/manager/plugin-manager.d.ts:71:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryLoader".
// src/manager/types.d.ts:28:1 - (ae-undocumented) Missing documentation for "LegacyPluginEnvironment".
// src/manager/types.d.ts:46:1 - (ae-undocumented) Missing documentation for "DynamicPluginProvider".
// src/manager/types.d.ts:47:5 - (ae-undocumented) Missing documentation for "plugins".
// src/manager/types.d.ts:50:5 - (ae-undocumented) Missing documentation for "getScannedPackage".
// src/manager/types.d.ts:55:1 - (ae-undocumented) Missing documentation for "BackendPluginProvider".
// src/manager/types.d.ts:56:5 - (ae-undocumented) Missing documentation for "backendPlugins".
// src/manager/types.d.ts:63:1 - (ae-undocumented) Missing documentation for "FrontendPluginProvider".
// src/manager/types.d.ts:64:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
// src/manager/types.d.ts:71:1 - (ae-undocumented) Missing documentation for "BaseDynamicPlugin".
// src/manager/types.d.ts:72:5 - (ae-undocumented) Missing documentation for "name".
// src/manager/types.d.ts:73:5 - (ae-undocumented) Missing documentation for "version".
// src/manager/types.d.ts:74:5 - (ae-undocumented) Missing documentation for "role".
// src/manager/types.d.ts:75:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:76:5 - (ae-undocumented) Missing documentation for "failure".
// src/manager/types.d.ts:81:1 - (ae-undocumented) Missing documentation for "DynamicPlugin".
// src/manager/types.d.ts:85:1 - (ae-undocumented) Missing documentation for "FrontendDynamicPlugin".
// src/manager/types.d.ts:86:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:91:1 - (ae-undocumented) Missing documentation for "BackendDynamicPlugin".
// src/manager/types.d.ts:92:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:93:5 - (ae-undocumented) Missing documentation for "installer".
// src/manager/types.d.ts:98:1 - (ae-undocumented) Missing documentation for "BackendDynamicPluginInstaller".
// src/manager/types.d.ts:102:1 - (ae-undocumented) Missing documentation for "NewBackendPluginInstaller".
// src/manager/types.d.ts:103:5 - (ae-undocumented) Missing documentation for "kind".
// src/manager/types.d.ts:104:5 - (ae-undocumented) Missing documentation for "install".
// src/manager/types.d.ts:117:1 - (ae-undocumented) Missing documentation for "LegacyBackendPluginInstaller".
// src/manager/types.d.ts:118:5 - (ae-undocumented) Missing documentation for "kind".
// src/manager/types.d.ts:119:5 - (ae-undocumented) Missing documentation for "router".
// src/manager/types.d.ts:123:5 - (ae-undocumented) Missing documentation for "catalog".
// src/manager/types.d.ts:124:5 - (ae-undocumented) Missing documentation for "scaffolder".
// src/manager/types.d.ts:125:5 - (ae-undocumented) Missing documentation for "search".
// src/manager/types.d.ts:126:5 - (ae-undocumented) Missing documentation for "events".
// src/manager/types.d.ts:127:5 - (ae-undocumented) Missing documentation for "permissions".
// src/manager/types.d.ts:134:1 - (ae-undocumented) Missing documentation for "isBackendDynamicPluginInstaller".
// src/scanner/types.d.ts:5:1 - (ae-undocumented) Missing documentation for "ScannedPluginPackage".
// src/scanner/types.d.ts:6:5 - (ae-undocumented) Missing documentation for "location".
// src/scanner/types.d.ts:7:5 - (ae-undocumented) Missing documentation for "manifest".
// src/scanner/types.d.ts:12:1 - (ae-undocumented) Missing documentation for "ScannedPluginManifest".
// src/schemas/appBackendModule.d.ts:2:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFrontendSchemas".
// src/schemas/rootLoggerServiceFactory.d.ts:2:22 - (ae-undocumented) Missing documentation for "dynamicPluginsRootLoggerServiceFactory".
// src/schemas/frontend.d.ts:5:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFrontendSchemas".
// src/schemas/rootLogger.d.ts:5:1 - (ae-undocumented) Missing documentation for "DynamicPluginsRootLoggerFactoryOptions".
// src/schemas/rootLogger.d.ts:10:22 - (ae-undocumented) Missing documentation for "dynamicPluginsRootLoggerServiceFactory".
// src/schemas/schemas.d.ts:7:1 - (ae-undocumented) Missing documentation for "DynamicPluginsSchemasService".
// src/schemas/schemas.d.ts:8:5 - (ae-undocumented) Missing documentation for "addDynamicPluginsSchemas".
// src/schemas/schemas.d.ts:21:1 - (ae-undocumented) Missing documentation for "DynamicPluginsSchemasOptions".
// src/schemas/schemas.d.ts:36:22 - (ae-undocumented) Missing documentation for "dynamicPluginsSchemasServiceFactoryWithOptions".
// src/schemas/schemas.d.ts:40:22 - (ae-undocumented) Missing documentation for "dynamicPluginsSchemasServiceFactory".
// src/schemas/schemas.d.ts:37:22 - (ae-undocumented) Missing documentation for "dynamicPluginsSchemasServiceFactory".
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,2 @@
!dist
!node_modules
@@ -0,0 +1,18 @@
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"test-backend": {
"type": "object",
"required": [
"secretValue"
],
"properties": {
"secretValue": {
"type": "string",
"visibility": "secret"
}
}
}
}
}
@@ -0,0 +1,59 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const { dynamicPluginsServiceRef } = require('../../../../../manager');
var backendPluginApi = require('@backstage/backend-plugin-api');
const express = require('express');
const path = require('path');
const url = require('url');
const privateDep = require('private-dep-with-frontend-plugin-index-path');
const testPlugin = backendPluginApi.createBackendPlugin({
pluginId: "test",
register(env) {
env.registerInit({
deps: {
http: backendPluginApi.coreServices.httpRouter,
logger: backendPluginApi.coreServices.rootLogger,
discovery: backendPluginApi.coreServices.discovery,
dynamicPlugins: dynamicPluginsServiceRef,
metadata: backendPluginApi.coreServices.pluginMetadata,
},
async init({
http,
logger,
discovery,
dynamicPlugins,
metadata,
}) {
logger.info("This secret value should be hidden by the dynamic-plugin-aware logger: AVerySecretValue");
const externalBaseUrl = await discovery.getExternalBaseUrl(metadata.getId());
const router = express.Router();
const frontendPluginsIndexPath = privateDep.frontendPluginsIndexPath;
const frontendPluginManifests = Object.fromEntries(dynamicPlugins.frontendPlugins().map(fp => {
const pluginScannedPackage = dynamicPlugins.getScannedPackage(fp);
const pkgDistLocation = path.resolve(
url.fileURLToPath(pluginScannedPackage.location),
'dist',
);
router.use(`/${frontendPluginsIndexPath}/${fp.name}`, express.static(pkgDistLocation))
return [fp.name, `${externalBaseUrl}/${frontendPluginsIndexPath}/${fp.name}/mf-manifest.json`]
}));
router.get(`/${frontendPluginsIndexPath}`, (req, res) => {
res.status(200).json(frontendPluginManifests);
});
http.use(router);
http.addAuthPolicy({
path: `/`,
allow: 'unauthenticated',
});
}
});
}
});
exports.default = testPlugin;
//# sourceMappingURL=alpha.cjs.js.map
@@ -0,0 +1,7 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const frontendPluginsIndexPath = 'frontend-plugins';
exports.frontendPluginsIndexPath = frontendPluginsIndexPath;
@@ -0,0 +1,7 @@
{
"name": "private-dep-with-frontend-plugin-index-path",
"version": "0.0.0",
"description": "private dependency of the test backend plugin",
"main": "index.js",
"dependencies": {}
}
@@ -0,0 +1,35 @@
{
"name": "plugin-test-backend-dynamic",
"version": "0.0.0",
"description": "A test dynamic backend module that exposes the dynamic frontend plugins to an endpoint.",
"backstage": {
"role": "backend-plugin",
"pluginId": "test",
"pluginPackages": [
"plugin-test",
"plugin-test-backend"
]
},
"publishConfig": {
"access": "public"
},
"keywords": [
"backstage",
"dynamic"
],
"exports": {
".": {
"require": "./dist/index.cjs.js",
"default": "./dist/index.cjs.js"
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs.js",
"files": [
"dist"
],
"dependencies": {
"private-dep-with-frontend-plugin-index-path": "0.0.0"
},
"bundleDependencies": true
}
@@ -0,0 +1,18 @@
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"test-frontend": {
"type": "object",
"required": [
"frontendValue"
],
"properties": {
"frontendValue": {
"type": "string",
"visibility": "frontend"
}
}
}
}
}
@@ -0,0 +1,29 @@
{
"id": "backstage__plugin_test",
"name": "backstage__plugin_test",
"metaData": {
"name": "backstage__plugin_test",
"type": "app",
"buildInfo": {
"buildVersion": "0.0.0",
"buildName": "@backstage/plugin-test"
},
"remoteEntry": {
"name": "remoteEntry.js",
"path": "",
"type": "global"
},
"types": {
"path": "",
"name": "",
"zip": "",
"api": ""
},
"globalName": "backstage__plugin_test",
"pluginVersion": "0.0.0",
"publicPath": "auto"
},
"shared": [],
"remotes": [],
"exposes": []
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
(function doNothing(){})();
@@ -0,0 +1,20 @@
{
"name": "plugin-test-dynamic",
"version": "0.0.0",
"description": "A test dynamic, module-federation-based, Backstage frontend plugin that does nothing",
"backstage": {
"role": "frontend-dynamic-container",
"pluginId": "test",
"pluginPackages": [
"plugin-test"
]
},
"publishConfig": {
"access": "public"
},
"keywords": [
"backstage",
"dynamic"
],
"main": "remote-entry.js"
}
@@ -0,0 +1 @@
throw new Error("False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader");
@@ -0,0 +1,7 @@
{
"name": "@backstage/backend-plugin-api",
"version": "0.0.0",
"description": "dummy backstage package that should be skipped by the ComonJSLoduleLoader",
"main": "index.js",
"dependencies": {}
}
@@ -0,0 +1,308 @@
/*
* 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 {
startTestBackend,
mockServices,
createMockDirectory,
} from '@backstage/backend-test-utils';
import { dynamicPluginsFeatureLoader } from './features';
import { DynamicPlugin, dynamicPluginsServiceRef } from '../manager';
import path, { resolve as resolvePath } from 'path';
import {
BackendFeature,
createBackendPlugin,
LoggerService,
} from '@backstage/backend-plugin-api';
import { CommonJSModuleLoader } from '../loader/CommonJSModuleLoader';
import * as winston from 'winston';
import { MESSAGE } from 'triple-beam';
import { overridePackagePathResolution } from '@backstage/backend-plugin-api/testUtils';
async function jestFreeTypescriptAwareModuleLoader(
logger: LoggerService,
dontBootstrap: boolean = false,
) {
const loader = new CommonJSModuleLoader(logger);
(loader as any).module = await loader.load('node:module');
loader.load(path.resolve(__dirname, '../../../cli/config/nodeTransform.cjs'));
if (dontBootstrap) {
loader.bootstrap = async () => {};
}
return loader;
}
class MockedTransport extends winston.transports.Console {
readonly logs: string[] = [];
public log(info: any, callback: () => void) {
if (!info[MESSAGE]?.includes('info: Plugin initialization ')) {
this.logs.push(info[MESSAGE]);
}
super.log!(info, callback);
}
public logv(info: any, callback: () => void) {
if (!info[MESSAGE]?.includes('info: Plugin initialization ')) {
this.logs.push(info[MESSAGE]);
}
super.log!(info, callback);
}
}
class DynamicPluginLister {
readonly loadedPlugins: DynamicPlugin[] = [];
feature(): BackendFeature {
// eslint-disable-next-line consistent-this
const that = this;
return createBackendPlugin({
pluginId: 'dynamicPluginsLister',
register(reg) {
reg.registerInit({
deps: {
dynamicPlugins: dynamicPluginsServiceRef,
},
async init({ dynamicPlugins }) {
that.loadedPlugins.push(
...dynamicPlugins.plugins({ includeFailed: true }),
);
},
});
},
});
}
}
describe('dynamicPluginsFeatureLoader', () => {
const dynamicPluginsRootDirectory = resolvePath(
__dirname,
'__fixtures__/dynamic-plugins-root',
);
// A dummy `@backstage/backend-plugin-api` package which throws an error is available inside the test fixtures,
// in a `node_modules` folder which is a sibling of the `dynamic-plugins-root`.
// This test demonstrates how, without the skipping logic implemented in the {@link CommonJSModelLoader},
// this dummy package would be loaded by the backend dynamic plugins instead of the one of the backstage root.
it('should fail because the model loader is not skipping modules living in unexpected locations.', async () => {
const dynamicPLuginsLister = new DynamicPluginLister();
const mockedTransport = new MockedTransport();
await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: logger =>
jestFreeTypescriptAwareModuleLoader(logger, true),
transports: [mockedTransport],
format: winston.format.simple(),
}),
dynamicPLuginsLister.feature(),
],
});
expect(mockedTransport.logs).toContainEqual(
expect.stringMatching(
"error: an error occurred while loading dynamic backend plugin 'plugin-test-backend-dynamic' from '.*/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic",
),
);
expect(dynamicPLuginsLister.loadedPlugins).toMatchObject([
{
name: 'plugin-test-backend-dynamic',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
failure:
'Error: False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader',
},
expect.anything(),
]);
});
it('should load and show the 2 dynamic plugins in a list of dynamic plugins returned by a static backend plugin', async () => {
const dynamicPLuginsLister = new DynamicPluginLister();
await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: jestFreeTypescriptAwareModuleLoader,
}),
dynamicPLuginsLister.feature(),
],
});
expect(dynamicPLuginsLister.loadedPlugins).toMatchObject([
{
installer: {
kind: 'new',
},
name: 'plugin-test-backend-dynamic',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'plugin-test-dynamic',
platform: 'web',
role: 'frontend-dynamic-container',
version: '0.0.0',
},
]);
});
it('should redact the secret config values of dynamic plugin config schemas in logs', async () => {
const mockedTransport = new MockedTransport();
await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
'test-backend': {
secretValue: 'AVerySecretValue',
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: jestFreeTypescriptAwareModuleLoader,
transports: [mockedTransport],
format: winston.format.simple(),
}),
],
});
expect(mockedTransport.logs).toContainEqual(
'info: Found 1 new secrets in config that will be redacted {"service":"backstage"}',
);
expect(mockedTransport.logs).toContainEqual(
'info: This secret value should be hidden by the dynamic-plugin-aware logger: *** {"service":"backstage"}',
);
});
const mockAppDir = createMockDirectory();
overridePackagePathResolution({
packageName: 'app',
path: mockAppDir.path,
});
it('should inject frontend config values of dynamic frontend plugin config schemas to the frontend application', async () => {
mockAppDir.setContent({
'package.json': '{}',
dist: {
static: {},
'index.html.tmpl': '<head></head>',
'.config-schema.json': `
{
"backstageConfigSchemaVersion": 1,
"schemas": []
}
`,
},
});
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
'test-frontend': {
frontendValue: 'AFrontendValue',
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: jestFreeTypescriptAwareModuleLoader,
}),
import('@backstage/plugin-app-backend'),
],
});
await expect(
fetch(`http://localhost:${server.port()}`).then(res => res.text()),
).resolves.toBe(`<head>
<script type="backstage.io/config">
[
{
"context": "app",
"deprecatedKeys": [],
"data": {
"test-frontend": {
"frontendValue": "AFrontendValue"
}
}
}
]
</script>
</head>`);
});
it('should access the module federation assets of the frontend plugin through the backend plugin', async () => {
const { server } = await startTestBackend({
features: [
mockServices.rootConfig.factory({
data: {
dynamicPlugins: {
rootDirectory: dynamicPluginsRootDirectory,
},
},
}),
dynamicPluginsFeatureLoader({
moduleLoader: jestFreeTypescriptAwareModuleLoader,
}),
],
});
const list = await fetch(
`http://localhost:${server.port()}/api/test/frontend-plugins`,
);
expect(list.ok).toBe(true);
expect(await list.json()).toEqual({
'plugin-test-dynamic': `http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
});
const manifest = await fetch(
`http://localhost:${server.port()}/api/test/frontend-plugins/plugin-test-dynamic/mf-manifest.json`,
);
expect(manifest.ok).toBe(true);
expect(await manifest.json()).toMatchObject({
exposes: [],
id: 'backstage__plugin_test',
name: 'backstage__plugin_test',
metaData: {
buildInfo: {
buildName: '@backstage/plugin-test',
buildVersion: '0.0.0',
},
globalName: 'backstage__plugin_test',
name: 'backstage__plugin_test',
pluginVersion: '0.0.0',
publicPath: 'auto',
},
});
});
});
@@ -0,0 +1,111 @@
/*
* 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 {
coreServices,
createBackendFeatureLoader,
} from '@backstage/backend-plugin-api';
import {
DynamicPluginsSchemasOptions,
dynamicPluginsFrontendSchemas,
dynamicPluginsRootLoggerServiceFactory,
dynamicPluginsSchemasServiceFactory,
} from '../schemas';
import {
DynamicPluginsFactoryOptions,
dynamicPluginsFeatureDiscoveryLoader,
dynamicPluginsServiceFactory,
} from '../manager';
import { DynamicPluginsRootLoggerFactoryOptions } from '../schemas';
import { configKey } from '../scanner/plugin-scanner';
/**
* @public
*/
export type DynamicPluginsFeatureLoaderOptions = DynamicPluginsFactoryOptions &
DynamicPluginsSchemasOptions &
DynamicPluginsRootLoggerFactoryOptions;
const dynamicPluginsFeatureLoaderWithOptions = (
options?: DynamicPluginsFeatureLoaderOptions,
) =>
createBackendFeatureLoader({
deps: {
config: coreServices.rootConfig,
},
*loader({ config }) {
const dynamicPluginsEnabled = config.has(configKey);
yield* [
dynamicPluginsSchemasServiceFactory(options),
dynamicPluginsServiceFactory(options),
];
if (dynamicPluginsEnabled) {
yield* [
dynamicPluginsRootLoggerServiceFactory(options),
dynamicPluginsFrontendSchemas,
dynamicPluginsFeatureDiscoveryLoader,
];
}
},
});
/**
* A backend feature loader that fully enable backend dynamic plugins.
* More precisely it:
* - adds the dynamic plugins root service (typically depended upon by plugins),
* - adds additional required features to allow supporting dynamic plugins config schemas
* in the frontend application and the backend root logger,
* - uses the dynamic plugins service to discover and expose dynamic plugins as features.
*
* @public
*
* @example
* Using the `dynamicPluginsFeatureLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureLoader } from '@backstage/backend-dynamic-feature-service';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureLoader);
* //...
* backend.start();
* ```
*
* @example
* Passing options to the `dynamicPluginsFeatureLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureLoader } from '@backstage/backend-dynamic-feature-service';
* import { myCustomModuleLoader } from './myCustomModuleLoader';
* import { myCustomSchemaLocator } from './myCustomSchemaLocator';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureLoader({
* moduleLoader: myCustomModuleLoader,
* schemaLocator: myCustomSchemaLocator,
*
* }));
* //...
* backend.start();
* ```
*/
export const dynamicPluginsFeatureLoader = Object.assign(
dynamicPluginsFeatureLoaderWithOptions,
dynamicPluginsFeatureLoaderWithOptions(),
);
@@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './features';
@@ -18,3 +18,4 @@ export * from './loader';
export * from './scanner';
export * from './manager';
export * from './schemas';
export * from './features';
@@ -18,7 +18,11 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import path from 'path';
export class CommonJSModuleLoader implements ModuleLoader {
constructor(public readonly logger: LoggerService) {}
private module: any;
constructor(public readonly logger: LoggerService) {
this.module = require('node:module');
}
async bootstrap(
backstageRoot: string,
@@ -28,9 +32,8 @@ export class CommonJSModuleLoader implements ModuleLoader {
const dynamicNodeModulesPaths = [
...dynamicPluginsPaths.map(p => path.resolve(p, 'node_modules')),
];
const Module = require('module');
const oldNodeModulePaths = Module._nodeModulePaths;
Module._nodeModulePaths = (from: string): string[] => {
const oldNodeModulePaths = this.module._nodeModulePaths;
this.module._nodeModulePaths = (from: string): string[] => {
const result: string[] = oldNodeModulePaths(from);
if (!dynamicPluginsPaths.some(p => from.startsWith(p))) {
return result;
@@ -49,6 +52,6 @@ export class CommonJSModuleLoader implements ModuleLoader {
}
async load(packagePath: string): Promise<any> {
return await require(/* webpackIgnore: true */ packagePath);
return await this.module.prototype.require(packagePath);
}
}
@@ -44,6 +44,7 @@ import { PluginScanner } from '../scanner/plugin-scanner';
import { findPaths } from '@backstage/cli-common';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
import { PackageRole } from '@backstage/cli-node';
describe('backend-dynamic-feature-service', () => {
const mockDir = createMockDirectory();
@@ -299,6 +300,29 @@ describe('backend-dynamic-feature-service', () => {
);
},
},
{
name: 'should ignore plugin package with incompatible role',
packageManifest: {
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
backstage: {
role: 'node-library',
},
main: 'dist/index.cjs.js',
},
expectedLogs(location) {
return {
infos: [
{
message: `skipping dynamic plugin package 'backend-dynamic-plugin-test' from '${location}': incompatible role 'node-library'`,
},
],
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([]);
},
},
{
name: 'should fail when no index file',
packageManifest: {
@@ -342,7 +366,17 @@ describe('backend-dynamic-feature-service', () => {
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([]);
expect(plugins).toMatchObject([
{
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
failure: expect.stringMatching(
`^Error: Cannot find module '[^']*' from .*`,
),
},
]);
},
},
{
@@ -369,7 +403,15 @@ describe('backend-dynamic-feature-service', () => {
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([]);
expect(plugins).toMatchObject([
{
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
failure: `the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
},
]);
},
},
{
@@ -397,7 +439,15 @@ describe('backend-dynamic-feature-service', () => {
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([]);
expect(plugins).toMatchObject([
{
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
failure: `the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
},
]);
},
},
{
@@ -428,7 +478,17 @@ describe('backend-dynamic-feature-service', () => {
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([]);
expect(plugins).toMatchObject([
{
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
failure: expect.stringMatching(
`^SyntaxError: Unexpected identifier.*`,
),
},
]);
},
},
{
@@ -495,6 +555,27 @@ describe('backend-dynamic-feature-service', () => {
]);
},
},
{
name: 'should successfully load a frontend plugin (experimental dynamic container)',
packageManifest: {
name: 'frontend-dynamic-plugin-test',
version: '0.0.0',
backstage: {
role: 'frontend-dynamic-container' as PackageRole,
},
main: 'dist/index.esm.js',
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([
{
name: 'frontend-dynamic-plugin-test',
version: '0.0.0',
role: 'frontend-dynamic-container',
platform: 'web',
},
]);
},
},
])('$name', async (tc: TestCase): Promise<void> => {
const plugin: ScannedPluginPackage = {
location: url.pathToFileURL(mockDir.resolve(randomUUID())),
@@ -538,33 +619,54 @@ describe('backend-dynamic-feature-service', () => {
});
});
describe('backendPlugins', () => {
describe('plugin getters', () => {
const plugins: BaseDynamicPlugin[] = [
{
name: 'a-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
},
{
name: 'a-frontend-module',
platform: 'web',
role: 'frontend-plugin-module',
version: '0.0.0',
},
{
name: 'a-failing-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
failure: 'Some frontend failure',
},
{
name: 'a-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-module',
platform: 'node',
role: 'backend-plugin-module',
version: '0.0.0',
},
{
name: 'a-failing-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
failure: 'Some backend failure',
},
];
it('should return only backend plugins and modules', async () => {
const logger = new MockedLogger();
const pluginManager = new (DynamicPluginManager as any)(
logger,
[],
) as DynamicPluginManager;
const plugins: BaseDynamicPlugin[] = [
{
name: 'a-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-module',
platform: 'node',
role: 'backend-plugin-module',
version: '0.0.0',
},
];
(pluginManager as any)._plugins = plugins;
expect(pluginManager.backendPlugins()).toEqual([
{
@@ -580,17 +682,109 @@ describe('backend-dynamic-feature-service', () => {
version: '0.0.0',
},
]);
expect(pluginManager.backendPlugins({ includeFailed: false })).toEqual([
{
name: 'a-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-module',
platform: 'node',
role: 'backend-plugin-module',
version: '0.0.0',
},
]);
expect(pluginManager.backendPlugins({ includeFailed: true })).toEqual([
{
name: 'a-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-module',
platform: 'node',
role: 'backend-plugin-module',
version: '0.0.0',
},
{
name: 'a-failing-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
failure: 'Some backend failure',
},
]);
});
});
describe('frontendPlugins', () => {
it('should return only frontend plugins', async () => {
const logger = new MockedLogger();
const pluginManager = new (DynamicPluginManager as any)(
logger,
[],
) as DynamicPluginManager;
const plugins: BaseDynamicPlugin[] = [
(pluginManager as any)._plugins = plugins;
expect(pluginManager.frontendPlugins()).toEqual([
{
name: 'a-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
},
{
name: 'a-frontend-module',
platform: 'web',
role: 'frontend-plugin-module',
version: '0.0.0',
},
]);
expect(pluginManager.frontendPlugins({ includeFailed: false })).toEqual([
{
name: 'a-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
},
{
name: 'a-frontend-module',
platform: 'web',
role: 'frontend-plugin-module',
version: '0.0.0',
},
]);
expect(pluginManager.frontendPlugins({ includeFailed: true })).toEqual([
{
name: 'a-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
},
{
name: 'a-frontend-module',
platform: 'web',
role: 'frontend-plugin-module',
version: '0.0.0',
},
{
name: 'a-failing-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
failure: 'Some frontend failure',
},
]);
});
it('should return all plugins', async () => {
const logger = new MockedLogger();
const pluginManager = new (DynamicPluginManager as any)(
logger,
[],
) as DynamicPluginManager;
(pluginManager as any)._plugins = plugins;
expect(pluginManager.plugins()).toEqual([
{
name: 'a-frontend-plugin',
platform: 'web',
@@ -615,9 +809,8 @@ describe('backend-dynamic-feature-service', () => {
role: 'backend-plugin-module',
version: '0.0.0',
},
];
(pluginManager as any)._plugins = plugins;
expect(pluginManager.frontendPlugins()).toEqual([
]);
expect(pluginManager.plugins({ includeFailed: false })).toEqual([
{
name: 'a-frontend-plugin',
platform: 'web',
@@ -630,7 +823,93 @@ describe('backend-dynamic-feature-service', () => {
role: 'frontend-plugin-module',
version: '0.0.0',
},
{
name: 'a-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-module',
platform: 'node',
role: 'backend-plugin-module',
version: '0.0.0',
},
]);
expect(pluginManager.plugins({ includeFailed: true })).toEqual([
{
name: 'a-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
},
{
name: 'a-frontend-module',
platform: 'web',
role: 'frontend-plugin-module',
version: '0.0.0',
},
{
name: 'a-failing-frontend-plugin',
platform: 'web',
role: 'frontend-plugin',
version: '0.0.0',
failure: 'Some frontend failure',
},
{
name: 'a-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
},
{
name: 'a-backend-module',
platform: 'node',
role: 'backend-plugin-module',
version: '0.0.0',
},
{
name: 'a-failing-backend-plugin',
platform: 'node',
role: 'backend-plugin',
version: '0.0.0',
failure: 'Some backend failure',
},
]);
});
});
describe('get scanned package', () => {
it('should return the scanned package of the plugin', async () => {
const logger = new MockedLogger();
const packageFolder = mockDir.resolve(randomUUID());
const scannedPackage = {
manifest: {
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
backstage: {
role: 'backend-plugin',
},
main: 'dist/index.cjs.js',
},
location: url.pathToFileURL(packageFolder),
};
const plugin = {
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
installer: {
kind: 'new',
},
} as BackendDynamicPlugin;
const pluginManager = new (DynamicPluginManager as any)(logger, [
scannedPackage,
]) as DynamicPluginManager;
(pluginManager as any)._plugins = [plugin];
expect(pluginManager.getScannedPackage(plugin)).toEqual(scannedPackage);
});
});
@@ -34,7 +34,7 @@ import {
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import { PackageRoles } from '@backstage/cli-node';
import { PackageRole, PackageRoles } from '@backstage/cli-node';
import { findPaths } from '@backstage/cli-common';
import path from 'path';
import * as fs from 'fs';
@@ -88,7 +88,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
),
);
moduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths);
await moduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths);
scanner.subscribeToRootDirectoryChange(async () => {
manager._availablePackages = (await scanner.scanRoot()).packages;
@@ -104,7 +104,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
private constructor(
private readonly logger: LoggerService,
private packages: ScannedPluginPackage[],
private readonly packages: ScannedPluginPackage[],
private readonly moduleLoader: ModuleLoader,
) {
this._plugins = [];
@@ -123,26 +123,39 @@ export class DynamicPluginManager implements DynamicPluginProvider {
const loadedPlugins: DynamicPlugin[] = [];
for (const scannedPlugin of this.packages) {
const platform = PackageRoles.getRoleInfo(
scannedPlugin.manifest.backstage.role,
).platform;
const role = scannedPlugin.manifest.backstage.role;
const platform = PackageRoles.getRoleInfo(role).platform;
const isPlugin =
role.endsWith('-plugin') ||
role.endsWith('-plugin-module') ||
role === ('frontend-dynamic-container' as PackageRole);
if (
platform === 'node' &&
scannedPlugin.manifest.backstage.role.includes('-plugin')
) {
const plugin = await this.loadBackendPlugin(scannedPlugin);
if (plugin !== undefined) {
loadedPlugins.push(plugin);
}
} else {
loadedPlugins.push({
name: scannedPlugin.manifest.name,
version: scannedPlugin.manifest.version,
role: scannedPlugin.manifest.backstage.role,
platform: 'web',
// TODO(davidfestal): add required front-end plugin information here.
});
if (!isPlugin) {
this.logger.info(
`skipping dynamic plugin package '${scannedPlugin.manifest.name}' from '${scannedPlugin.location}': incompatible role '${role}'`,
);
continue;
}
switch (platform) {
case 'node':
loadedPlugins.push(await this.loadBackendPlugin(scannedPlugin));
break;
case 'web':
loadedPlugins.push({
name: scannedPlugin.manifest.name,
version: scannedPlugin.manifest.version,
role: scannedPlugin.manifest.backstage.role,
platform: 'web',
// TODO(davidfestal): add required front-end plugin information here.
});
break;
default:
this.logger.info(
`skipping dynamic plugin package '${scannedPlugin.manifest.name}' from '${scannedPlugin.location}': unrelated platform '${platform}'`,
);
}
}
return loadedPlugins;
@@ -150,72 +163,97 @@ export class DynamicPluginManager implements DynamicPluginProvider {
private async loadBackendPlugin(
plugin: ScannedPluginPackage,
): Promise<BackendDynamicPlugin | undefined> {
): Promise<BackendDynamicPlugin> {
const packagePath = url.fileURLToPath(
`${plugin.location}/${plugin.manifest.main}`,
);
const dynamicPlugin: BackendDynamicPlugin = {
name: plugin.manifest.name,
version: plugin.manifest.version,
platform: 'node',
role: plugin.manifest.backstage.role,
};
try {
const pluginModule = await this.moduleLoader.load(packagePath);
let dynamicPluginInstaller;
if (isBackendFeature(pluginModule.default)) {
dynamicPluginInstaller = {
dynamicPlugin.installer = {
kind: 'new',
install: () => pluginModule.default,
};
} else if (isBackendFeatureFactory(pluginModule.default)) {
dynamicPluginInstaller = {
dynamicPlugin.installer = {
kind: 'new',
install: pluginModule.default,
};
} else {
dynamicPluginInstaller = pluginModule.dynamicPluginInstaller;
} else if (
isBackendDynamicPluginInstaller(pluginModule.dynamicPluginInstaller)
) {
dynamicPlugin.installer = pluginModule.dynamicPluginInstaller;
}
if (!isBackendDynamicPluginInstaller(dynamicPluginInstaller)) {
this.logger.error(
`dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${plugin.location}': the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
if (dynamicPlugin.installer) {
this.logger.info(
`loaded dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
);
} else {
dynamicPlugin.failure = `the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`;
this.logger.error(
`dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${plugin.location}': ${dynamicPlugin.failure}`,
);
return undefined;
}
this.logger.info(
`loaded dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
);
return {
name: plugin.manifest.name,
version: plugin.manifest.version,
platform: 'node',
role: plugin.manifest.backstage.role,
installer: dynamicPluginInstaller,
};
return dynamicPlugin;
} catch (error) {
const typedError =
typeof error === 'object' && 'message' in error && 'name' in error
? error
: new Error(error);
dynamicPlugin.failure = `${typedError.name}: ${typedError.message}`;
this.logger.error(
`an error occurred while loading dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
error,
typedError,
);
return undefined;
return dynamicPlugin;
}
}
backendPlugins(): BackendDynamicPlugin[] {
return this._plugins.filter(
backendPlugins(options?: {
includeFailed?: boolean;
}): BackendDynamicPlugin[] {
return this.plugins(options).filter(
(p): p is BackendDynamicPlugin => p.platform === 'node',
);
}
frontendPlugins(): FrontendDynamicPlugin[] {
return this._plugins.filter(
frontendPlugins(options?: {
includeFailed?: boolean;
}): FrontendDynamicPlugin[] {
return this.plugins(options).filter(
(p): p is FrontendDynamicPlugin => p.platform === 'web',
);
}
plugins(): DynamicPlugin[] {
return this._plugins;
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[] {
return this._plugins.filter(p => options?.includeFailed || !p.failure);
}
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage {
const pkg = this.packages.find(
p =>
p.manifest.name === plugin.name &&
p.manifest.version === plugin.version,
);
if (pkg === undefined) {
throw new Error(
`The scanned package of a dynamic plugin should always be available: ${plugin.name}/${plugin.version}`,
);
}
return pkg;
}
}
/**
* @public
* @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead.
*/
export const dynamicPluginsServiceRef = createServiceRef<DynamicPluginProvider>(
{
@@ -228,12 +266,12 @@ export const dynamicPluginsServiceRef = createServiceRef<DynamicPluginProvider>(
* @public
*/
export interface DynamicPluginsFactoryOptions {
moduleLoader?(logger: LoggerService): ModuleLoader;
moduleLoader?(logger: LoggerService): ModuleLoader | Promise<ModuleLoader>;
}
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureDiscoveryLoader} instead.
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsServiceFactoryWithOptions = (
options?: DynamicPluginsFactoryOptions,
@@ -249,17 +287,19 @@ export const dynamicPluginsServiceFactoryWithOptions = (
config,
logger,
preferAlpha: true,
moduleLoader: options?.moduleLoader?.(logger),
moduleLoader: await options?.moduleLoader?.(logger),
});
},
});
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureDiscoveryLoader} instead.
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsServiceFactory =
dynamicPluginsServiceFactoryWithOptions();
export const dynamicPluginsServiceFactory = Object.assign(
dynamicPluginsServiceFactoryWithOptions,
dynamicPluginsServiceFactoryWithOptions(),
);
class DynamicPluginsEnabledFeatureDiscoveryService
implements FeatureDiscoveryService
@@ -279,7 +319,7 @@ class DynamicPluginsEnabledFeatureDiscoveryService
...this.dynamicPlugins
.backendPlugins()
.flatMap((plugin): BackendFeature[] => {
if (plugin.installer.kind === 'new') {
if (plugin.installer?.kind === 'new') {
const installed = plugin.installer.install();
if (Array.isArray(installed)) {
return installed;
@@ -296,7 +336,7 @@ class DynamicPluginsEnabledFeatureDiscoveryService
/**
* @public
* @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead.
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsFeatureDiscoveryServiceFactory =
createServiceFactory({
@@ -310,65 +350,22 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory =
},
});
const dynamicPluginsFeatureDiscoveryLoaderWithOptions = (
options?: DynamicPluginsFactoryOptions,
) =>
createBackendFeatureLoader({
deps: {
config: coreServices.rootConfig,
logger: coreServices.rootLogger,
},
async loader({ config, logger }) {
const manager = await DynamicPluginManager.create({
config,
logger,
preferAlpha: true,
moduleLoader: options?.moduleLoader?.(logger),
});
const service = new DynamicPluginsEnabledFeatureDiscoveryService(manager);
const { features } = await service.getBackendFeatures();
return features;
},
});
/**
* A backend feature loader that uses the dynamic plugins system to discover features.
*
* @public
*
* @example
* Using the `dynamicPluginsFeatureDiscoveryLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureDiscoveryLoader } from '@backstage/backend-dynamic-feature-service';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureDiscoveryLoader);
* //...
* backend.start();
* ```
*
* @example
* Passing options to the `dynamicPluginsFeatureDiscoveryLoader` loader in a backend instance:
* ```ts
* //...
* import { createBackend } from '@backstage/backend-defaults';
* import { dynamicPluginsFeatureDiscoveryLoader } from '@backstage/backend-dynamic-feature-service';
* import { myCustomModuleLoader } from './myCustomModuleLoader';
*
* const backend = createBackend();
* backend.add(dynamicPluginsFeatureDiscoveryLoader({
* moduleLoader: myCustomModuleLoader
* }));
* //...
* backend.start();
* ```
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsFeatureDiscoveryLoader = Object.assign(
dynamicPluginsFeatureDiscoveryLoaderWithOptions,
dynamicPluginsFeatureDiscoveryLoaderWithOptions(),
);
export const dynamicPluginsFeatureDiscoveryLoader = createBackendFeatureLoader({
deps: {
dynamicPlugins: dynamicPluginsServiceRef,
},
async loader({ dynamicPlugins }) {
const service = new DynamicPluginsEnabledFeatureDiscoveryService(
dynamicPlugins,
);
const { features } = await service.getBackendFeatures();
return features;
},
});
function isBackendFeature(value: unknown): value is BackendFeature {
return (
@@ -43,6 +43,7 @@ import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { IndexBuilder } from '@backstage/plugin-search-backend-node';
import { EventsBackend } from '@backstage/plugin-events-backend';
import { PermissionPolicy } from '@backstage/plugin-permission-node';
import { ScannedPluginPackage } from '../scanner';
/**
* @public
@@ -78,21 +79,24 @@ export type LegacyPluginEnvironment = {
export interface DynamicPluginProvider
extends FrontendPluginProvider,
BackendPluginProvider {
plugins(): DynamicPlugin[];
plugins(options?: { includeFailed?: boolean }): DynamicPlugin[];
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
}
/**
* @public
*/
export interface BackendPluginProvider {
backendPlugins(): BackendDynamicPlugin[];
backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[];
}
/**
* @public
*/
export interface FrontendPluginProvider {
frontendPlugins(): FrontendDynamicPlugin[];
frontendPlugins(options?: {
includeFailed?: boolean;
}): FrontendDynamicPlugin[];
}
/**
@@ -103,6 +107,7 @@ export interface BaseDynamicPlugin {
version: string;
role: PackageRole;
platform: PackagePlatform;
failure?: string;
}
/**
@@ -122,7 +127,7 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin {
*/
export interface BackendDynamicPlugin extends BaseDynamicPlugin {
platform: 'node';
installer: BackendDynamicPluginInstaller;
installer?: BackendDynamicPluginInstaller;
}
/**
@@ -35,6 +35,8 @@ export interface ScanRootResponse {
packages: ScannedPluginPackage[];
}
export const configKey = 'dynamicPlugins';
export class PluginScanner {
private _rootDirectory?: string;
private configUnsubscribe?: () => void;
@@ -68,27 +70,27 @@ export class PluginScanner {
}
private applyConfig(): void | never {
const dynamicPlugins = this.config.getOptional('dynamicPlugins');
const dynamicPlugins = this.config.getOptional(configKey);
if (!dynamicPlugins) {
this.logger.info("'dynamicPlugins' config entry not found.");
this.logger.info(`'${configKey}' config entry not found.`);
this._rootDirectory = undefined;
return;
}
if (typeof dynamicPlugins !== 'object') {
this.logger.warn("'dynamicPlugins' config entry should be an object.");
this.logger.warn(`'${configKey}' config entry should be an object.`);
this._rootDirectory = undefined;
return;
}
if (!('rootDirectory' in dynamicPlugins)) {
this.logger.warn(
"'dynamicPlugins' config entry does not contain the 'rootDirectory' field.",
`'${configKey}' config entry does not contain the 'rootDirectory' field.`,
);
this._rootDirectory = undefined;
return;
}
if (typeof dynamicPlugins.rootDirectory !== 'string') {
this.logger.warn(
"'dynamicPlugins.rootDirectory' config entry should be a string.",
`'${configKey}.rootDirectory' config entry should be a string.`,
);
this._rootDirectory = undefined;
return;
@@ -25,7 +25,10 @@ import {
loadCompiledConfigSchema,
} from '@backstage/plugin-app-node';
/** @public */
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsFrontendSchemas = createBackendModule({
pluginId: 'app',
moduleId: 'core.dynamicplugins.frontendSchemas',
@@ -41,6 +44,7 @@ export const dynamicPluginsFrontendSchemas = createBackendModule({
config.getOptionalString('app.packageName') ?? 'app';
const appDistDir = resolvePackagePath(appPackageName, 'dist');
const compiledConfigSchema = await loadCompiledConfigSchema(appDistDir);
// TODO(davidfestal): Add dynamic pliugin config schemas even if the compiled schemas are empty.
if (compiledConfigSchema) {
configSchemaExtension.setConfigSchema(
(await schemas.addDynamicPluginsSchemas(compiledConfigSchema))
@@ -16,10 +16,12 @@
export {
dynamicPluginsSchemasServiceFactory,
dynamicPluginsSchemasServiceFactoryWithOptions,
type DynamicPluginsSchemasService,
type DynamicPluginsSchemasOptions,
} from './schemas';
export { dynamicPluginsFrontendSchemas } from './appBackendModule';
export { dynamicPluginsRootLoggerServiceFactory } from './rootLoggerServiceFactory';
export { dynamicPluginsFrontendSchemas } from './frontend';
export {
dynamicPluginsRootLoggerServiceFactory,
type DynamicPluginsRootLoggerFactoryOptions,
} from './rootLogger';
@@ -0,0 +1,86 @@
/*
* 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 {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
import {
WinstonLogger,
WinstonLoggerOptions,
} from '@backstage/backend-defaults/rootLogger';
import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootConfig';
import { transports, format } from 'winston';
import { loadConfigSchema } from '@backstage/config-loader';
import { getPackages } from '@manypkg/get-packages';
import { dynamicPluginsSchemasServiceRef } from './schemas';
/**
* @public
*/
export type DynamicPluginsRootLoggerFactoryOptions = Omit<
WinstonLoggerOptions,
'meta'
>;
const dynamicPluginsRootLoggerServiceFactoryWithOptions = (
options?: DynamicPluginsRootLoggerFactoryOptions,
) =>
createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.rootConfig,
schemas: dynamicPluginsSchemasServiceRef,
},
async factory({ config, schemas }) {
const logger = WinstonLogger.create({
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
...options,
meta: {
service: 'backstage',
},
});
const configSchema = await loadConfigSchema({
dependencies: (
await getPackages(process.cwd())
).packages.map(p => p.packageJson.name),
});
const secretEnumerator = await createConfigSecretEnumerator({
logger,
schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema,
});
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsRootLoggerServiceFactory = Object.assign(
dynamicPluginsRootLoggerServiceFactoryWithOptions,
dynamicPluginsRootLoggerServiceFactoryWithOptions(),
);
@@ -1,63 +0,0 @@
/*
* 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 {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
import { WinstonLogger } from '@backstage/backend-defaults/rootLogger';
import { transports, format } from 'winston';
import { createConfigSecretEnumerator } from '@backstage/backend-common';
import { loadConfigSchema } from '@backstage/config-loader';
import { getPackages } from '@manypkg/get-packages';
import { dynamicPluginsSchemasServiceRef } from './schemas';
/** @public */
export const dynamicPluginsRootLoggerServiceFactory = createServiceFactory({
service: coreServices.rootLogger,
deps: {
config: coreServices.rootConfig,
schemas: dynamicPluginsSchemasServiceRef,
},
async factory({ config, schemas }) {
const logger = WinstonLogger.create({
meta: {
service: 'backstage',
},
level: process.env.LOG_LEVEL || 'info',
format:
process.env.NODE_ENV === 'production'
? format.json()
: WinstonLogger.colorFormat(),
transports: [new transports.Console()],
});
const configSchema = await loadConfigSchema({
dependencies: (
await getPackages(process.cwd())
).packages.map(p => p.packageJson.name),
});
const secretEnumerator = await createConfigSecretEnumerator({
logger,
schema: (await schemas.addDynamicPluginsSchemas(configSchema)).schema,
});
logger.addRedactions(secretEnumerator(config));
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
return logger;
},
});
@@ -30,6 +30,7 @@ import { LoggerService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { PluginScanner } from '../scanner/plugin-scanner';
import { ConfigSchema, loadConfigSchema } from '@backstage/config-loader';
import { dynamicPluginsFeatureLoader } from '../features';
/**
*
@@ -68,10 +69,7 @@ export interface DynamicPluginsSchemasOptions {
schemaLocator?: (pluginPackage: ScannedPluginPackage) => string;
}
/**
* @public
*/
export const dynamicPluginsSchemasServiceFactoryWithOptions = (
const dynamicPluginsSchemasServiceFactoryWithOptions = (
options?: DynamicPluginsSchemasOptions,
) =>
createServiceFactory({
@@ -143,9 +141,12 @@ export const dynamicPluginsSchemasServiceFactoryWithOptions = (
/**
* @public
* @deprecated Use {@link dynamicPluginsFeatureLoader} instead, which gathers all services and features required for dynamic plugins.
*/
export const dynamicPluginsSchemasServiceFactory =
dynamicPluginsSchemasServiceFactoryWithOptions();
export const dynamicPluginsSchemasServiceFactory = Object.assign(
dynamicPluginsSchemasServiceFactoryWithOptions,
dynamicPluginsSchemasServiceFactoryWithOptions(),
);
/** @internal */
async function gatherDynamicPluginsSchemas(
+2
View File
@@ -3712,6 +3712,7 @@ __metadata:
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-app-backend": "workspace:^"
"@backstage/plugin-app-node": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-catalog-backend": "workspace:^"
@@ -3729,6 +3730,7 @@ __metadata:
express: ^4.17.1
fs-extra: ^11.2.0
lodash: ^4.17.21
triple-beam: ^1.4.1
wait-for-expect: ^3.0.2
winston: ^3.2.1
languageName: unknown