import { readFileSync, existsSync } from 'fs';
import { dirname, join, resolve } from 'path';
import type { StyleEntry } from '../types/index.js';
// CSS property categories (same as styled.ts)
const COLOR_PROPERTIES = [
'color', 'background', 'background-color', 'border-color',
'outline-color', 'box-shadow', 'text-shadow', 'fill', 'stroke'
];
const SPACING_PROPERTIES = [
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'gap', 'row-gap', 'column-gap',
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
'top', 'right', 'bottom', 'left', 'inset'
];
const TYPOGRAPHY_PROPERTIES = [
'font', 'font-family', 'font-size', 'font-weight', 'font-style',
'line-height', 'letter-spacing', 'text-align', 'text-transform',
'text-decoration', 'text-indent', 'white-space'
];
function categorizeProperty(property: string): 'colors' | 'spacing' | 'typography' | 'other' {
const normalizedProperty = property.toLowerCase().trim();
if (COLOR_PROPERTIES.includes(normalizedProperty)) return 'colors';
if (SPACING_PROPERTIES.includes(normalizedProperty)) return 'spacing';
if (TYPOGRAPHY_PROPERTIES.includes(normalizedProperty)) return 'typography';
if (normalizedProperty.includes('color')) return 'colors';
return 'other';
}
interface CSSRule {
selector: string;
declarations: { property: string; value: string }[];
}
function parseCSS(cssContent: string): CSSRule[] {
const rules: CSSRule[] = [];
// Remove comments
const noComments = cssContent.replace(/\/\*[\s\S]*?\*\//g, '');
// Simple CSS parser - match selector { declarations }
const ruleRegex = /([^{]+)\{([^}]*)\}/g;
let match;
while ((match = ruleRegex.exec(noComments)) !== null) {
const selector = match[1].trim();
const declarationsStr = match[2];
const declarations: { property: string; value: string }[] = [];
const declParts = declarationsStr.split(';').filter(d => d.trim());
for (const decl of declParts) {
const colonIndex = decl.indexOf(':');
if (colonIndex === -1) continue;
const property = decl.substring(0, colonIndex).trim();
const value = decl.substring(colonIndex + 1).trim();
if (property && value) {
declarations.push({ property, value });
}
}
if (declarations.length > 0) {
rules.push({ selector, declarations });
}
}
return rules;
}
export interface CSSModuleInfo {
importName: string;
modulePath: string;
resolvedPath: string | null;
usedClasses: string[];
}
export function detectCSSModuleImports(code: string, filePath: string): CSSModuleInfo[] {
const imports: CSSModuleInfo[] = [];
// Match: import styles from './xxx.module.css'
// Or: import styles from './xxx.module.scss'
const importRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+\.module\.(css|scss|sass|less))['"]/g;
let match;
while ((match = importRegex.exec(code)) !== null) {
const importName = match[1];
const modulePath = match[2];
// Resolve the CSS module path
const dir = dirname(filePath);
const resolvedPath = resolveModulePath(dir, modulePath);
// Find all usages of this import in the code
const usedClasses = findUsedClasses(code, importName);
imports.push({
importName,
modulePath,
resolvedPath,
usedClasses
});
}
return imports;
}
function resolveModulePath(baseDir: string, modulePath: string): string | null {
// Handle relative paths
if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
const resolved = resolve(baseDir, modulePath);
return existsSync(resolved) ? resolved : null;
}
// For node_modules or aliased paths, return null
return null;
}
function findUsedClasses(code: string, importName: string): string[] {
const usedClasses: string[] = [];
// Match styles.className or styles['className']
const dotRegex = new RegExp(`${importName}\\.(\\w+)`, 'g');
const bracketRegex = new RegExp(`${importName}\\[['"]([^'"]+)['"]\\]`, 'g');
let match;
while ((match = dotRegex.exec(code)) !== null) {
usedClasses.push(match[1]);
}
while ((match = bracketRegex.exec(code)) !== null) {
usedClasses.push(match[1]);
}
return [...new Set(usedClasses)];
}
export function extractCSSModuleStyles(
code: string,
filePath: string
): StyleEntry[] {
const styles: StyleEntry[] = [];
const moduleImports = detectCSSModuleImports(code, filePath);
for (const moduleInfo of moduleImports) {
if (!moduleInfo.resolvedPath) continue;
try {
const cssContent = readFileSync(moduleInfo.resolvedPath, 'utf-8');
const rules = parseCSS(cssContent);
// Filter rules that match used classes
for (const rule of rules) {
// Check if any used class matches this selector
const selectorClass = rule.selector.match(/\.([a-zA-Z_][\w-]*)/)?.[1];
if (!selectorClass || !moduleInfo.usedClasses.includes(selectorClass)) {
continue;
}
for (const decl of rule.declarations) {
styles.push({
property: decl.property,
value: decl.value,
source: 'css-modules',
raw: `${rule.selector} { ${decl.property}: ${decl.value} }`
});
}
}
} catch {
// CSS file not found or unreadable
continue;
}
}
return styles;
}
export function categorizeCSSModuleStyles(
code: string,
filePath: string
): {
colors: StyleEntry[];
spacing: StyleEntry[];
typography: StyleEntry[];
other: StyleEntry[];
} {
const styles = extractCSSModuleStyles(code, filePath);
const result = {
colors: [] as StyleEntry[],
spacing: [] as StyleEntry[],
typography: [] as StyleEntry[],
other: [] as StyleEntry[]
};
for (const style of styles) {
const category = categorizeProperty(style.property);
result[category].push(style);
}
return result;
}
export function detectCSSModules(code: string): boolean {
return /import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass|less)['"]/.test(code);
}