UNPKG

solid-styles

Version:

Zero-runtime styled components for SolidJS with Lightning CSS optimization and spring animations. Production-ready CSS-in-JS with 100% test coverage.

234 lines (196 loc) 6.31 kB
/** * Lightning CSS Runtime Resolver * * Resolves props to pre-generated CSS classes at runtime */ import type { ExtractedStyle, RuntimeResolverConfig } from "../types"; /** * Runtime resolver class */ export class RuntimeResolver { private staticClassMap: Map<string, string>; private fallbackToRuntime: boolean; private enableDevMode: boolean; constructor(config: RuntimeResolverConfig) { this.staticClassMap = new Map(); this.fallbackToRuntime = config.fallbackToRuntime ?? true; this.enableDevMode = config.enableDevMode ?? false; // Initialize with provided mappings if (config.staticClassMap) { Array.from(config.staticClassMap.entries()).forEach(([key, value]) => { this.staticClassMap.set(key, value.className); }); } } /** * Resolve props to a CSS class name */ resolveProps(props: Record<string, any>): string | null { const styleProps = this.filterStyleProps(props); const propsKey = this.createPropsKey(styleProps); if (this.enableDevMode) { console.log(`[Lightning CSS] Resolving props:`, props); console.log(`[Lightning CSS] Filtered style props:`, styleProps); console.log(`[Lightning CSS] Generated key:`, propsKey); console.log(`[Lightning CSS] Available keys in map:`, Array.from(this.staticClassMap.keys())); } // Try direct lookup first const directLookup = this.staticClassMap.get(propsKey); if (directLookup) { if (this.enableDevMode) { console.log(`[Lightning CSS] Direct match found: ${propsKey} -> ${directLookup}`); } return directLookup; } // Try alternative key formats for backward compatibility // Format 1: "variant:primary|size:large" const altKey1 = Object.entries(styleProps) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}:${value}`) .join("|"); const altLookup1 = this.staticClassMap.get(altKey1); if (altLookup1) { if (this.enableDevMode) { console.log(`[Lightning CSS] Alternative match found: ${altKey1} -> ${altLookup1}`); } return altLookup1; } // Format 2: "primary-large" (for simple cases) const values = Object.values(styleProps); if (values.length > 0) { const simpleKey = values.join("-"); const simpleLookup = this.staticClassMap.get(simpleKey); if (simpleLookup) { if (this.enableDevMode) { console.log(`[Lightning CSS] Simple match found: ${simpleKey} -> ${simpleLookup}`); } return simpleLookup; } } if (this.enableDevMode) { console.log(`[Lightning CSS] No match found for props:`, styleProps); console.log(`[Lightning CSS] Available keys:`, Array.from(this.staticClassMap.keys())); } return this.fallbackToRuntime ? null : null; } /** * Create a deterministic key from props */ private createPropsKey(props: Record<string, any>): string { // Filter out non-style props const styleProps = this.filterStyleProps(props); // Sort keys for deterministic ordering const sortedKeys = Object.keys(styleProps).sort(); // Create key const key = sortedKeys.map((k) => `${k}:${styleProps[k]}`).join("|"); return key; } /** * Filter out non-style props */ private filterStyleProps(props: Record<string, any>): Record<string, any> { const styleProps: Record<string, any> = {}; // List of known style prop names const stylePropNames = [ "variant", "size", "color", "disabled", "active", "theme", "rounded", // Add more as needed ]; for (const key in props) { // Skip template literal props if (key === "0" || key === "length" || key === "raw") { continue; } // Skip non-style props like children, ref, etc. if (key === "children" || key === "ref" || key === "style") { continue; } // Include known style props if (stylePropNames.includes(key)) { styleProps[key] = props[key]; } } return styleProps; } /** * Check if props should use runtime styles */ shouldUseRuntime(props: Record<string, any>): boolean { // Always use runtime for animated components if (props._isAnimated) return true; // Check if we have a static class const hasStaticClass = this.resolveProps(props) !== null; // Use runtime if no static class and fallback is enabled return !hasStaticClass && this.fallbackToRuntime; } /** * Get statistics about the resolver */ getStats(): { totalClasses: number; memoryUsage: number; } { return { totalClasses: this.staticClassMap.size, memoryUsage: this.estimateMemoryUsage(), }; } /** * Add class mappings to the resolver */ addClassMappings(mappings: Record<string, string>): void { Array.from(Object.entries(mappings)).forEach(([key, className]) => { this.staticClassMap.set(key, className); }); if (this.enableDevMode) { console.log(`[Lightning CSS] Added ${Object.keys(mappings).length} class mappings`); } } /** * Estimate memory usage */ private estimateMemoryUsage(): number { let totalSize = 0; for (const [key, value] of Array.from(this.staticClassMap)) { totalSize += key.length + value.length; } // Rough estimate in bytes return totalSize * 2; // 2 bytes per character } } /** * Global resolver instance */ let globalResolver: RuntimeResolver | null = null; /** * Initialize the global resolver */ export function initializeResolver(config: RuntimeResolverConfig): void { globalResolver = new RuntimeResolver(config); } /** * Get the global resolver */ export function getResolver(): RuntimeResolver { if (!globalResolver) { throw new Error("Runtime resolver not initialized. Call initializeResolver() first."); } return globalResolver; } /** * Resolve props using the global resolver */ export function resolvePropsToClass(props: Record<string, any>): string | null { return getResolver().resolveProps(props); } /** * Check if should use runtime styles */ export function shouldUseRuntimeStyles(props: Record<string, any>): boolean { return getResolver().shouldUseRuntime(props); }