Created
March 21, 2023 12:22
-
-
Save johnbcodes/46ccd7a6bc8029ec98721decaf7cbea5 to your computer and use it in GitHub Desktop.
Invoke NPM from Cargo experiment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// As of 2023/03/21 there's no way to use Cargo's file watching (using mtime) with NPM | |
// to tell if any dependencies have changed. NPM rewrites package-lock.json even if no | |
// change occurs. Even a few package directories inside of node_modules get touched | |
// during the process so watching that directory does not work. | |
// | |
// Taking the idea even further still, there's now way to tell when transitive dependencies | |
// need updating. | |
// | |
// Will revisit once Orogene hopefully becomes more mature and feature complete: | |
// https://github.com/orogene/orogene | |
// | |
// Adapted from: https://github.com/koute/bytehound/blob/master/server-core/build.rs | |
use fd_lock::RwLock; | |
use std::fs; | |
use std::io; | |
use std::path::{Path, PathBuf}; | |
use std::process::{Child, Command}; | |
use std::{env, path}; | |
fn main() { | |
let root: PathBuf = env::var_os("CARGO_MANIFEST_DIR") | |
.expect("missing CARGO_MANIFEST_DIR") | |
.into(); | |
let target: PathBuf = env::var_os("CARGO_TARGET_DIR") | |
.map(|directory| directory.into()) | |
.unwrap_or(root.join("target")); | |
track_changes(&root); | |
check_requirements(); | |
install_js_dependencies(&root); | |
// Directory in target/ and separate from rust paths (debug, release, etc) | |
let target_ui = target.join("ui"); | |
// Parcel build directory | |
let target_ui_build = target_ui.join("build"); | |
// Assets to be included binary | |
let target_ui_public = target_ui.join("public"); | |
// UI main directory | |
let ui = root.join("ui"); | |
// UI source files | |
let ui_src = ui.join("src"); | |
// Create target directory in case of first build | |
let _ = fs::create_dir_all(&target_ui); | |
// Coordinate with other cargo process invocations | |
let lock_path = target_ui.join(".ui-lock"); | |
let lock_file = fs::File::options() | |
.create(true) | |
.write(true) | |
.open(lock_path) | |
.unwrap(); | |
let _file_lock = RwLock::new(lock_file); | |
// Remove previous generated files if any | |
_ = fs::remove_dir_all(&target_ui_build); | |
_ = fs::remove_dir_all(&target_ui_public); | |
_ = fs::create_dir(&target_ui_build); | |
_ = fs::create_dir(&target_ui_public); | |
compile_css(&root, &target_ui_build, &ui_src); | |
prepare_web_compile(&root, &target_ui_build, &target_ui_public, &ui_src); | |
compile_web(&root, &target_ui_build, &target_ui_public); | |
// Copy altered layout template to askama source directory | |
fs::copy( | |
target_ui_public.join("base.html"), | |
ui.join("templates").join("base.html"), | |
) | |
.unwrap(); | |
} | |
fn check_requirements() { | |
if !check_command("node") { | |
panic!("Node.js not found; you need to install it before you can build the frontend"); | |
} | |
if !check_command("npm") { | |
panic!("NPM not found; you need to install it before you can build the frontend"); | |
} | |
} | |
fn install_js_dependencies(crate_root: &PathBuf) { | |
if !crate_root.join("node_modules").exists() { | |
let mut child = Command::new("npm") | |
.args(["install"]) | |
.current_dir(crate_root) | |
.spawn() | |
.expect("cannot launch a child process to install frontend JS/TS dependencies"); | |
match child.wait() { | |
Err(_) => { | |
panic!("Failed to install frontend dependencies!"); | |
} | |
Ok(status) if !status.success() => { | |
panic!("Failed to install frontend dependencies; child process exited with error code {:?}!", status.code()); | |
} | |
Ok(_) => {} | |
} | |
} | |
} | |
fn track_changes(crate_root: &Path) { | |
let mut paths: Vec<PathBuf> = Vec::new(); | |
paths.push(crate_root.join("package.json")); | |
paths.push(crate_root.join("tailwind.config.js")); | |
paths.push(crate_root.join("ui").join("src")); | |
for path in paths { | |
println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); | |
} | |
} | |
// Build CSS with Tailwind from source to build directory | |
fn compile_css(root: &Path, target_ui_build: &Path, ui_src: &Path) { | |
let mut child = Command::new("npx") | |
.args([ | |
"tailwind", | |
"-i", | |
ui_src.join("main.css").to_str().unwrap(), | |
"-o", | |
target_ui_build.join("main.css").to_str().unwrap(), | |
]) | |
.current_dir(root) | |
.spawn() | |
.expect("cannot launch Tailwind to build the frontend"); | |
check_child(&mut child, "Tailwind"); | |
} | |
// Copy JS from source to build directory | |
fn prepare_web_compile( | |
root: &PathBuf, | |
target_ui_build: &Path, | |
target_ui_public: &Path, | |
ui_src: &Path, | |
) { | |
let ui_path = ui_src.to_str().unwrap().to_owned() + path::MAIN_SEPARATOR_STR; | |
let build_path = target_ui_build.to_str().unwrap().to_owned() + path::MAIN_SEPARATOR_STR; | |
let mut child = Command::new("rsync") | |
.args([ | |
"-a", | |
"--prune-empty-dirs", | |
"--include", | |
"*/", | |
"--include", | |
"*.js", | |
"--include", | |
"*.ico", | |
"--exclude", | |
"*", | |
ui_path.as_str(), | |
build_path.as_str(), | |
]) | |
.current_dir(root) | |
.spawn() | |
.expect("cannot launch rsync to build the frontend"); | |
check_child(&mut child, "rsync"); | |
// Copy HTML from source to build directory | |
fs::copy(ui_src.join("base.html"), target_ui_build.join("base.html")).unwrap(); | |
fs::copy( | |
ui_src.join("favicon.ico"), | |
target_ui_public.join("favicon.ico"), | |
) | |
.unwrap(); | |
} | |
// Build JS and minify JS and CSS | |
fn compile_web(root: &Path, target_ui_build: &Path, target_ui_public: &Path) { | |
// Build JS and minify JS and CSS | |
let mut child = Command::new("npx") | |
.args([ | |
"parcel", | |
"build", | |
target_ui_build.join("base.html").to_str().unwrap(), | |
"--public-url=/dist", | |
format!("--dist-dir={}", target_ui_public.to_str().unwrap()).as_str(), | |
]) | |
.current_dir(root) | |
.spawn() | |
.expect("cannot launch Parcel to build the frontend"); | |
check_child(&mut child, "Parcel"); | |
} | |
fn check_command(command: &str) -> bool { | |
match Command::new(command).args(["--version"]).status() { | |
Err(ref error) if error.kind() == io::ErrorKind::NotFound => false, | |
Err(error) => { | |
panic!("Cannot launch `{}`: {}", command, error); | |
} | |
Ok(_) => true, | |
} | |
} | |
fn check_child(command: &mut Child, name: &str) -> bool { | |
match command.wait() { | |
Err(_) => { | |
panic!("Failed to run {}!", name); | |
} | |
Ok(status) if !status.success() => { | |
panic!( | |
"Failed to to run {}; exited with error code {:?}!", | |
name, | |
status.code() | |
); | |
} | |
Ok(_) => true, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment