@nx/devkit 23.1.2 → 23.1.3
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.
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare let dynamicImport: Function;
|
|
2
2
|
export declare function loadConfigFile<T extends object = any>(configFilePath: string, tsconfigFileNames?: string[]): Promise<T>;
|
|
3
|
+
export declare function isTranspilerRecoverableError(err: unknown, path: string): boolean;
|
|
4
|
+
export declare function clearConfigFromRequireCache(rootId: string, cache?: NodeJS.Dict<NodeModule>): void;
|
|
5
|
+
export declare function unwrapCjsInterop(path: string, module: unknown, cache?: NodeJS.Dict<NodeModule>): unknown;
|
|
3
6
|
export declare function getRootTsConfigPath(): string | null;
|
|
4
7
|
export declare function getRootTsConfigFileName(): string | null;
|
|
5
8
|
export declare function clearRequireCache(): void;
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dynamicImport = void 0;
|
|
4
4
|
exports.loadConfigFile = loadConfigFile;
|
|
5
|
+
exports.isTranspilerRecoverableError = isTranspilerRecoverableError;
|
|
6
|
+
exports.clearConfigFromRequireCache = clearConfigFromRequireCache;
|
|
7
|
+
exports.unwrapCjsInterop = unwrapCjsInterop;
|
|
5
8
|
exports.getRootTsConfigPath = getRootTsConfigPath;
|
|
6
9
|
exports.getRootTsConfigFileName = getRootTsConfigFileName;
|
|
7
10
|
exports.clearRequireCache = clearRequireCache;
|
|
8
11
|
const fs_1 = require("fs");
|
|
9
12
|
const node_url_1 = require("node:url");
|
|
13
|
+
const node_util_1 = require("node:util");
|
|
10
14
|
const devkit_exports_1 = require("nx/src/devkit-exports");
|
|
11
15
|
const devkit_internals_1 = require("nx/src/devkit-internals");
|
|
12
16
|
const path_1 = require("path");
|
|
@@ -27,8 +31,22 @@ function isTypeScriptFile(extension) {
|
|
|
27
31
|
}
|
|
28
32
|
async function loadTypeScriptModule(path, extension, tsconfigFileNames) {
|
|
29
33
|
const tsConfigPath = getTypeScriptConfigPath(path, tsconfigFileNames);
|
|
34
|
+
const modulePath = resolveModulePath(path);
|
|
30
35
|
if (!tsConfigPath) {
|
|
31
|
-
|
|
36
|
+
// The tsconfig-less path needs the same ESM-registry routing as below:
|
|
37
|
+
// require() of a sync-ESM .ts serves the stale registry entry on reloads.
|
|
38
|
+
if (esmRegistryPaths.has(modulePath)) {
|
|
39
|
+
// Clear first so an ESM-to-CJS rewrite re-evaluates instead of the
|
|
40
|
+
// import() serving the cached require.cache entry.
|
|
41
|
+
clearConfigFromRequireCache(modulePath);
|
|
42
|
+
return unwrapCjsInterop(path, await loadESM(path));
|
|
43
|
+
}
|
|
44
|
+
clearConfigFromRequireCache(modulePath);
|
|
45
|
+
const result = await loadModuleByExtension(path, extension);
|
|
46
|
+
if (node_util_1.types.isModuleNamespaceObject(result)) {
|
|
47
|
+
esmRegistryPaths.add(modulePath);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
32
50
|
}
|
|
33
51
|
// loadTsFile was added in nx@23. @nx/devkit's peer range supports older
|
|
34
52
|
// nx majors, so fall back to the legacy registerTsProject + require path
|
|
@@ -42,57 +60,202 @@ async function loadTypeScriptModule(path, extension, tsconfigFileNames) {
|
|
|
42
60
|
cleanup();
|
|
43
61
|
}
|
|
44
62
|
}
|
|
63
|
+
// require.cache busting cannot invalidate a module the ESM registry
|
|
64
|
+
// holds; reloads of known-ESM paths need a cache-busted import().
|
|
65
|
+
if (esmRegistryPaths.has(modulePath)) {
|
|
66
|
+
return await loadTsFileViaImport(path, tsConfigPath);
|
|
67
|
+
}
|
|
68
|
+
// A CJS-shaped .ts stays in require.cache and may have changed on disk
|
|
69
|
+
// since; clear it so the reload re-reads the file.
|
|
70
|
+
clearConfigFromRequireCache(modulePath);
|
|
45
71
|
// Both .ts and .mts go through loadTsFile first. Node 22.12+ supports
|
|
46
72
|
// require() of synchronous ESM by default, and loadTsFile's lazy fallback
|
|
47
73
|
// covers swc/ts-node + tsconfig-paths registration when needed (swc-node
|
|
48
74
|
// hooks .cts/.mts/.ts via Module._extensions). Async-only ESM modules
|
|
49
75
|
// (top-level await) throw ERR_REQUIRE_ASYNC_MODULE and fall through to
|
|
50
|
-
// dynamic import(). ERR_REQUIRE_ESM is the legacy code for the same case
|
|
51
|
-
//
|
|
76
|
+
// dynamic import(). ERR_REQUIRE_ESM is the legacy code for the same case,
|
|
77
|
+
// kept for older Node lines.
|
|
52
78
|
try {
|
|
53
|
-
|
|
79
|
+
const result = (0, devkit_internals_1.loadTsFile)(path, tsConfigPath);
|
|
80
|
+
if (node_util_1.types.isModuleNamespaceObject(result)) {
|
|
81
|
+
esmRegistryPaths.add(modulePath);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
54
84
|
}
|
|
55
85
|
catch (e) {
|
|
56
86
|
if (e?.code !== 'ERR_REQUIRE_ESM' &&
|
|
57
87
|
e?.code !== 'ERR_REQUIRE_ASYNC_MODULE') {
|
|
58
88
|
throw e;
|
|
59
89
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
return await loadTsFileViaImport(path, tsConfigPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Resolved paths of configs that require() loaded as synchronous ESM;
|
|
94
|
+
// reloads of these route through a cache-busted import(). On globalThis so the
|
|
95
|
+
// state survives clearRequireCache evicting this module itself (a
|
|
96
|
+
// workspace-linked devkit resolves outside node_modules).
|
|
97
|
+
const esmRegistryPaths = (globalThis[Symbol.for('@nx/devkit:esmRegistryPaths')] ??= new Set());
|
|
98
|
+
// Error codes from the load machinery (resolution, type stripping) that
|
|
99
|
+
// warrant registering swc/ts-node + tsconfig-paths and importing again.
|
|
100
|
+
// Unlisted errors propagate unchanged so a failing config is not
|
|
101
|
+
// re-evaluated. A not-found error can also come from the config's own
|
|
102
|
+
// dynamic import()/require() of a missing module, in which case the retry
|
|
103
|
+
// re-runs its top-level side effects; accepted so that static alias and
|
|
104
|
+
// extensionless imports (which fail at link time, before any user code
|
|
105
|
+
// runs) stay recoverable.
|
|
106
|
+
const ESM_LOAD_FALLBACK_ERROR_CODES = new Set([
|
|
107
|
+
'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX',
|
|
108
|
+
'ERR_MODULE_NOT_FOUND',
|
|
109
|
+
'MODULE_NOT_FOUND',
|
|
110
|
+
'ERR_UNKNOWN_FILE_EXTENSION',
|
|
111
|
+
]);
|
|
112
|
+
// Code-less errors that loadTsFile recovers on first load via the swc/
|
|
113
|
+
// ts-node fallback: a CJS-only global (__dirname, __filename, require) or a
|
|
114
|
+
// type-only named import evaluated on the native ESM path. Reloads must
|
|
115
|
+
// recover them too or an edit that introduces one turns into a hard graph
|
|
116
|
+
// failure. Prefer the host nx's classifiers so the gate matches what its
|
|
117
|
+
// loadTsFile recovers; nx versions that support the error classes without
|
|
118
|
+
// re-exporting the classifiers fall back to local replicas of them.
|
|
119
|
+
function isTranspilerRecoverableError(err, path) {
|
|
120
|
+
return ((typeof devkit_internals_1.isRequireInEsmScopeError === 'function'
|
|
121
|
+
? (0, devkit_internals_1.isRequireInEsmScopeError)(err, path)
|
|
122
|
+
: isRequireInEsmScopeErrorReplica(err, path)) ||
|
|
123
|
+
(typeof devkit_internals_1.isTsEsmNamedExportLinkageError === 'function'
|
|
124
|
+
? (0, devkit_internals_1.isTsEsmNamedExportLinkageError)(err, path)
|
|
125
|
+
: isTsEsmNamedExportLinkageErrorReplica(err, path)));
|
|
126
|
+
}
|
|
127
|
+
function isRequireInEsmScopeErrorReplica(err, filePath) {
|
|
128
|
+
if (!(err instanceof ReferenceError)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (!(filePath.endsWith('.ts') || filePath.endsWith('.mts'))) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return /(require|__dirname|__filename) is not defined/.test(err.message);
|
|
135
|
+
}
|
|
136
|
+
function isTsEsmNamedExportLinkageErrorReplica(err, filePath) {
|
|
137
|
+
if (!(err instanceof SyntaxError)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return ((filePath.endsWith('.ts') || filePath.endsWith('.mts')) &&
|
|
141
|
+
err.message.includes('does not provide an export named'));
|
|
142
|
+
}
|
|
143
|
+
// Invalidates a config module and its local CommonJS dependency subtree,
|
|
144
|
+
// leaving unrelated cached modules untouched (a broad clear re-evaluates
|
|
145
|
+
// them and breaks singleton identity). Walks each module's recorded
|
|
146
|
+
// children AND the cache-current instance for the same id, since another
|
|
147
|
+
// config may retain an older instance of a shared dependency. Detaches the
|
|
148
|
+
// root from its requiring parent so repeated reloads don't accumulate
|
|
149
|
+
// Module objects in the parent's children array.
|
|
150
|
+
function clearConfigFromRequireCache(rootId, cache = require.cache) {
|
|
151
|
+
const root = cache[rootId];
|
|
152
|
+
if (!root) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const visited = new Set();
|
|
156
|
+
const idsToDelete = new Set();
|
|
157
|
+
const queue = [root];
|
|
158
|
+
while (queue.length) {
|
|
159
|
+
const mod = queue.pop();
|
|
160
|
+
if (!mod || visited.has(mod)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
visited.add(mod);
|
|
164
|
+
if (packageInstallationDirectories.some((dir) => mod.id.includes(dir))) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
idsToDelete.add(mod.id);
|
|
168
|
+
const current = cache[mod.id];
|
|
169
|
+
if (current && current !== mod) {
|
|
170
|
+
queue.push(current);
|
|
171
|
+
}
|
|
172
|
+
for (const child of mod.children) {
|
|
173
|
+
queue.push(child);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const id of idsToDelete) {
|
|
177
|
+
delete cache[id];
|
|
178
|
+
}
|
|
179
|
+
if (root.parent) {
|
|
180
|
+
root.parent.children = root.parent.children.filter((child) => child.id !== rootId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Canonical key for require.cache checks and esmRegistryPaths: a symlinked
|
|
184
|
+
// config's caller path differs from require's resolved filename.
|
|
185
|
+
function resolveModulePath(path) {
|
|
186
|
+
try {
|
|
187
|
+
return require.resolve(path);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return path;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function loadTsFileViaImport(path, tsConfigPath) {
|
|
194
|
+
// Clear any prior require.cache entry so an ESM-to-CJS rewrite (or a
|
|
195
|
+
// loader-classified CJS load) re-evaluates instead of serving the cached
|
|
196
|
+
// copy.
|
|
197
|
+
clearConfigFromRequireCache(resolveModulePath(path));
|
|
198
|
+
// Try a bare native import first: Node 22.18+ strips TS types on the ESM
|
|
199
|
+
// path natively, and registerTsProject must NOT run up front. A
|
|
200
|
+
// registered swc/ts-node ESM loader intercepts every subsequent import
|
|
201
|
+
// in the process (it cannot be unregistered) and may classify a .ts
|
|
202
|
+
// config as CommonJS, defeating loadESM's cache-busting query on
|
|
203
|
+
// reloads. Register only when the native attempt fails.
|
|
204
|
+
try {
|
|
205
|
+
return unwrapCjsInterop(path, await loadESM(path));
|
|
206
|
+
}
|
|
207
|
+
catch (esmErr) {
|
|
208
|
+
if (!ESM_LOAD_FALLBACK_ERROR_CODES.has(esmErr?.code) &&
|
|
209
|
+
!isTranspilerRecoverableError(esmErr, path)) {
|
|
210
|
+
throw esmErr;
|
|
211
|
+
}
|
|
67
212
|
const cleanup = (0, devkit_internals_1.registerTsProject)(tsConfigPath);
|
|
68
213
|
try {
|
|
69
|
-
return await loadESM(path);
|
|
214
|
+
return unwrapCjsInterop(path, await loadESM(path));
|
|
70
215
|
}
|
|
71
|
-
catch (
|
|
72
|
-
if (
|
|
216
|
+
catch (retryErr) {
|
|
217
|
+
if (isTranspilerRecoverableError(retryErr, path)) {
|
|
218
|
+
// A registered swc ESM loader compiles the retry to CJS, but
|
|
219
|
+
// ts-node/esm keeps the file ESM, so CJS globals stay undefined on
|
|
220
|
+
// the import path. Route through the CJS transpiler hook
|
|
221
|
+
// registerTsProject installed; it reads fresh from disk. A
|
|
222
|
+
// namespace result means no hook intercepted and require() hit the
|
|
223
|
+
// stale ESM registry, so surface the error instead.
|
|
224
|
+
const required = require(path);
|
|
225
|
+
if (!node_util_1.types.isModuleNamespaceObject(required)) {
|
|
226
|
+
return required;
|
|
227
|
+
}
|
|
228
|
+
throw retryErr;
|
|
229
|
+
}
|
|
230
|
+
if (retryErr?.code !== 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' ||
|
|
73
231
|
typeof devkit_internals_1.forceRegisterEsmLoader !== 'function') {
|
|
74
|
-
throw
|
|
232
|
+
throw retryErr;
|
|
75
233
|
}
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// through the registered loader, forfeiting Node's native TS
|
|
79
|
-
// stripping for the dynamic-import path. If neither swc-node nor
|
|
80
|
-
// ts-node is installed, forceRegisterEsmLoader throws - surface the
|
|
81
|
-
// original ESM error in that case so the user sees the real
|
|
82
|
-
// problem, not a misleading "loader missing" message.
|
|
234
|
+
// Loader registration cannot be undone. Preserve the import error
|
|
235
|
+
// if registration itself fails.
|
|
83
236
|
try {
|
|
84
237
|
(0, devkit_internals_1.forceRegisterEsmLoader)();
|
|
85
238
|
}
|
|
86
239
|
catch {
|
|
87
|
-
throw
|
|
240
|
+
throw retryErr;
|
|
88
241
|
}
|
|
89
|
-
return await loadESM(path);
|
|
242
|
+
return unwrapCjsInterop(path, await loadESM(path));
|
|
90
243
|
}
|
|
91
244
|
finally {
|
|
92
245
|
cleanup();
|
|
93
246
|
}
|
|
94
247
|
}
|
|
95
248
|
}
|
|
249
|
+
// Some loaders emit CJS for a dynamic import, wrapping module.exports in
|
|
250
|
+
// namespace.default (the __esModule marker is optional), so cache identity is
|
|
251
|
+
// the discriminator: a require(esm) entry holds the namespace itself, and the
|
|
252
|
+
// entry check stops a default-less namespace from matching undefined.
|
|
253
|
+
function unwrapCjsInterop(path, module, cache = require.cache) {
|
|
254
|
+
const entry = cache[resolveModulePath(path)];
|
|
255
|
+
const cjsExports = module
|
|
256
|
+
?.default;
|
|
257
|
+
return entry && entry.exports === cjsExports ? cjsExports : module;
|
|
258
|
+
}
|
|
96
259
|
function getTypeScriptConfigPath(path, tsconfigFileNames) {
|
|
97
260
|
const siblingFiles = (0, fs_1.readdirSync)((0, path_1.dirname)(path));
|
|
98
261
|
const tsConfigFileName = (tsconfigFileNames ?? ['tsconfig.json']).find((name) => siblingFiles.includes(name));
|
|
@@ -165,7 +328,14 @@ async function loadCommonJS(path) {
|
|
|
165
328
|
}
|
|
166
329
|
return require(path);
|
|
167
330
|
}
|
|
331
|
+
// Global monotonic counter (not Date.now()) so two reloads never share a
|
|
332
|
+
// cache-busting URL, across same-millisecond loads, module reloads, and
|
|
333
|
+
// devkit copies.
|
|
334
|
+
const esmLoadState = (globalThis[Symbol.for('@nx/devkit:esmLoadState')] ??= { count: 0 });
|
|
168
335
|
async function loadESM(path) {
|
|
169
|
-
|
|
170
|
-
|
|
336
|
+
// Keep the full file URL (.pathname would drop a UNC authority); the
|
|
337
|
+
// unique query gives each reload a fresh ESM registry key.
|
|
338
|
+
const fileUrl = (0, node_url_1.pathToFileURL)(path);
|
|
339
|
+
fileUrl.searchParams.set('t', (esmLoadState.count++).toString());
|
|
340
|
+
return await (0, exports.dynamicImport)(fileUrl.href);
|
|
171
341
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nx/devkit",
|
|
3
|
-
"version": "23.1.
|
|
3
|
+
"version": "23.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"files": [
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"jest": "30.3.0",
|
|
71
|
-
"nx": "23.1.
|
|
71
|
+
"nx": "23.1.3"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"nx": ">= 22 <= 24 || ^23.0.0-0"
|