2323//! Category C - Local CLI Delegation:
2424//! - `delegate`: Local CLI delegation
2525
26+ use std:: { collections:: HashMap , io:: BufReader } ;
27+
2628use vite_install:: package_manager:: PackageManager ;
2729use vite_path:: AbsolutePath ;
2830use vite_shared:: { PrependOptions , prepend_to_path_env} ;
2931
3032use crate :: { error:: Error , js_executor:: JsExecutor } ;
3133
34+ #[ derive( serde:: Deserialize , Default ) ]
35+ #[ serde( rename_all = "camelCase" ) ]
36+ struct DepCheckPackageJson {
37+ #[ serde( default ) ]
38+ dependencies : HashMap < String , serde_json:: Value > ,
39+ #[ serde( default ) ]
40+ dev_dependencies : HashMap < String , serde_json:: Value > ,
41+ }
42+
43+ /// Check if vite-plus is listed in the nearest package.json's
44+ /// dependencies or devDependencies.
45+ ///
46+ /// Returns `true` if vite-plus is found, `false` if not found
47+ /// or if no package.json exists.
48+ pub fn has_vite_plus_dependency ( cwd : & AbsolutePath ) -> bool {
49+ let mut current = cwd;
50+ loop {
51+ let package_json_path = current. join ( "package.json" ) ;
52+ if package_json_path. as_path ( ) . exists ( ) {
53+ if let Ok ( file) = std:: fs:: File :: open ( & package_json_path) {
54+ if let Ok ( pkg) =
55+ serde_json:: from_reader :: < _ , DepCheckPackageJson > ( BufReader :: new ( file) )
56+ {
57+ return pkg. dependencies . contains_key ( "vite-plus" )
58+ || pkg. dev_dependencies . contains_key ( "vite-plus" ) ;
59+ }
60+ }
61+ return false ; // Found package.json but couldn't parse deps → treat as no dependency
62+ }
63+ match current. parent ( ) {
64+ Some ( parent) if parent != current => current = parent,
65+ _ => return false , // Reached filesystem root
66+ }
67+ }
68+ }
69+
3270/// Ensure a package.json exists in the given directory.
3371/// If it doesn't exist, create a minimal one with `{ "type": "module" }`.
3472pub async fn ensure_package_json ( project_path : & AbsolutePath ) -> Result < ( ) , Error > {
@@ -106,6 +144,7 @@ pub mod self_update;
106144
107145// Category C: Local CLI Delegation
108146pub mod delegate;
147+ pub mod run_or_delegate;
109148
110149// Re-export command structs for convenient access
111150pub use add:: AddCommand ;
@@ -118,3 +157,99 @@ pub use remove::RemoveCommand;
118157pub use unlink:: UnlinkCommand ;
119158pub use update:: UpdateCommand ;
120159pub use why:: WhyCommand ;
160+
161+ #[ cfg( test) ]
162+ mod tests {
163+ use vite_path:: AbsolutePathBuf ;
164+
165+ use super :: * ;
166+
167+ #[ test]
168+ fn test_has_vite_plus_in_dev_dependencies ( ) {
169+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
170+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
171+ std:: fs:: write (
172+ temp_path. join ( "package.json" ) ,
173+ r#"{ "devDependencies": { "vite-plus": "^1.0.0" } }"# ,
174+ )
175+ . unwrap ( ) ;
176+ assert ! ( has_vite_plus_dependency( & temp_path) ) ;
177+ }
178+
179+ #[ test]
180+ fn test_has_vite_plus_in_dependencies ( ) {
181+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
182+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
183+ std:: fs:: write (
184+ temp_path. join ( "package.json" ) ,
185+ r#"{ "dependencies": { "vite-plus": "^1.0.0" } }"# ,
186+ )
187+ . unwrap ( ) ;
188+ assert ! ( has_vite_plus_dependency( & temp_path) ) ;
189+ }
190+
191+ #[ test]
192+ fn test_no_vite_plus_dependency ( ) {
193+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
194+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
195+ std:: fs:: write (
196+ temp_path. join ( "package.json" ) ,
197+ r#"{ "devDependencies": { "vite": "^6.0.0" } }"# ,
198+ )
199+ . unwrap ( ) ;
200+ assert ! ( !has_vite_plus_dependency( & temp_path) ) ;
201+ }
202+
203+ #[ test]
204+ fn test_no_package_json ( ) {
205+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
206+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
207+ assert ! ( !has_vite_plus_dependency( & temp_path) ) ;
208+ }
209+
210+ #[ test]
211+ fn test_nested_directory_walks_up ( ) {
212+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
213+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
214+ std:: fs:: write (
215+ temp_path. join ( "package.json" ) ,
216+ r#"{ "devDependencies": { "vite-plus": "^1.0.0" } }"# ,
217+ )
218+ . unwrap ( ) ;
219+ let child_dir = temp_path. join ( "child" ) ;
220+ std:: fs:: create_dir ( & child_dir) . unwrap ( ) ;
221+ let child_path = AbsolutePathBuf :: new ( child_dir. as_path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
222+ assert ! ( has_vite_plus_dependency( & child_path) ) ;
223+ }
224+
225+ #[ test]
226+ fn test_empty_package_json ( ) {
227+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
228+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
229+ std:: fs:: write ( temp_path. join ( "package.json" ) , r#"{}"# ) . unwrap ( ) ;
230+ assert ! ( !has_vite_plus_dependency( & temp_path) ) ;
231+ }
232+
233+ #[ test]
234+ fn test_nested_dir_stops_at_nearest_package_json ( ) {
235+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
236+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
237+ // Parent has vite-plus
238+ std:: fs:: write (
239+ temp_path. join ( "package.json" ) ,
240+ r#"{ "devDependencies": { "vite-plus": "^1.0.0" } }"# ,
241+ )
242+ . unwrap ( ) ;
243+ // Child has its own package.json without vite-plus
244+ let child_dir = temp_path. join ( "child" ) ;
245+ std:: fs:: create_dir ( & child_dir) . unwrap ( ) ;
246+ std:: fs:: write (
247+ child_dir. join ( "package.json" ) ,
248+ r#"{ "devDependencies": { "vite": "^6.0.0" } }"# ,
249+ )
250+ . unwrap ( ) ;
251+ let child_path = AbsolutePathBuf :: new ( child_dir. as_path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
252+ // Should find the child's package.json first and return false
253+ assert ! ( !has_vite_plus_dependency( & child_path) ) ;
254+ }
255+ }
0 commit comments