nx 23.0.1 → 23.0.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 (48) hide show
  1. package/dist/bin/init-local.js +1 -1
  2. package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.d.ts +1 -0
  3. package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +6 -1
  4. package/dist/src/command-line/init/init-v2.js +1 -1
  5. package/dist/src/command-line/migrate/migrate.d.ts +2 -0
  6. package/dist/src/command-line/migrate/migrate.js +32 -6
  7. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.d.ts +1 -1
  8. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +28 -21
  9. package/dist/src/command-line/release/version/resolve-current-version.js +4 -1
  10. package/dist/src/core/graph/main.js +1 -1
  11. package/dist/src/daemon/server/project-graph-incremental-recomputation.js +30 -11
  12. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  13. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  14. package/dist/src/plugins/js/lock-file/bun-parser.js +61 -38
  15. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +1 -3
  16. package/dist/src/plugins/js/package-json/create-package-json.js +22 -1
  17. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.d.ts +7 -0
  18. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +26 -1
  19. package/dist/src/plugins/js/utils/register.d.ts +31 -10
  20. package/dist/src/plugins/js/utils/register.js +87 -18
  21. package/dist/src/plugins/package-json/create-nodes.js +5 -3
  22. package/dist/src/project-graph/operators.d.ts +0 -1
  23. package/dist/src/project-graph/operators.js +0 -44
  24. package/dist/src/project-graph/project-graph-builder.js +26 -10
  25. package/dist/src/project-graph/utils/project-configuration/target-defaults.js +23 -6
  26. package/dist/src/project-graph/utils/project-configuration-utils.js +28 -28
  27. package/dist/src/tasks-runner/batch/run-batch.js +3 -0
  28. package/dist/src/tasks-runner/cache.js +6 -0
  29. package/dist/src/tasks-runner/pseudo-terminal.d.ts +1 -0
  30. package/dist/src/tasks-runner/pseudo-terminal.js +18 -4
  31. package/dist/src/tasks-runner/run-command.js +24 -8
  32. package/dist/src/tasks-runner/utils.d.ts +1 -0
  33. package/dist/src/tasks-runner/utils.js +4 -0
  34. package/dist/src/utils/ab-testing.d.ts +4 -4
  35. package/dist/src/utils/ab-testing.js +5 -5
  36. package/dist/src/utils/catalog/manager-utils.d.ts +9 -0
  37. package/dist/src/utils/catalog/manager-utils.js +215 -0
  38. package/dist/src/utils/catalog/pnpm-manager.js +4 -102
  39. package/dist/src/utils/catalog/yarn-manager.js +4 -102
  40. package/dist/src/utils/is-sandbox.js +2 -0
  41. package/dist/src/utils/min-release-age/behavior/pnpm.d.ts +6 -5
  42. package/dist/src/utils/min-release-age/behavior/pnpm.js +87 -339
  43. package/dist/src/utils/package-json.d.ts +1 -0
  44. package/dist/src/utils/package-manager.d.ts +6 -3
  45. package/dist/src/utils/package-manager.js +21 -7
  46. package/dist/src/utils/spinner.d.ts +13 -1
  47. package/dist/src/utils/spinner.js +20 -4
  48. package/package.json +16 -12
