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
@@ -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
  }
@@ -391,9 +396,12 @@ function parseBaseVersion(rawVersion, isV5) {
391
396
  }
392
397
  function stringifyPnpmLockfile(graph, rootLockFileContent, packageJson, workspaceRoot) {
393
398
  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);
399
+ const { lockfileVersion, importers } = data;
400
+ // pnpm omits the packages block for workspace-only lockfiles (no external deps)
401
+ const packages = data.packages ?? {};
402
+ const packageIndex = indexPackagesByName(packages, +lockfileVersion);
403
+ const { snapshot: rootSnapshot, importers: requiredImporters } = mapRootSnapshot(packageJson, importers, packages, packageIndex, graph, +lockfileVersion, workspaceRoot);
404
+ const snapshots = mapSnapshots(packages, packageIndex, graph.externalNodes, +lockfileVersion);
397
405
  const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph);
398
406
  // Walk transitive workspace deps so every package copy-workspace-modules
399
407
  // writes to disk has a matching importer block. Without this, pnpm errors
@@ -455,12 +463,10 @@ function stringifyPnpmLockfile(graph, rootLockFileContent, packageJson, workspac
455
463
  };
456
464
  return (0, pnpm_normalizer_1.stringifyToPnpmYaml)(output);
457
465
  }
