Files
HomerandClaude Opus 5.5 3b0def78ec
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s
fetch-vendor: carry the server's process stub for @lezer/lr
lezer-lr.mjs imports esm.sh's /node/process.mjs, which the rewrite left
as an absolute path: a 404 in the app, so every parsing language failed.
It becomes ./node-process.mjs, copied from the server at the same ref,
and an absolute import surviving the rewrite now fails the script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:19:13 +00:00

114 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Put the server's vendored CodeMirror build where the app's own pages read it from:
# ui/vendor/*.mjs (copies of the server's static/vendor/cm-*.mjs and lezer-*.mjs)
#
# scripts/fetch-vendor.sh <server-ref> copy the modules at that ref (a tag such as
# v2.0.0-beta.1, or a branch) and write vendor.lock.json
# scripts/fetch-vendor.sh verify: ui/vendor matches vendor.lock.json
#
# The server is a git checkout, LLeMbas beside this repository by default (LLEMBAS_SERVER names
# another). Each module is checked against the server's own pin (its scripts/vendor.lock.json, at
# the same ref) before it is copied, so what the app carries is exactly what the server serves.
#
# The one change made to a copy: the server loads these modules through an import map
# ("@codemirror/view" -> its file), which the app's pages do not have -- an inline import map is a
# script the content security policy would have to allow. So the bare names in `import`/`from`
# are rewritten to the sibling files ("./cm-view.mjs"), and nothing else is touched. esm.sh's
# Node polyfill ("/node/process.mjs", which @lezer/lr imports to read process.env.LOG) becomes
# "./node-process.mjs", the server's own one-line stub (static/js/node-process.mjs), copied too:
# an absolute path is a 404 in the app, and a failed @lezer/lr takes every editor language with it.
#
# vendor.lock.json records the ref, the commit it resolved to, and for every file the server's
# SHA-256 and the copy's, which is what verify checks. Like fetch-tokens.sh: the file in the tree
# says which server version the editor comes from, and CI refuses a copy that drifted.
set -euo pipefail
cd "$(dirname "$0")/.."
SERVER="${LEMBAS_SERVER:-../LLeMbas}"
SOURCE="src/lembas/web/static/vendor"
SERVER_LOCK="scripts/vendor.lock.json"
OUT="ui/vendor"
LOCK="vendor.lock.json"
sha() { sha256sum "$1" | cut -d' ' -f1; }
if [ $# -eq 0 ]; then
[ -f "$LOCK" ] || { echo "fetch-vendor: no $LOCK; run with a server ref first" >&2; exit 1; }
ref="$(sed -n -E 's/^ *"tag": *"([^"]+)".*/\1/p' "$LOCK" | head -n1)"
bad=0
count=0
# One file a line: "name": {"server": "…", "copy": "…"}
while IFS=$'\t' read -r name want; do
count=$((count + 1))
if [ ! -f "$OUT/$name" ]; then
echo "fetch-vendor: $OUT/$name is missing" >&2
bad=1
elif [ "$(sha "$OUT/$name")" != "$want" ]; then
echo "fetch-vendor: $OUT/$name does not match $LOCK (server $ref)" >&2
bad=1
fi
done < <(sed -n -E 's/^ *"([a-z0-9-]+\.mjs)": *\{"server": *"[0-9a-f]{64}", *"copy": *"([0-9a-f]{64})"\}.*/\1\t\2/p' "$LOCK")
for f in "$OUT"/*.mjs; do
grep -q "\"$(basename "$f")\"" "$LOCK" || { echo "fetch-vendor: $f is not in $LOCK" >&2; bad=1; }
done
[ "$count" -gt 0 ] || { echo "fetch-vendor: $LOCK lists no file" >&2; exit 1; }
[ "$bad" -eq 0 ] || exit 1
echo "fetch-vendor: $count files in $OUT match $LOCK (server $ref)"
exit 0
fi
REF="$1"
[ -d "$SERVER/.git" ] || [ -f "$SERVER/.git" ] \
|| { echo "fetch-vendor: $SERVER is not a git checkout of the LLeMbas server (set LEMBAS_SERVER)" >&2; exit 1; }
commit="$(git -C "$SERVER" rev-parse --verify --quiet "$REF^{commit}")" \
|| { echo "fetch-vendor: $REF is not a commit in $SERVER" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
git -C "$SERVER" show "$commit:$SERVER_LOCK" > "$work/server-lock.json" \
|| { echo "fetch-vendor: $SERVER_LOCK is not in $SERVER at $REF" >&2; exit 1; }
names="$(git -C "$SERVER" ls-tree --name-only "$commit" "$SOURCE/" | sed -n -E 's#^.*/((cm|lezer)-[a-z0-9-]+\.mjs)$#\1#p' | sort)"
[ -n "$names" ] || { echo "fetch-vendor: no CodeMirror modules in $SOURCE at $REF" >&2; exit 1; }
mkdir -p "$work/out"
entries=()
for name in $names; do
git -C "$SERVER" show "$commit:$SOURCE/$name" > "$work/$name"
pinned="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], {}).get("sha256", ""))' "$work/server-lock.json" "$name")"
have="$(sha "$work/$name")"
[ -n "$pinned" ] || { echo "fetch-vendor: $name is not pinned in the server's $SERVER_LOCK" >&2; exit 1; }
[ "$pinned" = "$have" ] || { echo "fetch-vendor: $name does not match the server's pin ($have, pinned $pinned)" >&2; exit 1; }
# "@codemirror/view" -> "./cm-view.mjs", "@lezer/lr" -> "./lezer-lr.mjs", "codemirror" -> "./cm-codemirror.mjs".
perl -pe 's#\b(from|import)(\s*)"\@codemirror/([a-z-]+)"#$1$2"./cm-$3.mjs"#g; s#\b(from|import)(\s*)"\@lezer/([a-z-]+)"#$1$2"./lezer-$3.mjs"#g; s#\b(from|import)(\s*)"codemirror"#$1$2"./cm-codemirror.mjs"#g; s#\b(from|import)(\s*)"/node/process\.mjs"#$1$2"./node-process.mjs"#g' \
"$work/$name" > "$work/out/$name"
if grep -E -q '(from|import)\s*"((@codemirror|@lezer)/|/)' "$work/out/$name"; then
echo "fetch-vendor: a bare module name or an absolute path survived in $name; fix the script" >&2
exit 1
fi
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$(sha "$work/out/$name")")")
done
# The polyfill stub, from the server's own scripts (it is not a download, so it has no pin there).
stub="src/lembas/web/static/js/node-process.mjs"
git -C "$SERVER" show "$commit:$stub" > "$work/out/node-process.mjs" \
|| { echo "fetch-vendor: $stub is not in $SERVER at $REF" >&2; exit 1; }
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' node-process.mjs "$(sha "$work/out/node-process.mjs")" "$(sha "$work/out/node-process.mjs")")")
# Every module a copy imports must be one of the copies.
for f in "$work"/out/*.mjs; do
while read -r dep; do
[ -f "$work/out/$dep" ] || { echo "fetch-vendor: $(basename "$f") imports $dep, which is not copied" >&2; exit 1; }
done < <(grep -o -E '(from|import)\s*"\./[a-z0-9-]+\.mjs"' "$f" | sed -E 's/.*"\.\/([^"]+)"/\1/' | sort -u)
done
rm -rf "$OUT"
mkdir -p "$OUT"
cp "$work"/out/*.mjs "$OUT/"
{
printf '{\n "tag": "%s",\n "commit": "%s",\n "files": {\n' "$REF" "$commit"
last=$((${#entries[@]} - 1))
for i in "${!entries[@]}"; do
if [ "$i" -lt "$last" ]; then printf '%s,\n' "${entries[$i]}"; else printf '%s\n' "${entries[$i]}"; fi
done
printf ' }\n}\n'
} > "$LOCK"
echo "fetch-vendor: LLeMbas $REF (${commit:0:12}) $SOURCE -> $OUT (${#entries[@]} files); $LOCK written"