diff --git a/crates/vite_global_cli/src/commands/add.rs b/crates/vite_global_cli/src/commands/add.rs index 4cd90cdffc..30b51cb1f0 100644 --- a/crates/vite_global_cli/src/commands/add.rs +++ b/crates/vite_global_cli/src/commands/add.rs @@ -36,6 +36,7 @@ impl AddCommand { pass_through_args: Option<&[String]>, ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; + super::ensure_package_json(&self.cwd).await?; let add_command_options = AddCommandOptions { packages, diff --git a/crates/vite_global_cli/src/commands/dedupe.rs b/crates/vite_global_cli/src/commands/dedupe.rs index f99b392de4..d4187c4425 100644 --- a/crates/vite_global_cli/src/commands/dedupe.rs +++ b/crates/vite_global_cli/src/commands/dedupe.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::dedupe::DedupeCommandOptions, package_manager::PackageManager}; +use vite_install::commands::dedupe::DedupeCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Dedupe command for deduplicating dependencies by removing older versions. @@ -26,8 +26,7 @@ impl DedupeCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let dedupe_command_options = DedupeCommandOptions { check, pass_through_args }; Ok(package_manager.run_dedupe_command(&dedupe_command_options, &self.cwd).await?) diff --git a/crates/vite_global_cli/src/commands/dlx.rs b/crates/vite_global_cli/src/commands/dlx.rs index 605bf14c7b..6e23876902 100644 --- a/crates/vite_global_cli/src/commands/dlx.rs +++ b/crates/vite_global_cli/src/commands/dlx.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::dlx::DlxCommandOptions, package_manager::PackageManager}; +use vite_install::commands::dlx::DlxCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Dlx command for executing packages without installing them as dependencies. @@ -40,8 +40,7 @@ impl DlxCommand { let package_spec = &args[0]; let command_args: Vec = args[1..].to_vec(); - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let dlx_command_options = DlxCommandOptions { packages: &packages, diff --git a/crates/vite_global_cli/src/commands/install.rs b/crates/vite_global_cli/src/commands/install.rs index 02d80fd74f..1a52c1e055 100644 --- a/crates/vite_global_cli/src/commands/install.rs +++ b/crates/vite_global_cli/src/commands/install.rs @@ -18,6 +18,7 @@ impl InstallCommand { pub async fn execute(self, options: &InstallCommandOptions<'_>) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; + super::ensure_package_json(&self.cwd).await?; let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; @@ -83,6 +84,42 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn test_ensure_package_json_creates_when_missing() { + let temp_dir = TempDir::new().unwrap(); + let dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_json_path = dir_path.join("package.json"); + + // Verify no package.json exists + assert!(!package_json_path.as_path().exists()); + + // Call ensure_package_json + crate::commands::ensure_package_json(&dir_path).await.unwrap(); + + // Verify package.json was created with correct content + let content = fs::read_to_string(&package_json_path).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(parsed["type"], "module"); + } + + #[tokio::test] + async fn test_ensure_package_json_does_not_overwrite_existing() { + let temp_dir = TempDir::new().unwrap(); + let dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let package_json_path = dir_path.join("package.json"); + + // Create an existing package.json + let existing_content = r#"{"name": "existing-package"}"#; + fs::write(&package_json_path, existing_content).unwrap(); + + // Call ensure_package_json + crate::commands::ensure_package_json(&dir_path).await.unwrap(); + + // Verify existing package.json was NOT overwritten + let content = fs::read_to_string(&package_json_path).unwrap(); + assert_eq!(content, existing_content); + } + #[tokio::test] async fn test_install_command_execute_with_invalid_workspace() { let temp_dir = TempDir::new().unwrap(); diff --git a/crates/vite_global_cli/src/commands/link.rs b/crates/vite_global_cli/src/commands/link.rs index 34b310d623..3356ee4a65 100644 --- a/crates/vite_global_cli/src/commands/link.rs +++ b/crates/vite_global_cli/src/commands/link.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::link::LinkCommandOptions, package_manager::PackageManager}; +use vite_install::commands::link::LinkCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Link command for local package development. @@ -26,8 +26,7 @@ impl LinkCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let link_command_options = LinkCommandOptions { package, pass_through_args }; Ok(package_manager.run_link_command(&link_command_options, &self.cwd).await?) diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index f7a23a9ae2..734ae6438c 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -23,11 +23,26 @@ //! Category C - Local CLI Delegation: //! - `delegate`: Local CLI delegation +use vite_install::package_manager::PackageManager; use vite_path::AbsolutePath; use vite_shared::{PrependOptions, prepend_to_path_env}; use crate::{error::Error, js_executor::JsExecutor}; +/// 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. /// @@ -54,6 +69,17 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu Ok(()) } +/// Build a PackageManager, converting PackageJsonNotFound into a friendly error message. +pub async fn build_package_manager(cwd: &AbsolutePath) -> Result { + 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()), + } +} + // Category A: Package manager commands pub mod add; pub mod dedupe; diff --git a/crates/vite_global_cli/src/commands/outdated.rs b/crates/vite_global_cli/src/commands/outdated.rs index e1557de8c5..725e8a1d67 100644 --- a/crates/vite_global_cli/src/commands/outdated.rs +++ b/crates/vite_global_cli/src/commands/outdated.rs @@ -1,12 +1,9 @@ use std::process::ExitStatus; -use vite_install::{ - commands::outdated::{Format, OutdatedCommandOptions}, - package_manager::PackageManager, -}; +use vite_install::commands::outdated::{Format, OutdatedCommandOptions}; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Outdated command for checking outdated packages. @@ -41,8 +38,7 @@ impl OutdatedCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let outdated_command_options = OutdatedCommandOptions { packages, diff --git a/crates/vite_global_cli/src/commands/pm.rs b/crates/vite_global_cli/src/commands/pm.rs index 7f7e69a9fb..355d920b06 100644 --- a/crates/vite_global_cli/src/commands/pm.rs +++ b/crates/vite_global_cli/src/commands/pm.rs @@ -6,17 +6,14 @@ use std::process::ExitStatus; -use vite_install::{ - PackageManager, - commands::{ - cache::CacheCommandOptions, config::ConfigCommandOptions, list::ListCommandOptions, - owner::OwnerSubcommand, pack::PackCommandOptions, prune::PruneCommandOptions, - publish::PublishCommandOptions, view::ViewCommandOptions, - }, +use vite_install::commands::{ + cache::CacheCommandOptions, config::ConfigCommandOptions, list::ListCommandOptions, + owner::OwnerSubcommand, pack::PackCommandOptions, prune::PruneCommandOptions, + publish::PublishCommandOptions, view::ViewCommandOptions, }; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::{ cli::{ConfigCommands, OwnerCommands, PmCommands}, error::Error, @@ -32,7 +29,7 @@ pub async fn execute_info( ) -> Result { prepend_js_runtime_to_path_env(&cwd).await?; - let package_manager = PackageManager::builder(&cwd).build_with_default().await?; + let package_manager = build_package_manager(&cwd).await?; let options = ViewCommandOptions { package, field, json, pass_through_args }; @@ -51,24 +48,7 @@ pub async fn execute_pm_subcommand( prepend_js_runtime_to_path_env(&cwd).await?; - let package_manager = match PackageManager::builder(&cwd).build_with_default().await { - Ok(pm) => pm, - Err(e) => { - // For `list` command, silently succeed when no workspace is found - // (matches `pnpm list` behavior in dirs without package.json) - if matches!(&command, PmCommands::List { .. }) - && matches!( - &e, - vite_error::Error::WorkspaceError(vite_workspace::Error::PackageJsonNotFound( - _ - )) - ) - { - return Ok(ExitStatus::default()); - } - return Err(e.into()); - } - }; + let package_manager = build_package_manager(&cwd).await?; match command { PmCommands::Prune { prod, no_optional, pass_through_args } => { diff --git a/crates/vite_global_cli/src/commands/remove.rs b/crates/vite_global_cli/src/commands/remove.rs index dfe4c1f026..d1b43e02ce 100644 --- a/crates/vite_global_cli/src/commands/remove.rs +++ b/crates/vite_global_cli/src/commands/remove.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::remove::RemoveCommandOptions, package_manager::PackageManager}; +use vite_install::commands::remove::RemoveCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Remove command for removing packages from dependencies. @@ -33,8 +33,7 @@ impl RemoveCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let remove_command_options = RemoveCommandOptions { packages, diff --git a/crates/vite_global_cli/src/commands/unlink.rs b/crates/vite_global_cli/src/commands/unlink.rs index 8f88c869e7..1585ce5c3f 100644 --- a/crates/vite_global_cli/src/commands/unlink.rs +++ b/crates/vite_global_cli/src/commands/unlink.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::unlink::UnlinkCommandOptions, package_manager::PackageManager}; +use vite_install::commands::unlink::UnlinkCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Unlink command for removing package links. @@ -27,8 +27,7 @@ impl UnlinkCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let unlink_command_options = UnlinkCommandOptions { package, recursive, pass_through_args }; Ok(package_manager.run_unlink_command(&unlink_command_options, &self.cwd).await?) diff --git a/crates/vite_global_cli/src/commands/update.rs b/crates/vite_global_cli/src/commands/update.rs index 86a30727ae..9dada4a6d6 100644 --- a/crates/vite_global_cli/src/commands/update.rs +++ b/crates/vite_global_cli/src/commands/update.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::update::UpdateCommandOptions, package_manager::PackageManager}; +use vite_install::commands::update::UpdateCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Update command for updating packages to their latest versions. @@ -38,8 +38,7 @@ impl UpdateCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let update_command_options = UpdateCommandOptions { packages, diff --git a/crates/vite_global_cli/src/commands/why.rs b/crates/vite_global_cli/src/commands/why.rs index f2008abe52..d15d65b85f 100644 --- a/crates/vite_global_cli/src/commands/why.rs +++ b/crates/vite_global_cli/src/commands/why.rs @@ -1,9 +1,9 @@ use std::process::ExitStatus; -use vite_install::{commands::why::WhyCommandOptions, package_manager::PackageManager}; +use vite_install::commands::why::WhyCommandOptions; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{build_package_manager, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Why command for showing why a package is installed. @@ -40,8 +40,7 @@ impl WhyCommand { ) -> Result { prepend_js_runtime_to_path_env(&self.cwd).await?; - // Detect package manager - let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let package_manager = build_package_manager(&self.cwd).await?; let why_command_options = WhyCommandOptions { packages, diff --git a/crates/vite_global_cli/src/error.rs b/crates/vite_global_cli/src/error.rs index f9a1230c48..13f41d3a4a 100644 --- a/crates/vite_global_cli/src/error.rs +++ b/crates/vite_global_cli/src/error.rs @@ -43,6 +43,10 @@ pub enum Error { #[error("{0}")] Other(Str), + /// User-facing message printed without "Error: " prefix. + #[error("{0}")] + UserMessage(Str), + #[error( "Executable '{bin_name}' is already installed by {existing_package}\n\nPlease remove {existing_package} before installing {new_package}, or use --force to auto-replace" )] diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 85b706a3e9..9de22d01bb 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -90,7 +90,11 @@ async fn main() -> ExitCode { } } Err(e) => { - eprintln!("Error: {e}"); + if matches!(&e, error::Error::UserMessage(_)) { + eprintln!("{e}"); + } else { + eprintln!("Error: {e}"); + } ExitCode::FAILURE } } diff --git a/packages/global/snap-tests/command-install-auto-create-package-json/snap.txt b/packages/global/snap-tests/command-install-auto-create-package-json/snap.txt new file mode 100644 index 0000000000..2ec5ad94aa --- /dev/null +++ b/packages/global/snap-tests/command-install-auto-create-package-json/snap.txt @@ -0,0 +1,24 @@ +> test ! -f package.json && echo 'no package.json' # verify no package.json exists +no package.json + +> vp install --silent && cat package.json # should auto-create package.json and install +{ + "type": "module", + "packageManager": "pnpm@" +} +> vp add testnpm2 -D && cat package.json # should add package to auto-created package.json +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 + +Done in ms using pnpm v +{ + "type": "module", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} \ No newline at end of file diff --git a/packages/global/snap-tests/command-install-auto-create-package-json/steps.json b/packages/global/snap-tests/command-install-auto-create-package-json/steps.json new file mode 100644 index 0000000000..a600c41b6e --- /dev/null +++ b/packages/global/snap-tests/command-install-auto-create-package-json/steps.json @@ -0,0 +1,11 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "test ! -f package.json && echo 'no package.json' # verify no package.json exists", + "vp install --silent && cat package.json # should auto-create package.json and install", + "vp add testnpm2 -D && cat package.json # should add package to auto-created package.json" + ] +} diff --git a/packages/global/snap-tests/command-list-no-package-json/snap.txt b/packages/global/snap-tests/command-list-no-package-json/snap.txt index c6092decf4..1dbc16e03f 100644 --- a/packages/global/snap-tests/command-list-no-package-json/snap.txt +++ b/packages/global/snap-tests/command-list-no-package-json/snap.txt @@ -1,2 +1,5 @@ -> vp ls # should output nothing without package.json -> vp pm list # should output nothing without package.json \ No newline at end of file +[1]> vp ls # should output nothing without package.json +No package.json found. + +[1]> vp pm list # should output nothing without package.json +No package.json found. diff --git a/packages/global/snap-tests/command-pm-no-package-json/snap.txt b/packages/global/snap-tests/command-pm-no-package-json/snap.txt new file mode 100644 index 0000000000..4e36adb890 --- /dev/null +++ b/packages/global/snap-tests/command-pm-no-package-json/snap.txt @@ -0,0 +1,11 @@ +[1]> vp pm ls # should show friendly error +No package.json found. + +[1]> vp pm prune # should show friendly error +No package.json found. + +[1]> vp outdated # should show friendly error +No package.json found. + +[1]> vp why lodash # should show friendly error +No package.json found. diff --git a/packages/global/snap-tests/command-pm-no-package-json/steps.json b/packages/global/snap-tests/command-pm-no-package-json/steps.json new file mode 100644 index 0000000000..4710180dbb --- /dev/null +++ b/packages/global/snap-tests/command-pm-no-package-json/steps.json @@ -0,0 +1,8 @@ +{ + "commands": [ + "vp pm ls # should show friendly error", + "vp pm prune # should show friendly error", + "vp outdated # should show friendly error", + "vp why lodash # should show friendly error" + ] +}