@@ -208,7 +208,6 @@ class ProjectGraphBuilder {
208
208
  if (!this.graph.dependencies[source]) {
209
209
  this.graph.dependencies[source] = [];
210
210
  }
211
- const isDuplicate = !!this.graph.dependencies[source].find((d) => d.target === target && d.type === type);
212
211
  if (sourceFile) {
213
212
  let fileData = getProjectFileData(source, sourceFile, this.projectFileMap);
214
213
  const isProjectFileData = !!fileData;
@@ -225,14 +224,17 @@ class ProjectGraphBuilder {
225
224
  fileData.deps.push(dep);
226
225
  }
227
226
  }
228
- else if (!isDuplicate) {
227
+ else {
229
228
  // only add to dependencies section if the source file is not specified
230
229
  // and not already added
231
- this.graph.dependencies[source].push({
232
- source: source,
233
- target: target,
234
- type,
235
- });
230
+ const isDuplicate = !!this.graph.dependencies[source].find((d) => d.target === target && d.type === type);
231
+ if (!isDuplicate) {
232
+ this.graph.dependencies[source].push({
233
+ source: source,
234
+ target: target,
235
+ type,
236
+ });
237
+ }
236
238
  }
237
239
  }
238
240
  removeDependenciesWithNode(name) {
@@ -355,11 +357,25 @@ function validateStaticDependency(d, { projects }) {
355
357
  throw new Error(`Source project file is required`);
356
358
  }
357
359
  }
360
+ // Per-project file-name -> FileData index, keyed by the fileMap object so the
361
+ // linear scan (once per source project's file list) is paid once instead of on
362
+ // every addDependency/validateDependency call. Cached against the source's file
363
+ // array reference: a fresh array (new build, or reuse with a swapped list)
364
+ // rebuilds the index.
365
+ const projectFileDataIndex = new WeakMap();
358
366
  function getProjectFileData(source, sourceFile, fileMap) {
359
- let fileData = (fileMap[source] || []).find((f) => f.file === sourceFile);
360
- if (fileData) {
361
- return fileData;
367
+ let byProject = projectFileDataIndex.get(fileMap);
368
+ if (!byProject) {
369
+ byProject = new Map();
370
+ projectFileDataIndex.set(fileMap, byProject);
371
+ }
372
+ const files = fileMap[source] || [];
373
+ let entry = byProject.get(source);
374
+ if (!entry || entry.files !== files) {
375
+ entry = { files, byFile: new Map(files.map((f) => [f.file, f])) };
376
+ byProject.set(source, entry);
362
377
  }
378
+ return entry.byFile.get(sourceFile);
363
379
  }
364
380
  function getNonProjectFileData(sourceFile, files) {
365
381
  const fileData = files.find((f) => f.file === sourceFile);
@@ -91,15 +91,32 @@ function buildSyntheticTargetForRoot(targetName, root, specifiedTarget, defaultT
91
91
  // matching the default plugin's executor are useful.
92
92
  const targetDefaults = readAndPrepareTargetDefaults(targetName, resolvedDefault.executor, root, targetDefaultsConfig);
93
93
  if (targetDefaults && (0, target_merging_1.isCompatibleTarget)(resolvedDefault, targetDefaults)) {
94
- // Stamp executor/command so the default layer merges cleanly on top.
95
- return {
96
- ...targetDefaults,
97
- executor: resolvedDefault.executor,
98
- command: resolvedDefault.command,
99
- };
94
+ // Stamp the default's identity so the default layer merges cleanly on top.
95
+ const synthetic = { ...targetDefaults };
96
+ stampTargetIdentity(synthetic, resolvedDefault);
97
+ return synthetic;
100
98
  }
101
99
  return undefined;
102
100
  }
101
+ // Copies `source`'s identity (executor, command, and — for run-commands — its
102
+ // options.command/commands) onto `target` so the two compare compatible and
103
+ // the synthetic isn't dropped when the default replaces the specified target
104
+ // downstream (#36067). run-commands compatibility keys off
105
+ // options.command/commands, not the top-level command, so both are carried.
106
+ function stampTargetIdentity(target, source) {
107
+ target.executor = source.executor;
108
+ target.command = source.command;
109
+ if (source.executor !== 'nx:run-commands')
110
+ return;
111
+ const { command, commands } = source.options ?? {};
112
+ if (command === undefined && commands === undefined)
113
+ return;
114
+ target.options = {
115
+ ...target.options,
116
+ ...(command !== undefined ? { command } : {}),
117
+ ...(commands !== undefined ? { commands } : {}),
118
+ };
119
+ }
103
120
  function readAndPrepareTargetDefaults(targetName, executor, root, targetDefaultsConfig) {
104
121
  const rawTargetDefaults = readTargetDefaultsForTarget(targetName, targetDefaultsConfig, executor);
105
122
  if (!rawTargetDefaults)
@@ -267,31 +267,9 @@ function mergeCreateNodesResults(specifiedResults, defaultResults, nxJsonConfigu
267
267
  return { projectRootMap, externalNodes, rootMap, configurationSourceMaps };
268
268
  }
269
269
  /**
270
- * Fast matcher for patterns without negations - uses short-circuit evaluation.
271
- */
272
- function matchesSimplePatterns(file, patterns) {
273
- return patterns.some((pattern) => (0, minimatch_1.minimatch)(file, pattern, { dot: true }));
274
- }
275
- /**
276
- * Full matcher for patterns with negations - processes all patterns sequentially.
277
- * Patterns starting with '!' are negation patterns that remove files from the match set.
278
- * Patterns are processed in order, with later patterns overriding earlier ones.
279
- */
280
- function matchesNegationPatterns(file, patterns) {
281
- // If first pattern is negation, start by matching everything
282
- let isMatch = patterns[0].startsWith('!');
283
- for (const pattern of patterns) {
284
- const isNegation = pattern.startsWith('!');
285
- const actualPattern = isNegation ? pattern.substring(1) : pattern;
286
- if ((0, minimatch_1.minimatch)(file, actualPattern, { dot: true })) {
287
- // Last matching pattern wins
288
- isMatch = !isNegation;
289
- }
290
- }
291
- return isMatch;
292
- }
293
- /**
294
- * Creates a matcher function for the given patterns.
270
+ * Creates a matcher function for the given patterns. Globs are compiled once
271
+ * here so matching a file list only runs the pre-parsed regex per file, instead
272
+ * of recompiling every pattern on each call.
295
273
  * @param patterns Array of glob patterns (can include negation patterns starting with '!')
296
274
  * @param emptyValue Value to return when patterns array is empty
297
275
  * @returns A function that checks if a file matches the patterns
@@ -301,9 +279,31 @@ function createMatcher(patterns, emptyValue) {
301
279
  return () => emptyValue;
302
280
  }
303
281
  const hasNegationPattern = patterns.some((p) => p.startsWith('!'));
304
- return hasNegationPattern
305
- ? (file) => matchesNegationPatterns(file, patterns)
306
- : (file) => matchesSimplePatterns(file, patterns);
282
+ if (hasNegationPattern) {
283
+ // Patterns are processed in order, with later matches overriding earlier
284
+ // ones; a leading negation starts from "matches everything".
285
+ const compiled = patterns.map((pattern) => {
286
+ const isNegation = pattern.startsWith('!');
287
+ return {
288
+ isNegation,
289
+ matcher: new minimatch_1.Minimatch(isNegation ? pattern.substring(1) : pattern, {
290
+ dot: true,
291
+ }),
292
+ };
293
+ });
294
+ const initialMatch = patterns[0].startsWith('!');
295
+ return (file) => {
296
+ let isMatch = initialMatch;
297
+ for (const { isNegation, matcher } of compiled) {
298
+ if (matcher.match(file)) {
299
+ isMatch = !isNegation;
300
+ }
301
+ }
302
+ return isMatch;
303
+ };
304
+ }
305
+ const compiled = patterns.map((p) => new minimatch_1.Minimatch(p, { dot: true }));
306
+ return (file) => compiled.some((m) => m.match(file));
307
307
  }
308
308
  function findMatchingConfigFiles(projectFiles, include, exclude) {
309
309
  // projectFiles already comes from multiGlobWithWorkspaceContext for the
@@ -7,6 +7,9 @@ const project_graph_1 = require("../../project-graph/project-graph");
7
7
  const configuration_1 = require("../../config/configuration");
8
8
  const async_iterator_1 = require("../../utils/async-iterator");
9
9
  const executor_utils_1 = require("../../command-line/run/executor-utils");
10
+ // Batch workers are inside an Nx run just like task workers (see
11
+ // bin/run-executor.ts) — mark it so nested tooling can detect Nx.
12
+ process.env.NX_CLI_SET = 'true';
10
13
  function getBatchExecutor(executorName, projects) {
11
14
  const [nodeModule, exportName] = (0, executor_utils_1.parseExecutor)(executorName);
12
15
  return (0, executor_utils_1.getExecutorInformation)(nodeModule, exportName, workspace_root_1.workspaceRoot, projects);
@@ -251,6 +251,12 @@ class DbCache {
251
251
  logger_1.logger.warn('The HTTP remote cache is not yet supported in the wasm build of Nx.');
252
252
  return null;
253
253
  }
254
+ // The cache request runs in Rust (reqwest), which honors
255
+ // NODE_TLS_REJECT_UNAUTHORIZED but bypasses Node's TLS stack, so Node's own
256
+ // insecure-TLS warning never fires for it. Re-emit Node's warning here.
257
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
258
+ process.emitWarning("Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.");
259
+ }
254
260
  return new native_1.HttpRemoteCache();
255
261
  }
256
262
  return null;
@@ -13,6 +13,7 @@ export declare class PseudoTerminal {
13
13
  constructor(rustPseudoTerminal: RustPseudoTerminal);
14
14
  init(): Promise<void>;
15
15
  shutdown(code: number): void;
16
+ private releaseChild;
16
17
  runCommand(command: string, { cwd, execArgv, jsEnv, quiet, tty, }?: {
17
18
  cwd?: string;
18
19
  execArgv?: string[];
@@ -8,17 +8,18 @@ const socket_utils_1 = require("../daemon/socket-utils");
8
8
  const native_1 = require("../native");
9
9
  const pseudo_ipc_1 = require("./pseudo-ipc");
10
10
  const exit_codes_1 = require("../utils/exit-codes");
11
- // Register single event listeners for all pseudo-terminal instances
12
- const pseudoTerminalShutdownCallbacks = [];
11
+ // Kill any children still alive when Nx exits. Terminals remove themselves once
12
+ // their children exit (see releaseChild), so finished runs skip a per-terminal scan.
13
+ const activePseudoTerminals = new Set();
13
14
  process.on('exit', (code) => {
14
- pseudoTerminalShutdownCallbacks.forEach((cb) => cb(code));
15
+ activePseudoTerminals.forEach((t) => t.shutdown(code));
15
16
  });
16
17
  function createPseudoTerminal(skipSupportCheck = false) {
17
18
  if (!skipSupportCheck && !PseudoTerminal.isSupported()) {
18
19
  throw new Error('Pseudo terminal is not supported on this platform.');
19
20
  }
20
21
  const pseudoTerminal = new PseudoTerminal(new native_1.RustPseudoTerminal());
21
- pseudoTerminalShutdownCallbacks.push(pseudoTerminal.shutdown.bind(pseudoTerminal));
22
+ activePseudoTerminals.add(pseudoTerminal);
22
23
  return pseudoTerminal;
23
24
  }
24
25
  let id = 0;
@@ -56,9 +57,21 @@ class PseudoTerminal {
56
57
  this.pseudoIPC.close();
57
58
  }
58
59
  }
60
+ // Once all children have exited, drop the process-exit handler and close IPC.
61
+ releaseChild(cp) {
62
+ this.childProcesses.delete(cp);
63
+ if (this.childProcesses.size === 0) {
64
+ activePseudoTerminals.delete(this);
65
+ if (this.initialized) {
66
+ this.pseudoIPC.close();
67
+ this.initialized = false;
68
+ }
69
+ }
70
+ }
59
71
  runCommand(command, { cwd, execArgv, jsEnv, quiet, tty, } = {}) {
60
72
  const cp = new PseudoTtyProcess(this.rustPseudoTerminal, this.rustPseudoTerminal.runCommand(command, cwd, jsEnv, execArgv, quiet, tty));
61
73
  this.childProcesses.add(cp);
74
+ cp.onExit(() => this.releaseChild(cp));
62
75
  return cp;
63
76
  }
64
77
  async fork(id, script, { cwd, execArgv, jsEnv, quiet, commandLabel, }) {
@@ -67,6 +80,7 @@ class PseudoTerminal {
67
80
  }
68
81
  const cp = new PseudoTtyProcessWithSend(this.rustPseudoTerminal, this.rustPseudoTerminal.fork(id, script, this.pseudoIPCPath, cwd, jsEnv, execArgv, quiet, commandLabel), id, this.pseudoIPC);
69
82
  this.childProcesses.add(cp);
83
+ cp.onExit(() => this.releaseChild(cp));
70
84
  await this.pseudoIPC.waitForChildReady(id);
71
85
  return cp;
72
86
  }
@@ -50,6 +50,27 @@ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
50
50
  const originalStderrWrite = process.stderr.write.bind(process.stderr);
51
51
  const originalConsoleLog = console.log.bind(console);
52
52
  const originalConsoleError = console.error.bind(console);
53
+ // Only used in the non-TTY error path. In non-TTY the function `process.exit(1)`s
54
+ // before the `applyChanges` branch can auto-sync, so the auto-sync bullet keeps
55
+ // the "interactive environments" qualifier to avoid suggesting a config change
56
+ // that won't help in CI/agent contexts.
57
+ const SYNC_FIX_MESSAGE_LINES = [
58
+ 'To sync the workspace:',
59
+ '- Run `nx sync` (no flags) to sync now.',
60
+ '- Run `nx sync:check` to preview the changes without modifying any files.',
61
+ '- Set `sync.applyChanges` to `true` in your `nx.json` to sync automatically when running tasks in interactive environments.',
62
+ '',
63
+ 'For more information, refer to the docs: https://nx.dev/concepts/sync-generators',
64
+ ];
65
+ const APPLY_CHANGES_FALSE_FIX_MESSAGE_LINES = [
66
+ 'Your workspace is set to not sync automatically (`sync.applyChanges` is `false` in your `nx.json`).',
67
+ 'Run `nx sync` (no flags) to sync now, or set `sync.applyChanges` to `true` to sync automatically before each task run.',
68
+ ];
69
+ const SYNC_SKIPPED_FIX_MESSAGE_LINES = [
70
+ 'This could lead to unexpected results or errors when running tasks.',
71
+ '',
72
+ 'Run `nx sync` (no flags) later to sync the workspace.',
73
+ ];
53
74
  async function getTerminalOutputLifeCycle(initiatingProject, initiatingTasks, projectNames, tasks, taskGraph, nxArgs, nxJson, overrides) {
54
75
  const overridesWithoutHidden = { ...overrides };
55
76
  delete overridesWithoutHidden['__overrides_unparsed__'];
@@ -414,7 +435,6 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(projectGraph, nxJson, project
414
435
  const failedSyncGeneratorsFixMessageLines = (0, sync_generators_1.getFailedSyncGeneratorsFixMessageLines)(results, nxArgs.verbose);
415
436
  const outOfSyncTitle = 'The workspace is out of sync';
416
437
  const resultBodyLines = (0, sync_generators_1.getSyncGeneratorSuccessResultsMessageLines)(results);
417
- const fixMessage = 'Make sure to run `nx sync` to apply the identified changes or set `sync.applyChanges` to `true` in your `nx.json` to apply them automatically when running tasks in interactive environments.';
418
438
  if (!process.stdout.isTTY) {
419
439
  // If the user is running a non-TTY environment we
420
440
  // throw an error to stop the execution of the tasks.
@@ -429,7 +449,7 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(projectGraph, nxJson, project
429
449
  else {
430
450
  output_1.output.error({
431
451
  title: outOfSyncTitle,
432
- bodyLines: [...resultBodyLines, '', fixMessage],
452
+ bodyLines: [...resultBodyLines, '', ...SYNC_FIX_MESSAGE_LINES],
433
453
  });
434
454
  if (anySyncGeneratorsFailed) {
435
455
  output_1.output.error({
@@ -463,8 +483,7 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(projectGraph, nxJson, project
463
483
  bodyLines: [
464
484
  ...resultBodyLines,
465
485
  '',
466
- 'Your workspace is set to not apply the identified changes automatically (`sync.applyChanges` is set to `false` in your `nx.json`).',
467
- fixMessage,
486
+ ...APPLY_CHANGES_FALSE_FIX_MESSAGE_LINES,
468
487
  ],
469
488
  });
470
489
  if (anySyncGeneratorsFailed) {
@@ -549,10 +568,7 @@ async function ensureWorkspaceIsInSyncAndGetGraphs(projectGraph, nxJson, project
549
568
  else {
550
569
  output_1.output.warn({
551
570
  title: 'Syncing the workspace was skipped',
552
- bodyLines: [
553
- 'This could lead to unexpected results or errors when running tasks.',
554
- fixMessage,
555
- ],
571
+ bodyLines: SYNC_SKIPPED_FIX_MESSAGE_LINES,
556
572
  });
557
573
  }
558
574
  }
@@ -56,5 +56,6 @@ export declare function getUnparsedOverrideArgs(task: Task): string[];
56
56
  export declare function getPrintableCommandArgsForTask(task: Task): string[];
57
57
  export declare function getSerializedArgsForTask(task: Task, isVerbose: boolean): string[];
58
58
  export declare function shouldStreamOutput(task: Task, initiatingProject: string | null): boolean;
59
+ export declare function isCacheableTask(task: Task): boolean;
59
60
  export declare function unparse(options: Object): string[];
60
61
  export declare function createTaskId(project: string, target: string, configuration: string | undefined): string;
@@ -24,6 +24,7 @@ exports.getUnparsedOverrideArgs = getUnparsedOverrideArgs;
24
24
  exports.getPrintableCommandArgsForTask = getPrintableCommandArgsForTask;
25
25
  exports.getSerializedArgsForTask = getSerializedArgsForTask;
26
26
  exports.shouldStreamOutput = shouldStreamOutput;
27
+ exports.isCacheableTask = isCacheableTask;
27
28
  exports.unparse = unparse;
28
29
  exports.createTaskId = createTaskId;
29
30
  const minimatch_1 = require("minimatch");
@@ -420,6 +421,9 @@ function shouldStreamOutput(task, initiatingProject) {
420
421
  return true;
421
422
  return false;
422
423
  }
424
+ function isCacheableTask(task) {
425
+ return task.cache;
426
+ }
423
427
  function longRunningTask(task) {
424
428
  const t = task.target.target;
425
429
  return (task.continuous ||
@@ -2,11 +2,11 @@ export declare const NX_CLOUD_URL = "https://nx.dev/nx-cloud";
2
2
  /**
3
3
  * Clickable Nx Cloud marketing link for cloud prompt footers. The visible text
4
4
  * stays the clean `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
5
- * without OSC 8 support just render the bare URL (CLOUD-4642). The medium is
6
- * per-command because `nx init` and `nx migrate` share a footer but report
7
- * different mediums.
5
+ * without OSC 8 support just render the bare URL (CLOUD-4642). The content tag
6
+ * is per-command because `nx init` and `nx migrate` share a footer but report
7
+ * different commands.
8
8
  */
9
- export declare function nxCloudHyperlink(utmMedium: string): string;
9
+ export declare function nxCloudHyperlink(utmContent: string): string;
10
10
  /**
11
11
  * Meta payload types for recordStat telemetry (matches CNW format).
12
12
  */
@@ -14,12 +14,12 @@ exports.NX_CLOUD_URL = 'https://nx.dev/nx-cloud';
14
14
  /**
15
15
  * Clickable Nx Cloud marketing link for cloud prompt footers. The visible text
16
16
  * stays the clean `NX_CLOUD_URL` while clicks carry UTM attribution; terminals
17
- * without OSC 8 support just render the bare URL (CLOUD-4642). The medium is
18
- * per-command because `nx init` and `nx migrate` share a footer but report
19
- * different mediums.
17
+ * without OSC 8 support just render the bare URL (CLOUD-4642). The content tag
18
+ * is per-command because `nx init` and `nx migrate` share a footer but report
19
+ * different commands.
20
20
  */
21
- function nxCloudHyperlink(utmMedium) {
22
- const tracked = `${exports.NX_CLOUD_URL}?utm_source=nx-cli&utm_medium=${utmMedium}`;
21
+ function nxCloudHyperlink(utmContent) {
22
+ const tracked = `${exports.NX_CLOUD_URL}?utm_source=nx-cli&utm_medium=cli&utm_campaign=nx-cloud-connect&utm_content=${utmContent}`;
23
23
  return (0, terminal_link_1.terminalLink)(exports.NX_CLOUD_URL, tracked);
24
24
  }
25
25
  const messageOptions = {
@@ -0,0 +1,9 @@
1
+ import type { Tree } from '../../generators/tree';
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;
5
+ export declare function updateCatalogVersionsInFile(filename: string, treeOrRoot: Tree | string, updates: Array<{
6
+ packageName: string;
7
+ version: string;
8
+ catalogName?: string;
9
+ }>): void;
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readCatalogConfigFromFs = readCatalogConfigFromFs;
4
+ exports.readCatalogConfigFromTree = readCatalogConfigFromTree;
5
+ 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
+ const js_yaml_1 = require("@zkochan/js-yaml");
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = require("node:path");
12
+ const yaml_1 = require("yaml");
13
+ const fileutils_1 = require("../fileutils");
14
+ const output_1 = require("../output");
15
+ // Walks `path` resolving aliases at every step so structural checks can
16
+ // see through `key: *anchor` references. Neither `doc.getIn` nor
17
+ // `doc.hasIn` traverse aliases on their own. Returns the resolved node, or
18
+ // `undefined` if any ancestor isn't a map.
19
+ function resolveAt(doc, path) {
20
+ let node = doc.contents;
21
+ for (let i = 0; i < path.length; i++) {
22
+ if ((0, yaml_1.isAlias)(node))
23
+ node = node.resolve(doc);
24
+ if (!(0, yaml_1.isMap)(node))
25
+ return undefined;
26
+ node = node.get(path[i], true);
27
+ }
28
+ if ((0, yaml_1.isAlias)(node))
29
+ node = node.resolve(doc);
30
+ return node;
31
+ }
32
+ function isMapAt(doc, path) {
33
+ return (0, yaml_1.isMap)(resolveAt(doc, path));
34
+ }
35
+ function existsAt(doc, path) {
36
+ return resolveAt(doc, path) !== undefined;
37
+ }
38
+ // `doc.getIn` does not traverse aliases. For aliased paths it returns
39
+ // undefined even when the resolved node has a value. Use the alias-aware
40
+ // walk and unwrap scalar nodes to their JS value.
41
+ function getValueAt(doc, path) {
42
+ const node = resolveAt(doc, path);
43
+ return (0, yaml_1.isScalar)(node) ? node.value : node;
44
+ }
45
+ // Walks `targetPath` resolving aliases at every step and mutates the
46
+ // resolved map directly so anchors are preserved. When a step is missing
47
+ // or holds a null placeholder (`key:` with no value), creates a fresh map
48
+ // inside the current parent and carries over the placeholder's comments
49
+ // and anchor so they aren't dropped. A dropped anchor would leave any
50
+ // alias referencing it unresolved and make `String(doc)` throw. Falls back
51
+ // to `doc.setIn` when the current parent isn't a map: an empty-document
52
+ // root gets bootstrapped, while a non-map value where a catalog map was
53
+ // expected makes `setIn` throw, surfacing the malformed config.
54
+ function setThroughAliases(doc, targetPath, value) {
55
+ let parent = doc.contents;
56
+ if ((0, yaml_1.isAlias)(parent))
57
+ parent = parent.resolve(doc);
58
+ for (let i = 0; i < targetPath.length - 1; i++) {
59
+ if (!(0, yaml_1.isMap)(parent)) {
60
+ doc.setIn(targetPath, value);
61
+ return;
62
+ }
63
+ let next = parent.get(targetPath[i], true);
64
+ const nextWasAlias = (0, yaml_1.isAlias)(next);
65
+ if ((0, yaml_1.isAlias)(next))
66
+ next = next.resolve(doc);
67
+ const placeholder = (0, yaml_1.isScalar)(next) && next.value === null ? next : undefined;
68
+ if (next === undefined || placeholder) {
69
+ let cur = parent;
70
+ for (let j = i; j < targetPath.length - 1; j++) {
71
+ const fresh = new yaml_1.YAMLMap();
72
+ if (j === i && placeholder) {
73
+ if (placeholder.comment)
74
+ fresh.comment = placeholder.comment;
75
+ if (placeholder.commentBefore)
76
+ fresh.commentBefore = placeholder.commentBefore;
77
+ // Copy the anchor only for a directly-held placeholder. When it
78
+ // was reached through an alias, the anchor still lives on the
79
+ // definition node, so re-emitting it here would duplicate it.
80
+ if (!nextWasAlias && placeholder.anchor)
81
+ fresh.anchor = placeholder.anchor;
82
+ }
83
+ cur.set(targetPath[j], fresh);
84
+ cur = fresh;
85
+ }
86
+ cur.set(targetPath[targetPath.length - 1], value);
87
+ return;
88
+ }
89
+ parent = next;
90
+ }
91
+ if (!(0, yaml_1.isMap)(parent)) {
92
+ doc.setIn(targetPath, value);
93
+ return;
94
+ }
95
+ parent.set(targetPath[targetPath.length - 1], value);
96
+ }
97
+ function readCatalogConfigFromFs(filename, fullPath) {
98
+ try {
99
+ return (0, fileutils_1.readYamlFile)(fullPath);
100
+ }
101
+ catch (error) {
102
+ output_1.output.warn({
103
+ title: `Unable to parse ${filename}`,
104
+ bodyLines: [error.toString()],
105
+ });
106
+ return null;
107
+ }
108
+ }
109
+ function readCatalogConfigFromTree(filename, tree) {
110
+ const content = tree.read(filename, 'utf-8');
111
+ try {
112
+ return (0, js_yaml_1.load)(content, { filename });
113
+ }
114
+ catch (error) {
115
+ output_1.output.warn({
116
+ title: `Unable to parse ${filename}`,
117
+ bodyLines: [error.toString()],
118
+ });
119
+ return null;
120
+ }
121
+ }
122
+ function updateCatalogVersionsInFile(filename, treeOrRoot, updates) {
123
+ let checkExists;
124
+ let readYaml;
125
+ let writeYaml;
126
+ if (typeof treeOrRoot === 'string') {
127
+ const configPath = (0, node_path_1.join)(treeOrRoot, filename);
128
+ checkExists = () => (0, node_fs_1.existsSync)(configPath);
129
+ readYaml = () => (0, node_fs_1.readFileSync)(configPath, 'utf-8');
130
+ writeYaml = (content) => (0, node_fs_1.writeFileSync)(configPath, content, 'utf-8');
131
+ }
132
+ else {
133
+ checkExists = () => treeOrRoot.exists(filename);
134
+ readYaml = () => treeOrRoot.read(filename, 'utf-8');
135
+ writeYaml = (content) => treeOrRoot.write(filename, content);
136
+ }
137
+ if (!checkExists()) {
138
+ output_1.output.warn({
139
+ title: `No ${filename} found`,
140
+ bodyLines: [
141
+ `Cannot update catalog versions without a ${filename} file.`,
142
+ `Create a ${filename} file to use catalogs.`,
143
+ ],
144
+ });
145
+ return;
146
+ }
147
+ try {
148
+ // parseDocument keeps comments and anchors so a catalog bump doesn't
149
+ // rewrite the user's config file.
150
+ const lineCounter = new yaml_1.LineCounter();
151
+ const doc = (0, yaml_1.parseDocument)(readYaml(), { lineCounter });
152
+ // parseDocument collects syntax errors instead of throwing; surface them
153
+ // now, with their line/column detail, rather than failing later in
154
+ // `String(doc)` with a generic message or skipping a no-op write on a
155
+ // malformed file (the previous js-yaml `load()` threw here).
156
+ if (doc.errors.length > 0) {
157
+ throw new Error(doc.errors.map((e) => e.message).join('\n'));
158
+ }
159
+ // A dangling alias (`*ref` with no matching `&ref`) is not a syntax error,
160
+ // so parseDocument leaves it out of doc.errors. Surface it here with the
161
+ // same line/column detail the old js-yaml `load()` reported; otherwise a
162
+ // broken reference is silently overwritten or written back untouched.
163
+ const unresolvedAliases = [];
164
+ (0, yaml_1.visit)(doc, {
165
+ Alias(_key, node) {
166
+ if (node.resolve(doc) !== undefined)
167
+ return;
168
+ const pos = node.range ? lineCounter.linePos(node.range[0]) : undefined;
169
+ unresolvedAliases.push(pos
170
+ ? `Unresolved alias "${node.source}" at line ${pos.line}, column ${pos.col}`
171
+ : `Unresolved alias "${node.source}"`);
172
+ },
173
+ });
174
+ if (unresolvedAliases.length > 0) {
175
+ throw new Error(unresolvedAliases.join('\n'));
176
+ }
177
+ let hasChanges = false;
178
+ for (const update of updates) {
179
+ const { packageName, version, catalogName } = update;
180
+ const normalizedCatalogName = catalogName === 'default' ? undefined : catalogName;
181
+ let targetPath;
182
+ if (!normalizedCatalogName) {
183
+ // An empty `catalog:` placeholder must not claim the default route
184
+ // when `catalogs.default` is populated; that would create a
185
+ // duplicate-default config rejected by pnpm.
186
+ if (isMapAt(doc, ['catalog'])) {
187
+ targetPath = ['catalog', packageName];
188
+ }
189
+ else if (existsAt(doc, ['catalogs', 'default'])) {
190
+ targetPath = ['catalogs', 'default', packageName];
191
+ }
192
+ else {
193
+ targetPath = ['catalog', packageName];
194
+ }
195
+ }
196
+ else {
197
+ targetPath = ['catalogs', normalizedCatalogName, packageName];
198
+ }
199
+ if (getValueAt(doc, targetPath) !== version) {
200
+ setThroughAliases(doc, targetPath, version);
201
+ hasChanges = true;
202
+ }
203
+ }
204
+ if (hasChanges) {
205
+ writeYaml(String(doc));
206
+ }
207
+ }
208
+ catch (error) {
209
+ output_1.output.error({
210
+ title: 'Failed to update catalog versions',
211
+ bodyLines: [error instanceof Error ? error.message : String(error)],
212
+ });
213
+ throw error;
214
+ }
215
+ }