refactor(backend-dynamic-feature-service): better load failure management...

... and other small enhancements (e.g. ability to get the `ScannedPluginPackage` of a loaded plugin).

Signed-off-by: David Festal <dfestal@redhat.com>
This commit is contained in:
David Festal
2024-09-23 16:23:06 +02:00
parent 0d74ac9e71
commit e6c05502d5
7 changed files with 498 additions and 153 deletions
+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).
+1
View File
@@ -93,6 +93,7 @@
"jest-haste-map@^29.7.0": "patch:jest-haste-map@npm%3A29.7.0#./.yarn/patches/jest-haste-map-npm-29.7.0-e3be419eff.patch"
},
"dependencies": {
"@backstage/cli-node": "workspace:^",
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@types/global-agent": "^2.1.3",
@@ -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(includeFailed?: boolean): BackendDynamicPlugin[];
}
// @public (undocumented)
export interface BaseDynamicPlugin {
// (undocumented)
failure?: string;
// (undocumented)
name: string;
// (undocumented)
@@ -75,15 +78,17 @@ export class DynamicPluginManager implements DynamicPluginProvider {
// (undocumented)
get availablePackages(): ScannedPluginPackage[];
// (undocumented)
backendPlugins(): BackendDynamicPlugin[];
backendPlugins(includeFailed?: boolean): BackendDynamicPlugin[];
// (undocumented)
static create(
options: DynamicPluginManagerOptions,
): Promise<DynamicPluginManager>;
// (undocumented)
frontendPlugins(): FrontendDynamicPlugin[];
frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[];
// (undocumented)
plugins(): DynamicPlugin[];
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
// (undocumented)
plugins(includeFailed?: boolean): DynamicPlugin[];
}
// @public (undocumented)
@@ -103,20 +108,19 @@ export interface DynamicPluginProvider
extends FrontendPluginProvider,
BackendPluginProvider {
// (undocumented)
plugins(): DynamicPlugin[];
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
// (undocumented)
plugins(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 +129,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 +168,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 +201,7 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin {
// @public (undocumented)
export interface FrontendPluginProvider {
// (undocumented)
frontendPlugins(): FrontendDynamicPlugin[];
frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[];
}
// @public (undocumented)
@@ -278,6 +291,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".
@@ -293,54 +307,58 @@ export interface ScannedPluginPackage {
// 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:34:5 - (ae-undocumented) Missing documentation for "getScannedPackage".
// 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:65: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:48:5 - (ae-undocumented) Missing documentation for "getScannedPackage".
// src/manager/types.d.ts:53:1 - (ae-undocumented) Missing documentation for "BackendPluginProvider".
// src/manager/types.d.ts:54:5 - (ae-undocumented) Missing documentation for "backendPlugins".
// src/manager/types.d.ts:59:1 - (ae-undocumented) Missing documentation for "FrontendPluginProvider".
// src/manager/types.d.ts:60:5 - (ae-undocumented) Missing documentation for "frontendPlugins".
// src/manager/types.d.ts:65:1 - (ae-undocumented) Missing documentation for "BaseDynamicPlugin".
// src/manager/types.d.ts:66:5 - (ae-undocumented) Missing documentation for "name".
// src/manager/types.d.ts:67:5 - (ae-undocumented) Missing documentation for "version".
// src/manager/types.d.ts:68:5 - (ae-undocumented) Missing documentation for "role".
// src/manager/types.d.ts:69:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:70:5 - (ae-undocumented) Missing documentation for "failure".
// src/manager/types.d.ts:75:1 - (ae-undocumented) Missing documentation for "DynamicPlugin".
// src/manager/types.d.ts:79:1 - (ae-undocumented) Missing documentation for "FrontendDynamicPlugin".
// src/manager/types.d.ts:80:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:85:1 - (ae-undocumented) Missing documentation for "BackendDynamicPlugin".
// src/manager/types.d.ts:86:5 - (ae-undocumented) Missing documentation for "platform".
// src/manager/types.d.ts:87:5 - (ae-undocumented) Missing documentation for "installer".
// src/manager/types.d.ts:92:1 - (ae-undocumented) Missing documentation for "BackendDynamicPluginInstaller".
// src/manager/types.d.ts:96:1 - (ae-undocumented) Missing documentation for "NewBackendPluginInstaller".
// src/manager/types.d.ts:97:5 - (ae-undocumented) Missing documentation for "kind".
// src/manager/types.d.ts:98:5 - (ae-undocumented) Missing documentation for "install".
// src/manager/types.d.ts:111:1 - (ae-undocumented) Missing documentation for "LegacyBackendPluginInstaller".
// src/manager/types.d.ts:112:5 - (ae-undocumented) Missing documentation for "kind".
// src/manager/types.d.ts:113:5 - (ae-undocumented) Missing documentation for "router".
// src/manager/types.d.ts:117:5 - (ae-undocumented) Missing documentation for "catalog".
// src/manager/types.d.ts:118:5 - (ae-undocumented) Missing documentation for "scaffolder".
// src/manager/types.d.ts:119:5 - (ae-undocumented) Missing documentation for "search".
// src/manager/types.d.ts:120:5 - (ae-undocumented) Missing documentation for "events".
// src/manager/types.d.ts:121:5 - (ae-undocumented) Missing documentation for "permissions".
// src/manager/types.d.ts:128: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)
```
@@ -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(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(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(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(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(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(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';
@@ -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,66 +163,88 @@ 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(includeFailed?: boolean): BackendDynamicPlugin[] {
return this.plugins(includeFailed).filter(
(p): p is BackendDynamicPlugin => p.platform === 'node',
);
}
frontendPlugins(): FrontendDynamicPlugin[] {
return this._plugins.filter(
frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[] {
return this.plugins(includeFailed).filter(
(p): p is FrontendDynamicPlugin => p.platform === 'web',
);
}
plugins(): DynamicPlugin[] {
return this._plugins;
plugins(includeFailed?: boolean): DynamicPlugin[] {
return this._plugins.filter(p => 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;
}
}
@@ -279,7 +314,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;
@@ -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,22 @@ export type LegacyPluginEnvironment = {
export interface DynamicPluginProvider
extends FrontendPluginProvider,
BackendPluginProvider {
plugins(): DynamicPlugin[];
plugins(includeFailed?: boolean): DynamicPlugin[];
getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage;
}
/**
* @public
*/
export interface BackendPluginProvider {
backendPlugins(): BackendDynamicPlugin[];
backendPlugins(includeFailed?: boolean): BackendDynamicPlugin[];
}
/**
* @public
*/
export interface FrontendPluginProvider {
frontendPlugins(): FrontendDynamicPlugin[];
frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[];
}
/**
@@ -103,6 +105,7 @@ export interface BaseDynamicPlugin {
version: string;
role: PackageRole;
platform: PackagePlatform;
failure?: string;
}
/**
@@ -122,7 +125,7 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin {
*/
export interface BackendDynamicPlugin extends BaseDynamicPlugin {
platform: 'node';
installer: BackendDynamicPluginInstaller;
installer?: BackendDynamicPluginInstaller;
}
/**
+1
View File
@@ -40001,6 +40001,7 @@ __metadata:
resolution: "root@workspace:."
dependencies:
"@backstage/cli": "workspace:*"
"@backstage/cli-node": "workspace:^"
"@backstage/codemods": "workspace:*"
"@backstage/create-app": "workspace:*"
"@backstage/e2e-test-utils": "workspace:*"