feat(module-federation): move common executor logic to module-federation package (#29151)

## Current Behavior
The logic for the `module-federation-dev-server` and
`module-federation-ssr-dev-server` is duplicated across Angular, React
and Rspack.

The majority of this logic is the same, and the duplication causes an
increased maintenance tax.

## Expected Behavior
Move the logic into a utility that is exposed from
`@nx/module-federation`.
This commit is contained in:
Colum Ferry 2024-12-03 13:15:20 +00:00 committed by GitHub
parent da901dec08
commit 5448046f06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 761 additions and 1586 deletions

View File

@ -96,7 +96,6 @@ rust-toolchain @nrwl/nx-native-reviewers
/packages/webpack/** @nrwl/nx-js-reviewers
/e2e/webpack/** @nrwl/nx-js-reviewers
/packages/rspack/** @nrwl/nx-js-reviewers
/packages/rspack/src/utils/module-federation @nrwl/nx-js-reviewers
/e2e/rspack/** @nrwl/nx-js-reviewers
/packages/esbuild/** @nrwl/nx-js-reviewers
/e2e/esbuild/** @nrwl/nx-js-reviewers

View File

@ -1,91 +0,0 @@
import { type Schema } from '../schema';
import { type ExecutorContext, logger } from '@nx/devkit';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { fork } from 'node:child_process';
import { join } from 'node:path';
import { createWriteStream } from 'node:fs';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,
nxBin,
context: ExecutorContext,
options: Schema
) {
if (!staticRemotesConfig.remotes.length) {
return;
}
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of staticRemotesConfig.remotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${
staticRemotesConfig.config[app].urlSegment
}`;
}
await new Promise<void>((res, rej) => {
logger.info(
`NX Building ${staticRemotesConfig.remotes.length} static remotes...`
);
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${staticRemotesConfig.remotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
// File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log'
const remoteBuildLogFile = join(
workspaceDataDirectory,
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
);
const stdoutStream = createWriteStream(remoteBuildLogFile);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
stdoutStream.write(stdoutString);
// in addition to writing into the stdout stream, also show error directly in console
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
if (stdoutString.includes('ERROR in')) {
logger.log(stdoutString);
}
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(
`NX Built ${staticRemotesConfig.remotes.length} static remotes`
);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.once('exit', (code) => {
stdoutStream.end();
staticProcess.stdout.removeAllListeners('data');
staticProcess.stderr.removeAllListeners('data');
if (code !== 0) {
rej(
`Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}`
);
} else {
res();
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
return mappedLocationOfRemotes;
}

View File

@ -1,4 +1,2 @@
export * from './build-static-remotes';
export * from './normalize-options';
export * from './start-dev-remotes';
export * from './start-static-remotes-file-server';

View File

@ -26,6 +26,7 @@ export function normalizeOptions(schema: Schema): NormalizedSchema {
liveReload: schema.liveReload ?? true,
open: schema.open ?? false,
ssl: schema.ssl ?? false,
verbose: schema.verbose ?? false,
sslCert: schema.sslCert ? join(workspaceRoot, schema.sslCert) : undefined,
sslKey: schema.sslKey ? join(workspaceRoot, schema.sslKey) : undefined,
};

View File

@ -8,7 +8,7 @@ import {
export async function startRemotes(
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: Schema,
options: Pick<Schema, 'devRemotes' | 'verbose'>,
context: ExecutorContext,
target: 'serve' | 'serve-static' = 'serve'
) {

View File

@ -4,24 +4,14 @@ import {
readProjectsConfigurationFromProjectGraph,
} from '@nx/devkit';
import { type Schema } from './schema';
import {
buildStaticRemotes,
normalizeOptions,
startRemotes,
startStaticRemotesFileServer,
} from './lib';
import { normalizeOptions, startRemotes } from './lib';
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
import {
combineAsyncIterables,
createAsyncIterable,
mapAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import {
getModuleFederationConfig,
getRemotes,
startRemoteProxies,
parseStaticRemotesConfig,
} from '@nx/module-federation/src/utils';
import { startRemoteIterators } from '@nx/module-federation/src/executors/utils';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
@ -37,8 +27,6 @@ export async function* moduleFederationDevServerExecutor(
schema: Schema,
context: ExecutorContext
) {
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
const options = normalizeOptions(schema);
const { projects: workspaceProjects } =
@ -101,76 +89,14 @@ export async function* moduleFederationDevServerExecutor(
validateDevRemotes(options, workspaceProjects);
const moduleFederationConfig = getModuleFederationConfig(
project.targets.build.options.tsConfig,
context.root,
project.root,
'angular'
);
const remoteNames = options.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: project.name,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
remotes.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
project.name.replace(/-/g, '_'),
]);
const staticRemotesConfig = parseStaticRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options
);
const devRemoteIters = await startRemotes(
remotes.devRemotes,
workspaceProjects,
options,
context,
'serve'
);
const staticRemotesIter = startStaticRemotesFileServer(
staticRemotesConfig,
context,
options
);
startRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: options.sslCert,
pathToKey: options.sslKey,
}
: undefined
);
const { remotes, staticRemotesIter, devRemoteIters } =
await startRemoteIterators(
options,
context,
startRemotes,
pathToManifestFile,
'angular'
);
const removeBaseUrlEmission = (iter: AsyncIterable<unknown>) =>
mapAsyncIterable(iter, (v) => ({

View File

@ -43,4 +43,5 @@ export type NormalizedSchema = SchemaWithBuildTarget & {
liveReload: boolean;
open: boolean;
ssl: boolean;
verbose: boolean;
};

View File

@ -1,91 +0,0 @@
import type { Schema } from '../schema';
import { type ExecutorContext, logger } from '@nx/devkit';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { fork } from 'node:child_process';
import { join } from 'node:path';
import { createWriteStream } from 'node:fs';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,
nxBin,
context: ExecutorContext,
options: Schema
) {
if (!staticRemotesConfig.remotes.length) {
return;
}
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of staticRemotesConfig.remotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${
staticRemotesConfig.config[app].urlSegment
}`;
}
await new Promise<void>((resolve, reject) => {
logger.info(
`NX Building ${staticRemotesConfig.remotes.length} static remotes...`
);
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=server`,
`--projects=${staticRemotesConfig.remotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
// File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log'
const remoteBuildLogFile = join(
workspaceDataDirectory,
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
);
const stdoutStream = createWriteStream(remoteBuildLogFile);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
stdoutStream.write(stdoutString);
// in addition to writing into the stdout stream, also show error directly in console
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
if (stdoutString.includes('ERROR in')) {
logger.log(stdoutString);
}
if (stdoutString.includes('Successfully ran target server')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(
`NX Built ${staticRemotesConfig.remotes.length} static remotes`
);
resolve();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.once('exit', (code) => {
stdoutStream.end();
staticProcess.stdout.removeAllListeners('data');
staticProcess.stderr.removeAllListeners('data');
if (code !== 0) {
reject(
`Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}`
);
} else {
resolve();
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
return mappedLocationOfRemotes;
}

View File

@ -1,8 +1,8 @@
import { workspaceRoot } from '@nx/devkit';
import type { Schema } from '../schema';
import type { NormalizedSchema, Schema } from '../schema';
import { join } from 'path';
export function normalizeOptions(options: Schema): Schema {
export function normalizeOptions(options: Schema): NormalizedSchema {
const devServeRemotes = !options.devRemotes
? []
: Array.isArray(options.devRemotes)
@ -12,6 +12,7 @@ export function normalizeOptions(options: Schema): Schema {
return {
...options,
devRemotes: devServeRemotes,
verbose: options.verbose ?? false,
ssl: options.ssl ?? false,
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,

View File

@ -8,7 +8,7 @@ import {
export async function startRemotes(
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: Schema,
options: Pick<Schema, 'devRemotes' | 'verbose'>,
context: ExecutorContext
) {
const target = 'serve-ssr';

View File

@ -1,54 +0,0 @@
import { type ExecutorContext, workspaceRoot } from '@nx/devkit';
import { type Schema } from '../schema';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { join } from 'path';
import { cpSync, rmSync } from 'fs';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
export function startStaticRemotes(
ssrStaticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: Schema
) {
if (ssrStaticRemotesConfig.remotes.length === 0) {
return createAsyncIterable(({ next, done }) => {
next({ success: true });
done();
});
}
// The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory
const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of ssrStaticRemotesConfig.remotes) {
const remoteConfig = ssrStaticRemotesConfig.config[app];
cpSync(
remoteConfig.outputPath,
join(commonOutputDirectory, remoteConfig.urlSegment),
{
force: true,
recursive: true,
}
);
}
const staticRemotesIter = fileServerExecutor(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
cacheSeconds: -1,
},
context
);
return staticRemotesIter;
}

View File

@ -1,37 +1,29 @@
import { executeSSRDevServerBuilder } from '@angular-devkit/build-angular';
import { type ExecutorContext, logger } from '@nx/devkit';
import {
combineAsyncIterables,
createAsyncIterable,
mapAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
import {
getModuleFederationConfig,
getRemotes,
parseStaticSsrRemotesConfig,
startSsrRemoteProxies,
} from '@nx/module-federation/src/utils';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { existsSync } from 'fs';
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { extname, join } from 'path';
import {
getDynamicMfManifestFile,
validateDevRemotes,
} from '../../builders/utilities/module-federation';
import { buildStaticRemotes } from './lib/build-static-remotes';
import { normalizeOptions } from './lib/normalize-options';
import { startRemotes } from './lib/start-dev-remotes';
import { startStaticRemotes } from './lib/start-static-remotes';
import type { Schema } from './schema';
import { startRemoteIterators } from '@nx/module-federation/src/executors/utils';
import { startRemotes } from './lib/start-dev-remotes';
import {
combineAsyncIterables,
createAsyncIterable,
mapAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
import { normalizeOptions } from './lib/normalize-options';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
export async function* moduleFederationSsrDevServerExecutor(
schema: Schema,
context: ExecutorContext
) {
const nxBin = require.resolve('nx/bin/nx');
const options = normalizeOptions(schema);
const currIter = eachValueFrom(
@ -79,73 +71,15 @@ export async function* moduleFederationSsrDevServerExecutor(
validateDevRemotes({ devRemotes: options.devRemotes }, workspaceProjects);
const moduleFederationConfig = getModuleFederationConfig(
project.targets.build.options.tsConfig,
context.root,
project.root,
'angular'
);
const remoteNames = options.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: project.name,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
const staticRemotesConfig = parseStaticSsrRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options
);
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
options.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
project.name.replace(/-/g, '_'),
]);
const devRemotes = await startRemotes(
remotes.devRemotes,
workspaceProjects,
options,
context
);
const staticRemotes = startStaticRemotes(
staticRemotesConfig,
context,
options
);
startSsrRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? { pathToCert: options.sslCert, pathToKey: options.sslKey }
: undefined
);
const { remotes, staticRemotesIter, devRemoteIters } =
await startRemoteIterators(
options,
context,
startRemotes,
pathToManifestFile,
'angular',
true
);
const removeBaseUrlEmission = (iter: AsyncIterable<unknown>) =>
mapAsyncIterable(iter, (v) => ({
@ -154,8 +88,8 @@ export async function* moduleFederationSsrDevServerExecutor(
}));
const combined = combineAsyncIterables(
removeBaseUrlEmission(staticRemotes),
...(devRemotes ? devRemotes.map(removeBaseUrlEmission) : []),
removeBaseUrlEmission(staticRemotesIter),
...(devRemoteIters ? devRemoteIters.map(removeBaseUrlEmission) : []),
createAsyncIterable<{ success: true; baseUrl: string }>(
async ({ next, done }) => {
if (!options.isInitialHost) {
@ -172,7 +106,7 @@ export async function* moduleFederationSsrDevServerExecutor(
return;
}
try {
const portsToWaitFor = staticRemotes
const portsToWaitFor = staticRemotesIter
? [options.staticRemotesPort, ...remotes.remotePorts]
: [...remotes.remotePorts];
await Promise.all(
@ -198,7 +132,7 @@ export async function* moduleFederationSsrDevServerExecutor(
}
)
);
let refs = 2 + (devRemotes?.length ?? 0);
let refs = 2 + (devRemoteIters?.length ?? 0);
for await (const result of combined) {
if (result.success === false) throw new Error('Remotes failed to start');
if (result.success) refs--;

View File

@ -9,3 +9,9 @@ export interface Schema extends SSRDevServerBuilderOptions {
staticRemotesPort?: number;
isInitialHost?: boolean;
}
export interface NormalizedSchema extends Schema {
devRemotes: DevRemoteDefinition[];
ssl: boolean;
verbose: boolean;
}

View File

@ -27,6 +27,7 @@
"tslib": "^2.3.0",
"@nx/devkit": "file:../devkit",
"@nx/js": "file:../js",
"@nx/web": "file:../web",
"picocolors": "^1.1.0",
"webpack": "5.88.0",
"@rspack/core": "1.1.3",

View File

@ -1,7 +1,6 @@
import { StaticRemotesConfig } from '@nx/module-federation/src/utils';
import { ExecutorContext } from '@nx/devkit';
import { ModuleFederationDevServerOptions } from '../executors/module-federation-dev-server/schema';
import { logger } from 'nx/src/utils/logger';
import { ExecutorContext, logger } from '@nx/devkit';
import { type StaticRemotesConfig } from '../../utils';
import { type BuildStaticRemotesOptions } from './models';
import { fork } from 'node:child_process';
import { join } from 'path';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
@ -11,7 +10,8 @@ export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,
nxBin,
context: ExecutorContext,
options: ModuleFederationDevServerOptions
options: BuildStaticRemotesOptions,
buildTarget: 'build' | 'server' = 'build'
) {
if (!staticRemotesConfig.remotes.length) {
return;
@ -34,7 +34,7 @@ export async function buildStaticRemotes(
nxBin,
[
'run-many',
`--target=build`,
`--target=${buildTarget}`,
`--projects=${staticRemotesConfig.remotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
@ -66,7 +66,7 @@ export async function buildStaticRemotes(
logger.log(stdoutString);
}
if (stdoutString.includes('Successfully ran target build')) {
if (stdoutString.includes(`Successfully ran target ${buildTarget}`)) {
staticProcess.stdout.removeAllListeners('data');
logger.info(
`NX Built ${staticRemotesConfig.remotes.length} static remotes`

View File

@ -0,0 +1,4 @@
export * from './start-static-remotes-file-server';
export * from './build-static-remotes';
export * from './start-remote-iterators';
export { DevRemoteDefinition } from './models';

View File

@ -0,0 +1,38 @@
import type { ProjectConfiguration, ExecutorContext } from '@nx/devkit';
export type DevRemoteDefinition =
| string
| { remoteName: string; configuration: string };
export type StartRemoteFn = (
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: {
devRemotes: DevRemoteDefinition[];
verbose: boolean;
},
context: ExecutorContext,
target: 'serve' | 'serve-static'
) => Promise<AsyncIterable<{ success: boolean }>[]>;
export interface StaticRemotesOptions {
staticRemotesPort?: number;
host?: string;
ssl?: boolean;
sslCert?: string;
sslKey?: string;
}
export interface BuildStaticRemotesOptions extends StaticRemotesOptions {
parallel?: number;
}
export interface StartRemoteIteratorsOptions extends BuildStaticRemotesOptions {
devRemotes: DevRemoteDefinition[];
skipRemotes?: string[];
buildTarget?: string;
liveReload?: boolean;
open?: boolean;
ssl?: boolean;
verbose: boolean;
}

View File

@ -0,0 +1,123 @@
import { StartRemoteFn, type StartRemoteIteratorsOptions } from './models';
import {
getModuleFederationConfig,
getRemotes,
parseStaticRemotesConfig,
parseStaticSsrRemotesConfig,
startRemoteProxies,
startSsrRemoteProxies,
} from '../../utils';
import { buildStaticRemotes } from './build-static-remotes';
import {
startSsrStaticRemotesFileServer,
startStaticRemotesFileServer,
} from './start-static-remotes-file-server';
import {
type ExecutorContext,
readProjectsConfigurationFromProjectGraph,
} from '@nx/devkit';
export async function startRemoteIterators(
options: StartRemoteIteratorsOptions,
context: ExecutorContext,
startRemoteFn: StartRemoteFn,
pathToManifestFile: string | undefined,
pluginName: 'react' | 'angular' = 'react',
isServer = false
) {
const nxBin = require.resolve('nx/bin/nx');
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(context.projectGraph);
const project = workspaceProjects[context.projectName];
const moduleFederationConfig = getModuleFederationConfig(
project.targets.build.options.tsConfig,
context.root,
project.root,
pluginName
);
const remoteNames = options.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: project.name,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
remotes.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
project.name.replace(/-/g, '_'),
]);
const staticRemotesConfig = isServer
? parseStaticSsrRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
)
: parseStaticRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options,
isServer ? 'server' : 'build'
);
const devRemoteIters = await startRemoteFn(
remotes.devRemotes,
workspaceProjects,
options,
context,
'serve'
);
const staticRemotesIter = isServer
? startSsrStaticRemotesFileServer(staticRemotesConfig, context, options)
: startStaticRemotesFileServer(staticRemotesConfig, context, options);
isServer
? startSsrRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: options.sslCert,
pathToKey: options.sslKey,
}
: undefined
)
: startRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: options.sslCert,
pathToKey: options.sslKey,
}
: undefined
);
return {
remotes,
devRemoteIters,
staticRemotesIter,
};
}

View File

@ -1,14 +1,15 @@
import { type ExecutorContext, workspaceRoot } from '@nx/devkit';
import { type Schema } from '../schema';
import { type StaticRemotesOptions } from './models';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { join } from 'path';
import { cpSync } from 'fs';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
import type { StaticRemotesConfig } from '../../utils';
export function startStaticRemotesFileServer(
staticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: Schema
options: StaticRemotesOptions,
forceMoveToCommonLocation = false
) {
if (
!staticRemotesConfig.remotes ||
@ -16,15 +17,17 @@ export function startStaticRemotesFileServer(
) {
return;
}
let shouldMoveToCommonLocation = false;
let shouldMoveToCommonLocation = forceMoveToCommonLocation || false;
let commonOutputDirectory: string;
for (const app of staticRemotesConfig.remotes) {
const remoteBasePath = staticRemotesConfig.config[app].basePath;
if (!commonOutputDirectory) {
commonOutputDirectory = remoteBasePath;
} else if (commonOutputDirectory !== remoteBasePath) {
shouldMoveToCommonLocation = true;
break;
if (!forceMoveToCommonLocation) {
for (const app of staticRemotesConfig.remotes) {
const remoteBasePath = staticRemotesConfig.config[app].basePath;
if (!commonOutputDirectory) {
commonOutputDirectory = remoteBasePath;
} else if (commonOutputDirectory !== remoteBasePath) {
shouldMoveToCommonLocation = true;
break;
}
}
}
@ -62,3 +65,21 @@ export function startStaticRemotesFileServer(
);
return staticRemotesIter;
}
export async function* startSsrStaticRemotesFileServer(
staticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: StaticRemotesOptions
) {
const staticRemotesIter = startStaticRemotesFileServer(
staticRemotesConfig,
context,
options,
true
);
if (!staticRemotesIter) {
yield { success: true };
return;
}
yield* staticRemotesIter;
}

View File

@ -0,0 +1,2 @@
export * from './normalize-options';
export * from './start-remotes';

View File

@ -0,0 +1,29 @@
import {
ExecutorContext,
parseTargetString,
readTargetOptions,
} from '@nx/devkit';
import {
ModuleFederationDevServerOptions,
NormalizedModuleFederationDevServerOptions,
} from '../schema';
export function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}
export function normalizeOptions(
options: ModuleFederationDevServerOptions
): NormalizedModuleFederationDevServerOptions {
return {
...options,
devRemotes: options.devRemotes ?? [],
verbose: options.verbose ?? false,
};
}

View File

@ -0,0 +1,62 @@
import { ExecutorContext, ProjectConfiguration, runExecutor } from '@nx/devkit';
import { NormalizedModuleFederationDevServerOptions } from '../schema';
export async function startRemotes(
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: Pick<
NormalizedModuleFederationDevServerOptions,
'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose'
>,
context: ExecutorContext,
target: 'serve' | 'serve-static' = 'serve'
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of remotes) {
const remoteProjectServeTarget = workspaceProjects[app].targets[target];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(
r
): r is {
remoteName: string;
configuration: string;
} => typeof r !== 'string' && r.remoteName === app
)?.configuration;
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides =
target === 'serve'
? {
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
...defaultOverrides,
}
: { ...defaultOverrides };
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
return remoteIters;
}

View File

@ -1,162 +1,22 @@
import {
ExecutorContext,
logger,
parseTargetString,
readTargetOptions,
runExecutor,
workspaceRoot,
} from '@nx/devkit';
import { ExecutorContext, logger } from '@nx/devkit';
import devServerExecutor from '@nx/webpack/src/executors/dev-server/dev-server.impl';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { ModuleFederationDevServerOptions } from './schema';
import {
getModuleFederationConfig,
getRemotes,
startRemoteProxies,
parseStaticRemotesConfig,
type StaticRemotesConfig,
} from '@nx/module-federation/src/utils';
import { startRemoteIterators } from '@nx/module-federation/src/executors/utils';
import {
combineAsyncIterables,
createAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { cpSync, existsSync } from 'fs';
import { existsSync } from 'fs';
import { extname, join } from 'path';
import { buildStaticRemotes } from '../../utils/build-static.remotes';
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}
function startStaticRemotesFileServer(
staticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: ModuleFederationDevServerOptions
) {
if (
!staticRemotesConfig.remotes ||
staticRemotesConfig.remotes.length === 0
) {
return;
}
let shouldMoveToCommonLocation = false;
let commonOutputDirectory: string;
for (const app of staticRemotesConfig.remotes) {
const remoteBasePath = staticRemotesConfig.config[app].basePath;
if (!commonOutputDirectory) {
commonOutputDirectory = remoteBasePath;
} else if (commonOutputDirectory !== remoteBasePath) {
shouldMoveToCommonLocation = true;
break;
}
}
if (shouldMoveToCommonLocation) {
commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of staticRemotesConfig.remotes) {
const remoteConfig = staticRemotesConfig.config[app];
cpSync(
remoteConfig.outputPath,
join(commonOutputDirectory, remoteConfig.urlSegment),
{
force: true,
recursive: true,
}
);
}
}
const staticRemotesIter = fileServerExecutor(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
cacheSeconds: -1,
},
context
);
return staticRemotesIter;
}
async function startRemotes(
remotes: string[],
context: ExecutorContext,
options: ModuleFederationDevServerOptions,
target: 'serve' | 'serve-static' = 'serve'
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of remotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets[target];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(
r
): r is {
remoteName: string;
configuration: string;
} => typeof r !== 'string' && r.remoteName === app
)?.configuration;
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides =
target === 'serve'
? {
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
...defaultOverrides,
}
: { ...defaultOverrides };
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
return remoteIters;
}
import { getBuildOptions, normalizeOptions, startRemotes } from './lib';
export default async function* moduleFederationDevServer(
options: ModuleFederationDevServerOptions,
schema: ModuleFederationDevServerOptions,
context: ExecutorContext
): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
const options = normalizeOptions(schema);
const currIter = options.static
? fileServerExecutor(
{
@ -201,74 +61,14 @@ export default async function* moduleFederationDevServer(
return yield* currIter;
}
const moduleFederationConfig = getModuleFederationConfig(
buildOptions.tsConfig,
context.root,
p.root,
'react'
);
const remoteNames = options.devRemotes?.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: context.projectName,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
remotes.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
p.name.replace(/-/g, '_'),
]);
const staticRemotesConfig = parseStaticRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options
);
const devRemoteIters = await startRemotes(
remotes.devRemotes,
context,
options,
'serve'
);
const staticRemotesIter = startStaticRemotesFileServer(
staticRemotesConfig,
context,
options
);
startRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: join(workspaceRoot, options.sslCert),
pathToKey: join(workspaceRoot, options.sslKey),
}
: undefined
);
const { staticRemotesIter, devRemoteIters, remotes } =
await startRemoteIterators(
options,
context,
startRemotes,
pathToManifestFile,
'react'
);
return yield* combineAsyncIterables(
currIter,

View File

@ -1,17 +1,19 @@
import { WebDevServerOptions } from '@nx/webpack';
import { DevRemoteDefinition } from '@nx/module-federation/src/executors/utils';
export type ModuleFederationDevServerOptions = WebDevServerOptions & {
devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
devRemotes?: DevRemoteDefinition[];
skipRemotes?: string[];
static?: boolean;
isInitialHost?: boolean;
parallel?: number;
staticRemotesPort?: number;
pathToManifestFile?: string;
verbose?: boolean;
};
export type NormalizedModuleFederationDevServerOptions =
ModuleFederationDevServerOptions & {
devRemotes: DevRemoteDefinition[];
verbose: boolean;
};

View File

@ -0,0 +1,2 @@
export * from './normalize-options';
export * from './start-remotes';

View File

@ -0,0 +1,34 @@
import {
ModuleFederationSsrDevServerOptions,
NormalizedModuleFederationSsrDevServerOptions,
} from '../schema';
import { join } from 'path';
import {
workspaceRoot,
ExecutorContext,
parseTargetString,
readTargetOptions,
} from '@nx/devkit';
export function normalizeOptions(
options: ModuleFederationSsrDevServerOptions
): NormalizedModuleFederationSsrDevServerOptions {
return {
...options,
devRemotes: options.devRemotes ?? [],
verbose: options.verbose ?? false,
ssl: options.ssl ?? false,
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
};
}
export function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}

View File

@ -0,0 +1,58 @@
import { ModuleFederationSsrDevServerOptions } from '../schema';
import { runExecutor, ExecutorContext, ProjectConfiguration } from '@nx/devkit';
export async function startRemotes(
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: Partial<
Pick<
ModuleFederationSsrDevServerOptions,
'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose'
>
>,
context: ExecutorContext
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
const target = 'serve';
for (const app of remotes) {
const remoteProjectServeTarget = workspaceProjects[app].targets[target];
const isUsingModuleFederationSsrDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-ssr-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(remote): remote is { remoteName: string; configuration: string } =>
typeof remote !== 'string' && remote.remoteName === app
)?.configuration;
{
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides = {
watch: true,
...defaultOverrides,
...(isUsingModuleFederationSsrDevServerExecutor
? { isInitialHost: false }
: {}),
};
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
}
return remoteIters;
}

View File

@ -1,257 +1,21 @@
import {
ExecutorContext,
logger,
parseTargetString,
readTargetOptions,
runExecutor,
workspaceRoot,
} from '@nx/devkit';
import { ExecutorContext, logger } from '@nx/devkit';
import ssrDevServerExecutor from '@nx/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl';
import { WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema';
import { extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
parseStaticSsrRemotesConfig,
type StaticRemotesConfig,
startSsrRemoteProxies,
} from '@nx/module-federation/src/utils';
import { startRemoteIterators } from '@nx/module-federation/src/executors/utils';
import {
combineAsyncIterables,
createAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import { fork } from 'child_process';
import { cpSync, createWriteStream, existsSync } from 'fs';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { existsSync } from 'fs';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
type ModuleFederationSsrDevServerOptions = WebSsrDevServerOptions & {
devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
skipRemotes?: string[];
host: string;
pathToManifestFile?: string;
staticRemotesPort?: number;
parallel?: number;
ssl?: boolean;
sslKey?: string;
sslCert?: string;
isInitialHost?: boolean;
};
function normalizeOptions(
options: ModuleFederationSsrDevServerOptions
): ModuleFederationSsrDevServerOptions {
return {
...options,
ssl: options.ssl ?? false,
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
};
}
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}
async function* startSsrStaticRemotesFileServer(
ssrStaticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: ModuleFederationSsrDevServerOptions
):
| AsyncGenerator<{ success: boolean; baseUrl?: string }>
| AsyncIterable<{ success: boolean; baseUrl?: string }> {
if (ssrStaticRemotesConfig.remotes.length === 0) {
yield { success: true };
return;
}
// The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory
const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of ssrStaticRemotesConfig.remotes) {
const remoteConfig = ssrStaticRemotesConfig.config[app];
cpSync(
remoteConfig.outputPath,
join(commonOutputDirectory, remoteConfig.urlSegment),
{
force: true,
recursive: true,
}
);
}
const staticRemotesIter = fileServerExecutor(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
cacheSeconds: -1,
},
context
);
yield* staticRemotesIter;
}
async function startRemotes(
remotes: string[],
context: ExecutorContext,
options: ModuleFederationSsrDevServerOptions
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
const target = 'serve';
for (const app of remotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets[target];
const isUsingModuleFederationSsrDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-ssr-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(remote): remote is { remoteName: string; configuration: string } =>
typeof remote !== 'string' && remote.remoteName === app
)?.configuration;
{
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides = {
watch: true,
...defaultOverrides,
...(isUsingModuleFederationSsrDevServerExecutor
? { isInitialHost: false }
: {}),
};
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
}
return remoteIters;
}
async function buildSsrStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,
nxBin,
context: ExecutorContext,
options: ModuleFederationSsrDevServerOptions
) {
if (!staticRemotesConfig.remotes.length) {
return;
}
logger.info(
`Nx is building ${staticRemotesConfig.remotes.length} static remotes...`
);
const mapLocationOfRemotes: Record<string, string> = {};
for (const remoteApp of staticRemotesConfig.remotes) {
mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${
staticRemotesConfig.config[remoteApp].urlSegment
}`;
}
await new Promise<void>((resolve) => {
const childProcess = fork(
nxBin,
[
'run-many',
'--target=server',
'--projects',
staticRemotesConfig.remotes.join(','),
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
// Add a listener to the child process to capture the build log
const remoteBuildLogFile = join(
workspaceDataDirectory,
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
);
const remoteBuildLogStream = createWriteStream(remoteBuildLogFile);
childProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
remoteBuildLogStream.write(stdoutString);
// in addition to writing into the stdout stream, also show error directly in console
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
if (stdoutString.includes('ERROR in')) {
logger.log(stdoutString);
}
if (stdoutString.includes('Successfully ran target server')) {
childProcess.stdout.removeAllListeners('data');
logger.info(
`Nx Built ${staticRemotesConfig.remotes.length} static remotes.`
);
resolve();
}
});
process.on('SIGTERM', () => childProcess.kill('SIGTERM'));
process.on('exit', () => childProcess.kill('SIGTERM'));
});
return mapLocationOfRemotes;
}
import { ModuleFederationSsrDevServerOptions } from './schema';
import { getBuildOptions, normalizeOptions, startRemotes } from './lib';
export default async function* moduleFederationSsrDevServer(
ssrDevServerOptions: ModuleFederationSsrDevServerOptions,
context: ExecutorContext
) {
const options = normalizeOptions(ssrDevServerOptions);
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
let iter: any = ssrDevServerExecutor(options, context);
const projectConfig =
context.projectsConfigurations.projects[context.projectName];
@ -285,74 +49,15 @@ export default async function* moduleFederationSsrDevServer(
return yield* iter;
}
const moduleFederationConfig = getModuleFederationConfig(
buildOptions.tsConfig,
context.root,
projectConfig.root,
'react'
);
const remoteNames = options.devRemotes?.map((remote) =>
typeof remote === 'string' ? remote : remote.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: context.projectName,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
remotes.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
projectConfig.name.replace(/-/g, '_'),
]);
const staticRemotesConfig = parseStaticSsrRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildSsrStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options
);
const devRemoteIters = await startRemotes(
remotes.devRemotes,
context,
options
);
const staticRemotesIter = startSsrStaticRemotesFileServer(
staticRemotesConfig,
context,
options
);
startSsrRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: options.sslCert,
pathToKey: options.sslKey,
}
: undefined
);
const { staticRemotesIter, devRemoteIters, remotes } =
await startRemoteIterators(
options,
context,
startRemotes,
pathToManifestFile,
'react',
true
);
const combined = combineAsyncIterables(staticRemotesIter, ...devRemoteIters);

View File

@ -0,0 +1,29 @@
import { WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema';
import { DevRemoteDefinition } from '@nx/module-federation/src/executors/utils';
export type ModuleFederationSsrDevServerOptions = WebSsrDevServerOptions & {
devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
skipRemotes?: string[];
host: string;
pathToManifestFile?: string;
staticRemotesPort?: number;
parallel?: number;
ssl?: boolean;
sslKey?: string;
sslCert?: string;
isInitialHost?: boolean;
verbose?: boolean;
};
export type NormalizedModuleFederationSsrDevServerOptions =
ModuleFederationSsrDevServerOptions & {
devRemotes: DevRemoteDefinition[];
verbose: boolean;
};

View File

@ -16,7 +16,7 @@ import {
parseStaticRemotesConfig,
StaticRemotesConfig,
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from '../../utils/build-static.remotes';
import { buildStaticRemotes } from '@nx/module-federation/src/executors/utils';
import { fork } from 'child_process';
import type { WebpackExecutorOptions } from '@nx/webpack';
import * as process from 'node:process';

View File

@ -0,0 +1,2 @@
export * from './normalize-options';
export * from './start-remotes';

View File

@ -0,0 +1,29 @@
import {
ExecutorContext,
parseTargetString,
readTargetOptions,
} from '@nx/devkit';
import {
ModuleFederationDevServerOptions,
NormalizedModuleFederationDevServerOptions,
} from '../schema';
export function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}
export function normalizeOptions(
options: ModuleFederationDevServerOptions
): NormalizedModuleFederationDevServerOptions {
return {
...options,
devRemotes: options.devRemotes ?? [],
verbose: options.verbose ?? false,
};
}

View File

@ -0,0 +1,64 @@
import { ModuleFederationDevServerOptions } from '../schema';
import { ProjectConfiguration, ExecutorContext, runExecutor } from '@nx/devkit';
export async function startRemotes(
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: Partial<
Pick<
ModuleFederationDevServerOptions,
'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose'
>
>,
context: ExecutorContext,
target: 'serve' | 'serve-static' = 'serve'
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of remotes) {
const remoteProjectServeTarget = workspaceProjects[app].targets[target];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(
r
): r is {
remoteName: string;
configuration: string;
} => typeof r !== 'string' && r.remoteName === app
)?.configuration;
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides =
target === 'serve'
? {
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
...defaultOverrides,
}
: { ...defaultOverrides };
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
return remoteIters;
}

View File

@ -1,162 +1,22 @@
import {
ExecutorContext,
logger,
parseTargetString,
readTargetOptions,
runExecutor,
workspaceRoot,
} from '@nx/devkit';
import { ExecutorContext, logger } from '@nx/devkit';
import {
combineAsyncIterables,
createAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { cpSync, existsSync } from 'fs';
import { existsSync } from 'fs';
import { extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
parseStaticRemotesConfig,
type StaticRemotesConfig,
startRemoteProxies,
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
import { startRemoteIterators } from '@nx/module-federation/src/executors/utils';
import devServerExecutor from '../dev-server/dev-server.impl';
import { ModuleFederationDevServerOptions } from './schema';
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}
function startStaticRemotesFileServer(
staticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: ModuleFederationDevServerOptions
) {
if (
!staticRemotesConfig.remotes ||
staticRemotesConfig.remotes.length === 0
) {
return;
}
let shouldMoveToCommonLocation = false;
let commonOutputDirectory: string;
for (const app of staticRemotesConfig.remotes) {
const remoteBasePath = staticRemotesConfig.config[app].basePath;
if (!commonOutputDirectory) {
commonOutputDirectory = remoteBasePath;
} else if (commonOutputDirectory !== remoteBasePath) {
shouldMoveToCommonLocation = true;
break;
}
}
if (shouldMoveToCommonLocation) {
commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of staticRemotesConfig.remotes) {
const remoteConfig = staticRemotesConfig.config[app];
cpSync(
remoteConfig.outputPath,
join(commonOutputDirectory, remoteConfig.urlSegment),
{
force: true,
recursive: true,
}
);
}
}
const staticRemotesIter = fileServerExecutor(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
cacheSeconds: -1,
},
context
);
return staticRemotesIter;
}
async function startRemotes(
remotes: string[],
context: ExecutorContext,
options: ModuleFederationDevServerOptions,
target: 'serve' | 'serve-static' = 'serve'
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of remotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets[target];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(
r
): r is {
remoteName: string;
configuration: string;
} => typeof r !== 'string' && r.remoteName === app
)?.configuration;
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides =
target === 'serve'
? {
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
...defaultOverrides,
}
: { ...defaultOverrides };
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
return remoteIters;
}
import { getBuildOptions, normalizeOptions, startRemotes } from './lib';
export default async function* moduleFederationDevServer(
options: ModuleFederationDevServerOptions,
schema: ModuleFederationDevServerOptions,
context: ExecutorContext
): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
const options = normalizeOptions(schema);
const currIter = options.static
? fileServerExecutor(
{
@ -201,74 +61,14 @@ export default async function* moduleFederationDevServer(
return yield* currIter;
}
const moduleFederationConfig = getModuleFederationConfig(
buildOptions.tsConfig,
context.root,
p.root,
'react'
);
const remoteNames = options.devRemotes?.map((r) =>
typeof r === 'string' ? r : r.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: context.projectName,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
remotes.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
p.name.replace(/-/g, '_'),
]);
const staticRemotesConfig = parseStaticRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options
);
const devRemoteIters = await startRemotes(
remotes.devRemotes,
context,
options,
'serve'
);
const staticRemotesIter = startStaticRemotesFileServer(
staticRemotesConfig,
context,
options
);
startRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: join(workspaceRoot, options.sslCert),
pathToKey: join(workspaceRoot, options.sslKey),
}
: undefined
);
const { staticRemotesIter, devRemoteIters, remotes } =
await startRemoteIterators(
options,
context,
startRemotes,
pathToManifestFile,
'react'
);
return yield* combineAsyncIterables(
currIter,

View File

@ -15,4 +15,11 @@ export type ModuleFederationDevServerOptions = DevServerExecutorSchema & {
parallel?: number;
staticRemotesPort?: number;
pathToManifestFile?: string;
verbose?: boolean;
};
export type NormalizedModuleFederationDevServerOptions =
ModuleFederationDevServerOptions & {
devRemotes: DevServerExecutorSchema['devRemotes'];
verbose: boolean;
};

View File

@ -0,0 +1,2 @@
export * from './normalize-options';
export * from './start-remotes';

View File

@ -0,0 +1,34 @@
import {
ModuleFederationSsrDevServerOptions,
NormalizedModuleFederationSsrDevServerOptions,
} from '../schema';
import { join } from 'path';
import {
workspaceRoot,
ExecutorContext,
parseTargetString,
readTargetOptions,
} from '@nx/devkit';
export function normalizeOptions(
options: ModuleFederationSsrDevServerOptions
): NormalizedModuleFederationSsrDevServerOptions {
return {
...options,
devRemotes: options.devRemotes ?? [],
verbose: options.verbose ?? false,
ssl: options.ssl ?? false,
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
};
}
export function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}

View File

@ -0,0 +1,58 @@
import { ModuleFederationSsrDevServerOptions } from '../schema';
import { runExecutor, ExecutorContext, ProjectConfiguration } from '@nx/devkit';
export async function startRemotes(
remotes: string[],
workspaceProjects: Record<string, ProjectConfiguration>,
options: Partial<
Pick<
ModuleFederationSsrDevServerOptions,
'devRemotes' | 'host' | 'ssl' | 'sslCert' | 'sslKey' | 'verbose'
>
>,
context: ExecutorContext
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
const target = 'serve';
for (const app of remotes) {
const remoteProjectServeTarget = workspaceProjects[app].targets[target];
const isUsingModuleFederationSsrDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-ssr-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(remote): remote is { remoteName: string; configuration: string } =>
typeof remote !== 'string' && remote.remoteName === app
)?.configuration;
{
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides = {
watch: true,
...defaultOverrides,
...(isUsingModuleFederationSsrDevServerExecutor
? { isInitialHost: false }
: {}),
};
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
}
return remoteIters;
}

View File

@ -1,256 +1,22 @@
import {
ExecutorContext,
logger,
parseTargetString,
readTargetOptions,
runExecutor,
workspaceRoot,
} from '@nx/devkit';
import { ExecutorContext, logger } from '@nx/devkit';
import { extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
parseStaticSsrRemotesConfig,
type StaticRemotesConfig,
startSsrRemoteProxies,
} from '@nx/module-federation/src/utils';
import { RspackSsrDevServerOptions } from '../ssr-dev-server/schema';
import { startRemoteIterators } from '@nx/module-federation/src/executors/utils';
import ssrDevServerExecutor from '../ssr-dev-server/ssr-dev-server.impl';
import {
combineAsyncIterables,
createAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import { fork } from 'child_process';
import { cpSync, createWriteStream, existsSync } from 'fs';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
import { existsSync } from 'fs';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
type ModuleFederationSsrDevServerOptions = RspackSsrDevServerOptions & {
devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
skipRemotes?: string[];
host: string;
pathToManifestFile?: string;
staticRemotesPort?: number;
parallel?: number;
ssl?: boolean;
sslKey?: string;
sslCert?: string;
isInitialHost?: boolean;
};
function normalizeOptions(
options: ModuleFederationSsrDevServerOptions
): ModuleFederationSsrDevServerOptions {
return {
...options,
ssl: options.ssl ?? false,
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
};
}
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
const target = parseTargetString(buildTarget, context);
const buildOptions = readTargetOptions(target, context);
return {
...buildOptions,
};
}
function startSsrStaticRemotesFileServer(
ssrStaticRemotesConfig: StaticRemotesConfig,
context: ExecutorContext,
options: ModuleFederationSsrDevServerOptions
) {
if (ssrStaticRemotesConfig.remotes.length === 0) {
return;
}
// The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory
const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of ssrStaticRemotesConfig.remotes) {
const remoteConfig = ssrStaticRemotesConfig.config[app];
cpSync(
remoteConfig.outputPath,
join(commonOutputDirectory, remoteConfig.urlSegment),
{
force: true,
recursive: true,
}
);
}
const staticRemotesIter = fileServerExecutor(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
cacheSeconds: -1,
},
context
);
return staticRemotesIter;
}
async function startRemotes(
remotes: string[],
context: ExecutorContext,
options: ModuleFederationSsrDevServerOptions
) {
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
const target = 'serve';
for (const app of remotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets[target];
const isUsingModuleFederationSsrDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-ssr-dev-server'
);
const configurationOverride = options.devRemotes?.find(
(remote): remote is { remoteName: string; configuration: string } =>
typeof remote !== 'string' && remote.remoteName === app
)?.configuration;
{
const defaultOverrides = {
...(options.host ? { host: options.host } : {}),
...(options.ssl ? { ssl: options.ssl } : {}),
...(options.sslCert ? { sslCert: options.sslCert } : {}),
...(options.sslKey ? { sslKey: options.sslKey } : {}),
};
const overrides = {
watch: true,
...defaultOverrides,
...(isUsingModuleFederationSsrDevServerExecutor
? { isInitialHost: false }
: {}),
};
remoteIters.push(
await runExecutor(
{
project: app,
target,
configuration: configurationOverride ?? context.configurationName,
},
overrides,
context
)
);
}
}
return remoteIters;
}
async function buildSsrStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,
nxBin,
context: ExecutorContext,
options: ModuleFederationSsrDevServerOptions
) {
if (!staticRemotesConfig.remotes.length) {
return;
}
logger.info(
`Nx is building ${staticRemotesConfig.remotes.length} static remotes...`
);
const mapLocationOfRemotes: Record<string, string> = {};
for (const remoteApp of staticRemotesConfig.remotes) {
mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${
staticRemotesConfig.config[remoteApp].urlSegment
}`;
}
await new Promise<void>((resolve) => {
const childProcess = fork(
nxBin,
[
'run-many',
'--target=server',
'--projects',
staticRemotesConfig.remotes.join(','),
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
// Add a listener to the child process to capture the build log
const remoteBuildLogFile = join(
workspaceDataDirectory,
// eslint-disable-next-line
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
);
const remoteBuildLogStream = createWriteStream(remoteBuildLogFile);
childProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
remoteBuildLogStream.write(stdoutString);
// in addition to writing into the stdout stream, also show error directly in console
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
if (stdoutString.includes('ERROR in')) {
logger.log(stdoutString);
}
if (stdoutString.includes('Successfully ran target server')) {
childProcess.stdout.removeAllListeners('data');
logger.info(
`Nx Built ${staticRemotesConfig.remotes.length} static remotes.`
);
resolve();
}
});
process.on('SIGTERM', () => childProcess.kill('SIGTERM'));
process.on('exit', () => childProcess.kill('SIGTERM'));
});
return mapLocationOfRemotes;
}
import { ModuleFederationSsrDevServerOptions } from './schema';
import { getBuildOptions, normalizeOptions, startRemotes } from './lib';
export default async function* moduleFederationSsrDevServer(
ssrDevServerOptions: ModuleFederationSsrDevServerOptions,
context: ExecutorContext
) {
const options = normalizeOptions(ssrDevServerOptions);
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
const iter = ssrDevServerExecutor(options, context);
const projectConfig =
context.projectsConfigurations.projects[context.projectName];
@ -284,74 +50,15 @@ export default async function* moduleFederationSsrDevServer(
return yield* iter;
}
const moduleFederationConfig = getModuleFederationConfig(
buildOptions.tsConfig,
context.root,
projectConfig.root,
'react'
);
const remoteNames = options.devRemotes?.map((remote) =>
typeof remote === 'string' ? remote : remote.remoteName
);
const remotes = getRemotes(
remoteNames,
options.skipRemotes,
moduleFederationConfig,
{
projectName: context.projectName,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
options.staticRemotesPort ??= remotes.staticRemotePort;
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
...(
remotes.devRemotes.map((r) =>
typeof r === 'string' ? r : r.remoteName
) ?? []
).map((r) => r.replace(/-/g, '_')),
projectConfig.name.replace(/-/g, '_'),
]);
const staticRemotesConfig = parseStaticSsrRemotesConfig(
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
context
);
const mappedLocationsOfStaticRemotes = await buildSsrStaticRemotes(
staticRemotesConfig,
nxBin,
context,
options
);
const devRemoteIters = await startRemotes(
remotes.devRemotes,
context,
options
);
const staticRemotesIter = startSsrStaticRemotesFileServer(
staticRemotesConfig,
context,
options
);
startSsrRemoteProxies(
staticRemotesConfig,
mappedLocationsOfStaticRemotes,
options.ssl
? {
pathToCert: options.sslCert,
pathToKey: options.sslKey,
}
: undefined
);
const { staticRemotesIter, devRemoteIters, remotes } =
await startRemoteIterators(
options,
context,
startRemotes,
pathToManifestFile,
'react',
true
);
return yield* combineAsyncIterables(
iter,

View File

@ -0,0 +1,29 @@
import { DevRemoteDefinition } from '@nx/module-federation/src/executors/utils';
import { RspackSsrDevServerOptions } from '../ssr-dev-server/schema';
export type ModuleFederationSsrDevServerOptions = RspackSsrDevServerOptions & {
devRemotes?: (
| string
| {
remoteName: string;
configuration: string;
}
)[];
skipRemotes?: string[];
host: string;
pathToManifestFile?: string;
staticRemotesPort?: number;
parallel?: number;
ssl?: boolean;
sslKey?: string;
sslCert?: string;
isInitialHost?: boolean;
verbose?: boolean;
};
export type NormalizedModuleFederationSsrDevServerOptions =
ModuleFederationSsrDevServerOptions & {
devRemotes: DevRemoteDefinition[];
verbose: boolean;
};

View File

@ -22,7 +22,7 @@ import {
parseStaticRemotesConfig,
StaticRemotesConfig,
} from '@nx/module-federation/src/utils';
import { buildStaticRemotes } from '../../utils/module-federation/build-static.remotes';
import { buildStaticRemotes } from '@nx/module-federation/src/executors/utils';
import { ModuleFederationDevServerOptions } from '../module-federation-dev-server/schema';
import type { RspackExecutorSchema } from '../rspack/schema';
import { ModuleFederationStaticServerSchema } from './schema';

View File

@ -1,97 +0,0 @@
import { ExecutorContext } from '@nx/devkit';
import { createWriteStream } from 'fs';
import { fork } from 'node:child_process';
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { logger } from 'nx/src/utils/logger';
import { join } from 'path';
import { ModuleFederationDevServerOptions } from '../../executors/module-federation-dev-server/schema';
import type { StaticRemotesConfig } from '@nx/module-federation/src/utils';
export async function buildStaticRemotes(
staticRemotesConfig: StaticRemotesConfig,
nxBin,
context: ExecutorContext,
options: ModuleFederationDevServerOptions
) {
if (!staticRemotesConfig.remotes.length) {
return;
}
logger.info(
`NX Building ${staticRemotesConfig.remotes.length} static remotes...`
);
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of staticRemotesConfig.remotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${
staticRemotesConfig.config[app].urlSegment
}`;
}
await new Promise<void>((res, rej) => {
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${staticRemotesConfig.remotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
// File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log'
const remoteBuildLogFile = join(
workspaceDataDirectory,
// eslint-disable-next-line
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
);
const stdoutStream = createWriteStream(remoteBuildLogFile);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
stdoutStream.write(stdoutString);
// in addition to writing into the stdout stream, also show error directly in console
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
if (stdoutString.includes('ERROR in')) {
logger.log(stdoutString);
}
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(
`NX Built ${staticRemotesConfig.remotes.length} static remotes`
);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.once('exit', (code) => {
stdoutStream.end();
staticProcess.stdout.removeAllListeners('data');
staticProcess.stderr.removeAllListeners('data');
if (code !== 0) {
rej(
`Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}`
);
} else {
res();
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
return mappedLocationOfRemotes;
}