nx 23.0.0-beta.1 → 23.0.0-pr.33655.cdc3e72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/nx.js CHANGED
@@ -109,10 +109,10 @@ async function main() {
109
109
  warnIfUsingOutdatedGlobalInstall(GLOBAL_NX_VERSION, LOCAL_NX_VERSION);
110
110
  if (localNx.includes('.nx')) {
111
111
  const nxWrapperPath = localNx.replace(/\.nx.*/, '.nx/') + 'nxw.js';
112
- await import(nxWrapperPath);
112
+ require(nxWrapperPath);
113
113
  }
114
114
  else {
115
- await import(localNx);
115
+ require(localNx);
116
116
  }
117
117
  }
118
118
  }
@@ -15,6 +15,7 @@ const semver_1 = require("semver");
15
15
  const utils_1 = require("../../../tasks-runner/utils");
16
16
  const output_1 = require("../../../utils/output");
17
17
  const git_1 = require("./git");
18
+ const find_matching_projects_1 = require("../../../utils/find-matching-projects");
18
19
  exports.noDiffInChangelogMessage = pc.yellow(`NOTE: There was no diff detected for the changelog entry. Maybe you intended to pass alternative git references via --from and --to?`);
19
20
  function isPrerelease(version) {
20
21
  // prerelease returns an array of matching prerelease "components", or null if the version is not a prerelease
@@ -329,23 +330,32 @@ async function getCommitsRelevantToProjects(projectGraph, commits, projects, nxR
329
330
  // Try to get the graph associated with the commit shortHash
330
331
  // if not available, calculate it and store it in the cache
331
332
  let affectedGraph = await releaseGraph.resolveAffectedFilesPerCommitInProjectGraph(commit, projectGraph);
333
+ // Resolve commit scopes using Nx matcher
334
+ const scopePatterns = commit.scope
335
+ ? commit.scope.split(',').map((s) => s.trim())
336
+ : [];
337
+ let scopedProjects = null;
338
+ if (scopePatterns.length > 0) {
339
+ const matches = (0, find_matching_projects_1.findMatchingProjects)(scopePatterns, projectGraph.nodes);
340
+ // detect ambiguity
341
+ for (const pattern of scopePatterns) {
342
+ const perPatternMatches = (0, find_matching_projects_1.findMatchingProjects)([pattern], projectGraph.nodes);
343
+ if (perPatternMatches.length > 1) {
344
+ throw new Error(`Ambiguous scope "${pattern}" in commit "${commit.message}". ` +
345
+ `Matches: ${perPatternMatches.join(', ')}`);
346
+ }
347
+ }
348
+ scopedProjects = new Set(matches);
349
+ }
332
350
  for (const projectName of Object.keys(affectedGraph.nodes)) {
333
351
  if (projectSet.has(projectName)) {
334
352
  if (!relevantCommits.has(projectName)) {
335
353
  relevantCommits.set(projectName, []);
336
354
  }
337
- if (commit.scope === projectName ||
338
- commit.scope.split(',').includes(projectName) ||
339
- !commit.scope) {
340
- relevantCommits
341
- .get(projectName)
342
- ?.push({ commit, isProjectScopedCommit: true });
343
- }
344
- else {
345
- relevantCommits
346
- .get(projectName)
347
- ?.push({ commit, isProjectScopedCommit: false });
348
- }
355
+ const isProjectScopedCommit = scopedProjects === null || scopedProjects.has(projectName);
356
+ relevantCommits
357
+ .get(projectName)
358
+ ?.push({ commit, isProjectScopedCommit });
349
359
  }
350
360
  }
351
361
  }
@@ -7,7 +7,7 @@ const pc = tslib_1.__importStar(require("picocolors"));
7
7
  const child_process_1 = require("child_process");
8
8
  const npm_run_path_1 = require("npm-run-path");
9
9
  const path_1 = require("path");
10
- const tree_kill_1 = tslib_1.__importDefault(require("tree-kill"));
10
+ const native_1 = require("../../native");
11
11
  const pseudo_terminal_1 = require("../../tasks-runner/pseudo-terminal");
12
12
  const task_env_1 = require("../../tasks-runner/task-env");
13
13
  const task_io_service_1 = require("../../tasks-runner/task-io-service");
@@ -40,14 +40,7 @@ class ParallelRunningTasks {
40
40
  }
41
41
  }
42
42
  async kill(signal) {
43
- await Promise.all(this.childProcesses.map(async (p) => {
44
- try {
45
- return p.kill();
46
- }
47
- catch (e) {
48
- console.error(`Unable to terminate "${p.command}"\nError:`, e);
49
- }
50
- }));
43
+ await Promise.allSettled(this.childProcesses.map((p) => p.kill(signal)));
51
44
  }
52
45
  async run() {
53
46
  if (this.readyWhenStatus.length) {
@@ -93,7 +86,7 @@ class ParallelRunningTasks {
93
86
  hasFailure = true;
94
87
  failureDetails = { childProcess, code, terminalOutput };
95
88
  // Immediately terminate all other running processes
96
- await this.terminateRemainingProcesses(runningProcesses, childProcess);
89
+ this.terminateRemainingProcesses(runningProcesses, childProcess);
97
90
  }
98
91
  runningProcesses.delete(childProcess);
99
92
  }));
@@ -116,25 +109,11 @@ class ParallelRunningTasks {
116
109
  }
117
110
  }
118
111
  }
