import-in-the-middle 3.0.2 → 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 +14 -0
- package/README.md +60 -0
- package/create-hook.mjs +338 -158
- package/lib/get-exports.mjs +83 -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.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
|
+
|
|
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)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* 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))
|
|
16
|
+
|
|
3
17
|
## [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)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -115,6 +115,66 @@ 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
|
+
|
|
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
|
+
|
|
118
178
|
## Limitations
|
|
119
179
|
|
|
120
180
|
* 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,43 +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
|
-
// For the module.exports synthetic export (Node 23+), fall back to $default
|
|
269
|
-
// when namespace['module.exports'] is not exposed by the native ESM namespace
|
|
270
|
-
// (builtins don't expose it). This ensures the IITM hook proxy returns the
|
|
271
|
-
// actual CJS value (e.g. EventEmitter) when an instrumentor reads
|
|
272
|
-
// capturedExports['module.exports'], rather than undefined.
|
|
273
|
-
const moduleExportsFallback = n === 'module.exports' ? ' ?? $default' : ''
|
|
274
|
-
|
|
275
|
-
addSetter(n, `
|
|
276
|
-
let ${variableName}
|
|
277
|
-
__overridden[${objectKey}] = false
|
|
278
|
-
let ${variableName}Defer = false
|
|
279
|
-
try {
|
|
280
|
-
${variableName} = _[${objectKey}] = namespace[${objectKey}]${moduleExportsFallback}
|
|
281
|
-
} catch (err) {
|
|
282
|
-
if (!(err instanceof ReferenceError)) throw err
|
|
283
|
-
${variableName}Defer = true
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (${variableName}Defer || ${variableName} === undefined) {
|
|
287
|
-
__pending.push(__makeUpdater(
|
|
288
|
-
${objectKey},
|
|
289
|
-
() => namespace[${objectKey}]${moduleExportsFallback},
|
|
290
|
-
(v) => { ${variableName} = _[${objectKey}] = v }
|
|
291
|
-
))
|
|
292
|
-
}
|
|
293
|
-
${(n === 'module.exports' && (srcUrl.startsWith('node:') || builtinModules.includes(srcUrl))) ? '' : `export { ${variableName} as ${reExportedName} }`}
|
|
294
|
-
set[${objectKey}] = (v) => {
|
|
295
|
-
__overridden[${objectKey}] = true
|
|
296
|
-
${variableName} = v
|
|
297
|
-
return true
|
|
298
|
-
}
|
|
299
|
-
get[${objectKey}] = () => ${variableName}
|
|
300
|
-
`)
|
|
339
|
+
addSetter(n, buildSetter(n, srcUrl))
|
|
301
340
|
}
|
|
302
341
|
}
|
|
303
342
|
|
|
@@ -314,6 +353,7 @@ export function createHook (meta) {
|
|
|
314
353
|
let cachedResolve
|
|
315
354
|
const iitmURL = new URL('lib/register.js', meta.url).toString()
|
|
316
355
|
let includeModules, excludeModules
|
|
356
|
+
let shouldInclude = defaultShouldInclude
|
|
317
357
|
|
|
318
358
|
// Track CJS module URLs that IITM has wrapped. On Node 24+, CJS modules loaded
|
|
319
359
|
// via loadCJSModule (in an ESM import chain) have their require() calls for
|
|
@@ -323,55 +363,95 @@ export function createHook (meta) {
|
|
|
323
363
|
// patterns like `class App extends require('events') {}`.
|
|
324
364
|
const cjsInIitmChain = new Set()
|
|
325
365
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
329
383
|
}
|
|
384
|
+
function match (each) {
|
|
385
|
+
if (each instanceof RegExp) {
|
|
386
|
+
return each.test(url)
|
|
387
|
+
}
|
|
330
388
|
|
|
331
|
-
|
|
389
|
+
return each === specifier || each === url || (resultPath && each === resultPath)
|
|
390
|
+
}
|
|
332
391
|
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
392
|
+
if (includeModules && !includeModules.some(match)) {
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
336
395
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
includeModules = []
|
|
341
|
-
}
|
|
396
|
+
if (excludeModules && excludeModules.some(match)) {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
342
399
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
includeModules.push(`node:${each}`)
|
|
346
|
-
}
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
347
402
|
|
|
348
|
-
|
|
403
|
+
// Applies the include/exclude/message-port configuration. Shared by the
|
|
404
|
+
// asynchronous `initialize` (off-thread `module.register`, which receives
|
|
405
|
+
// `data` over the registration boundary) and by synchronous registration
|
|
406
|
+
// (`module.registerHooks`), which has no `initialize` step and passes the
|
|
407
|
+
// same options directly.
|
|
408
|
+
function applyOptions (data) {
|
|
409
|
+
includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
|
|
410
|
+
excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
|
|
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
|
+
|
|
419
|
+
if (data.addHookMessagePort) {
|
|
420
|
+
data.addHookMessagePort.on('message', (modules) => {
|
|
421
|
+
if (includeModules === undefined) {
|
|
422
|
+
includeModules = []
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const each of modules) {
|
|
426
|
+
if (!each.startsWith('node:') && builtinModules.includes(each)) {
|
|
427
|
+
includeModules.push(`node:${each}`)
|
|
349
428
|
}
|
|
350
429
|
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
430
|
+
includeModules.push(each)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
data.addHookMessagePort.postMessage('ack')
|
|
434
|
+
}).unref()
|
|
354
435
|
}
|
|
355
436
|
}
|
|
356
437
|
|
|
357
|
-
async function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// See https://github.com/nodejs/import-in-the-middle/pull/76.
|
|
361
|
-
if (specifier === iitmURL) {
|
|
362
|
-
return {
|
|
363
|
-
url: specifier,
|
|
364
|
-
shortCircuit: true
|
|
365
|
-
}
|
|
438
|
+
async function initialize (data) {
|
|
439
|
+
if (global.__import_in_the_middle_initialized__) {
|
|
440
|
+
process.emitWarning("The 'import-in-the-middle' hook has already been initialized")
|
|
366
441
|
}
|
|
367
442
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
|
|
443
|
+
global.__import_in_the_middle_initialized__ = true
|
|
444
|
+
|
|
445
|
+
if (data) {
|
|
446
|
+
applyOptions(data)
|
|
372
447
|
}
|
|
373
|
-
|
|
448
|
+
}
|
|
374
449
|
|
|
450
|
+
// Shared post-processing for the `resolve` hook: everything that happens
|
|
451
|
+
// once the parent loader has turned the specifier into a resolved URL. The
|
|
452
|
+
// only difference between the asynchronous and synchronous hooks is whether
|
|
453
|
+
// that resolution was awaited, so all the wrapping decisions live here.
|
|
454
|
+
function finishResolve (result, specifier, context, parentURL) {
|
|
375
455
|
// Do not wrap the entrypoint module. Many CLIs check whether they are the
|
|
376
456
|
// "main" module (e.g. require.main === module). Wrapping changes how they
|
|
377
457
|
// are evaluated, and can make them exit without doing anything.
|
|
@@ -382,37 +462,28 @@ export function createHook (meta) {
|
|
|
382
462
|
return result
|
|
383
463
|
}
|
|
384
464
|
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
// For non-bare specifier imports, we match to the full file URL because
|
|
389
|
-
// using relative paths would be very error prone!
|
|
390
|
-
let resultPath
|
|
391
|
-
if (result.url.startsWith('file:')) {
|
|
392
|
-
const stackTraceLimit = Error.stackTraceLimit
|
|
393
|
-
Error.stackTraceLimit = 0
|
|
394
|
-
try {
|
|
395
|
-
resultPath = fileURLToPath(result.url)
|
|
396
|
-
} catch {}
|
|
397
|
-
Error.stackTraceLimit = stackTraceLimit
|
|
398
|
-
}
|
|
399
|
-
function match (each) {
|
|
400
|
-
if (each instanceof RegExp) {
|
|
401
|
-
return each.test(result.url)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return each === specifier || each === result.url || (resultPath && each === resultPath)
|
|
405
|
-
}
|
|
406
|
-
|
|
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.
|
|
407
467
|
if (result.format && !HANDLED_FORMATS.has(result.format)) {
|
|
408
468
|
return result
|
|
409
469
|
}
|
|
410
470
|
|
|
411
|
-
|
|
471
|
+
// The synchronous hooks (`module.registerHooks`) fire for `require()` as well
|
|
472
|
+
// as `import`, but iitm only owns the ESM graph: CommonJS modules are
|
|
473
|
+
// instrumented separately through require-in-the-middle, and `require()` must
|
|
474
|
+
// return the native, mutable module value (e.g. graceful-fs does
|
|
475
|
+
// `Object.defineProperty(require('fs'), ...)`, which throws on a frozen ESM
|
|
476
|
+
// namespace). Node reports the active module system in `context.conditions`
|
|
477
|
+
// ('require' vs 'import'), so leave any require() resolution untouched. The
|
|
478
|
+
// asynchronous hook never sees the 'require' condition, so this is a no-op
|
|
479
|
+
// there and only affects the synchronous path.
|
|
480
|
+
if (context.conditions?.includes('require')) {
|
|
412
481
|
return result
|
|
413
482
|
}
|
|
414
483
|
|
|
415
|
-
|
|
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)) {
|
|
416
487
|
return result
|
|
417
488
|
}
|
|
418
489
|
|
|
@@ -456,31 +527,63 @@ export function createHook (meta) {
|
|
|
456
527
|
return {
|
|
457
528
|
url: addIitm(result.url),
|
|
458
529
|
shortCircuit: true,
|
|
459
|
-
format:
|
|
530
|
+
// Node's synchronous resolver drops `format: 'builtin'` for bare builtin
|
|
531
|
+
// specifiers (`require('crypto')` -> `node:crypto`), so restore it;
|
|
532
|
+
// otherwise the load hook reads `node:crypto` from disk and throws ENOENT.
|
|
533
|
+
format: result.format ?? (result.url.startsWith('node:') ? 'builtin' : undefined)
|
|
460
534
|
}
|
|
461
535
|
}
|
|
462
536
|
|
|
463
|
-
async function
|
|
464
|
-
|
|
465
|
-
const realUrl = deleteIitm(url)
|
|
466
|
-
const originalSpecifier = specifiers.get(realUrl)
|
|
537
|
+
async function resolve (specifier, context, parentResolve) {
|
|
538
|
+
cachedResolve = parentResolve
|
|
467
539
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
540
|
+
// See https://github.com/nodejs/import-in-the-middle/pull/76.
|
|
541
|
+
if (specifier === iitmURL) {
|
|
542
|
+
return {
|
|
543
|
+
url: specifier,
|
|
544
|
+
shortCircuit: true
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const { parentURL = '' } = context
|
|
549
|
+
const newSpecifier = deleteIitm(specifier)
|
|
550
|
+
if (isWin && parentURL.indexOf('file:node') === 0) {
|
|
551
|
+
context.parentURL = ''
|
|
552
|
+
}
|
|
553
|
+
const result = await parentResolve(newSpecifier, context)
|
|
554
|
+
|
|
555
|
+
return finishResolve(result, specifier, context, parentURL)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Synchronous counterpart to `resolve`, for `module.registerHooks`. The
|
|
559
|
+
// synchronous `nextResolve` returns its result directly. We stash it so the
|
|
560
|
+
// synchronous `load` hook can resolve star re-exports later, mirroring how
|
|
561
|
+
// `resolve` caches `parentResolve`.
|
|
562
|
+
function resolveSync (specifier, context, nextResolve) {
|
|
563
|
+
cachedResolve = nextResolve
|
|
564
|
+
|
|
565
|
+
if (specifier === iitmURL) {
|
|
566
|
+
return {
|
|
567
|
+
url: specifier,
|
|
568
|
+
shortCircuit: true
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const { parentURL = '' } = context
|
|
573
|
+
const newSpecifier = deleteIitm(specifier)
|
|
574
|
+
if (isWin && parentURL.indexOf('file:node') === 0) {
|
|
575
|
+
context.parentURL = ''
|
|
576
|
+
}
|
|
577
|
+
const result = nextResolve(newSpecifier, context)
|
|
578
|
+
|
|
579
|
+
return finishResolve(result, specifier, context, parentURL)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Builds the wrapper module source that re-exports the real module through
|
|
583
|
+
// iitm's proxy. Pure string generation shared by the asynchronous and
|
|
584
|
+
// synchronous `load` paths.
|
|
585
|
+
function buildWrapperSource (realUrl, setters, originalSpecifier) {
|
|
586
|
+
return `
|
|
484
587
|
import { register } from '${iitmURL}'
|
|
485
588
|
import * as namespace from ${JSON.stringify(realUrl)}
|
|
486
589
|
|
|
@@ -549,26 +652,46 @@ if (__pending.length > 0) {
|
|
|
549
652
|
|
|
550
653
|
register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpecifier)})
|
|
551
654
|
`
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Bookkeeping shared by the async and sync wrap paths once `processModule`
|
|
658
|
+
// succeeds: free the specifier entry early, and remember CJS modules so their
|
|
659
|
+
// transitive require() chain bypasses iitm (see `load`). Returns the wrapper
|
|
660
|
+
// module source.
|
|
661
|
+
function onWrapSuccess (realUrl, context, originalSpecifier, setters) {
|
|
662
|
+
specifiers.delete(realUrl)
|
|
663
|
+
// context.format is set to 'commonjs' by getCjsExports during processModule.
|
|
664
|
+
if (context.format === 'commonjs') {
|
|
665
|
+
cjsInIitmChain.add(realUrl)
|
|
666
|
+
}
|
|
667
|
+
return buildWrapperSource(realUrl, setters, originalSpecifier)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Bookkeeping shared by the async and sync wrap paths when `processModule`
|
|
671
|
+
// throws. iitm falls back to the parent loader so the module loads unwrapped
|
|
672
|
+
// (it just can't be Hook'ed) rather than taking down the whole app. We free
|
|
673
|
+
// the specifier entry to avoid a leak, and log because a failure here is
|
|
674
|
+
// usually an iitm bug and would otherwise be very tricky to debug.
|
|
675
|
+
function onWrapFailure (realUrl, cause) {
|
|
676
|
+
specifiers.delete(realUrl)
|
|
677
|
+
const err = new Error(`'import-in-the-middle' failed to wrap '${realUrl}'`)
|
|
678
|
+
err.cause = cause
|
|
679
|
+
emitWarning(err)
|
|
680
|
+
}
|
|
571
681
|
|
|
682
|
+
async function getSource (url, context, parentGetSource) {
|
|
683
|
+
if (hasIitm(url)) {
|
|
684
|
+
const realUrl = deleteIitm(url)
|
|
685
|
+
const originalSpecifier = specifiers.get(realUrl)
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
const setters = await driveAsync(
|
|
689
|
+
processModule({ srcUrl: realUrl, context }),
|
|
690
|
+
{ resolve: cachedResolve, load: parentGetSource }
|
|
691
|
+
)
|
|
692
|
+
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters) }
|
|
693
|
+
} catch (cause) {
|
|
694
|
+
onWrapFailure(realUrl, cause)
|
|
572
695
|
// Revert back to the non-iitm URL
|
|
573
696
|
url = realUrl
|
|
574
697
|
}
|
|
@@ -577,6 +700,29 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
577
700
|
return parentGetSource(url, context)
|
|
578
701
|
}
|
|
579
702
|
|
|
703
|
+
// Synchronous counterpart to `getSource`, for `module.registerHooks`. Drives
|
|
704
|
+
// `processModule` straight through; all bookkeeping and source generation is
|
|
705
|
+
// shared with `getSource`.
|
|
706
|
+
function getSourceSync (url, context, nextLoad) {
|
|
707
|
+
if (hasIitm(url)) {
|
|
708
|
+
const realUrl = deleteIitm(url)
|
|
709
|
+
const originalSpecifier = specifiers.get(realUrl)
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
const setters = driveSync(
|
|
713
|
+
processModule({ srcUrl: realUrl, context }),
|
|
714
|
+
{ resolve: cachedResolve, load: nextLoad }
|
|
715
|
+
)
|
|
716
|
+
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters) }
|
|
717
|
+
} catch (cause) {
|
|
718
|
+
onWrapFailure(realUrl, cause)
|
|
719
|
+
url = realUrl
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return nextLoad(url, context)
|
|
724
|
+
}
|
|
725
|
+
|
|
580
726
|
async function load (url, context, parentLoad) {
|
|
581
727
|
if (hasIitm(url)) {
|
|
582
728
|
const result = await getSource(url, context, parentLoad)
|
|
@@ -615,5 +761,39 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
615
761
|
return parentLoad(url, context)
|
|
616
762
|
}
|
|
617
763
|
|
|
618
|
-
|
|
764
|
+
// Synchronous counterpart to `load`, for `module.registerHooks`. Mirrors the
|
|
765
|
+
// async `load` exactly — wrapping via `getSourceSync` and applying the same
|
|
766
|
+
// CJS-in-iitm-chain source stripping — only without awaiting.
|
|
767
|
+
function loadSync (url, context, nextLoad) {
|
|
768
|
+
if (hasIitm(url)) {
|
|
769
|
+
const result = getSourceSync(url, context, nextLoad)
|
|
770
|
+
// If wrapping failed, `getSourceSync()` may have fallen back to `nextLoad`,
|
|
771
|
+
// which can legally return `source: null` (e.g. for non-JS formats).
|
|
772
|
+
if (result && typeof result === 'object' && result.source != null) {
|
|
773
|
+
return {
|
|
774
|
+
source: result.source,
|
|
775
|
+
shortCircuit: true,
|
|
776
|
+
format: 'module'
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Fall back to the parent loader with the original (non-iitm) URL.
|
|
781
|
+
return nextLoad(deleteIitm(url), context)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (cjsInIitmChain.has(url)) {
|
|
785
|
+
const result = nextLoad(url, context)
|
|
786
|
+
if (result.format === 'commonjs' && result.source != null) {
|
|
787
|
+
return {
|
|
788
|
+
format: result.format,
|
|
789
|
+
source: undefined
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return result
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return nextLoad(url, context)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return { initialize, load, resolve, resolveSync, loadSync, applyOptions }
|
|
619
799
|
}
|
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,16 +154,29 @@ 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
|
|
154
|
-
exports = new Set(addDefault(Object.getOwnPropertyNames(
|
|
179
|
+
exports = new Set(addDefault(Object.getOwnPropertyNames(loadBuiltin(name))))
|
|
155
180
|
// added in node 23 as alias for default in cjs modules
|
|
156
181
|
if (hasModuleExportsCJSDefault) {
|
|
157
182
|
exports.add('module.exports')
|
|
@@ -220,52 +245,54 @@ function resolvePackageImports (specifier, fromUrl) {
|
|
|
220
245
|
return null
|
|
221
246
|
}
|
|
222
247
|
|
|
223
|
-
|
|
248
|
+
function * getCjsExports (url, context, source) {
|
|
224
249
|
if (urlsBeingProcessed.has(url)) {
|
|
225
250
|
return new Set()
|
|
226
251
|
}
|
|
227
252
|
urlsBeingProcessed.add(url)
|
|
228
253
|
|
|
229
254
|
try {
|
|
230
|
-
|
|
231
|
-
await parserInit()
|
|
232
|
-
parserInitialized = true
|
|
233
|
-
}
|
|
255
|
+
ensureParserInitialized()
|
|
234
256
|
const result = parseCjs(source)
|
|
235
257
|
const full = addDefault(result.exports)
|
|
236
258
|
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
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)) {
|
|
240
262
|
full.add(each)
|
|
241
263
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
re = './'
|
|
245
|
-
}
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
246
266
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
+
}
|
|
253
280
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
if (!require) {
|
|
282
|
+
require = createRequire(import.meta.url)
|
|
283
|
+
}
|
|
284
|
+
const newUrl = pathToFileURL(
|
|
285
|
+
require.resolve(reSpecifier, { paths: [dirname(fileURLToPath(reUrl))] })
|
|
286
|
+
).href
|
|
259
287
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
288
|
+
if (newUrl.endsWith('.node') || newUrl.endsWith('.json')) {
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
263
291
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
292
|
+
for (const each of yield * getExports(newUrl, context)) {
|
|
293
|
+
full.add(each)
|
|
267
294
|
}
|
|
268
|
-
}
|
|
295
|
+
}
|
|
269
296
|
|
|
270
297
|
// added in node 23 as alias for default in cjs modules
|
|
271
298
|
if (full.has('default') && hasModuleExportsCJSDefault) {
|
|
@@ -281,26 +308,30 @@ async function getCjsExports (url, context, parentLoad, source) {
|
|
|
281
308
|
}
|
|
282
309
|
|
|
283
310
|
/**
|
|
284
|
-
* Inspects a module for its type (commonjs or module),
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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`.
|
|
287
319
|
*
|
|
288
|
-
* @param {string} url A file URL string pointing to the module that
|
|
289
|
-
*
|
|
290
|
-
* @param {object} context Context object as provided by the `load`
|
|
291
|
-
*
|
|
292
|
-
* @param {Function} parentLoad Next hook function in the loaders API
|
|
293
|
-
* 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.
|
|
294
324
|
*
|
|
295
|
-
* @returns {
|
|
325
|
+
* @returns {Generator<Array, Set<string>>} A generator that yields I/O
|
|
326
|
+
* operations and ultimately returns the identifiers exported by the module.
|
|
296
327
|
* Please see {@link getEsmExports} for caveats on special identifiers that may
|
|
297
328
|
* be included in the result set.
|
|
298
329
|
*/
|
|
299
|
-
export
|
|
300
|
-
// `
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
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]
|
|
304
335
|
let source = parentCtx.source
|
|
305
336
|
const format = parentCtx.format
|
|
306
337
|
|
|
@@ -336,7 +367,7 @@ export async function getExports (url, context, parentLoad) {
|
|
|
336
367
|
}
|
|
337
368
|
|
|
338
369
|
if (format === 'commonjs') {
|
|
339
|
-
return
|
|
370
|
+
return yield * getCjsExports(url, context, source)
|
|
340
371
|
}
|
|
341
372
|
|
|
342
373
|
// At this point our `format` is either undefined or not known by us. Fall
|
|
@@ -349,7 +380,7 @@ export async function getExports (url, context, parentLoad) {
|
|
|
349
380
|
if (!hasEsmSyntax(source)) {
|
|
350
381
|
// It might be possible to get here if the format
|
|
351
382
|
// isn't set at first and yet we have an ESM module with no exports.
|
|
352
|
-
return
|
|
383
|
+
return yield * getCjsExports(url, context, source)
|
|
353
384
|
}
|
|
354
385
|
}
|
|
355
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
|
+
}
|