import-in-the-middle 3.0.1 → 3.1.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 +14 -0
- package/README.md +30 -0
- package/create-hook.mjs +332 -126
- package/lib/get-exports.mjs +88 -52
- package/lib/io.mjs +78 -0
- package/package.json +1 -1
- package/register-hooks.d.ts +35 -0
- package/register-hooks.mjs +63 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [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
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add synchronous loader hooks via module.registerHooks ([#253](https://github.com/nodejs/import-in-the-middle/issues/253)) ([dd7e550](https://github.com/nodejs/import-in-the-middle/commit/dd7e5501f0cb901b8e4979f819e9deda6c84e2fc))
|
|
9
|
+
|
|
10
|
+
## [3.0.2](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.0.1...import-in-the-middle-v3.0.2) (2026-06-08)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* Updated proxying logic for builtins that are used as CJS ([#250](https://github.com/nodejs/import-in-the-middle/issues/250)) ([f69eb8f](https://github.com/nodejs/import-in-the-middle/commit/f69eb8f13c91b7088003e7390b4f54c5076aa50b))
|
|
16
|
+
|
|
3
17
|
## [3.0.1](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.0.0...import-in-the-middle-v3.0.1) (2026-04-07)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -115,6 +115,36 @@ fs.readFileSync('file.txt')
|
|
|
115
115
|
node --import=./instrument.mjs ./my-app.mjs
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## Synchronous loader hooks
|
|
119
|
+
|
|
120
|
+
On Node.js versions that provide
|
|
121
|
+
[`module.registerHooks()`](https://nodejs.org/api/module.html#moduleregisterhooksoptions)
|
|
122
|
+
(>= 22.15.0 / >= 24.0.0) the loader can run *synchronously*, on the application
|
|
123
|
+
thread, instead of on the separate thread that `module.register()` uses. Running
|
|
124
|
+
in-thread removes the message channel: `Hook()` registrations are visible to the
|
|
125
|
+
loader directly, so the `createAddHookMessageChannel` /
|
|
126
|
+
`waitForAllMessagesAcknowledged` step shown above is unnecessary.
|
|
127
|
+
|
|
128
|
+
`instrument.mjs`
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { register } from 'import-in-the-middle/register-hooks.mjs'
|
|
132
|
+
import { Hook } from 'import-in-the-middle'
|
|
133
|
+
|
|
134
|
+
register({ include: ['package-i-want-to-include'] })
|
|
135
|
+
|
|
136
|
+
Hook(['package-i-want-to-include'], (exported, name, baseDir) => {
|
|
137
|
+
// Instrument the module
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```shell
|
|
142
|
+
node --import=./instrument.mjs ./my-app.mjs
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`register()` accepts the same `include` / `exclude` options as the asynchronous
|
|
146
|
+
loader and throws on a Node.js version without `module.registerHooks()`.
|
|
147
|
+
|
|
118
148
|
## Limitations
|
|
119
149
|
|
|
120
150
|
* You cannot add new exports to a module. You can only modify existing ones.
|
package/create-hook.mjs
CHANGED
|
@@ -6,9 +6,10 @@ import { URL, fileURLToPath } from 'url'
|
|
|
6
6
|
import { inspect } from 'util'
|
|
7
7
|
import { builtinModules } from 'module'
|
|
8
8
|
import {
|
|
9
|
-
getExports
|
|
9
|
+
getExports,
|
|
10
10
|
hasModuleExportsCJSDefault
|
|
11
11
|
} from './lib/get-exports.mjs'
|
|
12
|
+
import { RESOLVE, driveSync, driveAsync } from './lib/io.mjs'
|
|
12
13
|
|
|
13
14
|
const specifiers = new Map()
|
|
14
15
|
const isWin = process.platform === 'win32'
|
|
@@ -16,17 +17,34 @@ const isWin = process.platform === 'win32'
|
|
|
16
17
|
// FIXME: Typescript extensions are added temporarily until we find a better
|
|
17
18
|
// way of supporting arbitrary extensions
|
|
18
19
|
const EXTENSION_RE = /\.(js|mjs|cjs|ts|mts|cts)$/
|
|
19
|
-
const NODE_VERSION = process.versions.node.split('.')
|
|
20
|
-
const NODE_MAJOR = Number(NODE_VERSION[0])
|
|
21
|
-
const NODE_MINOR = Number(NODE_VERSION[1])
|
|
22
20
|
const HANDLED_FORMATS = new Set(['builtin', 'module', 'commonjs'])
|
|
23
21
|
const TRACE_WARNINGS = process.execArgv.includes('--trace-warnings')
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// process.versions.node is always "major.minor.patch" (nightlies add a suffix
|
|
24
|
+
// the regex ignores).
|
|
25
|
+
const [, NODE_MAJOR, NODE_MINOR, NODE_PATCH] =
|
|
26
|
+
process.versions.node.match(/^(\d+)\.(\d+)\.(\d+)/).map(Number)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the running Node.js can correctly run the synchronous loader via
|
|
30
|
+
* `module.registerHooks`.
|
|
31
|
+
*
|
|
32
|
+
* `module.registerHooks` exists since v22.15, but its synchronous load hook
|
|
33
|
+
* rejected the nullish CommonJS `source` the loader returns for `require()`s
|
|
34
|
+
* pulled into the ESM graph (throwing `ERR_INVALID_RETURN_PROPERTY_VALUE`) until
|
|
35
|
+
* https://github.com/nodejs/node/pull/59929, released in 22.22.3, 24.11.1,
|
|
36
|
+
* 25.1.0 and 26.0.0. Earlier 24.x (<= 24.11.0) and 25.0.0 ship `registerHooks`
|
|
37
|
+
* but predate the fix, so the synchronous loader must fall back to the
|
|
38
|
+
* asynchronous one there.
|
|
39
|
+
*
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function supportsSyncHooks () {
|
|
43
|
+
if (NODE_MAJOR >= 26) return true
|
|
44
|
+
if (NODE_MAJOR === 25) return NODE_MINOR >= 1
|
|
45
|
+
if (NODE_MAJOR === 24) return NODE_MINOR > 11 || (NODE_MINOR === 11 && NODE_PATCH >= 1)
|
|
46
|
+
if (NODE_MAJOR === 22) return NODE_MINOR > 22 || (NODE_MINOR === 22 && NODE_PATCH >= 3)
|
|
47
|
+
return false
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
function hasIitm (url) {
|
|
@@ -181,21 +199,81 @@ function emitWarning (err) {
|
|
|
181
199
|
process.emitWarning(warnMessage)
|
|
182
200
|
}
|
|
183
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Builds the setter/getter/re-export block injected into the wrapper module for
|
|
204
|
+
* a single named export. This is pure string generation, identical regardless
|
|
205
|
+
* of how the loader is driven, so both the synchronous and asynchronous paths
|
|
206
|
+
* share it.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} n The exported name.
|
|
209
|
+
* @param {string} srcUrl The URL of the module the export belongs to.
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
function buildSetter (n, srcUrl) {
|
|
213
|
+
const variableName = `$${n.replace(/[^a-zA-Z0-9_$]/g, '_')}`
|
|
214
|
+
const objectKey = JSON.stringify(n)
|
|
215
|
+
const reExportedName = n === 'default' ? n : objectKey
|
|
216
|
+
|
|
217
|
+
// For the module.exports synthetic export (Node 23+), fall back to $default
|
|
218
|
+
// when namespace['module.exports'] is not exposed by the native ESM namespace
|
|
219
|
+
// (builtins don't expose it). This ensures the IITM hook proxy returns the
|
|
220
|
+
// actual CJS value (e.g. EventEmitter) when an instrumentor reads
|
|
221
|
+
// capturedExports['module.exports'], rather than undefined.
|
|
222
|
+
const moduleExportsFallback = n === 'module.exports' ? ' ?? $default' : ''
|
|
223
|
+
|
|
224
|
+
const reExportLine = (n === 'module.exports' && (srcUrl.startsWith('node:') || builtinModules.includes(srcUrl)))
|
|
225
|
+
? ''
|
|
226
|
+
: `export { ${variableName} as ${reExportedName} }`
|
|
227
|
+
|
|
228
|
+
return `
|
|
229
|
+
let ${variableName}
|
|
230
|
+
__overridden[${objectKey}] = false
|
|
231
|
+
let ${variableName}Defer = false
|
|
232
|
+
try {
|
|
233
|
+
${variableName} = _[${objectKey}] = namespace[${objectKey}]${moduleExportsFallback}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (!(err instanceof ReferenceError)) throw err
|
|
236
|
+
${variableName}Defer = true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (${variableName}Defer || ${variableName} === undefined) {
|
|
240
|
+
__pending.push(__makeUpdater(
|
|
241
|
+
${objectKey},
|
|
242
|
+
() => namespace[${objectKey}]${moduleExportsFallback},
|
|
243
|
+
(v) => { ${variableName} = _[${objectKey}] = v }
|
|
244
|
+
))
|
|
245
|
+
}
|
|
246
|
+
${reExportLine}
|
|
247
|
+
set[${objectKey}] = (v) => {
|
|
248
|
+
__overridden[${objectKey}] = true
|
|
249
|
+
${variableName} = v
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
get[${objectKey}] = () => ${variableName}
|
|
253
|
+
`
|
|
254
|
+
}
|
|
255
|
+
|
|
184
256
|
/**
|
|
185
257
|
* Processes a module's exports and builds a set of setter blocks.
|
|
186
258
|
*
|
|
259
|
+
* Written as a "sans-io" generator (see `lib/io.mjs`): instead of calling the
|
|
260
|
+
* loader's resolve/load hooks directly it `yield`s `[RESOLVE, ...]` to resolve
|
|
261
|
+
* star re-exports and `[LOAD, ...]` (via {@link getExports}) to read source,
|
|
262
|
+
* and is driven by either {@link driveSync} (for
|
|
263
|
+
* `module.registerHooks`) or {@link driveAsync} (for `module.register`). The
|
|
264
|
+
* body is identical for both, so there is a single implementation to maintain.
|
|
265
|
+
*
|
|
187
266
|
* @param {object} params
|
|
188
267
|
* @param {string} params.srcUrl The full URL to the module to process.
|
|
189
268
|
* @param {object} params.context Provided by the loaders API.
|
|
190
|
-
* @param {Function} params.parentGetSource Provides the source code for the parent module.
|
|
191
|
-
* @param {Function} params.parentResolve Provides the resolve function for the parent module.
|
|
192
269
|
* @param {boolean} [params.excludeDefault = false] Exclude the default export.
|
|
193
270
|
*
|
|
194
|
-
* @returns {
|
|
271
|
+
* @returns {Generator<Array, Map<string, string>>} A generator that yields I/O
|
|
272
|
+
* operations and ultimately returns the shimmed setters for all the exports
|
|
195
273
|
* from the module and any transitive export all modules.
|
|
196
274
|
*/
|
|
197
|
-
|
|
198
|
-
const exportNames =
|
|
275
|
+
function * processModule ({ srcUrl, context, excludeDefault = false }) {
|
|
276
|
+
const exportNames = yield * getExports(srcUrl, context)
|
|
199
277
|
const starExports = new Set()
|
|
200
278
|
const setters = new Map()
|
|
201
279
|
|
|
@@ -243,17 +321,14 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
|
|
|
243
321
|
|
|
244
322
|
// Relative paths need to be resolved relative to the parent module
|
|
245
323
|
const newSpecifier = isBareSpecifier(modFile) ? modFile : new URL(modFile, srcUrl).href
|
|
246
|
-
// We need to
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
const result = await parentResolve(newSpecifier, { parentURL: srcUrl })
|
|
324
|
+
// We need to resolve bare specifiers to a full URL. We also need to
|
|
325
|
+
// resolve all sub-modules to get the `format`. We can't rely on the
|
|
326
|
+
// parent's `format` to know if this sub-module is ESM or CJS!
|
|
327
|
+
const result = yield [RESOLVE, newSpecifier, { parentURL: srcUrl }]
|
|
251
328
|
|
|
252
|
-
const subSetters =
|
|
329
|
+
const subSetters = yield * processModule({
|
|
253
330
|
srcUrl: result.url,
|
|
254
331
|
context: { ...context, format: result.format },
|
|
255
|
-
parentGetSource,
|
|
256
|
-
parentResolve,
|
|
257
332
|
excludeDefault: true
|
|
258
333
|
})
|
|
259
334
|
|
|
@@ -261,36 +336,7 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
|
|
|
261
336
|
addSetter(name, setter, true)
|
|
262
337
|
}
|
|
263
338
|
} else {
|
|
264
|
-
|
|
265
|
-
const objectKey = JSON.stringify(n)
|
|
266
|
-
const reExportedName = n === 'default' ? n : objectKey
|
|
267
|
-
|
|
268
|
-
addSetter(n, `
|
|
269
|
-
let ${variableName}
|
|
270
|
-
__overridden[${objectKey}] = false
|
|
271
|
-
let ${variableName}Defer = false
|
|
272
|
-
try {
|
|
273
|
-
${variableName} = _[${objectKey}] = namespace[${objectKey}]
|
|
274
|
-
} catch (err) {
|
|
275
|
-
if (!(err instanceof ReferenceError)) throw err
|
|
276
|
-
${variableName}Defer = true
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (${variableName}Defer || ${variableName} === undefined) {
|
|
280
|
-
__pending.push(__makeUpdater(
|
|
281
|
-
${objectKey},
|
|
282
|
-
() => namespace[${objectKey}],
|
|
283
|
-
(v) => { ${variableName} = _[${objectKey}] = v }
|
|
284
|
-
))
|
|
285
|
-
}
|
|
286
|
-
export { ${variableName} as ${reExportedName} }
|
|
287
|
-
set[${objectKey}] = (v) => {
|
|
288
|
-
__overridden[${objectKey}] = true
|
|
289
|
-
${variableName} = v
|
|
290
|
-
return true
|
|
291
|
-
}
|
|
292
|
-
get[${objectKey}] = () => ${variableName}
|
|
293
|
-
`)
|
|
339
|
+
addSetter(n, buildSetter(n, srcUrl))
|
|
294
340
|
}
|
|
295
341
|
}
|
|
296
342
|
|
|
@@ -308,55 +354,59 @@ export function createHook (meta) {
|
|
|
308
354
|
const iitmURL = new URL('lib/register.js', meta.url).toString()
|
|
309
355
|
let includeModules, excludeModules
|
|
310
356
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
357
|
+
// Track CJS module URLs that IITM has wrapped. On Node 24+, CJS modules loaded
|
|
358
|
+
// via loadCJSModule (in an ESM import chain) have their require() calls for
|
|
359
|
+
// builtins routed through the ESM resolver. Without this guard, IITM would
|
|
360
|
+
// intercept those require() calls and return an ESM namespace object instead
|
|
361
|
+
// of the native CJS module value (e.g. EventEmitter constructor), breaking
|
|
362
|
+
// patterns like `class App extends require('events') {}`.
|
|
363
|
+
const cjsInIitmChain = new Set()
|
|
364
|
+
|
|
365
|
+
// Applies the include/exclude/message-port configuration. Shared by the
|
|
366
|
+
// asynchronous `initialize` (off-thread `module.register`, which receives
|
|
367
|
+
// `data` over the registration boundary) and by synchronous registration
|
|
368
|
+
// (`module.registerHooks`), which has no `initialize` step and passes the
|
|
369
|
+
// same options directly.
|
|
370
|
+
function applyOptions (data) {
|
|
371
|
+
includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
|
|
372
|
+
excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
|
|
373
|
+
|
|
374
|
+
if (data.addHookMessagePort) {
|
|
375
|
+
data.addHookMessagePort.on('message', (modules) => {
|
|
376
|
+
if (includeModules === undefined) {
|
|
377
|
+
includeModules = []
|
|
378
|
+
}
|
|
321
379
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
includeModules = []
|
|
380
|
+
for (const each of modules) {
|
|
381
|
+
if (!each.startsWith('node:') && builtinModules.includes(each)) {
|
|
382
|
+
includeModules.push(`node:${each}`)
|
|
326
383
|
}
|
|
327
384
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
includeModules.push(`node:${each}`)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
includeModules.push(each)
|
|
334
|
-
}
|
|
385
|
+
includeModules.push(each)
|
|
386
|
+
}
|
|
335
387
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
388
|
+
data.addHookMessagePort.postMessage('ack')
|
|
389
|
+
}).unref()
|
|
339
390
|
}
|
|
340
391
|
}
|
|
341
392
|
|
|
342
|
-
async function
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
// See https://github.com/nodejs/import-in-the-middle/pull/76.
|
|
346
|
-
if (specifier === iitmURL) {
|
|
347
|
-
return {
|
|
348
|
-
url: specifier,
|
|
349
|
-
shortCircuit: true
|
|
350
|
-
}
|
|
393
|
+
async function initialize (data) {
|
|
394
|
+
if (global.__import_in_the_middle_initialized__) {
|
|
395
|
+
process.emitWarning("The 'import-in-the-middle' hook has already been initialized")
|
|
351
396
|
}
|
|
352
397
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
398
|
+
global.__import_in_the_middle_initialized__ = true
|
|
399
|
+
|
|
400
|
+
if (data) {
|
|
401
|
+
applyOptions(data)
|
|
357
402
|
}
|
|
358
|
-
|
|
403
|
+
}
|
|
359
404
|
|
|
405
|
+
// Shared post-processing for the `resolve` hook: everything that happens
|
|
406
|
+
// once the parent loader has turned the specifier into a resolved URL. The
|
|
407
|
+
// only difference between the asynchronous and synchronous hooks is whether
|
|
408
|
+
// that resolution was awaited, so all the wrapping decisions live here.
|
|
409
|
+
function finishResolve (result, specifier, context, parentURL) {
|
|
360
410
|
// Do not wrap the entrypoint module. Many CLIs check whether they are the
|
|
361
411
|
// "main" module (e.g. require.main === module). Wrapping changes how they
|
|
362
412
|
// are evaluated, and can make them exit without doing anything.
|
|
@@ -367,6 +417,19 @@ export function createHook (meta) {
|
|
|
367
417
|
return result
|
|
368
418
|
}
|
|
369
419
|
|
|
420
|
+
// The synchronous hooks (`module.registerHooks`) fire for `require()` as well
|
|
421
|
+
// as `import`, but iitm only owns the ESM graph: CommonJS modules are
|
|
422
|
+
// instrumented separately through require-in-the-middle, and `require()` must
|
|
423
|
+
// return the native, mutable module value (e.g. graceful-fs does
|
|
424
|
+
// `Object.defineProperty(require('fs'), ...)`, which throws on a frozen ESM
|
|
425
|
+
// namespace). Node reports the active module system in `context.conditions`
|
|
426
|
+
// ('require' vs 'import'), so leave any require() resolution untouched. The
|
|
427
|
+
// asynchronous hook never sees the 'require' condition, so this is a no-op
|
|
428
|
+
// there and only affects the synchronous path.
|
|
429
|
+
if (context.conditions?.includes('require')) {
|
|
430
|
+
return result
|
|
431
|
+
}
|
|
432
|
+
|
|
370
433
|
// For included/excluded modules, we check the specifier to match libraries
|
|
371
434
|
// that are loaded with bare specifiers from node_modules.
|
|
372
435
|
//
|
|
@@ -405,6 +468,17 @@ export function createHook (meta) {
|
|
|
405
468
|
return result
|
|
406
469
|
}
|
|
407
470
|
|
|
471
|
+
// When a CJS module is loaded by an IITM shim, its require() calls for
|
|
472
|
+
// builtins may be routed through the ESM resolver on Node 24+. Skip IITM
|
|
473
|
+
// wrapping in that case so require() returns the native module value.
|
|
474
|
+
// We also propagate the membership to the resolved child so that its own
|
|
475
|
+
// transitive require() calls are likewise skipped (the entire synchronous
|
|
476
|
+
// CJS require chain must remain unwrapped to avoid ERR_VM_MODULE_LINK_FAILURE).
|
|
477
|
+
if (cjsInIitmChain.has(parentURL)) {
|
|
478
|
+
cjsInIitmChain.add(result.url)
|
|
479
|
+
return result
|
|
480
|
+
}
|
|
481
|
+
|
|
408
482
|
// We don't want to attempt to wrap native modules
|
|
409
483
|
if (result.url.endsWith('.node')) {
|
|
410
484
|
return result
|
|
@@ -430,26 +504,63 @@ export function createHook (meta) {
|
|
|
430
504
|
return {
|
|
431
505
|
url: addIitm(result.url),
|
|
432
506
|
shortCircuit: true,
|
|
433
|
-
format:
|
|
507
|
+
// Node's synchronous resolver drops `format: 'builtin'` for bare builtin
|
|
508
|
+
// specifiers (`require('crypto')` -> `node:crypto`), so restore it;
|
|
509
|
+
// otherwise the load hook reads `node:crypto` from disk and throws ENOENT.
|
|
510
|
+
format: result.format ?? (result.url.startsWith('node:') ? 'builtin' : undefined)
|
|
434
511
|
}
|
|
435
512
|
}
|
|
436
513
|
|
|
437
|
-
async function
|
|
438
|
-
|
|
439
|
-
const realUrl = deleteIitm(url)
|
|
440
|
-
const originalSpecifier = specifiers.get(realUrl)
|
|
514
|
+
async function resolve (specifier, context, parentResolve) {
|
|
515
|
+
cachedResolve = parentResolve
|
|
441
516
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
517
|
+
// See https://github.com/nodejs/import-in-the-middle/pull/76.
|
|
518
|
+
if (specifier === iitmURL) {
|
|
519
|
+
return {
|
|
520
|
+
url: specifier,
|
|
521
|
+
shortCircuit: true
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const { parentURL = '' } = context
|
|
526
|
+
const newSpecifier = deleteIitm(specifier)
|
|
527
|
+
if (isWin && parentURL.indexOf('file:node') === 0) {
|
|
528
|
+
context.parentURL = ''
|
|
529
|
+
}
|
|
530
|
+
const result = await parentResolve(newSpecifier, context)
|
|
531
|
+
|
|
532
|
+
return finishResolve(result, specifier, context, parentURL)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Synchronous counterpart to `resolve`, for `module.registerHooks`. The
|
|
536
|
+
// synchronous `nextResolve` returns its result directly. We stash it so the
|
|
537
|
+
// synchronous `load` hook can resolve star re-exports later, mirroring how
|
|
538
|
+
// `resolve` caches `parentResolve`.
|
|
539
|
+
function resolveSync (specifier, context, nextResolve) {
|
|
540
|
+
cachedResolve = nextResolve
|
|
541
|
+
|
|
542
|
+
if (specifier === iitmURL) {
|
|
543
|
+
return {
|
|
544
|
+
url: specifier,
|
|
545
|
+
shortCircuit: true
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const { parentURL = '' } = context
|
|
550
|
+
const newSpecifier = deleteIitm(specifier)
|
|
551
|
+
if (isWin && parentURL.indexOf('file:node') === 0) {
|
|
552
|
+
context.parentURL = ''
|
|
553
|
+
}
|
|
554
|
+
const result = nextResolve(newSpecifier, context)
|
|
555
|
+
|
|
556
|
+
return finishResolve(result, specifier, context, parentURL)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Builds the wrapper module source that re-exports the real module through
|
|
560
|
+
// iitm's proxy. Pure string generation shared by the asynchronous and
|
|
561
|
+
// synchronous `load` paths.
|
|
562
|
+
function buildWrapperSource (realUrl, setters, originalSpecifier) {
|
|
563
|
+
return `
|
|
453
564
|
import { register } from '${iitmURL}'
|
|
454
565
|
import * as namespace from ${JSON.stringify(realUrl)}
|
|
455
566
|
|
|
@@ -518,26 +629,46 @@ if (__pending.length > 0) {
|
|
|
518
629
|
|
|
519
630
|
register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpecifier)})
|
|
520
631
|
`
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Bookkeeping shared by the async and sync wrap paths once `processModule`
|
|
635
|
+
// succeeds: free the specifier entry early, and remember CJS modules so their
|
|
636
|
+
// transitive require() chain bypasses iitm (see `load`). Returns the wrapper
|
|
637
|
+
// module source.
|
|
638
|
+
function onWrapSuccess (realUrl, context, originalSpecifier, setters) {
|
|
639
|
+
specifiers.delete(realUrl)
|
|
640
|
+
// context.format is set to 'commonjs' by getCjsExports during processModule.
|
|
641
|
+
if (context.format === 'commonjs') {
|
|
642
|
+
cjsInIitmChain.add(realUrl)
|
|
643
|
+
}
|
|
644
|
+
return buildWrapperSource(realUrl, setters, originalSpecifier)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Bookkeeping shared by the async and sync wrap paths when `processModule`
|
|
648
|
+
// throws. iitm falls back to the parent loader so the module loads unwrapped
|
|
649
|
+
// (it just can't be Hook'ed) rather than taking down the whole app. We free
|
|
650
|
+
// the specifier entry to avoid a leak, and log because a failure here is
|
|
651
|
+
// usually an iitm bug and would otherwise be very tricky to debug.
|
|
652
|
+
function onWrapFailure (realUrl, cause) {
|
|
653
|
+
specifiers.delete(realUrl)
|
|
654
|
+
const err = new Error(`'import-in-the-middle' failed to wrap '${realUrl}'`)
|
|
655
|
+
err.cause = cause
|
|
656
|
+
emitWarning(err)
|
|
657
|
+
}
|
|
540
658
|
|
|
659
|
+
async function getSource (url, context, parentGetSource) {
|
|
660
|
+
if (hasIitm(url)) {
|
|
661
|
+
const realUrl = deleteIitm(url)
|
|
662
|
+
const originalSpecifier = specifiers.get(realUrl)
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const setters = await driveAsync(
|
|
666
|
+
processModule({ srcUrl: realUrl, context }),
|
|
667
|
+
{ resolve: cachedResolve, load: parentGetSource }
|
|
668
|
+
)
|
|
669
|
+
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters) }
|
|
670
|
+
} catch (cause) {
|
|
671
|
+
onWrapFailure(realUrl, cause)
|
|
541
672
|
// Revert back to the non-iitm URL
|
|
542
673
|
url = realUrl
|
|
543
674
|
}
|
|
@@ -546,6 +677,29 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
546
677
|
return parentGetSource(url, context)
|
|
547
678
|
}
|
|
548
679
|
|
|
680
|
+
// Synchronous counterpart to `getSource`, for `module.registerHooks`. Drives
|
|
681
|
+
// `processModule` straight through; all bookkeeping and source generation is
|
|
682
|
+
// shared with `getSource`.
|
|
683
|
+
function getSourceSync (url, context, nextLoad) {
|
|
684
|
+
if (hasIitm(url)) {
|
|
685
|
+
const realUrl = deleteIitm(url)
|
|
686
|
+
const originalSpecifier = specifiers.get(realUrl)
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
const setters = driveSync(
|
|
690
|
+
processModule({ srcUrl: realUrl, context }),
|
|
691
|
+
{ resolve: cachedResolve, load: nextLoad }
|
|
692
|
+
)
|
|
693
|
+
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters) }
|
|
694
|
+
} catch (cause) {
|
|
695
|
+
onWrapFailure(realUrl, cause)
|
|
696
|
+
url = realUrl
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return nextLoad(url, context)
|
|
701
|
+
}
|
|
702
|
+
|
|
549
703
|
async function load (url, context, parentLoad) {
|
|
550
704
|
if (hasIitm(url)) {
|
|
551
705
|
const result = await getSource(url, context, parentLoad)
|
|
@@ -563,8 +717,60 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
563
717
|
return parentLoad(deleteIitm(url), context)
|
|
564
718
|
}
|
|
565
719
|
|
|
720
|
+
// On Node 22+, when a CJS module is loaded through the ESM translator and
|
|
721
|
+
// another loader hook provides its source (instead of leaving source null
|
|
722
|
+
// for Node to read natively), require() calls inside that CJS module for
|
|
723
|
+
// packages using the "module-sync" exports condition fail with
|
|
724
|
+
// ERR_VM_MODULE_LINK_FAILURE. Work around this Node bug by stripping
|
|
725
|
+
// hook-provided source for CJS modules in the synchronous require chain,
|
|
726
|
+
// forcing Node to use its native CJS loader which handles this correctly.
|
|
727
|
+
if (cjsInIitmChain.has(url)) {
|
|
728
|
+
const result = await parentLoad(url, context)
|
|
729
|
+
if (result.format === 'commonjs' && result.source != null) {
|
|
730
|
+
return {
|
|
731
|
+
format: result.format,
|
|
732
|
+
source: undefined
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return result
|
|
736
|
+
}
|
|
737
|
+
|
|
566
738
|
return parentLoad(url, context)
|
|
567
739
|
}
|
|
568
740
|
|
|
569
|
-
|
|
741
|
+
// Synchronous counterpart to `load`, for `module.registerHooks`. Mirrors the
|
|
742
|
+
// async `load` exactly — wrapping via `getSourceSync` and applying the same
|
|
743
|
+
// CJS-in-iitm-chain source stripping — only without awaiting.
|
|
744
|
+
function loadSync (url, context, nextLoad) {
|
|
745
|
+
if (hasIitm(url)) {
|
|
746
|
+
const result = getSourceSync(url, context, nextLoad)
|
|
747
|
+
// If wrapping failed, `getSourceSync()` may have fallen back to `nextLoad`,
|
|
748
|
+
// which can legally return `source: null` (e.g. for non-JS formats).
|
|
749
|
+
if (result && typeof result === 'object' && result.source != null) {
|
|
750
|
+
return {
|
|
751
|
+
source: result.source,
|
|
752
|
+
shortCircuit: true,
|
|
753
|
+
format: 'module'
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Fall back to the parent loader with the original (non-iitm) URL.
|
|
758
|
+
return nextLoad(deleteIitm(url), context)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (cjsInIitmChain.has(url)) {
|
|
762
|
+
const result = nextLoad(url, context)
|
|
763
|
+
if (result.format === 'commonjs' && result.source != null) {
|
|
764
|
+
return {
|
|
765
|
+
format: result.format,
|
|
766
|
+
source: undefined
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return result
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return nextLoad(url, context)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return { initialize, load, resolve, resolveSync, loadSync, applyOptions }
|
|
570
776
|
}
|
package/lib/get-exports.mjs
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
import getEsmExports from './get-esm-exports.mjs'
|
|
4
|
-
import { parse as parseCjs,
|
|
4
|
+
import { parse as parseCjs, initSync } from 'cjs-module-lexer'
|
|
5
5
|
import { readFileSync, existsSync } from 'fs'
|
|
6
6
|
import { builtinModules, createRequire } from 'module'
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'url'
|
|
8
8
|
import { dirname, join } from 'path'
|
|
9
|
+
import { LOAD } from './io.mjs'
|
|
9
10
|
|
|
10
11
|
const nodeMajor = Number(process.versions.node.split('.')[0])
|
|
11
12
|
export const hasModuleExportsCJSDefault = nodeMajor >= 23
|
|
12
13
|
|
|
13
14
|
let parserInitialized = false
|
|
14
15
|
|
|
16
|
+
// The CJS export scanner is backed by WebAssembly. `initSync` compiles it
|
|
17
|
+
// up front so the scanner can run inside synchronous loader hooks
|
|
18
|
+
// (`module.registerHooks`) as well as the off-thread loader; it is a one-time
|
|
19
|
+
// cost on the first CommonJS module either way.
|
|
20
|
+
function ensureParserInitialized () {
|
|
21
|
+
if (!parserInitialized) {
|
|
22
|
+
initSync()
|
|
23
|
+
parserInitialized = true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function addDefault (arr) {
|
|
16
28
|
return new Set(['default', ...arr])
|
|
17
29
|
}
|
|
@@ -142,15 +154,33 @@ const BUILT_INS = new Map()
|
|
|
142
154
|
|
|
143
155
|
let require
|
|
144
156
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
// Returns a builtin's exports object. `process.getBuiltinModule` (Node >=
|
|
158
|
+
// 20.16 / >= 22.3) bypasses registered loader hooks; `require` does not. Under
|
|
159
|
+
// the in-thread `module.registerHooks` loader a plain `require(name)` here
|
|
160
|
+
// re-enters iitm's own hooks and resolves to the half-built wrapper instead of
|
|
161
|
+
// the native module. The off-thread `module.register` loader runs `require` on
|
|
162
|
+
// the loader thread where the hooks aren't installed, so the fallback stays
|
|
163
|
+
// correct on older Node that lacks getBuiltinModule.
|
|
164
|
+
function loadBuiltin (name) {
|
|
165
|
+
if (typeof process.getBuiltinModule === 'function') {
|
|
166
|
+
return process.getBuiltinModule(name)
|
|
167
|
+
}
|
|
148
168
|
if (!require) {
|
|
149
169
|
require = createRequire(import.meta.url)
|
|
150
170
|
}
|
|
171
|
+
return require(name)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getExportsForNodeBuiltIn (name) {
|
|
175
|
+
let exports = BUILT_INS.get(name)
|
|
151
176
|
|
|
152
177
|
if (!exports) {
|
|
153
|
-
|
|
178
|
+
// get all properties both enumerable and non-enumerable
|
|
179
|
+
exports = new Set(addDefault(Object.getOwnPropertyNames(loadBuiltin(name))))
|
|
180
|
+
// added in node 23 as alias for default in cjs modules
|
|
181
|
+
if (hasModuleExportsCJSDefault) {
|
|
182
|
+
exports.add('module.exports')
|
|
183
|
+
}
|
|
154
184
|
BUILT_INS.set(name, exports)
|
|
155
185
|
}
|
|
156
186
|
|
|
@@ -215,52 +245,54 @@ function resolvePackageImports (specifier, fromUrl) {
|
|
|
215
245
|
return null
|
|
216
246
|
}
|
|
217
247
|
|
|
218
|
-
|
|
248
|
+
function * getCjsExports (url, context, source) {
|
|
219
249
|
if (urlsBeingProcessed.has(url)) {
|
|
220
250
|
return new Set()
|
|
221
251
|
}
|
|
222
252
|
urlsBeingProcessed.add(url)
|
|
223
253
|
|
|
224
254
|
try {
|
|
225
|
-
|
|
226
|
-
await parserInit()
|
|
227
|
-
parserInitialized = true
|
|
228
|
-
}
|
|
255
|
+
ensureParserInitialized()
|
|
229
256
|
const result = parseCjs(source)
|
|
230
257
|
const full = addDefault(result.exports)
|
|
231
258
|
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
for (const each of getExportsForNodeBuiltIn(
|
|
259
|
+
for (const reexport of result.reexports) {
|
|
260
|
+
if (reexport.startsWith('node:') || builtinModules.includes(reexport)) {
|
|
261
|
+
for (const each of getExportsForNodeBuiltIn(reexport)) {
|
|
235
262
|
full.add(each)
|
|
236
263
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
re = './'
|
|
240
|
-
}
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
241
266
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
// Resolve each re-export relative to the current module. Keep the
|
|
268
|
+
// resolution scoped to this iteration: a `#`-import rewrites both the
|
|
269
|
+
// base URL and the specifier, and that rewrite must not leak into the
|
|
270
|
+
// next re-export.
|
|
271
|
+
let reUrl = url
|
|
272
|
+
let reSpecifier = reexport === '.' ? './' : reexport
|
|
273
|
+
|
|
274
|
+
// Entries in the import field should always start with #
|
|
275
|
+
if (reSpecifier.startsWith('#')) {
|
|
276
|
+
const resolved = resolvePackageImports(reSpecifier, url)
|
|
277
|
+
if (!resolved) continue
|
|
278
|
+
;[reUrl, reSpecifier] = resolved
|
|
279
|
+
}
|
|
248
280
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
if (!require) {
|
|
282
|
+
require = createRequire(import.meta.url)
|
|
283
|
+
}
|
|
284
|
+
const newUrl = pathToFileURL(
|
|
285
|
+
require.resolve(reSpecifier, { paths: [dirname(fileURLToPath(reUrl))] })
|
|
286
|
+
).href
|
|
254
287
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
288
|
+
if (newUrl.endsWith('.node') || newUrl.endsWith('.json')) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
258
291
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
292
|
+
for (const each of yield * getExports(newUrl, context)) {
|
|
293
|
+
full.add(each)
|
|
262
294
|
}
|
|
263
|
-
}
|
|
295
|
+
}
|
|
264
296
|
|
|
265
297
|
// added in node 23 as alias for default in cjs modules
|
|
266
298
|
if (full.has('default') && hasModuleExportsCJSDefault) {
|
|
@@ -276,26 +308,30 @@ async function getCjsExports (url, context, parentLoad, source) {
|
|
|
276
308
|
}
|
|
277
309
|
|
|
278
310
|
/**
|
|
279
|
-
* Inspects a module for its type (commonjs or module),
|
|
280
|
-
*
|
|
281
|
-
*
|
|
311
|
+
* Inspects a module for its type (commonjs or module), obtains the source code
|
|
312
|
+
* for said module from the loader API, and parses the result for the entities
|
|
313
|
+
* exported from that module.
|
|
314
|
+
*
|
|
315
|
+
* This is a "sans-io" generator: instead of calling the loader's `load` hook
|
|
316
|
+
* directly, it `yield`s `[LOAD, url, context]` and is driven by either
|
|
317
|
+
* {@link driveSync} or {@link driveAsync} (see `lib/io.mjs`). The same body
|
|
318
|
+
* therefore serves both the off-thread loader and `module.registerHooks`.
|
|
282
319
|
*
|
|
283
|
-
* @param {string} url A file URL string pointing to the module that
|
|
284
|
-
*
|
|
285
|
-
* @param {object} context Context object as provided by the `load`
|
|
286
|
-
*
|
|
287
|
-
* @param {Function} parentLoad Next hook function in the loaders API
|
|
288
|
-
* hook chain.
|
|
320
|
+
* @param {string} url A file URL string pointing to the module that we should
|
|
321
|
+
* get the exports of.
|
|
322
|
+
* @param {object} context Context object as provided by the `load` hook from
|
|
323
|
+
* the loaders API.
|
|
289
324
|
*
|
|
290
|
-
* @returns {
|
|
325
|
+
* @returns {Generator<Array, Set<string>>} A generator that yields I/O
|
|
326
|
+
* operations and ultimately returns the identifiers exported by the module.
|
|
291
327
|
* Please see {@link getEsmExports} for caveats on special identifiers that may
|
|
292
328
|
* be included in the result set.
|
|
293
329
|
*/
|
|
294
|
-
export
|
|
295
|
-
// `
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
const parentCtx =
|
|
330
|
+
export function * getExports (url, context) {
|
|
331
|
+
// `[LOAD, ...]` gives us the possibility of getting the source from an
|
|
332
|
+
// upstream loader. This doesn't always work though, so later on we fall back
|
|
333
|
+
// to reading it from disk.
|
|
334
|
+
const parentCtx = yield [LOAD, url, context]
|
|
299
335
|
let source = parentCtx.source
|
|
300
336
|
const format = parentCtx.format
|
|
301
337
|
|
|
@@ -331,7 +367,7 @@ export async function getExports (url, context, parentLoad) {
|
|
|
331
367
|
}
|
|
332
368
|
|
|
333
369
|
if (format === 'commonjs') {
|
|
334
|
-
return
|
|
370
|
+
return yield * getCjsExports(url, context, source)
|
|
335
371
|
}
|
|
336
372
|
|
|
337
373
|
// At this point our `format` is either undefined or not known by us. Fall
|
|
@@ -344,7 +380,7 @@ export async function getExports (url, context, parentLoad) {
|
|
|
344
380
|
if (!hasEsmSyntax(source)) {
|
|
345
381
|
// It might be possible to get here if the format
|
|
346
382
|
// isn't set at first and yet we have an ESM module with no exports.
|
|
347
|
-
return
|
|
383
|
+
return yield * getCjsExports(url, context, source)
|
|
348
384
|
}
|
|
349
385
|
}
|
|
350
386
|
return esmExports
|
package/lib/io.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// The export-collection logic (resolving star re-exports, reading source,
|
|
4
|
+
// parsing exports) is identical whether `import-in-the-middle` runs as an
|
|
5
|
+
// off-thread loader (`module.register`, asynchronous `nextResolve`/`nextLoad`)
|
|
6
|
+
// or as an in-thread synchronous loader (`module.registerHooks`). To keep a
|
|
7
|
+
// single implementation of that logic — instead of two copies that drift — it
|
|
8
|
+
// is written as "sans-io" generators that `yield` the I/O they need and let a
|
|
9
|
+
// driver fulfil it. The async driver awaits; the sync driver calls straight
|
|
10
|
+
// through. Everything between the yields is shared.
|
|
11
|
+
|
|
12
|
+
// Operation kinds a loader generator may yield. Each is `[KIND, ...args]`.
|
|
13
|
+
export const LOAD = 0 // [LOAD, url, context] -> resolves to { source, format }
|
|
14
|
+
export const RESOLVE = 1 // [RESOLVE, specifier, context] -> resolves to { url, format }
|
|
15
|
+
|
|
16
|
+
function runOp (op, io) {
|
|
17
|
+
if (op[0] === RESOLVE) {
|
|
18
|
+
return io.resolve(op[1], op[2])
|
|
19
|
+
}
|
|
20
|
+
return io.load(op[1], op[2])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Drives a loader generator to completion, fulfilling each yielded I/O
|
|
25
|
+
* operation synchronously. Used with `module.registerHooks`, whose
|
|
26
|
+
* `nextResolve`/`nextLoad` return their result directly.
|
|
27
|
+
*
|
|
28
|
+
* Errors from I/O are thrown back into the generator (via `gen.throw`) so its
|
|
29
|
+
* `try`/`finally` blocks run exactly as they would for an `await` rejection.
|
|
30
|
+
*
|
|
31
|
+
* @template T
|
|
32
|
+
* @param {Generator<Array, T>} gen
|
|
33
|
+
* @param {{ load: Function, resolve?: Function }} io
|
|
34
|
+
* @returns {T}
|
|
35
|
+
*/
|
|
36
|
+
export function driveSync (gen, io) {
|
|
37
|
+
let next = gen.next()
|
|
38
|
+
while (next.done === false) {
|
|
39
|
+
let result
|
|
40
|
+
let error
|
|
41
|
+
let threw = false
|
|
42
|
+
try {
|
|
43
|
+
result = runOp(next.value, io)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
threw = true
|
|
46
|
+
error = err
|
|
47
|
+
}
|
|
48
|
+
next = threw ? gen.throw(error) : gen.next(result)
|
|
49
|
+
}
|
|
50
|
+
return next.value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Drives a loader generator to completion, awaiting each yielded I/O
|
|
55
|
+
* operation. Used with the off-thread `module.register` loader, whose
|
|
56
|
+
* `nextResolve`/`nextLoad` are asynchronous.
|
|
57
|
+
*
|
|
58
|
+
* @template T
|
|
59
|
+
* @param {Generator<Array, T>} gen
|
|
60
|
+
* @param {{ load: Function, resolve?: Function }} io
|
|
61
|
+
* @returns {Promise<T>}
|
|
62
|
+
*/
|
|
63
|
+
export async function driveAsync (gen, io) {
|
|
64
|
+
let next = gen.next()
|
|
65
|
+
while (next.done === false) {
|
|
66
|
+
let result
|
|
67
|
+
let error
|
|
68
|
+
let threw = false
|
|
69
|
+
try {
|
|
70
|
+
result = await runOp(next.value, io)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
threw = true
|
|
73
|
+
error = err
|
|
74
|
+
}
|
|
75
|
+
next = threw ? gen.throw(error) : gen.next(result)
|
|
76
|
+
}
|
|
77
|
+
return next.value
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for {@link register}. `include`/`exclude` accept bare specifiers,
|
|
3
|
+
* `file:` URLs or regular expressions, matched against the module being
|
|
4
|
+
* resolved.
|
|
5
|
+
*/
|
|
6
|
+
export type RegisterHooksOptions = {
|
|
7
|
+
include?: Array<string | RegExp>
|
|
8
|
+
exclude?: Array<string | RegExp>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registers `import-in-the-middle` as a *synchronous*, in-thread loader hook via
|
|
13
|
+
* [`module.registerHooks()`](https://nodejs.org/api/module.html#moduleregisterhooksoptions).
|
|
14
|
+
*
|
|
15
|
+
* Unlike `module.register('import-in-the-middle/hook.mjs', ...)`, which runs the
|
|
16
|
+
* loader on a separate thread and pays an IPC round-trip per resolved module,
|
|
17
|
+
* synchronous hooks run on the application thread, so `Hook()` registrations are
|
|
18
|
+
* visible to the loader directly and no acknowledgement step is required.
|
|
19
|
+
*
|
|
20
|
+
* Requires a Node.js version with `module.registerHooks()` (>= 22.15.0 / >= 24).
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { register } from 'import-in-the-middle/register-hooks.mjs'
|
|
24
|
+
* import { Hook } from 'import-in-the-middle'
|
|
25
|
+
*
|
|
26
|
+
* register({ include: ['package-i-want-to-include'] })
|
|
27
|
+
*
|
|
28
|
+
* Hook(['package-i-want-to-include'], (exported, name, baseDir) => {
|
|
29
|
+
* // Instrument the module
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @throws If `module.registerHooks()` is unavailable in the running Node.js.
|
|
34
|
+
*/
|
|
35
|
+
export declare function register(options?: RegisterHooksOptions): void
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as module from 'module'
|
|
2
|
+
import { createHook, supportsSyncHooks } from './create-hook.mjs'
|
|
3
|
+
|
|
4
|
+
export { supportsSyncHooks }
|
|
5
|
+
|
|
6
|
+
const hook = createHook(import.meta)
|
|
7
|
+
|
|
8
|
+
let registered = false
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registers `import-in-the-middle` as a *synchronous*, in-thread loader hook via
|
|
12
|
+
* [`module.registerHooks()`](https://nodejs.org/api/module.html#moduleregisterhooksoptions).
|
|
13
|
+
*
|
|
14
|
+
* Unlike `module.register('import-in-the-middle/hook.mjs', ...)`, which runs the
|
|
15
|
+
* loader on a separate thread and pays an IPC round-trip per resolved module,
|
|
16
|
+
* synchronous hooks run in the application thread. There is no message channel
|
|
17
|
+
* to bridge, so `Hook()` registrations from the main `import-in-the-middle`
|
|
18
|
+
* entry point are visible to the loader directly and no acknowledgement step is
|
|
19
|
+
* required.
|
|
20
|
+
*
|
|
21
|
+
* Requires a Node.js version whose `module.registerHooks` accepts the nullish
|
|
22
|
+
* CommonJS source the loader relies on: >= 22.22.3, >= 24.11.1, >= 25.1.0, or
|
|
23
|
+
* >= 26.0.0 (see `supportsSyncHooks`). Use that predicate to fall back to the
|
|
24
|
+
* asynchronous `module.register` loader on unsupported versions.
|
|
25
|
+
*
|
|
26
|
+
* ```js
|
|
27
|
+
* import { register } from 'import-in-the-middle/register-hooks.mjs'
|
|
28
|
+
* import { Hook } from 'import-in-the-middle'
|
|
29
|
+
*
|
|
30
|
+
* register({ include: ['package-i-want-to-include'] })
|
|
31
|
+
*
|
|
32
|
+
* Hook(['package-i-want-to-include'], (exported, name, baseDir) => {
|
|
33
|
+
* // Instrument the module
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @param {object} [options]
|
|
38
|
+
* @param {Array<string|RegExp>} [options.include] Only intercept these modules.
|
|
39
|
+
* @param {Array<string|RegExp>} [options.exclude] Never intercept these modules.
|
|
40
|
+
* @returns {void}
|
|
41
|
+
*/
|
|
42
|
+
export function register (options) {
|
|
43
|
+
if (!supportsSyncHooks()) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"'import-in-the-middle' synchronous hooks require a Node.js version whose " +
|
|
46
|
+
'module.registerHooks accepts nullish CommonJS source ' +
|
|
47
|
+
'(>= 22.22.3, >= 24.11.1, >= 25.1.0, or >= 26.0.0); ' +
|
|
48
|
+
'see https://github.com/nodejs/node/pull/59929'
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (registered) {
|
|
53
|
+
process.emitWarning("'import-in-the-middle' synchronous hooks have already been registered")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
registered = true
|
|
57
|
+
|
|
58
|
+
if (options) {
|
|
59
|
+
hook.applyOptions(options)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.registerHooks({ resolve: hook.resolveSync, load: hook.loadSync })
|
|
63
|
+
}
|