nx/packages/angular/src/builders/module-federation-dev-server/module-federation-dev-server.impl.ts
Craigory Coppola 206247f9ad
feat(core): make createNodes async (#20195)
Co-authored-by: Jonathan Cammisuli <jon@cammisuli.ca>
2023-11-13 16:57:20 -05:00

408 lines
12 KiB
TypeScript

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 { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-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
) {
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) {
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;
const outputPathParts = outputPath.split('/');
cpSync(
outputPath,
join(
commonOutputDirectory,
outputPathParts[outputPathParts.length - 1]
),
{
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 executeWebpackDevServerBuilder | 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 = executeWebpackDevServerBuilder(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,
};
}