nx 23.1.0 → 23.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/src/ai/configure-ai-agents-disclaimer.d.ts +5 -0
  2. package/dist/src/ai/configure-ai-agents-disclaimer.js +31 -0
  3. package/dist/src/command-line/add/add.js +8 -1
  4. package/dist/src/command-line/import/import.js +1 -1
  5. package/dist/src/command-line/init/configure-plugins.js +3 -2
  6. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +4 -2
  7. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +4 -3
  8. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +4 -3
  9. package/dist/src/command-line/init/implementation/add-nx-to-turborepo.js +4 -3
  10. package/dist/src/command-line/init/implementation/angular/index.js +4 -2
  11. package/dist/src/command-line/init/implementation/utils.d.ts +3 -3
  12. package/dist/src/command-line/init/implementation/utils.js +22 -3
  13. package/dist/src/command-line/migrate/migrate.d.ts +13 -0
  14. package/dist/src/command-line/migrate/migrate.js +45 -0
  15. package/dist/src/command-line/release/utils/git.js +6 -2
  16. package/dist/src/command-line/show/show-target/info.js +11 -2
  17. package/dist/src/daemon/client/client.d.ts +0 -2
  18. package/dist/src/daemon/client/client.js +17 -19
  19. package/dist/src/devkit-internals.d.ts +2 -0
  20. package/dist/src/devkit-internals.js +7 -1
  21. package/dist/src/native/index.d.ts +1 -1
  22. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  23. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  24. package/dist/src/plugins/js/lock-file/npm-parser.js +71 -27
  25. package/dist/src/plugins/js/lock-file/pnpm-parser.js +46 -15
  26. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +1 -1
  27. package/dist/src/plugins/js/lock-file/yarn-parser.js +32 -5
  28. package/dist/src/plugins/js/utils/register.d.ts +18 -0
  29. package/dist/src/plugins/js/utils/register.js +81 -0
  30. package/dist/src/plugins/js/utils/typescript.d.ts +9 -0
  31. package/dist/src/plugins/js/utils/typescript.js +15 -0
  32. package/dist/src/project-graph/error-types.js +41 -1
  33. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +4 -0
  34. package/dist/src/project-graph/plugins/transpiler.js +4 -0
  35. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.d.ts +9 -12
  36. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.js +77 -61
  37. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.d.ts +1 -1
  38. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.js +1 -1
  39. package/dist/src/project-graph/utils/project-configuration-utils.js +4 -6
  40. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +2 -0
  41. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +15 -6
  42. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +2 -0
  43. package/dist/src/tasks-runner/life-cycles/performance-report.js +11 -2
  44. package/dist/src/tasks-runner/run-command.js +5 -3
  45. package/dist/src/utils/acknowledge-build-scripts.d.ts +13 -0
  46. package/dist/src/utils/acknowledge-build-scripts.js +100 -0
  47. package/dist/src/utils/catalog/manager-utils.d.ts +1 -2
  48. package/dist/src/utils/catalog/manager-utils.js +22 -5
  49. package/dist/src/utils/catalog/pnpm-manager.d.ts +1 -0
  50. package/dist/src/utils/catalog/pnpm-manager.js +3 -15
  51. package/dist/src/utils/catalog/yarn-manager.d.ts +1 -0
  52. package/dist/src/utils/catalog/yarn-manager.js +3 -15
  53. package/dist/src/utils/git-utils.d.ts +2 -3
  54. package/dist/src/utils/git-utils.js +101 -47
  55. package/dist/src/utils/min-release-age/behavior/pnpm.js +9 -7
  56. package/dist/src/utils/provenance.js +12 -2
  57. package/migrations.json +7 -1
  58. package/package.json +11 -11
  59. package/dist/src/migrations/update-17-3-0/nx-release-path.d.ts +0 -3
  60. package/dist/src/migrations/update-17-3-0/nx-release-path.js +0 -47
