diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 46f799ba..a77c52cb 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -13,6 +13,7 @@ on: - '.github/workflows/frontend-tests.yml' - '.github/frontend-hot-path-files.txt' - 'scripts/check-frontend-hot-path-coverage.sh' + - 'scripts/check-css-import-graph.mjs' push: branches: [main] paths: @@ -25,6 +26,7 @@ on: - '.github/workflows/frontend-tests.yml' - '.github/frontend-hot-path-files.txt' - 'scripts/check-frontend-hot-path-coverage.sh' + - 'scripts/check-css-import-graph.mjs' workflow_dispatch: permissions: diff --git a/package.json b/package.json index cf724f1f..7ed49765 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,16 @@ "version": "1.46.0-dev", "private": true, "scripts": { + "check:css-imports": "node scripts/check-css-import-graph.mjs", "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "tauri": "tauri", "tauri:dev": "tauri dev", "tauri:build": "tauri build", - "test": "vitest run", + "test": "vitest run && npm run check:css-imports", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage && npm run check:css-imports" }, "dependencies": { "@fontsource-variable/dm-sans": "^5.2.8", diff --git a/scripts/check-css-import-graph.mjs b/scripts/check-css-import-graph.mjs new file mode 100644 index 00000000..a7eb1c19 --- /dev/null +++ b/scripts/check-css-import-graph.mjs @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * Walk the same root CSS entrypoints as src/main.tsx and ensure every + * relative @import target exists on disk. Catches postcss-import ENOENT + * that Vitest does not surface (CSS is not fully resolved like vite build). + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, '..'); +const stylesRoot = path.join(repoRoot, 'src', 'styles'); + +const ENTRY_RELS = [ + 'themes/index.css', + 'layout/index.css', + 'components/index.css', + 'tracks/index.css', +]; + +/** Quoted path in @import '...' or "..." (postcss-import style in this repo). */ +const IMPORT_RE = /@import\s+(['"])([^'"]+)\1/g; + +function isFilesystemImport(spec) { + if (spec.startsWith('http://') || spec.startsWith('https://') || spec.startsWith('data:')) { + return false; + } + if (spec.startsWith('@')) return false; + return spec.startsWith('./') || spec.startsWith('../'); +} + +function walk(absPath, visited, missing) { + const key = path.resolve(absPath); + if (visited.has(key)) return; + if (!fs.existsSync(key)) { + missing.add(path.relative(repoRoot, key)); + visited.add(key); + return; + } + visited.add(key); + const text = fs.readFileSync(key, 'utf8'); + let m; + IMPORT_RE.lastIndex = 0; + while ((m = IMPORT_RE.exec(text)) !== null) { + const spec = m[2]; + if (!isFilesystemImport(spec)) continue; + const next = path.resolve(path.dirname(key), spec); + walk(next, visited, missing); + } +} + +const visited = new Set(); +const missing = new Set(); +for (const rel of ENTRY_RELS) { + walk(path.join(stylesRoot, rel), visited, missing); +} + +if (missing.size > 0) { + const list = [...missing].sort().join('\n '); + console.error(`check-css-import-graph: missing @import target(s):\n ${list}`); + process.exit(1); +} diff --git a/src/test/README.md b/src/test/README.md index af637a80..f0fa4084 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -33,8 +33,21 @@ src/test/ npm test # one-shot run npm run test:watch # watch mode npm run test:coverage # with v8 coverage → ./coverage/ +npm run check:css-imports # only the global stylesheet @import graph (see below) ``` +## CSS `@import` graph + +Vitest does not load the full global CSS bundle from `main.tsx`, so a broken +relative `@import` under `src/styles/**` can slip past the suite until Vite +runs (`ENOENT` from postcss-import). + +After **`vitest run`**, **`npm test`** and **`npm run test:coverage`** run +**`npm run check:css-imports`**, which executes **`scripts/check-css-import-graph.mjs`** +and walks the same four root stylesheets as **`src/main.tsx`**, resolving +only filesystem-relative imports (`./…`, `../…`). Package imports such as +`@fontsource/...` are ignored. + ## Where tests go - **Co-located with the unit under test**: `Foo.tsx` → `Foo.test.tsx`,