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>
114 lines
6.2 KiB
Bash
Executable File
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"
|