119
- async terminateRemainingProcesses(runningProcesses, failedProcess) {
120
- const terminationPromises = [];
112
+ terminateRemainingProcesses(runningProcesses, failedProcess) {
121
113
  const processesToTerminate = [...runningProcesses].filter((p) => p !== failedProcess);
122
114
  for (const process of processesToTerminate) {
123
115
  runningProcesses.delete(process);
124
- // Terminate the process
125
- terminationPromises.push(process.kill('SIGTERM').catch((err) => {
126
- // Log error but don't fail the entire operation
127
- if (this.streamOutput) {
128
- console.error(`Failed to terminate process "${process.command}":`, err);
129
- }
130
- }));
131
- }
132
- // Wait for all terminations to complete with a timeout
133
- if (terminationPromises.length > 0) {
134
- await Promise.race([
135
- Promise.all(terminationPromises),
136
- new Promise((resolve) => setTimeout(resolve, 5_000)),
137
- ]);
116
+ process.kill('SIGTERM').catch(() => { });
138
117
  }
139
118
  }
140
119
  }
@@ -182,7 +161,7 @@ class SeriallyRunningTasks {
182
161
  throw new Error('Not implemented');
183
162
  }
184
163
  kill(signal) {
185
- return this.currentProcess.kill(signal);
164
+ return this.currentProcess?.kill(signal);
186
165
  }
