feat(react): add recursive remote serve (#19638)

This commit is contained in:
Colum Ferry 2023-10-17 14:33:28 +01:00 committed by GitHub
parent b0fed89c11
commit 97fac1b0b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 644 additions and 202 deletions

View File

@ -112,6 +112,16 @@
"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."
},
"static": {
"type": "boolean",
"description": "Whether to use a static file server instead of the webpack-dev-server. This should be used for remote applications that are also host applications."
},
"isInitialHost": {
"type": "boolean",
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
"default": true,
"x-priority": "internal"
}
},
"additionalProperties": false,

View File

@ -90,6 +90,16 @@
"baseHref": {
"type": "string",
"description": "Base url for the application being built."
},
"static": {
"type": "boolean",
"description": "Whether to use a static file server instead of the webpack-dev-server. This should be used for remote applications that are also host applications."
},
"isInitialHost": {
"type": "boolean",
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
"default": true,
"x-priority": "internal"
}
},
"presets": []

View File

@ -385,4 +385,97 @@ describe('Angular Module Federation', () => {
await killProcessAndPorts(hostE2eResults.pid);
}
}, 500_000);
it('should federate a module from a library and create a remote that is served recursively', async () => {
const lib = uniq('lib');
const remote = uniq('remote');
const childRemote = uniq('childremote');
const module = uniq('module');
const host = uniq('host');
runCLI(
`generate @nx/angular:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
);
runCLI(
`generate @nx/js:lib ${lib} --no-interactive --projectNameAndRootFormat=as-provided`
);
// Federate Module
runCLI(
`generate @nx/angular:federate-module ${module} --remote=${childRemote} --path=${lib}/src/index.ts --no-interactive`
);
updateFile(`${lib}/src/index.ts`, `export { isEven } from './lib/${lib}';`);
updateFile(
`${lib}/src/lib/${lib}.ts`,
`export function isEven(num: number) { return num % 2 === 0; }`
);
// Update Host to use the module
updateFile(
`${remote}/src/app/remote-entry/entry.component.ts`,
`
import { Component } from '@angular/core';
import { isEven } from '${childRemote}/${module}';
@Component({
selector: 'proj-${remote}-entry',
template: \`<div class="childremote">{{title}}</div>\`
})
export class RemoteEntryComponent {
title = \`shell is \${isEven(2) ? 'even' : 'odd'}\`;
}`
);
updateFile(
`${remote}/module-federation.config.ts`,
`
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '${remote}',
remotes: ['${childRemote}'],
exposes: {
'./Module': '${remote}/src/app/remote-entry/entry.module.ts',
},
};
export default config;`
);
// Update e2e test to check the module
updateFile(
`${host}-e2e/src/e2e/app.cy.ts`,
`
describe('${host}', () => {
beforeEach(() => cy.visit('/${remote}'));
it('should display contain the remote library', () => {
expect(cy.get('div.childremote')).to.exist;
expect(cy.get('div.childremote').contains('shell is even'));
});
});
`
);
// Build host and remote
const buildOutput = await runCommandUntil(`build ${host}`, (output) =>
output.includes('Successfully ran target build')
);
await killProcessAndPorts(buildOutput.pid);
const remoteOutput = await runCommandUntil(`build ${remote}`, (output) =>
output.includes('Successfully ran target build')
);
await killProcessAndPorts(remoteOutput.pid);
if (runE2ETests()) {
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid);
}
}, 500_000);
});

View File

