@nx/devkit 23.2.0-beta.11 → 23.2.0-beta.12

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.
@@ -34,6 +34,14 @@ export declare function upsertTargetDefault(tree: Tree, nxJson: NxJsonConfigurat
34
34
  * caller intended to find a specific entry but forgot to populate the lookup.
35
35
  */
36
36
  export declare function findTargetDefault(targetDefaults: TargetDefaults | undefined, locator: Pick<TargetDefaultEntry, 'target' | 'executor' | 'projects' | 'plugin'>): TargetDefaultEntry | undefined;
37
+ /**
38
+ * Shape-level test: whether a `targetDefaults` key is a plain target name
39
+ * rather than executor-shaped (`a:b`) or a glob. Executor strings are not
40
+ * required to contain `:`, so a plain-named key can still resolve as an
41
+ * executor key when some target's effective executor equals it; callers
42
+ * needing that guarantee must exclude executor collisions themselves.
43
+ */
44
+ export declare function isExactTargetNameKey(key: string): boolean;
37
45
  /**
38
46
  * The subset of a project graph node that the `projects` narrowing reads —
39
47
  * names come from the map keys, `root`/`tags` drive `findMatchingProjects`.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.upsertTargetDefault = upsertTargetDefault;
4
4
  exports.findTargetDefault = findTargetDefault;
5
+ exports.isExactTargetNameKey = isExactTargetNameKey;
5
6
  exports.updateTargetDefault = updateTargetDefault;
6
7
  exports.readTargetDefaultsForTarget = readTargetDefaultsForTarget;
7
8
  exports.addBuildTargetDefaults = addBuildTargetDefaults;
@@ -219,13 +220,24 @@ function* logicalTargetDefaultEntries(targetDefaults) {
219
220
  // keys win), and such target names are vanishingly rare — accepted rather than
220
221
  // worked around.
221
222
  const GLOB_CHARACTERS = new Set(['*', '|', '{', '}', '(', ')', '[']);
222
- function isExecutorLikeKey(key) {
223
- if (!key.includes(':'))
224
- return false;
223
+ function hasGlobCharacter(key) {
225
224
  for (const c of key)
226
225
  if (GLOB_CHARACTERS.has(c))
227
- return false;
228
- return true;
226
+ return true;
227
+ return false;
228
+ }
229
+ function isExecutorLikeKey(key) {
230
+ return key.includes(':') && !hasGlobCharacter(key);
231
+ }
232
+ /**
233
+ * Shape-level test: whether a `targetDefaults` key is a plain target name
234
+ * rather than executor-shaped (`a:b`) or a glob. Executor strings are not
235
+ * required to contain `:`, so a plain-named key can still resolve as an
236
+ * executor key when some target's effective executor equals it; callers
237
+ * needing that guarantee must exclude executor collisions themselves.
238
+ */
239
+ function isExactTargetNameKey(key) {
240
+ return !key.includes(':') && !hasGlobCharacter(key);
229
241
  }
230
242
  /**
231
243
  * Walk the `targetDefaults` entries matching `context` and run `callback`
@@ -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
- return await loadModuleByExtension(path, extension);
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
- // - kept for older Node lines.
76
+ // dynamic import(). ERR_REQUIRE_ESM is the legacy code for the same case,
77
+ // kept for older Node lines.
52
78
  try {
53
- return (0, devkit_internals_1.loadTsFile)(path, tsConfigPath);
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
- // The module must be loaded via dynamic import(). Register
61
- // tsconfig-paths first so workspace alias imports resolve, then try a
62
- // native dynamic import. Node 22.18+ LTS strips TS types on the ESM
63
- // path natively, so pure-ESM TLA configs load without any swc/ts-node
64
- // ESM loader. Only escalate to forceRegisterEsmLoader (which throws
65
- // when neither @swc-node/register nor ts-node is installed) if the
66
- // native attempt hits unsupported TS syntax.
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 (esmErr) {
72
- if (esmErr?.code !== 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' ||
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 esmErr;
232
+ throw retryErr;
75
233
  }
76
- // Module.register is global and one-shot per process. After this
77
- // runs, every subsequent ESM import in the process is routed
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 esmErr;
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));
@@ -147,7 +310,7 @@ async function load(path) {
147
310
  return await loadCommonJS(path);
148
311
  }
149
312
  catch (e) {
150
- if (e.code === 'ERR_REQUIRE_ESM') {
313
+ if (['ERR_REQUIRE_ESM', 'ERR_REQUIRE_ASYNC_MODULE'].includes(e.code)) {
151
314
  // If `require` fails to load ESM, try dynamic `import()`. ESM requires file url protocol for handling absolute paths.
152
315
  return loadESM(path);
153
316
  }
@@ -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
- const pathAsFileUrl = (0, node_url_1.pathToFileURL)(path).pathname;
170
- return await (0, exports.dynamicImport)(`${pathAsFileUrl}?t=${Date.now()}`);
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.2.0-beta.11",
3
+ "version": "23.2.0-beta.12",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "files": [
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "jest": "30.3.0",
70
- "nx": "23.2.0-beta.11"
70
+ "nx": "23.2.0-beta.12"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "nx": ">= 22 <= 24 || ^23.0.0-0"