Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/nx/src/native/cache/expand_outputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,35 @@ mod test {
);
}

#[test]
fn should_get_files_for_scoped_output_glob() {
let temp = TempDir::new().unwrap();
temp.child("packages/@acme/producer/dist/index.d.mts")
.touch()
.unwrap();
temp.child("packages/@acme/producer/dist/nested/example.d.ts")
.touch()
.unwrap();
temp.child("packages/@acme/producer/dist/nested/example.js")
.touch()
.unwrap();

let entries = vec!["packages/@acme/producer/dist/**/*.{d.ts,d.cts,d.mts}".to_string()];
let mut hashed_outputs = get_files_for_outputs(temp.path(), entries.clone()).unwrap();
let mut expanded_outputs = expand_outputs(temp.display().to_string(), entries).unwrap();
hashed_outputs.sort();
expanded_outputs.sort();

assert_eq!(
hashed_outputs,
vec![
"packages/@acme/producer/dist/index.d.mts",
"packages/@acme/producer/dist/nested/example.d.ts",
]
);
assert_eq!(hashed_outputs, expanded_outputs);
}

#[test]
#[cfg(unix)]
fn should_expand_outputs_with_symlinks_and_globs() {
Expand Down
3 changes: 3 additions & 0 deletions packages/nx/src/native/glob/glob_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub enum GlobGroup<'a> {
NegatedFileName(Cow<'a, str>),
// !(a|b|c)*
NegatedWildcard(Cow<'a, str>),
// ?, +, or @ without a following parenthesized group
UngroupedSpecialChar(char),
NonSpecialGroup(Cow<'a, str>),
NonSpecial(Cow<'a, str>),
}
Expand Down Expand Up @@ -50,6 +52,7 @@ impl<'a> Display for GlobGroup<'a> {
write!(f, "{}*", s)
}
}
GlobGroup::UngroupedSpecialChar(character) => write!(f, "{}", character),
GlobGroup::NonSpecial(s) => write!(f, "{}", s),
}
}
Expand Down
89 changes: 79 additions & 10 deletions packages/nx/src/native/glob/glob_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ use nom::sequence::{preceded, terminated};
use nom::{Finish, IResult};
use std::borrow::Cow;

#[derive(Clone, Copy)]
enum UngroupedSpecialCharBehavior {
Discard,
Preserve,
}

/// Consumes special characters if they are not part of a group, otherwise returns an error
/// Example:
/// - ?snap -> snap
Expand All @@ -26,6 +32,15 @@ fn special_char_with_no_group(input: &str) -> IResult<&str, &str, VerboseError<&
})(input)
}

fn ungrouped_special_char(input: &str) -> IResult<&str, GlobGroup<'_>, VerboseError<&str>> {
context(
"ungrouped_special_char",
map(alt((tag("?"), tag("+"), tag("@"))), |character: &str| {
GlobGroup::UngroupedSpecialChar(character.chars().next().unwrap())
}),
)(input)
}

fn simple_group(input: &str) -> IResult<&str, GlobGroup<'_>, VerboseError<&str>> {
context(
"simple_group",
Expand Down Expand Up @@ -135,17 +150,25 @@ fn separated_group_items(input: &str) -> IResult<&str, Cow<'_, str>, VerboseErro
)(input)
}

