nx 22.7.0-beta.12 → 22.7.0-beta.13

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 (66) hide show
  1. package/dist/schemas/nx-schema.json +25 -0
  2. package/dist/schemas/project-schema.json +25 -0
  3. package/dist/src/adapter/ngcli-adapter.js +11 -12
  4. package/dist/src/command-line/daemon/daemon.js +8 -4
  5. package/dist/src/command-line/graph/graph.js +8 -1
  6. package/dist/src/command-line/show/show-target/utils.js +4 -3
  7. package/dist/src/command-line/yargs-utils/shared-options.js +5 -1
  8. package/dist/src/config/nx-json.d.ts +3 -1
  9. package/dist/src/config/workspace-json-project-json.d.ts +2 -1
  10. package/dist/src/core/graph/main.js +1 -1
  11. package/dist/src/daemon/client/client.d.ts +10 -3
  12. package/dist/src/daemon/client/client.js +21 -31
  13. package/dist/src/daemon/client/daemon-environment.d.ts +4 -0
  14. package/dist/src/daemon/client/daemon-environment.js +119 -0
  15. package/dist/src/daemon/client/daemon-socket-messenger.d.ts +2 -5
  16. package/dist/src/daemon/client/daemon-socket-messenger.js +1 -1
  17. package/dist/src/daemon/message-types/daemon-message.d.ts +6 -0
  18. package/dist/src/daemon/message-types/daemon-message.js +6 -0
  19. package/dist/src/daemon/server/handle-hash-tasks.d.ts +1 -1
  20. package/dist/src/daemon/server/handle-hash-tasks.js +1 -1
  21. package/dist/src/daemon/server/handle-outputs-tracking.d.ts +4 -4
  22. package/dist/src/daemon/server/handle-outputs-tracking.js +11 -11
  23. package/dist/src/daemon/server/outputs-tracking.d.ts +18 -3
  24. package/dist/src/daemon/server/outputs-tracking.js +49 -22
  25. package/dist/src/daemon/server/project-graph-incremental-recomputation.d.ts +2 -1
  26. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +20 -4
  27. package/dist/src/daemon/server/server.js +71 -40
  28. package/dist/src/executors/run-commands/running-tasks.js +2 -3
  29. package/dist/src/executors/run-script/run-script.impl.js +16 -8
  30. package/dist/src/hasher/hash-task.d.ts +9 -1
  31. package/dist/src/hasher/hash-task.js +41 -14
  32. package/dist/src/hasher/native-task-hasher-impl.d.ts +1 -1
  33. package/dist/src/hasher/native-task-hasher-impl.js +4 -6
  34. package/dist/src/hasher/task-hasher.d.ts +20 -9
  35. package/dist/src/hasher/task-hasher.js +34 -6
  36. package/dist/src/native/index.d.ts +34 -7
  37. package/dist/src/native/native-bindings.js +1 -1
  38. package/dist/src/native/nx.wasi-browser.js +0 -1
  39. package/dist/src/native/nx.wasi.cjs +0 -1
  40. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  41. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  42. package/dist/src/plugins/js/utils/register.js +83 -4
  43. package/dist/src/project-graph/error-types.js +31 -11
  44. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  45. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +9 -0
  46. package/dist/src/project-graph/plugins/isolation/message-types.d.ts +1 -1
  47. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +9 -0
  48. package/dist/src/project-graph/plugins/isolation/messaging.js +2 -0
  49. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +36 -68
  50. package/dist/src/project-graph/plugins/loaded-nx-plugin.d.ts +6 -0
  51. package/dist/src/tasks-runner/cache.d.ts +6 -0
  52. package/dist/src/tasks-runner/cache.js +58 -0
  53. package/dist/src/tasks-runner/init-tasks-runner.js +13 -7
  54. package/dist/src/tasks-runner/run-command.js +13 -5
  55. package/dist/src/tasks-runner/task-env.js +17 -1
  56. package/dist/src/tasks-runner/task-orchestrator.d.ts +79 -7
  57. package/dist/src/tasks-runner/task-orchestrator.js +327 -120
  58. package/dist/src/tasks-runner/tasks-schedule.d.ts +5 -2
  59. package/dist/src/tasks-runner/tasks-schedule.js +31 -17
  60. package/dist/src/tasks-runner/utils.d.ts +3 -3
  61. package/dist/src/tasks-runner/utils.js +5 -6
  62. package/package.json +12 -12
  63. package/dist/src/daemon/client/exec-is-server-available.d.ts +0 -1
  64. package/dist/src/daemon/client/exec-is-server-available.js +0 -11
  65. package/dist/src/daemon/client/generate-help-output.d.ts +0 -1
  66. package/dist/src/daemon/client/generate-help-output.js +0 -26
