Compare commits
7 Commits
v2026.05.2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 93aa73ab5e | |||
| 35cd438018 | |||
| 03c56a20db | |||
| 5c5d21bf43 | |||
| 2141afd3eb | |||
| 944a852470 | |||
| 009631bd43 |
@@ -18,9 +18,9 @@
|
|||||||
|
|
||||||
## 安装包
|
## 安装包
|
||||||
|
|
||||||
- `reactive-resume-clean-install-20260519`: 纯净迁移模板,域名、端口、FRP、密钥均为待填写占位。
|
- `reactive-resume-clean-install-20260520`: 纯净迁移模板,域名、端口、FRP、密钥均为待填写占位。
|
||||||
- `reactive-resume-personal-direct-20260519`: 服务器直接运行版,预置 `https://isiseg.huijutec.cn`、FRP `remotePort = 10004`,并内置当前简历与上传图片。
|
- `reactive-resume-personal-direct-20260520`: 服务器直接运行版,预置 `https://me.huijutec.cn`、FRP `remotePort = 10003`,并内置当前简历与上传图片。
|
||||||
- `reactive-resume-personal-qnap-nas-20260519`: 威联通 QNAP NAS 直接部署版,预置 `/share/Container/Reactive_Resume_Personal`、本地端口 `3004`、FRP `10004`,并内置当前简历与上传图片。
|
- `reactive-resume-personal-qnap-nas-20260520`: 威联通 QNAP NAS 直接部署版,预置 `/share/Container/reactive_resume`、本地端口 `3003`、FRP `10003`,并内置当前简历与上传图片。
|
||||||
|
|
||||||
## 启动
|
## 启动
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
reactive_resume_app:
|
reactive_resume_app:
|
||||||
image: amruthpillai/reactive-resume:latest
|
image: amruthpillai/reactive-resume@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
reactive_resume_permissions:
|
reactive_resume_permissions:
|
||||||
@@ -97,7 +97,7 @@ services:
|
|||||||
# QTS/Container Station 版默认启动 frpc。
|
# QTS/Container Station 版默认启动 frpc。
|
||||||
# 为避免 QTS 把 frpc 配置文件挂载路径改到应用临时目录,这里不挂载 frpc.yaml,
|
# 为避免 QTS 把 frpc 配置文件挂载路径改到应用临时目录,这里不挂载 frpc.yaml,
|
||||||
# 而是在容器启动时自动写入 /tmp/frpc.toml。
|
# 而是在容器启动时自动写入 /tmp/frpc.toml。
|
||||||
image: snowdreamtech/frpc:latest
|
image: fatedier/frpc:v0.68.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: ["/bin/sh"]
|
entrypoint: ["/bin/sh"]
|
||||||
command:
|
command:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
reactive-resume:
|
reactive-resume:
|
||||||
image: amruthpillai/reactive-resume:latest
|
image: amruthpillai/reactive-resume@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
9
dist/SHA256SUMS
vendored
9
dist/SHA256SUMS
vendored
@@ -1,6 +1,3 @@
|
|||||||
f9daa11eeb735e1920d822094a7caf3f7eebcccf8467755bb6f904c68a53bdbf reactive-resume-clean-install-20260519.tar.gz
|
1a1571bbed2c59c0003daa3c4c41cda7464b03cb8c4706bd7bf507e8abfaa7ea reactive-resume-clean-install-20260520.zip
|
||||||
18b247b33feaf9ccc4ce7516fb76ea3523c578e3099c77e7549bdfa2c68fd658 reactive-resume-personal-direct-20260519.tar.gz
|
34764a874e5477e439fa2860ef311e42264e90575c5a89e3f1651748708ef4df reactive-resume-personal-direct-20260520.zip
|
||||||
d2718004a8a0592f38096f426ef307c9df99b2f090dac733fb6c0f412d2e4916 reactive-resume-personal-qnap-nas-20260519.tar.gz
|
933541058567f73b6a0d9521863a0d592da6d0a546fd1c38587aeaa0f11cf61d reactive-resume-personal-qnap-nas-20260520.zip
|
||||||
54bf5114e8ca9f29ae1feb0510f738396f354174f691bf96e863b41058742a58 reactive-resume-clean-install-20260519.zip
|
|
||||||
b7cdb405f24ff7653aac9d21f3bd347884e377c8a993d8bb1b7d9d77295425d2 reactive-resume-personal-direct-20260519.zip
|
|
||||||
d847a76e3cac28f9e973e21ba7bcbac89ccc940345694b3a3483cde9eed01ee0 reactive-resume-personal-qnap-nas-20260519.zip
|
|
||||||
|
|||||||
6
dist/SHA256SUMS-20260520
vendored
6
dist/SHA256SUMS-20260520
vendored
@@ -1,3 +1,3 @@
|
|||||||
fa3b2b64a9afd7af60f57cfda8431af4e171cc1cdba4a6a2b89d50000a574f54 reactive-resume-clean-install-20260520.zip
|
1a1571bbed2c59c0003daa3c4c41cda7464b03cb8c4706bd7bf507e8abfaa7ea reactive-resume-clean-install-20260520.zip
|
||||||
4e95c039777ae2af6a216528ebd97911f59aa8a80c7816acde7ec8424eb6e59d reactive-resume-personal-direct-20260520.zip
|
34764a874e5477e439fa2860ef311e42264e90575c5a89e3f1651748708ef4df reactive-resume-personal-direct-20260520.zip
|
||||||
708209d2ea066633ebd099dfe6554e41681be114f124e2656fc1233496aad534 reactive-resume-personal-qnap-nas-20260520.zip
|
933541058567f73b6a0d9521863a0d592da6d0a546fd1c38587aeaa0f11cf61d reactive-resume-personal-qnap-nas-20260520.zip
|
||||||
|
|||||||
BIN
dist/reactive-resume-clean-install-20260520.zip
vendored
BIN
dist/reactive-resume-clean-install-20260520.zip
vendored
Binary file not shown.
BIN
dist/reactive-resume-personal-direct-20260520.zip
vendored
BIN
dist/reactive-resume-personal-direct-20260520.zip
vendored
Binary file not shown.
BIN
dist/reactive-resume-personal-qnap-nas-20260520.zip
vendored
BIN
dist/reactive-resume-personal-qnap-nas-20260520.zip
vendored
Binary file not shown.
@@ -34,7 +34,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
reactive_resume_app:
|
reactive_resume_app:
|
||||||
image: amruthpillai/reactive-resume:latest
|
image: amruthpillai/reactive-resume@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
reactive_resume_permissions:
|
reactive_resume_permissions:
|
||||||
@@ -68,7 +68,7 @@ services:
|
|||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
reactive_resume_frpc:
|
reactive_resume_frpc:
|
||||||
image: snowdreamtech/frpc:latest
|
image: fatedier/frpc:v0.68.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: ["/bin/sh"]
|
entrypoint: ["/bin/sh"]
|
||||||
command:
|
command:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
reactive-resume:
|
reactive-resume:
|
||||||
image: amruthpillai/reactive-resume:latest
|
image: amruthpillai/reactive-resume@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
TZ=Asia/Shanghai
|
TZ=Asia/Shanghai
|
||||||
APP_URL=https://isiseg.huijutec.cn
|
APP_URL=https://me.huijutec.cn
|
||||||
|
|
||||||
# Local debug access only: http://127.0.0.1:3004
|
# Local debug access only: http://127.0.0.1:3004
|
||||||
LOCAL_BIND_IP=127.0.0.1
|
LOCAL_BIND_IP=127.0.0.1
|
||||||
@@ -33,7 +33,7 @@ SMTP_HOST=
|
|||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_USER=
|
SMTP_USER=
|
||||||
SMTP_PASS=
|
SMTP_PASS=
|
||||||
SMTP_FROM=Reactive Resume <noreply@isiseg.huijutec.cn>
|
SMTP_FROM=Reactive Resume <noreply@me.huijutec.cn>
|
||||||
SMTP_SECURE=false
|
SMTP_SECURE=false
|
||||||
S3_ACCESS_KEY_ID=
|
S3_ACCESS_KEY_ID=
|
||||||
S3_SECRET_ACCESS_KEY=
|
S3_SECRET_ACCESS_KEY=
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Reactive Resume 个人简历直接运行安装包
|
# Reactive Resume 个人简历直接运行安装包
|
||||||
|
|
||||||
这套包已经按 `https://isiseg.huijutec.cn` 和 FRP `remotePort = 10004` 预置,可以在当前服务器上直接运行。包内已包含当前简历初始化数据、头像和作品集图片,首次启动后可直接访问公开简历。
|
这套包已经按 `https://me.huijutec.cn` 和 FRP `remotePort = 10003` 预置,可以在当前服务器上直接运行。包内已包含当前简历初始化数据、头像和作品集图片,首次启动后可直接访问公开简历。
|
||||||
|
|
||||||
## 启动
|
## 启动
|
||||||
|
|
||||||
@@ -11,18 +11,18 @@ docker compose -f compose.yml up -d
|
|||||||
启动后:
|
启动后:
|
||||||
|
|
||||||
- 本机调试地址:`http://127.0.0.1:3004`
|
- 本机调试地址:`http://127.0.0.1:3004`
|
||||||
- 公网访问地址:`https://isiseg.huijutec.cn`
|
- 公网访问地址:`https://me.huijutec.cn`
|
||||||
- 当前公开简历:`https://isiseg.huijutec.cn/audience/resume`
|
- 当前公开简历:`https://me.huijutec.cn/audience/resume`
|
||||||
- FRP 映射:本地 `reactive-resume:3000` -> 公网服务器 `10004`
|
- FRP 映射:本地 `reactive-resume:3000` -> 公网服务器 `10003`
|
||||||
|
|
||||||
## 反向代理要求
|
## 反向代理要求
|
||||||
|
|
||||||
公网服务器上的 Nginx Proxy Manager / 反向代理应配置:
|
公网服务器上的 Nginx Proxy Manager / 反向代理应配置:
|
||||||
|
|
||||||
- Domain Names:`isiseg.huijutec.cn`
|
- Domain Names:`me.huijutec.cn`
|
||||||
- Scheme:`http`
|
- Scheme:`http`
|
||||||
- Forward Hostname / IP:`82.157.255.195`
|
- Forward Hostname / IP:`82.157.255.195`
|
||||||
- Forward Port:`10004`
|
- Forward Port:`10003`
|
||||||
- Websockets Support:开启
|
- Websockets Support:开启
|
||||||
- SSL:按现有 huijutec.cn 域名策略配置
|
- SSL:按现有 huijutec.cn 域名策略配置
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,9 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
reactive-resume:
|
reactive-resume:
|
||||||
image: amruthpillai/reactive-resume:latest
|
image: amruthpillai/reactive-resume@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
entrypoint: ["/bin/sh", "/opt/reactive-resume-patches/reactive-resume-entrypoint.sh"]
|
||||||
command:
|
|
||||||
- |
|
|
||||||
sh /opt/reactive-resume-patches/reactive-resume-runtime-patch.sh
|
|
||||||
exec node .output/server/index.mjs
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
@@ -33,6 +29,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- reactive_resume_data:/app/data
|
- reactive_resume_data:/app/data
|
||||||
- ./patches/reactive-resume-runtime-patch.sh:/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh:ro
|
- ./patches/reactive-resume-runtime-patch.sh:/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh:ro
|
||||||
|
- ./patches/reactive-resume-entrypoint.sh:/opt/reactive-resume-patches/reactive-resume-entrypoint.sh:ro
|
||||||
networks:
|
networks:
|
||||||
- resume_net
|
- resume_net
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ proxies:
|
|||||||
type: "tcp"
|
type: "tcp"
|
||||||
localIP: "reactive-resume"
|
localIP: "reactive-resume"
|
||||||
localPort: 3000
|
localPort: 3000
|
||||||
remotePort: 10004
|
remotePort: 10003
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PATCH_SCRIPT="/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh"
|
||||||
|
|
||||||
|
if [ -f "$PATCH_SCRIPT" ]; then
|
||||||
|
sh "$PATCH_SCRIPT" || echo "Reactive Resume runtime patch failed, continuing with the image default startup" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
if [ -f /app/apps/server/dist/index.mjs ]; then
|
||||||
|
cd /app
|
||||||
|
set -- node apps/server/dist/index.mjs
|
||||||
|
elif [ -f /app/apps/web/.output/server/index.mjs ]; then
|
||||||
|
cd /app/apps/web
|
||||||
|
set -- node .output/server/index.mjs
|
||||||
|
else
|
||||||
|
server_entry="$(cat /tmp/reactive-resume-server-entry 2>/dev/null || true)"
|
||||||
|
if [ -n "$server_entry" ] && [ -f "$server_entry" ]; then
|
||||||
|
cd "$(dirname "$server_entry")"
|
||||||
|
set -- node "$(basename "$server_entry")"
|
||||||
|
else
|
||||||
|
echo "Reactive Resume startup failed: no known server entry found" >&2
|
||||||
|
find /app -maxdepth 5 \( -name index.mjs -o -name server.js -o -name main.js \) 2>/dev/null | head -50 >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v docker-entrypoint.sh >/dev/null 2>&1; then
|
||||||
|
exec docker-entrypoint.sh "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -1,24 +1,94 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"
|
APP_DIR="${REACTIVE_RESUME_APP_DIR:-}"
|
||||||
PUBLIC_FILENAME_FILE="/app/apps/web/.output/public/assets/file-D5WsIgJH.js"
|
SERVER_ENTRY=""
|
||||||
PUBLIC_PDF_FILE="/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js"
|
ASSETS_DIR=""
|
||||||
SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs"
|
SSR_DIR=""
|
||||||
CACHE_BUST_FILENAME="rr-filename-title-20260520"
|
SERVER_INDEX_FILE=""
|
||||||
CACHE_BUST_PDF="rr-glalie-layout-20260520"
|
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'
|
node - <<'NODE'
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const ssrFile = "/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs";
|
const appDir = process.env.APP_DIR || "/app";
|
||||||
const publicFilenameFile = "/app/apps/web/.output/public/assets/file-D5WsIgJH.js";
|
const outputDir = path.join(appDir, ".output");
|
||||||
const publicPdfFile = "/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js";
|
const assetsDir = process.env.ASSETS_DIR || path.join(outputDir, "public/assets");
|
||||||
const serverIndexFile = "/app/apps/web/.output/server/index.mjs";
|
const ssrDir = process.env.SSR_DIR || "";
|
||||||
const filenameCacheBust = "rr-filename-title-20260520";
|
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 pdfCacheBust = "rr-glalie-layout-20260520";
|
||||||
|
const appShellSuffix = "rr20260520c";
|
||||||
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
|
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
|
||||||
|
const serviceWorkerCleanup = `/* Reactive Resume personal deployment: disable stale PWA caches. */
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key)))));
|
||||||
|
});
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil((async () => {
|
||||||
|
await caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key))));
|
||||||
|
await self.clients.claim();
|
||||||
|
})());
|
||||||
|
});
|
||||||
|
self.addEventListener("fetch", () => {});
|
||||||
|
`;
|
||||||
|
|
||||||
|
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) {
|
function makeEtag(buffer) {
|
||||||
const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, "");
|
const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, "");
|
||||||
@@ -44,6 +114,31 @@ function patchStaticManifestEntry(source, urlPath, filePath) {
|
|||||||
return source.slice(0, start) + entry + source.slice(end);
|
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 listFilesRecursive(dir, predicate) {
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
const result = [];
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const file = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
result.push(...listFilesRecursive(file, predicate));
|
||||||
|
} else if (predicate(file)) {
|
||||||
|
result.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function replaceOnce(source, from, to) {
|
function replaceOnce(source, from, to) {
|
||||||
return source.includes(to) ? source : source.replace(from, to);
|
return source.includes(to) ? source : source.replace(from, to);
|
||||||
}
|
}
|
||||||
@@ -52,26 +147,43 @@ function replaceRegexOnce(source, regex, to) {
|
|||||||
return source.includes(to) ? source : source.replace(regex, to);
|
return source.includes(to) ? source : source.replace(regex, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchFilenameBundle() {
|
function patchFilenameBundle(file) {
|
||||||
let source = fs.readFileSync(publicFilenameFile, "utf8");
|
if (!/^file-[A-Za-z0-9_-]+\.js$/.test(path.basename(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}`:""}`}';
|
return false;
|
||||||
if (!source.includes(replacement)) {
|
}
|
||||||
|
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 start = source.indexOf("function t(");
|
||||||
const end = source.indexOf("function n(", start);
|
const end = source.indexOf("function n(", start);
|
||||||
if (start === -1 || end === -1) throw new Error("filename function marker not found");
|
if (start === -1 || end === -1) {
|
||||||
source = source.slice(0, start) + replacement + source.slice(end);
|
warn(`filename bundle marker not found in ${path.basename(file)}, skipped`);
|
||||||
fs.writeFileSync(publicFilenameFile, source);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
source = source.slice(0, start) + replacement + source.slice(end);
|
||||||
|
write(file, source);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchSsr(source) {
|
function patchSsr(source) {
|
||||||
source = source.replace(/\n\t\tname: "",\n\t\tdata: \{/, "\n\t\tname: resume.name,\n\t\tdata: {");
|
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}`;
|
|
||||||
|
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)) {
|
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 start = source.indexOf("function generateFilename(");
|
||||||
const end = source.indexOf("\nfunction downloadWithAnchor(", start);
|
const end = source.indexOf("\nfunction downloadWithAnchor(", start);
|
||||||
if (start === -1 || end === -1) throw new Error("SSR generateFilename marker not found");
|
if (start !== -1 && end !== -1) {
|
||||||
source = source.slice(0, start) + filenameReplacement + source.slice(end);
|
source = source.slice(0, start) + filenameReplacement + source.slice(end);
|
||||||
|
} else {
|
||||||
|
warn("SSR generateFilename marker not found, skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
source = source
|
source = source
|
||||||
@@ -82,6 +194,7 @@ function patchSsr(source) {
|
|||||||
.replace(/style: composeStyles\(styles\.sidebarContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.sidebarContent, { rowGap: 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(/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},");
|
.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 = replaceRegexOnce(
|
||||||
source,
|
source,
|
||||||
/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/,
|
/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/,
|
||||||
@@ -100,15 +213,90 @@ function patchSsr(source) {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBrowserBufferPolyfill(source) {
|
function patchAppManifest(file, oldBase, newBase) {
|
||||||
if (source.includes("rr-browser-buffer-polyfill")) return source;
|
if (!fs.existsSync(file)) return false;
|
||||||
const insertAt = source.indexOf(";") + 1;
|
let source = read(file);
|
||||||
if (insertAt <= 0 || !source.startsWith("import")) throw new Error("PDF bundle import prelude not found");
|
const next = source.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
|
||||||
return source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt);
|
if (next !== source) {
|
||||||
|
write(file, next);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneStaticManifestEntry(source, oldUrl, newUrl, oldBase, newBase, newFile) {
|
||||||
|
const marker = `\t"${oldUrl}": {`;
|
||||||
|
const start = source.indexOf(marker);
|
||||||
|
if (start === -1) {
|
||||||
|
warn(`static manifest entry not found for ${oldUrl}, skipped app shell cache bust`);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.includes(`\t"${newUrl}": {`)) {
|
||||||
|
return patchStaticManifestEntry(source, newUrl, newFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = source.indexOf("\n\t}", start);
|
||||||
|
if (close === -1 || source[close + 3] !== ",") {
|
||||||
|
warn(`static manifest entry close marker not found for ${oldUrl}, skipped app shell cache bust`);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = source.slice(start, close + 3);
|
||||||
|
const buffer = fs.readFileSync(newFile);
|
||||||
|
entry = entry
|
||||||
|
.replace(oldUrl, newUrl)
|
||||||
|
.replace(`../public/assets/${oldBase}`, `../public/assets/${newBase}`)
|
||||||
|
.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, close + 4) + "\n" + entry + "," + source.slice(close + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchAppShellEntry(assetFiles) {
|
||||||
|
const indexFile = assetFiles.find((file) => /^index-[A-Za-z0-9_-]+\.js$/.test(path.basename(file)));
|
||||||
|
if (!indexFile) {
|
||||||
|
warn("index app shell bundle not found, skipped app shell cache bust");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldBase = path.basename(indexFile);
|
||||||
|
const newBase = oldBase.replace(/\.js$/, `-${appShellSuffix}.js`);
|
||||||
|
const newFile = path.join(path.dirname(indexFile), newBase);
|
||||||
|
if (!fs.existsSync(newFile)) {
|
||||||
|
fs.copyFileSync(indexFile, newFile);
|
||||||
|
}
|
||||||
|
return { oldBase, newBase, newFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchAppShellImporters(assetFiles, oldBase, newBase) {
|
||||||
|
const touched = [];
|
||||||
|
for (const file of assetFiles) {
|
||||||
|
if (path.basename(file) === oldBase) continue;
|
||||||
|
let source = read(file);
|
||||||
|
let next = source
|
||||||
|
.replace(new RegExp(`\\./${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `./${newBase}`)
|
||||||
|
.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
|
||||||
|
if (next !== source) {
|
||||||
|
write(file, next);
|
||||||
|
touched.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return touched;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchPublicPdf(source) {
|
function patchPublicPdf(source) {
|
||||||
source = ensureBrowserBufferPolyfill(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
|
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(/([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\(3\.5\)/g, (m) => `${m[0]}.gapY(3.0)`)
|
||||||
@@ -118,43 +306,108 @@ function patchPublicPdf(source) {
|
|||||||
.replace(/style:\$\(a\.mainContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.mainContent,{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)?)?\}/, "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)}");
|
.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, "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}");
|
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;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchImporters() {
|
const assetFiles = listJsFiles(assetsDir);
|
||||||
const assetsDir = "/app/apps/web/.output/public/assets";
|
const filenameFiles = assetFiles
|
||||||
const files = fs.readdirSync(assetsDir).filter((name) => name.endsWith(".js")).map((name) => `${assetsDir}/${name}`);
|
.filter((file) => {
|
||||||
const touched = [];
|
if (!/^file-[A-Za-z0-9_-]+\.js$/.test(path.basename(file))) return false;
|
||||||
for (const file of files) {
|
const source = read(file);
|
||||||
let source = fs.readFileSync(file, "utf8");
|
return source.includes("URL.createObjectURL") && source.includes(".download") && source.includes("revokeObjectURL");
|
||||||
let next = source
|
})
|
||||||
.replace(/\.\/file-D5WsIgJH\.js(?:\?v=rr-filename-[A-Za-z0-9-]+)?/g, `./file-D5WsIgJH.js?v=${filenameCacheBust}`)
|
.sort((a, b) => fs.statSync(a).size - fs.statSync(b).size);
|
||||||
.replace(/\.\/pdf-document-BplbXx-0\.js(?:\?v=rr-[^"'`]+)?/g, `./pdf-document-BplbXx-0.js?v=${pdfCacheBust}`);
|
|
||||||
|
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 appShell = patchAppShellEntry(assetFiles);
|
||||||
|
const appShellTouchedImporters = appShell ? patchAppShellImporters(assetFiles, appShell.oldBase, appShell.newBase) : [];
|
||||||
|
if (appShell) {
|
||||||
|
const appManifestFiles = [
|
||||||
|
...listFilesRecursive(path.join(outputDir, "server"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
|
||||||
|
...listFilesRecursive(path.join(appDir, "apps/server/dist"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
|
||||||
|
];
|
||||||
|
for (const file of appManifestFiles) patchAppManifest(file, appShell.oldBase, appShell.newBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (next !== source) {
|
||||||
fs.writeFileSync(file, next);
|
write(file, next);
|
||||||
touched.push(file);
|
touchedImporters.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return touched;
|
|
||||||
}
|
|
||||||
|
|
||||||
patchFilenameBundle();
|
if (fs.existsSync(serverIndexFile)) {
|
||||||
fs.writeFileSync(ssrFile, patchSsr(fs.readFileSync(ssrFile, "utf8")));
|
let serverIndex = read(serverIndexFile);
|
||||||
fs.writeFileSync(publicPdfFile, patchPublicPdf(fs.readFileSync(publicPdfFile, "utf8")));
|
for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters, ...appShellTouchedImporters].filter(Boolean)) {
|
||||||
const importers = patchImporters();
|
serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file);
|
||||||
|
}
|
||||||
let serverIndex = fs.readFileSync(serverIndexFile, "utf8");
|
if (appShell) {
|
||||||
serverIndex = patchStaticManifestEntry(serverIndex, "/assets/file-D5WsIgJH.js", publicFilenameFile);
|
serverIndex = cloneStaticManifestEntry(
|
||||||
serverIndex = patchStaticManifestEntry(serverIndex, "/assets/pdf-document-BplbXx-0.js", publicPdfFile);
|
serverIndex,
|
||||||
for (const file of importers) {
|
`/assets/${appShell.oldBase}`,
|
||||||
serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${file.split("/").pop()}`, file);
|
`/assets/${appShell.newBase}`,
|
||||||
|
appShell.oldBase,
|
||||||
|
appShell.newBase,
|
||||||
|
appShell.newFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const serviceWorkerFile = path.join(path.dirname(assetsDir), "sw.js");
|
||||||
|
if (fs.existsSync(serviceWorkerFile)) {
|
||||||
|
write(serviceWorkerFile, serviceWorkerCleanup);
|
||||||
|
serverIndex = patchStaticManifestEntry(serverIndex, "/sw.js", serviceWorkerFile);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(serverIndexFile, serverIndex);
|
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
node --check "$SSR_FILE" >/dev/null
|
|
||||||
node --check "$PUBLIC_FILENAME_FILE" >/dev/null
|
|
||||||
node --check "$PUBLIC_PDF_FILE" >/dev/null
|
|
||||||
node --check "$SERVER_INDEX_FILE" >/dev/null
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"email": "zub572701190@stu.xjtu.edu.cn",
|
"email": "zub572701190@stu.xjtu.edu.cn",
|
||||||
"phone": "+86 139-4611-2059",
|
"phone": "+86 139-4611-2059",
|
||||||
"website": {
|
"website": {
|
||||||
"url": "https://isiseg.huijutec.cn/audience/resume",
|
"url": "https://me.huijutec.cn/audience/resume",
|
||||||
"label": "isiseg.huijutec.cn/audience/resume"
|
"label": "me.huijutec.cn/audience/resume"
|
||||||
},
|
},
|
||||||
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
||||||
"location": "陕西西安|西安交通大学",
|
"location": "陕西西安|西安交通大学",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- Reactive Resume personal seed data.
|
-- Reactive Resume personal seed data.
|
||||||
-- Generated from the current resume and bundled uploads so fresh deployments can open:
|
-- Generated from the current resume and bundled uploads so fresh deployments can open:
|
||||||
-- https://isiseg.huijutec.cn/audience/resume
|
-- https://me.huijutec.cn/audience/resume
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ INSERT INTO resume (
|
|||||||
"email": "zub572701190@stu.xjtu.edu.cn",
|
"email": "zub572701190@stu.xjtu.edu.cn",
|
||||||
"phone": "+86 139-4611-2059",
|
"phone": "+86 139-4611-2059",
|
||||||
"website": {
|
"website": {
|
||||||
"url": "https://isiseg.huijutec.cn/audience/resume",
|
"url": "https://me.huijutec.cn/audience/resume",
|
||||||
"label": "isiseg.huijutec.cn/audience/resume"
|
"label": "me.huijutec.cn/audience/resume"
|
||||||
},
|
},
|
||||||
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
||||||
"location": "陕西西安|西安交通大学",
|
"location": "陕西西安|西安交通大学",
|
||||||
|
|||||||
@@ -2,37 +2,38 @@
|
|||||||
|
|
||||||
这套包面向威联通 QNAP QTS / Container Station,已按以下参数预置:
|
这套包面向威联通 QNAP QTS / Container Station,已按以下参数预置:
|
||||||
|
|
||||||
- NAS 数据目录:`/share/Container/Reactive_Resume_Personal`
|
- NAS 数据目录:`/share/Container/reactive_resume`
|
||||||
- 本地端口:`3004:3000`
|
- 本地端口:`3003:3000`
|
||||||
- 公网域名:`https://isiseg.huijutec.cn`
|
- 公网域名:`https://me.huijutec.cn`
|
||||||
- 当前公开简历:`https://isiseg.huijutec.cn/audience/resume`
|
- 当前公开简历:`https://me.huijutec.cn/audience/resume`
|
||||||
- FRP 服务器:`82.157.255.195:7000`
|
- FRP 服务器:`82.157.255.195:7000`
|
||||||
- FRP remotePort:`10004`
|
- FRP remotePort:`10003`
|
||||||
|
- 容器运行时代理:`HTTP_PROXY=http://192.168.3.12:7893`、`HTTPS_PROXY=http://192.168.3.12:7893`,并已配置 `NO_PROXY` 绕过局域网和内部服务名
|
||||||
|
|
||||||
包内已包含当前简历初始化数据、头像和作品集图片,首次启动后会由 `reactive_resume_seed` 自动导入。
|
包内已包含当前简历初始化数据、头像和作品集图片,首次启动后会由 `reactive_resume_seed` 自动导入。
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
1. 将本安装包内容放到 `/share/Container/Reactive_Resume_Personal`
|
1. 将本安装包内容放到 `/share/Container/reactive_resume`
|
||||||
2. 打开 Container Station,导入 `/share/Container/Reactive_Resume_Personal/compose-Nas.yml`
|
2. 打开 Container Station,导入 `/share/Container/reactive_resume/compose-Nas.yml`
|
||||||
3. 启动项目
|
3. 启动项目
|
||||||
4. 访问 `https://isiseg.huijutec.cn/audience/resume`
|
4. 访问 `https://me.huijutec.cn/audience/resume`
|
||||||
|
|
||||||
发布归档中的顶层目录已经命名为 `Reactive_Resume_Personal/`,正常解压到 `/share/Container/` 下即可匹配上述路径。
|
发布归档中的顶层目录已经命名为 `reactive_resume/`,正常解压到 `/share/Container/` 下即可匹配上述路径。
|
||||||
|
|
||||||
## 反向代理要求
|
## 反向代理要求
|
||||||
|
|
||||||
公网服务器上的 Nginx Proxy Manager / 反向代理应配置:
|
公网服务器上的 Nginx Proxy Manager / 反向代理应配置:
|
||||||
|
|
||||||
- Domain Names:`isiseg.huijutec.cn`
|
- Domain Names:`me.huijutec.cn`
|
||||||
- Scheme:`http`
|
- Scheme:`http`
|
||||||
- Forward Hostname / IP:`82.157.255.195`
|
- Forward Hostname / IP:`82.157.255.195`
|
||||||
- Forward Port:`10004`
|
- Forward Port:`10003`
|
||||||
- Websockets Support:开启
|
- Websockets Support:开启
|
||||||
|
|
||||||
## 数据目录
|
## 数据目录
|
||||||
|
|
||||||
- PostgreSQL:`/share/Container/Reactive_Resume_Personal/data/postgres`
|
- PostgreSQL:`/share/Container/reactive_resume/data/postgres`
|
||||||
- 上传与本地存储:`/share/Container/Reactive_Resume_Personal/data/uploads`
|
- 上传与本地存储:`/share/Container/reactive_resume/data/uploads`
|
||||||
- 初始化种子:`/share/Container/Reactive_Resume_Personal/seed`
|
- 初始化种子:`/share/Container/reactive_resume/seed`
|
||||||
- 运行时补丁:`/share/Container/Reactive_Resume_Personal/patches`
|
- 运行时补丁:`/share/Container/reactive_resume/patches`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Reactive Resume 个人简历 / QNAP QTS 直接部署版。
|
# Reactive Resume 个人简历 / QNAP QTS 直接部署版。
|
||||||
# 本文件已按 /share/Container/Reactive_Resume_Personal、
|
# 本文件已按 /share/Container/reactive_resume、
|
||||||
# https://isiseg.huijutec.cn、192.168.31.5:3004 本地访问、
|
# https://me.huijutec.cn、192.168.31.5:3003 本地访问、
|
||||||
# frpc 公网映射 82.157.255.195:10004 预置。
|
# frpc 公网映射 82.157.255.195:10003 预置。
|
||||||
|
|
||||||
name: reactive-resume-personal-nas
|
name: reactive-resume-personal-nas
|
||||||
|
|
||||||
@@ -9,19 +9,32 @@ services:
|
|||||||
reactive_resume_permissions:
|
reactive_resume_permissions:
|
||||||
image: alpine:3.20
|
image: alpine:3.20
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
HTTP_PROXY: http://192.168.3.12:7893
|
||||||
|
HTTPS_PROXY: http://192.168.3.12:7893
|
||||||
|
http_proxy: http://192.168.3.12:7893
|
||||||
|
https_proxy: http://192.168.3.12:7893
|
||||||
|
NO_PROXY: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
|
no_proxy: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
command: ["sh", "-c", "mkdir -p /app/data && chown -R 1000:1000 /app/data"]
|
command: ["sh", "-c", "mkdir -p /app/data && chown -R 1000:1000 /app/data"]
|
||||||
volumes:
|
volumes:
|
||||||
- /share/Container/Reactive_Resume_Personal/data/uploads:/app/data
|
- /share/Container/reactive_resume/data/uploads:/app/data
|
||||||
|
|
||||||
reactive_resume_db:
|
reactive_resume_db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
HTTP_PROXY: http://192.168.3.12:7893
|
||||||
|
HTTPS_PROXY: http://192.168.3.12:7893
|
||||||
|
http_proxy: http://192.168.3.12:7893
|
||||||
|
https_proxy: http://192.168.3.12:7893
|
||||||
|
NO_PROXY: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
|
no_proxy: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
POSTGRES_DB: reactive_resume
|
POSTGRES_DB: reactive_resume
|
||||||
POSTGRES_USER: reactive_resume
|
POSTGRES_USER: reactive_resume
|
||||||
POSTGRES_PASSWORD: 5b341c0ca29fefd6d648661150c00fa4
|
POSTGRES_PASSWORD: 5b341c0ca29fefd6d648661150c00fa4
|
||||||
volumes:
|
volumes:
|
||||||
- /share/Container/Reactive_Resume_Personal/data/postgres:/var/lib/postgresql/data
|
- /share/Container/reactive_resume/data/postgres:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U reactive_resume -d reactive_resume"]
|
test: ["CMD-SHELL", "pg_isready -U reactive_resume -d reactive_resume"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -29,26 +42,29 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
reactive_resume_app:
|
reactive_resume_app:
|
||||||
image: amruthpillai/reactive-resume:latest
|
image: amruthpillai/reactive-resume@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
entrypoint: ["/bin/sh", "/opt/reactive-resume-patches/reactive-resume-entrypoint.sh"]
|
||||||
command:
|
|
||||||
- |
|
|
||||||
sh /opt/reactive-resume-patches/reactive-resume-runtime-patch.sh
|
|
||||||
exec node .output/server/index.mjs
|
|
||||||
depends_on:
|
depends_on:
|
||||||
reactive_resume_permissions:
|
reactive_resume_permissions:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
reactive_resume_db:
|
reactive_resume_db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "3004:3000"
|
- "3003:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- /share/Container/Reactive_Resume_Personal/data/uploads:/app/data
|
- /share/Container/reactive_resume/data/uploads:/app/data
|
||||||
- /share/Container/Reactive_Resume_Personal/patches/reactive-resume-runtime-patch.sh:/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh:ro
|
- /share/Container/reactive_resume/patches/reactive-resume-runtime-patch.sh:/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh:ro
|
||||||
|
- /share/Container/reactive_resume/patches/reactive-resume-entrypoint.sh:/opt/reactive-resume-patches/reactive-resume-entrypoint.sh:ro
|
||||||
environment:
|
environment:
|
||||||
|
HTTP_PROXY: http://192.168.3.12:7893
|
||||||
|
HTTPS_PROXY: http://192.168.3.12:7893
|
||||||
|
http_proxy: http://192.168.3.12:7893
|
||||||
|
https_proxy: http://192.168.3.12:7893
|
||||||
|
NO_PROXY: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
|
no_proxy: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
APP_URL: https://isiseg.huijutec.cn
|
APP_URL: https://me.huijutec.cn
|
||||||
DATABASE_URL: postgresql://reactive_resume:5b341c0ca29fefd6d648661150c00fa4@reactive_resume_db:5432/reactive_resume
|
DATABASE_URL: postgresql://reactive_resume:5b341c0ca29fefd6d648661150c00fa4@reactive_resume_db:5432/reactive_resume
|
||||||
AUTH_SECRET: c76b0eaf79f731e9ee95918dc69d41696aec9d1deffeabc122944898037bfab1
|
AUTH_SECRET: c76b0eaf79f731e9ee95918dc69d41696aec9d1deffeabc122944898037bfab1
|
||||||
ENCRYPTION_SECRET: df3a460fa2f92f6e8765927a169322980e18f63a88fbcfedb090819b5afb2408
|
ENCRYPTION_SECRET: df3a460fa2f92f6e8765927a169322980e18f63a88fbcfedb090819b5afb2408
|
||||||
@@ -73,7 +89,7 @@ services:
|
|||||||
SMTP_PORT: "587"
|
SMTP_PORT: "587"
|
||||||
SMTP_USER: ""
|
SMTP_USER: ""
|
||||||
SMTP_PASS: ""
|
SMTP_PASS: ""
|
||||||
SMTP_FROM: "Reactive Resume <noreply@isiseg.huijutec.cn>"
|
SMTP_FROM: "Reactive Resume <noreply@me.huijutec.cn>"
|
||||||
SMTP_SECURE: "false"
|
SMTP_SECURE: "false"
|
||||||
S3_ACCESS_KEY_ID: ""
|
S3_ACCESS_KEY_ID: ""
|
||||||
S3_SECRET_ACCESS_KEY: ""
|
S3_SECRET_ACCESS_KEY: ""
|
||||||
@@ -109,10 +125,16 @@ services:
|
|||||||
chown -R 1000:1000 /app/data/uploads || true
|
chown -R 1000:1000 /app/data/uploads || true
|
||||||
psql -h reactive_resume_db -U reactive_resume -d reactive_resume -v ON_ERROR_STOP=1 -f /seed/seed.sql
|
psql -h reactive_resume_db -U reactive_resume -d reactive_resume -v ON_ERROR_STOP=1 -f /seed/seed.sql
|
||||||
environment:
|
environment:
|
||||||
|
HTTP_PROXY: http://192.168.3.12:7893
|
||||||
|
HTTPS_PROXY: http://192.168.3.12:7893
|
||||||
|
http_proxy: http://192.168.3.12:7893
|
||||||
|
https_proxy: http://192.168.3.12:7893
|
||||||
|
NO_PROXY: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
|
no_proxy: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
PGPASSWORD: 5b341c0ca29fefd6d648661150c00fa4
|
PGPASSWORD: 5b341c0ca29fefd6d648661150c00fa4
|
||||||
volumes:
|
volumes:
|
||||||
- /share/Container/Reactive_Resume_Personal/data/uploads:/app/data
|
- /share/Container/reactive_resume/data/uploads:/app/data
|
||||||
- /share/Container/Reactive_Resume_Personal/seed:/seed:ro
|
- /share/Container/reactive_resume/seed:/seed:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
reactive_resume_permissions:
|
reactive_resume_permissions:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
@@ -122,8 +144,15 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
reactive_resume_frpc:
|
reactive_resume_frpc:
|
||||||
image: snowdreamtech/frpc:latest
|
image: fatedier/frpc:v0.68.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
HTTP_PROXY: http://192.168.3.12:7893
|
||||||
|
HTTPS_PROXY: http://192.168.3.12:7893
|
||||||
|
http_proxy: http://192.168.3.12:7893
|
||||||
|
https_proxy: http://192.168.3.12:7893
|
||||||
|
NO_PROXY: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
|
no_proxy: localhost,127.0.0.1,192.168.0.0/16,reactive_resume_db,reactive_resume_app,reactive_resume_seed,reactive_resume_frpc
|
||||||
entrypoint: ["/bin/sh"]
|
entrypoint: ["/bin/sh"]
|
||||||
command:
|
command:
|
||||||
- -c
|
- -c
|
||||||
@@ -139,11 +168,11 @@ services:
|
|||||||
transport.heartbeatTimeout = -1
|
transport.heartbeatTimeout = -1
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "Reactive_Resume_Personal_NAS"
|
name = "Reactive_Resume_NAS"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
localIP = "reactive_resume_app"
|
localIP = "reactive_resume_app"
|
||||||
localPort = 3000
|
localPort = 3000
|
||||||
remotePort = 10004
|
remotePort = 10003
|
||||||
EOF
|
EOF
|
||||||
exec frpc -c /tmp/frpc.toml
|
exec frpc -c /tmp/frpc.toml
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PATCH_SCRIPT="/opt/reactive-resume-patches/reactive-resume-runtime-patch.sh"
|
||||||
|
|
||||||
|
if [ -f "$PATCH_SCRIPT" ]; then
|
||||||
|
sh "$PATCH_SCRIPT" || echo "Reactive Resume runtime patch failed, continuing with the image default startup" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
if [ -f /app/apps/server/dist/index.mjs ]; then
|
||||||
|
cd /app
|
||||||
|
set -- node apps/server/dist/index.mjs
|
||||||
|
elif [ -f /app/apps/web/.output/server/index.mjs ]; then
|
||||||
|
cd /app/apps/web
|
||||||
|
set -- node .output/server/index.mjs
|
||||||
|
else
|
||||||
|
server_entry="$(cat /tmp/reactive-resume-server-entry 2>/dev/null || true)"
|
||||||
|
if [ -n "$server_entry" ] && [ -f "$server_entry" ]; then
|
||||||
|
cd "$(dirname "$server_entry")"
|
||||||
|
set -- node "$(basename "$server_entry")"
|
||||||
|
else
|
||||||
|
echo "Reactive Resume startup failed: no known server entry found" >&2
|
||||||
|
find /app -maxdepth 5 \( -name index.mjs -o -name server.js -o -name main.js \) 2>/dev/null | head -50 >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v docker-entrypoint.sh >/dev/null 2>&1; then
|
||||||
|
exec docker-entrypoint.sh "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -1,24 +1,94 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SSR_FILE="/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs"
|
APP_DIR="${REACTIVE_RESUME_APP_DIR:-}"
|
||||||
PUBLIC_FILENAME_FILE="/app/apps/web/.output/public/assets/file-D5WsIgJH.js"
|
SERVER_ENTRY=""
|
||||||
PUBLIC_PDF_FILE="/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js"
|
ASSETS_DIR=""
|
||||||
SERVER_INDEX_FILE="/app/apps/web/.output/server/index.mjs"
|
SSR_DIR=""
|
||||||
CACHE_BUST_FILENAME="rr-filename-title-20260520"
|
SERVER_INDEX_FILE=""
|
||||||
CACHE_BUST_PDF="rr-glalie-layout-20260520"
|
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'
|
node - <<'NODE'
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
const ssrFile = "/app/apps/web/.output/server/_ssr/pdf-document-COfeOLVC.mjs";
|
const appDir = process.env.APP_DIR || "/app";
|
||||||
const publicFilenameFile = "/app/apps/web/.output/public/assets/file-D5WsIgJH.js";
|
const outputDir = path.join(appDir, ".output");
|
||||||
const publicPdfFile = "/app/apps/web/.output/public/assets/pdf-document-BplbXx-0.js";
|
const assetsDir = process.env.ASSETS_DIR || path.join(outputDir, "public/assets");
|
||||||
const serverIndexFile = "/app/apps/web/.output/server/index.mjs";
|
const ssrDir = process.env.SSR_DIR || "";
|
||||||
const filenameCacheBust = "rr-filename-title-20260520";
|
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 pdfCacheBust = "rr-glalie-layout-20260520";
|
||||||
|
const appShellSuffix = "rr20260520c";
|
||||||
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
|
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
|
||||||
|
const serviceWorkerCleanup = `/* Reactive Resume personal deployment: disable stale PWA caches. */
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key)))));
|
||||||
|
});
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil((async () => {
|
||||||
|
await caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key))));
|
||||||
|
await self.clients.claim();
|
||||||
|
})());
|
||||||
|
});
|
||||||
|
self.addEventListener("fetch", () => {});
|
||||||
|
`;
|
||||||
|
|
||||||
|
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) {
|
function makeEtag(buffer) {
|
||||||
const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, "");
|
const digest = crypto.createHash("sha1").update(buffer).digest("base64").replace(/=+$/g, "");
|
||||||
@@ -44,6 +114,31 @@ function patchStaticManifestEntry(source, urlPath, filePath) {
|
|||||||
return source.slice(0, start) + entry + source.slice(end);
|
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 listFilesRecursive(dir, predicate) {
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
const result = [];
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const file = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
result.push(...listFilesRecursive(file, predicate));
|
||||||
|
} else if (predicate(file)) {
|
||||||
|
result.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function replaceOnce(source, from, to) {
|
function replaceOnce(source, from, to) {
|
||||||
return source.includes(to) ? source : source.replace(from, to);
|
return source.includes(to) ? source : source.replace(from, to);
|
||||||
}
|
}
|
||||||
@@ -52,26 +147,43 @@ function replaceRegexOnce(source, regex, to) {
|
|||||||
return source.includes(to) ? source : source.replace(regex, to);
|
return source.includes(to) ? source : source.replace(regex, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchFilenameBundle() {
|
function patchFilenameBundle(file) {
|
||||||
let source = fs.readFileSync(publicFilenameFile, "utf8");
|
if (!/^file-[A-Za-z0-9_-]+\.js$/.test(path.basename(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}`:""}`}';
|
return false;
|
||||||
if (!source.includes(replacement)) {
|
}
|
||||||
|
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 start = source.indexOf("function t(");
|
||||||
const end = source.indexOf("function n(", start);
|
const end = source.indexOf("function n(", start);
|
||||||
if (start === -1 || end === -1) throw new Error("filename function marker not found");
|
if (start === -1 || end === -1) {
|
||||||
source = source.slice(0, start) + replacement + source.slice(end);
|
warn(`filename bundle marker not found in ${path.basename(file)}, skipped`);
|
||||||
fs.writeFileSync(publicFilenameFile, source);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
source = source.slice(0, start) + replacement + source.slice(end);
|
||||||
|
write(file, source);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchSsr(source) {
|
function patchSsr(source) {
|
||||||
source = source.replace(/\n\t\tname: "",\n\t\tdata: \{/, "\n\t\tname: resume.name,\n\t\tdata: {");
|
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}`;
|
|
||||||
|
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)) {
|
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 start = source.indexOf("function generateFilename(");
|
||||||
const end = source.indexOf("\nfunction downloadWithAnchor(", start);
|
const end = source.indexOf("\nfunction downloadWithAnchor(", start);
|
||||||
if (start === -1 || end === -1) throw new Error("SSR generateFilename marker not found");
|
if (start !== -1 && end !== -1) {
|
||||||
source = source.slice(0, start) + filenameReplacement + source.slice(end);
|
source = source.slice(0, start) + filenameReplacement + source.slice(end);
|
||||||
|
} else {
|
||||||
|
warn("SSR generateFilename marker not found, skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
source = source
|
source = source
|
||||||
@@ -82,6 +194,7 @@ function patchSsr(source) {
|
|||||||
.replace(/style: composeStyles\(styles\.sidebarContent, \{ rowGap: metrics\.sectionGap \}\),/g, "style: composeStyles(styles.sidebarContent, { rowGap: 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(/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},");
|
.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 = replaceRegexOnce(
|
||||||
source,
|
source,
|
||||||
/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/,
|
/sectionHeading: \{\s*borderBottomWidth: 1,\s*borderBottomColor: primary,\s*paddingBottom: 1(?:\.3)?\s*\},\s*item: \{ rowGap: metrics\.gapY\(\.125\) \},/,
|
||||||
@@ -100,15 +213,90 @@ function patchSsr(source) {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBrowserBufferPolyfill(source) {
|
function patchAppManifest(file, oldBase, newBase) {
|
||||||
if (source.includes("rr-browser-buffer-polyfill")) return source;
|
if (!fs.existsSync(file)) return false;
|
||||||
const insertAt = source.indexOf(";") + 1;
|
let source = read(file);
|
||||||
if (insertAt <= 0 || !source.startsWith("import")) throw new Error("PDF bundle import prelude not found");
|
const next = source.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
|
||||||
return source.slice(0, insertAt) + browserBufferPolyfill + source.slice(insertAt);
|
if (next !== source) {
|
||||||
|
write(file, next);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneStaticManifestEntry(source, oldUrl, newUrl, oldBase, newBase, newFile) {
|
||||||
|
const marker = `\t"${oldUrl}": {`;
|
||||||
|
const start = source.indexOf(marker);
|
||||||
|
if (start === -1) {
|
||||||
|
warn(`static manifest entry not found for ${oldUrl}, skipped app shell cache bust`);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.includes(`\t"${newUrl}": {`)) {
|
||||||
|
return patchStaticManifestEntry(source, newUrl, newFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = source.indexOf("\n\t}", start);
|
||||||
|
if (close === -1 || source[close + 3] !== ",") {
|
||||||
|
warn(`static manifest entry close marker not found for ${oldUrl}, skipped app shell cache bust`);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = source.slice(start, close + 3);
|
||||||
|
const buffer = fs.readFileSync(newFile);
|
||||||
|
entry = entry
|
||||||
|
.replace(oldUrl, newUrl)
|
||||||
|
.replace(`../public/assets/${oldBase}`, `../public/assets/${newBase}`)
|
||||||
|
.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, close + 4) + "\n" + entry + "," + source.slice(close + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchAppShellEntry(assetFiles) {
|
||||||
|
const indexFile = assetFiles.find((file) => /^index-[A-Za-z0-9_-]+\.js$/.test(path.basename(file)));
|
||||||
|
if (!indexFile) {
|
||||||
|
warn("index app shell bundle not found, skipped app shell cache bust");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldBase = path.basename(indexFile);
|
||||||
|
const newBase = oldBase.replace(/\.js$/, `-${appShellSuffix}.js`);
|
||||||
|
const newFile = path.join(path.dirname(indexFile), newBase);
|
||||||
|
if (!fs.existsSync(newFile)) {
|
||||||
|
fs.copyFileSync(indexFile, newFile);
|
||||||
|
}
|
||||||
|
return { oldBase, newBase, newFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchAppShellImporters(assetFiles, oldBase, newBase) {
|
||||||
|
const touched = [];
|
||||||
|
for (const file of assetFiles) {
|
||||||
|
if (path.basename(file) === oldBase) continue;
|
||||||
|
let source = read(file);
|
||||||
|
let next = source
|
||||||
|
.replace(new RegExp(`\\./${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `./${newBase}`)
|
||||||
|
.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
|
||||||
|
if (next !== source) {
|
||||||
|
write(file, next);
|
||||||
|
touched.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return touched;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchPublicPdf(source) {
|
function patchPublicPdf(source) {
|
||||||
source = ensureBrowserBufferPolyfill(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
|
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(/([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\(3\.5\)/g, (m) => `${m[0]}.gapY(3.0)`)
|
||||||
@@ -118,43 +306,108 @@ function patchPublicPdf(source) {
|
|||||||
.replace(/style:\$\(a\.mainContent,\{rowGap:o\.sectionGap\}\)/g, "style:$(a.mainContent,{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)?)?\}/, "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)}");
|
.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, "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}");
|
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;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchImporters() {
|
const assetFiles = listJsFiles(assetsDir);
|
||||||
const assetsDir = "/app/apps/web/.output/public/assets";
|
const filenameFiles = assetFiles
|
||||||
const files = fs.readdirSync(assetsDir).filter((name) => name.endsWith(".js")).map((name) => `${assetsDir}/${name}`);
|
.filter((file) => {
|
||||||
const touched = [];
|
if (!/^file-[A-Za-z0-9_-]+\.js$/.test(path.basename(file))) return false;
|
||||||
for (const file of files) {
|
const source = read(file);
|
||||||
let source = fs.readFileSync(file, "utf8");
|
return source.includes("URL.createObjectURL") && source.includes(".download") && source.includes("revokeObjectURL");
|
||||||
let next = source
|
})
|
||||||
.replace(/\.\/file-D5WsIgJH\.js(?:\?v=rr-filename-[A-Za-z0-9-]+)?/g, `./file-D5WsIgJH.js?v=${filenameCacheBust}`)
|
.sort((a, b) => fs.statSync(a).size - fs.statSync(b).size);
|
||||||
.replace(/\.\/pdf-document-BplbXx-0\.js(?:\?v=rr-[^"'`]+)?/g, `./pdf-document-BplbXx-0.js?v=${pdfCacheBust}`);
|
|
||||||
|
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 appShell = patchAppShellEntry(assetFiles);
|
||||||
|
const appShellTouchedImporters = appShell ? patchAppShellImporters(assetFiles, appShell.oldBase, appShell.newBase) : [];
|
||||||
|
if (appShell) {
|
||||||
|
const appManifestFiles = [
|
||||||
|
...listFilesRecursive(path.join(outputDir, "server"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
|
||||||
|
...listFilesRecursive(path.join(appDir, "apps/server/dist"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
|
||||||
|
];
|
||||||
|
for (const file of appManifestFiles) patchAppManifest(file, appShell.oldBase, appShell.newBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (next !== source) {
|
||||||
fs.writeFileSync(file, next);
|
write(file, next);
|
||||||
touched.push(file);
|
touchedImporters.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return touched;
|
|
||||||
}
|
|
||||||
|
|
||||||
patchFilenameBundle();
|
if (fs.existsSync(serverIndexFile)) {
|
||||||
fs.writeFileSync(ssrFile, patchSsr(fs.readFileSync(ssrFile, "utf8")));
|
let serverIndex = read(serverIndexFile);
|
||||||
fs.writeFileSync(publicPdfFile, patchPublicPdf(fs.readFileSync(publicPdfFile, "utf8")));
|
for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters, ...appShellTouchedImporters].filter(Boolean)) {
|
||||||
const importers = patchImporters();
|
serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file);
|
||||||
|
}
|
||||||
let serverIndex = fs.readFileSync(serverIndexFile, "utf8");
|
if (appShell) {
|
||||||
serverIndex = patchStaticManifestEntry(serverIndex, "/assets/file-D5WsIgJH.js", publicFilenameFile);
|
serverIndex = cloneStaticManifestEntry(
|
||||||
serverIndex = patchStaticManifestEntry(serverIndex, "/assets/pdf-document-BplbXx-0.js", publicPdfFile);
|
serverIndex,
|
||||||
for (const file of importers) {
|
`/assets/${appShell.oldBase}`,
|
||||||
serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${file.split("/").pop()}`, file);
|
`/assets/${appShell.newBase}`,
|
||||||
|
appShell.oldBase,
|
||||||
|
appShell.newBase,
|
||||||
|
appShell.newFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const serviceWorkerFile = path.join(path.dirname(assetsDir), "sw.js");
|
||||||
|
if (fs.existsSync(serviceWorkerFile)) {
|
||||||
|
write(serviceWorkerFile, serviceWorkerCleanup);
|
||||||
|
serverIndex = patchStaticManifestEntry(serverIndex, "/sw.js", serviceWorkerFile);
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fs.writeFileSync(serverIndexFile, serverIndex);
|
|
||||||
NODE
|
NODE
|
||||||
|
|
||||||
node --check "$SSR_FILE" >/dev/null
|
|
||||||
node --check "$PUBLIC_FILENAME_FILE" >/dev/null
|
|
||||||
node --check "$PUBLIC_PDF_FILE" >/dev/null
|
|
||||||
node --check "$SERVER_INDEX_FILE" >/dev/null
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"email": "zub572701190@stu.xjtu.edu.cn",
|
"email": "zub572701190@stu.xjtu.edu.cn",
|
||||||
"phone": "+86 139-4611-2059",
|
"phone": "+86 139-4611-2059",
|
||||||
"website": {
|
"website": {
|
||||||
"url": "https://isiseg.huijutec.cn/audience/resume",
|
"url": "https://me.huijutec.cn/audience/resume",
|
||||||
"label": "isiseg.huijutec.cn/audience/resume"
|
"label": "me.huijutec.cn/audience/resume"
|
||||||
},
|
},
|
||||||
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
||||||
"location": "陕西西安|西安交通大学",
|
"location": "陕西西安|西安交通大学",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
-- Reactive Resume personal seed data.
|
-- Reactive Resume personal seed data.
|
||||||
-- Generated from the current resume and bundled uploads so fresh deployments can open:
|
-- Generated from the current resume and bundled uploads so fresh deployments can open:
|
||||||
-- https://isiseg.huijutec.cn/audience/resume
|
-- https://me.huijutec.cn/audience/resume
|
||||||
|
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ INSERT INTO resume (
|
|||||||
"email": "zub572701190@stu.xjtu.edu.cn",
|
"email": "zub572701190@stu.xjtu.edu.cn",
|
||||||
"phone": "+86 139-4611-2059",
|
"phone": "+86 139-4611-2059",
|
||||||
"website": {
|
"website": {
|
||||||
"url": "https://isiseg.huijutec.cn/audience/resume",
|
"url": "https://me.huijutec.cn/audience/resume",
|
||||||
"label": "isiseg.huijutec.cn/audience/resume"
|
"label": "me.huijutec.cn/audience/resume"
|
||||||
},
|
},
|
||||||
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
"headline": "AI 医工交叉博士|智能外科与微创手术导航|多模态大模型与临床转化",
|
||||||
"location": "陕西西安|西安交通大学",
|
"location": "陕西西安|西安交通大学",
|
||||||
|
|||||||
338
scripts/test-personal-install-packages.sh
Executable file
338
scripts/test-personal-install-packages.sh
Executable file
@@ -0,0 +1,338 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
DIRECT_COMPOSE="$ROOT_DIR/packages/reactive-resume-personal-direct/compose.yml"
|
||||||
|
DIRECT_ENV="$ROOT_DIR/packages/reactive-resume-personal-direct/.env"
|
||||||
|
QNAP_COMPOSE="$ROOT_DIR/packages/reactive-resume-personal-qnap-nas/compose-Nas.yml"
|
||||||
|
QNAP_PATCH_DIR="$ROOT_DIR/packages/reactive-resume-personal-qnap-nas/patches"
|
||||||
|
DIRECT_PATCH_DIR="$ROOT_DIR/packages/reactive-resume-personal-direct/patches"
|
||||||
|
QNAP_ZIP="$ROOT_DIR/dist/reactive-resume-personal-qnap-nas-20260520.zip"
|
||||||
|
DIRECT_ZIP="$ROOT_DIR/dist/reactive-resume-personal-direct-20260520.zip"
|
||||||
|
IMAGE_REPO="amruthpillai/reactive-resume"
|
||||||
|
IMAGE_INDEX="$IMAGE_REPO@sha256:b760446c4301af067e7d595537a877e378363aa6ce921b7349e62983621826aa"
|
||||||
|
PROJECT="reactive-resume-personal"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '\n[TEST] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '\n[FAIL] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_direct() {
|
||||||
|
docker compose -f "$DIRECT_COMPOSE" --env-file "$DIRECT_ENV" down -v >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_tmp() {
|
||||||
|
if [ -n "${TMP_DIR:-}" ] && [ -d "$TMP_DIR" ]; then
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_all() {
|
||||||
|
cleanup_direct
|
||||||
|
cleanup_tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup_all HUP INT TERM EXIT
|
||||||
|
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
log "检查补丁脚本语法"
|
||||||
|
sh -n "$QNAP_PATCH_DIR/reactive-resume-runtime-patch.sh"
|
||||||
|
sh -n "$QNAP_PATCH_DIR/reactive-resume-entrypoint.sh"
|
||||||
|
sh -n "$DIRECT_PATCH_DIR/reactive-resume-runtime-patch.sh"
|
||||||
|
sh -n "$DIRECT_PATCH_DIR/reactive-resume-entrypoint.sh"
|
||||||
|
|
||||||
|
log "检查 Compose 配置可解析"
|
||||||
|
docker compose -f "$QNAP_COMPOSE" config >/tmp/reactive-resume-qnap-compose-test.yml
|
||||||
|
docker compose -f "$DIRECT_COMPOSE" --env-file "$DIRECT_ENV" config >/tmp/reactive-resume-direct-compose-test.yml
|
||||||
|
grep -q 'reactive-resume-entrypoint.sh' /tmp/reactive-resume-qnap-compose-test.yml
|
||||||
|
grep -q 'reactive-resume-entrypoint.sh' /tmp/reactive-resume-direct-compose-test.yml
|
||||||
|
|
||||||
|
log "检查 zip 安装包内容"
|
||||||
|
unzip -t "$QNAP_ZIP" >/dev/null
|
||||||
|
unzip -t "$DIRECT_ZIP" >/dev/null
|
||||||
|
unzip -l "$QNAP_ZIP" | grep -q 'reactive_resume/compose-Nas.yml'
|
||||||
|
unzip -l "$QNAP_ZIP" | grep -q 'reactive_resume/patches/reactive-resume-entrypoint.sh'
|
||||||
|
unzip -l "$QNAP_ZIP" | grep -q 'reactive_resume/patches/reactive-resume-runtime-patch.sh'
|
||||||
|
unzip -l "$DIRECT_ZIP" | grep -q 'reactive-resume-personal-direct/compose.yml'
|
||||||
|
unzip -l "$DIRECT_ZIP" | grep -q 'reactive-resume-personal-direct/patches/reactive-resume-entrypoint.sh'
|
||||||
|
|
||||||
|
if unzip -p "$QNAP_ZIP" 'reactive_resume/*' 2>/dev/null | grep -E 'isiseg|10004|Reactive_Resume_Personal|/share/Container/Reactive_Resume_Personal' >/dev/null; then
|
||||||
|
fail "QNAP zip 中仍有旧域名、旧端口或旧路径"
|
||||||
|
fi
|
||||||
|
if unzip -p "$QNAP_ZIP" 'reactive_resume/*' 2>/dev/null | grep -E 'amruthpillai/reactive-resume:latest|snowdreamtech/frpc:latest' >/dev/null; then
|
||||||
|
fail "QNAP zip 中仍有 latest 镜像"
|
||||||
|
fi
|
||||||
|
if unzip -p "$DIRECT_ZIP" 'reactive-resume-personal-direct/*' 2>/dev/null | grep -E 'amruthpillai/reactive-resume:latest|snowdreamtech/frpc:latest' >/dev/null; then
|
||||||
|
fail "direct zip 中仍有 latest 镜像"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "真实启动 direct 包并检查健康状态"
|
||||||
|
cleanup_direct
|
||||||
|
docker compose -f "$DIRECT_COMPOSE" --env-file "$DIRECT_ENV" up -d postgres reactive-resume seed
|
||||||
|
|
||||||
|
attempt=0
|
||||||
|
until curl -fsS "http://127.0.0.1:3004/api/health" >/tmp/reactive-resume-health.json 2>/dev/null; do
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
if [ "$attempt" -ge 60 ]; then
|
||||||
|
docker logs "$PROJECT-reactive-resume-1" --tail 200 >&2 || true
|
||||||
|
fail "direct 包启动后 /api/health 未在 60 秒内就绪"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
docker wait "$PROJECT-seed-1" >/tmp/reactive-resume-seed-exit
|
||||||
|
seed_status="$(docker inspect "$PROJECT-seed-1" --format '{{.State.ExitCode}}' 2>/dev/null || printf 'missing')"
|
||||||
|
[ "$seed_status" = "0" ] || fail "seed 容器退出码不是 0:$seed_status"
|
||||||
|
|
||||||
|
attempt=0
|
||||||
|
until curl -fsS -I "http://127.0.0.1:3004/audience/resume" >/tmp/reactive-resume-audience.headers 2>/dev/null \
|
||||||
|
&& grep -q '200 OK' /tmp/reactive-resume-audience.headers; do
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
if [ "$attempt" -ge 30 ]; then
|
||||||
|
docker logs "$PROJECT-reactive-resume-1" --tail 200 >&2 || true
|
||||||
|
fail "seed 完成后 /audience/resume 未在 30 秒内返回 200"
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
docker logs "$PROJECT-reactive-resume-1" --tail 200 >/tmp/reactive-resume-direct.log 2>&1 || true
|
||||||
|
if grep -E 'Cannot find module|Buffer is not defined|Unexpected end of input' /tmp/reactive-resume-direct.log >/dev/null; then
|
||||||
|
cat /tmp/reactive-resume-direct.log >&2
|
||||||
|
fail "direct 包日志仍包含已知启动或前端错误"
|
||||||
|
fi
|
||||||
|
if docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-resume-app-dir); grep -R -E "index-[A-Za-z0-9_-]+\\.js\\?v=rr-filename-title" "$APP_DIR/.output/public/assets" >/dev/null 2>&1'; then
|
||||||
|
fail "direct 包错误地给 index 主入口追加了 rr-filename-title 缓存标记"
|
||||||
|
fi
|
||||||
|
docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-resume-app-dir); grep -R "String(t).trim().replace" "$APP_DIR/.output/public/assets"/file-*.js >/dev/null 2>&1' \
|
||||||
|
|| fail "direct 包未在 file-*.js 下载工具中应用文件名补丁"
|
||||||
|
curl -fsS "http://127.0.0.1:3004/" >/tmp/reactive-resume-home.html \
|
||||||
|
|| fail "direct 包首页无法访问"
|
||||||
|
grep -E '/assets/index-[A-Za-z0-9_-]+-rr[0-9a-z]+\.js' /tmp/reactive-resume-home.html >/dev/null \
|
||||||
|
|| fail "direct 包首页未改用防缓存的 index 主入口文件名"
|
||||||
|
curl -fsS "http://127.0.0.1:3004/sw.js" >/tmp/reactive-resume-sw.js \
|
||||||
|
|| fail "direct 包 sw.js 无法访问"
|
||||||
|
grep -q 'disable stale PWA caches' /tmp/reactive-resume-sw.js \
|
||||||
|
|| fail "direct 包 sw.js 未替换为清理旧缓存版本"
|
||||||
|
|
||||||
|
if command -v google-chrome >/dev/null 2>&1 && command -v node >/dev/null 2>&1; then
|
||||||
|
log "使用 Chrome 执行 direct 包首页 JS"
|
||||||
|
node <<'NODE'
|
||||||
|
const { spawn } = require("child_process");
|
||||||
|
const fs = require("fs");
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
const port = 9444;
|
||||||
|
const userData = "/tmp/reactive-resume-chrome-smoke";
|
||||||
|
fs.rmSync(userData, { recursive: true, force: true });
|
||||||
|
|
||||||
|
const chrome = spawn("google-chrome", [
|
||||||
|
"--headless=new",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--no-sandbox",
|
||||||
|
`--remote-debugging-port=${port}`,
|
||||||
|
`--user-data-dir=${userData}`,
|
||||||
|
"about:blank",
|
||||||
|
], { stdio: ["ignore", "ignore", "ignore"] });
|
||||||
|
|
||||||
|
function request(path, method = "GET") {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request({ host: "127.0.0.1", port, path, method }, (res) => {
|
||||||
|
let body = "";
|
||||||
|
res.on("data", (chunk) => body += chunk);
|
||||||
|
res.on("end", () => resolve({ status: res.statusCode, body }));
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForChrome() {
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
try {
|
||||||
|
const res = await request("/json/version");
|
||||||
|
if (res.status === 200) return;
|
||||||
|
} catch {}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
throw new Error("Chrome remote debugging was not ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await waitForChrome();
|
||||||
|
const tab = JSON.parse((await request("/json/new?about:blank", "PUT")).body);
|
||||||
|
const ws = new WebSocket(tab.webSocketDebuggerUrl);
|
||||||
|
let id = 0;
|
||||||
|
const pending = new Map();
|
||||||
|
const errors = [];
|
||||||
|
const indexRequests = [];
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
if (message.id && pending.has(message.id)) {
|
||||||
|
pending.get(message.id)(message);
|
||||||
|
pending.delete(message.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message.method === "Runtime.exceptionThrown") {
|
||||||
|
errors.push(message.params.exceptionDetails);
|
||||||
|
}
|
||||||
|
if (message.method === "Network.requestWillBeSent" && message.params.request.url.includes("index-")) {
|
||||||
|
indexRequests.push(message.params.request.url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await new Promise((resolve) => { ws.onopen = resolve; });
|
||||||
|
|
||||||
|
function send(method, params = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
pending.set(++id, resolve);
|
||||||
|
ws.send(JSON.stringify({ id, method, params }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await send("Runtime.enable");
|
||||||
|
await send("Page.enable");
|
||||||
|
await send("Network.enable");
|
||||||
|
await send("Page.navigate", { url: "http://127.0.0.1:3004/" });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
const result = await send("Runtime.evaluate", {
|
||||||
|
expression: "document.body.innerText.includes('A free and open-source resume builder')",
|
||||||
|
returnByValue: true,
|
||||||
|
});
|
||||||
|
ws.close();
|
||||||
|
chrome.kill("SIGKILL");
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(JSON.stringify(errors.map((error) => ({
|
||||||
|
text: error.text,
|
||||||
|
url: error.url,
|
||||||
|
line: error.lineNumber,
|
||||||
|
column: error.columnNumber,
|
||||||
|
exception: error.exception?.description,
|
||||||
|
})), null, 2));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!result.result?.result?.value) {
|
||||||
|
console.error("Chrome did not render the expected home page text");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (indexRequests.some((url) => /\/assets\/index-[A-Za-z0-9_-]+\.js$/.test(url) && !/-rr[0-9a-z]+\.js$/.test(url))) {
|
||||||
|
console.error(`Chrome still requested stale app shell: ${indexRequests.join(", ")}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})().catch((error) => {
|
||||||
|
chrome.kill("SIGKILL");
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
NODE
|
||||||
|
else
|
||||||
|
log "未找到 google-chrome,跳过浏览器级首页 JS 测试"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "离线检查 arm64/QNAP 镜像布局"
|
||||||
|
ARM64_DIGEST="$(
|
||||||
|
docker manifest inspect "$IMAGE_INDEX" \
|
||||||
|
| node -e '
|
||||||
|
let source = "";
|
||||||
|
process.stdin.on("data", (chunk) => source += chunk);
|
||||||
|
process.stdin.on("end", () => {
|
||||||
|
const manifest = JSON.parse(source);
|
||||||
|
const arm = manifest.manifests.find((item) => item.platform?.os === "linux" && item.platform?.architecture === "arm64");
|
||||||
|
if (!arm) process.exit(2);
|
||||||
|
process.stdout.write(arm.digest);
|
||||||
|
});
|
||||||
|
'
|
||||||
|
)"
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
CID="$(docker create --platform linux/arm64 "$IMAGE_INDEX" 2>/dev/null || true)"
|
||||||
|
[ -n "$CID" ] || fail "无法创建 arm64 镜像容器用于离线检查"
|
||||||
|
docker export "$CID" -o "$TMP_DIR/arm64-root.tar"
|
||||||
|
docker rm "$CID" >/dev/null
|
||||||
|
mkdir -p "$TMP_DIR/arm64-root"
|
||||||
|
tar -xf "$TMP_DIR/arm64-root.tar" -C "$TMP_DIR/arm64-root"
|
||||||
|
|
||||||
|
if [ -f "$TMP_DIR/arm64-root/app/apps/server/dist/index.mjs" ]; then
|
||||||
|
ARM64_SERVER_ENTRY="$TMP_DIR/arm64-root/app/apps/server/dist/index.mjs"
|
||||||
|
ARM64_FILENAME_ENTRY="$ARM64_SERVER_ENTRY"
|
||||||
|
ARM64_ASSETS_DIR="$TMP_DIR/arm64-root/app/apps/web/dist/assets"
|
||||||
|
EXPECTED_ENTRYPOINT_PWD="$TMP_DIR/arm64-root/app"
|
||||||
|
EXPECTED_ENTRYPOINT_ARGS="node apps/server/dist/index.mjs"
|
||||||
|
elif [ -f "$TMP_DIR/arm64-root/app/apps/web/.output/server/index.mjs" ]; then
|
||||||
|
ARM64_SERVER_ENTRY="$TMP_DIR/arm64-root/app/apps/web/.output/server/index.mjs"
|
||||||
|
ARM64_FILENAME_ENTRY="$(grep -Rsl 'function generateFilename' "$TMP_DIR/arm64-root/app/apps/web/.output/server/_ssr" 2>/dev/null | head -n 1 || true)"
|
||||||
|
[ -n "$ARM64_FILENAME_ENTRY" ] || fail "arm64 .output 布局中未找到 generateFilename SSR bundle"
|
||||||
|
ARM64_ASSETS_DIR="$TMP_DIR/arm64-root/app/apps/web/.output/public/assets"
|
||||||
|
EXPECTED_ENTRYPOINT_PWD="$TMP_DIR/arm64-root/app/apps/web"
|
||||||
|
EXPECTED_ENTRYPOINT_ARGS="node .output/server/index.mjs"
|
||||||
|
else
|
||||||
|
find "$TMP_DIR/arm64-root/app" -maxdepth 6 \( -name index.mjs -o -name server.js -o -name main.js \) 2>/dev/null >&2 || true
|
||||||
|
fail "arm64 镜像中未找到支持的服务入口"
|
||||||
|
fi
|
||||||
|
[ -d "$ARM64_ASSETS_DIR" ] || fail "arm64 镜像中未找到 assets 目录:$ARM64_ASSETS_DIR"
|
||||||
|
|
||||||
|
perl -0pe "
|
||||||
|
s#/app/apps#$TMP_DIR/arm64-root/app/apps#g;
|
||||||
|
s#for candidate in $TMP_DIR/arm64-root/app/apps/web /app#for candidate in $TMP_DIR/arm64-root/app/apps/web $TMP_DIR/arm64-root/app#g;
|
||||||
|
s#find /app#find $TMP_DIR/arm64-root/app#g;
|
||||||
|
s#APP_DIR=\"/app\"#APP_DIR=\"$TMP_DIR/arm64-root/app\"#g;
|
||||||
|
s#under /app#under $TMP_DIR/arm64-root/app#g;
|
||||||
|
" "$QNAP_PATCH_DIR/reactive-resume-runtime-patch.sh" > "$TMP_DIR/runtime-patch-arm64-test.sh"
|
||||||
|
sh "$TMP_DIR/runtime-patch-arm64-test.sh" >/tmp/reactive-resume-arm64-runtime.log 2>&1 || {
|
||||||
|
cat /tmp/reactive-resume-arm64-runtime.log >&2
|
||||||
|
fail "arm64 离线运行 runtime patch 失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -R 'rr-browser-buffer-polyfill' "$ARM64_ASSETS_DIR" >/dev/null \
|
||||||
|
|| fail "arm64 public PDF bundle 未注入 Buffer polyfill"
|
||||||
|
grep -R -F 'replace(/[\\/:*?"<>|]/g' "$ARM64_ASSETS_DIR" >/dev/null \
|
||||||
|
|| fail "arm64 文件名 bundle 未改为按标题下载"
|
||||||
|
if grep -R -E "index-[A-Za-z0-9_-]+\\.js\\?v=rr-filename-title" "$ARM64_ASSETS_DIR" >/dev/null 2>&1; then
|
||||||
|
fail "arm64 补丁错误地给 index 主入口追加了 rr-filename-title 缓存标记"
|
||||||
|
fi
|
||||||
|
find "$TMP_DIR/arm64-root/app" -type f -exec grep -qE 'index-[A-Za-z0-9_-]+-rr[0-9a-z]+\.js' {} \; -print -quit \
|
||||||
|
| grep -q . \
|
||||||
|
|| fail "arm64 补丁未改用防缓存的 index 主入口文件名"
|
||||||
|
find "$TMP_DIR/arm64-root/app" -path '*/sw.js' -type f -print0 | xargs -0 grep -l 'disable stale PWA caches' >/dev/null \
|
||||||
|
|| fail "arm64 sw.js 未替换为清理旧缓存版本"
|
||||||
|
grep -q 'function generateFilename(prefix, extension)' "$ARM64_FILENAME_ENTRY" \
|
||||||
|
|| fail "arm64 server entry 未包含 generateFilename"
|
||||||
|
grep -F 'filename.replace(/[\\/:*?"<>|]/g' "$ARM64_FILENAME_ENTRY" >/dev/null \
|
||||||
|
|| fail "arm64 server entry 未改为按标题生成下载文件名"
|
||||||
|
|
||||||
|
perl -0pe "
|
||||||
|
s#/app/apps#$TMP_DIR/arm64-root/app/apps#g;
|
||||||
|
s#cd /app#cd $TMP_DIR/arm64-root/app#g;
|
||||||
|
s#find /app#find $TMP_DIR/arm64-root/app#g;
|
||||||
|
s#under /app#under $TMP_DIR/arm64-root/app#g;
|
||||||
|
" "$QNAP_PATCH_DIR/reactive-resume-entrypoint.sh" > "$TMP_DIR/entrypoint-arm64-test.sh"
|
||||||
|
mkdir -p "$TMP_DIR/fakebin"
|
||||||
|
{
|
||||||
|
printf '#!/bin/sh\n'
|
||||||
|
printf 'printf "PWD=%%s\\n" "$PWD" > "%s/entrypoint-result.txt"\n' "$TMP_DIR"
|
||||||
|
printf 'printf "ARGS=%%s\\n" "$*" >> "%s/entrypoint-result.txt"\n' "$TMP_DIR"
|
||||||
|
} > "$TMP_DIR/fakebin/docker-entrypoint.sh"
|
||||||
|
chmod +x "$TMP_DIR/fakebin/docker-entrypoint.sh"
|
||||||
|
PATH="$TMP_DIR/fakebin:$PATH" sh "$TMP_DIR/entrypoint-arm64-test.sh" >/tmp/reactive-resume-arm64-entrypoint.log 2>&1 || {
|
||||||
|
cat /tmp/reactive-resume-arm64-entrypoint.log >&2
|
||||||
|
fail "arm64 entrypoint 选择测试失败"
|
||||||
|
}
|
||||||
|
|
||||||
|
grep -q "PWD=$EXPECTED_ENTRYPOINT_PWD" "$TMP_DIR/entrypoint-result.txt" \
|
||||||
|
|| fail "arm64 entrypoint 未切换到预期目录:$EXPECTED_ENTRYPOINT_PWD"
|
||||||
|
grep -q "ARGS=$EXPECTED_ENTRYPOINT_ARGS" "$TMP_DIR/entrypoint-result.txt" \
|
||||||
|
|| fail "arm64 entrypoint 未选择预期入口:$EXPECTED_ENTRYPOINT_ARGS"
|
||||||
|
|
||||||
|
log "清理 direct 测试容器"
|
||||||
|
cleanup_direct
|
||||||
|
|
||||||
|
log "全部测试通过"
|
||||||
|
printf 'direct health: %s\n' "$(cat /tmp/reactive-resume-health.json)"
|
||||||
|
printf 'arm64 digest: %s\n' "$ARM64_DIGEST"
|
||||||
Reference in New Issue
Block a user