nx 23.3.0 → 24.0.0-beta.1

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.
Files changed (114) hide show
  1. package/README.md +3 -6
  2. package/dist/nx-cloud-internals.d.ts +1 -0
  3. package/dist/nx-cloud-internals.js +7 -1
  4. package/dist/src/adapter/ngcli-adapter.js +1 -0
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
  6. package/dist/src/command-line/graph/graph.js +3 -5
  7. package/dist/src/command-line/init/command-object.js +0 -12
  8. package/dist/src/command-line/init/init-v2.js +0 -2
  9. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  10. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  11. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +76 -15
  12. package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
  13. package/dist/src/command-line/migrate/execute-migration.js +18 -10
  14. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
  15. package/dist/src/command-line/migrate/migrate-commits.js +10 -3
  16. package/dist/src/command-line/migrate/migrate.js +43 -6
  17. package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
  18. package/dist/src/command-line/migrate/run/agent-output.js +5 -0
  19. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  20. package/dist/src/command-line/migrate/run/broker.js +68 -20
  21. package/dist/src/command-line/migrate/run/index.d.ts +4 -3
  22. package/dist/src/command-line/migrate/run/index.js +8 -1
  23. package/dist/src/command-line/migrate/run/orchestrator.js +72 -13
  24. package/dist/src/command-line/migrate/run/runbook.js +19 -0
  25. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  26. package/dist/src/command-line/migrate/run/util.d.ts +3 -0
  27. package/dist/src/command-line/migrate/run/util.js +7 -1
  28. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
  29. package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
  30. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
  31. package/dist/src/command-line/run/executor-utils.js +1 -0
  32. package/dist/src/command-line/run/run-one.d.ts +20 -1
  33. package/dist/src/command-line/run/run-one.js +153 -5
  34. package/dist/src/command-line/show/show-target/info.js +1 -1
  35. package/dist/src/command-line/show/show-target/utils.js +1 -0
  36. package/dist/src/command-line/watch/command-object.js +3 -22
  37. package/dist/src/command-line/watch/watch.d.ts +0 -7
  38. package/dist/src/command-line/watch/watch.js +1 -1
  39. package/dist/src/config/misc-interfaces.d.ts +12 -0
  40. package/dist/src/config/task-graph.d.ts +1 -1
  41. package/dist/src/config/workspace-json-project-json.d.ts +65 -0
  42. package/dist/src/core/graph/main.js +1 -1
  43. package/dist/src/daemon/client/client.d.ts +9 -0
  44. package/dist/src/daemon/client/client.js +89 -37
  45. package/dist/src/daemon/server/shutdown-utils.js +0 -3
  46. package/dist/src/daemon/socket-utils.d.ts +9 -4
  47. package/dist/src/daemon/socket-utils.js +12 -49
  48. package/dist/src/daemon/tmp-dir.d.ts +19 -14
  49. package/dist/src/daemon/tmp-dir.js +142 -354
  50. package/dist/src/devkit-exports.d.ts +1 -1
  51. package/dist/src/devkit-internals.d.ts +1 -1
  52. package/dist/src/devkit-internals.js +1 -0
  53. package/dist/src/hasher/check-task-files.js +4 -4
  54. package/dist/src/hasher/file-hasher.js +9 -7
  55. package/dist/src/hasher/hash-task.js +4 -0
  56. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  57. package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
  58. package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
  59. package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
  60. package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +145 -0
  61. package/dist/src/native/index.d.ts +180 -12
  62. package/dist/src/native/native-bindings.js +8 -0
  63. package/dist/src/native/nx.wasi-browser.js +1 -0
  64. package/dist/src/native/nx.wasi.cjs +1 -0
  65. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  66. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  67. package/dist/src/nx-cloud/update-manager.d.ts +7 -0
  68. package/dist/src/nx-cloud/update-manager.js +237 -50
  69. package/dist/src/project-graph/affected/affected-tasks.js +1 -0
  70. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
  71. package/dist/src/tasks-runner/create-task-graph.d.ts +8 -7
  72. package/dist/src/tasks-runner/create-task-graph.js +47 -26
  73. package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
  74. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  75. package/dist/src/tasks-runner/life-cycle.js +7 -0
  76. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  77. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  78. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  79. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  80. package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
  81. package/dist/src/tasks-runner/readiness/probes.js +62 -0
  82. package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
  83. package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
  84. package/dist/src/tasks-runner/run-command.js +5 -4
  85. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  86. package/dist/src/tasks-runner/task-env.d.ts +10 -0
  87. package/dist/src/tasks-runner/task-env.js +26 -5
  88. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  89. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  90. package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -8
  91. package/dist/src/tasks-runner/task-orchestrator.js +460 -76
  92. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  93. package/dist/src/tasks-runner/tasks-schedule.js +64 -6
  94. package/dist/src/tasks-runner/utils.d.ts +20 -5
  95. package/dist/src/tasks-runner/utils.js +70 -35
  96. package/dist/src/ultracache/overrides.js +4 -0
  97. package/dist/src/utils/child-process.js +1 -0
  98. package/dist/src/utils/git-utils.d.ts +6 -1
  99. package/dist/src/utils/git-utils.js +26 -5
  100. package/dist/src/utils/owned-private-dir.d.ts +1 -17
  101. package/dist/src/utils/owned-private-dir.js +10 -62
  102. package/dist/src/utils/perf-hooks.d.ts +1 -0
  103. package/dist/src/utils/plugins/core-plugins.js +1 -1
  104. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  105. package/dist/src/utils/spawn-without-terminal.js +110 -0
  106. package/dist/src/utils/string-similarity.d.ts +38 -0
  107. package/dist/src/utils/string-similarity.js +108 -0
  108. package/migrations.json +7 -0
  109. package/package.json +11 -20
  110. package/readme-template.md +3 -3
  111. package/schemas/nx-schema.json +70 -0
  112. package/schemas/project-schema.json +67 -0
  113. package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
  114. package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
