From 09b94b923ca1456456c4120012f6e6d2af9b0433 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 5 Apr 2024 11:21:54 +0100 Subject: [PATCH 1/2] fix(angular): serve dynamic remotes statically in their own processes --- .../lib/start-dev-remotes.ts | 33 +++++++++---------- .../module-federation-dev-server.impl.ts | 18 +++++++--- .../module-federation/get-remotes-for-host.ts | 28 ++++++++++++---- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts b/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts index 1cacd71014892..3bda86f5ee58b 100644 --- a/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts +++ b/packages/angular/src/executors/module-federation-dev-server/lib/start-dev-remotes.ts @@ -5,41 +5,38 @@ import { runExecutor, } from '@nx/devkit'; -export async function startDevRemotes( - remotes: { - remotePorts: any[]; - staticRemotes: string[]; - devRemotes: string[]; - }, +export async function startRemotes( + remotes: string[], workspaceProjects: Record, options: Schema, - context: ExecutorContext + context: ExecutorContext, + target: 'serve' | 'serve-static' = 'serve' ) { - const devRemotesIters: AsyncIterable<{ success: boolean }>[] = []; - for (const app of remotes.devRemotes) { - if (!workspaceProjects[app].targets?.['serve']) { - throw new Error(`Could not find "serve" target in "${app}" project.`); - } else if (!workspaceProjects[app].targets?.['serve'].executor) { + const remoteIters: AsyncIterable<{ success: boolean }>[] = []; + for (const app of remotes) { + if (!workspaceProjects[app].targets?.[target]) { + throw new Error(`Could not find "${target}" target in "${app}" project.`); + } else if (!workspaceProjects[app].targets?.[target].executor) { throw new Error( - `Could not find executor for "serve" target in "${app}" project.` + `Could not find executor for "${target}" target in "${app}" project.` ); } const [collection, executor] = - workspaceProjects[app].targets['serve'].executor.split(':'); + workspaceProjects[app].targets[target].executor.split(':'); const isUsingModuleFederationDevServerExecutor = executor.includes( 'module-federation-dev-server' ); - devRemotesIters.push( + remoteIters.push( await runExecutor( { project: app, - target: 'serve', + target, configuration: context.configurationName, }, { - verbose: options.verbose ?? false, + ...(target === 'serve' ? { verbose: options.verbose ?? false } : {}), ...(isUsingModuleFederationDevServerExecutor ? { isInitialHost: false } : {}), @@ -48,5 +45,5 @@ export async function startDevRemotes( ) ); } - return devRemotesIters; + return remoteIters; } diff --git a/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index 3e2e6fc466cb5..bd9af8c9336cc 100644 --- a/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -8,7 +8,7 @@ import { buildStaticRemotes, normalizeOptions, parseStaticRemotesConfig, - startDevRemotes, + startRemotes, startStaticRemotesFileServer, } from './lib'; import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await'; @@ -138,11 +138,20 @@ export async function* moduleFederationDevServerExecutor( ); await buildStaticRemotes(staticRemotesConfig, nxBin, context, options); - const devRemoteIters = await startDevRemotes( - remotes, + const devRemoteIters = await startRemotes( + remotes.devRemotes, workspaceProjects, options, - context + context, + 'serve' + ); + + const dynamicRemoteIters = await startRemotes( + remotes.dynamicRemotes, + workspaceProjects, + options, + context, + 'serve-static' ); const staticRemotesIter = @@ -159,6 +168,7 @@ export async function* moduleFederationDevServerExecutor( return yield* combineAsyncIterables( removeBaseUrlEmission(currIter), ...devRemoteIters.map(removeBaseUrlEmission), + ...dynamicRemoteIters.map(removeBaseUrlEmission), ...(staticRemotesIter ? [removeBaseUrlEmission(staticRemotesIter)] : []), createAsyncIterable<{ success: true; baseUrl: string }>( async ({ next, done }) => { diff --git a/packages/webpack/src/utils/module-federation/get-remotes-for-host.ts b/packages/webpack/src/utils/module-federation/get-remotes-for-host.ts index 79888303d4d8c..2b84a32a7df7b 100644 --- a/packages/webpack/src/utils/module-federation/get-remotes-for-host.ts +++ b/packages/webpack/src/utils/module-federation/get-remotes-for-host.ts @@ -17,6 +17,7 @@ function extractRemoteProjectsFromConfig( pathToManifestFile?: string ) { const remotes = []; + const dynamicRemotes = []; if (pathToManifestFile && existsSync(pathToManifestFile)) { const moduleFederationManifestJson = readFileSync( pathToManifestFile, @@ -35,14 +36,14 @@ function extractRemoteProjectsFromConfig( typeof key === 'string' && typeof parsedManifest[key] === 'string' ) ) { - remotes.push(...Object.keys(parsedManifest)); + dynamicRemotes.push(...Object.keys(parsedManifest)); } } } const staticRemotes = config.remotes?.map((r) => (Array.isArray(r) ? r[0] : r)) ?? []; remotes.push(...staticRemotes); - return remotes; + return { remotes, dynamicRemotes }; } function collectRemoteProjects( @@ -64,7 +65,7 @@ function collectRemoteProjects( context.root, remoteProjectRoot ); - const remoteProjectRemotes = + const { remotes: remoteProjectRemotes } = extractRemoteProjectsFromConfig(remoteProjectConfig); remoteProjectRemotes.forEach((r) => @@ -80,7 +81,10 @@ export function getRemotes( pathToManifestFile?: string ) { const collectedRemotes = new Set(); - const remotes = extractRemoteProjectsFromConfig(config, pathToManifestFile); + const { remotes, dynamicRemotes } = extractRemoteProjectsFromConfig( + config, + pathToManifestFile + ); remotes.forEach((r) => collectRemoteProjects(r, collectedRemotes, context)); const remotesToSkip = new Set( findMatchingProjects(skipRemotes, context.projectGraph.nodes) ?? [] @@ -98,10 +102,14 @@ export function getRemotes( (r) => !remotesToSkip.has(r) ); + const knownDynamicRemotes = dynamicRemotes.filter( + (r) => !remotesToSkip.has(r) + ); + logger.info( `NX Starting module federation dev-server for ${chalk.bold( context.projectName - )} with ${knownRemotes.length} remotes` + )} with ${[...knownRemotes, ...knownDynamicRemotes].length} remotes` ); const devServeApps = new Set( @@ -113,14 +121,20 @@ export function getRemotes( ); const staticRemotes = knownRemotes.filter((r) => !devServeApps.has(r)); - const devServeRemotes = knownRemotes.filter((r) => devServeApps.has(r)); - const remotePorts = devServeRemotes.map( + const devServeRemotes = [...knownRemotes, ...dynamicRemotes].filter((r) => + devServeApps.has(r) + ); + const staticDynamicRemotes = knownDynamicRemotes.filter( + (r) => !devServeApps.has(r) + ); + const remotePorts = [...devServeRemotes, ...staticDynamicRemotes].map( (r) => context.projectGraph.nodes[r].data.targets['serve'].options.port ); return { staticRemotes, devRemotes: devServeRemotes, + dynamicRemotes: staticDynamicRemotes, remotePorts, }; } From 3c8d463884b969da0e0a38cf029f0371eb466e3c Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 5 Apr 2024 11:39:00 +0100 Subject: [PATCH 2/2] fix(react): serve dynamic remotes statically in their own processes --- .../module-federation-dev-server.json | 4 + .../module-federation-dev-server.impl.ts | 90 +++++++++++++------ .../module-federation-dev-server/schema.json | 4 + 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/docs/generated/packages/react/executors/module-federation-dev-server.json b/docs/generated/packages/react/executors/module-federation-dev-server.json index e8e479554b635..120f8ba40450c 100644 --- a/docs/generated/packages/react/executors/module-federation-dev-server.json +++ b/docs/generated/packages/react/executors/module-federation-dev-server.json @@ -108,6 +108,10 @@ "staticRemotesPort": { "type": "number", "description": "The port at which to serve the file-server for the static remotes." + }, + "pathToManifestFile": { + "type": "string", + "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root." } }, "presets": [] diff --git a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts index e362e139a42aa..4c6500d6c7547 100644 --- a/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts +++ b/packages/react/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts @@ -22,6 +22,8 @@ import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { fork } from 'node:child_process'; import { basename, dirname, join } from 'node:path'; import { createWriteStream, cpSync } from 'node:fs'; +import { existsSync } from 'fs'; +import { extname } from 'path'; type ModuleFederationDevServerOptions = WebDevServerOptions & { devRemotes?: string[]; @@ -30,6 +32,7 @@ type ModuleFederationDevServerOptions = WebDevServerOptions & { isInitialHost?: boolean; parallel?: number; staticRemotesPort?: number; + pathToManifestFile?: string; }; function getBuildOptions(buildTarget: string, context: ExecutorContext) { @@ -93,47 +96,48 @@ function startStaticRemotesFileServer( return staticRemotesIter; } -async function startDevRemotes( - remotes: { - remotePorts: any[]; - staticRemotes: string[]; - devRemotes: string[]; - }, +async function startRemotes( + remotes: string[], context: ExecutorContext, - options: ModuleFederationDevServerOptions + options: ModuleFederationDevServerOptions, + target: 'serve' | 'serve-static' = 'serve' ) { - const devRemoteIters: AsyncIterable<{ success: boolean }>[] = []; + const remoteIters: AsyncIterable<{ success: boolean }>[] = []; - for (const app of remotes.devRemotes) { + for (const app of remotes) { const remoteProjectServeTarget = - context.projectGraph.nodes[app].data.targets['serve']; + context.projectGraph.nodes[app].data.targets[target]; const isUsingModuleFederationDevServerExecutor = remoteProjectServeTarget.executor.includes( 'module-federation-dev-server' ); - devRemoteIters.push( + const overrides = + target === 'serve' + ? { + watch: true, + ...(options.host ? { host: options.host } : {}), + ...(options.ssl ? { ssl: options.ssl } : {}), + ...(options.sslCert ? { sslCert: options.sslCert } : {}), + ...(options.sslKey ? { sslKey: options.sslKey } : {}), + ...(isUsingModuleFederationDevServerExecutor + ? { isInitialHost: false } + : {}), + } + : {}; + remoteIters.push( await runExecutor( { project: app, - target: 'serve', + target, configuration: context.configurationName, }, - { - watch: true, - ...(options.host ? { host: options.host } : {}), - ...(options.ssl ? { ssl: options.ssl } : {}), - ...(options.sslCert ? { sslCert: options.sslCert } : {}), - ...(options.sslKey ? { sslKey: options.sslKey } : {}), - ...(isUsingModuleFederationDevServerExecutor - ? { isInitialHost: false } - : {}), - }, + overrides, context ) ); } - return devRemoteIters; + return remoteIters; } async function buildStaticRemotes( @@ -269,6 +273,29 @@ export default async function* moduleFederationDevServer( const p = context.projectsConfigurations.projects[context.projectName]; const buildOptions = getBuildOptions(options.buildTarget, context); + let pathToManifestFile = join( + context.root, + p.sourceRoot, + 'assets/module-federation.manifest.json' + ); + if (options.pathToManifestFile) { + const userPathToManifestFile = join( + context.root, + options.pathToManifestFile + ); + if (!existsSync(userPathToManifestFile)) { + throw new Error( + `The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".` + ); + } else if (extname(options.pathToManifestFile) !== '.json') { + throw new Error( + `The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.` + ); + } + + pathToManifestFile = userPathToManifestFile; + } + if (!options.isInitialHost) { return yield* currIter; } @@ -288,7 +315,8 @@ export default async function* moduleFederationDevServer( projectName: context.projectName, projectGraph: context.projectGraph, root: context.root, - } + }, + pathToManifestFile ); if (remotes.devRemotes.length > 0 && !initialStaticRemotesPorts) { @@ -309,7 +337,18 @@ export default async function* moduleFederationDevServer( ); await buildStaticRemotes(staticRemotesConfig, nxBin, context, options); - const devRemoteIters = await startDevRemotes(remotes, context, options); + const devRemoteIters = await startRemotes( + remotes.devRemotes, + context, + options, + 'serve' + ); + const dynamicRemotesIters = await startRemotes( + remotes.dynamicRemotes, + context, + options, + 'serve-static' + ); const staticRemotesIter = remotes.staticRemotes.length > 0 @@ -319,6 +358,7 @@ export default async function* moduleFederationDevServer( return yield* combineAsyncIterables( currIter, ...devRemoteIters, + ...dynamicRemotesIters, ...(staticRemotesIter ? [staticRemotesIter] : []), createAsyncIterable<{ success: true; baseUrl: string }>( async ({ next, done }) => { diff --git a/packages/react/src/executors/module-federation-dev-server/schema.json b/packages/react/src/executors/module-federation-dev-server/schema.json index 94154f2b92921..9e1d49442eaa5 100644 --- a/packages/react/src/executors/module-federation-dev-server/schema.json +++ b/packages/react/src/executors/module-federation-dev-server/schema.json @@ -109,6 +109,10 @@ "staticRemotesPort": { "type": "number", "description": "The port at which to serve the file-server for the static remotes." + }, + "pathToManifestFile": { + "type": "string", + "description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root." } } }