|
4 | 4 | * |
5 | 5 | * Runs against a live server (default http://localhost:3000, override with |
6 | 6 | * `BASE`). Designed to be run after `bun run start` — checks sitemap, robots, |
7 | | - * RSS feeds, OG images, canonicals, JSON-LD on posts, and internal links. |
| 7 | + * RSS feeds, OG images, canonicals, JSON-LD on posts, internal links, and |
| 8 | + * theme-token parity between `src/lib/theme.ts` and `src/app/globals.css`. |
8 | 9 | * |
9 | 10 | * Usage: |
10 | 11 | * bun run start & |
|
13 | 14 | * Exits 1 if any check fails. |
14 | 15 | */ |
15 | 16 |
|
| 17 | +import { readFile } from "node:fs/promises"; |
| 18 | +import path from "node:path"; |
| 19 | +import { THEME } from "../src/lib/theme"; |
| 20 | + |
16 | 21 | const BASE = process.env.BASE ?? "http://localhost:3000"; |
17 | 22 |
|
18 | 23 | type CheckResult = { |
@@ -369,6 +374,41 @@ async function checkInternalLinks( |
369 | 374 | pass("internal links", `${targets.size} checked`); |
370 | 375 | } |
371 | 376 |
|
| 377 | +// Asserts that the TS-side palette in `src/lib/theme.ts` matches the |
| 378 | +// CSS-side declarations in `globals.css`. Drift here would mean OG image |
| 379 | +// generation paints with one shade and the site renders with another — |
| 380 | +// exactly the bug this check is here to catch. Currently only |
| 381 | +// `THEME.background` has a corresponding CSS variable (`--background` in |
| 382 | +// `:root`); `THEME.foreground` is white-on-dark and uses Tailwind's |
| 383 | +// built-in `text-white`, so there's nothing to assert against in CSS. |
| 384 | +async function checkThemeParity(): Promise<void> { |
| 385 | + let css: string; |
| 386 | + try { |
| 387 | + css = await readFile( |
| 388 | + path.join(process.cwd(), "src", "app", "globals.css"), |
| 389 | + "utf-8" |
| 390 | + ); |
| 391 | + } catch (err) { |
| 392 | + fail( |
| 393 | + "theme parity", |
| 394 | + `failed to read globals.css: ${err instanceof Error ? err.message : String(err)}` |
| 395 | + ); |
| 396 | + return; |
| 397 | + } |
| 398 | + const re = new RegExp( |
| 399 | + `--background:\\s*${THEME.background.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")};`, |
| 400 | + "i" |
| 401 | + ); |
| 402 | + if (!re.test(css)) { |
| 403 | + fail( |
| 404 | + "theme parity", |
| 405 | + `--background in globals.css does not match THEME.background (${THEME.background})` |
| 406 | + ); |
| 407 | + return; |
| 408 | + } |
| 409 | + pass("theme parity", `--background = ${THEME.background}`); |
| 410 | +} |
| 411 | + |
372 | 412 | // The /api/subscribe handler instantiates `new Resend(...)` *inside* the |
373 | 413 | // request handler so the build doesn't fail when RESEND_API_KEY is unset. |
374 | 414 | // When the env var is missing it returns 503 with a JSON error body. We |
@@ -435,6 +475,7 @@ async function main(): Promise<void> { |
435 | 475 | await checkRssXml(); |
436 | 476 | await checkRssJson(); |
437 | 477 | await checkRssAtom(); |
| 478 | + await checkThemeParity(); |
438 | 479 | await checkSubscribe(); |
439 | 480 |
|
440 | 481 | const htmlByPath = new Map<string, string>(); |
|
0 commit comments