@@ -27,6 +27,7 @@ import { transformWithEsbuild } from '../plugins/esbuild'
27
27
import { ESBUILD_MODULES_TARGET } from '../constants'
28
28
import { resolvePackageData } from '../packages'
29
29
import type { ViteDevServer } from '../server'
30
+ import type { Logger } from '../logger'
30
31
import { esbuildCjsExternalPlugin , esbuildDepPlugin } from './esbuildDepPlugin'
31
32
import { scanImports } from './scan'
32
33
export {
@@ -359,6 +360,11 @@ export async function loadCachedDepOptimizationMetadata(
359
360
360
361
const depsCacheDir = getDepsCacheDir ( config , ssr )
361
362
363
+ // If the lock timed out, we cancel and return undefined
364
+ if ( ! ( await waitOptimizerWriteLock ( depsCacheDir , config . logger ) ) ) {
365
+ return
366
+ }
367
+
362
368
if ( ! force ) {
363
369
let cachedMetadata : DepOptimizationMetadata | undefined
364
370
try {
@@ -587,50 +593,82 @@ export function runOptimizeDeps(
587
593
`Dependencies bundled in ${ ( performance . now ( ) - start ) . toFixed ( 2 ) } ms` ,
588
594
)
589
595
590
- return {
591
- metadata,
592
- async commit ( ) {
593
- // Write this run of pre-bundled dependencies to the deps cache
594
-
595
- // Get a list of old files in the deps directory to delete the stale ones
596
- const oldFilesPaths : string [ ] = [ ]
597
- if ( ! fs . existsSync ( depsCacheDir ) ) {
598
- fs . mkdirSync ( depsCacheDir , { recursive : true } )
599
- } else {
600
- oldFilesPaths . push (
601
- ...( await fsp . readdir ( depsCacheDir ) ) . map ( ( f ) =>
602
- path . join ( depsCacheDir , f ) ,
603
- ) ,
604
- )
605
- }
596
+ // Write this run of pre-bundled dependencies to the deps cache
597
+ async function commitFiles ( ) {
598
+ // Get a list of old files in the deps directory to delete the stale ones
599
+ const oldFilesPaths : string [ ] = [ ]
600
+ // File used to tell other processes that we're writing the deps cache directory
601
+ const writingFilePath = path . resolve ( depsCacheDir , '_writing' )
602
+
603
+ if (
604
+ ! fs . existsSync ( depsCacheDir ) ||
605
+ ! ( await waitOptimizerWriteLock ( depsCacheDir , config . logger ) ) // unlock timed out
606
+ ) {
607
+ fs . mkdirSync ( depsCacheDir , { recursive : true } )
608
+ fs . writeFileSync ( writingFilePath , '' )
609
+ } else {
610
+ fs . writeFileSync ( writingFilePath , '' )
611
+ oldFilesPaths . push (
612
+ ...( await fsp . readdir ( depsCacheDir ) ) . map ( ( f ) =>
613
+ path . join ( depsCacheDir , f ) ,
614
+ ) ,
615
+ )
616
+ }
606
617
607
- const newFilesPaths = new Set < string > ( )
608
- const files : Promise < void > [ ] = [ ]
609
- const write = ( filePath : string , content : string ) => {
610
- newFilesPaths . add ( filePath )
611
- files . push ( fsp . writeFile ( filePath , content ) )
612
- }
618
+ const newFilesPaths = new Set < string > ( )
619
+ newFilesPaths . add ( writingFilePath )
620
+ const files : Promise < void > [ ] = [ ]
621
+ const write = ( filePath : string , content : string | Uint8Array ) => {
622
+ newFilesPaths . add ( filePath )
623
+ files . push ( fsp . writeFile ( filePath , content ) )
624
+ }
613
625
626
+ path . join ( depsCacheDir , '_metadata.json' ) ,
614
627
// a hint for Node.js
615
628
// all files in the cache directory should be recognized as ES modules
616
629
write (
617
630
path . resolve ( depsCacheDir , 'package.json' ) ,
618
631
'{\n "type": "module"\n}\n' ,
619
632
)
620
633
621
- write (
622
- path . join ( depsCacheDir , '_metadata.json' ) ,
623
- stringifyDepsOptimizerMetadata ( metadata , depsCacheDir ) ,
624
- )
634
+ write (
635
+ path . join ( depsCacheDir , '_metadata.json' ) ,
636
+ stringifyDepsOptimizerMetadata ( metadata , depsCacheDir ) ,
637
+ )
625
638
626
- for ( const outputFile of result . outputFiles ! )
627
- write ( outputFile . path , outputFile . text )
639
+ for ( const outputFile of result . outputFiles ! )
640
+ write ( outputFile . path , outputFile . contents )
628
641
629
- // Clean up old files in the background
630
- for ( const filePath of oldFilesPaths )
631
- if ( ! newFilesPaths . has ( filePath ) ) fs . unlink ( filePath , ( ) => { } ) // ignore errors
642
+ // Clean up old files in the background
643
+ for ( const filePath of oldFilesPaths )
644
+ if ( ! newFilesPaths . has ( filePath ) ) fs . unlink ( filePath , ( ) => { } ) // ignore errors
645
+
646
+ await Promise . all ( files )
647
+
648
+ // Successful write
649
+ fsp . unlink ( writingFilePath )
650
+
651
+ setTimeout ( ( ) => {
652
+ // Free up memory, these files aren't going to be re-requested because
653
+ // the requests are cached. If they do, then let them read from disk.
654
+ optimizedDepsCache . delete ( metadata )
655
+ } , 5000 )
656
+ }
632
657
633
- await Promise . all ( files )
658
+ return {
659
+ metadata,
660
+ async commit ( ) {
661
+ // Keep the output files in memory while we write them to disk in the
662
+ // background. These files are going to be sent right away to the browser
663
+ optimizedDepsCache . set (
664
+ metadata ,
665
+ new Map (
666
+ result . outputFiles ! . map ( ( f ) => [ normalizePath ( f . path ) , f . text ] ) ,
667
+ ) ,
668
+ )
669
+
670
+ // No need to wait, files are written in the background
671
+ setTimeout ( commitFiles , 0 )
634
672
} ,
635
673
cancel : ( ) => { } ,
636
674
}
@@ -1291,3 +1329,64 @@ export async function optimizedDepNeedsInterop(
1291
1329
}
1292
1330
return depInfo ?. needsInterop
1293
1331
}
1332
+
1333
+ const optimizedDepsCache = new WeakMap <
1334
+ DepOptimizationMetadata ,
1335
+ Map < string , string >
1336
+ > ( )
1337
+ export async function loadOptimizedDep (
1338
+ file : string ,
1339
+ depsOptimizer : DepsOptimizer ,
1340
+ ) : Promise < string > {
1341
+ const outputFiles = optimizedDepsCache . get ( depsOptimizer . metadata )
1342
+ if ( outputFiles ) {
1343
+ const outputFile = outputFiles . get ( file )
1344
+ if ( outputFile ) return outputFile
1345
+ }
1346
+ return fsp . readFile ( file , 'utf-8' )
1347
+ }
1348
+
1349
+ /**
1350
+ * Processes that write to the deps cache directory adds a `_writing` lock to
1351
+ * inform other processes of so. So before doing any work on it, they can wait
1352
+ * for the file to be removed to know it's ready.
1353
+ *
1354
+ * Returns true if successfully waited for unlock, false if lock timed out.
1355
+ */
1356
+ async function waitOptimizerWriteLock ( depsCacheDir : string , logger : Logger ) {
1357
+ const writingPath = path . join ( depsCacheDir , '_writing' )
1358
+ const tryAgainMs = 100
1359
+
1360
+ // if _writing exist, we wait for a maximum of 500ms before assuming something
1361
+ // is not right
1362
+ let maxWaitTime = 500
1363
+ let waited = 0
1364
+ let filesLength : number
1365
+
1366
+ while ( fs . existsSync ( writingPath ) ) {
1367
+ // on the first run, we check the number of files it started with for later use
1368
+ filesLength ??= ( await fsp . readdir ( depsCacheDir ) ) . length
1369
+
1370
+ await new Promise ( ( r ) => setTimeout ( r , tryAgainMs ) )
1371
+ waited += tryAgainMs
1372
+
1373
+ if ( waited >= maxWaitTime ) {
1374
+ const newFilesLength = ( await fsp . readdir ( depsCacheDir ) ) . length
1375
+
1376
+ // after 500ms, if the number of files is the same, assume previous process
1377
+ // terminated and didn't cleanup `_writing` lock. clear the directory.
1378
+ if ( filesLength === newFilesLength ) {
1379
+ logger . info ( 'Outdated deps cache, forcing re-optimization...' )
1380
+ await fsp . rm ( depsCacheDir , { recursive : true , force : true } )
1381
+ return false
1382
+ }
1383
+ // new files were saved, wait a bit longer to decide again.
1384
+ else {
1385
+ maxWaitTime += 500
1386
+ filesLength = newFilesLength
1387
+ }
1388
+ }
1389
+ }
1390
+
1391
+ return true
1392
+ }
0 commit comments