refactor(backend-dynamic-feature-service): Improve alpha package support
Signed-off-by: David Festal <dfestal@redhat.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Improve the way alpha packages are supported when loading dynamic backend plugins.
|
||||
The `ScannedPluginPackage` descriptor of dynamic backend plugins loaded from their alpha `package.json` now contain both the main package manifest and the alpha manifest. Previously it used to contain only the content of the alpha `package.json`, which is nearly empty.
|
||||
This will make it easier to use or display metadata of loaded dynamic backend plugins, which is contained in the main manifest.
|
||||
@@ -287,6 +287,8 @@ export type ScannedPluginManifest = BackstagePackageJson &
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ScannedPluginPackage {
|
||||
// (undocumented)
|
||||
alphaManifest?: BackstagePackageJson;
|
||||
// (undocumented)
|
||||
location: URL;
|
||||
// (undocumented)
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "plugin-test-backend-alpha-dynamic__alpha",
|
||||
"version": "0.0.0",
|
||||
"main": "../dist/alpha.cjs.js"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var backendPluginApi = require('@backstage/backend-plugin-api');
|
||||
|
||||
const testAlphaPlugin = backendPluginApi.createBackendPlugin({
|
||||
pluginId: "test-alpha",
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: backendPluginApi.coreServices.rootLogger,
|
||||
},
|
||||
async init({
|
||||
logger,
|
||||
}) {
|
||||
logger.info("This plugin has been loaded from the alpha package.");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.default = testAlphaPlugin;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "plugin-test-backend-alpha-dynamic",
|
||||
"version": "0.0.0",
|
||||
"description": "A test dynamic backend module that exposes alpha API.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "test-alpha",
|
||||
"pluginPackages": [
|
||||
"plugin-test-backend-alpha"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"dynamic"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs.js",
|
||||
"default": "./dist/index.cjs.js"
|
||||
},
|
||||
"./alpha": {
|
||||
"require": "./dist/alpha.cjs.js",
|
||||
"default": "./dist/alpha.cjs.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.cjs.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"alpha"
|
||||
],
|
||||
"bundleDependencies": true
|
||||
}
|
||||
@@ -29,8 +29,10 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CommonJSModuleLoader } from '../loader/CommonJSModuleLoader';
|
||||
import * as winston from 'winston';
|
||||
import * as url from 'url';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { overridePackagePathResolution } from '@backstage/backend-plugin-api/testUtils';
|
||||
import { ScannedPluginPackage } from '../scanner';
|
||||
|
||||
// these can get a bit slow in CI
|
||||
jest.setTimeout(60_000);
|
||||
@@ -67,6 +69,8 @@ class MockedTransport extends winston.transports.Console {
|
||||
|
||||
class DynamicPluginLister {
|
||||
readonly loadedPlugins: DynamicPlugin[] = [];
|
||||
getScannedPackage?: (plugin: DynamicPlugin) => ScannedPluginPackage;
|
||||
|
||||
feature(): BackendFeature {
|
||||
// eslint-disable-next-line consistent-this
|
||||
const that = this;
|
||||
@@ -78,6 +82,8 @@ class DynamicPluginLister {
|
||||
dynamicPlugins: dynamicPluginsServiceRef,
|
||||
},
|
||||
async init({ dynamicPlugins }) {
|
||||
that.getScannedPackage = plugin =>
|
||||
dynamicPlugins.getScannedPackage(plugin);
|
||||
that.loadedPlugins.push(
|
||||
...dynamicPlugins.plugins({ includeFailed: true }),
|
||||
);
|
||||
@@ -308,4 +314,70 @@ describe('dynamicPluginsFeatureLoader', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should load a backend plugin from the alpha package first', async () => {
|
||||
const dynamicPLuginsLister = new DynamicPluginLister();
|
||||
const mockedTransport = new MockedTransport();
|
||||
const dynamicPluginsRootForAlpha = resolvePath(
|
||||
__dirname,
|
||||
'__fixtures__/dynamic-plugins-root-for-alpha',
|
||||
);
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootForAlpha,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: jestFreeTypescriptAwareModuleLoader,
|
||||
transports: [mockedTransport],
|
||||
format: winston.format.simple(),
|
||||
}),
|
||||
dynamicPLuginsLister.feature(),
|
||||
],
|
||||
});
|
||||
|
||||
expect(mockedTransport.logs).toContainEqual(
|
||||
'info: This plugin has been loaded from the alpha package. {"service":"backstage"}',
|
||||
);
|
||||
|
||||
const loadedPlugins = dynamicPLuginsLister.loadedPlugins;
|
||||
expect(loadedPlugins).toMatchObject([
|
||||
{
|
||||
installer: {
|
||||
kind: 'new',
|
||||
},
|
||||
name: 'plugin-test-backend-alpha-dynamic',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
dynamicPLuginsLister.getScannedPackage?.(loadedPlugins[0]),
|
||||
).toMatchObject({
|
||||
location: url.pathToFileURL(
|
||||
path.resolve(dynamicPluginsRootForAlpha, 'test-backend-alpha-dynamic'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'plugin-test-backend-alpha-dynamic',
|
||||
version: '0.0.0',
|
||||
description: 'A test dynamic backend module that exposes alpha API.',
|
||||
backstage: {
|
||||
role: 'backend-plugin',
|
||||
pluginId: 'test-alpha',
|
||||
pluginPackages: ['plugin-test-backend-alpha'],
|
||||
},
|
||||
keywords: ['backstage', 'dynamic'],
|
||||
},
|
||||
alphaManifest: {
|
||||
name: 'plugin-test-backend-alpha-dynamic__alpha',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +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';
|
||||
import { BackstagePackageJson, PackageRole } from '@backstage/cli-node';
|
||||
|
||||
describe('backend-dynamic-feature-service', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
@@ -61,6 +61,13 @@ describe('backend-dynamic-feature-service', () => {
|
||||
relativePath: string[];
|
||||
content: string;
|
||||
};
|
||||
alpha?: {
|
||||
packageManifest: BackstagePackageJson;
|
||||
indexFile: {
|
||||
relativePath: string[];
|
||||
content: string;
|
||||
};
|
||||
};
|
||||
expectedLogs?(location: URL): {
|
||||
errors?: LogContent[];
|
||||
warns?: LogContent[];
|
||||
@@ -127,7 +134,7 @@ describe('backend-dynamic-feature-service', () => {
|
||||
},
|
||||
indexFile: {
|
||||
relativePath: ['dist', 'index.cjs.js'],
|
||||
content: `const alpha = { $$type: '@backstage/BackendFeature' }; exports["default"] = alpha;`,
|
||||
content: `const feature = { $$type: '@backstage/BackendFeature' }; exports["default"] = feature;`,
|
||||
},
|
||||
expectedLogs(location) {
|
||||
return {
|
||||
@@ -158,6 +165,60 @@ describe('backend-dynamic-feature-service', () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should load the alpha variant of a backend plugin in priority',
|
||||
packageManifest: {
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
backstage: {
|
||||
role: 'backend-plugin',
|
||||
},
|
||||
main: 'dist/index.cjs.js',
|
||||
},
|
||||
indexFile: {
|
||||
relativePath: ['dist', 'index.cjs.js'],
|
||||
content: `throw 'should not take this one';`,
|
||||
},
|
||||
alpha: {
|
||||
packageManifest: {
|
||||
name: 'backend-dynamic-plugin-test',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
},
|
||||
indexFile: {
|
||||
relativePath: ['dist', 'alpha.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}/alpha'`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
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() as BackendFeature).$$type).toEqual(
|
||||
'@backstage/BackendFeature',
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'should successfully load a new backend plugin by the default BackendFeatureFactory',
|
||||
packageManifest: {
|
||||
@@ -580,11 +641,12 @@ describe('backend-dynamic-feature-service', () => {
|
||||
const plugin: ScannedPluginPackage = {
|
||||
location: url.pathToFileURL(mockDir.resolve(randomUUID())),
|
||||
manifest: tc.packageManifest,
|
||||
alphaManifest: tc.alpha?.packageManifest,
|
||||
};
|
||||
|
||||
const mockedFiles = {
|
||||
[path.join(url.fileURLToPath(plugin.location), 'package.json')]:
|
||||
JSON.stringify(plugin),
|
||||
JSON.stringify(tc.packageManifest),
|
||||
};
|
||||
if (tc.indexFile) {
|
||||
mockedFiles[
|
||||
@@ -594,6 +656,18 @@ describe('backend-dynamic-feature-service', () => {
|
||||
)
|
||||
] = tc.indexFile.content;
|
||||
}
|
||||
if (tc.alpha) {
|
||||
mockedFiles[
|
||||
path.join(url.fileURLToPath(plugin.location), 'alpha', 'package.json')
|
||||
] = JSON.stringify(tc.alpha.packageManifest);
|
||||
mockedFiles[
|
||||
path.join(
|
||||
url.fileURLToPath(plugin.location),
|
||||
...tc.alpha.indexFile.relativePath,
|
||||
)
|
||||
] = tc.alpha.indexFile.content;
|
||||
}
|
||||
|
||||
mockDir.setContent(mockedFiles);
|
||||
|
||||
const logger = new MockedLogger();
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { PackageRole, PackageRoles } from '@backstage/cli-node';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import {
|
||||
FeatureDiscoveryService,
|
||||
@@ -79,13 +78,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
);
|
||||
|
||||
const dynamicPluginsPaths = scannedPlugins.map(p =>
|
||||
fs.realpathSync(
|
||||
path.dirname(
|
||||
path.dirname(
|
||||
path.resolve(url.fileURLToPath(p.location), p.manifest.main),
|
||||
),
|
||||
),
|
||||
),
|
||||
fs.realpathSync(url.fileURLToPath(p.location)),
|
||||
);
|
||||
|
||||
await moduleLoader.bootstrap(backstageRoot, dynamicPluginsPaths);
|
||||
@@ -164,8 +157,13 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
private async loadBackendPlugin(
|
||||
plugin: ScannedPluginPackage,
|
||||
): Promise<BackendDynamicPlugin> {
|
||||
const usedPluginManifest =
|
||||
plugin.alphaManifest?.main ?? plugin.manifest.main;
|
||||
const usedPluginLocation = plugin.alphaManifest?.main
|
||||
? `${plugin.location}/alpha`
|
||||
: plugin.location;
|
||||
const packagePath = url.fileURLToPath(
|
||||
`${plugin.location}/${plugin.manifest.main}`,
|
||||
`${usedPluginLocation}/${usedPluginManifest}`,
|
||||
);
|
||||
const dynamicPlugin: BackendDynamicPlugin = {
|
||||
name: plugin.manifest.name,
|
||||
@@ -194,12 +192,12 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
}
|
||||
if (dynamicPlugin.installer) {
|
||||
this.logger.info(
|
||||
`loaded dynamic backend plugin '${plugin.manifest.name}' from '${plugin.location}'`,
|
||||
`loaded dynamic backend plugin '${plugin.manifest.name}' from '${usedPluginLocation}'`,
|
||||
);
|
||||
} 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}`,
|
||||
`dynamic backend plugin '${plugin.manifest.name}' could not be loaded from '${usedPluginLocation}': ${dynamicPlugin.failure}`,
|
||||
);
|
||||
}
|
||||
return dynamicPlugin;
|
||||
@@ -210,7 +208,7 @@ export class DynamicPluginManager implements DynamicPluginProvider {
|
||||
: 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}'`,
|
||||
`an error occurred while loading dynamic backend plugin '${plugin.manifest.name}' from '${usedPluginLocation}'`,
|
||||
typedError,
|
||||
);
|
||||
return dynamicPlugin;
|
||||
|
||||
@@ -430,16 +430,19 @@ Please add '${mockDir.resolve(
|
||||
expectedPluginPackages: [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
),
|
||||
mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
},
|
||||
alphaManifest: {
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -21,7 +21,6 @@ import * as chokidar from 'chokidar';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { PackagePlatform, PackageRoles } from '@backstage/cli-node';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
|
||||
export interface DynamicPluginScannerOptions {
|
||||
@@ -37,6 +36,15 @@ export interface ScanRootResponse {
|
||||
|
||||
export const configKey = 'dynamicPlugins';
|
||||
|
||||
class WrappedError extends Error {
|
||||
wrapped: Error;
|
||||
|
||||
constructor(message: string, wrapped: Error) {
|
||||
super(message);
|
||||
this.wrapped = wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginScanner {
|
||||
private _rootDirectory?: string;
|
||||
private configUnsubscribe?: () => void;
|
||||
@@ -161,52 +169,25 @@ export class PluginScanner {
|
||||
}
|
||||
|
||||
let scannedPlugin: ScannedPluginPackage;
|
||||
let platform: PackagePlatform;
|
||||
try {
|
||||
scannedPlugin = await this.scanDir(pluginHome);
|
||||
if (!scannedPlugin.manifest.main) {
|
||||
throw new Error("field 'main' not found in 'package.json'");
|
||||
}
|
||||
if (scannedPlugin.manifest.backstage?.role) {
|
||||
platform = PackageRoles.getRoleInfo(
|
||||
scannedPlugin.manifest.backstage.role,
|
||||
).platform;
|
||||
} else {
|
||||
if (!scannedPlugin.manifest.backstage?.role) {
|
||||
throw new Error("field 'backstage.role' not found in 'package.json'");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`failed to load dynamic plugin manifest from '${pluginHome}'`,
|
||||
e,
|
||||
);
|
||||
if (e instanceof WrappedError) {
|
||||
this.logger.error(e.message, e.wrapped);
|
||||
} else {
|
||||
this.logger.error(
|
||||
`failed to load dynamic plugin manifest from '${pluginHome}'`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (platform === 'node') {
|
||||
if (this.preferAlpha) {
|
||||
const pluginHomeAlpha = path.resolve(pluginHome, 'alpha');
|
||||
if (existsSync(pluginHomeAlpha)) {
|
||||
if ((await fs.lstat(pluginHomeAlpha)).isDirectory()) {
|
||||
const backstage = scannedPlugin.manifest.backstage;
|
||||
try {
|
||||
scannedPlugin = await this.scanDir(pluginHomeAlpha);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`failed to load dynamic plugin manifest from '${pluginHomeAlpha}'`,
|
||||
e,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
scannedPlugin.manifest.backstage = backstage;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`skipping '${pluginHomeAlpha}' since it is not a directory`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scannedPlugins.push(scannedPlugin);
|
||||
}
|
||||
return { packages: scannedPlugins };
|
||||
@@ -216,10 +197,37 @@ export class PluginScanner {
|
||||
const manifestFile = path.resolve(pluginHome, 'package.json');
|
||||
const content = await fs.readFile(manifestFile);
|
||||
const manifest: ScannedPluginManifest = JSON.parse(content.toString());
|
||||
return {
|
||||
const scannedPluginPackage: ScannedPluginPackage = {
|
||||
location: url.pathToFileURL(pluginHome),
|
||||
manifest: manifest,
|
||||
};
|
||||
|
||||
if (this.preferAlpha) {
|
||||
const pluginHomeAlpha = path.resolve(pluginHome, 'alpha');
|
||||
if (existsSync(pluginHomeAlpha)) {
|
||||
if ((await fs.lstat(pluginHomeAlpha)).isDirectory()) {
|
||||
try {
|
||||
const alphaContent = await fs.readFile(
|
||||
path.resolve(pluginHomeAlpha, 'package.json'),
|
||||
);
|
||||
scannedPluginPackage.alphaManifest = JSON.parse(
|
||||
alphaContent.toString(),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new WrappedError(
|
||||
`failed to load dynamic plugin manifest from '${pluginHome}/alpha'`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`skipping '${pluginHomeAlpha}' since it is not a directory`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scannedPluginPackage;
|
||||
}
|
||||
|
||||
async trackChanges(): Promise<void> {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { BackstagePackageJson, PackageRole } from '@backstage/cli-node';
|
||||
export interface ScannedPluginPackage {
|
||||
location: URL;
|
||||
manifest: ScannedPluginManifest;
|
||||
alphaManifest?: BackstagePackageJson;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -161,10 +161,7 @@ async function gatherDynamicPluginsSchemas(
|
||||
let schemaLocation = schemaLocator(pluginPackage);
|
||||
|
||||
if (!path.isAbsolute(schemaLocation)) {
|
||||
let pluginLocation = url.fileURLToPath(pluginPackage.location);
|
||||
if (path.basename(pluginLocation) === 'alpha') {
|
||||
pluginLocation = path.dirname(pluginLocation);
|
||||
}
|
||||
const pluginLocation = url.fileURLToPath(pluginPackage.location);
|
||||
schemaLocation = path.resolve(pluginLocation, schemaLocation);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user