@tailwind-styled/compiler 2.0.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/package.json +37 -0
- package/src/astParser.ts +397 -0
- package/src/astTransform.ts +359 -0
- package/src/atomicCss.ts +275 -0
- package/src/classExtractor.ts +69 -0
- package/src/classMerger.ts +45 -0
- package/src/componentGenerator.ts +91 -0
- package/src/componentHoister.ts +183 -0
- package/src/deadStyleEliminator.ts +398 -0
- package/src/incrementalEngine.ts +786 -0
- package/src/index.ts +100 -0
- package/src/loadTailwindConfig.ts +144 -0
- package/src/routeCssCollector.ts +188 -0
- package/src/rscAnalyzer.ts +240 -0
- package/src/safelistGenerator.ts +116 -0
- package/src/staticVariantCompiler.ts +240 -0
- package/src/styleBucketSystem.ts +498 -0
- package/src/styleRegistry.ts +462 -0
- package/src/tailwindEngine.ts +285 -0
- package/src/twDetector.ts +54 -0
- package/src/variantCompiler.ts +87 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tailwind-styled-v4 — Style Bucket System
|
|
3
|
+
*
|
|
4
|
+
* Setiap CSS rule masuk ke "bucket" berdasarkan tipe property-nya.
|
|
5
|
+
* Bucket di-emit dalam urutan yang selalu sama → CSS order stabil
|
|
6
|
+
* meskipun rule di-generate dari banyak file secara incremental.
|
|
7
|
+
*
|
|
8
|
+
* Tanpa bucket system:
|
|
9
|
+
* .tw-color { color: blue } ← dari file A
|
|
10
|
+
* .tw-flex { display: flex } ← dari file B
|
|
11
|
+
* .tw-color2 { color: red } ← dari file C
|
|
12
|
+
* → urutan output bergantung urutan file di-process = TIDAK STABIL
|
|
13
|
+
*
|
|
14
|
+
* Dengan bucket system:
|
|
15
|
+
* /* reset *\/
|
|
16
|
+
* /* layout *\/ → display, position, flex, grid, overflow
|
|
17
|
+
* /* spacing *\/ → margin, padding, gap, inset
|
|
18
|
+
* /* sizing *\/ → width, height, max/min-width/height
|
|
19
|
+
* /* typography *\/ → font-size, font-weight, line-height, text-*
|
|
20
|
+
* /* visual *\/ → color, background, border, shadow, opacity
|
|
21
|
+
* /* interaction *\/ → cursor, pointer-events, user-select, transition
|
|
22
|
+
* /* responsive *\/ → @media queries (selalu di akhir)
|
|
23
|
+
* → SELALU urutan ini, terlepas dari urutan file
|
|
24
|
+
*
|
|
25
|
+
* Keuntungan utama:
|
|
26
|
+
* 1. CSS output deterministic antar build (reproducible builds)
|
|
27
|
+
* 2. Specificity conflict sangat kecil — base selalu lebih awal dari responsive
|
|
28
|
+
* 3. Debug lebih mudah — tahu section mana rule berada
|
|
29
|
+
*
|
|
30
|
+
* Integrasi:
|
|
31
|
+
* import { BucketEngine, bucketSort } from "./styleBucketSystem"
|
|
32
|
+
*
|
|
33
|
+
* const engine = new BucketEngine()
|
|
34
|
+
* engine.add(styleNode)
|
|
35
|
+
* const css = engine.emit()
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { StyleNode } from "./incrementalEngine"
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Bucket Types
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 8 bucket utama + 1 bucket "unknown" untuk fallback.
|
|
46
|
+
* Urutan angka = urutan emit di CSS output.
|
|
47
|
+
*/
|
|
48
|
+
export type StyleBucket =
|
|
49
|
+
| "reset" // 0 — *, box-sizing, root
|
|
50
|
+
| "layout" // 1 — display, position, flex, grid, overflow, z-index
|
|
51
|
+
| "spacing" // 2 — margin, padding, gap, inset, top/right/bottom/left
|
|
52
|
+
| "sizing" // 3 — width, height, max/min variants
|
|
53
|
+
| "typography" // 4 — font-size, font-weight, line-height, letter-spacing, text-align
|
|
54
|
+
| "visual" // 5 — color, background, border, border-radius, shadow, opacity, outline
|
|
55
|
+
| "interaction" // 6 — cursor, pointer-events, user-select, transition, transform, animation
|
|
56
|
+
| "responsive" // 7 — @media queries (semua)
|
|
57
|
+
| "unknown" // 8 — fallback untuk property yang tidak dikenal
|
|
58
|
+
|
|
59
|
+
const BUCKET_ORDER: StyleBucket[] = [
|
|
60
|
+
"reset",
|
|
61
|
+
"layout",
|
|
62
|
+
"spacing",
|
|
63
|
+
"sizing",
|
|
64
|
+
"typography",
|
|
65
|
+
"visual",
|
|
66
|
+
"interaction",
|
|
67
|
+
"responsive",
|
|
68
|
+
"unknown",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
// Property → Bucket Mapping
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/** Map dari CSS property prefix/exact ke bucket */
|
|
76
|
+
const PROPERTY_BUCKET_MAP: Record<string, StyleBucket> = {
|
|
77
|
+
// Layout
|
|
78
|
+
display: "layout",
|
|
79
|
+
position: "layout",
|
|
80
|
+
flex: "layout",
|
|
81
|
+
"flex-direction": "layout",
|
|
82
|
+
"flex-wrap": "layout",
|
|
83
|
+
"flex-grow": "layout",
|
|
84
|
+
"flex-shrink": "layout",
|
|
85
|
+
"flex-basis": "layout",
|
|
86
|
+
grid: "layout",
|
|
87
|
+
"grid-template": "layout",
|
|
88
|
+
"grid-column": "layout",
|
|
89
|
+
"grid-row": "layout",
|
|
90
|
+
"align-items": "layout",
|
|
91
|
+
"align-self": "layout",
|
|
92
|
+
"align-content": "layout",
|
|
93
|
+
"justify-content": "layout",
|
|
94
|
+
"justify-items": "layout",
|
|
95
|
+
"justify-self": "layout",
|
|
96
|
+
"place-items": "layout",
|
|
97
|
+
"place-content": "layout",
|
|
98
|
+
overflow: "layout",
|
|
99
|
+
"overflow-x": "layout",
|
|
100
|
+
"overflow-y": "layout",
|
|
101
|
+
"z-index": "layout",
|
|
102
|
+
float: "layout",
|
|
103
|
+
clear: "layout",
|
|
104
|
+
visibility: "layout",
|
|
105
|
+
|
|
106
|
+
// Spacing
|
|
107
|
+
padding: "spacing",
|
|
108
|
+
"padding-top": "spacing",
|
|
109
|
+
"padding-bottom": "spacing",
|
|
110
|
+
"padding-left": "spacing",
|
|
111
|
+
"padding-right": "spacing",
|
|
112
|
+
"padding-inline": "spacing",
|
|
113
|
+
"padding-block": "spacing",
|
|
114
|
+
margin: "spacing",
|
|
115
|
+
"margin-top": "spacing",
|
|
116
|
+
"margin-bottom": "spacing",
|
|
117
|
+
"margin-left": "spacing",
|
|
118
|
+
"margin-right": "spacing",
|
|
119
|
+
"margin-inline": "spacing",
|
|
120
|
+
"margin-block": "spacing",
|
|
121
|
+
gap: "spacing",
|
|
122
|
+
"column-gap": "spacing",
|
|
123
|
+
"row-gap": "spacing",
|
|
124
|
+
inset: "spacing",
|
|
125
|
+
"inset-inline": "spacing",
|
|
126
|
+
"inset-block": "spacing",
|
|
127
|
+
top: "spacing",
|
|
128
|
+
bottom: "spacing",
|
|
129
|
+
left: "spacing",
|
|
130
|
+
right: "spacing",
|
|
131
|
+
|
|
132
|
+
// Sizing
|
|
133
|
+
width: "sizing",
|
|
134
|
+
height: "sizing",
|
|
135
|
+
"max-width": "sizing",
|
|
136
|
+
"min-width": "sizing",
|
|
137
|
+
"max-height": "sizing",
|
|
138
|
+
"min-height": "sizing",
|
|
139
|
+
"aspect-ratio": "sizing",
|
|
140
|
+
|
|
141
|
+
// Typography
|
|
142
|
+
"font-size": "typography",
|
|
143
|
+
"font-weight": "typography",
|
|
144
|
+
"font-family": "typography",
|
|
145
|
+
"font-style": "typography",
|
|
146
|
+
"line-height": "typography",
|
|
147
|
+
"letter-spacing": "typography",
|
|
148
|
+
"text-align": "typography",
|
|
149
|
+
"text-decoration": "typography",
|
|
150
|
+
"text-transform": "typography",
|
|
151
|
+
"text-overflow": "typography",
|
|
152
|
+
"white-space": "typography",
|
|
153
|
+
"word-break": "typography",
|
|
154
|
+
"word-wrap": "typography",
|
|
155
|
+
"vertical-align": "typography",
|
|
156
|
+
|
|
157
|
+
// Visual
|
|
158
|
+
color: "visual",
|
|
159
|
+
background: "visual",
|
|
160
|
+
"background-color": "visual",
|
|
161
|
+
"background-image": "visual",
|
|
162
|
+
"background-size": "visual",
|
|
163
|
+
"background-position": "visual",
|
|
164
|
+
"background-repeat": "visual",
|
|
165
|
+
border: "visual",
|
|
166
|
+
"border-top": "visual",
|
|
167
|
+
"border-bottom": "visual",
|
|
168
|
+
"border-left": "visual",
|
|
169
|
+
"border-right": "visual",
|
|
170
|
+
"border-inline": "visual",
|
|
171
|
+
"border-block": "visual",
|
|
172
|
+
"border-color": "visual",
|
|
173
|
+
"border-width": "visual",
|
|
174
|
+
"border-style": "visual",
|
|
175
|
+
"border-radius": "visual",
|
|
176
|
+
"box-shadow": "visual",
|
|
177
|
+
opacity: "visual",
|
|
178
|
+
outline: "visual",
|
|
179
|
+
"outline-color": "visual",
|
|
180
|
+
"outline-width": "visual",
|
|
181
|
+
fill: "visual",
|
|
182
|
+
stroke: "visual",
|
|
183
|
+
"text-shadow": "visual",
|
|
184
|
+
"mix-blend-mode": "visual",
|
|
185
|
+
"object-fit": "visual",
|
|
186
|
+
"object-position": "visual",
|
|
187
|
+
|
|
188
|
+
// Interaction
|
|
189
|
+
cursor: "interaction",
|
|
190
|
+
"pointer-events": "interaction",
|
|
191
|
+
"user-select": "interaction",
|
|
192
|
+
transition: "interaction",
|
|
193
|
+
"transition-property": "interaction",
|
|
194
|
+
"transition-duration": "interaction",
|
|
195
|
+
"transition-timing-function": "interaction",
|
|
196
|
+
"transition-delay": "interaction",
|
|
197
|
+
transform: "interaction",
|
|
198
|
+
translate: "interaction",
|
|
199
|
+
rotate: "interaction",
|
|
200
|
+
scale: "interaction",
|
|
201
|
+
animation: "interaction",
|
|
202
|
+
"will-change": "interaction",
|
|
203
|
+
"scroll-behavior": "interaction",
|
|
204
|
+
"scroll-snap-type": "interaction",
|
|
205
|
+
|
|
206
|
+
// Reset (jarang dipakai langsung tapi handle untuk completeness)
|
|
207
|
+
"box-sizing": "reset",
|
|
208
|
+
appearance: "reset",
|
|
209
|
+
all: "reset",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
// Bucket Classifier
|
|
214
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Classify satu StyleNode ke bucket yang tepat.
|
|
218
|
+
*
|
|
219
|
+
* Priority:
|
|
220
|
+
* 1. Jika ada modifier @media → "responsive" (selalu paling akhir)
|
|
221
|
+
* 2. Cek declaration property → lookup PROPERTY_BUCKET_MAP
|
|
222
|
+
* 3. Fallback ke "unknown"
|
|
223
|
+
*/
|
|
224
|
+
export function classifyNode(node: StyleNode): StyleBucket {
|
|
225
|
+
// Media queries selalu masuk responsive bucket
|
|
226
|
+
if (node.modifier?.startsWith("@")) return "responsive"
|
|
227
|
+
|
|
228
|
+
// Extract property dari declaration "property: value; ..." (support multi-prop)
|
|
229
|
+
const declarations = node.declaration
|
|
230
|
+
.split(";")
|
|
231
|
+
.map((d: string) => d.trim())
|
|
232
|
+
.filter(Boolean)
|
|
233
|
+
const firstProp = declarations[0]?.split(":")[0]?.trim()
|
|
234
|
+
|
|
235
|
+
if (!firstProp) return "unknown"
|
|
236
|
+
|
|
237
|
+
// Exact match
|
|
238
|
+
if (PROPERTY_BUCKET_MAP[firstProp]) return PROPERTY_BUCKET_MAP[firstProp]
|
|
239
|
+
|
|
240
|
+
// Prefix match — untuk shorthand variants
|
|
241
|
+
for (const [prefix, bucket] of Object.entries(PROPERTY_BUCKET_MAP)) {
|
|
242
|
+
if (firstProp.startsWith(prefix)) return bucket
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return "unknown"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// Bucket Engine
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
export interface BucketStats {
|
|
253
|
+
totalNodes: number
|
|
254
|
+
perBucket: Record<StyleBucket, number>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* BucketEngine — menyimpan dan emit CSS dalam urutan bucket yang stabil.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const engine = new BucketEngine()
|
|
262
|
+
* for (const node of styleNodes) engine.add(node)
|
|
263
|
+
* const css = engine.emit()
|
|
264
|
+
*/
|
|
265
|
+
export class BucketEngine {
|
|
266
|
+
private buckets: Map<StyleBucket, Map<string, StyleNode>>
|
|
267
|
+
|
|
268
|
+
constructor() {
|
|
269
|
+
this.buckets = new Map()
|
|
270
|
+
for (const b of BUCKET_ORDER) {
|
|
271
|
+
this.buckets.set(b, new Map())
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Tambah StyleNode ke bucket yang tepat.
|
|
277
|
+
* Idempotent — atomic class yang sama tidak akan duplikat.
|
|
278
|
+
*/
|
|
279
|
+
add(node: StyleNode): void {
|
|
280
|
+
const bucket = classifyNode(node)
|
|
281
|
+
this.buckets.get(bucket)!.set(node.atomicClass, node)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Hapus node dari bucket (untuk incremental update).
|
|
286
|
+
*/
|
|
287
|
+
remove(atomicClass: string): void {
|
|
288
|
+
for (const bucket of this.buckets.values()) {
|
|
289
|
+
if (bucket.delete(atomicClass)) break
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Apply CssDiff dari incremental engine.
|
|
295
|
+
*/
|
|
296
|
+
applyDiff(diff: { added: StyleNode[]; removed: string[] }): void {
|
|
297
|
+
for (const node of diff.added) this.add(node)
|
|
298
|
+
for (const cls of diff.removed) this.remove(cls)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Emit seluruh CSS dalam urutan bucket yang deterministic.
|
|
303
|
+
*
|
|
304
|
+
* @param comments - Tambahkan komentar section per bucket. Default: true
|
|
305
|
+
* @returns CSS string yang siap di-write ke file
|
|
306
|
+
*/
|
|
307
|
+
emit(comments = true): string {
|
|
308
|
+
const sections: string[] = []
|
|
309
|
+
|
|
310
|
+
for (const bucketName of BUCKET_ORDER) {
|
|
311
|
+
const nodes = this.buckets.get(bucketName)!
|
|
312
|
+
if (nodes.size === 0) continue
|
|
313
|
+
|
|
314
|
+
const rules: string[] = []
|
|
315
|
+
|
|
316
|
+
for (const node of nodes.values()) {
|
|
317
|
+
rules.push(nodeToCSS(node))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (rules.length === 0) continue
|
|
321
|
+
|
|
322
|
+
if (comments) {
|
|
323
|
+
sections.push(`/* ── ${bucketName} ── */`)
|
|
324
|
+
}
|
|
325
|
+
sections.push(...rules)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return sections.join("\n")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Emit dengan @layer CSS untuk native browser layering.
|
|
333
|
+
* Lebih powerful — browser respects layer order untuk specificity.
|
|
334
|
+
*
|
|
335
|
+
* @example output:
|
|
336
|
+
* @layer tw-layout, tw-spacing, tw-visual, tw-responsive;
|
|
337
|
+
* @layer tw-layout { .tw-a1 { display: flex } }
|
|
338
|
+
*/
|
|
339
|
+
emitLayered(): string {
|
|
340
|
+
// Layer declaration
|
|
341
|
+
const layerNames = BUCKET_ORDER.filter(
|
|
342
|
+
(b) => b !== "unknown" && this.buckets.get(b)!.size > 0
|
|
343
|
+
).map((b) => `tw-${b}`)
|
|
344
|
+
|
|
345
|
+
if (layerNames.length === 0) return ""
|
|
346
|
+
|
|
347
|
+
const parts: string[] = [`@layer ${layerNames.join(", ")};`, ""]
|
|
348
|
+
|
|
349
|
+
for (const bucketName of BUCKET_ORDER) {
|
|
350
|
+
const nodes = this.buckets.get(bucketName)!
|
|
351
|
+
if (nodes.size === 0) continue
|
|
352
|
+
|
|
353
|
+
const rules = Array.from(nodes.values()).map(nodeToCSS).join("\n ")
|
|
354
|
+
parts.push(`@layer tw-${bucketName} {\n ${rules}\n}`)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return parts.join("\n")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Semua nodes dari semua bucket (untuk full registry access) */
|
|
361
|
+
allNodes(): StyleNode[] {
|
|
362
|
+
const all: StyleNode[] = []
|
|
363
|
+
for (const bucket of this.buckets.values()) {
|
|
364
|
+
for (const node of bucket.values()) {
|
|
365
|
+
all.push(node)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return all
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Stats per bucket */
|
|
372
|
+
stats(): BucketStats {
|
|
373
|
+
const perBucket = {} as Record<StyleBucket, number>
|
|
374
|
+
let total = 0
|
|
375
|
+
for (const [name, nodes] of this.buckets) {
|
|
376
|
+
perBucket[name] = nodes.size
|
|
377
|
+
total += nodes.size
|
|
378
|
+
}
|
|
379
|
+
return { totalNodes: total, perBucket }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Clear semua bucket */
|
|
383
|
+
clear(): void {
|
|
384
|
+
for (const bucket of this.buckets.values()) {
|
|
385
|
+
bucket.clear()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
391
|
+
// bucketSort — utility function untuk sort array StyleNodes
|
|
392
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Sort array StyleNodes dalam urutan bucket.
|
|
396
|
+
* Berguna untuk one-off sorting tanpa perlu BucketEngine instance.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* const sorted = bucketSort(allNodes)
|
|
400
|
+
* const css = sorted.map(nodeToCSS).join("\n")
|
|
401
|
+
*/
|
|
402
|
+
export function bucketSort(nodes: StyleNode[]): StyleNode[] {
|
|
403
|
+
const bucketIndex = Object.fromEntries(BUCKET_ORDER.map((b, i) => [b, i])) as Record<
|
|
404
|
+
StyleBucket,
|
|
405
|
+
number
|
|
406
|
+
>
|
|
407
|
+
|
|
408
|
+
return [...nodes].sort((a, b) => {
|
|
409
|
+
const ai = bucketIndex[classifyNode(a)]
|
|
410
|
+
const bi = bucketIndex[classifyNode(b)]
|
|
411
|
+
return ai - bi
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
416
|
+
// CSS generation helpers
|
|
417
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
function nodeToCSS(node: StyleNode): string {
|
|
420
|
+
const { atomicClass, declaration, modifier } = node
|
|
421
|
+
|
|
422
|
+
if (!modifier) {
|
|
423
|
+
return `.${atomicClass}{${declaration}}`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (modifier.startsWith("@")) {
|
|
427
|
+
return `${modifier}{.${atomicClass}{${declaration}}}`
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return `.${atomicClass}${modifier}{${declaration}}`
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
434
|
+
// CSSConflictDetector — dev-mode helper
|
|
435
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
export interface ConflictWarning {
|
|
438
|
+
property: string
|
|
439
|
+
classes: string[]
|
|
440
|
+
bucket: StyleBucket
|
|
441
|
+
message: string
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Detect potential CSS conflicts dalam satu set StyleNodes.
|
|
446
|
+
* Hanya untuk dev mode — tidak perlu run di production.
|
|
447
|
+
*
|
|
448
|
+
* Conflict = dua node dengan property yang sama tapi value berbeda
|
|
449
|
+
* di bucket yang sama (bukan responsive override).
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* const warnings = detectConflicts(nodes)
|
|
453
|
+
* if (warnings.length) console.warn(warnings)
|
|
454
|
+
*/
|
|
455
|
+
export function detectConflicts(nodes: StyleNode[]): ConflictWarning[] {
|
|
456
|
+
// property+modifier → node
|
|
457
|
+
const seen = new Map<string, StyleNode>()
|
|
458
|
+
const warnings: ConflictWarning[] = []
|
|
459
|
+
|
|
460
|
+
for (const node of nodes) {
|
|
461
|
+
// Skip responsive — by design override base
|
|
462
|
+
if (node.modifier?.startsWith("@")) continue
|
|
463
|
+
|
|
464
|
+
const firstProp = node.declaration.split(":")[0]?.trim()
|
|
465
|
+
if (!firstProp) continue
|
|
466
|
+
|
|
467
|
+
const key = `${firstProp}::${node.modifier ?? ""}`
|
|
468
|
+
const prev = seen.get(key)
|
|
469
|
+
|
|
470
|
+
if (prev) {
|
|
471
|
+
warnings.push({
|
|
472
|
+
property: firstProp,
|
|
473
|
+
classes: [prev.twClass, node.twClass],
|
|
474
|
+
bucket: classifyNode(node),
|
|
475
|
+
message: `Possible conflict: "${prev.twClass}" and "${node.twClass}" both set "${firstProp}"`,
|
|
476
|
+
})
|
|
477
|
+
} else {
|
|
478
|
+
seen.set(key, node)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return warnings
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
486
|
+
// Singleton BucketEngine
|
|
487
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
let _bucketEngine: BucketEngine | null = null
|
|
490
|
+
|
|
491
|
+
export function getBucketEngine(): BucketEngine {
|
|
492
|
+
if (!_bucketEngine) _bucketEngine = new BucketEngine()
|
|
493
|
+
return _bucketEngine
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function resetBucketEngine(): void {
|
|
497
|
+
_bucketEngine = null
|
|
498
|
+
}
|