UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

392 lines (335 loc) 10.7 kB
/** * @fileoverview CSS-in-JS Compiler for OrdoJS Framework * Handles compilation of CSS-in-JS expressions to optimized CSS */ import { type CSSDeclarationNode, type CSSRuleNode, type ExpressionNode, type StyleBlockNode } from '../types/index.js'; /** * CSS-in-JS compilation options */ export interface CSSInJSOptions { /** * Whether to generate scoped CSS */ scoped?: boolean; /** * Prefix for generated class names */ classPrefix?: string; /** * Whether to optimize generated CSS */ optimize?: boolean; /** * Whether to generate source maps */ generateSourceMaps?: boolean; } /** * Default CSS-in-JS options */ const DEFAULT_CSS_IN_JS_OPTIONS: CSSInJSOptions = { scoped: true, classPrefix: 'ordojs-css', optimize: true, generateSourceMaps: true }; /** * CSS-in-JS expression types */ export type CSSInJSExpression = | CSSObjectExpression | CSSTemplateExpression | CSSFunctionExpression; /** * CSS object expression (e.g., { color: 'red', fontSize: '16px' }) */ export interface CSSObjectExpression { type: 'CSSObject'; properties: Array<{ key: string; value: string | number | CSSObjectExpression; computed?: boolean; }>; } /** * CSS template expression (e.g., css`color: red; font-size: 16px;`) */ export interface CSSTemplateExpression { type: 'CSSTemplate'; template: string; expressions: ExpressionNode[]; } /** * CSS function expression (e.g., styled.div`...` or css(...)) */ export interface CSSFunctionExpression { type: 'CSSFunction'; functionName: string; arguments: (CSSInJSExpression | string)[]; } /** * CSS-in-JS compilation result */ export interface CSSInJSCompilationResult { css: string; className: string; styleBlock: StyleBlockNode; dependencies: string[]; } /** * CSS-in-JS Compiler for OrdoJS components */ export class OrdoJSCSSInJSCompiler { private options: CSSInJSOptions; private classCounter: number = 0; constructor(options: Partial<CSSInJSOptions> = {}) { this.options = { ...DEFAULT_CSS_IN_JS_OPTIONS, ...options }; } /** * Compile CSS-in-JS expression to CSS */ compile(expression: CSSInJSExpression, componentName?: string): CSSInJSCompilationResult { const className = this.generateClassName(componentName); const declarations = this.compileExpression(expression); const rule: CSSRuleNode = { type: 'CSSRule', selector: `.${className}`, declarations, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; const styleBlock: StyleBlockNode = { type: 'StyleBlock', rules: [rule], scoped: this.options.scoped || false, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; const css = this.generateCSS(styleBlock); const dependencies = this.extractDependencies(expression); return { css, className, styleBlock, dependencies }; } /** * Compile CSS-in-JS expression to CSS declarations */ private compileExpression(expression: CSSInJSExpression): CSSDeclarationNode[] { switch (expression.type) { case 'CSSObject': return this.compileCSSObject(expression); case 'CSSTemplate': return this.compileCSSTemplate(expression); case 'CSSFunction': return this.compileCSSFunction(expression); default: throw new Error(`Unsupported CSS-in-JS expression type: ${(expression as any).type}`); } } /** * Compile CSS object expression */ private compileCSSObject(expression: CSSObjectExpression): CSSDeclarationNode[] { const declarations: CSSDeclarationNode[] = []; for (const property of expression.properties) { if (typeof property.value === 'object' && property.value.type === 'CSSObject') { // Nested object - handle pseudo-classes, media queries, etc. const nestedDeclarations = this.compileCSSObject(property.value); // For now, we'll flatten nested objects - in a real implementation, // we'd need to handle nesting properly declarations.push(...nestedDeclarations); } else { const cssProperty = this.camelCaseToKebabCase(property.key); const cssValue = this.normalizeCSSValue(property.value); declarations.push({ type: 'CSSDeclaration', property: cssProperty, value: cssValue, important: false, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }); } } return declarations; } /** * Compile CSS template expression */ private compileCSSTemplate(expression: CSSTemplateExpression): CSSDeclarationNode[] { // Parse the template string as CSS let cssText = expression.template; // Replace template expressions with their values // This is a simplified implementation - in reality, we'd need proper template parsing expression.expressions.forEach((expr, index) => { const placeholder = `\${${index}}`; if (cssText.includes(placeholder)) { // For now, we'll assume expressions evaluate to strings // In a real implementation, we'd need to evaluate the expressions cssText = cssText.replace(placeholder, `/* expression ${index} */`); } }); return this.parseCSSDeclarations(cssText); } /** * Compile CSS function expression */ private compileCSSFunction(expression: CSSFunctionExpression): CSSDeclarationNode[] { const declarations: CSSDeclarationNode[] = []; for (const arg of expression.arguments) { if (typeof arg === 'string') { declarations.push(...this.parseCSSDeclarations(arg)); } else { declarations.push(...this.compileExpression(arg)); } } return declarations; } /** * Parse CSS declarations from a string */ private parseCSSDeclarations(cssText: string): CSSDeclarationNode[] { const declarations: CSSDeclarationNode[] = []; // Split by semicolons and parse each declaration const declarationStrings = cssText.split(';').map(s => s.trim()).filter(Boolean); for (const declStr of declarationStrings) { const colonIndex = declStr.indexOf(':'); if (colonIndex > 0) { const property = declStr.substring(0, colonIndex).trim(); const value = declStr.substring(colonIndex + 1).trim(); if (property && value) { declarations.push({ type: 'CSSDeclaration', property, value, important: value.includes('!important'), range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }); } } } return declarations; } /** * Convert camelCase to kebab-case for CSS properties */ private camelCaseToKebabCase(str: string): string { return str.replace(/([A-Z])/g, '-$1').toLowerCase(); } /** * Normalize CSS value (handle numbers, add units, etc.) */ private normalizeCSSValue(value: string | number | CSSObjectExpression): string { if (typeof value === 'number') { // Add 'px' unit to numeric values for properties that need units return `${value}px`; } if (typeof value === 'string') { return value; } // For nested objects, we'd need to handle them differently return 'inherit'; } /** * Generate unique class name */ private generateClassName(componentName?: string): string { const prefix = this.options.classPrefix || 'ordojs-css'; const suffix = componentName ? `${componentName.toLowerCase()}-${this.classCounter++}` : `${this.classCounter++}`; return `${prefix}-${suffix}`; } /** * Extract dependencies from CSS-in-JS expression */ private extractDependencies(expression: CSSInJSExpression): string[] { const dependencies: string[] = []; // This is a simplified implementation // In a real implementation, we'd analyze the expression for variable references return dependencies; } /** * Generate CSS string from StyleBlockNode */ private generateCSS(styleBlock: StyleBlockNode): string { return styleBlock.rules.map(rule => { const declarations = rule.declarations.map(decl => ` ${decl.property}: ${decl.value};` ).join('\n'); return `${rule.selector} {\n${declarations}\n}`; }).join('\n\n'); } /** * Compile multiple CSS-in-JS expressions and merge them */ compileMultiple(expressions: CSSInJSExpression[], componentName?: string): CSSInJSCompilationResult { const allDeclarations: CSSDeclarationNode[] = []; const allDependencies: string[] = []; for (const expression of expressions) { const declarations = this.compileExpression(expression); const dependencies = this.extractDependencies(expression); allDeclarations.push(...declarations); allDependencies.push(...dependencies); } const className = this.generateClassName(componentName); const rule: CSSRuleNode = { type: 'CSSRule', selector: `.${className}`, declarations: allDeclarations, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; const styleBlock: StyleBlockNode = { type: 'StyleBlock', rules: [rule], scoped: this.options.scoped || false, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; const css = this.generateCSS(styleBlock); return { css, className, styleBlock, dependencies: [...new Set(allDependencies)] }; } /** * Create CSS-in-JS expression from JavaScript object */ static createCSSObject(obj: Record<string, any>): CSSObjectExpression { const properties = Object.entries(obj).map(([key, value]) => ({ key, value: typeof value === 'object' && value !== null ? OrdoJSCSSInJSCompiler.createCSSObject(value) : value, computed: false })); return { type: 'CSSObject', properties }; } /** * Create CSS template expression */ static createCSSTemplate(template: string, expressions: ExpressionNode[] = []): CSSTemplateExpression { return { type: 'CSSTemplate', template, expressions }; } /** * Create CSS function expression */ static createCSSFunction(functionName: string, args: (CSSInJSExpression | string)[]): CSSFunctionExpression { return { type: 'CSSFunction', functionName, arguments: args }; } }