#!/usr/bin/env bash # ============================================================================ # prerender-static.sh — build aac.transaction.science as a self-contained # static site. # # AAC ships as a static CSR-hydrate SPA, but it's a Leptos SSR app: the # index.html the browser loads is the SSR server's *rendered* output (it carries # the hydration island markers and the pre-rendered board). A plain # `cargo leptos build` never emits that index.html, and it doesn't copy the # PWA assets that live in `static/`. This script closes both gaps and makes the # result safe to cache forever: # # build -> target/site/{pkg/, icons/} (cargo-leptos) # render -> target/site/index.html (capture SSR `/`) # hash -> pkg/aac-web..{js,wasm,css} (and rewrite references) # worker -> target/site/sw.js (static/sw.js + precache list) # # Output: a self-contained `target/site/` ready to rsync to prod. Deploy it with # no_cache_bust — the names are already content-hashed, and the service worker # lists them. Idempotent. # # Hosting under a sub-path, or under another name, is a build-time choice; see # AAC_BASE_PATH, AAC_SW_URL, AAC_APP_TITLE and AAC_HIDE_ENERGY in # crates/aac-web/src/app.rs. This script builds the root-mounted default. # ============================================================================ set -euo pipefail cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PORT="${AAC_PRERENDER_PORT:-39207}" SITE="target/site" CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/aac-rs" # wasm-bindgen's CLI must match the crate version exactly (Cargo.toml pins it). # Use a private copy of the right version rather than the machine's global one. WBG_VER="$(awk '/^name = "wasm-bindgen"$/{getline; gsub(/version = |"/, ""); print; exit}' Cargo.lock)" if ! wasm-bindgen --version 2>/dev/null | grep -qx "wasm-bindgen $WBG_VER"; then WBG_ROOT="$CACHE/wasm-bindgen-$WBG_VER" if [ ! -x "$WBG_ROOT/bin/wasm-bindgen" ]; then echo "▸ installing wasm-bindgen-cli $WBG_VER into $WBG_ROOT" cargo install --quiet --locked --root "$WBG_ROOT" wasm-bindgen-cli --version "$WBG_VER" fi export PATH="$WBG_ROOT/bin:$PATH" fi echo "▸ cargo leptos build --release" rm -rf "$SITE/pkg" cargo leptos build --release find "$SITE/pkg" -name '*.d.ts' -delete echo "▸ starting SSR server on 127.0.0.1:$PORT to prerender /" LOG="$(mktemp)" LEPTOS_OUTPUT_NAME=aac-web \ LEPTOS_SITE_ROOT="$SITE" \ LEPTOS_SITE_PKG_DIR=pkg \ LEPTOS_SITE_ADDR="127.0.0.1:$PORT" \ LEPTOS_ENV=PROD \ target/release/aac-web > "$LOG" 2>&1 & SRV=$! trap 'kill "$SRV" 2>/dev/null || true' EXIT # Wait for the listener (up to ~30s). ready=false for _ in $(seq 1 120); do if curl -sf "http://127.0.0.1:$PORT/" -o /dev/null 2>/dev/null; then ready=true; break; fi sleep 0.25 done if ! $ready; then echo "✗ SSR server did not come up; log:" >&2; tail -5 "$LOG" >&2; exit 1 fi echo "▸ capturing rendered / -> $SITE/index.html" curl -sf "http://127.0.0.1:$PORT/" -o "$SITE/index.html" kill "$SRV" 2>/dev/null || true trap - EXIT # Sanity-check the capture is the hydrating SSR shell, not an error page. test -s "$SITE/index.html" grep -q 'mod.hydrate' "$SITE/index.html" || { echo "✗ captured index.html is not the hydrating shell" >&2; exit 1; } grep -q 'AAC' "$SITE/index.html" || { echo "✗ captured index.html missing expected title" >&2; exit 1; } echo "▸ content-hashing pkg/" hash12() { shasum -a 256 "$1" | cut -c1-12; } # Portable in-place sed (BSD and GNU disagree about -i): sed_i <args…> <file> sed_i() { local f="${!#}"; sed "${@:1:$#-1}" "$f" > "$f.tmp" && mv "$f.tmp" "$f"; } P="$SITE/pkg" [ -f "$P/aac-web_bg.wasm" ] || mv "$P/aac-web.wasm" "$P/aac-web_bg.wasm" W="$(hash12 "$P/aac-web_bg.wasm")"; mv "$P/aac-web_bg.wasm" "$P/aac-web.$W.wasm" C="$(hash12 "$P/aac-web.css")"; mv "$P/aac-web.css" "$P/aac-web.$C.css" # The glue may name its wasm; point it at the hashed file before hashing the glue. sed_i -e "s#aac-web_bg\\.wasm#aac-web.$W.wasm#g" -e "s#aac-web\\.wasm#aac-web.$W.wasm#g" "$P/aac-web.js" J="$(hash12 "$P/aac-web.js")"; mv "$P/aac-web.js" "$P/aac-web.$J.js" sed_i \ -e "s#/pkg/aac-web_bg\\.wasm#/pkg/aac-web.$W.wasm#g" \ -e "s#/pkg/aac-web\\.wasm#/pkg/aac-web.$W.wasm#g" \ -e "s#/pkg/aac-web\\.js#/pkg/aac-web.$J.js#g" \ -e "s#/pkg/aac-web\\.css#/pkg/aac-web.$C.css#g" \ "$SITE/index.html" # Leptos's hydration loader builds its URLs from arguments: # (root, pkg_path, output_name, wasm_output_name) LOADER='("", "pkg", "aac-web", "aac-web")' grep -qF "$LOADER" "$SITE/index.html" || { echo "✗ hydration loader call not found — has Leptos changed its loader?" >&2; exit 1; } sed_i "s#(\"\", \"pkg\", \"aac-web\", \"aac-web\")#(\"\", \"pkg\", \"aac-web.$J\", \"aac-web.$W\")#" "$SITE/index.html" for f in "aac-web.$W.wasm" "aac-web.$J.js" "aac-web.$C.css" "\"aac-web.$J\", \"aac-web.$W\""; do grep -q "$f" "$SITE/index.html" || { echo "✗ index.html does not reference $f" >&2; exit 1; } done echo "▸ bundling PWA assets and the service worker" cp crates/aac-web/static/manifest.json "$SITE/manifest.json" # Precache: the shell, the manifest, every file in pkg/ (including wasm-bindgen # snippets such as the eye-tracking loader) and the icons. PRECACHE=("/" "/manifest.json") while IFS= read -r f; do PRECACHE+=("/${f#"$SITE"/}"); done < <(find "$SITE/pkg" "$SITE/icons" -type f | LC_ALL=C sort) VERSION="$(for u in "${PRECACHE[@]}"; do if [ "$u" = "/" ]; then f="$SITE/index.html"; else f="$SITE$u"; fi printf '%s %s\n' "$u" "$(shasum -a 256 "$f" | cut -c1-64)" done | shasum -a 256 | cut -c1-12)" LIST="[$(printf '"%s",' "${PRECACHE[@]}" | sed 's/,$//')]" sed -e "s#__VERSION__#$VERSION#" -e "s#__PRECACHE_URLS__#$LIST#" crates/aac-web/static/sw.js > "$SITE/sw.js" grep -q "__" "$SITE/sw.js" && grep -nE "__[A-Z_]+__" "$SITE/sw.js" && { echo "✗ sw.js has an unfilled placeholder" >&2; exit 1; } echo "✓ self-contained dist ready at $SITE/ (service worker $VERSION, ${#PRECACHE[@]} files precached)" ls -1 "$SITE" "$SITE/pkg"