Skip to content

Commit 9c67598

Browse files
wan9chiclaude
andcommitted
refactor(glob): split vite_glob into env (globset) and path (wax) modules
`vite_glob` wrapped `wax`, a filesystem path-glob engine, but most callers used it to match environment-variable *names* — flat strings, not paths — by pushing `&str` through `is_match(impl AsRef<Path>)`. That is semantically muddy, and `wax` exposes no option to disable its path semantics. Reorganize the crate into two modules, each with its own matchers and error type: - `vite_glob::env` — `EnvGlob` (single pattern) and `EnvGlobSet` (a set, any-match), backed by `globset` with `literal_separator(false)` and `case_insensitive(cfg!(windows))` so `*`/`?`/`[...]`/`{a,b}` are pure flat-string wildcards with the platform's env case rules. `!` is an ordinary character (no negation). Errors: `EnvGlobError`. - `vite_glob::path` — `PathGlobSet` (the former `GlobPatternSet`, renamed), still `wax`-backed with unchanged gitignore (first/last-match-wins) semantics. Errors: `PathGlobError`. This mirrors how Turborepo splits env (regex) from path (wax) matching. Env results are unchanged — globset matches the same `*`/`?`/`[...]`/`{}` syntax wax did for separator-free names, including SENSITIVE_PATTERNS and the Unix/Windows case split (covered by existing envs.rs tests). Migrate the pre-existing callers: `envs.rs` uses `EnvGlobSet` for its multi-pattern env matching (dropping the obsolete `!`-filter and its warning), and `vite_workspace` switches to `PathGlobSet` / `PathGlobError`. The runner-aware env-matching sites consume `EnvGlob` in the stacked PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d365a01 commit 9c67598

10 files changed

Lines changed: 592 additions & 400 deletions

File tree

Cargo.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ fspy_shared = { path = "crates/fspy_shared" }
8080
fspy_shared_unix = { path = "crates/fspy_shared_unix" }
8181
futures = "0.3.31"
8282
futures-util = "0.3.31"
83+
globset = "0.4.18"
8384
jsonc-parser = { version = "0.32.0", features = ["serde"] }
8485
libc = "0.2.185"
8586
libtest-mimic = "0.8.2"

crates/vite_glob/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ publish = false
88
rust-version.workspace = true
99

1010
[dependencies]
11+
globset = { workspace = true }
1112
thiserror = { workspace = true }
12-
vite_path = { workspace = true }
1313
wax = { workspace = true }
1414

1515
[dev-dependencies]

