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
@@ -178,9 +178,26 @@ async function getCachedSerializedProjectGraphPromise(socket) {
178
178
  }
179
179
  function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFiles) {
180
180
  ++fileChangeCounter;
181
- for (let f of [...createdFiles, ...updatedFiles]) {
181
+ // Hash the changed files up front and drop no-op rewrites before they can
182
+ // trigger an expensive recompute. Restoring a cached task output, a
183
+ // `git checkout` back to the same content, or a formatter that changes
184
+ // nothing all rewrite a file (new inode) the watcher reports as changed
185
+ // even though the bytes are identical. updateFilesInContext updates the
186
+ // workspace context and returns only the files whose content actually
187
+ // changed. Hashing here — once per watcher batch — rather than inside the
188
+ // recompute keeps it off the stale-retry path, which would otherwise see
189
+ // "no change" after the first pass already updated the context hashes.
190
+ perf_hooks_1.performance.mark('hash-watched-changes-start');
191
+ const changedFileHashes = createdFiles.length > 0 ||
192
+ updatedFiles.length > 0 ||
193
+ deletedFiles.length > 0
194
+ ? ((0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, [...createdFiles, ...updatedFiles], deletedFiles) ?? {})
195
+ : {};
196
+ perf_hooks_1.performance.mark('hash-watched-changes-end');
197
+ perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end');
198
+ for (const [f, hash] of Object.entries(changedFileHashes)) {
182
199
  collectedDeletedFiles.delete(f);
183
- collectedUpdatedFiles.set(f, fileChangeCounter);
200
+ collectedUpdatedFiles.set(f, { version: fileChangeCounter, hash });
184
201
  }
185
202
  for (let f of deletedFiles) {
186
203
  collectedUpdatedFiles.delete(f);
@@ -188,9 +205,7 @@ function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFi
188
205
  }
189
206
  // The native watcher already coalesces a burst of events into one batch,
190
207
  // so socket + listener notifications dispatch immediately.
191
- if (createdFiles.length > 0 ||
192
- updatedFiles.length > 0 ||
193
- deletedFiles.length > 0) {
208
+ if (Object.keys(changedFileHashes).length > 0 || deletedFiles.length > 0) {
194
209
  (0, file_change_events_1.notifyFileChangeListeners)({ createdFiles, updatedFiles, deletedFiles });
195
210
  (0, file_watcher_sockets_1.notifyFileWatcherSockets)(createdFiles, updatedFiles, deletedFiles);
196
211
  // Bump generation synchronously so any in-flight compute fails its
@@ -284,14 +299,18 @@ async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) {
284
299
  return cachedSerializedProjectGraphPromise;
285
300
  };
286
301
  try {
287
- perf_hooks_1.performance.mark('hash-watched-changes-start');
288
302
  const updatedFilesSnapshot = new Map(collectedUpdatedFiles);
289
303
  const deletedFilesSnapshot = new Map(collectedDeletedFiles);
290
304
  const updatedFiles = [...updatedFilesSnapshot.keys()];
291
305
  const deletedFiles = [...deletedFilesSnapshot.keys()];
292
- let updatedFileHashes = (0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, updatedFiles, deletedFiles);
293
- perf_hooks_1.performance.mark('hash-watched-changes-end');
294
- perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end');
306
+ // Hashes were already computed (and the workspace context updated) in
307
+ // scheduleProjectGraphRecomputation, which also dropped no-op rewrites.
308
+ // Reuse them so the context isn't re-hashed on every (possibly stale)
309
+ // recompute attempt.
310
+ const updatedFileHashes = {};
311
+ for (const [f, { hash }] of updatedFilesSnapshot) {
312
+ updatedFileHashes[f] = hash;
313
+ }
295
314
  logger_1.serverLogger.requestLog(`Updated workspace context based on watched changes, recomputing project graph...`);
296
315
  logger_1.serverLogger.requestLog(updatedFiles);
297
316
  logger_1.serverLogger.requestLog(deletedFiles);
@@ -333,8 +352,8 @@ async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) {
333
352
  // from the daemon's view (project graph misses recently added files).
334
353
  // Match version-stamps so a file modified mid-flight (higher version)
335
354
  // stays in the queue for reprocessing.
336
- for (const [f, version] of updatedFilesSnapshot) {
337
- if (collectedUpdatedFiles.get(f) === version) {
355
+ for (const [f, { version }] of updatedFilesSnapshot) {
356
+ if (collectedUpdatedFiles.get(f)?.version === version) {
338
357
  collectedUpdatedFiles.delete(f);
339
358
  }
340
359
  }
Binary file
@@ -26,6 +26,10 @@ let cachedPackageIndex;
26
26
  const keyMap = new Map();
27
27
  const packageVersions = new Map();
28
28
  const specParseCache = new Map();
29
+ // Memoizes (packageName, versionSpec) -> resolved version for the current
30
+ // lockfile. The same name/range pairs repeat across many dependency lists, so
31
+ // the semver resolution is done once per distinct pair instead of per edge.
32
+ const resolvedVersionCache = new Map();
29
33
  // Structured error types for better error handling
30
34
  class BunLockfileParseError extends Error {
31
35
  constructor(message, cause) {
@@ -72,6 +76,7 @@ function clearCache() {
72
76
  keyMap.clear();
73
77
  packageVersions.clear();
74
78
  specParseCache.clear();
79
+ resolvedVersionCache.clear();
75
80
  }
76
81
  // ===== UTILITY FUNCTIONS =====
77
82
  function getCachedSpecInfo(resolvedSpec) {
@@ -732,40 +737,38 @@ function createHoistedNodes(packageVersions, lockFile, index, keyMap, nodes) {
732
737
  * O(1) lookup using pre-computed index
733
738
  */
734
739
  function isNestedPackageKey(packageKey, index) {
735
- // If the key doesn't contain '/', it's a direct package entry
736
- if (!packageKey.includes('/')) {
740
+ // If the key doesn't contain '/', it's a direct package entry.
741
+ // Work off slash indices instead of split('/')+slice+join to avoid an
742
+ // array allocation per key (this runs for every package entry, twice).
743
+ const lastSlash = packageKey.lastIndexOf('/');
744
+ if (lastSlash === -1) {
737
745
  return false;
738
746
  }
739
- // Check if this looks like a workspace-specific or nested entry
740
- const parts = packageKey.split('/');
741
- // For multi-part keys, check if the prefix is a workspace path or package name
742
- if (parts.length >= 2) {
743
- const prefix = parts.slice(0, -1).join('/');
744
- // O(1) check against known workspace paths
745
- if (index.workspacePaths.has(prefix)) {
746
- return true;
747
- }
748
- // O(1) check against workspace package names (scoped packages)
749
- if (index.workspaceNames.has(prefix)) {
750
- return true;
751
- }
752
- // Check for scoped workspace packages (e.g., "@quz/pkg1/lodash")
753
- // The prefix must contain '/' to be a scoped package (e.g., "@scope/pkg")
754
- // A prefix like just "@scope" without '/' is not a scoped package
755
- if (prefix.startsWith('@') && prefix.includes('/')) {
756
- return true;
757
- }
758
- // If the key looks like a simple scoped package (e.g., "@custom/lodash")
759
- // where parts.length === 2 and first part starts with '@', it's likely
760
- // a scoped package alias, not a nested dependency
761
- if (parts.length === 2 && parts[0].startsWith('@')) {
762
- return false;
763
- }
764
- // This could be dependency nesting (e.g., "is-even/is-odd")
765
- // These should be filtered out as they're not direct packages
747
+ // prefix = everything before the last '/'
748
+ const prefix = packageKey.substring(0, lastSlash);
749
+ // O(1) check against known workspace paths
750
+ if (index.workspacePaths.has(prefix)) {
766
751
  return true;
767
752
  }
768
- return false;
753
+ // O(1) check against workspace package names (scoped packages)
754
+ if (index.workspaceNames.has(prefix)) {
755
+ return true;
756
+ }
757
+ // Check for scoped workspace packages (e.g., "@quz/pkg1/lodash")
758
+ // The prefix must contain '/' to be a scoped package (e.g., "@scope/pkg")
759
+ // A prefix like just "@scope" without '/' is not a scoped package
760
+ if (prefix.startsWith('@') && prefix.includes('/')) {
761
+ return true;
762
+ }
763
+ // If the key looks like a simple scoped package (e.g., "@custom/lodash")
764
+ // with a single '/' where the first segment starts with '@', it's likely
765
+ // a scoped package alias, not a nested dependency
766
+ if (packageKey.indexOf('/') === lastSlash && packageKey.startsWith('@')) {
767
+ return false;
768
+ }
769
+ // This could be dependency nesting (e.g., "is-even/is-odd")
770
+ // These should be filtered out as they're not direct packages
771
+ return true;
769
772
  }
770
773
  /**
771
774
  * Determines if a package should have a hoisted node created
@@ -829,6 +832,18 @@ function getHoistedVersion(packageName, availableVersions, index) {
829
832
  * O(1) lookup using pre-computed index instead of O(n) scan through all packages
830
833
  */
831
834
  function findResolvedVersion(packageName, versionSpec, index, manifests) {
835
+ // Resolution depends only on (packageName, versionSpec) for a given lockfile
836
+ // (index and manifests are constant per parse), so memoize across edges.
837
+ const cacheKey = `${packageName}\n${versionSpec}`;
838
+ const cached = resolvedVersionCache.get(cacheKey);
839
+ if (cached !== undefined) {
840
+ return cached;
841
+ }
842
+ const resolved = computeResolvedVersion(packageName, versionSpec, index, manifests);
843
+ resolvedVersionCache.set(cacheKey, resolved);
844
+ return resolved;
845
+ }
846
+ function computeResolvedVersion(packageName, versionSpec, index, manifests) {
832
847
  // O(1) lookup
833
848
  const candidates = index.byName.get(packageName);
834
849
  if (!candidates || candidates.length === 0) {
@@ -918,19 +933,27 @@ function findBestVersionMatch(versionSpec, candidates) {
918
933
  // No satisfying versions found, return the first semver candidate as fallback
919
934
  return semverVersions[0].version;
920
935
  }
921
- // Return the highest satisfying version (similar to npm behavior)
922
- // Sort versions in descending order and return the first one
923
- const sortedVersions = satisfyingVersions.sort((a, b) => {
936
+ // Return the highest satisfying version (similar to npm behavior).
937
+ // Single-pass max instead of a full sort: only the top element is used, and
938
+ // the numeric-collation comparator is expensive, so O(n) comparisons beat
939
+ // O(n log n). Keeps the first element on ties, matching the previous stable
940
+ // descending sort followed by [0].
941
+ let best = satisfyingVersions[0];
942
+ for (let i = 1; i < satisfyingVersions.length; i++) {
943
+ const candidate = satisfyingVersions[i];
944
+ let cmp;
924
945
  try {
925
- return b.version.localeCompare(a.version, undefined, {
946
+ cmp = candidate.version.localeCompare(best.version, undefined, {
926
947
  numeric: true,
927
948
  sensitivity: 'base',
928
949
  });
929
950
  }
930
951
  catch {
931
- // Fallback to string comparison
932
- return b.version.localeCompare(a.version);
952
+ cmp = candidate.version.localeCompare(best.version);
933
953
  }
934
- });
935
- return sortedVersions[0].version;
954
+ if (cmp > 0) {
955
+ best = candidate;
956
+ }
957
+ }
958
+ return best.version;
936
959
  }
@@ -36,9 +36,7 @@ function loadPnpmHoistedDepsDefinition() {
36
36
  const { load } = require('@zkochan/js-yaml');
37
37
  return load(content)?.hoistedDependencies ?? {};
38
38
  }
39
- else {
40
- throw new Error(`Could not find ".modules.yaml" at "${fullPath}"`);
41
- }
39
+ throw new Error(`pnpm lockfile detected, but "${fullPath}" is missing. This usually means that "node_modules" was not installed with pnpm. Run "pnpm install" or use a single package manager for this workspace.`);
42
40
  }
43
41
  /**
44
42
  * Parsing and mapping logic from pnpm lockfile `read` function
@@ -136,10 +136,31 @@ function createPackageJson(projectName, graph, options = {}, fileMap = null) {
136
136
  // region Overrides/Resolutions
137
137
  // npm
138
138
  if (rootPackageJson.overrides && !options.skipOverrides) {
139
- packageJson.overrides = {
139
+ // npm throws EOVERRIDE when an override key is also a direct dependency
140
+ // (unless specs match). The pruned dist pins exact versions and already
141
+ // resolved everything via the lockfile, so drop those redundant overrides.
142
+ const mergedOverrides = {
140
143
  ...rootPackageJson.overrides,
141
144
  ...packageJson.overrides,
142
145
  };
146
+ const overrides = {};
147
+ let hasOverrides = false;
148
+ for (const name in mergedOverrides) {
149
+ if (packageJson.dependencies?.[name] ||
150
+ packageJson.devDependencies?.[name] ||
151
+ packageJson.peerDependencies?.[name] ||
152
+ packageJson.optionalDependencies?.[name]) {
153
+ continue;
154
+ }
155
+ overrides[name] = mergedOverrides[name];
156
+ hasOverrides = true;
157
+ }
158
+ if (hasOverrides) {
159
+ packageJson.overrides = overrides;
160
+ }
161
+ else {
162
+ delete packageJson.overrides;
163
+ }
143
164
  }
144
165
  // pnpm
145
166
  if (rootPackageJson.pnpm?.overrides && !options.skipOverrides) {
@@ -56,6 +56,13 @@ export declare class TargetProjectLocator {
56
56
  private resolveImportWithTypescript;
57
57
  private resolveImportWithRequire;
58
58
  private findProjectOfResolvedModule;
59
+ /**
60
+ * Expand a `${configDir}` path mapping the same way TypeScript does. The
61
+ * template resolves to the directory of the tsconfig used for compilation,
62
+ * which for the importing file is its own project, so a configDir alias always
63
+ * points back into the source project (matching what `tsc` resolves).
64
+ */
65
+ private substituteConfigDirTemplate;
59
66
  private getAbsolutePath;
60
67
  private getRootTsConfig;
61
68
  private findMatchingProjectFiles;
@@ -22,6 +22,12 @@ function isBuiltinModuleImport(importExpr) {
22
22
  const packageName = (0, get_package_name_from_import_path_1.getPackageNameFromImportPath)(importExpr);
23
23
  return (0, node_module_1.isBuiltin)(packageName) || experimentalNodeModules.has(packageName);
24
24
  }
25
+ // TypeScript matches the `${configDir}` template case-insensitively and only as a
26
+ // prefix (commandLineParser.ts `startsWithConfigDirTemplate`).
27
+ const configDirTemplate = '${configDir}';
28
+ function startsWithConfigDirTemplate(value) {
29
+ return value.toLowerCase().startsWith(configDirTemplate.toLowerCase());
30
+ }
25
31
  class TargetProjectLocator {
26
32
  constructor(nodes, externalNodes = {}, npmResolutionCache = defaultNpmResolutionCache, packageJsonResolutionCache = defaultPackageJsonResolutionCache) {
27
33
  this.nodes = nodes;
@@ -75,7 +81,10 @@ class TargetProjectLocator {
75
81
  ? undefined
76
82
  : importExpr.substring(path.prefix.length, importExpr.length - path.suffix.length);
77
83
  for (let p of paths) {
78
- const path = matchedStar ? p.replace('*', matchedStar) : p;
84
+ let path = matchedStar ? p.replace('*', matchedStar) : p;
85
+ if (startsWithConfigDirTemplate(path)) {
86
+ path = this.substituteConfigDirTemplate(path, filePath);
87
+ }
79
88
  const maybeResolvedProject = this.findProjectOfResolvedModule(path);
80
89
  if (maybeResolvedProject) {
81
90
  return maybeResolvedProject;
@@ -343,6 +352,22 @@ class TargetProjectLocator {
343
352
  const importedProject = this.findMatchingProjectFiles(normalizedResolvedModule);
344
353
  return importedProject ? importedProject.name : void 0;
345
354
  }
355
+ /**
356
+ * Expand a `${configDir}` path mapping the same way TypeScript does. The
357
+ * template resolves to the directory of the tsconfig used for compilation,
358
+ * which for the importing file is its own project, so a configDir alias always
359
+ * points back into the source project (matching what `tsc` resolves).
360
+ */
361
+ substituteConfigDirTemplate(value, filePath) {
362
+ const sourceFilePath = (0, node_path_1.isAbsolute)(filePath)
363
+ ? (0, node_path_1.relative)(workspace_root_1.workspaceRoot, filePath)
364
+ : filePath;
365
+ const sourceProjectName = (0, find_project_for_path_1.findProjectForPath)(sourceFilePath, this.projectRootMappings);
366
+ const sourceProjectRoot = this.nodes[sourceProjectName]?.data.root ?? '.';
367
+ // tsc replaces the template with './' and normalizes against the config dir;
368
+ // here the config dir is the source project root (workspace-relative).
369
+ return node_path_1.posix.join(sourceProjectRoot, value.replace(configDirTemplate, './'));
370
+ }
346
371
  getAbsolutePath(path) {
347
372
  return (0, node_path_1.join)(workspace_root_1.workspaceRoot, path);
348
373
  }
@@ -35,22 +35,43 @@ export declare function forceRegisterEsmLoader(): void;
35
35
  * the importing module is itself TypeScript, and the specifier is relative, so
36
36
  * it never hijacks resolution that would otherwise succeed.
37
37
  *
38
+ * Used only as the fallback for runtimes without `module.registerHooks()`
39
+ * (Node < 22.15 / < 23.5); newer runtimes register the in-thread synchronous
40
+ * twin `nodeNextEsmResolveHook` instead. Keep the two in sync.
41
+ *
38
42
  * Exported so the hook can be exercised directly in unit tests.
39
43
  */
40
44
  export declare const NODENEXT_ESM_RESOLVER_SOURCE = "\nconst EXT_FALLBACK = { '.js': ['.ts', '.tsx'], '.mjs': ['.mts'], '.cjs': ['.cts'] };\nconst TS_PARENT_RE = /\\.(?:ts|tsx|mts|cts)(?:$|\\?)/;\nexport async function resolve(specifier, context, nextResolve) {\n try {\n return await nextResolve(specifier, context);\n } catch (err) {\n if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err;\n const parent = context.parentURL;\n if (!parent || !TS_PARENT_RE.test(parent)) throw err;\n if (!(specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('file:'))) throw err;\n const m = specifier.match(/(\\.(?:js|mjs|cjs))($|\\?)/);\n if (!m) throw err;\n const fallbacks = EXT_FALLBACK[m[1]];\n if (!fallbacks) throw err;\n const base = specifier.slice(0, m.index);\n const suffix = specifier.slice(m.index + m[1].length);\n for (const ext of fallbacks) {\n try { return await nextResolve(base + ext + suffix, context); } catch {}\n }\n throw err;\n }\n}\n";
41
45
  /**
42
- * Register a self-contained ESM resolution hook (via `Module.register`) that
43
- * rewrites TypeScript NodeNext-style `.js`/`.mjs`/`.cjs` relative specifiers to
44
- * their `.ts`/`.mts`/`.cts` sources. This is the ESM counterpart to
45
- * `ensureCjsResolverPatched`: Node's native type stripping loads the `.ts`
46
- * file, but neither native strip nor Node's ESM resolver rewrites the
47
- * extension, so `import './foo.js'` from a `.ts` source where only `foo.ts`
48
- * exists fails with ERR_MODULE_NOT_FOUND without it.
46
+ * Synchronous in-thread twin of `NODENEXT_ESM_RESOLVER_SOURCE` for
47
+ * `module.registerHooks()`: `nextResolve` throws synchronously rather than
48
+ * rejecting, so this uses try/catch instead of `await`. Keep the two in sync.
49
+ * Exported for unit tests.
50
+ */
51
+ export declare function nodeNextEsmResolveHook(specifier: string, context: {
52
+ parentURL?: string;
53
+ }, nextResolve: (specifier: string, context?: unknown) => {
54
+ url: string;
55
+ }): {
56
+ url: string;
57
+ };
58
+ /**
59
+ * Register an ESM resolution hook that rewrites TypeScript NodeNext-style
60
+ * `.js`/`.mjs`/`.cjs` relative specifiers to their `.ts`/`.mts`/`.cts` sources.
61
+ * This is the ESM counterpart to `ensureCjsResolverPatched`: Node's native type
62
+ * stripping loads the `.ts` file, but neither native strip nor Node's ESM
63
+ * resolver rewrites the extension, so `import './foo.js'` from a `.ts` source
64
+ * where only `foo.ts` exists fails with ERR_MODULE_NOT_FOUND without it.
49
65
  *
50
- * The hook is inlined as a `data:` module (see `NODENEXT_ESM_RESOLVER_SOURCE`)
51
- * and relies only on Node's default resolver, so it needs no ts-node/swc-node.
66
+ * Prefers `module.registerHooks()` (Node 22.15+ / 23.5+), passing the in-thread
67
+ * `nodeNextEsmResolveHook`. Falls back to `module.register()` with the inlined
68
+ * `data:` module (`NODENEXT_ESM_RESOLVER_SOURCE`) on older runtimes that lack
69
+ * `registerHooks` - `module.register()` emits a runtime DeprecationWarning
70
+ * (DEP0205) on Node 25.9+/26+, so it is only used when nothing better exists.
71
+ * Either way the hook relies only on Node's default resolver - no
72
+ * ts-node/swc-node.
52
73
  *
53
- * Idempotent and best-effort: a no-op when `Module.register` is unavailable,
74
+ * Idempotent and best-effort: a no-op when no registration API is available,
54
75
  * when a TypeScript transpiler is already preloaded (see
55
76
  * `isTsTranspilerPreloaded`), or if registration fails.
56
77
  */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NODENEXT_ESM_RESOLVER_SOURCE = void 0;
4
4
  exports.forceRegisterEsmLoader = forceRegisterEsmLoader;
5
+ exports.nodeNextEsmResolveHook = nodeNextEsmResolveHook;
5
6
  exports.ensureNodeNextEsmResolverRegistered = ensureNodeNextEsmResolverRegistered;
6
7
  exports.ensureCjsResolverPatched = ensureCjsResolverPatched;
7
8
  exports.isNativeStripPreferred = isNativeStripPreferred;
@@ -100,6 +101,10 @@ function ensureEsmLoaderRegistered(opts) {
100
101
  * the importing module is itself TypeScript, and the specifier is relative, so
101
102
  * it never hijacks resolution that would otherwise succeed.
102
103
  *
104
+ * Used only as the fallback for runtimes without `module.registerHooks()`
105
+ * (Node < 22.15 / < 23.5); newer runtimes register the in-thread synchronous
106
+ * twin `nodeNextEsmResolveHook` instead. Keep the two in sync.
107
+ *
103
108
  * Exported so the hook can be exercised directly in unit tests.
104
109
  */
105
110
  exports.NODENEXT_ESM_RESOLVER_SOURCE = `
@@ -126,20 +131,70 @@ export async function resolve(specifier, context, nextResolve) {
126
131
  }
127
132
  }
128
133
  `;
134
+ const NODENEXT_EXT_FALLBACK = {
135
+ '.js': ['.ts', '.tsx'],
136
+ '.mjs': ['.mts'],
137
+ '.cjs': ['.cts'],
138
+ };
139
+ const NODENEXT_TS_PARENT_RE = /\.(?:ts|tsx|mts|cts)(?:$|\?)/;
140
+ /**
141
+ * Synchronous in-thread twin of `NODENEXT_ESM_RESOLVER_SOURCE` for
142
+ * `module.registerHooks()`: `nextResolve` throws synchronously rather than
143
+ * rejecting, so this uses try/catch instead of `await`. Keep the two in sync.
144
+ * Exported for unit tests.
145
+ */
146
+ function nodeNextEsmResolveHook(specifier, context, nextResolve) {
147
+ try {
148
+ return nextResolve(specifier, context);
149
+ }
150
+ catch (err) {
151
+ if (err?.code !== 'ERR_MODULE_NOT_FOUND')
152
+ throw err;
153
+ const parent = context.parentURL;
154
+ if (!parent || !NODENEXT_TS_PARENT_RE.test(parent))
155
+ throw err;
156
+ if (!(specifier.startsWith('./') ||
157
+ specifier.startsWith('../') ||
158
+ specifier.startsWith('file:'))) {
159
+ throw err;
160
+ }
161
+ const m = specifier.match(/(\.(?:js|mjs|cjs))($|\?)/);
162
+ if (!m)
163
+ throw err;
164
+ const fallbacks = NODENEXT_EXT_FALLBACK[m[1]];
165
+ if (!fallbacks)
166
+ throw err;
167
+ const base = specifier.slice(0, m.index);
168
+ const suffix = specifier.slice(m.index + m[1].length);
169
+ for (const ext of fallbacks) {
170
+ try {
171
+ return nextResolve(base + ext + suffix, context);
172
+ }
173
+ catch {
174
+ // try the next fallback
175
+ }
176
+ }
177
+ throw err;
178
+ }
179
+ }
129
180
  let nodeNextEsmResolverRegistered = false;
130
181
  /**
131
- * Register a self-contained ESM resolution hook (via `Module.register`) that
132
- * rewrites TypeScript NodeNext-style `.js`/`.mjs`/`.cjs` relative specifiers to
133
- * their `.ts`/`.mts`/`.cts` sources. This is the ESM counterpart to
134
- * `ensureCjsResolverPatched`: Node's native type stripping loads the `.ts`
135
- * file, but neither native strip nor Node's ESM resolver rewrites the
136
- * extension, so `import './foo.js'` from a `.ts` source where only `foo.ts`
137
- * exists fails with ERR_MODULE_NOT_FOUND without it.
182
+ * Register an ESM resolution hook that rewrites TypeScript NodeNext-style
183
+ * `.js`/`.mjs`/`.cjs` relative specifiers to their `.ts`/`.mts`/`.cts` sources.
184
+ * This is the ESM counterpart to `ensureCjsResolverPatched`: Node's native type
185
+ * stripping loads the `.ts` file, but neither native strip nor Node's ESM
186
+ * resolver rewrites the extension, so `import './foo.js'` from a `.ts` source
187
+ * where only `foo.ts` exists fails with ERR_MODULE_NOT_FOUND without it.
138
188
  *
139
- * The hook is inlined as a `data:` module (see `NODENEXT_ESM_RESOLVER_SOURCE`)
140
- * and relies only on Node's default resolver, so it needs no ts-node/swc-node.
189
+ * Prefers `module.registerHooks()` (Node 22.15+ / 23.5+), passing the in-thread
190
+ * `nodeNextEsmResolveHook`. Falls back to `module.register()` with the inlined
191
+ * `data:` module (`NODENEXT_ESM_RESOLVER_SOURCE`) on older runtimes that lack
192
+ * `registerHooks` - `module.register()` emits a runtime DeprecationWarning
193
+ * (DEP0205) on Node 25.9+/26+, so it is only used when nothing better exists.
194
+ * Either way the hook relies only on Node's default resolver - no
195
+ * ts-node/swc-node.
141
196
  *
142
- * Idempotent and best-effort: a no-op when `Module.register` is unavailable,
197
+ * Idempotent and best-effort: a no-op when no registration API is available,
143
198
  * when a TypeScript transpiler is already preloaded (see
144
199
  * `isTsTranspilerPreloaded`), or if registration fails.
145
200
  */
@@ -148,16 +203,16 @@ function ensureNodeNextEsmResolverRegistered() {
148
203
  return;
149
204
  nodeNextEsmResolverRegistered = true;
150
205
  const module = require('node:module');
151
- if (typeof module.register !== 'function')
152
- return;
153
206
  // Skip when a transpiler was preloaded via `--require`/`--import` (e.g.
154
207
  // `--require ts-node/register`, which Nx uses only when it runs from `.ts`
155
- // source). `module.register()` spins up a loader-hook worker thread on which
156
- // Node re-runs those preloads, resolved relative to the *current* working
157
- // directory - and Nx plugin workers `chdir()` into the analyzed workspace
158
- // first. If that workspace can't resolve the preloaded module, the loader
159
- // worker throws and can leave module resolution in a bad state, so we must
160
- // avoid the call entirely; catching it is not a clean recovery.
208
+ // source). The `module.register()` fallback below spins up a loader-hook
209
+ // worker thread on which Node re-runs those preloads, resolved relative to
210
+ // the *current* working directory - and Nx plugin workers `chdir()` into the
211
+ // analyzed workspace first. If that workspace can't resolve the preloaded
212
+ // module, the loader worker throws and can leave module resolution in a bad
213
+ // state, so we must avoid the call entirely; catching it is not a clean
214
+ // recovery. `module.registerHooks()` runs in-thread with no such worker, but
215
+ // we keep the skip uniform so resolver coverage doesn't vary by Node version.
161
216
  //
162
217
  // Consequence: in that from-`.ts`-source invocation a `type: module` plugin
163
218
  // using NodeNext `.js` specifiers won't get this resolver (a preloaded
@@ -165,6 +220,20 @@ function ensureNodeNextEsmResolverRegistered() {
165
220
  // unaffected - its workers run compiled `.js` with no preload.
166
221
  if (isTsTranspilerPreloaded())
167
222
  return;
223
+ // Synchronous in-thread hooks (Node 22.15+ / 23.5+). Preferred because
224
+ // `module.register()` is deprecated (DEP0205) from Node 25.9+/26+.
225
+ const registerHooks = module.registerHooks;
226
+ if (typeof registerHooks === 'function') {
227
+ try {
228
+ registerHooks.call(module, { resolve: nodeNextEsmResolveHook });
229
+ }
230
+ catch {
231
+ // Best-effort: leave Node's native handling in place rather than failing.
232
+ }
233
+ return;
234
+ }
235
+ if (typeof module.register !== 'function')
236
+ return;
168
237
  try {
169
238
  module.register('data:text/javascript,' + encodeURIComponent(exports.NODENEXT_ESM_RESOLVER_SOURCE));
170
239
  }
@@ -97,10 +97,12 @@ function buildPackageJsonPatterns(workspaceRoot, readJson) {
97
97
  };
98
98
  }
99
99
  function buildPackageJsonWorkspacesMatcher(patterns) {
100
+ // Compile each glob once; the returned matcher runs per package.json path.
101
+ const positive = patterns.positive.map((p) => new minimatch_1.Minimatch(p));
102
+ const negative = patterns.negative.map((p) => new minimatch_1.Minimatch(p));
100
103
  return (p) =>
101
104
  // use lookup to avoid unnecessary minimatch calls
102
- (patterns.positiveLookup[p] ||
103
- patterns.positive.some((positive) => (0, minimatch_1.minimatch)(p, positive))) &&
105
+ (patterns.positiveLookup[p] || positive.some((m) => m.match(p))) &&
104
106
  /**
105
107
  * minimatch will return true if the given p is NOT excluded by the negative pattern.
106
108
  *
@@ -111,7 +113,7 @@ function buildPackageJsonWorkspacesMatcher(patterns) {
111
113
  * excluded by any of the negative patterns.
112
114
  */
113
115
  !patterns.negativeLookup[p] &&
114
- patterns.negative.every((negative) => (0, minimatch_1.minimatch)(p, negative));
116
+ negative.every((m) => m.match(p));
115
117
  }
116
118
  function createNodeFromPackageJson(pkgJsonPath, workspaceRoot, cache, isInPackageManagerWorkspaces, packageManagerCommand) {
117
119
  const json = (0, fileutils_1.readJsonFile)((0, node_path_1.join)(workspaceRoot, pkgJsonPath));
@@ -9,4 +9,3 @@ export declare function reverse(graph: ProjectGraph): ProjectGraph;
9
9
  export declare function filterNodes(predicate?: (n: ProjectGraphProjectNode) => boolean): (p: ProjectGraph) => ProjectGraph;
10
10
  export declare function isNpmProject(project: ProjectGraphProjectNode | ProjectGraphExternalNode): project is ProjectGraphExternalNode;
11
11
  export declare const pruneExternalNodes: (p: ProjectGraph) => ProjectGraph;
12
- export declare function withDeps(original: ProjectGraph, subsetNodes: ProjectGraphProjectNode[]): ProjectGraph;
@@ -4,7 +4,6 @@ exports.pruneExternalNodes = void 0;
4
4
  exports.reverse = reverse;
5
5
  exports.filterNodes = filterNodes;
6
6
  exports.isNpmProject = isNpmProject;
7
- exports.withDeps = withDeps;
8
7
  const reverseMemo = new Map();
9
8
  /**
10
9
  * Returns a new project graph where all the edges are reversed.
@@ -69,46 +68,3 @@ function isNpmProject(project) {
69
68
  return project?.type === 'npm';
70
69
  }
71
70
  exports.pruneExternalNodes = filterNodes();
72
- function withDeps(original, subsetNodes) {
73
- const res = { nodes: {}, dependencies: {} };
74
- const visitedNodes = [];
75
- const visitedEdges = [];
76
- Object.values(subsetNodes).forEach(recurNodes);
77
- Object.values(subsetNodes).forEach(recurEdges);
78
- return res;
79
- // ---------------------------------------------------------------------------
80
- function recurNodes(node) {
81
- if (visitedNodes.indexOf(node.name) > -1)
82
- return;
83
- res.nodes[node.name] = node;
84
- if (!res.dependencies[node.name]) {
85
- res.dependencies[node.name] = [];
86
- }
87
- visitedNodes.push(node.name);
88
- original.dependencies[node.name].forEach((n) => {
89
- if (original.nodes[n.target]) {
90
- recurNodes(original.nodes[n.target]);
91
- }
92
- });
93
- }
94
- function recurEdges(node) {
95
- if (visitedEdges.indexOf(node.name) > -1)
96
- return;
97
- visitedEdges.push(node.name);
98
- const ds = original.dependencies[node.name];
99
- ds.forEach((n) => {
100
- if (!original.nodes[n.target]) {
101
- return;
102
- }
103
- if (!res.dependencies[n.source]) {
104
- res.dependencies[n.source] = [];
105
- }
106
- res.dependencies[n.source].push(n);
107
- });
108
- ds.forEach((n) => {
109
- if (original.nodes[n.target]) {
110
- recurEdges(original.nodes[n.target]);
111
- }
112
- });
113
- }
114
- }