@@ -1,5 +1,5 @@
1
1
  import { Task } from '../config/task-graph';
2
- import { BatchInfo, BatchStatus, ExternalObject, PerformanceSummaryPayload, TaskResult, TaskStatus as NativeTaskStatus } from '../native';
2
+ import { BatchInfo, BatchStatus, ExternalObject, PerformanceSummaryPayload, TaskReadiness, TaskResult, TaskStatus as NativeTaskStatus } from '../native';
3
3
  import { TaskStatus } from './tasks-runner';
4
4
  /**
5
5
  * The result of a completed {@link Task}.
@@ -48,6 +48,8 @@ export interface LifeCycle {
48
48
  registerRunningTaskWithEmptyParser?(taskId: string): void;
49
49
  appendTaskOutput?(taskId: string, output: string, isPtyTask: boolean): void;
50
50
  setTaskStatus?(taskId: string, status: NativeTaskStatus): void;
51
+ /** Only continuous tasks that declare a `readyWhen` report readiness. */
52
+ setTaskReadiness?(taskId: string, readiness: TaskReadiness): void;
51
53
  setTaskTiming?(taskId: string, startTime: number, endTime: number): void;
52
54
  registerForcedShutdownCallback?(callback: () => void): void;
53
55
  setEstimatedTaskTimings?(timings: Record<string, number>): void;
@@ -82,6 +84,7 @@ export declare class CompositeLifeCycle implements LifeCycle {
82
84
  registerRunningTaskWithEmptyParser(taskId: string): void;
83
85
  appendTaskOutput(taskId: string, output: string, isPtyTask: boolean): void;
84
86
  setTaskStatus(taskId: string, status: NativeTaskStatus): void;
87
+ setTaskReadiness(taskId: string, readiness: TaskReadiness): void;
85
88
  setTaskTiming(taskId: string, startTime: number, endTime: number): void;
86
89
  registerForcedShutdownCallback(callback: () => void): void;
87
90
  setEstimatedTaskTimings(timings: Record<string, number>): void;
@@ -95,6 +95,13 @@ class CompositeLifeCycle {
95
95
  }
96
96
  }
97
97
  }
