nx 24.0.0-beta.1 → 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 (44) hide show
  1. package/dist/src/command-line/graph/graph.js +2 -1
  2. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +19 -23
  3. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +16 -19
  4. package/dist/src/command-line/migrate/agentic/master/run-master-session.js +25 -13
  5. package/dist/src/command-line/migrate/agentic/master/spawn-master.js +6 -7
  6. package/dist/src/command-line/migrate/agentic/select.js +3 -3
  7. package/dist/src/command-line/migrate/agentic/types.js +1 -3
  8. package/dist/src/command-line/migrate/command-object.js +5 -9
  9. package/dist/src/command-line/migrate/migrate-analytics.d.ts +6 -0
  10. package/dist/src/command-line/migrate/migrate-analytics.js +12 -0
  11. package/dist/src/command-line/migrate/migrate-config.d.ts +6 -6
  12. package/dist/src/command-line/migrate/migrate-config.js +12 -6
  13. package/dist/src/command-line/migrate/migrate.d.ts +3 -30
  14. package/dist/src/command-line/migrate/migrate.js +163 -231
  15. package/dist/src/command-line/migrate/run/existing-run-report.d.ts +1 -1
  16. package/dist/src/command-line/migrate/run/existing-run-report.js +7 -5
  17. package/dist/src/command-line/migrate/run/index.d.ts +3 -3
  18. package/dist/src/command-line/migrate/run/index.js +5 -1
  19. package/dist/src/command-line/migrate/run/orchestrator.d.ts +21 -0
  20. package/dist/src/command-line/migrate/run/orchestrator.js +77 -19
  21. package/dist/src/command-line/migrate/run/run-state.d.ts +13 -0
  22. package/dist/src/command-line/migrate/run/run-state.js +21 -0
  23. package/dist/src/command-line/migrate/sort-migrations.d.ts +1 -7
  24. package/dist/src/command-line/migrate/sort-migrations.js +1 -12
  25. package/dist/src/config/nx-json.d.ts +13 -4
  26. package/dist/src/config/workspace-json-project-json.d.ts +12 -0
  27. package/dist/src/core/graph/main.js +1 -1
  28. package/dist/src/hasher/task-hasher.d.ts +2 -0
  29. package/dist/src/hasher/task-hasher.js +4 -1
  30. package/dist/src/native/index.d.ts +32 -3
  31. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  32. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  33. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +6 -1
  34. package/dist/src/project-graph/utils/retrieve-workspace-files.js +27 -6
  35. package/dist/src/tasks-runner/default-tasks-runner.js +1 -1
  36. package/dist/src/tasks-runner/run-command.js +2 -2
  37. package/dist/src/tasks-runner/running-tasks/batch-process.js +6 -0
  38. package/dist/src/tasks-runner/task-orchestrator.d.ts +10 -6
  39. package/dist/src/tasks-runner/task-orchestrator.js +89 -59
  40. package/dist/src/tasks-runner/tasks-runner.d.ts +1 -1
  41. package/dist/src/utils/plugins/core-plugins.js +4 -0
  42. package/package.json +12 -11
  43. package/schemas/nx-schema.json +28 -4
  44. package/schemas/project-schema.json +22 -2
