@@ -34,6 +34,8 @@ export type ExportsData = ReturnType<typeof parse> & {
34
34
// es-module-lexer has a facade detection but isn't always accurate for our
35
35
// use case when the module has default export
36
36
hasReExports ?: true
37
+ // hint if the dep requires loading as jsx
38
+ jsxLoader ?: true
37
39
}
38
40
39
41
export interface OptimizedDeps {
@@ -64,6 +66,12 @@ export interface DepOptimizationOptions {
64
66
* cannot be globs).
65
67
*/
66
68
exclude ?: string [ ]
69
+ /**
70
+ * Force ESM interop when importing for these dependencies. Some legacy
71
+ * packages advertise themselves as ESM but use `require` internally
72
+ * @experimental
73
+ */
74
+ needsInterop ?: string [ ]
67
75
/**
68
76
* Options to pass to esbuild during the dep scanning and optimization
69
77
*
@@ -134,6 +142,11 @@ export interface OptimizedDepInfo {
134
142
* but the bundles may not yet be saved to disk
135
143
*/
136
144
processing ?: Promise < void >
145
+ /**
146
+ * ExportData cache, discovered deps will parse the src entry to get exports
147
+ * data used both to define if interop is needed and when pre-bundling
148
+ */
149
+ exportsData ?: Promise < ExportsData >
137
150
}
138
151
139
152
export interface DepOptimizationMetadata {
@@ -297,12 +310,13 @@ export async function discoverProjectDependencies(
297
310
)
298
311
const discovered : Record < string , OptimizedDepInfo > = { }
299
312
for ( const id in deps ) {
300
- const entry = deps [ id ]
313
+ const src = deps [ id ]
301
314
discovered [ id ] = {
302
315
id,
303
316
file : getOptimizedDepPath ( id , config ) ,
304
- src : entry ,
305
- browserHash : browserHash
317
+ src,
318
+ browserHash : browserHash ,
319
+ exportsData : extractExportsData ( src , config )
306
320
}
307
321
}
308
322
return discovered
@@ -368,17 +382,24 @@ export async function runOptimizeDeps(
368
382
369
383
const qualifiedIds = Object . keys ( depsInfo )
370
384
371
- if ( ! qualifiedIds . length ) {
372
- return {
373
- metadata,
374
- commit ( ) {
375
- // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps`
376
- return commitProcessingDepsCacheSync ( )
377
- } ,
378
- cancel
385
+ const processingResult : DepOptimizationResult = {
386
+ metadata,
387
+ async commit ( ) {
388
+ // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps`
389
+ // Processing is done, we can now replace the depsCacheDir with processingCacheDir
390
+ // Rewire the file paths from the temporal processing dir to the final deps cache dir
391
+ await removeDir ( depsCacheDir )
392
+ await renameDir ( processingCacheDir , depsCacheDir )
393
+ } ,
394
+ cancel ( ) {
395
+ fs . rmSync ( processingCacheDir , { recursive : true , force : true } )
379
396
}
380
397
}
381
398
399
+ if ( ! qualifiedIds . length ) {
400
+ return processingResult
401
+ }
402
+
382
403
// esbuild generates nested directory output with lowest common ancestor base
383
404
// this is unpredictable and makes it difficult to analyze entry / output
384
405
// mapping. So what we do here is:
@@ -392,51 +413,20 @@ export async function runOptimizeDeps(
392
413
const { plugins = [ ] , ...esbuildOptions } =
393
414
config . optimizeDeps ?. esbuildOptions ?? { }
394
415
395
- await init
396
416
for ( const id in depsInfo ) {
397
- const flatId = flattenId ( id )
398
- const filePath = ( flatIdDeps [ flatId ] = depsInfo [ id ] . src ! )
399
- let exportsData : ExportsData
400
- if ( config . optimizeDeps . extensions ?. some ( ( ext ) => filePath . endsWith ( ext ) ) ) {
401
- // For custom supported extensions, build the entry file to transform it into JS,
402
- // and then parse with es-module-lexer. Note that the `bundle` option is not `true`,
403
- // so only the entry file is being transformed.
404
- const result = await build ( {
405
- ...esbuildOptions ,
406
- plugins,
407
- entryPoints : [ filePath ] ,
408
- write : false ,
409
- format : 'esm'
410
- } )
411
- exportsData = parse ( result . outputFiles [ 0 ] . text ) as ExportsData
412
- } else {
413
- const entryContent = fs . readFileSync ( filePath , 'utf-8' )
414
- try {
415
- exportsData = parse ( entryContent ) as ExportsData
416
- } catch {
417
- const loader = esbuildOptions . loader ?. [ path . extname ( filePath ) ] || 'jsx'
418
- debug (
419
- `Unable to parse dependency: ${ id } . Trying again with a ${ loader } transform.`
420
- )
421
- const transformed = await transformWithEsbuild ( entryContent , filePath , {
422
- loader
423
- } )
424
- // Ensure that optimization won't fail by defaulting '.js' to the JSX parser.
425
- // This is useful for packages such as Gatsby.
426
- esbuildOptions . loader = {
427
- '.js' : 'jsx' ,
428
- ...esbuildOptions . loader
429
- }
430
- exportsData = parse ( transformed . code ) as ExportsData
431
- }
432
- for ( const { ss, se } of exportsData [ 0 ] ) {
433
- const exp = entryContent . slice ( ss , se )
434
- if ( / e x p o r t \s + \* \s + f r o m / . test ( exp ) ) {
435
- exportsData . hasReExports = true
436
- }
417
+ const src = depsInfo [ id ] . src !
418
+ const exportsData = await ( depsInfo [ id ] . exportsData ??
419
+ extractExportsData ( src , config ) )
420
+ if ( exportsData . jsxLoader ) {
421
+ // Ensure that optimization won't fail by defaulting '.js' to the JSX parser.
422
+ // This is useful for packages such as Gatsby.
423
+ esbuildOptions . loader = {
424
+ '.js' : 'jsx' ,
425
+ ...esbuildOptions . loader
437
426
}
438
427
}
439
-
428
+ const flatId = flattenId ( id )
429
+ flatIdDeps [ flatId ] = src
440
430
idToExports [ id ] = exportsData
441
431
flatIdToExports [ flatId ] = exportsData
442
432
}
@@ -483,15 +473,18 @@ export async function runOptimizeDeps(
483
473
for ( const id in depsInfo ) {
484
474
const output = esbuildOutputFromId ( meta . outputs , id , processingCacheDir )
485
475
476
+ const { exportsData, ...info } = depsInfo [ id ]
486
477
addOptimizedDepInfo ( metadata , 'optimized' , {
487
- ...depsInfo [ id ] ,
488
- needsInterop : needsInterop ( id , idToExports [ id ] , output ) ,
478
+ ...info ,
489
479
// We only need to hash the output.imports in to check for stability, but adding the hash
490
480
// and file path gives us a unique hash that may be useful for other things in the future
491
481
fileHash : getHash (
492
482
metadata . hash + depsInfo [ id ] . file + JSON . stringify ( output . imports )
493
483
) ,
494
- browserHash : metadata . browserHash
484
+ browserHash : metadata . browserHash ,
485
+ // After bundling we have more information and can warn the user about legacy packages
486
+ // that require manual configuration
487
+ needsInterop : needsInterop ( config , id , idToExports [ id ] , output )
495
488
} )
496
489
}
497
490
@@ -522,25 +515,7 @@ export async function runOptimizeDeps(
522
515
523
516
debug ( `deps bundled in ${ ( performance . now ( ) - start ) . toFixed ( 2 ) } ms` )
524
517
525
- return {
526
- metadata,
527
- commit ( ) {
528
- // Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync
529
- return commitProcessingDepsCacheSync ( )
530
- } ,
531
- cancel
532
- }
533
-
534
- async function commitProcessingDepsCacheSync ( ) {
535
- // Processing is done, we can now replace the depsCacheDir with processingCacheDir
536
- // Rewire the file paths from the temporal processing dir to the final deps cache dir
537
- await removeDir ( depsCacheDir )
538
- await renameDir ( processingCacheDir , depsCacheDir )
539
- }
540
-
541
- function cancel ( ) {
542
- fs . rmSync ( processingCacheDir , { recursive : true , force : true } )
543
- }
518
+ return processingResult
544
519
}
545
520
546
521
export async function findKnownImports (
@@ -735,17 +710,71 @@ function esbuildOutputFromId(
735
710
]
736
711
}
737
712
713
+ export async function extractExportsData (
714
+ filePath : string ,
715
+ config : ResolvedConfig
716
+ ) : Promise < ExportsData > {
717
+ await init
718
+ let exportsData : ExportsData
719
+
720
+ const esbuildOptions = config . optimizeDeps ?. esbuildOptions ?? { }
721
+ if ( config . optimizeDeps . extensions ?. some ( ( ext ) => filePath . endsWith ( ext ) ) ) {
722
+ // For custom supported extensions, build the entry file to transform it into JS,
723
+ // and then parse with es-module-lexer. Note that the `bundle` option is not `true`,
724
+ // so only the entry file is being transformed.
725
+ const result = await build ( {
726
+ ...esbuildOptions ,
727
+ entryPoints : [ filePath ] ,
728
+ write : false ,
729
+ format : 'esm'
730
+ } )
731
+ exportsData = parse ( result . outputFiles [ 0 ] . text ) as ExportsData
732
+ } else {
733
+ const entryContent = fs . readFileSync ( filePath , 'utf-8' )
734
+ try {
735
+ exportsData = parse ( entryContent ) as ExportsData
736
+ } catch {
737
+ const loader = esbuildOptions . loader ?. [ path . extname ( filePath ) ] || 'jsx'
738
+ debug (
739
+ `Unable to parse: ${ filePath } .\n Trying again with a ${ loader } transform.`
740
+ )
741
+ const transformed = await transformWithEsbuild ( entryContent , filePath , {
742
+ loader
743
+ } )
744
+ // Ensure that optimization won't fail by defaulting '.js' to the JSX parser.
745
+ // This is useful for packages such as Gatsby.
746
+ esbuildOptions . loader = {
747
+ '.js' : 'jsx' ,
748
+ ...esbuildOptions . loader
749
+ }
750
+ exportsData = parse ( transformed . code ) as ExportsData
751
+ exportsData . jsxLoader = true
752
+ }
753
+ for ( const { ss, se } of exportsData [ 0 ] ) {
754
+ const exp = entryContent . slice ( ss , se )
755
+ if ( / e x p o r t \s + \* \s + f r o m / . test ( exp ) ) {
756
+ exportsData . hasReExports = true
757
+ }
758
+ }
759
+ }
760
+ return exportsData
761
+ }
762
+
738
763
// https://github.com/vitejs/vite/issues/1724#issuecomment-767619642
739
764
// a list of modules that pretends to be ESM but still uses `require`.
740
765
// this causes esbuild to wrap them as CJS even when its entry appears to be ESM.
741
766
const KNOWN_INTEROP_IDS = new Set ( [ 'moment' ] )
742
767
743
768
function needsInterop (
769
+ config : ResolvedConfig ,
744
770
id : string ,
745
771
exportsData : ExportsData ,
746
- output : { exports : string [ ] }
772
+ output ? : { exports : string [ ] }
747
773
) : boolean {
748
- if ( KNOWN_INTEROP_IDS . has ( id ) ) {
774
+ if (
775
+ config . optimizeDeps ?. needsInterop ?. includes ( id ) ||
776
+ KNOWN_INTEROP_IDS . has ( id )
777
+ ) {
749
778
return true
750
779
}
751
780
const [ imports , exports ] = exportsData
@@ -754,16 +783,19 @@ function needsInterop(
754
783
return true
755
784
}
756
785
757
- // if a peer dependency used require() on a ESM dependency, esbuild turns the
758
- // ESM dependency's entry chunk into a single default export... detect
759
- // such cases by checking exports mismatch, and force interop.
760
- const generatedExports : string [ ] = output . exports
761
-
762
- if (
763
- ! generatedExports ||
764
- ( isSingleDefaultExport ( generatedExports ) && ! isSingleDefaultExport ( exports ) )
765
- ) {
766
- return true
786
+ if ( output ) {
787
+ // if a peer dependency used require() on a ESM dependency, esbuild turns the
788
+ // ESM dependency's entry chunk into a single default export... detect
789
+ // such cases by checking exports mismatch, and force interop.
790
+ const generatedExports : string [ ] = output . exports
791
+
792
+ if (
793
+ ! generatedExports ||
794
+ ( isSingleDefaultExport ( generatedExports ) &&
795
+ ! isSingleDefaultExport ( exports ) )
796
+ ) {
797
+ return true
798
+ }
767
799
}
768
800
return false
769
801
}
@@ -846,14 +878,17 @@ function findOptimizedDepInfoInRecord(
846
878
847
879
export async function optimizedDepNeedsInterop (
848
880
metadata : DepOptimizationMetadata ,
849
- file : string
881
+ file : string ,
882
+ config : ResolvedConfig
850
883
) : Promise < boolean | undefined > {
851
884
const depInfo = optimizedDepInfoFromFile ( metadata , file )
852
-
853
- if ( ! depInfo ) return undefined
854
-
855
- // Wait until the dependency has been pre-bundled
856
- await depInfo . processing
857
-
885
+ if ( depInfo ?. src && depInfo . needsInterop === undefined ) {
886
+ depInfo . exportsData ??= extractExportsData ( depInfo . src , config )
887
+ depInfo . needsInterop = needsInterop (
888
+ config ,
889
+ depInfo . id ,
890
+ await depInfo . exportsData
891
+ )
892
+ }
858
893
return depInfo ?. needsInterop
859
894
}
0 commit comments