nx 24.0.0-beta.0 → 24.0.0-beta.2

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 (90) hide show
  1. package/dist/nx-cloud-internals.d.ts +1 -0
  2. package/dist/nx-cloud-internals.js +7 -1
  3. package/dist/src/command-line/graph/graph.js +2 -1
  4. package/dist/src/command-line/migrate/agentic/close-agent-session.d.ts +1 -1
  5. package/dist/src/command-line/migrate/agentic/close-agent-session.js +4 -3
  6. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +19 -23
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +16 -19
  8. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +25 -13
  9. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +82 -22
  10. package/dist/src/command-line/migrate/agentic/select.js +3 -3
  11. package/dist/src/command-line/migrate/agentic/types.js +1 -3
  12. package/dist/src/command-line/migrate/command-object.js +5 -9
  13. package/dist/src/command-line/migrate/execute-migration.d.ts +2 -2
  14. package/dist/src/command-line/migrate/execute-migration.js +18 -10
  15. package/dist/src/command-line/migrate/migrate-analytics.d.ts +6 -0
  16. package/dist/src/command-line/migrate/migrate-analytics.js +12 -0
  17. package/dist/src/command-line/migrate/migrate-commits.d.ts +4 -1
  18. package/dist/src/command-line/migrate/migrate-commits.js +10 -3
  19. package/dist/src/command-line/migrate/migrate-config.d.ts +6 -6
  20. package/dist/src/command-line/migrate/migrate-config.js +12 -6
  21. package/dist/src/command-line/migrate/migrate.d.ts +3 -30
  22. package/dist/src/command-line/migrate/migrate.js +206 -237
  23. package/dist/src/command-line/migrate/run/agent-output.d.ts +1 -0
  24. package/dist/src/command-line/migrate/run/agent-output.js +5 -0
  25. package/dist/src/command-line/migrate/run/broker.d.ts +14 -5
  26. package/dist/src/command-line/migrate/run/broker.js +68 -20
  27. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +1 -1
  28. package/dist/src/command-line/migrate/run/existing-run-report.js +7 -5
  29. package/dist/src/command-line/migrate/run/index.d.ts +6 -5
  30. package/dist/src/command-line/migrate/run/index.js +12 -1
  31. package/dist/src/command-line/migrate/run/orchestrator.d.ts +21 -0
  32. package/dist/src/command-line/migrate/run/orchestrator.js +149 -32
  33. package/dist/src/command-line/migrate/run/run-state.d.ts +13 -0
  34. package/dist/src/command-line/migrate/run/run-state.js +21 -0
  35. package/dist/src/command-line/migrate/run/runbook.js +19 -0
  36. package/dist/src/command-line/migrate/run/state-machine.js +4 -1
  37. package/dist/src/command-line/migrate/run/util.d.ts +3 -0
  38. package/dist/src/command-line/migrate/run/util.js +7 -1
  39. package/dist/src/command-line/migrate/sort-migrations.d.ts +1 -7
  40. package/dist/src/command-line/migrate/sort-migrations.js +1 -12
  41. package/dist/src/command-line/show/show-target/info.js +1 -1
  42. package/dist/src/config/nx-json.d.ts +13 -4
  43. package/dist/src/config/task-graph.d.ts +1 -1
  44. package/dist/src/config/workspace-json-project-json.d.ts +72 -0
  45. package/dist/src/core/graph/main.js +1 -1
  46. package/dist/src/devkit-exports.d.ts +1 -1
  47. package/dist/src/hasher/check-task-files.js +4 -4
  48. package/dist/src/hasher/native-task-hasher-impl.js +3 -1
  49. package/dist/src/hasher/task-hasher.d.ts +2 -0
  50. package/dist/src/hasher/task-hasher.js +4 -1
  51. package/dist/src/native/index.d.ts +92 -14
  52. package/dist/src/native/native-bindings.js +3 -0
  53. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  54. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  55. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +6 -1
  56. package/dist/src/project-graph/utils/retrieve-workspace-files.js +27 -6
  57. package/dist/src/tasks-runner/create-task-graph.d.ts +7 -16
  58. package/dist/src/tasks-runner/create-task-graph.js +43 -57
  59. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  60. package/dist/src/tasks-runner/init-tasks-runner.js +13 -0
  61. package/dist/src/tasks-runner/life-cycle.d.ts +4 -1
  62. package/dist/src/tasks-runner/life-cycle.js +7 -0
  63. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +5 -2
  64. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +13 -7
  65. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.d.ts +4 -3
  66. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +9 -2
  67. package/dist/src/tasks-runner/readiness/probes.d.ts +9 -0
  68. package/dist/src/tasks-runner/readiness/probes.js +62 -0
  69. package/dist/src/tasks-runner/readiness/ready-when.d.ts +38 -0
  70. package/dist/src/tasks-runner/readiness/ready-when.js +190 -0
  71. package/dist/src/tasks-runner/run-command.js +7 -6
  72. package/dist/src/tasks-runner/running-tasks/batch-process.js +6 -0
  73. package/dist/src/tasks-runner/running-tasks/node-child-process.js +5 -4
  74. package/dist/src/tasks-runner/task-graph-utils.d.ts +9 -6
  75. package/dist/src/tasks-runner/task-graph-utils.js +17 -12
  76. package/dist/src/tasks-runner/task-orchestrator.d.ts +37 -10
  77. package/dist/src/tasks-runner/task-orchestrator.js +459 -79
  78. package/dist/src/tasks-runner/tasks-runner.d.ts +1 -1
  79. package/dist/src/tasks-runner/tasks-schedule.d.ts +18 -1
  80. package/dist/src/tasks-runner/tasks-schedule.js +64 -6
  81. package/dist/src/tasks-runner/utils.js +6 -22
  82. package/dist/src/utils/child-process.js +1 -0
  83. package/dist/src/utils/git-utils.d.ts +6 -1
  84. package/dist/src/utils/git-utils.js +26 -5
  85. package/dist/src/utils/plugins/core-plugins.js +4 -0
  86. package/dist/src/utils/spawn-without-terminal.d.ts +27 -0
  87. package/dist/src/utils/spawn-without-terminal.js +110 -0
  88. package/package.json +12 -11
  89. package/schemas/nx-schema.json +93 -4
  90. package/schemas/project-schema.json +84 -2
