Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(module-federation): serve dynamic remotes statically in their own processes #22688

Merged
merged 2 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ProjectConfiguration>,
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 }
: {}),
Expand All @@ -48,5 +45,5 @@ export async function startDevRemotes(
)
);
}
return devRemotesIters;
return remoteIters;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
buildStaticRemotes,
normalizeOptions,
parseStaticRemotesConfig,
startDevRemotes,
startRemotes,
startStaticRemotesFileServer,
} from './lib';
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
Expand Down Expand Up @@ -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 =
Expand All @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -30,6 +32,7 @@ type ModuleFederationDevServerOptions = WebDevServerOptions & {
isInitialHost?: boolean;
parallel?: number;
staticRemotesPort?: number;
pathToManifestFile?: string;
};

function getBuildOptions(buildTarget: string, context: ExecutorContext) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function extractRemoteProjectsFromConfig(
pathToManifestFile?: string
) {
const remotes = [];
const dynamicRemotes = [];
if (pathToManifestFile && existsSync(pathToManifestFile)) {
const moduleFederationManifestJson = readFileSync(
pathToManifestFile,
Expand All @@ -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(
Expand All @@ -64,7 +65,7 @@ function collectRemoteProjects(
context.root,
remoteProjectRoot
);
const remoteProjectRemotes =
const { remotes: remoteProjectRemotes } =
extractRemoteProjectsFromConfig(remoteProjectConfig);

remoteProjectRemotes.forEach((r) =>
Expand All @@ -80,7 +81,10 @@ export function getRemotes(
pathToManifestFile?: string
) {
const collectedRemotes = new Set<string>();
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) ?? []
Expand All @@ -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(
Expand All @@ -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,
};
}
Expand Down