nx 22.7.9 → 22.7.11

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.
Binary file
@@ -11,6 +11,8 @@ export declare class ForkedProcessTaskRunner {
11
11
  cliPath: string;
12
12
  private readonly verbose;
13
13
  private processes;
14
+ private processMessageHandler;
15
+ private processExitHandler;
14
16
  private finishedProcesses;
15
17
  private pseudoTerminals;
16
18
  constructor(options: DefaultTasksRunnerOptions, tuiEnabled: boolean);
@@ -39,4 +41,5 @@ export declare class ForkedProcessTaskRunner {
39
41
  private writeTerminalOutput;
40
42
  cleanup(signal?: NodeJS.Signals): void;
41
43
  private setupProcessEventListeners;
44
+ removeProcessEventListeners(): void;
42
45
  }
@@ -271,7 +271,7 @@ class ForkedProcessTaskRunner {
271
271
  this.cleanUpBatchProcesses();
272
272
  }
273
273
  setupProcessEventListeners() {
274
- const messageHandler = (message) => {
274
+ this.processMessageHandler = (message) => {
275
275
  this.pseudoTerminals.forEach((p) => {
276
276
  p.sendMessageToChildren(message);
277
277
  });
@@ -281,14 +281,23 @@ class ForkedProcessTaskRunner {
281
281
  }
282
282
  });
283
283
  };
284
+ this.processExitHandler = () => {
285
+ this.cleanup();
286
+ process.off('message', this.processMessageHandler);
287
+ };
284
288
  // When the nx process gets a message, it will be sent into the task's process
285
- process.on('message', messageHandler);
289
+ process.on('message', this.processMessageHandler);
286
290
  // Terminate any task processes on exit
287
- process.once('exit', () => {
288
- this.cleanup();
289
- process.off('message', messageHandler);
290
- });
291
+ process.once('exit', this.processExitHandler);
291
292
  // Signal handlers removed - TaskOrchestrator owns signal handling
292
293
  }
294
+ // Long-lived processes (Nx Cloud agents) create a runner per invocation;
295
+ // without this each instance stays reachable from `process` forever.
296
+ // Call only after cleanup(): the exit handler is the last-resort child
297
+ // kill, so it must not be removed while children may still be alive.
298
+ removeProcessEventListeners() {
299
+ process.off('message', this.processMessageHandler);
300
+ process.off('exit', this.processExitHandler);
301
+ }
293
302
  }
294
303
  exports.ForkedProcessTaskRunner = ForkedProcessTaskRunner;
@@ -14,6 +14,7 @@ const life_cycle_1 = require("./life-cycle");
14
14
  const task_orchestrator_1 = require("./task-orchestrator");
15
15
  const create_task_hasher_1 = require("../hasher/create-task-hasher");
16
16
  const client_1 = require("../daemon/client/client");
17
+ const shared_running_task_1 = require("./running-tasks/shared-running-task");
17
18
  const task_results_life_cycle_1 = require("./life-cycles/task-results-life-cycle");