@@ -25,6 +25,10 @@ function registerPluginTSTranspiler() {
25
25
  if (exports.unregisterPluginTSTranspiler !== null) {
26
26
  return;
27
27
  }
28
+ // Align Node's runtime resolution with the source-first plugin entry so the
29
+ // plugin's transitive workspace imports don't fall through to unbuilt `dist`.
30
+ // A no-op inside a plugin worker/daemon already spawned with `--conditions`.
31
+ (0, register_1.ensureResolveConditionsInjected)((0, typescript_1.getRootTsConfigResolveExportsConditions)(workspace_root_1.workspaceRoot));
28
32
  if ((0, register_1.isNativeStripPreferred)()) {
29
33
  // Native strip handles `.ts` syntax but doesn't rewrite NodeNext-style
30
34
  // `.js` relative specifiers to their `.ts` sources. Patch the CJS resolver
@@ -4,16 +4,13 @@ import { ProjectConfiguration } from '../../../config/workspace-json-project-jso
4
4
  * reference. `RootRef` carries the referenced project's root (resolved
5
5
  * via nameMap lookup); `UsageRef` carries the raw written name (for
6
6
  * forward refs, promoted to `RootRef` in place when the name is
7
- * identified). `parent` + `key` let the final pass write the resolved
8
- * name back; `targetPart` preserves the `:target` suffix from
7
+ * identified). `targetPart` preserves the `:target` suffix from
9
8
  * `dependsOn` strings.
10
9
  */
11
10
  export declare abstract class NameRef {
12
11
  value: string;
13
- parent: unknown;
14
- key: string | undefined;
15
12
  targetPart: string | undefined;
16
- constructor(value: string, parent: unknown, key: string | undefined, targetPart: string | undefined);
13
+ constructor(value: string, targetPart: string | undefined);
17
14
  }
18
15
  export declare class RootRef extends NameRef {
19
16
  }
@@ -27,15 +24,15 @@ export declare function isUsageRef(value: unknown): value is UsageRef;
27
24
  * then resolves them after all merging is done.
28
25
  *
29
26
  * Tracking by array position breaks once `'...'` spreads shuffle indices,
30
- * so each ref becomes a sentinel object. Arrays spread-merge by pushing
31
- * element references, so sentinel identity survives any downstream
32
- * merges the final pass walks a flat registry and writes the resolved
33
- * name back through each sentinel's `parent` back-reference. Orphaned
34
- * sentinels (from arrays dropped by a full-replace) write harmlessly.
27
+ * so each ref becomes a sentinel object. Merges copy sentinels by
28
+ * reference one sentinel can end up in many arrays (e.g. a pattern
29
+ * target's dependsOn applied to every matching target) so the final
30
+ * pass sweeps the merged rootMap and resolves every sentinel where it
31
+ * actually sits. Sentinels in arrays dropped by a full-replace are never
32
+ * visited and vanish with their array.
35
33
  */
36
34
  export declare class ProjectNameInNodePropsManager {
37
35
  private getNameMap;
38
- private allRefs;
39
36
  private pendingByName;
40
37
  constructor(getNameMap?: () => Record<string, ProjectConfiguration>);
41
38
  registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>): void;
@@ -45,6 +42,6 @@ export declare class ProjectNameInNodePropsManager {
45
42
  private createRef;
46
43
  identifyProjectWithRoot(root: string, name: string): void;
47
44
  applySubstitutions(rootMap: Record<string, ProjectConfiguration>): void;
45
+ private substituteInArray;
48
46
  private resolveFinalName;
49
- private writeReplacement;
50
47
  }
@@ -11,15 +11,12 @@ const split_target_1 = require("../../../utils/split-target");
11
11
  * reference. `RootRef` carries the referenced project's root (resolved
12
12
  * via nameMap lookup); `UsageRef` carries the raw written name (for
13
13
  * forward refs, promoted to `RootRef` in place when the name is
14
- * identified). `parent` + `key` let the final pass write the resolved
15
- * name back; `targetPart` preserves the `:target` suffix from
14
+ * identified). `targetPart` preserves the `:target` suffix from
16
15
  * `dependsOn` strings.
