static_files/mods/
npm_build.rs

1/*!
2`npm` support.
3*/
4use std::{
5    io::{self},
6    path::{Path, PathBuf},
7    process::{Command, Stdio},
8};
9
10use super::resource_dir::ResourceDir;
11
12#[cfg(not(windows))]
13const NPM_CMD: &str = "npm";
14
15#[cfg(windows)]
16const NPM_CMD: &str = "npm.cmd";
17
18/// Generate resources with run of `npm install` prior to collecting
19/// resources in `resource_dir`.
20///
21/// Resources collected in `node_modules` subdirectory.
22pub fn npm_resource_dir<P: AsRef<Path>>(resource_dir: P) -> io::Result<ResourceDir> {
23    let mut npm_build = NpmBuild::new(resource_dir).install()?;
24
25    #[cfg(feature = "change-detection")]
26    {
27        npm_build = npm_build.change_detection();
28    }
29
30    Ok(npm_build.into())
31}
32
33/// Executes `npm` commands before collecting resources.
34///
35/// Example usage:
36/// Add `build.rs` with call to bundle resources:
37///
38/// ```rust#ignore
39/// use static_files::NpmBuild;
40///
41/// fn main() {
42///     NpmBuild::new("./web")
43///         .install().unwrap() // runs npm install
44///         .run("build").unwrap() // runs npm run build
45///         .target("./web/dist")
46///         .to_resource_dir()
47///         .build().unwrap();
48/// }
49/// ```
50/// Include generated code in `main.rs`:
51///
52/// ```rust#ignore
53/// include!(concat!(env!("OUT_DIR"), "/generated.rs"));
54/// ```
55#[derive(Default, Debug)]
56pub struct NpmBuild {
57    package_json_dir: PathBuf,
58    executable: String,
59    target_dir: Option<PathBuf>,
60    stderr: Option<Stdio>,
61    stdout: Option<Stdio>,
62}
63
64impl NpmBuild {
65    pub fn new<P: AsRef<Path>>(package_json_dir: P) -> Self {
66        Self {
67            package_json_dir: package_json_dir.as_ref().into(),
68            executable: String::from(NPM_CMD),
69            ..Default::default()
70        }
71    }
72
73    /// Allow the user to set their own npm-like executable (like yarn, for instance)
74    pub fn executable(self, executable: &str) -> Self {
75        let executable = String::from(executable);
76        Self { executable, ..self }
77    }
78
79    /// Generates change detection instructions.
80    ///
81    /// It includes `package.json` directory, ignores by default `node_modules`, `package.json` and `package-lock.json` and target directory.
82    /// Each time `npm` changes timestamps on these files, so if we do not ignore them - it runs `npm` each time.
83    /// It is recommended to put your dist files one level deeper. For example, if you have `web` with `package.json`
84    /// and `dist` just below that, you better generate you index.html somewhere in `web\dist\sub_path\index.html`.
85    /// Reason is the same, `npm` touches `dist` each time and it touches the parent directory which in its turn triggers the build each time.
86    /// For complete example see: [Angular Router Sample](https://github.com/kilork/actix-web-static-files-example-angular-router).
87    /// If default behavior does not work for you, you can use [change-detection](https://crates.io/crates/change-detection) directly.
88    #[cfg(feature = "change-detection")]
89    pub fn change_detection(self) -> Self {
90        use ::change_detection::{
91            path_matchers::{any, equal, func, starts_with, PathMatcherExt},
92            ChangeDetection,
93        };
94
95        let package_json_dir = self.package_json_dir.clone();
96        let default_exclude_filter = any!(
97            equal(package_json_dir.clone()),
98            starts_with(self.package_json_dir.join("node_modules")),
99            equal(self.package_json_dir.join("package.json")),
100            equal(self.package_json_dir.join("package-lock.json")),
101            func(move |p| { p.is_file() && p.parent() != Some(package_json_dir.as_path()) })
102        );
103
104        {
105            let change_detection = if self.target_dir.is_none() {
106                ChangeDetection::exclude(default_exclude_filter)
107            } else {
108                let mut target_dir = self.target_dir.clone().unwrap();
109
110                if let Some(target_dir_parent) = target_dir.parent() {
111                    if target_dir_parent.starts_with(&self.package_json_dir) {
112                        while target_dir.parent() != Some(&self.package_json_dir) {
113                            target_dir = target_dir.parent().unwrap().into();
114                        }
115                    }
116                }
117
118                let exclude_filter = default_exclude_filter.or(starts_with(target_dir));
119                ChangeDetection::exclude(exclude_filter)
120            };
121
122            change_detection.path(&self.package_json_dir).generate();
123        }
124        self
125    }
126
127    /// Executes `npm install`.
128    pub fn install(mut self) -> io::Result<Self> {
129        self.package_command()
130            .arg("install")
131            .status()
132            .map_err(|err| {
133                eprintln!("Cannot execute {} install: {:?}", &self.executable, err);
134                err
135            })
136            .map(|_| self)
137    }
138
139    /// Executes `npm run CMD`.
140    pub fn run(mut self, cmd: &str) -> io::Result<Self> {
141        self.package_command()
142            .arg("run")
143            .arg(cmd)
144            .status()
145            .map_err(|err| {
146                eprintln!("Cannot execute {} run {}: {:?}", &self.executable, cmd, err);
147                err
148            })
149            .map(|_| self)
150    }
151
152    /// Sets target (default is node_modules).
153    pub fn target<P: AsRef<Path>>(mut self, target_dir: P) -> Self {
154        self.target_dir = Some(target_dir.as_ref().into());
155        self
156    }
157
158    /// Sets stderr for the next command.
159    ///
160    /// You should set it again, if you need also redirect output for the next command.
161    pub fn stderr<S: Into<Stdio>>(mut self, stdio: S) -> Self {
162        self.stderr = Some(stdio.into());
163        self
164    }
165
166    /// Sets stdout for the next command.
167    ///
168    /// You should set it again, if you need also redirect output for the next command.
169    pub fn stdout<S: Into<Stdio>>(mut self, stdio: S) -> Self {
170        self.stdout = Some(stdio.into());
171        self
172    }
173
174    /// Converts to `ResourceDir`.
175    pub fn to_resource_dir(self) -> ResourceDir {
176        self.into()
177    }
178
179    #[cfg(not(windows))]
180    fn command(&self) -> Command {
181        Command::new(&self.executable)
182    }
183
184    #[cfg(windows)]
185    fn command(&self) -> Command {
186        let mut cmd = Command::new("cmd");
187
188        cmd.arg("/c").arg(&self.executable);
189
190        cmd
191    }
192
193    fn package_command(&mut self) -> Command {
194        let mut cmd = self.command();
195
196        cmd.stderr(self.stderr.take().unwrap_or_else(|| Stdio::inherit()))
197            .stdout(self.stdout.take().unwrap_or_else(|| Stdio::inherit()))
198            .current_dir(&self.package_json_dir);
199
200        cmd
201    }
202}
203
204impl From<NpmBuild> for ResourceDir {
205    fn from(mut npm_build: NpmBuild) -> Self {
206        Self {
207            resource_dir: npm_build
208                .target_dir
209                .take()
210                .unwrap_or_else(|| npm_build.package_json_dir.join("node_modules")),
211            ..Default::default()
212        }
213    }
214}