import-in-the-middle 1.7.4 → 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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/hook.js +139 -155
  3. package/index.d.ts +1 -1
  4. package/index.js +1 -0
  5. package/lib/get-esm-exports.js +6 -27
  6. package/lib/get-exports.js +60 -43
  7. package/package.json +5 -1
  8. package/test/README.md +1 -1
  9. package/test/fixtures/cyclical-self.mjs +8 -0
  10. package/test/fixtures/duplicate-a.mjs +4 -0
  11. package/test/fixtures/duplicate-b.mjs +1 -0
  12. package/test/fixtures/duplicate-c.mjs +1 -0
  13. package/test/fixtures/duplicate-explicit.mjs +5 -0
  14. package/test/fixtures/duplicate.mjs +5 -0
  15. package/test/fixtures/export-types/default-call-expression-renamed.mjs +2 -0
  16. package/test/fixtures/export-types/default-call-expression.mjs +1 -0
  17. package/test/fixtures/got-alike.mjs +0 -6
  18. package/test/fixtures/json-attributes.mjs +5 -0
  19. package/test/fixtures/re-export-cjs-built-in.js +1 -0
  20. package/test/fixtures/re-export-cjs.js +1 -0
  21. package/test/fixtures/re-export-star-external.mjs +1 -0
  22. package/test/fixtures/sub-directory/re-export-star-external.mjs +1 -0
  23. package/test/get-esm-exports/v20-get-esm-exports.js +1 -1
  24. package/test/hook/cyclical-self.mjs +4 -0
  25. package/test/hook/default-export.mjs +8 -0
  26. package/test/hook/duplicate-exports-explicit.mjs +13 -0
  27. package/test/hook/duplicate-exports.mjs +19 -0
  28. package/test/hook/dynamic-import.js +1 -1
  29. package/test/hook/re-export-cjs.mjs +19 -0
  30. package/test/hook/re-export-star-module.mjs +17 -0
  31. package/test/hook/static-import-default.mjs +1 -1
  32. package/test/hook/v14-date-fns.mjs +10 -0
  33. package/test/hook/v14-react-email-components.mjs +16 -0
  34. package/test/hook/v16-got.mjs +16 -0
  35. package/test/hook/v18.19-openai.mjs +16 -0
  36. package/test/hook/v18.19-static-import-gotalike.mjs +4 -13
  37. package/test/hook/v20.10-static-import-attributes.mjs +17 -0
  38. package/test/multiple-loaders/multiple-loaders.test.mjs +6 -0
  39. package/test/multiple-loaders/typescript-loader.mjs +26 -0
  40. package/test/other/v20.10-import-executable.mjs +8 -0
  41. package/test/typescript/ts-node.test.mts +10 -2
  42. package/test/version-check.js +7 -1
  43. package/fix.patch +0 -52
  44. package/test/fixtures/default-class.mjs +0 -3
  45. package/test/fixtures/snake_case.mjs +0 -2
  46. package/test/other/import-executable.mjs +0 -19
  47. /package/test/{other → fixtures}/executable +0 -0
  48. /package/test/hook/{v18-static-import-assert.mjs → v18-v21-static-import-assert.mjs} +0 -0
package/README.md CHANGED
@@ -18,7 +18,7 @@ imported in ESM files, regardless of whether they're imported statically or
18
18
  dynamically.
19
19
 
