diff --git a/packages/backend-plugin-manager/api-report.md b/packages/backend-plugin-manager/api-report.md index b4221007ae..dc5973a3ab 100644 --- a/packages/backend-plugin-manager/api-report.md +++ b/packages/backend-plugin-manager/api-report.md @@ -65,6 +65,44 @@ export interface BaseDynamicPlugin { // @public (undocumented) export type DynamicPlugin = FrontendDynamicPlugin | BackendDynamicPlugin; +// @public (undocumented) +export class DynamicPluginManager implements DynamicPluginProvider { + // (undocumented) + addBackendPlugin(plugin: BackendDynamicPlugin): void; + // (undocumented) + get availablePackages(): ScannedPluginPackage[]; + // (undocumented) + backendPlugins(): BackendDynamicPlugin[]; + // (undocumented) + static create( + options: DynamicPluginManagerOptions, + ): Promise; + // (undocumented) + frontendPlugins(): FrontendDynamicPlugin[]; + // (undocumented) + plugins(): DynamicPlugin[]; +} + +// @public (undocumented) +export interface DynamicPluginManagerOptions { + // (undocumented) + config: Config; + // (undocumented) + logger: LoggerService; + // (undocumented) + moduleLoader?: ModuleLoader; + // (undocumented) + preferAlpha?: boolean; +} + +// @public (undocumented) +export interface DynamicPluginProvider + extends FrontendPluginProvider, + BackendPluginProvider { + // (undocumented) + plugins(): DynamicPlugin[]; +} + // @public (undocumented) export interface DynamicPluginsFactoryOptions { // (undocumented) @@ -80,11 +118,11 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory: () => ServiceFactory< // @public (undocumented) export const dynamicPluginsServiceFactory: ( options?: DynamicPluginsFactoryOptions | undefined, -) => ServiceFactory; +) => ServiceFactory; // @public (undocumented) export const dynamicPluginsServiceRef: ServiceRef< - BackendPluginProvider, + DynamicPluginProvider, 'root' >; @@ -94,6 +132,12 @@ export interface FrontendDynamicPlugin extends BaseDynamicPlugin { platform: 'web'; } +// @public (undocumented) +export interface FrontendPluginProvider { + // (undocumented) + frontendPlugins(): FrontendDynamicPlugin[]; +} + // @public (undocumented) export function isBackendDynamicPluginInstaller( obj: any, @@ -161,25 +205,6 @@ export interface NewBackendPluginInstaller { kind: 'new'; } -// @public (undocumented) -export class PluginManager implements BackendPluginProvider { - // (undocumented) - addBackendPlugin(plugin: BackendDynamicPlugin): void; - // (undocumented) - get availablePackages(): ScannedPluginPackage[]; - // (undocumented) - backendPlugins(): BackendDynamicPlugin[]; - // (undocumented) - static fromConfig( - config: Config, - logger: LoggerService, - preferAlpha?: boolean, - moduleLoader?: ModuleLoader, - ): Promise; - // (undocumented) - readonly plugins: DynamicPlugin[]; -} - // @public (undocumented) export type ScannedPluginManifest = BackstagePackageJson & Required> & diff --git a/packages/backend-plugin-manager/src/manager/index.ts b/packages/backend-plugin-manager/src/manager/index.ts index a9cd163617..dd6aaacfe9 100644 --- a/packages/backend-plugin-manager/src/manager/index.ts +++ b/packages/backend-plugin-manager/src/manager/index.ts @@ -25,14 +25,19 @@ export type { NewBackendPluginInstaller, LegacyBackendPluginInstaller, LegacyPluginEnvironment, + DynamicPluginProvider, + FrontendPluginProvider, BackendPluginProvider, } from './types'; export { - PluginManager, + DynamicPluginManager, dynamicPluginsFeatureDiscoveryServiceFactory, dynamicPluginsServiceFactory, dynamicPluginsServiceRef, } from './plugin-manager'; -export type { DynamicPluginsFactoryOptions } from './plugin-manager'; +export type { + DynamicPluginManagerOptions, + DynamicPluginsFactoryOptions, +} from './plugin-manager'; diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts index 5b1e1caf4d..405fd629f2 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { PluginManager, dynamicPluginsServiceFactory } from './plugin-manager'; +import { + DynamicPluginManager, + dynamicPluginsServiceFactory, +} from './plugin-manager'; import { BackendFeature, coreServices, @@ -462,12 +465,16 @@ describe('backend-plugin-manager', () => { mockDir.setContent(mockedFiles); const logger = new MockedLogger(); - const pluginManager = new (PluginManager as any)(logger, [plugin], { + const pluginManager = new (DynamicPluginManager as any)( logger, - async bootstrap(_: string, __: string[]): Promise {}, - load: async (packagePath: string) => - await require(/* webpackIgnore: true */ packagePath), - }); + [plugin], + { + logger, + async bootstrap(_: string, __: string[]): Promise {}, + load: async (packagePath: string) => + await require(/* webpackIgnore: true */ packagePath), + }, + ); const loadedPlugins: DynamicPlugin[] = await pluginManager.loadPlugins(); @@ -483,7 +490,10 @@ describe('backend-plugin-manager', () => { describe('backendPlugins', () => { it('should return only backend plugins and modules', async () => { const logger = new MockedLogger(); - const pluginManager = new (PluginManager as any)(logger, '', []); + const pluginManager = new (DynamicPluginManager as any)( + logger, + [], + ) as DynamicPluginManager; const plugins: BaseDynamicPlugin[] = [ { name: 'a-frontend-plugin', @@ -504,7 +514,7 @@ describe('backend-plugin-manager', () => { version: '0.0.0', }, ]; - pluginManager.plugins = plugins; + (pluginManager as any)._plugins = plugins; expect(pluginManager.backendPlugins()).toEqual([ { name: 'a-backend-plugin', @@ -522,6 +532,57 @@ describe('backend-plugin-manager', () => { }); }); + 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[] = [ + { + 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-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.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', + }, + ]); + }); + }); + describe('dynamicPluginsServiceFactory', () => { const otherMockDir = createMockDirectory(); @@ -547,27 +608,29 @@ describe('backend-plugin-manager', () => { 'a-dynamic-plugin': {}, }); - const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig'); + const fromConfigSpier = jest.spyOn(DynamicPluginManager, 'create'); const applyConfigSpier = jest .spyOn(PluginScanner.prototype as any, 'applyConfig') .mockImplementation(() => {}); const scanRootSpier = jest .spyOn(PluginScanner.prototype, 'scanRoot') - .mockImplementation(async () => [ - { - location: url.pathToFileURL( - mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), - ), - manifest: { - name: 'test', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { - role: 'backend-plugin', + .mockImplementation(async () => ({ + packages: [ + { + location: url.pathToFileURL( + mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), + ), + manifest: { + name: 'test', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { + role: 'backend-plugin', + }, }, }, - }, - ]); + ], + })); const mockedModuleLoader = { logger, bootstrap: jest.fn(), diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.ts index 5a5caa0a89..edc3e20f72 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.ts +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.ts @@ -15,10 +15,11 @@ */ import { Config } from '@backstage/config'; import { - BackendPluginProvider, + DynamicPluginProvider, BackendDynamicPlugin, isBackendDynamicPluginInstaller, DynamicPlugin, + FrontendDynamicPlugin, } from './types'; import { ScannedPluginPackage } from '../scanner'; import { PluginScanner } from '../scanner/plugin-scanner'; @@ -44,24 +45,37 @@ import { /** * @public */ -export class PluginManager implements BackendPluginProvider { - static async fromConfig( - config: Config, - logger: LoggerService, - preferAlpha: boolean = false, - moduleLoader: ModuleLoader = new CommonJSModuleLoader(logger), - ): Promise { +export interface DynamicPluginManagerOptions { + config: Config; + logger: LoggerService; + preferAlpha?: boolean; + moduleLoader?: ModuleLoader; +} + +/** + * @public + */ +export class DynamicPluginManager implements DynamicPluginProvider { + static async create( + options: DynamicPluginManagerOptions, + ): Promise { /* eslint-disable-next-line no-restricted-syntax */ const backstageRoot = findPaths(__dirname).targetRoot; - const scanner = new PluginScanner( - config, - logger, + const scanner = PluginScanner.create({ + config: options.config, + logger: options.logger, backstageRoot, - preferAlpha, - ); - const scannedPlugins = await scanner.scanRoot(); + preferAlpha: options.preferAlpha, + }); + const scannedPlugins = (await scanner.scanRoot()).packages; scanner.trackChanges(); - const manager = new PluginManager(logger, scannedPlugins, moduleLoader); + const moduleLoader = + options.moduleLoader || new CommonJSModuleLoader(options.logger); + const manager = new DynamicPluginManager( + options.logger, + scannedPlugins, + moduleLoader, + ); const dynamicPluginsPaths = scannedPlugins.map(p => fs.realpathSync( @@ -76,15 +90,15 @@ export class PluginManager implements BackendPluginProvider { moduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths); scanner.subscribeToRootDirectoryChange(async () => { - manager._availablePackages = await scanner.scanRoot(); + manager._availablePackages = (await scanner.scanRoot()).packages; // TODO: do not store _scannedPlugins again, but instead store a diff of the changes }); - manager.plugins.push(...(await manager.loadPlugins())); + manager._plugins.push(...(await manager.loadPlugins())); return manager; } - readonly plugins: DynamicPlugin[]; + private readonly _plugins: DynamicPlugin[]; private _availablePackages: ScannedPluginPackage[]; private constructor( @@ -92,7 +106,7 @@ export class PluginManager implements BackendPluginProvider { private packages: ScannedPluginPackage[], private readonly moduleLoader: ModuleLoader, ) { - this.plugins = []; + this._plugins = []; this._availablePackages = packages; } @@ -101,7 +115,7 @@ export class PluginManager implements BackendPluginProvider { } addBackendPlugin(plugin: BackendDynamicPlugin): void { - this.plugins.push(plugin); + this._plugins.push(plugin); } private async loadPlugins(): Promise { @@ -182,16 +196,26 @@ export class PluginManager implements BackendPluginProvider { } backendPlugins(): BackendDynamicPlugin[] { - return this.plugins.filter( + return this._plugins.filter( (p): p is BackendDynamicPlugin => p.platform === 'node', ); } + + frontendPlugins(): FrontendDynamicPlugin[] { + return this._plugins.filter( + (p): p is FrontendDynamicPlugin => p.platform === 'web', + ); + } + + plugins(): DynamicPlugin[] { + return this._plugins; + } } /** * @public */ -export const dynamicPluginsServiceRef = createServiceRef( +export const dynamicPluginsServiceRef = createServiceRef( { id: 'core.dynamicplugins', scope: 'root', @@ -216,15 +240,12 @@ export const dynamicPluginsServiceFactory = createServiceFactory( logger: coreServices.rootLogger, }, async factory({ config, logger }) { - if (options?.moduleLoader) { - return await PluginManager.fromConfig( - config, - logger, - true, - options.moduleLoader(logger), - ); - } - return await PluginManager.fromConfig(config, logger, true); + return await DynamicPluginManager.create({ + config, + logger, + preferAlpha: true, + moduleLoader: options?.moduleLoader?.(logger), + }); }, }), ); @@ -233,7 +254,7 @@ class DynamicPluginsEnabledFeatureDiscoveryService implements FeatureDiscoveryService { constructor( - private readonly dynamicPlugins: BackendPluginProvider, + private readonly dynamicPlugins: DynamicPluginProvider, private readonly featureDiscoveryService?: FeatureDiscoveryService, ) {} diff --git a/packages/backend-plugin-manager/src/manager/types.ts b/packages/backend-plugin-manager/src/manager/types.ts index 201941292d..0038470686 100644 --- a/packages/backend-plugin-manager/src/manager/types.ts +++ b/packages/backend-plugin-manager/src/manager/types.ts @@ -67,6 +67,15 @@ export type LegacyPluginEnvironment = { pluginProvider: BackendPluginProvider; }; +/** + * @public + */ +export interface DynamicPluginProvider + extends FrontendPluginProvider, + BackendPluginProvider { + plugins(): DynamicPlugin[]; +} + /** * @public */ @@ -74,6 +83,13 @@ export interface BackendPluginProvider { backendPlugins(): BackendDynamicPlugin[]; } +/** + * @public + */ +export interface FrontendPluginProvider { + frontendPlugins(): FrontendDynamicPlugin[]; +} + /** * @public */ diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts index 7b795aee4f..2d017d805b 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner-watcher.test.ts @@ -64,22 +64,23 @@ describe('plugin-scanner', () => { unsubscribe: configUnsubscribe, }; }; - const pluginScanner = new PluginScanner( + const pluginScanner = PluginScanner.create({ config, logger, - backstageRootDirectory, - false, - ); + backstageRoot: backstageRootDirectory, + preferAlpha: false, + }); await pluginScanner.trackChanges(); expect(onConfigChange).toBeDefined(); - let scannedPlugins: ScannedPluginPackage[] = - await pluginScanner.scanRoot(); + let scannedPlugins: ScannedPluginPackage[] = ( + await pluginScanner.scanRoot() + ).packages; expect(scannedPlugins).toEqual([]); const rootDirectorySubscriber = jest.fn(async () => { - scannedPlugins = await pluginScanner.scanRoot(); + scannedPlugins = (await pluginScanner.scanRoot()).packages; }); pluginScanner.subscribeToRootDirectoryChange(rootDirectorySubscriber); diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts index d4ab8940c9..e52eee87c4 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts @@ -206,11 +206,11 @@ Please add '${mockDir.resolve( const logger = new MockedLogger(); const backstageRoot = tc.backstageRoot ? tc.backstageRoot : ''; function toTest(): PluginScanner { - return new PluginScanner( - new ConfigReader(tc.config), + return PluginScanner.create({ + config: new ConfigReader(tc.config), logger, backstageRoot, - ); + }); } if (tc.fileSystem) { mockDir.setContent(tc.fileSystem); @@ -609,8 +609,8 @@ Please add '${mockDir.resolve( const logger = new MockedLogger(); const backstageRoot = mockDir.resolve('backstageRoot'); async function toTest(): Promise { - const pluginScanner = new PluginScanner( - new ConfigReader( + const pluginScanner = PluginScanner.create({ + config: new ConfigReader( tc.fileSystem ? { dynamicPlugins: { @@ -621,9 +621,9 @@ Please add '${mockDir.resolve( ), logger, backstageRoot, - tc.preferAlpha === undefined ? false : tc.preferAlpha, - ); - return await pluginScanner.scanRoot(); + preferAlpha: tc.preferAlpha, + }); + return (await pluginScanner.scanRoot()).packages; } if (tc.fileSystem) { mockDir.setContent(tc.fileSystem); diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts index 51e38002e9..25e0d614dd 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.ts @@ -24,28 +24,39 @@ import debounce from 'lodash/debounce'; import { PackagePlatform, PackageRoles } from '@backstage/cli-node'; import { LoggerService } from '@backstage/backend-plugin-api'; +export interface DynamicPluginScannerOptions { + config: Config; + backstageRoot: string; + logger: LoggerService; + preferAlpha?: boolean; +} + +export interface ScanRootResponse { + packages: ScannedPluginPackage[]; +} + export class PluginScanner { - private readonly logger: LoggerService; - private backstageRoot: string; - readonly #config: Config; private _rootDirectory?: string; - private readonly preferAlpha: boolean; private configUnsubscribe?: () => void; private rootDirectoryWatcher?: chokidar.FSWatcher; private subscribers: (() => void)[] = []; - constructor( - config: Config, - logger: LoggerService, - backstageRoot: string, - preferAlpha: boolean = false, - ) { - this.backstageRoot = backstageRoot; - this.logger = logger; - this.preferAlpha = preferAlpha; - this.#config = config; + private constructor( + private readonly config: Config, + private readonly logger: LoggerService, + private readonly backstageRoot: string, + private readonly preferAlpha: boolean, + ) {} - this.applyConfig(); + static create(options: DynamicPluginScannerOptions): PluginScanner { + const scanner = new PluginScanner( + options.config, + options.logger, + options.backstageRoot, + options.preferAlpha || false, + ); + scanner.applyConfig(); + return scanner; } subscribeToRootDirectoryChange(subscriber: () => void) { @@ -57,7 +68,7 @@ export class PluginScanner { } private applyConfig(): void | never { - const dynamicPlugins = this.#config.getOptional('dynamicPlugins'); + const dynamicPlugins = this.config.getOptional('dynamicPlugins'); if (!dynamicPlugins) { this.logger.info("'dynamicPlugins' config entry not found."); this._rootDirectory = undefined; @@ -114,9 +125,9 @@ export class PluginScanner { this._rootDirectory = dynamicPluginsRootPath; } - async scanRoot(): Promise { + async scanRoot(): Promise { if (!this._rootDirectory) { - return []; + return { packages: [] }; } const dynamicPluginsLocation = this._rootDirectory; @@ -189,7 +200,7 @@ export class PluginScanner { scannedPlugins.push(scannedPlugin); } - return scannedPlugins; + return { packages: scannedPlugins }; } private async scanDir(pluginHome: string): Promise { @@ -263,30 +274,28 @@ export class PluginScanner { }; await setupRootDirectoryWatcher(); - if (this.#config.subscribe) { - const { unsubscribe } = this.#config.subscribe( - async (): Promise => { - const oldRootDirectory = this._rootDirectory; - try { - this.applyConfig(); - } catch (e) { - this.logger.error( - 'failed to apply new config for dynamic plugins', - e, - ); + if (this.config.subscribe) { + const { unsubscribe } = this.config.subscribe(async (): Promise => { + const oldRootDirectory = this._rootDirectory; + try { + this.applyConfig(); + } catch (e) { + this.logger.error( + 'failed to apply new config for dynamic plugins', + e, + ); + } + if (oldRootDirectory !== this._rootDirectory) { + this.logger.info( + `rootDirectory changed in Config from '${oldRootDirectory}' to '${this._rootDirectory}'`, + ); + this.subscribers.forEach(s => s()); + if (this.rootDirectoryWatcher) { + await this.rootDirectoryWatcher.close(); } - if (oldRootDirectory !== this._rootDirectory) { - this.logger.info( - `rootDirectory changed in Config from '${oldRootDirectory}' to '${this._rootDirectory}'`, - ); - this.subscribers.forEach(s => s()); - if (this.rootDirectoryWatcher) { - await this.rootDirectoryWatcher.close(); - } - await setupRootDirectoryWatcher(); - } - }, - ); + await setupRootDirectoryWatcher(); + } + }); this.configUnsubscribe = unsubscribe; } }