Skip to content

Commit a777968

Browse files
authored
Fix theme flash (#3093)
The inline script was being blocked on Vercel by CSP like it would in prod. Moving it to a separate file fixes this. Ignore the debug-e2e skill, I just had that in flight and it feels stupid to make a separate PR.
1 parent 0e77012 commit a777968

File tree

4 files changed

+69
-16
lines changed

4 files changed

+69
-16
lines changed

.claude/skills/debug-e2e/SKILL.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@ Use this skill when investigating flaky Playwright E2E test failures from CI.
99

1010
## Workflow
1111

12+
### 0. Catalogue recent CI flakes (optional)
13+
14+
When asked to survey recent failures rather than debug a specific one, build a
15+
table of all Playwright flakes across recent CI runs.
16+
17+
```bash
18+
# List recent failed runs
19+
gh run list --limit 80 --json databaseId,headBranch,conclusion,displayTitle,createdAt \
20+
| jq -r '.[] | select(.conclusion == "failure") | "\(.databaseId)\t\(.headBranch)\t\(.displayTitle)\t\(.createdAt)"'
21+
22+
# For each failed run, find which Playwright jobs failed
23+
gh run view <RUN_ID> --json jobs \
24+
| jq -r '.jobs[] | select(.conclusion == "failure") | "\(.name)\t\(.conclusion)"'
25+
26+
# Extract test names and error summaries from failed runs
27+
gh run view <RUN_ID> --log-failed 2>&1 | rg '› .*e2e\.ts.*›' | rg -v 'Retry'
28+
```
29+
30+
Produce a markdown table with columns: Test, Browser, Run ID, Date.
31+
Sort by most recent first. Distinguish real flakes from bugs that were fixed
32+
between pushes (a test that fails across all browsers in one run and then
33+
disappears was likely a real bug, not a flake).
34+
1235
### 1. Get CI failure details
1336

1437
```bash

index.html

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,7 @@
1818
<link rel="icon" type="image/svg+xml" href="./app/assets/favicon.svg" />
1919
<link rel="icon" type="image/png" href="./app/assets/favicon.png" />
2020

21-
<!-- Set theme before first paint to prevent flash. Mirrors logic in app/stores/theme.ts. -->
22-
<script>
23-
;(function () {
24-
var p = 'dark'
25-
try {
26-
var raw = localStorage.getItem('theme-preference')
27-
var stored = raw ? JSON.parse(raw) : null
28-
// match zustand persist format
29-
if (stored && stored.state && stored.state.theme) p = stored.state.theme
30-
} catch (e) {}
31-
if (p === 'system')
32-
p = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
33-
document.documentElement.dataset.theme = p
34-
})()
35-
</script>
21+
<!-- theme-init.js injected by vite plugin to avoid Vite trying to bundle it -->
3622
</head>
3723
<body>
3824
<div id="root"></div>

public/theme-init.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
// Set theme before first paint to prevent flash of wrong color scheme.
10+
// Mirrors logic in app/stores/theme.ts. Must stay in sync.
11+
;(function () {
12+
var p = 'dark'
13+
try {
14+
var raw = localStorage.getItem('theme-preference')
15+
var stored = raw ? JSON.parse(raw) : null
16+
// match zustand persist format
17+
if (stored && stored.state && stored.state.theme) p = stored.state.theme
18+
} catch (_e) {}
19+
if (p === 'system')
20+
p = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
21+
document.documentElement.dataset.theme = p
22+
})()

vite.config.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { randomBytes } from 'crypto'
8+
import { createHash, randomBytes } from 'crypto'
9+
import { readFileSync } from 'fs'
910
import { resolve } from 'path'
1011

1112
import tailwindcss from '@tailwindcss/vite'
@@ -130,6 +131,27 @@ export default defineConfig(({ mode }) => ({
130131
name: 'inject-html-tags',
131132
transformIndexHtml: () => (process.env.VERCEL ? previewTags : []),
132133
},
134+
{
135+
// Inject theme-init.js as a classic (non-module) render-blocking script
136+
// so it sets data-theme before first paint. It lives in public/ so it
137+
// passes CSP default-src 'self'. We inject it here rather than putting
138+
// it in index.html because Vite tries to bundle any <script src> it finds
139+
// there. Content hash query param handles cache-busting since public/
140+
// files aren't fingerprinted by Vite. We cache static assets for a year,
141+
// so we need the hash.
142+
name: 'theme-init',
143+
transformIndexHtml() {
144+
const content = readFileSync(resolve(__dirname, 'public/theme-init.js'))
145+
const hash = createHash('sha256').update(content).digest('hex').slice(0, 8)
146+
return [
147+
{
148+
injectTo: 'head-prepend',
149+
tag: 'script',
150+
attrs: { src: `/theme-init.js?v=${hash}` },
151+
},
152+
]
153+
},
154+
},
133155
react(),
134156
apiMode === 'remote' && basicSsl(),
135157
],

0 commit comments

Comments
 (0)