static_files/mods/
npm_build.rs1use std::{
5 env, fs,
6 io::{self},
7 path::{Path, PathBuf},
8 process::{Command, Stdio},
9};
10
11use super::resource_dir::ResourceDir;
12
13#[cfg(not(windows))]
14const NPM_CMD: &str = "npm";
15
16#[cfg(windows)]
17const NPM_CMD: &str = "npm.cmd";
18
19pub fn npm_resource_dir<P: AsRef<Path>>(resource_dir: P) -> io::Result<ResourceDir> {
24 #[allow(unused_mut)]
25 let mut npm_build = NpmBuild::new(resource_dir)
26 .node_modules_strategy(NodeModulesStrategy::MoveToOutDir)
27 .install()?;
28
29 #[cfg(feature = "change-detection")]
30 {
31 npm_build = npm_build.change_detection();
32 }
33
34 Ok(npm_build.into())
35}
36
37#[derive(Default, Debug)]
60pub struct NpmBuild {
61 package_json_dir: PathBuf,
62 executable: String,
63 target_dir: Option<PathBuf>,
64 node_modules_strategy: NodeModulesStrategy,
65 stderr: Option<Stdio>,
66 stdout: Option<Stdio>,
67}
68
69impl NpmBuild {
70 pub fn new<P: AsRef<Path>>(package_json_dir: P) -> Self {
71 Self {
72 package_json_dir: package_json_dir.as_ref().into(),
73 executable: String::from(NPM_CMD),
74 ..Default::default()
75 }
76 }
77
78 #[must_use]
80 pub fn executable(self, executable: &str) -> Self {
81 let executable = String::from(executable);
82 Self { executable, ..self }
83 }
84
85 #[cfg(feature = "change-detection")]
95 #[must_use]
96 #[allow(clippy::missing_panics_doc)]
97 pub fn change_detection(self) -> Self {
98 use ::change_detection::{
99 path_matchers::{any, equal, func, starts_with, PathMatcherExt},
100 ChangeDetection,
101 };
102
103 let package_json_dir = self.package_json_dir.clone();
104 let default_exclude_filter = any!(
105 equal(package_json_dir.clone()),
106 starts_with(self.package_json_dir.join("node_modules")),
107 equal(self.package_json_dir.join("package.json")),
108 equal(self.package_json_dir.join("package-lock.json")),
109 func(move |p| { p.is_file() && p.parent() != Some(package_json_dir.as_path()) })
110 );
111
112 {
113 let change_detection = if self.target_dir.is_none() {
115 ChangeDetection::exclude(default_exclude_filter)
116 } else {
117 let mut target_dir = self.target_dir.clone().unwrap();
118
119 if let Some(target_dir_parent) = target_dir.parent() {
120 if target_dir_parent.starts_with(&self.package_json_dir) {
121 while target_dir.parent() != Some(&self.package_json_dir) {
122 target_dir = target_dir.parent().unwrap().into();
123 }
124 }
125 }
126
127 let exclude_filter = default_exclude_filter.or(starts_with(target_dir));
128 ChangeDetection::exclude(exclude_filter)
129 };
130
131 change_detection.path(&self.package_json_dir).generate();
132 }
133 self
134 }
135
136 pub fn install(mut self) -> io::Result<Self> {
138 self.package_command()
139 .arg("install")
140 .status()
141 .map_err(|err| {
142 eprintln!("Cannot execute {} install: {err:?}", self.executable);
143 err
144 })
145 .map(|_| self)
146 }
147
148 pub fn run(mut self, cmd: &str) -> io::Result<Self> {
150 self.package_command()
151 .arg("run")
152 .arg(cmd)
153 .status()
154 .map_err(|err| {
155 eprintln!("Cannot execute {} run {cmd}: {err:?}", self.executable);
156 err
157 })
158 .map(|_| self)
159 }
160
161 #[must_use]
167 pub fn target<P: AsRef<Path>>(mut self, target_dir: P) -> Self {
168 let target_dir = target_dir.as_ref();
169 self.target_dir = Some(if target_dir.is_absolute() {
170 target_dir.into()
171 } else if let Ok(out_dir) = env::var("OUT_DIR").map(PathBuf::from) {
172 out_dir.join(target_dir)
173 } else {
174 target_dir.into()
175 });
176 self
177 }
178
179 #[must_use]
183 pub fn stderr<S: Into<Stdio>>(mut self, stdio: S) -> Self {
184 self.stderr = Some(stdio.into());
185 self
186 }
187
188 #[must_use]
192 pub fn stdout<S: Into<Stdio>>(mut self, stdio: S) -> Self {
193 self.stdout = Some(stdio.into());
194 self
195 }
196
197 #[must_use]
201 pub fn node_modules_strategy(mut self, node_modules_strategy: NodeModulesStrategy) -> Self {
202 self.node_modules_strategy = node_modules_strategy;
203 self
204 }
205
206 #[allow(clippy::wrong_self_convention)]
208 #[must_use]
209 pub fn to_resource_dir(self) -> ResourceDir {
210 self.into()
211 }
212
213 #[cfg(not(windows))]
214 fn command(&self) -> Command {
215 Command::new(&self.executable)
216 }
217
218 #[cfg(windows)]
219 fn command(&self) -> Command {
220 let mut cmd = Command::new("cmd");
221
222 cmd.arg("/c").arg(&self.executable);
223
224 cmd
225 }
226
227 fn package_command(&mut self) -> Command {
228 let mut cmd = self.command();
229
230 cmd.stderr(self.stderr.take().unwrap_or_else(Stdio::inherit))
231 .stdout(self.stdout.take().unwrap_or_else(Stdio::inherit))
232 .current_dir(&self.package_json_dir);
233
234 cmd
235 }
236
237 fn to_node_modules_dir(&self) -> PathBuf {
238 self.package_json_dir.join("node_modules")
239 }
240
241 fn remove_node_modules(&self) -> io::Result<()> {
242 let node_modules_dir = self.to_node_modules_dir();
243
244 if node_modules_dir.is_dir() {
245 fs::remove_dir_all(node_modules_dir)?;
246 }
247
248 Ok(())
249 }
250
251 fn move_node_modules_to_out_dir(&self) -> io::Result<PathBuf> {
252 let node_modules_dir = self.to_node_modules_dir();
253
254 if !node_modules_dir.is_dir() {
255 return Ok(node_modules_dir);
256 }
257
258 let Ok(out_node_modules_dir) =
259 env::var("OUT_DIR").map(|out_dir| PathBuf::from(out_dir).join("node_modules"))
260 else {
261 return Ok(node_modules_dir);
262 };
263
264 if out_node_modules_dir.is_dir() {
265 fs::remove_dir_all(&out_node_modules_dir)?;
266 }
267
268 copy_dir_all(&node_modules_dir, &out_node_modules_dir)?;
269 fs::remove_dir_all(node_modules_dir)?;
270
271 Ok(out_node_modules_dir)
272 }
273}
274
275impl From<NpmBuild> for ResourceDir {
276 fn from(mut npm_build: NpmBuild) -> Self {
277 let out_node_modules_dir = npm_build.node_modules_strategy.execute(&npm_build);
278
279 let resource_dir = npm_build
280 .target_dir
281 .take()
282 .or(out_node_modules_dir)
283 .unwrap_or_else(|| npm_build.to_node_modules_dir());
284
285 Self {
286 resource_dir,
287 ..Default::default()
288 }
289 }
290}
291
292#[derive(Default, Debug)]
293pub enum NodeModulesStrategy {
294 #[default]
295 Clean,
296 MoveToOutDir,
297}
298
299impl NodeModulesStrategy {
300 fn execute(&self, npm_build: &NpmBuild) -> Option<PathBuf> {
301 match self {
302 Self::Clean => {
303 npm_build
304 .remove_node_modules()
305 .expect("remove node_modules dir");
306 None
307 }
308 Self::MoveToOutDir => Some(
309 npm_build
310 .move_node_modules_to_out_dir()
311 .expect("move node_modules to out dir"),
312 ),
313 }
314 }
315}
316
317fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
318 fs::create_dir_all(&dst)?;
319 for entry in fs::read_dir(src)? {
320 let entry = entry?;
321 let ty = entry.file_type()?;
322 if ty.is_dir() {
323 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
324 } else {
325 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
326 }
327 }
328 Ok(())
329}