From 873a82ced5008705acaad66ccfb5255f40b8c6c3 Mon Sep 17 00:00:00 2001 From: Jake Goulding Date: Fri, 3 May 2024 10:41:02 -0400 Subject: [PATCH] Move from a build script to xtasks A build script is great if the assets are guaranteed to be available, but they would not be available once packaged and uploaded to a registry. Instead, this commit moves us to a system where `cargo xtask assets` will generate the appropriate Rust file. This file can be checked in immediately before a release is tagged and can then be a part of the package. --- .cargo/config.toml | 2 + .github/workflows/ci.yml | 47 ++++-- Cargo.lock | 199 +++++++++++++++++++++-- Cargo.toml | 16 +- build.rs | 98 ------------ conformance/Cargo.toml | 2 + src/html.rs | 4 +- xtask/Cargo.toml | 12 ++ xtask/src/main.rs | 338 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 590 insertions(+), 128 deletions(-) create mode 100644 .cargo/config.toml delete mode 100644 build.rs create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..f0ccbc9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cca7ed..7915cb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,9 @@ name: Continuous integration on: [push, pull_request] jobs: - check: - name: Build and test - + assets: + name: Build assets runs-on: ubuntu-latest - strategy: - matrix: - rust: - - 1.77 # MSRV - - stable - - nightly steps: - name: Checkout code @@ -50,6 +43,42 @@ jobs: - name: Format UI assets run: pnpm fmt:check + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: 'stable' + + - name: Convert assets to Rust + run: cargo xtask assets + + - name: Upload assets + uses: actions/upload-artifact@v4 + with: + name: assets + path: srs/html/assets.rs + + check: + name: Build and test + + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.77 # MSRV + - stable + - nightly + + needs: assets + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download assets + uses: actions/download-artifact@v4 + with: + name: assets + - name: Install Rust uses: dtolnay/rust-toolchain@master with: diff --git a/Cargo.lock b/Cargo.lock index 8c48cdb..d5490b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.10.4" @@ -88,7 +94,7 @@ dependencies = [ "lazy_static", "libc", "unicode-width", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -161,7 +167,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -183,6 +189,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -237,12 +252,52 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -255,6 +310,12 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + [[package]] name = "margo" version = "0.1.0" @@ -266,7 +327,6 @@ dependencies = [ "hex", "indoc", "maud", - "regex", "serde", "serde_json", "sha2", @@ -314,6 +374,36 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -367,7 +457,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -643,13 +733,28 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi-util" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] @@ -658,7 +763,22 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -667,28 +787,46 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -701,24 +839,48 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.5" @@ -733,3 +895,14 @@ checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "argh", + "notify", + "quote", + "regex", + "snafu", +] diff --git a/Cargo.toml b/Cargo.toml index 9c98a37..6339412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,17 @@ default = ["html"] html = ["dep:maud", "dep:indoc"] -[dependencies] +[workspace] +members = [ + "xtask", +] + +[workspace.dependencies] argh = { version = "0.1.12", default-features = false } +snafu = { version = "0.8.2", default-features = false, features = ["rust_1_65", "std"] } + +[dependencies] +argh.workspace = true ascii = { version = "1.1.0", default-features = false, features = ["serde", "std"] } dialoguer = { version = "0.11.0", default-features = false } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"] } @@ -25,11 +34,8 @@ maud = { version = "0.26.0", default-features = false, optional = true } serde = { version = "1.0.197", default-features = false, features = ["derive", "std"] } serde_json = { version = "1.0.115", default-features = false, features = ["std"] } sha2 = { version = "0.10.8", default-features = false } -snafu = { version = "0.8.2", default-features = false, features = ["rust_1_65", "std"] } +snafu.workspace = true tar = { version = "0.4.40", default-features = false } toml = { version = "0.8.12", default-features = false, features = ["parse", "display"] } url = { version = "2.5.0", default-features = false, features = ["serde"] } walkdir = { version = "2.5.0", default-features = false } - -[build-dependencies] -regex = { version = "1.10.4", default-features = false, features = ["std"] } diff --git a/build.rs b/build.rs deleted file mode 100644 index 80b91b0..0000000 --- a/build.rs +++ /dev/null @@ -1,98 +0,0 @@ -use regex::Regex; -use std::{ - env, - fs::{self, File}, - io::Write, - path::{Path, PathBuf}, -}; - -fn main() { - if cfg!(feature = "html") { - capture_html_assets(); - } -} - -fn capture_html_assets() { - const ASSET_ROOT: &str = "ui/dist"; - const ASSET_INDEX: &str = "ui.html"; - - let root = env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` must be set"); - let root = PathBuf::from(root); - - let asset_root = root.join(ASSET_ROOT); - let asset_index = asset_root.join(ASSET_INDEX); - - let entry = fs::read_to_string(&asset_index).expect("Could not read the UI entrypoint"); - - let (css_name, css, css_map) = extract_asset(&entry, &asset_root, { - r#"href="/assets/(ui.[a-zA-Z0-9]+.css)""# - }); - let (js_name, js, js_map) = extract_asset(&entry, &asset_root, { - r#"src="/assets/(ui.[a-zA-Z0-9]+.js)""# - }); - - let out_path = env::var("OUT_DIR").expect("`OUT_DIR` must be set"); - let mut out_path = PathBuf::from(out_path); - out_path.push("html"); - - fs::create_dir_all(&out_path).unwrap_or_else(|e| { - panic!( - "Could not create the HTML assets directory `{path}`: {e}", - path = out_path.display(), - ); - }); - - out_path.push("assets.rs"); - let mut output = File::create(&out_path).unwrap_or_else(|e| { - panic!( - "Could not open the HTML assets file `{path}`: {e}", - path = out_path.display(), - ); - }); - - write!( - output, - r##" - pub const INDEX: &str = include_str!("{asset_index}"); - - pub const CSS_NAME: &str = "{css_name}"; - pub const CSS: &str = include_str!("{css}"); - pub const CSS_MAP: &str = include_str!("{css_map}"); - - pub const JS_NAME: &str = "{js_name}"; - pub const JS: &str = include_str!("{js}"); - pub const JS_MAP: &str = include_str!("{js_map}"); - "##, - asset_index = asset_index.display(), - css_name = css_name.escape_default(), - css = css.display(), - css_map = css_map.display(), - js_name = js_name.escape_default(), - js = js.display(), - js_map = js_map.display(), - ) - .expect("Could not write HTML assets file"); - - println!("cargo::rerun-if-changed=build.rs"); - println!( - "cargo::rerun-if-changed={asset_index}", - asset_index = asset_index.display(), - ); -} - -fn extract_asset<'a>(entry: &'a str, asset_root: &Path, re: &str) -> (&'a str, PathBuf, PathBuf) { - let find_asset = Regex::new(re).expect("Invalid asset regex"); - let (_, [asset_name]) = find_asset - .captures(entry) - .expect("Could not find asset") - .extract(); - - let asset = asset_root.join(asset_name); - let asset_map = { - let mut a = asset.clone(); - a.as_mut_os_string().push(".map"); - a - }; - - (asset_name, asset, asset_map) -} diff --git a/conformance/Cargo.toml b/conformance/Cargo.toml index fa1b107..ce032de 100644 --- a/conformance/Cargo.toml +++ b/conformance/Cargo.toml @@ -6,6 +6,8 @@ publish = false license = "MIT OR Apache-2.0" +[workspace] + [dependencies] registry-conformance = { git = "https://github.com/integer32llc/registry-conformance.git" } diff --git a/src/html.rs b/src/html.rs index 7371395..b7120fd 100644 --- a/src/html.rs +++ b/src/html.rs @@ -5,9 +5,7 @@ use std::{fs, io, path::PathBuf}; use crate::{ConfigV1, ListAll, Registry}; -mod assets { - include!(concat!(env!("OUT_DIR"), "/html/assets.rs")); -} +mod assets; pub fn write(registry: &Registry) -> Result<(), Error> { use error::*; diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..375f023 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +argh.workspace = true +notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } +quote = { version = "1.0.36", default-features = false } +regex = { version = "1.10.4", default-features = false, features = ["std"] } +snafu.workspace = true diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..e24136a --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,338 @@ +use notify::{RecursiveMode, Watcher}; +use quote::quote; +use regex::Regex; +use snafu::prelude::*; +use std::{ + env, fs, io, + path::{Path, PathBuf}, + process::Command, + sync::mpsc, + thread, + time::Duration, +}; + +#[derive(Debug, argh::FromArgs)] +/// Build tools for Margo +struct Args { + #[argh(subcommand)] + subcommand: Subcommand, +} + +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand)] +enum Subcommand { + Assets(AssetsArgs), +} + +/// Manage assets +#[derive(Debug, argh::FromArgs)] +#[argh(subcommand)] +#[argh(name = "assets")] +struct AssetsArgs { + /// use default values where possible, instead of prompting for them + #[argh(switch)] + watch: bool, +} + +#[snafu::report] +fn main() -> Result<(), Error> { + let args: Args = argh::from_env(); + + match args.subcommand { + Subcommand::Assets(args) => do_assets(args)?, + } + + Ok(()) +} + +#[derive(Debug, Snafu)] +enum Error { + #[snafu(transparent)] + Assets { source: AssetsError }, +} + +fn do_assets(args: AssetsArgs) -> Result<(), AssetsError> { + use assets_error::*; + + let root = env::var("CARGO_MANIFEST_DIR").context(CargoManifestSnafu)?; + let mut root = PathBuf::from(root); + root.pop(); // Exit the `xtask` directory + + let asset_root = join!(&root, "ui", "dist"); + let asset_index = join!(&asset_root, "ui.html"); + + pnpm("install")?; + + if args.watch { + do_assets_watch(root, asset_root, asset_index)?; + } else { + do_assets_once(root, asset_root, asset_index)?; + } + + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +enum AssetsError { + #[snafu(display("`CARGO_MANIFEST_DIR` must be set"))] + CargoManifest { source: env::VarError }, + + #[snafu(display("Could not install JS dependencies"))] + #[snafu(context(false))] + PnpmInstall { source: PnpmError }, + + #[snafu(transparent)] + Watch { source: AssetsWatchError }, + + #[snafu(transparent)] + Once { source: AssetsOnceError }, +} + +fn do_assets_watch( + root: PathBuf, + asset_root: PathBuf, + asset_index: PathBuf, +) -> Result<(), AssetsWatchError> { + use assets_watch_error::*; + + let (tx, rx) = mpsc::channel(); + + let mut watcher = notify::recommended_watcher(move |evt: notify::Result| { + if let Ok(evt) = evt { + if evt.paths.iter().any(|p| is_asset_file(p).unwrap_or(false)) { + let _ = tx.send(()); + } + } + }) + .context(WatcherCreateSnafu)?; + + watcher + .watch(&asset_root, RecursiveMode::NonRecursive) + .context(WatcherWatchSnafu)?; + + // Debounce notifications + thread::spawn(move || -> Result<(), AssetsWatchError> { + loop { + recv_debounced(&rx)?; + rebuild_asset_file(&root, &asset_root, &asset_index)?; + } + }); + + pnpm("watch")?; + + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +enum AssetsWatchError { + #[snafu(display("Could not create the filesystem watcher"))] + WatcherCreate { source: notify::Error }, + + #[snafu(display("Could not watch the asset directory"))] + WatcherWatch { source: notify::Error }, + + #[snafu(display("Event channel receiver closed unexpectedly"))] + #[snafu(context(false))] + RxClosed { source: mpsc::RecvError }, + + #[snafu(transparent)] + Rebuild { source: RebuildAssetFileError }, + + #[snafu(display("Could not watch assets"))] + #[snafu(context(false))] + PnpmWatch { source: PnpmError }, +} + +fn is_asset_file(p: &Path) -> Option { + let fname = p.file_name()?; + let fname = Path::new(fname); + let ext = fname.extension()?; + + let matched = if ext == "js" || ext == "css" || ext == "html" { + true + } else if ext == "map" { + let stem = fname.file_stem()?; + let stem = Path::new(stem); + let ext = stem.extension()?; + + ext == "js" || ext == "css" || ext == "html" + } else { + false + }; + + Some(matched) +} + +fn recv_debounced(rx: &mpsc::Receiver<()>) -> Result<(), mpsc::RecvError> { + // Wait for an initial event + rx.recv()?; + + loop { + // Wait for subsequent events to stop coming in + match rx.recv_timeout(Duration::from_millis(50)) { + Ok(()) => continue, + Err(mpsc::RecvTimeoutError::Timeout) => return Ok(()), + _ => return Err(mpsc::RecvError), + }; + } +} + +fn do_assets_once( + root: PathBuf, + asset_root: PathBuf, + asset_index: PathBuf, +) -> Result<(), AssetsOnceError> { + pnpm("build")?; + rebuild_asset_file(&root, &asset_root, &asset_index)?; + + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +enum AssetsOnceError { + #[snafu(display("Could not build assets"))] + #[snafu(context(false))] + PnpmBuild { source: PnpmError }, + + #[snafu(transparent)] + Rebuild { source: RebuildAssetFileError }, +} + +fn rebuild_asset_file( + root: &Path, + asset_root: &Path, + asset_index: &Path, +) -> Result<(), RebuildAssetFileError> { + use rebuild_asset_file_error::*; + + let entry = + fs::read_to_string(asset_index).context(ReadEntrypointSnafu { path: asset_index })?; + + let (css_name, css, css_map) = extract_asset(&entry, asset_root, { + r#"href="/assets/(ui.[a-zA-Z0-9]+.css)""# + }) + .context(ExtractCssSnafu)?; + + let (js_name, js, js_map) = extract_asset(&entry, asset_root, { + r#"src="/assets/(ui.[a-zA-Z0-9]+.js)""# + }) + .context(ExtractJsSnafu)?; + + let html_dir = join!(root, "src", "html"); + fs::create_dir_all(&html_dir).context(CreateHtmlDirSnafu { path: &html_dir })?; + + let asset_src = quote! { + pub const INDEX: &str = #entry; + + pub const CSS_NAME: &str = #css_name; + pub const CSS: &str = #css; + pub const CSS_MAP: &str = #css_map; + + pub const JS_NAME: &str = #js_name; + pub const JS: &str = #js; + pub const JS_MAP: &str = #js_map; + }; + + let out_path = join!(html_dir, "assets.rs"); + fs::write(&out_path, asset_src.to_string()).context(WriteAssetFileSnafu { path: out_path })?; + + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +enum RebuildAssetFileError { + #[snafu(display("Could not read the UI entrypoint from `{}`", path.display()))] + ReadEntrypoint { source: io::Error, path: PathBuf }, + + #[snafu(display("Could not extract the CSS filename"))] + ExtractCss { source: ExtractAssetError }, + + #[snafu(display("Could not extract the JS filename"))] + ExtractJs { source: ExtractAssetError }, + + #[snafu(display("Could not create the HTML assets directory `{}`", path.display()))] + CreateHtmlDir { source: io::Error, path: PathBuf }, + + #[snafu(display("Could not write HTML assets file `{}`", path.display()))] + WriteAssetFile { source: io::Error, path: PathBuf }, +} + +fn extract_asset<'a>( + entry: &'a str, + asset_root: &Path, + re: &str, +) -> Result<(&'a str, String, String), ExtractAssetError> { + use extract_asset_error::*; + + let find_asset = Regex::new(re)?; + let (_, [asset_name]) = find_asset + .captures(entry) + .context(AssetMissingSnafu)? + .extract(); + + let asset = join!(&asset_root, asset_name); + let asset_map = { + let mut a = asset.clone(); + a.as_mut_os_string().push(".map"); + a + }; + + let asset = fs::read_to_string(&asset).context(ReadAssetSnafu { path: asset })?; + let asset_map = + fs::read_to_string(&asset_map).context(ReadAssetMapSnafu { path: asset_map })?; + + Ok((asset_name, asset, asset_map)) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +enum ExtractAssetError { + #[snafu(display("Invalid asset regex"))] + #[snafu(context(false))] + Regex { source: regex::Error }, + + #[snafu(display("Could not find asset"))] + AssetMissing, + + #[snafu(display("Could not read the asset from `{}`", path.display()))] + ReadAsset { source: io::Error, path: PathBuf }, + + #[snafu(display("Could not read the asset sourcemap from `{}`", path.display()))] + ReadAssetMap { source: io::Error, path: PathBuf }, +} + +fn pnpm(subcommand: &str) -> Result<(), PnpmError> { + use pnpm_error::*; + + let status = Command::new("pnpm") + .arg(subcommand) + .status() + .context(SpawnSnafu)?; + ensure!(status.success(), SuccessSnafu); + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +enum PnpmError { + #[snafu(display("Could not start the `pnpm` process"))] + Spawn { source: io::Error }, + + #[snafu(display("The `pnpm` process did not succeed"))] + Success, +} + +macro_rules! join { + ($base:expr, $($c:expr),+ $(,)?) => {{ + let mut base = PathBuf::from($base); + $( + base.push($c); + )* + base + }}; +} +use join;