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:
@@ -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).
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user