98
+ setTaskReadiness(taskId, readiness) {
99
+ for (let l of this.lifeCycles) {
100
+ if (l.setTaskReadiness) {
101
+ l.setTaskReadiness(taskId, readiness);
102
+ }
103
+ }
104
+ }
98
105
  setTaskTiming(taskId, startTime, endTime) {
99
106
  for (let l of this.lifeCycles) {
100
107
  if (l.setTaskTiming) {
@@ -4,6 +4,8 @@ import { TaskResult } from '../life-cycle';
4
4
  export interface TaskTiming {
5
5
  startTime?: number;
6
6
  endTime?: number;
7
+ /** When a continuous task passed its `readyWhen` check. */
8
+ readyTime?: number;
7
9
  continuous: boolean;
8
10
  }
9
11
  /** A discrete task that ran, with its window and whether it monopolizes the pool (`parallelism: false`). */
@@ -122,8 +124,9 @@ export declare class PerformanceAnalysis {
122
124
  private finishEstimate;
123
125
  /**
124
126
  * Earliest this task became eligible, independent of slots: the latest of run
125
- * start, dependency ends, any continuous dependency's *start* (an ordering
126
- * constraint, not contention), and any earlier batch sibling's end.
127
+ * start, dependency ends, any continuous dependency's *start*, or its
128
+ * readiness when the edge waits for it (an ordering constraint, not
129
+ * contention), and any earlier batch sibling's end.
127
130
  */
128
131
  private readyTime;
129
132
  /**
@@ -101,7 +101,9 @@ class PerformanceAnalysis {
101
101
  */
102
102
  predecessorsFor(id, durations) {
103
103
  const start = this.timings.get(id)?.startTime ?? Infinity;
104
- const deps = (this.taskGraph.dependencies[id] ?? []).filter((d) => durations.has(d));
104
+ const deps = (this.taskGraph.dependencies[id] ?? [])
105
+ .map((d) => d.id)
106
+ .filter((d) => durations.has(d));
105
107
  const siblings = (this.batchSiblings.get(id) ?? []).filter((s) => {
106
108
  const sEnd = this.timings.get(s)?.endTime;
107
109
  return durations.has(s) && sEnd != null && sEnd <= start;
@@ -142,22 +144,26 @@ class PerformanceAnalysis {
142
144
  }
143
145
  /**
144
146
  * Earliest this task became eligible, independent of slots: the latest of run
145
- * start, dependency ends, any continuous dependency's *start* (an ordering
146
- * constraint, not contention), and any earlier batch sibling's end.
147
+ * start, dependency ends, any continuous dependency's *start*, or its
148
+ * readiness when the edge waits for it (an ordering constraint, not
149
+ * contention), and any earlier batch sibling's end.
147
150
  */
148
151
  readyTime(id, runStart) {
149
152
  const start = this.timings.get(id)?.startTime;
150
153
  let result = runStart;
151
154
  for (const dep of this.taskGraph.dependencies[id] ?? []) {
152
- const end = this.timings.get(dep)?.endTime;
155
+ const end = this.timings.get(dep.id)?.endTime;
153
156
  if (end != null) {
154
157
  result = Math.max(result, end);
155
158
  }
156
159
  }
157
160
  for (const cdep of this.taskGraph.continuousDependencies?.[id] ?? []) {
158
- const cStart = this.timings.get(cdep)?.startTime;
159
- if (cStart != null) {
160
- result = Math.max(result, cStart);
161
+ const producer = this.timings.get(cdep.id);
162
+ const usable = cdep.waitFor === 'ready'
163
+ ? (producer?.readyTime ?? producer?.startTime)
164
+ : producer?.startTime;
165
+ if (usable != null) {
166
+ result = Math.max(result, usable);
161
167
  }
162
168
  }
163
169
  for (const sibling of this.batchSiblings.get(id) ?? []) {
@@ -1,4 +1,4 @@
1
- import type { BatchInfo, PerformanceSummaryPayload } from '../../native';
1
+ import { TaskReadiness, type BatchInfo, type PerformanceSummaryPayload } from '../../native';
2
2
  import { TaskGraph } from '../../config/task-graph';
3
3
  import { LifeCycle, TaskResult } from '../life-cycle';
4
4
  import { type PerformanceLifeCycleOptions, type PerformanceSummary } from './performance-analysis';
@@ -14,8 +14,8 @@ import { type PerformanceLifeCycleOptions, type PerformanceSummary } from './per
14
14
  * coordinator time (hashing, scheduling, continuous-dep waits).
15
15
  *
16
16
  * Scope: discrete tasks only. Continuous tasks (no end time) are excluded; a
17
- * discrete task's wait for a continuous dependency to start is eligibility, not
18
- * contention.
17
+ * discrete task's wait for a continuous dependency to start, or to be ready, is
18
+ * eligibility, not contention.
19
19
  */
20
20
  export declare class PerformanceLifeCycle implements LifeCycle {
21
21
  private readonly taskGraph;
@@ -34,6 +34,7 @@ export declare class PerformanceLifeCycle implements LifeCycle {
34
34
  */
35
35
  startCommand(_threadCount?: number, parallel?: number): void;
36
36
  registerRunningBatch(_batchId: string, batchInfo: BatchInfo): void;
37
+ setTaskReadiness(taskId: string, readiness: TaskReadiness): void;
37
38
  endTasks(taskResults: TaskResult[]): void;
38
39
  private entry;
39
40
  /** Analyze the collected timings into a structured summary, or `null` when no discrete task timings were recorded. */
@@ -20,8 +20,8 @@ const performance_analysis_1 = require("./performance-analysis");
20
20
  * coordinator time (hashing, scheduling, continuous-dep waits).
21
21
  *
22
22
  * Scope: discrete tasks only. Continuous tasks (no end time) are excluded; a
23
- * discrete task's wait for a continuous dependency to start is eligibility, not
24
- * contention.
23
+ * discrete task's wait for a continuous dependency to start, or to be ready, is
24
+ * eligibility, not contention.
25
25
  */
26
26
  class PerformanceLifeCycle {
27
27
  constructor(taskGraph, options = {}) {
@@ -51,6 +51,13 @@ class PerformanceLifeCycle {
51
51
  this.batchSiblings.set(id, batchInfo.taskIds.filter((other) => other !== id));
52
52
  }
53
53
  }
54
+ setTaskReadiness(taskId, readiness) {
55
+ const entry = this.entry(taskId);
56
+ // Each waiter polling a shared producer reports Ready; the first one counts.
57
+ if (readiness === 1 /* TaskReadiness.Ready */ && entry.readyTime == null) {
58
+ entry.readyTime = Date.now();
59
+ }
60
+ }
54
61
  endTasks(taskResults) {
55
62
  // Called incrementally (per group/batch); accumulate so the last call sees every timing.
56
63
  for (const { task, status } of taskResults) {
@@ -0,0 +1,9 @@
1
+ import type { RunningTask } from '../running-tasks/running-task';
2
+ import { type NormalizedReadyWhen } from './ready-when';
3
+ export interface ReadinessProbeContext {
4
+ taskId: string;
5
+ runningTask: RunningTask;
6
+ cwd: string;
7
+ signal: AbortSignal;
8
+ }
9
+ export declare function waitForReadiness(readyWhen: NormalizedReadyWhen, context: ReadinessProbeContext): Promise<void>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.waitForReadiness = waitForReadiness;
4
+ const native_1 = require("../../native");
5
+ const ready_when_1 = require("./ready-when");
6
+ // Never rejects for a transient probe error: every failure is retried until
7
+ // the deadline.
8
+ async function waitForReadiness(readyWhen, context) {
9
+ if (native_1.IS_WASM) {
10
+ throw new Error(`The WASM build of Nx does not support "readyWhen", so "${context.taskId}" cannot be probed for readiness.`);
11
+ }
12
+ if (readyWhen.kind === 'logMatches') {
13
+ return waitForLogMatch(readyWhen, context);
14
+ }
15
+ const probe = new native_1.ReadinessProbe({
16
+ timeout: readyWhen.timeout,
17
+ interval: readyWhen.interval,
18
+ ...(readyWhen.kind === 'url'
19
+ ? { url: readyWhen.url }
20
+ : readyWhen.kind === 'port'
21
+ ? { port: readyWhen.port, host: readyWhen.host }
22
+ : { command: readyWhen.command }),
23
+ }, context.cwd);
24
+ const cancel = () => probe.cancel();
25
+ context.signal.addEventListener('abort', cancel);
26
+ if (context.signal.aborted) {
27
+ probe.cancel();
28
+ }
29
+ let ready;
30
+ try {
31
+ ready = await probe.wait();
32
+ }
33
+ finally {
34
+ context.signal.removeEventListener('abort', cancel);
35
+ }
36
+ if (!ready) {
37
+ throw context.signal.aborted
38
+ ? (0, ready_when_1.notReadyError)(context.taskId, 'exited')
39
+ : (0, ready_when_1.readinessTimeoutError)(context.taskId, readyWhen);
40
+ }
41
+ }
42
+ function waitForLogMatch(readyWhen, context) {
43
+ return new Promise((resolve, reject) => {
44
+ const matcher = new native_1.LogMatcher(readyWhen.logMatches);
45
+ let settled = false;
46
+ const settle = (error) => {
47
+ settled = true;
48
+ clearTimeout(timer);
49
+ context.signal.removeEventListener('abort', onAbort);
50
+ error ? reject(error) : resolve();
51
+ };
52
+ const onAbort = () => settle((0, ready_when_1.notReadyError)(context.taskId, 'exited'));
53
+ const timer = setTimeout(() => settle((0, ready_when_1.readinessTimeoutError)(context.taskId, readyWhen)), readyWhen.timeout);
54
+ timer.unref();
55
+ context.signal.addEventListener('abort', onAbort);
56
+ context.runningTask.onOutput((chunk) => {
57
+ if (!settled && matcher.feed(chunk)) {
58
+ settle();
59
+ }
60
+ });
61
+ });
62
+ }
@@ -0,0 +1,38 @@
1
+ import type { ProjectGraph } from '../../config/project-graph';
2
+ import type { Task, TaskGraph } from '../../config/task-graph';
3
+ import type { ProjectConfiguration, ReadyWhen } from '../../config/workspace-json-project-json';
4
+ export type NormalizedReadyWhen = {
5
+ timeout: number;
6
+ interval?: number;
7
+ } & ({
8
+ kind: 'url';
9
+ url: string;
10
+ } | {
11
+ kind: 'port';
12
+ port: number;
13
+ host?: string;
14
+ } | {
15
+ kind: 'command';
16
+ command: string;
17
+ } | {
18
+ kind: 'logMatches';
19
+ logMatches: string[];
20
+ });
21
+ export interface ReadyWhenData {
22
+ workspaceRoot: string;
23
+ projectRoot: string;
24
+ projectName: string;
25
+ options: Record<string, unknown>;
26
+ }
27
+ export declare function normalizeReadyWhen(readyWhen: ReadyWhen, taskId: string, data: ReadyWhenData): NormalizedReadyWhen;
28
+ export declare function readinessTimeoutError(taskId: string, readyWhen: NormalizedReadyWhen): Error;
29
+ export declare class ProducerFailedError extends Error {
30
+ constructor(taskId: string);
31
+ }
32
+ export declare class NotReadyError extends Error {
33
+ }
34
+ export declare function notReadyError(taskId: string, reason: 'exited' | 'was stopped' | 'failed'): Error;
35
+ export declare function getReadyWhenConfig(task: Task, projectGraph: ProjectGraph): ReadyWhen | undefined;
36
+ export declare function getReadyWhen(task: Task, projectGraph: ProjectGraph, projects: Record<string, ProjectConfiguration>, workspaceRoot: string): NormalizedReadyWhen | null;
37
+ export declare function getReadyProducerIds(task: Task, taskGraph: TaskGraph): string[];
38
+ export declare function getReadyDependencies(taskGraph: TaskGraph, projectGraph: ProjectGraph): Record<string, string[]>;
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NotReadyError = exports.ProducerFailedError = void 0;
4
+ exports.normalizeReadyWhen = normalizeReadyWhen;
5
+ exports.readinessTimeoutError = readinessTimeoutError;
6
+ exports.notReadyError = notReadyError;
7
+ exports.getReadyWhenConfig = getReadyWhenConfig;
8
+ exports.getReadyWhen = getReadyWhen;
9
+ exports.getReadyProducerIds = getReadyProducerIds;
10
+ exports.getReadyDependencies = getReadyDependencies;
11
+ const path_1 = require("path");
12
+ const params_1 = require("../../utils/params");
13
+ const utils_1 = require("../utils");
14
+ const DEFAULT_READY_TIMEOUT = 60_000;
15
+ // Node timers take at most a signed 32-bit count of milliseconds
16
+ const MAX_DURATION = 2_147_483_647;
17
+ const PROBE_KEYS = ['url', 'port', 'command', 'logMatches'];
18
+ // The placeholders `outputs` resolve, matched by name so that braces a shell
19
+ // command needs (`{{.State.Health.Status}}`, `${VAR}`, `{a,b}`) stay literal
20
+ const PLACEHOLDER = /{(workspaceRoot|projectRoot|projectName|options\.[^}]+)}/g;
21
+ function normalizeReadyWhen(readyWhen, taskId, data) {
22
+ const invalid = (reason) => new Error(`Task "${taskId}" has an invalid "readyWhen": ${reason}.`);
23
+ if (typeof readyWhen !== 'object' || readyWhen === null) {
24
+ throw invalid('expected an object');
25
+ }
26
+ const config = readyWhen;
27
+ const resolve = (value) => typeof value === 'string'
28
+ ? value.replace(PLACEHOLDER, (match, name) => {
29
+ let resolved = data;
30
+ for (const key of name.split('.')) {
31
+ resolved = resolved?.[key];
32
+ }
33
+ if (resolved == null) {
34
+ throw invalid(`"${match}" has no value`);
35
+ }
36
+ if (!['string', 'number', 'boolean'].includes(typeof resolved)) {
37
+ throw invalid(`"${match}" is not a string or a number`);
38
+ }
39
+ return String(resolved);
40
+ })
41
+ : value;
42
+ // Braces are never valid in a host or port once resolved, so a leftover is
43
+ // a typo. A url keeps them: they are legal in its path and query.
44
+ const resolveWithoutBraces = (key) => {
45
+ const value = resolve(config[key]);
46
+ if (typeof value === 'string' && /[{}]/.test(value)) {
47
+ throw invalid(`"${key}" contains an unknown placeholder`);
48
+ }
49
+ return value;
50
+ };
51
+ const present = PROBE_KEYS.filter((key) => config[key] != null);
52
+ if (present.length !== 1) {
53
+ throw invalid(`expected exactly one of ${PROBE_KEYS.map((k) => `"${k}"`).join(', ')}`);
54
+ }
55
+ const timeout = config.timeout ?? DEFAULT_READY_TIMEOUT;
56
+ if (!isDuration(timeout)) {
57
+ throw invalid(`"timeout" must be an integer from 1 to ${MAX_DURATION}`);
58
+ }
59
+ const shared = { timeout };
60
+ if (config.interval != null) {
61
+ if (!isDuration(config.interval)) {
62
+ throw invalid(`"interval" must be an integer from 1 to ${MAX_DURATION}`);
63
+ }
64
+ shared.interval = config.interval;
65
+ }
66
+ switch (present[0]) {
67
+ case 'url': {
68
+ const url = resolve(config.url);
69
+ if (typeof url !== 'string' || !isHttpUrl(url)) {
70
+ throw invalid('"url" must be an http or https URL');
71
+ }
72
+ return { kind: 'url', url, ...shared };
73
+ }
74
+ case 'port': {
75
+ let port = resolveWithoutBraces('port');
76
+ if (typeof port === 'string' && /^\d+$/.test(port)) {
77
+ port = Number(port);
78
+ }
79
+ if (!isPositiveInteger(port) || port > 65535) {
80
+ throw invalid('"port" must be an integer from 1 to 65535');
81
+ }
82
+ const host = resolveWithoutBraces('host');
83
+ if (host == null) {
84
+ return { kind: 'port', port, ...shared };
85
+ }
86
+ if (typeof host !== 'string' || !host) {
87
+ throw invalid('"host" must be a non-empty string');
88
+ }
89
+ return { kind: 'port', port, host, ...shared };
90
+ }
91
+ case 'command': {
92
+ const command = resolve(config.command);
93
+ if (typeof command !== 'string' || !command) {
94
+ throw invalid('"command" must be a non-empty string');
95
+ }
96
+ return { kind: 'command', command, ...shared };
97
+ }
98
+ case 'logMatches': {
99
+ const logMatches = (Array.isArray(config.logMatches)
100
+ ? config.logMatches
101
+ : [config.logMatches]).map(resolve);
102
+ if (logMatches.length === 0 ||
103
+ !logMatches.every((m) => typeof m === 'string' && !!m)) {
104
+ throw invalid('"logMatches" must be a non-empty string or an array of them');
105
+ }
106
+ return { kind: 'logMatches', logMatches, ...shared };
107
+ }
108
+ }
109
+ }
110
+ function readinessTimeoutError(taskId, readyWhen) {
111
+ return new Error(`Task "${taskId}" did not become ready within ${readyWhen.timeout}ms (readyWhen: ${describeReadyWhen(readyWhen)}).`);
112
+ }
113
+ // The row carries only the status; the owner reports the reason
114
+ class ProducerFailedError extends Error {
115
+ constructor(taskId) {
116
+ super(`Task "${taskId}" failed its readiness check in the process that started it.`);
117
+ }
118
+ }
119
+ exports.ProducerFailedError = ProducerFailedError;
120
+ // An exit or a stop is completed by its own handler, not as a verdict
121
+ class NotReadyError extends Error {
122
+ }
123
+ exports.NotReadyError = NotReadyError;
124
+ function notReadyError(taskId, reason) {
125
+ return new NotReadyError(`Task "${taskId}" ${reason} before it became ready.`);
126
+ }
127
+ function describeReadyWhen(readyWhen) {
128
+ switch (readyWhen.kind) {
129
+ case 'url':
130
+ return `url ${readyWhen.url}`;
131
+ case 'port':
132
+ return readyWhen.host
133
+ ? `port ${readyWhen.host}:${readyWhen.port}`
134
+ : `port ${readyWhen.port}`;
135
+ case 'command':
136
+ return `command "${readyWhen.command}"`;
137
+ case 'logMatches':
138
+ return `logMatches ${readyWhen.logMatches.map((m) => `"${m}"`).join(', ')}`;
139
+ }
140
+ }
141
+ // Raw config: scheduling must not throw on an invalid value, the producer's
142
+ // own start reports that
143
+ function getReadyWhenConfig(task, projectGraph) {
144
+ if (!task.continuous) {
145
+ return undefined;
146
+ }
147
+ return projectGraph.nodes[task.target.project]?.data?.targets?.[task.target.target]?.readyWhen;
148
+ }
149
+ // Placeholders resolve against the options the executor receives: aliases,
150
+ // schema defaults and the spelling of command-line overrides applied
151
+ function getReadyWhen(task, projectGraph, projects, workspaceRoot) {
152
+ const readyWhen = getReadyWhenConfig(task, projectGraph);
153
+ if (readyWhen == null) {
154
+ return null;
155
+ }
156
+ const node = projectGraph.nodes[task.target.project];
157
+ const target = node.data.targets[task.target.target];
158
+ const { schema } = (0, utils_1.getExecutorForTask)(task, projects);
159
+ return normalizeReadyWhen(readyWhen, task.id, {
160
+ workspaceRoot,
161
+ projectRoot: node.data.root,
162
+ projectName: node.name,
163
+ options: (0, params_1.combineOptionsForExecutor)(task.overrides, task.target.configuration ?? target.defaultConfiguration, target, schema, task.target.project, (0, path_1.relative)(task.projectRoot ?? workspaceRoot, process.cwd()), process.env.NX_VERBOSE_LOGGING === 'true'),
164
+ });
165
+ }
166
+ function getReadyProducerIds(task, taskGraph) {
167
+ return (taskGraph.continuousDependencies[task.id] ?? [])
168
+ .filter((edge) => edge.waitFor === 'ready')
169
+ .map((edge) => edge.id);
170
+ }
171
+ // Producers with a probe that each task waits on; tasks with none are left out
172
+ function getReadyDependencies(taskGraph, projectGraph) {
173
+ const readyDependencies = {};
174
+ for (const task of Object.values(taskGraph.tasks)) {
175
+ const producerIds = getReadyProducerIds(task, taskGraph).filter((id) => getReadyWhenConfig(taskGraph.tasks[id], projectGraph) != null);
176
+ if (producerIds.length > 0) {
177
+ readyDependencies[task.id] = producerIds;
178
+ }
179
+ }
180
+ return readyDependencies;
181
+ }
182
+ function isPositiveInteger(value) {
183
+ return Number.isInteger(value) && value > 0;
184
+ }
185
+ function isDuration(value) {
186
+ return isPositiveInteger(value) && value <= MAX_DURATION;
187
+ }
188
+ function isHttpUrl(value) {
189
+ return (URL.canParse(value) && ['http:', 'https:'].includes(new URL(value).protocol));
190
+ }
@@ -50,6 +50,7 @@ const task_results_life_cycle_1 = require("./life-cycles/task-results-life-cycle
50
50
  const task_telemetry_life_cycle_1 = require("./life-cycles/task-telemetry-life-cycle");
51
51
  const task_timings_life_cycle_1 = require("./life-cycles/task-timings-life-cycle");
52
52
  const tui_summary_life_cycle_1 = require("./life-cycles/tui-summary-life-cycle");
53
+ const ready_when_1 = require("./readiness/ready-when");
53
54
  const task_graph_utils_1 = require("./task-graph-utils");
54
55
  const utils_1 = require("./utils");
55
56
  const exit_codes_1 = require("../utils/exit-codes");
@@ -80,7 +81,7 @@ const SYNC_SKIPPED_FIX_MESSAGE_LINES = [
80
81
  '',
81
82
  'Run `nx sync` (no flags) later to sync the workspace.',
82
83
  ];
83
- async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, projectNames, tasks, taskGraph, nxArgs, nxJson, overrides) {
84
+ async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, projectNames, tasks, taskGraph, projectGraph, nxArgs, nxJson, overrides) {
84
85
  const overridesWithoutHidden = { ...overrides };
85
86
  delete overridesWithoutHidden['__overrides_unparsed__'];
86
87
  const isRunOne = initiatingProject != null;
@@ -133,7 +134,7 @@ async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, pr
133
134
  pinnedTasks.push(mainTaskId);
134
135
  const mainContinuousDependencies = taskGraph.continuousDependencies[mainTaskId];
135
136
  if (mainContinuousDependencies.length > 0) {
136
- pinnedTasks.push(mainContinuousDependencies[0]);
137
+ pinnedTasks.push(mainContinuousDependencies[0].id);
137
138
  }
138
139
  const [, target] = mainTaskId.split(':');
139
140
  titleText = `1 ${target} task`;
@@ -179,7 +180,7 @@ async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, pr
179
180
  const lifeCycles = [tsLifeCycle];
180
181
  // Only run the TUI if there are tasks to run
181
182
  if (tasks.length > 0) {
182
- appLifeCycle = new AppLifeCycle(tasks, initiatingTasks.map((t) => t.id), isRunOne ? 0 /* RunMode.RunOne */ : 1 /* RunMode.RunMany */, pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, titleText, workspace_root_1.workspaceRoot, taskGraph, (0, nx_cloud_utils_1.isNxCloudUsed)(nxJson));
183
+ appLifeCycle = new AppLifeCycle(tasks, initiatingTasks.map((t) => t.id), isRunOne ? 0 /* RunMode.RunOne */ : 1 /* RunMode.RunMany */, pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, titleText, workspace_root_1.workspaceRoot, taskGraph, (0, ready_when_1.getReadyDependencies)(taskGraph, projectGraph), (0, nx_cloud_utils_1.isNxCloudUsed)(nxJson));
183
184
  // The native endCommand renders the perf report in the exit popup; the runner
184
185
  // sources the payload and CompositeLifeCycle forwards it here.
185
186
  lifeCycles.unshift(appLifeCycle);
@@ -401,7 +402,7 @@ async function runTasksForCommand(taskSelection, currentProjectGraph, { nxJson }
401
402
  const projectNames = [
402
403
  ...new Set(initiatingTasks.map((t) => t.target.project)),
403
404
  ];
404
- const { lifeCycle, renderIsDone, printSummary, restoreTerminal } = await getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, projectNames, tasks, taskGraph, nxArgs, nxJson, overrides);
405
+ const { lifeCycle, renderIsDone, printSummary, restoreTerminal } = await getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, projectNames, tasks, taskGraph, projectGraph, nxArgs, nxJson, overrides);
405
406
  try {
406
407
  const taskResults = await invokeTasksRunner({
407
408
  tasks,
@@ -14,6 +14,9 @@ class NodeChildProcessWithNonDirectOutput {
14
14
  this.terminalOutputChunks = [];
15
15
  this.exitCallbacks = [];
16
16
  this.outputCallbacks = [];
17
+ // Decode as a stream so a code point split across chunks stays intact
18
+ this.childProcess.stdout.setEncoding('utf8');
19
+ this.childProcess.stderr.setEncoding('utf8');
17
20
  if (streamOutput) {
18
21
  if (process.env.NX_PREFIX_OUTPUT === 'true') {
19
22
  const color = (0, output_prefix_1.getColor)(prefix);
@@ -54,16 +57,14 @@ class NodeChildProcessWithNonDirectOutput {
54
57
  process.send(message);
55
58
  }
56
59
  });
57
- this.childProcess.stdout.on('data', (chunk) => {
58
- const output = chunk.toString();
60
+ this.childProcess.stdout.on('data', (output) => {
59
61
  this.terminalOutputChunks.push(output);
60
62
  // Stream output to TUI via callbacks
61
63
  for (const cb of this.outputCallbacks) {
62
64
  cb(output);
63
65
  }
64
66
  });
65
- this.childProcess.stderr.on('data', (chunk) => {
66
- const output = chunk.toString();
67
+ this.childProcess.stderr.on('data', (output) => {
67
68
  this.terminalOutputChunks.push(output);
68
69
  // Stream output to TUI via callbacks
69
70
  for (const cb of this.outputCallbacks) {
@@ -37,6 +37,16 @@ export declare function getTaskSpecificEnv(task: Task, graph: ProjectGraph): Nod
37
37
  */
38
38
  export declare function getGraphTimeDotEnvForTask(projectRoot: string, target: string, configuration?: string, nonAtomizedTarget?: string, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
39
39
  export declare function getEnvVariablesForTask(task: Task, taskSpecificEnv: NodeJS.ProcessEnv, forceColor: string, skipNxCache: boolean, captureStderr: boolean, outputPath: string, streamOutput: boolean): NodeJS.ProcessEnv;
40
+ /**
41
+ * Pids of the nx processes this one is nested inside, outermost first. Empty
42
+ * for a root invocation.
43
+ */
44
+ export declare function getInvocationAncestorPids(): number[];
45
+ /**
46
+ * Pid of the outermost nx process in this tree, which is this process when it
47
+ * is the root.
48
+ */
49
+ export declare function getInvocationRootPid(): number;
40
50
  /**
41
51
  * This function loads one or more .env files and expands the variables in them.
42
52
  * When multiple files are provided, all files are loaded first, then variable
@@ -5,6 +5,8 @@ exports.getEnvVariablesForBatchProcess = getEnvVariablesForBatchProcess;
5
5
  exports.getTaskSpecificEnv = getTaskSpecificEnv;
6
6
  exports.getGraphTimeDotEnvForTask = getGraphTimeDotEnvForTask;
7
7
  exports.getEnvVariablesForTask = getEnvVariablesForTask;
8
+ exports.getInvocationAncestorPids = getInvocationAncestorPids;
9
+ exports.getInvocationRootPid = getInvocationRootPid;
8
10
  exports.loadAndExpandDotEnvFile = loadAndExpandDotEnvFile;
9
11
  exports.unloadDotEnvFile = unloadDotEnvFile;
10
12
  exports.getEnvFilesForTask = getEnvFilesForTask;
@@ -128,6 +130,10 @@ function getNxEnvVariablesForForkedProcess(forceColor, skipNxCache, captureStder
128
130
  FORCE_COLOR: forceColor,
129
131
  NX_WORKSPACE_ROOT: workspace_root_1.workspaceRoot,
130
132
  NX_SKIP_NX_CACHE: skipNxCache ? 'true' : undefined,
133
+ // pids of the forked process's ancestor nx processes, outermost first.
134
+ // Only an ancestor re-invoking a task is a loop — sibling nx processes
135
+ // running the same task are legitimate.
136
+ NX_INVOCATION_ANCESTOR_PIDS: appendAncestorPid(process.env.NX_INVOCATION_ANCESTOR_PIDS),
131
137
  };
132
138
  if (outputPath) {
133
139
  env.NX_TERMINAL_OUTPUT_PATH = outputPath;
@@ -140,6 +146,26 @@ function getNxEnvVariablesForForkedProcess(forceColor, skipNxCache, captureStder
140
146
  }
141
147
  return env;
142
148
  }
149
+ function appendAncestorPid(inherited) {
150
+ return inherited ? `${inherited},${process.pid}` : String(process.pid);
151
+ }
152
+ /**
153
+ * Pids of the nx processes this one is nested inside, outermost first. Empty
154
+ * for a root invocation.
155
+ */
156
+ function getInvocationAncestorPids() {
157
+ return (process.env.NX_INVOCATION_ANCESTOR_PIDS ?? '')
158
+ .split(',')
159
+ .map((pid) => Number(pid))
160
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
161
+ }
162
+ /**
163
+ * Pid of the outermost nx process in this tree, which is this process when it
164
+ * is the root.
165
+ */
166
+ function getInvocationRootPid() {
167
+ return getInvocationAncestorPids()[0] ?? process.pid;
168
+ }
143
169
  function getNxEnvVariablesForTask(task, forceColor, skipNxCache, captureStderr, outputPath, streamOutput) {
144
170
  const env = {
145
171
  NX_TASK_TARGET_PROJECT: task.target.project,
@@ -153,16 +179,11 @@ function getNxEnvVariablesForTask(task, forceColor, skipNxCache, captureStderr,
153
179
  if (task.target.target === 'test') {
154
180
  env.NX_TERMINAL_CAPTURE_STDERR = 'true';
155
181
  }
156
- // Pass the root Nx process PID to nested processes for DB-based loop detection.
157
- // The root PID is used as a key in the task_invocations table to track which tasks
158
- // have been invoked across nested Nx processes.
159
182
  return {
160
183
  ...getNxEnvVariablesForForkedProcess(forceColor, skipNxCache, captureStderr, outputPath, streamOutput),
161
184
  ...env,
162
185
  // Ensure the TUI does not get spawned within the TUI if ever tasks invoke Nx again
163
186
  NX_TUI: 'false',
164
- // tracks the root PID for child nx tasks, used to verify nx is infinitely recursing through the same tasks
165
- NX_INVOCATION_ROOT_PID: process.env.NX_INVOCATION_ROOT_PID ?? String(process.pid),
166
187
  };
167
188
  }
168
189
  /**
@@ -1,24 +1,26 @@
1
1
  import { ProjectGraph } from '../config/project-graph';
2
- import { TaskGraph } from '../config/task-graph';
2
+ import { TaskGraph, TaskGraphEdge } from '../config/task-graph';
3
+ /** Task graph edges, or the project ids of the command graph. */
4
+ type Edge = string | TaskGraphEdge;
3
5
  /**
4
6
  * This function finds a cycle in the graph.
5
7
  * @returns the first cycle found, or null if no cycle is found.
6
8
  */
7
9
  export declare function findCycle(graph: {
8
- dependencies: Record<string, string[]>;
9
- continuousDependencies?: Record<string, string[]>;
10
+ dependencies: Record<string, Edge[]>;
11
+ continuousDependencies?: Record<string, Edge[]>;
10
12
  }): string[] | null;
11
13
  /**
12
14
  * This function finds all cycles in the graph.
13
15
  * @returns a list of unique task ids in all cycles found, or null if no cycle is found.
14
16
  */
15
17
  export declare function findCycles(graph: {
16
- dependencies: Record<string, string[]>;
17
- continuousDependencies?: Record<string, string[]>;
18
+ dependencies: Record<string, Edge[]>;
19
+ continuousDependencies?: Record<string, Edge[]>;
18
20
  }): Set<string> | null;
19
21
  export declare function makeAcyclic(graph: {
20
22
  roots: string[];
21
- dependencies: Record<string, string[]>;
23
+ dependencies: Record<string, Edge[]>;
22
24
  }): void;
23
25
  export declare function validateNoAtomizedTasks(taskGraph: TaskGraph, projectGraph: ProjectGraph): void;
24
26
  export declare function assertTaskGraphDoesNotContainInvalidTargets(taskGraph: TaskGraph): void;
@@ -28,3 +30,4 @@ export declare function getLeafTasks(taskGraph: TaskGraph): Set<string>;
28
30
  * Considers both dependencies and continuousDependencies.
29
31
  */
30
32
  export declare function walkTaskGraph(taskGraph: TaskGraph, walkFn: (rootTaskIds: string[]) => Promise<void>): Promise<void>;
33
+ export {};