nx 23.1.0 → 23.2.0-beta.1

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 (79) hide show
  1. package/dist/bin/nx.js +1 -1
  2. package/dist/src/ai/configure-ai-agents-disclaimer.d.ts +5 -0
  3. package/dist/src/ai/configure-ai-agents-disclaimer.js +31 -0
  4. package/dist/src/command-line/add/add.js +8 -1
  5. package/dist/src/command-line/import/import.js +1 -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/configure-plugins.js +3 -2
  10. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +4 -2
  11. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +4 -3
  12. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +4 -3
  13. package/dist/src/command-line/init/implementation/add-nx-to-turborepo.js +4 -3
  14. package/dist/src/command-line/init/implementation/angular/index.js +4 -2
  15. package/dist/src/command-line/init/implementation/utils.d.ts +3 -3
  16. package/dist/src/command-line/init/implementation/utils.js +22 -3
  17. package/dist/src/command-line/migrate/execute-migration.d.ts +72 -0
  18. package/dist/src/command-line/migrate/execute-migration.js +389 -0
  19. package/dist/src/command-line/migrate/migrate-ui-api.js +17 -12
  20. package/dist/src/command-line/migrate/migrate.d.ts +14 -45
  21. package/dist/src/command-line/migrate/migrate.js +58 -375
  22. package/dist/src/command-line/release/utils/git.js +6 -2
  23. package/dist/src/command-line/release/utils/release-graph.d.ts +2 -2
  24. package/dist/src/command-line/release/utils/shared.d.ts +2 -2
  25. package/dist/src/command-line/show/show-target/info.js +11 -2
  26. package/dist/src/commands-runner/create-command-graph.js +5 -0
  27. package/dist/src/config/nx-json.d.ts +1 -1
  28. package/dist/src/core/graph/main.js +1 -1
  29. package/dist/src/daemon/client/client.d.ts +0 -2
  30. package/dist/src/daemon/client/client.js +17 -19
  31. package/dist/src/devkit-internals.d.ts +2 -0
  32. package/dist/src/devkit-internals.js +7 -1
  33. package/dist/src/generators/utils/nx-json.js +13 -3
  34. package/dist/src/native/index.d.ts +1 -1
  35. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  36. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  37. package/dist/src/plugins/js/lock-file/npm-parser.js +71 -27
  38. package/dist/src/plugins/js/lock-file/pnpm-parser.js +46 -15
  39. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +1 -1
  40. package/dist/src/plugins/js/lock-file/yarn-parser.js +32 -5
  41. package/dist/src/plugins/js/utils/register.d.ts +18 -0
  42. package/dist/src/plugins/js/utils/register.js +81 -0
  43. package/dist/src/plugins/js/utils/typescript.d.ts +9 -0
  44. package/dist/src/plugins/js/utils/typescript.js +15 -0
  45. package/dist/src/project-graph/error-types.js +41 -1
  46. package/dist/src/project-graph/file-utils.js +6 -1
  47. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +4 -0
  48. package/dist/src/project-graph/plugins/transpiler.js +4 -0
  49. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.d.ts +9 -12
  50. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.js +77 -61
  51. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.d.ts +1 -1
  52. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.js +1 -1
  53. package/dist/src/project-graph/utils/project-configuration-utils.js +4 -6
  54. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +11 -4
  55. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +15 -6
  56. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +5 -4
  57. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +18 -8
  58. package/dist/src/tasks-runner/life-cycles/performance-report.js +66 -29
  59. package/dist/src/tasks-runner/run-command.js +5 -3
  60. package/dist/src/utils/acknowledge-build-scripts.d.ts +13 -0
  61. package/dist/src/utils/acknowledge-build-scripts.js +100 -0
  62. package/dist/src/utils/catalog/manager-utils.d.ts +1 -2
  63. package/dist/src/utils/catalog/manager-utils.js +22 -5
  64. package/dist/src/utils/catalog/pnpm-manager.d.ts +1 -0
  65. package/dist/src/utils/catalog/pnpm-manager.js +3 -15
  66. package/dist/src/utils/catalog/yarn-manager.d.ts +1 -0
  67. package/dist/src/utils/catalog/yarn-manager.js +3 -15
  68. package/dist/src/utils/command-line-utils.js +30 -20
  69. package/dist/src/utils/git-revision.d.ts +12 -0
  70. package/dist/src/utils/git-revision.js +25 -0
  71. package/dist/src/utils/git-utils.d.ts +2 -3
  72. package/dist/src/utils/git-utils.js +101 -47
  73. package/dist/src/utils/min-release-age/behavior/pnpm.js +9 -7
  74. package/dist/src/utils/package-manager.js +6 -2
  75. package/dist/src/utils/provenance.js +12 -2
  76. package/migrations.json +7 -1
  77. package/package.json +11 -11
  78. package/dist/src/migrations/update-17-3-0/nx-release-path.d.ts +0 -3
  79. package/dist/src/migrations/update-17-3-0/nx-release-path.js +0 -47
