#!/bin/sh set -eu APP_DIR="${REACTIVE_RESUME_APP_DIR:-}" SERVER_ENTRY="" ASSETS_DIR="" SSR_DIR="" SERVER_INDEX_FILE="" SSR_FILE="" if [ -z "$APP_DIR" ]; then for candidate in /app/apps/web /app; do if [ -f "$candidate/.output/server/index.mjs" ]; then APP_DIR="$candidate" SERVER_ENTRY="$candidate/.output/server/index.mjs" ASSETS_DIR="$candidate/.output/public/assets" SSR_DIR="$candidate/.output/server/_ssr" SERVER_INDEX_FILE="$SERVER_ENTRY" break fi done fi if [ -z "$SERVER_ENTRY" ]; then index_file="$(find /app -path "*/.output/server/index.mjs" -type f 2>/dev/null | head -n 1 || true)" if [ -n "$index_file" ]; then APP_DIR="${index_file%/.output/server/index.mjs}" SERVER_ENTRY="$index_file" ASSETS_DIR="$APP_DIR/.output/public/assets" SSR_DIR="$APP_DIR/.output/server/_ssr" SERVER_INDEX_FILE="$SERVER_ENTRY" fi fi if [ -z "$SERVER_ENTRY" ] && [ -f /app/apps/server/dist/index.mjs ]; then APP_DIR="/app" SERVER_ENTRY="/app/apps/server/dist/index.mjs" ASSETS_DIR="/app/apps/web/dist/assets" SERVER_INDEX_FILE="$SERVER_ENTRY" SSR_FILE="$SERVER_ENTRY" fi if [ -z "$SERVER_ENTRY" ] || [ ! -f "$SERVER_ENTRY" ]; then echo "Reactive Resume runtime patch skipped: server entry not found under /app" >&2 exit 0 fi printf "%s" "$APP_DIR" > /tmp/reactive-resume-app-dir printf "%s" "$SERVER_ENTRY" > /tmp/reactive-resume-server-entry export APP_DIR ASSETS_DIR SSR_DIR SERVER_INDEX_FILE SSR_FILE SERVER_ENTRY node - <<'NODE' const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const appDir = process.env.APP_DIR || "/app"; const outputDir = path.join(appDir, ".output"); const assetsDir = process.env.ASSETS_DIR || path.join(outputDir, "public/assets"); const ssrDir = process.env.SSR_DIR || ""; const explicitSsrFile = process.env.SSR_FILE || ""; const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs"); const filenameCacheBust = "rr-filename-title-20260520"; const pdfCacheBust = "rr-glalie-layout-20260520"; const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */"; function warn(message) { console.warn(`Reactive Resume runtime patch: ${message}`); } function read(file) { return fs.readFileSync(file, "utf8"); } function write(file, source) { fs.writeFileSync(file, source); } 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) { if (!fs.existsSync(filePath)) return source; const buffer = fs.readFileSync(filePath); const startMarker = `"${urlPath}": {`; const start = source.indexOf(startMarker); if (start === -1) return source; 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) return source; 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 escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function listJsFiles(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir) .filter((name) => name.endsWith(".js")) .map((name) => path.join(dir, name)); } function replaceOnce(source, from, to) { return source.includes(to) ? source : source.replace(from, to); } function replaceRegexOnce(source, regex, to) { return source.includes(to) ? source : source.replace(regex, to); } function patchFilenameBundle(file) { let source = read(file); const replacement = '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}`:""}`}'; if (source.includes(replacement)) return true; const start = source.indexOf("function t("); const end = source.indexOf("function n(", start); if (start === -1 || end === -1) { warn(`filename bundle marker not found in ${path.basename(file)}, skipped`); return false; } source = source.slice(0, start) + replacement + source.slice(end); write(file, source); return true; } function patchSsr(source) { source = source.replace(/\n\t\tname: "",\n\t\tdata: \{/, "\n\t\tname: resume.name,\n\t\tdata: {"); const filenameReplacement = `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 (!source.includes(filenameReplacement)) { const slugifiedPattern = /function generateFilename\(prefix, extension\) \{\s*return `\$\{slugify\(prefix\)\}\$\{extension \? `\.\$\{extension\}` : ""\}`;\s*\}/; if (slugifiedPattern.test(source)) { source = source.replace(slugifiedPattern, filenameReplacement); } else { const start = source.indexOf("function generateFilename("); const end = source.indexOf("\nfunction downloadWithAnchor(", start); if (start !== -1 && end !== -1) { source = source.slice(0, start) + filenameReplacement + source.slice(end); } else { warn("SSR generateFilename marker not found, skipped"); } } } source = source .replace(/const sideMargin = bodyLineHeight \* \.(?:2|08);/, "const sideMargin = bodyLineHeight * .08;") .replace(/metrics\.gapY\(3\.5\)/g, "metrics.gapY(3.0)") .replace(/metrics\.gapY\(2\.6\)/g, "metrics.gapY(3.0)") .replace(/metrics\.gapY\(2\.2\)/g, "metrics.gapY(3.0)") .replace(/style: composeStyles\(styles\.sidebarContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.sidebarContent, { rowGap: metrics.gapY(3.0) }),") .replace(/style: composeStyles\(styles\.mainContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.mainContent, { rowGap: metrics.gapY(3.0) }),") .replace(/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary(?:,\s*paddingBottom: 1(?:\.3)?)?\s*\},/, "sectionHeading: {\n\t\t\t\t\tborderBottomWidth: 1,\n\t\t\t\t\tborderBottomColor: primary,\n\t\t\t\t\tpaddingBottom: 1.3\n\t\t\t\t},"); source = replaceRegexOnce( source, /sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/, "sectionHeading: {\n\t\t\t\t\tborderBottomWidth: 1,\n\t\t\t\t\tborderBottomColor: primary,\n\t\t\t\t\tpaddingBottom: 1.3\n\t\t\t\t},\n\t\t\t\tsectionItems: { paddingTop: metrics.gapY(.55) },\n\t\t\t\titem: { rowGap: metrics.gapY(.2) },", ); source = replaceRegexOnce( source, /sidebarColumn: \{\s*zIndex: 1,\s*backgroundColor: primaryTint,\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical,\s*)?rowGap: (?:metrics\.sectionGap|metrics\.gapY\([^)]+\))\s*\},/, "sidebarColumn: {\n\t\t\t\t\tzIndex: 1,\n\t\t\t\t\tbackgroundColor: primaryTint,\n\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,\n\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,\n\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical,\n\t\t\t\t\trowGap: metrics.gapY(3.0)\n\t\t\t\t},", ); source = replaceRegexOnce( source, /mainContent: \{\s*paddingHorizontal: metrics\.page\.paddingHorizontal,\s*paddingTop: metrics\.page\.paddingVertical,\s*(?:paddingBottom: metrics\.page\.paddingVertical\s*)?\},/, "mainContent: {\n\t\t\t\t\tpaddingHorizontal: metrics.page.paddingHorizontal,\n\t\t\t\t\tpaddingTop: metrics.page.paddingVertical,\n\t\t\t\t\tpaddingBottom: metrics.page.paddingVertical\n\t\t\t\t},", ); return source; } function patchPublicPdf(source) { if (!source.includes("rr-browser-buffer-polyfill")) { const importPrelude = source.match(/^(?:import[^;]+;)+/); const insertAt = importPrelude ? importPrelude[0].length : source.indexOf(";") + 1; if (insertAt > 0 && source.startsWith("import")) { source = source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt); } else { warn("PDF bundle import prelude not found, Buffer shim skipped"); } } source = source .replace(/([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)\*\.(?:2|08);return\{paragraph:\{marginTop:\1,marginBottom:\1\},listItem:\{marginTop:\1,marginBottom:\1\}\}/, "$1=$2*.08;return{paragraph:{marginTop:$1,marginBottom:$1},listItem:{marginTop:$1,marginBottom:$1}}") .replace(/[oc]\.gapY\(3\.5\)/g, (m) => `${m[0]}.gapY(3.0)`) .replace(/[oc]\.gapY\(2\.6\)/g, (m) => `${m[0]}.gapY(3.0)`) .replace(/[oc]\.gapY\(2\.2\)/g, (m) => `${m[0]}.gapY(3.0)`) .replace(/style:\$\(a\.sidebarContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.sidebarContent,{rowGap:o.gapY(3.0)})") .replace(/style:\$\(a\.mainContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.mainContent,{rowGap:o.gapY(3.0)})") .replace(/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a(?:,paddingBottom:1(?:\.3)?)?\}/, "sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3}") .replace(/sectionHeading:\{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1(?:\.3)?\},item:\{rowGap:([a-zA-Z_$][\w$]*)\.gapY\(\.125\)\}/, "sectionHeading:{borderBottomWidth:1,borderBottomColor:a,paddingBottom:1.3},sectionItems:{paddingTop:$1.gapY(.55)},item:{rowGap:$1.gapY(.2)}"); source = replaceOnce(source, "sidebarColumn:{zIndex:1,backgroundColor:o,paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,rowGap:c.sectionGap}", "sidebarColumn:{zIndex:1,backgroundColor:o,paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,paddingBottom:c.page.paddingVertical,rowGap:c.gapY(3.0)}"); source = replaceOnce(source, "mainContent:{paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical}", "mainContent:{paddingHorizontal:c.page.paddingHorizontal,paddingTop:c.page.paddingVertical,paddingBottom:c.page.paddingVertical}"); return source; } const assetFiles = listJsFiles(assetsDir); const filenameFiles = assetFiles .filter((file) => { const source = read(file); return source.includes("URL.createObjectURL") && source.includes(".download"); }) .sort((a, b) => fs.statSync(a).size - fs.statSync(b).size); const patchedFilenameFiles = []; for (const file of filenameFiles) { if (patchFilenameBundle(file)) patchedFilenameFiles.push(file); } if (patchedFilenameFiles.length === 0) warn("no filename bundle patched"); let ssrFile = ""; if (explicitSsrFile && fs.existsSync(explicitSsrFile) && read(explicitSsrFile).includes("function generateFilename(")) { ssrFile = explicitSsrFile; } else if (ssrDir && fs.existsSync(ssrDir)) { ssrFile = fs.readdirSync(ssrDir) .filter((name) => name.endsWith(".mjs")) .map((name) => path.join(ssrDir, name)) .find((file) => read(file).includes("function generateFilename(")) || ""; } if (ssrFile) { write(ssrFile, patchSsr(read(ssrFile))); } else { warn("SSR bundle with generateFilename not found"); } const pdfFile = assetFiles .filter((file) => path.basename(file).startsWith("pdf-document-")) .sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || ""; if (pdfFile) { write(pdfFile, patchPublicPdf(read(pdfFile))); } else { warn("public PDF bundle not found"); } const filenameBases = patchedFilenameFiles.map((file) => path.basename(file)); const pdfBase = pdfFile ? path.basename(pdfFile) : ""; const touchedImporters = []; for (const file of assetFiles) { let source = read(file); let next = source; for (const base of filenameBases) { next = next.replace(new RegExp(`\\./${escapeRegex(base)}(?:\\?v=rr-filename-[A-Za-z0-9-]+)?`, "g"), `./${base}?v=${filenameCacheBust}`); } if (pdfBase) { next = next.replace(new RegExp(`\\./${escapeRegex(pdfBase)}(?:\\?v=rr-[^"'\\\`]+)?`, "g"), `./${pdfBase}?v=${pdfCacheBust}`); } if (next !== source) { write(file, next); touchedImporters.push(file); } } if (fs.existsSync(serverIndexFile)) { let serverIndex = read(serverIndexFile); for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters].filter(Boolean)) { serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file); } write(serverIndexFile, serverIndex); } for (const file of [...patchedFilenameFiles, pdfFile, ssrFile, serverIndexFile].filter(Boolean)) { try { new Function(read(file)); } catch { // ESM/import bundles are validated by Node at application startup; keep this // best-effort so a harmless syntax-check limitation never blocks boot. } } NODE