import { basename, dirname, join } from 'node:path'; import { describe, expect, it } from 'vitest'; // Vite types a style module as { [key: string]: string }, so a typo in styles.someThing // passes both the typecheck and the lint, and turns into className="undefined" — quietly and // without a trace. Caught in live showcase code, so it is checked by a test and not by eye. const modules = import.meta.glob('./**/*.module.css', { query: '?raw', eager: true, import: 'default', }); const sources = import.meta.glob('./**/*.{ts,tsx}', { query: '?raw', eager: true, import: 'default', }); // url(...) and string literals are thrown out before the search for classes: otherwise the file // extension from url(./x.woff2) arrives into the "declared classes" and covers up a typo (Ф-10). const classesOf = (css: string) => new Set( css .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/url\([^)]*\)/g, '') .replace(/"[^"]*"|'[^']*'/g, '') .match(/\.[a-zA-Z][\w-]*/g) ?.map((name) => name.slice(1)), ); // Pairs are built FROM THE IMPORTS and not from a match of file names: a component importing a // neighbouring module silently fell out of the check, and in S2 there were several such pairs at // once (Ф-10). const importsOf = (source: string) => [...source.matchAll(/import\s+(\w+)\s+from\s+['"]([^'"]+\.module\.css)['"]/g)].map((match) => ({ binding: match[1] ?? '', specifier: match[2] ?? '', })); // Three forms of addressing a module: styles.name · styles['name'] · const { name } = styles. // Destructuring the previous check did not see at all. const usedIn = (source: string, binding: string) => { const direct = [ ...source.matchAll(new RegExp(String.raw`\b${binding}(?:\.(\w+)|\[['"]([\w-]+)['"]\])`, 'g')), ].map((match) => match[1] ?? match[2] ?? ''); const destructured = [ ...source.matchAll(new RegExp(String.raw`\{([^{}]*)\}\s*=\s*${binding}\b`, 'g')), ] .flatMap((match) => (match[1] ?? '').split(',')) .map((part) => part.split(':')[0]?.trim() ?? '') .filter((name) => /^\w+$/.test(name)); return [...direct, ...destructured]; }; const pairs = Object.entries(sources).flatMap(([file, source]) => importsOf(source).map(({ binding, specifier }) => ({ file, module: `./${join(dirname(file), specifier)}`, binding, })), ); describe('CSS Modules', () => { it.each(pairs.map((pair) => [pair.file, pair.module, pair.binding] as const))( '%s does not reference a class that does not exist in %s', (file, module, binding) => { expect(modules[module], `module ${module} not found`).toBeDefined(); const defined = classesOf(modules[module] as string); const missing = usedIn(sources[file] as string, binding).filter((name) => !defined.has(name)); expect(missing, `not in ${basename(module)}`).toEqual([]); }, ); it('every style module is imported by someone', () => { const imported = new Set(pairs.map((pair) => pair.module)); const orphans = Object.keys(modules).filter((module) => !imported.has(module)); expect(orphans).toEqual([]); }); });