module-federation-common: followup to initial implementation
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -252,24 +252,7 @@ export async function createConfig(
|
||||
exposes,
|
||||
name: options.moduleFederationRemote.name,
|
||||
runtime: false,
|
||||
shared: Object.fromEntries(
|
||||
Object.entries(options.moduleFederationRemote.sharedDependencies).map(
|
||||
([name, p]) => [
|
||||
name,
|
||||
{
|
||||
...(p.version === undefined ? {} : { version: p.version }),
|
||||
...(p.requiredVersion === undefined
|
||||
? {}
|
||||
: { requiredVersion: p.requiredVersion }),
|
||||
...(p.singleton === undefined
|
||||
? {}
|
||||
: { singleton: p.singleton }),
|
||||
...(p.import === undefined ? {} : { import: p.import }),
|
||||
eager: false,
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
shared: options.moduleFederationRemote.sharedDependencies,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { prepareRuntimeSharedDependenciesScript } from './moduleFederation';
|
||||
import { BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL } from '@backstage/module-federation-common';
|
||||
|
||||
const GLOBAL = BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL;
|
||||
|
||||
describe('prepareRuntimeSharedDependenciesScript', () => {
|
||||
it('should generate script with a single dependency', () => {
|
||||
const result = prepareRuntimeSharedDependenciesScript({
|
||||
react: {
|
||||
version: '18.2.0',
|
||||
requiredVersion: '*',
|
||||
singleton: true,
|
||||
eager: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(`window['${GLOBAL}'] = {
|
||||
"items": [
|
||||
{
|
||||
"name": "react",
|
||||
"version": "18.2.0",
|
||||
"lib": () => import("react"),
|
||||
"shareConfig": {
|
||||
"singleton": true,
|
||||
"requiredVersion": "*",
|
||||
"eager": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "v1"
|
||||
};`);
|
||||
});
|
||||
|
||||
it('should generate script with multiple dependencies', () => {
|
||||
const result = prepareRuntimeSharedDependenciesScript({
|
||||
react: {
|
||||
version: '18.2.0',
|
||||
requiredVersion: '*',
|
||||
singleton: true,
|
||||
eager: true,
|
||||
},
|
||||
'react-dom': {
|
||||
version: '18.2.0',
|
||||
requiredVersion: '*',
|
||||
singleton: true,
|
||||
eager: true,
|
||||
},
|
||||
lodash: {
|
||||
version: '4.17.21',
|
||||
requiredVersion: '*',
|
||||
singleton: true,
|
||||
eager: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(`window['${GLOBAL}'] = {
|
||||
"items": [
|
||||
{
|
||||
"name": "react",
|
||||
"version": "18.2.0",
|
||||
"lib": () => import("react"),
|
||||
"shareConfig": {
|
||||
"singleton": true,
|
||||
"requiredVersion": "*",
|
||||
"eager": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "react-dom",
|
||||
"version": "18.2.0",
|
||||
"lib": () => import("react-dom"),
|
||||
"shareConfig": {
|
||||
"singleton": true,
|
||||
"requiredVersion": "*",
|
||||
"eager": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lodash",
|
||||
"version": "4.17.21",
|
||||
"lib": () => import("lodash"),
|
||||
"shareConfig": {
|
||||
"singleton": true,
|
||||
"requiredVersion": "*",
|
||||
"eager": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "v1"
|
||||
};`);
|
||||
});
|
||||
|
||||
it('should handle custom requiredVersion', () => {
|
||||
const result = prepareRuntimeSharedDependenciesScript({
|
||||
react: {
|
||||
version: '18.2.0',
|
||||
requiredVersion: '^18.0.0',
|
||||
singleton: true,
|
||||
eager: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain('"requiredVersion": "^18.0.0"');
|
||||
});
|
||||
|
||||
it('should handle scoped package names', () => {
|
||||
const result = prepareRuntimeSharedDependenciesScript({
|
||||
'@backstage/core-plugin-api': {
|
||||
version: '1.0.0',
|
||||
requiredVersion: '*',
|
||||
singleton: true,
|
||||
eager: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain('"name": "@backstage/core-plugin-api"');
|
||||
expect(result).toContain(
|
||||
'"lib": () => import("@backstage/core-plugin-api")',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty dependencies', () => {
|
||||
const result = prepareRuntimeSharedDependenciesScript({});
|
||||
|
||||
expect(result).toBe(`window['${GLOBAL}'] = {
|
||||
"items": [],
|
||||
"version": "v1"
|
||||
};`);
|
||||
});
|
||||
|
||||
it('should throw if version is missing', () => {
|
||||
expect(() =>
|
||||
prepareRuntimeSharedDependenciesScript({
|
||||
react: {
|
||||
requiredVersion: '*',
|
||||
singleton: true,
|
||||
eager: false,
|
||||
},
|
||||
}),
|
||||
).toThrow("Version is required for shared dependency 'react'");
|
||||
});
|
||||
});
|
||||
@@ -22,11 +22,11 @@ import {
|
||||
getEntryPointDefaultFeatureType,
|
||||
} from '../../../../lib/typeDistProject';
|
||||
import {
|
||||
SharedDependencies,
|
||||
Host,
|
||||
prepareRuntimeSharedDependenciesScript,
|
||||
BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL,
|
||||
defaultRemoteSharedDependencies,
|
||||
defaultHostSharedDependencies,
|
||||
HostSharedDependencies,
|
||||
RuntimeSharedDependenciesGlobal,
|
||||
} from '@backstage/module-federation-common';
|
||||
import { dirname, join as joinPath, resolve as resolvePath } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
@@ -82,6 +82,44 @@ export async function getModuleFederationRemoteOptions(
|
||||
|
||||
// Module federation host management utilities
|
||||
|
||||
/**
|
||||
* Prepares the runtime shared dependencies script for the module federation host,
|
||||
* which will be written by the CLI into a Javascript file added as an additional entry point for the frontend bundler.
|
||||
* This script is used in the browser to build the list of shared dependencies provided to the module federation runtime.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function prepareRuntimeSharedDependenciesScript(
|
||||
hostSharedDependencies: HostSharedDependencies,
|
||||
) {
|
||||
const items = Object.entries(hostSharedDependencies).map(
|
||||
([name, sharedDep]) => {
|
||||
if (!sharedDep.version) {
|
||||
throw new Error(`Version is required for shared dependency '${name}'`);
|
||||
}
|
||||
return {
|
||||
name,
|
||||
version: sharedDep.version,
|
||||
lib: name as unknown as () => Promise<unknown>, // Coverted into import below
|
||||
shareConfig: {
|
||||
singleton: sharedDep.singleton,
|
||||
requiredVersion: sharedDep.requiredVersion,
|
||||
eager: sharedDep.eager,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return `window['${BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL}'] = ${JSON.stringify(
|
||||
{ items, version: 'v1' } satisfies RuntimeSharedDependenciesGlobal,
|
||||
null,
|
||||
2,
|
||||
).replace(
|
||||
/"lib": ("[^"]+")/gm,
|
||||
(_, name) => `"lib": () => import(${name})`,
|
||||
)};`;
|
||||
}
|
||||
|
||||
const RUNTIME_SHARED_DEPENDENCIES_MODULE_NAME =
|
||||
'__backstage-module-federation-runtime-shared-dependencies__';
|
||||
|
||||
@@ -90,7 +128,7 @@ const writeQueue = new PQueue({ concurrency: 1 });
|
||||
|
||||
async function writeRuntimeSharedDependenciesModule(
|
||||
targetPath: string,
|
||||
runtimeSharedDependencies: SharedDependencies<Host & { version: string }>,
|
||||
runtimeSharedDependencies: HostSharedDependencies,
|
||||
) {
|
||||
const script = prepareRuntimeSharedDependenciesScript(
|
||||
runtimeSharedDependencies,
|
||||
@@ -110,41 +148,31 @@ async function writeRuntimeSharedDependenciesModule(
|
||||
|
||||
function resolveSharedDependencyVersions(
|
||||
targetPath: string,
|
||||
hostSharedDependencies: SharedDependencies<Host>,
|
||||
): SharedDependencies<Host & { version: string }> {
|
||||
hostSharedDependencies: HostSharedDependencies,
|
||||
): HostSharedDependencies {
|
||||
return Object.fromEntries(
|
||||
Object.entries(hostSharedDependencies)
|
||||
.filter(([_, sharedDep]) => sharedDep !== undefined)
|
||||
.map(([name, sharedDep]) => {
|
||||
// Use require.resolve to find the package
|
||||
// For scoped modules, keep the scope and the module name, but remove any sub-folder
|
||||
const nameParts = name.split('/');
|
||||
const moduleName =
|
||||
nameParts[0].startsWith('@') && nameParts.length > 1
|
||||
? `${nameParts[0]}/${nameParts[1]}`
|
||||
: nameParts[0];
|
||||
let packagePath: string;
|
||||
.flatMap(([importPath, sharedDep]) => {
|
||||
// Remove any sub-path exports from the import path
|
||||
const moduleName = importPath.startsWith('@')
|
||||
? importPath.split('/').slice(0, 2).join('/')
|
||||
: importPath.split('/')[0];
|
||||
|
||||
let version: string;
|
||||
try {
|
||||
packagePath = require.resolve(`${moduleName}/package.json`, {
|
||||
const packagePath = require.resolve(`${moduleName}/package.json`, {
|
||||
paths: [targetPath],
|
||||
});
|
||||
version = require(packagePath).version;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to resolve package.json for module federation shared dependency '${name}': ${e}`,
|
||||
);
|
||||
}
|
||||
const packageJson = require(packagePath);
|
||||
|
||||
if (sharedDep.version && packageJson.version !== sharedDep.version) {
|
||||
throw new Error(
|
||||
`Version mismatch for module federation shared dependency '${name}': '${sharedDep.version}' vs '${packageJson.version}' found in '${packagePath}'.`,
|
||||
console.log(
|
||||
`Skipping module federation shared dependency '${importPath}' because it could not be resolved.`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
name,
|
||||
{ ...sharedDep, version: sharedDep.version ?? packageJson.version },
|
||||
];
|
||||
return [[importPath, { ...sharedDep, version }]];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
import { AppConfig, Config } from '@backstage/config';
|
||||
import { BundlingPathsOptions } from './paths';
|
||||
import { ConfigSchema } from '@backstage/config-loader';
|
||||
import {
|
||||
SharedDependencies,
|
||||
Remote,
|
||||
} from '@backstage/module-federation-common';
|
||||
import { RemoteSharedDependencies } from '@backstage/module-federation-common';
|
||||
|
||||
export type ModuleFederationRemoteOptions = {
|
||||
// Unique name for this module federation bundle
|
||||
@@ -31,7 +28,7 @@ export type ModuleFederationRemoteOptions = {
|
||||
*/
|
||||
[k: string]: string;
|
||||
};
|
||||
sharedDependencies: SharedDependencies<Remote>;
|
||||
sharedDependencies: RemoteSharedDependencies;
|
||||
};
|
||||
|
||||
export type BundlingOptions = {
|
||||
|
||||
Reference in New Issue
Block a user