nx 23.2.0-beta.0 → 23.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/bin/nx.js +1 -1
  2. package/dist/src/analytics/analytics.d.ts +6 -0
  3. package/dist/src/analytics/analytics.js +7 -1
  4. package/dist/src/analytics/index.d.ts +1 -1
  5. package/dist/src/analytics/index.js +2 -1
  6. package/dist/src/command-line/init/ai-agent-prompts.d.ts +1 -1
  7. package/dist/src/command-line/init/ai-agent-prompts.js +7 -6
  8. package/dist/src/command-line/init/command-object.js +10 -2
  9. package/dist/src/command-line/init/implementation/angular/integrated-workspace.js +2 -5
  10. package/dist/src/command-line/init/implementation/angular/legacy-angular-versions.js +3 -0
  11. package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +4 -1
  12. package/dist/src/command-line/migrate/execute-migration.d.ts +72 -0
  13. package/dist/src/command-line/migrate/execute-migration.js +389 -0
  14. package/dist/src/command-line/migrate/migrate-ui-api.js +17 -12
  15. package/dist/src/command-line/migrate/migrate.d.ts +1 -45
  16. package/dist/src/command-line/migrate/migrate.js +17 -375
  17. package/dist/src/command-line/nx-cloud/connect/view-logs.js +2 -0
  18. package/dist/src/command-line/release/utils/release-graph.d.ts +2 -2
  19. package/dist/src/command-line/release/utils/shared.d.ts +2 -2
  20. package/dist/src/command-line/show/show-target/inputs.js +50 -153
  21. package/dist/src/command-line/show/show-target/outputs.js +48 -173
  22. package/dist/src/command-line/show/show-target/utils.d.ts +28 -0
  23. package/dist/src/command-line/show/show-target/utils.js +83 -3
  24. package/dist/src/command-line/yargs-utils/shared-options.d.ts +1 -1
  25. package/dist/src/commands-runner/create-command-graph.js +5 -0
  26. package/dist/src/config/nx-json.d.ts +1 -1
  27. package/dist/src/core/graph/main.js +1 -1
  28. package/dist/src/daemon/server/handle-tasks-execution-hooks.d.ts +1 -1
  29. package/dist/src/daemon/server/server.js +7 -4
  30. package/dist/src/devkit-internals.d.ts +1 -0
  31. package/dist/src/devkit-internals.js +5 -2
  32. package/dist/src/generators/utils/nx-json.js +13 -3
  33. package/dist/src/hasher/check-task-files.d.ts +86 -0
  34. package/dist/src/hasher/check-task-files.js +378 -0
  35. package/dist/src/native/index.d.ts +25 -2
  36. package/dist/src/native/native-bindings.js +2 -0
  37. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  38. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  39. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +3 -0
  40. package/dist/src/project-graph/build-project-graph.js +7 -1
  41. package/dist/src/project-graph/file-utils.js +6 -1
  42. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +1 -0
  43. package/dist/src/project-graph/project-graph.js +3 -0
  44. package/dist/src/tasks-runner/life-cycle.d.ts +2 -2
  45. package/dist/src/tasks-runner/life-cycle.js +2 -2
  46. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +9 -4
  47. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +5 -4
  48. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +16 -8
  49. package/dist/src/tasks-runner/life-cycles/performance-report.js +55 -27
  50. package/dist/src/tasks-runner/pseudo-terminal.d.ts +1 -1
  51. package/dist/src/tasks-runner/pseudo-terminal.js +2 -2
  52. package/dist/src/tasks-runner/task-orchestrator.d.ts +4 -1
  53. package/dist/src/tasks-runner/task-orchestrator.js +23 -6
  54. package/dist/src/utils/acknowledge-build-scripts.js +7 -5
  55. package/dist/src/utils/command-line-utils.js +30 -20
  56. package/dist/src/utils/git-revision.d.ts +12 -0
  57. package/dist/src/utils/git-revision.js +25 -0
  58. package/dist/src/utils/package-manager.js +6 -2
  59. package/package.json +11 -11
@@ -7,6 +7,7 @@ exports.applyProjectMetadata = applyProjectMetadata;
7
7
  const fs_1 = require("fs");
8
8
  const path_1 = require("path");
9
9
  const perf_hooks_1 = require("perf_hooks");
10
+ const analytics_1 = require("../analytics");
10
11
  const configuration_1 = require("../config/configuration");
11
12
  const file_hasher_1 = require("../hasher/file-hasher");
12
13
  const typescript_1 = require("../plugins/js/utils/typescript");
@@ -222,7 +223,12 @@ async function updateProjectGraphWithPlugins(context, initProjectGraph, plugins,
222
223
  perf_hooks_1.performance.measure(`${plugin.name}:createDependencies`, {
223
224
  start: `${plugin.name}:createDependencies - start`,
224
225
  end: `${plugin.name}:createDependencies - end`,
225
- detail: { track: true },
226
+ detail: {
227
+ track: true,
228
+ ...(analytics_1.customDimensions && {
229
+ [analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
230
+ }),
231
+ },
226
232
  });
227
233
  }));