20
20
  ```js
21
- import Hook from 'import-in-the-middle'
21
+ import { Hook } from 'import-in-the-middle'
22
22
  import { foo } from 'package-i-want-to-modify'
23
23
 
24
24
  console.log(foo) // whatever that module exported
package/hook.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
4
 
5
- const { randomBytes } = require('crypto')
5
+ const { URL } = require('url')
6
6
  const specifiers = new Map()
7
7
  const isWin = process.platform === 'win32'
8
8
 
@@ -19,7 +19,7 @@ let getExports
19
19
  if (NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)) {
20
20
  getExports = require('./lib/get-exports.js')
21
21
  } else {
22
- getExports = ({ url }) => import(url).then(Object.keys)
22
+ getExports = (url) => import(url).then(Object.keys)
23
23
  }
24
24
 
25
25
  function hasIitm (url) {
@@ -91,135 +91,112 @@ function isStarExportLine (line) {
91
91
  return /^\* from /.test(line)
92
92
  }
93
93
 
94
- /**
95
- * @typedef {object} ProcessedModule
96
- * @property {string[]} imports A set of ESM import lines to be added to the
97
- * shimmed module source.
98
- * @property {string[]} namespaces A set of identifiers representing the
99
- * modules in `imports`, e.g. for `import * as foo from 'bar'`, "foo" will be
100
- * present in this array.
101
- * @property {Map<string, string>} setters The shimmed setters for all the
102
- * exports from the module and any transitive export all modules. The key is
103
- * used to deduplicate conflicting exports, assigning a priority to `default`
104
- * exports.
105
- */
94
+ function isBareSpecifier (specifier) {
95
+ // Relative and absolute paths are not bare specifiers.
96
+ if (
97
+ specifier.startsWith('.') ||
98
+ specifier.startsWith('/')) {
99
+ return false
100
+ }
101
+
102
+ // Valid URLs are not bare specifiers. (file:, http:, node:, etc.)
103
+
104
+ // eslint-disable-next-line no-prototype-builtins
105
+ if (URL.hasOwnProperty('canParse')) {
106
+ return !URL.canParse(specifier)
107
+ }
108
+
109
+ try {
110
+ // eslint-disable-next-line no-new
111
+ new URL(specifier)
112
+ return false
113
+ } catch (err) {
114
+ return true
115
+ }
116
+ }
106
117
 
107
118
  /**
108
- * Processes a module's exports and builds a set of new import statements,
109
- * namespace names, and setter blocks. If an export all export if encountered,
110
- * the target exports will be hoisted to the current module via a generated
111
- * namespace.
119
+ * Processes a module's exports and builds a set of setter blocks.
112
120
  *
113
121
  * @param {object} params
114
122
  * @param {string} params.srcUrl The full URL to the module to process.
115
123
  * @param {object} params.context Provided by the loaders API.
116
- * @param {Function} params.parentGetSource Provides the source code for the
117
- * parent module.
118
- * @param {string} [params.ns='namespace'] A string identifier that will be
119
- * used as the namespace for the identifiers exported by the module.
120
- * @param {string} [params.defaultAs='default'] The name to give the default
121
- * identifier exported by the module (if one exists). This is really only
122
- * useful in a recursive situation where a transitive module's default export
123
- * needs to be renamed to the name of the module.
124
+ * @param {Function} params.parentGetSource Provides the source code for the parent module.
125
+ * @param {bool} params.excludeDefault Exclude the default export.
124
126
  *
125
- * @returns {Promise<ProcessedModule>}
127
+ * @returns {Promise<Map<string, string>>} The shimmed setters for all the exports
128
+ * from the module and any transitive export all modules.
126
129
  */
127
- async function processModule ({
128
- srcUrl,
129
- context,
130
- parentGetSource,
131
- ns = 'namespace',
132
- defaultAs = 'default'
133
- }) {
134
- const exportNames = await getExports({
135
- url: srcUrl,
136
- context,
137
- parentLoad: parentGetSource,
138
- defaultAs
139
- })
140
- const imports = [`import * as ${ns} from ${JSON.stringify(srcUrl)}`]
141
- const namespaces = [ns]
142
-
143
- // As we iterate found module exports we will add setter code blocks
144
- // to this map that will eventually be inserted into the shim module's
145
- // source code. We utilize a map in order to prevent duplicate exports.
146
- // As a consequence of default renaming, it is possible that a file named
147
- // `foo.mjs` which has `export function foo() {}` and `export default foo`
148
- // exports will result in the "foo" export being defined twice in our shim.
149
- // The map allows us to avoid this situation at the cost of losing the
150
- // named export in favor of the default export.
130
+ async function processModule ({ srcUrl, context, parentGetSource, parentResolve, excludeDefault }) {
131
+ const exportNames = await getExports(srcUrl, context, parentGetSource)
132
+ const starExports = new Set()
151
133
  const setters = new Map()
152
134
 
135
+ const addSetter = (name, setter, isStarExport = false) => {
136
+ if (setters.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
+
158
+ setters.set(name, setter)
159
+ }
160
+ }
161
+
153
162
  for (const n of exportNames) {
163
+ if (n === 'default' && excludeDefault) continue
164
+
154
165
  if (isStarExportLine(n) === true) {
155
166
  const [, modFile] = n.split('* from ')
156
- const normalizedModName = normalizeModName(modFile)
157
- const modUrl = new URL(modFile, srcUrl).toString()
158
- const modName = Buffer.from(modFile, 'hex') + Date.now() + randomBytes(4).toString('hex')
159
167
 
160
- const data = await processModule({
168
+ let modUrl
169
+ if (isBareSpecifier(modFile)) {
170
+ // Bare specifiers need to be resolved relative to the parent module.
171
+ const result = await parentResolve(modFile, { parentURL: srcUrl })
172
+ modUrl = result.url
173
+ } else {
174
+ modUrl = new URL(modFile, srcUrl).href
175
+ }
176
+
177
+ const setters = await processModule({
161
178
  srcUrl: modUrl,
162
179
  context,
163
180
  parentGetSource,
164
- ns: `$${modName}`,
165
- defaultAs: normalizedModName
181
+ parentResolve,
182
+ excludeDefault: true
166
183
  })
167
- Array.prototype.push.apply(imports, data.imports)
168
- Array.prototype.push.apply(namespaces, data.namespaces)
169
- for (const [k, v] of data.setters.entries()) {
170
- setters.set(k, v)
184
+ for (const [name, setter] of setters.entries()) {
185
+ addSetter(name, setter, true)
171
186
  }
172
-
173
- continue
174
- }
175
-
176
- const matches = /^rename (.+) as (.+)$/.exec(n)
177
- if (matches !== null) {
178
- // Transitive modules that export a default identifier need to have
179
- // that identifier renamed to the name of module. And our shim setter
180
- // needs to utilize that new name while being initialized from the
181
- // corresponding origin namespace.
182
- const renamedExport = matches[2]
183
- setters.set(`$${renamedExport}${ns}`, `
184
- let $${renamedExport} = ${ns}.default
185
- export { $${renamedExport} as ${renamedExport} }
186
- set.${renamedExport} = (v) => {
187
- $${renamedExport} = v
187
+ } else {
188
+ addSetter(n, `
189
+ let $${n} = _.${n}
190
+ export { $${n} as ${n} }
191
+ set.${n} = (v) => {
192
+ $${n} = v
188
193
  return true
189
194
  }
190
195
  `)
191
- continue
192
196
  }
