nx 23.1.0 → 23.1.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 (126) hide show
  1. package/dist/bin/nx.js +2 -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/analytics/analytics.d.ts +6 -0
  5. package/dist/src/analytics/analytics.js +16 -8
  6. package/dist/src/analytics/index.d.ts +1 -1
  7. package/dist/src/analytics/index.js +2 -1
  8. package/dist/src/command-line/add/add.js +8 -1
  9. package/dist/src/command-line/graph/graph.d.ts +1 -0
  10. package/dist/src/command-line/graph/graph.js +15 -6
  11. package/dist/src/command-line/import/import.js +1 -1
  12. package/dist/src/command-line/init/ai-agent-prompts.d.ts +1 -1
  13. package/dist/src/command-line/init/ai-agent-prompts.js +7 -6
  14. package/dist/src/command-line/init/command-object.js +10 -2
  15. package/dist/src/command-line/init/configure-plugins.js +3 -2
  16. package/dist/src/command-line/init/implementation/add-nx-to-monorepo.js +4 -2
  17. package/dist/src/command-line/init/implementation/add-nx-to-nest.js +4 -3
  18. package/dist/src/command-line/init/implementation/add-nx-to-npm-repo.js +4 -3
  19. package/dist/src/command-line/init/implementation/add-nx-to-turborepo.js +4 -3
  20. package/dist/src/command-line/init/implementation/angular/index.js +4 -2
  21. package/dist/src/command-line/init/implementation/angular/integrated-workspace.js +2 -5
  22. package/dist/src/command-line/init/implementation/angular/legacy-angular-versions.js +3 -0
  23. package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +4 -1
  24. package/dist/src/command-line/init/implementation/utils.d.ts +3 -3
  25. package/dist/src/command-line/init/implementation/utils.js +22 -3
  26. package/dist/src/command-line/migrate/execute-migration.d.ts +72 -0
  27. package/dist/src/command-line/migrate/execute-migration.js +389 -0
  28. package/dist/src/command-line/migrate/migrate-ui-api.js +17 -12
  29. package/dist/src/command-line/migrate/migrate.d.ts +1 -45
  30. package/dist/src/command-line/migrate/migrate.js +33 -376
  31. package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +1 -1
  32. package/dist/src/command-line/nx-cloud/connect/view-logs.js +2 -0
  33. package/dist/src/command-line/release/utils/git.js +6 -2
  34. package/dist/src/command-line/release/utils/release-graph.d.ts +2 -2
  35. package/dist/src/command-line/release/utils/remote-release-clients/github.js +1 -1
  36. package/dist/src/command-line/release/utils/remote-release-clients/gitlab.js +1 -1
  37. package/dist/src/command-line/release/utils/shared.d.ts +2 -2
  38. package/dist/src/command-line/show/show-target/info.js +11 -2
  39. package/dist/src/command-line/show/show-target/inputs.js +50 -153
  40. package/dist/src/command-line/show/show-target/outputs.js +48 -173
  41. package/dist/src/command-line/show/show-target/utils.d.ts +28 -0
  42. package/dist/src/command-line/show/show-target/utils.js +83 -3
  43. package/dist/src/command-line/yargs-utils/shared-options.d.ts +1 -1
  44. package/dist/src/commands-runner/create-command-graph.js +5 -0
  45. package/dist/src/config/nx-json.d.ts +5 -3
  46. package/dist/src/core/graph/main.js +1 -1
  47. package/dist/src/daemon/client/client.d.ts +0 -2
  48. package/dist/src/daemon/client/client.js +17 -19
  49. package/dist/src/daemon/server/handle-tasks-execution-hooks.d.ts +1 -1
  50. package/dist/src/daemon/server/server.js +7 -4
  51. package/dist/src/devkit-internals.d.ts +3 -0
  52. package/dist/src/devkit-internals.js +10 -1
  53. package/dist/src/generators/utils/nx-json.js +13 -3
  54. package/dist/src/hasher/check-task-files.d.ts +86 -0
  55. package/dist/src/hasher/check-task-files.js +378 -0
  56. package/dist/src/native/index.d.ts +21 -2
  57. package/dist/src/native/native-bindings.js +2 -0
  58. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  59. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  60. package/dist/src/plugins/js/lock-file/npm-parser.js +71 -27
  61. package/dist/src/plugins/js/lock-file/pnpm-parser.js +50 -17
  62. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +1 -1
  63. package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +3 -0
  64. package/dist/src/plugins/js/lock-file/yarn-parser.js +32 -5
  65. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +12 -4
  66. package/dist/src/plugins/js/utils/register.d.ts +18 -0
  67. package/dist/src/plugins/js/utils/register.js +81 -0
  68. package/dist/src/plugins/js/utils/typescript.d.ts +9 -0
  69. package/dist/src/plugins/js/utils/typescript.js +15 -0
  70. package/dist/src/project-graph/build-project-graph.js +7 -1
  71. package/dist/src/project-graph/error-types.js +41 -1
  72. package/dist/src/project-graph/file-utils.js +6 -1
  73. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +4 -0
  74. package/dist/src/project-graph/plugins/loaded-nx-plugin.js +1 -0
  75. package/dist/src/project-graph/plugins/transpiler.js +4 -0
  76. package/dist/src/project-graph/project-graph.js +3 -0
  77. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.d.ts +10 -12
  78. package/dist/src/project-graph/utils/project-configuration/name-substitution-manager.js +93 -63
  79. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.d.ts +5 -8
  80. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.js +11 -14
  81. package/dist/src/project-graph/utils/project-configuration/source-maps.d.ts +12 -5
  82. package/dist/src/project-graph/utils/project-configuration/source-maps.js +21 -7
  83. package/dist/src/project-graph/utils/project-configuration/target-defaults.d.ts +1 -1
  84. package/dist/src/project-graph/utils/project-configuration/target-defaults.js +26 -27
  85. package/dist/src/project-graph/utils/project-configuration/target-merging.js +50 -8
  86. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +16 -3
  87. package/dist/src/project-graph/utils/project-configuration/utils.d.ts +15 -0
  88. package/dist/src/project-graph/utils/project-configuration/utils.js +42 -6
  89. package/dist/src/project-graph/utils/project-configuration-utils.d.ts +15 -12
  90. package/dist/src/project-graph/utils/project-configuration-utils.js +79 -101
  91. package/dist/src/tasks-runner/life-cycles/performance-analysis.d.ts +11 -4
  92. package/dist/src/tasks-runner/life-cycles/performance-analysis.js +15 -6
  93. package/dist/src/tasks-runner/life-cycles/performance-life-cycle.js +5 -4
  94. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +18 -8
  95. package/dist/src/tasks-runner/life-cycles/performance-report.js +66 -29
  96. package/dist/src/tasks-runner/run-command.js +4 -2
  97. package/dist/src/tasks-runner/task-env.d.ts +9 -0
  98. package/dist/src/tasks-runner/task-env.js +26 -2
  99. package/dist/src/tasks-runner/task-orchestrator.d.ts +4 -1
  100. package/dist/src/tasks-runner/task-orchestrator.js +23 -10
  101. package/dist/src/utils/acknowledge-build-scripts.d.ts +13 -0
  102. package/dist/src/utils/acknowledge-build-scripts.js +102 -0
  103. package/dist/src/utils/analytics-prompt.d.ts +0 -6
  104. package/dist/src/utils/analytics-prompt.js +0 -51
  105. package/dist/src/utils/catalog/manager-utils.d.ts +1 -2
  106. package/dist/src/utils/catalog/manager-utils.js +22 -5
  107. package/dist/src/utils/catalog/pnpm-manager.d.ts +1 -0
  108. package/dist/src/utils/catalog/pnpm-manager.js +3 -15
  109. package/dist/src/utils/catalog/yarn-manager.d.ts +1 -0
  110. package/dist/src/utils/catalog/yarn-manager.js +3 -15
  111. package/dist/src/utils/command-line-utils.js +30 -20
  112. package/dist/src/utils/git-revision.d.ts +12 -0
  113. package/dist/src/utils/git-revision.js +25 -0
  114. package/dist/src/utils/git-utils.d.ts +16 -3
  115. package/dist/src/utils/git-utils.js +146 -52
  116. package/dist/src/utils/min-release-age/behavior/pnpm.js +9 -7
  117. package/dist/src/utils/package-json.js +11 -7
  118. package/dist/src/utils/package-manager.d.ts +3 -1
  119. package/dist/src/utils/package-manager.js +28 -4
  120. package/dist/src/utils/provenance.js +12 -2
  121. package/dist/src/utils/workspace-id.d.ts +20 -0
  122. package/dist/src/utils/workspace-id.js +54 -0
  123. package/migrations.json +7 -1
  124. package/package.json +22 -17
  125. package/dist/src/migrations/update-17-3-0/nx-release-path.d.ts +0 -3
  126. package/dist/src/migrations/update-17-3-0/nx-release-path.js +0 -47
