nx 23.3.0 → 24.0.0-beta.0

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/README.md +3 -6
  2. package/dist/src/adapter/ngcli-adapter.js +1 -0
  3. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +0 -12
  4. package/dist/src/command-line/graph/graph.js +3 -5
  5. package/dist/src/command-line/init/command-object.js +0 -12
  6. package/dist/src/command-line/init/init-v2.js +0 -2
  7. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +17 -12
  8. package/dist/src/command-line/release/utils/remote-release-clients/github.js +5 -6
  9. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +5 -6
  10. package/dist/src/command-line/run/executor-utils.js +1 -0
  11. package/dist/src/command-line/run/run-one.d.ts +20 -1
  12. package/dist/src/command-line/run/run-one.js +153 -5
  13. package/dist/src/command-line/show/show-target/utils.js +1 -0
  14. package/dist/src/command-line/watch/command-object.js +3 -22
  15. package/dist/src/command-line/watch/watch.d.ts +0 -7
  16. package/dist/src/command-line/watch/watch.js +1 -1
  17. package/dist/src/config/misc-interfaces.d.ts +12 -0
  18. package/dist/src/config/workspace-json-project-json.d.ts +5 -0
  19. package/dist/src/daemon/client/client.d.ts +9 -0
  20. package/dist/src/daemon/client/client.js +89 -37
  21. package/dist/src/daemon/server/shutdown-utils.js +0 -3
  22. package/dist/src/daemon/socket-utils.d.ts +9 -4
  23. package/dist/src/daemon/socket-utils.js +12 -49
  24. package/dist/src/daemon/tmp-dir.d.ts +19 -14
  25. package/dist/src/daemon/tmp-dir.js +142 -354
  26. package/dist/src/devkit-internals.d.ts +1 -1
  27. package/dist/src/devkit-internals.js +1 -0
  28. package/dist/src/hasher/file-hasher.js +9 -7
  29. package/dist/src/hasher/hash-task.js +4 -0
  30. package/dist/src/hasher/native-task-hasher-impl.js +1 -1
  31. package/dist/src/migrations/update-24-0-0/find-custom-hashers.d.ts +3 -0
  32. package/dist/src/migrations/update-24-0-0/find-custom-hashers.js +100 -0
  33. package/dist/src/migrations/update-24-0-0/find-custom-hashers.md +63 -0
  34. package/dist/src/migrations/update-24-0-0/move-custom-hashers-to-inputs.md +145 -0
  35. package/dist/src/native/index.d.ts +127 -8
  36. package/dist/src/native/native-bindings.js +5 -0
  37. package/dist/src/native/nx.wasi-browser.js +1 -0
  38. package/dist/src/native/nx.wasi.cjs +1 -0
  39. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  40. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  41. package/dist/src/nx-cloud/update-manager.d.ts +7 -0
  42. package/dist/src/nx-cloud/update-manager.js +237 -50
  43. package/dist/src/project-graph/affected/affected-tasks.js +1 -0
  44. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +7 -0
  45. package/dist/src/tasks-runner/create-task-graph.d.ts +10 -0
  46. package/dist/src/tasks-runner/create-task-graph.js +37 -2
  47. package/dist/src/tasks-runner/task-env.d.ts +10 -0
  48. package/dist/src/tasks-runner/task-env.js +26 -5
  49. package/dist/src/tasks-runner/task-orchestrator.d.ts +10 -4
  50. package/dist/src/tasks-runner/task-orchestrator.js +87 -53
  51. package/dist/src/tasks-runner/utils.d.ts +20 -5
  52. package/dist/src/tasks-runner/utils.js +80 -29
  53. package/dist/src/ultracache/overrides.js +4 -0
  54. package/dist/src/utils/owned-private-dir.d.ts +1 -17
  55. package/dist/src/utils/owned-private-dir.js +10 -62
  56. package/dist/src/utils/perf-hooks.d.ts +1 -0
  57. package/dist/src/utils/plugins/core-plugins.js +1 -1
  58. package/dist/src/utils/string-similarity.d.ts +38 -0
  59. package/dist/src/utils/string-similarity.js +108 -0
  60. package/migrations.json +7 -0
  61. package/package.json +11 -20
  62. package/readme-template.md +3 -3
  63. package/schemas/nx-schema.json +5 -0
  64. package/schemas/project-schema.json +5 -0
  65. package/dist/src/tasks-runner/legacy-depends-on-warning.d.ts +0 -18
  66. package/dist/src/tasks-runner/legacy-depends-on-warning.js +0 -109
@@ -78,11 +78,12 @@ class TaskOrchestrator {
78
78
  ? new native_1.RunningTasksService((0, db_connection_1.getLocalDbConnection)())
79
79
  : null;
80
80
  this.taskInvocationTracker = !native_1.IS_WASM
81
- ? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), Number(process.env.NX_INVOCATION_ROOT_PID ?? process.pid))
81
+ ? new native_1.TaskInvocationTracker((0, db_connection_1.getLocalDbConnection)(), (0, task_env_1.getInvocationRootPid)(), (0, task_env_1.getInvocationAncestorPids)())
82
82
  : null;
