use std::path::PathBuf;
use tracing::{info, warn};
use crate::compile::{BuildDriver, CargoDriver, CompileError, Compiler};
use crate::error::{Error, Result};
use crate::npm::Assembler;
use crate::project::Project;
use crate::target::TargetResolver;
pub const DEFAULT_OUT: &str = "dist/npm";
pub const DEFAULT_DRIVER: &str = "cargo";
const TAG_PREFIX: &str = "v";
#[derive(Debug)]
pub struct Generator<'a> {
projects: &'a [Project],
out: PathBuf,
tag: Option<String>,
no_build: bool,
driver: String,
targets: Vec<String>,
build_driver: Option<&'a dyn BuildDriver>,
}
impl<'a> Generator<'a> {
pub fn new(project: &'a Project) -> Self {
Self::for_projects(std::slice::from_ref(project))
}
pub fn for_projects(projects: &'a [Project]) -> Self {
Self {
projects,
out: PathBuf::from(DEFAULT_OUT),
tag: None,
no_build: false,
driver: DEFAULT_DRIVER.to_owned(),
targets: Vec::new(),
build_driver: None,
}
}
pub fn build_driver(mut self, driver: &'a dyn BuildDriver) -> Self {
self.build_driver = Some(driver);
self
}
pub fn out(mut self, out: impl Into<PathBuf>) -> Self {
self.out = out.into();
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
pub fn no_build(mut self, no_build: bool) -> Self {
self.no_build = no_build;
self
}
pub fn driver(mut self, driver: impl Into<String>) -> Self {
self.driver = driver.into();
self
}
pub fn targets(mut self, targets: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.targets = targets.into_iter().map(Into::into).collect();
self
}
pub fn run(&self) -> Result<()> {
let assembler = Assembler::new(&self.out)?;
if !self.no_build && self.build_driver.is_none() {
validate_driver(&self.driver)?;
}
let mut total_targets = 0;
let mut missing = Vec::new();
for project in self.projects {
if let Some(tag) = &self.tag {
let expected = format!("{TAG_PREFIX}{}", project.version);
if tag != &expected {
return Err(Error::TagMismatch {
tag: tag.clone(),
expected,
});
}
}
let targets = TargetResolver::new(&project.config, &project.workspace_root)
.resolve(&self.targets)?;
if !self.no_build {
let cargo = CargoDriver::new(&self.driver);
let driver: &dyn BuildDriver = match self.build_driver {
Some(injected) => injected,
None => &cargo,
};
Compiler::new(driver).compile_all(project, &targets)?;
}
total_targets += targets.len();
missing.extend(assembler.add(project, &targets)?);
}
assembler.commit()?;
if !missing.is_empty() {
warn!(
placed = total_targets - missing.len(),
total = total_targets,
missing = ?missing,
"platform packages have no binary yet; place them before publishing",
);
}
info!(
packages = self.projects.len(),
out = %self.out.display(),
"generated npm publish tree",
);
Ok(())
}
}
fn validate_driver(driver: &str) -> Result<()> {
if driver.is_empty() || driver.contains('/') || driver.contains('\\') {
return Err(CompileError::InvalidDriver {
driver: driver.to_owned(),
}
.into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::validate_driver;
use crate::error::Error;
#[test]
fn bare_command_drivers_are_accepted() {
assert!(validate_driver("cargo").is_ok());
assert!(validate_driver("cargo-zigbuild").is_ok());
assert!(validate_driver("cross").is_ok());
}
#[test]
fn path_like_or_empty_drivers_are_rejected() {
for bad in ["", "/tmp/evil", "../evil", "a/b", "a\\b"] {
assert!(matches!(
validate_driver(bad).unwrap_err(),
Error::Compile(_)
));
}
}
}