use std::collections::BTreeSet;
use std::path::Path;
use std::sync::Mutex;
use serde_json::Value;
use super::progress::{Progress, TaskStatus};
use super::source::{BareToken, Source};
use super::Res;
use crate::install::InstallEvent;
use crate::package_json::{manifest, spec};
use crate::registry::{PackumentDetail, Registry, ResolveEvent, Resolved};
pub(super) use crate::project::{read_manifest, write_manifest};
pub(super) fn add_specs(tokens: &[String], dir: &Path, verb: &str) -> Res<Value> {
let mut doc = if dir.join("package.json").exists() {
read_manifest(dir)?
} else {
manifest::scaffold(&default_name(dir), "1.0.0")
};
let registry = Registry::npm();
for token in tokens {
let (name, range) = Source::parse(token, BareToken::Spec)?.into_spec(verb)?;
let range = match range {
Some(r) => r,
None => format!("^{}", registry.resolve(&name, &spec::Range::any())?.version),
};
manifest::upsert_dependency(&mut doc, &name, &range);
println!("+ {name}@{range}");
}
write_manifest(dir, &doc)?;
Ok(doc)
}
pub(super) fn sync(dir: &Path, doc: &Value, detail: PackumentDetail, progress: &Progress) -> Res {
let tasks = SyncTasks::new(progress, "registry.npmjs.org");
let installed = crate::project::sync_observed(
dir,
doc,
detail,
|event| tasks.on_resolve(event),
|event| tasks.on_install(event),
)?;
tasks.done(installed.len());
report_installed(&installed);
Ok(())
}
pub(super) fn report_installed(installed: &[Resolved]) {
println!("installed {} package(s)", installed.len());
for r in installed {
println!(" {}@{}", r.name, r.version);
}
}
pub(super) struct ResolveTicker<'t> {
task: &'t TaskStatus,
in_flight: Mutex<BTreeSet<String>>,
}
impl<'t> ResolveTicker<'t> {
pub(super) fn new(task: &'t TaskStatus) -> ResolveTicker<'t> {
ResolveTicker {
task,
in_flight: Mutex::new(BTreeSet::new()),
}
}
pub(super) fn observe(&self, event: ResolveEvent<'_>) {
tick_resolve(self.task, &self.in_flight, event);
}
}
fn tick_resolve(task: &TaskStatus, in_flight: &Mutex<BTreeSet<String>>, event: ResolveEvent<'_>) {
match event {
ResolveEvent::FetchBegin { name } => {
let mut set = in_flight.lock().expect("in-flight set poisoned");
set.insert(name.to_string());
task.detail(&render_in_flight(&set));
}
ResolveEvent::FetchDone { name } => {
let mut set = in_flight.lock().expect("in-flight set poisoned");
set.remove(name);
task.detail(&render_in_flight(&set));
}
ResolveEvent::Resolved { package, .. } => {
task.inc(&format!("{}@{}", package.name, package.version));
}
}
}
pub(super) struct SyncTasks<'p> {
progress: &'p Progress,
resolve: Mutex<Option<TaskStatus>>,
in_flight: Mutex<BTreeSet<String>>,
install: Mutex<Option<TaskStatus>>,
}
impl<'p> SyncTasks<'p> {
pub(super) fn new(progress: &'p Progress, host: &str) -> SyncTasks<'p> {
let resolve = progress.task("resolve", format!("resolving dependency tree from {host}"));
SyncTasks {
progress,
resolve: Mutex::new(Some(resolve)),
in_flight: Mutex::new(BTreeSet::new()),
install: Mutex::new(None),
}
}
pub(super) fn on_resolve(&self, event: ResolveEvent<'_>) {
if let Some(task) = self.resolve.lock().expect("resolve task poisoned").as_ref() {
tick_resolve(task, &self.in_flight, event);
}
}
pub(super) fn on_install(&self, event: InstallEvent<'_>) {
let InstallEvent::Fetch {
total,
name,
version,
..
} = event;
let mut install = self.install.lock().expect("install task poisoned");
if install.is_none() {
self.finish_resolve();
let task = self.progress.task("install", "installing packages");
task.set_total(total as u64);
*install = Some(task);
}
if let Some(task) = install.as_ref() {
task.inc(&format!("{name}@{version}"));
}
}
pub(super) fn done(self, installed: usize) {
self.finish_resolve();
if let Some(task) = self.install.lock().expect("install task poisoned").take() {
task.finish(&format!("{installed} packages"));
}
}
fn finish_resolve(&self) {
if let Some(task) = self.resolve.lock().expect("resolve task poisoned").take() {
let count = task.count();
task.finish(&format!("{count} packages"));
}
}
}
fn render_in_flight(set: &BTreeSet<String>) -> String {
if set.is_empty() {
return String::new();
}
let shown: Vec<&str> = set.iter().take(3).map(String::as_str).collect();
let rest = set.len() - shown.len();
if rest == 0 {
format!("fetching {}", shown.join(", "))
} else {
format!("fetching {} (+{rest} more)", shown.join(", "))
}
}
pub(super) fn host_of(url: &str) -> &str {
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
.unwrap_or(url);
rest.split('/').next().unwrap_or(rest)
}
pub(super) fn split_name_range(pkg: &str) -> (&str, Option<&str>) {
match pkg.rfind('@') {
Some(i) if i > 0 => (&pkg[..i], Some(&pkg[i + 1..])),
_ => (pkg, None),
}
}
pub(super) fn default_name(dir: &Path) -> String {
std::fs::canonicalize(dir)
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.filter(|n| !n.is_empty())
.unwrap_or_else(|| "app".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_in_flight_caps_at_three_names() {
let set: BTreeSet<String> = BTreeSet::new();
assert_eq!(render_in_flight(&set), "");
let set: BTreeSet<String> = ["b", "a"].iter().map(|s| s.to_string()).collect();
assert_eq!(render_in_flight(&set), "fetching a, b");
let set: BTreeSet<String> = ["e", "d", "c", "b", "a"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(render_in_flight(&set), "fetching a, b, c (+2 more)");
}
#[test]
fn split_name_range_handles_scopes_and_bare_names() {
assert_eq!(split_name_range("lit"), ("lit", None));
assert_eq!(split_name_range("lit@^3"), ("lit", Some("^3")));
assert_eq!(
split_name_range("@lit/context@^1"),
("@lit/context", Some("^1"))
);
assert_eq!(split_name_range("@scope/pkg"), ("@scope/pkg", None));
}
}