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' )
6
5
const { URL } = require ( 'url' )
7
6
const specifiers = new Map ( )
8
7
const isWin = process . platform === 'win32'
@@ -20,7 +19,7 @@ let getExports
20
19
if ( NODE_MAJOR >= 20 || ( NODE_MAJOR === 18 && NODE_MINOR >= 19 ) ) {
21
20
getExports = require ( './lib/get-exports.js' )
22
21
} else {
23
- getExports = ( { url } ) => import ( url ) . then ( Object . keys )
22
+ getExports = ( url ) => import ( url ) . then ( Object . keys )
24
23
}
25
24
26
25
function hasIitm ( url ) {
@@ -117,70 +116,37 @@ function isBareSpecifier (specifier) {
117
116
}
118
117
119
118
/**
120
- * @typedef {object } ProcessedModule
121
- * @property {string[] } imports A set of ESM import lines to be added to the
122
- * shimmed module source.
123
- * @property {string[] } namespaces A set of identifiers representing the
124
- * modules in `imports`, e.g. for `import * as foo from 'bar'`, "foo" will be
125
- * present in this array.
126
- * @property {Map<string, string> } setters The shimmed setters for all the
127
- * exports from the module and any transitive export all modules. The key is
128
- * used to deduplicate conflicting exports, assigning a priority to `default`
129
- * exports.
130
- */
131
-
132
- /**
133
- * Processes a module's exports and builds a set of new import statements,
134
- * namespace names, and setter blocks. If an export all export if encountered,
135
- * the target exports will be hoisted to the current module via a generated
136
- * namespace.
119
+ * Processes a module's exports and builds a set of setter blocks.
137
120
*
138
121
* @param {object } params
139
122
* @param {string } params.srcUrl The full URL to the module to process.
140
123
* @param {object } params.context Provided by the loaders API.
141
- * @param {Function } params.parentGetSource Provides the source code for the
142
- * parent module.
143
- * @param {string } [params.ns='namespace'] A string identifier that will be
144
- * used as the namespace for the identifiers exported by the module.
145
- * @param {string } [params.defaultAs='default'] The name to give the default
146
- * identifier exported by the module (if one exists). This is really only
147
- * useful in a recursive situation where a transitive module's default export
148
- * 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.
149
126
*
150
- * @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.
151
129
*/
152
- async function processModule ( {
153
- srcUrl,
154
- context,
155
- parentGetSource,
156
- parentResolve,
157
- ns = 'namespace' ,
158
- defaultAs = 'default'
159
- } ) {
160
- const exportNames = await getExports ( {
161
- url : srcUrl ,
162
- context,
163
- parentLoad : parentGetSource ,
164
- defaultAs
165
- } )
166
- const imports = [ `import * as ${ ns } from ${ JSON . stringify ( srcUrl ) } ` ]
167
- const namespaces = [ ns ]
168
-
169
- // As we iterate found module exports we will add setter code blocks
170
- // to this map that will eventually be inserted into the shim module's
171
- // source code. We utilize a map in order to prevent duplicate exports.
172
- // As a consequence of default renaming, it is possible that a file named
173
- // `foo.mjs` which has `export function foo() {}` and `export default foo`
174
- // exports will result in the "foo" export being defined twice in our shim.
175
- // The map allows us to avoid this situation at the cost of losing the
176
- // 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 duplicates = new Set ( )
177
133
const setters = new Map ( )
178
134
135
+ const addSetter = ( name , setter ) => {
136
+ // When doing an `import *` duplicates become undefined, so do the same
137
+ if ( setters . has ( name ) ) {
138
+ duplicates . add ( name )
139
+ setters . delete ( name )
140
+ } else if ( ! duplicates . has ( name ) ) {
141
+ setters . set ( name , setter )
142
+ }
143
+ }
144
+
179
145
for ( const n of exportNames ) {
146
+ if ( n === 'default' && excludeDefault ) continue
147
+
180
148
if ( isStarExportLine ( n ) === true ) {
181
149
const [ , modFile ] = n . split ( '* from ' )
182
- const normalizedModName = normalizeModName ( modFile )
183
- const modName = Buffer . from ( modFile , 'hex' ) + Date . now ( ) + randomBytes ( 4 ) . toString ( 'hex' )
184
150
185
151
let modUrl
186
152
if ( isBareSpecifier ( modFile ) ) {
@@ -191,70 +157,28 @@ async function processModule ({
191
157
modUrl = new URL ( modFile , srcUrl ) . href
192
158
}
193
159
194
- const data = await processModule ( {
160
+ const setters = await processModule ( {
195
161
srcUrl : modUrl ,
196
162
context,
197
163
parentGetSource,
198
- parentResolve,
199
- ns : `$${ modName } ` ,
200
- defaultAs : normalizedModName
164
+ excludeDefault : true
201
165
} )
202
- Array . prototype . push . apply ( imports , data . imports )
203
- Array . prototype . push . apply ( namespaces , data . namespaces )
204
- for ( const [ k , v ] of data . setters . entries ( ) ) {
205
- setters . set ( k , v )
166
+ for ( const [ name , setter ] of setters . entries ( ) ) {
167
+ addSetter ( name , setter )
206
168
}
207
-
208
- continue
209
- }
210
-
211
- const matches = / ^ r e n a m e ( .+ ) a s ( .+ ) $ / . exec ( n )
212
- if ( matches !== null ) {
213
- // Transitive modules that export a default identifier need to have
214
- // that identifier renamed to the name of module. And our shim setter
215
- // needs to utilize that new name while being initialized from the
216
- // corresponding origin namespace.
217
- const renamedExport = matches [ 2 ]
218
- setters . set ( `$${ renamedExport } ${ ns } ` , `
219
- let $${ renamedExport } = ${ ns } .default
220
- export { $${ renamedExport } as ${ renamedExport } }
221
- set.${ renamedExport } = (v) => {
222
- $${ renamedExport } = v
169
+ } else {
170
+ addSetter ( n , `
171
+ let $${ n } = _.${ n }
172
+ export { $${ n } as ${ n } }
173
+ set.${ n } = (v) => {
174
+ $${ n } = v
223
175
return true
224
176
}
225
177
` )
226
- continue
227
178
}
228
-
229
- setters . set ( `$${ n } ` + ns , `
230
- let $${ n } = ${ ns } .${ n }
231
- export { $${ n } as ${ n } }
232
- set.${ n } = (v) => {
233
- $${ n } = v
234
- return true
235
- }
236
- ` )
237
179
}
238
180
239
- return { imports, namespaces, setters }
240
- }
241
-
242
- /**
243
- * Given a module name, e.g. 'foo-bar' or './foo-bar.js', normalize it to a
244
- * string that is a valid JavaScript identifier, e.g. `fooBar`. Normalization
245
- * means converting kebab-case to camelCase while removing any path tokens and
246
- * file extensions.
247
- *
248
- * @param {string } name The module name to normalize.
249
- *
250
- * @returns {string } The normalized identifier.
251
- */
252
- function normalizeModName ( name ) {
253
- return name
254
- . split ( '/' )
255
- . pop ( )
256
- . replace ( / ( .+ ) \. (?: j s | m j s ) $ / , '$1' )
257
- . replaceAll ( / ( - .) / g, x => x [ 1 ] . toUpperCase ( ) )
181
+ return setters
258
182
}
259
183
260
184
function addIitm ( url ) {
@@ -322,61 +246,25 @@ function createHook (meta) {
322
246
async function getSource ( url , context , parentGetSource ) {
323
247
if ( hasIitm ( url ) ) {
324
248
const realUrl = deleteIitm ( url )
325
- const { imports , namespaces , setters : mapSetters } = await processModule ( {
249
+ const setters = await processModule ( {
326
250
srcUrl : realUrl ,
327
251
context,
328
252
parentGetSource,
329
253
parentResolve : cachedResolve
330
254
} )
331
- const setters = Array . from ( mapSetters . values ( ) )
332
-
333
- // When we encounter modules that re-export all identifiers from other
334
- // modules, it is possible that the transitive modules export a default
335
- // identifier. Due to us having to merge all transitive modules into a
336
- // single common namespace, we need to recognize these default exports
337
- // and remap them to a name based on the module name. This prevents us
338
- // from overriding the top-level module's (the one actually being imported
339
- // by some source code) default export when we merge the namespaces.
340
- const renamedDefaults = setters
341
- . map ( s => {
342
- const matches = / l e t \$ ( .+ ) = ( \$ .+ ) \. d e f a u l t / . exec ( s )
343
- if ( matches === null ) return undefined
344
- return `_['${ matches [ 1 ] } '] = ${ matches [ 2 ] } .default`
345
- } )
346
- . filter ( s => s )
347
-
348
- // The for loops are how we merge namespaces into a common namespace that
349
- // can be proxied. We can't use a simple `Object.assign` style merging
350
- // because transitive modules can export a default identifier that would
351
- // override the desired default identifier. So we need to do manual
352
- // merging with some logic around default identifiers.
353
- //
354
- // Additionally, we need to make sure any renamed default exports in
355
- // transitive dependencies are added to the common namespace. This is
356
- // accomplished through the `renamedDefaults` array.
357
255
return {
358
256
source : `
359
257
import { register } from '${ iitmURL } '
360
- ${ imports . join ( '\n' ) }
258
+ import * as namespace from ${ JSON . stringify ( realUrl ) }
361
259
362
- const namespaces = [${ namespaces . join ( ', ' ) } ]
363
260
// Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
364
- const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } })
261
+ const _ = Object.assign(
262
+ Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } }),
263
+ namespace
264
+ )
365
265
const set = {}
366
266
367
- const primary = namespaces.shift()
368
- for (const [k, v] of Object.entries(primary)) {
369
- _[k] = v
370
- }
371
- for (const ns of namespaces) {
372
- for (const [k, v] of Object.entries(ns)) {
373
- if (k === 'default') continue
374
- _[k] = v
375
- }
376
- }
377
-
378
- ${ setters . join ( '\n' ) }
379
- ${ renamedDefaults . join ( '\n' ) }
267
+ ${ Array . from ( setters . values ( ) ) . join ( '\n' ) }
380
268
381
269
register(${ JSON . stringify ( realUrl ) } , _, set, ${ JSON . stringify ( specifiers . get ( realUrl ) ) } )
382
270
`
0 commit comments