204 lines
7.4 KiB
Shell
204 lines
7.4 KiB
Shell
|
|
#!/bin/sh
|
||
|
|
# Run the whole site locally, in the same shape as production.
|
||
|
|
#
|
||
|
|
# tools/dev.sh build both products and serve on :8080
|
||
|
|
# tools/dev.sh --no-build use whatever is already in bin/
|
||
|
|
#
|
||
|
|
# Why this exists: catcrafts-server serves PAGES only. Static assets — the wasm
|
||
|
|
# module, styles.css, the JS bridges — are Caddy's job in production, so running
|
||
|
|
# the server on its own gives you correct HTML with no stylesheet, which looks
|
||
|
|
# broken and isn't. This starts both and puts Caddy in front, so what you see
|
||
|
|
# locally is what the deployed site does, including the reverse-proxy split and
|
||
|
|
# the scoped cross-origin headers.
|
||
|
|
#
|
||
|
|
# Ctrl-C stops both.
|
||
|
|
|
||
|
|
set -eu
|
||
|
|
|
||
|
|
BUILD=1
|
||
|
|
[ "${1:-}" = "--no-build" ] && BUILD=0
|
||
|
|
|
||
|
|
PORT="${DEV_PORT:-8080}"
|
||
|
|
BACKEND_PORT="${DEV_BACKEND_PORT:-8081}"
|
||
|
|
WORK="$(mktemp -d)"
|
||
|
|
|
||
|
|
# Refuse to start if either port is taken.
|
||
|
|
#
|
||
|
|
# Without this, a leftover instance from an earlier run keeps serving: the new
|
||
|
|
# backend fails to bind, the old Caddy carries on proxying to the OLD binary, and
|
||
|
|
# the site looks like the build did not take effect. That has wasted real time
|
||
|
|
# twice — the symptom (stale content) points at the build, not at a process.
|
||
|
|
port_busy() {
|
||
|
|
if command -v ss >/dev/null 2>&1; then
|
||
|
|
ss -ltn 2>/dev/null | grep -qE "[:.]$1 "
|
||
|
|
else
|
||
|
|
curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$1/" 2>/dev/null
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
for _p in "$PORT" "$BACKEND_PORT"; do
|
||
|
|
if port_busy "$_p"; then
|
||
|
|
echo "dev: port $_p is already in use — another instance is probably still running." >&2
|
||
|
|
echo "dev: find it with: ss -ltnp | grep -E ':(8080|8081) '" >&2
|
||
|
|
echo "dev: then kill those PIDs, or set DEV_PORT / DEV_BACKEND_PORT." >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
cleanup() {
|
||
|
|
[ -n "${SRV_PID:-}" ] && kill "$SRV_PID" 2>/dev/null || true
|
||
|
|
[ -n "${CADDY_PID:-}" ] && kill "$CADDY_PID" 2>/dev/null || true
|
||
|
|
rm -rf "$WORK"
|
||
|
|
}
|
||
|
|
trap cleanup EXIT INT TERM
|
||
|
|
|
||
|
|
# Exactly one match or fail loudly. A variant directory name embeds a config
|
||
|
|
# hash, so two matches means the tree holds artifacts from two different
|
||
|
|
# configurations and picking either would be a coin flip — this has caused real
|
||
|
|
# confusion (testing a stale binary and believing the result).
|
||
|
|
onedir() {
|
||
|
|
_m=$(find bin -maxdepth 1 -type d -name "$1" 2>/dev/null | sort)
|
||
|
|
_n=$(printf '%s\n' "$_m" | grep -c . || true)
|
||
|
|
if [ "$_n" -ne 1 ]; then
|
||
|
|
echo "dev: expected exactly one $1 directory under bin/, found $_n" >&2
|
||
|
|
[ "$_n" -gt 1 ] && printf '%s\n' "$_m" >&2
|
||
|
|
echo "dev: run 'rm -rf bin' and try again" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
printf '%s' "$_m"
|
||
|
|
}
|
||
|
|
|
||
|
|
if [ "$BUILD" = 1 ]; then
|
||
|
|
echo "dev: building the server product..."
|
||
|
|
crafter-build --local -- --product=server >"$WORK/build-server.log" 2>&1 \
|
||
|
|
|| { echo "dev: server build failed:" >&2; tail -20 "$WORK/build-server.log" >&2; exit 1; }
|
||
|
|
SRV=$(onedir 'Catcrafts.Server-*')
|
||
|
|
|
||
|
|
# sitemap.xml and feed.xml are generated from the same route table and Post
|
||
|
|
# model the pages use, and the wasm build copies them into the bundle — so
|
||
|
|
# they have to exist before it runs.
|
||
|
|
"$SRV/catcrafts-server" --sitemap > sitemap.xml
|
||
|
|
"$SRV/catcrafts-server" --feed > feed.xml
|
||
|
|
|
||
|
|
echo "dev: building the wasm bundle..."
|
||
|
|
crafter-build --local >"$WORK/build-web.log" 2>&1 \
|
||
|
|
|| { echo "dev: wasm build failed:" >&2; tail -20 "$WORK/build-web.log" >&2; exit 1; }
|
||
|
|
fi
|
||
|
|
|
||
|
|
SRV=$(onedir 'Catcrafts.Server-*')
|
||
|
|
WEB=$(onedir 'Catcrafts.Net-*')
|
||
|
|
|
||
|
|
# Makes the static shell survive being served at a deep URL. Run unconditionally,
|
||
|
|
# not just after a build: --no-build may be pointing at a bundle someone produced
|
||
|
|
# with a bare crafter-build, and the script is idempotent.
|
||
|
|
./tools/fix-bundle-depth.sh "$WEB" >/dev/null
|
||
|
|
|
||
|
|
ABS_WEB="$(cd "$WEB" && pwd)"
|
||
|
|
# Mirrored post media. Absent is fine — the pages render, the media 404s — so
|
||
|
|
# this does not block running the site before fetch-media.sh has been run.
|
||
|
|
mkdir -p media
|
||
|
|
PWD_MEDIA="$(cd media && pwd)"
|
||
|
|
|
||
|
|
# Mirrors deploy/Caddyfile.example: static assets from disk, everything else
|
||
|
|
# proxied to the backend, cross-origin isolation only on the paths that boot the
|
||
|
|
# wasm module.
|
||
|
|
cat > "$WORK/Caddyfile" <<EOF
|
||
|
|
:$PORT {
|
||
|
|
root * $ABS_WEB
|
||
|
|
encode zstd gzip
|
||
|
|
|
||
|
|
@isolated path /demos/* /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js /catcrafts-head.js /files.json /variants.json /*.wgsl
|
||
|
|
header @isolated {
|
||
|
|
Cross-Origin-Opener-Policy "same-origin"
|
||
|
|
Cross-Origin-Embedder-Policy "require-corp"
|
||
|
|
Cross-Origin-Resource-Policy "same-origin"
|
||
|
|
}
|
||
|
|
|
||
|
|
@static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js /catcrafts-head.js /files.json /variants.json /styles.css /favicon.svg /robots.txt /*.wgsl /*.jpg /posts.json /rates.json
|
||
|
|
handle @static {
|
||
|
|
header Cache-Control "no-store"
|
||
|
|
file_server
|
||
|
|
}
|
||
|
|
|
||
|
|
handle_path /media/* {
|
||
|
|
root * $PWD_MEDIA
|
||
|
|
header Cache-Control "no-store"
|
||
|
|
file_server
|
||
|
|
}
|
||
|
|
|
||
|
|
handle {
|
||
|
|
reverse_proxy 127.0.0.1:$BACKEND_PORT
|
||
|
|
}
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
|
||
|
|
# Rail selection for dev:
|
||
|
|
# * a repo-root .env (gitignored, never committed) is sourced if present —
|
||
|
|
# put MOLLIE_API_KEY=test_… there to point dev at Mollie's real test mode;
|
||
|
|
# * DEV_RAIL=fake|mollie overrides the automatic choice;
|
||
|
|
# * default with no key is the fake rail: full order lifecycle, no network.
|
||
|
|
# "Pay" an order with: touch $WORK/orders.jsonl.fake-paid
|
||
|
|
#
|
||
|
|
# A live_ key is refused outright. Dev creates throwaway orders; pointing them
|
||
|
|
# at real money collection is never what anyone meant.
|
||
|
|
if [ -f .env ]; then
|
||
|
|
set -a; . ./.env; set +a
|
||
|
|
fi
|
||
|
|
RAIL="${DEV_RAIL:-}"
|
||
|
|
if [ -z "$RAIL" ]; then
|
||
|
|
RAIL=fake
|
||
|
|
[ -n "${MOLLIE_API_KEY:-}" ] && RAIL=mollie
|
||
|
|
fi
|
||
|
|
if [ "$RAIL" = mollie ]; then
|
||
|
|
case "${MOLLIE_API_KEY:-}" in
|
||
|
|
test_*) echo "dev: payments via Mollie TEST mode" ;;
|
||
|
|
live_*) echo "dev: refusing to run dev against a LIVE Mollie key." >&2
|
||
|
|
echo "dev: live keys belong in /etc/catcrafts/payments.env on the server." >&2
|
||
|
|
exit 1 ;;
|
||
|
|
*) echo "dev: MOLLIE_API_KEY is not a test_ or live_ key" >&2; exit 1 ;;
|
||
|
|
esac
|
||
|
|
fi
|
||
|
|
|
||
|
|
"$SRV/catcrafts-server" --serve "$BACKEND_PORT" \
|
||
|
|
--orders="$WORK/orders.jsonl" --rail="$RAIL" \
|
||
|
|
--redirect-base="http://localhost:$PORT" >"$WORK/server.log" 2>&1 &
|
||
|
|
SRV_PID=$!
|
||
|
|
|
||
|
|
caddy run --config "$WORK/Caddyfile" --adapter caddyfile >"$WORK/caddy.log" 2>&1 &
|
||
|
|
CADDY_PID=$!
|
||
|
|
|
||
|
|
# Wait for the front door rather than sleeping a fixed amount.
|
||
|
|
i=0
|
||
|
|
while [ "$i" -lt 100 ]; do
|
||
|
|
curl -s -o /dev/null "http://127.0.0.1:$PORT/api/healthz" 2>/dev/null && break
|
||
|
|
i=$((i + 1)); sleep 0.1
|
||
|
|
done
|
||
|
|
if [ "$i" -ge 100 ]; then
|
||
|
|
echo "dev: did not come up. server log:" >&2; cat "$WORK/server.log" >&2
|
||
|
|
echo "dev: caddy log:" >&2; cat "$WORK/caddy.log" >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
cat <<EOF
|
||
|
|
|
||
|
|
catcrafts.net is running: http://localhost:$PORT
|
||
|
|
|
||
|
|
/ home
|
||
|
|
/shop product list
|
||
|
|
/shop/fp6-pmos product page + checkout (fake payment rail)
|
||
|
|
/projects the Crafter suite
|
||
|
|
/posts fediverse posts
|
||
|
|
/demos demo list; /demos/raytracer loads the wasm
|
||
|
|
/legal/privacy privacy notice
|
||
|
|
/feed.xml Atom feed
|
||
|
|
/media/* mirrored post media (run tools/fetch-media.sh to populate)
|
||
|
|
|
||
|
|
Orders from this session go to a temp file and are discarded on exit.
|
||
|
|
Payment rail: $RAIL$([ "$RAIL" = fake ] && printf '%s' " — simulate a customer paying with:
|
||
|
|
touch $WORK/orders.jsonl.fake-paid")
|
||
|
|
Ctrl-C to stop.
|
||
|
|
|
||
|
|
EOF
|
||
|
|
|
||
|
|
# Surface backend output as it happens — this is where a render error shows up.
|
||
|
|
tail -f "$WORK/server.log" &
|
||
|
|
wait "$SRV_PID"
|