Merge pull request #27846 from davidfestal/all-exports-in-exposed-modules

CLI: have module federation settings take in account all the exports defined in the `package.json`
This commit is contained in:
Patrik Oldsberg
2024-12-17 11:48:37 +01:00
committed by GitHub
6 changed files with 84 additions and 15 deletions
+7
View File
@@ -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.
@@ -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,
});
@@ -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,
),
});
+12 -3
View File
@@ -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,
@@ -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<ModuleFederationOptions | undefined> {
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,
};
}
+6
View File
@@ -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 = {