@ -549,6 +549,89 @@ describe('React Module Federation', () => {
}
}, 500_000);
it('should federate a module from a library and create a remote and serve it recursively', async () => {
const lib = uniq('lib');
const remote = uniq('remote');
const childRemote = uniq('childremote');
const module = uniq('module');
const host = uniq('host');
runCLI(
`generate @nx/react:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
);
runCLI(
`generate @nx/js:lib ${lib} --no-interactive --projectNameAndRootFormat=as-provided`
);
// Federate Module
runCLI(
`generate @nx/react:federate-module ${module} --remote=${childRemote} --path=${lib}/src/index.ts --no-interactive`
);
updateFile(
`${lib}/src/index.ts`,
`export { default } from './lib/${lib}';`
);
updateFile(
`${lib}/src/lib/${lib}.ts`,
`export default function lib() { return 'Hello from ${lib}'; };`
);
// Update Host to use the module
updateFile(
`${remote}/src/app/app.tsx`,
`
import * as React from 'react';
import NxWelcome from './nx-welcome';
import myLib from '${childRemote}/${module}';
export function App() {
return (
<React.Suspense fallback={null}>
<div className='remote'>
My Remote Library: { myLib() }
</div>
<NxWelcome title="Host" />
</React.Suspense>
);
}
export default App;
`
);
// Update e2e test to check the module
updateFile(
`${host}-e2e/src/e2e/app.cy.ts`,
`
describe('${host}', () => {
beforeEach(() => cy.visit('/${remote}'));
it('should display contain the remote library', () => {
expect(cy.get('div.remote')).to.exist;
expect(cy.get('div.remote').contains('My Remote Library: Hello from ${lib}'));
});
});
`
);
// Build host and remote
const buildOutput = runCLI(`build ${host}`);
const remoteOutput = runCLI(`build ${remote}`);
expect(buildOutput).toContain('Successfully ran target build');
expect(remoteOutput).toContain('Successfully ran target build');
if (runE2ETests()) {
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
expect(hostE2eResults).toContain('All specs passed!');
}
}, 500_000);
describe('Promised based remotes', () => {
it('should support promised based remotes', async () => {
const remote = uniq('remote');

View File

@ -64,6 +64,7 @@
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint",
"@nx/webpack": "file:../webpack",
"@nx/web": "file:../web",
"@nx/workspace": "file:../workspace"
},
"peerDependencies": {

View File

@ -1,5 +1,10 @@
import type { Schema } from './schema';
import { logger, readCachedProjectGraph, workspaceRoot } from '@nx/devkit';
import {
logger,
readCachedProjectGraph,
readNxJson,
workspaceRoot,
} from '@nx/devkit';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-dev-server.impl';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
@ -11,18 +16,66 @@ import {
} from '../utilities/module-federation';
import { existsSync } from 'fs';
import { extname, join } from 'path';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import { fork } from 'child_process';
import { combineLatest, concatMap, from, switchMap } from 'rxjs';
export function executeModuleFederationDevServerBuilder(
schema: Schema,
context: import('@angular-devkit/architect').BuilderContext
): ReturnType<typeof executeWebpackDevServerBuilder> {
): ReturnType<typeof executeWebpackDevServerBuilder | any> {
const nxBin = require.resolve('nx');
const { ...options } = schema;
const projectGraph = readCachedProjectGraph();
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(projectGraph);
const project = workspaceProjects[context.target.project];
const staticFileServer = from(
import('@nx/web/src/executors/file-server/file-server.impl')
).pipe(
switchMap((fileServerExecutor) =>
fileServerExecutor.default(
{
port: options.port,
host: options.host,
ssl: options.ssl,
buildTarget: options.browserTarget,
parallel: false,
spa: false,
withDeps: false,
cors: true,
},
{
projectGraph,
root: context.workspaceRoot,
target:
projectGraph.nodes[context.target.project].data.targets[
context.target.target
],
targetName: context.target.target,
projectName: context.target.project,
configurationName: context.target.configuration,
cwd: context.currentDirectory,
isVerbose: options.verbose,
projectsConfigurations:
readProjectsConfigurationFromProjectGraph(projectGraph),
nxJsonConfiguration: readNxJson(),
}
)
)
);
const webpackDevServer = executeWebpackDevServerBuilder(options, context);
const currExecutor = options.static ? staticFileServer : webpackDevServer;
if (options.isInitialHost === false) {
return currExecutor;
}
let pathToManifestFile = join(
context.workspaceRoot,
project.sourceRoot,
@ -48,70 +101,107 @@ export function executeModuleFederationDevServerBuilder(
validateDevRemotes(options, workspaceProjects);
const remotesToSkip = new Set(
findMatchingProjects(options.skipRemotes, projectGraph.nodes) ?? []
const moduleFederationConfig = getModuleFederationConfig(
project.targets.build.options.tsConfig,
context.workspaceRoot,
project.root,
'angular'
);
if (remotesToSkip.size > 0) {
logger.info(
`Remotes not served automatically: ${[...remotesToSkip].join(', ')}`
);
}
const staticRemotes = getStaticRemotes(
project,
context,
workspaceProjects,
remotesToSkip
);
const dynamicRemotes = getDynamicRemotes(
project,
context,
workspaceProjects,
remotesToSkip,
const remotes = getRemotes(
options.devRemotes,
options.skipRemotes,
moduleFederationConfig,
{
projectName: project.name,
projectGraph,
root: context.workspaceRoot,
},
pathToManifestFile
);
const remotes = [...staticRemotes, ...dynamicRemotes];
const devServeRemotes = !options.devRemotes
? []
: Array.isArray(options.devRemotes)
? findMatchingProjects(options.devRemotes, projectGraph.nodes)
: findMatchingProjects([options.devRemotes], projectGraph.nodes);
let isCollectingStaticRemoteOutput = true;
for (const remote of remotes) {
const isDev = devServeRemotes.includes(remote);
const target = isDev ? 'serve' : 'serve-static';
if (!workspaceProjects[remote].targets?.[target]) {
throw new Error(
`Could not find "${target}" target in "${remote}" project.`
for (const app of remotes.staticRemotes) {
const remoteProjectServeTarget =
projectGraph.nodes[app].data.targets['serve-static'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
} else if (!workspaceProjects[remote].targets?.[target].executor) {
throw new Error(
`Could not find executor for "${target}" target in "${remote}" project.`
);
}
const runOptions: { verbose?: boolean } = {};
if (options.verbose) {
const [collection, executor] =
workspaceProjects[remote].targets[target].executor.split(':');
const { schema } = getExecutorInformation(
collection,
executor,
workspaceRoot
);
if (schema.additionalProperties || 'verbose' in schema.properties) {
runOptions.verbose = options.verbose;
let outWithErr: null | string[] = [];
const staticProcess = fork(
nxBin,
[
'run',
`${app}:serve-static${
context.target.configuration ? `:${context.target.configuration}` : ''
}`,
...(isUsingModuleFederationDevServerExecutor
? [`--isInitialHost=false`]
: []),
],
{
cwd: context.workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
staticProcess.stdout.on('data', (data) => {
if (isCollectingStaticRemoteOutput) {
outWithErr.push(data.toString());
} else {
outWithErr = null;
staticProcess.stdout.removeAllListeners('data');
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
logger.info(outWithErr.join(''));
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
}
const devRemotes$ = [];
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) {
throw new Error(
`Could not find executor for "serve" target in "${app}" project.`
);
}
scheduleTarget(
const runOptions: { verbose?: boolean; isInitialHost?: boolean } = {};
const [collection, executor] =
workspaceProjects[app].targets['serve'].executor.split(':');
const isUsingModuleFederationDevServerExecutor = executor.includes(
'module-federation-dev-server'
);
const { schema } = getExecutorInformation(
collection,
executor,
workspaceRoot
);
if (
(options.verbose && schema.additionalProperties) ||
'verbose' in schema.properties
) {
runOptions.verbose = options.verbose;
}
if (isUsingModuleFederationDevServerExecutor) {
runOptions.isInitialHost = false;
}
const serve$ = scheduleTarget(
context.workspaceRoot,
{
project: remote,
target,
project: app,
target: 'serve',
configuration: context.target.configuration,
runOptions,
},
@ -119,13 +209,17 @@ export function executeModuleFederationDevServerBuilder(
).then((obs) => {
obs.toPromise().catch((err) => {
throw new Error(
`Remote '${remote}' failed to serve correctly due to the following: \r\n${err.toString()}`
`Remote '${app}' failed to serve correctly due to the following: \r\n${err.toString()}`
);
});
});
devRemotes$.push(serve$);
}
return executeWebpackDevServerBuilder(options, context);
return devRemotes$.length > 0
? combineLatest([...devRemotes$]).pipe(concatMap(() => currExecutor))
: currExecutor;
}
export default require('@angular-devkit/architect').createBuilder(

View File

@ -20,4 +20,6 @@ export interface Schema {
devRemotes?: string[];
skipRemotes?: string[];
pathToManifestFile?: string;
static?: boolean;
isInitialHost?: boolean;
}

View File

@ -122,6 +122,16 @@
"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."
},
"static": {
"type": "boolean",
"description": "Whether to use a static file server instead of the webpack-dev-server. This should be used for remote applications that are also host applications."
},
"isInitialHost": {
"type": "boolean",
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
"default": true,
"x-priority": "internal"
}
},
"additionalProperties": false,

View File

@ -6,22 +6,24 @@ import {
runExecutor,
} 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 { WebDevServerOptions } from '@nx/webpack/src/executors/dev-server/schema';
import { join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import {
combineAsyncIterables,
createAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import * as chalk from 'chalk';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import { fork } from 'child_process';
import { existsSync } from 'fs';
import { registerTsProject } from '@nx/js/src/internal';
type ModuleFederationDevServerOptions = WebDevServerOptions & {
devRemotes?: string | string[];
devRemotes?: string[];
skipRemotes?: string[];
static?: boolean;
isInitialHost?: boolean;
};
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
@ -34,164 +36,118 @@ function getBuildOptions(buildTarget: string, context: ExecutorContext) {
};
}
function getModuleFederationConfig(
tsconfigPath: string,
workspaceRoot: string,
projectRoot: string
) {
const moduleFederationConfigPathJS = join(
workspaceRoot,
projectRoot,
'module-federation.config.js'
);
const moduleFederationConfigPathTS = join(
workspaceRoot,
projectRoot,
'module-federation.config.ts'
);
let moduleFederationConfigPath = moduleFederationConfigPathJS;
// create a no-op so this can be called with issue
const fullTSconfigPath = tsconfigPath.startsWith(workspaceRoot)
? tsconfigPath
: join(workspaceRoot, tsconfigPath);
let cleanupTranspiler = () => {};
if (existsSync(moduleFederationConfigPathTS)) {
cleanupTranspiler = registerTsProject(fullTSconfigPath);
moduleFederationConfigPath = moduleFederationConfigPathTS;
}
try {
const config = require(moduleFederationConfigPath);
cleanupTranspiler();
return config.default || config;
} catch {
throw new Error(
`Could not load ${moduleFederationConfigPath}. Was this project generated with "@nx/react:host"?\nSee: https://nx.dev/concepts/more-concepts/faster-builds-with-module-federation`
);
}
}
export default async function* moduleFederationDevServer(
options: ModuleFederationDevServerOptions,
context: ExecutorContext
): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
const nxBin = require.resolve('nx');
const currIter = devServerExecutor(options, context);
const currIter = options.static
? fileServerExecutor(
{
...options,
parallel: false,
withDeps: false,
spa: false,
cors: true,
},
context
)
: devServerExecutor(options, context);
const p = context.projectsConfigurations.projects[context.projectName];
const buildOptions = getBuildOptions(options.buildTarget, context);
if (!options.isInitialHost) {
return yield* currIter;
}
const moduleFederationConfig = getModuleFederationConfig(
buildOptions.tsConfig,
context.root,
p.root
p.root,
'react'
);
const remotesToSkip = new Set(
findMatchingProjects(options.skipRemotes, context.projectGraph.nodes) ?? []
);
if (remotesToSkip.size > 0) {
logger.info(
`Remotes not served automatically: ${[...remotesToSkip.values()].join(
', '
)}`
);
}
const remotesNotInWorkspace: string[] = [];
const knownRemotes = (moduleFederationConfig.remotes ?? []).filter((r) => {
const validRemote = Array.isArray(r) ? r[0] : r;
if (remotesToSkip.has(validRemote)) {
return false;
} else if (!context.projectGraph.nodes[validRemote]) {
remotesNotInWorkspace.push(validRemote);
return false;
} else {
return true;
const remotes = getRemotes(
options.devRemotes,
options.skipRemotes,
moduleFederationConfig,
{
projectName: context.projectName,
projectGraph: context.projectGraph,
root: context.root,
}
});
if (remotesNotInWorkspace.length > 0) {
logger.warn(
`Skipping serving ${remotesNotInWorkspace.join(
', '
)} as they could not be found in the workspace. Ensure they are served correctly.`
);
}
const remotePorts = knownRemotes.map(
(r) => context.projectGraph.nodes[r].data.targets['serve'].options.port
);
const devServeApps = !options.devRemotes
? []
: Array.isArray(options.devRemotes)
? findMatchingProjects(options.devRemotes, context.projectGraph.nodes)
: findMatchingProjects([options.devRemotes], context.projectGraph.nodes);
logger.info(
`NX Starting module federation dev-server for ${chalk.bold(
context.projectName
)} with ${knownRemotes.length} remotes`
);
const devRemoteIters: AsyncIterable<{ success: boolean }>[] = [];
let isCollectingStaticRemoteOutput = true;
const devRemoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of knownRemotes) {
const appName = Array.isArray(app) ? app[0] : app;
if (devServeApps.includes(appName)) {
devRemoteIters.push(
await runExecutor(
{
project: appName,
target: 'serve',
configuration: context.configurationName,
},
{
watch: true,
},
context
)
for (const app of remotes.devRemotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets['serve'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
} else {
let outWithErr: null | string[] = [];
const staticProcess = fork(
nxBin,
[
'run',
`${appName}:serve-static${
context.configurationName ? `:${context.configurationName}` : ''
}`,
],
devRemoteIters.push(
await runExecutor(
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
project: app,
target: 'serve',
configuration: context.configurationName,
},
{
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
},
context
)
);
}
for (const app of remotes.staticRemotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets['serve-static'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
staticProcess.stdout.on('data', (data) => {
if (isCollectingStaticRemoteOutput) {
outWithErr.push(data.toString());
} else {
outWithErr = null;
staticProcess.stdout.removeAllListeners('data');
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
logger.info(outWithErr.join(''));
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
}
let outWithErr: null | string[] = [];
const staticProcess = fork(
nxBin,
[
'run',
`${app}:serve-static${
context.configurationName ? `:${context.configurationName}` : ''
}`,
...(isUsingModuleFederationDevServerExecutor
? [`--isInitialHost=false`]
: []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
staticProcess.stdout.on('data', (data) => {
if (isCollectingStaticRemoteOutput) {
outWithErr.push(data.toString());
} else {
outWithErr = null;
staticProcess.stdout.removeAllListeners('data');
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
logger.info(outWithErr.join(''));
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
}
return yield* combineAsyncIterables(
@ -199,13 +155,17 @@ export default async function* moduleFederationDevServer(
...devRemoteIters,
createAsyncIterable<{ success: true; baseUrl: string }>(
async ({ next, done }) => {
if (remotePorts.length === 0) {
if (!options.isInitialHost) {
done();
return;
}
if (remotes.remotePorts.length === 0) {
done();
return;
}
try {
await Promise.all(
remotePorts.map((port) =>
remotes.remotePorts.map((port) =>
// Allow 20 minutes for each remote to start, which is plenty of time but we can tweak it later if needed.
// Most remotes should start in under 1 minute.
waitForPortOpen(port, {

View File

@ -91,6 +91,16 @@
"baseHref": {
"type": "string",
"description": "Base url for the application being built."
},
"static": {
"type": "boolean",
"description": "Whether to use a static file server instead of the webpack-dev-server. This should be used for remote applications that are also host applications."
},
"isInitialHost": {
"type": "boolean",
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
"default": true,
"x-priority": "internal"
}
}
}

View File

@ -0,0 +1,168 @@
import { logger, type ProjectGraph } from '@nx/devkit';
import { registerTsProject } from '@nx/js/src/internal';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import * as chalk from 'chalk';
import { join } from 'path';
import { existsSync, readFileSync } from 'fs';
import { ModuleFederationConfig } from './models/index';
interface ModuleFederationExecutorContext {
projectName: string;
projectGraph: ProjectGraph;
root: string;
}
function extractRemoteProjectsFromConfig(
config: ModuleFederationConfig,
pathToManifestFile?: string
) {
const remotes = [];
if (pathToManifestFile && existsSync(pathToManifestFile)) {
const moduleFederationManifestJson = readFileSync(
pathToManifestFile,
'utf-8'
);
if (moduleFederationManifestJson) {
// This should have shape of
// {
// "remoteName": "remoteLocation",
// }
const parsedManifest = JSON.parse(moduleFederationManifestJson);
if (
Object.keys(parsedManifest).every(
(key) =>
typeof key === 'string' && typeof parsedManifest[key] === 'string'
)
) {
remotes.push(Object.keys(parsedManifest));
}
}
}
const staticRemotes =
config.remotes?.map((r) => (Array.isArray(r) ? r[0] : r)) ?? [];
remotes.push(...staticRemotes);
return remotes;
}
function collectRemoteProjects(
remote: string,
collected: Set<string>,
context: ModuleFederationExecutorContext
) {
const remoteProject = context.projectGraph.nodes[remote]?.data;
if (!context.projectGraph.nodes[remote] || collected.has(remote)) {
return;
}
collected.add(remote);
const remoteProjectRoot = remoteProject.root;
const remoteProjectTsConfig = remoteProject.targets['build'].options.tsConfig;
const remoteProjectConfig = getModuleFederationConfig(
remoteProjectTsConfig,
context.root,
remoteProjectRoot
);
const remoteProjectRemotes =
extractRemoteProjectsFromConfig(remoteProjectConfig);
remoteProjectRemotes.forEach((r) =>
collectRemoteProjects(r, collected, context)
);
}
export function getRemotes(
devRemotes: string[],
skipRemotes: string[],
config: ModuleFederationConfig,
context: ModuleFederationExecutorContext,
pathToManifestFile?: string
) {
const collectedRemotes = new Set<string>();
const remotes = extractRemoteProjectsFromConfig(config, pathToManifestFile);
remotes.forEach((r) => collectRemoteProjects(r, collectedRemotes, context));
const remotesToSkip = new Set(
findMatchingProjects(skipRemotes, context.projectGraph.nodes) ?? []
);
if (remotesToSkip.size > 0) {
logger.info(
`Remotes not served automatically: ${[...remotesToSkip.values()].join(
', '
)}`
);
}
const knownRemotes = Array.from(collectedRemotes).filter(
(r) => !remotesToSkip.has(r)
);
logger.info(
`NX Starting module federation dev-server for ${chalk.bold(
context.projectName
)} with ${knownRemotes.length} remotes`
);
const devServeApps = new Set(
!devRemotes
? []
: Array.isArray(devRemotes)
? findMatchingProjects(devRemotes, context.projectGraph.nodes)
: findMatchingProjects([devRemotes], context.projectGraph.nodes)
);
const staticRemotes = knownRemotes.filter((r) => !devServeApps.has(r));
const devServeRemotes = knownRemotes.filter((r) => devServeApps.has(r));
const remotePorts = knownRemotes.map(
(r) => context.projectGraph.nodes[r].data.targets['serve'].options.port
);
return {
staticRemotes,
devRemotes: devServeRemotes,
remotePorts,
};
}
export function getModuleFederationConfig(
tsconfigPath: string,
workspaceRoot: string,
projectRoot: string,
pluginName: 'react' | 'angular' = 'react'
) {
const moduleFederationConfigPathJS = join(
workspaceRoot,
projectRoot,
'module-federation.config.js'
);
const moduleFederationConfigPathTS = join(
workspaceRoot,
projectRoot,
'module-federation.config.ts'
);
let moduleFederationConfigPath = moduleFederationConfigPathJS;
// create a no-op so this can be called with issue
const fullTSconfigPath = tsconfigPath.startsWith(workspaceRoot)
? tsconfigPath
: join(workspaceRoot, tsconfigPath);
let cleanupTranspiler = () => {};
if (existsSync(moduleFederationConfigPathTS)) {
cleanupTranspiler = registerTsProject(fullTSconfigPath);
moduleFederationConfigPath = moduleFederationConfigPathTS;
}
try {
const config = require(moduleFederationConfigPath);
cleanupTranspiler();
return config.default || config;
} catch {
throw new Error(
`Could not load ${moduleFederationConfigPath}. Was this project generated with "@nx/${pluginName}:host"?\nSee: https://nx.dev/concepts/more-concepts/faster-builds-with-module-federation`
);
}
}

View File

@ -3,3 +3,4 @@ export * from './dependencies';
export * from './package-json';
export * from './remotes';
export * from './models';
export * from './get-remotes-for-host';