chaincss 2.1.38 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  // src/core/compiler.ts
2
2
  import fs6 from "fs";
3
3
  import path5 from "path";
4
- import crypto4 from "crypto";
4
+ import crypto5 from "crypto";
5
5
  import chalk from "chalk";
6
6
  import { fileURLToPath, pathToFileURL } from "url";
7
7
 
@@ -2530,6 +2530,425 @@ var PersistentCache = class {
2530
2530
  }
2531
2531
  };
2532
2532
 
2533
+ // src/compiler/style-graph.ts
2534
+ import crypto4 from "crypto";
2535
+ function calculateSpecificity(selector) {
2536
+ let a = 0;
2537
+ let b = 0;
2538
+ let c = 0;
2539
+ const idMatches = selector.match(/#[a-zA-Z0-9_-]+/g);
2540
+ if (idMatches) a += idMatches.length;
2541
+ const classMatches = selector.match(/\.[a-zA-Z0-9_-]+/g);
2542
+ if (classMatches) b += classMatches.length;
2543
+ const attrMatches = selector.match(/\[[^\]]+\]/g);
2544
+ if (attrMatches) b += attrMatches.length;
2545
+ const pseudoClassMatches = selector.match(/:[a-zA-Z-]+(?:\([^)]*\))?/g);
2546
+ if (pseudoClassMatches) {
2547
+ const notMatches = selector.match(/:not\(([^)]+)\)/g);
2548
+ const regularPseudoClasses = pseudoClassMatches.length - (notMatches?.length || 0);
2549
+ b += Math.max(0, regularPseudoClasses);
2550
+ }
2551
+ const elementMatches = selector.match(/^[a-zA-Z]+|[a-zA-Z]+(?=[.#[:])/g);
2552
+ if (elementMatches) c += elementMatches.length;
2553
+ return a * 1e4 + b * 100 + c;
2554
+ }
2555
+ function hashProperties(properties) {
2556
+ const sorted = Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join(";");
2557
+ return crypto4.createHash("md5").update(sorted).digest("hex").slice(0, 8);
2558
+ }
2559
+ function kebab2(prop) {
2560
+ return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
2561
+ }
2562
+ var StyleGraphBuilder = class {
2563
+ entries = [];
2564
+ nodes = /* @__PURE__ */ new Map();
2565
+ edges = [];
2566
+ orderCounter = 0;
2567
+ addEntry(entry) {
2568
+ entry.sourceOrder = this.orderCounter++;
2569
+ this.entries.push(entry);
2570
+ }
2571
+ build() {
2572
+ this.nodes.clear();
2573
+ this.edges = [];
2574
+ for (const entry of this.entries) {
2575
+ const id = `node-${this.nodes.size}`;
2576
+ const node = {
2577
+ id,
2578
+ selector: entry.selector,
2579
+ properties: entry.properties,
2580
+ specificity: calculateSpecificity(entry.selector),
2581
+ dependencies: [],
2582
+ dependents: [],
2583
+ mediaQuery: entry.mediaQuery,
2584
+ isDead: false,
2585
+ hash: hashProperties(entry.properties),
2586
+ sourceComponent: entry.sourceComponent
2587
+ };
2588
+ this.nodes.set(id, node);
2589
+ }
2590
+ const nodeArray = Array.from(this.nodes.values());
2591
+ for (let i = 0; i < nodeArray.length; i++) {
2592
+ for (let j = i + 1; j < nodeArray.length; j++) {
2593
+ const a = nodeArray[i];
2594
+ const b = nodeArray[j];
2595
+ if (this.selectorsOverlap(a.selector, b.selector)) {
2596
+ if (a.specificity <= b.specificity) {
2597
+ this.edges.push({ from: a.id, to: b.id, type: "overrides" });
2598
+ a.dependents.push(b.id);
2599
+ b.dependencies.push(a.id);
2600
+ }
2601
+ if (b.specificity <= a.specificity) {
2602
+ this.edges.push({ from: b.id, to: a.id, type: "overrides" });
2603
+ b.dependents.push(a.id);
2604
+ a.dependencies.push(b.id);
2605
+ }
2606
+ }
2607
+ }
2608
+ }
2609
+ const rootNodes = nodeArray.filter((n) => n.dependencies.length === 0).map((n) => n.id);
2610
+ const leafNodes = nodeArray.filter((n) => n.dependents.length === 0).map((n) => n.id);
2611
+ return {
2612
+ nodes: this.nodes,
2613
+ edges: this.edges,
2614
+ rootNodes,
2615
+ leafNodes
2616
+ };
2617
+ }
2618
+ selectorsOverlap(a, b) {
2619
+ const partsA = a.split(/[\s>+~]+/).filter(Boolean);
2620
+ const partsB = b.split(/[\s>+~]+/).filter(Boolean);
2621
+ for (const pa of partsA) {
2622
+ for (const pb of partsB) {
2623
+ if (pa === pb) return true;
2624
+ if (pa.startsWith(".") && pb.startsWith(".") && pa === pb) return true;
2625
+ }
2626
+ }
2627
+ return false;
2628
+ }
2629
+ };
2630
+ function eliminateDeadStyles(graph, knownSelectors) {
2631
+ if (knownSelectors.length === 0) {
2632
+ return { eliminated: 0, graph };
2633
+ }
2634
+ const reachable = /* @__PURE__ */ new Set();
2635
+ const queue = [];
2636
+ for (const [id, node] of graph.nodes) {
2637
+ if (knownSelectors.some((ks) => node.selector.includes(ks) || ks.includes(node.selector))) {
2638
+ reachable.add(id);
2639
+ queue.push(id);
2640
+ }
2641
+ }
2642
+ while (queue.length > 0) {
2643
+ const current = queue.shift();
2644
+ const node = graph.nodes.get(current);
2645
+ if (!node) continue;
2646
+ for (const depId of node.dependents) {
2647
+ if (!reachable.has(depId)) {
2648
+ reachable.add(depId);
2649
+ queue.push(depId);
2650
+ }
2651
+ }
2652
+ }
2653
+ let eliminated = 0;
2654
+ for (const [id, node] of graph.nodes) {
2655
+ if (!reachable.has(id)) {
2656
+ node.isDead = true;
2657
+ eliminated++;
2658
+ }
2659
+ }
2660
+ return { eliminated, graph };
2661
+ }
2662
+ function mergeIdenticalRules(graph, threshold) {
2663
+ const hashGroups = /* @__PURE__ */ new Map();
2664
+ for (const [, node] of graph.nodes) {
2665
+ if (node.isDead) continue;
2666
+ if (Object.keys(node.properties).length < threshold) continue;
2667
+ const existing = hashGroups.get(node.hash) || [];
2668
+ existing.push(node);
2669
+ hashGroups.set(node.hash, existing);
2670
+ }
2671
+ let merged = 0;
2672
+ for (const [, group] of hashGroups) {
2673
+ if (group.length < 2) continue;
2674
+ const mergedSelector = group.map((n) => n.selector).join(", ");
2675
+ const primary = group[0];
2676
+ primary.selector = mergedSelector;
2677
+ for (let i = 1; i < group.length; i++) {
2678
+ group[i].isDead = true;
2679
+ merged++;
2680
+ }
2681
+ }
2682
+ return { merged, graph };
2683
+ }
2684
+ function topologicalSort(graph) {
2685
+ const visited = /* @__PURE__ */ new Set();
2686
+ const sorted = [];
2687
+ const visiting = /* @__PURE__ */ new Set();
2688
+ function visit(id) {
2689
+ if (visited.has(id)) return true;
2690
+ if (visiting.has(id)) return false;
2691
+ visiting.add(id);
2692
+ const node = graph.nodes.get(id);
2693
+ if (node) {
2694
+ for (const depId of node.dependencies) {
2695
+ if (!visit(depId)) return false;
2696
+ }
2697
+ }
2698
+ visiting.delete(id);
2699
+ visited.add(id);
2700
+ if (node && !node.isDead) {
2701
+ sorted.push(node);
2702
+ }
2703
+ return true;
2704
+ }
2705
+ for (const id of graph.rootNodes) {
2706
+ if (!visit(id)) {
2707
+ return Array.from(graph.nodes.values()).filter((n) => !n.isDead).sort((a, b) => a.sourceComponent?.localeCompare(b.sourceComponent || "") || 0);
2708
+ }
2709
+ }
2710
+ for (const [id] of graph.nodes) {
2711
+ if (!visited.has(id)) {
2712
+ visit(id);
2713
+ }
2714
+ }
2715
+ return sorted;
2716
+ }
2717
+ function generateCSSFromGraph(graph, sortOutput = "specificity") {
2718
+ let nodes;
2719
+ switch (sortOutput) {
2720
+ case "specificity":
2721
+ nodes = Array.from(graph.nodes.values()).filter((n) => !n.isDead).sort((a, b) => a.specificity - b.specificity);
2722
+ break;
2723
+ case "topological":
2724
+ nodes = topologicalSort(graph);
2725
+ break;
2726
+ case "source-order":
2727
+ default:
2728
+ nodes = Array.from(graph.nodes.values()).filter((n) => !n.isDead);
2729
+ break;
2730
+ }
2731
+ let css = "";
2732
+ let currentMediaQuery;
2733
+ for (const node of nodes) {
2734
+ if (node.isDead) continue;
2735
+ if (node.mediaQuery !== currentMediaQuery) {
2736
+ if (currentMediaQuery) {
2737
+ css += "}\n\n";
2738
+ }
2739
+ if (node.mediaQuery) {
2740
+ css += `@media ${node.mediaQuery} {
2741
+ `;
2742
+ }
2743
+ currentMediaQuery = node.mediaQuery;
2744
+ }
2745
+ const rules = Object.entries(node.properties).map(([prop, value]) => ` ${kebab2(prop)}: ${value};`).join("\n");
2746
+ if (rules) {
2747
+ css += `${node.selector} {
2748
+ ${rules}
2749
+ }
2750
+ `;
2751
+ }
2752
+ }
2753
+ if (currentMediaQuery) {
2754
+ css += "}\n";
2755
+ }
2756
+ return css;
2757
+ }
2758
+ var StyleGraphCompiler = class {
2759
+ options;
2760
+ constructor(options = {}) {
2761
+ this.options = {
2762
+ eliminateDead: options.eliminateDead ?? false,
2763
+ knownSelectors: options.knownSelectors ?? [],
2764
+ mergeIdentical: options.mergeIdentical ?? false,
2765
+ mergeThreshold: options.mergeThreshold ?? 3,
2766
+ sortOutput: options.sortOutput ?? "specificity",
2767
+ verbose: options.verbose ?? false
2768
+ };
2769
+ }
2770
+ /**
2771
+ * Compile a set of style definitions through the graph compiler.
2772
+ */
2773
+ compile(styles) {
2774
+ const startTime = Date.now();
2775
+ const builder = new StyleGraphBuilder();
2776
+ let preOptimizationSize = 0;
2777
+ for (const [componentName, styleDef] of Object.entries(styles)) {
2778
+ if (!styleDef || !styleDef.selectors) continue;
2779
+ for (const selector of styleDef.selectors) {
2780
+ const properties = {};
2781
+ for (const [prop, value] of Object.entries(styleDef)) {
2782
+ if (prop === "selectors" || prop === "atRules" || prop === "nestedRules" || prop === "hover" || prop === "themes" || prop.startsWith("_")) {
2783
+ continue;
2784
+ }
2785
+ if (typeof value === "string" || typeof value === "number") {
2786
+ properties[prop] = String(value);
2787
+ preOptimizationSize += String(value).length + prop.length;
2788
+ }
2789
+ }
2790
+ if (Object.keys(properties).length > 0) {
2791
+ builder.addEntry({
2792
+ selector,
2793
+ properties,
2794
+ sourceComponent: componentName,
2795
+ sourceOrder: 0
2796
+ });
2797
+ }
2798
+ if (styleDef.hover && typeof styleDef.hover === "object") {
2799
+ const hoverProperties = {};
2800
+ for (const [prop, value] of Object.entries(styleDef.hover)) {
2801
+ if (typeof value === "string" || typeof value === "number") {
2802
+ hoverProperties[prop] = String(value);
2803
+ }
2804
+ }
2805
+ if (Object.keys(hoverProperties).length > 0) {
2806
+ builder.addEntry({
2807
+ selector: `${selector}:hover`,
2808
+ properties: hoverProperties,
2809
+ sourceComponent: componentName,
2810
+ sourceOrder: 0
2811
+ });
2812
+ }
2813
+ }
2814
+ if (styleDef.atRules) {
2815
+ for (const rule of styleDef.atRules) {
2816
+ if (rule.type === "media" && rule.styles && rule.query) {
2817
+ const mediaProperties = {};
2818
+ for (const [prop, value] of Object.entries(rule.styles)) {
2819
+ if (typeof value === "string" || typeof value === "number") {
2820
+ mediaProperties[prop] = String(value);
2821
+ }
2822
+ }
2823
+ if (Object.keys(mediaProperties).length > 0) {
2824
+ builder.addEntry({
2825
+ selector,
2826
+ properties: mediaProperties,
2827
+ sourceComponent: componentName,
2828
+ sourceOrder: 0,
2829
+ mediaQuery: rule.query
2830
+ });
2831
+ }
2832
+ }
2833
+ }
2834
+ }
2835
+ }
2836
+ }
2837
+ let graph = builder.build();
2838
+ let eliminatedDead = 0;
2839
+ if (this.options.eliminateDead && this.options.knownSelectors.length > 0) {
2840
+ const result = eliminateDeadStyles(graph, this.options.knownSelectors);
2841
+ eliminatedDead = result.eliminated;
2842
+ graph = result.graph;
2843
+ }
2844
+ let mergedRules = 0;
2845
+ if (this.options.mergeIdentical) {
2846
+ const result = mergeIdenticalRules(graph, this.options.mergeThreshold);
2847
+ mergedRules = result.merged;
2848
+ graph = result.graph;
2849
+ }
2850
+ const css = generateCSSFromGraph(graph, this.options.sortOutput);
2851
+ let postOptimizationSize = css.length;
2852
+ if (postOptimizationSize === 0) {
2853
+ postOptimizationSize = preOptimizationSize;
2854
+ }
2855
+ const classMap = {};
2856
+ for (const [, node] of graph.nodes) {
2857
+ if (!node.isDead && node.sourceComponent) {
2858
+ if (classMap[node.sourceComponent]) {
2859
+ classMap[node.sourceComponent] += ` ${node.selector.replace(/^\./, "")}`;
2860
+ } else {
2861
+ classMap[node.sourceComponent] = node.selector.replace(/^\./, "");
2862
+ }
2863
+ }
2864
+ }
2865
+ const totalNodes = graph.nodes.size;
2866
+ const aliveNodes = totalNodes - eliminatedDead;
2867
+ const savingsPercent = preOptimizationSize > 0 ? `${((preOptimizationSize - postOptimizationSize) / preOptimizationSize * 100).toFixed(1)}%` : "0%";
2868
+ const stats = {
2869
+ totalStyles: totalNodes,
2870
+ atomicStyles: 0,
2871
+ uniqueProperties: new Set(
2872
+ Array.from(graph.nodes.values()).filter((n) => !n.isDead).flatMap((n) => Object.keys(n.properties))
2873
+ ).size,
2874
+ savings: savingsPercent,
2875
+ compileTime: Date.now() - startTime
2876
+ };
2877
+ return {
2878
+ css,
2879
+ classMap,
2880
+ atomicClasses: [],
2881
+ stats,
2882
+ graph,
2883
+ eliminatedDead,
2884
+ mergedRules,
2885
+ optimizationTime: Date.now() - startTime,
2886
+ preOptimizationSize,
2887
+ postOptimizationSize
2888
+ };
2889
+ }
2890
+ /**
2891
+ * Analyze a style graph without generating CSS.
2892
+ */
2893
+ analyze(styles) {
2894
+ const builder = new StyleGraphBuilder();
2895
+ for (const [componentName, styleDef] of Object.entries(styles)) {
2896
+ if (!styleDef || !styleDef.selectors) continue;
2897
+ for (const selector of styleDef.selectors) {
2898
+ const properties = {};
2899
+ for (const [prop, value] of Object.entries(styleDef)) {
2900
+ if (prop === "selectors" || prop.startsWith("_")) continue;
2901
+ if (typeof value === "string" || typeof value === "number") {
2902
+ properties[prop] = String(value);
2903
+ }
2904
+ }
2905
+ if (Object.keys(properties).length > 0) {
2906
+ builder.addEntry({ selector, properties, sourceComponent: componentName, sourceOrder: 0 });
2907
+ }
2908
+ }
2909
+ }
2910
+ return builder.build();
2911
+ }
2912
+ /**
2913
+ * Get optimization statistics for a graph.
2914
+ */
2915
+ getStats(graph) {
2916
+ const nodes = Array.from(graph.nodes.values());
2917
+ const deadNodes = nodes.filter((n) => n.isDead).length;
2918
+ const averageSpecificity = nodes.length > 0 ? nodes.reduce((sum, n) => sum + n.specificity, 0) / nodes.length : 0;
2919
+ let maxDepth = 0;
2920
+ const depths = /* @__PURE__ */ new Map();
2921
+ function getDepth(id) {
2922
+ if (depths.has(id)) return depths.get(id);
2923
+ const node = graph.nodes.get(id);
2924
+ if (!node || node.dependencies.length === 0) {
2925
+ depths.set(id, 0);
2926
+ return 0;
2927
+ }
2928
+ const max = Math.max(...node.dependencies.map((d) => getDepth(d)));
2929
+ const depth = max + 1;
2930
+ depths.set(id, depth);
2931
+ return depth;
2932
+ }
2933
+ for (const [id] of graph.nodes) {
2934
+ maxDepth = Math.max(maxDepth, getDepth(id));
2935
+ }
2936
+ return {
2937
+ totalNodes: nodes.length,
2938
+ deadNodes,
2939
+ mergedGroups: 0,
2940
+ averageSpecificity: Math.round(averageSpecificity * 100) / 100,
2941
+ deepestDependencyChain: maxDepth
2942
+ };
2943
+ }
2944
+ /**
2945
+ * Update options.
2946
+ */
2947
+ configure(options) {
2948
+ this.options = { ...this.options, ...options };
2949
+ }
2950
+ };
2951
+
2533
2952
  // src/core/compiler.ts
2534
2953
  var __filename = typeof import.meta !== "undefined" ? (() => {
2535
2954
  try {
@@ -2581,6 +3000,35 @@ var ChainCSSCompiler = class {
2581
3000
  this.initOptimizer();
2582
3001
  this.initPrefixer();
2583
3002
  }
3003
+ /**
3004
+ * Compile using the style graph compiler for advanced optimizations.
3005
+ *
3006
+ * @example
3007
+ * const result = compiler.compileWithGraph(styles, {
3008
+ * eliminateDead: true,
3009
+ * knownSelectors: ['.header', '.footer'],
3010
+ * mergeIdentical: true
3011
+ * });
3012
+ */
3013
+ compileWithGraph(styles, options) {
3014
+ const graphCompiler = new StyleGraphCompiler({
3015
+ ...options,
3016
+ verbose: this.config.verbose
3017
+ });
3018
+ const result = graphCompiler.compile(styles);
3019
+ if (this.config.verbose) {
3020
+ if (result.eliminatedDead > 0) {
3021
+ console.log(` \u{1F9F9} Eliminated ${result.eliminatedDead} dead styles`);
3022
+ }
3023
+ if (result.mergedRules > 0) {
3024
+ console.log(` \u{1F517} Merged ${result.mergedRules} identical rules`);
3025
+ }
3026
+ if (result.optimizationTime > 0) {
3027
+ console.log(` \u26A1 Graph compilation: ${result.optimizationTime}ms`);
3028
+ }
3029
+ }
3030
+ return result;
3031
+ }
2584
3032
  hasStyles() {
2585
3033
  const combined = this.getCombinedCSS();
2586
3034
  return !!(combined && combined.trim().length > 0);
@@ -2618,7 +3066,7 @@ var ChainCSSCompiler = class {
2618
3066
  if (result.css && result.css.trim()) {
2619
3067
  this.accumulatedCSS += result.css + "\n";
2620
3068
  }
2621
- const cacheKey = crypto4.createHash("sha256").update(`${componentName}-${JSON.stringify(styleObj)}`).digest("hex").slice(0, 16);
3069
+ const cacheKey = crypto5.createHash("sha256").update(`${componentName}-${JSON.stringify(styleObj)}`).digest("hex").slice(0, 16);
2622
3070
  this.addToCache(cacheKey, {
2623
3071
  result: {
2624
3072
  css: result.css || "",
@@ -2746,7 +3194,7 @@ var ChainCSSCompiler = class {
2746
3194
  // ============================================================================
2747
3195
  hashStyleDef(styleDef) {
2748
3196
  const { _componentName, _generateComponent, _framework, _propsDefinition, ...relevant } = styleDef;
2749
- return crypto4.createHash("sha256").update(JSON.stringify(relevant)).digest("hex").slice(0, 16);
3197
+ return crypto5.createHash("sha256").update(JSON.stringify(relevant)).digest("hex").slice(0, 16);
2750
3198
  }
2751
3199
  async importModule(filePath) {
2752
3200
  const absolutePath = path5.resolve(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaincss",
3
- "version": "2.1.38",
3
+ "version": "2.2.0",
4
4
  "description": "ChainCSS - The first CSS-in-JS library with true auto-detection mixed mode. Zero runtime by default, dynamic when you need it.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,62 @@
1
+ // src/compiler/analyzer.ts
2
+ import type { StyleDefinition, StyleDiagnostic, StyleAnalysis, BreakpointInference, DiagnosticSeverity } from '../core/types.js';
3
+ import { intent } from './intent-engine.js';
4
+ export type { StyleDiagnostic, StyleAnalysis, BreakpointInference, DiagnosticSeverity };
5
+
6
+ const CONFLICTS: Array<{props: string[]; msg: string; sev: DiagnosticSeverity}> = [
7
+ {props: ['position','z-index','zIndex'], msg: 'z-index only works on positioned elements (not static)', sev: 'warning'},
8
+ {props: ['flex-direction','align-items','justify-content'], msg: 'Flex properties require display: flex', sev: 'warning'},
9
+ {props: ['grid-template-columns','grid-template-rows'], msg: 'Grid properties require display: grid', sev: 'warning'},
10
+ {props: ['overflow','overflow-x','overflow-y'], msg: 'Consider using the overflow shorthand', sev: 'hint'},
11
+ ];
12
+
13
+ function detectShorthands(styles: Record<string,any>): StyleDiagnostic[] {
14
+ const d: StyleDiagnostic[] = [];
15
+ const m = ['margin-top','margin-right','margin-bottom','margin-left'].filter(p => p in styles);
16
+ if (m.length >= 3) d.push({property: 'margin', severity: 'hint', message: 'Use "margin" shorthand instead of: ' + m.join(', '), suggestion: 'margin'});
17
+ return d;
18
+ }
19
+
20
+ export class StyleAnalyzer {
21
+ private _diagnostics: StyleDiagnostic[] = [];
22
+ analyzeStyle(selector: string, styles: Record<string,any>, _opts?: any): StyleDiagnostic[] {
23
+ const r: StyleDiagnostic[] = [];
24
+ for (const [p, v] of Object.entries(styles)) {
25
+ if (p === 'selectors' || p.startsWith('_') || typeof v === 'object') continue;
26
+ const val = intent.validate(p, String(v));
27
+ if (!val.valid) r.push({property: p, value: String(v), selector, severity: 'warning', message: `"${v}" unrecognized for "${p}"`, suggestion: val.suggestion});
28
+ }
29
+ r.push(...detectShorthands(styles)); this._diagnostics.push(...r);
30
+ const sp = Object.keys(styles).filter(k => typeof styles[k] !== 'object');
31
+ for (const c of CONFLICTS) {
32
+ const ix = c.props.filter(k => sp.includes(k));
33
+ if (ix.length >= 2) r.push({property: c.props.join(', '), selector, severity: c.sev, message: c.msg});
34
+ }
35
+ return r;
36
+ }
37
+
38
+ analyze(sd: StyleDefinition): StyleAnalysis {
39
+ const ad: StyleDiagnostic[] = [];
40
+ const sels = sd.selectors || ['&'];
41
+ for (const s of sels) ad.push(...this.analyzeStyle(s, sd));
42
+ if (sd.hover && typeof sd.hover === 'object') for (const s of sels) ad.push(...this.analyzeStyle(s + ':hover', sd.hover as Record<string,any>));
43
+ return {
44
+ diagnostics: ad, conflicts: [],
45
+ breakpoints: [], unusedSelectors: [], deadStyles: [], duplicationWarnings: [], optimizationSuggestions: [],
46
+ stats: {
47
+ totalProperties: Object.keys(sd).filter(k => !['selectors','atRules','nestedRules','hover','themes'].includes(k) && !k.startsWith('_')).length,
48
+ totalSelectors: sels.length,
49
+ shorthandOpportunities: ad.filter(d => d.message.includes('shorthand')).length,
50
+ animationSuggestions: 0,
51
+ responsiveIssues: 0,
52
+ },
53
+ };
54
+ }
55
+
56
+ reset(): void { this._diagnostics = []; }
57
+ getDiagnostics(): StyleDiagnostic[] { return [...this._diagnostics]; }
58
+ }
59
+
60
+ export function analyze(sd: StyleDefinition): StyleAnalysis { return new StyleAnalyzer().analyze(sd); }
61
+ export function analyzeStyle(sd: StyleDefinition): StyleAnalysis { return new StyleAnalyzer().analyze(sd); }
62
+ export default StyleAnalyzer;
@@ -0,0 +1,117 @@
1
+ // src/compiler/css-if-transpiler.ts
2
+ /**
3
+ * CSS if() Transpiler
4
+ * Detects conditional style patterns and emits:
5
+ * 1. Native CSS if() — Chrome 137+
6
+ * 2. @supports fallback — Firefox, Safari
7
+ */
8
+
9
+ export interface IfCondition {
10
+ property: string;
11
+ variable: string;
12
+ conditions: Record<string, string | number>;
13
+ defaultValue: string | number;
14
+ }
15
+
16
+ export interface DetectedCondition {
17
+ property: string;
18
+ variable: string;
19
+ conditions: Record<string, string | number>;
20
+ defaultValue: string | number;
21
+ }
22
+
23
+ /**
24
+ * Detect conditional patterns from _conditions metadata.
25
+ * When chain.when() branches set the same property to different values,
26
+ * those can be compiled to CSS if().
27
+ */
28
+ export function detectIfPatterns(
29
+ styles: Record<string, any>
30
+ ): DetectedCondition[] {
31
+ const conditions: DetectedCondition[] = [];
32
+ if (!styles._conditions) return conditions;
33
+
34
+ const condEntries = Object.entries(styles._conditions || {});
35
+ for (const [variable, branches] of condEntries) {
36
+ const branch = branches as { true: Record<string, any>; false: Record<string, any> };
37
+ const trueStyles = branch.true || {};
38
+ const falseStyles = branch.false || {};
39
+
40
+ const allProps = new Set([...Object.keys(trueStyles), ...Object.keys(falseStyles)]);
41
+ for (const prop of allProps) {
42
+ if (prop.startsWith('_') || prop === 'selectors') continue;
43
+ const trueVal = trueStyles[prop];
44
+ const falseVal = falseStyles[prop];
45
+ if (trueVal !== undefined && falseVal !== undefined && trueVal !== falseVal) {
46
+ conditions.push({
47
+ property: prop,
48
+ variable: variable.startsWith('--') ? variable : '--' + variable,
49
+ conditions: { true: trueVal },
50
+ defaultValue: falseVal,
51
+ });
52
+ }
53
+ }
54
+ }
55
+ return conditions;
56
+ }
57
+
58
+ /**
59
+ * Generate CSS if() output for detected conditions.
60
+ */
61
+ export function emitCSSIf(
62
+ selector: string,
63
+ detectedConditions: DetectedCondition[],
64
+ baseProperties: Record<string, string | number> = {}
65
+ ): string {
66
+ if (detectedConditions.length === 0) return '';
67
+
68
+ let css = '';
69
+
70
+ // Native CSS if() block
71
+ css += '/* Native CSS if() — Chrome 137+ */\n';
72
+ css += selector + ' {\n';
73
+ for (const [prop, value] of Object.entries(baseProperties)) {
74
+ css += ' ' + prop + ': ' + value + ';\n';
75
+ }
76
+ for (const cond of detectedConditions) {
77
+ const entries = Object.entries(cond.conditions);
78
+ if (entries.length === 1) {
79
+ const [condition, val] = entries[0];
80
+ css += ' ' + cond.property + ': if(style(' + cond.variable + ': ' + condition + '): ' + val + ' else ' + cond.defaultValue + ');\n';
81
+ } else {
82
+ let chain = '';
83
+ for (let i = 0; i < entries.length; i++) {
84
+ const [condition, val] = entries[i];
85
+ chain += i === 0
86
+ ? 'if(style(' + cond.variable + ': ' + condition + '): ' + val
87
+ : ' else if(style(' + cond.variable + ': ' + condition + '): ' + val;
88
+ }
89
+ chain += ' else ' + cond.defaultValue + ')'.repeat(entries.length);
90
+ css += ' ' + cond.property + ': ' + chain + ';\n';
91
+ }
92
+ }
93
+ css += '}\n\n';
94
+
95
+ // @supports fallback
96
+ css += '/* Fallback for browsers without CSS if() */\n';
97
+ css += '@supports not (property: if()) {\n';
98
+ css += ' ' + selector + ' {\n';
99
+ for (const [prop, value] of Object.entries(baseProperties)) {
100
+ css += ' ' + prop + ': ' + value + ';\n';
101
+ }
102
+ for (const cond of detectedConditions) {
103
+ css += ' ' + cond.property + ': ' + cond.defaultValue + ';\n';
104
+ }
105
+ css += ' }\n';
106
+ for (const cond of detectedConditions) {
107
+ for (const [condition, val] of Object.entries(cond.conditions)) {
108
+ const modClass = selector + '--' + cond.variable.replace('--', '') + '-' + condition;
109
+ css += ' ' + modClass + ' { ' + cond.property + ': ' + val + '; }\n';
110
+ }
111
+ }
112
+ css += '}\n';
113
+
114
+ return css;
115
+ }
116
+
117
+ export default { detectIfPatterns, emitCSSIf };