@@ -8,6 +8,7 @@ exports.registerTranspiler = registerTranspiler;
8
8
  exports.registerTsConfigPaths = registerTsConfigPaths;
9
9
  exports.getTsNodeCompilerOptions = getTsNodeCompilerOptions;
10
10
  const path_1 = require("path");
11
+ const fs_1 = require("fs");
11
12
  const logger_1 = require("../../../utils/logger");
12
13
  const workspace_root_1 = require("../../../utils/workspace-root");
13
14
  const typescript_1 = require("./typescript");
@@ -49,6 +50,14 @@ const isInvokedByTsx = (() => {
49
50
  return (requireArgs.some((a) => isTsxPath(a)) ||
50
51
  importArgs.some((a) => isTsxPath(a)));
51
52
  })();
53
+ /**
54
+ * Whether the current Node.js version supports native TypeScript execution
55
+ * via type stripping (Node 22.6+).
56
+ *
57
+ * process.features.typescript is 'strip' | 'transform' | false in Node 22.6+
58
+ */
59
+ const nodeSupportsNativeTypescript = !!process.features
60
+ ?.typescript;
52
61
  /**
53
62
  * When process.features.typescript is truthy and the user has opted in via
54
63
  * NX_PREFER_NODE_STRIP_TYPES=true, we can skip registering swc-node or ts-node
@@ -63,8 +72,7 @@ const preferNodeStripTypes = (() => {
63
72
  if (process.env.NX_PREFER_NODE_STRIP_TYPES !== 'true') {
64
73
  return false;
65
74
  }
66
- // process.features.typescript is 'strip' | 'transform' | false in Node 22.6+
67
- return !!process.features?.typescript;
75
+ return nodeSupportsNativeTypescript;
68
76
  })();
69
77
  function registerTsProject(path, configFilename) {
70
78
  // See explanation alongside isInvokedByTsx declaration
@@ -257,7 +265,11 @@ function registerTranspiler(compilerOptions, tsConfigRaw) {
257
265
  // Function to register transpiler that returns cleanup function
258
266
  const transpiler = getTranspiler(compilerOptions, tsConfigRaw);
259
267
  if (!transpiler) {
260
- warnNoTranspiler();
268
+ // If Node.js natively supports TypeScript (22.6+), no transpiler is needed.
269
+ // Don't warn — Node will handle .ts files via type stripping.
270
+ if (!nodeSupportsNativeTypescript) {
271
+ warnNoTranspiler();
272
+ }
261
273
  return () => { };
262
274
  }
263
275
  return transpiler();
@@ -279,7 +291,7 @@ function registerTsConfigPaths(tsConfigPath) {
279
291
  */
280
292
  if (tsConfigResult.resultType === 'success') {
281
293
  return tsconfigPaths.register({
282
- baseUrl: tsConfigResult.absoluteBaseUrl,
294
+ baseUrl: resolvePathsBaseUrl(tsConfigPath),
283
295
  paths: tsConfigResult.paths,
284
296
  });
285
297
  }
@@ -404,3 +416,70 @@ function getTsNodeCompilerOptions(compilerOptions) {
404
416
  }
405
417
  return result;
406
418
  }
