1414 serde_json:: Value ,
1515 std:: {
1616 cell:: RefCell ,
17- collections:: HashMap ,
17+ collections:: { HashMap , HashSet } ,
1818 fs,
1919 path:: { Path , PathBuf } ,
2020 rc:: Rc ,
@@ -157,17 +157,29 @@ impl Packages {
157157}
158158
159159/// Normalize a source pattern by:
160- /// 1. Converting Windows backslashes to forward slashes for glob compatibility
161- /// 2. Ensuring pattern ends with /package.json
160+ /// 1. Preserving negation prefix (`!`) through normalization
161+ /// 2. Converting Windows backslashes to forward slashes for glob compatibility
162+ /// 3. Ensuring pattern ends with /package.json
162163///
163164/// Examples:
164165/// - "projects\\apps\\*" -> "projects/apps/*/package.json"
165166/// - "projects/libs/*" -> "projects/libs/*/package.json"
166167/// - "package.json" -> "package.json"
167168/// - "apps\\*/package.json" -> "apps/*/package.json"
168- pub fn normalize_pattern ( pattern : String ) -> String {
169+ /// - "!apps/test2" -> "!apps/test2/package.json"
170+ pub fn normalize_pattern ( mut pattern : String ) -> String {
171+ let negated = pattern. starts_with ( '!' ) ;
172+ if negated {
173+ pattern. remove ( 0 ) ;
174+ }
169175 let normalized = pattern. replace ( '\\' , "/" ) ;
170- if normalized. contains ( "package.json" ) {
176+ if negated {
177+ if normalized. contains ( "package.json" ) {
178+ format ! ( "!{normalized}" )
179+ } else {
180+ format ! ( "!{normalized}/package.json" )
181+ }
182+ } else if normalized. contains ( "package.json" ) {
171183 normalized
172184 } else {
173185 format ! ( "{normalized}/package.json" )
@@ -177,26 +189,43 @@ pub fn normalize_pattern(pattern: String) -> String {
177189/// Resolve every source glob pattern into their absolute file paths of
178190/// package.json files
179191fn get_file_paths ( config : & Config ) -> Vec < PathBuf > {
180- get_source_patterns ( config)
192+ let all_patterns = get_source_patterns ( config) ;
193+ let ( negatives, positives) : ( Vec < _ > , Vec < _ > ) = all_patterns. iter ( ) . partition ( |p| p. starts_with ( '!' ) ) ;
194+
195+ let to_absolute = |pattern : & str | -> String {
196+ if PathBuf :: from ( pattern) . is_absolute ( ) {
197+ pattern. to_string ( )
198+ } else {
199+ config. cli . cwd . join ( pattern) . to_str ( ) . unwrap ( ) . to_string ( )
200+ }
201+ } ;
202+
203+ let resolve_glob = |pattern : & str | -> Vec < PathBuf > {
204+ glob ( pattern)
205+ . map_err ( |err| debug ! ( "Invalid glob pattern '{pattern}': {err}" ) )
206+ . into_iter ( )
207+ . flat_map ( |paths| paths. filter_map ( Result :: ok) )
208+ . collect ( )
209+ } ;
210+
211+ let included: Vec < PathBuf > = positives
181212 . iter ( )
182- . map ( |pattern| {
183- if PathBuf :: from ( pattern) . is_absolute ( ) {
184- pattern. clone ( )
185- } else {
186- config. cli . cwd . join ( pattern) . to_str ( ) . unwrap ( ) . to_string ( )
187- }
188- } )
189- . flat_map ( |pattern| glob ( & pattern) . ok ( ) )
190- . flat_map ( |paths| {
191- paths
192- . filter_map ( Result :: ok)
193- . filter ( |path| !path. to_string_lossy ( ) . contains ( "node_modules" ) )
194- . fold ( vec ! [ ] , |mut paths, path| {
195- paths. push ( path. clone ( ) ) ;
196- paths
197- } )
198- } )
199- . collect ( )
213+ . map ( |p| to_absolute ( p) )
214+ . flat_map ( |pattern| resolve_glob ( & pattern) )
215+ . filter ( |path| !path. to_string_lossy ( ) . contains ( "node_modules" ) )
216+ . collect ( ) ;
217+
218+ if negatives. is_empty ( ) {
219+ return included;
220+ }
221+
222+ let excluded: HashSet < PathBuf > = negatives
223+ . iter ( )
224+ . map ( |p| to_absolute ( p. trim_start_matches ( '!' ) ) )
225+ . flat_map ( |pattern| resolve_glob ( & pattern) )
226+ . collect ( ) ;
227+
228+ included. into_iter ( ) . filter ( |path| !excluded. contains ( path) ) . collect ( )
200229}
201230
202231/// Based on the user's config file and command line `--source` options, return
0 commit comments