83
- // Tracks tasks registered by THIS process so that recursive code paths
84
- // (e.g. applyFromCacheOrRunBatch looping on incomplete batches) don't
85
- // re-register and trip the DB uniqueness constraint.
83
+ // Task ids this process has already registered. Recursive code paths (e.g.
84
+ // applyFromCacheOrRunBatch looping on incomplete batches) re-enter the
85
+ // detector for the same task; skipping them saves a DB round trip. A task id
86
+ // is unique within one process, so it is enough to identify the row.
86
87
  this.registeredInvocations = new Set();
87
88
  this.tasksSchedule = new tasks_schedule_1.TasksSchedule(this.projectGraph, this.projects, this.taskGraph, this.options);
88
89
  // region internal state
@@ -316,39 +317,59 @@ class TaskOrchestrator {
316
317
  }
317
318
  }
318
319
  /**
319
- * Registers a task invocation and checks for loops across nested Nx processes.
320
- * Uses the task_invocations DB table keyed by root PID. registerTask() throws
321
- * on unique constraint violation when a parent Nx process already registered
322
- * this task — indicating an infinite loop.
320
+ * Registers a task invocation and checks for loops across nested Nx
321
+ * processes. registerTask() returns the invocation chain only when an
322
+ * *ancestor* Nx process is already running this task — a genuine loop.
323
+ * Sibling processes running the same task are legitimate and register
324
+ * without complaint.
325
+ *
326
+ * The overrides hash rides along with the task id so the tracker can tell an
327
+ * ancestor re-invoking a task with different arguments from one repeating
328
+ * the same invocation.
323
329
  */
324
330
  detectTaskInvocationLoop(task) {
325
331
  if (!this.taskInvocationTracker)
326
332
  return;
327
333
  if (this.registeredInvocations.has(task.id))
328
334
  return;
335
+ let chain;
329
336
  try {
330
- this.taskInvocationTracker.registerTask(process.pid, task.id);
337
+ chain = this.taskInvocationTracker.registerTask(process.pid, task.id, (0, utils_1.hashTaskOverrides)(task));
338
+ }
339
+ catch {
340
+ // Loop detection is diagnostic only; a DB failure must not fail the run
341
+ // or be mistaken for a loop.
342
+ return;
343
+ }
344
+ if (!chain) {
331
345
  this.registeredInvocations.add(task.id);
346
+ return;
347
+ }
348
+ const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
349
+ output_1.output.error({
350
+ title: 'Recursive task invocation detected',
351
+ bodyLines: [
352
+ `Nx detected a recursive loop of task invocations:`,
353
+ ``,
354
+ ` ${chainDisplay} -> ${task.id}`,
355
+ ``,
356
+ `Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
357
+ `This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
358
+ `triggers a chain of tasks that eventually re-invokes itself.`,
359
+ ``,
360
+ `To fix this, review the command configuration for the tasks in the chain above.`,
361
+ ],
362
+ });
363
+ process.exit(1);
364
+ }
365
+ releaseTaskInvocation(task) {
366
+ if (!this.registeredInvocations.delete(task.id))
367
+ return;
368
+ try {
369
+ this.taskInvocationTracker?.unregisterTask(process.pid, task.id);
332
370
  }
333
371
  catch {
334
- // Unique constraint violation — task already invoked by an ancestor Nx process
335
- const chain = this.taskInvocationTracker.getInvocationChain();
336
- const chainDisplay = chain.map((r) => r.taskId).join(' -> ');
337
- output_1.output.error({
338
- title: 'Recursive task invocation detected',
339
- bodyLines: [
340
- `Nx detected a recursive loop of task invocations:`,
341
- ``,
342
- ` ${chainDisplay} -> ${task.id}`,
343
- ``,
344
- `Task "${task.id}" was already invoked by a parent Nx process in this chain.`,
345
- `This typically happens when a task's command (e.g., "nx ${task.target.target} ${task.target.project}")`,
346
- `triggers a chain of tasks that eventually re-invokes itself.`,
347
- ``,
348
- `To fix this, review the command configuration for the tasks in the chain above.`,
349
- ],
350
- });
351
- process.exit(1);
372
+ // Diagnostic only, like registration. A leftover row is swept as stale.
352
373
  }
353
374
  }
354
375
  // endregion Processing Scheduled Tasks
@@ -1186,31 +1207,45 @@ class TaskOrchestrator {
1186
1207
  await this.scheduleNextTasksAndReleaseThreads();
1187
1208
  return runningTask;
1188
1209
  }
