diff --git a/packages/nx/src/native/glob.rs b/packages/nx/src/native/glob.rs index 5770515a35..ee89242e0a 100644 --- a/packages/nx/src/native/glob.rs +++ b/packages/nx/src/native/glob.rs @@ -69,11 +69,22 @@ impl NxGlobSet { } } +fn potential_glob_split( + glob: &str, +) -> itertools::Either, std::iter::Once<&str>> { + use itertools::Either::*; + if glob.starts_with('{') && glob.ends_with('}') { + Left(glob.trim_matches('{').trim_end_matches('}').split(',')) + } else { + Right(std::iter::once(glob)) + } +} + pub(crate) fn build_glob_set + Debug>(globs: &[S]) -> anyhow::Result { let result = globs .iter() - .map(|s| { - let glob = s.as_ref(); + .flat_map(|s| potential_glob_split(s.as_ref())) + .map(|glob| { if glob.contains('!') || glob.contains('|') || glob.contains('(') { convert_glob(glob) } else { @@ -214,6 +225,12 @@ mod test { assert!(!glob_set.is_match("packages/package-a-b")); assert!(!glob_set.is_match("packages/package-a-b/nested")); assert!(!glob_set.is_match("packages/package-b/nested")); + + let glob_set = build_glob_set(&["packages/!(package-a)*/package.json"]).unwrap(); + assert!(glob_set.is_match("packages/package-b/package.json")); + assert!(glob_set.is_match("packages/package-c/package.json")); + assert!(!glob_set.is_match("packages/package-a/package.json")); + assert!(!glob_set.is_match("packages/package/a/package.json")); } #[test] @@ -283,4 +300,24 @@ mod test { assert!(!glob_set.is_match("test.module.spec.tsx")); assert!(!glob_set.is_match("nested/comp.test.component.spec.ts")); } + + #[test] + fn supports_brace_expansion() { + let glob_set = build_glob_set(&["{packages,apps}/*"]).unwrap(); + assert!(glob_set.is_match("packages/package-a")); + assert!(glob_set.is_match("apps/app-a")); + assert!(!glob_set.is_match("apps/app-a/nested")); + + let glob_set = build_glob_set(&["{package-lock.json,yarn.lock,pnpm-lock.yaml}"]).unwrap(); + assert!(glob_set.is_match("package-lock.json")); + assert!(glob_set.is_match("yarn.lock")); + assert!(glob_set.is_match("pnpm-lock.yaml")); + + let glob_set = + build_glob_set(&["{packages/!(package-a)*/package.json,packages/*/package.json}"]) + .unwrap(); + assert!(glob_set.is_match("packages/package-b/package.json")); + assert!(glob_set.is_match("packages/package-c/package.json")); + assert!(!glob_set.is_match("packages/package-a/package.json")); + } } diff --git a/packages/nx/src/native/glob/glob_group.rs b/packages/nx/src/native/glob/glob_group.rs index bfee8b76d5..79430f98a0 100644 --- a/packages/nx/src/native/glob/glob_group.rs +++ b/packages/nx/src/native/glob/glob_group.rs @@ -13,7 +13,10 @@ pub enum GlobGroup<'a> { ExactOne(Cow<'a, str>), // !(a|b|c) Negated(Cow<'a, str>), + // !(a|b|c).js NegatedFileName(Cow<'a, str>), + // !(a|b|c)* + NegatedWildcard(Cow<'a, str>), NonSpecialGroup(Cow<'a, str>), NonSpecial(Cow<'a, str>), } @@ -40,6 +43,13 @@ impl<'a> Display for GlobGroup<'a> { write!(f, "{}.", s) } } + GlobGroup::NegatedWildcard(s) => { + if s.contains(',') { + write!(f, "{{{}}}*", s) + } else { + write!(f, "{}*", s) + } + } GlobGroup::NonSpecial(s) => write!(f, "{}", s), } } diff --git a/packages/nx/src/native/glob/glob_parser.rs b/packages/nx/src/native/glob/glob_parser.rs index 46ac53fe4c..33b284057b 100644 --- a/packages/nx/src/native/glob/glob_parser.rs +++ b/packages/nx/src/native/glob/glob_parser.rs @@ -58,6 +58,14 @@ fn negated_file_group(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str })(input) } +fn negated_wildcard(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> { + context("negated_wildcard", |input| { + let (input, result) = preceded(tag("!("), group)(input)?; + let (input, _) = tag("*")(input)?; + Ok((input, GlobGroup::NegatedWildcard(result))) + })(input) +} + fn non_special_character(input: &str) -> IResult<&str, GlobGroup, VerboseError<&str>> { context( "non_special_character", @@ -107,6 +115,7 @@ fn parse_segment(input: &str) -> IResult<&str, Vec, VerboseError<&str one_or_more_group, exact_one_group, negated_file_group, + negated_wildcard, negated_group, non_special_character, )), @@ -149,6 +158,7 @@ pub fn parse_glob(input: &str) -> anyhow::Result<(bool, Vec>)> { #[cfg(test)] mod test { + use crate::native::glob::glob_group::GlobGroup; use crate::native::glob::glob_parser::parse_glob; @@ -272,5 +282,18 @@ mod test { ] ) ); + + let result = parse_glob("packages/!(package-a)*/package.json").unwrap(); + assert_eq!( + result, + ( + false, + vec![ + vec![GlobGroup::NonSpecial("packages".into())], + vec![GlobGroup::NegatedWildcard("package-a".into()),], + vec![GlobGroup::NonSpecial("package.json".into())] + ] + ) + ); } } diff --git a/packages/nx/src/native/glob/glob_transform.rs b/packages/nx/src/native/glob/glob_transform.rs index c3dd6ad828..d4b702d588 100644 --- a/packages/nx/src/native/glob/glob_transform.rs +++ b/packages/nx/src/native/glob/glob_transform.rs @@ -88,6 +88,11 @@ fn build_segment( let on_group = build_segment(&built_glob, &group[1..], is_last_segment, true); off_group.into_iter().chain(on_group).collect::>() } + GlobGroup::NegatedWildcard(_) => { + let off_group = build_segment("*", &group[1..], is_last_segment, is_negative); + let on_group = build_segment(&built_glob, &group[1..], is_last_segment, true); + off_group.into_iter().chain(on_group).collect::>() + } GlobGroup::OneOrMore(_) | GlobGroup::ExactOne(_) | GlobGroup::NonSpecial(_) @@ -153,6 +158,15 @@ mod test { assert_eq!(negative_single_dir, ["!packages/package-a*", "packages/*"]); } + #[test] + fn convert_globs_single_negative_wildcard_directory() { + let negative_single_dir = convert_glob("packages/!(package-a)*/package.json").unwrap(); + assert_eq!( + negative_single_dir, + ["!packages/package-a*/", "packages/*/package.json"] + ); + } + #[test] fn test_transforming_globs() { let globs = convert_glob("!(test|e2e)/?(*.)+(spec|test).[jt]s!(x)?(.snap)").unwrap();