catcrafts.net/tools/fix-bundle-depth.sh

76 lines
3.3 KiB
Shell
Raw Permalink Normal View History

2026-08-05 04:18:37 +02:00
#!/bin/sh
# Make the wasm bundle's index.html work when it is served at a URL deeper than
# "/", and verify it stayed that way.
#
# usage: tools/fix-bundle-depth.sh <bundle-dir>
#
# WHY THIS EXISTS
#
# Crafter.Build generates an index.html whose boot scripts are relative
# (src="runtime.js?v=…"), and runtime.js in turn does fetch("variants.json"),
# fetch("files.json"), one fetch per VFS entry, and fetches the .wasm named by
# variants.json — all relative. Relative to the DOCUMENT, not to the module.
#
# That is correct when the document is "/". It is broken for every deeper path,
# and this site has several: /demos/raytracer, /shop/<slug>, /legal/<page>. A
# document at /demos/raytracer sends the browser to /demos/runtime.js, which does
# not exist, so Caddy's `try_files {path} /index.html` returns index.html — and
# the browser refuses to execute a module served as text/html. The visible result
# is four NS_ERROR_CORRUPTED_CONTENT errors and a dead page.
#
# The SSR path solves this itself (Views::RenderDocument emits <base href="/"> on
# any page that boots wasm). This script covers the OTHER path: the static shell
# Caddy serves directly when the backend is down, where there is no SSR to help.
#
# Two changes, both idempotent:
# 1. <base href="/"> in <head>, which fixes every relative fetch runtime.js
# makes, since a bare relative fetch() resolves against the document base.
# 2. Root the boot script srcs, which <base> already handles but which is worth
# doing anyway so the tags are correct even if the <base> is ever dropped.
#
# The durable fix belongs upstream: runtime.js should resolve its own assets
# against import.meta.url rather than the document. Then no bundle would care how
# deep the page is. Until then, this.
set -eu
DIR="${1:-}"
[ -n "$DIR" ] || { echo "usage: tools/fix-bundle-depth.sh <bundle-dir>" >&2; exit 1; }
IDX="$DIR/index.html"
[ -f "$IDX" ] || { echo "fix-bundle-depth: $IDX not found" >&2; exit 1; }
TMP="$(mktemp)"
trap 'rm -f "$TMP"' EXIT
# 1. Root every relative script src. Anchored on `src="` immediately followed by
# something that is not / : and : covers http:, https: and any other scheme,
# / covers both already-rooted and protocol-relative //host.
sed -E 's|(<script[^>]*[[:space:]]src=")([^"/:][^"]*")|\1/\2|g' "$IDX" > "$TMP"
# 2. Insert <base href="/"> as the first thing in <head>, unless one is present.
# First, so it applies to everything after it — a <base> only governs the
# references that follow it.
if ! grep -qi '<base[[:space:]]' "$TMP"; then
sed -E '0,/<head>/s|<head>|<head>\n<base href="/">|' "$TMP" > "$TMP.b" \
&& mv "$TMP.b" "$TMP"
fi
mv "$TMP" "$IDX"
trap - EXIT
# Verify rather than assume. A silent no-op here would ship the broken page.
fail=0
if ! grep -qi '<base href="/">' "$IDX"; then
echo "fix-bundle-depth: FAILED to insert <base> into $IDX" >&2
fail=1
fi
rel=$(grep -oE '<script[^>]*[[:space:]]src="[^"/:][^"]*"' "$IDX" | wc -l)
if [ "$rel" -ne 0 ]; then
echo "fix-bundle-depth: $rel script src(s) are still relative in $IDX:" >&2
grep -oE '<script[^>]*[[:space:]]src="[^"/:][^"]*"' "$IDX" >&2
fail=1
fi
[ "$fail" -eq 0 ] || exit 1
n=$(grep -coE '<script[^>]*[[:space:]]src="/' "$IDX" || true)
echo "fix-bundle-depth: $IDX has <base href=\"/\"> and $n rooted script src(s)"