#!/bin/sh set -eu CONTAINER="${1:-reactive-resume-reactive-resume-1}" docker exec -u root -i "$CONTAINER" sh <<'SH' set -eu PUBLIC_FILE="/app/apps/web/.output/public/assets/file-D5WsIgJH.js" SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs" SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs" CACHE_BUST="rr-filename-title-20260519" cp "$PUBLIC_FILE" "$PUBLIC_FILE.bak-filename" 2>/dev/null || true cp "$SSR_FILE" "$SSR_FILE.bak-filename" 2>/dev/null || true cp "$SERVER_INDEX_FILE" "$SERVER_INDEX_FILE.bak-filename" 2>/dev/null || true node - <<'NODE' const fs = require('fs'); const crypto = require('crypto'); const publicFile = '/app/apps/web/.output/public/assets/file-D5WsIgJH.js'; const ssrFile = '/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs'; const serverIndexFile = '/app/apps/web/.output/server/index.mjs'; const cacheBust = 'rr-filename-title-20260519'; function makeEtag(buffer) { const digest = crypto.createHash('sha1').update(buffer).digest('base64').replace(/=+$/g, ''); return `"${buffer.length.toString(16)}-${digest}"`; } function patchStaticManifestEntry(source, urlPath, filePath) { const buffer = fs.readFileSync(filePath); const startMarker = `"${urlPath}": {`; const start = source.indexOf(startMarker); if (start === -1) throw new Error(`Static manifest entry not found for ${urlPath}`); const commaEnd = source.indexOf('\n\t},', start); const objectEnd = source.indexOf('\n\t}', start); const end = commaEnd === -1 ? objectEnd : Math.min(commaEnd, objectEnd); if (end === -1) throw new Error(`Static manifest entry end not found for ${urlPath}`); let entry = source.slice(start, end); entry = entry .replace(/"etag": "(?:\\.|[^"\\])*"/, `"etag": ${JSON.stringify(makeEtag(buffer))}`) .replace(/"mtime": "(?:\\.|[^"\\])*"/, `"mtime": ${JSON.stringify(new Date().toISOString())}`) .replace(/"size": \d+/, `"size": ${buffer.length}`); return source.slice(0, start) + entry + source.slice(end); } function patchPublicImporters() { const assetsDir = '/app/apps/web/.output/public/assets'; const files = fs .readdirSync(assetsDir) .filter((name) => name.endsWith('.js')) .map((name) => `${assetsDir}/${name}`) .filter((file) => fs.readFileSync(file, 'utf8').includes('file-D5WsIgJH.js')); for (const file of files) { let source = fs.readFileSync(file, 'utf8'); source = source.replace(/\.\/file-D5WsIgJH\.js(?:\?v=rr-filename-[A-Za-z0-9-]+)?/g, `./file-D5WsIgJH.js?v=${cacheBust}`); fs.writeFileSync(file, source); } return files; } const publicFilenameFunction = 'function t(e,t){let n=(e||"resume").toString().trim()||"resume";return n=n.replace(/[\\\\/:*?"<>|]/g,"-").replace(/\\s+/g," ").replace(/\\.+$/,"").trim()||"resume",t&&n.toLowerCase().endsWith("."+t.toLowerCase())?n:`${n}${t?`.${t}`:""}`}'; let publicJs = fs.readFileSync(publicFile, 'utf8'); const publicReplacement = publicFilenameFunction; if (!publicJs.includes(publicReplacement)) { const start = publicJs.indexOf('function t('); const end = publicJs.indexOf('function n(', start); if (start === -1 || end === -1) throw new Error('Public generateFilename marker not found'); publicJs = publicJs.slice(0, start) + publicReplacement + publicJs.slice(end); } fs.writeFileSync(publicFile, publicJs); let ssr = fs.readFileSync(ssrFile, 'utf8'); ssr = ssr.replace( /\n\t\tname: "",\n\t\tdata: \{/, '\n\t\tname: resume.name,\n\t\tdata: {', ); const ssrReplacement = `function generateFilename(prefix, extension) {\n\tlet filename = (prefix || "resume").toString().trim() || "resume";\n\tfilename = filename.replace(/[\\\\/:*?"<>|]/g, "-").replace(/\\s+/g, " ").replace(/\\.+$/, "").trim() || "resume";\n\treturn extension && filename.toLowerCase().endsWith(\`.\${extension.toLowerCase()}\`) ? filename : \`\${filename}\${extension ? \`.\${extension}\` : ""}\`;\n}`; if (!ssr.includes(ssrReplacement)) { const start = ssr.indexOf('function generateFilename('); const end = ssr.indexOf('\nfunction downloadWithAnchor(', start); if (start === -1 || end === -1) throw new Error('SSR generateFilename marker not found'); ssr = ssr.slice(0, start) + ssrReplacement + ssr.slice(end); } fs.writeFileSync(ssrFile, ssr); const importers = patchPublicImporters(); let serverIndex = fs.readFileSync(serverIndexFile, 'utf8'); serverIndex = patchStaticManifestEntry(serverIndex, '/assets/file-D5WsIgJH.js', publicFile); for (const file of importers) { const urlPath = `/assets/${file.split('/').pop()}`; serverIndex = patchStaticManifestEntry(serverIndex, urlPath, file); } fs.writeFileSync(serverIndexFile, serverIndex); NODE node --check "$PUBLIC_FILE" >/dev/null node --check "$SSR_FILE" >/dev/null node --check "$SERVER_INDEX_FILE" >/dev/null SH # Nitro loads the static asset manifest into memory at process startup. Restart # after patching so updated content-length/etag values are used immediately. docker restart "$CONTAINER" >/dev/null for _ in $(seq 1 60); do health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$CONTAINER")" [ "$health" = "healthy" ] && exit 0 sleep 2 done docker logs --tail 80 "$CONTAINER" >&2 exit 1