-
Notifications
You must be signed in to change notification settings - Fork 170
Expand file tree
/
Copy pathmod.rs
More file actions
293 lines (264 loc) · 10 KB
/
mod.rs
File metadata and controls
293 lines (264 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
//! Command implementations for the global CLI.
//!
//! Commands are organized by category:
//!
//! Category A - Package manager commands:
//! - `add`: Add packages to dependencies
//! - `install`: Install all dependencies
//! - `remove`: Remove packages from dependencies
//! - `update`: Update packages to their latest versions
//! - `dedupe`: Deduplicate dependencies
//! - `outdated`: Check for outdated packages
//! - `why`: Show why a package is installed
//! - `link`: Link packages for local development
//! - `unlink`: Unlink packages
//! - `dlx`: Execute a package binary without installing it
//! - `pm`: Forward commands to the package manager
//!
//! Category B - JS Script Commands:
//! - `create`: Project scaffolding
//! - `migrate`: Migration command
//! - `version`: Version display
//!
//! Category C - Local CLI Delegation:
//! - `delegate`: Local CLI delegation
use std::{collections::HashMap, io::BufReader};
use vite_install::package_manager::{PackageManager, PackageManagerType};
use vite_path::AbsolutePath;
use vite_shared::{PrependOptions, prepend_to_path_env};
use crate::{error::Error, js_executor::JsExecutor};
#[derive(serde::Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct DepCheckPackageJson {
#[serde(default)]
dependencies: HashMap<String, serde_json::Value>,
#[serde(default)]
dev_dependencies: HashMap<String, serde_json::Value>,
}
/// Check if vite-plus is listed in the nearest package.json's
/// dependencies or devDependencies.
///
/// Returns `true` if vite-plus is found, `false` if not found
/// or if no package.json exists.
pub fn has_vite_plus_dependency(cwd: &AbsolutePath) -> bool {
let mut current = cwd;
loop {
let package_json_path = current.join("package.json");
if package_json_path.as_path().exists() {
if let Ok(file) = std::fs::File::open(&package_json_path) {
if let Ok(pkg) =
serde_json::from_reader::<_, DepCheckPackageJson>(BufReader::new(file))
{
return pkg.dependencies.contains_key("vite-plus")
|| pkg.dev_dependencies.contains_key("vite-plus");
}
}
return false; // Found package.json but couldn't parse deps → treat as no dependency
}
match current.parent() {
Some(parent) if parent != current => current = parent,
_ => return false, // Reached filesystem root
}
}
}
/// Ensure a package.json exists in the given directory.
/// If it doesn't exist, create a minimal one with `{ "type": "module" }`.
pub async fn ensure_package_json(project_path: &AbsolutePath) -> Result<(), Error> {
let package_json_path = project_path.join("package.json");
if !package_json_path.as_path().exists() {
let content = serde_json::to_string_pretty(&serde_json::json!({
"type": "module"
}))?;
tokio::fs::write(&package_json_path, format!("{content}\n")).await?;
tracing::info!("Created package.json in {:?}", project_path);
}
Ok(())
}
/// Ensure the JS runtime is downloaded and prepend its bin directory to PATH.
/// This should be called before executing any package manager command.
///
/// If `project_path` contains a package.json, uses the project's runtime
/// (based on devEngines.runtime). Otherwise, falls back to the CLI's runtime.
pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Result<(), Error> {
let mut executor = JsExecutor::new(None);
// Use project runtime if package.json exists, otherwise use CLI runtime
let package_json_path = project_path.join("package.json");
let runtime = if package_json_path.as_path().exists() {
executor.ensure_project_runtime(project_path).await?
} else {
executor.ensure_cli_runtime().await?
};
let node_bin_prefix = runtime.get_bin_prefix();
// Use dedupe_anywhere=true to check if node bin already exists anywhere in PATH
let options = PrependOptions { dedupe_anywhere: true };
if prepend_to_path_env(&node_bin_prefix, options) {
tracing::debug!("Set PATH to include {:?}", node_bin_prefix);
}
Ok(())
}
/// Build a PackageManager, converting PackageJsonNotFound into a friendly error message.
pub async fn build_package_manager(cwd: &AbsolutePath) -> Result<PackageManager, Error> {
match PackageManager::builder(cwd).build_with_default().await {
Ok(pm) => Ok(pm),
Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) => {
Err(Error::UserMessage("No package.json found.".into()))
}
Err(e) => Err(e.into()),
}
}
/// Build a PackageManager, falling back to a default npm instance when no
/// package.json is found. Uses `build()` instead of `build_with_default()`
/// to skip the interactive package manager selection prompt on the fallback path.
///
/// Requires `prepend_js_runtime_to_path_env` to be called first so npm is on PATH.
pub async fn build_package_manager_or_npm_default(
cwd: &AbsolutePath,
) -> Result<PackageManager, Error> {
match PackageManager::builder(cwd).build().await {
Ok(pm) => Ok(pm),
Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_)))
| Err(vite_error::Error::UnrecognizedPackageManager) => {
Ok(default_npm_package_manager(cwd))
}
Err(e) => Err(e.into()),
}
}
fn default_npm_package_manager(cwd: &AbsolutePath) -> PackageManager {
PackageManager {
client: PackageManagerType::Npm,
package_name: "npm".into(),
version: "latest".into(),
hash: None,
bin_name: "npm".into(),
workspace_root: cwd.to_absolute_path_buf(),
is_monorepo: false,
install_dir: cwd.to_absolute_path_buf(),
}
}
// Category A: Package manager commands
pub mod add;
pub mod dedupe;
pub mod dlx;
pub mod install;
pub mod link;
pub mod outdated;
pub mod pm;
pub mod release;
pub mod remove;
pub mod unlink;
pub mod update;
pub mod why;
// Category B: JS Script Commands
pub mod config;
pub mod create;
pub mod migrate;
pub mod staged;
pub mod version;
// Category D: Environment Management
pub mod env;
// Standalone binary commands
pub mod vpr;
pub mod vpx;
// Self-Management
pub mod implode;
pub mod upgrade;
// Category C: Local CLI Delegation
pub mod delegate;
// Re-export command structs for convenient access
pub use add::AddCommand;
pub use dedupe::DedupeCommand;
pub use dlx::DlxCommand;
pub use install::InstallCommand;
pub use link::LinkCommand;
pub use outdated::OutdatedCommand;
pub use remove::RemoveCommand;
pub use unlink::UnlinkCommand;
pub use update::UpdateCommand;
pub use why::WhyCommand;
#[cfg(test)]
mod tests {
use vite_path::AbsolutePathBuf;
use super::*;
#[test]
fn test_has_vite_plus_in_dev_dependencies() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
std::fs::write(
temp_path.join("package.json"),
r#"{ "devDependencies": { "vite-plus": "^1.0.0" } }"#,
)
.unwrap();
assert!(has_vite_plus_dependency(&temp_path));
}
#[test]
fn test_has_vite_plus_in_dependencies() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
std::fs::write(
temp_path.join("package.json"),
r#"{ "dependencies": { "vite-plus": "^1.0.0" } }"#,
)
.unwrap();
assert!(has_vite_plus_dependency(&temp_path));
}
#[test]
fn test_no_vite_plus_dependency() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
std::fs::write(
temp_path.join("package.json"),
r#"{ "devDependencies": { "vite": "^6.0.0" } }"#,
)
.unwrap();
assert!(!has_vite_plus_dependency(&temp_path));
}
#[test]
fn test_no_package_json() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
assert!(!has_vite_plus_dependency(&temp_path));
}
#[test]
fn test_nested_directory_walks_up() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
std::fs::write(
temp_path.join("package.json"),
r#"{ "devDependencies": { "vite-plus": "^1.0.0" } }"#,
)
.unwrap();
let child_dir = temp_path.join("child");
std::fs::create_dir(&child_dir).unwrap();
let child_path = AbsolutePathBuf::new(child_dir.as_path().to_path_buf()).unwrap();
assert!(has_vite_plus_dependency(&child_path));
}
#[test]
fn test_empty_package_json() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
std::fs::write(temp_path.join("package.json"), r#"{}"#).unwrap();
assert!(!has_vite_plus_dependency(&temp_path));
}
#[test]
fn test_nested_dir_stops_at_nearest_package_json() {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
// Parent has vite-plus
std::fs::write(
temp_path.join("package.json"),
r#"{ "devDependencies": { "vite-plus": "^1.0.0" } }"#,
)
.unwrap();
// Child has its own package.json without vite-plus
let child_dir = temp_path.join("child");
std::fs::create_dir(&child_dir).unwrap();
std::fs::write(
child_dir.join("package.json"),
r#"{ "devDependencies": { "vite": "^6.0.0" } }"#,
)
.unwrap();
let child_path = AbsolutePathBuf::new(child_dir.as_path().to_path_buf()).unwrap();
// Should find the child's package.json first and return false
assert!(!has_vite_plus_dependency(&child_path));
}
}