import-in-the-middle 3.1.0 → 3.2.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 +7 -0
- package/README.md +30 -0
- package/create-hook.mjs +54 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.2.0](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.1.0...import-in-the-middle-v3.2.0) (2026-06-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add shouldInclude predicate for consumer-owned module matching ([#255](https://github.com/nodejs/import-in-the-middle/issues/255)) ([b2590d0](https://github.com/nodejs/import-in-the-middle/commit/b2590d054a5cfc6d6820c06edb4ff4987a10b5c4))
|
|
9
|
+
|
|
3
10
|
## [3.1.0](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.0.2...import-in-the-middle-v3.1.0) (2026-06-17)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -145,6 +145,36 @@ node --import=./instrument.mjs ./my-app.mjs
|
|
|
145
145
|
`register()` accepts the same `include` / `exclude` options as the asynchronous
|
|
146
146
|
loader and throws on a Node.js version without `module.registerHooks()`.
|
|
147
147
|
|
|
148
|
+
### Custom matching with `shouldInclude`
|
|
149
|
+
|
|
150
|
+
Instead of `include` / `exclude` lists, you can pass a `shouldInclude(url, specifier)`
|
|
151
|
+
predicate to decide which modules are intercepted. It is called for every resolved
|
|
152
|
+
module with the resolved URL and the import specifier; return a truthy value to
|
|
153
|
+
intercept the module. When a predicate is provided it takes over the decision and
|
|
154
|
+
the `include` / `exclude` options are ignored.
|
|
155
|
+
|
|
156
|
+
This is useful when matching doesn't map cleanly onto bare specifiers, file URLs and
|
|
157
|
+
regular expressions — for example a matcher built from your own configuration, or a
|
|
158
|
+
decision that depends on more than the specifier.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import { register } from 'import-in-the-middle/register-hooks.mjs'
|
|
162
|
+
|
|
163
|
+
register({
|
|
164
|
+
shouldInclude (url, specifier) {
|
|
165
|
+
return specifier === 'package-i-want-to-include' ||
|
|
166
|
+
url.includes('/node_modules/some-scope/')
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The predicate receives only the URL and the specifier, never a resolved file path.
|
|
172
|
+
Because `module.register()` transfers its `data` to the loader thread by structured
|
|
173
|
+
clone — which cannot carry a function — `shouldInclude` is supported for synchronous
|
|
174
|
+
registration (`register-hooks.mjs`, shown above) and for predicates constructed on
|
|
175
|
+
the loader thread; it is not accepted through the `data` option of the asynchronous
|
|
176
|
+
`module.register('import-in-the-middle/hook.mjs', ...)`.
|
|
177
|
+
|
|
148
178
|
## Limitations
|
|
149
179
|
|
|
150
180
|
* You cannot add new exports to a module. You can only modify existing ones.
|
package/create-hook.mjs
CHANGED
|
@@ -353,6 +353,7 @@ export function createHook (meta) {
|
|
|
353
353
|
let cachedResolve
|
|
354
354
|
const iitmURL = new URL('lib/register.js', meta.url).toString()
|
|
355
355
|
let includeModules, excludeModules
|
|
356
|
+
let shouldInclude = defaultShouldInclude
|
|
356
357
|
|
|
357
358
|
// Track CJS module URLs that IITM has wrapped. On Node 24+, CJS modules loaded
|
|
358
359
|
// via loadCJSModule (in an ESM import chain) have their require() calls for
|
|
@@ -362,6 +363,43 @@ export function createHook (meta) {
|
|
|
362
363
|
// patterns like `class App extends require('events') {}`.
|
|
363
364
|
const cjsInIitmChain = new Set()
|
|
364
365
|
|
|
366
|
+
// Default matcher, used unless the consumer supplies its own `shouldInclude`
|
|
367
|
+
// (see applyOptions). It applies the include/exclude lists, so finishResolve
|
|
368
|
+
// always has a predicate to call and never has to special-case its absence.
|
|
369
|
+
//
|
|
370
|
+
// We check the specifier to match libraries loaded with bare specifiers from
|
|
371
|
+
// node_modules, and the full file URL for non-bare specifier imports (relative
|
|
372
|
+
// paths would be error prone). An absolute path entry added via Hook over the
|
|
373
|
+
// message port matches the resolved file path, so it is resolved here.
|
|
374
|
+
function defaultShouldInclude (url, specifier) {
|
|
375
|
+
let resultPath
|
|
376
|
+
if (url.startsWith('file:')) {
|
|
377
|
+
const stackTraceLimit = Error.stackTraceLimit
|
|
378
|
+
Error.stackTraceLimit = 0
|
|
379
|
+
try {
|
|
380
|
+
resultPath = fileURLToPath(url)
|
|
381
|
+
} catch {}
|
|
382
|
+
Error.stackTraceLimit = stackTraceLimit
|
|
383
|
+
}
|
|
384
|
+
function match (each) {
|
|
385
|
+
if (each instanceof RegExp) {
|
|
386
|
+
return each.test(url)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return each === specifier || each === url || (resultPath && each === resultPath)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (includeModules && !includeModules.some(match)) {
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (excludeModules && excludeModules.some(match)) {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
|
|
365
403
|
// Applies the include/exclude/message-port configuration. Shared by the
|
|
366
404
|
// asynchronous `initialize` (off-thread `module.register`, which receives
|
|
367
405
|
// `data` over the registration boundary) and by synchronous registration
|
|
@@ -371,6 +409,13 @@ export function createHook (meta) {
|
|
|
371
409
|
includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
|
|
372
410
|
excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
|
|
373
411
|
|
|
412
|
+
// A consumer can supply its own matcher as `shouldInclude(url, specifier)`,
|
|
413
|
+
// taking ownership of the include/exclude decision instead of expressing it
|
|
414
|
+
// as bare-specifier / file-URL / regex lists. It replaces the default list
|
|
415
|
+
// matcher and is called with the resolved URL and specifier; otherwise the
|
|
416
|
+
// default applies the include/exclude options.
|
|
417
|
+
shouldInclude = typeof data.shouldInclude === 'function' ? data.shouldInclude : defaultShouldInclude
|
|
418
|
+
|
|
374
419
|
if (data.addHookMessagePort) {
|
|
375
420
|
data.addHookMessagePort.on('message', (modules) => {
|
|
376
421
|
if (includeModules === undefined) {
|
|
@@ -417,6 +462,12 @@ export function createHook (meta) {
|
|
|
417
462
|
return result
|
|
418
463
|
}
|
|
419
464
|
|
|
465
|
+
// Never wrap a module whose format we don't handle (e.g. json, wasm); this
|
|
466
|
+
// holds regardless of how inclusion is decided below.
|
|
467
|
+
if (result.format && !HANDLED_FORMATS.has(result.format)) {
|
|
468
|
+
return result
|
|
469
|
+
}
|
|
470
|
+
|
|
420
471
|
// The synchronous hooks (`module.registerHooks`) fire for `require()` as well
|
|
421
472
|
// as `import`, but iitm only owns the ESM graph: CommonJS modules are
|
|
422
473
|
// instrumented separately through require-in-the-middle, and `require()` must
|
|
@@ -430,37 +481,9 @@ export function createHook (meta) {
|
|
|
430
481
|
return result
|
|
431
482
|
}
|
|
432
483
|
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
// For non-bare specifier imports, we match to the full file URL because
|
|
437
|
-
// using relative paths would be very error prone!
|
|
438
|
-
let resultPath
|
|
439
|
-
if (result.url.startsWith('file:')) {
|
|
440
|
-
const stackTraceLimit = Error.stackTraceLimit
|
|
441
|
-
Error.stackTraceLimit = 0
|
|
442
|
-
try {
|
|
443
|
-
resultPath = fileURLToPath(result.url)
|
|
444
|
-
} catch {}
|
|
445
|
-
Error.stackTraceLimit = stackTraceLimit
|
|
446
|
-
}
|
|
447
|
-
function match (each) {
|
|
448
|
-
if (each instanceof RegExp) {
|
|
449
|
-
return each.test(result.url)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return each === specifier || each === result.url || (resultPath && each === resultPath)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (result.format && !HANDLED_FORMATS.has(result.format)) {
|
|
456
|
-
return result
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (includeModules && !includeModules.some(match)) {
|
|
460
|
-
return result
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (excludeModules && excludeModules.some(match)) {
|
|
484
|
+
// `shouldInclude` is always set (the include/exclude list matcher by default,
|
|
485
|
+
// a consumer-provided predicate otherwise), so no nullish check is needed.
|
|
486
|
+
if (!shouldInclude(result.url, specifier)) {
|
|
464
487
|
return result
|
|
465
488
|
}
|
|
466
489
|
|