feat(angular): convert module-federation-dev-server to executor (#20252)
This commit is contained in:
parent
afafb79494
commit
d22e860269
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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'));
|
||||
});
|
||||
}
|
||||
@ -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';
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user