Skip to content

Instantly share code, notes, and snippets.

@johnbcodes
Created March 21, 2023 12:22
Show Gist options
  • Save johnbcodes/46ccd7a6bc8029ec98721decaf7cbea5 to your computer and use it in GitHub Desktop.
Save johnbcodes/46ccd7a6bc8029ec98721decaf7cbea5 to your computer and use it in GitHub Desktop.
Invoke NPM from Cargo experiment
// 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