@@ -6,6 +6,13 @@ use napi_derive::napi;
66use std:: path:: Path ;
77use std:: sync:: { Arc , Mutex } ;
88
9+ // ignore crate 会对文件按 override 白名单过滤,但目录无论是否匹配都会被遍历(以便
10+ // 递归进去找匹配的子条目)。因此目录需要单独用 dir_matcher 测试其路径是否符合模式,
11+ // 只有匹配的目录才加入结果——这与 Node.js fs.globSync 的行为一致:
12+ // - "src/*" → 返回 src/ 下的文件 AND 子目录
13+ // - "**/*.rs" → 只返回 .rs 文件(目录不含 .rs 扩展名,不会匹配)
14+ // - "**" → 返回所有文件和目录(但不含 cwd 根节点自身)
15+
916#[ napi( object) ]
1017#[ derive( Clone ) ]
1118pub struct GlobOptions {
@@ -33,17 +40,15 @@ pub fn glob_sync(
3340 let with_file_types = opts. with_file_types . unwrap_or ( false ) ;
3441 let concurrency = opts. concurrency . unwrap_or ( 4 ) as usize ;
3542
36- // Build match rules (Matcher)
37- // ignore crate handles glob patterns via override
43+ // 构建 override(白名单模式):ignore crate 利用它来过滤文件;
44+ // 同时保留一份 dir_matcher 副本,用于判断目录自身是否匹配模式。
3845 let mut override_builder = OverrideBuilder :: new ( & cwd) ;
3946 override_builder
4047 . add ( & pattern)
4148 . map_err ( |e| Error :: from_reason ( e. to_string ( ) ) ) ?;
4249
43- if let Some ( excludes) = opts. exclude {
50+ if let Some ( ref excludes) = opts. exclude {
4451 for ex in excludes {
45- // ignore crate exclusions usually start with !, or use builder.add_ignore
46- // For simplicity here, we assume exclude is also a glob pattern, prepend !
4752 override_builder
4853 . add ( & format ! ( "!{}" , ex) )
4954 . map_err ( |e| Error :: from_reason ( e. to_string ( ) ) ) ?;
@@ -54,11 +59,14 @@ pub fn glob_sync(
5459 . build ( )
5560 . map_err ( |e| Error :: from_reason ( e. to_string ( ) ) ) ?;
5661
62+ // 复制一份给目录匹配用(walker 会消耗 overrides 所有权)
63+ let dir_matcher = Arc :: new ( overrides. clone ( ) ) ;
64+
5765 let mut builder = WalkBuilder :: new ( & cwd) ;
5866 builder
59- . overrides ( overrides) // Apply glob patterns
60- . standard_filters ( opts. git_ignore . unwrap_or ( true ) ) // Automatically handle .gitignore, .ignore etc
61- . threads ( concurrency) ; // Core: Enable multithreading with one line!
67+ . overrides ( overrides)
68+ . standard_filters ( opts. git_ignore . unwrap_or ( true ) )
69+ . threads ( concurrency) ;
6270
6371 // We use two vectors to avoid enum overhead in the lock if possible, but Mutex<Vec<T>> is easier
6472 let result_strings = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
@@ -73,68 +81,60 @@ pub fn glob_sync(
7381 let result_strings = result_strings_clone. clone ( ) ;
7482 let result_dirents = result_dirents_clone. clone ( ) ;
7583 let root = root_path. clone ( ) ;
84+ let dir_matcher = dir_matcher. clone ( ) ;
7685
7786 Box :: new ( move |entry| {
78- match entry {
79- Ok ( entry) => {
80- // WalkBuilder's overrides already help us include or exclude
81- // However, ignore crate returns directories too if they match.
82- // Usually globs like "**/*.js" only match files.
83- // But "src/*" matches both.
84- // Let's keep logic: if it matches, we keep it.
85- // But typically glob returns files.
86- // If the user wants directories, pattern usually ends with /.
87- // Standard glob behavior varies.
88- // For now, let's include everything that matches the pattern overrides.
89-
90- if entry. depth ( ) == 0 {
91- return ignore:: WalkState :: Continue ;
92- }
93-
94- if entry. file_type ( ) . map ( |ft| ft. is_dir ( ) ) . unwrap_or ( false ) {
95- return ignore:: WalkState :: Continue ;
96- }
97-
98- let path = entry. path ( ) ;
99- // Make path relative to cwd if possible, similar to node-glob
100- let relative_path = path. strip_prefix ( & root) . unwrap_or ( path) ;
101- let relative_path_str = relative_path. to_string_lossy ( ) . to_string ( ) ;
102-
103- if with_file_types {
104- let mut lock = result_dirents. lock ( ) . unwrap ( ) ;
105-
106- // Convert to Dirent
107- let parent_path = relative_path
108- . parent ( )
109- . unwrap_or ( Path :: new ( "" ) )
110- . to_string_lossy ( )
111- . to_string ( ) ;
112- let name = relative_path
113- . file_name ( )
114- . unwrap_or_default ( )
115- . to_string_lossy ( )
116- . to_string ( ) ;
117-
118- let file_type = if let Some ( ft) = entry. file_type ( ) {
119- get_file_type_id ( & ft)
120- } else {
121- 0 // Unknown
122- } ;
123-
124- lock. push ( Dirent {
125- name,
126- parent_path,
127- file_type,
128- } ) ;
129- } else {
130- let mut lock = result_strings. lock ( ) . unwrap ( ) ;
131- lock. push ( relative_path_str) ;
132- }
133- }
134- Err ( _) => {
135- // Handle errors or ignore permission errors
87+ let entry = match entry {
88+ Ok ( e) => e,
89+ Err ( _) => return ignore:: WalkState :: Continue ,
90+ } ;
91+
92+ // 跳过 cwd 根节点自身(depth 0)
93+ if entry. depth ( ) == 0 {
94+ return ignore:: WalkState :: Continue ;
95+ }
96+
97+ let path = entry. path ( ) ;
98+ let relative_path = path. strip_prefix ( & root) . unwrap_or ( path) ;
99+ let relative_path_str = relative_path. to_string_lossy ( ) . to_string ( ) ;
100+
101+ let is_dir = entry. file_type ( ) . map ( |ft| ft. is_dir ( ) ) . unwrap_or ( false ) ;
102+
103+ if is_dir {
104+ // 目录:ignore crate 只为遍历而产出,需单独测试路径是否符合模式。
105+ // 与 Node.js 行为一致:模式 "src/*" 会同时返回 src/ 下的文件和子目录。
106+ let matched = dir_matcher. matched ( relative_path, true ) ;
107+ if !matched. is_whitelist ( ) {
108+ // 目录本身不匹配模式,但仍继续遍历以便找到匹配的子条目
109+ return ignore:: WalkState :: Continue ;
136110 }
111+ // 目录匹配模式,加入结果后继续遍历
137112 }
113+ // 非目录条目:ignore crate 的 override 白名单已确保它们匹配模式
114+
115+ if with_file_types {
116+ let mut lock = result_dirents. lock ( ) . unwrap ( ) ;
117+ let parent_path = relative_path
118+ . parent ( )
119+ . unwrap_or ( Path :: new ( "" ) )
120+ . to_string_lossy ( )
121+ . to_string ( ) ;
122+ let name = relative_path
123+ . file_name ( )
124+ . unwrap_or_default ( )
125+ . to_string_lossy ( )
126+ . to_string ( ) ;
127+ let file_type = if let Some ( ft) = entry. file_type ( ) {
128+ get_file_type_id ( & ft)
129+ } else {
130+ 0
131+ } ;
132+ lock. push ( Dirent { name, parent_path, file_type } ) ;
133+ } else {
134+ let mut lock = result_strings. lock ( ) . unwrap ( ) ;
135+ lock. push ( relative_path_str) ;
136+ }
137+
138138 ignore:: WalkState :: Continue
139139 } )
140140 } ) ;
0 commit comments