A page from before a deploy can still find its chunks, or reload once
Sentry WIZWAR-5: a table open through a deploy asked for the replay module by its old hashed name, the deploy had deleted it, and the server answered with the app shell as text/html — "not a valid JavaScript MIME type". Three guards: deploys keep old chunks for a week instead of deleting them; a missing asset is a plain 404, never the shell; and the client reloads once when Vite reports a failed import, so the live bundle takes over. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
fa5a5a07a7
commit
0911312f98
+4
-1
@@ -4,11 +4,14 @@ set -euo pipefail
|
|||||||
HOST="${1:?usage: deploy.sh <droplet-ip-or-host>}"
|
HOST="${1:?usage: deploy.sh <droplet-ip-or-host>}"
|
||||||
|
|
||||||
npm run build --workspace=@wizwar/web
|
npm run build --workspace=@wizwar/web
|
||||||
rsync -az --delete \
|
# Old hashed chunks stay a week: a page loaded before this deploy can still
|
||||||
|
# fetch the module it was built against instead of failing mid-game.
|
||||||
|
rsync -az --delete --filter='P packages/web/dist/assets/*' \
|
||||||
--exclude node_modules --exclude data/ --exclude .git --exclude research \
|
--exclude node_modules --exclude data/ --exclude .git --exclude research \
|
||||||
./ "root@$HOST:/opt/wizwar/"
|
./ "root@$HOST:/opt/wizwar/"
|
||||||
ssh "root@$HOST" '
|
ssh "root@$HOST" '
|
||||||
cd /opt/wizwar && npm install --no-audit --no-fund
|
cd /opt/wizwar && npm install --no-audit --no-fund
|
||||||
|
find /opt/wizwar/packages/web/dist/assets -type f -mtime +7 -delete
|
||||||
chown -R wizwar:wizwar /opt/wizwar
|
chown -R wizwar:wizwar /opt/wizwar
|
||||||
cp /opt/wizwar/deploy/wizwar.service /etc/systemd/system/wizwar.service
|
cp /opt/wizwar/deploy/wizwar.service /etc/systemd/system/wizwar.service
|
||||||
# Cron runs the backup and the rollup from /usr/local/bin: install
|
# Cron runs the backup and the rollup from /usr/local/bin: install
|
||||||
|
|||||||
@@ -387,7 +387,14 @@ const httpServer = createServer((req, res) => {
|
|||||||
res.writeHead(403).end();
|
res.writeHead(403).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!existsSync(file)) file = join(staticRoot, "index.html"); // SPA fallback
|
if (!existsSync(file)) {
|
||||||
|
// A hashed asset that is gone is gone: a page built before the last
|
||||||
|
// deploy asking for its old chunk gets a plain 404 it can act on,
|
||||||
|
// never the app shell as text/html. Every other unknown path is the
|
||||||
|
// app's to route.
|
||||||
|
if (url.startsWith("/assets/")) { res.writeHead(404, { "content-type": "text/plain" }).end("gone"); return; }
|
||||||
|
file = join(staticRoot, "index.html");
|
||||||
|
}
|
||||||
// Resolve symlinks and re-verify the real location stays inside the root.
|
// Resolve symlinks and re-verify the real location stays inside the root.
|
||||||
const real = realpathSync(file);
|
const real = realpathSync(file);
|
||||||
if (real !== staticRoot && !real.startsWith(staticRoot + sep)) {
|
if (real !== staticRoot && !real.startsWith(staticRoot + sep)) {
|
||||||
|
|||||||
@@ -7,6 +7,19 @@ if (import.meta.env.VITE_SENTRY_DSN) {
|
|||||||
Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, environment: "production" });
|
Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, environment: "production" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A page loaded before a deploy may ask for a chunk the deploy retired.
|
||||||
|
// Vite reports the failed import here; one reload fetches the live bundle.
|
||||||
|
// The flag keeps a broken build from reloading forever.
|
||||||
|
window.addEventListener("vite:preloadError", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (sessionStorage.getItem("wizwar-reloaded")) return;
|
||||||
|
sessionStorage.setItem("wizwar-reloaded", "1");
|
||||||
|
} catch { /* storage may be unavailable; reload anyway */ }
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
try { sessionStorage.removeItem("wizwar-reloaded"); } catch { /* ignore */ }
|
||||||
|
|
||||||
// /watch/<id> is the public share page — one turn, anyone may look. It
|
// /watch/<id> is the public share page — one turn, anyone may look. It
|
||||||
// loads its own component so the full app (and its live socket appetite)
|
// loads its own component so the full app (and its live socket appetite)
|
||||||
// stays out of a shared link's way.
|
// stays out of a shared link's way.
|
||||||
|
|||||||
Reference in New Issue
Block a user