228
234
  perf_hooks_1.performance.mark('createDependencies:end');
@@ -12,6 +12,7 @@ const fs_1 = require("fs");
12
12
  const os_1 = require("os");
13
13
  const path_1 = require("path");
14
14
  const fileutils_1 = require("../utils/fileutils");
15
+ const git_revision_1 = require("../utils/git-revision");
15
16
  const ignore_1 = require("../utils/ignore");
16
17
  const json_diff_1 = require("../utils/json-diff");
17
18
  const workspace_root_1 = require("../utils/workspace-root");
@@ -116,11 +117,14 @@ function readLockFileAtRevision(file, lockFileName, revision, readFileAtRevision
116
117
  }
117
118
  exports.TEN_MEGABYTES = 1024 * 10000;
118
119
  function defaultReadFileAtRevision(file, revision) {
120
+ if (revision) {
121
+ (0, git_revision_1.assertValidGitRevision)(revision);
122
+ }
119
123
  try {
120
124
  const filePathInGitRepository = getFilePathInGitRepository(file);
121
125
  return !revision
122
126
  ? (0, fs_1.readFileSync)(file, 'utf-8')
123
- : (0, child_process_1.execSync)(`git show ${revision}:${filePathInGitRepository}`, {
127
+ : (0, child_process_1.execFileSync)('git', ['show', `${revision}:${filePathInGitRepository}`], {
124
128
  maxBuffer: exports.TEN_MEGABYTES,
125
129
  stdio: ['pipe', 'pipe', 'ignore'],
126
130
  windowsHide: true,
@@ -140,6 +144,7 @@ function defaultReadBunLockFileAtRevision(file, revision) {
140
144
  windowsHide: true,
141
145
  }).trim();
142
146
  }
147
+ (0, git_revision_1.assertValidGitRevision)(revision);
143
148
  const filePathInGitRepository = getFilePathInGitRepository(file);
144
149
  const tempDirectory = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'nx-bun-lock-'));
145
150
  const tempLockfilePath = (0, path_1.join)(tempDirectory, 'bun.lockb');
@@ -63,6 +63,7 @@ class LoadedNxPlugin {
63
63
  track: true,
64
64
  ...(analytics_1.customDimensions && {
65
65
  [analytics_1.customDimensions.projectCount]: projectCount,
66
+ [analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
66
67
  }),
67
68
  },
68
69
  });
@@ -244,6 +244,7 @@ async function createProjectGraphAndSourceMapsAsync(opts = {
244
244
  track: true,
245
245
  ...(analytics_1.customDimensions && {
246
246
  [analytics_1.customDimensions.projectCount]: Object.keys(currentProjectGraph.nodes).length,
247
+ [analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
247
248
  }),
248
249
  },
249
250
  });
@@ -310,6 +311,7 @@ async function createProjectGraphAndSourceMapsAsync(opts = {
310
311
  ...(analytics_1.customDimensions && {
311
312
  [analytics_1.customDimensions.projectCount]: Object.keys(res.projectGraph.nodes)
312
313
  .length,
314
+ [analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
313
315
  }),
314
316
  },
315
317
  });
@@ -333,6 +335,7 @@ async function createProjectGraphAndSourceMapsAsync(opts = {
333
335
  track: true,
334
336
  ...(analytics_1.customDimensions && {
335
337
  [analytics_1.customDimensions.projectCount]: Object.keys(projectGraphAndSourceMaps.projectGraph.nodes).length,
338
+ [analytics_1.customDimensions.sampleRate]: analytics_1.PERF_SPAN_SAMPLE_RATE,
336
339
  }),
337
340
  },
338
341
  });
