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.
- package/dist/bin/init-local.js +1 -1
- package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.d.ts +1 -0
- package/dist/src/command-line/init/implementation/dot-nx/add-nx-scripts.js +6 -1
- package/dist/src/command-line/init/init-v2.js +1 -1
- package/dist/src/command-line/migrate/migrate.d.ts +2 -0
- package/dist/src/command-line/migrate/migrate.js +32 -6
- package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.d.ts +1 -1
- package/dist/src/command-line/nx-cloud/connect/connect-to-nx-cloud.js +28 -21
- package/dist/src/command-line/release/version/resolve-current-version.js +4 -1
- package/dist/src/core/graph/main.js +1 -1
- package/dist/src/daemon/server/project-graph-incremental-recomputation.js +30 -11
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/plugins/js/lock-file/bun-parser.js +61 -38
- package/dist/src/plugins/js/lock-file/utils/pnpm-normalizer.js +1 -3
- package/dist/src/plugins/js/package-json/create-package-json.js +22 -1
- package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.d.ts +7 -0
- package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +26 -1
- package/dist/src/plugins/js/utils/register.d.ts +31 -10
- package/dist/src/plugins/js/utils/register.js +87 -18
- package/dist/src/plugins/package-json/create-nodes.js +5 -3
- package/dist/src/project-graph/operators.d.ts +0 -1
- package/dist/src/project-graph/operators.js +0 -44
- package/dist/src/project-graph/project-graph-builder.js +26 -10
- package/dist/src/project-graph/utils/project-configuration/target-defaults.js +23 -6
- package/dist/src/project-graph/utils/project-configuration-utils.js +28 -28
- package/dist/src/tasks-runner/batch/run-batch.js +3 -0
- package/dist/src/tasks-runner/cache.js +6 -0
- package/dist/src/tasks-runner/pseudo-terminal.d.ts +1 -0
- package/dist/src/tasks-runner/pseudo-terminal.js +18 -4
- package/dist/src/tasks-runner/run-command.js +24 -8
- package/dist/src/tasks-runner/utils.d.ts +1 -0
- package/dist/src/tasks-runner/utils.js +4 -0
- package/dist/src/utils/ab-testing.d.ts +4 -4
- package/dist/src/utils/ab-testing.js +5 -5
- package/dist/src/utils/catalog/manager-utils.d.ts +9 -0
- package/dist/src/utils/catalog/manager-utils.js +215 -0
- package/dist/src/utils/catalog/pnpm-manager.js +4 -102
- package/dist/src/utils/catalog/yarn-manager.js +4 -102
- package/dist/src/utils/is-sandbox.js +2 -0
- package/dist/src/utils/min-release-age/behavior/pnpm.d.ts +6 -5
- package/dist/src/utils/min-release-age/behavior/pnpm.js +87 -339
- package/dist/src/utils/package-json.d.ts +1 -0
- package/dist/src/utils/package-manager.d.ts +6 -3
- package/dist/src/utils/package-manager.js +21 -7
- package/dist/src/utils/spinner.d.ts +13 -1
- package/dist/src/utils/spinner.js +20 -4
- 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
|
-
|
|
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 (
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
740
|
-
const
|
|
741
|
-
//
|
|
742
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
923
|
-
|
|
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
|
-
|
|
946
|
+
cmp = candidate.version.localeCompare(best.version, undefined, {
|
|
926
947
|
numeric: true,
|
|
927
948
|
sensitivity: 'base',
|
|
928
949
|
});
|
|
929
950
|
}
|
|
930
951
|
catch {
|
|
931
|
-
|
|
932
|
-
return b.version.localeCompare(a.version);
|
|
952
|
+
cmp = candidate.version.localeCompare(best.version);
|
|
933
953
|
}
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
*
|
|
140
|
-
*
|
|
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
|
|
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
|
|
156
|
-
// Node re-runs those preloads, resolved relative to
|
|
157
|
-
// directory - and Nx plugin workers `chdir()` into the
|
|
158
|
-
// first. If that workspace can't resolve the preloaded
|
|
159
|
-
// worker throws and can leave module resolution in a bad
|
|
160
|
-
// avoid the call entirely; catching it is not a clean
|
|
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
|
-
|
|
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
|
-
}
|