@@ -142,6 +142,7 @@ export declare function getInputs(task: Task, projectGraph: ProjectGraph, nxJson
142
142
  projectInputs: {
143
143
  input: string;
144
144
  projects: string[];
145
+ dependencies?: true;
145
146
  }[];
146
147
  depsFilesets: {
147
148
  fileset: string;
@@ -158,6 +159,7 @@ export declare function splitInputsIntoSelfAndDependencies(inputs: ReadonlyArray
158
159
  projectInputs: {
159
160
  input: string;
160
161
  projects: string[];
162
+ dependencies?: true;
161
163
  }[];
162
164
  selfInputs: ExpandedSelfInput[];
163
165
  depsOutputs: ExpandedDepsOutput[];
@@ -200,7 +200,7 @@ function splitInputsIntoSelfAndDependencies(inputs, namedInputs) {
200
200
  }
201
201
  else if ('input' in d &&
202
202
  !('fileset' in d) &&
203
- (('dependencies' in d && d.dependencies) ||
203
+ (('dependencies' in d && d.dependencies && !('projects' in d)) ||
204
204
  // Todo(@AgentEnder): Remove check in v17
205
205
  ('projects' in d &&
206
206
  typeof d.projects === 'string' &&
@@ -219,6 +219,9 @@ function splitInputsIntoSelfAndDependencies(inputs, namedInputs) {
219
219
  projectInputs.push({
220
220
  input: d.input,
221
221
  projects: Array.isArray(d.projects) ? d.projects : [d.projects],
222
+ ...('dependencies' in d && d.dependencies
223
+ ? { dependencies: true }
224
+ : {}),
222
225
  });
223
226
  }
224
227
  else {
@@ -99,7 +99,8 @@ export declare class HashPlanner {
99
99
  constructor(nxJson: NxJson, projectGraph: ExternalObject<ProjectGraph>)
100
100
  /**
101
101
  * `configurations` is this run's Ultracache configurations; a task with an
102
- * eligible entry hashes its observed reads instead of its declared filesets.
102
+ * eligible entry hashes its observed reads instead of its declared
103
+ * filesets, except `always` ones.
103
104
  * `options` carries the task ids decided in JS, where executors and
104
105
  * target configuration are resolved.
105
106
  */
@@ -732,8 +733,12 @@ export declare const enum EventType {
732
733
  rescan = 'rescan'
733
734
  }
734
735
 
735
- /** The files an `includeIgnored` fileset group matches on disk, sorted. */
736
- export declare function expandFilesInput(workspaceRoot: string, globs: Array<string>): Array<string>
736
+ /**
737
+ * The files an `includeIgnored` fileset group matches on disk, sorted.
738
+ * Given the workspace's project roots, an entry skips projects nested below
739
+ * the one it reads from, as hashing does.
740
+ */
741
+ export declare function expandFilesInput(workspaceRoot: string, globs: Array<string>, projectRoots?: Array<string> | undefined | null): Array<string>
737
742
 
738
743
  export declare function expandOutputs(directory: string, entries: Array<string>): Array<string>
739
744
 
@@ -779,6 +784,11 @@ export interface FileSetInput {
779
784
  * instead of the workspace file map.
780
785
  */
781
786
  includeIgnored?: boolean
787
+ /**
788
+ * Keep the matched files in the hash even when the task is hashed from
789
+ * its ultracache recording.
790
+ */
791
+ always?: boolean
782
792
  }
783
793
 
784
794
  export declare function findImports(projectFileMap: Record<string, Array<string>>): Array<ImportResult>
@@ -937,8 +947,17 @@ export interface InputMatch {
937
947
 
938
948
  export interface InputsInput {
939
949
  input: string
950
+ /**
951
+ * With `projects`: the selected projects' dependencies' `input`, as `^input`
952
+ * resolves for each, instead of the projects' own files.
953
+ */
940
954
  dependencies?: boolean
941
955
  projects?: string | Array<string>
956
+ /**
957
+ * Keep the files this input resolves to in the hash even when the task
958
+ * is hashed from its ultracache recording.
959
+ */
960
+ always?: boolean
942
961
  }
943
962
 
944
963
  export declare function installNxConsole(): Promise<boolean>
@@ -978,6 +997,11 @@ export interface JsonInput {
978
997
  json: string
979
998
  fields?: Array<string>
980
999
  excludeFields?: Array<string>
1000
+ /**
1001
+ * Keep the file in the hash even when the task is hashed from its
1002
+ * ultracache recording.
1003
+ */
1004
+ always?: boolean
981
1005
  }
982
1006
 
983
1007
  /**
@@ -1435,6 +1459,11 @@ export interface TaskTarget {
1435
1459
  export interface TaskUltracacheSettings {
1436
1460
  /** How this target's tasks participate. Defaults to `on`. */
1437
1461
  mode?: 'on' | 'warn' | 'error' | 'off'
1462
+ /**
1463
+ * Collect successful stat metadata accesses as inputs. Defaults to `false`.
1464
+ * Missing-path probes are tracked independently of this setting.
1465
+ */
1466
+ collectStats?: boolean
1438
1467
  /**
1439
1468
  * Workspace-relative glob patterns for reads that should be excluded
1440
1469
  * from ultracache reports. The first path segment cannot contain `*`,
Binary file
@@ -186,6 +186,7 @@ function warnAboutLegacyCachedTargets(legacyCacheReads) {
186
186
  */
187
187
  const KNOWN_ULTRACACHE_KEYS = new Set([
188
188
  'mode',
189
+ 'collectStats',
189
190
  'ignoredReads',
190
191
  'ignoredWrites',
191
192
  utils_1.NX_SPREAD_TOKEN,
@@ -233,7 +234,7 @@ function validateTargetUltracache(ultracache, projectName, projectRoot, targetNa
233
234
  const errors = [];
234
235
  for (const key of Object.keys(ultracache)) {
235
236
  if (!KNOWN_ULTRACACHE_KEYS.has(key)) {
236
- errors.push(`"ultracache.${key}" for target ${where} is not an ultracache option. Supported options are "mode", "ignoredReads" and "ignoredWrites".`);
237
+ errors.push(`"ultracache.${key}" for target ${where} is not an ultracache option. Supported options are "mode", "collectStats", "ignoredReads" and "ignoredWrites".`);
237
238
  }
238
239
  }
239
240
  if (ultracache.mode !== undefined &&
@@ -243,6 +244,10 @@ function validateTargetUltracache(ultracache, projectName, projectRoot, targetNa
243
244
  ? `"${ultracache.mode}"`
244
245
  : describeUltracacheValue(ultracache.mode)}.`);
245
246
  }
247
+ if (ultracache.collectStats !== undefined &&
248
+ typeof ultracache.collectStats !== 'boolean') {
249
+ errors.push(`"ultracache.collectStats" for target ${where} must be a boolean, but it is ${describeUltracacheValue(ultracache.collectStats)}.`);
250
+ }
246
251
  for (const key of ['ignoredReads', 'ignoredWrites']) {
247
252
  const value = ultracache[key];
248
253
  if (value === undefined)
@@ -12,6 +12,7 @@ const angular_json_1 = require("../../adapter/angular-json");
12
12
  const nx_json_1 = require("../../config/nx-json");
13
13
  const project_configuration_utils_1 = require("./project-configuration-utils");
14
14
  const workspace_context_1 = require("../../utils/workspace-context");
15
+ const minimatch_1 = require("minimatch");
15
16
  const path_1 = require("path");
16
17
  const get_plugins_1 = require("../plugins/get-plugins");
17
18
  /**
@@ -45,11 +46,9 @@ async function retrieveWorkspaceFiles(workspaceRoot, projectRootMap) {
45
46
  async function retrieveProjectConfigurations(separatedPlugins, workspaceRoot, nxJson) {
46
47
  const specifiedWithCreateNodes = separatedPlugins.specifiedPlugins.filter((p) => !!p.createNodes);
47
48
  const defaultWithCreateNodes = separatedPlugins.defaultPlugins.filter((p) => !!p.createNodes);
48
- const specifiedGlobPatterns = getGlobPatternsOfPlugins(specifiedWithCreateNodes);
49
- const defaultGlobPatterns = getGlobPatternsOfPlugins(defaultWithCreateNodes);
50
49
  const [specifiedPluginFiles, defaultPluginFiles] = await Promise.all([
51
- (0, workspace_context_1.multiGlobWithWorkspaceContext)(workspaceRoot, specifiedGlobPatterns),
52
- (0, workspace_context_1.multiGlobWithWorkspaceContext)(workspaceRoot, defaultGlobPatterns),
50
+ globPluginFiles(workspaceRoot, specifiedWithCreateNodes),
51
+ globPluginFiles(workspaceRoot, defaultWithCreateNodes),
53
52
  ]);
54
53
  return (0, project_configuration_utils_1.createProjectConfigurationsWithPlugins)(workspaceRoot, nxJson, {
55
54
  specifiedPluginFiles: specifiedPluginFiles ?? [],
@@ -71,8 +70,7 @@ async function retrieveProjectConfigurationsWithAngularProjects(workspaceRoot, n
71
70
  return res;
72
71
  }
73
72
  async function retrieveProjectConfigurationPaths(root, plugins) {
74
- const projectGlobPatterns = getGlobPatternsOfPlugins(plugins);
75
- const pluginConfigFiles = await (0, workspace_context_1.multiGlobWithWorkspaceContext)(root, projectGlobPatterns);
73
+ const pluginConfigFiles = await globPluginFiles(root, plugins);
76
74
  return pluginConfigFiles.flat();
77
75
  }
78
76
  const projectsWithoutPluginCache = new Map();
@@ -105,6 +103,29 @@ async function retrieveProjectConfigurationsWithoutPluginInference(root) {
105
103
  function clearProjectsWithoutPluginInferenceCache() {
106
104
  projectsWithoutPluginCache.clear();
107
105
  }
106
+ /**
107
+ * Files each plugin's createNodes pattern matches, in plugin order. A plugin
108
+ * scoped by `include` globs only those patterns natively, so a broad pattern
109
+ * (every `.ts` file, say) is never matched against the whole workspace in JS.
110
+ */
111
+ async function globPluginFiles(root, plugins) {
112
+ const unscoped = plugins.filter((p) => !canGlobInclude(p));
113
+ const unscopedFiles = (await (0, workspace_context_1.multiGlobWithWorkspaceContext)(root, unscoped.map((p) => p.createNodes[0]))) ?? [];
114
+ return Promise.all(plugins.map(async (plugin) => {
115
+ if (!canGlobInclude(plugin)) {
116
+ return unscopedFiles[unscoped.indexOf(plugin)] ?? [];
117
+ }
118
+ const pattern = new minimatch_1.Minimatch(plugin.createNodes[0], { dot: true });
119
+ const included = await (0, workspace_context_1.globWithWorkspaceContext)(root, plugin.include);
120
+ return included.filter((file) => pattern.match(file));
121
+ }));
122
+ }
123
+ // Only narrow includes: a `**` scope can return more files than the plugin
124
+ // pattern does. Negations and extglobs keep the JS matcher's semantics.
125
+ function canGlobInclude(plugin) {
126
+ return (plugin.include?.length > 0 &&
127
+ plugin.include.every((pattern) => !pattern.includes('**') && !/[!()]/.test(pattern)));
128
+ }
108
129
  function getGlobPatternsOfPlugins(plugins) {
109
130
  return plugins.map((p) => p.createNodes[0]);
110
131
  }
@@ -68,7 +68,7 @@ const defaultTasksRunner = async (tasks, options, context) => {
68
68
  };
69
69
  exports.defaultTasksRunner = defaultTasksRunner;
70
70
  async function runAllTasks(options, context) {
71
- const orchestrator = new task_orchestrator_1.TaskOrchestrator(context.hasher, context.initiatingProject, context.initiatingTasks, context.projectGraph, context.taskGraph, context.nxJson, options, context.nxArgs?.nxBail, context.daemon, context.nxArgs?.specifiedOutputStyle, context.nxArgs?.resolvedOutputStyle ?? 'static-failures-only', context.taskGraph, context.deferredTaskIds);
71
+ const orchestrator = new task_orchestrator_1.TaskOrchestrator(context.hasher, context.initiatingProject, context.initiatingTasks, context.projectGraph, context.taskGraph, context.nxJson, options, context.nxArgs?.nxBail, context.daemon, context.nxArgs?.specifiedOutputStyle, context.nxArgs?.resolvedOutputStyle ?? 'static-failures-only', context.taskGraph, context.hashingDeferredTaskIds);
72
72
  return orchestrator.run();
73
73
  }
74
74
  exports.default = exports.defaultTasksRunner;
@@ -720,7 +720,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
720
720
  // this is used for two reasons: to fetch all remote cache hits AND
721
721
  // to submit everything that is known in advance to Nx Cloud to run in
722
722
  // a distributed fashion
723
- 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);
724
724
  reportUltracacheConfigurations(ultracacheConfigurationOutcome, projectGraph, taskGraph, nxArgs);
725
725
  const taskResultsLifecycle = new task_results_life_cycle_1.TaskResultsLifeCycle();
726
726
  const compositedLifeCycle = new life_cycle_1.CompositeLifeCycle([
@@ -791,7 +791,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
791
791
  },
792
792
  },
793
793
  daemon: client_1.daemonClient,
794
- deferredTaskIds,
794
+ hashingDeferredTaskIds,
795
795
  });
796
796
  if (promiseOrObservable.subscribe) {
797
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: {
@@ -28,7 +28,7 @@ export declare class TaskOrchestrator {
28
28
  private readonly resolvedOutputStyle;
29
29
  private readonly fullTaskGraph;
30
30
  /** Tasks the up-front pass left to hash once the tasks they read from have run. */
31
- private readonly deferredTaskIds?;
31
+ private readonly hashingDeferredTaskIds?;
32
32
  private taskDetails;
33
33
  private cache;
34
34
  private readonly tuiEnabled;
@@ -79,7 +79,7 @@ export declare class TaskOrchestrator {
79
79
  /** What this run renders with, after defaults. */
80
80
  resolvedOutputStyle: OutputStyle, fullTaskGraph?: TaskGraph,
81
81
  /** Tasks the up-front pass left to hash once the tasks they read from have run. */
82
- deferredTaskIds?: ReadonlySet<string>);
82
+ hashingDeferredTaskIds?: ReadonlySet<string>);
83
83
  init(): Promise<void>;
84
84
  run(): Promise<{
85
85
  [k: string]: TaskStatus;
@@ -146,14 +146,18 @@ export declare class TaskOrchestrator {
146
146
  /**
147
147
  * Hash all batch tasks and resolve cache hits topologically.
148
148
  *
149
- * Walks the task graph level by level. Every task gets a preliminary hash
150
- * (so startTasks always has a valid hash for Cloud). Tasks the up-front
151
- * pass deferred, whose deps weren't cached, are ineligible for cache lookup but still
152
- * receive a preliminary hash — they'll be re-hashed after execution.
149
+ * Walks the task graph level by level. A deferred task (its hash reads
150
+ * another task's outputs) behind an upstream batch task that still has to
151
+ * run is never looked up yet:
152
+ * - behind a non-cacheable task only, it is held back for a later wave,
153
+ * where it hashes after that task ran;
154
+ * - behind a cache miss, it runs in this wave and re-hashes after execution.
155
+ * Every task run in this wave gets a hash first, for Cloud's startTasks.
153
156
  */
154
157
  private applyBatchCachedResults;
155
158
  private hashBatchTasks;
156
159
  applyFromCacheOrRunBatch(doNotSkipCache: boolean, batch: Batch, groupId: number): Promise<TaskResult[]>;
160
+ private runBatchWave;
157
161
  private runBatch;
158
162
  /**
159
163
  * Tells the life cycle where the batch worker's own log is, when something
@@ -80,7 +80,7 @@ class TaskOrchestrator {
80
80
  /** What this run renders with, after defaults. */
81
81
  resolvedOutputStyle, fullTaskGraph = taskGraph,
82
82
  /** Tasks the up-front pass left to hash once the tasks they read from have run. */
83
- deferredTaskIds) {
83
+ hashingDeferredTaskIds) {
84
84
  this.hasher = hasher;
85
85
  this.initiatingProject = initiatingProject;
86
86
  this.initiatingTasks = initiatingTasks;
@@ -93,7 +93,7 @@ class TaskOrchestrator {
93
93
  this.specifiedOutputStyle = specifiedOutputStyle;
94
94
  this.resolvedOutputStyle = resolvedOutputStyle;
95
95
  this.fullTaskGraph = fullTaskGraph;
96
- this.deferredTaskIds = deferredTaskIds;
96
+ this.hashingDeferredTaskIds = hashingDeferredTaskIds;
97
97
  this.taskDetails = (0, hash_task_1.getTaskDetails)();
98
98
  this.cache = (0, cache_1.getCache)(this.options);
99
99
  this.tuiEnabled = (0, is_tui_enabled_1.isTuiEnabled)();
@@ -573,37 +573,63 @@ class TaskOrchestrator {
573
573
  /**
574
574
  * Hash all batch tasks and resolve cache hits topologically.
575
575
  *
576
- * Walks the task graph level by level. Every task gets a preliminary hash
577
- * (so startTasks always has a valid hash for Cloud). Tasks the up-front
578
- * pass deferred, whose deps weren't cached, are ineligible for cache lookup but still
579
- * receive a preliminary hash — they'll be re-hashed after execution.
576
+ * Walks the task graph level by level. A deferred task (its hash reads
577
+ * another task's outputs) behind an upstream batch task that still has to
578
+ * run is never looked up yet:
579
+ * - behind a non-cacheable task only, it is held back for a later wave,
580
+ * where it hashes after that task ran;
581
+ * - behind a cache miss, it runs in this wave and re-hashes after execution.
582
+ * Every task run in this wave gets a hash first, for Cloud's startTasks.
580
583
  */
581
584
  async applyBatchCachedResults(batch, doNotSkipCache, groupId) {
582
585
  const cachedResults = [];
583
- const needsRehashAfterExecution = new Set();
586
+ const rehashAfterRun = new Set();
584
587
  const tasks = Object.values(batch.taskGraph.tasks);
585
588
  if (!doNotSkipCache) {
586
589
  // Cache skipped — just hash so startTasks has valid hashes
587
590
  await this.hashBatchTasks(tasks);
588
- return { cachedResults, needsRehashAfterExecution };
589
- }
590
- const nonCachedTaskIds = new Set();
591
- await (0, task_graph_utils_1.walkTaskGraph)(batch.taskGraph, async (rootTaskIds) => {
592
- const rootTasks = rootTaskIds.map((id) => batch.taskGraph.tasks[id]);
593
- await this.hashBatchTasks(rootTasks);
594
- const eligible = [];
595
- for (const task of rootTasks) {
596
- const hasNonCachedDep = batch.taskGraph.dependencies[task.id].some(({ id }) => nonCachedTaskIds.has(id));
597
- if (hasNonCachedDep && this.deferredTaskIds?.has(task.id)) {
598
- nonCachedTaskIds.add(task.id);
599
- needsRehashAfterExecution.add(task.id);
591
+ return { cachedResults, tasksToRun: tasks, rehashAfterRun };
592
+ }
593
+ const toRun = new Map();
594
+ // Neither restored nor run in this wave; a later wave picks them up.
595
+ const heldBack = new Set();
596
+ // Tasks with a cache miss / a non-cacheable task upstream that still has
597
+ // to run in this wave.
598
+ const behindMiss = new Set();
599
+ const behindUncacheable = new Set();
600
+ const pending = (id, cacheable) => toRun.has(id) && !!toRun.get(id).cache === cacheable;
601
+ await (0, task_graph_utils_1.walkTaskGraph)(batch.taskGraph, async (levelIds) => {
602
+ const runNow = [];
603
+ const lookUp = [];
604
+ for (const id of levelIds) {
605
+ const task = batch.taskGraph.tasks[id];
606
+ const deps = batch.taskGraph.dependencies[id].map((dep) => dep.id);
607
+ if (deps.some((dep) => heldBack.has(dep))) {
608
+ heldBack.add(id);
609
+ continue;
610
+ }
611
+ const afterMiss = deps.some((dep) => behindMiss.has(dep) || pending(dep, true));
612
+ const afterUncacheable = deps.some((dep) => behindUncacheable.has(dep) || pending(dep, false));
613
+ if (afterMiss)
614
+ behindMiss.add(id);
615
+ if (afterUncacheable)
616
+ behindUncacheable.add(id);
617
+ const deferred = this.hashingDeferredTaskIds?.has(id);
618
+ if (deferred && afterMiss) {
619
+ runNow.push(task);
620
+ toRun.set(id, task);
621
+ rehashAfterRun.add(id);
622
+ }
623
+ else if (deferred && afterUncacheable) {
624
+ heldBack.add(id);
600
625
  }
601
626
  else {
602
- eligible.push(task);
627
+ lookUp.push(task);
603
628
  }
604
629
  }
605
- if (eligible.length > 0) {
606
- const cacheResults = await this.applyCachedResults(eligible);
630
+ await this.hashBatchTasks([...runNow, ...lookUp]);
631
+ if (lookUp.length > 0) {
632
+ const cacheResults = await this.applyCachedResults(lookUp);
607
633
  const cachedIds = new Set(cacheResults.map((r) => r.task.id));
608
634
  cachedResults.push(...cacheResults);
609
635
  if (cacheResults.length > 0) {
@@ -613,14 +639,14 @@ class TaskOrchestrator {
613
639
  // Replayed from the cache — don't write the results back.
614
640
  await this.postRunSteps(cacheResults, false, groupId);
615
641
  }
616
- for (const task of eligible) {
642
+ for (const task of lookUp) {
617
643
  if (!cachedIds.has(task.id)) {
618
- nonCachedTaskIds.add(task.id);
644
+ toRun.set(task.id, task);
619
645
  }
620
646
  }
621
647
  }
622
648
  });
623
- return { cachedResults, needsRehashAfterExecution };
649
+ return { cachedResults, tasksToRun: [...toRun.values()], rehashAfterRun };
624
650
  }
625
651
  async hashBatchTasks(tasks) {
626
652
  // Batch executors run every task in the same forked process, but
@@ -634,29 +660,52 @@ class TaskOrchestrator {
634
660
  await (0, hash_task_1.hashTasks)(this.hasher, this.projectGraph, this.fullTaskGraph, perTaskEnvs, this.taskDetails, tasks);
635
661
  }
636
662
  async applyFromCacheOrRunBatch(doNotSkipCache, batch, groupId) {
663
+ if (this.bailed || this.stopRequested) {
664
+ return [];
665
+ }
637
666
  const applyFromCacheOrRunBatchStart = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:start');
638
- const taskEntries = Object.entries(batch.taskGraph.tasks);
639
- const tasks = taskEntries.map(([, task]) => task);
667
+ const taskIds = Object.keys(batch.taskGraph.tasks);
640
668
  this.options.lifeCycle.registerRunningBatch?.(batch.id, {
641
669
  executorName: batch.executorName,
642
- taskIds: Object.keys(batch.taskGraph.tasks),
670
+ taskIds,
671
+ });
672
+ // Each wave restores or runs what it can; tasks held back for a later
673
+ // wave (or left out by the executor) stay incomplete and go again.
674
+ const results = [];
675
+ let taskGraph = batch.taskGraph;
676
+ while (Object.keys(taskGraph.tasks).length > 0 &&
677
+ !this.bailed &&
678
+ !this.stopRequested) {
679
+ results.push(...(await this.runBatchWave(doNotSkipCache, { ...batch, taskGraph }, groupId)));
680
+ this.forkedProcessTaskRunner.cleanUpBatchProcesses();
681
+ taskGraph = (0, utils_1.removeTasksFromTaskGraph)(taskGraph, Object.keys(taskGraph.tasks).filter((id) => this.completedTasks.has(id)));
682
+ }
683
+ // Once, after every wave: an earlier status would end the batch in the TUI.
684
+ const hasFailures = taskIds.some((taskId) => {
685
+ const status = this.completedTasks.get(taskId);
686
+ return status === 'failure' || status === 'skipped';
643
687
  });
644
- const { cachedResults, needsRehashAfterExecution } = await this.applyBatchCachedResults(batch, doNotSkipCache, groupId);
688
+ this.options.lifeCycle.setBatchStatus?.(batch.id, hasFailures ? "Failure" /* BatchStatus.Failure */ : "Success" /* BatchStatus.Success */);
689
+ const applyFromCacheOrRunBatchEnd = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:end');
690
+ perf_hooks_1.performance.measure('TaskOrchestrator-apply-from-cache-or-run-batch', applyFromCacheOrRunBatchStart.name, applyFromCacheOrRunBatchEnd.name);
691
+ return results;
692
+ }
693
+ async runBatchWave(doNotSkipCache, batch, groupId) {
694
+ const tasks = Object.values(batch.taskGraph.tasks);
695
+ const { cachedResults, tasksToRun: wave, rehashAfterRun, } = await this.applyBatchCachedResults(batch, doNotSkipCache, groupId);
645
696
  // Cached tasks were started and completed inside applyBatchCachedResults;
646
697
  // a member skipped meanwhile (a dependency failed) must not run either
647
- const nonCachedTasks = tasks.filter((t) => !this.completedTasks.has(t.id));
648
- if (nonCachedTasks.length > 0) {
649
- await Promise.all(nonCachedTasks.map((task) => this.options.lifeCycle.scheduleTask(task)));
650
- await this.preRunSteps(nonCachedTasks, { groupId });
698
+ const tasksToRun = wave.filter((t) => !this.completedTasks.has(t.id));
699
+ if (tasksToRun.length > 0) {
700
+ await Promise.all(tasksToRun.map((task) => this.options.lifeCycle.scheduleTask(task)));
701
+ await this.preRunSteps(tasksToRun, { groupId });
651
702
  }
652
- // Phase 2: Run non-cached tasks, then re-hash tasks that read their outputs
703
+ // Phase 2: Run this wave's tasks, then re-hash those run without a lookup
653
704
  let batchResults = [];
654
705
  // Read again: a member can be skipped while the hooks above ran
655
- const taskIdsToSkip = tasks
656
- .filter((t) => this.completedTasks.has(t.id))
657
- .map((t) => t.id);
658
- if (taskIdsToSkip.length < tasks.length) {
659
- const runGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, taskIdsToSkip);
706
+ const runIds = new Set(tasksToRun.filter((t) => !this.completedTasks.has(t.id)).map((t) => t.id));
707
+ if (runIds.size > 0) {
708
+ const runGraph = (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, tasks.filter((t) => !runIds.has(t.id)).map((t) => t.id));
660
709
  for (const task of Object.values(runGraph.tasks)) {
661
710
  this.detectTaskInvocationLoop(task);
662
711
  }
@@ -667,7 +716,7 @@ class TaskOrchestrator {
667
716
  }, this.batchEnv, groupId);
668
717
  // Re-hash tasks that read dep outputs — those outputs are now on disk
669
718
  const tasksToRehash = batchResults
670
- .filter((r) => needsRehashAfterExecution.has(r.task.id) &&
719
+ .filter((r) => rehashAfterRun.has(r.task.id) &&
671
720
  (r.status === 'success' || r.status === 'failure'))
672
721
  .map((r) => r.task);
673
722
  if (tasksToRehash.length > 0) {
@@ -683,25 +732,6 @@ class TaskOrchestrator {
683
732
  if (batchResults.length > 0) {
684
733
  await this.postRunSteps(batchResults, doNotSkipCache, groupId);
685
734
  }
686
- // Update batch status based on all task results
687
- const hasFailures = taskEntries.some(([taskId]) => {
688
- const status = this.completedTasks.get(taskId);
689
- return status === 'failure' || status === 'skipped';
690
- });
691
- this.options.lifeCycle.setBatchStatus?.(batch.id, hasFailures ? "Failure" /* BatchStatus.Failure */ : "Success" /* BatchStatus.Success */);
692
- this.forkedProcessTaskRunner.cleanUpBatchProcesses();
693
- const tasksCompleted = taskEntries.filter(([taskId]) => this.completedTasks.has(taskId));
694
- // Batch is still not done, run it again
695
- if (tasksCompleted.length !== taskEntries.length) {
696
- await this.applyFromCacheOrRunBatch(doNotSkipCache, {
697
- id: batch.id,
698
- executorName: batch.executorName,
699
- taskGraph: (0, utils_1.removeTasksFromTaskGraph)(batch.taskGraph, tasksCompleted.map(([taskId]) => taskId)),
700
- }, groupId);
701
- }
702
- // Batch is done, mark it as completed
703
- const applyFromCacheOrRunBatchEnd = perf_hooks_1.performance.mark('TaskOrchestrator-apply-from-cache-or-run-batch:end');
704
- perf_hooks_1.performance.measure('TaskOrchestrator-apply-from-cache-or-run-batch', applyFromCacheOrRunBatchStart.name, applyFromCacheOrRunBatchEnd.name);
705
735
  return [...cachedResults, ...batchResults];
706
736
  }
707
737
  async runBatch(batch, env, groupId) {
@@ -20,7 +20,7 @@ export type TasksRunner<T = unknown> = (tasks: Task[], options: T, context?: {
20
20
  hasher?: TaskHasher;
21
21
  daemon?: DaemonClient;
22
22
  /** Tasks hashed only once the tasks they read outputs from have run. */
23
- deferredTaskIds?: ReadonlySet<string>;
23
+ hashingDeferredTaskIds?: ReadonlySet<string>;
24
24
  }) => any | Promise<{
25
25
  [id: string]: TaskStatus;
26
26
  }>;
@@ -82,6 +82,10 @@ exports.CORE_PLUGINS = [
82
82
  name: '@nx/remix',
83
83
  capabilities: 'generators',
84
84
  },
85
+ {
86
+ name: '@nx/rolldown',
87
+ capabilities: 'generators',
88
+ },
85
89
  {
86
90
  name: '@nx/rollup',
87
91
  capabilities: 'executors,generators',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx",
3
- "version": "24.0.0-beta.1",
3
+ "version": "24.0.0-beta.2",
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.",
@@ -150,16 +150,16 @@
150
150
  }
151
151
  },
152
152
  "optionalDependencies": {
153
- "@nx/nx-darwin-arm64": "24.0.0-beta.1",
154
- "@nx/nx-darwin-x64": "24.0.0-beta.1",
155
- "@nx/nx-freebsd-x64": "24.0.0-beta.1",
156
- "@nx/nx-linux-arm-gnueabihf": "24.0.0-beta.1",
157
- "@nx/nx-linux-arm64-gnu": "24.0.0-beta.1",
158
- "@nx/nx-linux-arm64-musl": "24.0.0-beta.1",
159
- "@nx/nx-linux-x64-gnu": "24.0.0-beta.1",
160
- "@nx/nx-linux-x64-musl": "24.0.0-beta.1",
161
- "@nx/nx-win32-arm64-msvc": "24.0.0-beta.1",
162
- "@nx/nx-win32-x64-msvc": "24.0.0-beta.1"
153
+ "@nx/nx-darwin-arm64": "24.0.0-beta.2",
154
+ "@nx/nx-darwin-x64": "24.0.0-beta.2",
155
+ "@nx/nx-freebsd-x64": "24.0.0-beta.2",
156
+ "@nx/nx-linux-arm-gnueabihf": "24.0.0-beta.2",
157
+ "@nx/nx-linux-arm64-gnu": "24.0.0-beta.2",
158
+ "@nx/nx-linux-arm64-musl": "24.0.0-beta.2",
159
+ "@nx/nx-linux-x64-gnu": "24.0.0-beta.2",
160
+ "@nx/nx-linux-x64-musl": "24.0.0-beta.2",
161
+ "@nx/nx-win32-arm64-msvc": "24.0.0-beta.2",
162
+ "@nx/nx-win32-x64-msvc": "24.0.0-beta.2"
163
163
  },
164
164
  "nx-migrations": {
165
165
  "migrations": "./migrations.json",
@@ -192,6 +192,7 @@
192
192
  "@nx/plugin",
193
193
  "@nx/react",
194
194
  "@nx/react-native",
195
+ "@nx/rolldown",
195
196
  "@nx/rollup",
196
197
  "@nx/remix",
197
198
  "@nx/rspack",