@@ -44,7 +44,7 @@ export interface LifeCycle {
44
44
  startTasks?(task: Task[], metadata: TaskMetadata): void | Promise<void>;
45
45
  endTasks?(taskResults: TaskResult[], metadata: TaskMetadata): void | Promise<void>;
46
46
  printTaskTerminalOutput?(task: Task, status: TaskStatus, output: string): void;
47
- registerRunningTask?(taskId: string, parserAndWriter: ExternalObject<[any, any]>): void;
47
+ registerRunningTask?(taskId: string, ptyHandles: ExternalObject<[any, any, any]>): void;
48
48
  registerRunningTaskWithEmptyParser?(taskId: string): void;
49
49
  appendTaskOutput?(taskId: string, output: string, isPtyTask: boolean): void;
50
50
  setTaskStatus?(taskId: string, status: NativeTaskStatus): void;
@@ -72,7 +72,7 @@ export declare class CompositeLifeCycle implements LifeCycle {
72
72
  startTasks(tasks: Task[], metadata: TaskMetadata): Promise<void>;
73
73
  endTasks(taskResults: TaskResult[], metadata: TaskMetadata): Promise<void>;
74
74
  printTaskTerminalOutput(task: Task, status: TaskStatus, output: string): void;
75
- registerRunningTask(taskId: string, parserAndWriter: ExternalObject<[any, any]>): void;
75
+ registerRunningTask(taskId: string, ptyHandles: ExternalObject<[any, any, any]>): void;
76
76
  registerRunningTaskWithEmptyParser(taskId: string): void;
77
77
  appendTaskOutput(taskId: string, output: string, isPtyTask: boolean): void;
78
78
  setTaskStatus(taskId: string, status: NativeTaskStatus): void;
@@ -67,10 +67,10 @@ class CompositeLifeCycle {
67
67
  }
68
68
  }
69
69
  }
