import-in-the-middle 2.0.1 → 2.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.3](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.2...import-in-the-middle-v2.0.3) (2026-01-13)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add missing JSDoc type information ([40c1009](https://github.com/nodejs/import-in-the-middle/commit/40c1009ef3acc45b5eec89ed1b866866933edace))
9
+ * add missing name for fast builtin lookup ([40c1009](https://github.com/nodejs/import-in-the-middle/commit/40c1009ef3acc45b5eec89ed1b866866933edace))
10
+ * do not crash on missing setters ([#223](https://github.com/nodejs/import-in-the-middle/issues/223)) ([fe44778](https://github.com/nodejs/import-in-the-middle/commit/fe4477832aa9a3422ebecf0a2460cf77be3b3581))
11
+ * handle undefined exports properly ([40c1009](https://github.com/nodejs/import-in-the-middle/commit/40c1009ef3acc45b5eec89ed1b866866933edace))
12
+ * multiple minor issues ([#221](https://github.com/nodejs/import-in-the-middle/issues/221)) ([40c1009](https://github.com/nodejs/import-in-the-middle/commit/40c1009ef3acc45b5eec89ed1b866866933edace))
13
+ * remove small memory leak ([40c1009](https://github.com/nodejs/import-in-the-middle/commit/40c1009ef3acc45b5eec89ed1b866866933edace))
14
+
15
+
16
+ ### Performance Improvements
17
+
18
+ * improve perf by calculating less stack frames and fast paths ([#224](https://github.com/nodejs/import-in-the-middle/issues/224)) ([09ae8bf](https://github.com/nodejs/import-in-the-middle/commit/09ae8bfdeedf6c1c8c81c7338858004447e68233))
19
+
20
+ ## [2.0.2](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.1...import-in-the-middle-v2.0.2) (2026-01-11)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * grammar issue in README.md ([#216](https://github.com/nodejs/import-in-the-middle/issues/216)) ([46e4a2a](https://github.com/nodejs/import-in-the-middle/commit/46e4a2a9ad250c06fb52c9b782370071a6d1f3cc))
26
+ * properly handle internals when specifier matches ([#220](https://github.com/nodejs/import-in-the-middle/issues/220)) ([05e4216](https://github.com/nodejs/import-in-the-middle/commit/05e4216e10d11c7eb996d4124f36e476f3a6d42f))
27
+
3
28
  ## [2.0.1](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.0...import-in-the-middle-v2.0.1) (2025-12-18)
4
29
 
5
30
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # import-in-the-middle
2
2
 
3
- **`import-in-the-middle`** is an module loading interceptor inspired by
3
+ **`import-in-the-middle`** is a module loading interceptor inspired by
4
4
  [`require-in-the-middle`](https://npm.im/require-in-the-middle), but
5
5
  specifically for ESM modules. In fact, it can even modify modules after loading
6
6
  time.
package/create-hook.mjs CHANGED
@@ -21,6 +21,7 @@ const NODE_VERSION = process.versions.node.split('.')
21
21
  const NODE_MAJOR = Number(NODE_VERSION[0])
22
22
  const NODE_MINOR = Number(NODE_VERSION[1])
23
23
  const HANDLED_FORMATS = new Set(['builtin', 'module', 'commonjs'])
24
+ const TRACE_WARNINGS = process.execArgv.includes('--trace-warnings')
24
25
 
25
26
  let getExports
26
27
  if (NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)) {
@@ -32,6 +33,10 @@ if (NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)) {
32
33
  let entrypoint
33
34
 
34
35
  function hasIitm (url) {
36
+ // Fast path: avoid URL parsing on the hot path when there's clearly no iitm.
37
+ if (typeof url !== 'string' || url.indexOf('iitm') === -1) {
38
+ return false
39
+ }
35
40
  try {
36
41
  return new URL(url).searchParams.has('iitm')
37
42
  } catch {
@@ -44,8 +49,14 @@ function isIitm (url, meta) {
44
49
  }
45
50
 
46
51
  function deleteIitm (url) {
52
+ // Fast path: avoid URL parsing / try-catch on bare specifiers and normal file URLs.
53
+ if (typeof url !== 'string' || url.indexOf('iitm') === -1) {
54
+ return url
55
+ }
47
56
  let resultUrl
57
+ const stackTraceLimit = Error.stackTraceLimit
48
58
  try {
59
+ Error.stackTraceLimit = 0
49
60
  const urlObj = new URL(url)
50
61
  if (urlObj.searchParams.has('iitm')) {
51
62
  urlObj.searchParams.delete('iitm')
@@ -62,6 +73,7 @@ function deleteIitm (url) {
62
73
  } catch {
63
74
  resultUrl = url
64
75
  }
76
+ Error.stackTraceLimit = stackTraceLimit
65
77
  return resultUrl
66
78
  }
67
79
 
@@ -115,12 +127,16 @@ function isBareSpecifier (specifier) {
115
127
  return !URL.canParse(specifier)
116
128
  }
117
129
 
130
+ const stackTraceLimit = Error.stackTraceLimit
118
131
  try {
132
+ Error.stackTraceLimit = 0
119
133
  // eslint-disable-next-line no-new
120
134
  new URL(specifier)
121
135
  return false
122
136
  } catch (err) {
123
137
  return true
138
+ } finally {
139
+ Error.stackTraceLimit = stackTraceLimit
124
140
  }
125
141
  }
126
142
 
@@ -141,7 +157,9 @@ function isBareSpecifierFileUrlOrRegex (input) {
141
157
  return false
142
158
  }
143
159
 
160
+ const stackTraceLimit = Error.stackTraceLimit
144
161
  try {
162
+ Error.stackTraceLimit = 0
145
163
  // eslint-disable-next-line no-new
146
164
  const url = new URL(input)
147
165
  // We consider node: URLs bare specifiers in this context
@@ -149,6 +167,8 @@ function isBareSpecifierFileUrlOrRegex (input) {
149
167
  } catch (err) {
150
168
  // Anything that fails parsing is a bare specifier
151
169
  return true
170
+ } finally {
171
+ Error.stackTraceLimit = stackTraceLimit
152
172
  }
153
173
  }
154
174
 
@@ -185,7 +205,7 @@ function emitWarning (err) {
185
205
  // Unfortunately, process.emitWarning does not output the full error
186
206
  // with error.cause like console.warn does so we need to inspect it when
187
207
  // tracing warnings
188
- const warnMessage = process.execArgv.includes('--trace-warnings') ? inspect(err) : err
208
+ const warnMessage = TRACE_WARNINGS ? inspect(err) : err
189
209
  process.emitWarning(warnMessage)
190
210
  }
191
211
 
@@ -196,12 +216,13 @@ function emitWarning (err) {
196
216
  * @param {string} params.srcUrl The full URL to the module to process.
197
217
  * @param {object} params.context Provided by the loaders API.
198
218
  * @param {Function} params.parentGetSource Provides the source code for the parent module.
199
- * @param {bool} params.excludeDefault Exclude the default export.
219
+ * @param {Function} params.parentResolve Provides the resolve function for the parent module.
220
+ * @param {boolean} [params.excludeDefault = false] Exclude the default export.
200
221
  *
201
222
  * @returns {Promise<Map<string, string>>} The shimmed setters for all the exports
202
223
  * from the module and any transitive export all modules.
203
224
  */
204
- async function processModule ({ srcUrl, context, parentGetSource, parentResolve, excludeDefault }) {
225
+ async function processModule ({ srcUrl, context, parentGetSource, parentResolve, excludeDefault = false }) {
205
226
  const exportNames = await getExports(srcUrl, context, parentGetSource)
206
227
  const starExports = new Set()
207
228
  const setters = new Map()
@@ -365,12 +386,21 @@ export function createHook (meta) {
365
386
  //
366
387
  // For non-bare specifier imports, we match to the full file URL because
367
388
  // using relative paths would be very error prone!
389
+ let resultPath
390
+ if (result.url.startsWith('file:')) {
391
+ const stackTraceLimit = Error.stackTraceLimit
392
+ Error.stackTraceLimit = 0
393
+ try {
394
+ resultPath = fileURLToPath(result.url)
395
+ } catch {}
396
+ Error.stackTraceLimit = stackTraceLimit
397
+ }
368
398
  function match (each) {
369
399
  if (each instanceof RegExp) {
370
400
  return each.test(result.url)
371
401
  }
372
402
 
373
- return each === specifier || each === result.url || (result.url.startsWith('file:') && each === fileURLToPath(result.url))
403
+ return each === specifier || each === result.url || (resultPath && each === resultPath)
374
404
  }
375
405
 
376
406
  if (result.format && !HANDLED_FORMATS.has(result.format)) {
@@ -385,7 +415,7 @@ export function createHook (meta) {
385
415
  return result
386
416
  }
387
417
 
388
- if (isIitm(parentURL, meta) || hasIitm(parentURL)) {
418
+ if (isIitm(parentURL, meta) || (parentURL && hasIitm(parentURL))) {
389
419
  return result
390
420
  }
391
421
 
@@ -421,6 +451,7 @@ export function createHook (meta) {
421
451
  async function getSource (url, context, parentGetSource) {
422
452
  if (hasIitm(url)) {
423
453
  const realUrl = deleteIitm(url)
454
+ const originalSpecifier = specifiers.get(realUrl)
424
455
 
425
456
  try {
426
457
  const setters = await processModule({
@@ -429,6 +460,8 @@ export function createHook (meta) {
429
460
  parentGetSource,
430
461
  parentResolve: cachedResolve
431
462
  })
463
+ // If the module loaded successfully, we can remove the specifier to reduce memory usage early.
464
+ specifiers.delete(realUrl)
432
465
  return {
433
466
  source: `
434
467
  import { register } from '${iitmURL}'
@@ -442,10 +475,13 @@ const get = {}
442
475
 
443
476
  ${Array.from(setters.values()).join('\n')}
444
477
 
445
- register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(specifiers.get(realUrl))})
478
+ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpecifier)})
446
479
  `
447
480
  }
448
481
  } catch (cause) {
482
+ // If the module failed loading, the specifier will not be used again, so
483
+ // we can remove it to prevent a memory leak.
484
+ specifiers.delete(realUrl)
449
485
  // If there are other ESM loader hooks registered as well as iitm,
450
486
  // depending on the order they are registered, source might not be
451
487
  // JavaScript.
package/index.js CHANGED
@@ -29,7 +29,12 @@ function removeHook (hook) {
29
29
  function callHookFn (hookFn, namespace, name, baseDir) {
30
30
  const newDefault = hookFn(namespace, name, baseDir)
31
31
  if (newDefault && newDefault !== namespace) {
32
- namespace.default = newDefault
32
+ // Only ESM modules that actually export `default` can have it reassigned.
33
+ // Some hooks return a value unconditionally; avoid crashing when the module
34
+ // has no default export (see issue #188).
35
+ if ('default' in namespace) {
36
+ namespace.default = newDefault
37
+ }
33
38
  }
34
39
  }
35
40
 
@@ -127,9 +132,12 @@ function Hook (modules, options, hookFn) {
127
132
  name = name.replace(/^node:/, '')
128
133
  } else {
129
134
  if (name.startsWith('file://')) {
135
+ const stackTraceLimit = Error.stackTraceLimit
136
+ Error.stackTraceLimit = 0
130
137
  try {
131
138
  name = fileURLToPath(name)
132
139
  } catch (e) {}
140
+ Error.stackTraceLimit = stackTraceLimit
133
141
  }
134
142
  const details = parse(name)
135
143
  if (details) {
@@ -140,14 +148,16 @@ function Hook (modules, options, hookFn) {
140
148
 
141
149
  if (modules) {
142
150
  for (const moduleName of modules) {
143
- if (moduleName === specifier) {
144
- callHookFn(hookFn, namespace, name, baseDir)
145
- } else if (moduleName === name) {
151
+ const nameMatch = moduleName === name
152
+ const specMatch = moduleName === specifier
153
+ if (nameMatch || specMatch) {
146
154
  if (baseDir) {
147
155
  if (internals) {
148
156
  name = name + path.sep + path.relative(baseDir, fileURLToPath(filename))
149
157
  } else {
150
- if (!getExperimentalPatchInternals() && !baseDir.endsWith(specifiers.get(filename))) continue
158
+ if (!getExperimentalPatchInternals() && !specMatch && !baseDir.endsWith(specifiers.get(filename))) {
159
+ continue
160
+ }
151
161
  }
152
162
  }
153
163
  callHookFn(hookFn, namespace, name, baseDir)
@@ -16,13 +16,134 @@ function addDefault (arr) {
16
16
  return new Set(['default', ...arr])
17
17
  }
18
18
 
19
+ function hasEsmSyntax (source) {
20
+ // Lightweight scan (no full parse) to determine if the *source code*
21
+ // contains ESM-specific syntax. This is used only when:
22
+ // - the loader chain didn't tell us a `format`, and
23
+ // - `getEsmExports()` found no exports.
24
+ //
25
+ // Notes:
26
+ // - We ignore comments and strings to reduce false positives.
27
+ // - We treat `import.meta` and static `import ...` as ESM.
28
+ // - We do NOT treat `import(` (dynamic import) as ESM because it is allowed
29
+ // in CJS as an expression.
30
+ if (source.indexOf('import') === -1) return false
31
+
32
+ const isIdentCharCode = (code) => (
33
+ (code >= 48 && code <= 57) || // 0-9
34
+ (code >= 65 && code <= 90) || // A-Z
35
+ (code >= 97 && code <= 122) || // a-z
36
+ code === 95 || // _
37
+ code === 36 // $
38
+ )
39
+
40
+ const skipWhitespace = (idx) => {
41
+ while (idx < source.length) {
42
+ const c = source.charCodeAt(idx)
43
+ // space, tab, cr, lf
44
+ if (c !== 32 && c !== 9 && c !== 13 && c !== 10) break
45
+ idx++
46
+ }
47
+ return idx
48
+ }
49
+
50
+ let i = 0
51
+ while (i < source.length) {
52
+ const ch = source[i]
53
+
54
+ // Line comment
55
+ if (ch === '/' && source[i + 1] === '/') {
56
+ i += 2
57
+ while (i < source.length && source[i] !== '\n') i++
58
+ continue
59
+ }
60
+
61
+ // Block comment
62
+ if (ch === '/' && source[i + 1] === '*') {
63
+ i += 2
64
+ while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++
65
+ i += 2
66
+ continue
67
+ }
68
+
69
+ // Strings: '...' or "..."
70
+ if (ch === '\'' || ch === '"') {
71
+ const quote = ch
72
+ i++
73
+ while (i < source.length) {
74
+ const c = source[i]
75
+ if (c === '\\') {
76
+ i += 2
77
+ continue
78
+ }
79
+ if (c === quote) {
80
+ i++
81
+ break
82
+ }
83
+ i++
84
+ }
85
+ continue
86
+ }
87
+
88
+ // Template strings: `...`
89
+ if (ch === '`') {
90
+ i++
91
+ while (i < source.length) {
92
+ const c = source[i]
93
+ if (c === '\\') {
94
+ i += 2
95
+ continue
96
+ }
97
+ if (c === '`') {
98
+ i++
99
+ break
100
+ }
101
+ i++
102
+ }
103
+ continue
104
+ }
105
+
106
+ // Keyword scan (word-boundary): import
107
+ if (ch === 'i') {
108
+ const prev = source.charCodeAt(i - 1)
109
+ if (i > 0 && isIdentCharCode(prev)) {
110
+ i++
111
+ continue
112
+ }
113
+
114
+ if (source.startsWith('import', i)) {
115
+ const next = source.charCodeAt(i + 6)
116
+ if (isIdentCharCode(next)) {
117
+ i++
118
+ continue
119
+ }
120
+
121
+ const j = skipWhitespace(i + 6)
122
+ // `import.meta` is ESM-only
123
+ if (source[j] === '.') return true
124
+ // `import(` is dynamic import, allowed in CJS
125
+ if (source[j] === '(') {
126
+ i = j + 1
127
+ continue
128
+ }
129
+ // Otherwise assume it's a static import form
130
+ return true
131
+ }
132
+ }
133
+
134
+ i++
135
+ }
136
+
137
+ return false
138
+ }
139
+
19
140
  // Cached exports for Node built-in modules
20
141
  const BUILT_INS = new Map()
21
142
 
22
143
  let require
23
144
 
24
145
  function getExportsForNodeBuiltIn (name) {
25
- let exports = BUILT_INS.get()
146
+ let exports = BUILT_INS.get(name)
26
147
 
27
148
  if (!require) {
28
149
  require = createRequire(import.meta.url)
@@ -96,7 +217,7 @@ function resolvePackageImports (specifier, fromUrl) {
96
217
 
97
218
  async function getCjsExports (url, context, parentLoad, source) {
98
219
  if (urlsBeingProcessed.has(url)) {
99
- return []
220
+ return new Set()
100
221
  }
101
222
  urlsBeingProcessed.add(url)
102
223
 
@@ -201,13 +322,17 @@ export async function getExports (url, context, parentLoad) {
201
322
  // At this point our `format` is either undefined or not known by us. Fall
202
323
  // back to parsing as ESM/CJS.
203
324
  const esmExports = getEsmExports(source)
204
- if (!esmExports.length) {
205
- // TODO(bengl) it's might be possible to get here if somehow the format
206
- // isn't set at first and yet we have an ESM module with no exports.
207
- // I couldn't construct an example that would do this, so maybe it's
208
- // impossible?
209
- return await getCjsExports(url, context, parentLoad, source)
325
+ if (!esmExports.size) {
326
+ // If there's strong evidence this is ESM (static import/import.meta),
327
+ // prefer returning the empty ESM export set over incorrectly treating it
328
+ // as CJS.
329
+ if (!hasEsmSyntax(source)) {
330
+ // It might be possible to get here if the format
331
+ // isn't set at first and yet we have an ESM module with no exports.
332
+ return await getCjsExports(url, context, parentLoad, source)
333
+ }
210
334
  }
335
+ return esmExports
211
336
  } catch (cause) {
212
337
  const err = new Error(`Failed to parse '${url}'`)
213
338
  err.cause = cause
package/lib/register.js CHANGED
@@ -10,7 +10,14 @@ const toHook = []
10
10
 
11
11
  const proxyHandler = {
12
12
  set (target, name, value) {
13
- return setters.get(target)[name](value)
13
+ const set = setters.get(target)
14
+ const setter = set && set[name]
15
+ if (typeof setter === 'function') {
16
+ return setter(value)
17
+ }
18
+ // If a module doesn't export the property being assigned (e.g. no default
19
+ // export), there is no setter to call. Don't crash userland code.
20
+ return true
14
21
  },
15
22
 
16
23
  get (target, name) {
@@ -30,7 +37,12 @@ const proxyHandler = {
30
37
  throw new Error('Getters/setters are not supported for exports property descriptors.')
31
38
  }
32
39
 
33
- return setters.get(target)[property](descriptor.value)
40
+ const set = setters.get(target)
41
+ const setter = set && set[property]
42
+ if (typeof setter === 'function') {
43
+ return setter(descriptor.value)
44
+ }
45
+ return true
34
46
  }
35
47
  }
36
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,31 +34,31 @@
34
34
  "test-env": "NODE_OPTIONS=--no-warnings --require ./test/version-check.js --experimental-loader ./test/generic-loader.mjs"
35
35
  },
36
36
  "devDependencies": {
37
- "@babel/core": "^7.23.7",
38
- "@babel/eslint-parser": "^7.23.3",
39
- "@babel/plugin-syntax-import-assertions": "^7.23.3",
40
- "@node-rs/crc32": "^1.10.3",
37
+ "@babel/core": "^7.28.5",
38
+ "@babel/eslint-parser": "^7.28.5",
39
+ "@babel/plugin-syntax-import-assertions": "^7.27.1",
40
+ "@node-rs/crc32": "^1.10.6",
41
41
  "@react-email/components": "^0.0.19",
42
42
  "@types/node": "^18.0.6",
43
- "c8": "^7.8.0",
43
+ "c8": "^7.14.0",
44
44
  "date-fns": "^3.6.0",
45
- "eslint": "^8.55.0",
45
+ "eslint": "^8.57.1",
46
46
  "eslint-config-standard": "^17.1.0",
47
- "eslint-plugin-import": "^2.29.0",
48
- "eslint-plugin-n": "^16.4.0",
47
+ "eslint-plugin-import": "^2.32.0",
48
+ "eslint-plugin-n": "^16.6.2",
49
49
  "eslint-plugin-node": "^11.1.0",
50
- "eslint-plugin-promise": "^6.1.1",
51
- "got": "^14.3.0",
52
- "imhotap": "^2.1.0",
50
+ "eslint-plugin-promise": "^6.6.0",
51
+ "got": "^14.6.6",
52
+ "imhotap": "^2.2.0",
53
53
  "openai": "4.47.2",
54
- "ts-node": "^10.9.1",
55
- "typescript": "^4.7.4",
56
- "vue": "^3.4.31"
54
+ "ts-node": "^10.9.2",
55
+ "typescript": "^4.9.5",
56
+ "vue": "^3.5.26"
57
57
  },
58
58
  "dependencies": {
59
- "acorn": "^8.14.0",
59
+ "acorn": "^8.15.0",
60
60
  "acorn-import-attributes": "^1.9.5",
61
- "cjs-module-lexer": "^1.2.2",
62
- "module-details-from-path": "^1.0.3"
61
+ "cjs-module-lexer": "^2.2.0",
62
+ "module-details-from-path": "^1.0.4"
63
63
  }
64
64
  }