@@ -18,6 +18,8 @@ const CACHE_HIT_STATUSES = new Set([
18
18
  ]);
19
19
  /** Gap (ms) still counted as the same pre-dispatch hashing phase; a larger gap means older, unrelated hashing (e.g. a daemon's previous run). */
20
20
  const PRE_DISPATCH_HASH_GAP = 1000;
21
+ /** A critical-path task shorter than this fraction of the path is noise, not a speed-up target. */
22
+ const CRITICAL_PATH_TOP_MIN_FRACTION = 0.2;
21
23
  /**
22
24
  * Pure analysis over one finished run's collected timings — no lifecycle state and no
23
25
  * mutation. Built once per run from the {@link PerformanceLifeCycle}'s accumulated
@@ -171,15 +173,17 @@ class PerformanceAnalysis {
171
173
  * (their duration is just restore time); no-status tasks (synthetic test runs) are
172
174
  * kept.
173
175
  */
174
- computeCriticalPathTop(criticalPathTasks, durations) {
175
- return criticalPathTasks
176
+ computeCriticalPathTop(criticalPathTasks, durations, criticalPathDuration) {
177
+ return (criticalPathTasks
176
178
  .filter((id) => {
177
179
  const status = this.statuses.get(id);
178
180
  return !status || !CACHE_HIT_STATUSES.has(status);
179
181
  })
180
182
  .map((id) => ({ id, duration: durations.get(id) ?? 0 }))
183
+ // Only tasks that meaningfully shape the path are worth speeding up.
184
+ .filter((t) => t.duration >= CRITICAL_PATH_TOP_MIN_FRACTION * criticalPathDuration)
181
185
  .sort((a, b) => b.duration - a.duration)
182
- .slice(0, 3);
186
+ .slice(0, 3));
183
187
  }
184
188
  /** Cache outcome: tasks restored (`cacheHits`) and the total with a cache outcome (`cacheableCount` = hits + ran). No-status tasks count for neither. */
185
189
  computeCacheStats() {
@@ -244,12 +248,15 @@ class PerformanceAnalysis {
244
248
  cores,
245
249
  criticalPathDuration,
246
250
  });
247
- const criticalPathTop = this.computeCriticalPathTop(criticalPathTasks, durations);
251
+ const criticalPathTop = this.computeCriticalPathTop(criticalPathTasks, durations, criticalPathDuration);
248
252
  const { cacheHits, cacheableCount } = this.computeCacheStats();
249
253
  // `skipNxCache` already folds in NX_SKIP_NX_CACHE / NX_DISABLE_NX_CACHE
250
254
  // (normalized in command-line-utils) — don't re-read those env vars here.
251
255
  const cacheSkipped = this.options.skipNxCache === true;
252
256
  const remoteCacheEnabled = this.remoteCacheEnabled();
257
+ const cloudOptedOut = this.options.nxJson
258
+ ? !!(0, nx_cloud_utils_1.isNxCloudDisabled)(this.options.nxJson)
259
+ : false;
253
260
  // Coordinator-dominated: hashing/scheduling outweighs task work by >3x the
254
261
  // critical path, which keeps cold runs critical-path-bound.
255
262
  const coordinatorDominated = coordinatorOverhead >= performance_report_1.MEANINGFUL_OVERHEAD &&
@@ -271,14 +278,16 @@ class PerformanceAnalysis {
271
278
  parallel,
272
279
  cores,
273
280
  isCI,
274
- // Can only start distributing in CI when not already doing so.
275
- canDistribute: isCI && !distributing,
281
+ // Can only start distributing in CI when not already doing so — and never
282
+ // suggest Nx Agents to a workspace that opted out of Nx Cloud.
283
+ canDistribute: isCI && !distributing && !cloudOptedOut,
276
284
  distributing,
277
285
  coordinatorDominated,
278
286
  cacheHits,
279
287
  cacheableCount,
280
288
  cacheSkipped,
281
289
  remoteCacheEnabled,
290
+ cloudOptedOut,
282
291
  };
283
292
  }
