use std::path::Path;
use serde_json::Value;
use super::Res;
use crate::install::from_lockfile;
use crate::package_json::{lock, manifest, spec};
use crate::registry::{Registry, Resolved};
pub(super) fn sync(dir: &Path, doc: &Value) -> Res {
let direct = manifest::dependencies(doc);
let roots: Vec<(String, spec::Range)> = direct
.iter()
.filter(|(_, range)| spec::Spec::parse(range).is_registry())
.map(|(name, range)| -> Res<(String, spec::Range)> {
Ok((name.clone(), spec::Range::parse(range)?))
})
.collect::<Res<Vec<_>>>()?;
let resolved = Registry::npm().resolve_tree(&roots)?;
let entries: Vec<lock::LockEntry> = resolved
.iter()
.map(|r| lock::LockEntry {
name: r.name.clone(),
version: r.version.to_string(),
resolved: r.tarball_url.clone(),
integrity: r.integrity.clone(),
})
.collect();
let name = doc.get("name").and_then(Value::as_str).unwrap_or("");
let version = doc
.get("version")
.and_then(Value::as_str)
.unwrap_or("1.0.0");
std::fs::write(
dir.join("package-lock.json"),
lock::render_v3(name, version, &direct, &entries),
)?;
report_installed(&from_lockfile(&dir.join("package-lock.json"), dir)?);
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) fn read_manifest(dir: &Path) -> Res<Value> {
let path = dir.join("package.json");
let text =
std::fs::read_to_string(&path).map_err(|e| format!("reading {}: {e}", path.display()))?;
let doc: Value =
serde_json::from_str(&text).map_err(|e| format!("parsing {}: {e}", path.display()))?;
if !doc.is_object() {
return Err(format!("{} is not a JSON object", path.display()).into());
}
Ok(doc)
}
pub(super) fn write_manifest(dir: &Path, doc: &Value) -> Res {
std::fs::write(dir.join("package.json"), manifest::to_pretty(doc))?;
Ok(())
}
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 bump_floor(range: &str, version: &semver::Version) -> Option<String> {
match range.chars().next() {
Some(prefix @ ('^' | '~')) => Some(format!("{prefix}{version}")),
_ => 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 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));
}
#[test]
fn bump_floor_only_moves_floating_ranges() {
let v = semver::Version::parse("3.4.2").unwrap();
assert_eq!(bump_floor("^3.1.0", &v).as_deref(), Some("^3.4.2"));
assert_eq!(bump_floor("~3.1.0", &v).as_deref(), Some("~3.4.2"));
assert_eq!(bump_floor("3.1.0", &v), None);
assert_eq!(bump_floor("*", &v), None);
assert_eq!(bump_floor(">=3 <4", &v), None);
}
}