Files
Reactive_Resume/packages/reactive-resume-personal-qnap-nas/patches/reactive-resume-runtime-patch.sh
2026-05-20 01:57:22 +08:00

288 lines
13 KiB
Bash

#!/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-20260520b";
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) {
if (!/^file-[A-Za-z0-9_-]+\.js$/.test(path.basename(file))) {
return false;
}
let source = read(file);
const replacement = 'function t(e,t){let n=(e??"resume").toString().trim()||"resume",r=t==null?"":String(t).trim().replace(/^\\./,"");return n=n.replace(/[\\\\/:*?"<>|]/g,"-").replace(/\\s+/g," ").replace(/\\.+$/,"").trim()||"resume",r&&n.toLowerCase().endsWith("."+r.toLowerCase())?n:`${n}${r?`.${r}`:""}`}';
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\tlet ext = extension == null ? "" : String(extension).trim().replace(/^\\./, "");\n\tfilename = filename.replace(/[\\\\/:*?"<>|]/g, "-").replace(/\\s+/g, " ").replace(/\\.+$/, "").trim() || "resume";\n\treturn ext && filename.toLowerCase().endsWith(\`.\${ext.toLowerCase()}\`) ? filename : \`\${filename}\${ext ? \`.\${ext}\` : ""}\`;\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) => {
if (!/^file-[A-Za-z0-9_-]+\.js$/.test(path.basename(file))) return false;
const source = read(file);
return source.includes("URL.createObjectURL") && source.includes(".download") && source.includes("revokeObjectURL");
})
.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