Skip to main content
Glama
TailwindCSSClassesService.ts9.57 kB
/** * TailwindCSSClassesService * * DESIGN PATTERNS: * - Extends BaseCSSClassesService for Tailwind-specific extraction * - Uses postcss AST parser for robust CSS variable extraction * - Parses CSS variables from theme files to generate utility classes * * CODING STANDARDS: * - Implement abstract methods from base class * - Use async/await for file operations * - Throw descriptive errors with context * * AVOID: * - Hardcoding theme paths * - Synchronous file operations * - Fragile regex-based CSS parsing */ import { promises as fs } from 'node:fs'; import path from 'node:path'; import postcss from 'postcss'; import { BaseCSSClassesService } from './BaseCSSClassesService'; import type { CSSClassCategory, CSSClassesResult, CSSClassValue, StyleSystemConfig } from './types'; /** * Tailwind CSS class extraction service. * * Extracts CSS classes from Tailwind theme files by parsing CSS variables * using postcss AST and generating corresponding utility class names. * * @example * ```typescript * const service = new TailwindCSSClassesService(config); * const result = await service.extractClasses('colors', '/path/to/theme.css'); * console.log(result.classes.colors); // Array of color utility classes * ``` */ export class TailwindCSSClassesService extends BaseCSSClassesService { /** * Creates a new TailwindCSSClassesService instance * @param config - Style system configuration from toolkit.yaml */ constructor(config: StyleSystemConfig) { super(config); } /** * Get the CSS framework identifier * @returns Framework identifier string 'tailwind' */ getFrameworkId(): string { return 'tailwind'; } /** * Extract Tailwind CSS classes from a theme file. * * Uses postcss to parse the CSS AST and safely extract variable declarations, * then generates corresponding Tailwind utility classes (e.g., bg-*, text-*, border-*). * * Note: If an unrecognized category is passed, the method returns a result with * empty classes object. Use valid categories: 'colors', 'typography', 'spacing', 'effects', 'all'. * * @param category - Category filter ('colors', 'typography', 'spacing', 'effects', 'all') * @param themePath - Absolute path to the theme CSS file * @returns Promise resolving to extracted CSS classes organized by category * @throws Error if theme file cannot be read or parsed */ async extractClasses(category: CSSClassCategory | string, themePath: string): Promise<CSSClassesResult> { try { await this.validateThemePath(themePath); const resolvedThemePath = path.resolve(themePath); const themeContent = await fs.readFile(resolvedThemePath, 'utf-8'); const variables = await this.extractVariablesWithPostCSS(themeContent); const classes = this.generateClassesFromVariables(variables, category); return classes; } catch (error) { throw new Error( `Failed to extract classes from theme file ${themePath}: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Extract CSS variables with their values from theme content using postcss AST. * * Walks the CSS AST to find all custom property declarations (--*), * handling multi-line values, comments, and any CSS formatting. * * @param themeContent - Raw CSS content from theme file * @returns Promise resolving to Map of variable names (without --) to their values * * @example * ```typescript * // Handles standard declarations * // --color-primary: #3b82f6; * * // Handles multi-line declarations * // --shadow-lg: * // 0 10px 15px -3px rgba(0, 0, 0, 0.1), * // 0 4px 6px -4px rgba(0, 0, 0, 0.1); * * // Handles compressed CSS * // --color-primary:#3b82f6;--color-secondary:#10b981; * ``` */ private async extractVariablesWithPostCSS(themeContent: string): Promise<Map<string, string>> { const variables = new Map<string, string>(); try { const root = postcss.parse(themeContent); // Walk all declarations in the AST root.walkDecls((decl) => { // Check if this is a CSS custom property (starts with --) if (decl.prop.startsWith('--')) { const varName = decl.prop.slice(2); // Remove leading -- const varValue = decl.value.trim(); // Store the first occurrence (light mode values take precedence) // Variables in later rules (like .dark) won't overwrite if (!variables.has(varName)) { variables.set(varName, varValue); } } }); } catch (error) { throw new Error( `Failed to parse CSS content: ${error instanceof Error ? error.message : String(error)}`, ); } return variables; } /** * Generate utility classes with actual values from CSS variables. * * Maps CSS variable naming conventions to Tailwind utility classes: * - color-* → bg-*, text-*, border-*, ring-* * - sidebar* → bg-*, text-*, border-* * - text-* → text-* (typography) * - font-* → font-* (typography) * - space-* → p-*, m-*, gap-* * - shadow-* → shadow-* * * Note: If the variables Map is empty, returns a result with empty arrays * for all requested categories. * * @param variables - Map of CSS variable names to values * @param category - Category filter for which classes to generate * @returns CSSClassesResult with organized classes by category * * @example * ```typescript * const variables = new Map([ * ['color-primary', '#3b82f6'], * ['shadow-md', '0 4px 6px rgba(0,0,0,0.1)'] * ]); * const result = generateClassesFromVariables(variables, 'colors'); * // Returns: * // { * // category: 'colors', * // classes: { * // colors: [ * // { class: 'bg-primary', value: '#3b82f6' }, * // { class: 'text-primary', value: '#3b82f6' }, * // { class: 'border-primary', value: '#3b82f6' }, * // { class: 'ring-primary', value: '#3b82f6' } * // ] * // }, * // totalClasses: 4 * // } * ``` */ private generateClassesFromVariables( variables: Map<string, string>, category: CSSClassCategory | string, ): CSSClassesResult { const colorClasses: CSSClassValue[] = []; const typographyClasses: CSSClassValue[] = []; const spacingClasses: CSSClassValue[] = []; const effectsClasses: CSSClassValue[] = []; for (const [varName, varValue] of variables.entries()) { // Color utilities: --color-* and --sidebar* if (category === 'all' || category === 'colors') { if (varName.startsWith('color-')) { const colorName = varName.replace('color-', ''); colorClasses.push( { class: `bg-${colorName}`, value: varValue }, { class: `text-${colorName}`, value: varValue }, { class: `border-${colorName}`, value: varValue }, { class: `ring-${colorName}`, value: varValue }, ); } else if (varName.startsWith('sidebar')) { colorClasses.push( { class: `bg-${varName}`, value: varValue }, { class: `text-${varName}`, value: varValue }, { class: `border-${varName}`, value: varValue }, ); } } // Typography utilities: --text-* and --font-* if (category === 'all' || category === 'typography') { if (varName.startsWith('text-')) { typographyClasses.push({ class: varName, value: varValue }); } if (varName.startsWith('font-')) { const fontName = varName.replace('font-', ''); if (fontName.startsWith('weight-')) { typographyClasses.push({ class: `font-${fontName.replace('weight-', '')}`, value: varValue }); } else { typographyClasses.push({ class: `font-${fontName}`, value: varValue }); } } } // Spacing utilities: --space-* and --spacing if (category === 'all' || category === 'spacing') { if (varName.startsWith('space-')) { const spaceName = varName.replace('space-', ''); const calcValue = `calc(var(--${varName}) * 1)`; spacingClasses.push( { class: `p-${spaceName}`, value: calcValue }, { class: `m-${spaceName}`, value: calcValue }, { class: `gap-${spaceName}`, value: calcValue }, ); } else if (varName === 'spacing') { spacingClasses.push({ class: 'space', value: varValue }); } } // Effects utilities: --shadow-* if (category === 'all' || category === 'effects') { if (varName.startsWith('shadow-')) { const shadowName = varName.replace('shadow-', ''); effectsClasses.push({ class: `shadow-${shadowName}`, value: varValue }); } } } const result: CSSClassesResult = { category: category as string, classes: {}, totalClasses: colorClasses.length + typographyClasses.length + spacingClasses.length + effectsClasses.length, }; if (category === 'all' || category === 'colors') { result.classes.colors = colorClasses; } if (category === 'all' || category === 'typography') { result.classes.typography = typographyClasses; } if (category === 'all' || category === 'spacing') { result.classes.spacing = spacingClasses; } if (category === 'all' || category === 'effects') { result.classes.effects = effectsClasses; } return result; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AgiFlow/aicode-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server