|
| 1 | +/** |
| 2 | + * www theme-token gate: theme values in the site must come from open-props |
| 3 | + * tokens (packages/ui/src/open-props-tokens.css) and the www alias layer |
| 4 | + * (www/vite.config.ts), never from hardcoded literals. |
| 5 | + * |
| 6 | + * Rules for sources under www/app/ and www/islands/: |
| 7 | + * 1. No hex color literals. 6/8-digit forms always fail; 3/4-digit forms |
| 8 | + * fail only on lines carrying a CSS property keyword, so issue |
| 9 | + * references like `#390` in prose stay legal. |
| 10 | + * 2. No `font-family` declarations that bypass var(); `inherit` is allowed. |
| 11 | + * 3. No `font-size` literals in px/rem/em outside var(); clamp() fluid |
| 12 | + * typography is allowed. |
| 13 | + * |
| 14 | + * Token definitions belong in www/vite.config.ts (site aliases) or |
| 15 | + * packages/ui/src/open-props-tokens.css (source of truth). |
| 16 | + */ |
| 17 | + |
| 18 | +import { walk } from '@std/fs/walk'; |
| 19 | + |
| 20 | +const SCAN_ROOTS = ['www/app']; |
| 21 | +const SOURCE = /\.(ts|tsx)$/; |
| 22 | +const HEX_LONG = /#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/; |
| 23 | +const HEX_SHORT = /#(?:[0-9a-fA-F]{3,4})\b/; |
| 24 | +const CSS_KEYWORD = /color|background|border|shadow|fill|stroke|gradient|outline/i; |
| 25 | +const FONT_FAMILY = /font-family\s*:\s*([^;]+);/; |
| 26 | +const FONT_SIZE_LITERAL = /font-size\s*:\s*[0-9.]+(?:px|rem|em)\b/; |
| 27 | + |
| 28 | +export interface ThemeTokenFailure { |
| 29 | + file: string; |
| 30 | + line: number; |
| 31 | + rule: string; |
| 32 | + text: string; |
| 33 | +} |
| 34 | + |
| 35 | +export function findThemeTokenFailures( |
| 36 | + file: string, |
| 37 | + lines: string[], |
| 38 | +): ThemeTokenFailure[] { |
| 39 | + const failures: ThemeTokenFailure[] = []; |
| 40 | + for (let i = 0; i < lines.length; i++) { |
| 41 | + const text = lines[i]; |
| 42 | + if (HEX_LONG.test(text) || (HEX_SHORT.test(text) && CSS_KEYWORD.test(text))) { |
| 43 | + failures.push({ file, line: i + 1, rule: 'hex-literal', text: text.trim() }); |
| 44 | + } |
| 45 | + const family = FONT_FAMILY.exec(text); |
| 46 | + if (family && !family[1].includes('var(') && !family[1].includes('inherit')) { |
| 47 | + failures.push({ file, line: i + 1, rule: 'font-family-literal', text: text.trim() }); |
| 48 | + } |
| 49 | + if (FONT_SIZE_LITERAL.test(text)) { |
| 50 | + failures.push({ file, line: i + 1, rule: 'font-size-literal', text: text.trim() }); |
| 51 | + } |
| 52 | + } |
| 53 | + return failures; |
| 54 | +} |
| 55 | + |
| 56 | +async function main(): Promise<void> { |
| 57 | + const failures: ThemeTokenFailure[] = []; |
| 58 | + for (const root of SCAN_ROOTS) { |
| 59 | + for await (const entry of walk(root, { exts: ['.ts', '.tsx'] })) { |
| 60 | + if (!SOURCE.test(entry.path)) continue; |
| 61 | + if (entry.path.includes('/data/_generated-')) continue; |
| 62 | + const text = await Deno.readTextFile(entry.path); |
| 63 | + failures.push(...findThemeTokenFailures(entry.path, text.split('\n'))); |
| 64 | + } |
| 65 | + } |
| 66 | + if (failures.length > 0) { |
| 67 | + console.error('www theme token check failed:'); |
| 68 | + for (const failure of failures) { |
| 69 | + console.error(`- ${failure.file}:${failure.line} [${failure.rule}] ${failure.text}`); |
| 70 | + } |
| 71 | + console.error( |
| 72 | + 'Theme values must come from open-props tokens or the www/vite.config.ts alias layer.', |
| 73 | + ); |
| 74 | + Deno.exit(1); |
| 75 | + } |
| 76 | + console.log('www theme token check passed.'); |
| 77 | +} |
| 78 | + |
| 79 | +if (import.meta.main) { |
| 80 | + await main(); |
| 81 | +} |
0 commit comments