import-in-the-middle 2.0.5 → 3.0.0

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,11 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [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
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * drop support for Node.js < v18 ([#230](https://github.com/nodejs/import-in-the-middle/issues/230))
9
+
10
+ ### Miscellaneous Chores
11
+
12
+ * drop support for Node.js &lt; v18 ([#230](https://github.com/nodejs/import-in-the-middle/issues/230)) ([f463b13](https://github.com/nodejs/import-in-the-middle/commit/f463b1342ca946608432c9be39fecb86e7f7cbf0))
13
+
14
+ ## [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)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * 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))
20
+ * 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))
21
+ * 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))
22
+ * 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))
23
+
3
24
  ## [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)
4
25
 
5
26
 
6
27
  ### Bug Fixes
7
28
 
8
29
  * 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))
30
+ * 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))
9
31
 
10
32
  ## [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)
11
33
 
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
@@ -30,8 +29,6 @@ if (NODE_MAJOR > 16 || (NODE_MAJOR === 16 && NODE_MINOR >= 16)) {
30
29
  getExports = (url) => import(url).then(Object.keys)
31
30
  }
32
31
 
33
- let entrypoint
34
-
35
32
  function hasIitm (url) {
36
33
  // Fast path: avoid URL parsing on the hot path when there's clearly no iitm.
37
34
  if (typeof url !== 'string' || url.indexOf('iitm') === -1) {
@@ -61,9 +58,6 @@ function deleteIitm (url) {
61
58
  if (urlObj.searchParams.has('iitm')) {
62
59
  urlObj.searchParams.delete('iitm')
63
60
  resultUrl = urlObj.href
64
- if (resultUrl.startsWith('file:node:')) {
65
- resultUrl = resultUrl.replace('file:', '')
66
- }
67
61
  if (resultUrl.startsWith('file:///node:')) {
68
62
  resultUrl = resultUrl.replace('file:///', '')
69
63
  }
@@ -77,28 +71,6 @@ function deleteIitm (url) {
77
71
  return resultUrl
78
72
  }
79
73
 
80
- function isNodeMajor16AndMinor17OrGreater () {
81
- return NODE_MAJOR === 16 && NODE_MINOR >= 17
82
- }
83
-
84
- function isFileProtocol (urlObj) {
85
- return urlObj.protocol === 'file:'
86
- }
87
-
88
- function isNodeProtocol (urlObj) {
89
- return urlObj.protocol === 'node:'
90
- }
91
-
92
- function needsToAddFileProtocol (urlObj) {
93
- if (NODE_MAJOR === 17) {
94
- return !isFileProtocol(urlObj)
95
- }
96
- if (isNodeMajor16AndMinor17OrGreater()) {
97
- return !isFileProtocol(urlObj) && !isNodeProtocol(urlObj)
98
- }
99
- return !isFileProtocol(urlObj) && NODE_MAJOR < 18
100
- }
101
-
102
74
  /**
103
75
  * Determines if a specifier represents an export all ESM line.
104
76
  * Note that the expected `line` isn't 100% valid ESM. It is derived
@@ -291,7 +263,7 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
291
263
  } else {
292
264
  const variableName = `$${n.replace(/[^a-zA-Z0-9_$]/g, '_')}`
293
265
  const objectKey = JSON.stringify(n)
294
- const reExportedName = n === 'default' || NODE_MAJOR < 16 ? n : objectKey
266
+ const reExportedName = n === 'default' ? n : objectKey
295
267
 
296
268
  addSetter(n, `
297
269
  let ${variableName}
@@ -328,7 +300,7 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
328
300
  function addIitm (url) {
329
301
  const urlObj = new URL(url)
330
302
  urlObj.searchParams.set('iitm', 'true')
331
- return needsToAddFileProtocol(urlObj) ? 'file:' + urlObj.href : urlObj.href
303
+ return urlObj.href
332
304
  }
333
305
 
334
306
  export function createHook (meta) {
@@ -344,10 +316,6 @@ export function createHook (meta) {
344
316
  global.__import_in_the_middle_initialized__ = true
345
317
 
346
318
  if (data) {
347
- if (data.experimentalPatchInternals) {
348
- experimentalPatchInternals = true
349
- }
350
-
351
319
  includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
352
320
  excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
353
321
 
@@ -394,13 +362,9 @@ export function createHook (meta) {
394
362
  // are evaluated, and can make them exit without doing anything.
395
363
  if (parentURL === '') {
396
364
  if (!EXTENSION_RE.test(result.url) && !hasIitm(result.url)) {
397
- entrypoint = result.url
398
365
  return { url: result.url, format: 'commonjs' }
399
366
  }
400
- if (NODE_MAJOR > 16 || (NODE_MAJOR === 16 && NODE_MINOR >= 16)) {
401
- entrypoint = result.url
402
- return result
403
- }
367
+ return result
404
368
  }
405
369
 
406
370
  // For included/excluded modules, we check the specifier to match libraries
@@ -488,7 +452,6 @@ export function createHook (meta) {
488
452
  source: `
489
453
  import { register } from '${iitmURL}'
490
454
  import * as namespace from ${JSON.stringify(realUrl)}
491
- ${experimentalPatchInternals ? `import { setExperimentalPatchInternals } from '${iitmURL}'\nsetExperimentalPatchInternals(true)` : ''}
492
455
 
493
456
  // Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
494
457
  const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } })
@@ -583,7 +546,6 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
583
546
  return parentGetSource(url, context)
584
547
  }
585
548
 
586
- // For Node.js 16.12.0 and higher.
587
549
  async function load (url, context, parentLoad) {
588
550
  if (hasIitm(url)) {
589
551
  const result = await getSource(url, context, parentLoad)
@@ -604,28 +566,5 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
604
566
  return parentLoad(url, context)
605
567
  }
606
568
 
607
- if (NODE_MAJOR >= 17 || (NODE_MAJOR === 16 && NODE_MINOR >= 12)) {
608
- return { initialize, load, resolve }
609
- } else {
610
- return {
611
- initialize,
612
- load,
613
- resolve,
614
- getSource,
615
- getFormat (url, context, parentGetFormat) {
616
- if (hasIitm(url)) {
617
- return {
618
- format: 'module'
619
- }
620
- }
621
- if (url === entrypoint) {
622
- return {
623
- format: 'commonjs'
624
- }
625
- }
626
-
627
- return parentGetFormat(url, context)
628
- }
629
- }
630
- }
569
+ return { initialize, load, resolve }
631
570
  }
package/hook.mjs CHANGED
@@ -4,6 +4,6 @@
4
4
 
5
5
  import { createHook } from './create-hook.mjs'
6
6
 
7
- const { initialize, load, resolve, getFormat, getSource } = createHook(import.meta)
7
+ const { initialize, load, resolve } = createHook(import.meta)
8
8
 
9
- export { initialize, load, resolve, getFormat, getSource }
9
+ export { initialize, load, resolve }
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,16 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "2.0.5",
3
+ "version": "3.0.0",
4
4
  "description": "Intercept imports in Node.js",
5
+ "engines": {
6
+ "node": ">=18"
7
+ },
5
8
  "main": "index.js",
6
9
  "scripts": {
7
- "test": "c8 --reporter lcov --check-coverage --lines 50 imhotap --files test/{hook,low-level,other,get-esm-exports,register}/*",
10
+ "test": "c8 --reporter lcov --check-coverage --lines 45 imhotap --files test/{hook,low-level,other,get-esm-exports,register}/*",
8
11
  "test:e2e": "node test/check-exports/test.mjs",
9
12
  "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'",
13
+ "coverage": "c8 --reporter html imhotap --files test/{hook,low-level,other,get-esm-exports,register}/* && echo \"\nNow open coverage/index.html\n\"",
11
14
  "lint": "eslint .",
12
15
  "lint:fix": "eslint . --fix"
13
16
  },