UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

334 lines (333 loc) • 15.8 kB
"use strict"; 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;