Skip to content

Commit

Permalink
Merge e99fb85 into d07b82b
Browse files Browse the repository at this point in the history
  • Loading branch information
davidfestal committed Mar 12, 2024
2 parents d07b82b + e99fb85 commit bbe8ba0
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 1 deletion.
8 changes: 7 additions & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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) {
Expand Down
151 changes: 151 additions & 0 deletions packages/backend/src/loader/CommonJSModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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,
);
}

if (
// we're searching for the folder of a backstage package (through @backstage/backend-common/resolvePackagePath) ?
// => - we're 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`)
) {
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 === p.name ||
searchedPackageNameDynamic === 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<any> {
return await require(/* webpackIgnore: true */ packagePath);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/loader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CommonJSModuleLoader } from './CommonJSModuleLoader';

0 comments on commit bbe8ba0

Please sign in to comment.