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/ROADMAP.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
## v3.0 Ideas
|
|
3
|
+
|
|
4
|
+
### 1. Predictive & Self-Healing CSS
|
|
5
|
+
- Context-aware typo correction (not just fuzzy matching)
|
|
6
|
+
- Intent detection: `display: "flexbox"` → maps to `display: flex` + applies defaults
|
|
7
|
+
- Modes: strict (error), dev (auto-fix), smart (fix + log)
|
|
8
|
+
- Builds on: suggestions.ts, explain()
|
|
9
|
+
|
|
10
|
+
### 2. Style Graph Compiler
|
|
11
|
+
- Instead of linear compilation, build a dependency graph of styles
|
|
12
|
+
- Dead style elimination
|
|
13
|
+
- Automatic merging of identical rules
|
|
14
|
+
- Predictive pre-compilation
|
|
15
|
+
- Order-safe CSS output
|
|
16
|
+
- Builds on: compile(), atomic optimizer
|
|
17
|
+
|
|
18
|
+
### 3. Unit-Aware Math Engine
|
|
19
|
+
- Full CSS expression evaluator
|
|
20
|
+
- `add("10px", "2rem")` → resolves units → optimized calc()
|
|
21
|
+
- `fluidType(14, 20)` → responsive clamp()
|
|
22
|
+
- Context-aware scaling
|
|
23
|
+
- Builds on: helpers.ts
|
|
24
|
+
|
|
25
|
+
### 4. IDE Intelligence Layer (VS Code Extension)
|
|
26
|
+
- Real-time invalid property detection
|
|
27
|
+
- Shorthand translation hints
|
|
28
|
+
- Animation suggestion
|
|
29
|
+
- Breakpoint inference
|
|
30
|
+
- Style conflict detection
|
|
31
|
+
- Builds on: suggestions.ts
|
package/dist/cli/index.js
CHANGED
|
@@ -2612,6 +2612,431 @@ var init_content_addressable_cache = __esm({
|
|
|
2612
2612
|
}
|
|
2613
2613
|
});
|
|
2614
2614
|
|
|
2615
|
+
// src/compiler/style-graph.ts
|
|
2616
|
+
import crypto4 from "crypto";
|
|
2617
|
+
function calculateSpecificity(selector) {
|
|
2618
|
+
let a = 0;
|
|
2619
|
+
let b = 0;
|
|
2620
|
+
let c = 0;
|
|
2621
|
+
const idMatches = selector.match(/#[a-zA-Z0-9_-]+/g);
|
|
2622
|
+
if (idMatches) a += idMatches.length;
|
|
2623
|
+
const classMatches = selector.match(/\.[a-zA-Z0-9_-]+/g);
|
|
2624
|
+
if (classMatches) b += classMatches.length;
|
|
2625
|
+
const attrMatches = selector.match(/\[[^\]]+\]/g);
|
|
2626
|
+
if (attrMatches) b += attrMatches.length;
|
|
2627
|
+
const pseudoClassMatches = selector.match(/:[a-zA-Z-]+(?:\([^)]*\))?/g);
|
|
2628
|
+
if (pseudoClassMatches) {
|
|
2629
|
+
const notMatches = selector.match(/:not\(([^)]+)\)/g);
|
|
2630
|
+
const regularPseudoClasses = pseudoClassMatches.length - (notMatches?.length || 0);
|
|
2631
|
+
b += Math.max(0, regularPseudoClasses);
|
|
2632
|
+
}
|
|
2633
|
+
const elementMatches = selector.match(/^[a-zA-Z]+|[a-zA-Z]+(?=[.#[:])/g);
|
|
2634
|
+
if (elementMatches) c += elementMatches.length;
|
|
2635
|
+
return a * 1e4 + b * 100 + c;
|
|
2636
|
+
}
|
|
2637
|
+
function hashProperties(properties) {
|
|
2638
|
+
const sorted = Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join(";");
|
|
2639
|
+
return crypto4.createHash("md5").update(sorted).digest("hex").slice(0, 8);
|
|
2640
|
+
}
|
|
2641
|
+
function kebab2(prop) {
|
|
2642
|
+
return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
2643
|
+
}
|
|
2644
|
+
function eliminateDeadStyles(graph, knownSelectors) {
|
|
2645
|
+
if (knownSelectors.length === 0) {
|
|
2646
|
+
return { eliminated: 0, graph };
|
|
2647
|
+
}
|
|
2648
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
2649
|
+
const queue = [];
|
|
2650
|
+
for (const [id, node] of graph.nodes) {
|
|
2651
|
+
if (knownSelectors.some((ks) => node.selector.includes(ks) || ks.includes(node.selector))) {
|
|
2652
|
+
reachable.add(id);
|
|
2653
|
+
queue.push(id);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
while (queue.length > 0) {
|
|
2657
|
+
const current = queue.shift();
|
|
2658
|
+
const node = graph.nodes.get(current);
|
|
2659
|
+
if (!node) continue;
|
|
2660
|
+
for (const depId of node.dependents) {
|
|
2661
|
+
if (!reachable.has(depId)) {
|
|
2662
|
+
reachable.add(depId);
|
|
2663
|
+
queue.push(depId);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
let eliminated = 0;
|
|
2668
|
+
for (const [id, node] of graph.nodes) {
|
|
2669
|
+
if (!reachable.has(id)) {
|
|
2670
|
+
node.isDead = true;
|
|
2671
|
+
eliminated++;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return { eliminated, graph };
|
|
2675
|
+
}
|
|
2676
|
+
function mergeIdenticalRules(graph, threshold) {
|
|
2677
|
+
const hashGroups = /* @__PURE__ */ new Map();
|
|
2678
|
+
for (const [, node] of graph.nodes) {
|
|
2679
|
+
if (node.isDead) continue;
|
|
2680
|
+
if (Object.keys(node.properties).length < threshold) continue;
|
|
2681
|
+
const existing = hashGroups.get(node.hash) || [];
|
|
2682
|
+
existing.push(node);
|
|
2683
|
+
hashGroups.set(node.hash, existing);
|
|
2684
|
+
}
|
|
2685
|
+
let merged = 0;
|
|
2686
|
+
for (const [, group] of hashGroups) {
|
|
2687
|
+
if (group.length < 2) continue;
|
|
2688
|
+
const mergedSelector = group.map((n) => n.selector).join(", ");
|
|
2689
|
+
const primary = group[0];
|
|
2690
|
+
primary.selector = mergedSelector;
|
|
2691
|
+
for (let i = 1; i < group.length; i++) {
|
|
2692
|
+
group[i].isDead = true;
|
|
2693
|
+
merged++;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return { merged, graph };
|
|
2697
|
+
}
|
|
2698
|
+
function topologicalSort(graph) {
|
|
2699
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2700
|
+
const sorted = [];
|
|
2701
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2702
|
+
function visit(id) {
|
|
2703
|
+
if (visited.has(id)) return true;
|
|
2704
|
+
if (visiting.has(id)) return false;
|
|
2705
|
+
visiting.add(id);
|
|
2706
|
+
const node = graph.nodes.get(id);
|
|
2707
|
+
if (node) {
|
|
2708
|
+
for (const depId of node.dependencies) {
|
|
2709
|
+
if (!visit(depId)) return false;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
visiting.delete(id);
|
|
2713
|
+
visited.add(id);
|
|
2714
|
+
if (node && !node.isDead) {
|
|
2715
|
+
sorted.push(node);
|
|
2716
|
+
}
|
|
2717
|
+
return true;
|
|
2718
|
+
}
|
|
2719
|
+
for (const id of graph.rootNodes) {
|
|
2720
|
+
if (!visit(id)) {
|
|
2721
|
+
return Array.from(graph.nodes.values()).filter((n) => !n.isDead).sort((a, b) => a.sourceComponent?.localeCompare(b.sourceComponent || "") || 0);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
for (const [id] of graph.nodes) {
|
|
2725
|
+
if (!visited.has(id)) {
|
|
2726
|
+
visit(id);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
return sorted;
|
|
2730
|
+
}
|
|
2731
|
+
function generateCSSFromGraph(graph, sortOutput = "specificity") {
|
|
2732
|
+
let nodes;
|
|
2733
|
+
switch (sortOutput) {
|
|
2734
|
+
case "specificity":
|
|
2735
|
+
nodes = Array.from(graph.nodes.values()).filter((n) => !n.isDead).sort((a, b) => a.specificity - b.specificity);
|
|
2736
|
+
break;
|
|
2737
|
+
case "topological":
|
|
2738
|
+
nodes = topologicalSort(graph);
|
|
2739
|
+
break;
|
|
2740
|
+
case "source-order":
|
|
2741
|
+
default:
|
|
2742
|
+
nodes = Array.from(graph.nodes.values()).filter((n) => !n.isDead);
|
|
2743
|
+
break;
|
|
2744
|
+
}
|
|
2745
|
+
let css = "";
|
|
2746
|
+
let currentMediaQuery;
|
|
2747
|
+
for (const node of nodes) {
|
|
2748
|
+
if (node.isDead) continue;
|
|
2749
|
+
if (node.mediaQuery !== currentMediaQuery) {
|
|
2750
|
+
if (currentMediaQuery) {
|
|
2751
|
+
css += "}\n\n";
|
|
2752
|
+
}
|
|
2753
|
+
if (node.mediaQuery) {
|
|
2754
|
+
css += `@media ${node.mediaQuery} {
|
|
2755
|
+
`;
|
|
2756
|
+
}
|
|
2757
|
+
currentMediaQuery = node.mediaQuery;
|
|
2758
|
+
}
|
|
2759
|
+
const rules = Object.entries(node.properties).map(([prop, value]) => ` ${kebab2(prop)}: ${value};`).join("\n");
|
|
2760
|
+
if (rules) {
|
|
2761
|
+
css += `${node.selector} {
|
|
2762
|
+
${rules}
|
|
2763
|
+
}
|
|
2764
|
+
`;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (currentMediaQuery) {
|
|
2768
|
+
css += "}\n";
|
|
2769
|
+
}
|
|
2770
|
+
return css;
|
|
2771
|
+
}
|
|
2772
|
+
var StyleGraphBuilder, StyleGraphCompiler;
|
|
2773
|
+
var init_style_graph = __esm({
|
|
2774
|
+
"src/compiler/style-graph.ts"() {
|
|
2775
|
+
"use strict";
|
|
2776
|
+
StyleGraphBuilder = class {
|
|
2777
|
+
entries = [];
|
|
2778
|
+
nodes = /* @__PURE__ */ new Map();
|
|
2779
|
+
edges = [];
|
|
2780
|
+
orderCounter = 0;
|
|
2781
|
+
addEntry(entry) {
|
|
2782
|
+
entry.sourceOrder = this.orderCounter++;
|
|
2783
|
+
this.entries.push(entry);
|
|
2784
|
+
}
|
|
2785
|
+
build() {
|
|
2786
|
+
this.nodes.clear();
|
|
2787
|
+
this.edges = [];
|
|
2788
|
+
for (const entry of this.entries) {
|
|
2789
|
+
const id = `node-${this.nodes.size}`;
|
|
2790
|
+
const node = {
|
|
2791
|
+
id,
|
|
2792
|
+
selector: entry.selector,
|
|
2793
|
+
properties: entry.properties,
|
|
2794
|
+
specificity: calculateSpecificity(entry.selector),
|
|
2795
|
+
dependencies: [],
|
|
2796
|
+
dependents: [],
|
|
2797
|
+
mediaQuery: entry.mediaQuery,
|
|
2798
|
+
isDead: false,
|
|
2799
|
+
hash: hashProperties(entry.properties),
|
|
2800
|
+
sourceComponent: entry.sourceComponent
|
|
2801
|
+
};
|
|
2802
|
+
this.nodes.set(id, node);
|
|
2803
|
+
}
|
|
2804
|
+
const nodeArray = Array.from(this.nodes.values());
|
|
2805
|
+
for (let i = 0; i < nodeArray.length; i++) {
|
|
2806
|
+
for (let j = i + 1; j < nodeArray.length; j++) {
|
|
2807
|
+
const a = nodeArray[i];
|
|
2808
|
+
const b = nodeArray[j];
|
|
2809
|
+
if (this.selectorsOverlap(a.selector, b.selector)) {
|
|
2810
|
+
if (a.specificity <= b.specificity) {
|
|
2811
|
+
this.edges.push({ from: a.id, to: b.id, type: "overrides" });
|
|
2812
|
+
a.dependents.push(b.id);
|
|
2813
|
+
b.dependencies.push(a.id);
|
|
2814
|
+
}
|
|
2815
|
+
if (b.specificity <= a.specificity) {
|
|
2816
|
+
this.edges.push({ from: b.id, to: a.id, type: "overrides" });
|
|
2817
|
+
b.dependents.push(a.id);
|
|
2818
|
+
a.dependencies.push(b.id);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
const rootNodes = nodeArray.filter((n) => n.dependencies.length === 0).map((n) => n.id);
|
|
2824
|
+
const leafNodes = nodeArray.filter((n) => n.dependents.length === 0).map((n) => n.id);
|
|
2825
|
+
return {
|
|
2826
|
+
nodes: this.nodes,
|
|
2827
|
+
edges: this.edges,
|
|
2828
|
+
rootNodes,
|
|
2829
|
+
leafNodes
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
selectorsOverlap(a, b) {
|
|
2833
|
+
const partsA = a.split(/[\s>+~]+/).filter(Boolean);
|
|
2834
|
+
const partsB = b.split(/[\s>+~]+/).filter(Boolean);
|
|
2835
|
+
for (const pa of partsA) {
|
|
2836
|
+
for (const pb of partsB) {
|
|
2837
|
+
if (pa === pb) return true;
|
|
2838
|
+
if (pa.startsWith(".") && pb.startsWith(".") && pa === pb) return true;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
return false;
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
StyleGraphCompiler = class {
|
|
2845
|
+
options;
|
|
2846
|
+
constructor(options = {}) {
|
|
2847
|
+
this.options = {
|
|
2848
|
+
eliminateDead: options.eliminateDead ?? false,
|
|
2849
|
+
knownSelectors: options.knownSelectors ?? [],
|
|
2850
|
+
mergeIdentical: options.mergeIdentical ?? false,
|
|
2851
|
+
mergeThreshold: options.mergeThreshold ?? 3,
|
|
2852
|
+
sortOutput: options.sortOutput ?? "specificity",
|
|
2853
|
+
verbose: options.verbose ?? false
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Compile a set of style definitions through the graph compiler.
|
|
2858
|
+
*/
|
|
2859
|
+
compile(styles) {
|
|
2860
|
+
const startTime = Date.now();
|
|
2861
|
+
const builder = new StyleGraphBuilder();
|
|
2862
|
+
let preOptimizationSize = 0;
|
|
2863
|
+
for (const [componentName, styleDef] of Object.entries(styles)) {
|
|
2864
|
+
if (!styleDef || !styleDef.selectors) continue;
|
|
2865
|
+
for (const selector of styleDef.selectors) {
|
|
2866
|
+
const properties = {};
|
|
2867
|
+
for (const [prop, value] of Object.entries(styleDef)) {
|
|
2868
|
+
if (prop === "selectors" || prop === "atRules" || prop === "nestedRules" || prop === "hover" || prop === "themes" || prop.startsWith("_")) {
|
|
2869
|
+
continue;
|
|
2870
|
+
}
|
|
2871
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
2872
|
+
properties[prop] = String(value);
|
|
2873
|
+
preOptimizationSize += String(value).length + prop.length;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
if (Object.keys(properties).length > 0) {
|
|
2877
|
+
builder.addEntry({
|
|
2878
|
+
selector,
|
|
2879
|
+
properties,
|
|
2880
|
+
sourceComponent: componentName,
|
|
2881
|
+
sourceOrder: 0
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
if (styleDef.hover && typeof styleDef.hover === "object") {
|
|
2885
|
+
const hoverProperties = {};
|
|
2886
|
+
for (const [prop, value] of Object.entries(styleDef.hover)) {
|
|
2887
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
2888
|
+
hoverProperties[prop] = String(value);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
if (Object.keys(hoverProperties).length > 0) {
|
|
2892
|
+
builder.addEntry({
|
|
2893
|
+
selector: `${selector}:hover`,
|
|
2894
|
+
properties: hoverProperties,
|
|
2895
|
+
sourceComponent: componentName,
|
|
2896
|
+
sourceOrder: 0
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
if (styleDef.atRules) {
|
|
2901
|
+
for (const rule of styleDef.atRules) {
|
|
2902
|
+
if (rule.type === "media" && rule.styles && rule.query) {
|
|
2903
|
+
const mediaProperties = {};
|
|
2904
|
+
for (const [prop, value] of Object.entries(rule.styles)) {
|
|
2905
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
2906
|
+
mediaProperties[prop] = String(value);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (Object.keys(mediaProperties).length > 0) {
|
|
2910
|
+
builder.addEntry({
|
|
2911
|
+
selector,
|
|
2912
|
+
properties: mediaProperties,
|
|
2913
|
+
sourceComponent: componentName,
|
|
2914
|
+
sourceOrder: 0,
|
|
2915
|
+
mediaQuery: rule.query
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
let graph = builder.build();
|
|
2924
|
+
let eliminatedDead = 0;
|
|
2925
|
+
if (this.options.eliminateDead && this.options.knownSelectors.length > 0) {
|
|
2926
|
+
const result = eliminateDeadStyles(graph, this.options.knownSelectors);
|
|
2927
|
+
eliminatedDead = result.eliminated;
|
|
2928
|
+
graph = result.graph;
|
|
2929
|
+
}
|
|
2930
|
+
let mergedRules = 0;
|
|
2931
|
+
if (this.options.mergeIdentical) {
|
|
2932
|
+
const result = mergeIdenticalRules(graph, this.options.mergeThreshold);
|
|
2933
|
+
mergedRules = result.merged;
|
|
2934
|
+
graph = result.graph;
|
|
2935
|
+
}
|
|
2936
|
+
const css = generateCSSFromGraph(graph, this.options.sortOutput);
|
|
2937
|
+
let postOptimizationSize = css.length;
|
|
2938
|
+
if (postOptimizationSize === 0) {
|
|
2939
|
+
postOptimizationSize = preOptimizationSize;
|
|
2940
|
+
}
|
|
2941
|
+
const classMap = {};
|
|
2942
|
+
for (const [, node] of graph.nodes) {
|
|
2943
|
+
if (!node.isDead && node.sourceComponent) {
|
|
2944
|
+
if (classMap[node.sourceComponent]) {
|
|
2945
|
+
classMap[node.sourceComponent] += ` ${node.selector.replace(/^\./, "")}`;
|
|
2946
|
+
} else {
|
|
2947
|
+
classMap[node.sourceComponent] = node.selector.replace(/^\./, "");
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const totalNodes = graph.nodes.size;
|
|
2952
|
+
const aliveNodes = totalNodes - eliminatedDead;
|
|
2953
|
+
const savingsPercent = preOptimizationSize > 0 ? `${((preOptimizationSize - postOptimizationSize) / preOptimizationSize * 100).toFixed(1)}%` : "0%";
|
|
2954
|
+
const stats = {
|
|
2955
|
+
totalStyles: totalNodes,
|
|
2956
|
+
atomicStyles: 0,
|
|
2957
|
+
uniqueProperties: new Set(
|
|
2958
|
+
Array.from(graph.nodes.values()).filter((n) => !n.isDead).flatMap((n) => Object.keys(n.properties))
|
|
2959
|
+
).size,
|
|
2960
|
+
savings: savingsPercent,
|
|
2961
|
+
compileTime: Date.now() - startTime
|
|
2962
|
+
};
|
|
2963
|
+
return {
|
|
2964
|
+
css,
|
|
2965
|
+
classMap,
|
|
2966
|
+
atomicClasses: [],
|
|
2967
|
+
stats,
|
|
2968
|
+
graph,
|
|
2969
|
+
eliminatedDead,
|
|
2970
|
+
mergedRules,
|
|
2971
|
+
optimizationTime: Date.now() - startTime,
|
|
2972
|
+
preOptimizationSize,
|
|
2973
|
+
postOptimizationSize
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Analyze a style graph without generating CSS.
|
|
2978
|
+
*/
|
|
2979
|
+
analyze(styles) {
|
|
2980
|
+
const builder = new StyleGraphBuilder();
|
|
2981
|
+
for (const [componentName, styleDef] of Object.entries(styles)) {
|
|
2982
|
+
if (!styleDef || !styleDef.selectors) continue;
|
|
2983
|
+
for (const selector of styleDef.selectors) {
|
|
2984
|
+
const properties = {};
|
|
2985
|
+
for (const [prop, value] of Object.entries(styleDef)) {
|
|
2986
|
+
if (prop === "selectors" || prop.startsWith("_")) continue;
|
|
2987
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
2988
|
+
properties[prop] = String(value);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
if (Object.keys(properties).length > 0) {
|
|
2992
|
+
builder.addEntry({ selector, properties, sourceComponent: componentName, sourceOrder: 0 });
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
return builder.build();
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Get optimization statistics for a graph.
|
|
3000
|
+
*/
|
|
3001
|
+
getStats(graph) {
|
|
3002
|
+
const nodes = Array.from(graph.nodes.values());
|
|
3003
|
+
const deadNodes = nodes.filter((n) => n.isDead).length;
|
|
3004
|
+
const averageSpecificity = nodes.length > 0 ? nodes.reduce((sum, n) => sum + n.specificity, 0) / nodes.length : 0;
|
|
3005
|
+
let maxDepth = 0;
|
|
3006
|
+
const depths = /* @__PURE__ */ new Map();
|
|
3007
|
+
function getDepth(id) {
|
|
3008
|
+
if (depths.has(id)) return depths.get(id);
|
|
3009
|
+
const node = graph.nodes.get(id);
|
|
3010
|
+
if (!node || node.dependencies.length === 0) {
|
|
3011
|
+
depths.set(id, 0);
|
|
3012
|
+
return 0;
|
|
3013
|
+
}
|
|
3014
|
+
const max = Math.max(...node.dependencies.map((d) => getDepth(d)));
|
|
3015
|
+
const depth = max + 1;
|
|
3016
|
+
depths.set(id, depth);
|
|
3017
|
+
return depth;
|
|
3018
|
+
}
|
|
3019
|
+
for (const [id] of graph.nodes) {
|
|
3020
|
+
maxDepth = Math.max(maxDepth, getDepth(id));
|
|
3021
|
+
}
|
|
3022
|
+
return {
|
|
3023
|
+
totalNodes: nodes.length,
|
|
3024
|
+
deadNodes,
|
|
3025
|
+
mergedGroups: 0,
|
|
3026
|
+
averageSpecificity: Math.round(averageSpecificity * 100) / 100,
|
|
3027
|
+
deepestDependencyChain: maxDepth
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Update options.
|
|
3032
|
+
*/
|
|
3033
|
+
configure(options) {
|
|
3034
|
+
this.options = { ...this.options, ...options };
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
|
|
2615
3040
|
// src/core/compiler.ts
|
|
2616
3041
|
var compiler_exports = {};
|
|
2617
3042
|
__export(compiler_exports, {
|
|
@@ -2620,7 +3045,7 @@ __export(compiler_exports, {
|
|
|
2620
3045
|
});
|
|
2621
3046
|
import fs7 from "fs";
|
|
2622
3047
|
import path6 from "path";
|
|
2623
|
-
import
|
|
3048
|
+
import crypto5 from "crypto";
|
|
2624
3049
|
import chalk2 from "chalk";
|
|
2625
3050
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
2626
3051
|
async function compileChainCSS(inputFile, outputDir, config) {
|
|
@@ -2639,6 +3064,7 @@ var init_compiler = __esm({
|
|
|
2639
3064
|
init_cache_manager();
|
|
2640
3065
|
init_content_addressable_cache();
|
|
2641
3066
|
init_shorthands();
|
|
3067
|
+
init_style_graph();
|
|
2642
3068
|
__filename = typeof import.meta !== "undefined" ? (() => {
|
|
2643
3069
|
try {
|
|
2644
3070
|
return fileURLToPath(import.meta.url);
|
|
@@ -2689,6 +3115,35 @@ var init_compiler = __esm({
|
|
|
2689
3115
|
this.initOptimizer();
|
|
2690
3116
|
this.initPrefixer();
|
|
2691
3117
|
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Compile using the style graph compiler for advanced optimizations.
|
|
3120
|
+
*
|
|
3121
|
+
* @example
|
|
3122
|
+
* const result = compiler.compileWithGraph(styles, {
|
|
3123
|
+
* eliminateDead: true,
|
|
3124
|
+
* knownSelectors: ['.header', '.footer'],
|
|
3125
|
+
* mergeIdentical: true
|
|
3126
|
+
* });
|
|
3127
|
+
*/
|
|
3128
|
+
compileWithGraph(styles, options) {
|
|
3129
|
+
const graphCompiler = new StyleGraphCompiler({
|
|
3130
|
+
...options,
|
|
3131
|
+
verbose: this.config.verbose
|
|
3132
|
+
});
|
|
3133
|
+
const result = graphCompiler.compile(styles);
|
|
3134
|
+
if (this.config.verbose) {
|
|
3135
|
+
if (result.eliminatedDead > 0) {
|
|
3136
|
+
console.log(` \u{1F9F9} Eliminated ${result.eliminatedDead} dead styles`);
|
|
3137
|
+
}
|
|
3138
|
+
if (result.mergedRules > 0) {
|
|
3139
|
+
console.log(` \u{1F517} Merged ${result.mergedRules} identical rules`);
|
|
3140
|
+
}
|
|
3141
|
+
if (result.optimizationTime > 0) {
|
|
3142
|
+
console.log(` \u26A1 Graph compilation: ${result.optimizationTime}ms`);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
return result;
|
|
3146
|
+
}
|
|
2692
3147
|
hasStyles() {
|
|
2693
3148
|
const combined = this.getCombinedCSS();
|
|
2694
3149
|
return !!(combined && combined.trim().length > 0);
|
|
@@ -2726,7 +3181,7 @@ var init_compiler = __esm({
|
|
|
2726
3181
|
if (result.css && result.css.trim()) {
|
|
2727
3182
|
this.accumulatedCSS += result.css + "\n";
|
|
2728
3183
|
}
|
|
2729
|
-
const cacheKey =
|
|
3184
|
+
const cacheKey = crypto5.createHash("sha256").update(`${componentName}-${JSON.stringify(styleObj)}`).digest("hex").slice(0, 16);
|
|
2730
3185
|
this.addToCache(cacheKey, {
|
|
2731
3186
|
result: {
|
|
2732
3187
|
css: result.css || "",
|
|
@@ -2854,7 +3309,7 @@ var init_compiler = __esm({
|
|
|
2854
3309
|
// ============================================================================
|
|
2855
3310
|
hashStyleDef(styleDef) {
|
|
2856
3311
|
const { _componentName, _generateComponent, _framework, _propsDefinition, ...relevant } = styleDef;
|
|
2857
|
-
return
|
|
3312
|
+
return crypto5.createHash("sha256").update(JSON.stringify(relevant)).digest("hex").slice(0, 16);
|
|
2858
3313
|
}
|
|
2859
3314
|
async importModule(filePath) {
|
|
2860
3315
|
const absolutePath = path6.resolve(filePath);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StyleDefinition, StyleDiagnostic, StyleAnalysis, BreakpointInference, DiagnosticSeverity } from '../core/types.js';
|
|
2
|
+
export type { StyleDiagnostic, StyleAnalysis, BreakpointInference, DiagnosticSeverity };
|
|
3
|
+
export declare class StyleAnalyzer {
|
|
4
|
+
private _diagnostics;
|
|
5
|
+
analyzeStyle(selector: string, styles: Record<string, any>, _opts?: any): StyleDiagnostic[];
|
|
6
|
+
analyze(sd: StyleDefinition): StyleAnalysis;
|
|
7
|
+
reset(): void;
|
|
8
|
+
getDiagnostics(): StyleDiagnostic[];
|
|
9
|
+
}
|
|
10
|
+
export declare function analyze(sd: StyleDefinition): StyleAnalysis;
|
|
11
|
+
export declare function analyzeStyle(sd: StyleDefinition): StyleAnalysis;
|
|
12
|
+
export default StyleAnalyzer;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS if() Transpiler
|
|
3
|
+
* Detects conditional style patterns and emits:
|
|
4
|
+
* 1. Native CSS if() — Chrome 137+
|
|
5
|
+
* 2. @supports fallback — Firefox, Safari
|
|
6
|
+
*/
|
|
7
|
+
export interface IfCondition {
|
|
8
|
+
property: string;
|
|
9
|
+
variable: string;
|
|
10
|
+
conditions: Record<string, string | number>;
|
|
11
|
+
defaultValue: string | number;
|
|
12
|
+
}
|
|
13
|
+
export interface DetectedCondition {
|
|
14
|
+
property: string;
|
|
15
|
+
variable: string;
|
|
16
|
+
conditions: Record<string, string | number>;
|
|
17
|
+
defaultValue: string | number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Detect conditional patterns from _conditions metadata.
|
|
21
|
+
* When chain.when() branches set the same property to different values,
|
|
22
|
+
* those can be compiled to CSS if().
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectIfPatterns(styles: Record<string, any>): DetectedCondition[];
|
|
25
|
+
/**
|
|
26
|
+
* Generate CSS if() output for detected conditions.
|
|
27
|
+
*/
|
|
28
|
+
export declare function emitCSSIf(selector: string, detectedConditions: DetectedCondition[], baseProperties?: Record<string, string | number>): string;
|
|
29
|
+
declare const _default: {
|
|
30
|
+
detectIfPatterns: typeof detectIfPatterns;
|
|
31
|
+
emitCSSIf: typeof emitCSSIf;
|
|
32
|
+
};
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design System Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* 1. WCAG Contrast Ratio Checker — validates text/background combos at build time
|
|
5
|
+
* 2. Contextual Tokens — tokens that auto-flip based on container context
|
|
6
|
+
* 3. Token Relationship Validator — ensures design tokens are consistent
|
|
7
|
+
*/
|
|
8
|
+
export interface ContrastResult {
|
|
9
|
+
foreground: string;
|
|
10
|
+
background: string;
|
|
11
|
+
ratio: number;
|
|
12
|
+
passes: {
|
|
13
|
+
AA: boolean;
|
|
14
|
+
AALarge: boolean;
|
|
15
|
+
AAA: boolean;
|
|
16
|
+
AAALarge: boolean;
|
|
17
|
+
};
|
|
18
|
+
suggestion?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ContrastReport {
|
|
21
|
+
checks: ContrastResult[];
|
|
22
|
+
failures: ContrastResult[];
|
|
23
|
+
warnings: ContrastResult[];
|
|
24
|
+
passCount: number;
|
|
25
|
+
failCount: number;
|
|
26
|
+
summary: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ContextualToken {
|
|
29
|
+
name: string;
|
|
30
|
+
default: string;
|
|
31
|
+
contexts: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface TokenContext {
|
|
34
|
+
name: string;
|
|
35
|
+
parentSelector?: string;
|
|
36
|
+
tokens: Record<string, any>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse CSS color to RGBA components.
|
|
40
|
+
* Supports: hex, rgb(), rgba(), named colors
|
|
41
|
+
*/
|
|
42
|
+
declare function parseColor(color: string): {
|
|
43
|
+
r: number;
|
|
44
|
+
g: number;
|
|
45
|
+
b: number;
|
|
46
|
+
a: number;
|
|
47
|
+
} | null;
|
|
48
|
+
/**
|
|
49
|
+
* Calculate WCAG contrast ratio between two colors.
|
|
50
|
+
* Returns value between 1 (no contrast) and 21 (max contrast).
|
|
51
|
+
*/
|
|
52
|
+
export declare function contrastRatio(foreground: string, background: string): number;
|
|
53
|
+
/**
|
|
54
|
+
* Check WCAG compliance levels.
|
|
55
|
+
* AA: 4.5:1 normal, 3:1 large text
|
|
56
|
+
* AAA: 7:1 normal, 4.5:1 large text
|
|
57
|
+
*/
|
|
58
|
+
export declare function checkContrast(foreground: string, background: string): ContrastResult;
|
|
59
|
+
/**
|
|
60
|
+
* Run contrast checks across a set of style definitions.
|
|
61
|
+
*/
|
|
62
|
+
export declare function auditContrast(styles: Array<{
|
|
63
|
+
selector: string;
|
|
64
|
+
color: string;
|
|
65
|
+
backgroundColor: string;
|
|
66
|
+
}>): ContrastReport;
|
|
67
|
+
/**
|
|
68
|
+
* Contextual tokens that auto-resolve based on parent container.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const buttonText = contextualToken({
|
|
72
|
+
* default: '#1a1a1a',
|
|
73
|
+
* contexts: {
|
|
74
|
+
* '.dark-section': '#ffffff',
|
|
75
|
+
* '.hero': '#ffffff',
|
|
76
|
+
* },
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* resolveContextual(buttonText, '.dark-section .my-button')
|
|
80
|
+
* // => '#ffffff'
|
|
81
|
+
*/
|
|
82
|
+
export declare function createContextualToken(defaultValue: string, contexts?: Record<string, string>): ContextualToken;
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a contextual token based on the current selector path.
|
|
85
|
+
* Matches the most specific context that applies.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveContextual(token: ContextualToken, selectorPath: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Generate CSS custom property fallback for contextual tokens.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* generateContextualCSS('button-text', contextualToken)
|
|
93
|
+
* // => "
|
|
94
|
+
* // .my-button { --button-text: #1a1a1a; }
|
|
95
|
+
* // .dark-section .my-button { --button-text: #ffffff; }
|
|
96
|
+
* // "
|
|
97
|
+
*/
|
|
98
|
+
export declare function generateContextualCSS(propertyName: string, token: ContextualToken, baseSelector: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Validate that token references are consistent.
|
|
101
|
+
* E.g., "primary" should have both foreground and background variants
|
|
102
|
+
* that contrast well with each other.
|
|
103
|
+
*/
|
|
104
|
+
export declare function validateTokenRelationships(tokens: Record<string, any>, pairs: Array<{
|
|
105
|
+
foreground: string;
|
|
106
|
+
background: string;
|
|
107
|
+
label: string;
|
|
108
|
+
}>): ContrastReport;
|
|
109
|
+
export declare const orchestrator: {
|
|
110
|
+
contrastRatio: typeof contrastRatio;
|
|
111
|
+
checkContrast: typeof checkContrast;
|
|
112
|
+
auditContrast: typeof auditContrast;
|
|
113
|
+
createContextualToken: typeof createContextualToken;
|
|
114
|
+
resolveContextual: typeof resolveContextual;
|
|
115
|
+
generateContextualCSS: typeof generateContextualCSS;
|
|
116
|
+
validateTokenRelationships: typeof validateTokenRelationships;
|
|
117
|
+
parseColor: typeof parseColor;
|
|
118
|
+
};
|
|
119
|
+
export default orchestrator;
|