193
-
194
- setters.set(`$${n}` + ns, `
195
- let $${n} = ${ns}.${n}
196
- export { $${n} as ${n} }
197
- set.${n} = (v) => {
198
- $${n} = v
199
- return true
200
- }
201
- `)
202
197
  }
203
198
 
204
- return { imports, namespaces, setters }
205
- }
206
-
207
- /**
208
- * Given a module name, e.g. 'foo-bar' or './foo-bar.js', normalize it to a
209
- * string that is a valid JavaScript identifier, e.g. `fooBar`. Normalization
210
- * means converting kebab-case to camelCase while removing any path tokens and
211
- * file extensions.
212
- *
213
- * @param {string} name The module name to normalize.
214
- *
215
- * @returns {string} The normalized identifier.
216
- */
217
- function normalizeModName (name) {
218
- return name
219
- .split('/')
220
- .pop()
221
- .replace(/(.+)\.(?:js|mjs)$/, '$1')
222
- .replaceAll(/(-.)/g, x => x[1].toUpperCase())
199
+ return setters
223
200
  }
224
201
 
225
202
  function addIitm (url) {
@@ -229,7 +206,20 @@ function addIitm (url) {
229
206
  }
230
207
 
231
208
  function createHook (meta) {
209
+ let cachedResolve
210
+ const iitmURL = new URL('lib/register.js', meta.url).toString()
211
+
232
212
  async function resolve (specifier, context, parentResolve) {
213
+ cachedResolve = parentResolve
214
+
215
+ // See github.com/DataDog/import-in-the-middle/pull/76.
216
+ if (specifier === iitmURL) {
217
+ return {
218
+ url: specifier,
219
+ shortCircuit: true
220
+ }
221
+ }
222
+
233
223
  const { parentURL = '' } = context
234
224
  const newSpecifier = deleteIitm(specifier)
235
225
  if (isWin && parentURL.indexOf('file:node') === 0) {
@@ -253,6 +243,15 @@ function createHook (meta) {
253
243
  return url
254
244
  }
255
245
 
246
+ // If the file is referencing itself, we need to skip adding the iitm search params
247
+ if (url.url === parentURL) {
248
+ return {
249
+ url: url.url,
250
+ shortCircuit: true,
251
+ format: url.format
252
+ }
253
+ }
254
+
256
255
  specifiers.set(url.url, specifier)
257
256
 
258
257
  return {
@@ -262,67 +261,52 @@ function createHook (meta) {
262
261
  }
263
262
  }
264
263
 
265
- const iitmURL = new URL('lib/register.js', meta.url).toString()
266
264
  async function getSource (url, context, parentGetSource) {
267
265
  if (hasIitm(url)) {
268
266
  const realUrl = deleteIitm(url)
269
- const { imports, namespaces, setters: mapSetters } = await processModule({
270
- srcUrl: realUrl,
271
- context,
272
- parentGetSource
273
- })
274
- const setters = Array.from(mapSetters.values())
275
-
276
- // When we encounter modules that re-export all identifiers from other
277
- // modules, it is possible that the transitive modules export a default
278
- // identifier. Due to us having to merge all transitive modules into a
279
- // single common namespace, we need to recognize these default exports
280
- // and remap them to a name based on the module name. This prevents us
281
- // from overriding the top-level module's (the one actually being imported
282
- // by some source code) default export when we merge the namespaces.
283
- const renamedDefaults = setters
284
- .map(s => {
285
- const matches = /let \$(.+) = (\$.+)\.default/.exec(s)
286
- if (matches === null) return undefined
287
- return `_['${matches[1]}'] = ${matches[2]}.default`
267
+
268
+ try {
269
+ const setters = await processModule({
270
+ srcUrl: realUrl,
271
+ context,
272
+ parentGetSource,
273
+ parentResolve: cachedResolve
288
274
  })
289
- .filter(s => s)
290
-
291
- // The for loops are how we merge namespaces into a common namespace that
292
- // can be proxied. We can't use a simple `Object.assign` style merging
293
- // because transitive modules can export a default identifier that would
294
- // override the desired default identifier. So we need to do manual
295
- // merging with some logic around default identifiers.
296
- //
297
- // Additionally, we need to make sure any renamed default exports in
298
- // transitive dependencies are added to the common namespace. This is
299
- // accomplished through the `renamedDefaults` array.
300
- return {
301
- source: `
275
+ return {
276
+ source: `
302
277
  import { register } from '${iitmURL}'
303
- ${imports.join('\n')}
278
+ import * as namespace from ${JSON.stringify(realUrl)}
304
279
 
305
- const namespaces = [${namespaces.join(', ')}]
306
280
  // Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
307
- const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } })
281
+ const _ = Object.assign(
282
+ Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }),
283
+ namespace
284
+ )
308
285
  const set = {}
309
286
 
310
- const primary = namespaces.shift()
311
- for (const [k, v] of Object.entries(primary)) {
312
- _[k] = v
313
- }
314
- for (const ns of namespaces) {
315
- for (const [k, v] of Object.entries(ns)) {
316
- if (k === 'default') continue
317
- _[k] = v
318
- }
319
- }
320
-
321
- ${setters.join('\n')}
322
- ${renamedDefaults.join('\n')}
287
+ ${Array.from(setters.values()).join('\n')}
323
288
 
324
289
  register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(realUrl))})
325
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
326
310
  }
327
311
  }
