import-in-the-middle 1.8.0 → 1.8.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/hook.js CHANGED
@@ -129,15 +129,32 @@ function isBareSpecifier (specifier) {
129
129
  */
130
130
  async function processModule ({ srcUrl, context, parentGetSource, parentResolve, excludeDefault }) {
131
131
  const exportNames = await getExports(srcUrl, context, parentGetSource)
132
- const duplicates = new Set()
132
+ const starExports = new Set()
133
133
  const setters = new Map()
134
134
 
135
- const addSetter = (name, setter) => {
136
- // When doing an `import *` duplicates become undefined, so do the same
135
+ const addSetter = (name, setter, isStarExport = false) => {
137
136
  if (setters.has(name)) {
138
- duplicates.add(name)
139
- setters.delete(name)
140
- } else if (!duplicates.has(name)) {
137
+ if (isStarExport) {
138
+ // If there's already a matching star export, delete it
139
+ if (starExports.has(name)) {
140
+ setters.delete(name)
141
+ }
142
+ // and return so this is excluded
143
+ return
144
+ }
145
+
146
+ // if we already have this export but it is from a * export, overwrite it
147
+ if (starExports.has(name)) {
148
+ starExports.delete(name)
149
+ setters.set(name, setter)
150
+ }
151
+ } else {
152
+ // Store export * exports so we know they can be overridden by explicit
153
+ // named exports
154
+ if (isStarExport) {
155
+ starExports.add(name)
156
+ }
157
+
141
158
  setters.set(name, setter)
142
159
  }
143
160
  }
@@ -161,10 +178,11 @@ async function processModule ({ srcUrl, context, parentGetSource, parentResolve,
161
178
  srcUrl: modUrl,
162
179
  context,
163
180
  parentGetSource,
181
+ parentResolve,
164
182
  excludeDefault: true
165
183
  })
166
184
  for (const [name, setter] of setters.entries()) {
167
- addSetter(name, setter)
185
+ addSetter(name, setter, true)
168
186
  }
169
187
  } else {
170
188
  addSetter(n, `
@@ -246,14 +264,16 @@ function createHook (meta) {
246
264
  async function getSource (url, context, parentGetSource) {
247
265
  if (hasIitm(url)) {
248
266
  const realUrl = deleteIitm(url)
249
- const setters = await processModule({
250
- srcUrl: realUrl,
251
- context,
252
- parentGetSource,
253
- parentResolve: cachedResolve
254
- })
255
- return {
256
- source: `
267
+
268
+ try {
269
+ const setters = await processModule({
270
+ srcUrl: realUrl,
271
+ context,
272
+ parentGetSource,
273
+ parentResolve: cachedResolve
274
+ })
275
+ return {
276
+ source: `
257
277
  import { register } from '${iitmURL}'
258
278
  import * as namespace from ${JSON.stringify(realUrl)}
259
279
 
@@ -268,6 +288,25 @@ ${Array.from(setters.values()).join('\n')}
268
288
 
269
289
  register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(realUrl))})
270
290
  `
291
+ }
292
+ } catch (cause) {
293
+ // If there are other ESM loader hooks registered as well as iitm,
294
+ // depending on the order they are registered, source might not be
295
+ // JavaScript.
296
+ //
297
+ // If we fail to parse a module for exports, we should fall back to the
298
+ // parent loader. These modules will not be wrapped with proxies and
299
+ // cannot be Hook'ed but at least this does not take down the entire app
300
+ // and block iitm from being used.
301
+ //
302
+ // We log the error because there might be bugs in iitm and without this
303
+ // it would be very tricky to debug
304
+ const err = new Error(`'import-in-the-middle' failed to wrap '${realUrl}'`)
305
+ err.cause = cause
306
+ console.warn(err)
307
+
308
+ // Revert back to the non-iitm URL
309
+ url = realUrl
271
310
  }
272
311
  }
273
312
 
package/index.d.ts CHANGED
@@ -27,7 +27,7 @@ export type Options = {
27
27
  internals?: boolean
28
28
  }
29
29
 
30
- declare class Hook {
30
+ export declare class Hook {
31
31
  /**
32
32
  * Creates a hook to be run on any already loaded modules and any that will
33
33
  * be loaded in the future. It will be run once per loaded module. If
@@ -29,7 +29,7 @@ function warn (txt) {
29
29
  * @param {string} params.moduleSource The source code of the module to parse
30
30
  * and interpret.
31
31
  *
32
- * @returns {string[]} The identifiers exported by the module along with any
32
+ * @returns {Set<string>} The identifiers exported by the module along with any
33
33
  * custom directives.
34
34
  */
35
35
  function getEsmExports (moduleSource) {
@@ -62,7 +62,7 @@ function getEsmExports (moduleSource) {
62
62
  warn('unrecognized export type: ' + node.type)
63
63
  }
64
64
  }
65
- return Array.from(exportedNames)
65
+ return exportedNames
66
66
  }
67
67
 
68
68
  function parseDeclaration (node, exportedNames) {
@@ -1,36 +1,60 @@
1
1
  'use strict'
2
2
 
3
3
  const getEsmExports = require('./get-esm-exports.js')
4
- const { parse: getCjsExports } = require('cjs-module-lexer')
5
- const fs = require('fs')
4
+ const { parse: parseCjs } = require('cjs-module-lexer')
5
+ const { readFileSync } = require('fs')
6
+ const { builtinModules } = require('module')
6
7
  const { fileURLToPath, pathToFileURL } = require('url')
8
+ const { dirname } = require('path')
7
9
 
8
10
  function addDefault (arr) {
9
- return Array.from(new Set(['default', ...arr]))
11
+ return new Set(['default', ...arr])
12
+ }
13
+
14
+ // Cached exports for Node built-in modules
15
+ const BUILT_INS = new Map()
16
+
17
+ function getExportsForNodeBuiltIn (name) {
18
+ let exports = BUILT_INS.get()
19
+
20
+ if (!exports) {
21
+ exports = new Set(addDefault(Object.keys(require(name))))
22
+ BUILT_INS.set(name, exports)
23
+ }
24
+
25
+ return exports
10
26
  }
11
27
 
12
28
  const urlsBeingProcessed = new Set() // Guard against circular imports.
13
29
 
14
- async function getFullCjsExports (url, context, parentLoad, source) {
30
+ async function getCjsExports (url, context, parentLoad, source) {
15
31
  if (urlsBeingProcessed.has(url)) {
16
32
  return []
17
33
  }
18
34
  urlsBeingProcessed.add(url)
19
35
 
20
- const ex = getCjsExports(source)
21
- const full = Array.from(new Set([
22
- ...addDefault(ex.exports),
23
- ...(await Promise.all(ex.reexports.map(re => getExports(
24
- (/^(..?($|\/|\\))/).test(re)
25
- ? pathToFileURL(require.resolve(fileURLToPath(new URL(re, url)))).toString()
26
- : pathToFileURL(require.resolve(re)).toString(),
27
- context,
28
- parentLoad
29
- )))).flat()
30
- ]))
31
-
32
- urlsBeingProcessed.delete(url)
33
- return full
36
+ try {
37
+ const result = parseCjs(source)
38
+ const full = addDefault(result.exports)
39
+
40
+ await Promise.all(result.reexports.map(async re => {
41
+ if (re.startsWith('node:') || builtinModules.includes(re)) {
42
+ for (const each of getExportsForNodeBuiltIn(re)) {
43
+ full.add(each)
44
+ }
45
+ } else {
46
+ // Resolve the re-exported module relative to the current module.
47
+ const newUrl = pathToFileURL(require.resolve(re, { paths: [dirname(fileURLToPath(url))] })).href
48
+ for (const each of await getExports(newUrl, context, parentLoad)) {
49
+ full.add(each)
50
+ }
51
+ }
52
+ }))
53
+
54
+ return full
55
+ } finally {
56
+ urlsBeingProcessed.delete(url)
57
+ }
34
58
  }
35
59
 
36
60
  /**
@@ -45,7 +69,7 @@ async function getFullCjsExports (url, context, parentLoad, source) {
45
69
  * @param {Function} parentLoad Next hook function in the loaders API
46
70
  * hook chain.
47
71
  *
48
- * @returns {Promise<string[]>} An array of identifiers exported by the module.
72
+ * @returns {Promise<Set<string>>} An array of identifiers exported by the module.
49
73
  * Please see {@link getEsmExports} for caveats on special identifiers that may
50
74
  * be included in the result set.
51
75
  */
@@ -57,23 +81,23 @@ async function getExports (url, context, parentLoad) {
57
81
  let source = parentCtx.source
58
82
  const format = parentCtx.format
59
83
 
60
- // TODO support non-node/file urls somehow?
61
- if (format === 'builtin') {
62
- // Builtins don't give us the source property, so we're stuck
63
- // just requiring it to get the exports.
64
- return addDefault(Object.keys(require(url)))
65
- }
66
-
67
84
  if (!source) {
68
- // Sometimes source is retrieved by parentLoad, sometimes it isn't.
69
- source = fs.readFileSync(fileURLToPath(url), 'utf8')
85
+ if (format === 'builtin') {
86
+ // Builtins don't give us the source property, so we're stuck
87
+ // just requiring it to get the exports.
88
+ return getExportsForNodeBuiltIn(url)
89
+ }
90
+
91
+ // Sometimes source is retrieved by parentLoad, CommonJs isn't.
92
+ source = readFileSync(fileURLToPath(url), 'utf8')
70
93
  }
71
94
 
72
95
  if (format === 'module') {
73
96
  return getEsmExports(source)
74
97
  }
98
+
75
99
  if (format === 'commonjs') {
76
- return getFullCjsExports(url, context, parentLoad, source)
100
+ return getCjsExports(url, context, parentLoad, source)
77
101
  }
78
102
 
79
103
  // At this point our `format` is either undefined or not known by us. Fall
@@ -84,7 +108,7 @@ async function getExports (url, context, parentLoad) {
84
108
  // isn't set at first and yet we have an ESM module with no exports.
85
109
  // I couldn't construct an example that would do this, so maybe it's
86
110
  // impossible?
87
- return getFullCjsExports(url, context, parentLoad, source)
111
+ return getCjsExports(url, context, parentLoad, source)
88
112
  }
89
113
  }
90
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1 +1 @@
1
- export const foo = 'a'
1
+ export const foo = 'b'
@@ -0,0 +1 @@
1
+ export const foo = 'c'
@@ -0,0 +1,5 @@
1
+ export * from './duplicate-a.mjs'
2
+ export * from './duplicate-b.mjs'
3
+ export { foo } from './duplicate-c.mjs'
4
+ export * from './duplicate-a.mjs'
5
+ export * from './duplicate-b.mjs'
@@ -0,0 +1,5 @@
1
+ import coolFile from './something.json' with { type: 'json' }
2
+
3
+ export default {
4
+ data: coolFile.data
5
+ }
@@ -0,0 +1 @@
1
+ module.exports = require('util').deprecate
@@ -0,0 +1 @@
1
+ module.exports = require('some-external-cjs-module').foo
@@ -15,7 +15,7 @@ fixture.split('\n').forEach(line => {
15
15
  if (expectedNames[0] === '') {
16
16
  expectedNames.length = 0
17
17
  }
18
- const names = getEsmExports(mod)
18
+ const names = Array.from(getEsmExports(mod))
19
19
  assert.deepEqual(expectedNames, names)
20
20
  console.log(`${mod}\n ✅ contains exports: ${testStr}`)
21
21
  })
@@ -0,0 +1,13 @@
1
+ import * as lib from '../fixtures/duplicate-explicit.mjs'
2
+ import { strictEqual } from 'assert'
3
+ import Hook from '../../index.js'
4
+
5
+ Hook((exports, name) => {
6
+ if (name.endsWith('duplicate-explicit.mjs')) {
7
+ strictEqual(exports.foo, 'c')
8
+ exports.foo += '-wrapped'
9
+ }
10
+ })
11
+
12
+ // foo should not be exported because there are duplicate exports
13
+ strictEqual(lib.foo, 'c-wrapped')
@@ -4,8 +4,8 @@ import Hook from '../../index.js'
4
4
 
5
5
  Hook((exports, name) => {
6
6
  if (name.match(/duplicate\.mjs/)) {
7
- // foo should not be exported because there are duplicate exports
8
- strictEqual(exports.foo, undefined)
7
+ // foo should not be exported because there are duplicate * exports
8
+ strictEqual('foo' in exports, false)
9
9
  // default should be exported
10
10
  strictEqual(exports.default, 'foo')
11
11
  }
@@ -14,6 +14,6 @@ Hook((exports, name) => {
14
14
  notEqual(lib, undefined)
15
15
 
16
16
  // foo should not be exported because there are duplicate exports
17
- strictEqual(lib.foo, undefined)
17
+ strictEqual('foo' in lib, false)
18
18
  // default should be exported
19
19
  strictEqual(lib.default, 'foo')
@@ -0,0 +1,19 @@
1
+ import Hook from '../../index.js'
2
+ import foo from '../fixtures/re-export-cjs-built-in.js'
3
+ import foo2 from '../fixtures/re-export-cjs.js'
4
+ import { strictEqual } from 'assert'
5
+
6
+ Hook((exports, name) => {
7
+ if (name.endsWith('fixtures/re-export-cjs-built-in.js')) {
8
+ strictEqual(typeof exports.default, 'function')
9
+ exports.default = '1'
10
+ }
11
+
12
+ if (name.endsWith('fixtures/re-export-cjs.js')) {
13
+ strictEqual(exports.default, 'bar')
14
+ exports.default = '2'
15
+ }
16
+ })
17
+
18
+ strictEqual(foo, '1')
19
+ strictEqual(foo2, '2')
@@ -8,3 +8,9 @@ Hook((exports, name) => {
8
8
  })
9
9
 
10
10
  console.assert(OpenAI)
11
+
12
+ const openAI = new OpenAI({
13
+ apiKey: 'doesnt-matter'
14
+ })
15
+
16
+ console.assert(openAI)
@@ -0,0 +1,17 @@
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
+ import jsonMjs from '../fixtures/json-attributes.mjs'
6
+ import { strictEqual } from 'assert'
7
+
8
+ // Acorn does not support import attributes so an error is logged but the import
9
+ // still works!
10
+ //
11
+ // Hook((exports, name) => {
12
+ // if (name.match(/json\.mjs/)) {
13
+ // exports.default.data += '-dawg'
14
+ // }
15
+ // })
16
+
17
+ strictEqual(jsonMjs.data, 'dog')
@@ -0,0 +1,8 @@
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
+ (async () => {
6
+ const lib = await import('../fixtures/executable')
7
+ console.assert(lib)
8
+ })()
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict'
2
- import { addHook } from '../../index.js'
2
+ import defaultHook, { Hook, addHook } from '../../index.js'
3
3
  import { sayHi } from '../fixtures/say-hi.mjs'
4
4
 
5
5
  addHook((url, exported) => {
@@ -8,4 +8,12 @@ addHook((url, exported) => {
8
8
  }
9
9
  })
10
10
 
11
- assert.equal(sayHi('test'), 'Hooked')
11
+ new defaultHook((exported: any, name: string, baseDir: string|void) => {
12
+
13
+ });
14
+
15
+ new Hook((exported: any, name: string, baseDir: string|void) => {
16
+
17
+ });
18
+
19
+ assert.equal(sayHi('test'), 'Hooked')
@@ -1,19 +0,0 @@
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
- import { rejects } from 'assert'
6
- (async () => {
7
- const [processMajor, processMinor] = process.versions.node.split('.').map(Number)
8
- const extensionlessSupported = processMajor >= 21 ||
9
- (processMajor === 20 && processMinor >= 10) ||
10
- (processMajor === 18 && processMinor >= 19)
11
- if (extensionlessSupported) {
12
- // Files without extension are supported in Node.js ^21, ^20.10.0, and ^18.19.0
13
- return
14
- }
15
- await rejects(() => import('./executable'), {
16
- name: 'TypeError',
17
- code: 'ERR_UNKNOWN_FILE_EXTENSION'
18
- })
19
- })()
File without changes