|
1 | 1 | #!/usr/bin/env node |
| 2 | +// Asserts that utility classes used in the chat-panel HTML/JS are present in the compiled CSS. |
| 3 | +// Dependency-free script to prevent silent failures where the .vsix builds but lacks theme colors. |
2 | 4 |
|
3 | | -/** |
4 | | - * Verifies that the compiled chat-panel.css retains VS Code theme tokens. |
5 | | - * |
6 | | - * Tailwind v4 can silently strip custom CSS variables when purge detects |
7 | | - * them as unused. This script ensures the built CSS still contains |
8 | | - * --vscode-* property references so the webview can read them at runtime. |
9 | | - */ |
| 5 | +import {readFileSync, existsSync} from 'node:fs'; |
| 6 | +import {join, dirname} from 'node:path'; |
| 7 | +import {fileURLToPath} from 'node:url'; |
10 | 8 |
|
11 | | -const fs = require('node:fs'); |
12 | | -const path = require('node:path'); |
| 9 | +const here = dirname(fileURLToPath(import.meta.url)); |
| 10 | +const pkgRoot = join(here, '..'); |
| 11 | +const cssPath = join(pkgRoot, 'media', 'chat-panel.css'); |
| 12 | +const htmlPath = join(pkgRoot, 'media', 'chat-panel.html'); |
| 13 | +const jsPath = join(pkgRoot, 'media', 'chat-panel.js'); |
13 | 14 |
|
14 | | -const CSS_PATH = path.resolve(__dirname, '..', 'media', 'chat-panel.css'); |
15 | | - |
16 | | -if (!fs.existsSync(CSS_PATH)) { |
17 | | - console.error(`CSS file not found: ${CSS_PATH}`); |
| 15 | +if (!existsSync(cssPath)) { |
| 16 | + console.error( |
| 17 | + `verify-theme-css: ${cssPath} is missing. Run \`pnpm run build:vscode\` first.`, |
| 18 | + ); |
18 | 19 | process.exit(1); |
19 | 20 | } |
20 | 21 |
|
21 | | -const css = fs.readFileSync(CSS_PATH, 'utf-8'); |
| 22 | +// Extract vscode-* Tailwind classes (including variant prefixes like hover:). |
| 23 | +// 'vscode-…' must precede 'vscode' to avoid partial matches. |
| 24 | +const tailwindClass = /(?:[a-zA-Z-]+:)*[a-zA-Z][\w-]*-(?:vscode-[\w-]+|vscode)\b/g; |
22 | 25 |
|
23 | | -if (css.trim().length === 0) { |
24 | | - console.error('chat-panel.css is empty'); |
25 | | - process.exit(1); |
| 26 | +const referenced = new Set(); |
| 27 | +for (const path of [htmlPath, jsPath]) { |
| 28 | + if (!existsSync(path)) continue; |
| 29 | + const text = readFileSync(path, 'utf8'); |
| 30 | + for (const match of text.match(tailwindClass) ?? []) { |
| 31 | + referenced.add(match); |
| 32 | + } |
26 | 33 | } |
27 | 34 |
|
28 | | -const vscodeTokens = css.match(/--vscode-[\w-]+/g) || []; |
29 | | -const uniqueTokens = [...new Set(vscodeTokens)]; |
| 35 | +// Spot-check critical tokens whose absence would be visually obvious. |
| 36 | +const requiredTokens = [ |
| 37 | + 'bg-vscode-bg', |
| 38 | + 'text-vscode-fg', |
| 39 | + 'bg-vscode-input-bg', |
| 40 | + 'border-vscode-input-border', |
| 41 | + 'bg-vscode-button-bg', |
| 42 | + 'text-vscode-button-fg', |
| 43 | + 'border-vscode-border', |
| 44 | +]; |
30 | 45 |
|
31 | | -if (uniqueTokens.length === 0) { |
32 | | - console.error( |
33 | | - 'No --vscode-* theme tokens found in chat-panel.css. ' + |
34 | | - 'Tailwind may have stripped them during compilation.', |
| 46 | +const css = readFileSync(cssPath, 'utf8'); |
| 47 | + |
| 48 | +// Escape colons for variant selectors (e.g. .hover\:bg-vscode-foo). |
| 49 | +function compiledSelectorFor(token) { |
| 50 | + return token.replaceAll(':', '\\\\:'); |
| 51 | +} |
| 52 | + |
| 53 | +function compiledAsClass(token) { |
| 54 | + const selector = compiledSelectorFor(token); |
| 55 | + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp |
| 56 | + return new RegExp(`\\.${selector}(?![\\w-])`).test(css); |
| 57 | +} |
| 58 | + |
| 59 | +const missing = [...referenced].filter((cls) => !compiledAsClass(cls)); |
| 60 | + |
| 61 | +const missingRequired = requiredTokens.filter( |
| 62 | + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp |
| 63 | + (token) => !new RegExp(`\\.${token}(?![\\w-])`).test(css), |
| 64 | +); |
| 65 | + |
| 66 | +if (missing.length === 0 && missingRequired.length === 0) { |
| 67 | + const scanned = referenced.size; |
| 68 | + console.log( |
| 69 | + `verify-theme-css: ok — ${scanned} theme class(es) found in ${cssPath}`, |
35 | 70 | ); |
36 | | - process.exit(1); |
| 71 | + process.exit(0); |
37 | 72 | } |
38 | 73 |
|
39 | | -console.log( |
40 | | - `OK: chat-panel.css contains ${uniqueTokens.length} VS Code theme tokens`, |
| 74 | +console.error( |
| 75 | + 'verify-theme-css: the compiled CSS is missing theme utilities referenced', |
41 | 76 | ); |
| 77 | +console.error(' by the webview templates. The webview would render unthemed.'); |
| 78 | +console.error(''); |
| 79 | + |
| 80 | +if (missingRequired.length > 0) { |
| 81 | + console.error('Required tokens that did not compile:'); |
| 82 | + for (const token of missingRequired) { |
| 83 | + console.error(` - .${token}`); |
| 84 | + } |
| 85 | + console.error(''); |
| 86 | +} |
| 87 | + |
| 88 | +if (missing.length > 0) { |
| 89 | + const preview = missing.slice(0, 20); |
| 90 | + console.error(`Other missing classes (${missing.length} total, showing first ${preview.length}):`); |
| 91 | + for (const cls of preview) { |
| 92 | + console.error(` - .${cls}`); |
| 93 | + } |
| 94 | + if (missing.length > preview.length) { |
| 95 | + console.error(` ... and ${missing.length - preview.length} more`); |
| 96 | + } |
| 97 | + console.error(''); |
| 98 | +} |
| 99 | + |
| 100 | +console.error( |
| 101 | + 'This usually means the Tailwind theme moved (e.g. tailwind.config.js no', |
| 102 | +); |
| 103 | +console.error( |
| 104 | + 'longer being read by Tailwind v4) or @theme tokens were deleted. Check', |
| 105 | +); |
| 106 | +console.error('that src/styles.css declares every `--color-vscode-*` used by'); |
| 107 | +console.error('media/chat-panel.{html,js}.'); |
| 108 | + |
| 109 | +process.exit(1); |
0 commit comments