fn parse_segment(input: &str) -> IResult<&str, Vec<GlobGroup<'_>>, VerboseError<&str>> {
fn parse_segment(
input: &str,
ungrouped_special_char_behavior: UngroupedSpecialCharBehavior,
) -> IResult<&str, Vec<GlobGroup<'_>>, VerboseError<&str>> {
context(
"parse_segment",
many_till(
context("glob_group", |input| {
// check if the special character is part of a group
let group_input = match special_char_with_no_group(input) {
// if there was no (, then we know that the special character is not part of a group, we can return this input
Ok((no_group_input, _)) => no_group_input,
// otherwise, there was a ( after the special character, so we need to parse the original input
Err(_) => input,
let group_input = match ungrouped_special_char_behavior {
UngroupedSpecialCharBehavior::Discard => {
match special_char_with_no_group(input) {
// if there was no (, then we know that the special character is not part of a group, we can return this input
Ok((no_group_input, _)) => no_group_input,
// otherwise, there was a ( after the special character, so we need to parse the original input
Err(_) => input,
}
}
UngroupedSpecialCharBehavior::Preserve => input,
};
alt((
simple_group,
Expand All @@ -157,6 +180,7 @@ fn parse_segment(input: &str) -> IResult<&str, Vec<GlobGroup<'_>>, VerboseError<
negated_wildcard,
negated_group,
brace_group_with_empty_item,
ungrouped_special_char,
non_special_character,
))(group_input)
}),
Expand All @@ -166,8 +190,16 @@ fn parse_segment(input: &str) -> IResult<&str, Vec<GlobGroup<'_>>, VerboseError<
.map(|(i, (groups, _))| (i, groups))
}

fn separated_segments(input: &str) -> IResult<&str, Vec<Vec<GlobGroup<'_>>>, VerboseError<&str>> {
separated_list0(tag("/"), map_parser(take_till(|c| c == '/'), parse_segment))(input)
fn separated_segments(
input: &str,
ungrouped_special_char_behavior: UngroupedSpecialCharBehavior,
) -> IResult<&str, Vec<Vec<GlobGroup<'_>>>, VerboseError<&str>> {
separated_list0(
tag("/"),
map_parser(take_till(|c| c == '/'), move |segment| {
parse_segment(segment, ungrouped_special_char_behavior)
}),
)(input)
}

// match on !test/, but not !(test)/
Expand All @@ -183,9 +215,12 @@ fn negated_glob(input: &str) -> (&str, bool) {
}
}