419
+ function resolvePathsBaseUrl(tsconfigPath) {
420
+ const chain = [];
421
+ const queue = [tsconfigPath];
422
+ while (queue.length > 0) {
423
+ const absolute = (0, path_1.resolve)(queue.shift());
424
+ const dir = (0, path_1.dirname)(absolute);
425
+ try {
426
+ const raw = JSON.parse((0, fs_1.readFileSync)(absolute, 'utf-8'));
427
+ chain.push({ dir, raw });
428
+ const exts = raw.extends
429
+ ? Array.isArray(raw.extends)
430
+ ? raw.extends
431
+ : [raw.extends]
432
+ : [];
433
+ for (const ext of exts) {
434
+ const resolved = resolveExtendsPath(ext, dir);
435
+ if (resolved) {
436
+ queue.push(resolved);
437
+ }
438
+ }
439
+ }
440
+ catch {
441
+ // skip unreadable files
442
+ }
443
+ }
444
+ let pathsIndex = -1;
445
+ for (let i = 0; i < chain.length; i++) {
446
+ if (chain[i].raw.compilerOptions?.paths &&
447
+ Object.keys(chain[i].raw.compilerOptions.paths).length > 0) {
448
+ pathsIndex = i;
449
+ break;
450
+ }
451
+ }
452
+ const searchStart = pathsIndex >= 0 ? pathsIndex : 0;
453
+ for (let i = searchStart; i < chain.length; i++) {
454
+ if (chain[i].raw.compilerOptions?.baseUrl) {
455
+ return (0, path_1.resolve)(chain[i].dir, chain[i].raw.compilerOptions.baseUrl);
456
+ }
457
+ }
458
+ return pathsIndex >= 0
459
+ ? chain[pathsIndex].dir
460
+ : (0, path_1.dirname)((0, path_1.resolve)(tsconfigPath));
461
+ }
462
+ function resolveExtendsPath(ext, fromDir) {
463
+ if (ext.startsWith('.') || (0, path_1.isAbsolute)(ext)) {
464
+ let resolved = (0, path_1.resolve)(fromDir, ext);
465
+ if ((0, fs_1.existsSync)(resolved))
466
+ return resolved;
467
+ if (!resolved.endsWith('.json')) {
468
+ resolved += '.json';
469
+ if ((0, fs_1.existsSync)(resolved))
470
+ return resolved;
471
+ }
472
+ return null;
473
+ }
474
+ try {
475
+ return require.resolve(ext, { paths: [fromDir] });
476
+ }
477
+ catch {
478
+ try {
479
+ return require.resolve(`${ext}/tsconfig.json`, { paths: [fromDir] });
480
+ }
481
+ catch {
482
+ return null;
483
+ }
484
+ }
485
+ }
@@ -254,19 +254,39 @@ function formatAggregateCreateNodesError(error, pluginName) {
254
254
  `${errorCount} occurred while processing files for the ${pluginName} plugin${pluginLocation}.`,
255
255
  ];
256
256
  const errorStackLines = [];
