@nx/js
Version:
334 lines (333 loc) • 15.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.nodeExecutor = nodeExecutor;
const chalk = require("chalk");
const child_process_1 = require("child_process");
const devkit_1 = require("@nx/devkit");
const async_iterable_1 = require("@nx/devkit/src/utils/async-iterable");
const client_1 = require("nx/src/daemon/client/client");
const crypto_1 = require("crypto");
const path = require("path");
const path_1 = require("path");
const buildable_libs_utils_1 = require("../../utils/buildable-libs-utils");
const kill_tree_1 = require("./lib/kill-tree");
const fileutils_1 = require("nx/src/utils/fileutils");
const get_main_file_dir_1 = require("../../utils/get-main-file-dir");
const utils_1 = require("nx/src/tasks-runner/utils");
function debounce(fn, wait) {
let timeoutId;
let pendingPromise = null;
return () => {
clearTimeout(timeoutId);
if (!pendingPromise) {
pendingPromise = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
fn()
.then((result) => {
pendingPromise = null;
resolve(result);
})
.catch((error) => {
pendingPromise = null;
reject(error);
});
}, wait);
});
}
return pendingPromise;
};
}
async function* nodeExecutor(options, context) {
process.env.NODE_ENV ??= context?.configurationName ?? 'development';
const project = context.projectGraph.nodes[context.projectName];
const buildTarget = (0, devkit_1.parseTargetString)(options.buildTarget, context);
if (!project.data.targets[buildTarget.target]) {
throw new Error(`Cannot find build target ${chalk.bold(options.buildTarget)} for project ${chalk.bold(context.projectName)}`);
}
const buildTargetExecutor = project.data.targets[buildTarget.target]?.executor;
if (buildTargetExecutor === 'nx:run-commands') {
// Run commands does not emit build event, so we have to switch to run entire build through Nx CLI.
options.runBuildTargetDependencies = true;
}
const buildOptions = {
...(0, devkit_1.readTargetOptions)(buildTarget, context),
...options.buildTargetOptions,
target: buildTarget.target,
};
if (options.waitUntilTargets && options.waitUntilTargets.length > 0) {
const results = await runWaitUntilTargets(options, context);
for (const [i, result] of results.entries()) {
if (!result.success) {
throw new Error(`Wait until target failed: ${options.waitUntilTargets[i]}.`);
}
}
}
// Re-map buildable workspace projects to their output directory.
const mappings = calculateResolveMappings(context, options);
const fileToRun = getFileToRun(context, project, buildOptions, buildTargetExecutor);
let additionalExitHandler = null;
let currentTask = null;
const tasks = [];
yield* (0, async_iterable_1.createAsyncIterable)(async ({ done, next, error, registerCleanup }) => {
const processQueue = async () => {
if (tasks.length === 0)
return;
const previousTask = currentTask;
const task = tasks.shift();
currentTask = task;
await previousTask?.stop('SIGTERM');
await task.start();
};
const debouncedProcessQueue = debounce(processQueue, options.debounce ?? 1_000);
const addToQueue = async (childProcess, buildResult) => {
const task = {
id: (0, crypto_1.randomUUID)(),
killed: false,
childProcess,
promise: null,
start: async () => {
// Wait for build to finish.
const result = await buildResult;
if (result && !result.success) {
// If in watch-mode, don't throw or else the process exits.
if (options.watch) {
if (!task.killed) {
// Only log build error if task was not killed by a new change.
devkit_1.logger.error(`Build failed, waiting for changes to restart...`);
}
return;
}
else {
throw new Error(`Build failed. See above for errors.`);
}
}
// Before running the program, check if the task has been killed (by a new change during watch).
if (task.killed)
return;
// Run the program
task.promise = new Promise((resolve, reject) => {
task.childProcess = (0, child_process_1.fork)((0, devkit_1.joinPathFragments)(__dirname, 'node-with-require-overrides'), options.args ?? [], {
execArgv: getExecArgv(options),
stdio: [0, 1, 'pipe', 'ipc'],
env: {
...process.env,
NX_FILE_TO_RUN: fileToRunCorrectPath(fileToRun),
NX_MAPPINGS: JSON.stringify(mappings),
},
});
const handleStdErr = (data) => {
// Don't log out error if task is killed and new one has started.
// This could happen if a new build is triggered while new process is starting, since the operation is not atomic.
// Log the error in normal mode
if (!options.watch || !task.killed) {
devkit_1.logger.error(data.toString());
}
};
task.childProcess.stderr.on('data', handleStdErr);
task.childProcess.once('exit', (code) => {
task.childProcess.off('data', handleStdErr);
if (options.watch && !task.killed) {
devkit_1.logger.info(`NX Process exited with code ${code}, waiting for changes to restart...`);
}
if (!options.watch) {
if (code !== 0) {
error(new Error(`Process exited with code ${code}`));
}
else {
done();
}
}
resolve();
});
next({ success: true, options: buildOptions });
});
},
stop: async (signal = 'SIGTERM') => {
task.killed = true;
// Request termination and wait for process to finish gracefully.
// NOTE: `childProcess` may not have been set yet if the task did not have a chance to start.
// e.g. multiple file change events in a short time (like git checkout).
if (task.childProcess) {
await (0, kill_tree_1.killTree)(task.childProcess.pid, signal);
}
try {
await task.promise;
}
catch {
// Doesn't matter if task fails, we just need to wait until it finishes.
}
},
};
tasks.push(task);
};
const stopAllTasks = async (signal = 'SIGTERM') => {
additionalExitHandler?.();
await currentTask?.stop(signal);
for (const task of tasks) {
await task.stop(signal);
}
};
process.on('SIGTERM', async () => {
await stopAllTasks('SIGTERM');
process.exit(128 + 15);
});
process.on('SIGINT', async () => {
await stopAllTasks('SIGINT');
process.exit(128 + 2);
});
process.on('SIGHUP', async () => {
await stopAllTasks('SIGHUP');
process.exit(128 + 1);
});
registerCleanup(async () => {
await stopAllTasks('SIGTERM');
});
if (options.runBuildTargetDependencies) {
// If a all dependencies need to be rebuild on changes, then register with watcher
// and run through CLI, otherwise only the current project will rebuild.
const runBuild = async () => {
let childProcess = null;
const whenReady = new Promise(async (resolve) => {
childProcess = (0, child_process_1.fork)(require.resolve('nx'), [
'run',
`${context.projectName}:${buildTarget.target}${buildTarget.configuration ? `:${buildTarget.configuration}` : ''}`,
], {
cwd: context.root,
stdio: 'inherit',
});
childProcess.once('exit', (code) => {
if (code === 0)
resolve({ success: true });
// If process is killed due to current task being killed, then resolve with success.
else
resolve({ success: !!currentTask?.killed });
});
});
await addToQueue(childProcess, whenReady);
await debouncedProcessQueue();
};
if ((0, devkit_1.isDaemonEnabled)()) {
additionalExitHandler = await client_1.daemonClient.registerFileWatcher({
watchProjects: [context.projectName],
includeDependentProjects: true,
}, async (err, data) => {
if (err === 'closed') {
devkit_1.logger.error(`Watch error: Daemon closed the connection`);
process.exit(1);
}
else if (err) {
devkit_1.logger.error(`Watch error: ${err?.message ?? 'Unknown'}`);
}
else {
if (options.watch) {
devkit_1.logger.info(`NX File change detected. Restarting...`);
await runBuild();
}
}
});
}
else {
devkit_1.logger.warn(`NX Daemon is not running. Node process will not restart automatically after file changes.`);
}
await runBuild(); // run first build
}
else {
// Otherwise, run the build executor, which will not run task dependencies.
// This is mostly fine for bundlers like webpack that should already watch for dependency libs.
// For tsc/swc or custom build commands, consider using `runBuildTargetDependencies` instead.
const output = await (0, devkit_1.runExecutor)(buildTarget, {
...options.buildTargetOptions,
watch: options.watch,
}, context);
while (true) {
const event = await output.next();
await addToQueue(null, Promise.resolve(event.value));
await debouncedProcessQueue();
if (event.done || !options.watch) {
break;
}
}
}
});
}
function getExecArgv(options) {
const args = (options.runtimeArgs ??= []);
args.push('-r', require.resolve('source-map-support/register'));
if (options.inspect === true) {
options.inspect = "inspect" /* InspectType.Inspect */;
}
if (options.inspect) {
args.push(`--${options.inspect}=${options.host}:${options.port}`);
}
return args;
}
function calculateResolveMappings(context, options) {
const parsed = (0, devkit_1.parseTargetString)(options.buildTarget, context);
const { dependencies } = (0, buildable_libs_utils_1.calculateProjectBuildableDependencies)(context.taskGraph, context.projectGraph, context.root, parsed.project, parsed.target, parsed.configuration);
return dependencies.reduce((m, c) => {
if (c.node.type !== 'npm' && c.outputs[0] != null) {
m[c.name] = (0, devkit_1.joinPathFragments)(context.root, c.outputs[0]);
}
return m;
}, {});
}
function runWaitUntilTargets(options, context) {
return Promise.all(options.waitUntilTargets.map(async (waitUntilTarget) => {
const target = (0, devkit_1.parseTargetString)(waitUntilTarget, context);
const output = await (0, devkit_1.runExecutor)(target, {}, context);
return new Promise(async (resolve) => {
let event = await output.next();
// Resolve after first event
resolve(event.value);
// Continue iterating
while (!event.done) {
event = await output.next();
}
});
}));
}
function getFileToRun(context, project, buildOptions, buildTargetExecutor) {
// If using run-commands or another custom executor, then user should set
// outputFileName, but we can try the default value that we use.
if (!buildOptions?.outputPath && !buildOptions?.outputFileName) {
// If we are using crystal for infering the target, we can use the output path from the target.
// Since the output path has a token for the project name, we need to interpolate it.
// {workspaceRoot}/dist/{projectRoot} -> dist/my-app
const outputPath = project.data.targets[buildOptions.target]?.outputs?.[0];
if (outputPath) {
const outputFilePath = (0, utils_1.interpolate)(outputPath, {
projectName: project.name,
projectRoot: project.data.root,
workspaceRoot: context.root,
});
return path.join(outputFilePath, 'main.js');
}
const fallbackFile = path.join('dist', project.data.root, 'main.js');
devkit_1.logger.warn(`Build option ${chalk.bold('outputFileName')} not set for ${chalk.bold(project.name)}. Using fallback value of ${chalk.bold(fallbackFile)}.`);
return (0, path_1.join)(context.root, fallbackFile);
}
let outputFileName = buildOptions.outputFileName;
if (!outputFileName) {
const fileName = `${path.parse(buildOptions.main).name}.js`;
if (buildTargetExecutor === '@nx/js:tsc' ||
buildTargetExecutor === '@nx/js:swc') {
outputFileName = path.join((0, get_main_file_dir_1.getRelativeDirectoryToProjectRoot)(buildOptions.main, project.data.root), fileName);
}
else {
outputFileName = fileName;
}
}
return (0, path_1.join)(context.root, buildOptions.outputPath, outputFileName);
}
function fileToRunCorrectPath(fileToRun) {
if ((0, fileutils_1.fileExists)(fileToRun))
return fileToRun;
const extensionsToTry = ['.cjs', '.mjs', '.cjs.js', '.esm.js'];
for (const ext of extensionsToTry) {
const file = fileToRun.replace(/\.js$/, ext);
if ((0, fileutils_1.fileExists)(file))
return file;
}
throw new Error(`Could not find ${fileToRun}. Make sure your build succeeded.`);
}
exports.default = nodeExecutor;
;