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.
- package/ROADMAP.md +31 -0
- package/dist/cli/index.js +458 -3
- package/dist/compiler/analyzer.d.ts +12 -0
- package/dist/compiler/css-if-transpiler.d.ts +33 -0
- package/dist/compiler/design-orchestrator.d.ts +119 -0
- package/dist/compiler/intent-engine.d.ts +49 -0
- package/dist/compiler/math-engine.d.ts +89 -0
- package/dist/compiler/scroll-timeline.d.ts +91 -0
- package/dist/compiler/style-graph.d.ts +30 -0
- package/dist/core/compiler.d.ts +12 -0
- package/dist/core/types.d.ts +145 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1765 -9
- package/dist/plugins/vite.js +451 -3
- package/package.json +1 -1
- package/src/compiler/analyzer.ts +62 -0
- package/src/compiler/css-if-transpiler.ts +117 -0
- package/src/compiler/design-orchestrator.ts +322 -0
- package/src/compiler/intent-engine.ts +402 -0
- package/src/compiler/math-engine.ts +511 -0
- package/src/compiler/scroll-timeline.ts +284 -0
- package/src/compiler/style-graph.ts +660 -0
- package/src/core/compiler.ts +40 -0
- package/src/core/types.ts +206 -0
- package/src/index.ts +103 -1
- package/demo/demo/node_modules/caniuse-db/fulldata-json/data-2.0.json +0 -1
- package/demo/index.html +0 -16
- package/demo/package.json +0 -20
- package/demo/src/App.tsx +0 -117
- package/demo/src/chaincss-barrel.ts +0 -9
- package/demo/src/main.tsx +0 -8
- package/demo/src/styles.chain.ts +0 -300
- package/demo/vite.config.ts +0 -46
package/dist/plugins/vite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/core/compiler.ts
|
|
2
2
|
import fs6 from "fs";
|
|
3
3
|
import path5 from "path";
|
|
4
|
-
import
|
|
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 =
|
|
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
|
|
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
|
@@ -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 };
|