257
- const innerErrors = error.errors;
258
- for (const [file, e] of innerErrors) {
259
- if (file) {
260
- errorBodyLines.push(` - ${file}: ${e.message}`);
261
- errorStackLines.push(` - ${file}: ${e.stack}`);
257
+ // Group errors by file so repeated file paths aren't printed multiple times
258
+ const groupedErrors = new Map();
259
+ for (const [file, e] of error.errors) {
260
+ const key = file ?? null;
261
+ if (!groupedErrors.has(key)) {
262
+ groupedErrors.set(key, []);
262
263
  }
263
- else {
264
- errorBodyLines.push(` - ${e.message}`);
265
- errorStackLines.push(` - ${e.stack}`);
264
+ groupedErrors.get(key).push(e);
265
+ }
266
+ for (const [file, errors] of groupedErrors) {
267
+ if (file) {
268
+ errorBodyLines.push(` - ${file}:`);
269
+ errorStackLines.push(` - ${file}:`);
266
270
  }
267
- if (e.stack && process.env.NX_VERBOSE_LOGGING === 'true') {
268
- const innerStackTrace = ' ' + e.stack.split('\n')?.join('\n ');
269
- errorStackLines.push(innerStackTrace);
271
+ for (const e of errors) {
272
+ const messageLines = e.message.split('\n');
273
+ const stackLines = e.stack.split('\n');
274
+ if (file) {
275
+ errorBodyLines.push(...messageLines.map((line) => ` ${line}`));
276
+ errorStackLines.push(...stackLines.map((line) => ` ${line}`));
277
+ }
278
+ else {
279
+ errorBodyLines.push(` - ${messageLines[0]}`, ...messageLines.slice(1).map((line) => ` ${line}`));
280
+ errorStackLines.push(` - ${stackLines[0]}`, ...stackLines.slice(1).map((line) => ` ${line}`));
281
+ }
282
+ if (e.stack && process.env.NX_VERBOSE_LOGGING === 'true') {
283
+ const verboseIndent = file ? ' ' : ' ';
284
+ const innerStackTrace = e.stack
285
+ .split('\n')
286
+ .map((line) => `${verboseIndent}${line}`)
287
+ .join('\n');
288
+ errorStackLines.push(innerStackTrace);
289
+ }
270
290
  }
271
291
  }
272
292
  error.stack = errorStackLines.join('\n');
@@ -55,6 +55,7 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
55
55
  private generateTxId;
56
56
  private sendRequest;
57
57
  private shutdownIfInactive;
58
+ setWorkerEnv(env: Record<string, string>): Promise<void>;
58
59
  notifyPhaseAborted(phase: Phase, lastCompletedHook: Hook): void;
59
60
  shutdown(): void;
60
61
  private registerProcessMetrics;
@@ -272,6 +272,15 @@ class IsolatedPlugin {
272
272
  logger_1.logger.verbose(`[isolated-plugin] shutting down worker for "${this.name}" after ${hookName}`);
273
273
  this.shutdown();
274
274
  }
275
+ async setWorkerEnv(env) {
276
+ if (!this._alive) {
277
+ return;
278
+ }
279
+ const result = await this.sendRequest('setWorkerEnv', env);
280
+ if (result.success === false) {
281
+ throw result.error;
282
+ }
283
+ }
275
284
  notifyPhaseAborted(phase, lastCompletedHook) {
276
285
  if (this.lifecycle?.notifyPhaseAborted(phase, lastCompletedHook)) {
277
286
  this.shutdownIfInactive(lastCompletedHook);
@@ -55,5 +55,5 @@ export type AllResults<TDefs extends MessageDefs> = {
55
55
  export type Handlers<TDefs extends MessageDefs> = {
56
56
  [K in keyof TDefs & string]: (payload: TDefs[K]['payload']) => TDefs[K] extends {
57
57
  result: unknown;
58
- } ? MaybePromise<TDefs[K]['result'] | void> : MaybePromise<void>;
58
+ } ? MaybePromise<TDefs[K]['result']> : MaybePromise<void>;
59
59
  };
@@ -97,6 +97,15 @@ type PluginMessageDefs = DefineMessages<{
97
97
  error: Error;
98
98
  };
99
99
  };
100
+ setWorkerEnv: {
101
+ payload: Record<string, string>;
102
+ result: {
103
+ success: true;
104
+ } | {
105
+ success: false;
106
+ error: Error;
107
+ };
108
+ };
100
109
  }>;
101
110
  /** Union of all plugin worker message types */
102
111
  export type PluginWorkerMessage = AllMessages<PluginMessageDefs>;
@@ -15,6 +15,7 @@ const MESSAGE_TYPES = [
15
15
  'createMetadata',
16
16
  'preTasksExecution',
17
17
  'postTasksExecution',
18
+ 'setWorkerEnv',
18
19
  ];
19
20
  const RESULT_TYPES = [
20
21
  'loadResult',
@@ -23,6 +24,7 @@ const RESULT_TYPES = [
23
24
  'createMetadataResult',
24
25
  'preTasksExecutionResult',
25
26
  'postTasksExecutionResult',
27
+ 'setWorkerEnvResult',
26
28
  ];
27
29
  function isPluginWorkerMessage(message) {
28
30
  return (typeof message === 'object' &&
@@ -8,8 +8,8 @@ const serializable_error_1 = require("../../../utils/serializable-error");
8
8
  const messaging_1 = require("./messaging");
9
9
  const fs_1 = require("fs");
10
10
  const net_1 = require("net");
11
- require("../../../utils/perf-logging");
12
11
  const analytics_1 = require("../../../analytics");
12
+ require("../../../utils/perf-logging");
13
13
  const environment = process.env;
14
14
  (0, analytics_1.startAnalytics)();
15
15
  node_perf_hooks_1.performance.mark(`plugin worker ${process.pid} code loading -- end`);
@@ -41,7 +41,7 @@ const server = (0, net_1.createServer)((socket) => {
41
41
  load: async ({ plugin: pluginConfiguration, root, name, pluginPath, shouldRegisterTSTranspiler, }) => {
42
42
  loadErrorTimeout?.clear();
43
43
  process.chdir(root);
44
- try {
44
+ return withErrorHandling(async () => {
45
45
  const { loadResolvedNxPluginAsync } = await Promise.resolve(require(require.resolve('../load-resolved-plugin')));
46
46
  // Register the ts-transpiler if we are pointing to a
47
47
  // plain ts file that's not part of a plugin project
@@ -62,74 +62,30 @@ const server = (0, net_1.createServer)((socket) => {
62
62
  hasPostTasksExecution: 'postTasksExecution' in plugin && !!plugin.postTasksExecution,
63
63
  success: true,
64
64
  };
65
- }
66
- catch (e) {
67
- return {
68
- success: false,
69
- error: (0, serializable_error_1.createSerializableError)(e),
70
- };
71
- }
72
- },
73
- createNodes: async function createNodes({ configFiles, context }) {
74
- try {
75
- const result = await plugin.createNodes[1](configFiles, context);
76
- return { result, success: true };
77
- }
78
- catch (e) {
79
- return {
80
- success: false,
81
- error: (0, serializable_error_1.createSerializableError)(e),
82
- };
83
- }
84
- },
85
- createDependencies: async function createDependencies({ context }) {
86
- try {
87
- const result = await plugin.createDependencies(context);
88
- return { dependencies: result, success: true };
89
- }
90
- catch (e) {
91
- return {
92
- success: false,
93
- error: (0, serializable_error_1.createSerializableError)(e),
94
- };
95
- }
96
- },
97
- createMetadata: async function createMetadata({ graph, context }) {
98
- try {
99
- const result = await plugin.createMetadata(graph, context);
100
- return { metadata: result, success: true };
101
- }
102
- catch (e) {
103
- return {
104
- success: false,
105
- error: (0, serializable_error_1.createSerializableError)(e),
106
- };
107
- }
108
- },
109
- preTasksExecution: async ({ context }) => {
110
- try {
111
- const mutations = await plugin.preTasksExecution?.(context);
112
- return { success: true, mutations };
113
- }
114
- catch (e) {
115
- return {
116
- success: false,
117
- error: (0, serializable_error_1.createSerializableError)(e),
118
- };
119
- }
65
+ });
120
66
  },
121
- postTasksExecution: async ({ context }) => {
122
- try {
123
- await plugin.postTasksExecution?.(context);
124
- return { success: true };
67
+ createNodes: async ({ configFiles, context }) => withErrorHandling(async () => {
68
+ const result = await plugin.createNodes[1](configFiles, context);
69
+ return { result, success: true };
70
+ }),
71
+ createDependencies: async ({ context }) => withErrorHandling(async () => {
72
+ const result = await plugin.createDependencies(context);
73
+ return { dependencies: result, success: true };
74
+ }),
75
+ createMetadata: async ({ graph, context }) => withErrorHandling(async () => {
76
+ const result = await plugin.createMetadata(graph, context);
77
+ return { metadata: result, success: true };
78
+ }),
79
+ preTasksExecution: async ({ context }) => withErrorHandling(async () => {
80
+ const mutations = await plugin.preTasksExecution?.(context);
81
+ return { success: true, mutations };
82
+ }),
83
+ postTasksExecution: async ({ context }) => withErrorHandling(() => plugin.postTasksExecution?.(context)),
84
+ setWorkerEnv: (env) => withErrorHandling(() => {
85
+ for (const envKey in env) {
86
+ process.env[envKey] = env[envKey];
125
87
  }
126
- catch (e) {
127
- return {
128
- success: false,
129
- error: (0, serializable_error_1.createSerializableError)(e),
130
- };
131
- }
132
- },
88
+ }),
133
89
  });
134
90
  }));
135
91
  // When the host disconnects, clean up and exit.
@@ -144,6 +100,18 @@ const server = (0, net_1.createServer)((socket) => {
144
100
  });
145
101
  server.listen(socketPath);
146
102
  logger_1.logger.verbose(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) listening on ${socketPath}`);
103
+ async function withErrorHandling(cb) {
104
+ try {
105
+ const result = await cb();
106
+ return result ?? { success: true };
107
+ }
108
+ catch (e) {
109
+ return {
110
+ success: false,
111
+ error: (0, serializable_error_1.createSerializableError)(e),
112
+ };
113
+ }
114
+ }
147
115
  function setErrorTimeout(timeoutMs, errorMessage) {
148
116
  if (environment.NX_PLUGIN_NO_TIMEOUTS === 'true') {
149
117
  return;
@@ -31,5 +31,11 @@ export declare class LoadedNxPlugin {
31
31
  * abort (e.g. 'createNodes').
32
32
  */
33
33
  notifyPhaseAborted?(phase: string, lastCompletedHook: string): void;
34
+ /**
35
+ * Forwards updated environment variables to the plugin worker process.
36
+ * Only meaningful for isolated plugins; in-process plugins share the
37
+ * daemon's process.env automatically.
38
+ */
39
+ setWorkerEnv?(env: Record<string, string>): Promise<void>;
34
40
  constructor(plugin: NxPluginV2, pluginDefinition: PluginConfiguration, index?: number);
35
41
  }
@@ -26,6 +26,9 @@ export declare class DbCache {
26
26
  });
27
27
  init(): Promise<void>;
28
28
  get(task: Task): Promise<CachedResult | null>;
29
+ getBatch(tasks: Task[]): Promise<Map<string, CachedResult & {
30
+ remote: boolean;
31
+ }>>;
29
32
  getUsedCacheSpace(): number;
30
33
  private applyRemoteCacheResults;
31
34
  put(task: Task, terminalOutput: string | null, outputs: string[], code: number): Promise<void>;
@@ -54,6 +57,9 @@ export declare class Cache {
54
57
  constructor(options: DefaultTasksRunnerOptions);
55
58
  removeOldCacheRecords(): void;
56
59
  get(task: Task): Promise<CachedResult | null>;
60
+ getBatch(tasks: Task[]): Promise<Map<string, CachedResult & {
61
+ remote: boolean;
62
+ }>>;
57
63
  put(task: Task, terminalOutput: string | null, outputs: string[], code: number): Promise<void>;
58
64
  copyFilesFromCache(hash: string, cachedResult: CachedResult, outputs: string[]): Promise<void>;
59
65
  temporaryOutputPath(task: Task): string;
@@ -97,6 +97,54 @@ class DbCache {
97
97
  return null;
98
98
  }
99
99
  }
100
+ async getBatch(tasks) {
101
+ const results = new Map();
102
+ if (tasks.length === 0)
103
+ return results;
104
+ // Single-task fast path: direct primary-key lookup beats rarray
105
+ // vtable overhead when there's nothing to amortize over.
106
+ if (tasks.length === 1) {
107
+ const res = await this.get(tasks[0]);
108
+ if (res)
109
+ results.set(tasks[0].hash, res);
110
+ return results;
111
+ }
112
+ const hashes = tasks.map((t) => t.hash);
113
+ // 1. Local: one rarray SQL query + parallel terminal output reads.
114
+ // batchResults is index-aligned with tasks, so we walk in lockstep.
115
+ const batchResults = this.cache.getBatch(hashes);
116
+ const remoteMisses = [];
117
+ for (const [i, task] of tasks.entries()) {
118
+ const res = batchResults[i];
119
+ if (res) {
120
+ results.set(task.hash, {
121
+ ...res,
122
+ terminalOutput: res.terminalOutput ?? '',
123
+ remote: false,
124
+ });
125
+ }
126
+ else if (this.remoteCache) {
127
+ remoteMisses.push(task);
128
+ }
129
+ }
130
+ // 2. Remote: parallel HTTP retrievals for anything the local SQL missed.
131
+ // The RemoteCache interface has no batch endpoint, so this is just
132
+ // Promise.all over individual retrieve() calls.
133
+ if (remoteMisses.length > 0) {
134
+ await Promise.all(remoteMisses.map(async (task) => {
135
+ const res = await this.remoteCache.retrieve(task.hash, this.cache.cacheDirectory);
136
+ if (res) {
137
+ this.applyRemoteCacheResults(task.hash, res, task.outputs);
138
+ results.set(task.hash, {
139
+ ...res,
140
+ terminalOutput: res.terminalOutput ?? '',
141
+ remote: true,
142
+ });
143
+ }
144
+ }));
145
+ }
146
+ return results;
147
+ }
100
148
  getUsedCacheSpace() {
101
149
  return this.cache.getCacheSize();
102
150
  }
@@ -304,6 +352,16 @@ class Cache {
304
352
  return null;
305
353
  }
306
354
  }
355
+ async getBatch(tasks) {
356
+ // Legacy file-based cache has no native batch support — loop in parallel.
357
+ const results = new Map();
358
+ const entries = await Promise.all(tasks.map(async (t) => ({ task: t, res: await this.get(t) })));
359
+ for (const { task, res } of entries) {
360
+ if (res)
361
+ results.set(task.hash, res);
362
+ }
363
+ return results;
364
+ }
307
365
  async put(task, terminalOutput, outputs, code) {
308
366
  return tryAndRetry(async () => {
309
367
  /**
@@ -114,7 +114,7 @@ async function runDiscreteTasks(tasks, projectGraph, taskGraphForHashing, nxJson
114
114
  const orchestrator = await createOrchestrator(tasks, projectGraph, taskGraphForHashing, nxJson, lifeCycle);
115
115
  let groupId = 0;
116
116
  let nextBatch = orchestrator.nextBatch();
117
- let batchResults = [];
117
+ const batchResults = [];
118
118
  /**
119
119
  * Set of task ids that were part of batches
120
120
  */
@@ -126,12 +126,18 @@ async function runDiscreteTasks(tasks, projectGraph, taskGraphForHashing, nxJson
126
126
  batchResults.push(orchestrator.applyFromCacheOrRunBatch(true, nextBatch, groupId++));
127
127
  nextBatch = orchestrator.nextBatch();
128
128
  }
129
- const taskResults = tasks
130
- // Filter out tasks which were not part of batches
131
- .filter((task) => !batchTasks.has(task.id))
132
- .map((task) => orchestrator
133
- .applyFromCacheOrRunTask(true, task, groupId++)
134
- .then((r) => [r]));
129
+ const discreteTasks = tasks.filter((task) => !batchTasks.has(task.id));
130
+ // Bulk-resolve every discrete task's cache state in one shot —
131
+ // single SQL call plus parallel remote retrievals. Batches kicked
132
+ // off above continue running concurrently while we await this.
133
+ const cacheHits = await orchestrator.resolveCachedTasks(true, discreteTasks, groupId++);
134
+ const cacheHitsById = new Map(cacheHits.map((h) => [h.task.id, h]));
135
+ const taskResults = discreteTasks.map(async (task) => {
136
+ const hit = cacheHitsById.get(task.id);
137
+ if (hit)
138
+ return [hit];
139
+ return [await orchestrator.runTaskDirectly(true, task, groupId++)];
140
+ });
135
141
  return [...batchResults, ...taskResults];
136
142
  }
137
143
  async function runContinuousTasks(tasks, projectGraph, taskGraphForHashing, nxJson, lifeCycle) {
@@ -674,7 +674,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
674
674
  }
675
675
  return hasher.hashTask(task, taskGraph_, env);
676
676
  },
677
- hashTasks(task, taskGraph_, env) {
677
+ hashTasks(tasks, taskGraph_, envOrPerTaskEnvs) {
678
678
  if (!taskGraph_) {
679
679
  output_1.output.warn({
680
680
  title: `TaskGraph is now required as an argument to hashTasks`,
@@ -685,7 +685,7 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
685
685
  });
686
686
  taskGraph_ = taskGraph;
687
687
  }
688
- if (!env) {
688
+ if (!envOrPerTaskEnvs) {
689
689
  output_1.output.warn({
690
690
  title: `The environment variables are now required as an argument to hashTasks`,
691
691
  bodyLines: [
@@ -693,9 +693,11 @@ async function invokeTasksRunner({ tasks, projectGraph, taskGraph, lifeCycle, nx
693
693
  'This will result in an error in Nx 20',
694
694
  ],
695
695
  });
696
- env = process.env;
696
+ envOrPerTaskEnvs = process.env;
697
697
  }
698
- return hasher.hashTasks(task, taskGraph_, env);
698
+ // hasher.hashTasks accepts either legacy single-env or the new
699
+ // per-task-env shape and normalizes internally.
700
+ return hasher.hashTasks(tasks, taskGraph_, envOrPerTaskEnvs);
699
701
  },
700
702
  },
701
703
  daemon: client_1.daemonClient,
@@ -816,7 +818,13 @@ function getTasksRunnerPath(runner, nxJson) {
816
818
  process.env.NX_CLOUD_ACCESS_TOKEN ||
817
819
  // Nx Cloud ID specified in nxJson
818
820
  nxJson.nxCloudId;
819
- return isCloudRunner ? 'nx-cloud' : defaultTasksRunnerPath;
821
+ // NX_NO_CLOUD / neverConnectToCloud wins over any ambient token — otherwise
822
+ // a surrounding CI env variable would still route through the cloud shell,
823
+ // which resolves the default tasks runner via its own require bridge and
824
+ // can pull in a different Nx version than the workspace's own.
825
+ return isCloudRunner && !(0, nx_cloud_utils_1.isNxCloudDisabled)(nxJson)
826
+ ? 'nx-cloud'
827
+ : defaultTasksRunnerPath;
820
828
  }
821
829
  function getRunnerOptions(runner, nxJson, nxArgs, isCloudDefault) {
822
830
  const defaultCacheableOperations = [];
@@ -19,13 +19,29 @@ function getEnvVariablesForBatchProcess(skipNxCache, captureStderr) {
19
19
  ...getNxEnvVariablesForForkedProcess(process.env.FORCE_COLOR === undefined ? 'true' : process.env.FORCE_COLOR, skipNxCache, captureStderr),
20
20
  };
21
21
  }
22
+ // The orchestrator now calls this eagerly during the coordinator pre-hash
23
+ // in addition to processTask (and again in hashBatchTasks), so the same
24
+ // task hits this function multiple times per run. Each call reads 3+ .env
25
+ // files from disk — memoize by task.id to skip the repeat work.
26
+ //
27
+ // Cache lifetime is the current Nx invocation: the function is only called
28
+ // from CLI/orchestrator code (not the long-lived daemon), so the map is
29
+ // scoped to a single run. Callers must not mutate the returned env — they
30
+ // already spread it into new objects before adding task-specific overrides
31
+ // (see getEnvVariablesForTask).
32
+ const taskSpecificEnvCache = new Map();
22
33
  function getTaskSpecificEnv(task, graph) {
34
+ const cached = taskSpecificEnvCache.get(task.id);
35
+ if (cached)
36
+ return cached;
23
37
  // Unload any dot env files at the root of the workspace that were loaded on init of Nx.
24
38
  const taskEnv = unloadDotEnvFiles({ ...process.env });
25
- return process.env.NX_LOAD_DOT_ENV_FILES === 'true'
39
+ const env = process.env.NX_LOAD_DOT_ENV_FILES === 'true'
26
40
  ? loadDotEnvFilesForTask(task, graph, taskEnv)
27
41
  : // If not loading dot env files, ensure env vars created by system are still loaded
28
42
  taskEnv;
43
+ taskSpecificEnvCache.set(task.id, env);
44
+ return env;
29
45
  }
30
46
  function getEnvVariablesForTask(task, taskSpecificEnv, forceColor, skipNxCache, captureStderr, outputPath, streamOutput) {
31
47
  const res = {