Skip to content

Commit 0167510

Browse files
committed
refactor: simplify local CLI workspace resolution
1 parent cf861e8 commit 0167510

3 files changed

Lines changed: 77 additions & 124 deletions

File tree

crates/vp_global_cli/src/commands/local_install.rs

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ pub(crate) fn local_vite_plus_boundary(cwd: &AbsolutePath) -> Option<AbsolutePat
2727
return Some(package_root.to_absolute_path_buf());
2828
};
2929
let boundary = match workspace_contains_package(&workspace, package_root) {
30-
Some(true) => workspace.path.to_absolute_path_buf(),
31-
Some(false) => package_root.to_absolute_path_buf(),
30+
Some(true) => workspace.path.as_ref(),
31+
Some(false) => package_root,
3232
None => return Some(package_root.to_absolute_path_buf()),
3333
};
3434

3535
if boundary == package_root {
36-
return package.has_vite_plus().then_some(boundary);
36+
return package.has_vite_plus().then(|| boundary.to_absolute_path_buf());
3737
}
3838

3939
let workspace_package_json = boundary.join("package.json");
@@ -44,7 +44,7 @@ pub(crate) fn local_vite_plus_boundary(cwd: &AbsolutePath) -> Option<AbsolutePat
4444
}
4545
None => false,
4646
};
47-
(package.has_vite_plus() || workspace_declares).then_some(boundary)
47+
(package.has_vite_plus() || workspace_declares).then(|| boundary.to_absolute_path_buf())
4848
}
4949

5050
#[derive(Deserialize)]
@@ -91,30 +91,24 @@ fn workspace_contains_package(workspace: &WorkspaceRoot, package: &AbsolutePath)
9191
WorkspaceFile::NonWorkspacePackage(_) => return Some(false),
9292
};
9393

94-
// Match vt_workspace's WorkspaceMemberGlobs normalization and ordered
95-
// exclusions, without walking the filesystem or loading a package graph.
96-
let patterns: Vec<Str> = patterns
97-
.iter()
98-
.map(|pattern| {
99-
let exclusions = pattern.bytes().take_while(|byte| *byte == b'!').count();
100-
let path = &pattern[exclusions..];
101-
let without_dot = path.strip_prefix('.').unwrap_or(path);
102-
let path = if without_dot.starts_with('/') {
103-
without_dot.trim_start_matches('/')
104-
} else {
105-
path
106-
};
107-
let mut normalized = Str::with_capacity(pattern.len() + "/package.json".len());
108-
if exclusions % 2 == 1 {
109-
normalized.push('!');
110-
}
111-
normalized.push_str(path);
112-
if !path.is_empty() && !path.ends_with('/') {
113-
normalized.push('/');
114-
}
115-
normalized.push_str("package.json");
116-
normalized
117-
})
118-
.collect();
94+
let patterns: Vec<Str> = patterns.into_iter().map(workspace_package_json_pattern).collect();
11995
Some(PathGlobSet::new(&patterns).ok()?.is_match(relative.as_path()))
12096
}
97+
98+
/// Match vt_workspace's WorkspaceMemberGlobs normalization, including negation.
99+
fn workspace_package_json_pattern(pattern: Str) -> Str {
100+
let exclusions = pattern.bytes().take_while(|byte| *byte == b'!').count();
101+
let path = &pattern[exclusions..];
102+
let path = path.strip_prefix("./").unwrap_or(path).trim_start_matches('/');
103+
104+
let mut normalized = Str::with_capacity(pattern.len() + "/package.json".len());
105+
if exclusions % 2 == 1 {
106+
normalized.push('!');
107+
}
108+
normalized.push_str(path);
109+
if !path.is_empty() && !path.ends_with('/') {
110+
normalized.push('/');
111+
}
112+
normalized.push_str("package.json");
113+
normalized
114+
}

crates/vp_global_cli/src/js_executor.rs

