import-in-the-middle 3.0.0 → 3.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/CHANGELOG.md +14 -0
- package/create-hook.mjs +52 -3
- package/index.js +21 -1
- package/lib/get-exports.mjs +6 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.2](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.0.1...import-in-the-middle-v3.0.2) (2026-06-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* Updated proxying logic for builtins that are used as CJS ([#250](https://github.com/nodejs/import-in-the-middle/issues/250)) ([f69eb8f](https://github.com/nodejs/import-in-the-middle/commit/f69eb8f13c91b7088003e7390b4f54c5076aa50b))
|
|
9
|
+
|
|
10
|
+
## [3.0.1](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.0.0...import-in-the-middle-v3.0.1) (2026-04-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* handle file specifiers from turbopacked packages better ([#246](https://github.com/nodejs/import-in-the-middle/issues/246)) ([04539fb](https://github.com/nodejs/import-in-the-middle/commit/04539fb5ceb9e4816933bd4310c1334077af1643))
|
|
16
|
+
|
|
3
17
|
## [3.0.0](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.6...import-in-the-middle-v3.0.0) (2026-01-30)
|
|
4
18
|
|
|
5
19
|
|
package/create-hook.mjs
CHANGED
|
@@ -265,12 +265,19 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
|
|
|
265
265
|
const objectKey = JSON.stringify(n)
|
|
266
266
|
const reExportedName = n === 'default' ? n : objectKey
|
|
267
267
|
|
|
268
|
+
// For the module.exports synthetic export (Node 23+), fall back to $default
|
|
269
|
+
// when namespace['module.exports'] is not exposed by the native ESM namespace
|
|
270
|
+
// (builtins don't expose it). This ensures the IITM hook proxy returns the
|
|
271
|
+
// actual CJS value (e.g. EventEmitter) when an instrumentor reads
|
|
272
|
+
// capturedExports['module.exports'], rather than undefined.
|
|
273
|
+
const moduleExportsFallback = n === 'module.exports' ? ' ?? $default' : ''
|
|
274
|
+
|
|
268
275
|
addSetter(n, `
|
|
269
276
|
let ${variableName}
|
|
270
277
|
__overridden[${objectKey}] = false
|
|
271
278
|
let ${variableName}Defer = false
|
|
272
279
|
try {
|
|
273
|
-
${variableName} = _[${objectKey}] = namespace[${objectKey}]
|
|
280
|
+
${variableName} = _[${objectKey}] = namespace[${objectKey}]${moduleExportsFallback}
|
|
274
281
|
} catch (err) {
|
|
275
282
|
if (!(err instanceof ReferenceError)) throw err
|
|
276
283
|
${variableName}Defer = true
|
|
@@ -279,11 +286,11 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
|
|
|
279
286
|
if (${variableName}Defer || ${variableName} === undefined) {
|
|
280
287
|
__pending.push(__makeUpdater(
|
|
281
288
|
${objectKey},
|
|
282
|
-
() => namespace[${objectKey}],
|
|
289
|
+
() => namespace[${objectKey}]${moduleExportsFallback},
|
|
283
290
|
(v) => { ${variableName} = _[${objectKey}] = v }
|
|
284
291
|
))
|
|
285
292
|
}
|
|
286
|
-
export { ${variableName} as ${reExportedName} }
|
|
293
|
+
${(n === 'module.exports' && (srcUrl.startsWith('node:') || builtinModules.includes(srcUrl))) ? '' : `export { ${variableName} as ${reExportedName} }`}
|
|
287
294
|
set[${objectKey}] = (v) => {
|
|
288
295
|
__overridden[${objectKey}] = true
|
|
289
296
|
${variableName} = v
|
|
@@ -308,6 +315,14 @@ export function createHook (meta) {
|
|
|
308
315
|
const iitmURL = new URL('lib/register.js', meta.url).toString()
|
|
309
316
|
let includeModules, excludeModules
|
|
310
317
|
|
|
318
|
+
// Track CJS module URLs that IITM has wrapped. On Node 24+, CJS modules loaded
|
|
319
|
+
// via loadCJSModule (in an ESM import chain) have their require() calls for
|
|
320
|
+
// builtins routed through the ESM resolver. Without this guard, IITM would
|
|
321
|
+
// intercept those require() calls and return an ESM namespace object instead
|
|
322
|
+
// of the native CJS module value (e.g. EventEmitter constructor), breaking
|
|
323
|
+
// patterns like `class App extends require('events') {}`.
|
|
324
|
+
const cjsInIitmChain = new Set()
|
|
325
|
+
|
|
311
326
|
async function initialize (data) {
|
|
312
327
|
if (global.__import_in_the_middle_initialized__) {
|
|
313
328
|
process.emitWarning("The 'import-in-the-middle' hook has already been initialized")
|
|
@@ -405,6 +420,17 @@ export function createHook (meta) {
|
|
|
405
420
|
return result
|
|
406
421
|
}
|
|
407
422
|
|
|
423
|
+
// When a CJS module is loaded by an IITM shim, its require() calls for
|
|
424
|
+
// builtins may be routed through the ESM resolver on Node 24+. Skip IITM
|
|
425
|
+
// wrapping in that case so require() returns the native module value.
|
|
426
|
+
// We also propagate the membership to the resolved child so that its own
|
|
427
|
+
// transitive require() calls are likewise skipped (the entire synchronous
|
|
428
|
+
// CJS require chain must remain unwrapped to avoid ERR_VM_MODULE_LINK_FAILURE).
|
|
429
|
+
if (cjsInIitmChain.has(parentURL)) {
|
|
430
|
+
cjsInIitmChain.add(result.url)
|
|
431
|
+
return result
|
|
432
|
+
}
|
|
433
|
+
|
|
408
434
|
// We don't want to attempt to wrap native modules
|
|
409
435
|
if (result.url.endsWith('.node')) {
|
|
410
436
|
return result
|
|
@@ -448,6 +474,11 @@ export function createHook (meta) {
|
|
|
448
474
|
})
|
|
449
475
|
// If the module loaded successfully, we can remove the specifier to reduce memory usage early.
|
|
450
476
|
specifiers.delete(realUrl)
|
|
477
|
+
// Track CJS modules so their transitive require() calls bypass IITM.
|
|
478
|
+
// context.format is set to 'commonjs' by getCjsExports during processModule.
|
|
479
|
+
if (context.format === 'commonjs') {
|
|
480
|
+
cjsInIitmChain.add(realUrl)
|
|
481
|
+
}
|
|
451
482
|
return {
|
|
452
483
|
source: `
|
|
453
484
|
import { register } from '${iitmURL}'
|
|
@@ -563,6 +594,24 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
563
594
|
return parentLoad(deleteIitm(url), context)
|
|
564
595
|
}
|
|
565
596
|
|
|
597
|
+
// On Node 22+, when a CJS module is loaded through the ESM translator and
|
|
598
|
+
// another loader hook provides its source (instead of leaving source null
|
|
599
|
+
// for Node to read natively), require() calls inside that CJS module for
|
|
600
|
+
// packages using the "module-sync" exports condition fail with
|
|
601
|
+
// ERR_VM_MODULE_LINK_FAILURE. Work around this Node bug by stripping
|
|
602
|
+
// hook-provided source for CJS modules in the synchronous require chain,
|
|
603
|
+
// forcing Node to use its native CJS loader which handles this correctly.
|
|
604
|
+
if (cjsInIitmChain.has(url)) {
|
|
605
|
+
const result = await parentLoad(url, context)
|
|
606
|
+
if (result.format === 'commonjs' && result.source != null) {
|
|
607
|
+
return {
|
|
608
|
+
format: result.format,
|
|
609
|
+
source: undefined
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return result
|
|
613
|
+
}
|
|
614
|
+
|
|
566
615
|
return parentLoad(url, context)
|
|
567
616
|
}
|
|
568
617
|
|
package/index.js
CHANGED
|
@@ -18,6 +18,26 @@ const {
|
|
|
18
18
|
toHook
|
|
19
19
|
} = require('./lib/register')
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Checks turbopack specifiers separately (for Next.js 16+).
|
|
23
|
+
*
|
|
24
|
+
* If turbopack is used, specifiers will have an additional hash appended to the end.
|
|
25
|
+
* Something like "ai" might become "ai-5e7181a616786b24". This only happens in Next.js 16+.
|
|
26
|
+
* Just checking if the baseDir ends with this new specifier won't match, as the baseDir still has the plain package.
|
|
27
|
+
*
|
|
28
|
+
* This logic isolates a new check for checking the actual name in the case turbopack is being used.
|
|
29
|
+
*
|
|
30
|
+
* @param specifier {string}
|
|
31
|
+
* @param baseDir {string}
|
|
32
|
+
*/
|
|
33
|
+
function isTurbopackSpecifier (specifier, baseDir) {
|
|
34
|
+
const usingTurbopack = process.env.TURBOPACK ?? process.argv.includes('--turbo')
|
|
35
|
+
if (!usingTurbopack) return false
|
|
36
|
+
|
|
37
|
+
const specifierWithoutTurbopackHash = specifier.slice(0, specifier.lastIndexOf('-'))
|
|
38
|
+
return baseDir.endsWith(specifierWithoutTurbopackHash)
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
function addHook (hook) {
|
|
22
42
|
importHooks.push(hook)
|
|
23
43
|
toHook.forEach(([name, namespace, specifier]) => hook(name, namespace, specifier))
|
|
@@ -167,7 +187,7 @@ function Hook (modules, options, hookFn) {
|
|
|
167
187
|
if (!baseDir) {
|
|
168
188
|
// built-in module (or unexpected non file:// name?)
|
|
169
189
|
callHookFn(hookFn, namespace, name, baseDir)
|
|
170
|
-
} else if (baseDir.endsWith(specifiers.get(loadUrl))) {
|
|
190
|
+
} else if (baseDir.endsWith(specifiers.get(loadUrl)) || isTurbopackSpecifier(specifiers.get(loadUrl), baseDir)) {
|
|
171
191
|
// An import of the top-level module (e.g. `import 'ioredis'`).
|
|
172
192
|
// Note: Slight behaviour difference from RITM. RITM uses
|
|
173
193
|
// `require.resolve(name)` to see if filename is the module
|
package/lib/get-exports.mjs
CHANGED
|
@@ -150,7 +150,12 @@ function getExportsForNodeBuiltIn (name) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
if (!exports) {
|
|
153
|
-
|
|
153
|
+
// get all properties both enumerable and non-enumerable
|
|
154
|
+
exports = new Set(addDefault(Object.getOwnPropertyNames(require(name))))
|
|
155
|
+
// added in node 23 as alias for default in cjs modules
|
|
156
|
+
if (hasModuleExportsCJSDefault) {
|
|
157
|
+
exports.add('module.exports')
|
|
158
|
+
}
|
|
154
159
|
BUILT_INS.set(name, exports)
|
|
155
160
|
}
|
|
156
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "import-in-the-middle",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Intercept imports in Node.js",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "c8 --reporter lcov --check-coverage --lines 45 imhotap --files test/{hook,low-level,other,get-esm-exports,register}/*",
|
|
11
|
-
"test:e2e": "node test/check-exports/test.mjs",
|
|
11
|
+
"test:e2e": "node test/check-exports/test.mjs && node test/integration-tests/turbopack-dev.mjs && node test/integration-tests/turbopack-build-start.mjs",
|
|
12
12
|
"test:ts": "c8 --reporter lcov imhotap --files test/typescript/*.test.mts",
|
|
13
13
|
"coverage": "c8 --reporter html imhotap --files test/{hook,low-level,other,get-esm-exports,register}/* && echo \"\nNow open coverage/index.html\n\"",
|
|
14
14
|
"lint": "eslint .",
|