import-in-the-middle 2.0.4 → 2.0.6

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.6](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.5...import-in-the-middle-v2.0.6) (2026-01-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * ensure the callback 'name' arg is the module name when matching the module main file, even when 'internals: true' option is used ([#241](https://github.com/nodejs/import-in-the-middle/issues/241)) ([ad9d02c](https://github.com/nodejs/import-in-the-middle/commit/ad9d02cd774df110c5e2f72e6cca414d7c315404))
9
+ * fix a couple issues with duplicate entries and specifier (submodule) matching ([#237](https://github.com/nodejs/import-in-the-middle/issues/237)) ([fdc0b3d](https://github.com/nodejs/import-in-the-middle/commit/fdc0b3d5863a1338586e25a94b831fee1bd8bd0b))
10
+ * properly hook builtin modules that require the 'node:' prefix ([#240](https://github.com/nodejs/import-in-the-middle/issues/240)) ([de84589](https://github.com/nodejs/import-in-the-middle/commit/de8458962958182eac743e99edeb944160638e2c))
11
+ * properly hook builtin modules that require the 'node:' prefix ([#240](https://github.com/nodejs/import-in-the-middle/issues/240)) ([9d916a5](https://github.com/nodejs/import-in-the-middle/commit/9d916a59b4a95b2a22568d1e8f65948598178de9))
12
+
13
+ ## [2.0.5](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.4...import-in-the-middle-v2.0.5) (2026-01-20)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * handle lazy initialization and circular dependencies ([#229](https://github.com/nodejs/import-in-the-middle/issues/229)) ([d1421dc](https://github.com/nodejs/import-in-the-middle/commit/d1421dc0ae65ce6da5de5cb58f41af99f9d87371))
19
+ * entrypoint can be treated as CommonJS when loader chains add query params to file URLs ([#223](https://github.com/nodejs/import-in-the-middle/issues/223)) ([60ab14a](https://github.com/nodejs/import-in-the-middle/commit/60ab14aeed8960b5dec4bec571a81649363e256e))
20
+
3
21
  ## [2.0.4](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v2.0.3...import-in-the-middle-v2.0.4) (2026-01-14)
4
22
 
5
23
 
package/README.md CHANGED
@@ -44,11 +44,13 @@ API. However, for this to be able to hook non-dynamic imports, it needs to be
44
44
  registered before your app code is evaluated via the `--import` command-line option.
45
45
 
46
46
  `my-loader.mjs`
47
+
47
48
  ```js
48
49
  import * as module from 'module'
49
50
 
50
51
  module.register('import-in-the-middle/hook.mjs', import.meta.url)
51
52
  ```
53
+
52
54
  ```shell
53
55
  node --import=./my-loader.mjs ./my-code.mjs
54
56
  ```
@@ -56,7 +58,7 @@ node --import=./my-loader.mjs ./my-code.mjs
56
58
  When registering the loader hook programmatically, it's possible to pass a list
57
59
  of modules, file URLs or regular expressions to either `exclude` or specifically
58
60
  `include` which modules are intercepted. This is useful if a module is not
59
- compatible with the loader hook.
61
+ compatible with the loader hook.
60
62
 
61
63
  > **Note:** This feature is incompatible with the `{internals: true}` Hook option
62
64
 
@@ -74,7 +76,8 @@ module.register('import-in-the-middle/hook.mjs', import.meta.url, {
74
76
  })
75
77
  ```
76
78
 
77
- ### Only Intercepting Hooked modules
79
+ ### Only Intercepting Hooked modules
80
+
78
81
  > **Note:** This feature is experimental and is incompatible with the `{internals: true}` Hook option
79
82
 
80
83
  If you are `Hook`'ing all modules before they are imported, for example in a
@@ -82,6 +85,7 @@ module loaded via the Node.js `--import` CLI argument, you can configure the
82
85
  loader to intercept only modules that were specifically hooked.
83
86
 
84
87
  `instrument.mjs`
88
+
85
89
  ```js
86
90
  import { register } from 'module'
87
91
  import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'
@@ -94,11 +98,13 @@ Hook(['fs'], (exported, name, baseDir) => {
94
98
  // Instrument the fs module
95
99
  })
96
100
 
97
- // Ensure that the loader has acknowledged all the modules
101
+ // Ensure that the loader has acknowledged all the modules
98
102
  // before we allow execution to continue
99
103
  await waitForAllMessagesAcknowledged()
100
104
  ```
105
+
101
106
  `my-app.mjs`
107
+
102
108
  ```js
103
109
  import * as fs from 'fs'
104
110
  // fs will be instrumented!
@@ -109,23 +115,6 @@ fs.readFileSync('file.txt')
109
115
  node --import=./instrument.mjs ./my-app.mjs
110
116
  ```
111
117
 
112
- ### Experimental `experimentalPatchInternals` option
113
-
114
- It was found that `import-in-the-middle` didn't match the hooking behavior of `require-in-the-middle` in some cases:
115
- https://github.com/nodejs/import-in-the-middle/issues/185
116
-
117
- The `experimentalPatchInternals` option forces the loader to match the behavior of `require-in-the-middle` in these cases.
118
-
119
- This option is experimental and may be removed or made the default in the future.
120
-
121
- ```js
122
- import { register } from 'module'
123
-
124
- register('import-in-the-middle/hook.mjs', import.meta.url, {
125
- data: { experimentalPatchInternals: true }
126
- })
127
- ```
128
-
129
118
  ## Limitations
130
119
 
131
120
  * You cannot add new exports to a module. You can only modify existing ones.
package/create-hook.mjs CHANGED
@@ -12,7 +12,6 @@ import {
12
12
 
13
13
  const specifiers = new Map()
14
14
  const isWin = process.platform === 'win32'
15
- let experimentalPatchInternals = false
16
15
 
17
16
  // FIXME: Typescript extensions are added temporarily until we find a better
18
17
  // way of supporting arbitrary extensions
@@ -24,7 +23,7 @@ const HANDLED_FORMATS = new Set(['builtin', 'module', 'commonjs'])
24
23
  const TRACE_WARNINGS = process.execArgv.includes('--trace-warnings')
25
24
 
26
25
  let getExports
27
- if (NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)) {
26
+ if (NODE_MAJOR > 16 || (NODE_MAJOR === 16 && NODE_MINOR >= 16)) {
28
27
  getExports = getExportsImpl
29
28
  } else {
30
29
  getExports = (url) => import(url).then(Object.keys)
@@ -61,9 +60,6 @@ function deleteIitm (url) {
61
60
  if (urlObj.searchParams.has('iitm')) {
62
61
  urlObj.searchParams.delete('iitm')
63
62
  resultUrl = urlObj.href
64
- if (resultUrl.startsWith('file:node:')) {
65
- resultUrl = resultUrl.replace('file:', '')
66
- }
67
63
  if (resultUrl.startsWith('file:///node:')) {
68
64
  resultUrl = resultUrl.replace('file:///', '')
69
65
  }
@@ -295,13 +291,25 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
295
291
 
296
292
  addSetter(n, `
297
293
  let ${variableName}
294
+ __overridden[${objectKey}] = false
295
+ let ${variableName}Defer = false
298
296
  try {
299
297
  ${variableName} = _[${objectKey}] = namespace[${objectKey}]
300
298
  } catch (err) {
301
299
  if (!(err instanceof ReferenceError)) throw err
300
+ ${variableName}Defer = true
301
+ }
302
+
303
+ if (${variableName}Defer || ${variableName} === undefined) {
304
+ __pending.push(__makeUpdater(
305
+ ${objectKey},
306
+ () => namespace[${objectKey}],
307
+ (v) => { ${variableName} = _[${objectKey}] = v }
308
+ ))
302
309
  }
303
310
  export { ${variableName} as ${reExportedName} }
304
311
  set[${objectKey}] = (v) => {
312
+ __overridden[${objectKey}] = true
305
313
  ${variableName} = v
306
314
  return true
307
315
  }
@@ -332,10 +340,6 @@ export function createHook (meta) {
332
340
  global.__import_in_the_middle_initialized__ = true
333
341
 
334
342
  if (data) {
335
- if (data.experimentalPatchInternals) {
336
- experimentalPatchInternals = true
337
- }
338
-
339
343
  includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
340
344
  excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
341
345
 
@@ -381,7 +385,7 @@ export function createHook (meta) {
381
385
  // "main" module (e.g. require.main === module). Wrapping changes how they
382
386
  // are evaluated, and can make them exit without doing anything.
383
387
  if (parentURL === '') {
384
- if (!EXTENSION_RE.test(result.url)) {
388
+ if (!EXTENSION_RE.test(result.url) && !hasIitm(result.url)) {
385
389
  entrypoint = result.url
386
390
  return { url: result.url, format: 'commonjs' }
387
391
  }
@@ -476,15 +480,70 @@ export function createHook (meta) {
476
480
  source: `
477
481
  import { register } from '${iitmURL}'
478
482
  import * as namespace from ${JSON.stringify(realUrl)}
479
- ${experimentalPatchInternals ? `import { setExperimentalPatchInternals } from '${iitmURL}'\nsetExperimentalPatchInternals(true)` : ''}
480
483
 
481
484
  // Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
482
485
  const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } })
483
486
  const set = {}
484
487
  const get = {}
488
+ const __overridden = Object.create(null)
489
+ let __pending = []
490
+
491
+ function __makeUpdater (key, read, assign) {
492
+ return () => {
493
+ if (__overridden[key] === true) return true
494
+ try {
495
+ const v = read()
496
+ if (v !== undefined) {
497
+ assign(v)
498
+ return true
499
+ }
500
+ return false
501
+ } catch (err) {
502
+ if (err instanceof ReferenceError) return false
503
+ throw err
504
+ }
505
+ }
506
+ }
507
+
508
+ function __flushPendingOnce () {
509
+ if (__pending.length === 0) return
510
+ const next = []
511
+ for (const fn of __pending) {
512
+ // If it still throws ReferenceError, keep it for the (single) next attempt.
513
+ if (fn() !== true) next.push(fn)
514
+ }
515
+ __pending = next
516
+ }
485
517
 
486
518
  ${Array.from(setters.values()).join('\n')}
487
519
 
520
+ if (__pending.length > 0) {
521
+ queueMicrotask(() => {
522
+ __flushPendingOnce()
523
+
524
+ if (__pending.length > 0) {
525
+ const __retryDelays = [0, 10, 50]
526
+ const __schedulePending = (i) => {
527
+ if (__pending.length === 0) return
528
+ if (i >= __retryDelays.length) {
529
+ // Give up: leave exports as-is to avoid unbounded retries.
530
+ __pending = []
531
+ return
532
+ }
533
+
534
+ const t = setTimeout(() => {
535
+ __flushPendingOnce()
536
+ __schedulePending(i + 1)
537
+ }, __retryDelays[i])
538
+ // Don't keep the process alive just for best-effort retries.
539
+ if (t && typeof t.unref === 'function') t.unref()
540
+ }
541
+
542
+ __schedulePending(0)
543
+ }
544
+ })
545
+ }
546
+
488
547
  register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpecifier)})
489
548
  `
490
549
  }
@@ -518,12 +577,19 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
518
577
  // For Node.js 16.12.0 and higher.
519
578
  async function load (url, context, parentLoad) {
520
579
  if (hasIitm(url)) {
521
- const { source } = await getSource(url, context, parentLoad)
522
- return {
523
- source,
524
- shortCircuit: true,
525
- format: 'module'
580
+ const result = await getSource(url, context, parentLoad)
581
+ // If wrapping failed, `getSource()` may have fallen back to `parentLoad`,
582
+ // which can legally return `source: null` (e.g. for non-JS formats).
583
+ if (result && typeof result === 'object' && result.source != null) {
584
+ return {
585
+ source: result.source,
586
+ shortCircuit: true,
587
+ format: 'module'
588
+ }
526
589
  }
590
+
591
+ // Fall back to the parent loader with the original (non-iitm) URL.
592
+ return parentLoad(deleteIitm(url), context)
527
593
  }
528
594
 
529
595
  return parentLoad(url, context)
package/index.js CHANGED
@@ -3,15 +3,19 @@
3
3
  // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
4
 
5
5
  const path = require('path')
6
- const parse = require('module-details-from-path')
6
+ const moduleDetailsFromPath = require('module-details-from-path')
7
7
  const { fileURLToPath } = require('url')
8
8
  const { MessageChannel } = require('worker_threads')
9
9
 
10
+ let { isBuiltin } = require('module')
11
+ if (!isBuiltin) {
12
+ isBuiltin = () => true
13
+ }
14
+
10
15
  const {
11
16
  importHooks,
12
17
  specifiers,
13
- toHook,
14
- getExperimentalPatchInternals
18
+ toHook
15
19
  } = require('./lib/register')
16
20
 
17
21
  function addHook (hook) {
@@ -124,43 +128,58 @@ function Hook (modules, options, hookFn) {
124
128
  }
125
129
 
126
130
  this._iitmHook = (name, namespace, specifier) => {
127
- const filename = name
128
- const isBuiltin = name.startsWith('node:')
129
- let baseDir
130
-
131
- if (isBuiltin) {
132
- name = name.replace(/^node:/, '')
133
- } else {
134
- if (name.startsWith('file://')) {
135
- const stackTraceLimit = Error.stackTraceLimit
136
- Error.stackTraceLimit = 0
137
- try {
138
- name = fileURLToPath(name)
139
- } catch (e) {}
140
- Error.stackTraceLimit = stackTraceLimit
131
+ const loadUrl = name
132
+ const isNodeUrl = loadUrl.startsWith('node:')
133
+ let filePath, baseDir
134
+
135
+ if (isNodeUrl) {
136
+ // Normalize builtin module name to *not* have 'node:' prefix, unless
137
+ // required, as it is for 'node:test' and some others. `module.isBuiltin`
138
+ // is available in all Node.js versions that have node:-only modules.
139
+ const unprefixed = name.slice(5)
140
+ if (isBuiltin(unprefixed)) {
141
+ name = unprefixed
141
142
  }
142
- const details = parse(name)
143
- if (details) {
144
- name = details.name
145
- baseDir = details.basedir
143
+ } else if (loadUrl.startsWith('file://')) {
144
+ const stackTraceLimit = Error.stackTraceLimit
145
+ Error.stackTraceLimit = 0
146
+ try {
147
+ filePath = fileURLToPath(name)
148
+ name = filePath
149
+ } catch (e) {}
150
+ Error.stackTraceLimit = stackTraceLimit
151
+
152
+ if (filePath) {
153
+ const details = moduleDetailsFromPath(filePath)
154
+ if (details) {
155
+ name = details.name
156
+ baseDir = details.basedir
157
+ }
146
158
  }
147
159
  }
148
160
 
149
161
  if (modules) {
150
- for (const moduleName of modules) {
151
- const nameMatch = moduleName === name
152
- const specMatch = moduleName === specifier
153
- if (nameMatch || specMatch) {
154
- if (baseDir) {
155
- if (internals) {
156
- name = name + path.sep + path.relative(baseDir, fileURLToPath(filename))
157
- } else {
158
- if (!getExperimentalPatchInternals() && !specMatch && !baseDir.endsWith(specifiers.get(filename))) {
159
- continue
160
- }
161
- }
162
+ for (const matchArg of modules) {
163
+ if (filePath && matchArg === filePath) {
164
+ // abspath match
165
+ callHookFn(hookFn, namespace, filePath, undefined)
166
+ } else if (matchArg === name) {
167
+ if (!baseDir) {
168
+ // built-in module (or unexpected non file:// name?)
169
+ callHookFn(hookFn, namespace, name, baseDir)
170
+ } else if (baseDir.endsWith(specifiers.get(loadUrl))) {
171
+ // An import of the top-level module (e.g. `import 'ioredis'`).
172
+ // Note: Slight behaviour difference from RITM. RITM uses
173
+ // `require.resolve(name)` to see if filename is the module
174
+ // main file, which will catch `require('ioredis/built/index.js')`.
175
+ // The check here will not catch `import 'ioredis/built/index.js'`.
176
+ callHookFn(hookFn, namespace, name, baseDir)
177
+ } else if (internals) {
178
+ const internalPath = name + path.sep + path.relative(baseDir, filePath)
179
+ callHookFn(hookFn, namespace, internalPath, baseDir)
162
180
  }
163
- callHookFn(hookFn, namespace, name, baseDir)
181
+ } else if (matchArg === specifier) {
182
+ callHookFn(hookFn, namespace, specifier, baseDir)
164
183
  }
165
184
  }
166
185
  } else {
@@ -299,6 +299,21 @@ export async function getExports (url, context, parentLoad) {
299
299
  let source = parentCtx.source
300
300
  const format = parentCtx.format
301
301
 
302
+ // Loader hooks can return ArrayBuffer / TypedArray sources. Normalize to a
303
+ // string for parsing.
304
+ if (source && typeof source !== 'string') {
305
+ // Avoid copies where possible:
306
+ // - Buffer.from(Uint8Array) copies
307
+ // - Buffer.from(ArrayBuffer, offset, length) wraps the existing memory
308
+ if (Buffer.isBuffer(source)) {
309
+ source = source.toString('utf8')
310
+ } else if (ArrayBuffer.isView(source)) {
311
+ source = Buffer.from(source.buffer, source.byteOffset, source.byteLength).toString('utf8')
312
+ } else {
313
+ source = Buffer.from(source).toString('utf8')
314
+ }
315
+ }
316
+
302
317
  if (!source) {
303
318
  if (format === 'builtin') {
304
319
  // Builtins don't give us the source property, so we're stuck
package/lib/register.js CHANGED
@@ -55,19 +55,7 @@ function register (name, namespace, set, get, specifier) {
55
55
  toHook.push([name, proxy, specifier])
56
56
  }
57
57
 
58
- let experimentalPatchInternals = false
59
-
60
- function getExperimentalPatchInternals () {
61
- return experimentalPatchInternals
62
- }
63
-
64
- function setExperimentalPatchInternals (value) {
65
- experimentalPatchInternals = value
66
- }
67
-
68
58
  exports.register = register
69
59
  exports.importHooks = importHooks
70
60
  exports.specifiers = specifiers
71
61
  exports.toHook = toHook
72
- exports.getExperimentalPatchInternals = getExperimentalPatchInternals
73
- exports.setExperimentalPatchInternals = setExperimentalPatchInternals
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "c8 --reporter lcov --check-coverage --lines 50 imhotap --files test/{hook,low-level,other,get-esm-exports,register}/*",
7
+ "test": "c8 --reporter lcov --check-coverage --lines 45 imhotap --files test/{hook,low-level,other,get-esm-exports,register}/*",
8
8
  "test:e2e": "node test/check-exports/test.mjs",
9
9
  "test:ts": "c8 --reporter lcov imhotap --files test/typescript/*.test.mts",
10
- "coverage": "c8 --reporter html imhotap --files test/{hook,low-level,other,get-esm-exports}/* && echo '\nNow open coverage/index.html\n'",
10
+ "coverage": "c8 --reporter html imhotap --files test/{hook,low-level,other,get-esm-exports,register}/* && echo \"\nNow open coverage/index.html\n\"",
11
11
  "lint": "eslint .",
12
12
  "lint:fix": "eslint . --fix"
13
13
  },