use std::{
env, fs,
io::{self},
path::{Path, PathBuf},
process::{Command, Stdio},
};
use super::resource_dir::ResourceDir;
#[cfg(not(windows))]
const NPM_CMD: &str = "npm";
#[cfg(windows)]
const NPM_CMD: &str = "npm.cmd";
pub fn npm_resource_dir<P: AsRef<Path>>(resource_dir: P) -> io::Result<ResourceDir> {
#[allow(unused_mut)]
let mut npm_build = NpmBuild::new(resource_dir)
.node_modules_strategy(NodeModulesStrategy::MoveToOutDir)
.install()?;
#[cfg(feature = "change-detection")]
{
npm_build = npm_build.change_detection();
}
Ok(npm_build.into())
}
#[derive(Default, Debug)]
pub struct NpmBuild {
package_json_dir: PathBuf,
executable: String,
target_dir: Option<PathBuf>,
node_modules_strategy: NodeModulesStrategy,
stderr: Option<Stdio>,
stdout: Option<Stdio>,
}
impl NpmBuild {
pub fn new<P: AsRef<Path>>(package_json_dir: P) -> Self {
Self {
package_json_dir: package_json_dir.as_ref().into(),
executable: String::from(NPM_CMD),
..Default::default()
}
}
#[must_use]
pub fn executable(self, executable: &str) -> Self {
let executable = String::from(executable);
Self { executable, ..self }
}
#[cfg(feature = "change-detection")]
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn change_detection(self) -> Self {
use ::change_detection::{
path_matchers::{any, equal, func, starts_with, PathMatcherExt},
ChangeDetection,
};
let package_json_dir = self.package_json_dir.clone();
let default_exclude_filter = any!(
equal(package_json_dir.clone()),
starts_with(self.package_json_dir.join("node_modules")),
equal(self.package_json_dir.join("package.json")),
equal(self.package_json_dir.join("package-lock.json")),
func(move |p| { p.is_file() && p.parent() != Some(package_json_dir.as_path()) })
);
{
let change_detection = if self.target_dir.is_none() {
ChangeDetection::exclude(default_exclude_filter)
} else {
let mut target_dir = self.target_dir.clone().unwrap();
if let Some(target_dir_parent) = target_dir.parent() {
if target_dir_parent.starts_with(&self.package_json_dir) {
while target_dir.parent() != Some(&self.package_json_dir) {
target_dir = target_dir.parent().unwrap().into();
}
}
}
let exclude_filter = default_exclude_filter.or(starts_with(target_dir));
ChangeDetection::exclude(exclude_filter)
};
change_detection.path(&self.package_json_dir).generate();
}
self
}
pub fn install(mut self) -> io::Result<Self> {
self.package_command()
.arg("install")
.status()
.map_err(|err| {
eprintln!("Cannot execute {} install: {err:?}", self.executable);
err
})
.map(|_| self)
}
pub fn run(mut self, cmd: &str) -> io::Result<Self> {
self.package_command()
.arg("run")
.arg(cmd)
.status()
.map_err(|err| {
eprintln!("Cannot execute {} run {cmd}: {err:?}", self.executable);
err
})
.map(|_| self)
}
#[must_use]
pub fn target<P: AsRef<Path>>(mut self, target_dir: P) -> Self {
let target_dir = target_dir.as_ref();
self.target_dir = Some(if target_dir.is_absolute() {
target_dir.into()
} else if let Ok(out_dir) = env::var("OUT_DIR").map(PathBuf::from) {
out_dir.join(target_dir)
} else {
target_dir.into()
});
self
}
#[must_use]
pub fn stderr<S: Into<Stdio>>(mut self, stdio: S) -> Self {
self.stderr = Some(stdio.into());
self
}
#[must_use]
pub fn stdout<S: Into<Stdio>>(mut self, stdio: S) -> Self {
self.stdout = Some(stdio.into());
self
}
#[must_use]
pub fn node_modules_strategy(mut self, node_modules_strategy: NodeModulesStrategy) -> Self {
self.node_modules_strategy = node_modules_strategy;
self
}
#[allow(clippy::wrong_self_convention)]
#[must_use]
pub fn to_resource_dir(self) -> ResourceDir {
self.into()
}
#[cfg(not(windows))]
fn command(&self) -> Command {
Command::new(&self.executable)
}
#[cfg(windows)]
fn command(&self) -> Command {
let mut cmd = Command::new("cmd");
cmd.arg("/c").arg(&self.executable);
cmd
}
fn package_command(&mut self) -> Command {
let mut cmd = self.command();
cmd.stderr(self.stderr.take().unwrap_or_else(Stdio::inherit))
.stdout(self.stdout.take().unwrap_or_else(Stdio::inherit))
.current_dir(&self.package_json_dir);
cmd
}
fn to_node_modules_dir(&self) -> PathBuf {
self.package_json_dir.join("node_modules")
}
fn remove_node_modules(&self) -> io::Result<()> {
let node_modules_dir = self.to_node_modules_dir();
if node_modules_dir.is_dir() {
fs::remove_dir_all(node_modules_dir)?;
}
Ok(())
}
fn move_node_modules_to_out_dir(&self) -> io::Result<PathBuf> {
let node_modules_dir = self.to_node_modules_dir();
if !node_modules_dir.is_dir() {
return Ok(node_modules_dir);
}
let Ok(out_node_modules_dir) =
env::var("OUT_DIR").map(|out_dir| PathBuf::from(out_dir).join("node_modules"))
else {
return Ok(node_modules_dir);
};
if out_node_modules_dir.is_dir() {
fs::remove_dir_all(&out_node_modules_dir)?;
}
copy_dir_all(&node_modules_dir, &out_node_modules_dir)?;
fs::remove_dir_all(node_modules_dir)?;
Ok(out_node_modules_dir)
}
}
impl From<NpmBuild> for ResourceDir {
fn from(mut npm_build: NpmBuild) -> Self {
let out_node_modules_dir = npm_build.node_modules_strategy.execute(&npm_build);
let resource_dir = npm_build
.target_dir
.take()
.or(out_node_modules_dir)
.unwrap_or_else(|| npm_build.to_node_modules_dir());
Self {
resource_dir,
..Default::default()
}
}
}
#[derive(Default, Debug)]
pub enum NodeModulesStrategy {
#[default]
Clean,
MoveToOutDir,
}
impl NodeModulesStrategy {
fn execute(&self, npm_build: &NpmBuild) -> Option<PathBuf> {
match self {
Self::Clean => {
npm_build
.remove_node_modules()
.expect("remove node_modules dir");
None
}
Self::MoveToOutDir => Some(
npm_build
.move_node_modules_to_out_dir()
.expect("move node_modules to out dir"),
),
}
}
}
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
fs::create_dir_all(&dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}