import-in-the-middle

中文文档
3.5.3发布于 2026/10/26Apache-2.0TypeScriptCommonJS构建来源已验证近 30 天下载 3.3亿

Intercept imports in Node.js

原始使用文档
文档目录8 节
  1. Usage
  2. Only Intercepting Hooked modules
  3. Development benchmarks
  4. Synchronous loader hooks
  5. Custom matching with shouldInclude
  6. Opting out of CJS source stripping
  7. TypeScript modules
  8. Limitations

import-in-the-middle

import-in-the-middle is a module loading interceptor inspired by require-in-the-middle, but specifically for ESM modules. In fact, it can even modify modules after loading time.

Usage

The API for require-in-the-middle is followed as closely as possible as the default export. There are lower-level addHook and removeHook exports available which don't do any filtering of modules, and present the full file URL as a parameter to the hook. See the Typescript definition file for detailed API docs.

You can modify anything exported from any given ESM or CJS module that's imported in ESM files, regardless of whether they're imported statically or dynamically.

import { Hook } from 'import-in-the-middle'
import { foo } from 'package-i-want-to-modify'

console.log(foo) // whatever that module exported

Hook(['package-i-want-to-modify'], (exported, name, baseDir) => {
  // `exported` is effectively `import * as exported from ${url}`
  exported.foo += 1
})

console.log(foo) // 1 more than whatever that module exported

This requires the use of an ESM loader hook, which can be added with the following command-line option.

node --loader=import-in-the-middle/hook.mjs my-app.mjs

Since --loader has been deprecated you can also register the loader hook programmatically via the Node module.register() API. However, for this to be able to hook non-dynamic imports, it needs to be registered before your app code is evaluated via the --import command-line option.

my-loader.mjs

import * as module from 'module'

module.register('import-in-the-middle/hook.mjs', import.meta.url)
node --import=./my-loader.mjs ./my-code.mjs

When registering the loader hook programmatically, it's possible to pass a list of modules, file URLs or regular expressions to either exclude or specifically include which modules are intercepted. This is useful if a module is not compatible with the loader hook.

Note: This feature is incompatible with the {internals: true} Hook option

import * as module from 'module'

// Exclude intercepting a specific module by name
module.register('import-in-the-middle/hook.mjs', import.meta.url, {
  data: { exclude: ['package-i-want-to-exclude'] }
})

// Only intercept a specific module by name
module.register('import-in-the-middle/hook.mjs', import.meta.url, {
  data: { include: ['package-i-want-to-include'] }
})

Only Intercepting Hooked modules

Note: This feature is experimental and is incompatible with the {internals: true} Hook option

If you are Hook'ing all modules before they are imported, for example in a module loaded via the Node.js --import CLI argument, you can configure the loader to intercept only modules that were specifically hooked.

instrument.mjs

import { register } from 'module'
import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'

const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()

register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions)

Hook(['fs'], (exported, name, baseDir) => {
  // Instrument the fs module
})

// Ensure that the loader has acknowledged all the modules
// before we allow execution to continue
await waitForAllMessagesAcknowledged()

my-app.mjs

import * as fs from 'fs'
// fs will be instrumented!
fs.readFileSync('file.txt')
node --import=./instrument.mjs ./my-app.mjs

Development benchmarks

See benchmark/README.md for the PR benchmark method and how to check measurement noise locally.

Synchronous loader hooks

On Node.js versions that support module.registerHooks() the loader can run synchronously, on the application thread, instead of on the separate thread that module.register() uses. Running in-thread removes the message channel: Hook() registrations are visible to the loader directly, so the createAddHookMessageChannel / waitForAllMessagesAcknowledged step shown above is unnecessary.

module.registerHooks() was added in 22.15.0 / 24.0.0, but its synchronous load hook rejected the nullish CommonJS source the loader returns for require()s pulled into the ESM graph until nodejs/node#59929. The fix shipped in 22.22.3, 24.11.1, 25.1.0 and 26.0.0; earlier versions ship module.registerHooks() but cannot run the synchronous loader. Use supportsSyncHooks() to branch on this rather than a hand-written version check:

import { register, supportsSyncHooks } from 'import-in-the-middle/register-hooks.mjs'

if (supportsSyncHooks()) {
  register({ include: ['package-i-want-to-include'] })
} else {
  // Fall back to the asynchronous loader, e.g. module.register('import-in-the-middle/hook.mjs').
}

instrument.mjs

import { register } from 'import-in-the-middle/register-hooks.mjs'
import { Hook } from 'import-in-the-middle'

register({ include: ['package-i-want-to-include'] })

Hook(['package-i-want-to-include'], (exported, name, baseDir) => {
  // Instrument the module
})
node --import=./instrument.mjs ./my-app.mjs

register() accepts the same include / exclude options as the asynchronous loader and throws on a Node.js version where supportsSyncHooks() is false.