@@ -13,6 +13,7 @@ const shared_running_task_1 = require("./running-tasks/shared-running-task");
13
13
  const task_results_life_cycle_1 = require("./life-cycles/task-results-life-cycle");
14
14
  async function createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, lifeCycle, ultracacheConfigurations) {
15
15
  (0, dotenv_1.loadRootEnvFiles)();
16
+ fullTaskGraph = withEdgeObjects(fullTaskGraph);
16
17
  const invokeRunnerTerminalLifecycle = new invoke_runner_terminal_output_life_cycle_1.InvokeRunnerTerminalOutputLifeCycle(tasks);
17
18
  const taskResultsLifecycle = new task_results_life_cycle_1.TaskResultsLifeCycle();
18
19
  const compositedLifeCycle = new life_cycle_1.CompositeLifeCycle([
@@ -52,6 +53,18 @@ async function createOrchestrator(tasks, projectGraph, fullTaskGraph, nxJson, li
52
53
  orchestrator.processAllScheduledTasks();
53
54
  return orchestrator;
54
55
  }
56
+ // A client bundle built before edges became objects hands plain task ids
57
+ function withEdgeObjects(taskGraph) {
58
+ const normalize = (edges) => Object.fromEntries(Object.entries(edges).map(([taskId, taskEdges]) => [
59
+ taskId,
60
+ taskEdges.map((edge) => typeof edge === 'string' ? { id: edge } : edge),
61
+ ]));
62
+ return {
63
+ ...taskGraph,
64
+ dependencies: normalize(taskGraph.dependencies),
65
+ continuousDependencies: normalize(taskGraph.continuousDependencies),
66
+ };
67
+ }
55
68
  // Nothing awaits the dispose chains below, so an unhandled rejection would take
56
69
  // down a long-lived agent process.
57
70
  function logDisposeFailure(e) {
@@ -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,
@@ -719,7 +720,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
719
720
  // this is used for two reasons: to fetch all remote cache hits AND
720
721
  // to submit everything that is known in advance to Nx Cloud to run in
721
722
  // a distributed fashion
722
- const deferredTaskIds = await (0, hash_task_1.hashTasksThatDoNotDependOnOutputsOfOtherTasks)(hasher, projectGraph, taskGraph, nxJson, taskDetails, ultracacheConfigurations);
723
+ const hashingDeferredTaskIds = await (0, hash_task_1.hashTasksThatDoNotDependOnOutputsOfOtherTasks)(hasher, projectGraph, taskGraph, nxJson, taskDetails, ultracacheConfigurations);
723
724
  reportUltracacheConfigurations(ultracacheConfigurationOutcome, projectGraph, taskGraph, nxArgs);
724
725
  const taskResultsLifecycle = new task_results_life_cycle_1.TaskResultsLifeCycle();
725
726
  const compositedLifeCycle = new life_cycle_1.CompositeLifeCycle([
@@ -790,7 +791,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
790
791
  },
791
792
  },
792
793
  daemon: client_1.daemonClient,
793
- deferredTaskIds,
794
+ hashingDeferredTaskIds,
794
795
  });
795
796
  if (promiseOrObservable.subscribe) {
796
797
  promiseOrObservable = convertObservableToPromise(promiseOrObservable);
@@ -80,6 +80,12 @@ class BatchProcess {
80
80
  for (const cb of this.batchResultsCallbacks) {
81
81
  cb(message.results);
82
82
  }
83
+ // The worker runs a single batch. Its IPC channel would otherwise
84
+ // keep it alive until cleanup kills it, which waits out the grace
85
+ // period on Windows, where SIGTERM is a no-op.
86
+ if (this.childProcess.connected) {
87
+ this.childProcess.disconnect();
88
+ }
83
89
  break;
84
90
  }
85
91
  case batch_messages_1.BatchMessageType.RunTasks: {
@@ -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) {
@@ -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 {};