pub fn parse_glob(input: &str) -> anyhow::Result<(bool, Vec<Vec<GlobGroup<'_>>>)> {
fn parse_glob_with_behavior(
input: &str,
ungrouped_special_char_behavior: UngroupedSpecialCharBehavior,
) -> anyhow::Result<(bool, Vec<Vec<GlobGroup<'_>>>)> {
let (input, negated) = negated_glob(input);
let result = separated_segments(input).finish();
let result = separated_segments(input, ungrouped_special_char_behavior).finish();
if let Ok((_, result)) = result {
Ok((negated, result))
} else {
Expand All @@ -196,6 +231,18 @@ pub fn parse_glob(input: &str) -> anyhow::Result<(bool, Vec<Vec<GlobGroup<'_>>>)
}
}

pub fn parse_glob(input: &str) -> anyhow::Result<(bool, Vec<Vec<GlobGroup<'_>>>)> {
parse_glob_with_behavior(input, UngroupedSpecialCharBehavior::Discard)
}

/// Retains ungrouped special characters so literal path segments such as
/// `packages/@acme` can be reconstructed after parsing.
pub fn parse_glob_preserving_ungrouped_special_chars(
input: &str,
) -> anyhow::Result<(bool, Vec<Vec<GlobGroup<'_>>>)> {
parse_glob_with_behavior(input, UngroupedSpecialCharBehavior::Preserve)
}

#[cfg(test)]
mod test {
use crate::native::glob::glob_group::GlobGroup;
Expand Down Expand Up @@ -357,6 +404,28 @@ mod test {
)
);
}

#[test]
fn should_preserve_ungrouped_special_chars_when_requested() {
let result = super::parse_glob_preserving_ungrouped_special_chars("@scope/a+b").unwrap();
assert_eq!(
result,
(
false,
vec![
vec![
GlobGroup::UngroupedSpecialChar('@'),
GlobGroup::NonSpecial("scope".into())
],
vec![
GlobGroup::NonSpecial("a".into()),
GlobGroup::UngroupedSpecialChar('+'),
GlobGroup::NonSpecial("b".into())
]
]
)
);
}
#[test]
fn should_parse_globs_with_braces() {
let result = parse_glob("**/*.spec.ts{,.snap}").unwrap();
Expand Down
56 changes: 47 additions & 9 deletions packages/nx/src/native/glob/glob_transform.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::contains_glob_pattern;
use crate::native::glob::glob_group::GlobGroup;
use crate::native::glob::glob_parser::parse_glob;
use crate::native::glob::glob_parser::{parse_glob, parse_glob_preserving_ungrouped_special_chars};
use itertools::Either::{Left, Right};
use itertools::Itertools;
use std::collections::HashSet;
Expand Down Expand Up @@ -111,6 +111,10 @@ fn build_segment(
| GlobGroup::NonSpecialGroup(_) => {
build_segment(&built_glob, &group[1..], is_last_segment, is_negative)
}
// Skip ungrouped special characters when building converted patterns.
GlobGroup::UngroupedSpecialChar(_) => {
build_segment(existing, &group[1..], is_last_segment, is_negative)
}
}
} else if is_negative {
vec![GlobType::Negative(existing.to_string())]
Expand All @@ -119,21 +123,37 @@ fn build_segment(
}
}

fn static_segment(group: &[GlobGroup]) -> Option<String> {
let mut segment = String::new();
for glob_part in group {
match glob_part {
GlobGroup::NonSpecial(value) if !contains_glob_pattern(value) => {
segment.push_str(value)
}
GlobGroup::UngroupedSpecialChar(character @ ('@' | '+')) => segment.push(*character),
_ => return None,
}
}
Some(segment)
}

pub fn partition_glob(glob: &str) -> anyhow::Result<(String, Vec<String>)> {
let (negated, groups) = parse_glob(glob)?;
// Preserve ungrouped special characters so scoped directories such as
// `packages/@acme` remain part of the static root.
let (negated, groups) = parse_glob_preserving_ungrouped_special_chars(glob)?;
// Partition glob into leading directories and patterns that should be matched
let mut has_patterns = false;
let (leading_dir_segments, pattern_segments): (Vec<String>, _) = groups
.into_iter()
.filter(|group| !group.is_empty())
.partition_map(|group| match &group[0] {
GlobGroup::NonSpecial(value) if !contains_glob_pattern(&value) && !has_patterns => {
Left(value.to_string())
}
_ => {
has_patterns = true;
Right(group)
.partition_map(|group| {
if !has_patterns {
if let Some(segment) = static_segment(&group) {
return Left(segment);
}
}
has_patterns = true;
Right(group)
});

Ok((
Expand Down Expand Up @@ -298,6 +318,24 @@ mod test {
assert_eq!(globs, [] as [String; 0]);
}

#[test]
fn should_partition_literal_extglob_prefixes() {
let (leading_dirs, globs) =
super::partition_glob("packages/@acme/producer/dist/**/*.d.ts").unwrap();
assert_eq!(leading_dirs, "packages/@acme/producer/dist");
assert_eq!(globs, ["**/*.d.ts"]);

let (leading_dirs, globs) =
super::partition_glob("packages/a+b/producer/dist/**/*.d.ts").unwrap();
assert_eq!(leading_dirs, "packages/a+b/producer/dist");
assert_eq!(globs, ["**/*.d.ts"]);

let (leading_dirs, globs) =
super::partition_glob("packages/@(acme|other)/dist/**/*.d.ts").unwrap();
assert_eq!(leading_dirs, "packages");
assert_eq!(globs, ["{acme,other}/dist/**/*.d.ts"]);
}

#[test]
fn should_handle_test_optional_s_pattern() {
let globs = convert_glob("**/__test?(s)__/**/*").unwrap();
Expand Down
40 changes: 40 additions & 0 deletions packages/nx/src/native/tasks/hashers/hash_task_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,43 @@ pub fn hash_task_output(
files,
})
}

#[cfg(test)]
mod tests {
use super::*;
use assert_fs::TempDir;
use assert_fs::prelude::*;

#[test]
fn should_hash_scoped_output_files() {
let temp = TempDir::new().unwrap();
let declaration = temp.child("packages/@acme/producer/dist/index.d.ts");
declaration
.write_str("export declare const value: 1;\n")
.unwrap();
let outputs = vec!["packages/@acme/producer/dist/**/*.d.ts".to_string()];

let first = hash_task_output(
temp.path().to_str().unwrap(),
"**/*.d.ts",
&outputs,
&DashMap::new(),
)
.unwrap();

declaration
.write_str("export declare const value: 2;\n")
.unwrap();
let second = hash_task_output(
temp.path().to_str().unwrap(),
"**/*.d.ts",
&outputs,
&DashMap::new(),
)
.unwrap();

assert_eq!(first.files, ["packages/@acme/producer/dist/index.d.ts"]);
assert_eq!(first.files, second.files);
assert_ne!(first.hash, second.hash);
}
}