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 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
- // For included/excluded modules, we check the specifier to match libraries
434
- // that are loaded with bare specifiers from node_modules.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "engines": {
6
6
  "node": ">=18"