import-in-the-middle 1.10.0 → 1.11.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +44 -6
- package/hook.js +42 -2
- package/index.d.ts +36 -0
- package/index.js +75 -0
- package/package.json +1 -1
- package/test/fixtures/import-after.mjs +16 -0
- package/test/fixtures/import.mjs +23 -0
- package/test/register/v18.19-include-builtin.mjs +20 -0
- package/test/register/v18.19-include-message-port.mjs +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.0](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.10.0...import-in-the-middle-v1.11.0) (2024-07-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Optionally only wrap modules hooked in `--import` ([#146](https://github.com/nodejs/import-in-the-middle/issues/146)) ([71c8d7b](https://github.com/nodejs/import-in-the-middle/commit/71c8d7bac512df94566d12c96fc2e438b4de2e2a))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* `node:` prefixed build-in modules with `include`/`exclude` ([#149](https://github.com/nodejs/import-in-the-middle/issues/149)) ([736a944](https://github.com/nodejs/import-in-the-middle/commit/736a9446e209bc8649801a27cb431df663551dc5))
|
|
14
|
+
|
|
3
15
|
## [1.10.0](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.9.1...import-in-the-middle-v1.10.0) (2024-07-22)
|
|
4
16
|
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -34,14 +34,14 @@ console.log(foo) // 1 more than whatever that module exported
|
|
|
34
34
|
This requires the use of an ESM loader hook, which can be added with the following
|
|
35
35
|
command-line option.
|
|
36
36
|
|
|
37
|
-
```
|
|
38
|
-
--loader=import-in-the-middle/hook.mjs
|
|
37
|
+
```shell
|
|
38
|
+
node --loader=import-in-the-middle/hook.mjs my-app.mjs
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Since `--loader` has been deprecated you can also register the loader hook programmatically via the Node
|
|
42
42
|
[`module.register()`](https://nodejs.org/api/module.html#moduleregisterspecifier-parenturl-options)
|
|
43
43
|
API. However, for this to be able to hook non-dynamic imports, it needs to be
|
|
44
|
-
|
|
44
|
+
registered before your app code is evaluated via the `--import` command-line option.
|
|
45
45
|
|
|
46
46
|
`my-loader.mjs`
|
|
47
47
|
```js
|
|
@@ -54,9 +54,12 @@ node --import=./my-loader.mjs ./my-code.mjs
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
When registering the loader hook programmatically, it's possible to pass a list
|
|
57
|
-
of modules, file URLs or regular expressions to either exclude or specifically
|
|
58
|
-
include which modules are intercepted. This is useful if a module is not
|
|
57
|
+
of modules, file URLs or regular expressions to either `exclude` or specifically
|
|
58
|
+
`include` which modules are intercepted. This is useful if a module is not
|
|
59
59
|
compatible with the loader hook.
|
|
60
|
+
|
|
61
|
+
> **Note:** This feature is incompatible with the `{internals: true}` Hook option
|
|
62
|
+
|
|
60
63
|
```js
|
|
61
64
|
import * as module from 'module'
|
|
62
65
|
|
|
@@ -71,6 +74,41 @@ module.register('import-in-the-middle/hook.mjs', import.meta.url, {
|
|
|
71
74
|
})
|
|
72
75
|
```
|
|
73
76
|
|
|
77
|
+
### Only Intercepting Hooked modules
|
|
78
|
+
> **Note:** This feature is experimental and is incompatible with the `{internals: true}` Hook option
|
|
79
|
+
|
|
80
|
+
If you are `Hook`'ing all modules before they are imported, for example in a
|
|
81
|
+
module loaded via the Node.js `--import` CLI argument, you can configure the
|
|
82
|
+
loader to intercept only modules that were specifically hooked.
|
|
83
|
+
|
|
84
|
+
`instrument.mjs`
|
|
85
|
+
```js
|
|
86
|
+
import { register } from 'module'
|
|
87
|
+
import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'
|
|
88
|
+
|
|
89
|
+
const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()
|
|
90
|
+
|
|
91
|
+
register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions)
|
|
92
|
+
|
|
93
|
+
Hook(['fs'], (exported, name, baseDir) => {
|
|
94
|
+
// Instrument the fs module
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Ensure that the loader has acknowledged all the modules
|
|
98
|
+
// before we allow execution to continue
|
|
99
|
+
await waitForAllMessagesAcknowledged()
|
|
100
|
+
```
|
|
101
|
+
`my-app.mjs`
|
|
102
|
+
```js
|
|
103
|
+
import * as fs from 'fs'
|
|
104
|
+
// fs will be instrumented!
|
|
105
|
+
fs.readFileSync('file.txt')
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```shell
|
|
109
|
+
node --import=./instrument.mjs ./my-app.mjs
|
|
110
|
+
```
|
|
111
|
+
|
|
74
112
|
## Limitations
|
|
75
113
|
|
|
76
114
|
* You cannot add new exports to a module. You can only modify existing ones.
|
package/hook.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { URL } = require('url')
|
|
6
6
|
const { inspect } = require('util')
|
|
7
|
+
const { builtinModules } = require('module')
|
|
7
8
|
const specifiers = new Map()
|
|
8
9
|
const isWin = process.platform === 'win32'
|
|
9
10
|
|
|
@@ -116,6 +117,11 @@ function isBareSpecifier (specifier) {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Determines whether the input is a bare specifier, file URL or a regular expression.
|
|
122
|
+
*
|
|
123
|
+
* - node: prefixed URL strings are considered bare specifiers in this context.
|
|
124
|
+
*/
|
|
119
125
|
function isBareSpecifierFileUrlOrRegex (input) {
|
|
120
126
|
if (input instanceof RegExp) {
|
|
121
127
|
return true
|
|
@@ -131,13 +137,21 @@ function isBareSpecifierFileUrlOrRegex (input) {
|
|
|
131
137
|
try {
|
|
132
138
|
// eslint-disable-next-line no-new
|
|
133
139
|
const url = new URL(input)
|
|
134
|
-
|
|
140
|
+
// We consider node: URLs bare specifiers in this context
|
|
141
|
+
return url.protocol === 'file:' || url.protocol === 'node:'
|
|
135
142
|
} catch (err) {
|
|
136
143
|
// Anything that fails parsing is a bare specifier
|
|
137
144
|
return true
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Ensure an array only contains bare specifiers, file URLs or regular expressions.
|
|
150
|
+
*
|
|
151
|
+
* - We consider node: prefixed URL string as bare specifiers in this context.
|
|
152
|
+
* - For node built-in modules, we add additional node: prefixed modules to the
|
|
153
|
+
* output array.
|
|
154
|
+
*/
|
|
141
155
|
function ensureArrayWithBareSpecifiersFileUrlsAndRegex (array, type) {
|
|
142
156
|
if (!Array.isArray(array)) {
|
|
143
157
|
return undefined
|
|
@@ -149,6 +163,14 @@ function ensureArrayWithBareSpecifiersFileUrlsAndRegex (array, type) {
|
|
|
149
163
|
throw new Error(`'${type}' option only supports bare specifiers, file URLs or regular expressions. Invalid entries: ${inspect(invalid)}`)
|
|
150
164
|
}
|
|
151
165
|
|
|
166
|
+
// Rather than evaluate whether we have a node: scoped built-in-module for
|
|
167
|
+
// every call to resolve, we just add them to include/exclude now.
|
|
168
|
+
for (const each of array) {
|
|
169
|
+
if (typeof each === 'string' && !each.startsWith('node:') && builtinModules.includes(each)) {
|
|
170
|
+
array.push(`node:${each}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
152
174
|
return array
|
|
153
175
|
}
|
|
154
176
|
|
|
@@ -259,13 +281,31 @@ function createHook (meta) {
|
|
|
259
281
|
if (data) {
|
|
260
282
|
includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
|
|
261
283
|
excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
|
|
284
|
+
|
|
285
|
+
if (data.addHookMessagePort) {
|
|
286
|
+
data.addHookMessagePort.on('message', (modules) => {
|
|
287
|
+
if (includeModules === undefined) {
|
|
288
|
+
includeModules = []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const each of modules) {
|
|
292
|
+
if (!each.startsWith('node:') && builtinModules.includes(each)) {
|
|
293
|
+
includeModules.push(`node:${each}`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
includeModules.push(each)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
data.addHookMessagePort.postMessage('ack')
|
|
300
|
+
}).unref()
|
|
301
|
+
}
|
|
262
302
|
}
|
|
263
303
|
}
|
|
264
304
|
|
|
265
305
|
async function resolve (specifier, context, parentResolve) {
|
|
266
306
|
cachedResolve = parentResolve
|
|
267
307
|
|
|
268
|
-
// See github.com/nodejs/import-in-the-middle/pull/76.
|
|
308
|
+
// See https://github.com/nodejs/import-in-the-middle/pull/76.
|
|
269
309
|
if (specifier === iitmURL) {
|
|
270
310
|
return {
|
|
271
311
|
url: specifier,
|
package/index.d.ts
CHANGED
|
@@ -84,3 +84,39 @@ export declare function addHook(hookFn: HookFunction): void
|
|
|
84
84
|
* @param {HookFunction} hookFn The function to be removed.
|
|
85
85
|
*/
|
|
86
86
|
export declare function removeHook(hookFn: HookFunction): void
|
|
87
|
+
|
|
88
|
+
type CreateAddHookMessageChannelReturn<Data> = {
|
|
89
|
+
addHookMessagePort: MessagePort,
|
|
90
|
+
waitForAllMessagesAcknowledged: Promise<void>
|
|
91
|
+
registerOptions: { data?: Data; transferList?: any[]; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* EXPERIMENTAL
|
|
96
|
+
* This feature is experimental and may change in minor versions.
|
|
97
|
+
* **NOTE** This feature is incompatible with the {internals: true} Hook option.
|
|
98
|
+
*
|
|
99
|
+
* Creates a message channel with a port that can be used to add hooks to the
|
|
100
|
+
* list of exclusively included modules.
|
|
101
|
+
*
|
|
102
|
+
* This can be used to only wrap modules that are Hook'ed, however modules need
|
|
103
|
+
* to be hooked before they are imported.
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { register } from 'module'
|
|
107
|
+
* import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'
|
|
108
|
+
*
|
|
109
|
+
* const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()
|
|
110
|
+
*
|
|
111
|
+
* register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions)
|
|
112
|
+
*
|
|
113
|
+
* Hook(['fs'], (exported, name, baseDir) => {
|
|
114
|
+
* // Instrument the fs module
|
|
115
|
+
* })
|
|
116
|
+
*
|
|
117
|
+
* // Ensure that the loader has acknowledged all the modules
|
|
118
|
+
* // before we allow execution to continue
|
|
119
|
+
* await waitForAllMessagesAcknowledged()
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export declare function createAddHookMessageChannel<Data = any>(): CreateAddHookMessageChannelReturn<Data>;
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const parse = require('module-details-from-path')
|
|
7
7
|
const { fileURLToPath } = require('url')
|
|
8
|
+
const { MessageChannel } = require('worker_threads')
|
|
8
9
|
|
|
9
10
|
const {
|
|
10
11
|
importHooks,
|
|
@@ -31,6 +32,75 @@ function callHookFn (hookFn, namespace, name, baseDir) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
let sendModulesToLoader
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* EXPERIMENTAL
|
|
39
|
+
* This feature is experimental and may change in minor versions.
|
|
40
|
+
* **NOTE** This feature is incompatible with the {internals: true} Hook option.
|
|
41
|
+
*
|
|
42
|
+
* Creates a message channel with a port that can be used to add hooks to the
|
|
43
|
+
* list of exclusively included modules.
|
|
44
|
+
*
|
|
45
|
+
* This can be used to only wrap modules that are Hook'ed, however modules need
|
|
46
|
+
* to be hooked before they are imported.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { register } from 'module'
|
|
50
|
+
* import { Hook, createAddHookMessageChannel } from 'import-in-the-middle'
|
|
51
|
+
*
|
|
52
|
+
* const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()
|
|
53
|
+
*
|
|
54
|
+
* register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions)
|
|
55
|
+
*
|
|
56
|
+
* Hook(['fs'], (exported, name, baseDir) => {
|
|
57
|
+
* // Instrument the fs module
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* // Ensure that the loader has acknowledged all the modules
|
|
61
|
+
* // before we allow execution to continue
|
|
62
|
+
* await waitForAllMessagesAcknowledged()
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
function createAddHookMessageChannel () {
|
|
66
|
+
const { port1, port2 } = new MessageChannel()
|
|
67
|
+
let pendingAckCount = 0
|
|
68
|
+
let resolveFn
|
|
69
|
+
|
|
70
|
+
sendModulesToLoader = (modules) => {
|
|
71
|
+
pendingAckCount++
|
|
72
|
+
port1.postMessage(modules)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
port1.on('message', () => {
|
|
76
|
+
pendingAckCount--
|
|
77
|
+
|
|
78
|
+
if (resolveFn && pendingAckCount <= 0) {
|
|
79
|
+
resolveFn()
|
|
80
|
+
}
|
|
81
|
+
}).unref()
|
|
82
|
+
|
|
83
|
+
function waitForAllMessagesAcknowledged () {
|
|
84
|
+
// This timer is to prevent the process from exiting with code 13:
|
|
85
|
+
// 13: Unsettled Top-Level Await.
|
|
86
|
+
const timer = setInterval(() => { }, 1000)
|
|
87
|
+
const promise = new Promise((resolve) => {
|
|
88
|
+
resolveFn = resolve
|
|
89
|
+
}).then(() => { clearInterval(timer) })
|
|
90
|
+
|
|
91
|
+
if (pendingAckCount === 0) {
|
|
92
|
+
resolveFn()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return promise
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const addHookMessagePort = port2
|
|
99
|
+
const registerOptions = { data: { addHookMessagePort, include: [] }, transferList: [addHookMessagePort] }
|
|
100
|
+
|
|
101
|
+
return { registerOptions, addHookMessagePort, waitForAllMessagesAcknowledged }
|
|
102
|
+
}
|
|
103
|
+
|
|
34
104
|
function Hook (modules, options, hookFn) {
|
|
35
105
|
if ((this instanceof Hook) === false) return new Hook(modules, options, hookFn)
|
|
36
106
|
if (typeof modules === 'function') {
|
|
@@ -43,6 +113,10 @@ function Hook (modules, options, hookFn) {
|
|
|
43
113
|
}
|
|
44
114
|
const internals = options ? options.internals === true : false
|
|
45
115
|
|
|
116
|
+
if (sendModulesToLoader && Array.isArray(modules)) {
|
|
117
|
+
sendModulesToLoader(modules)
|
|
118
|
+
}
|
|
119
|
+
|
|
46
120
|
this._iitmHook = (name, namespace) => {
|
|
47
121
|
const filename = name
|
|
48
122
|
const isBuiltin = name.startsWith('node:')
|
|
@@ -92,3 +166,4 @@ module.exports = Hook
|
|
|
92
166
|
module.exports.Hook = Hook
|
|
93
167
|
module.exports.addHook = addHook
|
|
94
168
|
module.exports.removeHook = removeHook
|
|
169
|
+
module.exports.createAddHookMessageChannel = createAddHookMessageChannel
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { strictEqual } from 'assert'
|
|
2
|
+
import { sep } from 'path'
|
|
3
|
+
import * as os from 'node:os'
|
|
4
|
+
import { Hook } from '../../index.js'
|
|
5
|
+
|
|
6
|
+
const hooked = []
|
|
7
|
+
|
|
8
|
+
Hook((_, name) => {
|
|
9
|
+
hooked.push(name)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
strictEqual(hooked.length, 2)
|
|
13
|
+
strictEqual(hooked[0], 'path')
|
|
14
|
+
strictEqual(hooked[1], 'os')
|
|
15
|
+
strictEqual(sep, '@')
|
|
16
|
+
strictEqual(os.arch(), 'new_crazy_arch')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { register } from 'module'
|
|
2
|
+
import { Hook, createAddHookMessageChannel } from '../../index.js'
|
|
3
|
+
// We've imported path here to ensure that the hook is still applied later even
|
|
4
|
+
// if the library is used here.
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
|
|
7
|
+
const { registerOptions, waitForAllMessagesAcknowledged } = createAddHookMessageChannel()
|
|
8
|
+
|
|
9
|
+
register('../../hook.mjs', import.meta.url, registerOptions)
|
|
10
|
+
|
|
11
|
+
Hook(['path'], (exports) => {
|
|
12
|
+
exports.sep = '@'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
Hook(['os'], (exports) => {
|
|
16
|
+
exports.arch = function () {
|
|
17
|
+
return 'new_crazy_arch'
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
console.assert(path.sep !== '@')
|
|
22
|
+
|
|
23
|
+
await waitForAllMessagesAcknowledged()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { register } from 'module'
|
|
2
|
+
import Hook from '../../index.js'
|
|
3
|
+
import { strictEqual } from 'assert'
|
|
4
|
+
|
|
5
|
+
register('../../hook.mjs', import.meta.url, { data: { include: ['node:util', 'os'] } })
|
|
6
|
+
|
|
7
|
+
const hooked = []
|
|
8
|
+
|
|
9
|
+
Hook((exports, name) => {
|
|
10
|
+
hooked.push(name)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
await import('util')
|
|
14
|
+
await import('node:os')
|
|
15
|
+
await import('fs')
|
|
16
|
+
await import('path')
|
|
17
|
+
|
|
18
|
+
strictEqual(hooked.length, 2)
|
|
19
|
+
strictEqual(hooked[0], 'util')
|
|
20
|
+
strictEqual(hooked[1], 'os')
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
const out = spawnSync(process.execPath,
|
|
4
|
+
['--import', './test/fixtures/import.mjs', './test/fixtures/import-after.mjs'],
|
|
5
|
+
{ stdio: 'inherit', env: {} }
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
if (out.error) {
|
|
9
|
+
console.error(out.error)
|
|
10
|
+
}
|
|
11
|
+
if (out.status !== 0) {
|
|
12
|
+
console.error(`Expected exit code 0, got ${out.status}`)
|
|
13
|
+
}
|
|
14
|
+
process.exit(out.status)
|