70
- registerRunningTask(taskId, parserAndWriter) {
70
+ registerRunningTask(taskId, ptyHandles) {
71
71
  for (let l of this.lifeCycles) {
72
72
  if (l.registerRunningTask) {
73
- l.registerRunningTask(taskId, parserAndWriter);
73
+ l.registerRunningTask(taskId, ptyHandles);
74
74
  }
75
75
  }
76
76
  }
@@ -29,15 +29,20 @@ export interface Timespan {
29
29
  /** `parallelism: false` tasks running during the slice (each one holds every slot). */
30
30
  nonParallel: number;
31
31
  }
32
+ /**
33
+ * A task and how long it ran (ms). The unit the report's task lists are built from —
34
+ * shared with the renderers so the producer and the formatters can't drift.
35
+ */
36
+ export interface TaskDurationRow {
37
+ id: string;
38
+ duration: number;
39
+ }
32
40
  export interface PerformanceSummary {
33
41
  runDuration: number;
34
42
  criticalPathDuration: number;
35
43
  criticalPathTaskCount: number;
36
44
  /** Longest critical-path tasks that ran (desc, capped at a few), cache hits excluded; empty when the path was fully cached. */
37
- criticalPathTop: Array<{
38
- id: string;
39
- duration: number;
40
- }>;
45
+ criticalPathTop: TaskDurationRow[];
41
46
  /** Ids of tasks that failed (slowest first), for the GitHub Actions summary's failed-tasks list. Continuous tasks and tasks without a complete window are excluded. */
42
47
  failedTasks: string[];
43
48
  /** runDuration − criticalPathDuration. */
@@ -142,8 +142,9 @@ function flushPerformanceReport() {
142
142
  // restore_terminal cooks the terminal back post-TUI, so console.log's plain \n
143
143
  // renders fine; it also supplies the single trailing newline formatReport omits.
144
144
  console.log((0, performance_report_1.formatReport)(summary));
145
- // In GitHub Actions, also append the report (plus a per-task table) to the job
146
- // summary page. Independent of the console.log above so neither masks the other.
145
+ // In GitHub Actions, also append the report to the job summary page the same stats
146
+ // as above, led by the run's outcome (a failed-tasks list, or a success line).
147
+ // Independent of the console.log above so neither masks the other.
147
148
  writePerformanceReportToGitHubActions(summary);
148
149
  }
149
150
  catch (e) {
@@ -157,8 +158,8 @@ function flushPerformanceReport() {
157
158
  /**
158
159
  * Append the performance report to the GitHub Actions job summary page when running in
159
160
  * Actions (`$GITHUB_STEP_SUMMARY` is set there and nowhere else). No-op otherwise. The
160
- * per-task table is computed lazily so non-CI runs don't pay for it. Best-effort: a
161
- * write failure must never affect the run.
161
+ * Markdown is rendered below the guard, so non-CI runs don't pay to format a report
162
+ * nothing reads. Best-effort: a write failure must never affect the run.
162
163
  *
163
164
  * Skipped for a nested run (one nx command invoked by another nx task's command), so only
164
165
  * the outermost run writes to the summary. Nx sets `NX_TASK_TARGET_PROJECT` on every task's
@@ -1,12 +1,16 @@
1
1
  import type { PerformanceSummaryPayload } from '../../native';
2
- import type { PerformanceSummary } from './performance-analysis';
2
+ import type { PerformanceSummary, TaskDurationRow } from './performance-analysis';
3
3
  /**
4
- * A recommendation built from structured parts so the link text comes from the
5
- * link definition (not a substring scanned out of the assembled report). A part
6
- * is either literal text or a {@link RecLink}; the renderers below project the
7
- * same parts to the terminal string, the payload string, and the popup links.
4
+ * A recommendation built from structured parts so the link text comes from the link
5
+ * definition (not a substring scanned out of the assembled report). A part is literal
6
+ * text, a {@link RecLink}, or a {@link RecTaskRows}, projected to each output string by
7
+ * {@link renderRecommendation}.
8
+ *
9
+ * String parts must be single-line — multi-line content needs its own structured part (as
10
+ * {@link RecTaskRows} is). TS can't enforce this: `string` is already a handled member, so
11
+ * a multi-line one compiles and then breaks the Markdown nested list.
8
12
  */
9
- type RecPart = string | RecLink;
13
+ type RecPart = string | RecLink | RecTaskRows;
10
14
  export type Recommendation = RecPart[];
11
15
  /**
12
16
  * A docs link inside a recommendation: a sentence is the visible text and the whole of
@@ -15,11 +19,15 @@ export type Recommendation = RecPart[];
15
19
  * re-links the phrase from {@link PerformanceSummaryPayload.links}.
16
20
  */
17
21
  interface RecLink {
18
- /** Visible label: the sentence that links. */
19
22
  visible: string;
20
- /** OSC 8 click target / appended URL: the utm-tagged URL. */
21
23
  href: string;
22
24
  }
25
+ /**
26
+ * The critical path's longest tasks as data, so each renderer formats them natively:
27
+ * the terminal and payload as space-aligned columns, Markdown as a nested list
28
+ * (HTML collapses space runs, so aligned columns don't survive rendering there).
29
+ */
30
+ type RecTaskRows = TaskDurationRow[];
23
31
  /**
24
32
  * The recommendation string the napi payload ships and the Rust popup matches against.
25
33
  * Links are URL-less (the popup re-links them from {@link PerformanceSummaryPayload.links}).
@@ -25,15 +25,49 @@ const NX_DISTRIBUTE_CTA = 'Distribute across machines with Nx Agents';
25
25
  function phraseLink(phrase, taggedUrl) {
26
26
  return { visible: phrase, href: taggedUrl };
27
27
  }
28
+ // Discriminate positively — test for what each part *is*. A `!isRecTaskRows` catch-all
29
+ // would misclassify a future `RecPart` member as a link; TS can't catch that (it never
30
+ // checks a predicate body), so `recommendationLinks`' `.filter(isRecLink)` would ship
31
+ // `{text: undefined, href: undefined}` to the popup.
28
32
  function isRecLink(part) {
29
- return typeof part !== 'string';
33
+ return typeof part !== 'string' && 'href' in part;
34
+ }
35
+ function isRecTaskRows(part) {
36
+ return Array.isArray(part);
37
+ }
38
+ /**
39
+ * Project a recommendation to a string, formatting each non-text part with the caller's
40
+ * renderers. The three output targets (payload, terminal, Markdown) share this one dispatch.
41
+ * After the string and task-rows branches a part is a {@link RecLink}, so `render.link`
42
+ * takes it directly — and a new {@link RecPart} member that is neither would fail to satisfy
43
+ * that `RecLink` parameter, turning "forgot to handle it" into a compile error right here.
44
+ */
45
+ function renderRecommendation(rec, render) {
46
+ return rec
47
+ .map((part) => {
48
+ if (typeof part === 'string') {
49
+ return part;
50
+ }
51
+ if (isRecTaskRows(part)) {
52
+ return render.taskRows(part);
53
+ }
54
+ return render.link(part);
55
+ })
56
+ .join('');
30
57
  }
31
58
  /**
32
59
  * The recommendation string the napi payload ships and the Rust popup matches against.
33
60
  * Links are URL-less (the popup re-links them from {@link PerformanceSummaryPayload.links}).
34
61
  */
35
62
  function recommendationToPayloadString(rec) {
36
- return rec.map((part) => (!isRecLink(part) ? part : part.visible)).join('');
63
+ return renderRecommendation(rec, {
64
+ link: (link) => link.visible,
65
+ taskRows: taskRowsToText,
66
+ });
67
+ }
68
+ /** Task rows as the text block the terminal and payload embed: newline-led, space-aligned columns. */
69
+ function taskRowsToText(tasks) {
70
+ return ['', ...formatTopTaskRows(tasks)].join('\n');
37
71
  }
38
72
  /**
39
73
  * The recommendation as a terminal string. With OSC 8 the phrase becomes a hyperlink
@@ -42,16 +76,12 @@ function recommendationToPayloadString(rec) {
42
76
  * are off.
43
77
  */
44
78
  function recommendationToTerminalString(rec, hyperlinks) {
45
- return rec
46
- .map((part) => {
47
- if (!isRecLink(part)) {
48
- return part;
49
- }
50
- return hyperlinks
51
- ? (0, terminal_link_1.terminalLink)(part.visible, part.href)
52
- : `${part.visible} → ${part.href}`;
53
- })
54
- .join('');
79
+ return renderRecommendation(rec, {
80
+ link: (link) => hyperlinks
81
+ ? (0, terminal_link_1.terminalLink)(link.visible, link.href)
82
+ : `${link.visible} → ${link.href}`,
83
+ taskRows: taskRowsToText,
84
+ });
55
85
  }
56
86
  /** The popup links (phrase + href) for every link in a recommendation list, for OSC 8 re-linking. */
57
87
  function recommendationLinks(recommendations) {
@@ -77,7 +107,8 @@ function recoverableTime(s) {
77
107
  }
78
108
  /** Render the longest critical-path tasks as aligned columns: task (left), duration (right). */
79
109
  function formatTopTaskRows(tasks) {
80
- // The only caller returns early when empty, so `tasks` is non-empty here.
110
+ // Non-empty by construction: the only recommendation carrying task rows requires
111
+ // `criticalPathTop.length > 0` to apply, so no empty array reaches the widths below.
81
112
  const idWidth = Math.max(...tasks.map((t) => t.id.length));
82
113
  const durations = tasks.map((t) => (0, native_1.formatDuration)(t.duration));
83
114
  const durWidth = Math.max(...durations.map((d) => d.length));
@@ -172,10 +203,8 @@ const RECOMMENDATIONS = [
172
203
  // only multi-line rec). Nothing ran (fully cached) → it doesn't apply.
173
204
  isApplicable: (c) => criticalPathBound(c) && c.criticalPathTop.length > 0,
174
205
  build: (c) => [
175
- [
176
- `Speed up or split the longest tasks on the critical path:`,
177
- ...formatTopTaskRows(c.criticalPathTop),
178
- ].join('\n'),
206
+ `Speed up or split the longest tasks on the critical path:`,
207
+ c.criticalPathTop,
179
208
  ],
180
209
  },
181
210
  ];
@@ -261,18 +290,17 @@ function formatReport(s) {
261
290
  }
262
291
  /**
263
292
  * A recommendation as Markdown: every link becomes `[phrase](href)` (no OSC 8, unlike the
264
- * terminal renderer) — the whole sentence reads as prose and is the link text. The
265
- * critical-path rec embeds newline-separated, space-aligned task rows; collapse them to
266
- * `<br>`-joined lines so they render inside the list item.
293
+ * terminal renderer) — the whole sentence reads as prose and is the link text. Task rows
294
+ * become a nested list under the recommendation's bullet (space-aligned columns don't
295
+ * survive HTML's whitespace collapsing).
267
296
  */
268
297
  function recommendationToMarkdownString(rec) {
269
- return rec
270
- .map((part) => !isRecLink(part) ? part : `[${part.visible}](${part.href})`)
271
- .join('')
272
- .split('\n')
273
- .map((line) => line.trim())
274
- .filter(Boolean)
275
- .join('<br>');
298
+ return renderRecommendation(rec, {
299
+ link: (link) => `[${link.visible}](${link.href})`,
300
+ taskRows: (rows) => rows
301
+ .map((t) => `\n - \`${t.id}\` — ${(0, native_1.formatDuration)(t.duration)}`)
302
+ .join(''),
303
+ });
276
304
  }
277
305
  /**
278
306
  * The performance report as GitHub-flavored Markdown for the Actions job summary
@@ -47,7 +47,7 @@ export declare class PseudoTtyProcess implements RunningTask {
47
47
  onOutput(callback: (message: string) => void): void;
48
48
  getPid(): number | undefined;
49
49
  kill(s?: NodeJS.Signals): Promise<void>;
50
- getParserAndWriter(): import("../native").ExternalObject<[import("../native").ParserArc, import("../native").WriterArc]>;
50
+ getPtyHandles(): import("../native").ExternalObject<[import("../native").ParserArc, import("../native").WriterArc, import("../native").MasterArc]>;
51
51
  }
52
52
  export declare class PseudoTtyProcessWithSend extends PseudoTtyProcess {
53
53
  rustPseudoTerminal: RustPseudoTerminal;
@@ -150,8 +150,8 @@ class PseudoTtyProcess {
150
150
  }
151
151
  }
152
152
  }
153
- getParserAndWriter() {
154
- return this.childProcess.getParserAndWriter();
153
+ getPtyHandles() {
154
+ return this.childProcess.getPtyHandles();
155
155
  }
156
156
  }
157
157
  exports.PseudoTtyProcess = PseudoTtyProcess;
@@ -35,6 +35,7 @@ export declare class TaskOrchestrator {
35
35
  private reverseTaskDeps;
36
36
  private initializingTaskIds;
37
37
  private processedTasks;
38
+ private cacheMissedHashes;
38
39
  private completedTasks;
39
40
  private waitingForTasks;
40
41
  private pendingDiscreteWorkers;
@@ -100,7 +101,9 @@ export declare class TaskOrchestrator {
100
101
  * The coordinator relies on this running unconditionally (when cache
101
102
  * is enabled): tasks dispatched in step 5 via runTaskDirectly skip
102
103
  * their own cache lookup on the assumption that this has already
103
- * confirmed them as misses. Don't add length-based bails.
104
+ * confirmed them as misses. Excluding cacheMissedHashes preserves that
105
+ * invariant — every dispatched hash was queried exactly once — but
106
+ * don't add other length-based bails.
104
107
  */
105
108
  private resolveCachedTasksBulk;
106
109
  /**
@@ -79,6 +79,12 @@ class TaskOrchestrator {
79
79
  // underlying real tasks become the anchors.
80
80
  this.initializingTaskIds = (0, utils_1.expandInitiatingTasksThroughNoop)(this.initiatingTasks, this.taskGraph, this.projectGraph);
81
81
  this.processedTasks = new Map();
82
+ // Hashes confirmed absent from the cache this run. A confirmed miss can
83
+ // only become a hit when the task itself runs — and then it leaves the
84
+ // schedule — so a missed hash never needs re-querying. Without this, a
85
+ // miss waiting for a worker slot is re-queried (including the remote
86
+ // retrieval) on every coordinator cycle.
87
+ this.cacheMissedHashes = new Set();
82
88
  this.completedTasks = new Map();
83
89
  this.waitingForTasks = [];
84
90
  this.pendingDiscreteWorkers = new Set();
@@ -324,9 +330,12 @@ class TaskOrchestrator {
324
330
  * inside DbCache.getBatch.
325
331
  */
326
332
  async fetchCacheHits(tasks) {
327
- const batchResults = await this.cache.getBatch(tasks);
333
+ const tasksToQuery = tasks.filter((t) => t.hash && !this.cacheMissedHashes.has(t.hash));
334
+ if (tasksToQuery.length === 0)
335
+ return [];
336
+ const batchResults = await this.cache.getBatch(tasksToQuery);
328
337
  const cacheHits = [];
329
- for (const task of tasks) {
338
+ for (const task of tasksToQuery) {
330
339
  const cachedResult = batchResults.get(task.hash);
331
340
  // Replay a cached result only under the same condition it was cached
332
341
  // under (shouldCacheTaskResult): successes always, failures only when
@@ -335,6 +344,9 @@ class TaskOrchestrator {
335
344
  if (cachedResult && this.shouldCacheTaskResult(task, cachedResult.code)) {
336
345
  cacheHits.push({ task, cachedResult });
337
346
  }
347
+ else {
348
+ this.cacheMissedHashes.add(task.hash);
349
+ }
338
350
  }
339
351
  return cacheHits;
340
352
  }
@@ -396,14 +408,19 @@ class TaskOrchestrator {
396
408
  * The coordinator relies on this running unconditionally (when cache
397
409
  * is enabled): tasks dispatched in step 5 via runTaskDirectly skip
398
410
  * their own cache lookup on the assumption that this has already
399
- * confirmed them as misses. Don't add length-based bails.
411
+ * confirmed them as misses. Excluding cacheMissedHashes preserves that
412
+ * invariant — every dispatched hash was queried exactly once — but
413
+ * don't add other length-based bails.
400
414
  */
401
415
  async resolveCachedTasksBulk() {
402
416
  const { scheduledTasks } = this.tasksSchedule.getAllScheduledTasks();
403
417
  const candidates = [];
404
418
  for (const id of scheduledTasks) {
405
419
  const task = this.taskGraph.tasks[id];
406
- if (task.hash && !task.continuous && task.cache) {
420
+ if (task.hash &&
421
+ !this.cacheMissedHashes.has(task.hash) &&
422
+ !task.continuous &&
423
+ task.cache) {
407
424
  candidates.push(task);
408
425
  }
409
426
  }
@@ -790,7 +807,7 @@ class TaskOrchestrator {
790
807
  else if (this.tuiEnabled) {
791
808
  if (runningTask instanceof pseudo_terminal_1.PseudoTtyProcess) {
792
809
  // This is an external of a the pseudo terminal where a task is running and can be passed to the TUI
793
- this.options.lifeCycle.registerRunningTask(task.id, runningTask.getParserAndWriter());
810
+ this.options.lifeCycle.registerRunningTask(task.id, runningTask.getPtyHandles());
794
811
  runningTask.onOutput((output) => {
795
812
  this.options.lifeCycle.appendTaskOutput(task.id, output, true);
796
813
  });
@@ -839,7 +856,7 @@ class TaskOrchestrator {
839
856
  if (this.tuiEnabled) {
840
857
  if (runningTask instanceof pseudo_terminal_1.PseudoTtyProcess) {
841
858
  // This is an external of a the pseudo terminal where a task is running and can be passed to the TUI
842
- this.options.lifeCycle.registerRunningTask(task.id, runningTask.getParserAndWriter());
859
+ this.options.lifeCycle.registerRunningTask(task.id, runningTask.getPtyHandles());
843
860
  runningTask.onOutput((output) => {
844
861
  this.options.lifeCycle.appendTaskOutput(task.id, output, true);
845
862
  });
@@ -7,6 +7,7 @@ const yaml_1 = require("yaml");
7
7
  const semver_1 = require("semver");
8
8
  const package_manager_1 = require("./package-manager");
9
9
  const fileutils_1 = require("./fileutils");
10
+ const json_1 = require("./json");
10
11
  const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
11
12
  /**
12
13
  * Records build-script decisions for dependencies that are about to be
@@ -42,23 +43,24 @@ function acknowledgePnpmBuildScripts(treeOrRoot, entries) {
42
43
  // A file that doesn't parse cleanly or whose root isn't a mapping is
43
44
  // malformed for pnpm; leave it alone rather than crashing or replacing the
44
45
  // user's content. pnpm's own error on the file is the actionable signal.
46
+ // Empty and comment-only files have no contents at all; setIn creates the
47
+ // mapping for them while keeping whatever comments they carry.
45
48
  if (parsed.errors.length > 0 ||
46
49
  (parsed.contents != null && !(parsed.contents instanceof yaml_1.YAMLMap))) {
47
50
  return;
48
51
  }
49
- const doc = parsed.contents instanceof yaml_1.YAMLMap ? parsed : new yaml_1.Document({});
50
52
  let changed = false;
51
53
  for (const [pkg, allowed] of Object.entries(entries)) {
52
54
  // Only a real boolean is a user decision. pnpm's non-strict installs stub
53
55
  // undecided packages with a placeholder string ("set this to true or
54
56
  // false"), which would fail the next strict install if left in place.
55
- if (typeof doc.getIn(['allowBuilds', pkg]) !== 'boolean') {
56
- doc.setIn(['allowBuilds', pkg], allowed);
57
+ if (typeof parsed.getIn(['allowBuilds', pkg]) !== 'boolean') {
58
+ parsed.setIn(['allowBuilds', pkg], allowed);
57
59
  changed = true;
58
60
  }
59
61
  }
60
62
  if (changed) {
61
- host.write(PNPM_WORKSPACE_FILE, doc.toString());
63
+ host.write(PNPM_WORKSPACE_FILE, parsed.toString());
62
64
  }
63
65
  }
64
66
  function createHost(treeOrRoot) {
@@ -76,7 +78,7 @@ function createHost(treeOrRoot) {
76
78
  exists: (p) => treeOrRoot.exists(p),
77
79
  read: (p) => treeOrRoot.read(p, 'utf-8'),
78
80
  write: (p, c) => treeOrRoot.write(p, c),
79
- readJson: (p) => JSON.parse(treeOrRoot.read(p, 'utf-8')),
81
+ readJson: (p) => (0, json_1.parseJson)(treeOrRoot.read(p, 'utf-8')),
80
82
  };
81
83
  }
82
84
  function getPnpmVersion(host) {
@@ -11,6 +11,7 @@ const yargs_parser_1 = tslib_1.__importDefault(require("yargs-parser"));
11
11
  const file_utils_1 = require("../project-graph/file-utils");
12
12
  const output_1 = require("./output");
13
13
  const child_process_1 = require("child_process");
14
+ const git_revision_1 = require("./git-revision");
14
15
  const workspace_root_1 = require("./workspace-root");
15
16
  const shared_options_1 = require("../command-line/yargs-utils/shared-options");
16
17
  function createOverrides(__overrides_unparsed__ = []) {
@@ -192,30 +193,27 @@ function parseFiles(options) {
192
193
  }
193
194
  }
194
195
  function getUncommittedFiles() {
195
- return parseGitOutput(`git diff --name-only --no-renames --relative HEAD .`);
196
+ return parseGitOutput([
197
+ 'diff',
198
+ '--name-only',
199
+ '--no-renames',
200
+ '--relative',
201
+ 'HEAD',
202
+ '.',
203
+ ]);
196
204
  }
197
205
  function getUntrackedFiles() {
198
- return parseGitOutput(`git ls-files --others --exclude-standard`);
206
+ return parseGitOutput(['ls-files', '--others', '--exclude-standard']);
199
207
  }
200
208
  function getMergeBase(base, head = 'HEAD') {
209
+ (0, git_revision_1.assertValidGitRevision)(base);
210
+ (0, git_revision_1.assertValidGitRevision)(head);
201
211
  try {
202
- return (0, child_process_1.execSync)(`git merge-base "${base}" "${head}"`, {
203
- maxBuffer: file_utils_1.TEN_MEGABYTES,
204
- cwd: workspace_root_1.workspaceRoot,
205
- stdio: 'pipe',
206
- windowsHide: true,
207
- })
208
- .toString()
209
- .trim();
212
+ return runGit(['merge-base', base, head]).toString().trim();
210
213
  }
211
214
  catch {
212
215
  try {
213
- return (0, child_process_1.execSync)(`git merge-base --fork-point "${base}" "${head}"`, {
214
- maxBuffer: file_utils_1.TEN_MEGABYTES,
215
- cwd: workspace_root_1.workspaceRoot,
216
- stdio: 'pipe',
217
- windowsHide: true,
218
- })
216
+ return runGit(['merge-base', '--fork-point', base, head])
219
217
  .toString()
220
218
  .trim();
221
219
  }
@@ -225,15 +223,27 @@ function getMergeBase(base, head = 'HEAD') {
225
223
  }
226
224
  }
227
225
  function getFilesUsingBaseAndHead(base, head) {
228
- return parseGitOutput(`git diff --name-only --no-renames --relative "${base}" "${head}"`);
226
+ (0, git_revision_1.assertValidGitRevision)(base);
227
+ (0, git_revision_1.assertValidGitRevision)(head);
228
+ return parseGitOutput([
229
+ 'diff',
230
+ '--name-only',
231
+ '--no-renames',
232
+ '--relative',
233
+ base,
234
+ head,
235
+ ]);
229
236
  }
230
- function parseGitOutput(command) {
231
- return (0, child_process_1.execSync)(command, {
237
+ function runGit(args) {
238
+ return (0, child_process_1.execFileSync)('git', args, {
232
239
  maxBuffer: file_utils_1.TEN_MEGABYTES,
233
240
  cwd: workspace_root_1.workspaceRoot,
234
241
  stdio: 'pipe',
235
242
  windowsHide: true,
236
- })
243
+ });
244
+ }
245
+ function parseGitOutput(args) {
246
+ return runGit(args)
237
247
  .toString('utf-8')
238
248
  .split('\n')
239
249
  .map((a) => a.trim())
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Git parses any argument beginning with `-` as an option rather than a
3
+ * revision, so a flag-shaped revision could smuggle options such as
4
+ * `--upload-pack` into the git commands Nx runs.
5
+ */
6
+ export declare function assertValidGitRevision(revision: string): void;
7
+ /**
8
+ * For refs Nx itself recorded from `git rev-parse` and later read back off
9
+ * disk, where anything other than a commit sha means the value was tampered
10
+ * with rather than that the user picked an unusual revision.
11
+ */
12
+ export declare function assertValidGitSha(sha: string): void;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertValidGitRevision = assertValidGitRevision;
4
+ exports.assertValidGitSha = assertValidGitSha;
5
+ /**
6
+ * Git parses any argument beginning with `-` as an option rather than a
7
+ * revision, so a flag-shaped revision could smuggle options such as
8
+ * `--upload-pack` into the git commands Nx runs.
9
+ */
10
+ function assertValidGitRevision(revision) {
11
+ if (revision.startsWith('-')) {
12
+ throw new Error(`Invalid git revision: "${revision}". Git revisions cannot start with "-".`);
13
+ }
14
+ }
15
+ const COMMIT_SHA = /^[0-9a-f]{7,40}$/i;
16
+ /**
17
+ * For refs Nx itself recorded from `git rev-parse` and later read back off
18
+ * disk, where anything other than a commit sha means the value was tampered
19
+ * with rather than that the user picked an unusual revision.
20
+ */
21
+ function assertValidGitSha(sha) {
22
+ if (!COMMIT_SHA.test(sha)) {
23
+ throw new Error(`Invalid git commit sha: "${sha}". Expected a hexadecimal commit sha.`);
24
+ }
25
+ }
@@ -165,8 +165,12 @@ function getPackageManagerCommand(packageManager = detectPackageManager(), root
165
165
  install: 'pnpm install --no-frozen-lockfile', // explicitly disable in case of CI
166
166
  ciInstall: 'pnpm install --frozen-lockfile',
167
167
  updateLockFile: 'pnpm install --lockfile-only',
168
- add: isPnpmWorkspace ? 'pnpm add -w' : 'pnpm add',
169
- addDev: isPnpmWorkspace ? 'pnpm add -Dw' : 'pnpm add -D',
168
+ add: isPnpmWorkspace
169
+ ? 'pnpm add -w --config.frozen-lockfile=false'
170
+ : 'pnpm add --config.frozen-lockfile=false',
171
+ addDev: isPnpmWorkspace
172
+ ? 'pnpm add -Dw --config.frozen-lockfile=false'
173
+ : 'pnpm add -D --config.frozen-lockfile=false',
170
174
  rm: 'pnpm rm',
171
175
  exec: modernPnpm ? 'pnpm exec' : 'pnpx',
172
176
  dlx: modernPnpm ? 'pnpm dlx' : 'pnpx',