-
Notifications
You must be signed in to change notification settings - Fork 7
feat: implement package manager #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||
| [package] | ||||||
| name = "xmas-package-manager" | ||||||
| version = "0.1.0" | ||||||
| edition = "2021" | ||||||
|
|
||||||
| [dependencies] | ||||||
| tracing = "0.1.40" | ||||||
|
|
||||||
| tokio = { version = "1.37.0", features = ["full"] } | ||||||
| tokio-tar = { version = "*" } | ||||||
| tokio-util = { version = "0.7.10", features = ["compat"] } | ||||||
|
|
||||||
| itertools = "0.14.0" | ||||||
|
|
||||||
|
|
||||||
| async-compression = { version = "0.4.9", features = ["tokio", "gzip"] } | ||||||
| async-recursion = "1.1.1" | ||||||
| cached = { version = "0.56.0", features = ["async"] } | ||||||
|
|
||||||
| clap = { version = "4.5.4", features = ["derive"] } | ||||||
| color-eyre = "0.6.3" | ||||||
| compact_str = { version = "0.9.0", features = ["serde"] } | ||||||
| dashmap = { version = "6.0.0", features = ["serde"] } | ||||||
| async-channel = "2.5.0" | ||||||
| futures = "0.3.31" | ||||||
| indexmap = { version = "2.2.6", features = ["serde"] } | ||||||
| indicatif = "0.18.0" | ||||||
| multimap = "0.10.0" | ||||||
| node-semver = { git = "https://github.com/danielhuang/node-semver-rs", rev = "bf4b103dc88b310c9dc049433aff1a14716e1e68" } | ||||||
| notify = "=8.2.0" | ||||||
|
||||||
| notify = "=8.2.0" | |
| notify = "^8.2.0" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| use std::{fmt::Debug, hash::Hash, sync::Arc}; | ||
|
|
||
| use dashmap::DashMap; | ||
| use futures::{ | ||
| future::{BoxFuture, Shared}, | ||
| Future, FutureExt, | ||
| }; | ||
|
|
||
| use crate::progress::PROGRESS_BAR; | ||
|
|
||
| type SharedBoxFuture<T> = Shared<BoxFuture<'static, T>>; | ||
|
|
||
| pub struct Cache<K: Eq + Hash + Clone + Send + Debug + 'static, V: Clone + Send + 'static> { | ||
| loader: Box<dyn Fn(K) -> BoxFuture<'static, V> + Send + Sync + 'static>, | ||
| map: DashMap<K, SharedBoxFuture<V>>, | ||
| } | ||
|
Comment on lines
+13
to
+16
|
||
|
|
||
| impl<K: Eq + Hash + Clone + Send + Debug + 'static, V: Clone + Send + 'static> Cache<K, V> { | ||
| pub fn new<T, F>(loader: T) -> Self | ||
| where | ||
| F: Future<Output = V> + Sized + Send + 'static, | ||
| T: Fn(K) -> F + Send + Sync + Clone + 'static, | ||
| { | ||
| let loader = Arc::new(loader); | ||
|
|
||
| Self { | ||
| loader: Box::new({ | ||
| move |key| { | ||
| let loader = loader.clone(); | ||
| Box::pin({ | ||
| async move { | ||
| PROGRESS_BAR.inc_length(1); | ||
| let v = loader(key).await; | ||
| PROGRESS_BAR.inc(1); | ||
| v | ||
| } | ||
| }) | ||
| } | ||
| }), | ||
| map: DashMap::new(), | ||
| } | ||
| } | ||
|
|
||
| pub async fn get(&self, key: K) -> V { | ||
| let f = self | ||
| .map | ||
| .entry(key.clone()) | ||
| .or_insert_with(|| (self.loader)(key).boxed().shared()) | ||
| .clone(); | ||
|
|
||
| f.await | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| //! Command-line interface definitions for Cotton. | ||
|
|
||
| use clap::Parser; | ||
| use compact_str::CompactString; | ||
| use node_semver::Version; | ||
| use std::ffi::OsString; | ||
| use std::path::PathBuf; | ||
|
|
||
| #[derive(Parser, Debug)] | ||
| #[clap(author, version, about, long_about = None)] | ||
| pub struct Args { | ||
| /// Print verbose logs (including progress indicators) | ||
| #[clap(short, long, global = true)] | ||
| pub verbose: bool, | ||
| /// Prevent any modifications to the lockfile | ||
| #[clap(long, global = true)] | ||
| pub immutable: bool, | ||
| /// Run in a custom working directory | ||
| #[clap(long, global = true, alias = "cwd")] | ||
| pub working_dir: Option<PathBuf>, | ||
|
|
||
| /// Subcommand to execute | ||
| #[clap(subcommand)] | ||
| pub cmd: Subcommand, | ||
| } | ||
|
|
||
| #[derive(Parser, Debug, Clone)] | ||
| pub enum Subcommand { | ||
| /// Install packages defined in package.json | ||
| #[clap(alias = "i")] | ||
| Install, | ||
| /// Prepare and save a newly planned lockfile | ||
| Update, | ||
| /// Add package to package.json | ||
| #[clap(alias = "a")] | ||
| Add { | ||
| names: Vec<CompactString>, | ||
| /// Add to `devDependencies` instead of `dependencies` | ||
| #[clap(short = 'D', long)] | ||
| dev: bool, | ||
| /// Pin dependencies to a specific version | ||
| #[clap(long, alias = "exact")] | ||
| pin: bool, | ||
| }, | ||
| /// Run a script defined in package.json | ||
| Run { | ||
| name: CompactString, | ||
| #[clap(long)] | ||
| watch: Vec<PathBuf>, | ||
| }, | ||
| /// Clean packages installed in `node_modules` and remove cache | ||
| Clean, | ||
| /// Update packages specified in package.json to the latest available version | ||
| Upgrade { | ||
| /// Pin dependencies to a specific version | ||
| #[clap(long)] | ||
| pin: bool, | ||
| }, | ||
| /// Execute a command that is not specified as a script | ||
| Exec { exe: OsString, args: Vec<OsString> }, | ||
| /// Remove package from package.json | ||
| Remove { | ||
| names: Vec<CompactString>, | ||
| /// Remove from `devDependencies` instead of `dependencies` | ||
| #[clap(short = 'D', long)] | ||
| dev: bool, | ||
| }, | ||
| /// Find all uses of a given package | ||
| Why { | ||
| name: CompactString, | ||
| version: Option<Version>, | ||
| }, | ||
| /// Create new projects from a `create-` starter kit | ||
| Create { name: CompactString }, | ||
| /// Download (if needed) and execute a command | ||
| #[clap(name = "x")] | ||
| DownloadAndExec { name: OsString, args: Vec<OsString> }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| //! Add command implementation. | ||
|
|
||
| use color_eyre::eyre::{ContextCompat, Result}; | ||
| use color_eyre::owo_colors::OwoColorize; | ||
| use compact_str::CompactString; | ||
| use futures::future::try_join_all; | ||
| use serde_json::Value; | ||
|
|
||
| use crate::npm::fetch_package; | ||
| use crate::progress::{log_progress, PROGRESS_BAR}; | ||
| use crate::util::{read_package_or_default, save_package}; | ||
|
|
||
| /// Execute the add command. | ||
| pub async fn cmd_add(names: &[CompactString], dev: bool, pin: bool) -> Result<()> { | ||
| if names.is_empty() { | ||
| PROGRESS_BAR.suspend(|| println!("Note: no packages specified")); | ||
| } | ||
|
|
||
| add_packages(names, dev, pin).await | ||
| } | ||
|
|
||
| /// Add packages to package.json. | ||
| pub async fn add_packages(names: &[CompactString], dev: bool, pin: bool) -> Result<()> { | ||
| let mut package: Value = read_package_or_default().await?; | ||
| let dependencies = package | ||
| .as_object_mut() | ||
| .wrap_err("`package.json` is invalid")? | ||
| .entry(if dev { | ||
| "devDependencies" | ||
| } else { | ||
| "dependencies" | ||
| }) | ||
| .or_insert(Value::Object(Default::default())) | ||
| .as_object_mut() | ||
| .wrap_err("`package.json` contains non-object dependencies field")?; | ||
|
|
||
| PROGRESS_BAR.set_message("Resolving packages".to_string()); | ||
| PROGRESS_BAR.set_length(names.len() as u64); | ||
|
|
||
| for (name, res) in try_join_all(names.iter().map(|name| async move { | ||
| let x = fetch_package(name).await.map(|res| (name, res)); | ||
| PROGRESS_BAR.inc(1); | ||
| PROGRESS_BAR.set_message(format!("Resolved {name}")); | ||
| x | ||
| })) | ||
| .await? | ||
| { | ||
| let latest = res | ||
| .dist_tags | ||
| .get("latest") | ||
| .wrap_err("Package `latest` tag not specified")?; | ||
|
|
||
| let version = if pin { | ||
| latest.to_string() | ||
| } else { | ||
| format!("^{latest}") | ||
| }; | ||
|
|
||
| dependencies.insert(name.to_string(), Value::String(version.to_string())); | ||
|
|
||
| PROGRESS_BAR.suspend(|| println!("Added {} {}", name.yellow(), version.yellow())); | ||
| } | ||
|
|
||
| PROGRESS_BAR.finish_and_clear(); | ||
| save_package(&package).await?; | ||
|
|
||
| Ok(()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| //! Clean command implementation. | ||
|
|
||
| use color_eyre::eyre::Result; | ||
| use std::fs::remove_dir_all; | ||
| use std::io::ErrorKind; | ||
|
|
||
| /// Execute the clean command. | ||
| pub fn cmd_clean() -> Result<()> { | ||
| for dir in ["node_modules", ".xmas"] { | ||
| match remove_dir_all(dir) { | ||
| Ok(()) => {} | ||
| Err(e) if e.kind() == ErrorKind::NotFound => {} | ||
| r => r?, | ||
| } | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version wildcard "*" for tokio-tar is problematic. This allows any version to be used, which can lead to unexpected breaking changes and makes builds non-reproducible. Specify an exact version or at least a semver range.