import-in-the-middle 1.10.0 → 1.11.1

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/.eslintrc.yaml CHANGED
@@ -21,3 +21,6 @@ ignorePatterns:
21
21
  - test/fixtures/circular-a.js
22
22
  - test/fixtures/circular-b.js
23
23
  - test/fixtures/reexport.js
24
+ - test/fixtures/duplicate-explicit.mjs
25
+ - test/fixtures/duplicate.mjs
26
+ - test/fixtures/export-types/default-call-expression-renamed.mjs
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.10.0"
2
+ ".": "1.11.1"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.11.1](https://github.com/nodejs/import-in-the-middle/compare/import-in-the-middle-v1.11.0...import-in-the-middle-v1.11.1) (2024-09-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * Support Hooking multiple times ([#153](https://github.com/nodejs/import-in-the-middle/issues/153)) ([e0d8080](https://github.com/nodejs/import-in-the-middle/commit/e0d808041eff228f4b4519454f7eea8f0930238a))
9
+
10
+ ## [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)
11
+
12
+
13
+ ### Features
14
+
15
+ * 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))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * `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))
21
+
3
22
  ## [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
23
 
5
24
 
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
- It's also possible to register the loader hook programmatically via the Node
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
- loaded before your app code is evaluated via the `--import` command-line option.
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
- return url.protocol === 'file:'
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
 
@@ -237,6 +259,7 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
237
259
  $${n} = v
238
260
  return true
239
261
  }
262
+ get.${n} = () => $${n}
240
263
  `)
241
264
  }
242
265
  }
@@ -259,13 +282,31 @@ function createHook (meta) {
259
282
  if (data) {
260
283
  includeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.include, 'include')
261
284
  excludeModules = ensureArrayWithBareSpecifiersFileUrlsAndRegex(data.exclude, 'exclude')
285
+
286
+ if (data.addHookMessagePort) {
287
+ data.addHookMessagePort.on('message', (modules) => {
288
+ if (includeModules === undefined) {
289
+ includeModules = []
290
+ }
291
+
292
+ for (const each of modules) {
293
+ if (!each.startsWith('node:') && builtinModules.includes(each)) {
294
+ includeModules.push(`node:${each}`)
295
+ }
296
+
297
+ includeModules.push(each)
298
+ }
299
+
300
+ data.addHookMessagePort.postMessage('ack')
301
+ }).unref()
302
+ }
262
303
  }
263
304
  }
264
305
 
265
306
  async function resolve (specifier, context, parentResolve) {
266
307
  cachedResolve = parentResolve
267
308
 
268
- // See github.com/nodejs/import-in-the-middle/pull/76.
309
+ // See https://github.com/nodejs/import-in-the-middle/pull/76.
269
310
  if (specifier === iitmURL) {
270
311
  return {
271
312
  url: specifier,
@@ -362,10 +403,11 @@ const _ = Object.assign(
362
403
  namespace
363
404
  )
364
405
  const set = {}
406
+ const get = {}
365
407
 
366
408
  ${Array.from(setters.values()).join('\n')}
367
409
 
368
- register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(realUrl))})
410
+ register(${JSON.stringify(realUrl)}, _, set, get, ${JSON.stringify(specifiers.get(realUrl))})
369
411
  `
370
412
  }
371
413
  } catch (cause) {
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/lib/register.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const importHooks = [] // TODO should this be a Set?
6
6
  const setters = new WeakMap()
7
+ const getters = new WeakMap()
7
8
  const specifiers = new Map()
8
9
  const toHook = []
9
10
 
@@ -12,6 +13,13 @@ const proxyHandler = {
12
13
  return setters.get(target)[name](value)
13
14
  },
14
15
 
16
+ get (target, name) {
17
+ if (name === Symbol.toStringTag) {
18
+ return 'Module'
19
+ }
20
+ return getters.get(target)[name]()
21
+ },
22
+
15
23
  defineProperty (target, property, descriptor) {
16
24
  if ((!('value' in descriptor))) {
17
25
  throw new Error('Getters/setters are not supported for exports property descriptors.')
@@ -21,9 +29,10 @@ const proxyHandler = {
21
29
  }
22
30
  }
23
31
 
24
- function register (name, namespace, set, specifier) {
32
+ function register (name, namespace, set, get, specifier) {
25
33
  specifiers.set(name, specifier)
26
34
  setters.set(namespace, set)
35
+ getters.set(namespace, get)
27
36
  const proxy = new Proxy(namespace, proxyHandler)
28
37
  importHooks.forEach(hook => hook(name, proxy))
29
38
  toHook.push([name, proxy])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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,24 @@
1
+ import { fileURLToPath } from 'url'
2
+ import { join } from 'path'
3
+ import { strictEqual } from 'assert'
4
+ import Hook from '../../index.js'
5
+
6
+ const toWrap = join(fileURLToPath(import.meta.url), '..', '..', 'fixtures', 'foo.mjs')
7
+
8
+ Hook([toWrap], (exports) => {
9
+ const original = exports.foo
10
+ exports.foo = function foo () {
11
+ return original() + '-first'
12
+ }
13
+ })
14
+
15
+ Hook([toWrap], (exports) => {
16
+ const original = exports.foo
17
+ exports.foo = function foo () {
18
+ return original() + '-second'
19
+ }
20
+ })
21
+
22
+ const { foo } = await import('../fixtures/foo.mjs')
23
+
24
+ strictEqual(foo(), 'foo-first-second')
@@ -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)