@@ -16,8 +16,8 @@ const {
1616 StringPrototypeEndsWith,
1717} = primordials ;
1818
19- const { lstatSync, readdirSync } = require ( 'fs' ) ;
20- const { lstat, readdir } = require ( 'fs/promises' ) ;
19+ const { lstatSync, readdirSync, statSync } = require ( 'fs' ) ;
20+ const { lstat, readdir, stat } = require ( 'fs/promises' ) ;
2121const { join, resolve, basename, isAbsolute, dirname } = require ( 'path' ) ;
2222
2323const {
@@ -48,28 +48,46 @@ function lazyMinimatch() {
4848
4949/**
5050 * @param {string } path
51+ * @param {boolean } followSymlinks
5152 * @returns {Promise<DirentFromStats|null> }
5253 */
53- async function getDirent ( path ) {
54- let stat ;
54+ async function getDirent ( path , followSymlinks = false ) {
55+ let statResult ;
5556 try {
56- stat = await lstat ( path ) ;
57+ statResult = await lstat ( path ) ;
58+ // If it's a symlink and followSymlinks is true, use stat to follow it
59+ if ( followSymlinks && statResult . isSymbolicLink ( ) ) {
60+ try {
61+ statResult = await stat ( path ) ;
62+ } catch {
63+ // If stat fails (e.g., broken symlink), keep the lstat result
64+ }
65+ }
5766 } catch {
5867 return null ;
5968 }
60- return new DirentFromStats ( basename ( path ) , stat , dirname ( path ) ) ;
69+ return new DirentFromStats ( basename ( path ) , statResult , dirname ( path ) ) ;
6170}
6271
6372/**
6473 * @param {string } path
74+ * @param {boolean } followSymlinks
6575 * @returns {DirentFromStats|null }
6676 */
67- function getDirentSync ( path ) {
68- const stat = lstatSync ( path , { throwIfNoEntry : false } ) ;
69- if ( stat === undefined ) {
77+ function getDirentSync ( path , followSymlinks = false ) {
78+ let statResult = lstatSync ( path , { throwIfNoEntry : false } ) ;
79+ if ( statResult === undefined ) {
7080 return null ;
7181 }
72- return new DirentFromStats ( basename ( path ) , stat , dirname ( path ) ) ;
82+ // If it's a symlink and followSymlinks is true, use statSync to follow it
83+ if ( followSymlinks && statResult . isSymbolicLink ( ) ) {
84+ const followedStat = statSync ( path , { throwIfNoEntry : false } ) ;
85+ if ( followedStat !== undefined ) {
86+ statResult = followedStat ;
87+ }
88+ // If followedStat is undefined (broken symlink), keep the lstat result
89+ }
90+ return new DirentFromStats ( basename ( path ) , statResult , dirname ( path ) ) ;
7391}
7492
7593/**
@@ -115,13 +133,22 @@ class Cache {
115133 #cache = new SafeMap ( ) ;
116134 #statsCache = new SafeMap ( ) ;
117135 #readdirCache = new SafeMap ( ) ;
136+ #followSymlinks = false ;
137+
138+ setFollowSymlinks ( followSymlinks ) {
139+ this . #followSymlinks = followSymlinks ;
140+ }
141+
142+ isFollowSymlinks ( ) {
143+ return this . #followSymlinks;
144+ }
118145
119146 stat ( path ) {
120147 const cached = this . #statsCache. get ( path ) ;
121148 if ( cached ) {
122149 return cached ;
123150 }
124- const promise = getDirent ( path ) ;
151+ const promise = getDirent ( path , this . #followSymlinks ) ;
125152 this . #statsCache. set ( path , promise ) ;
126153 return promise ;
127154 }
@@ -131,7 +158,7 @@ class Cache {
131158 if ( cached && ! ( cached instanceof Promise ) ) {
132159 return cached ;
133160 }
134- const val = getDirentSync ( path ) ;
161+ const val = getDirentSync ( path , this . #followSymlinks ) ;
135162 this . #statsCache. set ( path , val ) ;
136163 return val ;
137164 }
@@ -267,9 +294,12 @@ class Glob {
267294 #isExcluded = ( ) => false ;
268295 constructor ( pattern , options = kEmptyObject ) {
269296 validateObject ( options , 'options' ) ;
270- const { exclude, cwd, withFileTypes } = options ;
297+ const { exclude, cwd, withFileTypes, followSymlinks } = options ;
271298 this . #root = toPathIfFileURL ( cwd ) ?? '.' ;
272299 this . #withFileTypes = ! ! withFileTypes ;
300+ if ( followSymlinks === true ) {
301+ this . #cache. setFollowSymlinks ( true ) ;
302+ }
273303 if ( exclude != null ) {
274304 validateStringArrayOrFunction ( exclude , 'options.exclude' ) ;
275305 if ( ArrayIsArray ( exclude ) ) {
@@ -427,7 +457,18 @@ class Glob {
427457 for ( let i = 0 ; i < children . length ; i ++ ) {
428458 const entry = children [ i ] ;
429459 const entryPath = join ( path , entry . name ) ;
430- this . #cache. addToStatCache ( join ( fullpath , entry . name ) , entry ) ;
460+
461+ // If followSymlinks is enabled and entry is a symlink, resolve it
462+ let resolvedEntry = entry ;
463+ if ( this . #cache. isFollowSymlinks ( ) && entry . isSymbolicLink ( ) ) {
464+ const resolved = this . #cache. statSync ( join ( fullpath , entry . name ) ) ;
465+ if ( resolved && ! resolved . isSymbolicLink ( ) ) {
466+ resolvedEntry = resolved ;
467+ resolvedEntry . name = entry . name ;
468+ }
469+ }
470+
471+ this . #cache. addToStatCache ( join ( fullpath , entry . name ) , resolvedEntry ) ;
431472
432473 const subPatterns = new SafeSet ( ) ;
433474 const nSymlinks = new SafeSet ( ) ;
@@ -453,10 +494,10 @@ class Glob {
453494 const matchesDot = isDot && pattern . test ( nextNonGlobIndex , entry . name ) ;
454495
455496 if ( ( isDot && ! matchesDot ) ||
456- ( this . #exclude && this . #exclude( this . #withFileTypes ? entry : entry . name ) ) ) {
497+ ( this . #exclude && this . #exclude( this . #withFileTypes ? resolvedEntry : entry . name ) ) ) {
457498 continue ;
458499 }
459- if ( ! fromSymlink && entry . isDirectory ( ) ) {
500+ if ( ! fromSymlink && resolvedEntry . isDirectory ( ) ) {
460501 // If directory, add ** to its potential patterns
461502 subPatterns . add ( index ) ;
462503 } else if ( ! fromSymlink && index === last ) {
@@ -469,24 +510,24 @@ class Glob {
469510 if ( nextMatches && nextIndex === last && ! isLast ) {
470511 // If next pattern is the last one, add to results
471512 this . #results. add ( entryPath ) ;
472- } else if ( nextMatches && entry . isDirectory ( ) ) {
513+ } else if ( nextMatches && resolvedEntry . isDirectory ( ) ) {
473514 // Pattern matched, meaning two patterns forward
474515 // are also potential patterns
475516 // e.g **/b/c when entry is a/b - add c to potential patterns
476517 subPatterns . add ( index + 2 ) ;
477518 }
478519 if ( ( nextMatches || pattern . at ( 0 ) === '.' ) &&
479- ( entry . isDirectory ( ) || entry . isSymbolicLink ( ) ) && ! fromSymlink ) {
520+ ( resolvedEntry . isDirectory ( ) || resolvedEntry . isSymbolicLink ( ) ) && ! fromSymlink ) {
480521 // If pattern after ** matches, or pattern starts with "."
481522 // and entry is a directory or symlink, add to potential patterns
482523 subPatterns . add ( nextIndex ) ;
483524 }
484525
485- if ( entry . isSymbolicLink ( ) ) {
526+ if ( resolvedEntry . isSymbolicLink ( ) ) {
486527 nSymlinks . add ( index ) ;
487528 }
488529
489- if ( next === '..' && entry . isDirectory ( ) ) {
530+ if ( next === '..' && resolvedEntry . isDirectory ( ) ) {
490531 // In case pattern is "**/..",
491532 // both parent and current directory should be added to the queue
492533 // if this is the last pattern, add to results instead
@@ -529,7 +570,7 @@ class Glob {
529570 // add next pattern to potential patterns, or to results if it's the last pattern
530571 if ( index === last ) {
531572 this . #results. add ( entryPath ) ;
532- } else if ( entry . isDirectory ( ) ) {
573+ } else if ( resolvedEntry . isDirectory ( ) ) {
533574 subPatterns . add ( nextIndex ) ;
534575 }
535576 }
@@ -637,7 +678,18 @@ class Glob {
637678 for ( let i = 0 ; i < children . length ; i ++ ) {
638679 const entry = children [ i ] ;
639680 const entryPath = join ( path , entry . name ) ;
640- this . #cache. addToStatCache ( join ( fullpath , entry . name ) , entry ) ;
681+
682+ // If followSymlinks is enabled and entry is a symlink, resolve it
683+ let resolvedEntry = entry ;
684+ if ( this . #cache. isFollowSymlinks ( ) && entry . isSymbolicLink ( ) ) {
685+ const resolved = await this . #cache. stat ( join ( fullpath , entry . name ) ) ;
686+ if ( resolved && ! resolved . isSymbolicLink ( ) ) {
687+ resolvedEntry = resolved ;
688+ resolvedEntry . name = entry . name ;
689+ }
690+ }
691+
692+ this . #cache. addToStatCache ( join ( fullpath , entry . name ) , resolvedEntry ) ;
641693
642694 const subPatterns = new SafeSet ( ) ;
643695 const nSymlinks = new SafeSet ( ) ;
@@ -663,16 +715,16 @@ class Glob {
663715 const matchesDot = isDot && pattern . test ( nextNonGlobIndex , entry . name ) ;
664716
665717 if ( ( isDot && ! matchesDot ) ||
666- ( this . #exclude && this . #exclude( this . #withFileTypes ? entry : entry . name ) ) ) {
718+ ( this . #exclude && this . #exclude( this . #withFileTypes ? resolvedEntry : entry . name ) ) ) {
667719 continue ;
668720 }
669- if ( ! fromSymlink && entry . isDirectory ( ) ) {
721+ if ( ! fromSymlink && resolvedEntry . isDirectory ( ) ) {
670722 // If directory, add ** to its potential patterns
671723 subPatterns . add ( index ) ;
672724 } else if ( ! fromSymlink && index === last ) {
673725 // If ** is last, add to results
674726 if ( ! this . #results. has ( entryPath ) && this . #results. add ( entryPath ) ) {
675- yield this . #withFileTypes ? entry : entryPath ;
727+ yield this . #withFileTypes ? resolvedEntry : entryPath ;
676728 }
677729 }
678730
@@ -681,26 +733,26 @@ class Glob {
681733 if ( nextMatches && nextIndex === last && ! isLast ) {
682734 // If next pattern is the last one, add to results
683735 if ( ! this . #results. has ( entryPath ) && this . #results. add ( entryPath ) ) {
684- yield this . #withFileTypes ? entry : entryPath ;
736+ yield this . #withFileTypes ? resolvedEntry : entryPath ;
685737 }
686- } else if ( nextMatches && entry . isDirectory ( ) ) {
738+ } else if ( nextMatches && resolvedEntry . isDirectory ( ) ) {
687739 // Pattern matched, meaning two patterns forward
688740 // are also potential patterns
689741 // e.g **/b/c when entry is a/b - add c to potential patterns
690742 subPatterns . add ( index + 2 ) ;
691743 }
692744 if ( ( nextMatches || pattern . at ( 0 ) === '.' ) &&
693- ( entry . isDirectory ( ) || entry . isSymbolicLink ( ) ) && ! fromSymlink ) {
745+ ( resolvedEntry . isDirectory ( ) || resolvedEntry . isSymbolicLink ( ) ) && ! fromSymlink ) {
694746 // If pattern after ** matches, or pattern starts with "."
695747 // and entry is a directory or symlink, add to potential patterns
696748 subPatterns . add ( nextIndex ) ;
697749 }
698750
699- if ( entry . isSymbolicLink ( ) ) {
751+ if ( resolvedEntry . isSymbolicLink ( ) ) {
700752 nSymlinks . add ( index ) ;
701753 }
702754
703- if ( next === '..' && entry . isDirectory ( ) ) {
755+ if ( next === '..' && resolvedEntry . isDirectory ( ) ) {
704756 // In case pattern is "**/..",
705757 // both parent and current directory should be added to the queue
706758 // if this is the last pattern, add to results instead
@@ -742,7 +794,7 @@ class Glob {
742794 if ( nextIndex === last ) {
743795 if ( ! this . #results. has ( entryPath ) ) {
744796 if ( this . #results. add ( entryPath ) ) {
745- yield this . #withFileTypes ? entry : entryPath ;
797+ yield this . #withFileTypes ? resolvedEntry : entryPath ;
746798 }
747799 }
748800 } else {
@@ -756,10 +808,10 @@ class Glob {
756808 if ( index === last ) {
757809 if ( ! this . #results. has ( entryPath ) ) {
758810 if ( this . #results. add ( entryPath ) ) {
759- yield this . #withFileTypes ? entry : entryPath ;
811+ yield this . #withFileTypes ? resolvedEntry : entryPath ;
760812 }
761813 }
762- } else if ( entry . isDirectory ( ) ) {
814+ } else if ( resolvedEntry . isDirectory ( ) ) {
763815 subPatterns . add ( nextIndex ) ;
764816 }
765817 }
0 commit comments