From 3522f3d81e70683c87bf2c93f5a4b4b42171608f Mon Sep 17 00:00:00 2001 From: David Festal Date: Tue, 12 Mar 2024 21:07:15 +0100 Subject: [PATCH] Support `resolvePackagePath` for dynamic backend plugins. (#1071) Signed-off-by: David Festal --- packages/backend/src/index.ts | 8 +- .../src/loader/CommonJSModuleLoader.ts | 151 ++++++++++++++++++ packages/backend/src/loader/index.ts | 1 + 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/loader/CommonJSModuleLoader.ts create mode 100644 packages/backend/src/loader/index.ts diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index ae99d35f6..c67909879 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -16,6 +16,7 @@ import { metricsHandler } from './metrics'; import { statusCheckHandler } from '@backstage/backend-common'; import { RequestHandler } from 'express'; import * as path from 'path'; +import { CommonJSModuleLoader } from './loader'; const backend = createBackend(); @@ -43,7 +44,12 @@ backend.add( }), ); backend.add(dynamicPluginsFeatureDiscoveryServiceFactory()); // overridden version of the FeatureDiscoveryService which provides features loaded by dynamic plugins -backend.add(dynamicPluginsServiceFactory()); +backend.add( + dynamicPluginsServiceFactory({ + moduleLoader: logger => new CommonJSModuleLoader(logger), + }), +); + backend.add( dynamicPluginsSchemasServiceFactory({ schemaLocator(pluginPackage) { diff --git a/packages/backend/src/loader/CommonJSModuleLoader.ts b/packages/backend/src/loader/CommonJSModuleLoader.ts new file mode 100644 index 000000000..c737f789c --- /dev/null +++ b/packages/backend/src/loader/CommonJSModuleLoader.ts @@ -0,0 +1,151 @@ +import { + ModuleLoader, + ScannedPluginManifest, +} from '@backstage/backend-dynamic-feature-service'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import path from 'path'; +import * as fs from 'fs'; + +export class CommonJSModuleLoader implements ModuleLoader { + constructor(public readonly logger: LoggerService) {} + + async bootstrap( + backstageRoot: string, + dynamicPluginsPaths: string[], + ): Promise { + const backstageRootNodeModulesPath = `${backstageRoot}/node_modules`; + const dynamicNodeModulesPaths = [ + ...dynamicPluginsPaths.map(p => path.resolve(p, 'node_modules')), + ]; + const ModuleObject = require('module'); + const oldNodeModulePaths = ModuleObject._nodeModulePaths; + ModuleObject._nodeModulePaths = (from: string): string[] => { + const result: string[] = oldNodeModulePaths(from); + if (!dynamicPluginsPaths.some(p => from.startsWith(p))) { + return result; + } + const filtered = result.filter(nodeModulePath => { + return ( + nodeModulePath === backstageRootNodeModulesPath || + dynamicNodeModulesPaths.some(p => nodeModulePath.startsWith(p)) + ); + }); + this.logger.debug( + `Overriding node_modules search path for dynamic plugin ${from} to: ${filtered}`, + ); + return filtered; + }; + + let dynamicPluginPackages: { + name: string; + dependencies: string[]; + path: string; + }[] = []; + let dynamicPluginPackagesFilled = false; + + const oldResolveFileName = ModuleObject._resolveFilename; + ModuleObject._resolveFilename = ( + request: string, + mod: NodeModule, + _: boolean, + options: any, + ): any => { + let errorToThrow: any; + try { + return oldResolveFileName(request, mod, _, options); + } catch (e) { + errorToThrow = e; + this.logger.debug( + `Could not resolve '${request}' in the Core backstage backend application`, + e instanceof Error ? e : undefined, + ); + } + + const mostProbablyCallingResolvePackagePath = + // Are we searching for the folder of a backstage package by calling @backstage/backend-common/resolvePackagePath ? + // => are we trying to resolve a `package.json` ... + request?.endsWith('/package.json') && + // ... from the `backend-common` core backstage application module + mod?.path && + !dynamicPluginsPaths.some(p => mod.path.startsWith(p)) && + mod.path.includes(`backend-common`); + + if (!mostProbablyCallingResolvePackagePath) { + throw errorToThrow; + } + + this.logger.info(`Resolving '${request}' in the dynamic backend plugins`); + + if (!dynamicPluginPackagesFilled) { + dynamicPluginPackagesFilled = true; + dynamicPluginPackages = + this.buildDynamicPluginPackages(dynamicPluginsPaths); + } + + const searchedPackageName = request.replace(/\/package.json$/, ''); + const searchedPackageNameDynamic = `${searchedPackageName}-dynamic`; + for (const p of dynamicPluginPackages) { + // Case of a dynamic plugin package + if ( + [searchedPackageName, searchedPackageNameDynamic].includes(p.name) + ) { + const resolvedPath = path.resolve(p.path, 'package.json'); + this.logger.info(`Resolved '${request}' at ${resolvedPath}`); + return resolvedPath; + } + + // Case of a dynamic plugin wrapper package + if (p.dependencies.includes(searchedPackageName)) { + const searchPath = path.resolve(p.path, 'node_modules'); + try { + const resolvedPath = require.resolve( + `${searchedPackageName}/package.json`, + { + paths: [searchPath], + }, + ); + this.logger.info(`Resolved '${request}' at ${resolvedPath}`); + return resolvedPath; + } catch (e) { + this.logger.error( + `Error when resolving '${searchedPackageName}' with search path: '[${searchPath}]'`, + e instanceof Error ? e : undefined, + ); + } + } + } + + throw errorToThrow; + }; + } + + buildDynamicPluginPackages(dynamicPluginsPaths: string[]) { + const dynamicPluginPackages: { + name: string; + dependencies: string[]; + path: string; + }[] = []; + dynamicPluginsPaths.forEach(p => { + try { + const manifestFile = path.resolve(p, 'package.json'); + const content = fs.readFileSync(manifestFile); + const manifest: ScannedPluginManifest = JSON.parse(content.toString()); + dynamicPluginPackages.push({ + name: manifest.name, + dependencies: Object.keys(manifest.dependencies || {}), + path: p, + }); + } catch (e) { + this.logger.error( + `Error when reading 'package.json' in '${p}'`, + e instanceof Error ? e : undefined, + ); + } + }); + return dynamicPluginPackages; + } + + async load(packagePath: string): Promise { + return await require(/* webpackIgnore: true */ packagePath); + } +} diff --git a/packages/backend/src/loader/index.ts b/packages/backend/src/loader/index.ts new file mode 100644 index 000000000..f73d02cb0 --- /dev/null +++ b/packages/backend/src/loader/index.ts @@ -0,0 +1 @@ +export { CommonJSModuleLoader } from './CommonJSModuleLoader';