Skip to content

Commit 6529a63

Browse files
authored
fix(install): auto-create package.json when missing in vp install / vp add (#545)
Running `vp install` or `vp add` in a directory without package.json previously failed with "Package not found in workspace". Now a minimal `{ "type": "module" }` package.json is created automatically, allowing PM detection and install to proceed.
1 parent 194c0dc commit 6529a63

19 files changed

Lines changed: 163 additions & 65 deletions

File tree

crates/vite_global_cli/src/commands/add.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl AddCommand {
3636
pass_through_args: Option<&[String]>,
3737
) -> Result<ExitStatus, Error> {
3838
prepend_js_runtime_to_path_env(&self.cwd).await?;
39+
super::ensure_package_json(&self.cwd).await?;
3940

4041
let add_command_options = AddCommandOptions {
4142
packages,

crates/vite_global_cli/src/commands/dedupe.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::process::ExitStatus;
22

3-
use vite_install::{commands::dedupe::DedupeCommandOptions, package_manager::PackageManager};
3+
use vite_install::commands::dedupe::DedupeCommandOptions;
44
use vite_path::AbsolutePathBuf;
55

6-
use super::prepend_js_runtime_to_path_env;
6+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
77
use crate::error::Error;
88

99
/// Dedupe command for deduplicating dependencies by removing older versions.
@@ -26,8 +26,7 @@ impl DedupeCommand {
2626
) -> Result<ExitStatus, Error> {
2727
prepend_js_runtime_to_path_env(&self.cwd).await?;
2828

29-
// Detect package manager
30-
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
29+
let package_manager = build_package_manager(&self.cwd).await?;
3130

3231
let dedupe_command_options = DedupeCommandOptions { check, pass_through_args };
3332
Ok(package_manager.run_dedupe_command(&dedupe_command_options, &self.cwd).await?)

crates/vite_global_cli/src/commands/dlx.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::process::ExitStatus;
22

3-
use vite_install::{commands::dlx::DlxCommandOptions, package_manager::PackageManager};
3+
use vite_install::commands::dlx::DlxCommandOptions;
44
use vite_path::AbsolutePathBuf;
55

6-
use super::prepend_js_runtime_to_path_env;
6+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
77
use crate::error::Error;
88

99
/// Dlx command for executing packages without installing them as dependencies.
@@ -40,8 +40,7 @@ impl DlxCommand {
4040
let package_spec = &args[0];
4141
let command_args: Vec<String> = args[1..].to_vec();
4242

43-
// Detect package manager
44-
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
43+
let package_manager = build_package_manager(&self.cwd).await?;
4544

4645
let dlx_command_options = DlxCommandOptions {
4746
packages: &packages,

crates/vite_global_cli/src/commands/install.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ impl InstallCommand {
1818

1919
pub async fn execute(self, options: &InstallCommandOptions<'_>) -> Result<ExitStatus, Error> {
2020
prepend_js_runtime_to_path_env(&self.cwd).await?;
21+
super::ensure_package_json(&self.cwd).await?;
2122

2223
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
2324

@@ -83,6 +84,42 @@ mod tests {
8384
assert!(result.is_ok());
8485
}
8586

87+
#[tokio::test]
88+
async fn test_ensure_package_json_creates_when_missing() {
89+
let temp_dir = TempDir::new().unwrap();
90+
let dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
91+
let package_json_path = dir_path.join("package.json");
92+
93+
// Verify no package.json exists
94+
assert!(!package_json_path.as_path().exists());
95+
96+
// Call ensure_package_json
97+
crate::commands::ensure_package_json(&dir_path).await.unwrap();
98+
99+
// Verify package.json was created with correct content
100+
let content = fs::read_to_string(&package_json_path).unwrap();
101+
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
102+
assert_eq!(parsed["type"], "module");
103+
}
104+
105+
#[tokio::test]
106+
async fn test_ensure_package_json_does_not_overwrite_existing() {
107+
let temp_dir = TempDir::new().unwrap();
108+
let dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
109+
let package_json_path = dir_path.join("package.json");
110+
111+
// Create an existing package.json
112+
let existing_content = r#"{"name": "existing-package"}"#;
113+
fs::write(&package_json_path, existing_content).unwrap();
114+
115+
// Call ensure_package_json
116+
crate::commands::ensure_package_json(&dir_path).await.unwrap();
117+
118+
// Verify existing package.json was NOT overwritten
119+
let content = fs::read_to_string(&package_json_path).unwrap();
120+
assert_eq!(content, existing_content);
121+
}
122+
86123
#[tokio::test]
87124
async fn test_install_command_execute_with_invalid_workspace() {
88125
let temp_dir = TempDir::new().unwrap();

crates/vite_global_cli/src/commands/link.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::process::ExitStatus;
22

3-
use vite_install::{commands::link::LinkCommandOptions, package_manager::PackageManager};
3+
use vite_install::commands::link::LinkCommandOptions;
44
use vite_path::AbsolutePathBuf;
55

6-
use super::prepend_js_runtime_to_path_env;
6+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
77
use crate::error::Error;
88

99
/// Link command for local package development.
@@ -26,8 +26,7 @@ impl LinkCommand {
2626
) -> Result<ExitStatus, Error> {
2727
prepend_js_runtime_to_path_env(&self.cwd).await?;
2828

29-
// Detect package manager
30-
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
29+
let package_manager = build_package_manager(&self.cwd).await?;
3130

3231
let link_command_options = LinkCommandOptions { package, pass_through_args };
3332
Ok(package_manager.run_link_command(&link_command_options, &self.cwd).await?)

crates/vite_global_cli/src/commands/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,26 @@
2323
//! Category C - Local CLI Delegation:
2424
//! - `delegate`: Local CLI delegation
2525
26+
use vite_install::package_manager::PackageManager;
2627
use vite_path::AbsolutePath;
2728
use vite_shared::{PrependOptions, prepend_to_path_env};
2829

2930
use crate::{error::Error, js_executor::JsExecutor};
3031

32+
/// Ensure a package.json exists in the given directory.
33+
/// If it doesn't exist, create a minimal one with `{ "type": "module" }`.
34+
pub async fn ensure_package_json(project_path: &AbsolutePath) -> Result<(), Error> {
35+
let package_json_path = project_path.join("package.json");
36+
if !package_json_path.as_path().exists() {
37+
let content = serde_json::to_string_pretty(&serde_json::json!({
38+
"type": "module"
39+
}))?;
40+
tokio::fs::write(&package_json_path, format!("{content}\n")).await?;
41+
tracing::info!("Created package.json in {:?}", project_path);
42+
}
43+
Ok(())
44+
}
45+
3146
/// Ensure the JS runtime is downloaded and prepend its bin directory to PATH.
3247
/// This should be called before executing any package manager command.
3348
///
@@ -54,6 +69,17 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu
5469
Ok(())
5570
}
5671

72+
/// Build a PackageManager, converting PackageJsonNotFound into a friendly error message.
73+
pub async fn build_package_manager(cwd: &AbsolutePath) -> Result<PackageManager, Error> {
74+
match PackageManager::builder(cwd).build_with_default().await {
75+
Ok(pm) => Ok(pm),
76+
Err(vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(_))) => {
77+
Err(Error::UserMessage("No package.json found.".into()))
78+
}
79+
Err(e) => Err(e.into()),
80+
}
81+
}
82+
5783
// Category A: Package manager commands
5884
pub mod add;
5985
pub mod dedupe;

crates/vite_global_cli/src/commands/outdated.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
use std::process::ExitStatus;
22

3-
use vite_install::{
4-
commands::outdated::{Format, OutdatedCommandOptions},
5-
package_manager::PackageManager,
6-
};
3+
use vite_install::commands::outdated::{Format, OutdatedCommandOptions};
74
use vite_path::AbsolutePathBuf;
85

9-
use super::prepend_js_runtime_to_path_env;
6+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
107
use crate::error::Error;
118

129
/// Outdated command for checking outdated packages.
@@ -41,8 +38,7 @@ impl OutdatedCommand {
4138
) -> Result<ExitStatus, Error> {
4239
prepend_js_runtime_to_path_env(&self.cwd).await?;
4340

44-
// Detect package manager
45-
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
41+
let package_manager = build_package_manager(&self.cwd).await?;
4642

4743
let outdated_command_options = OutdatedCommandOptions {
4844
packages,

crates/vite_global_cli/src/commands/pm.rs

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@
66
77
use std::process::ExitStatus;
88

9-
use vite_install::{
10-
PackageManager,
11-
commands::{
12-
cache::CacheCommandOptions, config::ConfigCommandOptions, list::ListCommandOptions,
13-
owner::OwnerSubcommand, pack::PackCommandOptions, prune::PruneCommandOptions,
14-
publish::PublishCommandOptions, view::ViewCommandOptions,
15-
},
9+
use vite_install::commands::{
10+
cache::CacheCommandOptions, config::ConfigCommandOptions, list::ListCommandOptions,
11+
owner::OwnerSubcommand, pack::PackCommandOptions, prune::PruneCommandOptions,
12+
publish::PublishCommandOptions, view::ViewCommandOptions,
1613
};
1714
use vite_path::AbsolutePathBuf;
1815

19-
use super::prepend_js_runtime_to_path_env;
16+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
2017
use crate::{
2118
cli::{ConfigCommands, OwnerCommands, PmCommands},
2219
error::Error,
@@ -32,7 +29,7 @@ pub async fn execute_info(
3229
) -> Result<ExitStatus, Error> {
3330
prepend_js_runtime_to_path_env(&cwd).await?;
3431

35-
let package_manager = PackageManager::builder(&cwd).build_with_default().await?;
32+
let package_manager = build_package_manager(&cwd).await?;
3633

3734
let options = ViewCommandOptions { package, field, json, pass_through_args };
3835

@@ -51,24 +48,7 @@ pub async fn execute_pm_subcommand(
5148

5249
prepend_js_runtime_to_path_env(&cwd).await?;
5350

54-
let package_manager = match PackageManager::builder(&cwd).build_with_default().await {
55-
Ok(pm) => pm,
56-
Err(e) => {
57-
// For `list` command, silently succeed when no workspace is found
58-
// (matches `pnpm list` behavior in dirs without package.json)
59-
if matches!(&command, PmCommands::List { .. })
60-
&& matches!(
61-
&e,
62-
vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound(
63-
_
64-
))
65-
)
66-
{
67-
return Ok(ExitStatus::default());
68-
}
69-
return Err(e.into());
70-
}
71-
};
51+
let package_manager = build_package_manager(&cwd).await?;
7252

7353
match command {
7454
PmCommands::Prune { prod, no_optional, pass_through_args } => {

crates/vite_global_cli/src/commands/remove.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::process::ExitStatus;
22

3-
use vite_install::{commands::remove::RemoveCommandOptions, package_manager::PackageManager};
3+
use vite_install::commands::remove::RemoveCommandOptions;
44
use vite_path::AbsolutePathBuf;
55

6-
use super::prepend_js_runtime_to_path_env;
6+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
77
use crate::error::Error;
88

99
/// Remove command for removing packages from dependencies.
@@ -33,8 +33,7 @@ impl RemoveCommand {
3333
) -> Result<ExitStatus, Error> {
3434
prepend_js_runtime_to_path_env(&self.cwd).await?;
3535

36-
// Detect package manager
37-
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
36+
let package_manager = build_package_manager(&self.cwd).await?;
3837

3938
let remove_command_options = RemoveCommandOptions {
4039
packages,

crates/vite_global_cli/src/commands/unlink.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::process::ExitStatus;
22

3-
use vite_install::{commands::unlink::UnlinkCommandOptions, package_manager::PackageManager};
3+
use vite_install::commands::unlink::UnlinkCommandOptions;
44
use vite_path::AbsolutePathBuf;
55

6-
use super::prepend_js_runtime_to_path_env;
6+
use super::{build_package_manager, prepend_js_runtime_to_path_env};
77
use crate::error::Error;
88

99
/// Unlink command for removing package links.
@@ -27,8 +27,7 @@ impl UnlinkCommand {
2727
) -> Result<ExitStatus, Error> {
2828
prepend_js_runtime_to_path_env(&self.cwd).await?;
2929

30-
// Detect package manager
31-
let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?;
30+
let package_manager = build_package_manager(&self.cwd).await?;
3231

3332
let unlink_command_options = UnlinkCommandOptions { package, recursive, pass_through_args };
3433
Ok(package_manager.run_unlink_command(&unlink_command_options, &self.cwd).await?)

0 commit comments

Comments
 (0)