Custom matching with shouldInclude

Instead of include / exclude lists, you can pass a shouldInclude(url, specifier) predicate to decide which modules are intercepted. It is called for every resolved module with the resolved URL and the import specifier; return a truthy value to intercept the module. When a predicate is provided it takes over the decision and the include / exclude options are ignored.

This is useful when matching doesn't map cleanly onto bare specifiers, file URLs and regular expressions — for example a matcher built from your own configuration, or a decision that depends on more than the specifier.

import { register } from 'import-in-the-middle/register-hooks.mjs'

register({
  shouldInclude (url, specifier) {
    return specifier === 'package-i-want-to-include' ||
      url.includes('/node_modules/some-scope/')
  }
})

The predicate receives only the URL and the specifier, never a resolved file path. Because module.register() transfers its data to the loader thread by structured clone — which cannot carry a function — shouldInclude is supported for synchronous registration (register-hooks.mjs, shown above) and for predicates constructed on the loader thread; it is not accepted through the data option of the asynchronous module.register('import-in-the-middle/hook.mjs', ...).

Opting out of CJS source stripping

On Node 22+, when a CJS module is loaded through the ESM translator and another loader hook provides its source (instead of leaving it null for Node to read natively), require() calls inside that CJS module for packages using the "module-sync" exports condition fail with ERR_VM_MODULE_LINK_FAILURE. To work around this Node bug, import-in-the-middle strips hook-provided source for CJS modules in the synchronous require chain, forcing Node to use its native CJS loader which handles this correctly.

If you rely on a downstream loader hook seeing (and acting on) the source of CJS modules in the require chain, you can opt out of this workaround by passing disableCjsSourceStripping: true in the hook config. The workaround remains enabled by default.

import { register } from 'import-in-the-middle/register-hooks.mjs'

register({
  include: ['package-i-want-to-include'],
  disableCjsSourceStripping: true
})

This option is accepted by both synchronous (register-hooks.mjs) and asynchronous (module.register('import-in-the-middle/hook.mjs', ...)) registration.

TypeScript modules

On Node.js versions that strip TypeScript types natively (those exposing module.stripTypeScriptTypes(),

= 22.13.0 / >= 23.9.0 / >= 24.0.0), import-in-the-middle intercepts .ts, .mts and .cts modules just like their JavaScript counterparts. The types are stripped before the module's exports are read, so type-only exports (export type, export interface) are not present on the intercepted namespace; value exports are.

// math.mts
export type Op = '+' | '-'
export function add (a: number, b: number): number {
  return a + b
}
Hook(['./math.mts'], (exported) => {
  // `exported.add` is interceptable; the `Op` type is not part of the namespace
})

On Node.js versions where type stripping is not enabled by default, run with --experimental-strip-types. Older versions that predate module.stripTypeScriptTypes() leave TypeScript modules untouched.

Limitations

  • You cannot add new exports to a module. You can only modify existing ones.
  • While bindings to module exports end up being "re-bound" when modified in a hook, dynamically imported modules cannot be altered after they're loaded.
  • Modules loaded via require are not affected at all.
  • A module's set of export names is assumed to be stable for the lifetime of the process. import-in-the-middle reads a module's source once to lex its exports and reuses that export set on later loads of the same URL. An upstream loader that returns a different set of exports for the same URL across calls — a stateful codegen loader, or one that varies its output by context — is not supported and will be instrumented with the export set from its first load. Idempotent transforms (type stripping, AST instrumentation, minifiers) are unaffected, and the actual module still executes the source its real load returns; only the interceptable export names are memoized.

版本与发布记录

import-in-the-middle 共发布 65 个版本,首次发布于 2021/08/19;过去 12 个月发布了 20 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.5.3latest2026/10/2610 天前
3.5.22026/10/011 个月前
3.5.12026/09/111 个月前
3.5.02026/09/091 个月前
3.4.02026/08/312 个月前
3.3.32026/08/013 个月前

关于 import-in-the-middle 的常见问题

import-in-the-middle 是什么?

Intercept imports in Node.js

如何安装 import-in-the-middle?

在项目目录运行 npm install import-in-the-middle,或使用 pnpm add / yarn add / bun add。

import-in-the-middle 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

import-in-the-middle 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

import-in-the-middle 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

import-in-the-middle 可以商用吗?

import-in-the-middle 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

import-in-the-middle 的下载量有多少?

根据 npm 官方下载统计,import-in-the-middle 在 2026/10/05 – 2026/11/03 这 30 天内被下载 330,821,495 次,日均约 11,027,383 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

import-in-the-middle 还在维护吗?

最新版本 3.5.3 发布于 2026/10/26(10 天前);至今共发布 65 个版本,其中过去 12 个月发布了 20 个;首次发布于 2021/08/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。