feat(angular): convert module-federation-dev-server to executor (#20252)

This commit is contained in:
Colum Ferry 2023-11-30 12:58:48 +00:00 committed by GitHub
parent afafb79494
commit d22e860269
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 489 additions and 450 deletions

View File

@ -6536,6 +6536,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "module-federation-dev-server",
"path": "/nx-api/angular/executors/module-federation-dev-server",
"name": "module-federation-dev-server",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "webpack-browser",
"path": "/nx-api/angular/executors/webpack-browser",
@ -6560,14 +6568,6 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "module-federation-dev-server",
"path": "/nx-api/angular/executors/module-federation-dev-server",
"name": "module-federation-dev-server",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "module-federation-dev-ssr",
"path": "/nx-api/angular/executors/module-federation-dev-ssr",

View File

@ -67,6 +67,15 @@
"path": "/nx-api/angular/executors/browser-esbuild",
"type": "executor"
},
"/nx-api/angular/executors/module-federation-dev-server": {
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"file": "generated/packages/angular/executors/module-federation-dev-server.json",
"hidden": false,
"name": "module-federation-dev-server",
"originalFilePath": "/packages/angular/src/executors/module-federation-dev-server/schema.json",
"path": "/nx-api/angular/executors/module-federation-dev-server",
"type": "executor"
},
"/nx-api/angular/executors/webpack-browser": {
"description": "The `webpack-browser` executor is very similar to the standard `browser` builder provided by the Angular Devkit. It allows you to build your Angular application to a build artifact that can be hosted online. There are some key differences: \n- Supports Custom Webpack Configurations \n- Supports Incremental Building",
"file": "generated/packages/angular/executors/webpack-browser.json",
@ -94,15 +103,6 @@
"path": "/nx-api/angular/executors/webpack-server",
"type": "executor"
},
"/nx-api/angular/executors/module-federation-dev-server": {
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"file": "generated/packages/angular/executors/module-federation-dev-server.json",
"hidden": false,
"name": "module-federation-dev-server",
"originalFilePath": "/packages/angular/src/builders/module-federation-dev-server/schema.json",
"path": "/nx-api/angular/executors/module-federation-dev-server",
"type": "executor"
},
"/nx-api/angular/executors/module-federation-dev-ssr": {
"description": "The module-federation-dev-ssr executor is reserved exclusively for use with host Module Federation applications that use SSR. It allows the user to specify which remote applications should be served with the host.",
"file": "generated/packages/angular/executors/module-federation-dev-ssr.json",

View File

@ -62,6 +62,15 @@
"path": "angular/executors/browser-esbuild",
"type": "executor"
},
{
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"file": "generated/packages/angular/executors/module-federation-dev-server.json",
"hidden": false,
"name": "module-federation-dev-server",
"originalFilePath": "/packages/angular/src/executors/module-federation-dev-server/schema.json",
"path": "angular/executors/module-federation-dev-server",
"type": "executor"
},
{
"description": "The `webpack-browser` executor is very similar to the standard `browser` builder provided by the Angular Devkit. It allows you to build your Angular application to a build artifact that can be hosted online. There are some key differences: \n- Supports Custom Webpack Configurations \n- Supports Incremental Building",
"file": "generated/packages/angular/executors/webpack-browser.json",
@ -89,15 +98,6 @@
"path": "angular/executors/webpack-server",
"type": "executor"
},
{
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"file": "generated/packages/angular/executors/module-federation-dev-server.json",
"hidden": false,
"name": "module-federation-dev-server",
"originalFilePath": "/packages/angular/src/builders/module-federation-dev-server/schema.json",
"path": "angular/executors/module-federation-dev-server",
"type": "executor"
},
{
"description": "The module-federation-dev-ssr executor is reserved exclusively for use with host Module Federation applications that use SSR. It allows the user to specify which remote applications should be served with the host.",
"file": "generated/packages/angular/executors/module-federation-dev-ssr.json",

View File

@ -1,11 +1,10 @@
{
"name": "module-federation-dev-server",
"implementation": "/packages/angular/src/builders/module-federation-dev-server/module-federation-dev-server.impl.ts",
"implementation": "/packages/angular/src/executors/module-federation-dev-server/module-federation-dev-server.impl.ts",
"schema": {
"version": 2,
"outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Schema for Module Federation Dev Server",
"outputCapture": "direct-nodejs",
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"type": "object",
"presets": [
@ -148,6 +147,6 @@
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"aliases": [],
"hidden": false,
"path": "/packages/angular/src/builders/module-federation-dev-server/schema.json",
"path": "/packages/angular/src/executors/module-federation-dev-server/schema.json",
"type": "executor"
}

View File

@ -323,10 +323,10 @@
- [ng-packagr-lite](/nx-api/angular/executors/ng-packagr-lite)
- [package](/nx-api/angular/executors/package)
- [browser-esbuild](/nx-api/angular/executors/browser-esbuild)
- [module-federation-dev-server](/nx-api/angular/executors/module-federation-dev-server)
- [webpack-browser](/nx-api/angular/executors/webpack-browser)
- [dev-server](/nx-api/angular/executors/dev-server)
- [webpack-server](/nx-api/angular/executors/webpack-server)
- [module-federation-dev-server](/nx-api/angular/executors/module-federation-dev-server)
- [module-federation-dev-ssr](/nx-api/angular/executors/module-federation-dev-ssr)
- [generators](/nx-api/angular/generators)
- [add-linting](/nx-api/angular/generators/add-linting)

View File

@ -19,6 +19,11 @@
"implementation": "./src/executors/browser-esbuild/browser-esbuild.impl",
"schema": "./src/executors/browser-esbuild/schema.json",
"description": "Builds your application with esbuild and adds support for incremental builds."
},
"module-federation-dev-server": {
"implementation": "./src/executors/module-federation-dev-server/module-federation-dev-server.impl",
"schema": "./src/executors/module-federation-dev-server/schema.json",
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host."
}
},
"builders": {
@ -37,11 +42,6 @@
"schema": "./src/builders/webpack-server/schema.json",
"description": "The `webpack-server` executor is very similar to the standard `server` builder provided by the Angular Devkit. It is usually used in tandem with `@nrwl/angular:webpack-browser` when your Angular application uses a custom webpack configuration and NgUniversal for SSR."
},
"module-federation-dev-server": {
"implementation": "./src/builders/module-federation-dev-server/module-federation-dev-server.impl",
"schema": "./src/builders/module-federation-dev-server/schema.json",
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host."
},
"module-federation-dev-ssr": {
"implementation": "./src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl",
"schema": "./src/builders/module-federation-dev-ssr/schema.json",

View File

@ -1,7 +1,7 @@
export * from './src/builders/module-federation-dev-server/module-federation-dev-server.impl';
export * from './src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl';
export * from './src/builders/webpack-browser/webpack-browser.impl';
export * from './src/builders/webpack-server/webpack-server.impl';
export * from './src/executors/module-federation-dev-server/module-federation-dev-server.impl';
export * from './src/executors/delegate-build/delegate-build.impl';
export * from './src/executors/ng-packagr-lite/ng-packagr-lite.impl';
export * from './src/executors/package/package.impl';

View File

@ -1,405 +0,0 @@
import type {
NormalizedSchema,
Schema,
SchemaWithBrowserTarget,
SchemaWithBuildTarget,
} from './schema';
import {
logger,
type ProjectConfiguration,
type ProjectGraph,
readCachedProjectGraph,
readNxJson,
workspaceRoot,
} from '@nx/devkit';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { executeDevServerBuilder } from '../dev-server/dev-server.impl';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
import { validateDevRemotes } from '../utilities/module-federation';
import { existsSync } from 'fs';
import { dirname, extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import { fork } from 'child_process';
import { combineLatest, concatMap, from, switchMap } from 'rxjs';
import { cpSync } from 'fs';
function buildStaticRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
nxBin,
context: import('@angular-devkit/architect').BuilderContext,
options: Schema
) {
if (!remotes.staticRemotes.length) {
return Promise.resolve();
}
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of remotes.staticRemotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${app}`;
}
process.env.NX_MF_DEV_SERVER_STATIC_REMOTES = JSON.stringify(
mappedLocationOfRemotes
);
const staticRemoteBuildPromise = new Promise<void>((res) => {
logger.info(
`NX Building ${remotes.staticRemotes.length} static remotes...`
);
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${remotes.staticRemotes.join(',')}`,
...(context.target.configuration
? [`--configuration=${context.target.configuration}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
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, '');
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(`NX Built ${remotes.staticRemotes.length} static remotes`);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
throw new Error(`Remotes failed to build. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
return staticRemoteBuildPromise;
}
function startStaticRemotesFileServer(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
projectGraph: ProjectGraph,
options: Schema,
context: import('@angular-devkit/architect').BuilderContext
) {
let shouldMoveToCommonLocation = false;
let commonOutputDirectory: string;
for (const app of remotes.staticRemotes) {
const outputPath =
projectGraph.nodes[app].data.targets['build'].options.outputPath;
const directoryOfOutputPath = dirname(outputPath);
if (!commonOutputDirectory) {
commonOutputDirectory = directoryOfOutputPath;
} else if (
commonOutputDirectory !== directoryOfOutputPath ||
!outputPath.endsWith(app)
) {
shouldMoveToCommonLocation = true;
}
}
if (shouldMoveToCommonLocation) {
commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of remotes.staticRemotes) {
const outputPath =
projectGraph.nodes[app].data.targets['build'].options.outputPath;
cpSync(outputPath, join(commonOutputDirectory, app), {
force: true,
recursive: true,
});
}
}
const staticRemotesIter$ = from(
import('@nx/web/src/executors/file-server/file-server.impl')
).pipe(
switchMap((fileServerExecutor) =>
fileServerExecutor.default(
{
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,
},
{
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(),
}
)
)
);
return staticRemotesIter$;
}
function startDevRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
workspaceProjects: Record<string, ProjectConfiguration>,
options: Schema,
context: import('@angular-devkit/architect').BuilderContext
) {
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.`
);
}
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,
workspaceProjects
);
if (
(options.verbose && schema.additionalProperties) ||
'verbose' in schema.properties
) {
runOptions.verbose = options.verbose;
}
if (isUsingModuleFederationDevServerExecutor) {
runOptions.isInitialHost = false;
}
const serve$ = scheduleTarget(
context.workspaceRoot,
{
project: app,
target: 'serve',
configuration: context.target.configuration,
runOptions,
projects: workspaceProjects,
},
options.verbose
).then((obs) => {
obs.toPromise().catch((err) => {
throw new Error(
`Remote '${app}' failed to serve correctly due to the following: \r\n${err.toString()}`
);
});
});
devRemotes$.push(serve$);
}
return devRemotes$;
}
export function executeModuleFederationDevServerBuilder(
schema: Schema,
context: import('@angular-devkit/architect').BuilderContext
): ReturnType<typeof executeDevServerBuilder | any> {
// 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);
options.staticRemotesPort ??= options.port + 1;
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.buildTarget,
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: { projects: workspaceProjects, version: 2 },
nxJsonConfiguration: readNxJson(),
}
)
)
);
const webpackDevServer = executeDevServerBuilder(options, context);
const currExecutor = options.static ? staticFileServer : webpackDevServer;
if (options.isInitialHost === false) {
return currExecutor;
}
let pathToManifestFile = join(
context.workspaceRoot,
project.sourceRoot,
'assets/module-federation.manifest.json'
);
if (options.pathToManifestFile) {
const userPathToManifestFile = join(
context.workspaceRoot,
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;
}
validateDevRemotes(options, workspaceProjects);
const moduleFederationConfig = getModuleFederationConfig(
project.targets.build.options.tsConfig,
context.workspaceRoot,
project.root,
'angular'
);
const remotes = getRemotes(
options.devRemotes,
options.skipRemotes,
moduleFederationConfig,
{
projectName: project.name,
projectGraph,
root: context.workspaceRoot,
},
pathToManifestFile
);
if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) {
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
const remotePort =
projectGraph.nodes[r].data.targets['serve'].options.port;
if (remotePort >= portToUse) {
return remotePort + 1;
}
}, options.staticRemotesPort);
}
const staticRemoteBuildPromise = buildStaticRemotes(
remotes,
nxBin,
context,
options
);
return from(staticRemoteBuildPromise).pipe(
concatMap(() => {
const staticRemotesIter$ =
remotes.staticRemotes.length > 0
? startStaticRemotesFileServer(
remotes,
projectGraph,
options,
context
)
: from(Promise.resolve());
const devRemotes$ = startDevRemotes(
remotes,
workspaceProjects,
options,
context
);
return devRemotes$.length > 0
? combineLatest([...devRemotes$, staticRemotesIter$]).pipe(
concatMap(() => currExecutor)
)
: from(staticRemotesIter$).pipe(concatMap(() => currExecutor));
})
);
}
export default require('@angular-devkit/architect').createBuilder(
executeModuleFederationDevServerBuilder
);
function normalizeOptions(schema: Schema): NormalizedSchema {
let buildTarget = (schema as SchemaWithBuildTarget).buildTarget;
if ((schema as SchemaWithBrowserTarget).browserTarget) {
buildTarget ??= (schema as SchemaWithBrowserTarget).browserTarget;
delete (schema as SchemaWithBrowserTarget).browserTarget;
}
return {
...schema,
buildTarget,
host: schema.host ?? 'localhost',
port: schema.port ?? 4200,
liveReload: schema.liveReload ?? true,
open: schema.open ?? false,
ssl: schema.ssl ?? false,
};
}

View File

@ -158,9 +158,8 @@ export function validateDevRemotes(
options: { devRemotes?: string[] },
workspaceProjects: Record<string, ProjectConfiguration>
): void {
const invalidDevRemotes = options.devRemotes?.filter(
(remote) => !workspaceProjects[remote]
);
const invalidDevRemotes =
options.devRemotes?.filter((remote) => !workspaceProjects[remote]) ?? [];
if (invalidDevRemotes.length) {
throw new Error(

View File

@ -0,0 +1,70 @@
import { type Schema } from '../schema';
import { logger, type ExecutorContext } from '@nx/devkit';
import { fork } from 'child_process';
export async function buildStaticRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
nxBin,
context: ExecutorContext,
options: Schema
) {
if (
!remotes.staticRemotes ||
(Array.isArray(remotes.staticRemotes) && !remotes.staticRemotes.length)
) {
return;
}
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of remotes.staticRemotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${app}`;
}
process.env.NX_MF_DEV_SERVER_STATIC_REMOTES = JSON.stringify(
mappedLocationOfRemotes
);
await new Promise<void>((res) => {
logger.info(
`NX Building ${remotes.staticRemotes.length} static remotes...`
);
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${remotes.staticRemotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
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, '');
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(`NX Built ${remotes.staticRemotes.length} static remotes`);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
throw new Error(`Remotes failed to build. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
}

View File

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

View File

@ -0,0 +1,24 @@
import type {
NormalizedSchema,
Schema,
SchemaWithBrowserTarget,
SchemaWithBuildTarget,
} from '../schema';
export function normalizeOptions(schema: Schema): NormalizedSchema {
let buildTarget = (schema as SchemaWithBuildTarget).buildTarget;
if ((schema as SchemaWithBrowserTarget).browserTarget) {
buildTarget ??= (schema as SchemaWithBrowserTarget).browserTarget;
delete (schema as SchemaWithBrowserTarget).browserTarget;
}
return {
...schema,
buildTarget,
host: schema.host ?? 'localhost',
port: schema.port ?? 4200,
liveReload: schema.liveReload ?? true,
open: schema.open ?? false,
ssl: schema.ssl ?? false,
};
}

View File

@ -0,0 +1,52 @@
import { type Schema } from '../schema';
import {
type ExecutorContext,
type ProjectConfiguration,
runExecutor,
} from '@nx/devkit';
export async function startDevRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
workspaceProjects: Record<string, ProjectConfiguration>,
options: Schema,
context: ExecutorContext
) {
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) {
throw new Error(
`Could not find executor for "serve" target in "${app}" project.`
);
}
const [collection, executor] =
workspaceProjects[app].targets['serve'].executor.split(':');
const isUsingModuleFederationDevServerExecutor = executor.includes(
'module-federation-dev-server'
);
devRemotesIters.push(
await runExecutor(
{
project: app,
target: 'serve',
configuration: context.configurationName,
},
{
verbose: options.verbose ?? false,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
},
context
)
);
}
return devRemotesIters;
}

View File

@ -0,0 +1,63 @@
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 { dirname, join } from 'path';
import { cpSync } from 'fs';
export function startStaticRemotesFileServer(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
context: ExecutorContext,
options: Schema
) {
let shouldMoveToCommonLocation = false;
let commonOutputDirectory: string;
for (const app of remotes.staticRemotes) {
const outputPath =
context.projectGraph.nodes[app].data.targets['build'].options.outputPath;
const directoryOfOutputPath = dirname(outputPath);
if (!commonOutputDirectory) {
commonOutputDirectory = directoryOfOutputPath;
} else if (
commonOutputDirectory !== directoryOfOutputPath ||
!outputPath.endsWith(app)
) {
shouldMoveToCommonLocation = true;
}
}
if (shouldMoveToCommonLocation) {
commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of remotes.staticRemotes) {
const outputPath =
context.projectGraph.nodes[app].data.targets['build'].options
.outputPath;
cpSync(outputPath, join(commonOutputDirectory, app), {
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,
},
context
);
return staticRemotesIter;
}

View File

@ -0,0 +1,198 @@
import {
type ExecutorContext,
logger,
readProjectsConfigurationFromProjectGraph,
} from '@nx/devkit';
import { type Schema } from './schema';
import {
buildStaticRemotes,
normalizeOptions,
startDevRemotes,
startStaticRemotesFileServer,
} 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,
} from '@nx/webpack/src/utils/module-federation';
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';
import { executeDevServerBuilder } from '../../builders/dev-server/dev-server.impl';
import { validateDevRemotes } from '../../builders/utilities/module-federation';
import { extname, join } from 'path';
import { existsSync } from 'fs';
export default 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);
options.staticRemotesPort ??= options.port + 1;
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(context.projectGraph);
const project = workspaceProjects[context.projectName];
const currIter = options.static
? fileServerExecutor(
{
port: options.port,
host: options.host,
ssl: options.ssl,
buildTarget: options.buildTarget,
parallel: false,
spa: false,
withDeps: false,
cors: true,
},
context
)
: eachValueFrom(
executeDevServerBuilder(
options,
await createBuilderContext(
{
builderName: '@nx/angular:webpack-browser',
description: 'Build a browser application',
optionSchema: await import(
'../../builders/webpack-browser/schema.json'
),
},
context
)
)
);
if (options.isInitialHost === false) {
return yield* currIter;
}
let pathToManifestFile = join(
context.root,
project.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;
}
validateDevRemotes(options, workspaceProjects);
const moduleFederationConfig = getModuleFederationConfig(
project.targets.build.options.tsConfig,
context.root,
project.root,
'angular'
);
const remotes = getRemotes(
options.devRemotes,
options.skipRemotes,
moduleFederationConfig,
{
projectName: project.name,
projectGraph: context.projectGraph,
root: context.root,
},
pathToManifestFile
);
if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) {
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
const remotePort =
context.projectGraph.nodes[r].data.targets['serve'].options.port;
if (remotePort >= portToUse) {
return remotePort + 1;
}
}, options.staticRemotesPort);
}
await buildStaticRemotes(remotes, nxBin, context, options);
const devRemoteIters = await startDevRemotes(
remotes,
workspaceProjects,
options,
context
);
const staticRemotesIter =
remotes.staticRemotes.length > 0
? startStaticRemotesFileServer(remotes, context, options)
: undefined;
const removeBaseUrlEmission = (iter: AsyncIterable<unknown>) =>
mapAsyncIterable(iter, (v) => ({
...v,
baseUrl: undefined,
}));
return yield* combineAsyncIterables(
removeBaseUrlEmission(currIter),
...devRemoteIters.map(removeBaseUrlEmission),
...(staticRemotesIter ? [removeBaseUrlEmission(staticRemotesIter)] : []),
createAsyncIterable<{ success: true; baseUrl: string }>(
async ({ next, done }) => {
if (!options.isInitialHost) {
done();
return;
}
if (remotes.remotePorts.length === 0) {
logger.info(
`NX All remotes started, server ready at http://localhost:${options.port}`
);
next({ success: true, baseUrl: `http://localhost:${options.port}` });
done();
return;
}
try {
const portsToWaitFor = staticRemotesIter
? [options.staticRemotesPort, ...remotes.remotePorts]
: [...remotes.remotePorts];
await Promise.all(
portsToWaitFor.map((port) =>
waitForPortOpen(port, {
retries: 480,
retryDelay: 2500,
host: 'localhost',
})
)
);
logger.info(
`NX All remotes started, server ready at http://localhost:${options.port}`
);
next({ success: true, baseUrl: `http://localhost:${options.port}` });
} catch {
throw new Error(
`Timed out waiting for remote to start. Check above for any errors.`
);
} finally {
done();
}
}
)
);
}

