import-in-the-middle 1.7.2 → 1.7.4
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 +23 -0
- package/fix.patch +52 -0
- package/hook.js +142 -30
- package/index.js +4 -4
- package/lib/get-esm-exports.js +47 -5
- package/lib/get-exports.js +54 -7
- package/lib/register.js +3 -4
- package/package.json +20 -5
- package/test/README.md +9 -5
- package/test/fixtures/a.mjs +1 -1
- package/test/fixtures/b.mjs +1 -1
- package/test/fixtures/circular-a.js +18 -0
- package/test/fixtures/circular-b.js +15 -0
- package/test/fixtures/cyclical-a.mjs +7 -0
- package/test/fixtures/cyclical-b.mjs +6 -0
- package/test/fixtures/default-class.mjs +3 -0
- package/test/fixtures/env.mjs +8 -0
- package/test/fixtures/export-types/declarations.mjs +19 -0
- package/test/fixtures/export-types/default-class-anon.mjs +1 -0
- package/test/fixtures/export-types/default-class.mjs +1 -0
- package/test/fixtures/export-types/default-expression-array.mjs +1 -0
- package/test/fixtures/export-types/default-expression-num.mjs +1 -0
- package/test/fixtures/export-types/default-expression-string.mjs +1 -0
- package/test/fixtures/export-types/default-function-anon.mjs +1 -0
- package/test/fixtures/export-types/default-function.mjs +1 -0
- package/test/fixtures/export-types/default-generator-anon.mjs +1 -0
- package/test/fixtures/export-types/default-generator.mjs +1 -0
- package/test/fixtures/export-types/fn-default-export.mjs +3 -0
- package/test/fixtures/export-types/import-default-export.mjs +2 -0
- package/test/fixtures/export-types/list.mjs +12 -0
- package/test/fixtures/export-types/variable-default-export.mjs +5 -0
- package/test/fixtures/foo.mjs +1 -1
- package/test/fixtures/got-alike.mjs +17 -0
- package/test/fixtures/json.mjs +2 -2
- package/test/fixtures/lib/baz.mjs +2 -1
- package/test/fixtures/reexport.js +15 -0
- package/test/fixtures/snake_case.mjs +2 -0
- package/test/fixtures/something.js +1 -1
- package/test/generic-loader.mjs +14 -0
- package/test/get-esm-exports/v20-get-esm-exports.js +1 -1
- package/test/hook/circular-imports.mjs +11 -0
- package/test/hook/default-export.mjs +78 -0
- package/test/hook/dynamic-import-default.js +2 -2
- package/test/hook/dynamic-import-default.mjs +1 -1
- package/test/hook/import-reexport-cjs.mjs +11 -0
- package/test/hook/module-toStringTag.mjs +25 -0
- package/test/hook/static-import-default.mjs +2 -2
- package/test/hook/static-import-package-internals-enabled.mjs +1 -1
- package/test/hook/static-import-star.mjs +12 -10
- package/test/hook/v14-declaration-exports.mjs +84 -0
- package/test/hook/v18.19-static-import-gotalike.mjs +35 -0
- package/test/low-level/dynamic-import-default.js +1 -1
- package/test/low-level/dynamic-import-default.mjs +1 -1
- package/test/low-level/static-import-default.mjs +1 -1
- package/test/other/v14-assert-cyclical-dependency-failure.mjs +36 -0
- package/test/typescript/iitm-ts-node-loader.mjs +4 -4
- package/test/version-check.js +27 -0
- package/test/runtest +0 -46
package/.eslintrc.yaml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
overrides:
|
|
2
|
+
- files:
|
|
3
|
+
- '**/*.{js,cjs,mjs}'
|
|
4
|
+
|
|
5
|
+
parser: '@babel/eslint-parser'
|
|
6
|
+
parserOptions:
|
|
7
|
+
ecmaVersion: latest
|
|
8
|
+
requireConfigFile: false
|
|
9
|
+
sourceType: 'script'
|
|
10
|
+
babelOptions:
|
|
11
|
+
plugins:
|
|
12
|
+
- '@babel/plugin-syntax-import-assertions'
|
|
13
|
+
|
|
14
|
+
rules:
|
|
15
|
+
"import/first": off
|
|
16
|
+
|
|
17
|
+
extends:
|
|
18
|
+
- "standard"
|
|
19
|
+
|
|
20
|
+
ignorePatterns:
|
|
21
|
+
- test/fixtures/circular-a.js
|
|
22
|
+
- test/fixtures/circular-b.js
|
|
23
|
+
- test/fixtures/reexport.js
|
package/fix.patch
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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
|
package/hook.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { randomBytes } = require('crypto')
|
|
6
6
|
const specifiers = new Map()
|
|
7
|
-
const isWin = process.platform ===
|
|
7
|
+
const isWin = process.platform === 'win32'
|
|
8
8
|
|
|
9
9
|
// FIXME: Typescript extensions are added temporarily until we find a better
|
|
10
10
|
// way of supporting arbitrary extensions
|
|
@@ -16,10 +16,10 @@ const NODE_MINOR = Number(NODE_VERSION[1])
|
|
|
16
16
|
let entrypoint
|
|
17
17
|
|
|
18
18
|
let getExports
|
|
19
|
-
if (NODE_MAJOR >= 20 || (NODE_MAJOR
|
|
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) {
|
|
@@ -56,7 +56,7 @@ function deleteIitm (url) {
|
|
|
56
56
|
return resultUrl
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
59
|
+
function isNodeMajor16AndMinor17OrGreater () {
|
|
60
60
|
return NODE_MAJOR === 16 && NODE_MINOR >= 17
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -68,11 +68,11 @@ function isNodeProtocol (urlObj) {
|
|
|
68
68
|
return urlObj.protocol === 'node:'
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function needsToAddFileProtocol(urlObj) {
|
|
71
|
+
function needsToAddFileProtocol (urlObj) {
|
|
72
72
|
if (NODE_MAJOR === 17) {
|
|
73
73
|
return !isFileProtocol(urlObj)
|
|
74
74
|
}
|
|
75
|
-
if (
|
|
75
|
+
if (isNodeMajor16AndMinor17OrGreater()) {
|
|
76
76
|
return !isFileProtocol(urlObj) && !isNodeProtocol(urlObj)
|
|
77
77
|
}
|
|
78
78
|
return !isFileProtocol(urlObj) && NODE_MAJOR < 18
|
|
@@ -87,7 +87,7 @@ function needsToAddFileProtocol(urlObj) {
|
|
|
87
87
|
* @param {string} line
|
|
88
88
|
* @returns {boolean}
|
|
89
89
|
*/
|
|
90
|
-
function isStarExportLine(line) {
|
|
90
|
+
function isStarExportLine (line) {
|
|
91
91
|
return /^\* from /.test(line)
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -98,8 +98,10 @@ function isStarExportLine(line) {
|
|
|
98
98
|
* @property {string[]} namespaces A set of identifiers representing the
|
|
99
99
|
* modules in `imports`, e.g. for `import * as foo from 'bar'`, "foo" will be
|
|
100
100
|
* present in this array.
|
|
101
|
-
* @property {string
|
|
102
|
-
* from the module and any transitive export all modules.
|
|
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.
|
|
103
105
|
*/
|
|
104
106
|
|
|
105
107
|
/**
|
|
@@ -111,33 +113,86 @@ function isStarExportLine(line) {
|
|
|
111
113
|
* @param {object} params
|
|
112
114
|
* @param {string} params.srcUrl The full URL to the module to process.
|
|
113
115
|
* @param {object} params.context Provided by the loaders API.
|
|
114
|
-
* @param {
|
|
115
|
-
* module.
|
|
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
|
+
*
|
|
116
125
|
* @returns {Promise<ProcessedModule>}
|
|
117
126
|
*/
|
|
118
|
-
async function processModule({
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
151
|
+
const setters = new Map()
|
|
123
152
|
|
|
124
153
|
for (const n of exportNames) {
|
|
125
154
|
if (isStarExportLine(n) === true) {
|
|
126
|
-
const [
|
|
155
|
+
const [, modFile] = n.split('* from ')
|
|
156
|
+
const normalizedModName = normalizeModName(modFile)
|
|
127
157
|
const modUrl = new URL(modFile, srcUrl).toString()
|
|
128
158
|
const modName = Buffer.from(modFile, 'hex') + Date.now() + randomBytes(4).toString('hex')
|
|
129
159
|
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
const data = await processModule({
|
|
161
|
+
srcUrl: modUrl,
|
|
162
|
+
context,
|
|
163
|
+
parentGetSource,
|
|
164
|
+
ns: `$${modName}`,
|
|
165
|
+
defaultAs: normalizedModName
|
|
166
|
+
})
|
|
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)
|
|
171
|
+
}
|
|
132
172
|
|
|
133
|
-
|
|
134
|
-
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
135
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
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
`)
|
|
136
191
|
continue
|
|
137
192
|
}
|
|
138
193
|
|
|
139
|
-
setters.
|
|
140
|
-
let $${n} =
|
|
194
|
+
setters.set(`$${n}` + ns, `
|
|
195
|
+
let $${n} = ${ns}.${n}
|
|
141
196
|
export { $${n} as ${n} }
|
|
142
197
|
set.${n} = (v) => {
|
|
143
198
|
$${n} = v
|
|
@@ -149,6 +204,24 @@ async function processModule({ srcUrl, context, parentGetSource }) {
|
|
|
149
204
|
return { imports, namespaces, setters }
|
|
150
205
|
}
|
|
151
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())
|
|
223
|
+
}
|
|
224
|
+
|
|
152
225
|
function addIitm (url) {
|
|
153
226
|
const urlObj = new URL(url)
|
|
154
227
|
urlObj.searchParams.set('iitm', 'true')
|
|
@@ -180,7 +253,6 @@ function createHook (meta) {
|
|
|
180
253
|
return url
|
|
181
254
|
}
|
|
182
255
|
|
|
183
|
-
|
|
184
256
|
specifiers.set(url.url, specifier)
|
|
185
257
|
|
|
186
258
|
return {
|
|
@@ -194,21 +266,61 @@ function createHook (meta) {
|
|
|
194
266
|
async function getSource (url, context, parentGetSource) {
|
|
195
267
|
if (hasIitm(url)) {
|
|
196
268
|
const realUrl = deleteIitm(url)
|
|
197
|
-
const { imports, namespaces, setters } = await processModule({
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
269
|
+
const { imports, namespaces, setters: mapSetters } = await processModule({
|
|
270
|
+
srcUrl: realUrl,
|
|
271
|
+
context,
|
|
272
|
+
parentGetSource
|
|
201
273
|
})
|
|
202
|
-
|
|
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`
|
|
288
|
+
})
|
|
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.
|
|
203
300
|
return {
|
|
204
301
|
source: `
|
|
205
302
|
import { register } from '${iitmURL}'
|
|
206
303
|
${imports.join('\n')}
|
|
207
304
|
|
|
208
|
-
const
|
|
305
|
+
const namespaces = [${namespaces.join(', ')}]
|
|
306
|
+
// Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
|
|
307
|
+
const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } })
|
|
209
308
|
const set = {}
|
|
210
309
|
|
|
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
|
+
|
|
211
321
|
${setters.join('\n')}
|
|
322
|
+
${renamedDefaults.join('\n')}
|
|
323
|
+
|
|
212
324
|
register(${JSON.stringify(realUrl)}, _, set, ${JSON.stringify(specifiers.get(realUrl))})
|
|
213
325
|
`
|
|
214
326
|
}
|
package/index.js
CHANGED
|
@@ -12,26 +12,26 @@ const {
|
|
|
12
12
|
toHook
|
|
13
13
|
} = require('./lib/register')
|
|
14
14
|
|
|
15
|
-
function addHook(hook) {
|
|
15
|
+
function addHook (hook) {
|
|
16
16
|
importHooks.push(hook)
|
|
17
17
|
toHook.forEach(([name, namespace]) => hook(name, namespace))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function removeHook(hook) {
|
|
20
|
+
function removeHook (hook) {
|
|
21
21
|
const index = importHooks.indexOf(hook)
|
|
22
22
|
if (index > -1) {
|
|
23
23
|
importHooks.splice(index, 1)
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function callHookFn(hookFn, namespace, name, baseDir) {
|
|
27
|
+
function callHookFn (hookFn, namespace, name, baseDir) {
|
|
28
28
|
const newDefault = hookFn(namespace, name, baseDir)
|
|
29
29
|
if (newDefault && newDefault !== namespace) {
|
|
30
30
|
namespace.default = newDefault
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function Hook(modules, options, hookFn) {
|
|
34
|
+
function Hook (modules, options, hookFn) {
|
|
35
35
|
if ((this instanceof Hook) === false) return new Hook(modules, options, hookFn)
|
|
36
36
|
if (typeof modules === 'function') {
|
|
37
37
|
hookFn = modules
|
package/lib/get-esm-exports.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Parser } = require('acorn')
|
|
4
|
-
const { importAssertions } = require('acorn-import-
|
|
4
|
+
const { importAssertions } = require('acorn-import-attributes')
|
|
5
5
|
|
|
6
6
|
const acornOpts = {
|
|
7
7
|
ecmaVersion: 'latest',
|
|
@@ -14,9 +14,36 @@ function warn (txt) {
|
|
|
14
14
|
process.emitWarning(txt, 'get-esm-exports')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Utilizes an AST parser to interpret ESM source code and build a list of
|
|
19
|
+
* exported identifiers. In the baseline case, the list of identifiers will be
|
|
20
|
+
* the simple identifier names as written in the source code of the module.
|
|
21
|
+
* However, there are some special cases:
|
|
22
|
+
*
|
|
23
|
+
* 1. When an `export * from './foo.js'` line is encountered it is rewritten
|
|
24
|
+
* as `* from ./foo.js`. This allows the interpreting code to recognize a
|
|
25
|
+
* transitive export and recursively parse the indicated module. The returned
|
|
26
|
+
* identifier list will have "* from ./foo.js" as an item.
|
|
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
|
+
* @param {object} params
|
|
33
|
+
* @param {string} params.moduleSource The source code of the module to parse
|
|
34
|
+
* 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
|
+
*
|
|
41
|
+
* @returns {string[]} The identifiers exported by the module along with any
|
|
42
|
+
* custom directives.
|
|
43
|
+
*/
|
|
44
|
+
function getEsmExports ({ moduleSource, defaultAs = 'default' }) {
|
|
18
45
|
const exportedNames = new Set()
|
|
19
|
-
const tree = parser.parse(
|
|
46
|
+
const tree = parser.parse(moduleSource, acornOpts)
|
|
20
47
|
for (const node of tree.body) {
|
|
21
48
|
if (!node.type.startsWith('Export')) continue
|
|
22
49
|
switch (node.type) {
|
|
@@ -27,9 +54,24 @@ function getEsmExports (moduleStr) {
|
|
|
27
54
|
parseSpecifiers(node, exportedNames)
|
|
28
55
|
}
|
|
29
56
|
break
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
|
|
58
|
+
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
|
+
|
|
32
72
|
break
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
case 'ExportAllDeclaration':
|
|
34
76
|
if (node.exported) {
|
|
35
77
|
exportedNames.add(node.exported.name)
|
package/lib/get-exports.js
CHANGED
|
@@ -3,13 +3,60 @@
|
|
|
3
3
|
const getEsmExports = require('./get-esm-exports.js')
|
|
4
4
|
const { parse: getCjsExports } = require('cjs-module-lexer')
|
|
5
5
|
const fs = require('fs')
|
|
6
|
-
const { fileURLToPath } = require('url')
|
|
6
|
+
const { fileURLToPath, pathToFileURL } = require('url')
|
|
7
7
|
|
|
8
|
-
function addDefault(arr) {
|
|
8
|
+
function addDefault (arr) {
|
|
9
9
|
return Array.from(new Set(['default', ...arr]))
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const urlsBeingProcessed = new Set() // Guard against circular imports.
|
|
13
|
+
|
|
14
|
+
async function getFullCjsExports (url, context, parentLoad, source) {
|
|
15
|
+
if (urlsBeingProcessed.has(url)) {
|
|
16
|
+
return []
|
|
17
|
+
}
|
|
18
|
+
urlsBeingProcessed.add(url)
|
|
19
|
+
|
|
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
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Inspects a module for its type (commonjs or module), attempts to get the
|
|
38
|
+
* source code for said module from the loader API, and parses the result
|
|
39
|
+
* for the entities exported from that module.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} params
|
|
42
|
+
* @param {string} params.url A file URL string pointing to the module that
|
|
43
|
+
* we should get the exports of.
|
|
44
|
+
* @param {object} params.context Context object as provided by the `load`
|
|
45
|
+
* hook from the loaders API.
|
|
46
|
+
* @param {Function} params.parentLoad Next hook function in the loaders API
|
|
47
|
+
* 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
|
+
*
|
|
55
|
+
* @returns {Promise<string[]>} An array of identifiers exported by the module.
|
|
56
|
+
* Please see {@link getEsmExports} for caveats on special identifiers that may
|
|
57
|
+
* be included in the result set.
|
|
58
|
+
*/
|
|
59
|
+
async function getExports ({ url, context, parentLoad, defaultAs = 'default' }) {
|
|
13
60
|
// `parentLoad` gives us the possibility of getting the source
|
|
14
61
|
// from an upstream loader. This doesn't always work though,
|
|
15
62
|
// so later on we fall back to reading it from disk.
|
|
@@ -30,21 +77,21 @@ async function getExports (url, context, parentLoad) {
|
|
|
30
77
|
}
|
|
31
78
|
|
|
32
79
|
if (format === 'module') {
|
|
33
|
-
return getEsmExports(source)
|
|
80
|
+
return getEsmExports({ moduleSource: source, defaultAs })
|
|
34
81
|
}
|
|
35
82
|
if (format === 'commonjs') {
|
|
36
|
-
return
|
|
83
|
+
return getFullCjsExports(url, context, parentLoad, source)
|
|
37
84
|
}
|
|
38
85
|
|
|
39
86
|
// At this point our `format` is either undefined or not known by us. Fall
|
|
40
87
|
// back to parsing as ESM/CJS.
|
|
41
|
-
const esmExports = getEsmExports(source)
|
|
88
|
+
const esmExports = getEsmExports({ moduleSource: source, defaultAs })
|
|
42
89
|
if (!esmExports.length) {
|
|
43
90
|
// TODO(bengl) it's might be possible to get here if somehow the format
|
|
44
91
|
// isn't set at first and yet we have an ESM module with no exports.
|
|
45
92
|
// I couldn't construct an example that would do this, so maybe it's
|
|
46
93
|
// impossible?
|
|
47
|
-
return
|
|
94
|
+
return getFullCjsExports(url, context, parentLoad, source)
|
|
48
95
|
}
|
|
49
96
|
}
|
|
50
97
|
|
package/lib/register.js
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
//
|
|
3
3
|
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
const importHooks = [] // TODO should this be a Set?
|
|
7
6
|
const setters = new WeakMap()
|
|
8
7
|
const specifiers = new Map()
|
|
9
8
|
const toHook = []
|
|
10
9
|
|
|
11
10
|
const proxyHandler = {
|
|
12
|
-
set(target, name, value) {
|
|
11
|
+
set (target, name, value) {
|
|
13
12
|
return setters.get(target)[name](value)
|
|
14
13
|
},
|
|
15
14
|
|
|
16
|
-
defineProperty(target, property, descriptor) {
|
|
15
|
+
defineProperty (target, property, descriptor) {
|
|
17
16
|
if ((!('value' in descriptor))) {
|
|
18
17
|
throw new Error('Getters/setters are not supported for exports property descriptors.')
|
|
19
18
|
}
|
|
@@ -22,7 +21,7 @@ const proxyHandler = {
|
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
function register(name, namespace, set, specifier) {
|
|
24
|
+
function register (name, namespace, set, specifier) {
|
|
26
25
|
specifiers.set(name, specifier)
|
|
27
26
|
setters.set(namespace, set)
|
|
28
27
|
const proxy = new Proxy(namespace, proxyHandler)
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "import-in-the-middle",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
4
4
|
"description": "Intercept imports in Node.js",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "c8 --reporter lcov --check-coverage --lines
|
|
8
|
-
"test:ts": "c8 --reporter lcov imhotap --
|
|
9
|
-
"coverage": "c8 --reporter html imhotap --
|
|
7
|
+
"test": "c8 --reporter lcov --check-coverage --lines 50 imhotap --files test/{hook,low-level,other,get-esm-exports}/*",
|
|
8
|
+
"test:ts": "c8 --reporter lcov imhotap --files test/typescript/*.test.mts",
|
|
9
|
+
"coverage": "c8 --reporter html imhotap --files test/{hook,low-level,other,get-esm-exports}/* && echo '\nNow open coverage/index.html\n'",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"lint:fix": "eslint . --fix"
|
|
10
12
|
},
|
|
11
13
|
"repository": {
|
|
12
14
|
"type": "git",
|
|
@@ -26,16 +28,29 @@
|
|
|
26
28
|
"url": "https://github.com/DataDog/import-in-the-middle/issues"
|
|
27
29
|
},
|
|
28
30
|
"homepage": "https://github.com/DataDog/import-in-the-middle#readme",
|
|
31
|
+
"imhotap": {
|
|
32
|
+
"runner": "node",
|
|
33
|
+
"test-env": "NODE_OPTIONS=--no-warnings --require ./test/version-check.js --experimental-loader ./test/generic-loader.mjs"
|
|
34
|
+
},
|
|
29
35
|
"devDependencies": {
|
|
36
|
+
"@babel/core": "^7.23.7",
|
|
37
|
+
"@babel/eslint-parser": "^7.23.3",
|
|
38
|
+
"@babel/plugin-syntax-import-assertions": "^7.23.3",
|
|
30
39
|
"@types/node": "^18.0.6",
|
|
31
40
|
"c8": "^7.8.0",
|
|
41
|
+
"eslint": "^8.55.0",
|
|
42
|
+
"eslint-config-standard": "^17.1.0",
|
|
43
|
+
"eslint-plugin-import": "^2.29.0",
|
|
44
|
+
"eslint-plugin-n": "^16.4.0",
|
|
45
|
+
"eslint-plugin-node": "^11.1.0",
|
|
46
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
32
47
|
"imhotap": "^2.1.0",
|
|
33
48
|
"ts-node": "^10.9.1",
|
|
34
49
|
"typescript": "^4.7.4"
|
|
35
50
|
},
|
|
36
51
|
"dependencies": {
|
|
37
52
|
"acorn": "^8.8.2",
|
|
38
|
-
"acorn-import-
|
|
53
|
+
"acorn-import-attributes": "^1.9.5",
|
|
39
54
|
"cjs-module-lexer": "^1.2.2",
|
|
40
55
|
"module-details-from-path": "^1.0.3"
|
|
41
56
|
}
|
package/test/README.md
CHANGED
|
@@ -3,11 +3,15 @@ These tests are organized as follows:
|
|
|
3
3
|
* Located in the `hook` directory if they use the `Hook` class.
|
|
4
4
|
* Located in the `low-level` directory if they use the "low-level" API,
|
|
5
5
|
`addHook` and `removeHook`.
|
|
6
|
+
* Other tests are in other adjacent directories.
|
|
6
7
|
|
|
7
|
-
The tests
|
|
8
|
-
|
|
8
|
+
The tests can be run individually as Node.js programs with non-zero exit codes
|
|
9
|
+
upon failures. They should be run with the following Node.js command-line
|
|
10
|
+
options (assuming they're run from the project root):
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
```
|
|
13
|
+
--require ./test/version-check.js
|
|
14
|
+
--experimental-loader ./test/generic-loader.mmjs
|
|
15
|
+
```
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
can run `npm run coverage` to get coverage data in HTML form.
|
|
17
|
+
The entire test suite can be run with `npm test`.
|
package/test/fixtures/a.mjs
CHANGED
package/test/fixtures/b.mjs
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The following is generated by tslib. __exportStar is a format which cjs-module-lexer exposes as a reexport
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
+
}) : (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
o[k2] = m[k];
|
|
8
|
+
}));
|
|
9
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
10
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__exportStar(require("./circular-b"), exports);
|
|
16
|
+
|
|
17
|
+
exports.foo = 42;
|
|
18
|
+
|