use anyhow::{anyhow, Context};
use log::warn;
use std::{
collections::HashMap,
env,
ffi::OsString,
fs::{File, OpenOptions},
io::Write,
process,
};
use structopt::StructOpt;
use super::Command;
use crate::{
app::{AppBuilder, AppSession},
atry,
errors::Result,
graph::{GraphQueryBuilder, ProjectGraphBuilder},
project::{DepRequirement, DependencyTarget, ProjectId},
repository::{ChangeList, RepoPath, RepoPathBuf, Repository},
rewriters::Rewriter,
version::Version,
};
const DEPENDENCY_KEYS: &[&str] = &["dependencies", "devDependencies", "optionalDependencies"];
#[derive(Debug, Default)]
pub struct NpmLoader {
npm_to_graph: HashMap<String, PackageLoadData>,
}
#[derive(Debug)]
struct PackageLoadData {
ident: ProjectId,
json_path: RepoPathBuf,
pkg_data: serde_json::Map<String, serde_json::Value>,
}
impl NpmLoader {
pub fn process_index_item(
&mut self,
repo: &Repository,
graph: &mut ProjectGraphBuilder,
repopath: &RepoPath,
dirname: &RepoPath,
basename: &RepoPath,
) -> Result<()> {
if basename.as_ref() != b"package.json" {
return Ok(());
}
let path = repo.resolve_workdir(repopath);
let f = atry!(
File::open(&path);
["failed to open repository file `{}`", path.display()]
);
let pkg_data: serde_json::Map<String, serde_json::Value> = atry!(
serde_json::from_reader(f);
["failed to parse file `{}` as JSON", path.display()]
);
const CONTENT_KEYS: &[&str] = &["bin", "browser", "files", "main", "types", "version"];
let has_content = CONTENT_KEYS.iter().any(|k| pkg_data.contains_key(*k));
if !has_content {
return Ok(());
}
let name = pkg_data
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| {
anyhow!(
"NPM file `{}` does not have a string-typed `name` field",
path.display()
)
})?
.to_owned();
let version = pkg_data
.get("version")
.and_then(|v| v.as_str())
.ok_or_else(|| {
anyhow!(
"NPM file `{}` does not have a string-typed `version` field",
path.display()
)
})?;
let version = atry!(
semver::Version::parse(version);
["cannot parse `version` field \"{}\" in `{}` as a semver version",
version, path.display()]
);
let ident = graph.add_project();
let mut proj = graph.lookup_mut(ident);
proj.qnames = vec![name.to_owned(), "npm".to_owned()];
proj.prefix = Some(dirname.to_owned());
proj.version = Some(Version::Semver(version));
let rewrite = PackageJsonRewriter::new(ident, repopath.to_owned());
proj.rewriters.push(Box::new(rewrite));
self.npm_to_graph.insert(
name,
PackageLoadData {
ident,
pkg_data,
json_path: repopath.to_owned(),
},
);
Ok(())
}
pub fn finalize(self, app: &mut AppBuilder) -> Result<()> {
for (name, load_data) in &self.npm_to_graph {
let maybe_internal_specs = load_data
.pkg_data
.get("internalDepVersions")
.and_then(|v| v.as_object());
for dep_key in DEPENDENCY_KEYS {
if let Some(dep_map) = load_data.pkg_data.get(*dep_key).and_then(|v| v.as_object())
{
for (dep_name, dep_spec) in dep_map {
if let Some(dep_data) = &self.npm_to_graph.get(dep_name) {
let req = if let Some(cranko_spec) = maybe_internal_specs
.and_then(|d| d.get(dep_name))
.and_then(|v| v.as_str())
{
match app.repo.parse_history_ref(cranko_spec).and_then(|cref| {
app.repo.resolve_history_ref(&cref, &load_data.json_path)
}) {
Ok(r) => r,
Err(e) => {
warn!("invalid `package.json` key `internalDepVersions.{}` for {}: {}",
dep_name, name, e);
DepRequirement::Unavailable
}
}
} else {
DepRequirement::Unavailable
};
app.graph.add_dependency(
load_data.ident,
DependencyTarget::Ident(dep_data.ident),
dep_spec.as_str().unwrap_or("UNDEFINED").to_owned(),
req,
);
}
}
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct PackageJsonRewriter {
proj_id: ProjectId,
json_path: RepoPathBuf,
}
impl PackageJsonRewriter {
pub fn new(proj_id: ProjectId, json_path: RepoPathBuf) -> Self {
PackageJsonRewriter { proj_id, json_path }
}
}
impl Rewriter for PackageJsonRewriter {
fn rewrite(&self, app: &AppSession, changes: &mut ChangeList) -> Result<()> {
let path = app.repo.resolve_workdir(&self.json_path);
let mut pkg_data: serde_json::Map<String, serde_json::Value> = {
let f = atry!(
File::open(&path);
["failed to open file `{}`", path.display()]
);
atry!(
serde_json::from_reader(f);
["failed to parse file `{}` as JSON", path.display()]
)
};
let proj = app.graph().lookup(self.proj_id);
let mut internal_reqs = HashMap::new();
for dep in &proj.internal_deps[..] {
let req_text = match dep.cranko_requirement {
DepRequirement::Manual(ref t) => t.clone(),
DepRequirement::Commit(_) => {
if let Some(ref v) = dep.resolved_version {
format!("^{}", v)
} else {
continue;
}
}
DepRequirement::Unavailable => continue,
};
internal_reqs.insert(
app.graph().lookup(dep.ident).qualified_names()[0].clone(),
req_text,
);
}
pkg_data["version"] = serde_json::Value::String(proj.version.to_string());
for dep_key in DEPENDENCY_KEYS {
if let Some(dep_map) = pkg_data.get_mut(*dep_key).and_then(|v| v.as_object_mut()) {
for (dep_name, dep_spec) in dep_map.iter_mut() {
if let Some(text) = internal_reqs.get(dep_name) {
*dep_spec = serde_json::Value::String(text.clone());
}
}
}
}
{
let mut f = File::create(&path)?;
atry!(
serde_json::to_writer_pretty(&mut f, &pkg_data);
["failed to overwrite JSON file `{}`", path.display()]
);
atry!(
writeln!(f, "");
["failed to overwrite JSON file `{}`", path.display()]
);
changes.add_path(&self.json_path);
}
Ok(())
}
fn rewrite_cranko_requirements(
&self,
app: &AppSession,
changes: &mut ChangeList,
) -> Result<()> {
if app.graph().lookup(self.proj_id).internal_deps.is_empty() {
return Ok(());
}
let path = app.repo.resolve_workdir(&self.json_path);
let mut pkg_data: serde_json::Map<String, serde_json::Value> = {
let f = atry!(
File::open(&path);
["failed to open file `{}`", path.display()]
);
atry!(
serde_json::from_reader(f);
["failed to parse file `{}` as JSON", path.display()]
)
};
let reqs = match pkg_data
.get_mut("internalDepVersions")
.and_then(|v| v.as_object_mut())
{
Some(t) => t,
None => {
pkg_data.insert(
"internalDepVersions".to_owned(),
serde_json::Value::Object(serde_json::Map::new()),
);
pkg_data["internalDepVersions"].as_object_mut().unwrap()
}
};
let graph = app.graph();
let proj = graph.lookup(self.proj_id);
for dep in &proj.internal_deps {
let target = &graph.lookup(dep.ident).qualified_names()[0];
let spec = match &dep.cranko_requirement {
DepRequirement::Commit(cid) => cid.to_string(),
DepRequirement::Manual(t) => format!("manual:{}", t),
DepRequirement::Unavailable => continue,
};
reqs.insert(target.to_owned(), serde_json::Value::String(spec));
}
{
let f = File::create(&path)?;
atry!(
serde_json::to_writer_pretty(f, &pkg_data);
["failed to overwrite JSON file `{}`", path.display()]
);
changes.add_path(&self.json_path);
}
Ok(())
}
}
#[derive(Debug, PartialEq, StructOpt)]
pub enum NpmCommands {
#[structopt(name = "foreach-released")]
ForeachReleased(ForeachReleasedCommand),
#[structopt(name = "install-token")]
InstallToken(InstallTokenCommand),
}
#[derive(Debug, PartialEq, StructOpt)]
pub struct NpmCommand {
#[structopt(subcommand)]
command: NpmCommands,
}
impl Command for NpmCommand {
fn execute(self) -> Result<i32> {
match self.command {
NpmCommands::ForeachReleased(o) => o.execute(),
NpmCommands::InstallToken(o) => o.execute(),
}
}
}
#[derive(Debug, PartialEq, StructOpt)]
pub struct ForeachReleasedCommand {
#[structopt(help = "The command to run", required = true)]
command: Vec<OsString>,
}
impl Command for ForeachReleasedCommand {
fn execute(self) -> Result<i32> {
let sess = AppSession::initialize_default()?;
let (dev_mode, rel_info) = sess.ensure_ci_release_mode()?;
if dev_mode {
warn!("proceeding even though in dev mode");
}
let mut q = GraphQueryBuilder::default();
q.only_new_releases(rel_info);
q.only_project_type("npm");
let idents = sess
.graph()
.query(q)
.context("could not select projects for `npm foreach-released`")?;
let mut cmd = process::Command::new(&self.command[0]);
if self.command.len() > 1 {
cmd.args(&self.command[1..]);
}
let print_which = idents.len() > 1;
let mut first = true;
for ident in &idents {
let proj = sess.graph().lookup(*ident);
let dir = sess.repo.resolve_workdir(&proj.prefix());
cmd.current_dir(&dir);
if print_which {
if first {
first = false;
} else {
println!();
}
println!("### in `{}`:", dir.display());
}
let status = cmd.status().context(format!(
"could not run the command for NPM project `{}`",
proj.user_facing_name
))?;
if !status.success() {
return Err(anyhow!(
"the command failed for NPM project `{}`",
proj.user_facing_name
));
}
}
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
pub struct InstallTokenCommand {
#[structopt(
long = "registry",
default_value = "//registry.npmjs.org/",
help = "The registry base URL."
)]
registry: String,
}
impl Command for InstallTokenCommand {
fn execute(self) -> Result<i32> {
let token = atry!(
env::var("NPM_TOKEN");
["missing or non-textual environment variable NPM_TOKEN"]
);
let mut p =
dirs::home_dir().ok_or_else(|| anyhow!("cannot determine user's home directory"))?;
p.push(".npmrc");
let mut file = atry!(
OpenOptions::new().write(true).create(true).append(true).open(&p);
["failed to open file `{}` for appending", p.display()]
);
atry!(
writeln!(file, "{}:_authToken={}", self.registry, token);
["failed to write token data to file `{}`", p.display()]
);
Ok(0)
}
}