1use std::path::PathBuf;
9
10use tracing::{info, warn};
11
12use crate::compile::{BuildDriver, CargoDriver, CompileError, Compiler};
13use crate::error::{Error, Result};
14use crate::npm::Assembler;
15use crate::project::Project;
16use crate::target::TargetResolver;
17
18pub const DEFAULT_OUT: &str = "dist/npm";
20pub const DEFAULT_DRIVER: &str = "cargo";
22
23const TAG_PREFIX: &str = "v";
25
26#[derive(Debug)]
31pub struct Generator<'a> {
32 projects: &'a [Project],
33 out: PathBuf,
34 tag: Option<String>,
35 no_build: bool,
36 driver: String,
37 targets: Vec<String>,
38 build_driver: Option<&'a dyn BuildDriver>,
39}
40
41impl<'a> Generator<'a> {
42 pub fn new(project: &'a Project) -> Self {
44 Self::for_projects(std::slice::from_ref(project))
45 }
46
47 pub fn for_projects(projects: &'a [Project]) -> Self {
50 Self {
51 projects,
52 out: PathBuf::from(DEFAULT_OUT),
53 tag: None,
54 no_build: false,
55 driver: DEFAULT_DRIVER.to_owned(),
56 targets: Vec::new(),
57 build_driver: None,
58 }
59 }
60
61 pub fn build_driver(mut self, driver: &'a dyn BuildDriver) -> Self {
64 self.build_driver = Some(driver);
65 self
66 }
67
68 pub fn out(mut self, out: impl Into<PathBuf>) -> Self {
70 self.out = out.into();
71 self
72 }
73
74 pub fn tag(mut self, tag: impl Into<String>) -> Self {
76 self.tag = Some(tag.into());
77 self
78 }
79
80 pub fn no_build(mut self, no_build: bool) -> Self {
82 self.no_build = no_build;
83 self
84 }
85
86 pub fn driver(mut self, driver: impl Into<String>) -> Self {
88 self.driver = driver.into();
89 self
90 }
91
92 pub fn targets(mut self, targets: impl IntoIterator<Item = impl Into<String>>) -> Self {
94 self.targets = targets.into_iter().map(Into::into).collect();
95 self
96 }
97
98 pub fn run(&self) -> Result<()> {
101 let assembler = Assembler::new(&self.out)?;
102 if !self.no_build && self.build_driver.is_none() {
103 validate_driver(&self.driver)?;
104 }
105 let mut total_targets = 0;
106 let mut missing = Vec::new();
107
108 for project in self.projects {
109 if let Some(tag) = &self.tag {
110 let expected = format!("{TAG_PREFIX}{}", project.version);
111 if tag != &expected {
112 return Err(Error::TagMismatch {
113 tag: tag.clone(),
114 expected,
115 });
116 }
117 }
118
119 let targets = TargetResolver::new(&project.config, &project.workspace_root)
120 .resolve(&self.targets)?;
121
122 if !self.no_build {
123 let cargo = CargoDriver::new(&self.driver);
124 let driver: &dyn BuildDriver = match self.build_driver {
125 Some(injected) => injected,
126 None => &cargo,
127 };
128 Compiler::new(driver).compile_all(project, &targets)?;
129 }
130
131 total_targets += targets.len();
132 missing.extend(assembler.add(project, &targets)?);
133 }
134
135 assembler.commit()?;
136
137 if !missing.is_empty() {
138 warn!(
139 placed = total_targets - missing.len(),
140 total = total_targets,
141 missing = ?missing,
142 "platform packages have no binary yet; place them before publishing",
143 );
144 }
145 info!(
146 packages = self.projects.len(),
147 out = %self.out.display(),
148 "generated npm publish tree",
149 );
150 Ok(())
151 }
152}
153
154fn validate_driver(driver: &str) -> Result<()> {
158 if driver.is_empty() || driver.contains('/') || driver.contains('\\') {
159 return Err(CompileError::InvalidDriver {
160 driver: driver.to_owned(),
161 }
162 .into());
163 }
164 Ok(())
165}
166
167#[cfg(test)]
168mod tests {
169 use super::validate_driver;
170 use crate::error::Error;
171
172 #[test]
173 fn bare_command_drivers_are_accepted() {
174 assert!(validate_driver("cargo").is_ok());
175 assert!(validate_driver("cargo-zigbuild").is_ok());
176 assert!(validate_driver("cross").is_ok());
177 }
178
179 #[test]
180 fn path_like_or_empty_drivers_are_rejected() {
181 for bad in ["", "/tmp/evil", "../evil", "a/b", "a\\b"] {
182 assert!(matches!(
183 validate_driver(bad).unwrap_err(),
184 Error::Compile(_)
185 ));
186 }
187 }
188}