18
19
  /**
19
20
  * This function is deprecated. Do not use this
@@ -110,6 +111,11 @@ async function createOrchestrator(tasks, projectGraph, taskGraphForHashing, nxJs
110
111
  orchestrator.processAllScheduledTasks();
111
112
  return orchestrator;
112
113
  }
114
+ // Nothing awaits the dispose chains below, so an unhandled rejection would take
115
+ // down a long-lived agent process.
116
+ function logDisposeFailure(e) {
117
+ console.error('Failed to dispose the task orchestrator:', e);
118
+ }
113
119
  async function runDiscreteTasks(tasks, projectGraph, taskGraphForHashing, nxJson, lifeCycle) {
114
120
  const orchestrator = await createOrchestrator(tasks, projectGraph, taskGraphForHashing, nxJson, lifeCycle);
115
121
  let groupId = 0;
@@ -138,12 +144,36 @@ async function runDiscreteTasks(tasks, projectGraph, taskGraphForHashing, nxJson
138
144
  return [hit];
139
145
  return [await orchestrator.runTaskDirectly(true, task, groupId++)];
140
146
  });
141
- return [...batchResults, ...taskResults];
147
+ const results = [...batchResults, ...taskResults];
148
+ // Callers like Nx Cloud agents create an orchestrator per invocation in a
149
+ // long-lived process; release its process-level listeners once all tasks
150
+ // settle, otherwise every invocation's orchestrator stays reachable forever.
151
+ // Not awaited, so callers keep consuming handles as they settle; the forked
152
+ // runner's exit handler still reaps children until dispose() runs.
153
+ Promise.allSettled(results)
154
+ .then(() => orchestrator.dispose())
155
+ .catch(logDisposeFailure);
156
+ return results;
142
157
  }
143
158
  async function runContinuousTasks(tasks, projectGraph, taskGraphForHashing, nxJson, lifeCycle) {
144
159
  const orchestrator = await createOrchestrator(tasks, projectGraph, taskGraphForHashing, nxJson, lifeCycle);
145
- return tasks.reduce((current, task, index) => {
160
+ const runningTasks = tasks.reduce((current, task, index) => {
146
161
  current[task.id] = orchestrator.startContinuousTask(task, index);
147
162
  return current;
148
163
  }, {});
164
+ // Unlike runDiscreteTasks, this must resolve at task start: callers keep
165
+ // the RunningTask handles to kill later, so disposal has to be deferred
166
+ // until every task actually exits.
167
+ Promise.allSettled(Object.entries(runningTasks).map(async ([taskId, promise]) => {
168
+ const runningTask = await promise;
169
+ // A shared task is owned by another nx process; this orchestrator has
170
+ // no child to protect for it, so disposal does not wait on it.
171
+ if (runningTask instanceof shared_running_task_1.SharedRunningTask) {
172
+ return;
173
+ }
174
+ await orchestrator.waitForContinuousTaskExit(taskId);
175
+ }))
176
+ .then(() => orchestrator.dispose())
177
+ .catch(logDisposeFailure);
178
+ return runningTasks;
149
179
  }
@@ -22,6 +22,8 @@ function getTasksHistoryLifeCycle() {
22
22
  class TaskHistoryLifeCycle {
23
23
  constructor() {
24
24
  this.startTimings = {};
25
+ // Holds no terminalOutput: this singleton outlives every run in the
26
+ // process, and agents complete thousands of tasks in one process.
25
27
  this.pendingResults = new Map();
26
28
  this.taskRuns = new Map();
27
29
  this.taskHistory = (0, task_history_1.getTaskHistory)();
@@ -36,25 +38,28 @@ class TaskHistoryLifeCycle {
36
38
  }
37
39
  }
38
40
  async endTasks(taskResults) {
39
- for (const taskResult of taskResults) {
40
- this.pendingResults.set(taskResult.task.id, taskResult);
41
+ for (const { task, code, status } of taskResults) {
42
+ this.pendingResults.set(task.id, { task, code, status });
41
43
  }
42
44
  }
43
45
  async endCommand() {
46
+ const pendingResults = Array.from(this.pendingResults.values());
47
+ this.pendingResults.clear();
44
48
  if (!this.taskHistory) {
45
49
  return;
46
50
  }
47
51
  // Build TaskRun objects now — task.hash is guaranteed to be set by this point
48
- for (const [, taskResult] of this.pendingResults) {
49
- this.taskRuns.set(taskResult.task.hash, {
50
- hash: taskResult.task.hash,
51
- target: taskResult.task.target,
52
- code: taskResult.code,
53
- status: taskResult.status,
54
- start: taskResult.task.startTime ?? this.startTimings[taskResult.task.id],
55
- end: taskResult.task.endTime ?? Date.now(),
56
- cacheable: taskResult.task.cache === true,
52
+ for (const { task, code, status } of pendingResults) {
53
+ this.taskRuns.set(task.hash, {
54
+ hash: task.hash,
55
+ target: task.target,
56
+ code,
57
+ status,
58
+ start: task.startTime ?? this.startTimings[task.id],
59
+ end: task.endTime ?? Date.now(),
60
+ cacheable: task.cache === true,
57
61
  });
62
+ delete this.startTimings[task.id];
58
63
  }
59
64
  const runs = [];
60
65
  // Only check for flaky tasks among cacheable tasks
@@ -82,6 +82,9 @@ class BatchProcess {
82
82
  onOutput(cb) {
83
83
  this.outputCallbacks.push(cb);
84
84
  }
85
+ // Resolves on the CompleteBatchExecution message, not process exit: the
86
+ // child can outlive its results (open handles keep it alive), so callers
87
+ // must not treat a settled batch as an exited process.
85
88
  async getResults() {
86
89
  return Promise.race([
87
90
  new Promise((_, rej) => {
@@ -3,6 +3,7 @@ import { RunningTasksService } from '../../native';
3
3
  export declare class SharedRunningTask implements RunningTask {
4
4
  private runningTasksService;
5
5
  private exitCallbacks;
6
+ private detached;
6
7
  constructor(runningTasksService: RunningTasksService, taskId: string);
7
8
  getResults(): Promise<{
8
9
  code: number;
@@ -5,7 +5,11 @@ class SharedRunningTask {
5
5
  constructor(runningTasksService, taskId) {
6
6
  this.runningTasksService = runningTasksService;
7
7
  this.exitCallbacks = [];
8
+ this.detached = false;
8
9
  this.waitForTaskToFinish(taskId).then(() => {
10
+ if (this.detached) {
11
+ return;
12
+ }
9
13
  // notify exit callbacks
10
14
  this.exitCallbacks.forEach((cb) => cb(0));
11
15
  });
@@ -13,8 +17,14 @@ class SharedRunningTask {
13
17
  async getResults() {
14
18
  throw new Error('Results cannot be retrieved from a shared task');
15
19
  }
20
+ // Detaches from the shared task; the owning process keeps running it.
16
21
  kill() {
22
+ if (this.detached) {
23
+ return;
24
+ }
25
+ this.detached = true;
17
26
  this.exitCallbacks.forEach((cb) => cb(0));
27
+ this.exitCallbacks = [];
18
28
  }
19
29
  onExit(cb) {
20
30
  this.exitCallbacks.push(cb);
@@ -24,7 +34,8 @@ class SharedRunningTask {
24
34
  // wait for the running task to finish
25
35
  do {
26
36
  await new Promise((resolve) => setTimeout(resolve, 100));
27
- } while (this.runningTasksService.getRunningTasks([taskId]).length);
37
+ } while (!this.detached &&
38
+ this.runningTasksService.getRunningTasks([taskId]).length);
28
39
  }
29
40
  }
30
41
  exports.SharedRunningTask = SharedRunningTask;
@@ -47,6 +47,7 @@ export declare class TaskOrchestrator {
47
47
  private discreteTaskExitHandled;
48
48
  private continuousTaskExitHandled;
49
49
  private cleanupPromise;
50
+ private signalHandlers;
50
51
  constructor(hasher: TaskHasher, initiatingProject: string | undefined, initiatingTasks: Task[], projectGraph: ProjectGraph, taskGraph: TaskGraph, nxJson: NxJsonConfiguration, options: NxArgs & DefaultTasksRunnerOptions, bail: boolean, daemon: DaemonClient, outputStyle: string, taskGraphForHashing?: TaskGraph);
51
52
  init(): Promise<void>;
52
53
  run(): Promise<{
@@ -170,6 +171,8 @@ export declare class TaskOrchestrator {
170
171
  private cleanup;
171
172
  private performCleanup;
172
173
  private setupSignalHandlers;
174
+ waitForContinuousTaskExit(taskId: string): Promise<void>;
175
+ dispose(): Promise<void>;
173
176
  private cleanUpUnneededContinuousTasks;
174
177
  }
175
178
  export declare function getThreadPoolSize(options: NxArgs & DefaultTasksRunnerOptions, taskGraph: TaskGraph): {
@@ -86,6 +86,7 @@ class TaskOrchestrator {
86
86
  this.discreteTaskExitHandled = new Map();
87
87
  this.continuousTaskExitHandled = new Map();
88
88
  this.cleanupPromise = null;
89
+ this.signalHandlers = [];
89
90
  }
90
91
  async init() {
91
92
  this.setupSignalHandlers();
@@ -139,6 +140,7 @@ class TaskOrchestrator {
139
140
  this.cache.removeOldCacheRecords();
140
141
  }
141
142
  await this.cleanup();
143
+ await this.dispose();
142
144
  // Public API (defaultTasksRunner) returns a plain object keyed by
143
145
  // task id. Internal state is a Map for faster lookup.
144
146
  return Object.fromEntries(this.completedTasks);
@@ -1184,7 +1186,7 @@ class TaskOrchestrator {
1184
1186
  await Promise.all(this.discreteTaskExitHandled.values());
1185
1187
  }
1186
1188
  setupSignalHandlers() {
1187
- process.once('SIGINT', () => {
1189
+ const sigintHandler = () => {
1188
1190
  this.stopRequested = true;
1189
1191
  if (!this.tuiEnabled) {
1190
1192
  // Synchronously remove DB entries before async cleanup to prevent
@@ -1216,23 +1218,55 @@ class TaskOrchestrator {
1216
1218
  process.exit((0, exit_codes_1.signalToCode)('SIGINT'));
1217
1219
  }
1218
1220
  });
1219
- });
1220
- process.once('SIGTERM', () => {
1221
+ };
1222
+ const sigtermHandler = () => {
1221
1223
  this.stopRequested = true;
1222
1224
  this.cleanup().finally(() => {
1223
1225
  if (this.resolveStopPromise) {
1224
1226
  this.resolveStopPromise();
1225
1227
  }
1226
1228
  });
1227
- });
1228
- process.once('SIGHUP', () => {
1229
+ };
1230
+ const sighupHandler = () => {
1229
1231
  this.stopRequested = true;
1230
1232
  this.cleanup().finally(() => {
1231
1233
  if (this.resolveStopPromise) {
1232
1234
  this.resolveStopPromise();
1233
1235
  }
1234
1236
  });
1235
- });
1237
+ };
1238
+ for (const [signal, handler] of [
1239
+ ['SIGINT', sigintHandler],
1240
+ ['SIGTERM', sigtermHandler],
1241
+ ['SIGHUP', sighupHandler],
1242
+ ]) {
1243
+ this.signalHandlers.push([signal, handler]);
1244
+ process.once(signal, handler);
1245
+ }
1246
+ }
1247
+ // Registered at child creation, so unlike subscribing to RunningTask.onExit
1248
+ // after the fact it cannot miss an exit that already happened.
1249
+ waitForContinuousTaskExit(taskId) {
1250
+ return this.continuousTaskExitHandled.get(taskId) ?? Promise.resolve();
1251
+ }
1252
+ // Releases the process-level listeners registered by setupSignalHandlers.
1253
+ // Each closes over `this`, so a long-lived caller (an Nx Cloud agent creates
1254
+ // an orchestrator per invocation) leaks whole orchestrators until they run.
1255
+ async dispose() {
1256
+ // The forked runner's exit handler is the last-resort kill for child
1257
+ // processes, and a batch child can outlive its results message. Signal the
1258
+ // children first so removing that handler cannot orphan a live one.
1259
+ try {
1260
+ await this.forkedProcessTaskRunner.cleanup();
1261
+ }
1262
+ catch (e) {
1263
+ console.error('Failed to clean up child processes on dispose:', e);
1264
+ }
1265
+ for (const [signal, handler] of this.signalHandlers) {
1266
+ process.off(signal, handler);
1267
+ }
1268
+ this.signalHandlers = [];
1269
+ this.forkedProcessTaskRunner.removeProcessEventListeners();
1236
1270
  }
1237
1271
  cleanUpUnneededContinuousTasks() {
1238
1272
  const incompleteTasks = this.tasksSchedule.getIncompleteTasks();
@@ -22,6 +22,7 @@ const target_merging_1 = require("../project-graph/utils/project-configuration/t
22
22
  const catalog_1 = require("./catalog");
23
23
  const fileutils_1 = require("./fileutils");
24
24
  const has_nx_js_plugin_1 = require("./has-nx-js-plugin");
25
+ const path_2 = require("./path");
25
26
  const installation_directory_1 = require("./installation-directory");
26
27
  const package_manager_1 = require("./package-manager");
27
28
  const workspace_root_1 = require("./workspace-root");
@@ -34,15 +35,25 @@ function normalizePackageGroup(packageGroup) {
34
35
  }));
35
36
  }
36
37
  function readNxMigrateConfig(json) {
38
+ // Registry fetching uses this value to build a temporary extraction path.
39
+ // Reject unsupported absolute paths and escaping parent traversal before extraction.
40
+ const assertContained = (migrations) => {
41
+ if (!(0, path_2.isContainedRelativePath)(migrations)) {
42
+ throw new Error(`Invalid migrations path "${migrations}" in package "${json.name ?? 'unknown'}@${json.version ?? 'unknown'}": migration paths must not be absolute or escape their base directory through parent traversal.`);
43
+ }
44
+ return migrations;
45
+ };
37
46
  const parseNxMigrationsConfig = (fromJson) => {
38
47
  if (!fromJson) {
39
48
  return {};
40
49
  }
41
50
  if (typeof fromJson === 'string') {
42
- return { migrations: fromJson, packageGroup: [] };
51
+ return { migrations: assertContained(fromJson), packageGroup: [] };
43
52
  }
44
53
  return {
45
- ...(fromJson.migrations ? { migrations: fromJson.migrations } : {}),
54
+ ...(fromJson.migrations
55
+ ? { migrations: assertContained(fromJson.migrations) }
56
+ : {}),
46
57
  ...(fromJson.packageGroup
47
58
  ? { packageGroup: normalizePackageGroup(fromJson.packageGroup) }
48
59
  : {}),
@@ -8,6 +8,12 @@ export declare function normalizePath(osSpecificPath: string): string;
8
8
  * This should not be used to read files on disk because of the removal of Windows drive letters.
9
9
  */