458
- function mapSnapshots(packages, nodes, lockfileVersion) {
466
+ function mapSnapshots(packages, packageIndex, nodes, lockfileVersion) {
459
467
  const result = {};
460
468
  Object.values(nodes).forEach((node) => {
461
- const matchedKeys = findOriginalKeys(packages, node, lockfileVersion, {
462
- returnFullKey: true,
463
- });
469
+ const matchedKeys = findOriginalKeys(packages, packageIndex, node, lockfileVersion, { returnFullKey: true });
464
470
  // the package manager doesn't check for types of dependencies
465
471
  // so we can safely set all to prod
466
472
  matchedKeys.forEach(([key, snapshot]) => {
@@ -500,10 +506,35 @@ function remapDependencies(snapshot) {
500
506
  }
501
507
  });
502
508
  }
503
- function findOriginalKeys(packages, { data: { packageName, version } }, lockfileVersion, { returnFullKey } = {}) {
504
- const matchedKeys = [];
509
+ // Bucket package keys by their package name so a node only scans its own name's
510
+ // versions instead of every key (was O(nodes * allPackages)). v5 is excluded
511
+ // below and keeps the full scan: its standard keys use a "/" separator while
512
+ // tarball keys use "@", so a single name index would misfile v5 tarballs.
513
+ function indexPackagesByName(packages, lockfileVersion) {
514
+ const isV5 = lockfileVersion < 6;
515
+ const index = new Map();
505
516
  for (const key of Object.keys(packages)) {
506
- const snapshot = packages[key];
517
+ const name = extractNameFromKey(key, isV5);
518
+ let bucket = index.get(name);
519
+ if (!bucket)
520
+ index.set(name, (bucket = []));
521
+ bucket.push([key, packages[key]]);
522
+ }
523
+ return index;
524
+ }
525
+ // npm alias version is "npm:<name>@<ver>"; extract <name> the same way
526
+ // versionIsAlias does so the index lookup matches the alias branch below.
527
+ function aliasTargetName(version) {
528
+ return version.slice('npm:'.length, version.indexOf('@', 'npm:'.length + 1));
529
+ }
530
+ const NO_CANDIDATES = [];
531
+ function findOriginalKeys(packages, packageIndex, node, lockfileVersion, { returnFullKey } = {}) {
532
+ const { data: { packageName, version }, } = node;
533
+ const candidates = lockfileVersion >= 6
534
+ ? (packageIndex.get(version.startsWith('npm:') ? aliasTargetName(version) : packageName) ?? NO_CANDIDATES)
535
+ : Object.entries(packages);
536
+ const matchedKeys = [];
537
+ for (const [key, snapshot] of candidates) {
507
538
  // tarball package
508
539
  if (key.startsWith(`${packageName}@${version}`) &&
509
540
  snapshot.resolution?.['tarball']) {
@@ -562,10 +593,11 @@ function versionIsAlias(key, versionExpr, lockfileVersion) {
562
593
  ? key.startsWith(`${packageName}/${version}`)
563
594
  : key.startsWith(`${packageName}@${version}`);
564
595
  }
565
- function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVersion, workspaceRoot) {
596
+ function mapRootSnapshot(packageJson, rootImporters, packages, packageIndex, graph, lockfileVersion, workspaceRoot) {
566
597
  const workspaceModules = (0, get_workspace_packages_from_graph_1.getWorkspacePackagesFromGraph)(graph);
567
598
  const snapshot = { specifiers: {} };
568
599
  const importers = {};
600
+ const manager = (0, catalog_1.getCatalogManager)(workspaceRoot);
569
601
  [
570
602
  'dependencies',
571
603
  'optionalDependencies',
@@ -575,7 +607,6 @@ function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVe
575
607
  if (packageJson[depType]) {
576
608
  Object.keys(packageJson[depType]).forEach((packageName) => {
577
609
  let version = packageJson[depType][packageName];
578
- const manager = (0, catalog_1.getCatalogManager)(workspaceRoot);
579
610
  if (manager?.isCatalogReference(version)) {
580
611
  version = manager.resolveCatalogReference(workspaceRoot, packageName, version);
581
612
  if (!version) {
@@ -612,7 +643,7 @@ function mapRootSnapshot(packageJson, rootImporters, packages, graph, lockfileVe
612
643
  // peer dependencies are mapped to dependencies
613
644
  let section = depType === 'peerDependencies' ? 'dependencies' : depType;
614
645
  snapshot[section] = snapshot[section] || {};
615
- snapshot[section][packageName] = findOriginalKeys(packages, node, lockfileVersion)[0][0];
646
+ snapshot[section][packageName] = findOriginalKeys(packages, packageIndex, node, lockfileVersion)[0][0];
616
647
  }
617
648
  });
618
649
  }
@@ -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) {
@@ -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:`)) {
@@ -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
@@ -20,4 +20,13 @@ export declare function getRootTsConfigCustomConditions(root?: string): string[]
20
20
  * `migrate-development-custom-condition` (21.5).
21
21
  */
22
22
  export declare function getRootTsConfigResolveExportsConditions(root?: string): string[];
23
+ /**
24
+ * Node `--conditions <name>` CLI args for spawning a plugin worker or the daemon
25
+ * with the plugin-resolution conditions active at startup. Mirrors the set Nx
26
+ * uses to resolve the plugin entry (`getRootTsConfigResolveExportsConditions`)
27
+ * so the entry and the plugin's transitive workspace imports resolve the same
28
+ * way; Node's own resolver otherwise ignores TypeScript `customConditions` and a
29
+ * source-loaded plugin's imports land on their unbuilt `dist`.
30
+ */
31
+ export declare function getPluginResolveConditionNodeArgs(root?: string): string[];
23
32
  export declare function findNodes(node: Node, kind: SyntaxKind | SyntaxKind[], max?: number): Node[];
@@ -8,6 +8,7 @@ exports.getRootTsConfigFileName = getRootTsConfigFileName;
8
8
  exports.getRootTsConfigPath = getRootTsConfigPath;
9
9
  exports.getRootTsConfigCustomConditions = getRootTsConfigCustomConditions;
10
10
  exports.getRootTsConfigResolveExportsConditions = getRootTsConfigResolveExportsConditions;
11
+ exports.getPluginResolveConditionNodeArgs = getPluginResolveConditionNodeArgs;
11
12
  exports.findNodes = findNodes;
12
13
  const workspace_root_1 = require("../../../utils/workspace-root");
13
14
  const fs_1 = require("fs");
@@ -113,6 +114,20 @@ function getRootTsConfigResolveExportsConditions(root = workspace_root_1.workspa
113
114
  ? conditions
114
115
  : [...conditions, 'development'];
115
116
  }
117
+ /**
118
+ * Node `--conditions <name>` CLI args for spawning a plugin worker or the daemon
119
+ * with the plugin-resolution conditions active at startup. Mirrors the set Nx
120
+ * uses to resolve the plugin entry (`getRootTsConfigResolveExportsConditions`)
121
+ * so the entry and the plugin's transitive workspace imports resolve the same
122
+ * way; Node's own resolver otherwise ignores TypeScript `customConditions` and a
123
+ * source-loaded plugin's imports land on their unbuilt `dist`.
124
+ */
125
+ function getPluginResolveConditionNodeArgs(root = workspace_root_1.workspaceRoot) {
126
+ return getRootTsConfigResolveExportsConditions(root).flatMap((c) => [
127
+ '--conditions',
128
+ c,
129
+ ]);
130
+ }
116
131
  function findNodes(node, kind, max = Infinity) {
117
132
  if (!node || max == 0) {
118
133
  return [];
@@ -242,9 +242,48 @@ class AggregateCreateNodesError extends Error {
242
242
  errorTuple.length === 2)) {
243
243
  throw new Error('AggregateCreateNodesError must be constructed with an array of tuples where the first element is a filename or undefined and the second element is the underlying error.');
244
244
  }
245
+ // Plugins pass through whatever value they caught, which is not
246
+ // guaranteed to be an Error. Coerce so formatting can rely on
247
+ // message and stack being present.
248
+ for (const errorTuple of errors) {
249
+ errorTuple[1] = coerceToError(errorTuple[1]);
250
+ }
245
251
  }
246
252
  }
247
253
  exports.AggregateCreateNodesError = AggregateCreateNodesError;
254
+ function coerceToError(value) {
255
+ if (value instanceof Error) {
256
+ return value;
257
+ }
258
+ let message;
259
+ let stack;
260
+ if (typeof value === 'object' && value !== null) {
261
+ const candidate = value;
262
+ if (typeof candidate.message === 'string') {
263
+ message = candidate.message;
264
+ if (typeof candidate.stack === 'string') {
265
+ stack = candidate.stack;
266
+ }
267
+ }
268
+ else {
269
+ try {
270
+ message = JSON.stringify(value);
271
+ }
272
+ catch {
273
+ // Circular structures cannot be stringified.
274
+ message = String(value);
275
+ }
276
+ }
277
+ }
278
+ else {
279
+ message = String(value);
280
+ }
281
+ const error = new Error(message);
282
+ // A synthesized stack would point at this coercion site rather than
283
+ // the original failure, so prefer the original stack or just the message.
284
+ error.stack = stack ?? message;
285
+ return error;
286
+ }
248
287
  function formatAggregateCreateNodesError(error, pluginName) {
249
288
  const errorCount = error.errors.length > 1 ? `${error.errors.length} errors` : 'An error';
250
289
  const pluginLocation = error.pluginIndex
@@ -270,7 +309,8 @@ function formatAggregateCreateNodesError(error, pluginName) {
270
309
  }
271
310
  for (const e of errors) {
272
311
  const messageLines = e.message.split('\n');
273
- const stackLines = e.stack.split('\n');
312
+ // Errors deserialized from a plugin worker may arrive without a stack.
313
+ const stackLines = (e.stack ?? e.message).split('\n');
274
314
  if (file) {
275
315
  errorBodyLines.push(...messageLines.map((line) => ` ${line}`));
276
316
  errorStackLines.push(...stackLines.map((line) => ` ${line}`));
@@ -7,6 +7,7 @@ const path = require("path");
7
7
  const logger_1 = require("../../../daemon/logger");
8
8
  const socket_utils_1 = require("../../../daemon/socket-utils");
9
9
  const consume_messages_from_socket_1 = require("../../../utils/consume-messages-from-socket");
10
+ const typescript_1 = require("../../../plugins/js/utils/typescript");
10
11
  const installation_directory_1 = require("../../../utils/installation-directory");
11
12
  const logger_2 = require("../../../utils/logger");
12
13
  const progress_topics_1 = require("../../../utils/progress-topics");
@@ -353,6 +354,9 @@ async function startPluginWorker(name) {
353
354
  };
354
355
  const ipcPath = (0, socket_utils_1.getPluginOsSocketPath)([process.pid, global.nxPluginWorkerCount++, performance.now()].join('-'));
355
356
  const worker = (0, child_process_1.spawn)(process.execPath, [
357
+ // Spawn the worker with the same resolve conditions Nx uses for plugin
358
+ // entries so the plugin's transitive workspace imports resolve to source.
359
+ ...(0, typescript_1.getPluginResolveConditionNodeArgs)(),
356
360
  ...(isWorkerTypescript ? ['--require', 'ts-node/register'] : []),
357
361
  workerPath,
358
362
  ipcPath,