diff --git a/Cargo.lock b/Cargo.lock index 15ab80216..d63ca88ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1023,6 +1023,7 @@ version = "0.0.1" dependencies = [ "anyhow", "clap", + "pacquet_executor", "pacquet_package_json", "pacquet_package_manager", "tempfile", @@ -1031,6 +1032,13 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "pacquet_executor" +version = "0.0.1" +dependencies = [ + "thiserror", +] + [[package]] name = "pacquet_lockfile" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 6194492b0..9a5a80a69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ pacquet_package_json = { path = "crates/package_json" } pacquet_lockfile = { path = "crates/lockfile" } pacquet_npmrc = { path = "crates/npmrc" } pacquet_package_manager = { path = "crates/package_manager" } +pacquet_executor = { path = "crates/executor" } anyhow = { version = "1.0.71", features = ["backtrace"] } async-recursion = { version = "1.0.4" } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 98bbd3684..b37f1bf43 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] pacquet_package_json = { workspace = true } pacquet_package_manager = { workspace = true } +pacquet_executor = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } diff --git a/crates/cli/README.md b/crates/cli/README.md index 9395aaa8c..a13624b97 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -68,3 +68,13 @@ ## `pacquet test` [pnpm documentation](https://pnpm.io/cli/test) + +## `pacquet start` + +[pnpm documentation](https://pnpm.io/cli/start) + +# Misc. + +## `pacquet init` + +[pnpm documentation](https://pnpm.io/cli/init) diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index b4aa1e6a9..fa19ca1fc 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -23,8 +23,9 @@ pub enum Subcommands { /// Runs a package's "test" script, if one was provided. Test, /// Runs a defined package script. - #[clap(name = "run")] - RunScript(RunScriptArgs), + Run(RunArgs), + /// Runs an arbitrary command specified in the package's start property of its scripts object. + Start, } #[derive(Parser, Debug)] @@ -80,7 +81,7 @@ impl AddArgs { } #[derive(Parser, Debug)] -pub struct RunScriptArgs { +pub struct RunArgs { /// A pre-defined package script. pub command: String, /// You can use the --if-present flag to avoid exiting with a non-zero exit code when the diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 1126b14be..860f33662 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -6,6 +6,7 @@ use std::env; use anyhow::{Context, Result}; use clap::Parser; use commands::{Cli, Subcommands}; +use pacquet_executor::execute_shell; use pacquet_package_json::PackageJson; use pacquet_package_manager::PackageManager; @@ -25,27 +26,60 @@ async fn run_commands(cli: Cli) -> Result<()> { match &cli.subcommand { Subcommands::Init => { // init command throws an error if package.json file exist. - PackageJson::init(&package_json_path)?; + PackageJson::init(&package_json_path).with_context(|| { + format!("Failed to initialize package.json at path {:?}", &package_json_path) + })?; } Subcommands::Add(args) => { - let mut package_manager = PackageManager::new(package_json_path)?; + let mut package_manager = + PackageManager::new(&package_json_path).with_context(|| { + format!("Failed to read package.json at path {:?}", &package_json_path) + })?; // TODO if a package already exists in another dependency group, we don't remove // the existing entry. package_manager .add(&args.package, args.get_dependency_group(), args.save_exact) - .await?; + .await + .with_context(|| format!("Failed to add package {}", &args.package))?; } Subcommands::Install(args) => { - let mut package_manager = PackageManager::new(package_json_path)?; - package_manager.install(args.dev, !args.no_optional).await?; + let mut package_manager = + PackageManager::new(&package_json_path).with_context(|| { + format!("Failed to read package.json at path {:?}", &package_json_path) + })?; + package_manager.install(args.dev, !args.no_optional).await.with_context(|| { + format!("Failed to install packages on {:?}", package_json_path) + })?; } Subcommands::Test => { - PackageJson::from_path(&package_json_path)?.execute_command("test", false)?; + let package_json = PackageJson::from_path(&package_json_path).with_context(|| { + format!("Failed to read package.json at path {:?}", &package_json_path) + })?; + if let Some(script) = package_json.get_script("test", false)? { + execute_shell(script)?; + } } - Subcommands::RunScript(args) => { - let command = &args.command; - PackageJson::from_path(&package_json_path)? - .execute_command(command, args.if_present)?; + Subcommands::Run(args) => { + let package_json = PackageJson::from_path(&package_json_path).with_context(|| { + format!("Failed to read package.json at path {:?}", &package_json_path) + })?; + if let Some(script) = package_json.get_script(&args.command, args.if_present)? { + execute_shell(script)?; + } + } + Subcommands::Start => { + // Runs an arbitrary command specified in the package's start property of its scripts + // object. If no start property is specified on the scripts object, it will attempt to + // run node server.js as a default, failing if neither are present. + // The intended usage of the property is to specify a command that starts your program. + let package_json = PackageJson::from_path(&package_json_path).with_context(|| { + format!("Failed to read package.json at path {:?}", &package_json_path) + })?; + if let Some(script) = package_json.get_script("start", true)? { + execute_shell(script)?; + } else { + execute_shell("node server.js")?; + } } } @@ -67,7 +101,6 @@ mod tests { env::set_current_dir(parent_folder.path()).unwrap(); let cli = Cli::parse_from(["", "init"]); run_commands(cli).await.unwrap(); - assert!(parent_folder.path().join("package.json").exists()); env::set_current_dir(¤t_directory).unwrap(); } diff --git a/crates/executor/Cargo.toml b/crates/executor/Cargo.toml new file mode 100644 index 000000000..add7459bf --- /dev/null +++ b/crates/executor/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pacquet_executor" +version = "0.0.1" +publish = false +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +thiserror = { workspace = true } diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs new file mode 100644 index 000000000..1636d6176 --- /dev/null +++ b/crates/executor/src/lib.rs @@ -0,0 +1,18 @@ +use std::process::Command; + +use thiserror::Error; + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum ExecutorError { + #[error("io error")] + Io(#[from] std::io::Error), +} + +pub fn execute_shell(command: &str) -> Result<(), ExecutorError> { + let mut cmd = Command::new(command).spawn()?; + + cmd.wait()?; + + Ok(()) +} diff --git a/crates/lockfile/src/lib.rs b/crates/lockfile/src/lib.rs index 18639691c..54e446113 100644 --- a/crates/lockfile/src/lib.rs +++ b/crates/lockfile/src/lib.rs @@ -15,9 +15,9 @@ use crate::package::LockfilePackage; #[derive(Error, Debug)] #[non_exhaustive] pub enum LockfileError { - #[error("filesystem error: `{0}`")] + #[error("filesystem error")] FileSystem(#[from] std::io::Error), - #[error("serialization error: `{0}")] + #[error("serialization error")] Serialization(#[from] serde_yaml::Error), } diff --git a/crates/package_json/src/error.rs b/crates/package_json/src/error.rs index 846a073d4..f100b7aa6 100644 --- a/crates/package_json/src/error.rs +++ b/crates/package_json/src/error.rs @@ -3,9 +3,9 @@ use thiserror::Error; #[derive(Error, Debug)] #[non_exhaustive] pub enum PackageJsonError { - #[error("serialization failed: {0}")] + #[error("serialization failed with {0}")] Serialization(#[from] serde_json::Error), - #[error("io error: `{0}`")] + #[error("io error")] Io(#[from] std::io::Error), #[error("package.json file already exists")] AlreadyExist, diff --git a/crates/package_json/src/lib.rs b/crates/package_json/src/lib.rs index 6dca47357..963d5c40f 100644 --- a/crates/package_json/src/lib.rs +++ b/crates/package_json/src/lib.rs @@ -7,7 +7,6 @@ use std::{ fs, io::{Read, Write}, path::PathBuf, - process::{Command, Stdio}, }; use serde_json::{json, Map, Value}; @@ -148,35 +147,20 @@ impl PackageJson { Ok(()) } - pub fn execute_command(&self, command: &str, if_present: bool) -> Result<(), PackageJsonError> { - match self - .value - .get("scripts") - .unwrap_or(&Value::default()) - .get(command) - .unwrap_or(&Value::default()) - .as_str() - { - Some(command) => { - let mut cmd = Command::new(command) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .stdin(Stdio::inherit()) - .spawn() - .unwrap(); - - cmd.wait().unwrap(); - - Ok(()) - } - None => { - if if_present { - Ok(()) - } else { - Err(PackageJsonError::NoScript(command.to_string())) + pub fn get_script( + &self, + command: &str, + if_present: bool, + ) -> Result, PackageJsonError> { + if let Some(scripts) = self.value.get("scripts") { + if let Some(script) = scripts.get(command) { + if let Some(script_str) = script.as_str() { + return Ok(Some(script_str)); } } } + + if if_present { Ok(None) } else { Err(PackageJsonError::NoScript(command.to_string())) } } } @@ -243,7 +227,7 @@ mod tests { let dir = tempdir().unwrap(); let tmp = dir.path().join("package.json"); let package_json = PackageJson::create_if_needed(&tmp).unwrap(); - package_json.execute_command("test", false).expect_err("test command should not exist"); + package_json.get_script("test", false).expect_err("test command should not exist"); } #[test] @@ -258,11 +242,9 @@ mod tests { let tmp = NamedTempFile::new().unwrap(); write!(tmp.as_file(), "{}", data).unwrap(); let package_json = PackageJson::create_if_needed(&tmp.path().to_path_buf()).unwrap(); - package_json.execute_command("test", false).unwrap(); - package_json - .execute_command("invalid", false) - .expect_err("invalid command should not exist"); - package_json.execute_command("invalid", true).unwrap(); + package_json.get_script("test", false).unwrap(); + package_json.get_script("invalid", false).expect_err("invalid command should not exist"); + package_json.get_script("invalid", true).unwrap(); } #[test] diff --git a/crates/package_manager/src/lib.rs b/crates/package_manager/src/lib.rs index 05137f19a..bcf250c85 100644 --- a/crates/package_manager/src/lib.rs +++ b/crates/package_manager/src/lib.rs @@ -12,11 +12,11 @@ pub mod install; #[derive(Error, Debug)] #[non_exhaustive] pub enum PackageManagerError { - #[error("tarball error: {0}")] + #[error("tarball error")] Tarball(#[from] pacquet_tarball::TarballError), - #[error("package.json error: {0}")] + #[error("package.json error")] PackageJson(#[from] pacquet_package_json::error::PackageJsonError), - #[error("registry error: {0}")] + #[error("registry error")] Registry(#[from] pacquet_registry::RegistryError), } diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index 208262423..ae10a851a 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -11,19 +11,19 @@ use crate::package::{Package, PackageVersion}; pub enum RegistryError { #[error("missing latest tag on {0}")] MissingLatestTag(String), - #[error("missing version {0} on package {0}")] + #[error("missing version {0} on package {1}")] MissingVersionRelease(String, String), #[error("network error while fetching {0}")] Network(#[from] reqwest::Error), - #[error("network middleware error")] + #[error("network middleware error with {0}")] NetworkMiddleware(#[from] reqwest_middleware::Error), - #[error("io error {0}")] + #[error("io error with {0}")] Io(#[from] std::io::Error), - #[error("serialization failed: {0}")] + #[error("serialization failed")] Serialization(String), - #[error("tarball error: {0}")] + #[error("tarball error with {0}")] Tarball(#[from] pacquet_tarball::TarballError), - #[error("package.json error: {0}")] + #[error("package.json error")] PackageJson(#[from] pacquet_package_json::error::PackageJsonError), }