187
166
  async run(options, context) {
188
167
  for (const c of options.commands) {
@@ -236,6 +215,7 @@ class RunningNodeProcess {
236
215
  this.terminalOutputChunks = [];
237
216
  this.exitCallbacks = [];
238
217
  this.outputCallbacks = [];
218
+ this.killPromise = null;
239
219
  env = processEnv(color, cwd, env, envFile);
240
220
  this.command = commandConfig.command;
241
221
  const header = pc.dim('> ') + commandConfig.command + '\r\n\r\n';
@@ -245,9 +225,14 @@ class RunningNodeProcess {
245
225
  }
246
226
  this.childProcess = (0, child_process_1.spawn)(commandConfig.command, [], {
247
227
  shell: true,
228
+ detached: process.platform !== 'win32',
248
229
  env,
249
230
  cwd,
250
231
  windowsHide: true,
232
+ stdio: ['inherit', 'pipe', 'pipe'],
233
+ });
234
+ this.closedPromise = new Promise((resolve) => {
235
+ this.childProcess.on('close', resolve);
251
236
  });
252
237
  this.childProcess.stdout?.setEncoding('utf8');
253
238
  this.childProcess.stderr?.setEncoding('utf8');
@@ -275,18 +260,21 @@ class RunningNodeProcess {
275
260
  this.childProcess.send(message);
276
261
  }
277
262
  kill(signal) {
278
- return new Promise((res, rej) => {
279
- (0, tree_kill_1.default)(this.childProcess.pid, signal, (err) => {
280
- // On Windows, tree-kill (which uses taskkill) may fail when the process or its child process is already terminated.
281
- // Ignore the errors, otherwise we will log them unnecessarily.
282
- if (err && process.platform !== 'win32') {
283
- rej(err);
284
- }
285
- else {
286
- res();
287
- }
288
- });
289
- });
263
+ if (this.killPromise)
264
+ return this.killPromise;
265
+ if (!this.childProcess.pid)
266
+ return Promise.resolve();
267
+ // Cache the promise so concurrent callers (e.g. cleanup() after a
268
+ // fire-and-forget kill from cleanUpUnneededContinuousTasks) await the
269
+ // same in-progress operation instead of no-oping.
270
+ this.killPromise = (0, native_1.killProcessTreeGraceful)(this.childProcess.pid, signal).then(() =>
271
+ // Wait for stdio drain and close events before the orchestrator
272
+ // tears down the event loop via process.exit().
273
+ Promise.race([
274
+ this.closedPromise,
275
+ new Promise((resolve) => setTimeout(resolve, 1000)),
276
+ ]));
277
+ return this.killPromise;
290
278
  }
291
279
  triggerOutputListeners(output) {
292
280
  for (const cb of this.outputCallbacks) {
@@ -294,6 +282,34 @@ class RunningNodeProcess {
294
282
  }
295
283
  }
296
284
  addListeners(commandConfig, streamOutput) {
285
+ // Named handlers so they can be removed when the child exits.
286
+ // Otherwise each RunningNodeProcess leaks process listeners; with
287
+ // many run-commands tasks this triggers MaxListenersExceededWarning.
288
+ const isForked = !!process.env.NX_FORKED_TASK_EXECUTOR;
289
+ const onProcessExit = () => {
290
+ if (this.childProcess.pid && !this.killPromise) {
291
+ (0, native_1.killProcessTree)(this.childProcess.pid);
292
+ }
293
+ };
294
+ const onSigInt = () => {
295
+ this.kill('SIGTERM');
296
+ };
297
+ const onSigTerm = () => {
298
+ this.kill('SIGTERM');
299
+ };
300
+ const onSigHup = () => {
301
+ this.kill('SIGTERM');
302
+ };
303
+ const removeProcessListeners = () => {
304
+ process.removeListener('exit', onProcessExit);
305
+ if (isForked) {
306
+ process.removeListener('SIGINT', onSigInt);
307
+ process.removeListener('SIGTERM', onSigTerm);
308
+ process.removeListener('SIGHUP', onSigHup);
309
+ }
310
+ };
311
+ this.childProcess.stdout.setEncoding('utf8');
312
+ this.childProcess.stderr.setEncoding('utf8');
297
313
  this.childProcess.stdout.on('data', (data) => {
298
314
  const output = addColorAndPrefix(data, commandConfig);
299
315
  this.terminalOutputChunks.push(output);
@@ -301,8 +317,7 @@ class RunningNodeProcess {
301
317
  if (streamOutput) {
302
318
  process.stdout.write(output);
303
319
  }
304
- if (this.readyWhenStatus.length &&
305
- isReady(this.readyWhenStatus, data.toString())) {
320
+ if (this.readyWhenStatus.length && isReady(this.readyWhenStatus, data)) {
306
321
  for (const cb of this.exitCallbacks) {
307
322
  cb(0, this.terminalOutputChunks.join(''));
308
323
  }
@@ -315,8 +330,7 @@ class RunningNodeProcess {
315
330
  if (streamOutput) {
316
331
  process.stderr.write(output);
317
332
  }
318
- if (this.readyWhenStatus.length &&
319
- isReady(this.readyWhenStatus, err.toString())) {
333
+ if (this.readyWhenStatus.length && isReady(this.readyWhenStatus, err)) {
320
334
  for (const cb of this.exitCallbacks) {
321
335
  cb(1, this.terminalOutputChunks.join(''));
322
336
  }
@@ -335,29 +349,6 @@ class RunningNodeProcess {
335
349
  cb(1, terminalOutput);
336
350
  }
337
351
  });
338
- // Store signal/exit handlers so they can be removed when the child exits.
339
- // Without cleanup, each RunningNodeProcess leaks 4 process listeners.
340
- // In a large monorepo (2600+ run-commands tasks), these accumulate and
341
- // cause a multi-minute synchronous hang at process.exit() as each handler
342
- // calls treeKill on an already-dead PID.
343
- const onExit = () => {
344
- this.kill();
345
- };
346
- const onSigInt = () => {
347
- this.kill('SIGTERM');
348
- };
349
- const onSigTerm = () => {
350
- this.kill('SIGTERM');
351
- };
352
- const onSigHup = () => {
353
- this.kill('SIGTERM');
354
- };
355
- const removeProcessListeners = () => {
356
- process.removeListener('exit', onExit);
357
- process.removeListener('SIGINT', onSigInt);
358
- process.removeListener('SIGTERM', onSigTerm);
359
- process.removeListener('SIGHUP', onSigHup);
360
- };
361
352
  this.childProcess.on('exit', (code, signal) => {
362
353
  if (code === null) {
363
354
  code = (0, exit_codes_1.signalToCode)(signal);
@@ -371,11 +362,19 @@ class RunningNodeProcess {
371
362
  }
372
363
  }
373
364
  });
374
- // Terminate any task processes on exit
375
- process.on('exit', onExit);
376
- process.on('SIGINT', onSigInt);
377
- process.on('SIGTERM', onSigTerm);
378
- process.on('SIGHUP', onSigHup);
365
+ // Terminate any task processes on exit (sync, last resort).
366
+ // Skip if graceful kill is already in progress — it tracks cleanup
367
+ // subprocesses and we must not interfere with them.
368
+ process.on('exit', onProcessExit);
369
+ // In the direct path, detached children don't get OS SIGINT (own process
370
+ // group via setsid); the orchestrator's cleanup() sends SIGTERM via
371
+ // killProcessTreeGraceful. Signal handlers here are only needed in the
372
+ // forked path where there's no orchestrator to dispatch signals.
373
+ if (isForked) {
374
+ process.on('SIGINT', onSigInt);
375
+ process.on('SIGTERM', onSigTerm);
376
+ process.on('SIGHUP', onSigHup);
377
+ }
379
378
  }
380
379
  }
381
380
  async function runSingleCommandWithPseudoTerminal(normalized, context, taskId) {
@@ -493,21 +492,31 @@ function registerProcessListener(runningTask, pseudoTerminal) {
493
492
  runningTask.send(message);
494
493
  }
495
494
  });
496
- // Terminate any task processes on exit
495
+ // Terminate any task processes on exit (sync, last resort).
496
+ // The per-child exit handlers and PseudoTerminal.shutdown() use the
497
+ // sync killProcessTree for this path. We call kill() here as a
498
+ // best-effort fallback — it returns a Promise but on 'exit' only
499
+ // synchronous work runs, so the initial signal is sent but the
500
+ // grace period won't be awaited. That's acceptable: 'exit' is the
501
+ // last resort after SIGINT/SIGTERM handlers have already had their
502
+ // chance to do graceful shutdown.
497
503
  process.on('exit', () => {
498
504
  runningTask.kill();
499
505
  });
500
- process.on('SIGINT', () => {
501
- runningTask.kill('SIGTERM');
502
- });
503
- process.on('SIGTERM', () => {
504
- runningTask.kill('SIGTERM');
505
- // no exit here because we expect child processes to terminate which
506
- // will store results to the cache and will terminate this process
507
- });
508
- process.on('SIGHUP', () => {
509
- runningTask.kill('SIGTERM');
510
- // no exit here because we expect child processes to terminate which
511
- // will store results to the cache and will terminate this process
512
- });
506
+ // In the direct path, the orchestrator handles signal dispatch to children.
507
+ // These handlers are only needed in the forked path where there's no
508
+ // orchestrator.
509
+ if (process.env.NX_FORKED_TASK_EXECUTOR) {
510
+ process.on('SIGINT', () => {
511
+ Promise.resolve(runningTask.kill('SIGTERM')).finally(() => {
512
+ process.exit((0, exit_codes_1.signalToCode)('SIGINT'));
513
+ });
514
+ });
515
+ process.on('SIGTERM', () => {
516
+ runningTask.kill('SIGTERM');
517
+ });
518
+ process.on('SIGHUP', () => {
519
+ runningTask.kill('SIGTERM');
520
+ });
521
+ }
513
522
  }
@@ -4,7 +4,7 @@ exports.default = default_1;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const path = tslib_1.__importStar(require("path"));
7
- const tree_kill_1 = tslib_1.__importDefault(require("tree-kill"));
7
+ const native_1 = require("../../native");
8
8
  const pseudo_terminal_1 = require("../../tasks-runner/pseudo-terminal");
9
9
  const package_manager_1 = require("../../utils/package-manager");
10
10
  async function default_1(options, context) {
@@ -56,15 +56,8 @@ function nodeProcess(command, cwd, env) {
56
56
  });
57
57
  const exitHandler = (signal) => {
58
58
  if (cp && cp.pid && !cp.killed) {
59
- (0, tree_kill_1.default)(cp.pid, signal, (error) => {
60
- // On Windows, tree-kill (which uses taskkill) may fail when the process or its child process is already terminated.
61
- // Ignore the errors, otherwise we will log them unnecessarily.
62
- if (error && process.platform !== 'win32') {
63
- rej(error);
64
- }
65
- else {
66
- res();
67
- }
59
+ (0, native_1.killProcessTreeGraceful)(cp.pid, signal).finally(() => {
60
+ res();
68
61
  });
69
62
  }
70
63
  };
@@ -52,7 +52,7 @@ export declare class AppLifeCycle {
52
52
  export declare class ChildProcess {
53
53
  getParserAndWriter(): ExternalObject<[ParserArc, WriterArc]>
54
54
  getPid(): number
55
- kill(signal?: NodeJS.Signals): void
55
+ kill(signal?: NodeJS.Signals | number): void
56
56
  onExit(callback: (message: string) => void): void
57
57
  onOutput(callback: (message: string) => void): void
58
58
  cleanup(): void
@@ -465,6 +465,23 @@ export interface JsonInput {
465
465
  excludeFields?: Array<string>
466
466
  }
467
467
 
468
+ /**
469
+ * Kill a process and all its descendants (fire-and-forget).
470
+ *
471
+ * Sends the requested signal but does NOT wait for processes to exit.
472
+ * Use `killProcessTreeGraceful` when cleanup handlers must run.
473
+ */
474
+ export declare function killProcessTree(rootPid: number, signal?: string | number | undefined | null): void
475
+
476
+ /**
477
+ * Kill a process tree gracefully: signal → wait → SIGKILL.
478
+ *
479
+ * Signals leaf processes first, waits for them to exit, then signals
480
+ * their parents (now leaves). Repeats until the tree is empty or the
481
+ * grace period expires, then force-kills survivors.
482
+ */
483
+ export declare function killProcessTreeGraceful(rootPid: number, signal?: string | number | undefined | null, gracePeriodMs?: number | undefined | null): Promise<void>
484
+
468
485
  export declare function logDebug(message: string): void
469
486
 
470
487
  /** Combined metadata for groups and processes */
@@ -620,6 +620,8 @@ module.exports.installNxConsoleForEditor = nativeBinding.installNxConsoleForEdit
620
620
  module.exports.IS_WASM = nativeBinding.IS_WASM
621
621
  module.exports.isAiAgent = nativeBinding.isAiAgent
622
622
  module.exports.isEditorInstalled = nativeBinding.isEditorInstalled
623
+ module.exports.killProcessTree = nativeBinding.killProcessTree
624
+ module.exports.killProcessTreeGraceful = nativeBinding.killProcessTreeGraceful
623
625
  module.exports.logDebug = nativeBinding.logDebug
624
626
  module.exports.parseTaskStatus = nativeBinding.parseTaskStatus
625
627
  module.exports.remove = nativeBinding.remove
Binary file
@@ -37,6 +37,6 @@ export declare class ForkedProcessTaskRunner {
37
37
  private forkProcessWithPrefixAndNotTTY;
38
38
  private forkProcessDirectOutputCapture;
39
39
  private writeTerminalOutput;
40
- cleanup(signal?: NodeJS.Signals): void;
40
+ cleanup(signal?: NodeJS.Signals): Promise<void>;
41
41
  private setupProcessEventListeners;
42
42
  }
@@ -264,10 +264,8 @@ class ForkedProcessTaskRunner {
264
264
  writeTerminalOutput(outputPath, content) {
265
265
  (0, fs_1.writeFileSync)(outputPath, content);
266
266
  }
267
- cleanup(signal) {
268
- this.processes.forEach((p) => {
269
- p.kill(signal);
270
- });
267
+ async cleanup(signal) {
268
+ await Promise.all([...this.processes].map((p) => p.kill(signal)));
271
269
  this.cleanUpBatchProcesses();
272
270
  }
273
271
  setupProcessEventListeners() {
@@ -283,12 +281,27 @@ class ForkedProcessTaskRunner {
283
281
  };
284
282
  // When the nx process gets a message, it will be sent into the task's process
285
283
  process.on('message', messageHandler);
286
- // Terminate any task processes on exit
284
+ // Terminate any task processes on exit (sync, last resort).
285
+ // cleanup() is async but the initial signal dispatch is synchronous
286
+ // (killProcessTreeGraceful snapshots and signals before the async
287
+ // grace period). The grace period won't complete here, but each
288
+ // child also has its own sync exit handler as a final fallback.
287
289
  process.once('exit', () => {
288
290
  this.cleanup();
289
291
  process.off('message', messageHandler);
290
292
  });
291
- // Signal handlers removed - TaskOrchestrator owns signal handling
293
+ process.once('SIGINT', () => {
294
+ this.cleanup('SIGTERM').finally(() => {
295
+ process.off('message', messageHandler);
296
+ // No process.exit() here — the orchestrator's setupSignalHandlers()
297
+ // owns the exit decision for the direct path, and in the forked path
298
+ // the cleanup above is a no-op (0 processes).
299
+ });
300
+ });
301
+ // SIGTERM/SIGHUP are NOT handled here. The orchestrator's
302
+ // setupSignalHandlers() handles them and calls FPTR.cleanup()
303
+ // as part of its own cleanup sequence. Using process.once() here
304
+ // would consume the event before the orchestrator sees it.
292
305
  }
293
306
  }
294
307
  exports.ForkedProcessTaskRunner = ForkedProcessTaskRunner;
@@ -45,7 +45,7 @@ export declare class PseudoTtyProcess implements RunningTask {
45
45
  onExit(callback: (code: number, terminalOutput: string) => void): void;
46
46
  onOutput(callback: (message: string) => void): void;
47
47
  getPid(): number | undefined;
48
- kill(s?: NodeJS.Signals): void;
48
+ kill(s?: NodeJS.Signals): Promise<void>;
49
49
  getParserAndWriter(): import("../native").ExternalObject<[import("../native").ParserArc, import("../native").WriterArc]>;
50
50
  }
51
51
  export declare class PseudoTtyProcessWithSend extends PseudoTtyProcess {
@@ -41,9 +41,14 @@ class PseudoTerminal {
41
41
  this.initialized = true;
42
42
  }
43
43
  shutdown(code) {
44
+ // Called from process.on('exit') — must be synchronous/best-effort.
45
+ // Use fire-and-forget killProcessTree, not the async graceful variant.
44
46
  for (const cp of this.childProcesses) {
45
47
  try {
46
- cp.kill((0, exit_codes_1.codeToSignal)(code));
48
+ const pid = cp.getPid();
49
+ if (pid) {
50
+ (0, native_1.killProcessTree)(pid, (0, exit_codes_1.codeToSignal)(code));
51
+ }
47
52
  }
48
53
  catch { }
49
54
  }
@@ -110,17 +115,24 @@ class PseudoTtyProcess {
110
115
  getPid() {
111
116
  return this.childProcess.getPid();
112
117
  }
113
- kill(s) {
118
+ async kill(s) {
114
119
  if (this.isAlive) {
115
- try {
116
- this.childProcess.kill(s || 'SIGTERM');
117
- }
118
- catch {
119
- // when the child process completes before we explicitly call kill, this will throw
120
- // do nothing
120
+ this.isAlive = false;
121
+ const pid = this.childProcess.getPid();
122
+ // Gracefully kill the entire process tree. This snapshots the tree
123
+ // BEFORE sending signals, so even if the root exits quickly from
124
+ // the signal, all descendants are already tracked and will be
125
+ // cleaned up (including any reparented to init/PID 1).
126
+ if (pid) {
127
+ await (0, native_1.killProcessTreeGraceful)(pid, s || 'SIGTERM');
121
128
  }
122
- finally {
123
- this.isAlive = false;
129
+ else {
130
+ try {
131
+ this.childProcess.kill(s || 'SIGTERM');
132
+ }
133
+ catch {
134
+ // child may have already exited
135
+ }
124
136
  }
125
137
  }
126
138
  }
@@ -15,5 +15,5 @@ export declare class BatchProcess {
15
15
  onOutput(cb: (output: string) => void): void;
16
16
  getResults(): Promise<BatchResults>;
17
17
  send(message: Serializable): void;
18
- kill(signal?: NodeJS.Signals): void;
18
+ kill(signal?: NodeJS.Signals): Promise<void>;
19
19
  }
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BatchProcess = void 0;
4
- const tslib_1 = require("tslib");
5
- const tree_kill_1 = tslib_1.__importDefault(require("tree-kill"));
4
+ const native_1 = require("../../native");
6
5
  const exit_codes_1 = require("../../utils/exit-codes");
7
6
  const batch_messages_1 = require("../batch/batch-messages");
8
7
  class BatchProcess {
@@ -103,10 +102,9 @@ class BatchProcess {
103
102
  }
104
103
  kill(signal) {
105
104
  if (this.childProcess?.pid) {
106
- (0, tree_kill_1.default)(this.childProcess.pid, signal, () => {
107
- // Ignore errors - process may have already exited
108
- });
105
+ return (0, native_1.killProcessTreeGraceful)(this.childProcess.pid, signal);
109
106
  }
107
+ return Promise.resolve();
110
108
  }
111
109
  }
112
110
  exports.BatchProcess = BatchProcess;
@@ -18,7 +18,7 @@ export declare class NodeChildProcessWithNonDirectOutput implements RunningTask
18
18
  terminalOutput: string;
19
19
  }>;
20
20
  send(message: Serializable): void;
21
- kill(signal?: NodeJS.Signals): void;
21
+ kill(signal?: NodeJS.Signals): Promise<void>;
22
22
  }
23
23
  export declare class NodeChildProcessWithDirectOutput implements RunningTask {
24
24
  private childProcess;
@@ -36,5 +36,5 @@ export declare class NodeChildProcessWithDirectOutput implements RunningTask {
36
36
  }>;
37
37
  waitForExit(): Promise<void>;
38
38
  getTerminalOutput(): string;
39
- kill(signal?: NodeJS.Signals): void;
39
+ kill(signal?: NodeJS.Signals): Promise<void>;
40
40
  }
@@ -5,7 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const pc = tslib_1.__importStar(require("picocolors"));
6
6
  const fs_1 = require("fs");
7
7
  const stream_1 = require("stream");
8
- const tree_kill_1 = tslib_1.__importDefault(require("tree-kill"));
8
+ const native_1 = require("../../native");
9
9
  const exit_codes_1 = require("../../utils/exit-codes");
10
10
  const output_prefix_1 = require("./output-prefix");
11
11
  class NodeChildProcessWithNonDirectOutput {
@@ -97,10 +97,9 @@ class NodeChildProcessWithNonDirectOutput {
97
97
  }
98
98
  kill(signal) {
99
99
  if (this.childProcess?.pid) {
100
- (0, tree_kill_1.default)(this.childProcess.pid, signal, () => {
101
- // Ignore errors - process may have already exited
102
- });
100
+ return (0, native_1.killProcessTreeGraceful)(this.childProcess.pid, signal);
103
101
  }
102
+ return Promise.resolve();
104
103
  }
105
104
  }
106
105
  exports.NodeChildProcessWithNonDirectOutput = NodeChildProcessWithNonDirectOutput;
@@ -168,10 +167,9 @@ class NodeChildProcessWithDirectOutput {
168
167
  }
169
168
  kill(signal) {
170
169
  if (this.childProcess?.pid) {
171
- (0, tree_kill_1.default)(this.childProcess.pid, signal, () => {
172
- // Ignore errors - process may have already exited
173
- });
170
+ return (0, native_1.killProcessTreeGraceful)(this.childProcess.pid, signal);
174
171
  }
172
+ return Promise.resolve();
175
173
  }
176
174
  }
177
175
  exports.NodeChildProcessWithDirectOutput = NodeChildProcessWithDirectOutput;
@@ -166,6 +166,7 @@ export declare class TaskOrchestrator {
166
166
  private shouldCopyOutputsFromCacheBatch;
167
167
  private recordOutputsHashBatch;
168
168
  private handleContinuousTaskExit;
169
+ private isContinuousTaskNeeded;
169
170
  private completeContinuousTask;
170
171
  private cleanup;
171
172
  private performCleanup;
@@ -57,7 +57,12 @@ class TaskOrchestrator {
57
57
  // region internal state
58
58
  this.batchEnv = (0, task_env_1.getEnvVariablesForBatchProcess)(this.options.skipNxCache, this.options.captureStderr);
59
59
  this.reverseTaskDeps = (0, utils_1.calculateReverseDeps)(this.taskGraph);
60
- this.initializingTaskIds = new Set(this.initiatingTasks.map((t) => t.id));
60
+ // `nx:noop` initiating tasks exit instantly via the fast-path in
61
+ // `spawnProcess`. If we treat the noop itself as the keep-alive anchor for
62
+ // its continuous dependencies, `cleanUpUnneededContinuousTasks` kills those
63
+ // children the moment the noop finishes. Expand through noops so the
64
+ // underlying real tasks become the anchors.
65
+ this.initializingTaskIds = (0, utils_1.expandInitiatingTasksThroughNoop)(this.initiatingTasks, this.taskGraph, this.projectGraph);
61
66
  this.processedTasks = new Map();
62
67
  this.completedTasks = new Map();
63
68
  this.waitingForTasks = [];
@@ -1071,11 +1076,21 @@ class TaskOrchestrator {
1071
1076
  const reason = stoppingReason === 'fulfilled' ? 'fulfilled' : 'interrupted';
1072
1077
  await this.completeContinuousTask(task, groupId, ownsRunningTasksService, reason);
1073
1078
  }
1079
+ else if (!this.isContinuousTaskNeeded(task.id)) {
1080
+ // No remaining tasks depend on this — the task was about to be
1081
+ // killed by cleanUpUnneededContinuousTasks anyway.
1082
+ await this.completeContinuousTask(task, groupId, ownsRunningTasksService, 'fulfilled');
1083
+ }
1074
1084
  else {
1075
1085
  console.error(`Task "${task.id}" is continuous but exited with code ${code}`);
1076
1086
  await this.completeContinuousTask(task, groupId, ownsRunningTasksService, 'crashed');
1077
1087
  }
1078
1088
  }
1089
+ isContinuousTaskNeeded(taskId) {
1090
+ return this.tasksSchedule
1091
+ .getIncompleteTasks()
1092
+ .some((t) => this.taskGraph.continuousDependencies[t.id]?.includes(taskId));
1093
+ }
1079
1094
  async completeContinuousTask(task, groupId, ownsRunningTasksService, reason) {
1080
1095
  if (this.completedTasks.has(task.id))
1081
1096
  return;
@@ -1129,9 +1144,11 @@ class TaskOrchestrator {
1129
1144
  continue;
1130
1145
  await this.completeContinuousTask(task, groupId, ownsRunningTasksService, reason);
1131
1146
  }
1132
- // Kill all processes
1133
- this.forkedProcessTaskRunner.cleanup();
1147
+ // Kill all processes — await forked runner cleanup for graceful shutdown
1148
+ const forkedCleanup = this.forkedProcessTaskRunner.cleanup();
1149
+ const continuousTaskIds = new Set(continuousSnapshot.map(([id]) => id));
1134
1150
  await Promise.all([
1151
+ forkedCleanup,
1135
1152
  ...continuousSnapshot.map(async ([taskId, { runningTask }]) => {
1136
1153
  try {
1137
1154
  await runningTask.kill();
@@ -1148,7 +1165,10 @@ class TaskOrchestrator {
1148
1165
  console.error(`Unable to terminate ${taskId}\nError:`, e);
1149
1166
  }
1150
1167
  }),
1151
- ...Array.from(this.runningRunCommandsTasks).map(async ([taskId, t]) => {
1168
+ // Skip tasks already killed via continuousSnapshot to avoid duplicate signals
1169
+ ...Array.from(this.runningRunCommandsTasks)
1170
+ .filter(([taskId]) => !continuousTaskIds.has(taskId))
1171
+ .map(async ([taskId, t]) => {
1152
1172
  try {
1153
1173
  await t.kill();
1154
1174
  }
@@ -1163,19 +1183,26 @@ class TaskOrchestrator {
1163
1183
  await Promise.all(this.discreteTaskExitHandled.values());
1164
1184
  }
1165
1185
  setupSignalHandlers() {
1166
- process.once('SIGINT', () => {
1186
+ // Use process.on (not once) so the handler stays registered and absorbs
1187
+ // re-raised signals from signal-exit. Without this, signal-exit's handler
1188
+ // sees no remaining listeners after our once-handler auto-removes, and
1189
+ // re-raises the signal — killing the process before async cleanup completes.
1190
+ // The cleanup() idempotency guard (cleanupPromise) prevents double execution.
1191
+ const handleSignal = (signal) => {
1192
+ if (this.stopRequested)
1193
+ return;
1167
1194
  this.stopRequested = true;
1168
1195
  if (!this.tuiEnabled) {
1169
1196
  // Synchronously remove DB entries before async cleanup to prevent
1170
1197
  // new nx processes from seeing stale "Waiting for ..." messages.
1171
- // This replicates the cleanup that process.exit() + Rust Drop
1172
- // previously provided.
1173
1198
  for (const [taskId, { ownsRunningTasksService }] of this
1174
1199
  .runningContinuousTasks) {
1175
1200
  if (ownsRunningTasksService) {
1176
1201
  this.runningTasksService?.removeRunningTask(taskId);
1177
1202
  }
1178
1203
  }
1204
+ }
1205
+ if (signal === 'SIGINT' && !this.tuiEnabled) {
1179
1206
  // Silence output — pnpm (and similar wrappers) may exit before nx
1180
1207
  // finishes cleanup, returning the shell prompt. Any output after
1181
1208
  // that point would appear after the prompt.
@@ -1192,26 +1219,13 @@ class TaskOrchestrator {
1192
1219
  this.resolveStopPromise();
1193
1220
  }
1194
1221
  else {
1195
- process.exit((0, exit_codes_1.signalToCode)('SIGINT'));
1196
- }
1197
- });
1198
- });
1199
- process.once('SIGTERM', () => {
1200
- this.stopRequested = true;
1201
- this.cleanup().finally(() => {
1202
- if (this.resolveStopPromise) {
1203
- this.resolveStopPromise();
1222
+ process.exit((0, exit_codes_1.signalToCode)(signal));
1204
1223
  }
1205
1224
  });
1206
- });
1207
- process.once('SIGHUP', () => {
1208
- this.stopRequested = true;
1209
- this.cleanup().finally(() => {
1210
- if (this.resolveStopPromise) {
1211
- this.resolveStopPromise();
1212
- }
1213
- });
1214
- });
1225
+ };
1226
+ process.on('SIGINT', () => handleSignal('SIGINT'));
1227
+ process.on('SIGTERM', () => handleSignal('SIGTERM'));
1228
+ process.on('SIGHUP', () => handleSignal('SIGHUP'));
1215
1229
  }
1216
1230
  cleanUpUnneededContinuousTasks() {
1217
1231
  const incompleteTasks = this.tasksSchedule.getIncompleteTasks();
@@ -31,6 +31,17 @@ export declare function getOutputsForTargetAndConfiguration(target: Task['target
31
31
  export declare function interpolate(template: string, data: any): string;
32
32
  export declare function getTargetConfigurationForTask(task: Task, projectGraph: ProjectGraph): TargetConfiguration | undefined;
33
33
  export declare function getExecutorNameForTask(task: Task, projectGraph: ProjectGraph): string;
34
+ /**
35
+ * Expand a set of initiating task IDs by walking through any `nx:noop` tasks
36
+ * and replacing them with their direct dependencies + continuous dependencies.
37
+ * Non-noop tasks are kept as-is; cycles are safe.
38
+ *
39
+ * An `nx:noop` executor returns immediately, so if it is the only thing
40
+ * anchoring a continuous child, the child gets killed by
41
+ * `cleanUpUnneededContinuousTasks` the moment the noop completes. Treating the
42
+ * noop's dependencies as the real anchors preserves the intended orchestration.
43
+ */
44
+ export declare function expandInitiatingTasksThroughNoop(initiatingTasks: Task[], taskGraph: TaskGraph, projectGraph: ProjectGraph): Set<string>;
34
45
  export declare function getExecutorForTask(task: Task, projects: Record<string, ProjectConfiguration>): ExecutorConfig & {
35
46
  isNgCompat: boolean;
36
47
  isNxExecutor: boolean;
@@ -14,6 +14,7 @@ exports.getOutputsForTargetAndConfiguration = getOutputsForTargetAndConfiguratio
14
14
  exports.interpolate = interpolate;
15
15
  exports.getTargetConfigurationForTask = getTargetConfigurationForTask;
16
16
  exports.getExecutorNameForTask = getExecutorNameForTask;
17
+ exports.expandInitiatingTasksThroughNoop = expandInitiatingTasksThroughNoop;
17
18
  exports.getExecutorForTask = getExecutorForTask;
18
19
  exports.getCustomHasher = getCustomHasher;
19
20
  exports.removeTasksFromTaskGraph = removeTasksFromTaskGraph;
@@ -291,6 +292,42 @@ function getTargetConfigurationForTask(task, projectGraph) {
291
292
  function getExecutorNameForTask(task, projectGraph) {
292
293
  return getTargetConfigurationForTask(task, projectGraph)?.executor;
293
294
  }
295
+ /**
296
+ * Expand a set of initiating task IDs by walking through any `nx:noop` tasks
297
+ * and replacing them with their direct dependencies + continuous dependencies.
298
+ * Non-noop tasks are kept as-is; cycles are safe.
299
+ *
300
+ * An `nx:noop` executor returns immediately, so if it is the only thing
301
+ * anchoring a continuous child, the child gets killed by
302
+ * `cleanUpUnneededContinuousTasks` the moment the noop completes. Treating the
303
+ * noop's dependencies as the real anchors preserves the intended orchestration.
304
+ */
305
+ function expandInitiatingTasksThroughNoop(initiatingTasks, taskGraph, projectGraph) {
306
+ const expanded = new Set();
307
+ const visited = new Set();
308
+ const queue = initiatingTasks.map((t) => t.id);
309
+ while (queue.length > 0) {
310
+ const taskId = queue.shift();
311
+ if (visited.has(taskId))
312
+ continue;
313
+ visited.add(taskId);
314
+ const task = taskGraph.tasks[taskId];
315
+ if (!task)
316
+ continue;
317
+ if (getExecutorNameForTask(task, projectGraph) === 'nx:noop') {
318
+ for (const dep of taskGraph.dependencies[taskId] ?? []) {
319
+ queue.push(dep);
320
+ }
321
+ for (const dep of taskGraph.continuousDependencies[taskId] ?? []) {
322
+ queue.push(dep);
323
+ }
324
+ }
325
+ else {
326
+ expanded.add(taskId);
327
+ }
328
+ }
329
+ return expanded;
330
+ }
294
331
  function getExecutorForTask(task, projects) {
295
332
  const executor = projects[task.target.project]?.targets?.[task.target.target]?.executor;
296
333
  const [nodeModule, executorName] = (0, executor_utils_1.parseExecutor)(executor);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx",
3
- "version": "23.0.0-beta.1",
3
+ "version": "23.0.0-pr.33655.cdc3e72",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.",
@@ -146,7 +146,6 @@
146
146
  "supports-color": "7.2.0",
147
147
  "tar-stream": "2.2.0",
148
148
  "tmp": "0.2.4",
149
- "tree-kill": "1.2.2",
150
149
  "tsconfig-paths": "4.2.0",
151
150
  "tslib": "2.8.1",
152
151
  "util-deprecate": "1.0.2",
@@ -171,16 +170,16 @@
171
170
  }
172
171
  },
173
172
  "optionalDependencies": {
174
- "@nx/nx-darwin-arm64": "23.0.0-beta.1",
175
- "@nx/nx-darwin-x64": "23.0.0-beta.1",
176
- "@nx/nx-freebsd-x64": "23.0.0-beta.1",
177
- "@nx/nx-linux-arm-gnueabihf": "23.0.0-beta.1",
178
- "@nx/nx-linux-arm64-gnu": "23.0.0-beta.1",
179
- "@nx/nx-linux-arm64-musl": "23.0.0-beta.1",
180
- "@nx/nx-linux-x64-gnu": "23.0.0-beta.1",
181
- "@nx/nx-linux-x64-musl": "23.0.0-beta.1",
182
- "@nx/nx-win32-arm64-msvc": "23.0.0-beta.1",
183
- "@nx/nx-win32-x64-msvc": "23.0.0-beta.1"
173
+ "@nx/nx-darwin-arm64": "23.0.0-pr.33655.cdc3e72",
174
+ "@nx/nx-darwin-x64": "23.0.0-pr.33655.cdc3e72",
175
+ "@nx/nx-freebsd-x64": "23.0.0-pr.33655.cdc3e72",
176
+ "@nx/nx-linux-arm-gnueabihf": "23.0.0-pr.33655.cdc3e72",
177
+ "@nx/nx-linux-arm64-gnu": "23.0.0-pr.33655.cdc3e72",
178
+ "@nx/nx-linux-arm64-musl": "23.0.0-pr.33655.cdc3e72",
179
+ "@nx/nx-linux-x64-gnu": "23.0.0-pr.33655.cdc3e72",
180
+ "@nx/nx-linux-x64-musl": "23.0.0-pr.33655.cdc3e72",
181
+ "@nx/nx-win32-arm64-msvc": "23.0.0-pr.33655.cdc3e72",
182
+ "@nx/nx-win32-x64-msvc": "23.0.0-pr.33655.cdc3e72"
184
183
  },
185
184
  "nx-migrations": {
186
185
  "migrations": "./migrations.json",