284
293
  }
@@ -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,16 +19,22 @@ 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}).
26
34
  */
27
35
  export declare function recommendationToPayloadString(rec: Recommendation): string;
36
+ /** Below this run duration (ms), the run is already fast — recommend nothing. */
37
+ export declare const MIN_RECOMMENDATION_RUN_DURATION = 30000;
28
38
  /** Below this (ms) overhead is noise, not worth a recommendation. */
29
39
  export declare const MEANINGFUL_OVERHEAD = 1000;
30
40
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MEANINGFUL_OVERHEAD = void 0;
3
+ exports.MEANINGFUL_OVERHEAD = exports.MIN_RECOMMENDATION_RUN_DURATION = void 0;
4
4
  exports.recommendationToPayloadString = recommendationToPayloadString;
5
5
  exports.buildRecommendations = buildRecommendations;
6
6
  exports.formatReport = formatReport;
@@ -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) {
@@ -59,6 +89,8 @@ function recommendationLinks(recommendations) {
59
89
  .filter(isRecLink)
60
90
  .map((part) => ({ text: part.visible, href: part.href })));
61
91
  }
92
+ /** Below this run duration (ms), the run is already fast — recommend nothing. */
93
+ exports.MIN_RECOMMENDATION_RUN_DURATION = 30_000;
62
94
  /** At/below this hit rate, recommend remote cache (if off); above it caching works. */
63
95
  const LOW_CACHE_HIT_RATE = 0.1;
64
96
  /** Below this (ms) overhead is noise, not worth a recommendation. */
@@ -75,7 +107,8 @@ function recoverableTime(s) {
75
107
  }
76
108
  /** Render the longest critical-path tasks as aligned columns: task (left), duration (right). */