328
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
package/index.js CHANGED
@@ -89,5 +89,6 @@ Hook.prototype.unhook = function () {
89
89
  }
90
90
 
91
91
  module.exports = Hook
92
+ module.exports.Hook = Hook
92
93
  module.exports.addHook = addHook
93
94
  module.exports.removeHook = removeHook
@@ -18,30 +18,21 @@ function warn (txt) {
18
18
  * Utilizes an AST parser to interpret ESM source code and build a list of
19
19
  * exported identifiers. In the baseline case, the list of identifiers will be
20
20
  * the simple identifier names as written in the source code of the module.
21
- * However, there are some special cases:
21
+ * However, there is a special case:
22
22
  *
23
- * 1. When an `export * from './foo.js'` line is encountered it is rewritten
23
+ * When an `export * from './foo.js'` line is encountered it is rewritten
24
24
  * as `* from ./foo.js`. This allows the interpreting code to recognize a
25
25
  * transitive export and recursively parse the indicated module. The returned
26
26
  * identifier list will have "* from ./foo.js" as an item.
27
27
  *
28
- * 2. When `defaultAs` has a value other than 'default', the export line will
29
- * be rewritten as `rename <identifier> as <defaultAsValue>`. This rename string
30
- * will be an item in the returned identifier list.
31
- *
32
28
  * @param {object} params
33
29
  * @param {string} params.moduleSource The source code of the module to parse
34
30
  * and interpret.
35
- * @param {string} [defaultAs='default'] When anything other than 'default' any
36
- * `export default` lines will be rewritten utilizing the value provided. For
37
- * example, if a module 'foo-bar.js' has the line `export default foo` and the
38
- * value of this parameter is 'baz', then the export will be rewritten to
39
- * `rename foo as baz`.
40
31
  *
41
- * @returns {string[]} The identifiers exported by the module along with any
32
+ * @returns {Set<string>} The identifiers exported by the module along with any
42
33
  * custom directives.
43
34
  */
44
- function getEsmExports ({ moduleSource, defaultAs = 'default' }) {
35
+ function getEsmExports (moduleSource) {
45
36
  const exportedNames = new Set()
46
37
  const tree = parser.parse(moduleSource, acornOpts)
47
38
  for (const node of tree.body) {
@@ -56,19 +47,7 @@ function getEsmExports ({ moduleSource, defaultAs = 'default' }) {
56
47
  break
57
48
 
58
49
  case 'ExportDefaultDeclaration': {
59
- if (defaultAs === 'default') {
60
- exportedNames.add('default')
61
- break
62
- }
63
-
64
- if (node.declaration.type.toLowerCase() === 'identifier') {
65
- // e.g. `export default foo`
66
- exportedNames.add(`rename ${node.declaration.name} as ${defaultAs}`)
67
- } else {
68
- // e.g. `export function foo () {}
69
- exportedNames.add(`rename ${node.declaration.id.name} as ${defaultAs}`)
70
- }
71
-
50
+ exportedNames.add('default')
72
51
  break
73
52
  }
74
53
 
@@ -83,7 +62,7 @@ function getEsmExports ({ moduleSource, defaultAs = 'default' }) {
83
62
  warn('unrecognized export type: ' + node.type)
84
63
  }
85
64
  }
86
- return Array.from(exportedNames)
65
+ return exportedNames
87
66
  }
88
67
 
89
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
- url: (/^(..?($|\/|\\))/).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
  /**
@@ -38,25 +62,18 @@ async function getFullCjsExports (url, context, parentLoad, source) {
38
62
  * source code for said module from the loader API, and parses the result
39
63
  * for the entities exported from that module.
40
64
  *
41
- * @param {object} params
42
- * @param {string} params.url A file URL string pointing to the module that
65
+ * @param {string} url A file URL string pointing to the module that
43
66
  * we should get the exports of.
44
- * @param {object} params.context Context object as provided by the `load`
67
+ * @param {object} context Context object as provided by the `load`
45
68
  * hook from the loaders API.
46
- * @param {Function} params.parentLoad Next hook function in the loaders API
69
+ * @param {Function} parentLoad Next hook function in the loaders API
47
70
  * hook chain.
48
- * @param {string} [defaultAs='default'] When anything other than 'default',
49
- * will trigger remapping of default exports in ESM source files to the
50
- * provided name. For example, if a submodule has `export default foo` and
51
- * 'myFoo' is provided for this parameter, the export line will be rewritten
52
- * to `rename foo as myFoo`. This is key to being able to support
53
- * `export * from 'something'` exports.
54
71
  *
55
- * @returns {Promise<string[]>} An array of identifiers exported by the module.
72
+ * @returns {Promise<Set<string>>} An array of identifiers exported by the module.
56
73
  * Please see {@link getEsmExports} for caveats on special identifiers that may
57
74
  * be included in the result set.
58
75
  */
59
- async function getExports ({ url, context, parentLoad, defaultAs = 'default' }) {
76
+ async function getExports (url, context, parentLoad) {
60
77
  // `parentLoad` gives us the possibility of getting the source
61
78
  // from an upstream loader. This doesn't always work though,
62
79
  // so later on we fall back to reading it from disk.
@@ -64,34 +81,34 @@ async function getExports ({ url, context, parentLoad, defaultAs = 'default' })
64
81
  let source = parentCtx.source
65
82
  const format = parentCtx.format
66
83
 
67
- // TODO support non-node/file urls somehow?
68
- if (format === 'builtin') {
69
- // Builtins don't give us the source property, so we're stuck
70
- // just requiring it to get the exports.
71
- return addDefault(Object.keys(require(url)))
72
- }
73
-
74
84
  if (!source) {
75
- // Sometimes source is retrieved by parentLoad, sometimes it isn't.
76
- 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')
77
93
  }
78
94
 
79
95
  if (format === 'module') {
80
- return getEsmExports({ moduleSource: source, defaultAs })
96
+ return getEsmExports(source)
81
97
  }
98
+
82
99
  if (format === 'commonjs') {
83
- return getFullCjsExports(url, context, parentLoad, source)
100
+ return getCjsExports(url, context, parentLoad, source)
84
101
  }
85
102
 
86
103
  // At this point our `format` is either undefined or not known by us. Fall
87
104
  // back to parsing as ESM/CJS.
88
- const esmExports = getEsmExports({ moduleSource: source, defaultAs })
105
+ const esmExports = getEsmExports(source)
89
106
  if (!esmExports.length) {
90
107
  // TODO(bengl) it's might be possible to get here if somehow the format
91
108
  // isn't set at first and yet we have an ESM module with no exports.
92
109
  // I couldn't construct an example that would do this, so maybe it's
93
110
  // impossible?
94
- return getFullCjsExports(url, context, parentLoad, source)
111
+ return getCjsExports(url, context, parentLoad, source)
95
112
  }
96
113
  }
97
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "import-in-the-middle",
3
- "version": "1.7.4",
3
+ "version": "1.8.1",
4
4
  "description": "Intercept imports in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -36,15 +36,19 @@
36
36
  "@babel/core": "^7.23.7",
37
37
  "@babel/eslint-parser": "^7.23.3",
38
38
  "@babel/plugin-syntax-import-assertions": "^7.23.3",
39
+ "@react-email/components": "^0.0.19",
39
40
  "@types/node": "^18.0.6",
40
41
  "c8": "^7.8.0",
42
+ "date-fns": "^3.6.0",
41
43
  "eslint": "^8.55.0",
42
44
  "eslint-config-standard": "^17.1.0",
43
45
  "eslint-plugin-import": "^2.29.0",
44
46
  "eslint-plugin-n": "^16.4.0",
45
47
  "eslint-plugin-node": "^11.1.0",
46
48
  "eslint-plugin-promise": "^6.1.1",
49
+ "got": "^14.3.0",
47
50
  "imhotap": "^2.1.0",
51
+ "openai": "^4.47.2",
48
52
  "ts-node": "^10.9.1",
49
53
  "typescript": "^4.7.4"
50
54
  },
package/test/README.md CHANGED
@@ -11,7 +11,7 @@ options (assuming they're run from the project root):
11
11
 
12
12
  ```
13
13
  --require ./test/version-check.js
14
- --experimental-loader ./test/generic-loader.mmjs
14
+ --experimental-loader ./test/generic-loader.mjs
15
15
  ```
16
16
 
17
17
  The entire test suite can be run with `npm test`.
@@ -0,0 +1,8 @@
1
+ // File generated from our OpenAPI spec by Stainless.
2
+ import * as BatchesAPI from './cyclical-self.mjs'
3
+ export class Batches {}
4
+ export class BatchesPage {}
5
+ (function (Batches) {
6
+ Batches.BatchesPage = BatchesAPI.BatchesPage
7
+ // eslint-disable-next-line no-class-assign
8
+ })(Batches || (Batches = {}))
@@ -0,0 +1,4 @@
1
+ export const foo = 'a'
2
+ export default function () {
3
+ return 'c'
4
+ }
@@ -0,0 +1 @@
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
+ export * from './duplicate-a.mjs'
2
+ export * from './duplicate-b.mjs'
3
+
4
+ const foo = 'foo'
5
+ export default foo
@@ -0,0 +1,2 @@
1
+ export * from './default-call-expression.mjs'
2
+ export { default as somethingElse } from './default-call-expression.mjs'
@@ -0,0 +1 @@
1
+ export default parseInt('1')
@@ -1,7 +1,3 @@
1
- // The purpose of this fixture is to replicate a situation where we may
2
- // end up exporting `default` twice: first from this script itself and second
3
- // via the `export * from` line.
4
- //
5
1
  // This replicates the way the in-the-wild `got` module does things:
6
2
  // https://github.com/sindresorhus/got/blob/3822412/source/index.ts
7
3
 
@@ -12,6 +8,4 @@ class got {
12
8
  export default got
13
9
  export { got }
14
10
  export * from './something.mjs'
15
- export * from './default-class.mjs'
16
- export * from './snake_case.mjs'
17
11
  export { default as renamedDefaultExport } from './lib/baz.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
@@ -0,0 +1 @@
1
+ export * from 'some-external-module'
@@ -0,0 +1 @@
1
+ export * from 'some-external-module/sub'
@@ -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({ moduleSource: 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,4 @@
1
+ import { Batches, BatchesPage } from '../fixtures/cyclical-self.mjs'
2
+ import { strictEqual } from 'assert'
3
+
4
+ strictEqual(Batches.BatchesPage, BatchesPage)
@@ -12,6 +12,8 @@ import gfn from '../fixtures/export-types/default-generator.mjs'
12
12
  import afn from '../fixtures/export-types/default-function-anon.mjs'
13
13
  import acn from '../fixtures/export-types/default-class-anon.mjs'
14
14
  import agfn from '../fixtures/export-types/default-generator-anon.mjs'
15
+ import callEx from '../fixtures/export-types/default-call-expression.mjs'
16
+ import { somethingElse } from '../fixtures/export-types/default-call-expression-renamed.mjs'
15
17
  import defaultImportExport from '../fixtures/export-types/import-default-export.mjs'
16
18
  import varDefaultExport from '../fixtures/export-types/variable-default-export.mjs'
17
19
  import { strictEqual } from 'assert'
@@ -61,6 +63,10 @@ Hook((exports, name) => {
61
63
  exports.default = function () {
62
64
  return orig4() + 1
63
65
  }
66
+ } else if (name.match(/default-call-expression\.m?js/)) {
67
+ exports.default += 1
68
+ } else if (name.match(/default-call-expression-renamed\.m?js/)) {
69
+ exports.somethingElse += 1
64
70
  }
65
71
  })
66
72
 
@@ -76,3 +82,5 @@ strictEqual(new acn().getFoo(), 2)
76
82
  strictEqual(agfn().next().value, 2)
77
83
  strictEqual(n, 2)
78
84
  strictEqual(s, 'dogdawg')
85
+ strictEqual(callEx, 2)
86
+ strictEqual(somethingElse, 2)
@@ -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')
@@ -0,0 +1,19 @@
1
+ import * as lib from '../fixtures/duplicate.mjs'
2
+ import { notEqual, strictEqual } from 'assert'
3
+ import Hook from '../../index.js'
4
+
5
+ Hook((exports, name) => {
6
+ if (name.match(/duplicate\.mjs/)) {
7
+ // foo should not be exported because there are duplicate * exports
8
+ strictEqual('foo' in exports, false)
9
+ // default should be exported
10
+ strictEqual(exports.default, 'foo')
11
+ }
12
+ })
13
+
14
+ notEqual(lib, undefined)
15
+
16
+ // foo should not be exported because there are duplicate exports
17
+ strictEqual('foo' in lib, false)
18
+ // default should be exported
19
+ strictEqual(lib.default, 'foo')
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
4
 
5
- const Hook = require('../../index.js')
5
+ const { Hook } = require('../../index.js')
6
6
  const { strictEqual } = require('assert')
7
7
 
8
8
  Hook((exports, name) => {
@@ -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')
@@ -0,0 +1,17 @@
1
+ import Hook from '../../index.js'
2
+ import { foo } from '../fixtures/re-export-star-external.mjs'
3
+ import { bar } from '../fixtures/sub-directory/re-export-star-external.mjs'
4
+ import { strictEqual } from 'assert'
5
+
6
+ Hook((exports, name) => {
7
+ if (name.endsWith('fixtures/re-export-star-external.mjs')) {
8
+ exports.foo = '1'
9
+ }
10
+
11
+ if (name.endsWith('fixtures/sub-directory/re-export-star-external.mjs')) {
12
+ exports.bar = '2'
13
+ }
14
+ })
15
+
16
+ strictEqual(foo, '1')
17
+ strictEqual(bar, '2')
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
4
4
 
5
- import Hook from '../../index.js'
5
+ import { Hook } from '../../index.js'
6
6
  import barMjs from '../fixtures/something.mjs'
7
7
  import barJs from '../fixtures/something.js'
8
8
  import { strictEqual } from 'assert'
@@ -0,0 +1,10 @@
1
+ import { format, parse } from 'date-fns'
2
+ import Hook from '../../index.js'
3
+
4
+ Hook((exports, name) => {
5
+ if (name === 'date-fns') {
6
+ //
7
+ }
8
+ })
9
+
10
+ console.assert(format, parse)
@@ -0,0 +1,16 @@
1
+ import * as lib from '@react-email/components'
2
+ import { Heading } from '@react-email/components'
3
+ import Hook from '../../index.js'
4
+ import { strictEqual } from 'assert'
5
+
6
+ Hook((exports, name) => {
7
+ if (name.match(/@react-email\/components/)) {
8
+ exports.Heading = function wrappedHeading () {
9
+ return 'heading-wrapped'
10
+ }
11
+ }
12
+ })
13
+
14
+ strictEqual(typeof lib.Button, 'function')
15
+ strictEqual(lib.Heading(), 'heading-wrapped')
16
+ strictEqual(Heading(), 'heading-wrapped')
@@ -0,0 +1,16 @@
1
+ import got, { Options } from 'got'
2
+ import { strictEqual } from 'assert'
3
+ import Hook from '../../index.js'
4
+
5
+ Hook((exports, name) => {
6
+ if (name === 'got' && 'Options' in exports) {
7
+ exports.Options = 'nothing'
8
+ }
9
+ })
10
+
11
+ strictEqual(typeof got, 'function')
12
+ strictEqual(typeof got.post, 'function')
13
+ strictEqual(typeof got.stream, 'function')
14
+ strictEqual(typeof got.extend, 'function')
15
+
16
+ strictEqual(Options, 'nothing')
@@ -0,0 +1,16 @@
1
+ import OpenAI from 'openai'
2
+ import Hook from '../../index.js'
3
+
4
+ Hook((exports, name) => {
5
+ if (name === 'openai') {
6
+ console.assert(name, exports)
7
+ }
8
+ })
9
+
10
+ console.assert(OpenAI)
11
+
12
+ const openAI = new OpenAI({
13
+ apiKey: 'doesnt-matter'
14
+ })
15
+
16
+ console.assert(openAI)
@@ -3,10 +3,8 @@ import Hook from '../../index.js'
3
3
  Hook((exports, name) => {
4
4
  if (/got-alike\.mjs/.test(name) === false) return
5
5
 
6
- const bar = exports.something
7
- exports.something = function barWrapped () {
8
- return bar() + '-wrapped'
9
- }
6
+ const foo = exports.foo
7
+ exports.foo = foo + '-wrapped'
10
8
 
11
9
  const renamedDefaultExport = exports.renamedDefaultExport
12
10
  exports.renamedDefaultExport = function bazWrapped () {
@@ -15,21 +13,14 @@ Hook((exports, name) => {
15
13
  })
16
14
 
17
15
  /* eslint-disable import/no-named-default */
18
- /* eslint-disable camelcase */
19
16
  import {
20
17
  default as Got,
21
- something,
22
- defaultClass as DefaultClass,
23
- snake_case,
18
+ foo,
24
19
  renamedDefaultExport
25
20
  } from '../fixtures/got-alike.mjs'
26
21
 
27
- strictEqual(something(), '42-wrapped')
22
+ strictEqual(foo, '42-wrapped')
28
23
  const got = new Got()
29
24
  strictEqual(got.foo, 'foo')
30
25
 
31
- const dc = new DefaultClass()
32
- strictEqual(dc.value, 'DefaultClass')
33
-
34
- strictEqual(snake_case, 'snake_case')
35
26
  strictEqual(renamedDefaultExport(), 'baz-wrapped')
@@ -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,6 @@
1
+ import { register } from 'node:module'
2
+
3
+ register('./typescript-loader.mjs', import.meta.url)
4
+ register('../../hook.mjs', import.meta.url)
5
+
6
+ await import('node:util')
@@ -0,0 +1,26 @@
1
+ /**
2
+ * This simulates what something like `tsx` (https://github.com/privatenumber/tsx)
3
+ * will do: it will try to resolve a URL with a `.js` extension to a `.ts` extension.
4
+ *
5
+ * Combined with the test case in the adjacent `multiple-loaders.test.mjs` file,
6
+ * this forces `import-in-the-middle` into what used to be a failure state: where
7
+ * `context.parentURL` is a `node:*` specifier and the `specifier` refers to a file
8
+ * that does not exist.
9
+ *
10
+ * See https://github.com/nodejs/node/issues/52987 for more details.
11
+ */
12
+ export async function resolve (specifier, context, defaultResolve) {
13
+ if (!specifier.endsWith('.js') && !specifier.endsWith('.mjs')) {
14
+ return await defaultResolve(specifier, context)
15
+ }
16
+
17
+ try {
18
+ return await defaultResolve(specifier.replace(/\.m?js/, '.ts'), context)
19
+ } catch (err) {
20
+ if (err.code !== 'ERR_MODULE_NOT_FOUND') {
21
+ throw err
22
+ }
23
+
24
+ return await defaultResolve(specifier, context)
25
+ }
26
+ }
@@ -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')
@@ -12,10 +12,11 @@ process.env.IITM_TEST_FILE = filename
12
12
 
13
13
  const [processMajor, processMinor] = process.versions.node.split('.').map(Number)
14
14
 
15
- const match = filename.match(/v([0-9]+)(?:\.([0-9]+))?/)
15
+ const match = filename.match(/v([0-9]+)(?:\.([0-9]+))?(?:-v([0-9]+))?/)
16
16
 
17
17
  const majorRequirement = match ? match[1] : 0
18
18
  const minorRequirement = match && match[2]
19
+ const majorMax = match ? match[3] : Infinity
19
20
 
20
21
  if (processMajor < majorRequirement) {
21
22
  console.log(`skipping ${filename} as this is Node.js v${processMajor} and test wants v${majorRequirement}`)
@@ -25,3 +26,8 @@ if (processMajor <= majorRequirement && processMinor < minorRequirement) {
25
26
  console.log(`skipping ${filename} as this is Node.js v${processMajor}.${processMinor} and test wants >=v${majorRequirement}.${minorRequirement}`)
26
27
  process.exit(0)
27
28
  }
29
+
30
+ if (processMajor > majorMax) {
31
+ console.log(`skipping ${filename} as this is Node.js v${processMajor} and test wants <=v${majorMax}`)
32
+ process.exit(0)
33
+ }
package/fix.patch DELETED
@@ -1,52 +0,0 @@
1
- From 5a9ae09d53e43a30709c7162fb351f3a57626f79 Mon Sep 17 00:00:00 2001
2
- From: Bryan English <bryan.english@datadoghq.com>
3
- Date: Fri, 21 Jul 2023 11:50:34 -0400
4
- Subject: [PATCH] sanitize URLs
5
-
6
- ---
7
- hook.js | 4 ++--
8
- test/low-level/sanitized-url.mjs | 11 +++++++++++
9
- 2 files changed, 13 insertions(+), 2 deletions(-)
10
- create mode 100644 test/low-level/sanitized-url.mjs
11
-
12
- diff --git a/hook.js b/hook.js
13
- index 884ee3a..3639fbf 100644
14
- --- a/hook.js
15
- +++ b/hook.js
16
- @@ -122,7 +122,7 @@ function createHook (meta) {
17
- return {
18
- source: `
19
- import { register } from '${iitmURL}'
20
- -import * as namespace from '${url}'
21
- +import * as namespace from ${JSON.stringify(url)}
22
- const set = {}
23
- ${exportNames.map((n) => `
24
- let $${n} = namespace.${n}
25
- @@ -132,7 +132,7 @@ set.${n} = (v) => {
26
- return true
27
- }
28
- `).join('\n')}
29
- -register('${realUrl}', namespace, set, '${specifiers.get(realUrl)}')
30
- +register(${JSON.stringify(realUrl)}, namespace, set, ${JSON.stringify(specifiers.get(realUrl))})
31
- `
32
- }
33
- }
34
- diff --git a/test/low-level/sanitized-url.mjs b/test/low-level/sanitized-url.mjs
35
- new file mode 100644
36
- index 0000000..6fe5e81
37
- --- /dev/null
38
- +++ b/test/low-level/sanitized-url.mjs
39
- @@ -0,0 +1,11 @@
40
- +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2.0 License.
41
- +//
42
- +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
43
- +
44
- +import { addHook } from '../../index.js'
45
- +
46
- +addHook(() => {})
47
- +
48
- +;(async () => {
49
- + await import("../fixtures/something.mjs#*/'/*';eval('process.exit\x281\x29\x0A')")
50
- +})()
51
- --
52
- 2.39.0
@@ -1,3 +0,0 @@
1
- export default class DefaultClass {
2
- value = 'DefaultClass'
3
- }
@@ -1,2 +0,0 @@
1
- const snakeCase = 'snake_case'
2
- export default snakeCase
@@ -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