What happened?
The web app crashes on every page load with an uncaught syntax error, so the UI never renders (white screen / Next.js error overlay):
Uncaught SyntaxError: '' literal not terminated before end of script
at (app-pages-browser)/./src/hooks/useProjectRunStatuses.ts (…SettingsDia-16f400.js:11103:1)
at … EntryNavRail.tsx → EntryShell.tsx → EntryView.tsx → App.tsx → ClientApp → Page
The stack is always the same, it is deterministic (not a flaky dev-server race), and it survives a full tools-dev restart.
Root cause
apps/web/src/hooks/useProjectRunStatuses.ts contains raw NUL bytes (0x00) inside string literals, where the escape \u0000 should be. The byte 0x00 is committed directly into the source (GitHub serves the file as application/octet-stream because of it):
const idsKey = useMemo(() => [...projectIds].sort().join('<0x00>'), [projectIds]); // line 238
...
const subscription: Subscription = { ids: idsKey.split('<0x00>'), contextRef }; // line 245
...
for (const projectId of idsKey.split('<0x00>')) { // line 253
(<0x00> = a literal NUL byte, not text.)
Modern Chromium/V8 rejects a raw control byte inside a string literal as an unterminated string, which produces '' literal not terminated before end of script. Node's older V8 (e.g. Node 24, V8 13.6) tolerates it, which is why this only surfaces in the browser.
Steps to reproduce
- Check out
main at 1b47e60 (2026-09-24).
corepack enable && pnpm install
pnpm tools-dev run web (or start web) and open http://127.0.0.1:5175.
- Observe the page fails to load with the syntax error above.
Expected behavior
The app should render normally. The join/split separator should be the escape \u0000, not a raw NUL byte.
Proposed fix
Replace the 3 raw 0x00 bytes with the \u0000 escape:
- const idsKey = useMemo(() => [...projectIds].sort().join('^@'), [projectIds]);
+ const idsKey = useMemo(() => [...projectIds].sort().join('\u0000'), [projectIds]);
- const subscription: Subscription = { ids: idsKey.split('^@'), contextRef };
+ const subscription: Subscription = { ids: idsKey.split('\u0000'), contextRef };
- for (const projectId of idsKey.split('^@')) {
+ for (const projectId of idsKey.split('\u0000')) {
This matches the escaping already used elsewhere in the codebase (e.g. join('\u0001'), join('\u0002')).
Environment
- OpenDesign version: 0.23.1 (commit
1b47e60bd46641469fcd8b69c496c4e3a548bc28)
- Platform: Linux (Fedora 44)
- Toolchain: Node v24.21.0, pnpm 10.33.2
- Web: Next.js 16.2.6 (Webpack dev server)
- Browser: Chromium-based (Brave) — the error is V8-version-dependent
Verification
Confirming the committed blob contains the raw bytes:
$ git show HEAD:apps/web/src/hooks/useProjectRunStatuses.ts | tr -cd '\000' | wc -c
3
After replacing them with \u0000, the page loads (GET / → 200) with no error.
What happened?
The web app crashes on every page load with an uncaught syntax error, so the UI never renders (white screen / Next.js error overlay):
The stack is always the same, it is deterministic (not a flaky dev-server race), and it survives a full
tools-devrestart.Root cause
apps/web/src/hooks/useProjectRunStatuses.tscontains raw NUL bytes (0x00) inside string literals, where the escape\u0000should be. The byte0x00is committed directly into the source (GitHub serves the file asapplication/octet-streambecause of it):(
<0x00>= a literal NUL byte, not text.)Modern Chromium/V8 rejects a raw control byte inside a string literal as an unterminated string, which produces
'' literal not terminated before end of script. Node's older V8 (e.g. Node 24, V8 13.6) tolerates it, which is why this only surfaces in the browser.Steps to reproduce
mainat1b47e60(2026-09-24).corepack enable && pnpm installpnpm tools-dev run web(orstart web) and openhttp://127.0.0.1:5175.Expected behavior
The app should render normally. The join/split separator should be the escape
\u0000, not a raw NUL byte.Proposed fix
Replace the 3 raw
0x00bytes with the\u0000escape:This matches the escaping already used elsewhere in the codebase (e.g.
join('\u0001'),join('\u0002')).Environment
1b47e60bd46641469fcd8b69c496c4e3a548bc28)Verification
Confirming the committed blob contains the raw bytes:
After replacing them with
\u0000, the page loads (GET / → 200) with no error.