77
109
  function formatTopTaskRows(tasks) {
78
- // 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.
79
112
  const idWidth = Math.max(...tasks.map((t) => t.id.length));
80
113
  const durations = tasks.map((t) => (0, native_1.formatDuration)(t.duration));
81
114
  const durWidth = Math.max(...durations.map((d) => d.length));
@@ -136,8 +169,10 @@ const RECOMMENDATIONS = [
136
169
  },
137
170
  {
138
171
  // Barely-used cache with no remote: set up Nx Cloud. Whole-phrase link; the payload
139
- // string stays URL-less (the popup re-links the phrase).
172
+ // string stays URL-less (the popup re-links the phrase). Never pushed at a
173
+ // workspace that opted out of Nx Cloud.
140
174
  isApplicable: (c) => !c.cacheSkipped &&
175
+ !c.cloudOptedOut &&
141
176
  c.cacheableCount > 0 &&
142
177
  !c.remoteCacheEnabled &&
143
178
  c.cacheHits / c.cacheableCount <= LOW_CACHE_HIT_RATE,
@@ -168,10 +203,8 @@ const RECOMMENDATIONS = [
168
203
  // only multi-line rec). Nothing ran (fully cached) → it doesn't apply.
169
204
  isApplicable: (c) => criticalPathBound(c) && c.criticalPathTop.length > 0,
170
205
  build: (c) => [
171
- [
172
- `Speed up or split the longest tasks on the critical path:`,
173
- ...formatTopTaskRows(c.criticalPathTop),
174
- ].join('\n'),
206
+ `Speed up or split the longest tasks on the critical path:`,
207
+ c.criticalPathTop,
175
208
  ],
176
209
  },
177
210
  ];
@@ -181,6 +214,10 @@ const RECOMMENDATIONS = [
181
214
  * the GitHub-summary Markdown, and the TUI payload.
182
215
  */
183
216
  function buildRecommendations(s) {
217
+ // A fast run has nothing worth optimizing — stats only, no advice.
218
+ if (s.runDuration < exports.MIN_RECOMMENDATION_RUN_DURATION) {
219
+ return [];
220
+ }
184
221
  const c = {
185
222
  recoverableByParallel: s.recoverableByParallel,
186
223
  recoverableByMachines: s.recoverableByMachines,
@@ -194,6 +231,7 @@ function buildRecommendations(s) {
194
231
  cacheableCount: s.cacheableCount,
195
232
  cacheSkipped: s.cacheSkipped,
196
233
  remoteCacheEnabled: s.remoteCacheEnabled,
234
+ cloudOptedOut: s.cloudOptedOut,
197
235
  };
198
236
  return RECOMMENDATIONS.filter((r) => r.isApplicable(c)).map((r) => r.build(c));
199
237
  }
@@ -252,18 +290,17 @@ function formatReport(s) {
252
290
  }
253
291
  /**
254
292
  * A recommendation as Markdown: every link becomes `[phrase](href)` (no OSC 8, unlike the
255
- * terminal renderer) — the whole sentence reads as prose and is the link text. The
256
- * critical-path rec embeds newline-separated, space-aligned task rows; collapse them to
257
- * `<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).
258
296
  */
259
297
  function recommendationToMarkdownString(rec) {
260
- return rec
261
- .map((part) => !isRecLink(part) ? part : `[${part.visible}](${part.href})`)
262
- .join('')
263
- .split('\n')
264
- .map((line) => line.trim())
265
- .filter(Boolean)
266
- .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
+ });
267
304
  }
268
305
  /**
269
306
  * The performance report as GitHub-flavored Markdown for the Actions job summary
@@ -25,6 +25,7 @@ const update_manager_1 = require("../nx-cloud/update-manager");
25
25
  const logger_1 = require("../utils/logger");
26
26
  const nx_key_1 = require("../utils/nx-key");
27
27
  const output_1 = require("../utils/output");
28
+ const configure_ai_agents_disclaimer_1 = require("../ai/configure-ai-agents-disclaimer");
28
29
  const sync_generators_1 = require("../utils/sync-generators");
29
30
  const workspace_root_1 = require("../utils/workspace-root");
30
31
  const create_task_graph_1 = require("./create-task-graph");
@@ -168,7 +169,7 @@ async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, pr
168
169
  const lifeCycles = [tsLifeCycle];
169
170
  // Only run the TUI if there are tasks to run
170
171
  if (tasks.length > 0) {
171
- appLifeCycle = new AppLifeCycle(tasks, initiatingTasks.map((t) => t.id), isRunOne ? 0 /* RunMode.RunOne */ : 1 /* RunMode.RunMany */, pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, titleText, workspace_root_1.workspaceRoot, taskGraph);
172
+ appLifeCycle = new AppLifeCycle(tasks, initiatingTasks.map((t) => t.id), isRunOne ? 0 /* RunMode.RunOne */ : 1 /* RunMode.RunMany */, pinnedTasks, nxArgs ?? {}, nxJson.tui ?? {}, titleText, workspace_root_1.workspaceRoot, taskGraph, (0, nx_cloud_utils_1.isNxCloudUsed)(nxJson));
172
173
  // The native endCommand renders the perf report in the exit popup; the runner
173
174
  // sources the payload and CompositeLifeCycle forwards it here.
174
175
  lifeCycles.unshift(appLifeCycle);
@@ -410,9 +411,10 @@ async function printConfigureAiAgentsDisclaimer() {
410
411
  return;
411
412
  }
412
413
  const { outdatedAgents } = await client_1.daemonClient.getConfigureAiAgentsStatus();
413
- if (outdatedAgents.length > 0) {
414
- output_1.output.logRawLine(output_1.output.dim('Your AI agent configuration is outdated. Run "nx configure-ai-agents" to update.'));
414
+ if (!(0, configure_ai_agents_disclaimer_1.shouldPrintConfigureAiAgentsDisclaimer)(outdatedAgents, workspace_root_1.workspaceRoot)) {
415
+ return;
415
416
  }
417
+ output_1.output.logRawLine(output_1.output.dim('Your AI agent configuration is outdated. Run "nx configure-ai-agents" to update.'));
416
418
  }
