build_my_react_js/
lib.rs

1//! This goes in your build script, for use with react-scripts,
2//! and cra template projects.
3//!
4//! It can be helpful for both development and deployment.
5//!
6//! So, as an example:
7//!
8//! with the directory structure like
9//!
10//! ```toml
11//! .gitignore
12//! src/
13//! my-frontend/
14//!   src/
15//!     index.js
16//!   package.json
17//! Cargo.toml
18//! ```
19//!
20//! ```
21//! // `build.rs` see The Cargo Book >> Build Scripts
22//! use build_my_react_js::*;
23//!
24//! fn main() {
25//!     build_react_under!("my-frontend");
26//! }
27//! ```
28//!
29//! Provided the system is configured with NPM, and the repositories
30//! are reachable, then it will attempt to compile your React project
31//! when changes are detected (and only when changes are detected)
32//!
33//! Provided your crate is structured with an additional, uniquely-named
34//! subdirectory containing your `package.json` this can be instructed
35//! to enter and build it. Feedback is provided through the Cargo IPC
36//! mechanism as build warnings. Panics are fatal to builds, when the
37//! commands report failure, but options are available, see the docs.
38//!
39//! I'm not sure how "clean" works in the npm ecosystem, but this crate
40//! assumes you start potentially without node_modules, and attempts
41//! npm install when the project is not built yet.
42//!
43//! This could become flaky, but:
44//! - attempts to preserve quality feedback,
45//! - rely on quality sources of information,
46//! - deliver quality feedback,
47//! - benefit from pipelined builds to speed development,
48//! - benefit from the full power of the cargo ecosystem
49//!
50//! Enjoy!
51//!
52
53use core::str;
54use inline_colorization::*;
55use std::{
56    path::{Component, PathBuf},
57    process::Command,
58};
59
60#[macro_export]
61macro_rules! build_react_under {
62    ($s:expr) => {
63        ($crate::build_my_react_js($s, env!("CARGO_MANIFEST_DIR")))
64    };
65}
66
67/// This is the default flavor, it will panic on detection of major error
68/// and generate warnings indicating progress.
69pub fn build_my_react_js(path: &str, outer_env: &str) {
70    match build_my_react_js_fallible(path, outer_env, false) {
71        Ok(_) => (),
72        Err(err_msg) => panic!("{err_msg}"),
73    }
74}
75
76/// This will panic on detection of major error and **not** generate warnings.
77pub fn build_my_react_js_silent(path: &str, outer_env: &str) {
78    match build_my_react_js_fallible(path, outer_env, true) {
79        Ok(_) => (),
80        Err(err_msg) => panic!("{err_msg}"),
81    }
82}
83
84/// This performs the following:
85///
86/// Check for a build/index.html file, an indication that a React build
87/// has previously succeeded in the indicated crate subdirectory
88///
89/// Check for NPM and connection to servers using `npm ping`
90/// If first run try NPM install to fetch deps
91/// Check for NPM and connection to servers, possibly again
92/// Attempt to build using `npm run build`
93///
94/// After the build has succeeded once, subsequent runs will
95/// instruct your cargo to only run `build.rs` on updates.
96///
97pub fn build_my_react_js_fallible(path: &str, outer_env: &str, silent: bool) -> Result<(), String> {
98    let mut d = PathBuf::from(outer_env);
99    d.push(format!("{path}/build/index.html"));
100    if d.components().any(|z| {
101        z == Component::ParentDir
102            || z.as_os_str()
103                .as_encoded_bytes()
104                .iter()
105                .find(|&c| *c == b'*')
106                != None
107    }) {
108        return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} Invalid separator provided, '{path}'"));
109    }
110    match std::fs::exists(PathBuf::from(d)) {
111        Ok(defined) => {
112            if defined {
113                let mut d = PathBuf::from(outer_env);
114                d.push(format!("{path}/src/"));
115                println!("cargo::rerun-if-changed={}", d.to_string_lossy());
116
117                let mut d = PathBuf::from(outer_env);
118                d.push(format!("{path}/package.json"));
119                println!("cargo::rerun-if-changed={}", d.to_string_lossy());
120            } else {
121                let mut d = PathBuf::from(outer_env);
122                d.push(format!("{path}/"));
123                if let Ok(output) = Command::new("npm").arg("ping").output() {
124                    if !output.status.success() {
125                        print_warning(
126                            format!("Unable to locate npm, cannot complete build."),
127                            silent,
128                        );
129
130                        print_warning(
131                            format!("Failed with: {}", str::from_utf8(&output.stdout).unwrap()),
132                            silent,
133                        );
134                        return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset}NPM unavailable"));
135                    } else {
136                        print_warning(format!("Located NPM for frontend build."), silent);
137                    }
138                } else {
139                    return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} Node Package Manager not found, or npm registry unreachable! Ensure the system is configured with npm."));
140                }
141
142                let mut d = PathBuf::from(outer_env);
143                d.push(format!("{path}/"));
144                if let Ok(output) = Command::new("npm")
145                    .current_dir(d)
146                    .arg(format!("install"))
147                    .output()
148                {
149                    if !output.status.success() {
150                        print_warning(format!("NPM build failed."), silent);
151                        print_warning(
152                            format!(
153                                "NPM build reported:{}",
154                                str::from_utf8(&output.stdout).unwrap()
155                            ),
156                            silent,
157                        );
158                        return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} NPM unavailable"));
159                    } else {
160                        print_warning(format!("Installed **node_modules**"), silent);
161                    }
162                } else {
163                    return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} Node Package Manager error! Check system logs."));
164                }
165            }
166        }
167        Err(e) => return Err(e.to_string()),
168    }
169
170    let mut d = PathBuf::from(outer_env);
171    d.push(format!("{path}/"));
172    if let Ok(output) = Command::new("npm").arg("ping").output() {
173        if !output.status.success() {
174            print_warning(
175                format!("Unable to locate npm, cannot complete build."),
176                silent,
177            );
178            print_warning(
179                format!("Failed with: {}", str::from_utf8(&output.stdout).unwrap()),
180                silent,
181            );
182            return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} NPM unavailable"));
183        } else {
184            print_warning(format!("Located NPM for frontend build."), silent);
185        }
186    } else {
187        return Err(
188            format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} Node Package Manager not found! Ensure the system is configured with npm."),
189        );
190    }
191
192    let mut d = PathBuf::from(outer_env);
193    d.push(format!("{path}/"));
194    if let Ok(output) = Command::new("npm")
195        .current_dir(d)
196        .arg("run")
197        .arg("build")
198        .output()
199    {
200        if !output.status.success() {
201            print_warning(format!("NPM build failed."), silent);
202            print_warning(
203                format!(
204                    "NPM build reported:{}",
205                    str::from_utf8(&output.stdout).unwrap()
206                ),
207                silent,
208            );
209            return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} NPM unavailable"));
210        } else {
211            print_warning(format!("Frontend build completed successfully!"), silent);
212        }
213    } else {
214        return Err(format!("{style_bold}{color_bright_red}ReactJS Frontend build error:{color_reset}{style_reset} Node Package Manager error! Check system logs."));
215    }
216
217    Ok(())
218}
219
220#[doc(hidden)]
221fn print_warning(s: String, silent: bool) {
222    if !silent {
223        println!("cargo::warning={}", s);
224    }
225}