crates/vite_glob/src/env.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//! Glob matching for environment-variable **names** (flat strings, never paths).
2+
//!
3+
//! Backed by `globset` with path-separator handling disabled, so `*`, `?`,
4+
//! `[...]`, and `{a,b}` behave as plain-string wildcards. Matching is
5+
//! case-sensitive on Unix and case-insensitive on Windows, mirroring how
6+
//! environment variables are looked up on each platform. `!` is an ordinary
7+
//! character — there is no negation.
8+
9+
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
10+
11+
/// Error compiling an environment-variable name pattern.
12+
#[derive(Debug, thiserror::Error)]
13+
#[error(transparent)]
14+
pub struct EnvGlobError(#[from] globset::Error);
15+
16+
/// Compiles `pattern` into a `globset::Glob` configured for env-name matching:
17+
/// separators are not special, and case follows the platform's env semantics.
18+
fn build(pattern: &str) -> Result<Glob, globset::Error> {
19+
GlobBuilder::new(pattern)
20+
// Env names contain no path separators, so disabling separator handling
21+
// makes `*`/`?` match any character — a pure string match.
22+
.literal_separator(false)
23+
// Env lookups are case-insensitive on Windows, case-sensitive elsewhere.
24+
.case_insensitive(cfg!(windows))
25+
.build()
26+
}
27+
28+
/// Matches a single environment-variable name against one glob pattern.
29+
#[derive(Debug, Clone)]
30+
pub struct EnvGlob {
31+
matcher: GlobMatcher,
32+
}
33+
34+
impl EnvGlob {
35+
/// Compiles `pattern` into an env-name matcher.
36+
///
37+
/// # Errors
38+
/// Returns an error if `pattern` is not a valid glob.
39+
pub fn new(pattern: &str) -> Result<Self, EnvGlobError> {
40+
Ok(Self { matcher: build(pattern)?.compile_matcher() })
41+
}
42+
43+
/// Returns whether `name` matches the pattern.
44+
#[must_use]
45+
pub fn is_match(&self, name: &str) -> bool {
46+
self.matcher.is_match(name)
47+
}
48+
}
49+
50+
/// Matches an environment-variable name against a **set** of glob patterns.
51+
///
52+
/// A name matches when it matches any one of the patterns (the patterns are
53+
/// compiled into a single combined matcher). An empty set matches nothing.
54+
#[derive(Debug, Clone)]
55+
pub struct EnvGlobSet {
56+
set: GlobSet,
57+
}
58+
59+
impl EnvGlobSet {
60+
/// Compiles `patterns` into a combined env-name matcher.
61+
///
62+
/// # Errors
63+
/// Returns an error if any pattern is not a valid glob.
64+
pub fn new<I, S>(patterns: I) -> Result<Self, EnvGlobError>
65+
where
66+
I: IntoIterator<Item = S>,
67+
S: AsRef<str>,
68+
{
69+
let mut builder = GlobSetBuilder::new();
70+
for pattern in patterns {
71+
builder.add(build(pattern.as_ref())?);
72+
}
73+
Ok(Self { set: builder.build()? })
74+
}
75+
76+
/// Returns whether `name` matches any pattern in the set.
77+
#[must_use]
78+
pub fn is_match(&self, name: &str) -> bool {
79+
self.set.is_match(name)
80+
}
81+
}
82+
83+
#[cfg(test)]
84+
mod tests {
85+
use super::{EnvGlob, EnvGlobSet};
86+
87+
#[test]
88+
fn matches_star_prefix_and_suffix() {
89+
let g = EnvGlob::new("VITE_*").unwrap();
90+
assert!(g.is_match("VITE_FOO"));
91+
assert!(g.is_match("VITE_")); // `*` matches the empty string
92+
assert!(!g.is_match("MYVITE_FOO"));
93+
94+
let g = EnvGlob::new("*_KEY").unwrap();
95+
assert!(g.is_match("MY_KEY"));
96+
assert!(!g.is_match("MY_KEYS"));
97+
98+
let g = EnvGlob::new("*_CREDENTIAL*").unwrap();
99+
assert!(g.is_match("AWS_CREDENTIALS"));
100+
assert!(g.is_match("X_CREDENTIAL_Y"));
101+
}
102+
103+
#[test]
104+
fn question_mark_matches_exactly_one_char() {
105+
let g = EnvGlob::new("APP?_*").unwrap();
106+
assert!(g.is_match("APP1_TOKEN"));
107+
assert!(g.is_match("APP2_NAME"));
108+
// `?` requires exactly one character, so `APP_X` (nothing before `_`) does not match.
109+
assert!(!g.is_match("APP_X"));
110+
}
111+
112+
#[test]
113+
fn brace_alternation_is_supported() {
114+
let g = EnvGlob::new("{VITE,NEXT}_*").unwrap();
115+
assert!(g.is_match("VITE_FOO"));
116+
assert!(g.is_match("NEXT_BAR"));
117+
assert!(!g.is_match("NUXT_BAR"));
118+
}
119+
120+
#[test]
121+
fn dot_and_separators_are_literal_not_path_special() {
122+
// Env names are flat strings: `*` spans `.` and `/` (no path semantics),
123+
// and a literal `.` in the pattern matches a literal `.`.
124+
assert!(EnvGlob::new("A*").unwrap().is_match("A.B"));
125+
assert!(EnvGlob::new("A*").unwrap().is_match("A/B"));
126+
assert!(EnvGlob::new("*.local").unwrap().is_match("APP.local"));
127+
assert!(!EnvGlob::new("*.local").unwrap().is_match("APPXlocal"));
128+
}
129+
130+
#[test]
131+
fn bang_is_a_literal_character() {
132+
// There is no negation: `!FOO` matches the literal name `!FOO`, not `FOO`.
133+
let g = EnvGlob::new("!FOO").unwrap();
134+
assert!(g.is_match("!FOO"));
135+
assert!(!g.is_match("FOO"));
136+
}
137+
138+
#[test]
139+
fn non_match_default_is_false() {
140+
assert!(!EnvGlob::new("VITE_*").unwrap().is_match("PATH"));
141+
}
142+
143+
#[test]
144+
fn set_matches_any_pattern() {
145+
let set = EnvGlobSet::new(["VITE_*", "*_KEY", "APP?_*"]).unwrap();
146+
assert!(set.is_match("VITE_FOO"));
147+
assert!(set.is_match("MY_KEY"));
148+
assert!(set.is_match("APP1_TOKEN"));
149+
assert!(!set.is_match("PATH"));
150+
assert!(!set.is_match("APP_X"));
151+
}
152+
153+
#[test]
154+
fn empty_set_matches_nothing() {
155+
let set = EnvGlobSet::new(std::iter::empty::<&str>()).unwrap();
156+
assert!(!set.is_match("VITE_FOO"));
157+
}
158+
159+
#[test]
160+
#[cfg(not(windows))]
161+
fn unix_matching_is_case_sensitive() {
162+
let g = EnvGlob::new("VITE_*").unwrap();
163+
assert!(g.is_match("VITE_FOO"));
164+
assert!(!g.is_match("vite_foo"));
165+
let set = EnvGlobSet::new(["VITE_*"]).unwrap();
166+
assert!(!set.is_match("vite_foo"));
167+
}
168+
169+
#[test]
170+
#[cfg(windows)]
171+
fn windows_matching_is_case_insensitive() {
172+
let g = EnvGlob::new("VITE_*").unwrap();
173+
assert!(g.is_match("VITE_FOO"));
174+
assert!(g.is_match("vite_foo"));
175+
let set = EnvGlobSet::new(["VITE_*"]).unwrap();
176+
assert!(set.is_match("vite_foo"));
177+
}
178+
}

crates/vite_glob/src/error.rs

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)