View File

@ -1,8 +1,7 @@
{
"version": 2,
"outputCapture": "direct-nodejs",
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Schema for Module Federation Dev Server",
"outputCapture": "direct-nodejs",
"description": "The module-federation-dev-server executor is reserved exclusively for use with host Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
"type": "object",
"presets": [
@ -149,6 +148,13 @@
}
},
"additionalProperties": false,
"anyOf": [{ "required": ["buildTarget"] }, { "required": ["browserTarget"] }],
"anyOf": [
{
"required": ["buildTarget"]
},
{
"required": ["browserTarget"]
}
],
"examplesFile": "../../../docs/module-federation-dev-server-examples.md"
}

View File

@ -113,6 +113,35 @@ export async function createBuilderContext(
architect['_scheduler'].schedule('..getProjectMetadata', target).output
);
const getBuilderNameForTarget = (target: Target | string) => {
if (typeof target === 'string') {
return Promise.resolve(
context.projectGraph.nodes[context.projectName].data.targets[target]
.executor
);
}
return Promise.resolve(
context.projectGraph.nodes[target.project].data.targets[target.target]
.executor
);
};
const getTargetOptions = (target: Target | string) => {
if (typeof target === 'string') {
return Promise.resolve({
...context.projectGraph.nodes[context.projectName].data.targets[target]
.options,
});
}
return Promise.resolve({
...context.projectGraph.nodes[target.project].data.targets[target.target]
.options,
...context.projectGraph.nodes[target.project].data.targets[target.target]
.configurations[target.configuration],
});
};
const builderContext: import('@angular-devkit/architect').BuilderContext = {
workspaceRoot: context.root,
target: {
@ -127,9 +156,7 @@ export async function createBuilderContext(
id: 1,
currentDirectory: process.cwd(),
scheduleTarget: architect.scheduleTarget,
getBuilderNameForTarget: architectHost.getBuilderNameForTarget,
scheduleBuilder: architect.scheduleBuilder,
getTargetOptions: architectHost.getOptionsForTarget,
addTeardown(teardown: () => Promise<void> | void) {
// No-op as Nx doesn't require an implementation of this function
return;
@ -146,8 +173,10 @@ export async function createBuilderContext(
// No-op as Nx doesn't require an implementation of this function
return;
},
getBuilderNameForTarget,
getProjectMetadata,
validateOptions,
getTargetOptions,
};
return builderContext;