17
16
  */
18
17
  class NameRef {
19
- constructor(value, parent, key, targetPart) {
18
+ constructor(value, targetPart) {
20
19
  this.value = value;
21
- this.parent = parent;
22
- this.key = key;
23
20
  this.targetPart = targetPart;
24
21
  }
25
22
  }
@@ -44,15 +41,15 @@ function isUsageRef(value) {
44
41
  * then resolves them after all merging is done.
45
42
  *
46
43
  * Tracking by array position breaks once `'...'` spreads shuffle indices,
47
- * so each ref becomes a sentinel object. Arrays spread-merge by pushing
48
- * element references, so sentinel identity survives any downstream
49
- * merges the final pass walks a flat registry and writes the resolved
50
- * name back through each sentinel's `parent` back-reference. Orphaned
51
- * sentinels (from arrays dropped by a full-replace) write harmlessly.
44
+ * so each ref becomes a sentinel object. Merges copy sentinels by
45
+ * reference one sentinel can end up in many arrays (e.g. a pattern
46
+ * target's dependsOn applied to every matching target) so the final
47
+ * pass sweeps the merged rootMap and resolves every sentinel where it
48
+ * actually sits. Sentinels in arrays dropped by a full-replace are never
49
+ * visited and vanish with their array.
52
50
  */
53
51
  class ProjectNameInNodePropsManager {
54
52
  constructor(getNameMap) {
55
- this.allRefs = new Set();
56
53
  this.pendingByName = new Map();
57
54
  this.getNameMap = getNameMap ?? (() => ({}));
58
55
  }
@@ -82,26 +79,20 @@ class ProjectNameInNodePropsManager {
82
79
  processInputs(inputs) {
83
80
  for (let i = 0; i < inputs.length; i++) {
84
81
  const entry = inputs[i];
85
- // Existing sentinel: spread merges may have copied it out of its
86
- // original array, so rebind parent to this one.
87
- if (isNameRef(entry)) {
88
- entry.parent = inputs;
82
+ if (isNameRef(entry))
89
83
  continue;
90
- }
91
84
  if (!entry || typeof entry !== 'object')
92
85
  continue;
93
86
  if (!('projects' in entry))
94
87
  continue;
95
88
  const element = entry;
96
89
  const projects = element.projects;
97
- if (isNameRef(projects)) {
98
- // Object-parent sentinel — element identity is stable across spread.
90
+ if (isNameRef(projects))
99
91
  continue;
100
- }
101
92
  if (typeof projects === 'string') {
102
93
  if (projects === 'self' || projects === 'dependencies')
103
94
  continue;
104
- element.projects = this.createRef(projects, element, 'projects');
95
+ element.projects = this.createRef(projects);
105
96
  }
106
97
  else if (Array.isArray(projects)) {
107
98
  this.processProjectsArray(projects);
@@ -111,12 +102,8 @@ class ProjectNameInNodePropsManager {
111
102
  processDependsOn(dependsOn, ownerTargets, ownerName) {
112
103
  for (let i = 0; i < dependsOn.length; i++) {
113
104
  const dep = dependsOn[i];
114
- // Existing sentinel: rebind parent to this array in case a spread
115
- // merge copied it out of its original.
116
- if (isNameRef(dep)) {
117
- dep.parent = dependsOn;
105
+ if (isNameRef(dep))
118
106
  continue;
119
- }
120
107
  if (typeof dep === 'string') {
121
108
  // `^target` and same-project targets aren't cross-project refs.
122
109
  if (dep.startsWith('^') || (ownerTargets && dep in ownerTargets)) {
@@ -126,7 +113,7 @@ class ProjectNameInNodePropsManager {
126
113
  if (rest.length === 0)
127
114
  continue;
128
115
  const targetPart = rest.join(':');
129
- dependsOn[i] = this.createRef(maybeProject, dependsOn, undefined, targetPart);
116
+ dependsOn[i] = this.createRef(maybeProject, targetPart);
130
117
  continue;
131
118
  }
132
119
  if (!dep || typeof dep !== 'object' || !('projects' in dep))
@@ -142,7 +129,7 @@ class ProjectNameInNodePropsManager {
142
129
  projects === 'dependencies') {
143
130
  continue;
144
131
  }
145
- element.projects = this.createRef(projects, element, 'projects');
132
+ element.projects = this.createRef(projects);
146
133
  }
147
134
  else if (Array.isArray(projects)) {
148
135
  this.processProjectsArray(projects);
@@ -152,24 +139,21 @@ class ProjectNameInNodePropsManager {
152
139
  processProjectsArray(projects) {
153
140
  for (let j = 0; j < projects.length; j++) {
154
141
  const name = projects[j];
155
- if (isNameRef(name)) {
156
- name.parent = projects;
142
+ if (isNameRef(name))
157
143
  continue;
158
- }
159
144
  if (typeof name !== 'string')
160
145
  continue;
161
146
  if ((0, globs_1.isGlobPattern)(name))
162
147
  continue;
163
- projects[j] = this.createRef(name, projects, undefined);
148
+ projects[j] = this.createRef(name);
164
149
  }
165
150
  }
166
151
  // Builds a sentinel and registers it.
167
- createRef(referencedName, parent, key, targetPart) {
152
+ createRef(referencedName, targetPart) {
168
153
  const referencedRoot = this.getNameMap()[referencedName]?.root;
169
154
  const ref = referencedRoot !== undefined
170
- ? new RootRef(referencedRoot, parent, key, targetPart)
171
- : new UsageRef(referencedName, parent, key, targetPart);
172
- this.allRefs.add(ref);
155
+ ? new RootRef(referencedRoot, targetPart)
156
+ : new UsageRef(referencedName, targetPart);
173
157
  if (ref instanceof UsageRef) {
174
158
  let set = this.pendingByName.get(referencedName);
175
159
  if (!set) {
@@ -195,25 +179,72 @@ class ProjectNameInNodePropsManager {
195
179
  ref.value = root;
196
180
  }
197
181
  }
198
- // Writes each sentinel's current resolved name back into its owning slot.
199
- // Called once after all plugin results have been merged.
182
+ // Resolves every sentinel in the merged rootMap in place. Sweeping the
183
+ // final config (rather than writing through back-references held by the
184
+ // sentinels) covers sentinels that merges copied into arrays other than
185
+ // the one they were created in, e.g. a pattern target's dependsOn applied
186
+ // to every matching target. Called once after all plugin results have
187
+ // been merged.
200
188
  applySubstitutions(rootMap) {
201
189
  const nameByRoot = {};
202
190
  for (const root in rootMap) {
203
191
  nameByRoot[root] = rootMap[root]?.name;
204
192
  }
205
- for (const ref of this.allRefs) {
206
- const finalName = this.resolveFinalName(ref, nameByRoot);
207
- if (finalName === undefined)
208
- continue;
209
- const replacement = ref.targetPart !== undefined
210
- ? `${finalName}:${ref.targetPart}`
211
- : finalName;
212
- this.writeReplacement(ref, replacement);
193
+ for (const root in rootMap) {
194
+ const targets = rootMap[root]?.targets;
195
+ if (!targets)
196
+ continue;
197
+ for (const targetName in targets) {
198
+ const targetConfig = targets[targetName];
199
+ if (!targetConfig || typeof targetConfig !== 'object')
200
+ continue;
201
+ if (Array.isArray(targetConfig.inputs)) {
202
+ this.substituteInArray(targetConfig.inputs, nameByRoot);
203
+ }
204
+ if (Array.isArray(targetConfig.dependsOn)) {
205
+ this.substituteInArray(targetConfig.dependsOn, nameByRoot);
206
+ }
207
+ }
213
208
  }
214
- this.allRefs.clear();
215
209
  this.pendingByName.clear();
216
210
  }
211
+ substituteInArray(entries, nameByRoot) {
212
+ for (let i = 0; i < entries.length; i++) {
213
+ const entry = entries[i];
214
+ if (isNameRef(entry)) {
215
+ const finalName = this.resolveFinalName(entry, nameByRoot);
216
+ if (finalName !== undefined) {
217
+ entries[i] =
218
+ entry.targetPart !== undefined
219
+ ? `${finalName}:${entry.targetPart}`
220
+ : finalName;
221
+ }
222
+ continue;
223
+ }
224
+ if (!entry || typeof entry !== 'object' || !('projects' in entry)) {
225
+ continue;
226
+ }
227
+ const element = entry;
228
+ if (isNameRef(element.projects)) {
229
+ const finalName = this.resolveFinalName(element.projects, nameByRoot);
230
+ if (finalName !== undefined) {
231
+ element.projects = finalName;
232
+ }
233
+ }
234
+ else if (Array.isArray(element.projects)) {
235
+ const projects = element.projects;
236
+ for (let j = 0; j < projects.length; j++) {
237
+ const name = projects[j];
238
+ if (!isNameRef(name))
239
+ continue;
240
+ const finalName = this.resolveFinalName(name, nameByRoot);
241
+ if (finalName !== undefined) {
242
+ projects[j] = finalName;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
217
248
  resolveFinalName(ref, nameByRoot) {
218
249
  if (ref instanceof RootRef) {
219
250
  return nameByRoot[ref.value];
@@ -221,20 +252,5 @@ class ProjectNameInNodePropsManager {
221
252
  // Unpromoted forward ref — best effort, fall back to the written name.
222
253
  return this.getNameMap()[ref.value]?.name ?? ref.value;
223
254
  }
224
- writeReplacement(ref, replacement) {
225
- const parent = ref.parent;
226
- if (Array.isArray(parent)) {
227
- // One sentinel may appear at multiple indices (e.g. `[..., ...]`
228
- // pushed the same reference twice via spread), so replace all.
229
- for (let i = 0; i < parent.length; i++) {
230
- if (parent[i] === ref)
231
- parent[i] = replacement;
232
- }
233
- return;
234
- }
235
- if (parent && typeof parent === 'object' && ref.key !== undefined) {
236
- parent[ref.key] = replacement;
237
- }
238
- }
239
255
  }
240
256
  exports.ProjectNameInNodePropsManager = ProjectNameInNodePropsManager;
@@ -34,7 +34,7 @@ export declare class ProjectNodesManager {
34
34
  *
35
35
  * Pass a different `mergedRootMap` for the default-plugin intermediate
36
36
  * pass, then call again with `this.rootMap` after it's applied so
37
- * sentinel parents rebind onto the final arrays.
37
+ * name refs introduced by that merge are sentinelized as well.
38
38
  */
39
39
  registerNameRefs(pluginResultProjects?: Record<string, Omit<ProjectConfiguration, 'root'> & Partial<ProjectConfiguration>>, mergedRootMap?: Record<string, ProjectConfiguration>): void;
40
40
  /**
@@ -263,7 +263,7 @@ class ProjectNodesManager {
263
263
  *
264
264
  * Pass a different `mergedRootMap` for the default-plugin intermediate
265
265
  * pass, then call again with `this.rootMap` after it's applied so
266
- * sentinel parents rebind onto the final arrays.
266
+ * name refs introduced by that merge are sentinelized as well.
267
267
  */
268
268
  registerNameRefs(pluginResultProjects, mergedRootMap = this.rootMap) {
269
269
  if (!pluginResultProjects)
@@ -225,12 +225,10 @@ function mergeCreateNodesResults(specifiedResults, defaultResults, nxJsonConfigu
225
225
  }));
226
226
  }
227
227
  }
228
- // The intermediate apply may have rebuilt dependsOn / inputs arrays
229
- // via spread merges, leaving sentinels inserted against the
230
- // intermediate rootMap pointing at now-orphaned arrays. Re-walking
231
- // the final merged targets rebinds each encountered sentinel's
232
- // `parent` to the current array (see
233
- // ProjectNameInNodePropsManager#processInputs / processDependsOn).
228
+ // The intermediate apply may have rebuilt dependsOn / inputs arrays via
229
+ // spread merges, introducing name-ref strings that weren't visible in any
230
+ // single plugin result. Re-walking the final merged targets sentinelizes
231
+ // them so the final substitution sweep resolves them too.
234
232
  nodesManager.registerNameRefs(intermediateDefaultRootMap);
235
233
  // Overlay default-plugin attribution onto the main source maps using
236
234
  // "only fill missing" semantics. Any key already present in
@@ -60,6 +60,8 @@ export interface PerformanceSummary {
60
60
  cacheableCount: number;
61
61
  cacheSkipped: boolean;
62
62
  remoteCacheEnabled: boolean;
63
+ /** The workspace opted out of Nx Cloud (`neverConnectToCloud` / NX_NO_CLOUD) — never recommend it. */
64
+ cloudOptedOut: boolean;
63
65
  }
64
66
  /** Construction-time inputs for {@link PerformanceLifeCycle}. */
65
67
  export interface PerformanceLifeCycleOptions {
@@ -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
  }
@@ -25,6 +25,8 @@ interface RecLink {
25
25
  * Links are URL-less (the popup re-links them from {@link PerformanceSummaryPayload.links}).
26
26
  */
27
27
  export declare function recommendationToPayloadString(rec: Recommendation): string;
28
+ /** Below this run duration (ms), the run is already fast — recommend nothing. */
29
+ export declare const MIN_RECOMMENDATION_RUN_DURATION = 30000;
28
30
  /** Below this (ms) overhead is noise, not worth a recommendation. */
29
31
  export declare const MEANINGFUL_OVERHEAD = 1000;
30
32
  /**
@@ -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;
@@ -59,6 +59,8 @@ function recommendationLinks(recommendations) {
59
59
  .filter(isRecLink)
60
60
  .map((part) => ({ text: part.visible, href: part.href })));
61
61
  }
62
+ /** Below this run duration (ms), the run is already fast — recommend nothing. */
63
+ exports.MIN_RECOMMENDATION_RUN_DURATION = 30_000;
62
64
  /** At/below this hit rate, recommend remote cache (if off); above it caching works. */
63
65
  const LOW_CACHE_HIT_RATE = 0.1;
64
66
  /** Below this (ms) overhead is noise, not worth a recommendation. */
@@ -136,8 +138,10 @@ const RECOMMENDATIONS = [
136
138
  },
137
139
  {
138
140
  // 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).
141
+ // string stays URL-less (the popup re-links the phrase). Never pushed at a
142
+ // workspace that opted out of Nx Cloud.
140
143
  isApplicable: (c) => !c.cacheSkipped &&
144
+ !c.cloudOptedOut &&
141
145
  c.cacheableCount > 0 &&
142
146
  !c.remoteCacheEnabled &&
143
147
  c.cacheHits / c.cacheableCount <= LOW_CACHE_HIT_RATE,
@@ -181,6 +185,10 @@ const RECOMMENDATIONS = [
181
185
  * the GitHub-summary Markdown, and the TUI payload.
182
186
  */
183
187
  function buildRecommendations(s) {
188
+ // A fast run has nothing worth optimizing — stats only, no advice.
189
+ if (s.runDuration < exports.MIN_RECOMMENDATION_RUN_DURATION) {
190
+ return [];
191
+ }
184
192
  const c = {
185
193
  recoverableByParallel: s.recoverableByParallel,
186
194
  recoverableByMachines: s.recoverableByMachines,
@@ -194,6 +202,7 @@ function buildRecommendations(s) {
194
202
  cacheableCount: s.cacheableCount,
195
203
  cacheSkipped: s.cacheSkipped,
196
204
  remoteCacheEnabled: s.remoteCacheEnabled,
205
+ cloudOptedOut: s.cloudOptedOut,
197
206
  };
198
207
  return RECOMMENDATIONS.filter((r) => r.isApplicable(c)).map((r) => r.build(c));
199
208
  }
@@ -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;