From e6c05502d558bfbe96ac5028963dd58cb3171fea Mon Sep 17 00:00:00 2001 From: David Festal Date: Mon, 23 Sep 2024 16:23:06 +0200 Subject: [PATCH 1/6] 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 --- .changeset/kind-avocados-speak.md | 8 + package.json | 1 + .../report.api.md | 154 ++++---- .../src/manager/plugin-manager.test.ts | 341 ++++++++++++++++-- .../src/manager/plugin-manager.ts | 135 ++++--- .../src/manager/types.ts | 11 +- yarn.lock | 1 + 7 files changed, 498 insertions(+), 153 deletions(-) create mode 100644 .changeset/kind-avocados-speak.md diff --git a/.changeset/kind-avocados-speak.md b/.changeset/kind-avocados-speak.md new file mode 100644 index 0000000000..b57d271907 --- /dev/null +++ b/.changeset/kind-avocados-speak.md @@ -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). diff --git a/package.json b/package.json index 425b1c1a38..4826bcd56b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/backend-dynamic-feature-service/report.api.md b/packages/backend-dynamic-feature-service/report.api.md index 9170fedf43..615b053869 100644 --- a/packages/backend-dynamic-feature-service/report.api.md +++ b/packages/backend-dynamic-feature-service/report.api.md @@ -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; // (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; } -// @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) & + ServiceFactory; + // @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; +) => ServiceFactory) & + ServiceFactory; // @public @deprecated (undocumented) -export const dynamicPluginsServiceFactory: ServiceFactory< - DynamicPluginProvider, - 'root', - 'singleton' ->; +export const dynamicPluginsServiceFactory: (( + options?: DynamicPluginsFactoryOptions, +) => ServiceFactory) & + ServiceFactory; // @public @deprecated (undocumented) export const dynamicPluginsServiceFactoryWithOptions: ( options?: DynamicPluginsFactoryOptions, ) => ServiceFactory; -// @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) ``` diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts index 5c01a3dfe5..6c129c07e2 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts @@ -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 => { 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); }); }); diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index fef0b6000d..5b88c0619b 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -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 { + ): Promise { 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; diff --git a/packages/backend-dynamic-feature-service/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts index a4f2937fd5..d3989b5ff9 100644 --- a/packages/backend-dynamic-feature-service/src/manager/types.ts +++ b/packages/backend-dynamic-feature-service/src/manager/types.ts @@ -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; } /** diff --git a/yarn.lock b/yarn.lock index f5184e9149..7285cc8a19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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:*" From d18d4942f9159e9b57f21e1fbd007e7a416f03fd Mon Sep 17 00:00:00 2001 From: David Festal Date: Wed, 25 Sep 2024 15:40:10 +0200 Subject: [PATCH 2/6] refactor(backend-dynamic-feature-service): single line activation. - DynamicPlugins service is restored, since it is required for plugins to depend on it in order to get the details of loaded dynamic plugins - An all-in-one feature loader is provided that allows 1-liner installation of both the dynamic features and additional services or plugins required to have the dynamic plugins work correctly with dynamic plugins config schemas. Signed-off-by: David Festal --- .changeset/fluffy-dogs-mate.md | 8 ++ .../backend-dynamic-feature-service/README.md | 3 +- .../src/features/features.ts | 111 ++++++++++++++++++ .../src/features/index.ts | 17 +++ .../src/index.ts | 1 + .../src/manager/plugin-manager.ts | 82 ++++--------- .../src/scanner/plugin-scanner.ts | 4 +- .../{appBackendModule.ts => frontend.ts} | 5 +- .../src/schemas/index.ts | 8 +- .../src/schemas/rootLogger.ts | 86 ++++++++++++++ .../src/schemas/rootLoggerServiceFactory.ts | 63 ---------- .../src/schemas/schemas.ts | 13 +- 12 files changed, 263 insertions(+), 138 deletions(-) create mode 100644 .changeset/fluffy-dogs-mate.md create mode 100644 packages/backend-dynamic-feature-service/src/features/features.ts create mode 100644 packages/backend-dynamic-feature-service/src/features/index.ts rename packages/backend-dynamic-feature-service/src/schemas/{appBackendModule.ts => frontend.ts} (92%) create mode 100644 packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts delete mode 100644 packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts diff --git a/.changeset/fluffy-dogs-mate.md b/.changeset/fluffy-dogs-mate.md new file mode 100644 index 0000000000..cb7a78d0fb --- /dev/null +++ b/.changeset/fluffy-dogs-mate.md @@ -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. diff --git a/packages/backend-dynamic-feature-service/README.md b/packages/backend-dynamic-feature-service/README.md index 33033cf884..f4ca11bb1f 100644 --- a/packages/backend-dynamic-feature-service/README.md +++ b/packages/backend-dynamic-feature-service/README.md @@ -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) which provides features loaded by dynamic plugins + ``` diff --git a/packages/backend-dynamic-feature-service/src/features/features.ts b/packages/backend-dynamic-feature-service/src/features/features.ts new file mode 100644 index 0000000000..4788f1da75 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/features.ts @@ -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(), +); diff --git a/packages/backend-dynamic-feature-service/src/features/index.ts b/packages/backend-dynamic-feature-service/src/features/index.ts new file mode 100644 index 0000000000..c877359d42 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/index.ts @@ -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'; diff --git a/packages/backend-dynamic-feature-service/src/index.ts b/packages/backend-dynamic-feature-service/src/index.ts index 3768146124..abdbff677b 100644 --- a/packages/backend-dynamic-feature-service/src/index.ts +++ b/packages/backend-dynamic-feature-service/src/index.ts @@ -18,3 +18,4 @@ export * from './loader'; export * from './scanner'; export * from './manager'; export * from './schemas'; +export * from './features'; diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index 5b88c0619b..87ff4334ed 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -250,7 +250,6 @@ export class DynamicPluginManager implements DynamicPluginProvider { /** * @public - * @deprecated The `featureDiscoveryService` is deprecated in favor of using {@link dynamicPluginsFeatureDiscoveryLoader} instead. */ export const dynamicPluginsServiceRef = createServiceRef( { @@ -268,7 +267,7 @@ export interface DynamicPluginsFactoryOptions { /** * @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, @@ -291,10 +290,12 @@ export const dynamicPluginsServiceFactoryWithOptions = ( /** * @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 @@ -331,7 +332,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({ @@ -345,65 +346,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 ( diff --git a/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts index 9229f5d78a..80ae2ed876 100644 --- a/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts +++ b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts @@ -35,6 +35,8 @@ export interface ScanRootResponse { packages: ScannedPluginPackage[]; } +export const configKey = 'dynamicPlugins'; + export class PluginScanner { private _rootDirectory?: string; private configUnsubscribe?: () => void; @@ -68,7 +70,7 @@ 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._rootDirectory = undefined; diff --git a/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts b/packages/backend-dynamic-feature-service/src/schemas/frontend.ts similarity index 92% rename from packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts rename to packages/backend-dynamic-feature-service/src/schemas/frontend.ts index 6e54379e90..306ecc9255 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/appBackendModule.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/frontend.ts @@ -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', diff --git a/packages/backend-dynamic-feature-service/src/schemas/index.ts b/packages/backend-dynamic-feature-service/src/schemas/index.ts index 14c6734b6b..d67e77232b 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/index.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/index.ts @@ -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'; diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts new file mode 100644 index 0000000000..ac4bf3fa5e --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/schemas/rootLogger.ts @@ -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(), +); diff --git a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts b/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts deleted file mode 100644 index 00d5c85e6e..0000000000 --- a/packages/backend-dynamic-feature-service/src/schemas/rootLoggerServiceFactory.ts +++ /dev/null @@ -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; - }, -}); diff --git a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts index 1cfb42a093..52fb5eab6d 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/schemas.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/schemas.ts @@ -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( From 4c89e4759d183aeb15964f30ae6cc7362681ea48 Mon Sep 17 00:00:00 2001 From: David Festal Date: Mon, 7 Oct 2024 13:33:44 +0200 Subject: [PATCH 3/6] refactor(backend-dynamic-feature-service): allow passing an async module loader in the `DynamicPluginsFeatureLoaderOptions`. Signed-off-by: David Festal --- .changeset/lemon-badgers-share.md | 5 +++++ .../src/loader/CommonJSModuleLoader.ts | 13 ++++++++----- .../src/manager/plugin-manager.ts | 6 +++--- 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 .changeset/lemon-badgers-share.md diff --git a/.changeset/lemon-badgers-share.md b/.changeset/lemon-badgers-share.md new file mode 100644 index 0000000000..c00b43c403 --- /dev/null +++ b/.changeset/lemon-badgers-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dynamic-feature-service': patch +--- + +Allow passing an async module loader in the `DynamicPluginsFeatureLoaderOptions`. diff --git a/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts b/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts index 66af367cea..f9d4a48a44 100644 --- a/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts +++ b/packages/backend-dynamic-feature-service/src/loader/CommonJSModuleLoader.ts @@ -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 { - return await require(/* webpackIgnore: true */ packagePath); + return await this.module.prototype.require(packagePath); } } diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index 87ff4334ed..a3b4bfbf98 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -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; @@ -262,7 +262,7 @@ export const dynamicPluginsServiceRef = createServiceRef( * @public */ export interface DynamicPluginsFactoryOptions { - moduleLoader?(logger: LoggerService): ModuleLoader; + moduleLoader?(logger: LoggerService): ModuleLoader | Promise; } /** @@ -283,7 +283,7 @@ export const dynamicPluginsServiceFactoryWithOptions = ( config, logger, preferAlpha: true, - moduleLoader: options?.moduleLoader?.(logger), + moduleLoader: await options?.moduleLoader?.(logger), }); }, }); From bb5b95f61309834ac5e98a65fa673f6fa6198998 Mon Sep 17 00:00:00 2001 From: David Festal Date: Mon, 7 Oct 2024 13:35:36 +0200 Subject: [PATCH 4/6] refactor(backend-dynamic-feature-service): all-in-one feature integration tests Signed-off-by: David Festal --- .../package.json | 2 + .../src/features/__fixtures__/.gitignore | 2 + .../dist/configSchema.json | 18 ++ .../test-backend-dynamic/dist/index.cjs.js | 59 ++++ .../index.js | 7 + .../package.json | 7 + .../test-backend-dynamic/package.json | 35 ++ .../test-dynamic/dist/configSchema.json | 18 ++ .../test-dynamic/dist/mf-manifest.json | 29 ++ .../test-dynamic/dist/remoteEntry.js | 17 + .../test-dynamic/package.json | 20 ++ .../node_modules/.shouldNotBeUsed | 0 .../@backstage/backend-plugin-api/index.js | 1 + .../backend-plugin-api/package.json | 7 + .../src/features/features.test.ts | 306 ++++++++++++++++++ .../src/schemas/frontend.ts | 1 + yarn.lock | 2 + 17 files changed, 531 insertions(+) create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/.gitignore create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/configSchema.json create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/package.json create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/package.json create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/configSchema.json create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/mf-manifest.json create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/remoteEntry.js create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/package.json create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/.shouldNotBeUsed create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/index.js create mode 100644 packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/package.json create mode 100644 packages/backend-dynamic-feature-service/src/features/features.test.ts diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index b1f6cf79e7..567f8b75ca 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -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" } } diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/.gitignore b/packages/backend-dynamic-feature-service/src/features/__fixtures__/.gitignore new file mode 100644 index 0000000000..8f91d44d47 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/.gitignore @@ -0,0 +1,2 @@ +!dist +!node_modules diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/configSchema.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/configSchema.json new file mode 100644 index 0000000000..445c79a413 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/configSchema.json @@ -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" + } + } + } + } +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js new file mode 100644 index 0000000000..6cc4a4f125 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/dist/index.cjs.js @@ -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 diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js new file mode 100644 index 0000000000..e0fb6a55de --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/index.js @@ -0,0 +1,7 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +const frontendPluginsIndexPath = 'frontend-plugins'; + +exports.frontendPluginsIndexPath = frontendPluginsIndexPath; diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/package.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/package.json new file mode 100644 index 0000000000..4717963f57 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/node_modules/private-dep-with-frontend-plugin-index-path/package.json @@ -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": {} +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/package.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/package.json new file mode 100644 index 0000000000..4fb0100125 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-backend-dynamic/package.json @@ -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 +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/configSchema.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/configSchema.json new file mode 100644 index 0000000000..a1f22b0374 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/configSchema.json @@ -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" + } + } + } + } +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/mf-manifest.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/mf-manifest.json new file mode 100644 index 0000000000..99a2949671 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/mf-manifest.json @@ -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": [] +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/remoteEntry.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/remoteEntry.js new file mode 100644 index 0000000000..b4554e2a8c --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/dist/remoteEntry.js @@ -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(){})(); diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/package.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/package.json new file mode 100644 index 0000000000..06531a2363 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/dynamic-plugins-root/test-dynamic/package.json @@ -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" +} diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/.shouldNotBeUsed b/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/.shouldNotBeUsed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/index.js b/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/index.js new file mode 100644 index 0000000000..05b446cbe6 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/index.js @@ -0,0 +1 @@ +throw new Error("False @backstage/backend-plugin-api package which should be skipped by the CommonJSModuleLoader"); \ No newline at end of file diff --git a/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/package.json b/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/package.json new file mode 100644 index 0000000000..7b2e68e264 --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/__fixtures__/node_modules/@backstage/backend-plugin-api/package.json @@ -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": {} +} diff --git a/packages/backend-dynamic-feature-service/src/features/features.test.ts b/packages/backend-dynamic-feature-service/src/features/features.test.ts new file mode 100644 index 0000000000..daee127edc --- /dev/null +++ b/packages/backend-dynamic-feature-service/src/features/features.test.ts @@ -0,0 +1,306 @@ +/* + * 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(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': '', + '.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/alpha'), + ], + }); + + await expect( + fetch(`http://localhost:${server.port()}`).then(res => res.text()), + ).resolves.toBe(` + +`); + }); + + 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', + }, + }); + }); +}); diff --git a/packages/backend-dynamic-feature-service/src/schemas/frontend.ts b/packages/backend-dynamic-feature-service/src/schemas/frontend.ts index 306ecc9255..d101487318 100644 --- a/packages/backend-dynamic-feature-service/src/schemas/frontend.ts +++ b/packages/backend-dynamic-feature-service/src/schemas/frontend.ts @@ -44,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)) diff --git a/yarn.lock b/yarn.lock index 7285cc8a19..c360f2247d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 From 67f98d051d9e02b4d59c672a7d8db880b4224c5f Mon Sep 17 00:00:00 2001 From: David Festal Date: Tue, 8 Oct 2024 14:47:11 +0200 Subject: [PATCH 5/6] Fix review comments Signed-off-by: David Festal --- package.json | 1 - .../backend-dynamic-feature-service/README.md | 2 +- .../report.api.md | 94 ++++++++++--------- .../src/features/features.test.ts | 4 +- .../src/manager/plugin-manager.test.ts | 12 +-- .../src/manager/plugin-manager.ts | 16 ++-- .../src/manager/types.ts | 8 +- .../src/scanner/plugin-scanner.ts | 8 +- yarn.lock | 1 - 9 files changed, 78 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index 4826bcd56b..425b1c1a38 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,6 @@ "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", diff --git a/packages/backend-dynamic-feature-service/README.md b/packages/backend-dynamic-feature-service/README.md index f4ca11bb1f..6f5af13636 100644 --- a/packages/backend-dynamic-feature-service/README.md +++ b/packages/backend-dynamic-feature-service/README.md @@ -15,7 +15,7 @@ In the `backend` application, it can be enabled by adding the `backend-dynamic-f ```ts const backend = createBackend(); + -+ backend.add(dynamicPluginsFeatureLoader) which provides features loaded by dynamic plugins ++ backend.add(dynamicPluginsFeatureLoader) // provides features loaded by dynamic plugins + ``` diff --git a/packages/backend-dynamic-feature-service/report.api.md b/packages/backend-dynamic-feature-service/report.api.md index 615b053869..edbe48b263 100644 --- a/packages/backend-dynamic-feature-service/report.api.md +++ b/packages/backend-dynamic-feature-service/report.api.md @@ -51,7 +51,7 @@ export type BackendDynamicPluginInstaller = // @public (undocumented) export interface BackendPluginProvider { // (undocumented) - backendPlugins(includeFailed?: boolean): BackendDynamicPlugin[]; + backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[]; } // @public (undocumented) @@ -78,17 +78,19 @@ export class DynamicPluginManager implements DynamicPluginProvider { // (undocumented) get availablePackages(): ScannedPluginPackage[]; // (undocumented) - backendPlugins(includeFailed?: boolean): BackendDynamicPlugin[]; + backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[]; // (undocumented) static create( options: DynamicPluginManagerOptions, ): Promise; // (undocumented) - frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[]; + frontendPlugins(options?: { + includeFailed?: boolean; + }): FrontendDynamicPlugin[]; // (undocumented) getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage; // (undocumented) - plugins(includeFailed?: boolean): DynamicPlugin[]; + plugins(options?: { includeFailed?: boolean }): DynamicPlugin[]; } // @public (undocumented) @@ -110,7 +112,7 @@ export interface DynamicPluginProvider // (undocumented) getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage; // (undocumented) - plugins(includeFailed?: boolean): DynamicPlugin[]; + plugins(options?: { includeFailed?: boolean }): DynamicPlugin[]; } // @public (undocumented) @@ -201,7 +203,9 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin { // @public (undocumented) export interface FrontendPluginProvider { // (undocumented) - frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[]; + frontendPlugins(options?: { + includeFailed?: boolean; + }): FrontendDynamicPlugin[]; } // @public (undocumented) @@ -305,49 +309,49 @@ 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: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/plugin-manager.d.ts:65:22 - (ae-undocumented) Missing documentation for "dynamicPluginsFeatureDiscoveryLoader". +// 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: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: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: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/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". diff --git a/packages/backend-dynamic-feature-service/src/features/features.test.ts b/packages/backend-dynamic-feature-service/src/features/features.test.ts index daee127edc..851e1d0578 100644 --- a/packages/backend-dynamic-feature-service/src/features/features.test.ts +++ b/packages/backend-dynamic-feature-service/src/features/features.test.ts @@ -75,7 +75,9 @@ class DynamicPluginLister { dynamicPlugins: dynamicPluginsServiceRef, }, async init({ dynamicPlugins }) { - that.loadedPlugins.push(...dynamicPlugins.plugins(true)); + that.loadedPlugins.push( + ...dynamicPlugins.plugins({ includeFailed: true }), + ); }, }); }, diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts index 6c129c07e2..b6854e3263 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.test.ts @@ -682,7 +682,7 @@ describe('backend-dynamic-feature-service', () => { version: '0.0.0', }, ]); - expect(pluginManager.backendPlugins(false)).toEqual([ + expect(pluginManager.backendPlugins({ includeFailed: false })).toEqual([ { name: 'a-backend-plugin', platform: 'node', @@ -696,7 +696,7 @@ describe('backend-dynamic-feature-service', () => { version: '0.0.0', }, ]); - expect(pluginManager.backendPlugins(true)).toEqual([ + expect(pluginManager.backendPlugins({ includeFailed: true })).toEqual([ { name: 'a-backend-plugin', platform: 'node', @@ -740,7 +740,7 @@ describe('backend-dynamic-feature-service', () => { version: '0.0.0', }, ]); - expect(pluginManager.frontendPlugins(false)).toEqual([ + expect(pluginManager.frontendPlugins({ includeFailed: false })).toEqual([ { name: 'a-frontend-plugin', platform: 'web', @@ -754,7 +754,7 @@ describe('backend-dynamic-feature-service', () => { version: '0.0.0', }, ]); - expect(pluginManager.frontendPlugins(true)).toEqual([ + expect(pluginManager.frontendPlugins({ includeFailed: true })).toEqual([ { name: 'a-frontend-plugin', platform: 'web', @@ -810,7 +810,7 @@ describe('backend-dynamic-feature-service', () => { version: '0.0.0', }, ]); - expect(pluginManager.plugins(false)).toEqual([ + expect(pluginManager.plugins({ includeFailed: false })).toEqual([ { name: 'a-frontend-plugin', platform: 'web', @@ -836,7 +836,7 @@ describe('backend-dynamic-feature-service', () => { version: '0.0.0', }, ]); - expect(pluginManager.plugins(true)).toEqual([ + expect(pluginManager.plugins({ includeFailed: true })).toEqual([ { name: 'a-frontend-plugin', platform: 'web', diff --git a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts index a3b4bfbf98..a94a01f0f1 100644 --- a/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts +++ b/packages/backend-dynamic-feature-service/src/manager/plugin-manager.ts @@ -217,20 +217,24 @@ export class DynamicPluginManager implements DynamicPluginProvider { } } - backendPlugins(includeFailed?: boolean): BackendDynamicPlugin[] { - return this.plugins(includeFailed).filter( + backendPlugins(options?: { + includeFailed?: boolean; + }): BackendDynamicPlugin[] { + return this.plugins(options).filter( (p): p is BackendDynamicPlugin => p.platform === 'node', ); } - frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[] { - return this.plugins(includeFailed).filter( + frontendPlugins(options?: { + includeFailed?: boolean; + }): FrontendDynamicPlugin[] { + return this.plugins(options).filter( (p): p is FrontendDynamicPlugin => p.platform === 'web', ); } - plugins(includeFailed?: boolean): DynamicPlugin[] { - return this._plugins.filter(p => includeFailed || !p.failure); + plugins(options?: { includeFailed?: boolean }): DynamicPlugin[] { + return this._plugins.filter(p => options?.includeFailed || !p.failure); } getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage { diff --git a/packages/backend-dynamic-feature-service/src/manager/types.ts b/packages/backend-dynamic-feature-service/src/manager/types.ts index d3989b5ff9..89ac140bc6 100644 --- a/packages/backend-dynamic-feature-service/src/manager/types.ts +++ b/packages/backend-dynamic-feature-service/src/manager/types.ts @@ -79,7 +79,7 @@ export type LegacyPluginEnvironment = { export interface DynamicPluginProvider extends FrontendPluginProvider, BackendPluginProvider { - plugins(includeFailed?: boolean): DynamicPlugin[]; + plugins(options?: { includeFailed?: boolean }): DynamicPlugin[]; getScannedPackage(plugin: DynamicPlugin): ScannedPluginPackage; } @@ -87,14 +87,16 @@ export interface DynamicPluginProvider * @public */ export interface BackendPluginProvider { - backendPlugins(includeFailed?: boolean): BackendDynamicPlugin[]; + backendPlugins(options?: { includeFailed?: boolean }): BackendDynamicPlugin[]; } /** * @public */ export interface FrontendPluginProvider { - frontendPlugins(includeFailed?: boolean): FrontendDynamicPlugin[]; + frontendPlugins(options?: { + includeFailed?: boolean; + }): FrontendDynamicPlugin[]; } /** diff --git a/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts index 80ae2ed876..7091999e7b 100644 --- a/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts +++ b/packages/backend-dynamic-feature-service/src/scanner/plugin-scanner.ts @@ -72,25 +72,25 @@ export class PluginScanner { private applyConfig(): void | never { 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; diff --git a/yarn.lock b/yarn.lock index c360f2247d..36f19e9093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40003,7 +40003,6 @@ __metadata: resolution: "root@workspace:." dependencies: "@backstage/cli": "workspace:*" - "@backstage/cli-node": "workspace:^" "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/e2e-test-utils": "workspace:*" From 307c8ab6b6bfc69b167fbf32f4b9d4a9d388e1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 14 Oct 2024 15:49:18 +0200 Subject: [PATCH 6/6] Update packages/backend-dynamic-feature-service/src/features/features.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/features/features.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-dynamic-feature-service/src/features/features.test.ts b/packages/backend-dynamic-feature-service/src/features/features.test.ts index 851e1d0578..791b806f54 100644 --- a/packages/backend-dynamic-feature-service/src/features/features.test.ts +++ b/packages/backend-dynamic-feature-service/src/features/features.test.ts @@ -238,7 +238,7 @@ describe('dynamicPluginsFeatureLoader', () => { dynamicPluginsFeatureLoader({ moduleLoader: jestFreeTypescriptAwareModuleLoader, }), - import('@backstage/plugin-app-backend/alpha'), + import('@backstage/plugin-app-backend'), ], });