Merge pull request #30967 from davidfestal/runtime-module-federation-enablement

Runtime module federation enablement
This commit is contained in:
Patrik Oldsberg
2026-02-13 01:13:25 +01:00
committed by GitHub
37 changed files with 2163 additions and 571 deletions
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/backend-dynamic-feature-service': patch
'@backstage/frontend-dynamic-feature-loader': patch
'@backstage/module-federation-common': patch
'@backstage/cli': patch
---
Enable Module Federation support in the frontend application (Module Federation host) through API only, without using the ModuleFederationPlugin at build time, nor producing specific generated bundled assets.
Module federation remotes still use ModuleFederationPlugin at build time to provide module-federation enabled remote modules, like plugin bundles or dynamic frontend plugins.
Default shared dependencies are provided for both the frontend application (Module Federation host), and Module Federation remotes, maintaining consistency between both sides.
@@ -0,0 +1,172 @@
---
id: module-federation
title: Module Federation
sidebar_label: Module Federation
description: Using Module Federation in Backstage
---
## Introduction
Module Federation is a feature that enables sharing code and dependencies between separately built JavaScript applications at runtime. In Backstage, module federation support allows you to:
- Build your frontend application as a **module federation host** that can load remote modules at runtime
- Package individual plugins or bundles of several plugins as **module federation remotes** that can be loaded dynamically
- Share dependencies efficiently between the host and remotes to avoid code duplication
This guide explains how to configure and build both module federation hosts and remotes in Backstage, and how to initialize module federation at runtime using the standard Module Federation Runtime API.
## Overview
### Module Federation Host vs Remotes
In module federation terminology:
- **Host**: The main frontend application that loads and consumes remote modules. In Backstage, this is your app package (typically `packages/app`).
- **Remote**: A separately built module that can be loaded by the host at runtime. In Backstage, these are typically plugin packages built as module federation remotes.
### Shared Dependencies
A critical aspect of module federation is **shared dependencies**. When a host loads remote modules, both need to share common dependencies (like React, React Router, Material-UI) in order to ensure singleton dependencies only have one instance.
Backstage provides a list of default shared dependencies for common packages like React, React Router, and Material-UI. At build-time the `version` field is automatically resolved from your `package.json` files.
## Building the Module Federation Host
The module federation host is your main frontend application. By default, Backstage frontend applications include a default list of module federation shared dependencies.
When building and bundling the frontend application, the CLI automatically:
1. Resolves versions of the shared dependencies based on the monorepo dependencies
2. Adds an additional entrypoint to the frontend application bundle with the list of resolved runtime shared dependencies
## Building Module Federation Remotes
Plugin packages can be built as module federation remotes, allowing them to be loaded dynamically by a host application.
### Using the CLI
To build a plugin as a module federation remote, use the `--module-federation` option with the `package build` command:
```bash
cd plugins/my-plugin
yarn build --module-federation
```
### Build Output
When building a plugin as a module federation remote, the CLI:
1. Resolves versions of the shared dependencies based on the monorepo dependencies (done automatically by the Rspack/Webpack module federation plugin)
2. Produces the bundle assets in the `dist` folder, including:
- a `mf-manifest.json` file which contains the module federation manifest
- a `remoteEntry.js` file which is the main entrypoint for the remote module
## Runtime Usage
To use module federation in your Backstage app, you need to initialize the Module Federation Runtime with the shared dependencies configuration.
### Basic Usage
Here's how to initialize module federation in your app, and load remote modules:
```typescript title="packages/app/src/moduleFederation.ts"
import {
createInstance,
ModuleFederation,
} from '@module-federation/enhanced/runtime';
import { buildRuntimeSharedUserOption } from '@backstage/module-federation-common';
export async function initializeModuleFederation(): Promise<ModuleFederation> {
// Build the shared dependencies configuration
const { shared, errors } = await buildRuntimeSharedUserOption();
// Log any errors loading shared dependencies
if (errors.length > 0) {
for (const err of errors) {
console.error(err.message, err.cause);
}
}
// Initialize Module Federation Runtime
return createInstance({
name: 'app-next',
remotes: [
{
name: 'my_plugin',
entry: 'http://localhost:3001/mf-manifest.json',
},
],
shared,
});
}
export async function loadRemote(
instance: ModuleFederation,
name: string,
): Promise<any> {
return await instance.loadRemote<any>(name);
}
```
### Integration with Feature Loaders
Standard Module Federation runtime API integrates very well with frontend feature loaders,
as shown in the example below:
```typescript title="packages/app/src/App.tsx"
import { createInstance } from '@module-federation/enhanced/runtime';
import { buildRuntimeSharedUserOption } from '@backstage/module-federation-common';
import { createFrontendFeatureLoader } from '@backstage/frontend-plugin-api';
const moduleFederationInstance = createInstance({
name: 'app-next',
remotes: [],
});
const moduleFederationLoader = createFrontendFeatureLoader({
async loader() {
moduleFederationInstance.registerShared(
(await buildRuntimeSharedUserOption()).shared,
);
moduleFederationInstance.registerRemotes([
{
name: 'myFirstRemoteWith2ExposedModules',
entry:
'https://someCDN.org/myFirstRemoteWith2ExposedModules/mf-manifest.json',
},
{
name: 'mySecondRemote',
entry: 'https://someCDN.org/mySecondRemote/mf-manifest.json',
},
]);
const myFirstRemoteModule1 = await moduleFederationInstance.loadRemote<any>(
'myFirstRemoteWith2ExposedModules/module1',
);
const myFirstRemoteModule2 = await moduleFederationInstance.loadRemote<any>(
'myFirstRemoteWith2ExposedModules/module2',
);
const mySecondRemoteModule = await moduleFederationInstance.loadRemote<any>(
'mySecondRemote',
);
return [
myFirstRemoteModule1.default,
myFirstRemoteModule2.default,
mySecondRemoteModule.default,
];
},
});
const app = createApp({
features: [moduleFederationLoader],
});
export default app.createRoot();
```
Note that, on top of the standard API, we plan to provide a more simplified way to configure module federation remotes.
Additionally, the [`dynamicFrontendFeaturesLoader`](https://github.com/backstage/backstage/blob/master/packages/frontend-dynamic-feature-loader/src/loader.ts) provided in the [`@backstage/frontend-dynamic-feature-loader`](https://github.com/backstage/backstage/blob/master/packages/frontend-dynamic-feature-loader/README.md) package, which provides an integrated solution to load module federation remotes as dynamic frontend plugins, is a more complete example of a feature loader based on the module federation support.
## Default Shared Dependencies
Default shared dependencies are the same for both the host and remotes, and the list can be found in the [`@backstage/module-federation-common`](https://github.com/backstage/backstage/blob/master/packages/module-federation-common/src/defaults.ts) package.
+6
View File
@@ -270,6 +270,12 @@ that are exported from the package, leaving a much cleaner type definition file
and making sure that the type definitions are in sync with the generated
JavaScript.
### Building Module Federation Remotes
Frontend plugin packages can be built as module federation remotes, which allows them to be loaded dynamically at runtime by a module federation host (typically your main frontend app). To build a package as a module federation remote, use the `--module-federation` option with the `package build` command.
More details are given in the [Module Federation](../../frontend-system/building-apps/07-module-federation.md#building-module-federation-remotes) documentation.
## Bundling
The goal of the bundling process is to combine multiple packages together into a
+1
View File
@@ -200,6 +200,7 @@ Options:
--skip-build-dependencies Skip the automatic building of local dependencies. Applies to backend packages only.
--stats If bundle stats are available, write them to the output directory. Applies to app packages only.
--config <path> Config files to load instead of app-config.yaml. Applies to app packages only. (default: [])
--module-federation Build a package as a module federation remote. Applies to frontend plugin packages only.
```
## package lint
+1
View File
@@ -586,6 +586,7 @@ export default {
'frontend-system/building-apps/configuring-extensions',
'frontend-system/building-apps/built-in-extensions',
'frontend-system/building-apps/plugin-conversion',
'frontend-system/building-apps/module-federation',
'frontend-system/building-apps/migrating',
],
),
@@ -68,7 +68,7 @@
"@backstage/plugin-search-common": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@module-federation/sdk": "^0.9.0",
"@module-federation/sdk": "^0.21.6",
"chokidar": "^3.5.3",
"express": "^4.22.0",
"express-promise-router": "^4.1.0",
+1
View File
@@ -260,6 +260,7 @@ Usage: program [options]
Options:
--config <path>
--minify
--module-federation
--role <name>
--skip-build-dependencies
--stats
+3 -2
View File
@@ -55,10 +55,11 @@
"@backstage/errors": "workspace:^",
"@backstage/eslint-plugin": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/module-federation-common": "workspace:^",
"@backstage/release-manifests": "workspace:^",
"@backstage/types": "workspace:^",
"@manypkg/get-packages": "^1.1.3",
"@module-federation/enhanced": "^0.9.0",
"@module-federation/enhanced": "^0.21.6",
"@octokit/request": "^8.0.0",
"@rollup/plugin-commonjs": "^26.0.0",
"@rollup/plugin-json": "^6.0.0",
@@ -197,7 +198,7 @@
},
"peerDependencies": {
"@jest/environment-jsdom-abstract": "^30.0.0",
"@module-federation/enhanced": "^0.9.0",
"@module-federation/enhanced": "^0.21.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
"esbuild-loader": "^4.0.0",
"eslint-webpack-plugin": "^4.2.0",
@@ -60,18 +60,26 @@ export async function command(opts: OptionValues): Promise<void> {
});
}
// experimental
let isModuleFederationRemote: boolean | undefined = undefined;
if ((role as string) === 'frontend-dynamic-container') {
console.log(
chalk.yellow(
`⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`,
),
);
isModuleFederationRemote = true;
}
if (opts.moduleFederation) {
isModuleFederationRemote = true;
}
if (isModuleFederationRemote) {
console.log('Building package as a module federation remote');
return buildFrontend({
targetDir: paths.targetDir,
configPaths: [],
writeStats: Boolean(opts.stats),
isModuleFederationRemote: true,
isModuleFederationRemote,
webpack,
});
}
@@ -17,7 +17,7 @@
import { readJson } from 'fs-extra';
import { resolve as resolvePath } from 'node:path';
import {
getModuleFederationOptions,
getModuleFederationRemoteOptions,
serveBundle,
} from '../../../../build/lib/bundler';
import { paths } from '../../../../../lib/paths';
@@ -55,11 +55,12 @@ export async function startFrontend(options: StartAppOptions) {
verifyVersions: options.verifyVersions,
skipOpenBrowser: options.skipOpenBrowser,
linkedWorkspace: options.linkedWorkspace,
moduleFederation: await getModuleFederationOptions(
packageJson,
resolvePath(paths.targetDir),
options.isModuleFederationRemote,
),
moduleFederationRemote: options.isModuleFederationRemote
? await getModuleFederationRemoteOptions(
packageJson,
resolvePath(paths.targetDir),
)
: undefined,
});
await waitForExit();
+8
View File
@@ -42,6 +42,10 @@ export function registerPackageCommands(command: Command) {
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
Array<string>(),
)
.option(
'--module-federation',
'Build a package as a module federation remote. Applies to frontend plugin packages only.',
)
.action(lazy(() => import('./commands/package/build'), 'command'));
}
@@ -77,6 +81,10 @@ export const buildPlugin = createCliPlugin({
(opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]),
Array<string>(),
)
.option(
'--module-federation',
'Build a package as a module federation remote. Applies to frontend plugin packages only.',
)
.action(lazy(() => import('./commands/package/build'), 'command'));
await defaultCommand.parseAsync(args, { from: 'user' });
},
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'node:path';
import { buildBundle, getModuleFederationOptions } from './bundler';
import { buildBundle, getModuleFederationRemoteOptions } from './bundler';
import { getEnvironmentParallelism } from '../../../lib/parallel';
import { loadCliConfig } from '../../config/lib/config';
import { BackstagePackageJson } from '@backstage/cli-node';
@@ -25,7 +25,7 @@ interface BuildAppOptions {
targetDir: string;
writeStats: boolean;
configPaths: string[];
isModuleFederationRemote?: true;
isModuleFederationRemote?: boolean;
webpack?: typeof import('webpack');
}
@@ -39,11 +39,12 @@ export async function buildFrontend(options: BuildAppOptions) {
entry: 'src/index',
parallelism: getEnvironmentParallelism(),
statsJsonEnabled: writeStats,
moduleFederation: await getModuleFederationOptions(
packageJson,
resolvePath(targetDir),
options.isModuleFederationRemote,
),
moduleFederationRemote: options.isModuleFederationRemote
? await getModuleFederationRemoteOptions(
packageJson,
resolvePath(targetDir),
)
: undefined,
...(await loadCliConfig({
args: configPaths,
fromPackage: packageJson.name,
@@ -28,6 +28,7 @@ import { BuildOptions } from './types';
import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
import chalk from 'chalk';
import { createDetectedModulesEntryPoint } from './packageDetection';
import { createRuntimeSharedDependenciesEntryPoint } from './moduleFederation';
// TODO(Rugvip): Limits from CRA, we might want to tweak these though.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
@@ -55,8 +56,7 @@ export async function buildBundle(options: BuildOptions) {
};
const configs: Configuration[] = [];
if (options.moduleFederation?.mode === 'remote') {
if (options.moduleFederationRemote) {
// Package detection is disabled for remote bundles
configs.push(await createConfig(paths, commonConfigOptions));
} else {
@@ -65,10 +65,18 @@ export async function buildBundle(options: BuildOptions) {
targetPath: paths.targetPath,
});
const moduleFederationSharedDependenciesEntryPoint =
await createRuntimeSharedDependenciesEntryPoint({
targetPath: paths.targetPath,
});
configs.push(
await createConfig(paths, {
...commonConfigOptions,
additionalEntryPoints: detectedModulesEntryPoint,
additionalEntryPoints: [
...detectedModulesEntryPoint,
...moduleFederationSharedDependenciesEntryPoint,
],
appMode: publicPaths ? 'protected' : 'public',
}),
);
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { BundlingOptions, ModuleFederationOptions } from './types';
import { resolve as resolvePath } from 'node:path';
import { BundlingOptions, ModuleFederationRemoteOptions } from './types';
import { rspack, Configuration } from '@rspack/core';
import { BundlingPaths } from './paths';
@@ -39,14 +39,13 @@ import { ConfigInjectingHtmlWebpackPlugin } from './ConfigInjectingHtmlWebpackPl
export function resolveBaseUrl(
config: Config,
moduleFederation?: ModuleFederationOptions,
moduleFederationRemote?: ModuleFederationRemoteOptions,
): URL {
const baseUrl = config.getOptionalString('app.baseUrl');
const defaultBaseUrl =
moduleFederation?.mode === 'remote'
? `http://localhost:${process.env.PORT ?? '3000'}`
: 'http://localhost:3000';
const defaultBaseUrl = moduleFederationRemote
? `http://localhost:${process.env.PORT ?? '3000'}`
: 'http://localhost:3000';
try {
return new URL(baseUrl ?? '/', defaultBaseUrl);
@@ -57,12 +56,12 @@ export function resolveBaseUrl(
export function resolveEndpoint(
config: Config,
moduleFederation?: ModuleFederationOptions,
moduleFederationRemote?: ModuleFederationRemoteOptions,
): {
host: string;
port: number;
} {
const url = resolveBaseUrl(config, moduleFederation);
const url = resolveBaseUrl(config, moduleFederationRemote);
return {
host: config.getOptionalString('app.listen.host') ?? url.hostname,
@@ -117,7 +116,7 @@ export async function createConfig(
checksEnabled,
isDev,
frontendConfig,
moduleFederation,
moduleFederationRemote,
publicSubPath = '',
webpack,
} = options;
@@ -126,7 +125,7 @@ export async function createConfig(
// Any package that is part of the monorepo but outside the monorepo root dir need
// separate resolution logic.
const validBaseUrl = resolveBaseUrl(frontendConfig, moduleFederation);
const validBaseUrl = resolveBaseUrl(frontendConfig, moduleFederationRemote);
let publicPath = validBaseUrl.pathname.replace(/\/$/, '');
if (publicSubPath) {
publicPath = `${publicPath}${publicSubPath}`.replace('//', '/');
@@ -135,7 +134,7 @@ export async function createConfig(
if (isDev) {
const { host, port } = resolveEndpoint(
options.frontendConfig,
options.moduleFederation,
options.moduleFederationRemote,
);
const refreshOptions = {
@@ -186,7 +185,7 @@ export async function createConfig(
}),
);
if (options.moduleFederation?.mode !== 'remote') {
if (!options.moduleFederationRemote) {
const templateOptions = {
meta: {
'backstage-app-mode': options?.appMode ?? 'public',
@@ -231,20 +230,17 @@ export async function createConfig(
);
}
if (options.moduleFederation) {
const isRemote = options.moduleFederation?.mode === 'remote';
if (options.moduleFederationRemote) {
const AdaptedModuleFederationPlugin = webpack
? (require('@module-federation/enhanced/webpack')
.ModuleFederationPlugin as unknown as typeof ModuleFederationPlugin)
: ModuleFederationPlugin;
const exposes = options.moduleFederation?.exposes
const exposes = options.moduleFederationRemote.exposes
? Object.fromEntries(
Object.entries(options.moduleFederation?.exposes).map(([k, v]) => [
k,
resolvePath(paths.targetPath, v),
]),
Object.entries(options.moduleFederationRemote?.exposes).map(
([k, v]) => [k, resolvePath(paths.targetPath, v)],
),
)
: {
'.': paths.targetEntry,
@@ -252,66 +248,28 @@ export async function createConfig(
plugins.push(
new AdaptedModuleFederationPlugin({
...(isRemote && {
filename: 'remoteEntry.js',
exposes,
}),
name: options.moduleFederation.name,
filename: 'remoteEntry.js',
exposes,
name: options.moduleFederationRemote.name,
runtime: false,
shared: {
// React
react: {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
...(isRemote && { import: false }),
},
'react-dom': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
...(isRemote && { import: false }),
},
// React Router
'react-router': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
...(isRemote && { import: false }),
},
'react-router-dom': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
...(isRemote && { import: false }),
},
// MUI v4
// not setting import: false for MUI packages as this
// will break once Backstage moves to BUI
'@material-ui/core/styles': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
},
'@material-ui/styles': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
},
// MUI v5
// not setting import: false for MUI packages as this
// will break once Backstage moves to BUI
'@mui/material/styles/': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
},
'@emotion/react': {
singleton: true,
requiredVersion: '*',
eager: !isRemote,
},
},
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,
},
],
),
),
}),
);
}
@@ -426,10 +384,9 @@ export async function createConfig(
rules: loaders,
},
output: {
uniqueName: options.moduleFederation?.name,
uniqueName: options.moduleFederationRemote?.name,
path: paths.targetDist,
publicPath:
options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`,
publicPath: options.moduleFederationRemote ? 'auto' : `${publicPath}/`,
filename: isDev ? '[name].js' : 'static/[name].[contenthash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
@@ -15,5 +15,5 @@
*/
export { buildBundle } from './bundle';
export { getModuleFederationOptions } from './moduleFederation';
export { getModuleFederationRemoteOptions } from './moduleFederation';
export { serveBundle } from './server';
@@ -14,36 +14,34 @@
* limitations under the License.
*/
import chalk from 'chalk';
import { ModuleFederationOptions } from './types';
import { ModuleFederationRemoteOptions } from './types';
import { BackstagePackageJson } from '@backstage/cli-node';
import { readEntryPoints } from '../../../../lib/entryPoints';
import {
createTypeDistProject,
getEntryPointDefaultFeatureType,
} from '../../../../lib/typeDistProject';
import {
SharedDependencies,
Host,
prepareRuntimeSharedDependenciesScript,
defaultRemoteSharedDependencies,
defaultHostSharedDependencies,
} from '@backstage/module-federation-common';
import { dirname, join as joinPath, resolve as resolvePath } from 'path';
import fs from 'fs-extra';
import chokidar from 'chokidar';
import PQueue from 'p-queue';
export async function getModuleFederationOptions(
// Remote modules management utilities
export async function getModuleFederationRemoteOptions(
packageJson: BackstagePackageJson,
packageDir: string,
isModuleFederationRemote?: boolean,
): Promise<ModuleFederationOptions | undefined> {
if (
!isModuleFederationRemote &&
!process.env.EXPERIMENTAL_MODULE_FEDERATION
) {
return undefined;
}
console.log(
chalk.yellow(
`⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`,
),
);
let exposes: ModuleFederationOptions['exposes'];
): Promise<ModuleFederationRemoteOptions | undefined> {
let exposes: ModuleFederationRemoteOptions['exposes'];
const packageRole = packageJson.backstage?.role;
if (isModuleFederationRemote && packageJson.exports && packageRole) {
if (packageJson.exports && packageRole) {
const project = await createTypeDistProject();
exposes = Object.fromEntries(
readEntryPoints(packageJson)
@@ -70,7 +68,6 @@ export async function getModuleFederationOptions(
}
return {
mode: isModuleFederationRemote ? 'remote' : 'host',
// The default output mode requires the name to be a usable as a code
// symbol, there might be better options here but for now we need to
// sanitize the name.
@@ -79,5 +76,101 @@ export async function getModuleFederationOptions(
.replaceAll('/', '__')
.replaceAll('-', '_'),
exposes,
sharedDependencies: defaultRemoteSharedDependencies(),
};
}
// Module federation host management utilities
const RUNTIME_SHARED_DEPENDENCIES_MODULE_NAME =
'__backstage-module-federation-runtime-shared-dependencies__';
// Make sure we're not issuing multiple writes at the same time, which can cause partial overwrites
const writeQueue = new PQueue({ concurrency: 1 });
async function writeRuntimeSharedDependenciesModule(
targetPath: string,
runtimeSharedDependencies: SharedDependencies<Host & { version: string }>,
) {
const script = prepareRuntimeSharedDependenciesScript(
runtimeSharedDependencies,
);
await writeQueue.add(async () => {
const path = joinPath(
targetPath,
'node_modules',
`${RUNTIME_SHARED_DEPENDENCIES_MODULE_NAME}.js`,
);
await fs.ensureDir(dirname(path));
await fs.writeFile(path, script);
});
}
function resolveSharedDependencyVersions(
targetPath: string,
hostSharedDependencies: SharedDependencies<Host>,
): SharedDependencies<Host & { version: string }> {
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;
try {
packagePath = require.resolve(`${moduleName}/package.json`, {
paths: [targetPath],
});
} 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}'.`,
);
}
return [
name,
{ ...sharedDep, version: sharedDep.version ?? packageJson.version },
];
}),
);
}
export async function createRuntimeSharedDependenciesEntryPoint(options: {
targetPath: string;
watch?: () => void;
}): Promise<string[]> {
const { targetPath, watch } = options;
const doWriteSharedDependenciesModule = async () => {
const sharedDependencies = defaultHostSharedDependencies();
await writeRuntimeSharedDependenciesModule(
targetPath,
resolveSharedDependencyVersions(targetPath, sharedDependencies),
);
};
if (watch) {
const watcher = chokidar.watch(resolvePath(targetPath, 'package.json'));
watcher.on('change', async () => {
await doWriteSharedDependenciesModule();
watch();
});
}
await doWriteSharedDependenciesModule();
return [RUNTIME_SHARED_DEPENDENCIES_MODULE_NAME];
}
@@ -28,6 +28,7 @@ import { createConfig, resolveBaseUrl, resolveEndpoint } from './config';
import { createDetectedModulesEntryPoint } from './packageDetection';
import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths';
import { ServeOptions } from './types';
import { createRuntimeSharedDependenciesEntryPoint } from './moduleFederation';
export async function serveBundle(options: ServeOptions) {
const paths = resolveBundlingPaths(options);
@@ -108,10 +109,10 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
}
const { frontendConfig, fullConfig } = cliConfig;
const url = resolveBaseUrl(frontendConfig, options.moduleFederation);
const url = resolveBaseUrl(frontendConfig, options.moduleFederationRemote);
const { host, port } = resolveEndpoint(
frontendConfig,
options.moduleFederation,
options.moduleFederationRemote,
);
const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({
@@ -122,6 +123,14 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
},
});
const moduleFederationSharedDependenciesEntryPoint =
await createRuntimeSharedDependenciesEntryPoint({
targetPath: paths.targetPath,
watch() {
triggerReload();
},
});
const webpack = process.env.LEGACY_WEBPACK_BUILD
? (require('webpack') as typeof import('webpack'))
: undefined;
@@ -140,8 +149,11 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
const config = await createConfig(paths, {
...commonConfigOptions,
additionalEntryPoints: detectedModulesEntryPoint,
moduleFederation: options.moduleFederation,
additionalEntryPoints: [
...detectedModulesEntryPoint,
...moduleFederationSharedDependenciesEntryPoint,
],
moduleFederationRemote: options.moduleFederationRemote,
});
const bundler = (webpack ?? rspack) as typeof rspack;
@@ -181,17 +193,16 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
directory: paths.targetPublic,
}
: undefined,
historyApiFallback:
options.moduleFederation?.mode === 'remote'
? false
: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
historyApiFallback: options.moduleFederationRemote
? false
: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
// The index needs to be rewritten relative to the new public path, including subroutes.
index: `${config.output?.publicPath}index.html`,
},
// The index needs to be rewritten relative to the new public path, including subroutes.
index: `${config.output?.publicPath}index.html`,
},
server:
url.protocol === 'https:'
? {
@@ -17,18 +17,21 @@
import { AppConfig, Config } from '@backstage/config';
import { BundlingPathsOptions } from './paths';
import { ConfigSchema } from '@backstage/config-loader';
import {
SharedDependencies,
Remote,
} from '@backstage/module-federation-common';
export type ModuleFederationOptions = {
export type ModuleFederationRemoteOptions = {
// Unique name for this module federation bundle
name: string;
// Whether this is a host or remote bundle
mode: 'host' | 'remote';
exposes?: {
/**
* Modules that should be exposed by this container.
*/
[k: string]: string;
};
sharedDependencies: SharedDependencies<Remote>;
};
export type BundlingOptions = {
@@ -44,7 +47,7 @@ export type BundlingOptions = {
appMode?: string;
// An external linked workspace to include in the bundling
linkedWorkspace?: string;
moduleFederation?: ModuleFederationOptions;
moduleFederationRemote?: ModuleFederationRemoteOptions;
webpack?: typeof import('webpack');
};
@@ -54,7 +57,7 @@ export type ServeOptions = BundlingPathsOptions & {
configPaths: string[];
verifyVersions?: boolean;
skipOpenBrowser?: boolean;
moduleFederation?: ModuleFederationOptions;
moduleFederationRemote?: ModuleFederationRemoteOptions;
// An external linked workspace to include in the bundling
linkedWorkspace?: string;
};
@@ -68,7 +71,7 @@ export type BuildOptions = BundlingPathsOptions & {
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
fullConfig: Config;
moduleFederation?: ModuleFederationOptions;
moduleFederationRemote?: ModuleFederationRemoteOptions;
webpack?: typeof import('webpack');
};
@@ -34,9 +34,10 @@
"dependencies": {
"@backstage/config": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@module-federation/enhanced": "^0.9.0",
"@module-federation/runtime": "^0.9.0",
"@module-federation/sdk": "^0.9.0",
"@backstage/module-federation-common": "workspace:^",
"@module-federation/enhanced": "^0.21.6",
"@module-federation/runtime": "^0.21.6",
"@module-federation/sdk": "^0.21.6",
"cross-fetch": "^4.0.0",
"uri-template": "^2.0.0"
},
@@ -3,8 +3,9 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api';
import { ModuleFederation } from '@module-federation/enhanced/runtime';
import { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import { ShareStrategy } from '@module-federation/runtime/types';
import { UserOptions } from '@module-federation/runtime/types';
@@ -18,7 +19,8 @@ export type DynamicFrontendFeaturesLoaderOptions = {
moduleFederation: {
shared?: UserOptions['shared'];
shareStrategy?: ShareStrategy;
plugins?: Array<FederationRuntimePlugin>;
plugins?: Array<ModuleFederationRuntimePlugin>;
instance?: ModuleFederation;
};
};
```
@@ -15,21 +15,45 @@
*/
import { mockApis, registerMswTestHooks } from '@backstage/test-utils';
import {
DynamicFrontendFeaturesLoaderOptions,
dynamicFrontendFeaturesLoader,
} from './loader';
import { dynamicFrontendFeaturesLoader } from './loader';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';
import { RemoteEntryExports } from '@module-federation/runtime/types';
import { Module } from '@module-federation/sdk';
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { InternalFrontendFeatureLoader } from '../../frontend-plugin-api/src/wiring/createFrontendFeatureLoader';
import { resetFederationGlobalInfo } from '@module-federation/runtime/core';
import { Config } from '@backstage/config';
const baseUrl = 'http://localhost:7007';
function mockDefaultConfig(): Config {
return mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
});
}
const globalSpy = jest.fn();
Object.defineProperty(
global,
'__backstage-module-federation-shared-dependencies__',
{
get: globalSpy,
},
);
describe('dynamicFrontendFeaturesLoader', () => {
const server = setupServer();
registerMswTestHooks(server);
@@ -46,31 +70,27 @@ describe('dynamicFrontendFeaturesLoader', () => {
},
};
const getCommonOptions = (): DynamicFrontendFeaturesLoaderOptions => ({
moduleFederation: {
const testModuleFederationPlugins: ModuleFederationRuntimePlugin[] = [
{
// We add this module federation plugin to mock the
// effective retrieval of the remote content, since it
// normally requires a host application built with module federation support,
// and won't work by default in Jest tests.
plugins: [
{
name: 'load-entry-mock',
loadEntry: async args => {
return {
get: (id: string) => async () => {
return await mocks.federation.get({
name: args.remoteInfo.name,
id,
});
},
init: async () => {},
} as RemoteEntryExports;
name: 'load-entry-mock',
loadEntry: async args => {
return {
get: (id: string) => async () => {
return await mocks.federation.get({
name: args.remoteInfo.name,
id,
});
},
onLoad: mocks.federation.onLoad,
},
],
init: async () => {},
} as RemoteEntryExports;
},
onLoad: mocks.federation.onLoad,
},
});
];
const manifestDummyData = {
metaData: {
@@ -104,9 +124,73 @@ describe('dynamicFrontendFeaturesLoader', () => {
mocks.console.debug.mockReset();
mocks.federation.get.mockReset();
mocks.federation.onLoad.mockReset();
globalSpy.mockReset();
resetFederationGlobalInfo();
});
const mockedDefaultSharedDependencies = {
react: {
version: '18.3.1',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'react-dom': {
version: '18.3.1',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'react-router': {
version: '6.28.2',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'react-router-dom': {
version: '6.28.2',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'@material-ui/core/styles': {
version: '4.12.4',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'@material-ui/styles': {
version: '4.11.5',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'@mui/material/styles/': {
version: '5.16.14',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
'@emotion/react': {
version: '11.13.5',
requiredVersion: '*',
singleton: true,
eager: true,
module: () => ({ default: {} }),
},
};
beforeEach(() => {
globalSpy.mockReturnValue(mockedDefaultSharedDependencies);
});
it('should return immediately if dynamic plugins are not enabled in config', async () => {
let manifestsEndpointCalled = false;
server.use(
@@ -121,7 +205,9 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
@@ -196,22 +282,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -320,22 +396,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -435,22 +501,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -526,22 +582,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -588,22 +634,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -635,22 +671,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -739,22 +765,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -867,22 +883,12 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
@@ -978,30 +984,21 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
"Failed loading remote module 'plugin_1' of dynamic plugin 'plugin-1': Error: [ Federation Runtime ]: [ Federation Runtime ]: [ Federation Runtime ]: http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json is not a federation manifest",
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(warnCalls).toEqual(['[ Federation Runtime ]']);
expect(errorCalls).toEqual([
"Failed loading remote module 'plugin_1' of dynamic plugin 'plugin-1': Error: [ Federation Runtime ]: http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json is not a federation manifest",
]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
@@ -1086,30 +1083,20 @@ describe('dynamicFrontendFeaturesLoader', () => {
const features = await (
dynamicFrontendFeaturesLoader({
...getCommonOptions(),
moduleFederation: {
plugins: testModuleFederationPlugins,
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockApis.config({
data: {
app: {
packages: {
include: [],
},
},
backend: {
baseUrl,
},
dynamicPlugins: {},
},
}),
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
"Failed loading remote module 'plugin_1' of dynamic plugin 'plugin-1': Error: [ Federation Runtime ]: [ Federation Runtime ]: [ Federation Runtime ]: http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json is not a federation manifest",
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
expect(warnCalls).toEqual(['[ Federation Runtime ]']);
expect(errorCalls).toEqual([
"Failed loading remote module 'plugin_1' of dynamic plugin 'plugin-1': Error: [ Federation Runtime ]: http://localhost:7007/.backstage/dynamic-features/remotes/plugin-1/mf-manifest.json is not a federation manifest",
]);
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
@@ -1133,4 +1120,187 @@ describe('dynamicFrontendFeaturesLoader', () => {
},
]);
});
it('should initialize module federation with resolved shared dependencies', async () => {
server.use(
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) => res(ctx.json([])),
),
);
const spyInit = jest.fn();
await (
dynamicFrontendFeaturesLoader({
moduleFederation: {
plugins: [
{
name: 'spy-init',
init: args => {
spyInit(args);
return args;
},
},
...testModuleFederationPlugins,
],
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockDefaultConfig(),
});
expect(spyInit).toHaveBeenCalledTimes(1);
expect(spyInit.mock.calls[0][0].options).toMatchObject({
shared: Object.fromEntries(
Object.entries(mockedDefaultSharedDependencies).map(
([name, shared]) => [
name,
[
{
version: shared.version,
shareConfig: {
singleton: shared.singleton,
requiredVersion: shared.requiredVersion,
eager: shared.eager,
},
strategy: 'version-first',
},
],
],
),
),
});
});
it('should initialize module federation with shareStrategy option', async () => {
server.use(
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) => res(ctx.json([])),
),
);
const spyInit = jest.fn();
await (
dynamicFrontendFeaturesLoader({
moduleFederation: {
shareStrategy: 'loaded-first',
plugins: [
{
name: 'spy-init',
init: args => {
spyInit(args);
return args;
},
},
...testModuleFederationPlugins,
],
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([]);
expect(spyInit).toHaveBeenCalledTimes(1);
expect(spyInit.mock.calls[0][0].options).toMatchObject({
shareStrategy: 'loaded-first',
shared: {
react: [
{
version: '18.3.1',
shareConfig: {
singleton: true,
requiredVersion: '*',
},
strategy: 'loaded-first',
},
],
},
});
});
it('should return an empty list of features if module federation initialization fails', async () => {
server.use(
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes`,
(_, res, ctx) =>
res(
ctx.json([
{
packageName: 'plugin-test-dynamic',
exposedModules: ['.'],
remoteInfo: {
name: 'test_plugin',
entry: `${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
},
},
]),
),
),
rest.get(
`${baseUrl}/.backstage/dynamic-features/remotes/plugin-test-dynamic/mf-manifest.json`,
(_, res, ctx) =>
res(
ctx.json({
name: 'test_plugin',
...manifestDummyData,
exposes: [
{
id: 'test_plugin:.',
name: '.',
path: '.',
...manifestExposedRemoteDummyData,
},
],
}),
),
),
);
mocks.federation.get.mockReturnValue({
default: createFrontendPlugin({
pluginId: 'test-plugin',
extensions: [],
}),
});
const features = await (
dynamicFrontendFeaturesLoader({
moduleFederation: {
plugins: [
...testModuleFederationPlugins,
{
name: 'fail-init',
init: () => {
throw new Error('An initialization error');
},
},
],
},
}) as InternalFrontendFeatureLoader
).loader({
config: mockDefaultConfig(),
});
const errorCalls = mocks.console.error.mock.calls.flatMap(e => e[0]);
expect(errorCalls).toEqual([
'Failed initializing module federation: Error: An initialization error',
]);
const warnCalls = mocks.console.warn.mock.calls.flatMap(e => e[0]);
expect(warnCalls).toEqual([]);
const infoCalls = mocks.console.info.mock.calls.flatMap(e => e[0]);
expect(infoCalls).toEqual([]);
const debugCalls = mocks.console.debug.mock.calls.flatMap(e => e[0]);
expect(debugCalls).toEqual([]);
expect(mocks.federation.get.mock.calls.flatMap(e => e[0])).toEqual([]);
expect(features).toEqual([]);
});
});
@@ -15,9 +15,9 @@
*/
import {
FederationRuntimePlugin,
init,
loadRemote,
ModuleFederationRuntimePlugin,
createInstance,
ModuleFederation,
} from '@module-federation/enhanced/runtime';
import { Module } from '@module-federation/sdk';
import { DefaultApiClient, Remote } from './schema/openapi';
@@ -27,6 +27,7 @@ import {
createFrontendFeatureLoader,
} from '@backstage/frontend-plugin-api';
import { ShareStrategy, UserOptions } from '@module-federation/runtime/types';
import { buildRuntimeSharedUserOption } from '@backstage/module-federation-common';
/**
*
@@ -39,7 +40,8 @@ export type DynamicFrontendFeaturesLoaderOptions = {
moduleFederation: {
shared?: UserOptions['shared'];
shareStrategy?: ShareStrategy;
plugins?: Array<FederationRuntimePlugin>;
plugins?: Array<ModuleFederationRuntimePlugin>;
instance?: ModuleFederation;
};
};
@@ -98,18 +100,50 @@ export function dynamicFrontendFeaturesLoader(
return [];
}
let instance: ModuleFederation;
try {
init({
...options?.moduleFederation,
name: appPackageName
.replaceAll('@', '')
.replaceAll('/', '__')
.replaceAll('-', '_'),
remotes: frontendPluginRemotes.map(remote => ({
alias: remote.packageName,
...remote.remoteInfo,
})),
});
if (options?.moduleFederation?.instance) {
instance = options.moduleFederation.instance;
} else {
const { shared, errors } = await buildRuntimeSharedUserOption();
for (const err of errors) {
error(err.message, err.cause);
}
const createOptions: UserOptions = {
name: appPackageName
.replaceAll('@', '')
.replaceAll('/', '__')
.replaceAll('-', '_'),
shared,
remotes: [],
};
if (options?.moduleFederation?.shareStrategy) {
createOptions.shareStrategy =
options.moduleFederation.shareStrategy;
}
instance = createInstance(createOptions);
}
const userOptions: UserOptions = {
name: instance.name,
remotes: [],
};
if (options?.moduleFederation?.plugins) {
userOptions.plugins = options.moduleFederation.plugins;
}
if (options?.moduleFederation?.shareStrategy) {
userOptions.shareStrategy = options.moduleFederation.shareStrategy;
}
if (options?.moduleFederation?.shared) {
userOptions.shared = options.moduleFederation.shared;
}
userOptions.remotes = frontendPluginRemotes.map(remote => ({
alias: remote.packageName,
...remote.remoteInfo,
}));
instance.initOptions(userOptions);
} catch (err) {
error(`Failed initializing module federation`, err);
return [];
@@ -131,7 +165,7 @@ export function dynamicFrontendFeaturesLoader(
: `${remote.remoteInfo.name}/${exposedModuleName}`;
let module: Module;
try {
module = await loadRemote<Module>(remoteModuleName);
module = await instance.loadRemote<Module>(remoteModuleName);
} catch (err) {
error(
`Failed loading remote module '${remoteModuleName}' of dynamic plugin '${remote.packageName}'`,
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,11 @@
# @backstage/module-federation-common
This package provides a helper library for module federation, enabling a consistent management of shared dependencies
in both the module federation host (frontend application) at runtime, and remote modules at build-time through the CLI.
It provides:
- TypeScript types for both host and remote shared dependency definitions,
- a default list of shared dependencies (React, React Router, Material-UI, etc.) for both the host and remote modules,
- utilities used by the CLI to resolve versions of the shared dependencies for the host at build-time,
- utilities available for the frontend application to provide the list of resolved shared dependencies at runtime.
@@ -0,0 +1,10 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-module-federation-common
title: '@backstage/module-federation-common'
description: Helper library for module federation
spec:
lifecycle: experimental
type: backstage-common-library
owner: framework-maintainers
@@ -0,0 +1,68 @@
{
"name": "@backstage/module-federation-common",
"version": "0.0.0",
"description": "Helper library for module federation",
"backstage": {
"role": "common-library"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"keywords": [
"backstage"
],
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/module-federation-common"
},
"license": "Apache-2.0",
"sideEffects": false,
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^",
"@module-federation/runtime": "^0.21.6"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@types/react": "^18.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router-dom": "^6.30.2"
},
"peerDependencies": {
"@emotion/react": "^11.10.5",
"@material-ui/core": "^4.12.2",
"@material-ui/styles": "^4.10.0",
"@mui/material": "^5.12.2",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-router": "^6.30.2",
"react-router-dom": "^6.30.2"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
}
@@ -0,0 +1,50 @@
## API Report File for "@backstage/module-federation-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ForwardedError } from '@backstage/errors';
import type { UserOptions } from '@module-federation/runtime/types';
// @public
export function buildRuntimeSharedUserOption(): Promise<{
shared: UserOptions['shared'];
errors: ForwardedError[];
}>;
// @public
export function defaultHostSharedDependencies(): SharedDependencies<Host>;
// @public
export function defaultRemoteSharedDependencies(): SharedDependencies<Remote>;
// @public
export type Host = {
eager?: boolean;
requiredVersion: false | string;
version?: string;
};
// @public
export function prepareRuntimeSharedDependenciesScript(
hostSharedDependencies: SharedDependencies<
Host & {
version: string;
}
>,
): string;
// @public
export type Remote = {
import?: false | string;
requiredVersion?: false | string;
version?: false | string;
};
// @public
export type SharedDependencies<ContextFields> = {
[name: string]: {
singleton?: boolean;
} & ContextFields;
};
```
@@ -0,0 +1,44 @@
/*
* 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 {
defaultHostSharedDependencies,
defaultRemoteSharedDependencies,
} from './defaults';
describe('defaultHostSharedDependencies', () => {
it('should return shared dependencies with the host-only eager property', () => {
const result = defaultHostSharedDependencies();
expect(result.react).toEqual({
requiredVersion: '*',
singleton: true,
eager: true,
});
});
});
describe('defaultRemoteSharedDependencies', () => {
it('should return shared dependencies with the remote-only import property', () => {
const result = defaultRemoteSharedDependencies();
expect(result.react).toEqual({
requiredVersion: '*',
singleton: true,
import: false,
});
});
});
@@ -0,0 +1,155 @@
/*
* 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 { Host, Remote, SharedDependencies } from './types';
/**
* The list of default shared dependencies, expected to be the same
* for both the module federation host and remotes.
*
* @internal
*/
const defaultSharedDependencies: SharedDependencies<{
host: Host;
remote: Remote;
}> = {
// React
react: {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
import: false,
requiredVersion: '*',
},
},
'react-dom': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
import: false,
requiredVersion: '*',
},
},
// React Router
'react-router': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
import: false,
requiredVersion: '*',
},
},
'react-router-dom': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
import: false,
requiredVersion: '*',
},
},
// MUI v4
// not setting import: false for MUI packages as this
// will break once Backstage moves to BUI
'@material-ui/core/styles': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
requiredVersion: '*',
},
},
'@material-ui/styles': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
requiredVersion: '*',
},
},
// MUI v5
// not setting import: false for MUI packages as this
// will break once Backstage moves to BUI
'@mui/material/styles/': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
requiredVersion: '*',
},
},
'@emotion/react': {
singleton: true,
host: {
eager: true,
requiredVersion: '*',
},
remote: {
requiredVersion: '*',
},
},
};
/**
* Returns the list of default shared dependencies for the host, with host-only properties.
*
* @public
*/
export function defaultHostSharedDependencies(): SharedDependencies<Host> {
return Object.fromEntries(
Object.entries(defaultSharedDependencies).map(([name, p]) => [
name,
{
singleton: p.singleton,
...p.host,
},
]),
);
}
/**
* Returns the list of default shared dependencies for the remote, with remote-only properties.
*
* @public
*/
export function defaultRemoteSharedDependencies(): SharedDependencies<Remote> {
return Object.fromEntries(
Object.entries(defaultSharedDependencies).map(([name, p]) => [
name,
{
singleton: p.singleton,
...p.remote,
},
]),
);
}
@@ -0,0 +1,33 @@
/*
* 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.
*/
/**
* Helper library for module federation, enabling a consistent management of shared dependencies
* in both the module federation host (frontend application) at runtime,
* and remote modules at build time through the CLI.
*
* @packageDocumentation
*/
export {
prepareRuntimeSharedDependenciesScript,
buildRuntimeSharedUserOption,
} from './runtime';
export {
defaultHostSharedDependencies,
defaultRemoteSharedDependencies,
} from './defaults';
export type { Host, Remote, SharedDependencies } from './types';
@@ -0,0 +1,523 @@
/*
* 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,
buildRuntimeSharedUserOption,
} from './runtime';
import { ForwardedError } from '@backstage/errors';
describe('prepareRuntimeSharedDependenciesScript', () => {
it('should generate script with minimal required properties', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: '*',
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": "*",
"module": () => import('react')
}
};`);
});
it('should handle multiple shared dependencies', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: '*',
},
'react-dom': {
version: '18.2.0',
requiredVersion: '*',
},
lodash: {
version: '4.17.21',
requiredVersion: '*',
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": "*",
"module": () => import('react')
},
"react-dom": {
"version": "18.2.0",
"requiredVersion": "*",
"module": () => import('react-dom')
},
"lodash": {
"version": "4.17.21",
"requiredVersion": "*",
"module": () => import('lodash')
}
};`);
});
it('should include requiredVersion when provided', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: '^18.0.0',
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": "^18.0.0",
"module": () => import('react')
}
};`);
});
it('should include requiredVersion when set to false', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: false as const,
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": false,
"module": () => import('react')
}
};`);
});
it('should include singleton when true', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: '*',
singleton: true,
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": "*",
"singleton": true,
"module": () => import('react')
}
};`);
});
it('should include singleton when false', () => {
const sharedDependencies = {
lodash: {
version: '4.17.21',
requiredVersion: '*',
singleton: false,
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"lodash": {
"version": "4.17.21",
"requiredVersion": "*",
"singleton": false,
"module": () => import('lodash')
}
};`);
});
it('should include eager when true', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: '*',
eager: true,
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": "*",
"eager": true,
"module": () => import('react')
}
};`);
});
it('should include eager when false', () => {
const sharedDependencies = {
lodash: {
version: '4.17.21',
requiredVersion: '*',
eager: false,
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"lodash": {
"version": "4.17.21",
"requiredVersion": "*",
"eager": false,
"module": () => import('lodash')
}
};`);
});
it('should handle all properties together', () => {
const sharedDependencies = {
react: {
version: '18.2.0',
requiredVersion: '^18.0.0',
singleton: true,
eager: false,
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"react": {
"version": "18.2.0",
"requiredVersion": "^18.0.0",
"singleton": true,
"eager": false,
"module": () => import('react')
}
};`);
});
it('should handle empty object', () => {
const sharedDependencies = {};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result).toBe(
`window['__backstage-module-federation-shared-dependencies__'] = {};`,
);
});
it('should handle scoped package names and special characters', () => {
const sharedDependencies = {
'@backstage/core-plugin-api': {
version: '1.0.0',
requiredVersion: '*',
},
};
const result = prepareRuntimeSharedDependenciesScript(sharedDependencies);
expect(result)
.toBe(`window['__backstage-module-federation-shared-dependencies__'] = {
"@backstage/core-plugin-api": {
"version": "1.0.0",
"requiredVersion": "*",
"module": () => import('@backstage/core-plugin-api')
}
};`);
});
});
const globalSpy = jest.fn();
Object.defineProperty(
global,
'__backstage-module-federation-shared-dependencies__',
{
get: globalSpy,
},
);
describe('getRuntimeSharedDependencies', () => {
afterEach(jest.resetAllMocks);
it('should get runtime shared dependencies with minimal required properties', async () => {
const reactMock = { default: { React: 'react' } };
const reactDomMock = { default: { ReactDom: 'react-dom' } };
globalSpy.mockReturnValue({
react: {
version: '18.2.0',
requiredVersion: '*',
module: async () => reactMock,
},
'react-dom': {
version: '18.2.0',
requiredVersion: '*',
module: async () => reactDomMock,
},
});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toEqual([]);
expect(result.shared).toEqual({
react: {
version: '18.2.0',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '*',
},
},
'react-dom': {
version: '18.2.0',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '*',
},
},
});
// Test that lib functions return the correct modules
expect((result?.shared?.react as { lib: () => any }).lib()).toBe(reactMock);
expect((result?.shared?.['react-dom'] as { lib: () => any }).lib()).toBe(
reactDomMock,
);
});
it('should get runtime shared dependencies with custom version and requiredVersion', async () => {
globalSpy.mockReturnValue({
react: {
module: async () => ({ default: {} }),
version: '18.2.0',
requiredVersion: '^18.0.0',
},
lodash: {
module: async () => ({ default: {} }),
version: '4.17.21',
requiredVersion: '^4.17.0',
},
});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toEqual([]);
expect(result.shared).toEqual({
react: {
version: '18.2.0',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '^18.0.0',
},
},
lodash: {
version: '4.17.21',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '^4.17.0',
},
},
});
});
it('should handle eager', async () => {
globalSpy.mockReturnValue({
react: {
version: '18.2.0',
requiredVersion: '*',
eager: false,
module: async () => ({ default: {} }),
},
lodash: {
version: '4.17.21',
requiredVersion: '*',
eager: true,
module: async () => ({ default: {} }),
},
});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toEqual([]);
expect(result.shared).toEqual({
react: {
version: '18.2.0',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '*',
eager: false,
},
},
lodash: {
version: '4.17.21',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '*',
eager: true,
},
},
});
});
it('should handle singleton', async () => {
globalSpy.mockReturnValue({
react: {
module: async () => ({ default: {} }),
version: '18.2.0',
requiredVersion: '*',
singleton: true,
},
lodash: {
module: async () => ({ default: {} }),
version: '4.17.21',
requiredVersion: '*',
singleton: false,
},
});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toEqual([]);
expect(result.shared).toEqual({
react: {
version: '18.2.0',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '*',
singleton: true,
},
},
lodash: {
version: '4.17.21',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '*',
singleton: false,
},
},
});
});
it('should handle an empty object', async () => {
globalSpy.mockReturnValue({});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toEqual([]);
expect(result.shared).toEqual({});
});
it('should handle module import failures and collect errors', async () => {
const mockError = new Error('Module import failed');
const reactMock = { default: { React: 'react' } };
const lodashMock = { default: { _: 'lodash' } };
globalSpy.mockReturnValue({
react: {
module: async () => reactMock,
version: '18.2.0',
requiredVersion: '^18.0.0',
},
'failing-module': {
module: async () => {
throw mockError;
},
},
lodash: {
module: async () => lodashMock,
version: '4.17.21',
requiredVersion: '^4.17.0',
},
});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toBeInstanceOf(ForwardedError);
expect(result.errors[0].message).toContain(
'Failed to dynamically import "failing-module" and add it to module federation shared dependencies:',
);
expect(result.errors[0].cause).toBe(mockError);
// Should still include successful modules
expect(result.shared).toEqual({
react: {
version: '18.2.0',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '^18.0.0',
},
},
lodash: {
version: '4.17.21',
lib: expect.any(Function),
shareConfig: {
requiredVersion: '^4.17.0',
},
},
});
// Test that lib functions work correctly
expect((result?.shared?.react as { lib: () => any }).lib()).toBe(reactMock);
expect((result?.shared?.lodash as { lib: () => any }).lib()).toBe(
lodashMock,
);
});
it('should handle multiple module import failures', async () => {
const mockError1 = new Error('First module failed');
const mockError2 = new Error('Second module failed');
globalSpy.mockReturnValue({
'failing-module-1': {
module: async () => {
throw mockError1;
},
},
'failing-module-2': {
module: async () => {
throw mockError2;
},
},
});
const result = await buildRuntimeSharedUserOption();
expect(result.errors).toHaveLength(2);
expect(result.errors[0]).toBeInstanceOf(ForwardedError);
expect(result.errors[0].message).toContain('failing-module-1');
expect(result.errors[0].cause).toBe(mockError1);
expect(result.errors[1]).toBeInstanceOf(ForwardedError);
expect(result.errors[1].message).toContain('failing-module-2');
expect(result.errors[1].cause).toBe(mockError2);
expect(result.shared).toEqual({});
});
});
@@ -0,0 +1,110 @@
/*
* 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 { Host, Runtime, SharedDependencies } from './types';
import type { UserOptions } from '@module-federation/runtime/types';
import { ForwardedError } from '@backstage/errors';
const BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL =
'__backstage-module-federation-shared-dependencies__';
/**
* 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.
*
* @see {@link buildRuntimeSharedUserOption}
*
* @param hostSharedDependencies - The shared dependencies for the module federation host.
* @returns The runtime shared dependencies script.
*
* @public
*/
export function prepareRuntimeSharedDependenciesScript(
hostSharedDependencies: SharedDependencies<Host & { version: string }>,
) {
type StringModule = Omit<Runtime, 'module'> & { module: string };
const runtimeSharedDependencies: SharedDependencies<Host & StringModule> =
Object.fromEntries(
Object.entries(hostSharedDependencies).map(([name, sharedDep]) => [
name,
{
version: sharedDep.version,
requiredVersion: sharedDep.requiredVersion,
...(sharedDep.singleton !== undefined
? { singleton: sharedDep.singleton }
: {}),
...(sharedDep.eager !== undefined ? { eager: sharedDep.eager } : {}),
module: `() => import('${name}')`,
},
]),
);
return `window['${BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL}'] = ${JSON.stringify(
runtimeSharedDependencies,
null,
2,
).replace(
/(^\s+"module":\s*)"([^"]+)"$/gm,
(_, start, unquoted) => `${start}${unquoted}`,
)};`;
}
/**
* Builds the list of shared dependencies provided to the module federation runtime.
* It uses the runtime shared dependencies script prepared by {@link prepareRuntimeSharedDependenciesScript}.
*
* @public
*/
export async function buildRuntimeSharedUserOption(): Promise<{
shared: UserOptions['shared'];
errors: ForwardedError[];
}> {
const runtimeSharedDependencies =
(
window as {
[BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL]?: SharedDependencies<
Host & Runtime
>;
}
)[BACKSTAGE_RUNTIME_SHARED_DEPENDENCIES_GLOBAL] ?? {};
const result: UserOptions['shared'] = {};
const errors: ForwardedError[] = [];
for (const [name, sharedDep] of Object.entries(runtimeSharedDependencies)) {
try {
const module = await sharedDep.module();
result[name] = {
version: sharedDep.version,
lib: () => module,
shareConfig: {
singleton: sharedDep.singleton,
requiredVersion: sharedDep.requiredVersion,
eager: sharedDep.eager,
},
};
} catch (e) {
errors.push(
new ForwardedError(
`Failed to dynamically import "${name}" and add it to module federation shared dependencies:`,
e,
),
);
}
}
return { shared: result, errors };
}
@@ -0,0 +1,69 @@
/*
* 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.
*/
/**
* Generic type for shared dependencies configuration in module federation.
*
* The ContextFields type parameter is used to provide additional fields
* to the shared dependencies configuration according to the context.
*
* @public
*/
export type SharedDependencies<ContextFields> = {
[name: string]: {
/** Whether this dependency should be a singleton */
singleton?: boolean;
} & ContextFields;
};
/**
* Specific fields for shared dependencies configured in module federation remote modules.
*
* @public
*/
export type Remote = {
/** Provided module for fallback. Set to false to not provide a fallback, or a custom import path. */
import?: false | string;
/** Required version range. Optional for remotes - can be auto-filled from package.json at build time. */
requiredVersion?: false | string;
/** Version of the shared dependency. Will be resolved at build time by default but can be overridden, but not completely removed. */
version?: false | string;
};
/**
* Specific fields for shared dependencies configured in the module federation host.
*
* @public
*/
export type Host = {
/** Whether to load this dependency eagerly */
eager?: boolean;
/** Required version range. Required for host. */
requiredVersion: false | string;
/** Version of the shared dependency. Will be resolved at build time by default but can be overridden, but not completely removed. */
version?: string;
};
/**
* Specific fields for shared dependencies configured when bootstrapping the module federation host at runtime.
*
* @internal
*/
export type Runtime = {
// version is always expected for runtime host dependencies
version: string;
module: () => Promise<any>;
};
+2 -2
View File
@@ -25,6 +25,8 @@
"./css/styles.css": "./src/css/styles.css",
"./package.json": "./package.json"
},
"main": "src/index.ts",
"types": "src/index.ts",
"typesVersions": {
"*": {
"package.json": [
@@ -32,8 +34,6 @@
]
}
},
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
@@ -1,10 +1,12 @@
{
"name": "@backstage/plugin-catalog-backend-module-gitea",
"version": "0.1.9-next.2",
"license": "Apache-2.0",
"description": "The gitea backend module for the catalog plugin.",
"main": "src/index.ts",
"types": "src/index.ts",
"backstage": {
"role": "backend-plugin-module",
"pluginId": "catalog",
"pluginPackage": "@backstage/plugin-catalog-backend"
},
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
@@ -15,19 +17,20 @@
"url": "https://github.com/backstage/backstage",
"directory": "plugins/catalog-backend-module-gitea"
},
"backstage": {
"role": "backend-plugin-module",
"pluginId": "catalog",
"pluginPackage": "@backstage/plugin-catalog-backend"
},
"license": "Apache-2.0",
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"dist"
],
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"lint": "backstage-cli package lint",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
"postpack": "backstage-cli package postpack",
"start": "backstage-cli package start",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
@@ -42,8 +45,5 @@
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
]
}
}
+5 -5
View File
@@ -71,6 +71,11 @@
"lodash": "^4.17.21",
"yaml": "^2.0.0"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"msw": "^1.0.0"
},
"peerDependencies": {
"@backstage/backend-test-utils": "workspace:^"
},
@@ -78,10 +83,5 @@
"@backstage/backend-test-utils": {
"optional": true
}
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"msw": "^1.0.0"
}
}
+239 -210
View File
@@ -3103,7 +3103,7 @@ __metadata:
"@backstage/repo-tools": "workspace:^"
"@backstage/types": "workspace:^"
"@manypkg/get-packages": "npm:^1.1.3"
"@module-federation/sdk": "npm:^0.9.0"
"@module-federation/sdk": "npm:^0.21.6"
"@types/express": "npm:^4.17.6"
chokidar: "npm:^3.5.3"
express: "npm:^4.22.0"
@@ -3297,6 +3297,7 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/eslint-plugin": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/module-federation-common": "workspace:^"
"@backstage/plugin-auth-backend": "workspace:^"
"@backstage/plugin-auth-backend-module-guest-provider": "workspace:^"
"@backstage/plugin-catalog-node": "workspace:^"
@@ -3308,7 +3309,7 @@ __metadata:
"@backstage/types": "workspace:^"
"@jest/environment-jsdom-abstract": "npm:^30.0.0"
"@manypkg/get-packages": "npm:^1.1.3"
"@module-federation/enhanced": "npm:^0.9.0"
"@module-federation/enhanced": "npm:^0.21.6"
"@octokit/request": "npm:^8.0.0"
"@pmmmwh/react-refresh-webpack-plugin": "npm:^0.6.0"
"@rollup/plugin-commonjs": "npm:^26.0.0"
@@ -3427,7 +3428,7 @@ __metadata:
zod-validation-error: "npm:^4.0.2"
peerDependencies:
"@jest/environment-jsdom-abstract": ^30.0.0
"@module-federation/enhanced": ^0.9.0
"@module-federation/enhanced": ^0.21.6
"@pmmmwh/react-refresh-webpack-plugin": ^0.6.0
esbuild-loader: ^4.0.0
eslint-webpack-plugin: ^4.2.0
@@ -3911,10 +3912,11 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/module-federation-common": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@module-federation/enhanced": "npm:^0.9.0"
"@module-federation/runtime": "npm:^0.9.0"
"@module-federation/sdk": "npm:^0.9.0"
"@module-federation/enhanced": "npm:^0.21.6"
"@module-federation/runtime": "npm:^0.21.6"
"@module-federation/sdk": "npm:^0.21.6"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
"@types/react": "npm:^18.0.0"
@@ -4080,6 +4082,36 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/module-federation-common@workspace:^, @backstage/module-federation-common@workspace:packages/module-federation-common":
version: 0.0.0-use.local
resolution: "@backstage/module-federation-common@workspace:packages/module-federation-common"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@module-federation/runtime": "npm:^0.21.6"
"@types/react": "npm:^18.0.0"
react: "npm:^18.0.2"
react-dom: "npm:^18.0.2"
react-router-dom: "npm:^6.30.2"
peerDependencies:
"@emotion/react": ^11.10.5
"@material-ui/core": ^4.12.2
"@material-ui/styles": ^4.10.0
"@mui/material": ^5.12.2
"@types/react": ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
react-router: ^6.30.2
react-router-dom: ^6.30.2
peerDependenciesMeta:
"@types/react":
optional: true
languageName: unknown
linkType: soft
"@backstage/plugin-api-docs-module-protoc-gen-doc@workspace:plugins/api-docs-module-protoc-gen-doc":
version: 0.0.0-use.local
resolution: "@backstage/plugin-api-docs-module-protoc-gen-doc@workspace:plugins/api-docs-module-protoc-gen-doc"
@@ -11294,46 +11326,61 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/bridge-react-webpack-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.9.1"
"@module-federation/bridge-react-webpack-plugin@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/bridge-react-webpack-plugin@npm:0.21.6"
dependencies:
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/sdk": "npm:0.21.6"
"@types/semver": "npm:7.5.8"
semver: "npm:7.6.3"
checksum: 10/4ff197741b1bdccf8f9e2236781e5ce3ef434e4207c5462b4b95b044c4c57a3ab3dd4dce48490d9fde5bd06fb855b9918841c7d04306726ee224d3e288074091
checksum: 10/35762b352875289703b5bdd7f8ec14da4912400bb8408c6d7afccd5d27f9137f98465f000dac08720dc672122f8223600da3b6d643b702ad36598f69af755e48
languageName: node
linkType: hard
"@module-federation/data-prefetch@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/data-prefetch@npm:0.9.1"
"@module-federation/cli@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/cli@npm:0.21.6"
dependencies:
"@module-federation/runtime": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/dts-plugin": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
chalk: "npm:3.0.0"
commander: "npm:11.1.0"
jiti: "npm:2.4.2"
bin:
mf: bin/mf.js
checksum: 10/7b982871e817465f0effe0cf43f1c3642f7f7188a084953a3c7776b8ec8ec6aef641b98e18c703071c17145681c3aba88ab14ffc8cb9e7e17434d1088d8d99dc
languageName: node
linkType: hard
"@module-federation/data-prefetch@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/data-prefetch@npm:0.21.6"
dependencies:
"@module-federation/runtime": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
fs-extra: "npm:9.1.0"
peerDependencies:
react: ">=16.9.0"
react-dom: ">=16.9.0"
checksum: 10/a3c0e8d77f4d06e3851d041175ef3f55d47ff4069a526b9ac773c8aba71a520a0490653c87cdcf3761e96f9eb9ea16d45026f3aafdf0851c34ff4db6d71ec113
checksum: 10/1ae3961aac317f5c63c21a4fd7c640b1ce0be96a0b787af58fd12dc599010087397552b9a2dedb37742ea7544163cbdb6e27b87064b3cf842cfded084c99dccf
languageName: node
linkType: hard
"@module-federation/dts-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/dts-plugin@npm:0.9.1"
"@module-federation/dts-plugin@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/dts-plugin@npm:0.21.6"
dependencies:
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/third-party-dts-extractor": "npm:0.9.1"
"@module-federation/error-codes": "npm:0.21.6"
"@module-federation/managers": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
"@module-federation/third-party-dts-extractor": "npm:0.21.6"
adm-zip: "npm:^0.5.10"
ansi-colors: "npm:^4.1.3"
axios: "npm:^1.7.4"
axios: "npm:^1.12.0"
chalk: "npm:3.0.0"
fs-extra: "npm:9.1.0"
isomorphic-ws: "npm:5.0.0"
koa: "npm:2.15.4"
koa: "npm:3.0.3"
lodash.clonedeepwith: "npm:4.5.0"
log4js: "npm:6.9.1"
node-schedule: "npm:2.1.1"
@@ -11345,25 +11392,27 @@ __metadata:
peerDependenciesMeta:
vue-tsc:
optional: true
checksum: 10/e9fd11b150456f2621636587d181f6456fe5cd60a0720843fe1310e350c3ff8ebef02ea87d1bf40dc04a4b4cd65cfb2e9007ca1c20e4e5664142b7c670c4a57d
checksum: 10/abc35da5cb602e9e05d9485d14a1a7a3eed754e123d8b6a703720041bb890fd2ac5045c8d95f3d1720a86ae523d391cf1559e4d7cc385c6915cb8b022cf0b8c7
languageName: node
linkType: hard
"@module-federation/enhanced@npm:^0.9.0":
version: 0.9.1
resolution: "@module-federation/enhanced@npm:0.9.1"
"@module-federation/enhanced@npm:^0.21.6":
version: 0.21.6
resolution: "@module-federation/enhanced@npm:0.21.6"
dependencies:
"@module-federation/bridge-react-webpack-plugin": "npm:0.9.1"
"@module-federation/data-prefetch": "npm:0.9.1"
"@module-federation/dts-plugin": "npm:0.9.1"
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/manifest": "npm:0.9.1"
"@module-federation/rspack": "npm:0.9.1"
"@module-federation/runtime-tools": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/bridge-react-webpack-plugin": "npm:0.21.6"
"@module-federation/cli": "npm:0.21.6"
"@module-federation/data-prefetch": "npm:0.21.6"
"@module-federation/dts-plugin": "npm:0.21.6"
"@module-federation/error-codes": "npm:0.21.6"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.21.6"
"@module-federation/managers": "npm:0.21.6"
"@module-federation/manifest": "npm:0.21.6"
"@module-federation/rspack": "npm:0.21.6"
"@module-federation/runtime-tools": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
btoa: "npm:^1.2.1"
schema-utils: "npm:^4.3.0"
upath: "npm:2.0.1"
peerDependencies:
typescript: ^4.9.0 || ^5.0.0
@@ -11376,7 +11425,16 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/a7955711f37ba02a18f3570289dfd8fbed30511b82fcf593e56808559cfd49d8852028aa67950db5e45617d0917536d067d85847892e62c20b68ba6ab40e17c4
bin:
mf: bin/mf.js
checksum: 10/ee67837a6bbfade36424088d6e2d18518e33f7cfb76303f03a6758bde051060105417e51eb6c10e823510376e195f7b8c4090e905a9ac53f0faa3e87edbe7a4b
languageName: node
linkType: hard
"@module-federation/error-codes@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/error-codes@npm:0.21.6"
checksum: 10/6ded1ecab780f1f9ec46a59adb200e75cdf11580d70aa79dd75d71fbbf276615690da277ea67aa1ceb5bc88386f5708cc1d2ba5526be5c9ff02397a6123e36bf
languageName: node
linkType: hard
@@ -11387,57 +11445,51 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/error-codes@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/error-codes@npm:0.9.1"
checksum: 10/545aecc606a506ee47f061835e0eaa41b8d1b02f6bf71b36ec9ae85a1b0370af1f7b7cf92a8f52c3c4b35da858653244316de5ab06bea5dac5b92995467631cc
languageName: node
linkType: hard
"@module-federation/inject-external-runtime-core-plugin@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/inject-external-runtime-core-plugin@npm:0.9.1"
"@module-federation/inject-external-runtime-core-plugin@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/inject-external-runtime-core-plugin@npm:0.21.6"
peerDependencies:
"@module-federation/runtime-tools": 0.9.1
checksum: 10/931eef6292c278450fc8cdb017073fa0b721796461eee12a254fc60a88a2f17e91395b12371f2c8b3b25b4056fc2dd2b76d1022e679be7353947c3d797e75ea5
"@module-federation/runtime-tools": 0.21.6
checksum: 10/579d6d1208ff22f5073b6e96c978fc68f9f130d6e12eed94bd2b37905476fad8daa85adae0d655f77ab3cd07c948cb06a78788d1bfbd7de1c3d5e7f08620f84e
languageName: node
linkType: hard
"@module-federation/managers@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/managers@npm:0.9.1"
"@module-federation/managers@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/managers@npm:0.21.6"
dependencies:
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/sdk": "npm:0.21.6"
find-pkg: "npm:2.0.0"
fs-extra: "npm:9.1.0"
checksum: 10/32c1666244ba98644ab6eccdc844415d1aece1c105f6ba2ff17a3836866e7cdb2f61e556cb5a2b506b898ec709951f318d397f4d153efeff377067237968462f
checksum: 10/2267a340ae44e62b0cb6f565bb19745996d4840e47a9d16fdf8d1a27273f7f6cb08487b727d03b223e7bfe7cd11254c5f0ab29def369ce25765ae1b20a592024
languageName: node
linkType: hard
"@module-federation/manifest@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/manifest@npm:0.9.1"
"@module-federation/manifest@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/manifest@npm:0.21.6"
dependencies:
"@module-federation/dts-plugin": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/dts-plugin": "npm:0.21.6"
"@module-federation/managers": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
chalk: "npm:3.0.0"
find-pkg: "npm:2.0.0"
checksum: 10/539b86bd5388296fb35a34c7b732b92788600ab6625d53f11588ad67c3a603ac3c1974541d9cf04db2d24635be1610414c29388e849d96bbb812cad3b1c79425
checksum: 10/957c4642cd54b328740e3f45e84d6c449fbc39969afed169f34ed2b19da13a2d8c029321276500f1212aa78a7853612d2dc945f9d9679012b753708306392be3
languageName: node
linkType: hard
"@module-federation/rspack@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/rspack@npm:0.9.1"
"@module-federation/rspack@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/rspack@npm:0.21.6"
dependencies:
"@module-federation/bridge-react-webpack-plugin": "npm:0.9.1"
"@module-federation/dts-plugin": "npm:0.9.1"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.9.1"
"@module-federation/managers": "npm:0.9.1"
"@module-federation/manifest": "npm:0.9.1"
"@module-federation/runtime-tools": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
"@module-federation/bridge-react-webpack-plugin": "npm:0.21.6"
"@module-federation/dts-plugin": "npm:0.21.6"
"@module-federation/inject-external-runtime-core-plugin": "npm:0.21.6"
"@module-federation/managers": "npm:0.21.6"
"@module-federation/manifest": "npm:0.21.6"
"@module-federation/runtime-tools": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
btoa: "npm:1.2.1"
peerDependencies:
"@rspack/core": ">=0.7"
typescript: ^4.9.0 || ^5.0.0
@@ -11447,7 +11499,17 @@ __metadata:
optional: true
vue-tsc:
optional: true
checksum: 10/e6deb7236ccc66d7f0475a09916ec714c9855847bfd3d3ec6cdbee991016a222a22f6ae3fd14d71351d55e22e8949140f37ce9ce79dece5416731c6698726096
checksum: 10/8dd952ee1499a897e30d2ba363c29c2c1eba80f0b712f008a9f1bd84b49a2204e4657033e3086e31afe0452cbf023f132db27fc61b4e9caa66170bca1d78896a
languageName: node
linkType: hard
"@module-federation/runtime-core@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/runtime-core@npm:0.21.6"
dependencies:
"@module-federation/error-codes": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
checksum: 10/85efa2042d6f3a7cf0e4971b991472d4339d88f6f15684afb6d451f19ed934e225b2510c86b7bb4d2c5f64253ed7b0175f08c17f95bfc2b9929930a8a03fff1e
languageName: node
linkType: hard
@@ -11461,13 +11523,13 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/runtime-core@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/runtime-core@npm:0.9.1"
"@module-federation/runtime-tools@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/runtime-tools@npm:0.21.6"
dependencies:
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
checksum: 10/6f9edbe23013395d7896fc2a24cb4055bc78df5a335f090e079df951835c1cf91c567228f19879eee3fddb0b34128abd0b50feaca1cf3fb2828c7b9bacc22169
"@module-federation/runtime": "npm:0.21.6"
"@module-federation/webpack-bundler-runtime": "npm:0.21.6"
checksum: 10/36e7ccab948e11f310e87397a1a2185b56064e5691e553b34173686e2bc7372ec710e5ad48c026eb28c85b168765788b743aa2111513f3b57118b47636312dd1
languageName: node
linkType: hard
@@ -11481,13 +11543,14 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/runtime-tools@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/runtime-tools@npm:0.9.1"
"@module-federation/runtime@npm:0.21.6, @module-federation/runtime@npm:^0.21.6":
version: 0.21.6
resolution: "@module-federation/runtime@npm:0.21.6"
dependencies:
"@module-federation/runtime": "npm:0.9.1"
"@module-federation/webpack-bundler-runtime": "npm:0.9.1"
checksum: 10/9436e814a4ab72839b8aed1aa097f32cf7d4d49728dd863c9ce6dc4fb149fe49da6dc9cdeb97814f0023cc91489c9aca06cbfdf9fb4e43d20498ab6ee78c95dd
"@module-federation/error-codes": "npm:0.21.6"
"@module-federation/runtime-core": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
checksum: 10/93fd9bb284630933cab7e4bc070d648b56272f3636038c05eec7d1e3eeb189be3ccebe5f8ecc450197ee992d2616ed282d54e673ec0acd63adee4faddf80b144
languageName: node
linkType: hard
@@ -11502,14 +11565,10 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/runtime@npm:0.9.1, @module-federation/runtime@npm:^0.9.0":
version: 0.9.1
resolution: "@module-federation/runtime@npm:0.9.1"
dependencies:
"@module-federation/error-codes": "npm:0.9.1"
"@module-federation/runtime-core": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
checksum: 10/71eb1c3e81b307ebfe06c43ce70bfa56b217e9dfbed27f0b4235d3d0d05cc0fe2eb1dc57fffb3260ed9e0239257ef117ae13924c642b5ff97d9c65bdf48206fe
"@module-federation/sdk@npm:0.21.6, @module-federation/sdk@npm:^0.21.6":
version: 0.21.6
resolution: "@module-federation/sdk@npm:0.21.6"
checksum: 10/effc4aa932e2f06742bda8f02aaec84e138f5512b50f18c38b051490020b20d3d8edf7ece853fccffc1f78a0b43dec78e69bf02150e7e2801d5ce03c3ee367b9
languageName: node
linkType: hard
@@ -11520,21 +11579,24 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/sdk@npm:0.9.1, @module-federation/sdk@npm:^0.9.0":
version: 0.9.1
resolution: "@module-federation/sdk@npm:0.9.1"
checksum: 10/ea0320feff328a05405e65503d1df28e46a9ad17ef99b77f1428db5c89efbadd2a76ec82d99a54ac1d21286cee6d9ddbba881a9c46748dfe8abdb11e9afef7da
languageName: node
linkType: hard
"@module-federation/third-party-dts-extractor@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/third-party-dts-extractor@npm:0.9.1"
"@module-federation/third-party-dts-extractor@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/third-party-dts-extractor@npm:0.21.6"
dependencies:
find-pkg: "npm:2.0.0"
fs-extra: "npm:9.1.0"
resolve: "npm:1.22.8"
checksum: 10/68c70c79573cd927212d95879aa7b35490519d0ec9f58dd264c9d61e22fea8d452e45a52a94155128d8378e5cdcaecb229707b5b0f4e4c0d53ae96e2fbac366a
checksum: 10/d9328575d3b2c64711dacae38c8186a38732c900d5b7c63d84ec3d4b42b199aa484cfe69dad228e1197170c33d1b39a1671467d274046780e6bddbe0c68b1e25
languageName: node
linkType: hard
"@module-federation/webpack-bundler-runtime@npm:0.21.6":
version: 0.21.6
resolution: "@module-federation/webpack-bundler-runtime@npm:0.21.6"
dependencies:
"@module-federation/runtime": "npm:0.21.6"
"@module-federation/sdk": "npm:0.21.6"
checksum: 10/a5ceb72ee3867acad5d7d3c654eb568068b1d5288f60ce9301bdc9f56effa5a4c26a732a2cec7176a81b87139566cd51dd8dfbc6112da05d47b870fa3ad3ba1f
languageName: node
linkType: hard
@@ -11548,16 +11610,6 @@ __metadata:
languageName: node
linkType: hard
"@module-federation/webpack-bundler-runtime@npm:0.9.1":
version: 0.9.1
resolution: "@module-federation/webpack-bundler-runtime@npm:0.9.1"
dependencies:
"@module-federation/runtime": "npm:0.9.1"
"@module-federation/sdk": "npm:0.9.1"
checksum: 10/430cac0a770b3c46bc195088eb4c1892e1e29a69238dbe72423d64b2b67050afeca2b5026b1a30659b4fe8d9faa038ff97cceba7e2ddf6193b93763f270d9df6
languageName: node
linkType: hard
"@motionone/animation@npm:^10.12.0":
version: 10.16.3
resolution: "@motionone/animation@npm:10.16.3"
@@ -24538,7 +24590,7 @@ __metadata:
languageName: node
linkType: hard
"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.8":
"accepts@npm:^1.3.7, accepts@npm:^1.3.8, accepts@npm:~1.3.4, accepts@npm:~1.3.8":
version: 1.3.8
resolution: "accepts@npm:1.3.8"
dependencies:
@@ -25615,7 +25667,7 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.7.4":
"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.12.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.7.4":
version: 1.13.5
resolution: "axios@npm:1.13.5"
dependencies:
@@ -26367,7 +26419,7 @@ __metadata:
languageName: node
linkType: hard
"btoa@npm:^1.2.1":
"btoa@npm:1.2.1, btoa@npm:^1.2.1":
version: 1.2.1
resolution: "btoa@npm:1.2.1"
bin:
@@ -26609,16 +26661,6 @@ __metadata:
languageName: node
linkType: hard
"cache-content-type@npm:^1.0.0":
version: 1.0.1
resolution: "cache-content-type@npm:1.0.1"
dependencies:
mime-types: "npm:^2.1.18"
ylru: "npm:^1.2.0"
checksum: 10/18db4d59452669ccbfd7146a1510a37eb28e9eccf18ca7a4eb603dff2edc5cccdca7498fc3042a2978f76f11151fba486eb9eb69d9afa3fb124957870aef4fd3
languageName: node
linkType: hard
"cacheable-lookup@npm:^5.0.3":
version: 5.0.3
resolution: "cacheable-lookup@npm:5.0.3"
@@ -27534,6 +27576,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:11.1.0, commander@npm:^11.0.0":
version: 11.1.0
resolution: "commander@npm:11.1.0"
checksum: 10/66bd2d8a0547f6cb1d34022efb25f348e433b0e04ad76a65279b1b09da108f59a4d3001ca539c60a7a46ea38bcf399fc17d91adad76a8cf43845d8dcbaf5cda1
languageName: node
linkType: hard
"commander@npm:7.1.0":
version: 7.1.0
resolution: "commander@npm:7.1.0"
@@ -27562,13 +27611,6 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^11.0.0":
version: 11.1.0
resolution: "commander@npm:11.1.0"
checksum: 10/66bd2d8a0547f6cb1d34022efb25f348e433b0e04ad76a65279b1b09da108f59a4d3001ca539c60a7a46ea38bcf399fc17d91adad76a8cf43845d8dcbaf5cda1
languageName: node
linkType: hard
"commander@npm:^12.0.0, commander@npm:^12.1.0":
version: 12.1.0
resolution: "commander@npm:12.1.0"
@@ -27856,7 +27898,7 @@ __metadata:
languageName: node
linkType: hard
"content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2, content-disposition@npm:~0.5.4":
"content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.4":
version: 0.5.4
resolution: "content-disposition@npm:0.5.4"
dependencies:
@@ -27874,7 +27916,7 @@ __metadata:
languageName: node
linkType: hard
"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
checksum: 10/585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662
@@ -27954,7 +27996,7 @@ __metadata:
languageName: node
linkType: hard
"cookies@npm:~0.9.0":
"cookies@npm:~0.9.1":
version: 0.9.1
resolution: "cookies@npm:0.9.1"
dependencies:
@@ -29150,7 +29192,7 @@ __metadata:
languageName: node
linkType: hard
"destroy@npm:1.2.0, destroy@npm:^1.0.4, destroy@npm:~1.2.0":
"destroy@npm:1.2.0, destroy@npm:^1.0.4, destroy@npm:^1.2.0, destroy@npm:~1.2.0":
version: 1.2.0
resolution: "destroy@npm:1.2.0"
checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38
@@ -29181,9 +29223,9 @@ __metadata:
linkType: hard
"detect-libc@npm:^2.0.0":
version: 2.0.3
resolution: "detect-libc@npm:2.0.3"
checksum: 10/b4ea018d623e077bd395f168a9e81db77370dde36a5b01d067f2ad7989924a81d31cb547ff764acb2aa25d50bb7fdde0b0a93bec02212b0cb430621623246d39
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766
languageName: node
linkType: hard
@@ -29881,13 +29923,6 @@ __metadata:
languageName: node
linkType: hard
"encodeurl@npm:^1.0.2, encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
checksum: 10/e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c
languageName: node
linkType: hard
"encodeurl@npm:^2.0.0, encodeurl@npm:~2.0.0":
version: 2.0.0
resolution: "encodeurl@npm:2.0.0"
@@ -29895,6 +29930,13 @@ __metadata:
languageName: node
linkType: hard
"encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
checksum: 10/e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c
languageName: node
linkType: hard
"encoding@npm:^0.1.12, encoding@npm:^0.1.13":
version: 0.1.13
resolution: "encoding@npm:0.1.13"
@@ -33968,7 +34010,7 @@ __metadata:
languageName: node
linkType: hard
"http-assert@npm:^1.3.0":
"http-assert@npm:^1.5.0":
version: 1.5.0
resolution: "http-assert@npm:1.5.0"
dependencies:
@@ -34027,19 +34069,6 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:^1.6.3, http-errors@npm:~1.8.0":
version: 1.8.1
resolution: "http-errors@npm:1.8.1"
dependencies:
depd: "npm:~1.1.2"
inherits: "npm:2.0.4"
setprototypeof: "npm:1.2.0"
statuses: "npm:>= 1.5.0 < 2"
toidentifier: "npm:1.0.1"
checksum: 10/76fc491bd8df2251e21978e080d5dae20d9736cfb29bb72b5b76ec1bcebb1c14f0f58a3a128dd89288934379d2173cfb0421c571d54103e93dd65ef6243d64d8
languageName: node
linkType: hard
"http-errors@npm:^2.0.0, http-errors@npm:~2.0.0, http-errors@npm:~2.0.1":
version: 2.0.1
resolution: "http-errors@npm:2.0.1"
@@ -34065,6 +34094,19 @@ __metadata:
languageName: node
linkType: hard
"http-errors@npm:~1.8.0":
version: 1.8.1
resolution: "http-errors@npm:1.8.1"
dependencies:
depd: "npm:~1.1.2"
inherits: "npm:2.0.4"
setprototypeof: "npm:1.2.0"
statuses: "npm:>= 1.5.0 < 2"
toidentifier: "npm:1.0.1"
checksum: 10/76fc491bd8df2251e21978e080d5dae20d9736cfb29bb72b5b76ec1bcebb1c14f0f58a3a128dd89288934379d2173cfb0421c571d54103e93dd65ef6243d64d8
languageName: node
linkType: hard
"http-parser-js@npm:>=0.5.1":
version: 0.5.3
resolution: "http-parser-js@npm:0.5.3"
@@ -36292,6 +36334,15 @@ __metadata:
languageName: node
linkType: hard
"jiti@npm:2.4.2":
version: 2.4.2
resolution: "jiti@npm:2.4.2"
bin:
jiti: lib/jiti-cli.mjs
checksum: 10/e2b07eb2e3fbb245e29ad288dddecab31804967fc84d5e01d39858997d2743b5e248946defcecf99272275a00284ecaf7ec88b8c841331324f0c946d8274414b
languageName: node
linkType: hard
"jiti@npm:^2.0.0, jiti@npm:^2.6.0":
version: 2.6.1
resolution: "jiti@npm:2.6.1"
@@ -37184,44 +37235,29 @@ __metadata:
languageName: node
linkType: hard
"koa-convert@npm:^2.0.0":
version: 2.0.0
resolution: "koa-convert@npm:2.0.0"
"koa@npm:3.0.3":
version: 3.0.3
resolution: "koa@npm:3.0.3"
dependencies:
co: "npm:^4.6.0"
koa-compose: "npm:^4.1.0"
checksum: 10/7385b3391995f59c1312142e110d5dff677f9850dbfbcf387cd36a7b0af03b5d26e82b811eb9bb008b4f3e661cdab1f8817596e46b1929da2cf6e97a2f7456ed
languageName: node
linkType: hard
"koa@npm:2.15.4":
version: 2.15.4
resolution: "koa@npm:2.15.4"
dependencies:
accepts: "npm:^1.3.5"
cache-content-type: "npm:^1.0.0"
content-disposition: "npm:~0.5.2"
content-type: "npm:^1.0.4"
cookies: "npm:~0.9.0"
debug: "npm:^4.3.2"
accepts: "npm:^1.3.8"
content-disposition: "npm:~0.5.4"
content-type: "npm:^1.0.5"
cookies: "npm:~0.9.1"
delegates: "npm:^1.0.0"
depd: "npm:^2.0.0"
destroy: "npm:^1.0.4"
encodeurl: "npm:^1.0.2"
destroy: "npm:^1.2.0"
encodeurl: "npm:^2.0.0"
escape-html: "npm:^1.0.3"
fresh: "npm:~0.5.2"
http-assert: "npm:^1.3.0"
http-errors: "npm:^1.6.3"
is-generator-function: "npm:^1.0.7"
http-assert: "npm:^1.5.0"
http-errors: "npm:^2.0.0"
koa-compose: "npm:^4.1.0"
koa-convert: "npm:^2.0.0"
on-finished: "npm:^2.3.0"
only: "npm:~0.0.2"
parseurl: "npm:^1.3.2"
statuses: "npm:^1.5.0"
type-is: "npm:^1.6.16"
mime-types: "npm:^3.0.1"
on-finished: "npm:^2.4.1"
parseurl: "npm:^1.3.3"
statuses: "npm:^2.0.1"
type-is: "npm:^2.0.1"
vary: "npm:^1.1.2"
checksum: 10/98de77173822f0a28c0f5d1ebd261ab02f3f905d40602e51957a0c7202122647a60c5b6c59be03361dd24bf6a5685eac97af84b306914efd057751e71f93cb0f
checksum: 10/15df9a7777ad357851253deaba534403dcc97eb81efce263c8a8b30b5bc3e5db7d61e35b7a89ce359ec3cf043b50244996df0efe6f29785adefbc6f691a1eda6
languageName: node
linkType: hard
@@ -39160,7 +39196,7 @@ __metadata:
languageName: node
linkType: hard
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
@@ -40936,7 +40972,7 @@ __metadata:
languageName: node
linkType: hard
"on-finished@npm:2.4.1, on-finished@npm:^2.3.0, on-finished@npm:^2.4.1, on-finished@npm:~2.4.1":
"on-finished@npm:2.4.1, on-finished@npm:^2.4.1, on-finished@npm:~2.4.1":
version: 2.4.1
resolution: "on-finished@npm:2.4.1"
dependencies:
@@ -41015,13 +41051,6 @@ __metadata:
languageName: node
linkType: hard
"only@npm:~0.0.2":
version: 0.0.2
resolution: "only@npm:0.0.2"
checksum: 10/e2ad03e486534dc6bfb983393be83125a4669052b4a19a353eb00475b46971fb238a18223f2b609fe0d1bcb61ff8373964ccac64d05cbf970865299f655ed0ba
languageName: node
linkType: hard
"ono@npm:^7.1.3":
version: 7.1.3
resolution: "ono@npm:7.1.3"
@@ -41779,7 +41808,7 @@ __metadata:
languageName: node
linkType: hard
"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3":
"parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3":
version: 1.3.3
resolution: "parseurl@npm:1.3.3"
checksum: 10/407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2
@@ -47296,7 +47325,7 @@ __metadata:
languageName: node
linkType: hard
"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0, statuses@npm:~1.5.0":
"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:~1.5.0":
version: 1.5.0
resolution: "statuses@npm:1.5.0"
checksum: 10/c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c
@@ -49220,7 +49249,7 @@ __metadata:
languageName: node
linkType: hard
"type-is@npm:^1.6.16, type-is@npm:^1.6.18, type-is@npm:~1.6.18":
"type-is@npm:^1.6.18, type-is@npm:~1.6.18":
version: 1.6.18
resolution: "type-is@npm:1.6.18"
dependencies:
@@ -51452,7 +51481,7 @@ __metadata:
languageName: node
linkType: hard
"ylru@npm:^1.2.0, ylru@npm:^1.3.2":
"ylru@npm:^1.3.2":
version: 1.4.0
resolution: "ylru@npm:1.4.0"
checksum: 10/5437f8eb2fb5dd515845c657dde3cecaa9f6bd4c6386d2a5212d3fafe02189c7d8ebfdfc84940a7811607cb3524eb362ce95d3180d355cd5deb610aa8c82c9bc