fix(backend-dynamic-feature-service): improve package resolution for bundled dynamic plugins
Updated `resolvePackagePath` to correctly resolve `package.json` for dynamic plugins that bundle their own copy of `@backstage/backend-plugin-api`. This change ensures that the resolution works for both the host application and bundled dependencies, enhancing compatibility with plugins created using the `backstage-cli package bundle` command. Signed-off-by: David Festal <dfestal@redhat.com> Assisted-by: Cursor
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-dynamic-feature-service': patch
|
||||
---
|
||||
|
||||
Fixed `resolvePackagePath` resolution for bundled dynamic plugins. When a plugin bundles its own copy of `@backstage/backend-plugin-api` inside `node_modules`, the `CommonJSModuleLoader` fallback now correctly resolves the plugin's `package.json` by name. Previously the fallback only applied when the resolution originated from the host application; it now also applies when originating from a bundled dependency, which is the case for plugins produced by the `backstage-cli package bundle` command.
|
||||
@@ -38,7 +38,6 @@ The `package` command category, `yarn backstage-cli package --help`:
|
||||
```text
|
||||
start [options] Start a package for local development
|
||||
build [options] Build a package for production deployment or publishing
|
||||
bundle [options] Bundle a plugin for dynamic loading (backend or frontend)
|
||||
lint [options] [directories...] Lint a package
|
||||
test Run tests, forwarding args to Jest, defaulting to watch mode
|
||||
clean Delete cache directories
|
||||
@@ -207,6 +206,11 @@ Options:
|
||||
|
||||
## package bundle
|
||||
|
||||
:::caution Experimental
|
||||
This command is experimental and may receive breaking changes in future releases
|
||||
without a deprecation period. It is hidden from the main `--help` output.
|
||||
:::
|
||||
|
||||
Bundle a plugin for dynamic loading. This creates a self-contained plugin
|
||||
package that can be deployed independently and loaded dynamically by a Backstage
|
||||
application. Supports both backend and frontend plugins.
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
// This require resolves to the bundled proxy copy inside this plugin's
|
||||
// own node_modules/@backstage/backend-plugin-api, NOT the host's copy.
|
||||
var backendPluginApi = require('@backstage/backend-plugin-api');
|
||||
|
||||
// Triggers the _resolveFilename fallback: require.resolve runs from
|
||||
// the bundled @backstage/backend-plugin-api whose mod.path is inside
|
||||
// this plugin's node_modules.
|
||||
var pkgDir = backendPluginApi.resolvePackagePath('plugin-test-backend-bundled');
|
||||
|
||||
const testBundledPlugin = backendPluginApi.createBackendPlugin({
|
||||
pluginId: "test-bundled",
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: backendPluginApi.coreServices.rootLogger,
|
||||
},
|
||||
async init({ logger }) {
|
||||
logger.info("Bundled backend plugin loaded successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
exports.default = testBundledPlugin;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
// Proxy that simulates a bundled copy of @backstage/backend-plugin-api.
|
||||
// Re-exports everything from the real workspace package, but provides a
|
||||
// local resolvePackagePath so that require.resolve() runs from THIS
|
||||
// file's context (mod.path inside node_modules/@backstage/backend-plugin-api).
|
||||
|
||||
const nodePath = require('node:path');
|
||||
const real = require('../../../../../../../../../backend-plugin-api/src');
|
||||
|
||||
function resolvePackagePath(name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
const pkgJson = require.resolve(name + '/package.json');
|
||||
return nodePath.resolve.apply(
|
||||
nodePath,
|
||||
[nodePath.dirname(pkgJson)].concat(args),
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Object.assign({}, real, {
|
||||
resolvePackagePath: resolvePackagePath,
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/backend-plugin-api",
|
||||
"version": "0.0.0",
|
||||
"description": "Proxy that re-exports the real @backstage/backend-plugin-api with a local resolvePackagePath, simulating a bundled copy.",
|
||||
"main": "index.js"
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "plugin-test-backend-bundled-dynamic",
|
||||
"version": "0.0.0",
|
||||
"description": "A test dynamic backend plugin that bundles its own @backstage/backend-plugin-api.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin",
|
||||
"pluginId": "test-bundled",
|
||||
"pluginPackages": [
|
||||
"plugin-test-backend-bundled"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"backstage",
|
||||
"dynamic"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs.js",
|
||||
"default": "./dist/index.cjs.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"main": "./dist/index.cjs.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "0.0.0"
|
||||
},
|
||||
"bundleDependencies": true
|
||||
}
|
||||
@@ -465,6 +465,45 @@ Require stack:
|
||||
});
|
||||
});
|
||||
|
||||
it('should load a backend plugin that bundles its own @backstage/backend-plugin-api', async () => {
|
||||
const dynamicPluginsLister = new DynamicPluginLister();
|
||||
const dynamicPluginsRootForBundled = resolvePath(
|
||||
__dirname,
|
||||
'__fixtures__/dynamic-plugins-root-for-bundled',
|
||||
);
|
||||
await startTestBackend({
|
||||
features: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: dynamicPluginsRootForBundled,
|
||||
},
|
||||
backend: {
|
||||
baseUrl: `http://localhost:0`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
dynamicPluginsFeatureLoader({
|
||||
moduleLoader: logger =>
|
||||
jestFreeTypescriptAwareModuleLoader({ logger }),
|
||||
}),
|
||||
dynamicPluginsLister.feature(),
|
||||
],
|
||||
});
|
||||
|
||||
expect(dynamicPluginsLister.loadedPlugins).toMatchObject([
|
||||
{
|
||||
installer: {
|
||||
kind: 'new',
|
||||
},
|
||||
name: 'plugin-test-backend-bundled-dynamic',
|
||||
platform: 'node',
|
||||
role: 'backend-plugin',
|
||||
version: '0.0.0',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('module federation support', () => {
|
||||
const createRemoteProviderPlugin = (
|
||||
provider: FrontendRemoteResolverProvider,
|
||||
|
||||
@@ -99,15 +99,21 @@ export class CommonJSModuleLoader implements ModuleLoader {
|
||||
);
|
||||
}
|
||||
|
||||
// Are we trying to resolve a `package.json` from an originating module of the core backstage application
|
||||
// (this is mostly done by calling `@backstage/backend-plugin-api/resolvePackagePath`).
|
||||
const resolvingPackageJsonFromBackstageApplication =
|
||||
// Is this a `resolvePackagePath` call from `@backstage/backend-plugin-api`?
|
||||
// This covers both the host application's copy and a bundled copy living
|
||||
// inside a dynamic plugin's own node_modules.
|
||||
// The regex matches mod.path against the various ways the package can be resolved on disk
|
||||
// (with optional subdirectory such as /src or /dist after the package name):
|
||||
// - .../node_modules/@backstage/backend-plugin-api[/...] (npm-installed)
|
||||
// - .../<plugin>/node_modules/@backstage/backend-plugin-api[/...] (bundled)
|
||||
// - .../packages/backend-plugin-api[/...] (symlinked workspace in monorepo)
|
||||
const resolvingPackageJsonViaResolvePackagePath =
|
||||
request?.endsWith('/package.json') &&
|
||||
mod?.path &&
|
||||
!dynamicPluginsPaths.some(p => mod.path.startsWith(p));
|
||||
/[/\\](?:@backstage|packages)[/\\]backend-plugin-api(?:[/\\]|$)/.test(
|
||||
mod?.path ?? '',
|
||||
);
|
||||
|
||||
// If not, we don't need the dedicated specific case below.
|
||||
if (!resolvingPackageJsonFromBackstageApplication) {
|
||||
if (!resolvingPackageJsonViaResolvePackagePath) {
|
||||
throw errorToThrow;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ Options:
|
||||
|
||||
Commands:
|
||||
build
|
||||
bundle
|
||||
clean
|
||||
help [command]
|
||||
postpack
|
||||
@@ -61,22 +60,6 @@ Options:
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli-module-build package bundle`
|
||||
|
||||
```
|
||||
Usage: @backstage/cli-module-build package bundle
|
||||
|
||||
Options:
|
||||
--clean
|
||||
--no-build
|
||||
--no-install
|
||||
--output-destination <string>
|
||||
--output-name <string>
|
||||
--pre-packed-dir <string>
|
||||
--verbose
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli-module-build package clean`
|
||||
|
||||
```
|
||||
|
||||
@@ -38,7 +38,8 @@ export default createCliModule({
|
||||
description:
|
||||
'Bundle a plugin for dynamic loading. Creates a self-contained plugin ' +
|
||||
'package that can be deployed and loaded dynamically by a Backstage application. ' +
|
||||
'Supports both backend and frontend plugins.',
|
||||
'Supports both backend and frontend plugins. Experimental.',
|
||||
experimental: true,
|
||||
execute: { loader: () => import('./commands/package/bundle') },
|
||||
});
|
||||
|
||||
|
||||
@@ -317,7 +317,6 @@ Options:
|
||||
|
||||
Commands:
|
||||
build
|
||||
bundle
|
||||
clean
|
||||
help [command]
|
||||
lint
|
||||
@@ -342,22 +341,6 @@ Options:
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli package bundle`
|
||||
|
||||
```
|
||||
Usage: backstage-cli package bundle
|
||||
|
||||
Options:
|
||||
--clean
|
||||
--no-build
|
||||
--no-install
|
||||
--output-destination <string>
|
||||
--output-name <string>
|
||||
--pre-packed-dir <string>
|
||||
--verbose
|
||||
-h, --help
|
||||
```
|
||||
|
||||
### `backstage-cli package clean`
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user