chaincss 2.2.0 → 2.3.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/dist/compiler/accessibility-engine.d.ts +57 -0
- package/dist/compiler/constraint-solver.d.ts +85 -0
- package/dist/compiler/intent-api.d.ts +73 -0
- package/dist/compiler/layout-intelligence.d.ts +71 -0
- package/dist/compiler/pass-manager.d.ts +157 -0
- package/dist/compiler/pattern-learner.d.ts +112 -0
- package/dist/compiler/responsive-inference.d.ts +63 -0
- package/dist/compiler/semantic-tokens.d.ts +57 -0
- package/dist/compiler/source-optimizer.d.ts +109 -0
- package/dist/compiler/style-ir.d.ts +183 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3475 -0
- package/package.json +1 -1
- package/src/compiler/accessibility-engine.ts +502 -0
- package/src/compiler/constraint-solver.ts +407 -0
- package/src/compiler/intent-api.ts +505 -0
- package/src/compiler/layout-intelligence.ts +697 -0
- package/src/compiler/pass-manager.ts +657 -0
- package/src/compiler/pattern-learner.ts +398 -0
- package/src/compiler/responsive-inference.ts +415 -0
- package/src/compiler/semantic-tokens.ts +468 -0
- package/src/compiler/source-optimizer.ts +541 -0
- package/src/compiler/style-ir.ts +495 -0
- package/src/index.ts +175 -0
- package/ROADMAP.md +0 -31
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// src/compiler/pattern-learner.ts
|
|
2
|
+
/**
|
|
3
|
+
* Style Pattern Learner
|
|
4
|
+
*
|
|
5
|
+
* Observes all styles across a codebase and:
|
|
6
|
+
* 1. DETECTS repeated patterns by content-hashing style blocks
|
|
7
|
+
* 2. RANKS by frequency × property count
|
|
8
|
+
* 3. SUGGESTS extraction as chain.recipe() or intent macros
|
|
9
|
+
* 4. REPORTS savings (lines eliminated, bundle size reduction)
|
|
10
|
+
*
|
|
11
|
+
* This is compiler-assisted design system extraction.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { StyleIR, IRRule, IRPass } from './style-ir.js';
|
|
15
|
+
import crypto from 'crypto';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export interface StyleFingerprint {
|
|
22
|
+
/** Content hash of the property set */
|
|
23
|
+
hash: string;
|
|
24
|
+
/** Human-readable signature */
|
|
25
|
+
signature: string;
|
|
26
|
+
/** Properties and their values */
|
|
27
|
+
properties: Record<string, string | number>;
|
|
28
|
+
/** Number of properties */
|
|
29
|
+
propertyCount: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PatternCluster {
|
|
33
|
+
/** The fingerprint that defines this cluster */
|
|
34
|
+
fingerprint: StyleFingerprint;
|
|
35
|
+
/** All rules that match this pattern */
|
|
36
|
+
occurrences: Array<{
|
|
37
|
+
ruleId: string;
|
|
38
|
+
selector: string;
|
|
39
|
+
sourceFile?: string;
|
|
40
|
+
component?: string;
|
|
41
|
+
}>;
|
|
42
|
+
/** How many times it appears */
|
|
43
|
+
frequency: number;
|
|
44
|
+
/** Across how many files */
|
|
45
|
+
fileCount: number;
|
|
46
|
+
/** Importance score (frequency × propertyCount) */
|
|
47
|
+
score: number;
|
|
48
|
+
/** Suggested recipe name */
|
|
49
|
+
suggestedName: string;
|
|
50
|
+
/** Suggested extraction as recipe */
|
|
51
|
+
suggestedRecipe: string;
|
|
52
|
+
/** Estimated savings */
|
|
53
|
+
savings: {
|
|
54
|
+
declarations: number;
|
|
55
|
+
linesEliminated: number;
|
|
56
|
+
bundleReduction: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface LearningReport {
|
|
61
|
+
clusters: PatternCluster[];
|
|
62
|
+
totalPatterns: number;
|
|
63
|
+
highValuePatterns: PatternCluster[];
|
|
64
|
+
totalSavings: {
|
|
65
|
+
declarations: number;
|
|
66
|
+
estimatedBytes: number;
|
|
67
|
+
};
|
|
68
|
+
summary: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Fingerprinting
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a content hash from a set of declarations.
|
|
77
|
+
* Ignores selectors, focuses on the actual style properties.
|
|
78
|
+
*/
|
|
79
|
+
function fingerprintDeclarations(
|
|
80
|
+
declarations: Array<{ property: string; value: string | number }>
|
|
81
|
+
): StyleFingerprint {
|
|
82
|
+
// Sort properties alphabetically for consistent hashing
|
|
83
|
+
const sorted = [...declarations].sort((a, b) => a.property.localeCompare(b.property));
|
|
84
|
+
|
|
85
|
+
const properties: Record<string, string | number> = {};
|
|
86
|
+
const propertyList: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (const decl of sorted) {
|
|
89
|
+
properties[decl.property] = decl.value;
|
|
90
|
+
propertyList.push(decl.property + ':' + decl.value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const signature = propertyList.join('; ');
|
|
94
|
+
const hash = crypto.createHash('md5').update(signature).digest('hex').slice(0, 12);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
hash,
|
|
98
|
+
signature,
|
|
99
|
+
properties,
|
|
100
|
+
propertyCount: sorted.length,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Pattern Clustering
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Cluster rules by their style fingerprint.
|
|
110
|
+
* Only considers rules with a minimum number of declarations.
|
|
111
|
+
*/
|
|
112
|
+
function clusterPatterns(
|
|
113
|
+
rules: IRRule[],
|
|
114
|
+
options: { minProperties?: number; minFrequency?: number } = {}
|
|
115
|
+
): PatternCluster[] {
|
|
116
|
+
const minProperties = options.minProperties || 3;
|
|
117
|
+
const minFrequency = options.minFrequency || 2;
|
|
118
|
+
|
|
119
|
+
// Group by hash
|
|
120
|
+
const groups = new Map<string, {
|
|
121
|
+
fingerprint: StyleFingerprint;
|
|
122
|
+
occurrences: PatternCluster['occurrences'];
|
|
123
|
+
files: Set<string>;
|
|
124
|
+
}>();
|
|
125
|
+
|
|
126
|
+
for (const rule of rules) {
|
|
127
|
+
if (rule.isDead) continue;
|
|
128
|
+
if (rule.declarations.length < minProperties) continue;
|
|
129
|
+
|
|
130
|
+
const fp = fingerprintDeclarations(rule.declarations);
|
|
131
|
+
|
|
132
|
+
const existing = groups.get(fp.hash);
|
|
133
|
+
if (existing) {
|
|
134
|
+
existing.occurrences.push({
|
|
135
|
+
ruleId: rule.id,
|
|
136
|
+
selector: rule.selector,
|
|
137
|
+
sourceFile: rule.source.file,
|
|
138
|
+
component: rule.source.component,
|
|
139
|
+
});
|
|
140
|
+
if (rule.source.file) existing.files.add(rule.source.file);
|
|
141
|
+
} else {
|
|
142
|
+
groups.set(fp.hash, {
|
|
143
|
+
fingerprint: fp,
|
|
144
|
+
occurrences: [{
|
|
145
|
+
ruleId: rule.id,
|
|
146
|
+
selector: rule.selector,
|
|
147
|
+
sourceFile: rule.source.file,
|
|
148
|
+
component: rule.source.component,
|
|
149
|
+
}],
|
|
150
|
+
files: new Set(rule.source.file ? [rule.source.file] : []),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Filter by frequency and convert to clusters
|
|
156
|
+
const clusters: PatternCluster[] = [];
|
|
157
|
+
|
|
158
|
+
for (const [, group] of groups) {
|
|
159
|
+
if (group.occurrences.length < minFrequency) continue;
|
|
160
|
+
|
|
161
|
+
const frequency = group.occurrences.length;
|
|
162
|
+
const score = frequency * group.fingerprint.propertyCount;
|
|
163
|
+
|
|
164
|
+
// Generate a suggested name from the properties
|
|
165
|
+
const suggestedName = generatePatternName(group.fingerprint.properties);
|
|
166
|
+
|
|
167
|
+
// Calculate savings
|
|
168
|
+
const declarations = frequency * group.fingerprint.propertyCount;
|
|
169
|
+
const linesEliminated = declarations - 1; // Replaced by 1 macro call
|
|
170
|
+
const bundleReduction = estimateBundleSavings(declarations, frequency);
|
|
171
|
+
|
|
172
|
+
clusters.push({
|
|
173
|
+
fingerprint: group.fingerprint,
|
|
174
|
+
occurrences: group.occurrences,
|
|
175
|
+
frequency,
|
|
176
|
+
fileCount: group.files.size,
|
|
177
|
+
score,
|
|
178
|
+
suggestedName,
|
|
179
|
+
suggestedRecipe: generateRecipeCode(suggestedName, group.fingerprint.properties),
|
|
180
|
+
savings: {
|
|
181
|
+
declarations,
|
|
182
|
+
linesEliminated,
|
|
183
|
+
bundleReduction,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Sort by score descending
|
|
189
|
+
clusters.sort((a, b) => b.score - a.score);
|
|
190
|
+
|
|
191
|
+
return clusters;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Name Generation
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generate a human-readable pattern name from properties.
|
|
200
|
+
*/
|
|
201
|
+
function generatePatternName(properties: Record<string, string | number>): string {
|
|
202
|
+
const keys = Object.keys(properties);
|
|
203
|
+
|
|
204
|
+
// Try to infer semantic name from common property combinations
|
|
205
|
+
if (hasAll(keys, ['display', 'justifyContent', 'alignItems']) && properties['display'] === 'flex') {
|
|
206
|
+
return 'flexCenter';
|
|
207
|
+
}
|
|
208
|
+
if (hasAll(keys, ['display', 'flexDirection', 'justifyContent', 'alignItems']) && properties['flexDirection'] === 'column') {
|
|
209
|
+
return 'stack';
|
|
210
|
+
}
|
|
211
|
+
if (hasAll(keys, ['padding', 'borderRadius', 'backgroundColor', 'color'])) {
|
|
212
|
+
const bg = String(properties['backgroundColor'] || '');
|
|
213
|
+
if (bg.includes('2563eb') || bg.includes('blue')) return 'primaryButton';
|
|
214
|
+
if (bg.includes('e5e7eb') || bg.includes('gray')) return 'secondaryButton';
|
|
215
|
+
return 'button';
|
|
216
|
+
}
|
|
217
|
+
if (hasAll(keys, ['position', 'top']) && properties['position'] === 'sticky') {
|
|
218
|
+
return 'stickyHeader';
|
|
219
|
+
}
|
|
220
|
+
if (hasAll(keys, ['position', 'top', 'left']) && properties['position'] === 'absolute') {
|
|
221
|
+
return 'absoluteOverlay';
|
|
222
|
+
}
|
|
223
|
+
if (hasAll(keys, ['borderRadius']) && properties['borderRadius'] === '9999px') {
|
|
224
|
+
return 'pill';
|
|
225
|
+
}
|
|
226
|
+
if (hasAll(keys, ['overflow', 'textOverflow', 'whiteSpace'])) {
|
|
227
|
+
return 'truncate';
|
|
228
|
+
}
|
|
229
|
+
if (hasAll(keys, ['backdropFilter'])) {
|
|
230
|
+
return 'glass';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Fallback: use most significant property
|
|
234
|
+
if (keys.includes('display')) return String(properties['display']) + 'Layout';
|
|
235
|
+
if (keys.includes('position')) return String(properties['position']) + 'Element';
|
|
236
|
+
return 'pattern-' + keys.slice(0, 3).join('-');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hasAll(haystack: string[], needles: string[]): boolean {
|
|
240
|
+
return needles.every(n => haystack.includes(n));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Generate recipe code from a pattern.
|
|
245
|
+
*/
|
|
246
|
+
function generateRecipeCode(
|
|
247
|
+
name: string,
|
|
248
|
+
properties: Record<string, string | number>
|
|
249
|
+
): string {
|
|
250
|
+
const lines = Object.entries(properties).map(
|
|
251
|
+
([prop, value]) => ' ' + prop + ": '" + value + "',"
|
|
252
|
+
);
|
|
253
|
+
return "chain.recipe('" + name + "', {\n" + lines.join('\n') + "\n})";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Estimate bundle size savings.
|
|
258
|
+
*/
|
|
259
|
+
function estimateBundleSavings(declarations: number, frequency: number): string {
|
|
260
|
+
const avgBytesPerDecl = 25; // ~25 bytes per CSS declaration
|
|
261
|
+
const totalBytes = declarations * avgBytesPerDecl;
|
|
262
|
+
if (totalBytes > 10000) return '~' + Math.round(totalBytes / 1000) + 'KB';
|
|
263
|
+
return '~' + totalBytes + 'B';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Learning Report
|
|
268
|
+
// ============================================================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generate a full learning report from clustered patterns.
|
|
272
|
+
*/
|
|
273
|
+
function generateReport(clusters: PatternCluster[]): LearningReport {
|
|
274
|
+
const highValue = clusters.filter(c => c.score >= 10);
|
|
275
|
+
|
|
276
|
+
const totalDeclarations = clusters.reduce((sum, c) => sum + c.savings.declarations, 0);
|
|
277
|
+
const totalBytes = totalDeclarations * 25;
|
|
278
|
+
|
|
279
|
+
let summary: string;
|
|
280
|
+
if (clusters.length === 0) {
|
|
281
|
+
summary = 'No repeated patterns found. Your styles are already unique!';
|
|
282
|
+
} else if (highValue.length > 0) {
|
|
283
|
+
summary = 'Found ' + clusters.length + ' patterns. ' + highValue.length + ' high-value patterns worth extracting.';
|
|
284
|
+
} else {
|
|
285
|
+
summary = 'Found ' + clusters.length + ' patterns. None high-value enough to suggest extraction yet.';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
clusters,
|
|
290
|
+
totalPatterns: clusters.length,
|
|
291
|
+
highValuePatterns: highValue,
|
|
292
|
+
totalSavings: {
|
|
293
|
+
declarations: totalDeclarations,
|
|
294
|
+
estimatedBytes: totalBytes,
|
|
295
|
+
},
|
|
296
|
+
summary,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// IR Pass
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Pattern Learning IR pass.
|
|
306
|
+
* Analyzes rules, clusters patterns, and adds diagnostics.
|
|
307
|
+
*/
|
|
308
|
+
export const patternLearningPass: IRPass = (ir: StyleIR): StyleIR => {
|
|
309
|
+
const clusters = clusterPatterns(ir.rules, {
|
|
310
|
+
minProperties: 3,
|
|
311
|
+
minFrequency: 2,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (clusters.length === 0) return ir;
|
|
315
|
+
|
|
316
|
+
// Report top patterns as diagnostics
|
|
317
|
+
for (const cluster of clusters.slice(0, 5)) {
|
|
318
|
+
ir.diagnostics.push({
|
|
319
|
+
id: 'pattern-' + cluster.fingerprint.hash,
|
|
320
|
+
nodeId: ir.rules[0]?.id || ir.id,
|
|
321
|
+
severity: 'info',
|
|
322
|
+
message: 'Pattern "' + cluster.suggestedName + '" found ' + cluster.frequency + ' times across ' + cluster.fileCount + ' files. ' + cluster.savings.bundleReduction + ' potential savings.',
|
|
323
|
+
suggestion: cluster.suggestedRecipe,
|
|
324
|
+
pass: 'pattern-learner',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Store clusters for reporting
|
|
329
|
+
ir.meta = ir.meta || {};
|
|
330
|
+
(ir.meta as any).patternClusters = clusters;
|
|
331
|
+
(ir.meta as any).learningReport = generateReport(clusters);
|
|
332
|
+
|
|
333
|
+
return ir;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Standalone API
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Learn patterns from a set of style rules.
|
|
342
|
+
*/
|
|
343
|
+
export function learnPatterns(
|
|
344
|
+
rules: Array<{ selector: string; declarations: Record<string, string | number>; sourceFile?: string }>,
|
|
345
|
+
options?: { minProperties?: number; minFrequency?: number }
|
|
346
|
+
): LearningReport {
|
|
347
|
+
const irRules: IRRule[] = rules.map(r => ({
|
|
348
|
+
id: 'learn-' + Math.random().toString(36).slice(2, 8),
|
|
349
|
+
selector: r.selector,
|
|
350
|
+
declarations: Object.entries(r.declarations).map(([prop, value]) => ({
|
|
351
|
+
id: 'learn-decl-' + prop,
|
|
352
|
+
property: prop,
|
|
353
|
+
value,
|
|
354
|
+
history: [],
|
|
355
|
+
meta: {},
|
|
356
|
+
})),
|
|
357
|
+
pseudoClasses: [],
|
|
358
|
+
atRules: [],
|
|
359
|
+
nestedRules: [],
|
|
360
|
+
conditions: [],
|
|
361
|
+
isDead: false,
|
|
362
|
+
specificity: 0,
|
|
363
|
+
hash: '',
|
|
364
|
+
source: { file: r.sourceFile },
|
|
365
|
+
history: [],
|
|
366
|
+
meta: {},
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
const clusters = clusterPatterns(irRules, options);
|
|
370
|
+
return generateReport(clusters);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get the top patterns worth extracting.
|
|
375
|
+
*/
|
|
376
|
+
export function getExtractionCandidates(
|
|
377
|
+
rules: Array<{ selector: string; declarations: Record<string, string | number>; sourceFile?: string }>,
|
|
378
|
+
minScore?: number
|
|
379
|
+
): PatternCluster[] {
|
|
380
|
+
const report = learnPatterns(rules);
|
|
381
|
+
const threshold = minScore || 10;
|
|
382
|
+
return report.clusters.filter(c => c.score >= threshold);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// Quick API
|
|
387
|
+
// ============================================================================
|
|
388
|
+
|
|
389
|
+
export const patternLearner = {
|
|
390
|
+
learn: learnPatterns,
|
|
391
|
+
extract: getExtractionCandidates,
|
|
392
|
+
fingerprint: fingerprintDeclarations,
|
|
393
|
+
cluster: clusterPatterns,
|
|
394
|
+
report: generateReport,
|
|
395
|
+
pass: patternLearningPass,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
export default patternLearner;
|