1189
- const taskSpecificEnv = await this.processedTasks.get(task.id);
1190
- await this.preRunSteps([task], { groupId });
1191
- const pipeOutput = await this.pipeOutputCapture(task);
1192
- // obtain metadata
1193
- const temporaryOutputPath = this.cache.temporaryOutputPath(task);
1194
- // Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
1195
- // `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
1196
- // only reports at `endCommand`, which never runs for a task that does not
1197
- // end - so suppressing here would leave a `nx serve` under `summary` with a
1198
- // permanently silent terminal and no file to read yet. Streaming is the only
1199
- // channel a continuous task has.
1200
- const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
1201
- ? false
1202
- : (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
1203
- let env = pipeOutput
1204
- ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
1205
- : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
1206
- this.detectTaskInvocationLoop(task);
1207
- const childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
1210
+ // Claim the task before doing any of the work below. getRunningTasks()
1211
+ // above is a check-then-act: until this row exists, a sibling Nx process
1212
+ // asking the same question is told the task is not running and starts a
1213
+ // duplicate of it.
1208
1214
  this.runningTasksService?.addRunningTask(task.id);
1209
- this.runningContinuousTasks.set(task.id, {
1210
- runningTask: childProcess,
1211
- groupId,
1212
- ownsRunningTasksService: true,
1213
- });
1215
+ let childProcess;
1216
+ try {
1217
+ const taskSpecificEnv = await this.processedTasks.get(task.id);
1218
+ await this.preRunSteps([task], { groupId });
1219
+ const pipeOutput = await this.pipeOutputCapture(task);
1220
+ // obtain metadata
1221
+ const temporaryOutputPath = this.cache.temporaryOutputPath(task);
1222
+ // Deliberately not gated on `printsTaskOutput`, unlike `runTaskDirectly`.
1223
+ // `SummaryTerminalOutputLifeCycle.printTaskTerminalOutput` is a no-op and it
1224
+ // only reports at `endCommand`, which never runs for a task that does not
1225
+ // end - so suppressing here would leave a `nx serve` under `summary` with a
1226
+ // permanently silent terminal and no file to read yet. Streaming is the only
1227
+ // channel a continuous task has.
1228
+ const streamOutput = (0, output_1.isStaticOutputStyle)(this.specifiedOutputStyle)
1229
+ ? false
1230
+ : (0, utils_1.shouldStreamOutput)(task, this.initiatingProject);
1231
+ let env = pipeOutput
1232
+ ? (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, (0, task_env_1.getForceColorForChild)(), this.options.skipNxCache, this.options.captureStderr, null, null)
1233
+ : (0, task_env_1.getEnvVariablesForTask)(task, taskSpecificEnv, undefined, this.options.skipNxCache, this.options.captureStderr, temporaryOutputPath, streamOutput);
1234
+ this.detectTaskInvocationLoop(task);
1235
+ childProcess = await this.runTask(task, streamOutput, env, temporaryOutputPath, pipeOutput);
1236
+ this.runningContinuousTasks.set(task.id, {
1237
+ runningTask: childProcess,
1238
+ groupId,
1239
+ ownsRunningTasksService: true,
1240
+ });
1241
+ }
1242
+ catch (e) {
1243
+ // Nothing owns the claim until runningContinuousTasks holds it — the
1244
+ // release paths (completeContinuousTask, the signal handler) both iterate
1245
+ // that map. Drop it here or siblings wait on a task no one is running.
1246
+ this.runningTasksService?.removeRunningTask(task.id);
1247
+ throw e;
1248
+ }
1214
1249
  this.continuousTaskExitHandled.set(task.id, new Promise((resolve) => {
1215
1250
  childProcess.onExit(async (code) => {
1216
1251
  await this.handleContinuousTaskExit(code, task, groupId, true);
@@ -1425,8 +1460,7 @@ class TaskOrchestrator {
1425
1460
  if (this.completedTasks.has(task.id))
1426
1461
  continue;
1427
1462
  this.completedTasks.set(task.id, status);
1428
- this.taskInvocationTracker?.unregisterTask(task.id);
1429
- this.registeredInvocations.delete(task.id);
1463
+ this.releaseTaskInvocation(task);
1430
1464
  if (this.tuiEnabled) {
1431
1465
  this.options.lifeCycle.setTaskStatus(task.id, displayStatus ?? (0, native_1.parseTaskStatus)(status));
1432
1466
  }
@@ -2,7 +2,6 @@ import { CustomHasher, ExecutorConfig } from '../config/misc-interfaces';
2
2
  import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
3
3
  import { Task, TaskGraph } from '../config/task-graph';
4
4
  import { ProjectConfiguration, TargetConfiguration, TargetDependencyConfig } from '../config/workspace-json-project-json';
5
- import { LegacyDependsOnLocation } from './legacy-depends-on-warning';
6
5
  export type NormalizedTargetDependencyConfig = TargetDependencyConfig & {
7
6
  projects: string[];
8
7
  };
@@ -10,9 +9,8 @@ export declare function getDependencyConfigs({ project, target }: {
10
9
  project: string;
11
10
  target: string;
12
11
  }, extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>, projectGraph: ProjectGraph, allTargetNames: string[]): NormalizedTargetDependencyConfig[] | undefined;
13
- export type DependsOnEntryLocation = LegacyDependsOnLocation;
14
- export declare function normalizeDependencyConfigDefinition(definition: string | TargetDependencyConfig, currentProject: string, graph: ProjectGraph, allTargetNames: string[], location?: DependsOnEntryLocation): NormalizedTargetDependencyConfig[];
15
- export declare function normalizeDependencyConfigProjects(dependencyConfig: TargetDependencyConfig, currentProject: string, graph: ProjectGraph, location?: DependsOnEntryLocation): NormalizedTargetDependencyConfig;
12
+ export declare function normalizeDependencyConfigDefinition(definition: string | TargetDependencyConfig, currentProject: string, graph: ProjectGraph, allTargetNames: string[]): NormalizedTargetDependencyConfig[];
13
+ export declare function normalizeDependencyConfigProjects(dependencyConfig: TargetDependencyConfig, currentProject: string, graph: ProjectGraph): NormalizedTargetDependencyConfig;
16
14
  export declare function expandDependencyConfigSyntaxSugar(dependencyConfigString: string | TargetDependencyConfig, graph: ProjectGraph, currentProject?: string): TargetDependencyConfig;
17
15
  export declare function expandWildcardTargetConfiguration(dependencyConfig: NormalizedTargetDependencyConfig, allTargetNames: string[]): NormalizedTargetDependencyConfig[];
18
16
  export declare function readProjectAndTargetFromTargetString(targetString: string, projects: Record<string, ProjectGraphProjectNode>, currentProject?: string): {
@@ -20,7 +18,7 @@ export declare function readProjectAndTargetFromTargetString(targetString: strin
20
18
  target: string;
21
19
  };
22
20
  export declare function getOutputs(p: Record<string, ProjectGraphProjectNode>, target: Task['target'], overrides: Task['overrides']): string[];
23
- export declare function normalizeTargetDependencyWithStringProjects(dependencyConfig: TargetDependencyConfig, currentProject?: string, location?: DependsOnEntryLocation): Omit<TargetDependencyConfig, 'projects'> & {
21
+ export declare function normalizeTargetDependencyWithStringProjects(dependencyConfig: TargetDependencyConfig): Omit<TargetDependencyConfig, 'projects'> & {
24
22
  projects?: string[];
25
23
  };
26
24
  export declare function validateOutputs(outputs: string[]): void;
@@ -49,6 +47,8 @@ export declare function getExecutorForTask(task: Task, projects: Record<string,
49
47
  isNxExecutor: boolean;
50
48
  };
51
49
  export declare function getCustomHasher(task: Task, projects: Record<string, ProjectConfiguration>): CustomHasher | null;
50
+ /** Test-only: forget which executors have been warned about. */
51
+ export declare function resetCustomHasherDeprecationWarnings(): void;
52
52
  /**
53
53
  * `taskGraph` pruned to `required`, which must already hold its dependencies.
54
54
  * Unknown ids are ignored: a sync generator can remove a task after selection.
@@ -64,3 +64,18 @@ export declare function shouldStreamOutput(task: Task, initiatingProject: string
64
64
  export declare function isCacheableTask(task: Task): boolean;
65
65
  export declare function unparse(options: Object): string[];
66
66
  export declare function createTaskId(project: string, target: string, configuration: string | undefined): string;
67
+ /**
68
+ * Distinguishes two invocations of the same task for the recursive invocation
69
+ * detector. The task id alone would report `nx run app:build --flag` invoked
70
+ * from within `app:build` as a loop, so the detector pairs the id with this
71
+ * hash and only treats an exact repeat as an immediate loop.
72
+ *
73
+ * The hash cannot decide recursion on its own. Arguments that accumulate
74
+ * through forwarding (`app:build` -> `app:build hello` -> `app:build hello
75
+ * hello`) hash differently on every hop, so the tracker also bounds how many
76
+ * ancestors may be running one task id.
77
+ *
78
+ * `__overrides_unparsed__` is dropped, matching how the task hasher treats it:
79
+ * it restates the other keys as raw argv, down to flag order and spelling.
80
+ */
81
+ export declare function hashTaskOverrides(task: Task): string;
@@ -17,6 +17,7 @@ exports.getExecutorNameForTask = getExecutorNameForTask;
17
17
  exports.expandInitiatingTasksThroughNoop = expandInitiatingTasksThroughNoop;
18
18
  exports.getExecutorForTask = getExecutorForTask;
19
19
  exports.getCustomHasher = getCustomHasher;
20
+ exports.resetCustomHasherDeprecationWarnings = resetCustomHasherDeprecationWarnings;
20
21
  exports.pruneToSelectedTasks = pruneToSelectedTasks;
21
22
  exports.removeTasksFromTaskGraph = removeTasksFromTaskGraph;
22
23
  exports.calculateReverseDeps = calculateReverseDeps;
@@ -28,37 +29,35 @@ exports.shouldStreamOutput = shouldStreamOutput;
28
29
  exports.isCacheableTask = isCacheableTask;
29
30
  exports.unparse = unparse;
30
31
  exports.createTaskId = createTaskId;
32
+ exports.hashTaskOverrides = hashTaskOverrides;
31
33
  const minimatch_1 = require("minimatch");
32
34
  const node_path_1 = require("node:path");
33
35
  const posix_1 = require("node:path/posix");
34
36
  const executor_utils_1 = require("../command-line/run/executor-utils");
37
+ const file_hasher_1 = require("../hasher/file-hasher");
35
38
  const native_1 = require("../native");
36
39
  const fileutils_1 = require("../utils/fileutils");
37
40
  const find_matching_projects_1 = require("../utils/find-matching-projects");
38
- const legacy_depends_on_warning_1 = require("./legacy-depends-on-warning");
39
41
  const globs_1 = require("../utils/globs");
40
42
  const long_running_target_1 = require("../utils/long-running-target");
43
+ const output_1 = require("../utils/output");
41
44
  const path_1 = require("../utils/path");
42
45
  const serialize_overrides_into_command_line_1 = require("../utils/serialize-overrides-into-command-line");
43
46
  const split_target_1 = require("../utils/split-target");
44
47
  const workspace_root_1 = require("../utils/workspace-root");
45
48
  const is_tui_enabled_1 = require("./is-tui-enabled");
46
49
  function getDependencyConfigs({ project, target }, extraTargetDependencies, projectGraph, allTargetNames) {
47
- const legacyViolations = [];
48
- const dependencyConfigs = (projectGraph.nodes[project].data?.targets[target]?.dependsOn ??
50
+ return (projectGraph.nodes[project].data?.targets[target]?.dependsOn ??
49
51
  // This is passed into `run-command` from programmatic invocations
50
52
  extraTargetDependencies[target] ??
51
- []).flatMap((config, index) => normalizeDependencyConfigDefinition(config, project, projectGraph, allTargetNames, { ownerTarget: target, index, legacyViolations }));
52
- if (legacyViolations.length) {
53
- (0, legacy_depends_on_warning_1.flushLegacyDependsOnViolations)(project, target, legacyViolations, projectGraph.nodes[project]?.data?.root);
54
- }
55
- return dependencyConfigs;
53
+ []).flatMap((config) => normalizeDependencyConfigDefinition(config, project, projectGraph, allTargetNames));
56
54
  }
57
- function normalizeDependencyConfigDefinition(definition, currentProject, graph, allTargetNames, location) {
58
- return expandWildcardTargetConfiguration(normalizeDependencyConfigProjects(expandDependencyConfigSyntaxSugar(definition, graph, currentProject), currentProject, graph, location), allTargetNames);
55
+ function normalizeDependencyConfigDefinition(definition, currentProject, graph, allTargetNames) {
56
+ return expandWildcardTargetConfiguration(normalizeDependencyConfigProjects(expandDependencyConfigSyntaxSugar(definition, graph, currentProject), currentProject, graph), allTargetNames);
59
57
  }
60
- function normalizeDependencyConfigProjects(dependencyConfig, currentProject, graph, location) {
61
- const noStringConfig = normalizeTargetDependencyWithStringProjects(dependencyConfig, currentProject, location);
58
+ function normalizeDependencyConfigProjects(dependencyConfig, currentProject, graph) {
59
+ assertNotLegacyProjectsString(dependencyConfig, currentProject, graph);
60
+ const noStringConfig = normalizeTargetDependencyWithStringProjects(dependencyConfig);
62
61
  if (noStringConfig.projects) {
63
62
  dependencyConfig.projects = (0, find_matching_projects_1.findMatchingProjects)(noStringConfig.projects, graph.nodes);
64
63
  }
@@ -67,6 +66,13 @@ function normalizeDependencyConfigProjects(dependencyConfig, currentProject, gra
67
66
  }
68
67
  return dependencyConfig;
69
68
  }
69
+ function assertNotLegacyProjectsString(dependencyConfig, currentProject, graph) {
70
+ const projects = dependencyConfig.projects;
71
+ if ((projects === 'self' || projects === 'dependencies') &&
72
+ !graph.nodes[projects]) {
73
+ throw new Error(`${currentProject} has a dependsOn entry using projects: '${projects}', which is no longer supported: ${JSON.stringify(dependencyConfig)}\nPlease run "nx repair" to repair your configuration`);
74
+ }
75
+ }
70
76
  function expandDependencyConfigSyntaxSugar(dependencyConfigString, graph, currentProject) {
71
77
  if (typeof dependencyConfigString !== 'string') {
72
78
  return dependencyConfigString;
@@ -133,23 +139,9 @@ function readProjectAndTargetFromTargetString(targetString, projects, currentPro
133
139
  function getOutputs(p, target, overrides) {
134
140
  return getOutputsForTargetAndConfiguration(target, overrides, p[target.project]);
135
141
  }
136
- function normalizeTargetDependencyWithStringProjects(dependencyConfig, currentProject, location) {
142
+ function normalizeTargetDependencyWithStringProjects(dependencyConfig) {
137
143
  if (typeof dependencyConfig.projects === 'string') {
138
- // TODO(v24): Remove the `self` / `dependencies` magic-string shim.
139
- // The v16 `update-depends-on-to-tokens` migration already rewrites
140
- // these to the modern shape, and `nx repair` will re-run it on demand.
141
- if (dependencyConfig.projects === 'self') {
142
- (0, legacy_depends_on_warning_1.warnLegacyDependsOnMagicString)(currentProject, dependencyConfig, location);
143
- delete dependencyConfig.projects;
144
- }
145
- else if (dependencyConfig.projects === 'dependencies') {
146
- (0, legacy_depends_on_warning_1.warnLegacyDependsOnMagicString)(currentProject, dependencyConfig, location);
147
- dependencyConfig.dependencies = true;
148
- delete dependencyConfig.projects;
149
- }
150
- else {
151
- dependencyConfig.projects = [dependencyConfig.projects];
152
- }
144
+ dependencyConfig.projects = [dependencyConfig.projects];
153
145
  }
154
146
  return dependencyConfig;
155
147
  }
@@ -341,9 +333,33 @@ function getExecutorForTask(task, projects) {
341
333
  const [nodeModule, executorName] = (0, executor_utils_1.parseExecutor)(executor);
342
334
  return (0, executor_utils_1.getExecutorInformation)(nodeModule, executorName, workspace_root_1.workspaceRoot, projects);
343
335
  }
336
+ // TODO(v25): remove custom hasher support.
344
337
  function getCustomHasher(task, projects) {
345
338
  const factory = getExecutorForTask(task, projects).hasherFactory;
346
- return factory ? factory() : null;
339
+ if (!factory) {
340
+ return null;
341
+ }
342
+ warnCustomHasherDeprecation(projects[task.target.project].targets[task.target.target].executor);
343
+ return factory();
344
+ }
345
+ const customHasherDeprecationWarned = new Set();
346
+ function warnCustomHasherDeprecation(executor) {
347
+ if (customHasherDeprecationWarned.has(executor)) {
348
+ return;
349
+ }
350
+ customHasherDeprecationWarned.add(executor);
351
+ output_1.output.warn({
352
+ title: `The "${executor}" executor uses a custom hasher, which is deprecated and will be removed in Nx 25.`,
353
+ bodyLines: [
354
+ 'Replace the custom hasher with target inputs, following the migration guide:',
355
+ 'https://nx.dev/docs/kb/local-executors#replace-a-custom-hasher-with-inputs',
356
+ 'If the executor comes from a third-party plugin, report this to its maintainers.',
357
+ ],
358
+ });
359
+ }
360
+ /** Test-only: forget which executors have been warned about. */
361
+ function resetCustomHasherDeprecationWarnings() {
362
+ customHasherDeprecationWarned.clear();
347
363
  }
348
364
  /**
349
365
  * `taskGraph` pruned to `required`, which must already hold its dependencies.
@@ -355,13 +371,29 @@ function pruneToSelectedTasks(taskGraph, required) {
355
371
  }
356
372
  function removeTasksFromTaskGraph(graph, ids) {
357
373
  const newGraph = removeIdsFromTaskGraph(graph, ids, graph.tasks);
374
+ const continuousDependenciesWithoutInputs = pruneEdges(graph.continuousDependenciesWithoutInputs, newGraph.mapWithIds);
358
375
  return {
359
376
  dependencies: newGraph.dependencies,
360
377
  continuousDependencies: newGraph.continuousDependencies,
378
+ ...(continuousDependenciesWithoutInputs
379
+ ? { continuousDependenciesWithoutInputs }
380
+ : {}),
361
381
  roots: newGraph.roots,
362
382
  tasks: newGraph.mapWithIds,
363
383
  };
364
384
  }
385
+ function pruneEdges(edges, kept) {
386
+ let pruned;
387
+ for (const [id, deps] of Object.entries(edges ?? {})) {
388
+ if (!(id in kept))
389
+ continue;
390
+ const keptDeps = deps.filter((dep) => dep in kept);
391
+ if (keptDeps.length > 0) {
392
+ (pruned ??= {})[id] = keptDeps;
393
+ }
394
+ }
395
+ return pruned;
396
+ }
365
397
  function removeIdsFromTaskGraph(graph, ids, mapWithIds) {
366
398
  const filteredMapWithIds = {};
367
399
  const dependencies = {};
@@ -457,3 +489,22 @@ function createTaskId(project, target, configuration) {
457
489
  }
458
490
  return id;
459
491
  }
492
+ /**
493
+ * Distinguishes two invocations of the same task for the recursive invocation
494
+ * detector. The task id alone would report `nx run app:build --flag` invoked
495
+ * from within `app:build` as a loop, so the detector pairs the id with this
496
+ * hash and only treats an exact repeat as an immediate loop.
497
+ *
498
+ * The hash cannot decide recursion on its own. Arguments that accumulate
499
+ * through forwarding (`app:build` -> `app:build hello` -> `app:build hello
500
+ * hello`) hash differently on every hop, so the tracker also bounds how many
501
+ * ancestors may be running one task id.
502
+ *
503
+ * `__overrides_unparsed__` is dropped, matching how the task hasher treats it:
504
+ * it restates the other keys as raw argv, down to flag order and spelling.
505
+ */
506
+ function hashTaskOverrides(task) {
507
+ const overrides = { ...task.overrides };
508
+ delete overrides['__overrides_unparsed__'];
509
+ return (0, file_hasher_1.hashObject)(overrides);
510
+ }
@@ -19,6 +19,7 @@ function customHasherTaskIds(projectGraph, taskGraph) {
19
19
  return memoized;
20
20
  }
21
21
  const projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(projectGraph).projects;
22
+ // TODO(v25): remove custom hasher support.
22
23
  const ids = Object.values(taskGraph.tasks)
23
24
  .filter((task) => {
24
25
  try {
@@ -48,6 +49,9 @@ function getUltracacheSettings(taskGraph) {
48
49
  function ultracacheEligibilityOptions(projectGraph, taskGraph) {
49
50
  return {
50
51
  customHasherTaskIds: customHasherTaskIds(projectGraph, taskGraph),
52
+ continuousTaskIds: Object.values(taskGraph.tasks)
53
+ .filter((task) => task.continuous)
54
+ .map((task) => task.id),
51
55
  };
52
56
  }
53
57
  /**
@@ -25,7 +25,7 @@ export type OwnedRealDir = string & {
25
25
  export type OwnedPrivateDir = string & {
26
26
  readonly [ownedPrivateDirBrand]: true;
27
27
  };
28
- /** Why a guard refused a directory. Data, not a sentence: `remedyFor` decides on it. */
28
+ /** Why a guard refused a directory. Data, not a sentence. */
29
29
  export type DirRefusal = {
30
30
  kind: 'not-created';
31
31
  dir: string;
@@ -65,8 +65,6 @@ export type GuardResult<T> = {
65
65
  };
66
66
  /** The user-facing sentence for a refusal. The only place wording is decided. */
67
67
  export declare function describeRefusal(r: DirRefusal): string;
68
- /** What the user can do about a refusal, or `undefined` when there is nothing. */
69
- export declare function remedyFor(r: DirRefusal): string | undefined;
70
68
  /** One refusal as an `Error`, so several can travel in an `AggregateError`. */
71
69
  export declare class DirectoryRefusedError extends Error {
72
70
  readonly refusal: DirRefusal;
@@ -98,20 +96,6 @@ export declare function canonicalDir(dir: string): string;
98
96
  * verified private directory beneath it.
99
97
  */
100
98
  export declare function isSafeSharedRoot(dir: string): GuardResult<SafeSharedRoot>;
101
- /**
102
- * Whether other users on this machine can write into `dir`. Gates whether a
103
- * refusal message may cite other users, so it must not over-report: `false` on
104
- * Windows, where libuv synthesizes `st_mode` from the READONLY attribute and
105
- * reports an ordinary per-account directory as `0666`, and `false` on a path
106
- * that cannot be inspected.
107
- *
108
- * `statSync`, not `lstatSync`: the question is about the directory that will be
109
- * used, not the link pointing at it. Link modes mislead in both directions —
110
- * Linux creates symlinks `0777`, macOS applies the umask. Nothing here decides
111
- * whether a path is accepted, so following the link costs nothing the guards
112
- * do not already re-check.
113
- */
114
- export declare function isPeerWritable(dir: string): boolean;
115
99
  /**
116
100
  * Create a shared container as sticky + world-writable if it does not exist,
117
101
  * and report whether the resulting path is safe for the current user.
@@ -2,14 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DirectoryRefusedError = void 0;
4
4
  exports.describeRefusal = describeRefusal;
5
- exports.remedyFor = remedyFor;
6
5
  exports.canonicalDir = canonicalDir;
7
6
  exports.isSafeSharedRoot = isSafeSharedRoot;
8
- exports.isPeerWritable = isPeerWritable;
9
7
  exports.ensureSafeSharedRoot = ensureSafeSharedRoot;
10
8
  exports.isOwnedRealDirectory = isOwnedRealDirectory;
11
9
  exports.getUserSegment = getUserSegment;
12
10
  exports.ensureOwnedPrivateDir = ensureOwnedPrivateDir;
11
+ /**
12
+ * Directory guards for Nx's runtime state: create a directory only when it is
13
+ * ours and unreachable by other users on this machine.
14
+ *
15
+ * `native/utils/owned_dir.rs` implements the same rules and must change with
16
+ * this file. This copy exists because the native binding loader has to place
17
+ * and lock down the `.node` before Rust can be called, and `cache-directory.ts`,
18
+ * `git-utils.ts` and `nx-tmp-dir.ts` share it. Node builtins only, for the
19
+ * loader's sake.
20
+ */
13
21
  const node_fs_1 = require("node:fs");
14
22
  const node_os_1 = require("node:os");
15
23
  const node_path_1 = require("node:path");
@@ -56,42 +64,6 @@ function describeRefusal(r) {
56
64
  }
57
65
  }
58
66
  }
59
- /** Single-quoted for a shell, so a path with a space or quote survives a paste. */
60
- const shellQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
61
- /** What the user can do about a refusal, or `undefined` when there is nothing. */
62
- function remedyFor(r) {
63
- switch (r.kind) {
64
- case 'not-a-directory':
65
- return r.symlink
66
- ? `${r.dir} is a symlink where Nx expects a directory. If you did not create it, treat it as hostile: remove the link itself (not what it points at) and run the command again.`
67
- : undefined;
68
- case 'foreign-owner':
69
- return `${r.dir} belongs to another user on this machine, so Nx cannot keep its own directory there. Set NX_SOCKET_DIR to a short directory your user owns, or move it aside — which you can do yourself if you own the directory it sits in, and otherwise needs an administrator.`;
70
- case 'not-tightenable':
71
- // Both producers reach here having already established that the directory
72
- // is ours, so `chmod` is the user's to run. Names no cause: one producer
73
- // is a mount that discards the mode, the other is any `chmod` failure.
74
- return `${r.dir} is reachable by other users (mode ${asMode(r.mode)}) and Nx could not restrict it. Run \`chmod 0700 ${shellQuote(r.dir)}\` and try again; if the mode does not stick, set NX_SOCKET_DIR to a short directory on a filesystem that keeps POSIX permissions.`;
75
- case 'foreign-shared-container':
76
- // Unreachable today: `isSafeSharedRoot` denies with this kind only when
77
- // `stats.uid !== 0`, so a root-owned container never reaches here.
78
- if (r.uid === 0) {
79
- return undefined;
80
- }
81
- const q = shellQuote(r.dir);
82
- return `${r.dir} belongs to another user on this machine, so Nx cannot keep a private directory beneath it. Ask an administrator to hand it to root with \`sudo chown root ${q} && sudo chmod 1777 ${q}\`; every user can then keep their own directory under it.`;
83
- case 'not-created':
84
- case 'not-inspectable':
85
- case 'peer-writable-not-sticky':
86
- return undefined;
87
- default: {
88
- // Matches `describeRefusal`'s arm, so a new kind cannot ship with a
89
- // sentence and no advice — which is how `not-tightenable` went unadvised.
90
- const unhandled = r;
91
- return undefined;
92
- }
93
- }
94
- }
95
67
  /** One refusal as an `Error`, so several can travel in an `AggregateError`. */
96
68
  class DirectoryRefusedError extends Error {
97
69
  constructor(refusal) {
@@ -207,30 +179,6 @@ function isSafeSharedRoot(dir) {
207
179
  return deny({ kind: 'not-inspectable', dir, code: e?.code });
208
180
  }
209
181
  }
210
- /**
211
- * Whether other users on this machine can write into `dir`. Gates whether a
212
- * refusal message may cite other users, so it must not over-report: `false` on
213
- * Windows, where libuv synthesizes `st_mode` from the READONLY attribute and
214
- * reports an ordinary per-account directory as `0666`, and `false` on a path
215
- * that cannot be inspected.
216
- *
217
- * `statSync`, not `lstatSync`: the question is about the directory that will be
218
- * used, not the link pointing at it. Link modes mislead in both directions —
219
- * Linux creates symlinks `0777`, macOS applies the umask. Nothing here decides
220
- * whether a path is accepted, so following the link costs nothing the guards
221
- * do not already re-check.
222
- */
223
- function isPeerWritable(dir) {
224
- if (process.platform === 'win32') {
225
- return false;
226
- }
227
- try {
228
- return !!((0, node_fs_1.statSync)(dir).mode & 0o022);
229
- }
230
- catch {
231
- return false;
232
- }
233
- }
234
182
  /**
235
183
  * Create a shared container as sticky + world-writable if it does not exist,
236
184
  * and report whether the resulting path is safe for the current user.
@@ -1,3 +1,4 @@
1
+ // tsc doesn't emit hand-written .d.ts files; assets.json copies this one into dist.
1
2
  import type { EventParameters } from '../analytics/analytics';
2
3
 
3
4
  export interface TrackedDetail extends EventParameters {
@@ -80,7 +80,7 @@ exports.CORE_PLUGINS = [
80
80
  },
81
81
  {
82
82
  name: '@nx/remix',
83
- capabilities: 'executors,generators',
83
+ capabilities: 'generators',
84
84
  },
85
85
  {
86
86
  name: '@nx/rollup',
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Computes the Levenshtein edit distance between two strings, i.e. the minimum
3
+ * number of single-character insertions, deletions, or substitutions required
4
+ * to turn `a` into `b`.
5
+ */
6
+ export declare function levenshteinDistance(a: string, b: string): number;
7
+ /**
8
+ * Ranks every candidate by how closely it resembles `input`, from closest to
9
+ * furthest, tie-broken alphabetically so the order is deterministic. Each entry
10
+ * keeps its computed distance so callers can reuse it (e.g. to both pick the
11
+ * closest match and order a list) without recomputing.
12
+ */
13
+ export declare function rankByDistance(input: string, candidates: readonly string[]): {
14
+ candidate: string;
15
+ distance: number;
16
+ }[];
17
+ /**
18
+ * Whether a candidate at edit `distance` from `input` is close enough to be a
19
+ * useful suggestion. Exposed so callers that already have a distance (e.g. from
20
+ * `rankByDistance`) can gate on it without recomputing.
21
+ */
22
+ export declare function isWithinSuggestionThreshold(input: string, candidate: string, distance: number): boolean;
23
+ /**
24
+ * Ranks the candidates that are similar enough to `input` to be worth
25
+ * suggesting, closest first (ties broken alphabetically). Each entry keeps its
26
+ * distance so callers can compare suggestions found for different forms of the
27
+ * same input without recomputing.
28
+ */
29
+ export declare function rankSuggestions(input: string, candidates: readonly string[]): {
30
+ candidate: string;
31
+ distance: number;
32
+ }[];
33
+ /**
34
+ * Returns up to `limit` candidates that most closely resemble `input`, sorted
35
+ * from closest to furthest. Candidates that are not similar enough to be a
36
+ * useful suggestion are excluded.
37
+ */
38
+ export declare function findClosestMatches(input: string, candidates: readonly string[], limit?: number): string[];