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..ec89008b81 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,22 @@ 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, + moduleFederation: await getModuleFederationOptions( + packageJson, + resolvePath(targetDir), 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..d47f539527 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -15,8 +15,10 @@ */ import { readJson } from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import { getModuleFederationOptions, serveBundle } from '../../lib/bundler'; import { paths } from '../../lib/paths'; +import { BackstagePackageJson } from '@backstage/cli-node'; interface StartAppOptions { verifyVersions?: boolean; @@ -30,7 +32,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, @@ -39,8 +43,9 @@ export async function startFrontend(options: StartAppOptions) { verifyVersions: options.verifyVersions, skipOpenBrowser: options.skipOpenBrowser, linkedWorkspace: options.linkedWorkspace, - moduleFederation: getModuleFederationOptions( - name, + moduleFederation: await getModuleFederationOptions( + packageJson, + resolvePath(paths.targetDir), 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..26d1c4784b 100644 --- a/packages/cli/src/lib/bundler/moduleFederation.ts +++ b/packages/cli/src/lib/bundler/moduleFederation.ts @@ -16,11 +16,18 @@ import chalk from 'chalk'; import { ModuleFederationOptions } from './types'; +import { BackstagePackageJson } from '@backstage/cli-node'; +import { readEntryPoints } from '../entryPoints'; +import { + createTypeDistProject, + getEntryPointDefaultFeatureType, +} from '../typeDistProject'; -export function getModuleFederationOptions( - name: string, +export async function getModuleFederationOptions( + packageJson: BackstagePackageJson, + packageDir: string, isModuleFederationRemote?: boolean, -): ModuleFederationOptions | undefined { +): Promise { if ( !isModuleFederationRemote && !process.env.EXPERIMENTAL_MODULE_FEDERATION @@ -34,11 +41,43 @@ export function getModuleFederationOptions( ), ); + let exposes: ModuleFederationOptions['exposes']; + const packageRole = packageJson.backstage?.role; + if (isModuleFederationRemote && packageJson.exports && packageRole) { + const project = await createTypeDistProject(); + exposes = Object.fromEntries( + readEntryPoints(packageJson) + .filter(ep => { + if (ep.mount === './package.json') { + return false; + } + if (ep.mount === '.') { + return true; + } + // Include this additional entry point in the exposed modules + // if it exports a feature as default export. + return ( + getEntryPointDefaultFeatureType( + packageRole, + packageDir, + project, + ep.path, + ) !== null + ); + }) + .map(ep => [ep.mount, ep.path]), + ); + } + 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. - name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), + name: packageJson.name + .replaceAll('@', '') + .replaceAll('/', '__') + .replaceAll('-', '_'), + exposes, }; } 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 = {