chaincss 2.0.7 → 2.1.1
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/CHANGELOG.md +30 -0
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +28 -0
- package/README.md +455 -226
- package/demo/demo/node_modules/caniuse-db/fulldata-json/data-2.0.json +1 -0
- package/demo/index.html +16 -0
- package/demo/package.json +20 -0
- package/demo/src/App.tsx +117 -0
- package/demo/src/chaincss-barrel.ts +9 -0
- package/demo/src/main.tsx +8 -0
- package/demo/src/styles.chain.ts +300 -0
- package/demo/vite.config.ts +46 -0
- package/dist/cli/commands/build.d.ts +0 -1
- package/dist/cli/commands/cache.d.ts +1 -0
- package/dist/cli/commands/init.d.ts +6 -3
- package/dist/cli/commands/timeline.d.ts +0 -1
- package/dist/cli/commands/watch.d.ts +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +3213 -5296
- package/dist/cli/types.d.ts +51 -20
- package/dist/cli/utils/config-loader.d.ts +0 -1
- package/dist/cli/utils/file-utils.d.ts +27 -3
- package/dist/cli/utils/logger.d.ts +0 -1
- package/dist/compiler/Chain.d.ts +215 -0
- package/dist/compiler/animations.d.ts +76 -0
- package/dist/compiler/atomic-optimizer.d.ts +47 -12
- package/dist/compiler/breakpoints.d.ts +46 -0
- package/dist/compiler/btt.d.ts +36 -60
- package/dist/compiler/cache-manager.d.ts +58 -4
- package/dist/compiler/commonProps.d.ts +0 -1
- package/dist/compiler/content-addressable-cache.d.ts +78 -0
- package/dist/compiler/helpers.d.ts +54 -0
- package/dist/compiler/index.d.ts +16 -9
- package/dist/compiler/index.js +4450 -4316
- package/dist/compiler/prefixer.d.ts +17 -1
- package/dist/compiler/shorthands.d.ts +28 -0
- package/dist/compiler/suggestions.d.ts +43 -0
- package/dist/compiler/theme-contract.d.ts +16 -27
- package/dist/compiler/token-resolver.d.ts +69 -0
- package/dist/compiler/tokens.d.ts +33 -8
- package/dist/core/auto-detector.d.ts +34 -0
- package/dist/core/common-utils.d.ts +97 -0
- package/dist/core/compiler.d.ts +63 -23
- package/dist/core/constants.d.ts +137 -36
- package/dist/core/smart-chain.d.ts +3 -0
- package/dist/core/types.d.ts +122 -15
- package/dist/core/utils.d.ts +134 -17
- package/dist/index.d.ts +52 -8
- package/dist/index.js +7090 -5578
- package/dist/plugins/vite.d.ts +7 -5
- package/dist/plugins/vite.js +2964 -25641
- package/dist/plugins/webpack.d.ts +24 -1
- package/dist/plugins/webpack.js +209 -72
- package/dist/runtime/Chain.d.ts +32 -0
- package/dist/runtime/auto-hooks.d.ts +11 -0
- package/dist/runtime/hmr.d.ts +22 -2
- package/dist/runtime/index.d.ts +3 -2
- package/dist/runtime/index.js +3648 -301
- package/dist/runtime/injector.d.ts +39 -72
- package/dist/runtime/react.d.ts +17 -12
- package/dist/runtime/svelte.d.ts +15 -0
- package/dist/runtime/types.d.ts +126 -4
- package/dist/runtime/utils.d.ts +0 -1
- package/dist/runtime/vue.d.ts +34 -14
- package/package.json +59 -66
- package/src/cli/commands/build.ts +133 -0
- package/src/cli/commands/cache.ts +371 -0
- package/src/cli/commands/init.ts +230 -0
- package/src/cli/commands/timeline.ts +435 -0
- package/src/cli/commands/watch.ts +211 -0
- package/src/cli/index.ts +226 -0
- package/src/cli/types.ts +100 -0
- package/src/cli/utils/config-loader.ts +174 -0
- package/src/cli/utils/file-utils.ts +139 -0
- package/src/cli/utils/logger.ts +74 -0
- package/src/compiler/Chain.ts +831 -0
- package/src/compiler/animations.ts +517 -0
- package/src/compiler/atomic-optimizer.ts +786 -0
- package/src/compiler/breakpoints.ts +347 -0
- package/src/compiler/btt.ts +1147 -0
- package/src/compiler/cache-manager.ts +446 -0
- package/src/compiler/commonProps.ts +18 -0
- package/src/compiler/content-addressable-cache.ts +478 -0
- package/src/compiler/helpers.ts +407 -0
- package/src/compiler/index.ts +72 -0
- package/src/compiler/prefixer.ts +720 -0
- package/src/compiler/shorthands.ts +558 -0
- package/src/compiler/suggestions.ts +436 -0
- package/src/compiler/theme-contract.ts +197 -0
- package/src/compiler/token-resolver.ts +241 -0
- package/src/compiler/tokens.ts +612 -0
- package/src/core/auto-detector.ts +187 -0
- package/src/core/common-utils.ts +423 -0
- package/src/core/compiler.ts +835 -0
- package/src/core/constants.ts +424 -0
- package/src/core/index.ts +107 -0
- package/src/core/smart-chain.ts +163 -0
- package/src/core/types.ts +257 -0
- package/src/core/utils.ts +598 -0
- package/src/index.ts +208 -0
- package/src/plugins/vite.d.ts +316 -0
- package/src/plugins/vite.ts +424 -0
- package/src/plugins/webpack.d.ts +289 -0
- package/src/plugins/webpack.ts +416 -0
- package/src/runtime/Chain.ts +242 -0
- package/src/runtime/auto-hooks.tsx +127 -0
- package/src/runtime/auto-vue.ts +72 -0
- package/src/runtime/hmr.ts +212 -0
- package/src/runtime/index.ts +82 -0
- package/src/runtime/injector.ts +273 -0
- package/src/runtime/react.tsx +269 -0
- package/src/runtime/svelte.ts +15 -0
- package/src/runtime/types.ts +256 -0
- package/src/runtime/utils.ts +128 -0
- package/src/runtime/vite-env.d.ts +120 -0
- package/src/runtime/vue.ts +231 -0
- package/tsconfig.build.json +41 -0
- package/tsconfig.json +25 -0
- package/tsconfig.runtimes.json +18 -0
- package/dist/cli/cli.cjs +0 -7
- package/dist/cli/commands/build.d.ts.map +0 -1
- package/dist/cli/commands/compile.d.ts +0 -3
- package/dist/cli/commands/compile.d.ts.map +0 -1
- package/dist/cli/commands/init.d.ts.map +0 -1
- package/dist/cli/commands/timeline.d.ts.map +0 -1
- package/dist/cli/commands/watch.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/utils/config-loader.d.ts.map +0 -1
- package/dist/cli/utils/file-utils.d.ts.map +0 -1
- package/dist/cli/utils/logger.d.ts.map +0 -1
- package/dist/compiler/atomic-optimizer.d.ts.map +0 -1
- package/dist/compiler/btt.d.ts.map +0 -1
- package/dist/compiler/cache-manager.d.ts.map +0 -1
- package/dist/compiler/commonProps.d.ts.map +0 -1
- package/dist/compiler/index.d.ts.map +0 -1
- package/dist/compiler/prefixer.d.ts.map +0 -1
- package/dist/compiler/theme-contract.d.ts.map +0 -1
- package/dist/compiler/tokens.d.ts.map +0 -1
- package/dist/compiler/types.d.ts +0 -57
- package/dist/compiler/types.d.ts.map +0 -1
- package/dist/core/compiler.d.ts.map +0 -1
- package/dist/core/constants.d.ts.map +0 -1
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/utils.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/plugins/vite.d.ts.map +0 -1
- package/dist/plugins/webpack.d.ts.map +0 -1
- package/dist/runtime/hmr.d.ts.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/injector.d.ts.map +0 -1
- package/dist/runtime/react.d.ts.map +0 -1
- package/dist/runtime/react.js +0 -324
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/utils.d.ts.map +0 -1
- package/dist/runtime/vue.d.ts.map +0 -1
- package/dist/runtime/vue.js +0 -286
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
// chaincss/src/compiler/atomic-optimizer.ts
|
|
2
|
+
import { ChainCSSConfig } from '../cli/types.js';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export interface AtomicClass {
|
|
9
|
+
className: string;
|
|
10
|
+
prop: string;
|
|
11
|
+
value: any;
|
|
12
|
+
usageCount: number;
|
|
13
|
+
rules?: string;
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
hash?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AtomicOptimizerStats {
|
|
19
|
+
totalStyles: number;
|
|
20
|
+
atomicStyles: number;
|
|
21
|
+
standardStyles: number;
|
|
22
|
+
uniqueProperties: number;
|
|
23
|
+
savings: string;
|
|
24
|
+
cacheHitRate?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AtomicOptimizerOptions {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
threshold?: number;
|
|
30
|
+
naming?: 'hash' | 'readable';
|
|
31
|
+
cache?: boolean;
|
|
32
|
+
cachePath?: string;
|
|
33
|
+
minify?: boolean;
|
|
34
|
+
mode?: 'standard' | 'atomic' | 'hybrid';
|
|
35
|
+
outputStrategy?: 'component-first' | 'utility-first';
|
|
36
|
+
alwaysAtomic?: string[];
|
|
37
|
+
neverAtomic?: string[];
|
|
38
|
+
frameworkOutput?: {
|
|
39
|
+
react?: boolean;
|
|
40
|
+
vue?: boolean;
|
|
41
|
+
vanilla?: boolean;
|
|
42
|
+
};
|
|
43
|
+
preserveSelectors?: boolean;
|
|
44
|
+
verbose?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ComponentClassMapEntry {
|
|
48
|
+
atomicClasses: string[];
|
|
49
|
+
hoverAtomicClasses: string[];
|
|
50
|
+
selectors: string[];
|
|
51
|
+
componentClassName?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface OptimizeResult {
|
|
55
|
+
css: string;
|
|
56
|
+
map: Record<string, string>;
|
|
57
|
+
stats: AtomicOptimizerStats;
|
|
58
|
+
atomicCSS: string;
|
|
59
|
+
componentCSS: string;
|
|
60
|
+
componentMap?: Map<string, ComponentClassMapEntry>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Utility functions
|
|
64
|
+
function hashKey(key: string): string {
|
|
65
|
+
return crypto.createHash('sha1').update(key).digest('hex').slice(0, 6);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function kebab(s: string): string {
|
|
69
|
+
return s.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class AtomicOptimizer {
|
|
73
|
+
private config: ChainCSSConfig;
|
|
74
|
+
options: Required<AtomicOptimizerOptions>;
|
|
75
|
+
private usageCount: Map<string, number>;
|
|
76
|
+
private atomicClasses: Map<string, AtomicClass>;
|
|
77
|
+
public atomicMap: Record<string, string> = {};
|
|
78
|
+
componentClassMap: Map<string, ComponentClassMapEntry>;
|
|
79
|
+
stats: {
|
|
80
|
+
totalStyles: number;
|
|
81
|
+
atomicStyles: number;
|
|
82
|
+
standardStyles: number;
|
|
83
|
+
uniqueProperties: number;
|
|
84
|
+
savings: number;
|
|
85
|
+
cacheHits: number;
|
|
86
|
+
cacheMisses: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
constructor(config: any) {
|
|
90
|
+
this.config = config;
|
|
91
|
+
const atomicOptions = config.atomic || {};
|
|
92
|
+
|
|
93
|
+
this.options = {
|
|
94
|
+
enabled: atomicOptions.enabled !== false,
|
|
95
|
+
threshold: atomicOptions.threshold ?? 2,
|
|
96
|
+
naming: atomicOptions.naming ?? (process.env.NODE_ENV === 'production' ? 'hash' : 'readable'),
|
|
97
|
+
cache: atomicOptions.cache !== false,
|
|
98
|
+
cachePath: atomicOptions.cachePath || './.chaincss-cache/atomic-cache.json',
|
|
99
|
+
minify: config.output?.minify ?? true,
|
|
100
|
+
mode: atomicOptions.mode || 'hybrid',
|
|
101
|
+
outputStrategy: atomicOptions.outputStrategy || 'component-first',
|
|
102
|
+
alwaysAtomic: atomicOptions.alwaysAtomic || [],
|
|
103
|
+
neverAtomic: atomicOptions.neverAtomic || [
|
|
104
|
+
'content', 'animation', 'transition', 'keyframes',
|
|
105
|
+
'animation-name', 'animation-duration', 'animation-timing-function',
|
|
106
|
+
'transition-property', 'transition-duration'
|
|
107
|
+
],
|
|
108
|
+
frameworkOutput: { react: false, vue: false, vanilla: true },
|
|
109
|
+
preserveSelectors: atomicOptions.preserveSelectors || false,
|
|
110
|
+
verbose: config.verbose || false,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.usageCount = new Map();
|
|
114
|
+
this.atomicClasses = new Map();
|
|
115
|
+
this.componentClassMap = new Map();
|
|
116
|
+
this.stats = {
|
|
117
|
+
totalStyles: 0,
|
|
118
|
+
atomicStyles: 0,
|
|
119
|
+
standardStyles: 0,
|
|
120
|
+
uniqueProperties: 0,
|
|
121
|
+
savings: 0,
|
|
122
|
+
cacheHits: 0,
|
|
123
|
+
cacheMisses: 0
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (this.options.cache) {
|
|
127
|
+
this.loadCache();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public componentMap = new Map<string, ComponentClassMapEntry>();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get usage count for a specific property-value pair
|
|
135
|
+
*/
|
|
136
|
+
public getUsageCount(prop: string, value: string, context: string = ''): number {
|
|
137
|
+
const key = context ? `${context}|${prop}:${value}` : `${prop}:${value}`;
|
|
138
|
+
return this.usageCount.get(key) || 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Increment usage count for a specific property-value pair
|
|
143
|
+
*/
|
|
144
|
+
public incrementUsageCount(prop: string, value: string, context: string = ''): void {
|
|
145
|
+
const key = context ? `${context}|${prop}:${value}` : `${prop}:${value}`;
|
|
146
|
+
const current = this.usageCount.get(key) || 0;
|
|
147
|
+
this.usageCount.set(key, current + 1);
|
|
148
|
+
this.stats.totalStyles++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get the usage count map for debugging
|
|
153
|
+
*/
|
|
154
|
+
public getUsageCountMap(): Map<string, number> {
|
|
155
|
+
return new Map(this.usageCount);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Cache Management
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
private loadCache(): void {
|
|
163
|
+
try {
|
|
164
|
+
const cacheDir = path.dirname(this.options.cachePath);
|
|
165
|
+
if (!fs.existsSync(cacheDir)) {
|
|
166
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!fs.existsSync(this.options.cachePath)) return;
|
|
170
|
+
|
|
171
|
+
const data = JSON.parse(fs.readFileSync(this.options.cachePath, 'utf8'));
|
|
172
|
+
|
|
173
|
+
if (data.version !== '2.0.0') {
|
|
174
|
+
if (this.options.verbose) {
|
|
175
|
+
console.log('Cache version mismatch, rebuilding...');
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (data.config?.threshold !== this.options.threshold) {
|
|
181
|
+
if (this.options.verbose) {
|
|
182
|
+
console.log('Threshold changed, rebuilding cache...');
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Restore atomic classes
|
|
188
|
+
if (data.atomicClasses) {
|
|
189
|
+
for (const [key, value] of data.atomicClasses) {
|
|
190
|
+
this.atomicClasses.set(key, value);
|
|
191
|
+
// Also rebuild atomicMap
|
|
192
|
+
const atomic = value;
|
|
193
|
+
this.atomicMap[`${atomic.prop}:${atomic.value}`] = atomic.className;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (data.componentClassMap) {
|
|
198
|
+
for (const [key, value] of data.componentClassMap) {
|
|
199
|
+
this.componentClassMap.set(key, value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (data.stats) {
|
|
204
|
+
this.stats = { ...this.stats, ...data.stats };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this.options.verbose) {
|
|
208
|
+
console.log(`✅ Cache loaded: ${this.atomicClasses.size} atomic classes`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (this.options.verbose) {
|
|
213
|
+
console.log('Could not load cache:', (err as Error).message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private saveCache(): void {
|
|
219
|
+
if (!this.options.cache) return;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const cacheDir = path.dirname(this.options.cachePath);
|
|
223
|
+
if (!fs.existsSync(cacheDir)) {
|
|
224
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const cache = {
|
|
228
|
+
version: '2.0.0',
|
|
229
|
+
timestamp: Date.now(),
|
|
230
|
+
atomicClasses: Array.from(this.atomicClasses.entries()),
|
|
231
|
+
componentClassMap: Array.from(this.componentClassMap.entries()),
|
|
232
|
+
stats: {
|
|
233
|
+
totalStyles: this.stats.totalStyles,
|
|
234
|
+
atomicStyles: this.stats.atomicStyles,
|
|
235
|
+
standardStyles: this.stats.standardStyles,
|
|
236
|
+
uniqueProperties: this.stats.uniqueProperties
|
|
237
|
+
},
|
|
238
|
+
config: {
|
|
239
|
+
threshold: this.options.threshold,
|
|
240
|
+
naming: this.options.naming,
|
|
241
|
+
mode: this.options.mode,
|
|
242
|
+
outputStrategy: this.options.outputStrategy
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
fs.writeFileSync(this.options.cachePath, JSON.stringify(cache, null, 2), 'utf8');
|
|
247
|
+
|
|
248
|
+
if (this.options.verbose) {
|
|
249
|
+
console.log(`💾 Cache saved: ${this.atomicClasses.size} atomic classes`);
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (this.options.verbose) {
|
|
253
|
+
console.log('Could not save cache:', (err as Error).message);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Style Tracking
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
public trackStyles(styles: any[]): void {
|
|
263
|
+
if (!Array.isArray(styles)) return;
|
|
264
|
+
|
|
265
|
+
for (const styleDef of styles) {
|
|
266
|
+
if (!styleDef || typeof styleDef !== 'object') continue;
|
|
267
|
+
|
|
268
|
+
for (const [prop, value] of Object.entries(styleDef)) {
|
|
269
|
+
if (prop === 'selectors' || prop === 'path' || prop === 'atRules' || prop === 'nestedRules') continue;
|
|
270
|
+
if (prop.startsWith('_')) continue;
|
|
271
|
+
|
|
272
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
273
|
+
this.incrementUsageCount(prop, String(value));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle hover states
|
|
278
|
+
if (styleDef.hover && typeof styleDef.hover === 'object') {
|
|
279
|
+
for (const [hProp, hValue] of Object.entries(styleDef.hover)) {
|
|
280
|
+
this.incrementUsageCount(hProp, String(hValue), 'hover');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle nested rules
|
|
285
|
+
if (styleDef.nestedRules && Array.isArray(styleDef.nestedRules)) {
|
|
286
|
+
for (const nested of styleDef.nestedRules) {
|
|
287
|
+
if (nested.styles) {
|
|
288
|
+
this.trackStyles([nested.styles]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// String-Based Scanning
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
public process(styleChain: string): void {
|
|
300
|
+
try {
|
|
301
|
+
const styleObj: Record<string, any> = {};
|
|
302
|
+
// Better regex for parsing chain methods
|
|
303
|
+
const methodRegex = /\.([a-zA-Z0-9]+)\s*\(\s*(['"]?)([^'")]+)\2\s*\)/g;
|
|
304
|
+
let match;
|
|
305
|
+
|
|
306
|
+
while ((match = methodRegex.exec(styleChain)) !== null) {
|
|
307
|
+
const [, prop, , value] = match;
|
|
308
|
+
if (prop && value) {
|
|
309
|
+
styleObj[prop] = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (Object.keys(styleObj).length > 0) {
|
|
314
|
+
this.trackStyles([styleObj]);
|
|
315
|
+
|
|
316
|
+
for (const [prop, value] of Object.entries(styleObj)) {
|
|
317
|
+
const className = this.getOrCreateAtomic(prop, value as string);
|
|
318
|
+
this.atomicMap[`${prop}:${value}`] = className;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
// Silent fail for malformed chains
|
|
323
|
+
if (this.options.verbose) {
|
|
324
|
+
console.log('Failed to process style chain:', e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private processStyleObject(style: any, context: string = 'base'): void {
|
|
330
|
+
if (!style) return;
|
|
331
|
+
|
|
332
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
333
|
+
if (['selectors', 'atRules', 'nestedRules', 'hover'].includes(prop)) continue;
|
|
334
|
+
if (prop.startsWith('_')) continue;
|
|
335
|
+
|
|
336
|
+
this.incrementUsage(prop, value as string, context);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (style.hover) {
|
|
340
|
+
for (const [hProp, hVal] of Object.entries(style.hover)) {
|
|
341
|
+
this.incrementUsage(hProp, hVal as string, `${context}:hover`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (style.nestedRules) {
|
|
346
|
+
style.nestedRules.forEach((nested: any) =>
|
|
347
|
+
this.processStyleObject(nested.styles, context)
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (style.atRules) {
|
|
352
|
+
style.atRules.forEach((rule: any) => {
|
|
353
|
+
if (rule.styles) this.processStyleObject(rule.styles, rule.query || context);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Fixed: Class name generation with proper prefixes
|
|
359
|
+
private generateClassName(prop: string, value: string, type: string): string {
|
|
360
|
+
const hash = crypto.createHash('md5')
|
|
361
|
+
.update(`${prop}${value}`)
|
|
362
|
+
.digest('hex')
|
|
363
|
+
.slice(0, 6);
|
|
364
|
+
|
|
365
|
+
const propKebab = kebab(prop);
|
|
366
|
+
|
|
367
|
+
// For atomic utilities, use 'a-' prefix
|
|
368
|
+
if (type === 'atomic') {
|
|
369
|
+
return `a-${propKebab}-${hash}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// For components, use single 'c-' prefix
|
|
373
|
+
return `c-${propKebab}-${hash}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private incrementUsage(prop: string, value: string, context: string = 'base'): void {
|
|
377
|
+
const key = `${context}|${prop}:${value}`;
|
|
378
|
+
const count = (this.usageCount.get(key) || 0) + 1;
|
|
379
|
+
this.usageCount.set(key, count);
|
|
380
|
+
this.stats.totalStyles++;
|
|
381
|
+
|
|
382
|
+
if (this.atomicClasses.has(key)) {
|
|
383
|
+
const atomic = this.atomicClasses.get(key)!;
|
|
384
|
+
atomic.usageCount = count;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private shouldBeAtomic(prop: string, value: string, context: string = 'base'): boolean {
|
|
389
|
+
if (this.options.mode === 'standard') return false;
|
|
390
|
+
if (this.options.mode === 'atomic') return true;
|
|
391
|
+
|
|
392
|
+
const kebabProp = kebab(prop);
|
|
393
|
+
|
|
394
|
+
if (this.options.neverAtomic.includes(kebabProp)) return false;
|
|
395
|
+
if (this.options.alwaysAtomic.includes(kebabProp)) return true;
|
|
396
|
+
|
|
397
|
+
const key = `${context}|${prop}:${value}`;
|
|
398
|
+
const usage = this.usageCount.get(key) || 0;
|
|
399
|
+
|
|
400
|
+
return usage >= this.options.threshold;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private getOrCreateAtomic(prop: string, value: string, context: string = 'base'): string {
|
|
404
|
+
const key = `${context}|${prop}:${value}`;
|
|
405
|
+
|
|
406
|
+
// Check cache hit
|
|
407
|
+
if (this.atomicClasses.has(key)) {
|
|
408
|
+
this.stats.cacheHits++;
|
|
409
|
+
return this.atomicClasses.get(key)!.className;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.stats.cacheMisses++;
|
|
413
|
+
|
|
414
|
+
const className = this.generateClassName(prop, value, 'atomic');
|
|
415
|
+
const propKebab = kebab(prop);
|
|
416
|
+
|
|
417
|
+
this.atomicClasses.set(key, {
|
|
418
|
+
className,
|
|
419
|
+
prop,
|
|
420
|
+
value,
|
|
421
|
+
rules: `${propKebab}: ${value};`,
|
|
422
|
+
usageCount: this.usageCount.get(key) || 0,
|
|
423
|
+
createdAt: Date.now(),
|
|
424
|
+
hash: crypto.createHash('md5').update(key).digest('hex').slice(0, 8)
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
this.stats.atomicStyles++;
|
|
428
|
+
this.stats.uniqueProperties++;
|
|
429
|
+
|
|
430
|
+
return className;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public getKeyFromClassName(className: string): string | null {
|
|
434
|
+
for (const [key, atomic] of this.atomicClasses) {
|
|
435
|
+
if (atomic.className === className) return key;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// CSS Generation
|
|
442
|
+
// ============================================================================
|
|
443
|
+
|
|
444
|
+
public generateAtomicCSS(): string {
|
|
445
|
+
let css = "/* ChainCSS Atomic Bundle */\n";
|
|
446
|
+
|
|
447
|
+
// Sort atomic classes for consistent output
|
|
448
|
+
const sorted = Array.from(this.atomicClasses.values()).sort((a, b) =>
|
|
449
|
+
a.className.localeCompare(b.className)
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
for (const data of sorted) {
|
|
453
|
+
css += `.${data.className} { ${data.rules} }\n`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return css;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public generateComponentCSS(style: any, selectors: string[], context: string = 'base'): {
|
|
460
|
+
css: string;
|
|
461
|
+
atomicClasses: string[];
|
|
462
|
+
} {
|
|
463
|
+
const atomicClasses: string[] = [];
|
|
464
|
+
let standardRules = '';
|
|
465
|
+
const selectorStr = selectors.join(', ');
|
|
466
|
+
|
|
467
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
468
|
+
if (['selectors', 'atRules', 'nestedRules', 'hover'].includes(prop) || prop.startsWith('_')) continue;
|
|
469
|
+
if (typeof value === 'object') continue;
|
|
470
|
+
|
|
471
|
+
if (this.shouldBeAtomic(prop, value as string, context)) {
|
|
472
|
+
const atomicClass = this.getOrCreateAtomic(prop, value as string, context);
|
|
473
|
+
atomicClasses.push(atomicClass);
|
|
474
|
+
} else {
|
|
475
|
+
standardRules += ` ${kebab(prop)}: ${value};\n`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let css = standardRules ? `${selectorStr} {\n${standardRules}}\n` : '';
|
|
480
|
+
|
|
481
|
+
if (style.nestedRules) {
|
|
482
|
+
style.nestedRules.forEach((nested: any) => {
|
|
483
|
+
const nestedSelector = nested.selector.replace('&', selectorStr);
|
|
484
|
+
const nestedResult = this.generateComponentCSS(nested.styles, [nestedSelector], context);
|
|
485
|
+
css += nestedResult.css;
|
|
486
|
+
atomicClasses.push(...nestedResult.atomicClasses);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (style.atRules) {
|
|
491
|
+
style.atRules.forEach((rule: any) => {
|
|
492
|
+
if (rule.styles) {
|
|
493
|
+
const ruleResult = this.generateComponentCSS(rule.styles, selectors, rule.query || context);
|
|
494
|
+
css += `@${rule.type} ${rule.query} {\n${ruleResult.css}}\n`;
|
|
495
|
+
atomicClasses.push(...ruleResult.atomicClasses);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { css, atomicClasses };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Generate a clean component name without any prefixes
|
|
505
|
+
*/
|
|
506
|
+
private getCleanComponentName(rawName: string, componentId: string): string {
|
|
507
|
+
// Remove leading dot
|
|
508
|
+
let clean = rawName.replace(/^\./, '');
|
|
509
|
+
|
|
510
|
+
// Remove any existing 'c-' or 'c-c-' prefixes
|
|
511
|
+
clean = clean.replace(/^c-+/, '');
|
|
512
|
+
|
|
513
|
+
// Replace special characters with hyphens
|
|
514
|
+
clean = clean.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
515
|
+
|
|
516
|
+
// Ensure it's not empty
|
|
517
|
+
if (!clean || clean === '&') {
|
|
518
|
+
clean = componentId.replace(/^c-+/, '');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Remove componentId prefix if it's duplicated
|
|
522
|
+
const componentIdClean = componentId.replace(/^c-+/, '');
|
|
523
|
+
if (clean === componentIdClean || clean === `c-${componentIdClean}`) {
|
|
524
|
+
clean = componentIdClean;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return clean;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Helper method to generate pseudo-class CSS
|
|
531
|
+
private generatePseudoCSS(
|
|
532
|
+
pseudoClass: string,
|
|
533
|
+
styles: Record<string, any>,
|
|
534
|
+
selector: string
|
|
535
|
+
): string {
|
|
536
|
+
let css = '';
|
|
537
|
+
const pseudoSelector = `${selector}:${pseudoClass}`;
|
|
538
|
+
let rules = '';
|
|
539
|
+
|
|
540
|
+
for (const [prop, value] of Object.entries(styles)) {
|
|
541
|
+
if (prop === 'selectors' || prop.startsWith('_')) continue;
|
|
542
|
+
const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
543
|
+
rules += ` ${kebabProp}: ${value};\n`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (rules) {
|
|
547
|
+
css = `${pseudoSelector} {\n${rules}}\n`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return css;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ============================================================================
|
|
554
|
+
// ✅ MAIN OPTIMIZE METHOD - FULLY FIXED
|
|
555
|
+
// ============================================================================
|
|
556
|
+
|
|
557
|
+
public optimize(styles: Record<string, any>): OptimizeResult {
|
|
558
|
+
const componentId = Object.keys(styles)[0];
|
|
559
|
+
let styleDef = styles[componentId];
|
|
560
|
+
let atomicClasses: string[] = [];
|
|
561
|
+
|
|
562
|
+
if (!styleDef || typeof styleDef !== 'object') {
|
|
563
|
+
return {
|
|
564
|
+
css: '',
|
|
565
|
+
map: {},
|
|
566
|
+
stats: this.getStats(),
|
|
567
|
+
atomicCSS: '',
|
|
568
|
+
componentCSS: '',
|
|
569
|
+
componentMap: this.componentMap
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 1. Selector & Class Name Generation
|
|
574
|
+
let rawName = Array.isArray(styleDef.selectors) ? styleDef.selectors[0] : styleDef.selectors;
|
|
575
|
+
if (!rawName || rawName === '&') {
|
|
576
|
+
rawName = componentId;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let cleanName = rawName.replace(/^\./, '').replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
580
|
+
if (!cleanName || cleanName.length > 50) {
|
|
581
|
+
cleanName = 'component';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const componentHash = crypto.createHash('md5')
|
|
585
|
+
.update(cleanName + componentId)
|
|
586
|
+
.digest('hex')
|
|
587
|
+
.slice(0, 6);
|
|
588
|
+
|
|
589
|
+
const componentClassName = `c-${cleanName}-${componentHash}`;
|
|
590
|
+
const selector = `.${componentClassName}`;
|
|
591
|
+
|
|
592
|
+
if (this.options.verbose) {
|
|
593
|
+
console.log(`[AtomicOptimizer] Optimizing component: ${componentId} -> ${componentClassName}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let classList: string[] = [componentClassName];
|
|
597
|
+
let localRules = "";
|
|
598
|
+
let pseudoRules = "";
|
|
599
|
+
|
|
600
|
+
// 2. Process each style property
|
|
601
|
+
for (const [prop, value] of Object.entries(styleDef)) {
|
|
602
|
+
// Skip metadata
|
|
603
|
+
if (prop === 'selectors' || prop === 'path' || prop.startsWith('_')) continue;
|
|
604
|
+
|
|
605
|
+
// Handle Pseudo-classes (hover, focus, active, etc.)
|
|
606
|
+
if (typeof value === 'object' && value !== null) {
|
|
607
|
+
if (['hover', 'focus', 'active', 'visited', 'disabled', 'checked'].includes(prop)) {
|
|
608
|
+
pseudoRules += this.generatePseudoCSS(prop, value as Record<string, any>, selector);
|
|
609
|
+
}
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (value === null || value === undefined) continue;
|
|
614
|
+
|
|
615
|
+
const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
616
|
+
const stringValue = String(value);
|
|
617
|
+
|
|
618
|
+
// 3. ATOMIC CHECK
|
|
619
|
+
if (this.shouldBeAtomic(prop, stringValue)) {
|
|
620
|
+
// Get or Create the global rule
|
|
621
|
+
const atomicClass = this.getOrCreateAtomic(prop, stringValue);
|
|
622
|
+
classList.push(atomicClass);
|
|
623
|
+
atomicClasses.push(atomicClass);
|
|
624
|
+
|
|
625
|
+
// POPULATE THE MAP: Fixes the empty atomicMap issue
|
|
626
|
+
this.atomicMap[`${prop}:${stringValue}`] = atomicClass;
|
|
627
|
+
|
|
628
|
+
// Update stats
|
|
629
|
+
this.stats.atomicStyles++;
|
|
630
|
+
|
|
631
|
+
if (this.options.verbose) {
|
|
632
|
+
console.log(` [Atomic] ${kebabProp}: ${stringValue} -> .${atomicClass}`);
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
// It stays local to the component
|
|
636
|
+
localRules += ` ${kebabProp}: ${stringValue};\n`;
|
|
637
|
+
this.stats.standardStyles++;
|
|
638
|
+
|
|
639
|
+
if (this.options.verbose) {
|
|
640
|
+
console.log(` [Standard] ${kebabProp}: ${stringValue}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Store component mapping
|
|
646
|
+
this.componentMap.set(componentId, {
|
|
647
|
+
atomicClasses: atomicClasses,
|
|
648
|
+
hoverAtomicClasses: [], // Will be populated if hover styles exist
|
|
649
|
+
selectors: [selector],
|
|
650
|
+
componentClassName: componentClassName
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
this.componentClassMap.set(componentId, {
|
|
654
|
+
atomicClasses: atomicClasses,
|
|
655
|
+
hoverAtomicClasses: [],
|
|
656
|
+
selectors: [selector]
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// 4. Build final CSS Output
|
|
660
|
+
let componentCSS = '';
|
|
661
|
+
if (localRules) {
|
|
662
|
+
componentCSS = `${selector} {\n${localRules}}\n`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (pseudoRules) {
|
|
666
|
+
componentCSS += pseudoRules;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const finalClassName = classList.join(' ');
|
|
670
|
+
|
|
671
|
+
// Save cache for future builds
|
|
672
|
+
this.saveCache();
|
|
673
|
+
|
|
674
|
+
// 5. Return the full result for the compiler to write to disk
|
|
675
|
+
return {
|
|
676
|
+
css: componentCSS,
|
|
677
|
+
map: {
|
|
678
|
+
[componentId]: finalClassName
|
|
679
|
+
},
|
|
680
|
+
stats: this.getStats(),
|
|
681
|
+
atomicCSS: this.generateAtomicCSS(),
|
|
682
|
+
componentCSS: componentCSS,
|
|
683
|
+
componentMap: this.componentMap
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Helper to process pseudo-states
|
|
688
|
+
private processPseudoState(
|
|
689
|
+
state: string,
|
|
690
|
+
styles: Record<string, any>,
|
|
691
|
+
selector: string
|
|
692
|
+
): string {
|
|
693
|
+
let css = '';
|
|
694
|
+
const pseudoSelector = `${selector}:${state}`;
|
|
695
|
+
let rules = '';
|
|
696
|
+
|
|
697
|
+
for (const [prop, value] of Object.entries(styles)) {
|
|
698
|
+
if (prop === 'selectors' || prop.startsWith('_')) continue;
|
|
699
|
+
const kebabProp = kebab(prop);
|
|
700
|
+
rules += ` ${kebabProp}: ${value};\n`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (rules) {
|
|
704
|
+
css = `${pseudoSelector} {\n${rules}}\n`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return css;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
public reset(): void {
|
|
711
|
+
this.usageCount.clear();
|
|
712
|
+
this.atomicClasses.clear();
|
|
713
|
+
this.componentClassMap.clear();
|
|
714
|
+
this.componentMap.clear();
|
|
715
|
+
this.atomicMap = {};
|
|
716
|
+
this.stats = {
|
|
717
|
+
totalStyles: 0,
|
|
718
|
+
atomicStyles: 0,
|
|
719
|
+
standardStyles: 0,
|
|
720
|
+
uniqueProperties: 0,
|
|
721
|
+
savings: 0,
|
|
722
|
+
cacheHits: 0,
|
|
723
|
+
cacheMisses: 0
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
if (this.options.verbose) {
|
|
727
|
+
console.log('AtomicOptimizer reset');
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
getStats(): AtomicOptimizerStats {
|
|
732
|
+
const total = this.stats.totalStyles;
|
|
733
|
+
const generatedRules = this.stats.uniqueProperties + this.stats.standardStyles;
|
|
734
|
+
|
|
735
|
+
// Calculate real savings
|
|
736
|
+
let savingsValue = 0;
|
|
737
|
+
if (total > 0) {
|
|
738
|
+
savingsValue = ((total - generatedRules) / total) * 100;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const totalCacheRequests = this.stats.cacheHits + this.stats.cacheMisses;
|
|
742
|
+
const cacheHitRate = totalCacheRequests > 0 ? (this.stats.cacheHits / totalCacheRequests) : 0;
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
totalStyles: total,
|
|
746
|
+
atomicStyles: this.atomicClasses.size,
|
|
747
|
+
standardStyles: this.stats.standardStyles,
|
|
748
|
+
uniqueProperties: this.stats.uniqueProperties,
|
|
749
|
+
savings: `${savingsValue.toFixed(1)}%`,
|
|
750
|
+
cacheHitRate: cacheHitRate
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
getAtomicClass(prop: string, value: string, context: string = ''): string | null {
|
|
755
|
+
const key = context ? `${context}|${prop}:${value}` : `${prop}:${value}`;
|
|
756
|
+
const atomic = this.atomicClasses.get(key);
|
|
757
|
+
return atomic ? atomic.className : null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
getAllAtomicClasses(): AtomicClass[] {
|
|
761
|
+
return Array.from(this.atomicClasses.values());
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
clearCache(): void {
|
|
765
|
+
this.atomicClasses.clear();
|
|
766
|
+
this.componentClassMap.clear();
|
|
767
|
+
this.usageCount.clear();
|
|
768
|
+
this.atomicMap = {};
|
|
769
|
+
if (this.options.cache && fs.existsSync(this.options.cachePath)) {
|
|
770
|
+
fs.unlinkSync(this.options.cachePath);
|
|
771
|
+
if (this.options.verbose) {
|
|
772
|
+
console.log('Cache cleared');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
public getComponentMapEntry(name: string): ComponentClassMapEntry | undefined {
|
|
778
|
+
return this.componentClassMap.get(name);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
public getAtomicMap(): Record<string, string> {
|
|
782
|
+
return this.atomicMap;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export { AtomicOptimizer as default };
|