@@ -40,8 +40,37 @@ export const chmod: (path: string, mode: number | string) => Promise<void> = pro
40
40
export const link : ( src : string , dst : string ) => Promise < fs . Stats > = promisify ( fs . link ) ;
41
41
export const glob : ( path : string , options ?: Object ) => Promise < Array < string >> = promisify ( globModule ) ;
42
42
43
- const CONCURRENT_QUEUE_ITEMS = 4 ;
44
-
43
+ // fs.copyFile uses the native file copying instructions on the system, performing much better
44
+ // than any JS-based solution and consumes fewer resources. Repeated testing to fine tune the
45
+ // concurrency level revealed 128 as the sweet spot on a quad-core, 16 CPU Intel system with SSD.
46
+ const CONCURRENT_QUEUE_ITEMS = fs . copyFile ? 128 : 4 ;
47
+
48
+ const open : ( path : string , flags : string | number , mode : number ) => Promise < number > = promisify(fs.open);
49
+ const close: (fd: number) => Promise < void > = promisify(fs.close);
50
+ const write: (
51
+ fd: number,
52
+ buffer: Buffer,
53
+ offset: ?number,
54
+ length: ?number,
55
+ position: ?number,
56
+ ) => Promise < void > = promisify(fs.write);
57
+ const futimes: (fd: number, atime: number, mtime: number) => Promise < void > = promisify(fs.futimes);
58
+ const copyFile: (src: string, dest: string, flags: number, data: CopyFileAction) => Promise < void > = fs.copyFile
59
+ ? // Don't use `promisify` to avoid passing the last, argument `data`, to the native method
60
+ (src, dest, flags, data) =>
61
+ new Promise ( ( resolve , reject ) => fs . copyFile ( src , dest , flags , err => ( err ? reject ( err ) : resolve ( err ) ) ) )
62
+ : async ( src , dest , flags , data ) => {
63
+ // Use open -> write -> futimes -> close sequence to avoid opening the file twice:
64
+ // one with writeFile and one with utimes
65
+ const fd = await open ( dest , 'w' , data . mode ) ;
66
+ try {
67
+ const buffer = await readFileBuffer ( src ) ;
68
+ await write ( fd , buffer , 0 , buffer . length ) ;
69
+ await futimes ( fd , data . atime , data . mtime ) ;
70
+ } finally {
71
+ await close ( fd ) ;
72
+ }
73
+ } ;
45
74
const fsSymlink : ( target : string , path : string , type ?: 'dir ' | 'file ' | 'junction ') => Promise<void> = promisify(
46
75
fs . symlink ,
47
76
) ;
@@ -61,7 +90,6 @@ export type CopyQueueItem = {
61
90
type CopyQueue = Array < CopyQueueItem > ;
62
91
63
92
type CopyFileAction = {
64
- type : 'file' ,
65
93
src : string ,
66
94
dest : string ,
67
95
atime : number ,
@@ -70,19 +98,21 @@ type CopyFileAction = {
70
98
} ;
71
99
72
100
type LinkFileAction = {
73
- type : 'link' ,
74
101
src : string ,
75
102
dest : string ,
76
103
removeDest : boolean ,
77
104
} ;
78
105
79
106
type CopySymlinkAction = {
80
- type : 'symlink' ,
81
107
dest : string ,
82
108
linkname : string ,
83
109
} ;
84
110
85
- type CopyActions = Array < CopyFileAction | CopySymlinkAction | LinkFileAction > ;
111
+ type CopyActions = {
112
+ file : Array < CopyFileAction > ,
113
+ symlink : Array < CopySymlinkAction > ,
114
+ link : Array < LinkFileAction > ,
115
+ } ;
86
116
87
117
type CopyOptions = {
88
118
onProgress : ( dest : string ) => void ,
@@ -154,7 +184,11 @@ async function buildActionsForCopy(
154
184
events.onStart(queue.length);
155
185
156
186
// start building actions
157
- const actions: CopyActions = [];
187
+ const actions: CopyActions = {
188
+ file : [ ] ,
189
+ symlink : [ ] ,
190
+ link : [ ] ,
191
+ } ;
158
192
159
193
// custom concurrency logic as we're always executing stacks of CONCURRENT_QUEUE_ITEMS queue items
160
194
// at a time due to the requirement to push items onto the queue
@@ -180,7 +214,7 @@ async function buildActionsForCopy(
180
214
return actions ;
181
215
182
216
//
183
- async function build ( data ) : Promise < void > {
217
+ async function build ( data : CopyQueueItem ) : Promise < void > {
184
218
const { src , dest , type } = data;
185
219
const onFresh = data.onFresh || noop;
186
220
const onDone = data.onDone || noop;
@@ -196,8 +230,7 @@ async function buildActionsForCopy(
196
230
if (type === 'symlink') {
197
231
await mkdirp ( path . dirname ( dest ) ) ;
198
232
onFresh ( ) ;
199
- actions . push ( {
200
- type : 'symlink' ,
233
+ actions . symlink . push ( {
201
234
dest,
202
235
linkname : src ,
203
236
} ) ;
@@ -288,10 +321,9 @@ async function buildActionsForCopy(
288
321
if (srcStat.isSymbolicLink()) {
289
322
onFresh ( ) ;
290
323
const linkname = await readlink ( src ) ;
291
- actions . push ( {
324
+ actions . symlink . push ( {
292
325
dest,
293
326
linkname,
294
- type : 'symlink' ,
295
327
} ) ;
296
328
onDone ( ) ;
297
329
} else if (srcStat.isDirectory()) {
@@ -326,8 +358,7 @@ async function buildActionsForCopy(
326
358
}
327
359
} else if ( srcStat . isFile ( ) ) {
328
360
onFresh ( ) ;
329
- actions . push ( {
330
- type : 'file' ,
361
+ actions . file . push ( {
331
362
src,
332
363
dest,
333
364
atime : srcStat . atime ,
@@ -352,18 +383,20 @@ async function buildActionsForHardlink(
352
383
353
384
// initialise events
354
385
for ( const item of queue ) {
355
- const onDone = item . onDone ;
386
+ const onDone = item . onDone || noop ;
356
387
item . onDone = ( ) => {
357
388
events . onProgress ( item . dest ) ;
358
- if ( onDone ) {
359
- onDone ( ) ;
360
- }
389
+ onDone ( ) ;
361
390
} ;
362
391
}
363
392
events.onStart(queue.length);
364
393
365
394
// start building actions
366
- const actions: CopyActions = [];
395
+ const actions: CopyActions = {
396
+ file : [ ] ,
397
+ symlink : [ ] ,
398
+ link : [ ] ,
399
+ } ;
367
400
368
401
// custom concurrency logic as we're always executing stacks of CONCURRENT_QUEUE_ITEMS queue items
369
402
// at a time due to the requirement to push items onto the queue
@@ -389,7 +422,7 @@ async function buildActionsForHardlink(
389
422
return actions ;
390
423
391
424
//
392
- async function build ( data ) : Promise < void > {
425
+ async function build ( data : CopyQueueItem ) : Promise < void > {
393
426
const { src , dest } = data;
394
427
const onFresh = data.onFresh || noop;
395
428
const onDone = data.onDone || noop;
@@ -474,8 +507,7 @@ async function buildActionsForHardlink(
474
507
if ( srcStat . isSymbolicLink ( ) ) {
475
508
onFresh ( ) ;
476
509
const linkname = await readlink ( src ) ;
477
- actions . push ( {
478
- type : 'symlink' ,
510
+ actions . symlink . push ( {
479
511
dest,
480
512
linkname,
481
513
} ) ;
@@ -510,8 +542,7 @@ async function buildActionsForHardlink(
510
542
}
511
543
} else if ( srcStat . isFile ( ) ) {
512
544
onFresh ( ) ;
513
- actions . push ( {
514
- type : 'link' ,
545
+ actions . link . push ( {
515
546
src,
516
547
dest,
517
548
removeDest : destExists ,
@@ -527,6 +558,23 @@ export function copy(src: string, dest: string, reporter: Reporter): Promise<voi
527
558
return copyBulk ( [ { src, dest} ] , reporter ) ;
528
559
}
529
560
561
+ /**
562
+ * Unlinks the destination to force a recreation. This is needed on case-insensitive file systems
563
+ * to force the correct naming when the filename has changed only in charater-casing. (Jest -> jest ) .
564
+ * It also calls a cleanup function once it is done .
565
+ *
566
+ * `data ` contains target file attributes like mode , atime and mtime . Built - in copyFile copies these
567
+ * automatically but our polyfill needs the do this manually , thus needs the info .
568
+ * /
569
+ const safeCopyFile = async function ( data : CopyFileAction , cleanup : ( ) => mixed ) : Promise < void > {
570
+ try {
571
+ await unlink ( data . dest ) ;
572
+ await copyFile ( data . src , data . dest , 0 , data ) ;
573
+ } finally {
574
+ cleanup ( ) ;
575
+ }
576
+ } ;
577
+
530
578
export async function copyBulk (
531
579
queue : CopyQueue ,
532
580
reporter : Reporter ,
@@ -547,57 +595,31 @@ export async function copyBulk(
547
595
} ;
548
596
549
597
const actions : CopyActions = await buildActionsForCopy ( queue , events , events . possibleExtraneous , reporter ) ;
550
- events . onStart ( actions . length ) ;
598
+ events . onStart ( actions . file . length + actions . symlink . length + actions . link . length ) ;
551
599
552
- const fileActions : Array < CopyFileAction > = ( actions . filter ( action => action . type === ' file' ) : any ) ;
600
+ const fileActions : Array < CopyFileAction > = actions . file ;
553
601
554
- const currentlyWriting : { [ dest : string ] : Promise < void > } = { } ;
602
+ const currentlyWriting : Map < string , Promise < void >> = new Map ( ) ;
555
603
556
604
await promise . queue (
557
605
fileActions ,
558
- async ( data ) : Promise < void > => {
559
- let writePromise : Promise < void > ;
560
- while ( ( writePromise = currentlyWriting [ data . dest ] ) ) {
561
- await writePromise ;
606
+ ( data : CopyFileAction ) : Promise < void > => {
607
+ const writePromise = currentlyWriting . get ( data . dest ) ;
608
+ if ( writePromise ) {
609
+ return writePromise ;
562
610
}
563
611
564
- const cleanup = ( ) => delete currentlyWriting [ data . dest ] ;
565
612
reporter . verbose ( reporter . lang ( 'verboseFileCopy' , data . src , data . dest ) ) ;
566
- return ( currentlyWriting [ data . dest ] = readFileBuffer ( data . src )
567
- . then ( async d => {
568
- // we need to do this because of case-insensitive filesystems, which wouldn't properly
569
- // change the file name in case of a file being renamed
570
- await unlink ( data . dest ) ;
571
-
572
- return writeFile ( data . dest , d , { mode : data . mode } ) ;
573
- } )
574
- . then ( ( ) => {
575
- return new Promise ( ( resolve , reject ) => {
576
- fs . utimes ( data . dest , data . atime , data . mtime , err => {
577
- if ( err ) {
578
- reject ( err ) ;
579
- } else {
580
- resolve ( ) ;
581
- }
582
- } ) ;
583
- } ) ;
584
- } )
585
- . then (
586
- ( ) => {
587
- events . onProgress ( data . dest ) ;
588
- cleanup ( ) ;
589
- } ,
590
- err => {
591
- cleanup ( ) ;
592
- throw err ;
593
- } ,
594
- ) ) ;
613
+ const copier = safeCopyFile ( data , ( ) => currentlyWriting . delete ( data . dest ) ) ;
614
+ currentlyWriting . set ( data . dest , copier ) ;
615
+ events . onProgress ( data . dest ) ;
616
+ return copier ;
595
617
} ,
596
618
CONCURRENT_QUEUE_ITEMS ,
597
619
) ;
598
620
599
621
// we need to copy symlinks last as they could reference files we were copying
600
- const symlinkActions : Array < CopySymlinkAction > = ( actions . filter ( action => action . type === ' symlink' ) : any ) ;
622
+ const symlinkActions : Array < CopySymlinkAction > = actions . symlink ;
601
623
await promise . queue ( symlinkActions , ( data ) : Promise < void > => {
602
624
const linkname = path . resolve ( path . dirname ( data . dest ) , data . linkname ) ;
603
625
reporter . verbose ( reporter . lang ( 'verboseFileSymlink' , data . dest , linkname ) ) ;
@@ -624,9 +646,9 @@ export async function hardlinkBulk(
624
646
} ;
625
647
626
648
const actions : CopyActions = await buildActionsForHardlink ( queue , events , events . possibleExtraneous , reporter ) ;
627
- events . onStart ( actions . length ) ;
649
+ events . onStart ( actions . file . length + actions . symlink . length + actions . link . length ) ;
628
650
629
- const fileActions : Array < LinkFileAction > = ( actions . filter ( action => action . type === ' link' ) : any ) ;
651
+ const fileActions : Array < LinkFileAction > = actions . link ;
630
652
631
653
await promise . queue (
632
654
fileActions ,
@@ -641,7 +663,7 @@ export async function hardlinkBulk(
641
663
) ;
642
664
643
665
// we need to copy symlinks last as they could reference files we were copying
644
- const symlinkActions : Array < CopySymlinkAction > = ( actions . filter ( action => action . type === ' symlink' ) : any ) ;
666
+ const symlinkActions : Array < CopySymlinkAction > = actions.symlink;
645
667
await promise.queue(symlinkActions, (data): Promise< void > => {
646
668
const linkname = path . resolve ( path . dirname ( data . dest ) , data . linkname ) ;
647
669
reporter . verbose ( reporter . lang ( 'verboseFileSymlink' , data . dest , linkname ) ) ;
@@ -836,7 +858,7 @@ export async function writeFilePreservingEol(path: string, data: string): Promis
836
858
if ( eol !== '\n' ) {
837
859
data = data . replace ( / \n / g, eol ) ;
838
860
}
839
- await promisify(fs. writeFile) (path, data);
861
+ await writeFile(path, data);
840
862
}
841
863
842
864
export async function hardlinksWork ( dir : string ) : Promise < boolean > {
0 commit comments