Lines changed: 48 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,7 @@ impl JsExecutor {
440440
) -> Option<AbsolutePathBuf> {
441441
use oxc_resolver::{ResolveOptions, Resolver};
442442

443-
// For projects that declare a vite-plus dependency, only trust an
444-
// install within their workspace; the Node-semantics resolution below
445-
// would otherwise walk past it (see `local_vite_plus_install_host`).
443+
// Enforce the workspace boundary before using Node's unbounded resolver.
446444
Self::local_vite_plus_install_host(project_path)?;
447445

448446
let resolver = Resolver::new(ResolveOptions {
@@ -560,19 +558,23 @@ mod tests {
560558
dir
561559
}
562560

563-
/// An independent project that *declares* a vite-plus dependency (with
564-
/// its own workspace marker) checked out inside another project's tree
565-
/// must not resolve the outer project's vite-plus when its own install is
566-
/// missing — the declaration makes "run `vp install`" the right answer,
567-
/// not silently delegating to an unrelated copy.
561+
fn write_local_cli(root: &AbsolutePath, version: &str) {
562+
let package_dir = root.join("node_modules/vite-plus");
563+
std::fs::create_dir_all(package_dir.join("dist")).unwrap();
564+
std::fs::write(
565+
package_dir.join("package.json"),
566+
vt_str::format!(r#"{{"version":"{version}"}}"#).as_bytes(),
567+
)
568+
.unwrap();
569+
std::fs::write(package_dir.join("dist/bin.js"), "").unwrap();
570+
}
571+
572+
/// A declared dependency must not resolve outside an independent workspace.
568573
#[test]
569574
fn local_resolution_stays_within_the_workspace() {
570575
let temp = tempfile::tempdir().unwrap();
571-
let outer = temp.path();
572-
std::fs::create_dir_all(outer.join("node_modules/vite-plus/dist")).unwrap();
573-
std::fs::write(outer.join("node_modules/vite-plus/package.json"), r#"{"version":"0.2.1"}"#)
574-
.unwrap();
575-
std::fs::write(outer.join("node_modules/vite-plus/dist/bin.js"), "").unwrap();
576+
let outer = AbsolutePath::new(temp.path()).unwrap();
577+
write_local_cli(outer, "0.2.1");
576578
std::fs::write(outer.join("pnpm-workspace.yaml"), "packages: []\n").unwrap();
577579
std::fs::write(outer.join("package.json"), r#"{"name":"outer"}"#).unwrap();
578580

@@ -585,21 +587,16 @@ mod tests {
585587
.unwrap();
586588
std::fs::write(inner.join("pnpm-workspace.yaml"), "packages: []\n").unwrap();
587589

588-
let inner = AbsolutePath::new(inner.as_path()).unwrap();
589-
assert_eq!(JsExecutor::local_vite_plus_install_host(inner), None);
590-
assert_eq!(JsExecutor::resolve_local_vite_plus_package_dir(inner), None);
591-
assert_eq!(JsExecutor::resolve_local_vite_plus(inner), None);
590+
assert_eq!(JsExecutor::local_vite_plus_install_host(&inner), None);
591+
assert_eq!(JsExecutor::resolve_local_vite_plus_package_dir(&inner), None);
592+
assert_eq!(JsExecutor::resolve_local_vite_plus(&inner), None);
592593
}
593594

594-
/// A workspace member still resolves the workspace root's install: the
595-
/// boundary is the workspace root, not the member directory. The root
596-
/// declares the dependency so the bounded walk is actually engaged —
597-
/// without a declaration this case would pass trivially via the
598-
/// unbounded default.
595+
/// The root declares vite-plus so this exercises bounded workspace lookup.
599596
#[test]
600597
fn workspace_member_resolves_the_workspace_root_install() {
601598
let temp = tempfile::tempdir().unwrap();
602-
let ws = temp.path();
599+
let ws = AbsolutePath::new(temp.path()).unwrap();
603600
std::fs::write(ws.join("pnpm-workspace.yaml"), "packages:\n - packages/*\n").unwrap();
604601
std::fs::write(
605602
ws.join("package.json"),
@@ -613,11 +610,10 @@ mod tests {
613610
std::fs::create_dir_all(&member).unwrap();
614611
std::fs::write(member.join("package.json"), r#"{"name":"app"}"#).unwrap();
615612

616-
let member = AbsolutePath::new(member.as_path()).unwrap();
617-
let host = JsExecutor::local_vite_plus_install_host(member)
613+
let host = JsExecutor::local_vite_plus_install_host(&member)
618614
.expect("workspace root install must stay resolvable");
619-
assert_eq!(host.as_path(), ws);
620-
let pkg_dir = JsExecutor::resolve_local_vite_plus_package_dir(member)
615+
assert_eq!(&host, ws);
616+
let pkg_dir = JsExecutor::resolve_local_vite_plus_package_dir(&member)
621617
.expect("workspace root install must stay resolvable");
622618
assert!(pkg_dir.as_path().ends_with("node_modules/vite-plus"));
623619
}
@@ -628,23 +624,16 @@ mod tests {
628624
[None, Some("{}"), Some(r#"{"devDependencies":{"vite-plus":"0.3.0"}}"#), Some("{")]
629625
{
630626
let temp = tempfile::tempdir().unwrap();
631-
let outer = temp.path();
627+
let outer = AbsolutePath::new(temp.path()).unwrap();
632628
if let Some(ancestor) = ancestor {
633629
std::fs::write(outer.join("package.json"), ancestor).unwrap();
634630
}
635-
std::fs::create_dir_all(outer.join("node_modules/vite-plus/dist")).unwrap();
636-
std::fs::write(
637-
outer.join("node_modules/vite-plus/package.json"),
638-
r#"{"version":"0.2.1"}"#,
639-
)
640-
.unwrap();
641-
std::fs::write(outer.join("node_modules/vite-plus/dist/bin.js"), "").unwrap();
631+
write_local_cli(outer, "0.2.1");
642632

643633
let workspace = outer.join("inner");
644634
std::fs::create_dir_all(workspace.join("src")).unwrap();
645635
std::fs::write(workspace.join("pnpm-workspace.yaml"), "packages: []\n").unwrap();
646636
for cwd in [&workspace, &workspace.join("src")] {
647-
let cwd = AbsolutePath::new(cwd).unwrap();
648637
assert_eq!(
649638
JsExecutor::local_vite_plus_install_host(cwd),
650639
None,
@@ -654,15 +643,11 @@ mod tests {
654643
}
655644

656645
// A missing root manifest must not prevent a workspace-local install.
657-
std::fs::create_dir_all(workspace.join("node_modules/vite-plus/dist")).unwrap();
658-
std::fs::write(
659-
workspace.join("node_modules/vite-plus/package.json"),
660-
r#"{"version":"0.3.0"}"#,
661-
)
662-
.unwrap();
663-
std::fs::write(workspace.join("node_modules/vite-plus/dist/bin.js"), "").unwrap();
664-
let cwd = AbsolutePath::new(&workspace).unwrap();
665-
assert_eq!(JsExecutor::local_vite_plus_install_host(cwd).as_deref(), Some(cwd));
646+
write_local_cli(&workspace, "0.3.0");
647+
assert_eq!(
648+
JsExecutor::local_vite_plus_install_host(&workspace).as_deref(),
649+
Some(workspace.as_ref())
650+
);
666651
}
667652
}
668653

@@ -699,16 +684,10 @@ mod tests {
699684
),
700685
] {
701686
let temp = tempfile::tempdir().unwrap();
702-
let root = temp.path();
687+
let root = AbsolutePath::new(temp.path()).unwrap();
703688
std::fs::write(root.join("package.json"), r#"{"name":"outer"}"#).unwrap();
704689
std::fs::write(root.join(workspace_file), content).unwrap();
705-
std::fs::create_dir_all(root.join("node_modules/vite-plus/dist")).unwrap();
706-
std::fs::write(
707-
root.join("node_modules/vite-plus/package.json"),
708-
r#"{"version":"0.3.0"}"#,
709-
)
710-
.unwrap();
711-
std::fs::write(root.join("node_modules/vite-plus/dist/bin.js"), "").unwrap();
690+
write_local_cli(root, "0.3.0");
712691
// Membership must not require reading unrelated members' manifests.
713692
std::fs::create_dir_all(root.join("packages/broken")).unwrap();
714693
std::fs::write(root.join("packages/broken/package.json"), "{").unwrap();
@@ -721,9 +700,8 @@ mod tests {
721700
)
722701
.unwrap();
723702
let cwd = project.join("src");
724-
let cwd = AbsolutePath::new(&cwd).unwrap();
725703
assert_eq!(
726-
JsExecutor::resolve_local_vite_plus(cwd).is_some(),
704+
JsExecutor::resolve_local_vite_plus(&cwd).is_some(),
727705
is_member,
728706
"{workspace_file}: {content}, package: {package}",
729707
);
@@ -742,80 +720,60 @@ mod tests {
742720
("{}", "\u{feff}{\"devDependencies\":{\"vite-plus\":\"0.3.0\"}}", None),
743721
] {
744722
let temp = tempfile::tempdir().unwrap();
745-
let root = temp.path();
723+
let root = AbsolutePath::new(temp.path()).unwrap();
746724
std::fs::write(root.join("package.json"), ancestor).unwrap();
747725
if let Some(workspace) = workspace {
748726
std::fs::write(root.join("pnpm-workspace.yaml"), workspace).unwrap();
749727
}
750-
std::fs::create_dir_all(root.join("node_modules/vite-plus/dist")).unwrap();
751-
std::fs::write(
752-
root.join("node_modules/vite-plus/package.json"),
753-
r#"{"version":"0.2.1"}"#,
754-
)
755-
.unwrap();
756-
std::fs::write(root.join("node_modules/vite-plus/dist/bin.js"), "").unwrap();
728+
write_local_cli(root, "0.2.1");
757729
let inner = root.join("inner");
758730
std::fs::create_dir_all(&inner).unwrap();
759731
std::fs::write(inner.join("package.json"), project).unwrap();
760-
let inner = AbsolutePath::new(&inner).unwrap();
761732
assert_eq!(
762-
JsExecutor::local_vite_plus_install_host(inner),
733+
JsExecutor::local_vite_plus_install_host(&inner),
763734
None,
764735
"{ancestor}: {project}"
765736
);
766-
assert_eq!(JsExecutor::resolve_local_vite_plus(inner), None);
737+
assert_eq!(JsExecutor::resolve_local_vite_plus(&inner), None);
767738
}
768739
}
769740

770-
/// A project that does *not* declare a vite-plus dependency keeps Node's
771-
/// unbounded upward resolution even across its own workspace marker —
772-
/// this is the layout the snapshot harness depends on (staged workspaces
773-
/// with no `node_modules` of their own, resolving a run-root install).
741+
/// Undeclared projects retain Node's upward lookup, as used by snapshot fixtures.
774742
#[test]
775743
fn undeclared_project_keeps_the_unbounded_walk() {
776744
let temp = tempfile::tempdir().unwrap();
777-
let outer = temp.path();
778-
std::fs::create_dir_all(outer.join("node_modules/vite-plus/dist")).unwrap();
779-
std::fs::write(outer.join("node_modules/vite-plus/package.json"), r#"{"version":"0.2.1"}"#)
780-
.unwrap();
781-
std::fs::write(outer.join("node_modules/vite-plus/dist/bin.js"), "").unwrap();
745+
let outer = AbsolutePath::new(temp.path()).unwrap();
746+
write_local_cli(outer, "0.2.1");
782747

783748
let inner = outer.join("cases/one/workspace");
784749
std::fs::create_dir_all(&inner).unwrap();
785750
std::fs::write(inner.join("package.json"), r#"{"name":"inner"}"#).unwrap();
786751
std::fs::write(inner.join("pnpm-workspace.yaml"), "packages: []\n").unwrap();
787752

788-
let inner = AbsolutePath::new(inner.as_path()).unwrap();
789-
let host = JsExecutor::local_vite_plus_install_host(inner)
753+
let host = JsExecutor::local_vite_plus_install_host(&inner)
790754
.expect("undeclared projects keep the unbounded walk");
791-
assert_eq!(host.as_path(), outer);
792-
let pkg_dir = JsExecutor::resolve_local_vite_plus_package_dir(inner)
755+
assert_eq!(&host, outer);
756+
let pkg_dir = JsExecutor::resolve_local_vite_plus_package_dir(&inner)
793757
.expect("undeclared projects keep the unbounded walk");
794758
assert!(pkg_dir.as_path().ends_with("node_modules/vite-plus"));
795759
}
796760

797-
/// Without any project marker around (`find_workspace_root` errors) there
798-
/// is no boundary to protect; the walk stays unbounded as before.
799-
///
800-
/// Unix-only: the premise is that no ancestor of the tempdir carries a
801-
/// package.json, which holds for `/tmp` / `/var/folders` but not for
802-
/// Windows, where `%TEMP%` lives under the user profile and a stray
803-
/// `package.json` there would create a boundary and fail the test.
761+
/// Unix-only: Windows tempdirs can have a package.json in an ancestor profile
762+
/// directory, which would invalidate this test's markerless layout.
804763
#[cfg(unix)]
805764
#[test]
806765
fn unbounded_walk_without_project_markers() {
807766
let temp = tempfile::tempdir().unwrap();
808-
let root = temp.path();
767+
let root = AbsolutePath::new(temp.path()).unwrap();
809768
std::fs::create_dir_all(root.join("node_modules/vite-plus")).unwrap();
810769
std::fs::write(root.join("node_modules/vite-plus/package.json"), r#"{"version":"1.0.0"}"#)
811770
.unwrap();
812771
let nested = root.join("a/b");
813772
std::fs::create_dir_all(&nested).unwrap();
814773

815-
let nested = AbsolutePath::new(nested.as_path()).unwrap();
816-
let host = JsExecutor::local_vite_plus_install_host(nested)
774+
let host = JsExecutor::local_vite_plus_install_host(&nested)
817775
.expect("markerless directories keep the unbounded walk");
818-
assert_eq!(host.as_path(), root);
776+
assert_eq!(&host, root);
819777
}
820778

821779
#[test]

crates/vp_setup/src/install.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -835,16 +835,17 @@ mod tests {
835835
tokio::fs::create_dir_all(&node_bin).await.unwrap();
836836
tokio::fs::create_dir_all(&pnpm_bin).await.unwrap();
837837

838-
// Execute an existing shell, with the generated script as input.
839-
// Parallel process creation can briefly inherit a newly written
840-
// executable's open descriptor and cause ETXTBSY on Linux.
841-
// Keep the sh basename: BusyBox selects its applet from argv[0].
838+
// Use an existing executable to avoid ETXTBSY from inherited writable
839+
// descriptors. Keep the sh basename so BusyBox selects the shell applet.
842840
let runtime_binary = node_bin.join("sh");
843841
std::os::unix::fs::symlink("/bin/sh", &runtime_binary).unwrap();
844842
let pnpm_entry = pnpm_bin.join("pnpm.cjs");
845843
tokio::fs::write(
846844
&pnpm_entry,
847-
"printf '%s\\n' \"$0\" \"$@\" > invocation.txt\nprintf '%s' \"$PATH\" > path.txt\nprintf '%s' \"$npm_config_registry\" > registry.txt\n",
845+
r#"printf '%s\n' "$0" "$@" > invocation.txt
846+
printf '%s' "$PATH" > path.txt
847+
printf '%s' "$npm_config_registry" > registry.txt
848+
"#,
848849
)
849850
.await
850851
.unwrap();

0 commit comments

Comments
 (0)