From fd4dcb20aa7540c417c30829f01d035e4a4a851d Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 19 Jul 2026 14:08:19 +0800 Subject: [PATCH 1/3] fix(workspace): support dot-relative workspace patterns Co-authored-by: GPT-5 Codex --- crates/vite_workspace/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/vite_workspace/src/lib.rs b/crates/vite_workspace/src/lib.rs index 3b22d8bc9..555f1061c 100644 --- a/crates/vite_workspace/src/lib.rs +++ b/crates/vite_workspace/src/lib.rs @@ -100,6 +100,14 @@ impl WorkspaceMemberGlobs { let mut all = Vec::::new(); for mut pattern in self.workspaces { pattern.push_str(if pattern.ends_with('/') { "package.json" } else { "/package.json" }); + // npm treats workspace patterns as root-relative and strips a leading `./`. + let path_start = usize::from(pattern.starts_with('!')); + if pattern[path_start..].starts_with("./") { + let mut normalized = Str::with_capacity(pattern.len() - 2); + normalized.push_str(&pattern[..path_start]); + normalized.push_str(&pattern[path_start + 2..]); + pattern = normalized; + } if pattern.starts_with('!') { has_negated = true; } else { @@ -925,7 +933,7 @@ mod tests { let root_package = serde_json::json!({ "name": "npm-monorepo", "private": true, - "workspaces": ["packages/*", "apps/*"] + "workspaces": ["./packages/*", "./apps/*"] }); fs::write(temp_dir_path.join("package.json"), root_package.to_string()).unwrap(); From fcac813ae1c23b14282ac3d31a8930b02c150dd3 Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 19 Jul 2026 14:31:20 +0800 Subject: [PATCH 2/3] docs: add workspace pattern changelog entry Co-authored-by: GPT-5 Codex --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05973e2be..cafb0e42b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** npm workspace patterns beginning with `./` now discover matching packages correctly ([vite-plus#2201](https://github.com/voidzero-dev/vite-plus/issues/2201), [#547](https://github.com/voidzero-dev/vite-task/pull/547)). - **Fixed** An issue where Bun tasks on macOS did not rerun when files they read, wrote, or listed changed ([#532](https://github.com/voidzero-dev/vite-task/issues/532), [#542](https://github.com/voidzero-dev/vite-task/pull/542)). - **Improved** Windows file-access tracking now uses sparse temporary backing files where supported, avoiding upfront allocation of the full backing file on disk ([#524](https://github.com/voidzero-dev/vite-task/pull/524)). - **Fixed** Automatic file-access tracking on Linux now works in containers and Kubernetes runners with limited `/dev/shm` space ([#353](https://github.com/voidzero-dev/vite-task/issues/353), [#523](https://github.com/voidzero-dev/vite-task/pull/523)). From c737c97643875f6bce2dd734223e040c0542e53b Mon Sep 17 00:00:00 2001 From: Liang Date: Mon, 20 Jul 2026 17:54:00 +0800 Subject: [PATCH 3/3] wip --- crates/vite_workspace/src/lib.rs | 87 ++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/crates/vite_workspace/src/lib.rs b/crates/vite_workspace/src/lib.rs index 555f1061c..ba8e98236 100644 --- a/crates/vite_workspace/src/lib.rs +++ b/crates/vite_workspace/src/lib.rs @@ -98,17 +98,31 @@ impl WorkspaceMemberGlobs { let mut has_negated = false; let mut inclusions = Vec::::new(); let mut all = Vec::::new(); - for mut pattern in self.workspaces { - pattern.push_str(if pattern.ends_with('/') { "package.json" } else { "/package.json" }); - // npm treats workspace patterns as root-relative and strips a leading `./`. - let path_start = usize::from(pattern.starts_with('!')); - if pattern[path_start..].starts_with("./") { - let mut normalized = Str::with_capacity(pattern.len() - 2); - normalized.push_str(&pattern[..path_start]); - normalized.push_str(&pattern[path_start + 2..]); - pattern = normalized; + for pattern in self.workspaces { + // Ported from npm's workspace pattern normalization: + // https://github.com/npm/map-workspaces/blob/76ea3c852171790e245052e006329de15b09e60e/lib/index.js#L17 + let exclusion_count = pattern.bytes().take_while(|byte| *byte == b'!').count(); + let path = &pattern[exclusion_count..]; + let path_without_optional_dot = path.strip_prefix('.').unwrap_or(path); + let path = if path_without_optional_dot.starts_with('/') { + path_without_optional_dot.trim_start_matches('/') + } else { + path + }; + let is_negated = exclusion_count % 2 == 1; + + let mut pattern = Str::with_capacity(pattern.len() + "/package.json".len()); + if is_negated { + pattern.push('!'); } - if pattern.starts_with('!') { + pattern.push_str(path); + pattern.push_str(if path.is_empty() || path.ends_with('/') { + "package.json" + } else { + "/package.json" + }); + + if is_negated { has_negated = true; } else { inclusions.push(pattern.clone()); @@ -1016,6 +1030,59 @@ mod tests { assert!(found_web_to_ui, "Web app should depend on UI"); } + #[test] + fn test_get_package_graph_npm_workspace_with_root_relative_prefixes() { + let temp_dir = TempDir::new().unwrap(); + let temp_dir_path = AbsolutePath::new(temp_dir.path()).unwrap(); + + let root_package = serde_json::json!({ + "name": "npm-monorepo", + "private": true, + "workspaces": [ + "./packages/single-slash", + ".//packages/double-slash", + ".///packages/triple-slash", + "/packages/rooted", + "//packages/double-rooted", + "!.///packages/excluded", + "!!./packages/even-negations", + "./packages/odd-negations", + "!!!.//packages/odd-negations" + ] + }); + fs::write(temp_dir_path.join("package.json"), root_package.to_string()).unwrap(); + + let package_names = [ + "single-slash", + "double-slash", + "triple-slash", + "rooted", + "double-rooted", + "even-negations", + ]; + for package_name in package_names.iter().copied().chain(["excluded", "odd-negations"]) { + let package_path = temp_dir_path.join("packages").join(package_name); + fs::create_dir_all(&package_path).unwrap(); + fs::write( + package_path.join("package.json"), + serde_json::json!({ "name": package_name }).to_string(), + ) + .unwrap(); + } + + let graph = discover_package_graph(temp_dir_path).unwrap(); + let packages_found: FxHashSet<_> = + graph.node_weights().map(|node| node.package_json.name.as_str()).collect(); + + assert_eq!(graph.node_count(), package_names.len() + 1); + assert!(packages_found.contains("npm-monorepo")); + for package_name in package_names { + assert!(packages_found.contains(package_name), "Should have found {package_name}"); + } + assert!(!packages_found.contains("excluded")); + assert!(!packages_found.contains("odd-negations")); + } + #[test] fn test_get_package_graph_yarn_workspace() { let temp_dir = TempDir::new().unwrap();