417
419
  catch {
418
420
  // Silently ignore errors
@@ -0,0 +1,13 @@
1
+ import type { Tree } from '../generators/tree';
2
+ import type { PackageManager } from './package-manager';
3
+ /**
4
+ * Records build-script decisions for dependencies that are about to be
5
+ * installed, in whatever form the given package manager understands.
6
+ *
7
+ * Only pnpm needs this today: pnpm 11+ refuses to install a dependency whose
8
+ * build scripts are neither allowed nor denied, so the generator or command
9
+ * that introduces such a dependency records the decision up front. Other
10
+ * package managers run build scripts unconditionally, so this is a no-op for
11
+ * them.
12
+ */
13
+ export declare function acknowledgeBuildScripts(treeOrRoot: Tree | string, packageManager: PackageManager, entries: Record<string, boolean>): void;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.acknowledgeBuildScripts = acknowledgeBuildScripts;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const yaml_1 = require("yaml");
7
+ const semver_1 = require("semver");
8
+ const package_manager_1 = require("./package-manager");
9
+ const fileutils_1 = require("./fileutils");
10
+ const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
11
+ /**
12
+ * Records build-script decisions for dependencies that are about to be
13
+ * installed, in whatever form the given package manager understands.
14
+ *
15
+ * Only pnpm needs this today: pnpm 11+ refuses to install a dependency whose
16
+ * build scripts are neither allowed nor denied, so the generator or command
17
+ * that introduces such a dependency records the decision up front. Other
18
+ * package managers run build scripts unconditionally, so this is a no-op for
19
+ * them.
20
+ */
21
+ function acknowledgeBuildScripts(treeOrRoot, packageManager, entries) {
22
+ if (packageManager !== 'pnpm') {
23
+ return;
24
+ }
25
+ acknowledgePnpmBuildScripts(treeOrRoot, entries);
26
+ }
27
+ /**
28
+ * Records `allowBuilds` decisions in pnpm-workspace.yaml, creating the file
29
+ * when missing (mirroring `pnpm approve-builds` in single-package repos).
30
+ *
31
+ * Comment-preserving. Existing entries are never overwritten, so user
32
+ * decisions always win. No-op for pnpm < 11, which warns instead of erroring
33
+ * and does not read `allowBuilds`.
34
+ */
35
+ function acknowledgePnpmBuildScripts(treeOrRoot, entries) {
36
+ const host = createHost(treeOrRoot);
37
+ const pnpmVersion = getPnpmVersion(host);
38
+ if (!pnpmVersion || !(0, semver_1.gte)(pnpmVersion, '11.0.0')) {
39
+ return;
40
+ }
41
+ const parsed = (0, yaml_1.parseDocument)(host.exists(PNPM_WORKSPACE_FILE) ? host.read(PNPM_WORKSPACE_FILE) : '');
42
+ // A file that doesn't parse cleanly or whose root isn't a mapping is
43
+ // malformed for pnpm; leave it alone rather than crashing or replacing the
44
+ // user's content. pnpm's own error on the file is the actionable signal.
45
+ if (parsed.errors.length > 0 ||
46
+ (parsed.contents != null && !(parsed.contents instanceof yaml_1.YAMLMap))) {
47
+ return;
48
+ }
49
+ const doc = parsed.contents instanceof yaml_1.YAMLMap ? parsed : new yaml_1.Document({});
50
+ let changed = false;
51
+ for (const [pkg, allowed] of Object.entries(entries)) {
52
+ // Only a real boolean is a user decision. pnpm's non-strict installs stub
53
+ // undecided packages with a placeholder string ("set this to true or
54
+ // 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
+ changed = true;
58
+ }
59
+ }
60
+ if (changed) {
61
+ host.write(PNPM_WORKSPACE_FILE, doc.toString());
62
+ }
63
+ }
64
+ function createHost(treeOrRoot) {
65
+ if (typeof treeOrRoot === 'string') {
66
+ return {
67
+ root: treeOrRoot,
68
+ exists: (p) => (0, fs_1.existsSync)((0, path_1.join)(treeOrRoot, p)),
69
+ read: (p) => (0, fs_1.readFileSync)((0, path_1.join)(treeOrRoot, p), 'utf-8'),
70
+ write: (p, c) => (0, fs_1.writeFileSync)((0, path_1.join)(treeOrRoot, p), c),
71
+ readJson: (p) => (0, fileutils_1.readJsonFile)((0, path_1.join)(treeOrRoot, p)),
72
+ };
73
+ }
74
+ return {
75
+ root: treeOrRoot.root,
76
+ exists: (p) => treeOrRoot.exists(p),
77
+ read: (p) => treeOrRoot.read(p, 'utf-8'),
78
+ write: (p, c) => treeOrRoot.write(p, c),
79
+ readJson: (p) => JSON.parse(treeOrRoot.read(p, 'utf-8')),
80
+ };
81
+ }
82
+ function getPnpmVersion(host) {
83
+ // The host's packageManager field wins: during workspace creation the
84
+ // in-flight package.json only exists in the tree, not on disk.
85
+ if (host.exists('package.json')) {
86
+ const { packageManager } = host.readJson('package.json');
87
+ const version = (0, package_manager_1.parseVersionFromPackageManagerField)('pnpm', typeof packageManager === 'string' ? packageManager : undefined);
88
+ if (version) {
89
+ return version;
90
+ }
91
+ }
92
+ try {
93
+ return (0, package_manager_1.getPackageManagerVersion)('pnpm', host.root);
94
+ }
95
+ catch {
96
+ // The version cannot be probed (e.g. pnpm is not on PATH). Leave the
97
+ // workspace file untouched; pnpm's own install error remains actionable.
98
+ return null;
99
+ }
100
+ }
@@ -1,7 +1,6 @@
1
1
  import type { Tree } from '../../generators/tree';