10
10
  export declare function joinPathFragments(...fragments: string[]): string;
11
+ /**
12
+ * True when `relativePath` stays inside the directory it is resolved against.
13
+ * Absolute paths are rejected because `path.join` silently treats them as
14
+ * relative, so they would otherwise slip past a `..` check.
15
+ */
16
+ export declare function isContainedRelativePath(relativePath: string): boolean;
11
17
  /**
12
18
  * When running a script with the package manager (e.g. `npm run`), the package manager will
13
19
  * traverse the directory tree upwards until it finds a `package.json` and will set `process.cwd()`
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizePath = normalizePath;
4
4
  exports.joinPathFragments = joinPathFragments;
5
+ exports.isContainedRelativePath = isContainedRelativePath;
5
6
  exports.getCwd = getCwd;
6
7
  const tslib_1 = require("tslib");
7
8
  const path = tslib_1.__importStar(require("path"));
@@ -23,6 +24,20 @@ function normalizePath(osSpecificPath) {
23
24
  function joinPathFragments(...fragments) {
24
25
  return normalizePath(path.join(...fragments));
25
26
  }
27
+ /**
28
+ * True when `relativePath` stays inside the directory it is resolved against.
29
+ * Absolute paths are rejected because `path.join` silently treats them as
30
+ * relative, so they would otherwise slip past a `..` check.
31
+ */
32
+ function isContainedRelativePath(relativePath) {
33
+ if (path.isAbsolute(relativePath)) {
34
+ return false;
35
+ }
36
+ const normalized = path.normalize(relativePath);
37
+ return !(normalized === '..' ||
38
+ normalized.startsWith(`..${path.sep}`) ||
39
+ normalized.startsWith(`..${path.posix.sep}`));
40
+ }
26
41
  /**
27
42
  * When running a script with the package manager (e.g. `npm run`), the package manager will
28
43
  * traverse the directory tree upwards until it finds a `package.json` and will set `process.cwd()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx",
3
- "version": "22.7.9",
3
+ "version": "22.7.11",
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.",
@@ -175,16 +175,16 @@
175
175
  }
176
176
  },
177
177
  "optionalDependencies": {
178
- "@nx/nx-darwin-arm64": "22.7.9",
179
- "@nx/nx-darwin-x64": "22.7.9",
180
- "@nx/nx-freebsd-x64": "22.7.9",
181
- "@nx/nx-linux-arm-gnueabihf": "22.7.9",
182
- "@nx/nx-linux-arm64-gnu": "22.7.9",
183
- "@nx/nx-linux-arm64-musl": "22.7.9",
184
- "@nx/nx-linux-x64-gnu": "22.7.9",
185
- "@nx/nx-linux-x64-musl": "22.7.9",
186
- "@nx/nx-win32-arm64-msvc": "22.7.9",
187
- "@nx/nx-win32-x64-msvc": "22.7.9"
178
+ "@nx/nx-darwin-arm64": "22.7.11",
179
+ "@nx/nx-darwin-x64": "22.7.11",
180
+ "@nx/nx-freebsd-x64": "22.7.11",
181
+ "@nx/nx-linux-arm-gnueabihf": "22.7.11",
182
+ "@nx/nx-linux-arm64-gnu": "22.7.11",
183
+ "@nx/nx-linux-arm64-musl": "22.7.11",
184
+ "@nx/nx-linux-x64-gnu": "22.7.11",
185
+ "@nx/nx-linux-x64-musl": "22.7.11",
186
+ "@nx/nx-win32-arm64-msvc": "22.7.11",
187
+ "@nx/nx-win32-x64-msvc": "22.7.11"
188
188
  },
189
189
  "nx-migrations": {
190
190
  "migrations": "./migrations.json",