Skip to content

Commit aaf5032

Browse files
committed
fix(core): handle negated source globs
Closes #319
1 parent bf40c89 commit aaf5032

File tree

7 files changed

+94
-28
lines changed

7 files changed

+94
-28
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "test1",
3+
"dependencies": {
4+
"typescript": "5.9.3"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "test2",
3+
"dependencies": {
4+
"typescript": "5.9.2"
5+
}
6+
}

fixtures/issue-319/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "syncpack-issue-319",
3+
"packageManager": "pnpm@10.26.0"
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
packages:
2+
- "apps/*"
3+
- "!apps/test2"

justfile

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,26 @@ test:
158158

159159
test-glob-matching:
160160
#!/usr/bin/env bash
161-
cargo build && cd fixtures/issue-311 && pnpm install
161+
set -euo pipefail
162+
cargo build
163+
PROJECT_ROOT=$(pwd)
164+
165+
echo "--- issue-311: packages outside workspace globs excluded ---"
166+
cd "$PROJECT_ROOT/fixtures/issue-311" && pnpm install
162167
if ../../target/debug/syncpack json | jq '.package' | grep -q "do-not-include"; then
163-
echo "Error: 'do-not-include' found in output!"
168+
echo "FAIL: 'do-not-include' found in output"
169+
exit 1
170+
else
171+
echo "PASS: 'do-not-include' not found in output"
172+
fi
173+
174+
echo "--- issue-319: negative globs exclude packages ---"
175+
cd "$PROJECT_ROOT/fixtures/issue-319"
176+
if (../../target/debug/syncpack json || true) | jq '.package' | grep -q "test2"; then
177+
echo "FAIL: 'test2' found in output (negative glob ignored)"
164178
exit 1
165179
else
166-
echo "Success: 'do-not-include' not found in output!"
167-
exit 0
180+
echo "PASS: 'test2' correctly excluded by negative glob"
168181
fi
169182

170183
# Run test in watch mode

src/packages.rs

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use {
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
179191
fn 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

src/packages_test.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ fn normalizes_backslashes_to_forward_slashes() {
2424
// Complex patterns
2525
("**\\*\\package.json", "**/*/package.json"),
2626
("src\\**\\tests", "src/**/tests/package.json"),
27+
// Negative globs (! prefix)
28+
("!apps/test2", "!apps/test2/package.json"),
29+
("!packages/*", "!packages/*/package.json"),
30+
("!apps/test2/package.json", "!apps/test2/package.json"),
31+
("!projects\\apps\\*", "!projects/apps/*/package.json"),
2732
];
2833

2934
for (input, expected) in cases {

0 commit comments

Comments
 (0)