diff --git a/.changeset/shaggy-tomatoes-travel.md b/.changeset/shaggy-tomatoes-travel.md new file mode 100644 index 0000000000..71dd9124be --- /dev/null +++ b/.changeset/shaggy-tomatoes-travel.md @@ -0,0 +1,7 @@ +--- +'@backstage/cli': patch +--- + +Enhance the behavior of the experimental support for module federation in the backstage CLI, +by using the `package.json` exports (when present) to complete the list of exposed modules. +This allows, for example, using exported `alpha` definitions through module federation. diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 0ba0669c32..ff84f4f15c 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -19,6 +19,7 @@ import { resolve as resolvePath } from 'path'; import { buildBundle, getModuleFederationOptions } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../modules/config/lib/config'; +import { BackstagePackageJson } from '@backstage/cli-node'; interface BuildAppOptions { targetDir: string; @@ -30,20 +31,21 @@ interface BuildAppOptions { export async function buildFrontend(options: BuildAppOptions) { const { targetDir, writeStats, configPaths, rspack } = options; - const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); - + const packageJson = (await fs.readJson( + resolvePath(targetDir, 'package.json'), + )) as BackstagePackageJson; await buildBundle({ targetDir, entry: 'src/index', parallelism: getEnvironmentParallelism(), statsJsonEnabled: writeStats, moduleFederation: getModuleFederationOptions( - name, + packageJson, options.isModuleFederationRemote, ), ...(await loadCliConfig({ args: configPaths, - fromPackage: name, + fromPackage: packageJson.name, })), rspack, }); diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index df317799f4..a06d6eaca0 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -17,6 +17,7 @@ import { readJson } from 'fs-extra'; import { getModuleFederationOptions, serveBundle } from '../../lib/bundler'; import { paths } from '../../lib/paths'; +import { BackstagePackageJson } from '@backstage/cli-node'; interface StartAppOptions { verifyVersions?: boolean; @@ -30,7 +31,9 @@ interface StartAppOptions { } export async function startFrontend(options: StartAppOptions) { - const { name } = await readJson(paths.resolveTarget('package.json')); + const packageJson = (await readJson( + paths.resolveTarget('package.json'), + )) as BackstagePackageJson; const waitForExit = await serveBundle({ entry: options.entry, @@ -40,7 +43,7 @@ export async function startFrontend(options: StartAppOptions) { skipOpenBrowser: options.skipOpenBrowser, linkedWorkspace: options.linkedWorkspace, moduleFederation: getModuleFederationOptions( - name, + packageJson, options.isModuleFederationRemote, ), }); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 5e379783ba..cc0dbd7c2a 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -235,13 +235,22 @@ export async function createConfig( .ModuleFederationPlugin as unknown as typeof ModuleFederationPlugin) : ModuleFederationPlugin; + const exposes = options.moduleFederation?.exposes + ? Object.fromEntries( + Object.entries(options.moduleFederation?.exposes).map(([k, v]) => [ + k, + resolvePath(paths.targetPath, v), + ]), + ) + : { + '.': paths.targetEntry, + }; + plugins.push( new AdaptedModuleFederationPlugin({ ...(isRemote && { filename: 'remoteEntry.js', - exposes: { - '.': paths.targetEntry, - }, + exposes, }), name: options.moduleFederation.name, runtime: false, diff --git a/packages/cli/src/lib/bundler/moduleFederation.ts b/packages/cli/src/lib/bundler/moduleFederation.ts index ae3f9ab048..77c7356b74 100644 --- a/packages/cli/src/lib/bundler/moduleFederation.ts +++ b/packages/cli/src/lib/bundler/moduleFederation.ts @@ -16,9 +16,11 @@ import chalk from 'chalk'; import { ModuleFederationOptions } from './types'; +import { BackstagePackageJson } from '@backstage/cli-node'; +import { readEntryPoints } from '../entryPoints'; export function getModuleFederationOptions( - name: string, + packageJson: BackstagePackageJson, isModuleFederationRemote?: boolean, ): ModuleFederationOptions | undefined { if ( @@ -39,6 +41,17 @@ export function getModuleFederationOptions( // 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. - name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), + name: packageJson.name + .replaceAll('@', '') + .replaceAll('/', '__') + .replaceAll('-', '_'), + exposes: + isModuleFederationRemote && packageJson.exports + ? Object.fromEntries( + readEntryPoints(packageJson) + .filter(ep => ep.mount !== './package.json') + .map(ep => [ep.mount, ep.path]), + ) + : undefined, }; } diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index c5afb86b88..e44236a602 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -23,6 +23,12 @@ export type ModuleFederationOptions = { 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; + }; }; export type BundlingOptions = {