Merge pull request #30967 from davidfestal/runtime-module-federation-enablement
Runtime module federation enablement
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -260,6 +260,7 @@ Usage: program [options]
|
||||
Options:
|
||||
--config <path>
|
||||
--minify
|
||||
--module-federation
|
||||
--role <name>
|
||||
--skip-build-dependencies
|
||||
--stats
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user