BackendPluginManager use default exports

for dynamic plugins on the new backend system.

This allows making a plugin dynamic without any source code change
(which was previously required to add a dedicated entrypoint).

Signed-off-by: David Festal <dfestal@redhat.com>
This commit is contained in:
David Festal
2023-11-28 18:40:40 +01:00
parent b70b16c5b6
commit 6a18c333f3
3 changed files with 127 additions and 8 deletions
@@ -49,6 +49,6 @@ export class CommonJSModuleLoader implements ModuleLoader {
}
async load(packagePath: string): Promise<any> {
return await import(/* webpackIgnore: true */ packagePath);
return await require(/* webpackIgnore: true */ packagePath);
}
}
@@ -113,6 +113,94 @@ describe('backend-plugin-manager', () => {
>([]);
},
},
{
name: 'should successfully load a new backend plugin by the default BackendFeature',
packageManifest: {
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
backstage: {
role: 'backend-plugin',
},
main: 'dist/index.cjs.js',
},
indexFile: {
retativePath: ['dist', 'index.cjs.js'],
content: `const alpha = { $$type: '@backstage/BackendFeature' }; exports["default"] = alpha;`,
},
expectedLogs(location) {
return {
infos: [
{
message: `loaded dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`,
},
],
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([
{
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
installer: {
kind: 'new',
},
},
]);
const installer: NewBackendPluginInstaller = (
plugins[0] as BackendDynamicPlugin
).installer as NewBackendPluginInstaller;
expect(installer.install()).toEqual<
BackendFeature | BackendFeature[]
>({ $$type: '@backstage/BackendFeature' });
},
},
{
name: 'should successfully load a new backend plugin by the default BackendFeatureFactory',
packageManifest: {
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
backstage: {
role: 'backend-plugin',
},
main: 'dist/index.cjs.js',
},
indexFile: {
retativePath: ['dist', 'index.cjs.js'],
content: `const alpha = () => { return { $$type: '@backstage/BackendFeature' } };
alpha.$$type = '@backstage/BackendFeatureFactory';
exports["default"] = alpha;`,
},
expectedLogs(location) {
return {
infos: [
{
message: `loaded dynamic backend plugin 'backend-dynamic-plugin-test' from '${location}'`,
},
],
};
},
checkLoadedPlugins(plugins) {
expect(plugins).toMatchObject([
{
name: 'backend-dynamic-plugin-test',
version: '0.0.0',
role: 'backend-plugin',
platform: 'node',
installer: {
kind: 'new',
},
},
]);
const installer: NewBackendPluginInstaller = (
plugins[0] as BackendDynamicPlugin
).installer as NewBackendPluginInstaller;
expect(installer.install()).toEqual<
BackendFeature | BackendFeature[]
>({ $$type: '@backstage/BackendFeature' });
},
},
{
name: 'should successfully load a new backend plugin module',
packageManifest: {
@@ -221,7 +309,7 @@ describe('backend-plugin-manager', () => {
return {
errors: [
{
message: `dynamic backend plugin 'backend-dynamic-plugin-test' could not be loaded from '${location}': the module should export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field.`,
message: `dynamic backend plugin 'backend-dynamic-plugin-test' could not be loaded from '${location}': the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
},
],
};
@@ -249,7 +337,7 @@ describe('backend-plugin-manager', () => {
return {
errors: [
{
message: `dynamic backend plugin 'backend-dynamic-plugin-test' could not be loaded from '${location}': the module should export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field.`,
message: `dynamic backend plugin 'backend-dynamic-plugin-test' could not be loaded from '${location}': the module should either export a 'BackendFeature' or 'BackendFeatureFactory' as default export, or export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field as dynamic loading entrypoint.`,
},
],
};
@@ -378,7 +466,7 @@ describe('backend-plugin-manager', () => {
logger,
async bootstrap(_: string, __: string[]): Promise<void> {},
load: async (packagePath: string) =>
await import(/* webpackIgnore: true */ packagePath),
await require(/* webpackIgnore: true */ packagePath),
});
const loadedPlugins: DynamicPlugin[] = await pluginManager.loadPlugins();
@@ -140,12 +140,25 @@ export class PluginManager implements BackendPluginProvider {
`${plugin.location}/${plugin.manifest.main}`,
);
try {
const { dynamicPluginInstaller } = await this.moduleLoader.load(
packagePath,
);
const pluginModule = await this.moduleLoader.load(packagePath);
let dynamicPluginInstaller;
if (isBackendFeature(pluginModule.default)) {
dynamicPluginInstaller = {
kind: 'new',
install: () => pluginModule.default,
};
} else if (isBackendFeatureFactory(pluginModule.default)) {
dynamicPluginInstaller = {
kind: 'new',
install: pluginModule.default,
};
} else {
dynamicPluginInstaller = pluginModule.dynamicPluginInstaller;
}
if (!isBackendDynamicPluginInstaller(dynamicPluginInstaller)) {
this.logger.error(
`dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${plugin.location}': the module should export a 'const dynamicPluginInstaller: BackendDynamicPluginInstaller' field.`,
`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.`,
);
return undefined;
}
@@ -263,3 +276,21 @@ export const dynamicPluginsFeatureDiscoveryServiceFactory =
return new DynamicPluginsEnabledFeatureDiscoveryService(dynamicPlugins);
},
});
function isBackendFeature(value: unknown): value is BackendFeature {
return (
!!value &&
typeof value === 'object' &&
(value as BackendFeature).$$type === '@backstage/BackendFeature'
);
}
function isBackendFeatureFactory(
value: unknown,
): value is () => BackendFeature {
return (
!!value &&
typeof value === 'function' &&
(value as any).$$type === '@backstage/BackendFeatureFactory'
);
}