Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/vite_global_cli/src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl AddCommand {
pass_through_args: Option<&[String]>,
) -> Result<ExitStatus, Error> {
prepend_js_runtime_to_path_env(&self.cwd).await?;
super::ensure_package_json(&self.cwd).await?;

let add_command_options = AddCommandOptions {
packages,
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/dedupe.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -26,8 +26,7 @@ impl DedupeCommand {
) -> Result<ExitStatus, Error> {
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?)
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/dlx.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -40,8 +40,7 @@ impl DlxCommand {
let package_spec = &args[0];
let command_args: Vec<String> = 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,
Expand Down
37 changes: 37 additions & 0 deletions crates/vite_global_cli/src/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ impl InstallCommand {

pub async fn execute(self, options: &InstallCommandOptions<'_>) -> Result<ExitStatus, Error> {
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?;

Expand Down Expand Up @@ -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();
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/link.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -26,8 +26,7 @@ impl LinkCommand {
) -> Result<ExitStatus, Error> {
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?)
Expand Down
26 changes: 26 additions & 0 deletions crates/vite_global_cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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<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()),
}
}

// Category A: Package manager commands
pub mod add;
pub mod dedupe;
Expand Down
10 changes: 3 additions & 7 deletions crates/vite_global_cli/src/commands/outdated.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -41,8 +38,7 @@ impl OutdatedCommand {
) -> Result<ExitStatus, Error> {
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,
Expand Down
34 changes: 7 additions & 27 deletions crates/vite_global_cli/src/commands/pm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,7 +29,7 @@ pub async fn execute_info(
) -> Result<ExitStatus, Error> {
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 };

Expand All @@ -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 } => {
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/remove.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -33,8 +33,7 @@ impl RemoveCommand {
) -> Result<ExitStatus, Error> {
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,
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/unlink.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -27,8 +27,7 @@ impl UnlinkCommand {
) -> Result<ExitStatus, Error> {
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?)
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/update.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -38,8 +38,7 @@ impl UpdateCommand {
) -> Result<ExitStatus, Error> {
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,
Expand Down
7 changes: 3 additions & 4 deletions crates/vite_global_cli/src/commands/why.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -40,8 +40,7 @@ impl WhyCommand {
) -> Result<ExitStatus, Error> {
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,
Expand Down
4 changes: 4 additions & 0 deletions crates/vite_global_cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)]
Expand Down
6 changes: 5 additions & 1 deletion crates/vite_global_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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@<semver>"
}
> vp add testnpm2 -D && cat package.json # should add package to auto-created package.json
Packages: +<variable>
+<repeat>
Progress: resolved <variable>, reused <variable>, downloaded <variable>, added <variable>, done

devDependencies:
+ testnpm2 <semver>

Done in <variable>ms using pnpm v<semver>
{
"type": "module",
"packageManager": "pnpm@<semver>",
"devDependencies": {
"testnpm2": "^1.0.1"
}
}
Loading
Loading