#!/usr/bin/env bash # Per-site build entrypoint for the docker export pipeline. Runs inside the # Dockerfile.build image, once per SITE_ID, in a parallel fan-out on the host. # # The host has already run Phase A (build:data + build:archives), so the shared # LMDB index, the .export-index staging, and the archive cache are all warm and # mounted READ-ONLY. This container only composes ITS site's public/ and runs # `next build`, writing everything into the per-site mount at /site. # # Expected run-time mounts (see runDockerBuildOne in common/publish/build.ts): # -> /data/transcripts (ro) # -> /data/export/.export-index (ro) # / -> /site (rw) set -euo pipefail : "${SITE_ID:?SITE_ID is required}" export HOME="${HOME:-/tmp}" export NODE_ENV=production export TRANSCRIPTS_DIR=/data/transcripts export EXPORT_INDEX_DIR=/data/export/.export-index export EXPORT_PUBLIC_DIR=/site/public # Materialize-only: consume the host-warmed archive cache (read-only mount), # never re-generate — see compose-site.ts / archiveTranscripts.ts. export ARCHIVES_READONLY=1 mkdir -p /site/out /site/public # .next stays INSIDE the container, and starts empty. It used to be a symlink to # /site/.next, but turbopack's server runtime imports next's own externals (the # icon routes' next/dist/compiled/@vercel/og) from each chunk's REAL path — # under /site, where there is no node_modules — so `next build` died # prerendering /favicon.ico. Nothing measurable is lost: a Turbopack production # build keeps no filesystem cache unless experimental. # turbopackFileSystemCacheForBuild is set (export/next.config.ts does not), and # carrying .next/cache (the TypeScript .tsbuildinfo, the fetch cache) between # runs was measured to save nothing (plans/release-13.md, W3b). rm -rf export/.next # `next build` publishes export/public into out/, so export/public must BE the # public/ compose writes for this site (EXPORT_PUBLIC_DIR=/site/public). It used # to be the copy baked into the image, which shipped as the site's data: none at # all, or whatever site the image's build context had composed last. The export # dir was made writable in the image so this link (and Next's next-env.d.ts) can # be created as an arbitrary runtime uid. # # The repo's tracked assets in public/ — all .svg (Dockerfile.build.dockerignore; # common/publish/buildImage.test.ts fails if one is not) — are synced into the # site's public/ first, on EVERY run, because /site/public persists between runs: # - each is copied over whatever copy is there, so a changed asset is shipped; # - one the repo no longer has is removed, so a dropped asset stops shipping. # Only a name the previous run copied is ever removed — they are listed in # , outside public/ so the list never ships — so nothing compose wrote # is touched. # Nothing but the .svg assets is copied: anything else in an image's public/ # would be data, and not this site's. sync_public_assets() { # local src="$1" dest="$2" list="$3" f name if [ -f "$list" ]; then while IFS= read -r name; do case "$name" in "" | */* | . | ..) continue ;; esac case "$name" in *.svg) ;; *) continue ;; esac # only an asset is ever removed if [ ! -e "$src/$name" ] && [ -f "$dest/$name" ]; then rm -f "$dest/$name"; fi done < "$list" fi : > "$list.new" for f in "$src"/*.svg; do [ -f "$f" ] || continue cp -a "$f" "$dest/" printf '%s\n' "${f##*/}" >> "$list.new" done mv "$list.new" "$list" } sync_public_assets export/public /site/public /site/.tracked-public-assets rm -rf export/public ln -s /site/public export/public echo "[build-site] building site '${SITE_ID}'" # `build site --nodata` = compose:site + next build, WITHOUT the data phase # (already done on the host). compose writes straight into the mounted # /site/public (EXPORT_PUBLIC_DIR). BUILD_ARCHIVES=0 (from `-e`) still reaches # compose through the environment. pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts build site "${SITE_ID}" --nodata # Refuse to hand back a bundle that is not this site's own: its site.json and # corpus.json must both name SITE_ID (common/lib/builtExport.ts, # builtBundleProblem — the check the deploy phase makes again on the host). On a # refusal /site/out keeps whatever it held, and the build fails. BUILT_OUT_DIR="$PWD/export/out" pnpm --filter yt-dlp-transcript-common exec tsx -e ' import("./lib/builtExport.ts").then(({ builtBundleProblem }) => { const problem = builtBundleProblem(process.env.BUILT_OUT_DIR, process.env.SITE_ID); if (problem) { console.error(`[build-site] REFUSED: ${problem} — /site/out is left as it was`); process.exit(1); } console.log(`[build-site] out/ is the bundle of ${process.env.SITE_ID} (site.json, corpus.json)`); }); ' # next build writes export/out as a FRESH real dir (it removes+recreates out, so # a symlink there wouldn't survive) — publish it into the per-site mount. Reached # only when the build succeeded and the bundle is the site's (set -e aborts # otherwise). echo "[build-site] publishing out/ -> /site/out" rm -rf /site/out mkdir -p /site/out cp -a export/out/. /site/out/ echo "[build-site] done -> /site/out ($(find /site/out -type f | wc -l) files)"