@@ -326,6 +326,7 @@ export interface EventDimensions {
326
326
  packageName: string
327
327
  packageVersion: string
328
328
  duration: string
329
+ sampleRate: string
329
330
  taskCount: string
330
331
  projectCount: string
331
332
  cachedTaskCount: string
@@ -487,14 +488,14 @@ export interface HashInputs {
487
488
  * so the caller can set it as an env var for child processes.
488
489
  * Used by CLI and daemon.
489
490
  */
490
- export declare function initializeTelemetry(connection: ExternalObject<NxDbConnection>, workspaceId: string, userId: string, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): string
491
+ export declare function initializeTelemetry(connection: ExternalObject<NxDbConnection>, workspaceId: string, userId: string | undefined | null, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): string
491
492
 
492
493
  /**
493
494
  * Initialize telemetry with a pre-fetched session ID.
494
495
  * No DB connection — used by plugin workers that inherit the
495
496
  * session ID from their parent process via env var.
496
497
  */
497
- export declare function initializeTelemetryWithSessionId(sessionId: string, workspaceId: string, userId: string, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): void
498
+ export declare function initializeTelemetryWithSessionId(sessionId: string, workspaceId: string, userId: string | undefined | null, nxVersion: string, packageManagerName: string, packageManagerVersion: string | undefined | null, nodeVersion: string, osArch: string, osPlatform: string, osRelease: string, isCi: boolean, isNxCloud: boolean): void
498
499
 
499
500
  export interface InputsInput {
500
501
  input: string
@@ -557,6 +558,24 @@ export interface Link {
557
558
 
558
559
  export declare function logDebug(message: string): void
559
560
 
561
+ /**
562
+ * Checks which `paths` match the given `globs`, using the same glob engine
563
+ * as the task hasher (`build_glob_set`). Used to statically match
564
+ * `dependentTasksOutputFiles` globs against candidate paths.
565
+ */
566
+ export declare function matchGlobPaths(globs: Array<string>, paths: Array<string>): Array<boolean>
567
+
568
+ /**
569
+ * Statically checks which `paths` would be captured by the given output
570
+ * `entries`, without touching the file system. Mirrors `expand_outputs`
571
+ * semantics: entries match themselves and anything nested under them (so a
572
+ * directory entry captures its contents), negated (`!`-prefixed) entries
573
+ * exclude matches from the whole entry set, and a non-empty list with only
574
+ * negated entries matches everything not excluded. An empty list matches
575
+ * nothing.
576
+ */
577
+ export declare function matchOutputPaths(entries: Array<string>, paths: Array<string>): Array<boolean>
578
+
560
579
  /** Combined metadata for groups and processes */
561
580
  export interface Metadata {
562
581
  /** Group-level metadata */
@@ -625,6 +625,8 @@ module.exports.isEditorInstalled = nativeBinding.isEditorInstalled
625
625
  module.exports.killProcessTree = nativeBinding.killProcessTree
626
626
  module.exports.killProcessTreeGraceful = nativeBinding.killProcessTreeGraceful
627
627
  module.exports.logDebug = nativeBinding.logDebug
628
+ module.exports.matchGlobPaths = nativeBinding.matchGlobPaths
629
+ module.exports.matchOutputPaths = nativeBinding.matchOutputPaths
628
630
  module.exports.parseTaskStatus = nativeBinding.parseTaskStatus
629
631
  module.exports.remove = nativeBinding.remove
630
632
  module.exports.restoreTerminal = nativeBinding.restoreTerminal
Binary file
@@ -133,6 +133,22 @@ function findV3Version(snapshot, packageName) {
133
133
  }
134
134
  function getDependencies(data, keyMap, ctx) {
135
135
  const dependencies = [];
136
+ // Memoizes semver `satisfies(version, range)` for this dependency walk. The
137
+ // same (version, range) pairs recur across many edges, so the range parse
138
+ // happens once per distinct pair instead of once per edge. Scoped to the walk
139
+ // (not module-global) so V8 collects it when dependency creation finishes
140
+ // rather than retaining it for the daemon's lifetime.
141
+ const versionSatisfiesCache = new Map();
142
+ const cachedSatisfies = (version, range) => {
143
+ const key = `${version}\n${range}`;
144
+ const cached = versionSatisfiesCache.get(key);
145
+ if (cached !== undefined) {
146
+ return cached;
147
+ }
148
+ const result = (0, semver_1.satisfies)(version, range);
149
+ versionSatisfiesCache.set(key, result);
150
+ return result;
151
+ };
136
152
  if (data.lockfileVersion > 1) {
137
153
  Object.entries(data.packages).forEach(([path, snapshot]) => {
138
154
  // we are skipping workspaces packages
@@ -147,7 +163,7 @@ function getDependencies(data, keyMap, ctx) {
147
163
  ].forEach((section) => {
148
164
  if (section) {
149
165
  Object.entries(section).forEach(([name, versionRange]) => {
150
- const target = findTarget(path, keyMap, name, versionRange);
166
+ const target = findTarget(path, keyMap, name, versionRange, cachedSatisfies);
151
167
  if (target) {
152
168
  const dep = {
153
169
  source: sourceName,
@@ -164,12 +180,12 @@ function getDependencies(data, keyMap, ctx) {
164
180
  }
165
181
  else {
166
182
  Object.entries(data.dependencies).forEach(([packageName, snapshot]) => {
167
- addV1NodeDependencies(`node_modules/${packageName}`, snapshot, dependencies, keyMap, ctx);
183
+ addV1NodeDependencies(`node_modules/${packageName}`, snapshot, dependencies, keyMap, ctx, cachedSatisfies);
168
184
  });
169
185
  }
170
186
  return dependencies;
171
187
  }
172
- function findTarget(sourcePath, keyMap, targetName, versionRange,
188
+ function findTarget(sourcePath, keyMap, targetName, versionRange, cachedSatisfies,
173
189
  // When a package is found at a path but its version doesn't satisfy the
174
190
  // range (e.g. due to npm overrides), we keep it as a fallback. npm already
175
191
  // resolved this dependency to that location, so it is the correct target
@@ -186,12 +202,13 @@ fallback) {
186
202
  versionRange.startsWith('npm:')) {
187
203
  const nodeVersion = child.data.version.slice(child.data.version.indexOf('@', 5) + 1);
188
204
  const depVersion = versionRange.slice(versionRange.indexOf('@', 5) + 1);
189
- if (nodeVersion === depVersion || (0, semver_1.satisfies)(nodeVersion, depVersion)) {
205
+ if (nodeVersion === depVersion ||
206
+ cachedSatisfies(nodeVersion, depVersion)) {
190
207
  return child;
191
208
  }
192
209
  }
193
210
  else if (child.data.version === versionRange ||
194
- (0, semver_1.satisfies)(child.data.version, versionRange)) {
211
+ cachedSatisfies(child.data.version, versionRange)) {
195
212
  return child;
196
213
  }
197
214
  // Version mismatch — save as fallback (could be an npm override)
@@ -203,13 +220,17 @@ fallback) {
203
220
  if (!sourcePath) {
204
221
  return fallback;
205
222
  }
206
- return findTarget(sourcePath.split('node_modules/').slice(0, -1).join('node_modules/'), keyMap, targetName, versionRange, fallback);
223
+ // Walk one level up the nesting chain by dropping the trailing
224
+ // `node_modules/<pkg>` segment. Slash-index arithmetic avoids the
225
+ // split/slice/join array allocation on every hop.
226
+ const lastNodeModules = sourcePath.lastIndexOf('node_modules/');
227
+ return findTarget(lastNodeModules === -1 ? '' : sourcePath.substring(0, lastNodeModules), keyMap, targetName, versionRange, cachedSatisfies, fallback);
207
228
  }
208
- function addV1NodeDependencies(path, snapshot, dependencies, keyMap, ctx) {
229
+ function addV1NodeDependencies(path, snapshot, dependencies, keyMap, ctx, cachedSatisfies) {
209
230
  if (keyMap.has(path) && snapshot.requires) {
210
231
  const source = keyMap.get(path).name;
211
232
  Object.entries(snapshot.requires).forEach(([name, versionRange]) => {
212
- const target = findTarget(path, keyMap, name, versionRange);
233
+ const target = findTarget(path, keyMap, name, versionRange, cachedSatisfies);
213
234
  if (target) {
214
235
  const dep = {
215
236
  source: source,
@@ -223,14 +244,14 @@ function addV1NodeDependencies(path, snapshot, dependencies, keyMap, ctx) {
223
244
  }
224
245
  if (snapshot.dependencies) {
225
246
  Object.entries(snapshot.dependencies).forEach(([depName, depSnapshot]) => {
226
- addV1NodeDependencies(`${path}/node_modules/${depName}`, depSnapshot, dependencies, keyMap, ctx);
247
+ addV1NodeDependencies(`${path}/node_modules/${depName}`, depSnapshot, dependencies, keyMap, ctx, cachedSatisfies);
227
248
  });
228
249
  }
229
250
  const { peerDependencies } = getPeerDependencies(path);
230
251
  if (peerDependencies) {
231
252
  const node = keyMap.get(path);
232
253
  Object.entries(peerDependencies).forEach(([depName, depSpec]) => {
233
- const target = findTarget(path, keyMap, depName, depSpec);
254
+ const target = findTarget(path, keyMap, depName, depSpec, cachedSatisfies);
234
255
  if (target) {
235
256
  const dep = {
236
257
  source: node.name,
@@ -358,10 +379,11 @@ function mapSnapshots(rootLockFile, graph) {
358
379
  const nestedNodes = new Set();
359
380
  const visitedNodes = new Map();
360
381
  const remappedPackages = new Map();
382
+ const packageIndex = buildV3Index(rootLockFile.packages);
361
383
  // add first level children
362
384
  Object.values(graph.externalNodes).forEach((node) => {
363
385
  if (node.name === `npm:${node.data.packageName}`) {
364
- const mappedPackage = mapPackage(rootLockFile, node.data.packageName, node.data.version);
386
+ const mappedPackage = mapPackage(rootLockFile, packageIndex, node.data.packageName, node.data.version);
365
387
  remappedPackages.set(mappedPackage.path, mappedPackage);
366
388
  visitedNodes.set(node, {
367
389
  packagePaths: new Set([mappedPackage.path]),
@@ -375,7 +397,7 @@ function mapSnapshots(rootLockFile, graph) {
375
397
  let remappedPackagesArray;
376
398
  if (nestedNodes.size) {
377
399
  const invertedGraph = (0, operators_1.reverse)(graph);
378
- nestMappedPackages(invertedGraph, remappedPackages, nestedNodes, visitedNodes, rootLockFile);
400
+ nestMappedPackages(invertedGraph, remappedPackages, nestedNodes, visitedNodes, rootLockFile, packageIndex);
379
401
  // initially we naively map package paths to topParent/../parent/child
380
402
  // but some of those should be nested higher up the tree
381
403
  remappedPackagesArray = elevateNestedPaths(remappedPackages);
@@ -385,14 +407,14 @@ function mapSnapshots(rootLockFile, graph) {
385
407
  }
386
408
  return remappedPackagesArray.sort((a, b) => a.path.localeCompare(b.path));
387
409
  }
388
- function mapPackage(rootLockFile, packageName, version, parentPath = '') {
410
+ function mapPackage(rootLockFile, packageIndex, packageName, version, parentPath = '') {
389
411
  const lockfileVersion = rootLockFile.lockfileVersion;
390
412
  let valueV3, valueV1;
391
413
  if (lockfileVersion < 3) {
392
414
  valueV1 = findMatchingPackageV1(rootLockFile.dependencies, packageName, version);
393
415
  }
394
416
  if (lockfileVersion > 1) {
395
- valueV3 = findMatchingPackageV3(rootLockFile.packages, packageName, version);
417
+ valueV3 = findMatchingPackageV3(packageIndex, packageName, version);
396
418
  }
397
419
  return {
398
420
  path: parentPath + `node_modules/${packageName}`,
@@ -401,7 +423,7 @@ function mapPackage(rootLockFile, packageName, version, parentPath = '') {
401
423
  valueV3,
402
424
  };
403
425
  }
404
- function nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile) {
426
+ function nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile, packageIndex) {
405
427
  const initialSize = nestedNodes.size;
406
428
  if (!initialSize) {
407
429
  return;
@@ -421,7 +443,7 @@ function nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, ro
421
443
  if (visitedNodes.has(targetNode) &&
422
444
  !visitedNodes.get(targetNode).unresolvedParents.size) {
423
445
  visitedNodes.get(targetNode).packagePaths.forEach((path) => {
424
- const mappedPackage = mapPackage(rootLockFile, node.data.packageName, node.data.version, path + '/');
446
+ const mappedPackage = mapPackage(rootLockFile, packageIndex, node.data.packageName, node.data.version, path + '/');
425
447
  result.set(mappedPackage.path, mappedPackage);
426
448
  visitedNodes.get(node).packagePaths.add(mappedPackage.path);
427
449
  visitedNodes.get(node).unresolvedParents.delete(target);
@@ -439,7 +461,7 @@ function nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, ro
439
461
  ].join('\n'));
440
462
  }
441
463
  else {
442
- nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile);
464
+ nestMappedPackages(invertedGraph, result, nestedNodes, visitedNodes, rootLockFile, packageIndex);
443
465
  }
444
466
  }
445
467
  // sort paths by number of segments and then alphabetically
@@ -495,16 +517,38 @@ function elevateNestedPaths(remappedPackages) {
495
517
  });
496
518
  return Array.from(result.values());
497
519
  }
498
- function findMatchingPackageV3(packages, name, version) {
499
- for (const [key, { dev, peer, ...snapshot }] of Object.entries(packages)) {
500
- if (key.endsWith(`node_modules/${name}`)) {
501
- if ([
502
- snapshot.version,
503
- snapshot.resolved,
504
- `npm:${snapshot.name}@${snapshot.version}`,
505
- ].includes(version)) {
506
- return snapshot;
507
- }
520
+ // Bucket packages by their trailing "node_modules/<name>" segment so a lookup
521
+ // scans only that name's copies instead of every package (was O(nodes *
522
+ // allPackages)). Mirrors the old `key.endsWith(node_modules/<name>)` match:
523
+ // the name is whatever follows the last "node_modules/" in the key.
524
+ function buildV3Index(packages) {
525
+ const index = new Map();
526
+ if (!packages)
527
+ return index;
528
+ const marker = 'node_modules/';
529
+ for (const key of Object.keys(packages)) {
530
+ const i = key.lastIndexOf(marker);
531
+ if (i === -1)
532
+ continue; // root "" / workspace paths never matched endsWith
533
+ const name = key.slice(i + marker.length);
534
+ let bucket = index.get(name);
535
+ if (!bucket)
536
+ index.set(name, (bucket = []));
537
+ bucket.push(packages[key]);
538
+ }
539
+ return index;
540
+ }
541
+ function findMatchingPackageV3(packageIndex, name, version) {
542
+ const bucket = packageIndex.get(name);
543
+ if (!bucket)
544
+ return undefined;
545
+ for (const { dev, peer, ...snapshot } of bucket) {
546
+ if ([
547
+ snapshot.version,
548
+ snapshot.resolved,
549
+ `npm:${snapshot.name}@${snapshot.version}`,
550
+ ].includes(version)) {
551
+ return snapshot;
508
552
  }
509
553
  }
510
554
  }
@@ -142,7 +142,12 @@ function getNodes(data, isV5) {
142
142
  if (data.patchedDependencies) {
143
143
  for (const specifier of Object.keys(data.patchedDependencies)) {
144
144
  const patchInfo = data.patchedDependencies[specifier];
145
- if (patchInfo && typeof patchInfo === 'object' && 'hash' in patchInfo) {
145
+ const patchHash = typeof patchInfo === 'string'
146
+ ? patchInfo
147
+ : patchInfo && typeof patchInfo === 'object' && 'hash' in patchInfo
148
+ ? patchInfo.hash
149
+ : undefined;
150
+ if (patchHash) {
146
151
  const packageName = extractNameFromKey(specifier, false);
147
152
  const versionSpecifier = getVersion(specifier, packageName) || null;
148
153
  if (!patchEntriesByPackage.has(packageName)) {
@@ -150,7 +155,7 @@ function getNodes(data, isV5) {
150
155
  }
151
156
  patchEntriesByPackage.get(packageName).push({
152
157
  versionSpecifier,
153
- hash: patchInfo.hash,
158
+ hash: patchHash,
154
159
  });
155
160
  }
156
161
  }
@@ -178,7 +183,8 @@ function getNodes(data, isV5) {
178
183
  }
179
184
  }
180
185
  const packageNames = new Set();
181
- for (const [key, snapshot] of Object.entries(data.packages)) {
186
+ // pnpm omits the packages block for workspace-only lockfiles (no external deps)
187
+ for (const [key, snapshot] of Object.entries(data.packages ?? {})) {
182
188
  let packageNameObj;
183
189
  const originalPackageName = extractNameFromKey(key, isV5);
184
190
  if (!originalPackageName) {
@@ -359,7 +365,8 @@ function getHoistedVersion(packageName, isV5, hoistedKeysByPackage) {
359
365
  }
360
366
  function getDependencies(data, keyMap, isV5, ctx) {
361
367
  const results = [];
362
- Object.keys(data.packages).forEach((key) => {
368
+ // pnpm omits the packages block for workspace-only lockfiles (no external deps)
369
+ Object.keys(data.packages ?? {}).forEach((key) => {
363
370
  const snapshot = data.packages[key];
364
371
  const nodes = keyMap.get(key);
365
372
  nodes.forEach((node) => {
@@ -391,9 +398,12 @@ function parseBaseVersion(rawVersion, isV5) {
391
398
  }
392
399
  function stringifyPnpmLockfile(graph, rootLockFileContent, packageJson, workspaceRoot) {
393
400
  const data = (0, pnpm_normalizer_1.parseAndNormalizePnpmLockfile)(rootLockFileContent);
394
- const { lockfileVersion, packages, importers } = data;
395
- const { snapshot: rootSnapshot, importers: requiredImporters } = mapRootSnapshot(packageJson, importers, packages, graph, +lockfileVersion, workspaceRoot);
396
- const snapshots = mapSnapshots(data.packages, graph.externalNodes, +lockfileVersion);
401
+ const { lockfileVersion, importers } = data;
402
+ // pnpm omits the packages block for workspace-only lockfiles (no external deps)
403
+ const packages = data.packages ?? {};
404
+ const packageIndex = indexPackagesByName(packages, +lockfileVersion);
405
+ const { snapshot: rootSnapshot, importers: requiredImporters } = mapRootSnapshot(packageJson, importers, packages, packageIndex, graph, +lockfileVersion, workspaceRoot);
406
+ const snapshots = mapSnapshots(packages, packageIndex, graph.externalNodes, +lockfileVersion);
397
407
  const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph);
398
408
  // Walk transitive workspace deps so every package copy-workspace-modules
399
409
  // writes to disk has a matching importer block. Without this, pnpm errors
@@ -455,12 +465,10 @@ function stringifyPnpmLockfile(graph, rootLockFileContent, packageJson, workspac
455
465
  };
456
466
  return (0, pnpm_normalizer_1.stringifyToPnpmYaml)(output);
457
467
  }
458
- function mapSnapshots(packages, nodes, lockfileVersion) {
468
+ function mapSnapshots(packages, packageIndex, nodes, lockfileVersion) {
459
469
  const result = {};
460
470
  Object.values(nodes).forEach((node) => {
461
- const matchedKeys = findOriginalKeys(packages, node, lockfileVersion, {
462
- returnFullKey: true,
463
- });
471
+ const matchedKeys = findOriginalKeys(packages, packageIndex, node, lockfileVersion, { returnFullKey: true });
464
472
  // the package manager doesn't check for types of dependencies
465
473
  // so we can safely set all to prod
466
474
  matchedKeys.forEach(([key, snapshot]) => {
@@ -500,10 +508,35 @@ function remapDependencies(snapshot) {
500
508
  }
501
509
  });
502
510
  }
503
- function findOriginalKeys(packages, { data: { packageName, version } }, lockfileVersion, { returnFullKey } = {}) {
504
- const matchedKeys = [];
511
+ // Bucket package keys by their package name so a node only scans its own name's
512
+ // versions instead of every key (was O(nodes * allPackages)). v5 is excluded
513
+ // below and keeps the full scan: its standard keys use a "/" separator while
514
+ // tarball keys use "@", so a single name index would misfile v5 tarballs.
515
+ function indexPackagesByName(packages, lockfileVersion) {
516
+ const isV5 = lockfileVersion < 6;
517
+ const index = new Map();
505
518
  for (const key of Object.keys(packages)) {
506
- const snapshot = packages[key];
519
+ const name = extractNameFromKey(key, isV5);
520
+ let bucket = index.get(name);
521
+ if (!bucket)
522
+ index.set(name, (bucket = []));
523
+ bucket.push([key, packages[key]]);
524
+ }
525
+ return index;
526
+ }
527
+ // npm alias version is "npm:<name>@<ver>"; extract <name> the same way
528
+ // versionIsAlias does so the index lookup matches the alias branch below.
529
+ function aliasTargetName(version) {
530
+ return version.slice('npm:'.length, version.indexOf('@', 'npm:'.length + 1));
531
+ }
532
+ const NO_CANDIDATES = [];
533
+ function findOriginalKeys(packages, packageIndex, node, lockfileVersion, { returnFullKey } = {}) {
534
+ const { data: { packageName, version }, } = node;
535
+ const candidates = lockfileVersion >= 6
536
+ ? (packageIndex.get(version.startsWith('npm:') ? aliasTargetName(version) : packageName) ?? NO_CANDIDATES)
537
+ : Object.entries(packages);
538
+ const matchedKeys = [];
539
+ for (const [key, snapshot] of candidates) {
507
540
  // tarball package
508
541
  if (key.startsWith(`${packageName}@${version}`) &&
509
542
  snapshot.resolution?.['tarball']) {
@@ -562,10 +595,11 @@ function versionIsAlias(key, versionExpr, lockfileVersion) {
562
595
  ? key.startsWith(`${packageName}/${version}`)
563
596
  : key.startsWith(`${packageName}@${version}`);
564
597
  }
565
- function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVersion, workspaceRoot) {
598
+ function mapRootSnapshot(packageJson, rootImporters, packages, packageIndex, graph, lockfileVersion, workspaceRoot) {
566
599
  const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph);
567
600
  const snapshot = { specifiers: {} };
568
601
  const importers = {};
602
+ const manager = (0, catalog_1.getCatalogManager)(workspaceRoot);
569
603
  [
570
604
  'dependencies',
571
605
  'optionalDependencies',
@@ -575,7 +609,6 @@ function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVe
575
609
  if (packageJson[depType]) {
576
610
  Object.keys(packageJson[depType]).forEach((packageName) => {
577
611
  let version = packageJson[depType][packageName];
578
- const manager = (0, catalog_1.getCatalogManager)(workspaceRoot);
579
612
  if (manager?.isCatalogReference(version)) {
580
613
  version = manager.resolveCatalogReference(workspaceRoot, packageName, version);
581
614
  if (!version) {
@@ -612,7 +645,7 @@ function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVe
612
645
  // peer dependencies are mapped to dependencies
613
646
  let section = depType === 'peerDependencies' ? 'dependencies' : depType;
614
647
  snapshot[section] = snapshot[section] || {};
615
- snapshot[section][packageName] = findOriginalKeys(packages, node, lockfileVersion)[0][0];
648
+ snapshot[section][packageName] = findOriginalKeys(packages, packageIndex, node, lockfileVersion)[0][0];
616
649
  }
617
650
  });
618
651
  }
@@ -38,9 +38,9 @@ function normalizeDependencies(packageJson, graph, workspacePackages, workspaceR
38
38
  ...optionalDependencies,
39
39
  ...peerDependencies,
40
40
  };
41
+ const manager = (0, catalog_1.getCatalogManager)(workspaceRootPath);
41
42
  Object.entries(combinedDependencies).forEach(([packageName, versionRange]) => {
42
43
  let resolvedVersionRange = versionRange;
43
- const manager = (0, catalog_1.getCatalogManager)(workspaceRootPath);
44
44
  if (manager?.isCatalogReference(versionRange)) {
45
45
  resolvedVersionRange = manager.resolveCatalogReference(workspaceRootPath, packageName, versionRange);
46
46
  if (!resolvedVersionRange) {
@@ -54,6 +54,9 @@ const YAML_DOCUMENT_SEPARATOR = '\n---\n';
54
54
  // always read the workspace document.
55
55
  // https://github.com/pnpm/pnpm/blob/main/lockfile/fs/src/yamlDocuments.ts
56
56
  function extractMainLockfileDocument(content) {
57
+ // Lockfiles written on Windows may use CRLF line endings, which would never
58
+ // match the LF-only document markers.
59
+ content = content.replace(/\r\n/g, '\n');
57
60
  if (!content.startsWith(YAML_DOCUMENT_START)) {
58
61
  return content;
59
62
  }
@@ -314,9 +314,10 @@ function mapSnapshots(rootDependencies, nodes, packageJson, workspaceModules, is
314
314
  };
315
315
  // yarn classic splits keys when parsing so we need to stich them back together
316
316
  const groupedDependencies = groupDependencies(rootDependencies, isBerry);
317
+ const keyIndex = buildYarnKeyIndex(groupedDependencies);
317
318
  // collect snapshots and their matching keys
318
319
  Object.values(nodes).forEach((node) => {
319
- const foundOriginalKeys = findOriginalKeys(groupedDependencies, node, workspaceModules);
320
+ const foundOriginalKeys = findOriginalKeys(groupedDependencies, keyIndex, node, workspaceModules);
320
321
  if (!foundOriginalKeys) {
321
322
  throw new Error(`Original key(s) not found for "${node.data.packageName}@${node.data.version}" while pruning yarn.lock.`);
322
323
  }
@@ -349,7 +350,7 @@ function mapSnapshots(rootDependencies, nodes, packageJson, workspaceModules, is
349
350
  }
350
351
  if (isBerry) {
351
352
  // look for patched versions
352
- const patch = findPatchedKeys(groupedDependencies, node, resolutions[node.data.packageName]);
353
+ const patch = findPatchedKeys(groupedDependencies, keyIndex, node, resolutions[node.data.packageName]);
353
354
  if (patch) {
354
355
  const [matchedKeys, snapshot] = patch;
355
356
  snapshotMap.set(snapshot, new Set(matchedKeys));
@@ -424,8 +425,34 @@ function isClassicAlias(node, keys) {
424
425
  return (node.data.version.startsWith('npm:') &&
425
426
  keys.some((k) => k === `${node.data.packageName}@${node.data.version}`));
426
427
  }
427
- function findOriginalKeys(dependencies, node, workspaceModules) {
428
+ // Bucket grouped-dependency key expressions by the package names they contain
429
+ // so a node scans only its own name's entries (was O(nodes * allEntries)). A
430
+ // key like "foo@npm:1.0" indexes under "foo"; the inner match checks below are
431
+ // unchanged, so candidates are exactly the entries the old full scan would not
432
+ // have skipped and the result is identical.
433
+ function extractYarnKeyName(key) {
434
+ const at = key.indexOf('@', 1);
435
+ return at === -1 ? key : key.slice(0, at);
436
+ }
437
+ function buildYarnKeyIndex(dependencies) {
438
+ const index = new Map();
428
439
  for (const keyExpr of Object.keys(dependencies)) {
440
+ const seen = new Set();
441
+ for (const k of keyExpr.split(', ')) {
442
+ const name = extractYarnKeyName(k);
443
+ if (seen.has(name))
444
+ continue;
445
+ seen.add(name);
446
+ let bucket = index.get(name);
447
+ if (!bucket)
448
+ index.set(name, (bucket = []));
449
+ bucket.push(keyExpr);
450
+ }
451
+ }
452
+ return index;
453
+ }
454
+ function findOriginalKeys(dependencies, keyIndex, node, workspaceModules) {
455
+ for (const keyExpr of keyIndex.get(node.data.packageName) ?? []) {
429
456
  const snapshot = dependencies[keyExpr];
430
457
  const keys = keyExpr.split(', ');
431
458
  if (!keys.some((k) => k.startsWith(`${node.data.packageName}@`))) {
@@ -450,8 +477,8 @@ function findOriginalKeys(dependencies, node, workspaceModules) {
450
477
  }
451
478
  }
452
479
  }
453
- function findPatchedKeys(dependencies, node, resolutionVersion) {
454
- for (const keyExpr of Object.keys(dependencies)) {
480
+ function findPatchedKeys(dependencies, keyIndex, node, resolutionVersion) {
481
+ for (const keyExpr of keyIndex.get(node.data.packageName) ?? []) {
455
482
  const snapshot = dependencies[keyExpr];
456
483
  const keys = keyExpr.split(', ');
457
484
  if (!keys[0].startsWith(`${node.data.packageName}@patch:`)) {
@@ -161,11 +161,19 @@ class TargetProjectLocator {
161
161
  return externalNodeName;
162
162
  }
163
163
  const version = (0, semver_1.clean)(externalPackageJson.version);
164
- let matchingExternalNode = this.npmProjects[`npm:${externalPackageJson.name}@${version}`];
165
- if (!matchingExternalNode) {
166
- // check if it's a package alias, where the resolved package key is used as the version
164
+ const isAliasImport = packageName !== externalPackageJson.name;
165
+ let matchingExternalNode = null;
166
+ if (isAliasImport) {
167
+ // Prefer the alias node when both the alias import and the resolved package
168
+ // exist in the graph, otherwise generated package.json files lose the alias key.
167
169
  const aliasNpmProjectKey = `npm:${packageName}@npm:${externalPackageJson.name}@${version}`;
168
- matchingExternalNode = this.npmProjects[aliasNpmProjectKey];
170
+ matchingExternalNode =
171
+ this.npmProjects[aliasNpmProjectKey] ??
172
+ this.npmProjects[`npm:${packageName}`];
173
+ }
174
+ if (!matchingExternalNode) {
175
+ matchingExternalNode =
176
+ this.npmProjects[`npm:${externalPackageJson.name}@${version}`];
169
177
  }
170
178
  if (!matchingExternalNode) {
171
179
  // Fallback to package name as key. This can happen if the version in project graph is not the same as in the resolved package.json.
@@ -96,6 +96,24 @@ export declare function ensureNodeNextEsmResolverRegistered(): void;
96
96
  * unaffected when both files exist. Idempotent on repeat calls.
97
97
  */
98
98
  export declare function ensureCjsResolverPatched(): void;
99
+ /**
100
+ * Make Node's module resolver honor the given export conditions for the rest of
101
+ * the process, via `module.registerHooks()`. Used when a local plugin is loaded
102
+ * from source in-process (no child process to pass `--conditions` to at spawn):
103
+ * the hook appends the conditions to every resolution so the plugin's transitive
104
+ * workspace imports resolve to source the same way the plugin entry did.
105
+ *
106
+ * No-op when:
107
+ * - `conditions` is empty (nothing to inject);
108
+ * - every target condition is already active at startup (a spawned plugin
109
+ * worker or daemon was launched with the full `--conditions` set, so Node's
110
+ * resolver already honors them and the per-resolve hook would be redundant);
111
+ * - `module.registerHooks` is unavailable (Node < 22.15 / < 23.5). Those
112
+ * runtimes keep the `NODE_OPTIONS=--conditions` escape hatch.
113
+ *
114
+ * Idempotent and best-effort.
115
+ */
116
+ export declare function ensureResolveConditionsInjected(conditions: string[]): void;
99
117
  /**
100
118
  * Whether Nx will defer to Node's native TypeScript stripping for the next
101
119
  * `.ts` load. Mirrors the gate used by `loadTsFile`/`registerTsProject` so
@@ -5,6 +5,7 @@ exports.forceRegisterEsmLoader = forceRegisterEsmLoader;
5
5
  exports.nodeNextEsmResolveHook = nodeNextEsmResolveHook;
6
6
  exports.ensureNodeNextEsmResolverRegistered = ensureNodeNextEsmResolverRegistered;
7
7
  exports.ensureCjsResolverPatched = ensureCjsResolverPatched;
8
+ exports.ensureResolveConditionsInjected = ensureResolveConditionsInjected;
8
9
  exports.isNativeStripPreferred = isNativeStripPreferred;
9
10
  exports.registerTsProject = registerTsProject;
10
11
  exports.getSwcTranspiler = getSwcTranspiler;
@@ -388,6 +389,86 @@ const isInvokedByTsx = (() => {
388
389
  return (requireArgs.some((a) => isTsxPath(a)) ||
389
390
  importArgs.some((a) => isTsxPath(a)));
390
391
  })();
392
+ let resolveConditionsInjected = false;
393
+ /**
394
+ * Make Node's module resolver honor the given export conditions for the rest of
395
+ * the process, via `module.registerHooks()`. Used when a local plugin is loaded
396
+ * from source in-process (no child process to pass `--conditions` to at spawn):
397
+ * the hook appends the conditions to every resolution so the plugin's transitive
398
+ * workspace imports resolve to source the same way the plugin entry did.
399
+ *
400
+ * No-op when:
401
+ * - `conditions` is empty (nothing to inject);
402
+ * - every target condition is already active at startup (a spawned plugin
403
+ * worker or daemon was launched with the full `--conditions` set, so Node's
404
+ * resolver already honors them and the per-resolve hook would be redundant);
405
+ * - `module.registerHooks` is unavailable (Node < 22.15 / < 23.5). Those
406
+ * runtimes keep the `NODE_OPTIONS=--conditions` escape hatch.
407
+ *
408
+ * Idempotent and best-effort.
409
+ */
410
+ function ensureResolveConditionsInjected(conditions) {
411
+ if (resolveConditionsInjected)
412
+ return;
413
+ resolveConditionsInjected = true;
414
+ if (!conditions.length)
415
+ return;
416
+ // Skip only when Node already honors every target condition (e.g. a worker or
417
+ // daemon spawned with the full `--conditions` set); a partial overlap still
418
+ // needs the hook to add the missing ones.
419
+ const activeConditions = getConditionsActiveAtStartup();
420
+ if (conditions.every((condition) => activeConditions.has(condition)))
421
+ return;
422
+ const module = require('node:module');
423
+ const registerHooks = module.registerHooks;
424
+ if (typeof registerHooks !== 'function')
425
+ return;
426
+ try {
427
+ registerHooks.call(module, {
428
+ resolve(specifier, context, nextResolve) {
429
+ const merged = context.conditions
430
+ ? [...context.conditions, ...conditions]
431
+ : conditions;
432
+ return nextResolve(specifier, { ...context, conditions: merged });
433
+ },
434
+ });
435
+ }
436
+ catch {
437
+ // Best-effort: leave Node's native resolution in place rather than failing.
438
+ }
439
+ }
440
+ /**
441
+ * Export conditions this process was started with, parsed from `--conditions`
442
+ * (or its `-C` alias) in `process.execArgv` and `NODE_OPTIONS`. Node's resolver
443
+ * already honors these, so the injected hook only needs to cover target
444
+ * conditions not present here.
445
+ */
446
+ function getConditionsActiveAtStartup() {
447
+ const active = new Set();
448
+ const collect = (tokens) => {
449
+ for (let i = 0; i < tokens.length; i++) {
450
+ const token = tokens[i];
451
+ if (token === '--conditions' || token === '-C') {
452
+ const value = tokens[i + 1];
453
+ if (value) {
454
+ active.add(value);
455
+ i++;
456
+ }
457
+ }
458
+ else if (token.startsWith('--conditions=')) {
459
+ active.add(token.slice('--conditions='.length));
460
+ }
461
+ else if (token.startsWith('-C=')) {
462
+ active.add(token.slice('-C='.length));
463
+ }
464
+ }
465
+ };
466
+ collect(process.execArgv ?? []);
467
+ const nodeOptions = process.env.NODE_OPTIONS;
468
+ if (nodeOptions)
469
+ collect(nodeOptions.split(/\s+/).filter(Boolean));
470
+ return active;
471
+ }
391
472
  /**
392
473
  * Whether the current Node.js runtime exposes native TypeScript type
393
474
  * stripping. This is the authoritative gate - it correctly handles every