2
2
  import type { CatalogDefinitions } from './types';
3
- export declare function readCatalogConfigFromFs(filename: string, fullPath: string): CatalogDefinitions | null;
4
- export declare function readCatalogConfigFromTree(filename: string, tree: Tree): CatalogDefinitions | null;
3
+ export declare function readCatalogDefinitions(filename: string, treeOrRoot: Tree | string, cache: Map<string, CatalogDefinitions | null>): CatalogDefinitions | null;
5
4
  export declare function updateCatalogVersionsInFile(filename: string, treeOrRoot: Tree | string, updates: Array<{
6
5
  packageName: string;
7
6
  version: string;
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readCatalogConfigFromFs = readCatalogConfigFromFs;
4
- exports.readCatalogConfigFromTree = readCatalogConfigFromTree;
3
+ exports.readCatalogDefinitions = readCatalogDefinitions;
5
4
  exports.updateCatalogVersionsInFile = updateCatalogVersionsInFile;
6
- // Keep in sync with packages/devkit/src/utils/catalog/manager-utils.ts; the body
7
- // below the imports is duplicated because @nx/devkit supports a range of nx majors
8
- // and this logic isn't part of the nx surface it can import across that range.
9
5
  const js_yaml_1 = require("@zkochan/js-yaml");
10
6
  const node_fs_1 = require("node:fs");
11
7
  const node_path_1 = require("node:path");
@@ -119,6 +115,27 @@ function readCatalogConfigFromTree(filename, tree) {
119
115
  return null;
120
116
  }
121
117
  }
118
+ // Managers are created per operation (getCatalogManager news one up), so the
119
+ // fs (string-root) branch is cached to read the file once per pass instead of
120
+ // once per catalog reference. The Tree branch stays live since the tree is
121
+ // mutable within a generator.
122
+ function readCatalogDefinitions(filename, treeOrRoot, cache) {
123
+ if (typeof treeOrRoot === 'string') {
124
+ if (cache.has(treeOrRoot)) {
125
+ return cache.get(treeOrRoot);
126
+ }
127
+ const configPath = (0, node_path_1.join)(treeOrRoot, filename);
128
+ const defs = (0, node_fs_1.existsSync)(configPath)
129
+ ? readCatalogConfigFromFs(filename, configPath)
130
+ : null;
131
+ cache.set(treeOrRoot, defs);
132
+ return defs;
133
+ }
134
+ if (!treeOrRoot.exists(filename)) {
135
+ return null;
136
+ }
137
+ return readCatalogConfigFromTree(filename, treeOrRoot);
138
+ }
122
139
  function updateCatalogVersionsInFile(filename, treeOrRoot, updates) {
123
140
  let checkExists;
124
141
  let readYaml;
@@ -7,6 +7,7 @@ import type { CatalogDefinitions, CatalogReference } from './types';
7
7
  export declare class PnpmCatalogManager implements CatalogManager {
8
8
  readonly name = "pnpm";
9
9
  readonly catalogProtocol = "catalog:";
10
+ private definitionsByRoot;
10
11
  isCatalogReference(version: string): boolean;
11
12
  parseCatalogReference(version: string): CatalogReference | null;
12
13
  getCatalogDefinitionFilePaths(): string[];
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PnpmCatalogManager = void 0;
4
- const node_fs_1 = require("node:fs");
5
- const node_path_1 = require("node:path");
6
4
  const manager_1 = require("./manager");
7
5
  const manager_utils_1 = require("./manager-utils");
8
6
  const PNPM_WORKSPACE_FILENAME = 'pnpm-workspace.yaml';
@@ -13,6 +11,8 @@ class PnpmCatalogManager {
13
11
  constructor() {
14
12
  this.name = 'pnpm';
15
13
  this.catalogProtocol = 'catalog:';
14
+ // Parsed fs-root definitions, cached per pass. See readCatalogDefinitions.
15
+ this.definitionsByRoot = new Map();
16
16
  }
17
17
  isCatalogReference(version) {
18
18
  return version.startsWith(this.catalogProtocol);
@@ -33,19 +33,7 @@ class PnpmCatalogManager {
33
33
  return [PNPM_WORKSPACE_FILENAME];
34
34
  }
35
35
  getCatalogDefinitions(treeOrRoot) {
36
- if (typeof treeOrRoot === 'string') {
37
- const configPath = (0, node_path_1.join)(treeOrRoot, PNPM_WORKSPACE_FILENAME);
38
- if (!(0, node_fs_1.existsSync)(configPath)) {
39
- return null;
40
- }
41
- return (0, manager_utils_1.readCatalogConfigFromFs)(PNPM_WORKSPACE_FILENAME, configPath);
42
- }
43
- else {
44
- if (!treeOrRoot.exists(PNPM_WORKSPACE_FILENAME)) {
45
- return null;
46
- }
47
- return (0, manager_utils_1.readCatalogConfigFromTree)(PNPM_WORKSPACE_FILENAME, treeOrRoot);
48
- }
36
+ return (0, manager_utils_1.readCatalogDefinitions)(PNPM_WORKSPACE_FILENAME, treeOrRoot, this.definitionsByRoot);
49
37
  }
50
38
  resolveCatalogReference(treeOrRoot, packageName, version) {
51
39
  const catalogRef = this.parseCatalogReference(version);
@@ -7,6 +7,7 @@ import type { CatalogDefinitions, CatalogReference } from './types';
7
7
  export declare class YarnCatalogManager implements CatalogManager {
8
8
  readonly name = "yarn";
9
9
  readonly catalogProtocol = "catalog:";
10
+ private definitionsByRoot;
10
11
  isCatalogReference(version: string): boolean;
11
12
  parseCatalogReference(version: string): CatalogReference | null;
12
13
  getCatalogDefinitionFilePaths(): string[];
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.YarnCatalogManager = void 0;
4
- const node_fs_1 = require("node:fs");
5
- const node_path_1 = require("node:path");
6
4
  const manager_1 = require("./manager");
7
5
  const manager_utils_1 = require("./manager-utils");
8
6
  const YARNRC_FILENAME = '.yarnrc.yml';
@@ -13,6 +11,8 @@ class YarnCatalogManager {
13
11
  constructor() {
14
12
  this.name = 'yarn';
15
13
  this.catalogProtocol = 'catalog:';
14
+ // Parsed fs-root definitions, cached per pass. See readCatalogDefinitions.
15
+ this.definitionsByRoot = new Map();
16
16
  }
17
17
  isCatalogReference(version) {
18
18
  return version.startsWith(this.catalogProtocol);
@@ -33,19 +33,7 @@ class YarnCatalogManager {
33
33
  return [YARNRC_FILENAME];
34
34
  }
35
35
  getCatalogDefinitions(treeOrRoot) {
36
- if (typeof treeOrRoot === 'string') {
37
- const configPath = (0, node_path_1.join)(treeOrRoot, YARNRC_FILENAME);
38
- if (!(0, node_fs_1.existsSync)(configPath)) {
39
- return null;
40
- }
41
- return (0, manager_utils_1.readCatalogConfigFromFs)(YARNRC_FILENAME, configPath);
42
- }
43
- else {
44
- if (!treeOrRoot.exists(YARNRC_FILENAME)) {
45
- return null;
46
- }
47
- return (0, manager_utils_1.readCatalogConfigFromTree)(YARNRC_FILENAME, treeOrRoot);
48
- }
36
+ return (0, manager_utils_1.readCatalogDefinitions)(YARNRC_FILENAME, treeOrRoot, this.definitionsByRoot);
49
37
  }
50
38
  resolveCatalogReference(treeOrRoot, packageName, version) {
51
39
  const catalogRef = this.parseCatalogReference(version);