Skip to content

Commit 4e26a0a

Browse files
Fix the live 500: import __exportAll from the runtime chunk.
Rolldown copied the helper into the router chunk. The sibling then imported that copy, those two files imported each other, and Nitro loaded the parent first — so __exportAll was still undefined and Vercel returned HTTP 500. Rewrite the import to _runtime.mjs after the Nitro emit. Home, the pack, the wall, about, and GOAT all render HTML again.
1 parent 7ec5ea1 commit 4e26a0a

8 files changed

Lines changed: 178 additions & 23 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
"scripts": {
3636
"dev": "node scripts/with-app-env.mjs vite dev --host 0.0.0.0 --port 8080",
37-
"build": "node scripts/with-app-env.mjs vite build && npm run db:migrate",
37+
"build": "node scripts/with-app-env.mjs vite build && node scripts/fix-ssr-exportall.mjs && npm run db:migrate",
3838
"db:migrate": "node scripts/migrate.mjs",
3939
"build:dev": "node scripts/with-app-env.mjs vite build --mode development",
4040
"preview": "node scripts/with-app-env.mjs vite preview",

scripts/fix-ssr-exportall.mjs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Rolldown sometimes copies `__exportAll` into an app chunk and then a sibling
4+
* chunk imports that copy. Those two files import each other, so when Nitro
5+
* loads the parent first the helper is still `undefined` — Vercel 500s with
6+
* `__exportAll is not a function`.
7+
*
8+
* Rewrite those imports to the real helper in `_runtime.mjs`, which has no
9+
* cycle.
10+
*/
11+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
12+
import { dirname, join, relative } from "node:path";
13+
import { fileURLToPath } from "node:url";
14+
15+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
16+
17+
export function vercelFuncDir(root = ROOT) {
18+
return join(root, ".vercel/output/functions/__server.func");
19+
}
20+
21+
function walkMjs(dir, out = []) {
22+
if (!existsSync(dir)) return out;
23+
for (const ent of readdirSync(dir, { withFileTypes: true })) {
24+
const p = join(dir, ent.name);
25+
if (ent.isDirectory()) walkMjs(p, out);
26+
else if (ent.name.endsWith(".mjs")) out.push(p);
27+
}
28+
return out;
29+
}
30+
31+
function findRuntime(funcDir) {
32+
const p = join(funcDir, "_runtime.mjs");
33+
return existsSync(p) ? p : null;
34+
}
35+
36+
/** Strip `Foo as __exportAll` / `__exportAll` from an `import { … }` spec list. */
37+
function stripExportAllSpec(spec) {
38+
const kept = spec
39+
.split(",")
40+
.map((s) => s.trim())
41+
.filter(Boolean)
42+
.filter((s) => s !== "__exportAll" && !/\bas\s+__exportAll$/.test(s));
43+
return kept;
44+
}
45+
46+
/**
47+
* Merge `n as __exportAll` into an existing `_runtime.mjs` import, or prepend
48+
* a new one. Returns the patched source, or null if nothing changed.
49+
*/
50+
export function rewriteExportAllImports(code, runtimeSpecifier) {
51+
if (!code.includes("__exportAll(")) return null;
52+
if (/var __exportAll\s*=/.test(code) || /function __exportAll\s*\(/.test(code)) return null;
53+
54+
let stripped = false;
55+
let next = code.replace(
56+
/import\s*\{([^}]*)\}\s*from\s*(["'])([^"']+)\2;?/g,
57+
(full, spec, quote, from) => {
58+
if (!spec.includes("__exportAll")) return full;
59+
if (from.includes("_runtime")) return full;
60+
const kept = stripExportAllSpec(spec);
61+
stripped = true;
62+
if (kept.length === 0) return "";
63+
return `import { ${kept.join(", ")} } from ${quote}${from}${quote};`;
64+
},
65+
);
66+
if (!stripped) return null;
67+
68+
if (
69+
/from\s*(["'])[^"']*_runtime[^"']*\1/.test(next) &&
70+
/as\s+__exportAll/.test(
71+
next.match(/import\s*\{([^}]*)\}\s*from\s*["'][^"']*_runtime[^"']*["']/)?.[1] ?? "",
72+
)
73+
) {
74+
return next;
75+
}
76+
77+
const runtimeImport = `import { n as __exportAll } from ${JSON.stringify(runtimeSpecifier)};`;
78+
const runtimeRe = /import\s*\{([^}]*)\}\s*from\s*(["'])([^"']*_runtime[^"']*)\2;?/;
79+
if (runtimeRe.test(next)) {
80+
next = next.replace(runtimeRe, (full, spec, quote, from) => {
81+
if (spec.includes("__exportAll")) return full;
82+
const specs = spec
83+
.split(",")
84+
.map((s) => s.trim())
85+
.filter(Boolean);
86+
specs.push("n as __exportAll");
87+
return `import { ${specs.join(", ")} } from ${quote}${from}${quote};`;
88+
});
89+
return next;
90+
}
91+
92+
const firstImport = next.search(/^import\s/m);
93+
if (firstImport === -1) return `${runtimeImport}\n${next}`;
94+
return `${next.slice(0, firstImport)}${runtimeImport}\n${next.slice(firstImport)}`;
95+
}
96+
97+
export function patchFuncDir(funcDir) {
98+
const runtime = findRuntime(funcDir);
99+
if (!runtime) return [];
100+
const patched = [];
101+
for (const file of walkMjs(funcDir)) {
102+
const before = readFileSync(file, "utf8");
103+
const rel = relative(dirname(file), runtime).replaceAll("\\", "/");
104+
const specifier = rel.startsWith(".") ? rel : `./${rel}`;
105+
const after = rewriteExportAllImports(before, specifier);
106+
if (after && after !== before) {
107+
writeFileSync(file, after);
108+
patched.push(relative(funcDir, file));
109+
}
110+
}
111+
return patched;
112+
}
113+
114+
export function patchWorkspace(root = ROOT) {
115+
return patchFuncDir(vercelFuncDir(root));
116+
}
117+
118+
if (import.meta.url === `file://${process.argv[1]}`) {
119+
const patched = patchWorkspace();
120+
if (patched.length === 0) {
121+
console.log("[fix-ssr-exportall] nothing to patch");
122+
} else {
123+
for (const f of patched) console.log(`[fix-ssr-exportall] ${f}`);
124+
}
125+
}

scripts/fix-ssr-exportall.test.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { test } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { rewriteExportAllImports } from "./fix-ssr-exportall.mjs";
4+
5+
test("rewrites sibling __exportAll import to runtime", () => {
6+
const src = `import { i as __toESM } from "../_runtime.mjs";
7+
import { A as __exportAll, B as WNBA } from "./router-abc.mjs";
8+
var router_exports = /* @__PURE__ */ __exportAll({ getRouter: () => getRouter });
9+
`;
10+
const out = rewriteExportAllImports(src, "../_runtime.mjs");
11+
assert.ok(out);
12+
assert.equal(out.includes("A as __exportAll"), false);
13+
assert.match(out, /n as __exportAll/);
14+
assert.match(out, /from "\.\.\/_runtime\.mjs"/);
15+
assert.match(out, /B as WNBA/);
16+
assert.match(out, /__exportAll\(\{/);
17+
});
18+
19+
test("leaves chunks that define the helper alone", () => {
20+
const src = `var __exportAll = (all, no_symbols) => { return {}; };
21+
var x = __exportAll({ a: () => 1 });
22+
`;
23+
assert.equal(rewriteExportAllImports(src, "../_runtime.mjs"), null);
24+
});
25+
26+
test("leaves runtime imports alone", () => {
27+
const src = `import { n as __exportAll } from "../_runtime.mjs";
28+
var ssr_exports = /* @__PURE__ */ __exportAll({ default: () => d });
29+
`;
30+
assert.equal(rewriteExportAllImports(src, "../_runtime.mjs"), null);
31+
});

src/components/crest.tsx

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
11
import type { ReactNode } from "react";
22
import { marksFor, type CrestOp } from "@/lib/crest-marks";
33
import { clubAbbr as clubCode, type Era } from "@/lib/nba";
4-
import {
5-
initials,
6-
plateFor,
7-
plateForPlayer,
8-
plateCrop,
9-
cardSerial,
10-
nameParts,
11-
PLATES,
12-
} from "@/lib/plates";
134
import { cn } from "@/lib/utils";
145

15-
export {
16-
initials,
17-
plateFor,
18-
plateForPlayer,
19-
plateCrop,
20-
cardSerial,
21-
nameParts,
22-
PLATES,
23-
};
24-
256
function Mark({ children, className }: { children: ReactNode; className?: string }) {
267
return (
278
<svg viewBox="0 0 40 40" className={cn("size-10 shrink-0", className)} aria-hidden="true">

src/components/name-plate.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { initials } from "@/components/crest";
1+
import { initials } from "@/lib/plates";
22
import { cn } from "@/lib/utils";
33

44
const SIZES = {

src/components/player-card.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CSSProperties } from "react";
2-
import { Crest, cardSerial, initials, nameParts, plateCrop, plateForPlayer } from "@/components/crest";
2+
import { Crest } from "@/components/crest";
3+
import { cardSerial, initials, nameParts, plateCrop, plateForPlayer } from "@/lib/plates";
34
import { CourtBack, HoopMark, PlateRosette } from "@/components/court-mark";
45
import { emblemSrc } from "@/components/pack-emblem";
56
import { clubName, houseInk } from "@/lib/house-ink";

src/routes/games.goat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createFileRoute, Link } from "@tanstack/react-router";
22
import { useEffect, useMemo, useState } from "react";
3-
import { initials, cardSerial } from "@/components/crest";
3+
import { initials, cardSerial } from "@/lib/plates";
44
import { DraftFilters, GameBar, StepKicker } from "@/components/game-bar";
55
import { LithographLoader } from "@/components/lithograph-loader";
66
import { MathSheet } from "@/components/math-sheet";

vite.config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { nitro } from "nitro/vite";
1010
import { grokPwaPlugin } from "./scripts/grok-pwa-plugin.mjs";
1111
// @ts-expect-error JS plugin alongside the TS vite config
1212
import { appEnvPlugin } from "./scripts/app-env-plugin.mjs";
13+
// @ts-expect-error JS plugin alongside the TS vite config
14+
import { patchWorkspace } from "./scripts/fix-ssr-exportall.mjs";
1315
import { isMigrationFile } from "./scripts/migration-plan.mjs";
1416

1517
/** The files `src/lib/db.ts` globs — same directory, same non-recursive scope. */
@@ -30,6 +32,20 @@ function hasGlobbedMigrations(root: string): boolean {
3032
* migrations — no schema to apply — skips it entirely rather than paying for a
3133
* PGLite instance it never queries.
3234
*/
35+
function fixSsrExportAllPlugin(): Plugin {
36+
return {
37+
name: "fix-ssr-exportall-cycle",
38+
apply: "build",
39+
enforce: "post",
40+
closeBundle() {
41+
const patched = patchWorkspace();
42+
if (patched.length > 0) {
43+
console.log("[fix-ssr-exportall]", patched.join(", "));
44+
}
45+
},
46+
};
47+
}
48+
3349
function pgliteBootstrapPlugin(): Plugin {
3450
return {
3551
name: "app-builder:pglite-bootstrap",
@@ -179,5 +195,6 @@ export default defineConfig(({ command, isPreview }) => ({
179195
]
180196
: []),
181197
viteReact(),
198+
fixSsrExportAllPlugin(),
182199
],
183200
}));

0 commit comments

Comments
 (0)