nx 23.3.0-beta.4 → 23.3.0-beta.6

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 (113) hide show
  1. package/dist/nx-cloud-internals.d.ts +1 -0
  2. package/dist/nx-cloud-internals.js +9 -0
  3. package/dist/plugins/package-json.js +1 -1
  4. package/dist/src/command-line/affected/affected.js +51 -9
  5. package/dist/src/command-line/graph/graph.d.ts +31 -0
  6. package/dist/src/command-line/graph/graph.js +71 -17
  7. package/dist/src/command-line/release/publish.js +7 -11
  8. package/dist/src/command-line/release/utils/get-touched-projects-for-group.js +3 -3
  9. package/dist/src/command-line/run/run-one.js +5 -1
  10. package/dist/src/command-line/run-many/run-many.js +6 -12
  11. package/dist/src/command-line/show/projects.js +30 -1
  12. package/dist/src/config/task-graph.d.ts +1 -1
  13. package/dist/src/config/workspace-json-project-json.d.ts +21 -1
  14. package/dist/src/core/graph/main.js +1 -1
  15. package/dist/src/core/graph/runtime.js +1 -1
  16. package/dist/src/core/graph/styles.js +1 -1
  17. package/dist/src/daemon/client/client.d.ts +7 -2
  18. package/dist/src/daemon/client/client.js +15 -2
  19. package/dist/src/daemon/message-types/io-snapshot-version.d.ts +5 -0
  20. package/dist/src/daemon/message-types/io-snapshot-version.js +2 -0
  21. package/dist/src/daemon/message-types/select-affected-tasks.d.ts +18 -0
  22. package/dist/src/daemon/message-types/select-affected-tasks.js +11 -0
  23. package/dist/src/daemon/server/handle-hash-tasks.d.ts +2 -0
  24. package/dist/src/daemon/server/handle-hash-tasks.js +5 -3
  25. package/dist/src/daemon/server/handle-select-affected-tasks.d.ts +3 -0
  26. package/dist/src/daemon/server/handle-select-affected-tasks.js +23 -0
  27. package/dist/src/daemon/server/io-snapshots-state.d.ts +7 -0
  28. package/dist/src/daemon/server/io-snapshots-state.js +35 -0
  29. package/dist/src/daemon/server/planning-context.d.ts +8 -0
  30. package/dist/src/daemon/server/planning-context.js +18 -0
  31. package/dist/src/daemon/server/server.d.ts +1 -0
  32. package/dist/src/daemon/server/server.js +6 -0
  33. package/dist/src/executors/run-commands/run-commands.impl.d.ts +2 -1
  34. package/dist/src/executors/run-commands/run-commands.impl.js +19 -5
  35. package/dist/src/executors/run-commands/running-tasks.d.ts +5 -4
  36. package/dist/src/executors/run-commands/running-tasks.js +13 -13
  37. package/dist/src/hasher/create-task-hasher.d.ts +8 -1
  38. package/dist/src/hasher/create-task-hasher.js +13 -3
  39. package/dist/src/hasher/hash-plan-inspector.js +2 -2
  40. package/dist/src/hasher/hash-task.d.ts +2 -2
  41. package/dist/src/hasher/hash-task.js +7 -1
  42. package/dist/src/hasher/native-task-hasher-impl.d.ts +13 -5
  43. package/dist/src/hasher/native-task-hasher-impl.js +38 -15
  44. package/dist/src/hasher/task-hasher.d.ts +17 -9
  45. package/dist/src/hasher/task-hasher.js +16 -11
  46. package/dist/src/hasher/task-planning-context.d.ts +13 -0
  47. package/dist/src/hasher/task-planning-context.js +12 -0
  48. package/dist/src/io-snapshots/config.d.ts +23 -0
  49. package/dist/src/io-snapshots/config.js +32 -0
  50. package/dist/src/io-snapshots/fetch.d.ts +21 -0
  51. package/dist/src/io-snapshots/fetch.js +43 -0
  52. package/dist/src/io-snapshots/outputs.d.ts +12 -0
  53. package/dist/src/io-snapshots/outputs.js +33 -0
  54. package/dist/src/io-snapshots/overrides.d.ts +12 -0
  55. package/dist/src/io-snapshots/overrides.js +49 -0
  56. package/dist/src/io-snapshots/report.d.ts +13 -0
  57. package/dist/src/io-snapshots/report.js +68 -0
  58. package/dist/src/io-snapshots/store.d.ts +37 -0
  59. package/dist/src/io-snapshots/store.js +162 -0
  60. package/dist/src/native/index.d.ts +264 -2
  61. package/dist/src/native/native-bindings.js +10 -0
  62. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  63. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  64. package/dist/src/native/transform-objects.d.ts +11 -2
  65. package/dist/src/native/transform-objects.js +46 -0
  66. package/dist/src/nx-cloud/update-manager.d.ts +3 -0
  67. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.d.ts +10 -2
  68. package/dist/src/plugins/js/project-graph/affected/lock-file-changes.js +51 -29
  69. package/dist/src/plugins/js/project-graph/affected/npm-packages.d.ts +10 -2
  70. package/dist/src/plugins/js/project-graph/affected/npm-packages.js +32 -11
  71. package/dist/src/plugins/package-json/create-nodes.d.ts +2 -2
  72. package/dist/src/plugins/package-json/create-nodes.js +13 -13
  73. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +14 -0
  74. package/dist/src/project-graph/affected/affected-project-graph.js +5 -18
  75. package/dist/src/project-graph/affected/affected-projects.d.ts +7 -0
  76. package/dist/src/project-graph/affected/affected-projects.js +42 -0
  77. package/dist/src/project-graph/affected/affected-tasks.d.ts +90 -0
  78. package/dist/src/project-graph/affected/affected-tasks.js +214 -0
  79. package/dist/src/project-graph/file-utils.js +6 -2
  80. package/dist/src/project-graph/utils/project-configuration/target-merging.js +67 -0
  81. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +83 -0
  82. package/dist/src/project-graph/utils/project-configuration/utils.d.ts +1 -0
  83. package/dist/src/project-graph/utils/project-configuration/utils.js +1 -0
  84. package/dist/src/tasks-runner/create-task-graph.d.ts +24 -1
  85. package/dist/src/tasks-runner/create-task-graph.js +56 -2
  86. package/dist/src/tasks-runner/forked-process-task-runner.js +3 -3
  87. package/dist/src/tasks-runner/init-tasks-runner.d.ts +7 -2
  88. package/dist/src/tasks-runner/init-tasks-runner.js +17 -6
  89. package/dist/src/tasks-runner/run-command.d.ts +41 -5
  90. package/dist/src/tasks-runner/run-command.js +84 -18
  91. package/dist/src/tasks-runner/task-io-service.d.ts +11 -3
  92. package/dist/src/tasks-runner/task-io-service.js +14 -5
  93. package/dist/src/tasks-runner/task-orchestrator.js +1 -1
  94. package/dist/src/tasks-runner/utils.d.ts +5 -0
  95. package/dist/src/tasks-runner/utils.js +9 -0
  96. package/dist/src/utils/db-connection.d.ts +1 -0
  97. package/dist/src/utils/db-connection.js +1 -0
  98. package/dist/src/utils/json-diff.d.ts +4 -2
  99. package/dist/src/utils/json-diff.js +6 -94
  100. package/dist/src/utils/nx-cloud-utils.d.ts +2 -0
  101. package/dist/src/utils/nx-cloud-utils.js +6 -1
  102. package/dist/src/utils/project-graph-utils.d.ts +2 -0
  103. package/dist/src/utils/project-graph-utils.js +12 -0
  104. package/package.json +22 -13
  105. package/readme-template.md +16 -0
  106. package/schemas/nx-schema.json +34 -0
  107. package/schemas/project-schema.json +31 -0
  108. package/dist/src/project-graph/affected/locators/project-glob-changes.d.ts +0 -2
  109. package/dist/src/project-graph/affected/locators/project-glob-changes.js +0 -40
  110. package/dist/src/project-graph/affected/locators/workspace-json-changes.d.ts +0 -4
  111. package/dist/src/project-graph/affected/locators/workspace-json-changes.js +0 -47
  112. package/dist/src/project-graph/affected/locators/workspace-projects.d.ts +0 -3
  113. package/dist/src/project-graph/affected/locators/workspace-projects.js +0 -74
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectsAffectedTasks = selectsAffectedTasks;
4
+ exports.computeAffectedTasks = computeAffectedTasks;
5
+ exports.selectAffectedTasks = selectAffectedTasks;
6
+ const native_1 = require("../../native");
7
+ const outputs_1 = require("../../io-snapshots/outputs");
8
+ const overrides_1 = require("../../io-snapshots/overrides");
9
+ const store_1 = require("../../io-snapshots/store");
10
+ const create_task_graph_1 = require("../../tasks-runner/create-task-graph");
11
+ const project_graph_utils_1 = require("../../utils/project-graph-utils");
12
+ const utils_1 = require("../../tasks-runner/utils");
13
+ const project_graph_1 = require("../project-graph");
14
+ const file_utils_1 = require("../file-utils");
15
+ const find_matching_projects_1 = require("../../utils/find-matching-projects");
16
+ const logger_1 = require("../../utils/logger");
17
+ const error_types_1 = require("../error-types");
18
+ const workspace_root_1 = require("../../utils/workspace-root");
19
+ const task_planning_context_1 = require("../../hasher/task-planning-context");
20
+ const client_1 = require("../../daemon/client/client");
21
+ const is_on_daemon_1 = require("../../daemon/is-on-daemon");
22
+ const affected_projects_1 = require("./affected-projects");
23
+ const lock_file_changes_1 = require("../../plugins/js/project-graph/affected/lock-file-changes");
24
+ const npm_packages_1 = require("../../plugins/js/project-graph/affected/npm-packages");
25
+ /**
26
+ * Whether `nx affected` selects the individual tasks whose inputs a change
27
+ * reaches, rather than whole projects. Off by default while the task path
28
+ * settles.
29
+ *
30
+ * Env-var only, so `nx show projects --affected` and `nx affected` in one CI
31
+ * script agree. The run also executes the selected tasks' dependencies.
32
+ */
33
+ function selectsAffectedTasks() {
34
+ return process.env.NX_LEGACY_AFFECTED === 'false';
35
+ }
36
+ /**
37
+ * Selects the tasks a change reaches, rather than the projects that own a
38
+ * changed file.
39
+ *
40
+ * With the daemon on, the daemon selects: it hashes the run's tasks, so its
41
+ * planner is the one that remembers the plans. It returns the graph it selected
42
+ * against, which the command must run with.
43
+ */
44
+ async function computeAffectedTasks(opts) {
45
+ const request = {
46
+ targets: opts.targets,
47
+ changedFiles: opts.touchedFiles.map((f) => f.file),
48
+ fileChangeArgs: opts.fileChangeArgs,
49
+ configuration: opts.configuration,
50
+ overrides: opts.overrides ?? {},
51
+ extraTargetDependencies: opts.extraTargetDependencies ?? {},
52
+ excludeTaskDependencies: opts.excludeTaskDependencies ?? false,
53
+ exclude: opts.exclude ?? [],
54
+ selectivelyHashTsConfig: opts.selectivelyHashTsConfig,
55
+ };
56
+ const ioSnapshots = (0, store_1.snapshotsOf)(opts.ioSnapshotOutcome ?? null);
57
+ if (ioSnapshots) {
58
+ request.ioSnapshots = {
59
+ commit: ioSnapshots.commit,
60
+ fetchedAt: ioSnapshots.resolution.fetchedAt,
61
+ };
62
+ }
63
+ if (!(0, is_on_daemon_1.isOnDaemon)() && client_1.daemonClient.enabled()) {
64
+ try {
65
+ const selection = await client_1.daemonClient.selectAffectedTasks(request);
66
+ return {
67
+ ...selection,
68
+ affectedTaskIds: new Set(selection.affectedTaskIds),
69
+ taskSelection: {
70
+ ...selection.taskSelection,
71
+ ioSnapshotOutcome: opts.ioSnapshotOutcome,
72
+ },
73
+ };
74
+ }
75
+ catch (e) {
76
+ if (e?.name === error_types_1.DaemonProjectGraphError.name) {
77
+ throw error_types_1.ProjectGraphError.fromDaemonProjectGraphError(e);
78
+ }
79
+ // Not rethrown: the graph fetch handles an unreachable daemon, and a
80
+ // selection error recurs in-process below.
81
+ logger_1.logger.verbose(`Selecting affected tasks in the daemon failed: ${e}`);
82
+ }
83
+ }
84
+ const projectGraph = opts.projectGraph ?? (await (0, project_graph_1.createProjectGraphAsync)({ exitOnError: true }));
85
+ const planningContext = (0, task_planning_context_1.createTaskPlanningContext)(projectGraph, opts.nxJson);
86
+ const selection = await selectAffectedTasks(projectGraph, opts.nxJson, planningContext, request, {
87
+ touchedFiles: opts.touchedFiles,
88
+ packageJson: opts.packageJson,
89
+ ioSnapshots,
90
+ });
91
+ return {
92
+ projectGraph,
93
+ affectedTaskIds: selection.affectedTaskIds,
94
+ taskGraph: selection.taskGraph,
95
+ taskSelection: {
96
+ ...selection.taskSelection,
97
+ // The planner remembers what selection planned, so the run's hashing reuses it.
98
+ planningContext,
99
+ ioSnapshotOutcome: opts.ioSnapshotOutcome,
100
+ },
101
+ };
102
+ }
103
+ /**
104
+ * Plans the targets' full task graph and matches the changed paths against
105
+ * every plan. No project-level prefilter: under an I/O snapshot a task can read
106
+ * files no affected project owns. Shared by the client and the daemon.
107
+ */
108
+ async function selectAffectedTasks(projectGraph, nxJson, planningContext, request, { touchedFiles = (0, file_utils_1.calculateFileChanges)(request.changedFiles, request.fileChangeArgs), packageJson, ioSnapshots, } = {}) {
109
+ const { targets } = request;
110
+ // Only projects that have one of the targets: with a single target,
111
+ // createTaskGraph tries to create a task for projects that lack it and
112
+ // createTask throws.
113
+ const candidates = [...(0, project_graph_utils_1.runnableForTarget)(projectGraph.nodes, targets)];
114
+ if (!candidates.length) {
115
+ const empty = {
116
+ roots: [],
117
+ tasks: {},
118
+ dependencies: {},
119
+ continuousDependencies: {},
120
+ };
121
+ return {
122
+ affectedTaskIds: new Set(),
123
+ taskGraph: empty,
124
+ taskSelection: { taskGraph: empty, initiatingTaskIds: [], taskIds: [] },
125
+ };
126
+ }
127
+ const { taskGraph, dependencyOverrides } = (0, create_task_graph_1.createTaskGraphWithDependencyOverrides)(projectGraph, request.extraTargetDependencies, candidates, targets, request.configuration, request.overrides,
128
+ // Dependencies carry a change even when they won't run; `keep` drops them.
129
+ false);
130
+ const taskIds = Object.keys(taskGraph.tasks);
131
+ // As the run hashes: observed outputs carry output reads, observed inputs match changes.
132
+ if (ioSnapshots) {
133
+ (0, outputs_1.applyIoSnapshotOutputs)(projectGraph, taskGraph, ioSnapshots);
134
+ }
135
+ const plans = ioSnapshots
136
+ ? planningContext.planner.getPlansReference(taskIds, taskGraph, ioSnapshots, (0, overrides_1.ioSnapshotEligibilityOptions)(projectGraph, taskGraph))
137
+ : planningContext.planner.getPlansReference(taskIds, taskGraph);
138
+ const dependencies = dependencyChanges(projectGraph, touchedFiles, nxJson, packageJson);
139
+ const namedProjects = new Set(dependencies.projects);
140
+ const projects = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(projectGraph).projects;
141
+ const selection = (0, native_1.affectedTasks)(planningContext.projectGraphRef, plans, taskGraph, request.changedFiles, {
142
+ projectGlobPatterns: await (0, affected_projects_1.getProjectGlobPatterns)(nxJson),
143
+ workspaceRoot: workspace_root_1.workspaceRoot,
144
+ alwaysTouchedTaskIds: taskIds.filter((id) => namedProjects.has(taskGraph.tasks[id].target.project) ||
145
+ hasCustomHasher(taskGraph.tasks[id], projects)),
146
+ changedExternals: dependencies.externals,
147
+ changedExternalTypes: dependencies.changedExternalTypes,
148
+ excludedProjects: request.exclude.length
149
+ ? (0, find_matching_projects_1.findMatchingProjects)(request.exclude, projectGraph.nodes)
150
+ : [],
151
+ targets,
152
+ revisions: fileRevisions(request.fileChangeArgs),
153
+ selectivelyHashTsConfig: request.selectivelyHashTsConfig ?? false,
154
+ });
155
+ // The run builds from the owning projects, so only their requested tasks
156
+ // take the CLI overrides there; every other task is a dependency.
157
+ const owning = new Set(selection.affected.map((id) => taskGraph.tasks[id].target.project));
158
+ const initial = new Set(taskIds.filter((id) => {
159
+ const { project, target } = taskGraph.tasks[id].target;
160
+ return owning.has(project) && targets.includes(target);
161
+ }));
162
+ // Without dependencies the run builds its initial tasks and nothing else.
163
+ const keep = request.excludeTaskDependencies
164
+ ? selection.required.filter((id) => initial.has(id))
165
+ : selection.required;
166
+ return {
167
+ affectedTaskIds: new Set(selection.affected),
168
+ taskGraph,
169
+ taskSelection: {
170
+ // Edges that disagree on a dependency's overrides leave it to a build
171
+ // from the owning projects, which settles them the way the run always has.
172
+ taskGraph: (0, create_task_graph_1.filterTaskGraphToSelection)(projectGraph, taskGraph, dependencyOverrides, initial, new Set(keep)) ??
173
+ (0, utils_1.pruneToSelectedTasks)((0, create_task_graph_1.createTaskGraph)(projectGraph, request.extraTargetDependencies, [...owning], targets, request.configuration, request.overrides, request.excludeTaskDependencies), keep),
174
+ initiatingTaskIds: keep.filter((id) => initial.has(id)),
175
+ taskIds: keep,
176
+ },
177
+ };
178
+ }
179
+ /** Unset for `--files`, which names files without a diff to compare. */
180
+ function fileRevisions(args) {
181
+ return args?.base && !args.files?.length
182
+ ? { base: args.base, head: args.head }
183
+ : undefined;
184
+ }
185
+ /**
186
+ * A custom hasher hashes outside the task's plan, so no instruction says what
187
+ * reaches it and the task is always selected. An executor that cannot be
188
+ * resolved cannot run either, so it is left to the plan.
189
+ */
190
+ function hasCustomHasher(task, projects) {
191
+ try {
192
+ return !!(0, utils_1.getExecutorForTask)(task, projects).hasherFactory;
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
198
+ /**
199
+ * A lockfile or package.json change reaches a hash as `External(name)`, a
200
+ * package rather than a path, so it is handed to the plans as package names.
201
+ */
202
+ function dependencyChanges(projectGraph, touchedFiles, nxJson, packageJson = (0, file_utils_1.readPackageJson)()) {
203
+ const changes = [
204
+ (0, lock_file_changes_1.lockFileDependencyChanges)(touchedFiles, projectGraph.nodes, nxJson, packageJson, projectGraph),
205
+ (0, npm_packages_1.packageJsonDependencyChanges)(touchedFiles, nxJson, projectGraph),
206
+ ];
207
+ return {
208
+ externals: [...new Set(changes.flatMap((c) => c.externals))],
209
+ changedExternalTypes: [
210
+ ...new Set(changes.flatMap((c) => c.changedExternalTypes)),
211
+ ],
212
+ projects: [...new Set(changes.flatMap((c) => c.projects))],
213
+ };
214
+ }
@@ -15,6 +15,7 @@ const path_1 = require("path");
15
15
  const fileutils_1 = require("../utils/fileutils");
16
16
  const git_revision_1 = require("../utils/git-revision");
17
17
  const ignore_1 = require("../utils/ignore");
18
+ const native_1 = require("../native");
18
19
  const json_diff_1 = require("../utils/json-diff");
19
20
  const workspace_root_1 = require("../utils/workspace-root");
20
21
  class WholeFileChange {
@@ -90,7 +91,10 @@ function calculateFileChanges(files, nxArgs, readFileAtRevision = defaultReadFil
90
91
  try {
91
92
  const atBase = readFileAtRevision(f, nxArgs.base);
92
93
  const atHead = readFileAtRevision(f, nxArgs.head);
93
- return (0, json_diff_1.jsonDiff)(JSON.parse(atBase), JSON.parse(atHead));
94
+ // Unset when either side is not strict JSON.
95
+ return ((0, native_1.diffJson)(atBase, atHead) ?? [
96
+ new WholeFileChange(),
97
+ ]);
94
98
  }
95
99
  catch (e) {
96
100
  return [new WholeFileChange()];
@@ -128,7 +132,7 @@ function defaultReadFileAtRevision(file, revision) {
128
132
  try {
129
133
  const filePathInGitRepository = getFilePathInGitRepository(file);
130
134
  return !revision
131
- ? (0, fs_1.readFileSync)(file, 'utf-8')
135
+ ? (0, fs_1.readFileSync)((0, path_1.join)(workspace_root_1.workspaceRoot, file), 'utf-8')
132
136
  : (0, child_process_1.execFileSync)('git', ['show', `${revision}:${filePathInGitRepository}`], {
133
137
  maxBuffer: exports.TEN_MEGABYTES,
134
138
  stdio: ['pipe', 'pipe', 'ignore'],
@@ -139,6 +139,60 @@ function mergeConfigurationValue(newConfig, baseConfig, projectConfigSourceMap,
139
139
  }
140
140
  return merged;
141
141
  }
142
+ // `ultracache` keeps replace-unless-`'...'` semantics at the object level, but its
143
+ // values still have to be merged per key: neither the wholesale replace nor
144
+ // `mergeObjectWithSpread` recurses, so an `ignoredReads: ['...', 'tmp/**']`
145
+ // would otherwise reach the task graph with `'...'` intact — a glob matching
146
+ // nothing, silently dropping the inherited patterns.
147
+ function mergeUltracache(newUltracache, baseUltracache, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase) {
148
+ if (newUltracache === undefined) {
149
+ return baseUltracache;
150
+ }
151
+ const sourceMapContext = projectConfigSourceMap
152
+ ? {
153
+ sourceMap: projectConfigSourceMap,
154
+ key: `${targetIdentifier}.ultracache`,
155
+ sourceInformation,
156
+ }
157
+ : undefined;
158
+ // Object level first: this settles which keys survive, their order, and
159
+ // their source-map attribution.
160
+ const merged = (0, utils_1.getMergeValueResult)(baseUltracache, newUltracache, sourceMapContext, deferSpreadsWithoutBase);
161
+ if (!merged || typeof merged !== 'object' || Array.isArray(merged)) {
162
+ return merged;
163
+ }
164
+ // Copy before resolving: with no object-level spread `getMergeValueResult`
165
+ // returns `newUltracache` itself, and writing through it would edit the
166
+ // caller's target in place, corrupting later merge layers.
167
+ const resolved = { ...merged };
168
+ // Keys authored before `'...'` let the base win, exactly as they do at the
169
+ // target level. Re-resolving those would overwrite the object-level merge's
170
+ // decision and make authored position meaningless for array keys while
171
+ // still honouring it for scalar ones.
172
+ const authoredKeys = Object.keys(newUltracache);
173
+ const spreadPosition = authoredKeys.indexOf(utils_1.NX_SPREAD_TOKEN);
174
+ const keysBeforeSpread = spreadPosition >= 0
175
+ ? new Set(authoredKeys.slice(0, spreadPosition))
176
+ : new Set();
177
+ // Then re-resolve any key the target authored as an array, so a nested
178
+ // `'...'` expands against the inherited value rather than surviving as a
179
+ // literal element.
180
+ for (const key of Object.keys(resolved)) {
181
+ if (!Array.isArray(newUltracache[key]))
182
+ continue;
183
+ if (keysBeforeSpread.has(key) && baseUltracache && key in baseUltracache) {
184
+ continue;
185
+ }
186
+ resolved[key] = (0, utils_1.getMergeValueResult)(baseUltracache?.[key], newUltracache[key], projectConfigSourceMap
187
+ ? {
188
+ sourceMap: projectConfigSourceMap,
189
+ key: `${targetIdentifier}.ultracache.${key}`,
190
+ sourceInformation,
191
+ }
192
+ : undefined, deferSpreadsWithoutBase);
193
+ }
194
+ return resolved;
195
+ }
142
196
  function mergeConfigurations(newConfigurations, baseConfigurations, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase) {
143
197
  const mergedConfigurations = {};
144
198
  // Keys before '...' let base win for shared names; keys after '...'
@@ -305,6 +359,7 @@ function mergeTargetConfigurations(target, baseTarget, projectConfigSourceMap, s
305
359
  const skipForOwnMerge = new Set([
306
360
  'options',
307
361
  'configurations',
362
+ 'ultracache',
308
363
  utils_1.NX_SPREAD_TOKEN,
309
364
  ]);
310
365
  const processKey = (key) => {
@@ -428,6 +483,18 @@ function mergeTargetConfigurations(target, baseTarget, projectConfigSourceMap, s
428
483
  sourceInformation;
429
484
  }
430
485
  }
486
+ // merge ultracache if either side declares one
487
+ // as with options, an incompatible target discards the base.
488
+ // `ultracache` is in `skipForOwnMerge` so the key-by-key pass leaves it to
489
+ // this block; removing it from that set would change only the key's position
490
+ // in the result, since this overwrites whatever the generic pass produced.
491
+ if ('ultracache' in target ||
492
+ (isCompatible && baseTarget && 'ultracache' in baseTarget)) {
493
+ const mergedUltracache = mergeUltracache(target.ultracache, isCompatible ? baseTarget?.ultracache : undefined, projectConfigSourceMap, sourceInformation, targetIdentifier, deferSpreadsWithoutBase);
494
+ if (mergedUltracache !== undefined) {
495
+ result.ultracache = mergedUltracache;
496
+ }
497
+ }
431
498
  if (target.metadata) {
432
499
  result.metadata = mergeMetadata(projectConfigSourceMap, sourceInformation, `${targetIdentifier}.metadata`, target.metadata, baseTarget?.metadata);
433
500
  }
@@ -10,6 +10,7 @@ const output_1 = require("../../../utils/output");
10
10
  const to_project_name_1 = require("../../../config/to-project-name");
11
11
  const error_types_1 = require("../../error-types");
12
12
  const target_merging_1 = require("./target-merging");
13
+ const utils_1 = require("./utils");
13
14
  const node_fs_1 = require("node:fs");
14
15
  const git_worktrees_1 = require("../../../utils/git-worktrees");
15
16
  const path_1 = require("path");
@@ -176,6 +177,87 @@ function warnAboutLegacyCachedTargets(legacyCacheReads) {
176
177
  bodyLines,
177
178
  });
178
179
  }
180
+ /**
181
+ * Nothing downstream rejects a key it does not know — the Rust hasher drops it,
182
+ * the Cloud runner reads only the keys it reads, and the Kotlin API decodes with
183
+ * `ignoreUnknownKeys`. So a typo is silent everywhere else, and this is the only
184
+ * place a misspelled option can be reported at all. `'...'` is listed because a
185
+ * spread with no base to resolve against survives merging.
186
+ */
187
+ const KNOWN_ULTRACACHE_KEYS = new Set([
188
+ 'mode',
189
+ 'ignoredReads',
190
+ 'ignoredWrites',
191
+ utils_1.NX_SPREAD_TOKEN,
192
+ ]);
193
+ const ULTRACACHE_MODES = ['on', 'warn', 'error', 'off'];
194
+ function describeUltracacheValue(value) {
195
+ if (Array.isArray(value))
196
+ return 'an array';
197
+ if (value === null)
198
+ return 'null';
199
+ return `a ${typeof value}`;
200
+ }
201
+ /**
202
+ * Describes every way an `ultracache` violates the shape the schema forbids.
203
+ *
204
+ * The schema is editor-only, and everything downstream — the Rust task hasher,
205
+ * the cloud runner's Go and Kotlin deserializers — is strict. A bad value that
206
+ * gets this far is reported far from its source, or silently drops the task's
207
+ * tracking, so it is worth reporting here where the project, target and file
208
+ * are all still in hand.
209
+ *
210
+ * Returns messages rather than throwing: only a WorkspaceValidityError is
211
+ * collected by `validateAndNormalizeProjectRootMap`, and anything else escapes
212
+ * as far as the daemon, which exits on an error it cannot classify.
213
+ */
214
+ function validateTargetUltracache(ultracache, projectName, projectRoot, targetName, sourceMaps) {
215
+ if (ultracache === undefined) {
216
+ return [];
217
+ }
218
+ const targetSourceMaps = sourceMaps?.[projectRoot];
219
+ const [file, plugin] = targetSourceMaps?.[`targets.${targetName}.ultracache`] ??
220
+ targetSourceMaps?.[`targets.${targetName}`] ??
221
+ [];
222
+ const origin = file
223
+ ? ` (defined in ${file})`
224
+ : plugin
225
+ ? ` (defined by ${plugin})`
226
+ : '';
227
+ const where = `"${targetName}" in project "${projectName}"${origin}`;
228
+ if (!(0, utils_1.isObject)(ultracache)) {
229
+ return [
230
+ `The "ultracache" configuration for target ${where} must be an object, but it is ${describeUltracacheValue(ultracache)}.`,
231
+ ];
232
+ }
233
+ const errors = [];
234
+ for (const key of Object.keys(ultracache)) {
235
+ 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
+ }
238
+ }
239
+ if (ultracache.mode !== undefined &&
240
+ !ULTRACACHE_MODES.includes(ultracache.mode)) {
241
+ const supported = ULTRACACHE_MODES.map((mode) => `"${mode}"`).join(', ');
242
+ errors.push(`"ultracache.mode" for target ${where} must be one of ${supported}, but it is ${typeof ultracache.mode === 'string'
243
+ ? `"${ultracache.mode}"`
244
+ : describeUltracacheValue(ultracache.mode)}.`);
245
+ }
246
+ for (const key of ['ignoredReads', 'ignoredWrites']) {
247
+ const value = ultracache[key];
248
+ if (value === undefined)
249
+ continue;
250
+ if (!Array.isArray(value)) {
251
+ errors.push(`"ultracache.${key}" for target ${where} must be an array of glob patterns, but it is ${describeUltracacheValue(value)}.`);
252
+ continue;
253
+ }
254
+ const badIndex = value.findIndex((glob) => typeof glob !== 'string');
255
+ if (badIndex !== -1) {
256
+ errors.push(`"ultracache.${key}[${badIndex}]" for target ${where} must be a glob pattern string, but it is ${describeUltracacheValue(value[badIndex])}.`);
257
+ }
258
+ }
259
+ return errors;
260
+ }
179
261
  function normalizeTargets(project, sourceMaps, nxJsonConfiguration, workspaceRoot,
180
262
  /**
181
263
  * Project configurations keyed by project name
@@ -190,6 +272,7 @@ legacyCacheReads) {
190
272
  for (const targetName in project.targets) {
191
273
  project.targets[targetName] = normalizeTarget(project.targets[targetName], project, workspaceRoot, projects, [project.root, targetName].join(':'));
192
274
  const target = project.targets[targetName];
275
+ targetErrorMessage.push(...validateTargetUltracache(target.ultracache, project.name ?? project.root, project.root, targetName, sourceMaps).map((message) => `- ${message}`));
193
276
  const targetDefaults = nxJsonConfiguration.targetDefaults;
194
277
  if (isLegacyCachedTarget(targetName, targetDefaults, target)) {
195
278
  target.cache = true;
@@ -35,4 +35,5 @@ type SourceMapContext = {
35
35
  * preserved so a later merge layer can expand it.
36
36
  */
37
37
  export declare function getMergeValueResult<T>(baseValue: unknown, newValue: T | undefined, sourceMapContext?: SourceMapContext, deferSpreadsWithoutBase?: boolean): T | undefined;
38
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
38
39
  export {};
@@ -4,6 +4,7 @@ exports.IntegerLikeSpreadKeyError = exports.INTEGER_LIKE_KEY_PATTERN = exports.N
4
4
  exports.uniqueKeysInObjects = uniqueKeysInObjects;
5
5
  exports.assertNoIntegerLikeSpreadKey = assertNoIntegerLikeSpreadKey;
6
6
  exports.getMergeValueResult = getMergeValueResult;
7
+ exports.isObject = isObject;
7
8
  const source_maps_1 = require("./source-maps");
8
9
  exports.NX_SPREAD_TOKEN = '...';
9
10
  /**
@@ -1,9 +1,20 @@
1
1
  import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
2
2
  import { Task, TaskGraph } from '../config/task-graph';
3
3
  import { TargetDependencies } from '../config/nx-json';
4
+ /**
5
+ * For each task, the overrides every `dependsOn` edge into it would give it,
6
+ * whether or not that edge created the task. `from` is the task the edge
7
+ * leaves; an edge crossing a project without the target is recorded against
8
+ * the task it started from.
9
+ */
10
+ export type DependencyOverrides = Record<string, Array<{
11
+ from: string;
12
+ overrides: Record<string, unknown>;
13
+ }>>;
4
14
  export declare class ProcessTasks {
5
15
  private readonly extraTargetDependencies;
6
16
  private readonly projectGraph;
17
+ private readonly recordDependencyOverrides;
7
18
  private readonly seen;
8
19
  readonly tasks: {
9
20
  [id: string]: Task;
@@ -14,18 +25,30 @@ export declare class ProcessTasks {
14
25
  readonly continuousDependencies: {
15
26
  [k: string]: string[];
16
27
  };
28
+ readonly dependencyOverrides: DependencyOverrides;
17
29
  private readonly allTargetNames;
18
- constructor(extraTargetDependencies: TargetDependencies, projectGraph: ProjectGraph);
30
+ constructor(extraTargetDependencies: TargetDependencies, projectGraph: ProjectGraph, recordDependencyOverrides?: boolean);
19
31
  processTasks(projectNames: string[], targets: string[], configuration: string, overrides: Record<string, unknown>, excludeTaskDependencies: boolean): string[];
20
32
  processTask(task: Task, projectUsedToDeriveDependencies: string, configuration: string, overrides: Record<string, unknown>): void;
21
33
  private processTasksForMultipleProjects;
22
34
  private processTasksForSingleProject;
23
35
  private processTasksForDependencies;
36
+ private recordEdge;
24
37
  private createDummyTask;
25
38
  createTask(id: string, project: ProjectGraphProjectNode, target: string, resolvedConfiguration: string | undefined, overrides: Record<string, unknown>): Task;
26
39
  resolveConfiguration(project: ProjectGraphProjectNode, target: string, configuration: string | undefined): string;
27
40
  }
28
41
  export declare function createTaskGraph(projectGraph: ProjectGraph, extraTargetDependencies: TargetDependencies, projectNames: string[], targets: string[], configuration: string | undefined, overrides: Record<string, unknown>, excludeTaskDependencies?: boolean): TaskGraph;
42
+ /** `createTaskGraph`, plus the overrides each dependency edge would give. */
43
+ export declare function createTaskGraphWithDependencyOverrides(projectGraph: ProjectGraph, extraTargetDependencies: TargetDependencies, projectNames: string[], targets: string[], configuration: string | undefined, overrides: Record<string, unknown>, excludeTaskDependencies?: boolean): {
44
+ taskGraph: TaskGraph;
45
+ dependencyOverrides: DependencyOverrides;
46
+ };
47
+ /**
48
+ * `taskGraph` pruned to `keep`, as if built from `initial` alone: a kept dependency
49
+ * takes the overrides its edges give it. Undefined when those edges disagree or are missing.
50
+ */
51
+ export declare function filterTaskGraphToSelection(projectGraph: ProjectGraph, taskGraph: TaskGraph, dependencyOverrides: DependencyOverrides, initial: Set<string>, keep: Set<string>): TaskGraph | undefined;
29
52
  /**
30
53
  * This function is used to filter out the dummy tasks from the dependencies
31
54
  * It will manipulate the dependencies object in place
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ProcessTasks = void 0;
4
4
  exports.createTaskGraph = createTaskGraph;
5
+ exports.createTaskGraphWithDependencyOverrides = createTaskGraphWithDependencyOverrides;
6
+ exports.filterTaskGraphToSelection = filterTaskGraphToSelection;
5
7
  exports.filterDummyTasks = filterDummyTasks;
6
8
  exports.getNonDummyDeps = getNonDummyDeps;
7
9
  const utils_1 = require("./utils");
@@ -10,13 +12,15 @@ const output_1 = require("../utils/output");
10
12
  const task_graph_utils_1 = require("./task-graph-utils");
11
13
  const DUMMY_TASK_TARGET = '__nx_dummy_task__';
12
14
  class ProcessTasks {
13
- constructor(extraTargetDependencies, projectGraph) {
15
+ constructor(extraTargetDependencies, projectGraph, recordDependencyOverrides = false) {
14
16
  this.extraTargetDependencies = extraTargetDependencies;
15
17
  this.projectGraph = projectGraph;
18
+ this.recordDependencyOverrides = recordDependencyOverrides;
16
19
  this.seen = new Set();
17
20
  this.tasks = {};
18
21
  this.dependencies = {};
19
22
  this.continuousDependencies = {};
23
+ this.dependencyOverrides = {};
20
24
  const allTargetNames = new Set();
21
25
  for (const projectName in projectGraph.nodes) {
22
26
  const project = projectGraph.nodes[projectName];
@@ -126,6 +130,7 @@ class ProcessTasks {
126
130
  this.processTask(newTask, newTask.target.project, configuration, overrides);
127
131
  }
128
132
  if (task.id !== selfTaskId) {
133
+ this.recordEdge(task, selfTaskId, taskOverrides);
129
134
  if (this.tasks[selfTaskId].continuous) {
130
135
  this.continuousDependencies[task.id].push(selfTaskId);
131
136
  }
@@ -149,6 +154,7 @@ class ProcessTasks {
149
154
  const depTargetId = (0, utils_1.createTaskId)(depProject.name, dependencyConfig.target, resolvedConfiguration);
150
155
  const depTargetConfiguration = this.projectGraph.nodes[depProject.name].data.targets[dependencyConfig.target];
151
156
  if (task.id !== depTargetId) {
157
+ this.recordEdge(task, depTargetId, taskOverrides);
152
158
  if (depTargetConfiguration.continuous) {
153
159
  this.continuousDependencies[task.id].push(depTargetId);
154
160
  }
@@ -180,6 +186,14 @@ class ProcessTasks {
180
186
  }
181
187
  }
182
188
  }
189
+ recordEdge(task, dependencyId, overrides) {
190
+ if (!this.recordDependencyOverrides) {
191
+ return;
192
+ }
193
+ // A dummy task keeps its creator's target, so `from` is the real task the edge started from.
194
+ const from = (0, utils_1.createTaskId)(task.target.project, task.target.target, task.target.configuration);
195
+ (this.dependencyOverrides[dependencyId] ??= []).push({ from, overrides });
196
+ }
183
197
  createDummyTask(id, task) {
184
198
  return {
185
199
  ...task,
@@ -208,6 +222,7 @@ class ProcessTasks {
208
222
  cache: project.data.targets[target].cache ?? false,
209
223
  parallelism: project.data.targets[target].parallelism ?? true,
210
224
  continuous: project.data.targets[target].continuous ?? false,
225
+ ultracache: project.data.targets[target].ultracache,
211
226
  };
212
227
  }
213
228
  resolveConfiguration(project, target, configuration) {
@@ -220,7 +235,46 @@ class ProcessTasks {
220
235
  }
221
236
  exports.ProcessTasks = ProcessTasks;
222
237
  function createTaskGraph(projectGraph, extraTargetDependencies, projectNames, targets, configuration, overrides, excludeTaskDependencies = false) {
223
- const p = new ProcessTasks(extraTargetDependencies, projectGraph);
238
+ return buildTaskGraph(new ProcessTasks(extraTargetDependencies, projectGraph), projectNames, targets, configuration, overrides, excludeTaskDependencies);
239
+ }
240
+ /** `createTaskGraph`, plus the overrides each dependency edge would give. */
241
+ function createTaskGraphWithDependencyOverrides(projectGraph, extraTargetDependencies, projectNames, targets, configuration, overrides, excludeTaskDependencies = false) {
242
+ const p = new ProcessTasks(extraTargetDependencies, projectGraph, true);
243
+ const taskGraph = buildTaskGraph(p, projectNames, targets, configuration, overrides, excludeTaskDependencies);
244
+ return { taskGraph, dependencyOverrides: p.dependencyOverrides };
245
+ }
246
+ /**
247
+ * `taskGraph` pruned to `keep`, as if built from `initial` alone: a kept dependency
248
+ * takes the overrides its edges give it. Undefined when those edges disagree or are missing.
249
+ */
250
+ function filterTaskGraphToSelection(projectGraph, taskGraph, dependencyOverrides, initial, keep) {
251
+ const tasks = {};
252
+ for (const id of keep) {
253
+ const task = taskGraph.tasks[id];
254
+ if (initial.has(id)) {
255
+ tasks[id] = { ...task };
256
+ continue;
257
+ }
258
+ const edges = dependencyOverrides[id] ?? [];
259
+ if (!edges.length) {
260
+ return undefined;
261
+ }
262
+ const first = JSON.stringify(edges[0].overrides);
263
+ if (edges.some((e) => JSON.stringify(e.overrides) !== first)) {
264
+ return undefined;
265
+ }
266
+ const project = projectGraph.nodes[task.target.project];
267
+ const overrides = interpolateOverrides(edges[0].overrides, project.name, project.data);
268
+ tasks[id] = {
269
+ ...task,
270
+ overrides,
271
+ outputs: (0, utils_1.getOutputs)(projectGraph.nodes, task.target, overrides),
272
+ };
273
+ }
274
+ const pruned = (0, utils_1.removeTasksFromTaskGraph)(taskGraph, Object.keys(taskGraph.tasks).filter((id) => !keep.has(id)));
275
+ return { ...pruned, tasks };
276
+ }
277
+ function buildTaskGraph(p, projectNames, targets, configuration, overrides, excludeTaskDependencies) {
224
278
  const roots = p.processTasks(projectNames, targets, configuration, overrides, excludeTaskDependencies);
225
279
  return {
226
280
  roots,
@@ -144,7 +144,7 @@ class ForkedProcessTaskRunner {
144
144
  // Register forked process for metrics collection
145
145
  const pid = p.getPid();
146
146
  if (pid) {
147
- (0, task_io_service_1.registerTaskProcessStart)(task.id, pid);
147
+ (0, task_io_service_1.registerTaskProcessStart)(task, pid);
148
148
  }
149
149
  p.send({
150
150
  targetDescription: task.target,
@@ -181,7 +181,7 @@ class ForkedProcessTaskRunner {
181
181
  });
182
182
  // Register forked process for metrics collection
183
183
  if (p.pid) {
184
- (0, task_io_service_1.registerTaskProcessStart)(task.id, p.pid);
184
+ (0, task_io_service_1.registerTaskProcessStart)(task, p.pid);
185
185
  }
186
186
  // Send message to run the executor
187
187
  p.send({
@@ -224,7 +224,7 @@ class ForkedProcessTaskRunner {
224
224
  });
225
225
  // Register forked process for metrics collection
226
226
  if (p.pid) {
227
- (0, task_io_service_1.registerTaskProcessStart)(task.id, p.pid);
227
+ (0, task_io_service_1.registerTaskProcessStart)(task, p.pid);
228
228
  }
229
229
  const cp = new node_child_process_1.NodeChildProcessWithDirectOutput(p, temporaryOutputPath);
230
230
  this.processes.add(cp);
@@ -1,7 +1,12 @@
1
1
  import type { NxJsonConfiguration } from '../config/nx-json';
2
2
  import { Task, TaskGraph } from '../config/task-graph';
3
3
  import { LifeCycle, TaskResult } from './life-cycle';
4
+ import type { IoSnapshots } from '../native';
4
5
  import type { ProjectGraph } from '../config/project-graph';
5
6
  import { RunningTask } from './running-tasks/running-task';
6
- export declare function runDiscreteTasks(tasks: Task[], projectGraph: ProjectGraph, fullTaskGraph: TaskGraph, nxJson: NxJsonConfiguration, lifeCycle: LifeCycle): Promise<Array<Promise<TaskResult[]>>>;
7
- export declare function runContinuousTasks(tasks: Task[], projectGraph: ProjectGraph, fullTaskGraph: TaskGraph, nxJson: NxJsonConfiguration, lifeCycle: LifeCycle): Promise<Record<string, Promise<RunningTask>>>;
7
+ export declare function runDiscreteTasks(tasks: Task[], projectGraph: ProjectGraph, fullTaskGraph: TaskGraph, nxJson: NxJsonConfiguration, lifeCycle: LifeCycle,
8
+ /** The set to hash from, e.g. from `importIoSnapshots`; omitted hashes natively. */
9
+ ioSnapshots?: IoSnapshots): Promise<Array<Promise<TaskResult[]>>>;
10
+ export declare function runContinuousTasks(tasks: Task[], projectGraph: ProjectGraph, fullTaskGraph: TaskGraph, nxJson: NxJsonConfiguration, lifeCycle: LifeCycle,
11
+ /** The set to hash from, e.g. from `importIoSnapshots`; omitted hashes natively. */
12
+ ioSnapshots?: IoSnapshots): Promise<Record<string, Promise<RunningTask>>>;