import-in-the-middle 3.2.0 → 3.3.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 +20 -0
- package/README.md +54 -7
- package/create-hook.mjs +190 -98
- package/lib/get-esm-exports.mjs +123 -93
- package/lib/get-exports.mjs +30 -138
- package/package.json +2 -3
- package/register-hooks.d.ts +11 -2
- package/register-hooks.mjs +2 -1
- package/supports-sync-hooks.mjs +47 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.3.0](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v3.2.0...import-in-the-middle-v3.3.0) (2026-07-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* instrument type-stripped TypeScript modules ([#258](https://github.com/nodejs/import-in-the-middle/issues/258)) ([3c6b456](https://github.com/nodejs/import-in-the-middle/commit/3c6b4568a3d23028576ffb728e908a4d06e48523))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* guard against circular export-star recursion ([#261](https://github.com/nodejs/import-in-the-middle/issues/261)) ([b957de3](https://github.com/nodejs/import-in-the-middle/commit/b957de3667292ec2ea3b8729704664d1324eaa8f))
|
|
14
|
+
* keep get-esm-exports synchronous so require() works ([#266](https://github.com/nodejs/import-in-the-middle/issues/266)) ([4244b25](https://github.com/nodejs/import-in-the-middle/commit/4244b2512f6256af0ff916f6dc98cc65d988187a))
|
|
15
|
+
* resolve re-exported values from their defining module ([#263](https://github.com/nodejs/import-in-the-middle/issues/263)) ([5ef34cb](https://github.com/nodejs/import-in-the-middle/commit/5ef34cbb59fb65cd0e25b27e06236d6c95c69176))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Performance Improvements
|
|
19
|
+
|
|
20
|
+
* replace acorn with es-module-lexer for ESM parsing ([#259](https://github.com/nodejs/import-in-the-middle/issues/259)) ([b8cc8af](https://github.com/nodejs/import-in-the-middle/commit/b8cc8af150f46d09b1f447e01a89344d2e647c2c))
|
|
21
|
+
* shrink generated wrapper source ([#264](https://github.com/nodejs/import-in-the-middle/issues/264)) ([491d968](https://github.com/nodejs/import-in-the-middle/commit/491d968d5e80cf0f0add2caeb0f447106caf32c3))
|
|
22
|
+
|
|
3
23
|
## [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
24
|
|
|
5
25
|
|
package/README.md
CHANGED
|
@@ -117,13 +117,32 @@ node --import=./instrument.mjs ./my-app.mjs
|
|
|
117
117
|
|
|
118
118
|
## Synchronous loader hooks
|
|
119
119
|
|
|
120
|
-
On Node.js versions that
|
|
120
|
+
On Node.js versions that support
|
|
121
121
|
[`module.registerHooks()`](https://nodejs.org/api/module.html#moduleregisterhooksoptions)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
the loader can run *synchronously*, on the application thread, instead of on the
|
|
123
|
+
separate thread that `module.register()` uses. Running in-thread removes the
|
|
124
|
+
message channel: `Hook()` registrations are visible to the loader directly, so
|
|
125
|
+
the `createAddHookMessageChannel` / `waitForAllMessagesAcknowledged` step shown
|
|
126
|
+
above is unnecessary.
|
|
127
|
+
|
|
128
|
+
`module.registerHooks()` was added in 22.15.0 / 24.0.0, but its synchronous load
|
|
129
|
+
hook rejected the nullish CommonJS `source` the loader returns for `require()`s
|
|
130
|
+
pulled into the ESM graph until [nodejs/node#59929][]. The fix shipped in
|
|
131
|
+
**22.22.3, 24.11.1, 25.1.0 and 26.0.0**; earlier versions ship
|
|
132
|
+
`module.registerHooks()` but cannot run the synchronous loader. Use
|
|
133
|
+
`supportsSyncHooks()` to branch on this rather than a hand-written version check:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { register, supportsSyncHooks } from 'import-in-the-middle/register-hooks.mjs'
|
|
137
|
+
|
|
138
|
+
if (supportsSyncHooks()) {
|
|
139
|
+
register({ include: ['package-i-want-to-include'] })
|
|
140
|
+
} else {
|
|
141
|
+
// Fall back to the asynchronous loader, e.g. module.register('import-in-the-middle/hook.mjs').
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
[nodejs/node#59929]: https://github.com/nodejs/node/pull/59929
|
|
127
146
|
|
|
128
147
|
`instrument.mjs`
|
|
129
148
|
|
|
@@ -143,7 +162,7 @@ node --import=./instrument.mjs ./my-app.mjs
|
|
|
143
162
|
```
|
|
144
163
|
|
|
145
164
|
`register()` accepts the same `include` / `exclude` options as the asynchronous
|
|
146
|
-
loader and throws on a Node.js version
|
|
165
|
+
loader and throws on a Node.js version where `supportsSyncHooks()` is `false`.
|
|
147
166
|
|
|
148
167
|
### Custom matching with `shouldInclude`
|
|
149
168
|
|
|
@@ -175,6 +194,34 @@ registration (`register-hooks.mjs`, shown above) and for predicates constructed
|
|
|
175
194
|
the loader thread; it is not accepted through the `data` option of the asynchronous
|
|
176
195
|
`module.register('import-in-the-middle/hook.mjs', ...)`.
|
|
177
196
|
|
|
197
|
+
## TypeScript modules
|
|
198
|
+
|
|
199
|
+
On Node.js versions that strip TypeScript types natively (those exposing
|
|
200
|
+
[`module.stripTypeScriptTypes()`](https://nodejs.org/api/module.html#modulestriptypescripttypescode-options),
|
|
201
|
+
>= 22.13.0 / >= 23.9.0 / >= 24.0.0), `import-in-the-middle` intercepts `.ts`,
|
|
202
|
+
`.mts` and `.cts` modules just like their JavaScript counterparts. The types are
|
|
203
|
+
stripped before the module's exports are read, so type-only exports
|
|
204
|
+
(`export type`, `export interface`) are not present on the intercepted
|
|
205
|
+
namespace; value exports are.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// math.mts
|
|
209
|
+
export type Op = '+' | '-'
|
|
210
|
+
export function add (a: number, b: number): number {
|
|
211
|
+
return a + b
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
Hook(['./math.mts'], (exported) => {
|
|
217
|
+
// `exported.add` is interceptable; the `Op` type is not part of the namespace
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
On Node.js versions where type stripping is not enabled by default, run with
|
|
222
|
+
`--experimental-strip-types`. Older versions that predate
|
|
223
|
+
`module.stripTypeScriptTypes()` leave TypeScript modules untouched.
|
|
224
|
+
|
|
178
225
|
## Limitations
|
|
179
226
|
|
|
180
227
|
* You cannot add new exports to a module. You can only modify existing ones.
|
package/create-hook.mjs
CHANGED
|
@@ -10,43 +10,33 @@ import {
|
|
|
10
10
|
hasModuleExportsCJSDefault
|
|
11
11
|
} from './lib/get-exports.mjs'
|
|
12
12
|
import { RESOLVE, driveSync, driveAsync } from './lib/io.mjs'
|
|
13
|
+
import { supportsSyncHooks } from './supports-sync-hooks.mjs'
|
|
14
|
+
|
|
15
|
+
// Re-exported for backwards compatibility: `supportsSyncHooks` now lives in its
|
|
16
|
+
// own import-free module so a CommonJS preloader can check it without loading
|
|
17
|
+
// this file's acorn / cjs-module-lexer dependency graph.
|
|
18
|
+
export { supportsSyncHooks }
|
|
13
19
|
|
|
14
20
|
const specifiers = new Map()
|
|
15
21
|
const isWin = process.platform === 'win32'
|
|
16
22
|
|
|
23
|
+
// Depth at which `processModule` starts tracking visited URLs to break an
|
|
24
|
+
// `export *` cycle. Real re-export chains are only a few levels deep, so this
|
|
25
|
+
// is far beyond any legitimate graph yet well below the call-stack limit a
|
|
26
|
+
// cycle would otherwise hit. Below it the recursion pays only an integer
|
|
27
|
+
// compare per level and allocates no set.
|
|
28
|
+
const STAR_CYCLE_DEPTH = 100
|
|
29
|
+
|
|
17
30
|
// FIXME: Typescript extensions are added temporarily until we find a better
|
|
18
31
|
// way of supporting arbitrary extensions
|
|
19
32
|
const EXTENSION_RE = /\.(js|mjs|cjs|ts|mts|cts)$/
|
|
20
|
-
|
|
33
|
+
// The `-typescript` formats are listed unconditionally; getExports strips the
|
|
34
|
+
// types when the runtime supports it and otherwise falls back to onWrapFailure.
|
|
35
|
+
const HANDLED_FORMATS = new Set([
|
|
36
|
+
'builtin', 'module', 'commonjs', 'module-typescript', 'commonjs-typescript'
|
|
37
|
+
])
|
|
21
38
|
const TRACE_WARNINGS = process.execArgv.includes('--trace-warnings')
|
|
22
39
|
|
|
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
|
|
48
|
-
}
|
|
49
|
-
|
|
50
40
|
function hasIitm (url) {
|
|
51
41
|
// Fast path: avoid URL parsing on the hot path when there's clearly no iitm.
|
|
52
42
|
if (typeof url !== 'string' || url.indexOf('iitm') === -1) {
|
|
@@ -205,52 +195,37 @@ function emitWarning (err) {
|
|
|
205
195
|
* of how the loader is driven, so both the synchronous and asynchronous paths
|
|
206
196
|
* share it.
|
|
207
197
|
*
|
|
198
|
+
* The value is read from `namespaceVar`, the wrapper's namespace binding for the
|
|
199
|
+
* module that *defines* the export. For a module's own exports that is the
|
|
200
|
+
* wrapped module itself; for a name re-exported through `export *` it is the
|
|
201
|
+
* leaf that declares it. Reading from the defining module rather than the
|
|
202
|
+
* aggregating one keeps the value resolvable when the same binding reaches the
|
|
203
|
+
* aggregator through more than one re-export chain — Node sees those chains as
|
|
204
|
+
* distinct wrapper modules and leaves the name ambiguous (hence `undefined`) on
|
|
205
|
+
* the aggregate namespace, while the defining module always holds it (#171).
|
|
206
|
+
*
|
|
208
207
|
* @param {string} n The exported name.
|
|
209
208
|
* @param {string} srcUrl The URL of the module the export belongs to.
|
|
209
|
+
* @param {string} namespaceVar The wrapper binding holding `srcUrl`'s namespace.
|
|
210
210
|
* @returns {string}
|
|
211
211
|
*/
|
|
212
|
-
function buildSetter (n, srcUrl) {
|
|
212
|
+
function buildSetter (n, srcUrl, namespaceVar) {
|
|
213
213
|
const variableName = `$${n.replace(/[^a-zA-Z0-9_$]/g, '_')}`
|
|
214
214
|
const objectKey = JSON.stringify(n)
|
|
215
215
|
const reExportedName = n === 'default' ? n : objectKey
|
|
216
216
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
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' : ''
|
|
217
|
+
// `1` makes __bind fall back to namespace['default'] for the module.exports
|
|
218
|
+
// synthetic export, which builtins don't expose on the native ESM namespace.
|
|
219
|
+
const fallback = n === 'module.exports' ? ', 1' : ''
|
|
223
220
|
|
|
221
|
+
// Builtins don't expose the module.exports synthetic name, so skip its re-export.
|
|
224
222
|
const reExportLine = (n === 'module.exports' && (srcUrl.startsWith('node:') || builtinModules.includes(srcUrl)))
|
|
225
223
|
? ''
|
|
226
224
|
: `export { ${variableName} as ${reExportedName} }`
|
|
227
225
|
|
|
228
|
-
return `
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
`
|
|
226
|
+
return `let ${variableName}
|
|
227
|
+
__bind(${objectKey}, ${namespaceVar}, v => { ${variableName} = v }, () => ${variableName}${fallback})
|
|
228
|
+
${reExportLine}`
|
|
254
229
|
}
|
|
255
230
|
|
|
256
231
|
/**
|
|
@@ -267,37 +242,82 @@ function buildSetter (n, srcUrl) {
|
|
|
267
242
|
* @param {string} params.srcUrl The full URL to the module to process.
|
|
268
243
|
* @param {object} params.context Provided by the loaders API.
|
|
269
244
|
* @param {boolean} [params.excludeDefault = false] Exclude the default export.
|
|
245
|
+
* @param {number} [params.depth = 0] Star-re-export recursion depth. Used to
|
|
246
|
+
* detect `export *` cycles (`a` re-exports `b`, `b` re-exports `a`) cheaply:
|
|
247
|
+
* the acyclic common case pays only an integer compare per level, and the
|
|
248
|
+
* cycle-tracking set is allocated only once recursion is implausibly deep.
|
|
249
|
+
* @param {Set<string>} [params.seen] URLs currently on the recursion stack,
|
|
250
|
+
* created lazily once `depth` crosses {@link STAR_CYCLE_DEPTH}. A URL is added
|
|
251
|
+
* before descending into its subtree and removed once that subtree finishes, so
|
|
252
|
+
* it tracks the active path rather than every URL ever visited.
|
|
253
|
+
* @param {Map<string, string>} [params.originNamespaces] Shared registry mapping
|
|
254
|
+
* a defining-module URL to the wrapper namespace alias a same-origin `export *`
|
|
255
|
+
* collision must read it from. Absent until the first such collision; then
|
|
256
|
+
* threaded through the recursion so one defining module yields one alias and
|
|
257
|
+
* {@link buildWrapperSource} imports each once. Only `*`-collided names use it;
|
|
258
|
+
* every other export reads from the wrapped module's own `namespace`.
|
|
270
259
|
*
|
|
271
|
-
* @returns {Generator<Array, Map<string, string
|
|
272
|
-
* operations and ultimately returns the shimmed
|
|
273
|
-
* from the module and any transitive export all
|
|
260
|
+
* @returns {Generator<Array, { setters: Map<string, string>, origins: (Map<string, string> | undefined), originNamespaces: (Map<string, string> | undefined) }>}
|
|
261
|
+
* A generator that yields I/O operations and ultimately returns the shimmed
|
|
262
|
+
* setters for all the exports from the module and any transitive export all
|
|
263
|
+
* modules. `origins` (the defining module per `*`-sourced name) is `undefined`
|
|
264
|
+
* for a module with no `export *`; `originNamespaces` stays `undefined` unless a
|
|
265
|
+
* same-origin `*` collision actually needed an alias.
|
|
274
266
|
*/
|
|
275
|
-
function * processModule ({ srcUrl, context, excludeDefault = false }) {
|
|
267
|
+
function * processModule ({ srcUrl, context, excludeDefault = false, depth = 0, seen, originNamespaces }) {
|
|
276
268
|
const exportNames = yield * getExports(srcUrl, context)
|
|
277
|
-
const starExports = new Set()
|
|
278
269
|
const setters = new Map()
|
|
279
270
|
|
|
280
|
-
|
|
271
|
+
// Maps each live `*`-sourced name to the module that defined it. Its keys
|
|
272
|
+
// double as "this name came from a `*` re-export" (so an explicit export can
|
|
273
|
+
// override it), and its values let two `*` re-exports of the same name be told
|
|
274
|
+
// apart. Allocated on the first `export *`, never for a module without one; a
|
|
275
|
+
// single Map carries both facts so a star with no collision pays one structure
|
|
276
|
+
// and one write per name, not two.
|
|
277
|
+
let starOrigins
|
|
278
|
+
|
|
279
|
+
// A name pulled in through more than one `export *` chain that all bottom out
|
|
280
|
+
// at the same module stays exported (ECMAScript ResolveExport;
|
|
281
|
+
// tc39/ecma262#3715), but the *aggregate* namespace this wrapper imports drops
|
|
282
|
+
// it: under iitm the chains are distinct wrapped modules, so Node sees the
|
|
283
|
+
// re-export as ambiguous and the name reads back undefined. Only those names
|
|
284
|
+
// must instead read from their defining module's own namespace, which always
|
|
285
|
+
// holds the value. `originNamespaces` maps such a defining module to the alias
|
|
286
|
+
// the wrapper imports for it; it is allocated on the first surviving
|
|
287
|
+
// collision, so a module without one emits no extra import (#171).
|
|
288
|
+
const ensureOriginNamespace = (origin) => {
|
|
289
|
+
originNamespaces ??= new Map()
|
|
290
|
+
let alias = originNamespaces.get(origin)
|
|
291
|
+
if (alias === undefined) {
|
|
292
|
+
alias = `__ns${originNamespaces.size}`
|
|
293
|
+
originNamespaces.set(origin, alias)
|
|
294
|
+
}
|
|
295
|
+
return alias
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const addSetter = (name, setter, isStarExport, origin) => {
|
|
281
299
|
if (setters.has(name)) {
|
|
282
300
|
if (isStarExport) {
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
301
|
+
// `starOrigins.has(name)` means the existing entry also came from a `*`
|
|
302
|
+
// re-export (an explicit export would not be tracked here).
|
|
303
|
+
if (starOrigins.has(name)) {
|
|
304
|
+
if (starOrigins.get(name) === origin) {
|
|
305
|
+
// The same binding reached through two `*` re-export chains. It
|
|
306
|
+
// stays exported, but the aggregate namespace dropped it, so point
|
|
307
|
+
// its setter at the defining module's namespace instead.
|
|
308
|
+
setters.set(name, buildSetter(name, origin, ensureOriginNamespace(origin)))
|
|
309
|
+
} else {
|
|
310
|
+
// Genuinely ambiguous: two `*` re-exports name it from different
|
|
311
|
+
// modules. Per ResolveExport the name is excluded entirely.
|
|
312
|
+
setters.delete(name)
|
|
313
|
+
starOrigins.delete(name)
|
|
314
|
+
}
|
|
286
315
|
}
|
|
287
|
-
//
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// if we already have this export but it is from a * export, overwrite it
|
|
292
|
-
if (starExports.has(name)) {
|
|
293
|
-
starExports.delete(name)
|
|
294
|
-
setters.set(name, setter)
|
|
316
|
+
// An explicit export already shadows the `*` re-export; leave it.
|
|
295
317
|
}
|
|
296
318
|
} else {
|
|
297
|
-
// Store export * exports so we know they can be overridden by explicit
|
|
298
|
-
// named exports
|
|
299
319
|
if (isStarExport) {
|
|
300
|
-
|
|
320
|
+
starOrigins.set(name, origin)
|
|
301
321
|
}
|
|
302
322
|
|
|
303
323
|
setters.set(name, setter)
|
|
@@ -326,21 +346,56 @@ function * processModule ({ srcUrl, context, excludeDefault = false }) {
|
|
|
326
346
|
// parent's `format` to know if this sub-module is ESM or CJS!
|
|
327
347
|
const result = yield [RESOLVE, newSpecifier, { parentURL: srcUrl }]
|
|
328
348
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
349
|
+
// First `*` re-export: allocate the origin bookkeeping lazily.
|
|
350
|
+
starOrigins ??= new Map()
|
|
351
|
+
|
|
352
|
+
// `export *` graphs are normally only a handful of levels deep. A cycle
|
|
353
|
+
// (`a` re-exports `b`, `b` re-exports `a`) instead recurses without bound
|
|
354
|
+
// and exhausts memory. Rather than track every URL on the common shallow
|
|
355
|
+
// path, only start recording once the depth is implausibly large for a
|
|
356
|
+
// real graph; from there a re-export pointing back at a module already on
|
|
357
|
+
// the recursion stack is the cycle, and is skipped (its exports are
|
|
358
|
+
// collected by the in-progress ancestor frame). `seen` mirrors the stack,
|
|
359
|
+
// not every URL visited: a module reached and fully processed through one
|
|
360
|
+
// sibling branch must stay reachable through a later, more direct branch,
|
|
361
|
+
// so it is removed again once its subtree finishes.
|
|
362
|
+
if (depth >= STAR_CYCLE_DEPTH) {
|
|
363
|
+
seen ??= new Set()
|
|
364
|
+
if (seen.has(result.url)) continue
|
|
365
|
+
seen.add(result.url)
|
|
366
|
+
}
|
|
334
367
|
|
|
335
|
-
|
|
336
|
-
|
|
368
|
+
try {
|
|
369
|
+
const sub = yield * processModule({
|
|
370
|
+
srcUrl: result.url,
|
|
371
|
+
context: { ...context, format: result.format },
|
|
372
|
+
excludeDefault: true,
|
|
373
|
+
depth: depth + 1,
|
|
374
|
+
seen,
|
|
375
|
+
originNamespaces
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// Adopt any registry a nested `export *` minted before processing this
|
|
379
|
+
// child's results, so a collision detected here extends the same Map the
|
|
380
|
+
// child's setters already reference (one alias per defining module across
|
|
381
|
+
// the whole tree) rather than orphaning the child's into a second Map.
|
|
382
|
+
originNamespaces ??= sub.originNamespaces
|
|
383
|
+
|
|
384
|
+
// Star targets build their setters against `namespace` like any other
|
|
385
|
+
// module; only a surviving same-origin collision (in addSetter) rewrites
|
|
386
|
+
// the affected name to read from its defining module's alias.
|
|
387
|
+
for (const [name, setter] of sub.setters) {
|
|
388
|
+
addSetter(name, setter, true, sub.origins?.get(name) ?? result.url)
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
seen?.delete(result.url)
|
|
337
392
|
}
|
|
338
393
|
} else {
|
|
339
|
-
addSetter(n, buildSetter(n, srcUrl))
|
|
394
|
+
addSetter(n, buildSetter(n, srcUrl, 'namespace'), false)
|
|
340
395
|
}
|
|
341
396
|
}
|
|
342
397
|
|
|
343
|
-
return setters
|
|
398
|
+
return { setters, origins: starOrigins, originNamespaces }
|
|
344
399
|
}
|
|
345
400
|
|
|
346
401
|
function addIitm (url) {
|
|
@@ -582,11 +637,23 @@ export function createHook (meta) {
|
|
|
582
637
|
// Builds the wrapper module source that re-exports the real module through
|
|
583
638
|
// iitm's proxy. Pure string generation shared by the asynchronous and
|
|
584
639
|
// synchronous `load` paths.
|
|
585
|
-
function buildWrapperSource (realUrl, setters, originalSpecifier) {
|
|
640
|
+
function buildWrapperSource (realUrl, setters, originalSpecifier, originNamespaces) {
|
|
641
|
+
// The wrapped module imports its namespace as `namespace`, which serves
|
|
642
|
+
// every export but the ones a same-origin `export *` collision forced onto
|
|
643
|
+
// their defining module (#171): the aggregate namespace drops those as
|
|
644
|
+
// ambiguous under iitm, so each such defining module gets its own alias the
|
|
645
|
+
// wrapper imports. Absent the registry (no such collision) nothing is added.
|
|
646
|
+
let originImports = ''
|
|
647
|
+
if (originNamespaces !== undefined) {
|
|
648
|
+
for (const [originUrl, alias] of originNamespaces) {
|
|
649
|
+
originImports += `import * as ${alias} from ${JSON.stringify(originUrl)}\n`
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
586
653
|
return `
|
|
587
654
|
import { register } from '${iitmURL}'
|
|
588
655
|
import * as namespace from ${JSON.stringify(realUrl)}
|
|
589
|
-
|
|
656
|
+
${originImports}
|
|
590
657
|
// Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
|
|
591
658
|
const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } })
|
|
592
659
|
const set = {}
|
|
@@ -621,6 +688,31 @@ function __flushPendingOnce () {
|
|
|
621
688
|
__pending = next
|
|
622
689
|
}
|
|
623
690
|
|
|
691
|
+
function __bind (key, source, write, read, useFallback) {
|
|
692
|
+
const readSource = useFallback
|
|
693
|
+
? () => source[key] ?? source['default']
|
|
694
|
+
: () => source[key]
|
|
695
|
+
__overridden[key] = false
|
|
696
|
+
let deferred = false
|
|
697
|
+
try {
|
|
698
|
+
const v = readSource()
|
|
699
|
+
write(v)
|
|
700
|
+
_[key] = v
|
|
701
|
+
} catch (err) {
|
|
702
|
+
if (!(err instanceof ReferenceError)) throw err
|
|
703
|
+
deferred = true
|
|
704
|
+
}
|
|
705
|
+
if (deferred || read() === undefined) {
|
|
706
|
+
__pending.push(__makeUpdater(key, readSource, (v) => { write(v); _[key] = v }))
|
|
707
|
+
}
|
|
708
|
+
set[key] = (v) => {
|
|
709
|
+
__overridden[key] = true
|
|
710
|
+
write(v)
|
|
711
|
+
return true
|
|
712
|
+
}
|
|
713
|
+
get[key] = read
|
|
714
|
+
}
|
|
715
|
+
|
|
624
716
|
${Array.from(setters.values()).join('\n')}
|
|
625
717
|
|
|
626
718
|
if (__pending.length > 0) {
|
|
@@ -658,13 +750,13 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
658
750
|
// succeeds: free the specifier entry early, and remember CJS modules so their
|
|
659
751
|
// transitive require() chain bypasses iitm (see `load`). Returns the wrapper
|
|
660
752
|
// module source.
|
|
661
|
-
function onWrapSuccess (realUrl, context, originalSpecifier, setters) {
|
|
753
|
+
function onWrapSuccess (realUrl, context, originalSpecifier, setters, originNamespaces) {
|
|
662
754
|
specifiers.delete(realUrl)
|
|
663
755
|
// context.format is set to 'commonjs' by getCjsExports during processModule.
|
|
664
756
|
if (context.format === 'commonjs') {
|
|
665
757
|
cjsInIitmChain.add(realUrl)
|
|
666
758
|
}
|
|
667
|
-
return buildWrapperSource(realUrl, setters, originalSpecifier)
|
|
759
|
+
return buildWrapperSource(realUrl, setters, originalSpecifier, originNamespaces)
|
|
668
760
|
}
|
|
669
761
|
|
|
670
762
|
// Bookkeeping shared by the async and sync wrap paths when `processModule`
|
|
@@ -685,11 +777,11 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
685
777
|
const originalSpecifier = specifiers.get(realUrl)
|
|
686
778
|
|
|
687
779
|
try {
|
|
688
|
-
const setters = await driveAsync(
|
|
780
|
+
const { setters, originNamespaces } = await driveAsync(
|
|
689
781
|
processModule({ srcUrl: realUrl, context }),
|
|
690
782
|
{ resolve: cachedResolve, load: parentGetSource }
|
|
691
783
|
)
|
|
692
|
-
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters) }
|
|
784
|
+
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters, originNamespaces) }
|
|
693
785
|
} catch (cause) {
|
|
694
786
|
onWrapFailure(realUrl, cause)
|
|
695
787
|
// Revert back to the non-iitm URL
|
|
@@ -709,11 +801,11 @@ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(originalSpeci
|
|
|
709
801
|
const originalSpecifier = specifiers.get(realUrl)
|
|
710
802
|
|
|
711
803
|
try {
|
|
712
|
-
const setters = driveSync(
|
|
804
|
+
const { setters, originNamespaces } = driveSync(
|
|
713
805
|
processModule({ srcUrl: realUrl, context }),
|
|
714
806
|
{ resolve: cachedResolve, load: nextLoad }
|
|
715
807
|
)
|
|
716
|
-
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters) }
|
|
808
|
+
return { source: onWrapSuccess(realUrl, context, originalSpecifier, setters, originNamespaces) }
|
|
717
809
|
} catch (cause) {
|
|
718
810
|
onWrapFailure(realUrl, cause)
|
|
719
811
|
url = realUrl
|
package/lib/get-esm-exports.mjs
CHANGED
|
@@ -1,116 +1,146 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { readFileSync } from 'fs'
|
|
4
|
+
import { createRequire, Module } from 'module'
|
|
5
|
+
import { initSync, parse as parseWasm } from 'es-module-lexer'
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
// The default es-module-lexer build is WebAssembly backed and decodes string
|
|
10
|
+
// literals (import specifiers, quoted export names) with an internal `eval`.
|
|
11
|
+
// Under `--disallow-code-generation-from-strings` that `eval` silently no-ops,
|
|
12
|
+
// so specifiers and quoted names come back undecoded (`* from undefined`,
|
|
13
|
+
// `"string name"` with the quotes still attached). The eval-free asm.js build
|
|
14
|
+
// (`es-module-lexer/js`) does the same work, so fall back to it when code
|
|
15
|
+
// generation from strings is disallowed. Resolve the parser once at load time
|
|
16
|
+
// so the per-module path stays a bare `parse(source)` call.
|
|
17
|
+
const flag = '--disallow-code-generation-from-strings'
|
|
18
|
+
const disallowCodegen = process.execArgv.includes(flag) ||
|
|
19
|
+
(process.env.NODE_OPTIONS?.includes(flag) ?? false)
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
// initSync compiles the Wasm module up front so `parse` can run inside
|
|
22
|
+
// synchronous loader hooks (`module.registerHooks`) as well as the off-thread
|
|
23
|
+
// loader; it is a one-time cost on the first ESM module either way. It stays the
|
|
24
|
+
// default and remains callable even under the flag, so a parse before the asm.js
|
|
25
|
+
// swap still works (with the flag's known specifier-decoding limitation) rather
|
|
26
|
+
// than throwing.
|
|
27
|
+
initSync()
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
let parse = parseWasm
|
|
30
|
+
|
|
31
|
+
if (disallowCodegen) {
|
|
32
|
+
parse = loadAsmParse()
|
|
15
33
|
}
|
|
16
34
|
|
|
17
35
|
/**
|
|
18
|
-
*
|
|
19
|
-
* exported identifiers. In the baseline case, the list of identifiers will be
|
|
20
|
-
* the simple identifier names as written in the source code of the module.
|
|
21
|
-
* However, there is a special case:
|
|
36
|
+
* Loads the eval-free asm.js parser synchronously, on every Node version.
|
|
22
37
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
38
|
+
* The asm.js build (`es-module-lexer/js`) is ESM-only with no CommonJS variant,
|
|
39
|
+
* and its only export is a top-level `parse` (no async wasm `init` to await).
|
|
40
|
+
* `require`-ing it is not portable: before require(esm) (Node < 20.19 / < 22.12)
|
|
41
|
+
* it throws `ERR_REQUIRE_ESM`, and `import()`-ing it from inside a loader hook
|
|
42
|
+
* deadlocks the loader on itself. Compiling the source as CommonJS sidesteps
|
|
43
|
+
* both: it runs synchronously, keeps this module require()-able for the
|
|
44
|
+
* synchronous `module.registerHooks` path, and never re-enters the ESM loader.
|
|
27
45
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
46
|
+
* `Module._compile` uses V8's script compiler, not `eval`, so it is unaffected
|
|
47
|
+
* by `--disallow-code-generation-from-strings` — the same reason the asm.js
|
|
48
|
+
* build itself decodes specifiers under the flag. The only export is rewritten
|
|
49
|
+
* to a CommonJS one; the source is a vendored build we control.
|
|
31
50
|
*
|
|
32
|
-
* @returns {
|
|
33
|
-
* custom directives.
|
|
51
|
+
* @returns {typeof parseWasm} The asm.js parse function.
|
|
34
52
|
*/
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
parseSpecifiers(node, exportedNames)
|
|
46
|
-
}
|
|
47
|
-
break
|
|
48
|
-
|
|
49
|
-
case 'ExportDefaultDeclaration': {
|
|
50
|
-
exportedNames.add('default')
|
|
51
|
-
break
|
|
52
|
-
}
|
|
53
|
+
function loadAsmParse () {
|
|
54
|
+
const asmPath = require.resolve('es-module-lexer/js')
|
|
55
|
+
const source = readFileSync(asmPath, 'utf8')
|
|
56
|
+
.replace('export function parse', 'function parse') +
|
|
57
|
+
'\nmodule.exports = { parse }\n'
|
|
58
|
+
const mod = new Module(asmPath)
|
|
59
|
+
mod.filename = asmPath
|
|
60
|
+
mod._compile(source, asmPath)
|
|
61
|
+
return mod.exports.parse
|
|
62
|
+
}
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Decodes an exported identifier the way the JS engine would. es-module-lexer
|
|
66
|
+
* leaves Unicode escapes in bare identifier exports (`export const \u0061 = 1`)
|
|
67
|
+
* as their raw spelling, while the module namespace exposes the cooked name
|
|
68
|
+
* (`a`). Quoted export names are already decoded by the lexer and start with a
|
|
69
|
+
* quote in the source, so the cheap quote check keeps them on the fast path;
|
|
70
|
+
* only a bare identifier carrying a backslash needs cooking. A malformed escape
|
|
71
|
+
* falls back to the raw name rather than throwing inside the loader.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} name The export name as reported by the lexer.
|
|
74
|
+
* @returns {string} The cooked export name.
|
|
75
|
+
*/
|
|
76
|
+
function decodeExportName (name) {
|
|
77
|
+
const first = name.charCodeAt(0)
|
|
78
|
+
if (first === 0x22 /* " */ || first === 0x27 /* ' */ || !name.includes('\\')) {
|
|
79
|
+
return name
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(`"${name}"`)
|
|
83
|
+
} catch {
|
|
84
|
+
return name
|
|
64
85
|
}
|
|
65
|
-
return exportedNames
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// es-module-lexer reports a bare `export * from <mod>` only as an import with no
|
|
89
|
+
// matching export entry, indistinguishable from `import <mod>` except by the
|
|
90
|
+
// statement text. This matches that text to rewrite it as the transitive
|
|
91
|
+
// `* from <specifier>` marker the interpreting code recognizes. `export * as ns
|
|
92
|
+
// from` binds a real name and is reported as a normal export, so it must not
|
|
93
|
+
// match here. `GAP` allows whitespace and comments between the tokens, the way
|
|
94
|
+
// the parser does (e.g. `export /* c */ * from`).
|
|
95
|
+
const GAP = '(?:\\s|/\\*[^]*?\\*/|//[^\\n]*\\n)*'
|
|
96
|
+
const STAR_REEXPORT = new RegExp(`^export${GAP}\\*${GAP}from`)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Lexes ESM source code with es-module-lexer and builds a list of exported
|
|
100
|
+
* identifiers. In the baseline case the list is the simple identifier names as
|
|
101
|
+
* written in the source. There is one special case:
|
|
102
|
+
*
|
|
103
|
+
* When an `export * from './foo.js'` line is encountered it is rewritten as
|
|
104
|
+
* `* from ./foo.js`. This lets the interpreting code recognize a transitive
|
|
105
|
+
* export and recursively parse the indicated module. The returned identifier
|
|
106
|
+
* list will have "* from ./foo.js" as an item.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} moduleSource The source code of the module to lex.
|
|
109
|
+
* @returns {Set<string>} The identifiers exported by the module along with any
|
|
110
|
+
* custom directives.
|
|
111
|
+
*/
|
|
112
|
+
export default function getEsmExports (moduleSource) {
|
|
113
|
+
return lexEsm(moduleSource).exportNames
|
|
84
114
|
}
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Lexes ESM source code once and reports both the exported identifiers and
|
|
118
|
+
* whether the source uses ESM syntax. Sharing a single `parse` lets the
|
|
119
|
+
* unknown-format path in `getExports` decide between ESM and CommonJS without a
|
|
120
|
+
* second pass over the source.
|
|
121
|
+
*
|
|
122
|
+
* `hasModuleSyntax` is es-module-lexer's own signal: static `import`/`export`
|
|
123
|
+
* and `import.meta` set it, while a lone dynamic `import(...)` (valid in CJS)
|
|
124
|
+
* does not.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} moduleSource The source code of the module to lex.
|
|
127
|
+
* @returns {{ exportNames: Set<string>, hasModuleSyntax: boolean }}
|
|
128
|
+
*/
|
|
129
|
+
export function lexEsm (moduleSource) {
|
|
130
|
+
const exportNames = new Set()
|
|
131
|
+
const [imports, exports, , hasModuleSyntax] = parse(moduleSource)
|
|
132
|
+
|
|
133
|
+
for (const exported of exports) {
|
|
134
|
+
exportNames.add(decodeExportName(exported.n))
|
|
103
135
|
}
|
|
104
|
-
}
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
exportedNames.add(specifier.exported.value)
|
|
112
|
-
} else {
|
|
113
|
-
warn('unrecognized specifier type: ' + specifier.exported.type)
|
|
137
|
+
// Bare `export * from <mod>` re-exports report no export name; reconstruct
|
|
138
|
+
// the transitive marker from the import statement that carries the specifier.
|
|
139
|
+
for (const imported of imports) {
|
|
140
|
+
if (STAR_REEXPORT.test(moduleSource.slice(imported.ss, imported.se))) {
|
|
141
|
+
exportNames.add(`* from ${imported.n}`)
|
|
114
142
|
}
|
|
115
143
|
}
|
|
144
|
+
|
|
145
|
+
return { exportNames, hasModuleSyntax }
|
|
116
146
|
}
|
package/lib/get-exports.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { lexEsm } from './get-esm-exports.mjs'
|
|
4
4
|
import { parse as parseCjs, initSync } from 'cjs-module-lexer'
|
|
5
5
|
import { readFileSync, existsSync } from 'fs'
|
|
6
6
|
import { builtinModules, createRequire } from 'module'
|
|
@@ -11,6 +11,11 @@ import { LOAD } from './io.mjs'
|
|
|
11
11
|
const nodeMajor = Number(process.versions.node.split('.')[0])
|
|
12
12
|
export const hasModuleExportsCJSDefault = nodeMajor >= 23
|
|
13
13
|
|
|
14
|
+
// Resolve `stripTypeScriptTypes` (Node >= 22.13) via `getBuiltinModule` rather
|
|
15
|
+
// than a static named import (throws on older runtimes) or `require` (re-enters
|
|
16
|
+
// iitm's own loader hooks). `undefined` on runtimes that lack it.
|
|
17
|
+
const stripTypeScriptTypes = process.getBuiltinModule?.('module')?.stripTypeScriptTypes
|
|
18
|
+
|
|
14
19
|
let parserInitialized = false
|
|
15
20
|
|
|
16
21
|
// The CJS export scanner is backed by WebAssembly. `initSync` compiles it
|
|
@@ -28,127 +33,6 @@ function addDefault (arr) {
|
|
|
28
33
|
return new Set(['default', ...arr])
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
function hasEsmSyntax (source) {
|
|
32
|
-
// Lightweight scan (no full parse) to determine if the *source code*
|
|
33
|
-
// contains ESM-specific syntax. This is used only when:
|
|
34
|
-
// - the loader chain didn't tell us a `format`, and
|
|
35
|
-
// - `getEsmExports()` found no exports.
|
|
36
|
-
//
|
|
37
|
-
// Notes:
|
|
38
|
-
// - We ignore comments and strings to reduce false positives.
|
|
39
|
-
// - We treat `import.meta` and static `import ...` as ESM.
|
|
40
|
-
// - We do NOT treat `import(` (dynamic import) as ESM because it is allowed
|
|
41
|
-
// in CJS as an expression.
|
|
42
|
-
if (source.indexOf('import') === -1) return false
|
|
43
|
-
|
|
44
|
-
const isIdentCharCode = (code) => (
|
|
45
|
-
(code >= 48 && code <= 57) || // 0-9
|
|
46
|
-
(code >= 65 && code <= 90) || // A-Z
|
|
47
|
-
(code >= 97 && code <= 122) || // a-z
|
|
48
|
-
code === 95 || // _
|
|
49
|
-
code === 36 // $
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
const skipWhitespace = (idx) => {
|
|
53
|
-
while (idx < source.length) {
|
|
54
|
-
const c = source.charCodeAt(idx)
|
|
55
|
-
// space, tab, cr, lf
|
|
56
|
-
if (c !== 32 && c !== 9 && c !== 13 && c !== 10) break
|
|
57
|
-
idx++
|
|
58
|
-
}
|
|
59
|
-
return idx
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let i = 0
|
|
63
|
-
while (i < source.length) {
|
|
64
|
-
const ch = source[i]
|
|
65
|
-
|
|
66
|
-
// Line comment
|
|
67
|
-
if (ch === '/' && source[i + 1] === '/') {
|
|
68
|
-
i += 2
|
|
69
|
-
while (i < source.length && source[i] !== '\n') i++
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Block comment
|
|
74
|
-
if (ch === '/' && source[i + 1] === '*') {
|
|
75
|
-
i += 2
|
|
76
|
-
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++
|
|
77
|
-
i += 2
|
|
78
|
-
continue
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Strings: '...' or "..."
|
|
82
|
-
if (ch === '\'' || ch === '"') {
|
|
83
|
-
const quote = ch
|
|
84
|
-
i++
|
|
85
|
-
while (i < source.length) {
|
|
86
|
-
const c = source[i]
|
|
87
|
-
if (c === '\\') {
|
|
88
|
-
i += 2
|
|
89
|
-
continue
|
|
90
|
-
}
|
|
91
|
-
if (c === quote) {
|
|
92
|
-
i++
|
|
93
|
-
break
|
|
94
|
-
}
|
|
95
|
-
i++
|
|
96
|
-
}
|
|
97
|
-
continue
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Template strings: `...`
|
|
101
|
-
if (ch === '`') {
|
|
102
|
-
i++
|
|
103
|
-
while (i < source.length) {
|
|
104
|
-
const c = source[i]
|
|
105
|
-
if (c === '\\') {
|
|
106
|
-
i += 2
|
|
107
|
-
continue
|
|
108
|
-
}
|
|
109
|
-
if (c === '`') {
|
|
110
|
-
i++
|
|
111
|
-
break
|
|
112
|
-
}
|
|
113
|
-
i++
|
|
114
|
-
}
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Keyword scan (word-boundary): import
|
|
119
|
-
if (ch === 'i') {
|
|
120
|
-
const prev = source.charCodeAt(i - 1)
|
|
121
|
-
if (i > 0 && isIdentCharCode(prev)) {
|
|
122
|
-
i++
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (source.startsWith('import', i)) {
|
|
127
|
-
const next = source.charCodeAt(i + 6)
|
|
128
|
-
if (isIdentCharCode(next)) {
|
|
129
|
-
i++
|
|
130
|
-
continue
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const j = skipWhitespace(i + 6)
|
|
134
|
-
// `import.meta` is ESM-only
|
|
135
|
-
if (source[j] === '.') return true
|
|
136
|
-
// `import(` is dynamic import, allowed in CJS
|
|
137
|
-
if (source[j] === '(') {
|
|
138
|
-
i = j + 1
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
// Otherwise assume it's a static import form
|
|
142
|
-
return true
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
i++
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return false
|
|
150
|
-
}
|
|
151
|
-
|
|
152
36
|
// Cached exports for Node built-in modules
|
|
153
37
|
const BUILT_INS = new Map()
|
|
154
38
|
|
|
@@ -362,28 +246,36 @@ export function * getExports (url, context) {
|
|
|
362
246
|
}
|
|
363
247
|
|
|
364
248
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
249
|
+
// Node hands the load hook the original TypeScript source, so strip the
|
|
250
|
+
// types before the JS parsers run, then treat the module as the JS it
|
|
251
|
+
// compiles to. Early type-stripping releases tag the format but lack the
|
|
252
|
+
// API; the un-stripped parse then fails cleanly into onWrapFailure.
|
|
253
|
+
let moduleFormat = format
|
|
254
|
+
if (format === 'module-typescript' || format === 'commonjs-typescript') {
|
|
255
|
+
if (stripTypeScriptTypes !== undefined) {
|
|
256
|
+
source = stripTypeScriptTypes(source, { mode: 'strip' })
|
|
257
|
+
}
|
|
258
|
+
moduleFormat = format === 'module-typescript' ? 'module' : 'commonjs'
|
|
367
259
|
}
|
|
368
260
|
|
|
369
|
-
if (
|
|
261
|
+
if (moduleFormat === 'commonjs') {
|
|
370
262
|
return yield * getCjsExports(url, context, source)
|
|
371
263
|
}
|
|
372
264
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
265
|
+
const { exportNames, hasModuleSyntax } = lexEsm(source)
|
|
266
|
+
|
|
267
|
+
if (moduleFormat === 'module') {
|
|
268
|
+
return exportNames
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// At this point our `format` is either undefined or not known by us. When
|
|
272
|
+
// there are no exports and no ESM syntax, fall back to CommonJS detection.
|
|
273
|
+
// Strong evidence of ESM (static import/import.meta) keeps the empty ESM
|
|
274
|
+
// export set rather than incorrectly treating the module as CJS.
|
|
275
|
+
if (!exportNames.size && !hasModuleSyntax) {
|
|
276
|
+
return yield * getCjsExports(url, context, source)
|
|
385
277
|
}
|
|
386
|
-
return
|
|
278
|
+
return exportNames
|
|
387
279
|
} catch (cause) {
|
|
388
280
|
const err = new Error(`Failed to parse '${url}'`)
|
|
389
281
|
err.cause = cause
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "import-in-the-middle",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Intercept imports in Node.js",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -59,9 +59,8 @@
|
|
|
59
59
|
"vue": "^3.5.26"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"acorn": "^8.15.0",
|
|
63
|
-
"acorn-import-attributes": "^1.9.5",
|
|
64
62
|
"cjs-module-lexer": "^2.2.0",
|
|
63
|
+
"es-module-lexer": "^2.2.0",
|
|
65
64
|
"module-details-from-path": "^1.0.4"
|
|
66
65
|
}
|
|
67
66
|
}
|
package/register-hooks.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export type RegisterHooksOptions = {
|
|
|
17
17
|
* synchronous hooks run on the application thread, so `Hook()` registrations are
|
|
18
18
|
* visible to the loader directly and no acknowledgement step is required.
|
|
19
19
|
*
|
|
20
|
-
* Requires a Node.js version
|
|
20
|
+
* Requires a Node.js version where {@link supportsSyncHooks} is `true`
|
|
21
|
+
* (>= 22.22.3, >= 24.11.1, >= 25.1.0, or >= 26.0.0).
|
|
21
22
|
*
|
|
22
23
|
* ```ts
|
|
23
24
|
* import { register } from 'import-in-the-middle/register-hooks.mjs'
|
|
@@ -30,6 +31,14 @@ export type RegisterHooksOptions = {
|
|
|
30
31
|
* })
|
|
31
32
|
* ```
|
|
32
33
|
*
|
|
33
|
-
* @throws If
|
|
34
|
+
* @throws If {@link supportsSyncHooks} is `false` in the running Node.js.
|
|
34
35
|
*/
|
|
35
36
|
export declare function register(options?: RegisterHooksOptions): void
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether the running Node.js can correctly run the synchronous loader via
|
|
40
|
+
* `module.registerHooks()`. `false` on versions that ship `module.registerHooks()`
|
|
41
|
+
* but predate the nullish-CommonJS-`source` fix (nodejs/node#59929); branch on it
|
|
42
|
+
* to fall back to the asynchronous `module.register()` loader.
|
|
43
|
+
*/
|
|
44
|
+
export declare function supportsSyncHooks(): boolean
|
package/register-hooks.mjs
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License.
|
|
2
|
+
//
|
|
3
|
+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
|
|
4
|
+
|
|
5
|
+
// This module intentionally has no imports. `supportsSyncHooks` is a pure
|
|
6
|
+
// Node.js version check, and consumers (e.g. a CommonJS preloader deciding
|
|
7
|
+
// whether to require the ESM loader at all) need to call it without pulling in
|
|
8
|
+
// `create-hook.mjs` and its acorn / cjs-module-lexer dependency graph.
|
|
9
|
+
|
|
10
|
+
// `process.versions.node` is always "major.minor.patch" (nightlies append a
|
|
11
|
+
// "-suffix" that parseInt stops at). Only release lines 22/24/25 need the minor
|
|
12
|
+
// and patch, so parse the major eagerly and read those lazily.
|
|
13
|
+
const version = process.versions.node
|
|
14
|
+
const NODE_MAJOR = parseInt(version, 10)
|
|
15
|
+
let NODE_MINOR
|
|
16
|
+
let NODE_PATCH
|
|
17
|
+
|
|
18
|
+
function readMinorAndPatch () {
|
|
19
|
+
const firstDot = version.indexOf('.')
|
|
20
|
+
const secondDot = version.indexOf('.', firstDot + 1)
|
|
21
|
+
NODE_MINOR = parseInt(version.slice(firstDot + 1, secondDot), 10)
|
|
22
|
+
NODE_PATCH = parseInt(version.slice(secondDot + 1), 10)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the running Node.js can correctly run the synchronous loader via
|
|
27
|
+
* `module.registerHooks`.
|
|
28
|
+
*
|
|
29
|
+
* `module.registerHooks` exists since v22.15, but its synchronous load hook
|
|
30
|
+
* rejected the nullish CommonJS `source` the loader returns for `require()`s
|
|
31
|
+
* pulled into the ESM graph (throwing `ERR_INVALID_RETURN_PROPERTY_VALUE`) until
|
|
32
|
+
* https://github.com/nodejs/node/pull/59929, released in 22.22.3, 24.11.1,
|
|
33
|
+
* 25.1.0 and 26.0.0. Earlier 24.x (<= 24.11.0) and 25.0.0 ship `registerHooks`
|
|
34
|
+
* but predate the fix, so the synchronous loader must fall back to the
|
|
35
|
+
* asynchronous one there.
|
|
36
|
+
*
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
export function supportsSyncHooks () {
|
|
40
|
+
if (NODE_MAJOR >= 26) return true
|
|
41
|
+
if (NODE_MAJOR < 22 || NODE_MAJOR === 23) return false
|
|
42
|
+
|
|
43
|
+
readMinorAndPatch()
|
|
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
|
+
return NODE_MINOR > 22 || (NODE_MINOR === 22 && NODE_PATCH >= 3)
|
|
47
|
+
}
|