@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
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tailwind-styled/compiler",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Compiler pipeline for tailwind-styled-v4",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"tailwind-merge": "^3",
|
|
19
|
+
"postcss": "^8"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"oxc-parser": "^0.118.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"tailwindcss": "^4",
|
|
26
|
+
"@tailwindcss/postcss": "^4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20",
|
|
30
|
+
"tsup": "^8",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist --clean",
|
|
35
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --out-dir dist --watch"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/astParser.ts
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tailwind-styled-v4 — astParser
|
|
3
|
+
*
|
|
4
|
+
* UPGRADE RUST: oxc-parser (Rust-based, via napi-rs) menggantikan
|
|
5
|
+
* hand-written bracket-counting tokenizer.
|
|
6
|
+
*
|
|
7
|
+
* Keuntungan oxc-parser:
|
|
8
|
+
* - ~10x lebih cepat dari tokenizer TypeScript
|
|
9
|
+
* - Handles semua edge case TypeScript/JS secara native
|
|
10
|
+
* - Same parser yang dipakai Rolldown, Vite 6, Biome
|
|
11
|
+
* - Zero maintenance — battle-tested di ekosistem besar
|
|
12
|
+
*
|
|
13
|
+
* Strategy: oxc-parser sebagai primary, tokenizer lama sebagai fallback.
|
|
14
|
+
* Jika oxc gagal parse (malformed input), fallback transparan — zero breakage.
|
|
15
|
+
*
|
|
16
|
+
* Compatibility: Next.js, Vite, Rspack — semua fully supported.
|
|
17
|
+
* oxc-parser adalah native Node addon (napi-rs), tidak ada WASM overhead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Public types
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface ParsedComponentConfig {
|
|
25
|
+
base: string
|
|
26
|
+
variants: Record<string, Record<string, string>>
|
|
27
|
+
compounds: Array<{ class: string; [key: string]: any }>
|
|
28
|
+
defaults: Record<string, string>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// oxc-parser — Rust AST walker (primary)
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/** Resolve key from Identifier or Literal node */
|
|
36
|
+
function oxcKey(node: any): string | null {
|
|
37
|
+
if (!node) return null
|
|
38
|
+
if (node.type === "Identifier") return node.name as string
|
|
39
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolve string value from Literal or no-expression TemplateLiteral */
|
|
44
|
+
function oxcStringVal(node: any): string | null {
|
|
45
|
+
if (!node) return null
|
|
46
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value
|
|
47
|
+
if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
|
|
48
|
+
return (node.quasis as any[]).map((q: any) => q.value?.cooked ?? q.value?.raw ?? "").join("")
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Recursively walk ObjectExpression → plain Record */
|
|
54
|
+
function oxcWalkObject(node: any): Record<string, any> {
|
|
55
|
+
const result: Record<string, any> = {}
|
|
56
|
+
if (node?.type !== "ObjectExpression") return result
|
|
57
|
+
|
|
58
|
+
for (const prop of node.properties ?? []) {
|
|
59
|
+
if (prop.type !== "Property") continue
|
|
60
|
+
const key = oxcKey(prop.key)
|
|
61
|
+
if (!key) continue
|
|
62
|
+
|
|
63
|
+
const val = prop.value
|
|
64
|
+
const strVal = oxcStringVal(val)
|
|
65
|
+
|
|
66
|
+
if (strVal !== null) {
|
|
67
|
+
result[key] = strVal
|
|
68
|
+
} else if (val?.type === "ObjectExpression") {
|
|
69
|
+
result[key] = oxcWalkObject(val)
|
|
70
|
+
} else if (val?.type === "ArrayExpression") {
|
|
71
|
+
result[key] = (val.elements as any[])
|
|
72
|
+
.filter((el: any) => el?.type === "ObjectExpression")
|
|
73
|
+
.map((el: any) => oxcWalkObject(el))
|
|
74
|
+
}
|
|
75
|
+
// skip dynamic expressions, functions, computed props, etc.
|
|
76
|
+
}
|
|
77
|
+
return result
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse config object string using oxc-parser (Rust).
|
|
82
|
+
* Wraps string sebagai valid statement agar oxc bisa parse.
|
|
83
|
+
* Returns null jika parse gagal → fallback ke tokenizer.
|
|
84
|
+
*/
|
|
85
|
+
function parseWithOxc(objectStr: string): ParsedComponentConfig | null {
|
|
86
|
+
let parseSync: (filename: string, source: string, options?: any) => any
|
|
87
|
+
try {
|
|
88
|
+
// Dynamic require agar tidak crash jika oxc-parser tidak terinstall
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
90
|
+
parseSync = require("oxc-parser").parseSync
|
|
91
|
+
} catch {
|
|
92
|
+
return null // oxc-parser not available → silently fallback
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const source = `const __c = ${objectStr}`
|
|
97
|
+
const { program, errors } = parseSync("config.ts", source, { sourceType: "script" })
|
|
98
|
+
|
|
99
|
+
if (errors?.length > 0 || !program?.body?.[0]) return null
|
|
100
|
+
|
|
101
|
+
const varDecl = program.body[0]
|
|
102
|
+
if (varDecl.type !== "VariableDeclaration") return null
|
|
103
|
+
|
|
104
|
+
const init = varDecl.declarations?.[0]?.init
|
|
105
|
+
if (init?.type !== "ObjectExpression") return null
|
|
106
|
+
|
|
107
|
+
const raw = oxcWalkObject(init)
|
|
108
|
+
|
|
109
|
+
// ── base ────────────────────────────────────────────────────────────
|
|
110
|
+
const base = typeof raw.base === "string" ? raw.base.trim() : ""
|
|
111
|
+
|
|
112
|
+
// ── variants ─────────────────────────────────────────────────────────
|
|
113
|
+
const variants: Record<string, Record<string, string>> = {}
|
|
114
|
+
const rawVariants = raw.variants
|
|
115
|
+
if (rawVariants && typeof rawVariants === "object" && !Array.isArray(rawVariants)) {
|
|
116
|
+
for (const [vName, vMap] of Object.entries(rawVariants)) {
|
|
117
|
+
if (vMap && typeof vMap === "object" && !Array.isArray(vMap)) {
|
|
118
|
+
variants[vName] = {}
|
|
119
|
+
for (const [vVal, cls] of Object.entries(vMap as Record<string, any>)) {
|
|
120
|
+
if (typeof cls === "string") variants[vName][vVal] = cls.trim()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── compoundVariants ─────────────────────────────────────────────────
|
|
127
|
+
const compounds: Array<{ class: string; [key: string]: any }> = []
|
|
128
|
+
const rawCompounds = raw.compoundVariants
|
|
129
|
+
if (Array.isArray(rawCompounds)) {
|
|
130
|
+
for (const item of rawCompounds) {
|
|
131
|
+
if (item && typeof item.class === "string") {
|
|
132
|
+
compounds.push(item as { class: string })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── defaultVariants ──────────────────────────────────────────────────
|
|
138
|
+
const defaults: Record<string, string> = {}
|
|
139
|
+
const rawDefaults = raw.defaultVariants
|
|
140
|
+
if (rawDefaults && typeof rawDefaults === "object" && !Array.isArray(rawDefaults)) {
|
|
141
|
+
for (const [k, v] of Object.entries(rawDefaults)) {
|
|
142
|
+
if (typeof v === "string") defaults[k] = v
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { base, variants, compounds, defaults }
|
|
147
|
+
} catch {
|
|
148
|
+
return null // parse error → fallback
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
// Tokenizer fallback (original implementation — preserved as-is)
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
type TokenType =
|
|
157
|
+
| "string"
|
|
158
|
+
| "key"
|
|
159
|
+
| "colon"
|
|
160
|
+
| "comma"
|
|
161
|
+
| "lbrace"
|
|
162
|
+
| "rbrace"
|
|
163
|
+
| "lbracket"
|
|
164
|
+
| "rbracket"
|
|
165
|
+
| "other"
|
|
166
|
+
|
|
167
|
+
interface Token {
|
|
168
|
+
type: TokenType
|
|
169
|
+
value: string
|
|
170
|
+
pos: number
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function tokenize(src: string): Token[] {
|
|
174
|
+
const tokens: Token[] = []
|
|
175
|
+
let i = 0
|
|
176
|
+
|
|
177
|
+
while (i < src.length) {
|
|
178
|
+
const ch = src[i]
|
|
179
|
+
|
|
180
|
+
if (/\s/.test(ch)) {
|
|
181
|
+
i++
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
186
|
+
const quote = ch
|
|
187
|
+
let j = i + 1
|
|
188
|
+
let str = ch
|
|
189
|
+
while (j < src.length) {
|
|
190
|
+
if (src[j] === "\\" && quote !== "`") {
|
|
191
|
+
str += src[j] + src[j + 1]
|
|
192
|
+
j += 2
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if (src[j] === "\\" && quote === "`") {
|
|
196
|
+
str += src[j] + src[j + 1]
|
|
197
|
+
j += 2
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
str += src[j]
|
|
201
|
+
if (src[j] === quote) {
|
|
202
|
+
j++
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
j++
|
|
206
|
+
}
|
|
207
|
+
tokens.push({ type: "string", value: str.slice(1, -1), pos: i })
|
|
208
|
+
i = j
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (ch === ":") {
|
|
213
|
+
tokens.push({ type: "colon", value: ":", pos: i })
|
|
214
|
+
i++
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (ch === ",") {
|
|
218
|
+
tokens.push({ type: "comma", value: ",", pos: i })
|
|
219
|
+
i++
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (ch === "{") {
|
|
223
|
+
tokens.push({ type: "lbrace", value: "{", pos: i })
|
|
224
|
+
i++
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (ch === "}") {
|
|
228
|
+
tokens.push({ type: "rbrace", value: "}", pos: i })
|
|
229
|
+
i++
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
if (ch === "[") {
|
|
233
|
+
tokens.push({ type: "lbracket", value: "[", pos: i })
|
|
234
|
+
i++
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
if (ch === "]") {
|
|
238
|
+
tokens.push({ type: "rbracket", value: "]", pos: i })
|
|
239
|
+
i++
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (/[\w$]/.test(ch)) {
|
|
244
|
+
let j = i
|
|
245
|
+
while (j < src.length && /[\w$]/.test(src[j])) j++
|
|
246
|
+
tokens.push({ type: "key", value: src.slice(i, j), pos: i })
|
|
247
|
+
i = j
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
tokens.push({ type: "other", value: ch, pos: i })
|
|
252
|
+
i++
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return tokens
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
interface ParsedObject {
|
|
259
|
+
[key: string]: string | ParsedObject | Array<ParsedObject>
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseObject(tokens: Token[], startIdx: number): { obj: ParsedObject; endIdx: number } {
|
|
263
|
+
const obj: ParsedObject = {}
|
|
264
|
+
let i = startIdx
|
|
265
|
+
|
|
266
|
+
if (tokens[i]?.type !== "lbrace") return { obj, endIdx: i }
|
|
267
|
+
i++
|
|
268
|
+
|
|
269
|
+
while (i < tokens.length && tokens[i]?.type !== "rbrace") {
|
|
270
|
+
if (tokens[i]?.type === "comma") {
|
|
271
|
+
i++
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let key: string | null = null
|
|
276
|
+
if (tokens[i]?.type === "string") {
|
|
277
|
+
key = tokens[i].value
|
|
278
|
+
i++
|
|
279
|
+
} else if (tokens[i]?.type === "key") {
|
|
280
|
+
key = tokens[i].value
|
|
281
|
+
i++
|
|
282
|
+
} else {
|
|
283
|
+
i++
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (tokens[i]?.type !== "colon") continue
|
|
288
|
+
i++
|
|
289
|
+
|
|
290
|
+
if (tokens[i]?.type === "lbrace") {
|
|
291
|
+
const { obj: nested, endIdx } = parseObject(tokens, i)
|
|
292
|
+
obj[key] = nested
|
|
293
|
+
i = endIdx + 1
|
|
294
|
+
} else if (tokens[i]?.type === "lbracket") {
|
|
295
|
+
const { arr, endIdx } = parseArray(tokens, i)
|
|
296
|
+
obj[key] = arr as any
|
|
297
|
+
i = endIdx + 1
|
|
298
|
+
} else if (tokens[i]?.type === "string") {
|
|
299
|
+
obj[key] = tokens[i].value
|
|
300
|
+
i++
|
|
301
|
+
} else if (tokens[i]?.type === "key") {
|
|
302
|
+
obj[key] = tokens[i].value
|
|
303
|
+
i++
|
|
304
|
+
} else {
|
|
305
|
+
i++
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { obj, endIdx: i }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseArray(tokens: Token[], startIdx: number): { arr: ParsedObject[]; endIdx: number } {
|
|
313
|
+
const arr: ParsedObject[] = []
|
|
314
|
+
let i = startIdx
|
|
315
|
+
|
|
316
|
+
if (tokens[i]?.type !== "lbracket") return { arr, endIdx: i }
|
|
317
|
+
i++
|
|
318
|
+
|
|
319
|
+
while (i < tokens.length && tokens[i]?.type !== "rbracket") {
|
|
320
|
+
if (tokens[i]?.type === "comma") {
|
|
321
|
+
i++
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
if (tokens[i]?.type === "lbrace") {
|
|
325
|
+
const { obj, endIdx } = parseObject(tokens, i)
|
|
326
|
+
arr.push(obj)
|
|
327
|
+
i = endIdx + 1
|
|
328
|
+
} else {
|
|
329
|
+
i++
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { arr, endIdx: i }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function parseComponentConfigFallback(objectStr: string): ParsedComponentConfig {
|
|
337
|
+
const tokens = tokenize(objectStr)
|
|
338
|
+
const { obj } = parseObject(tokens, 0)
|
|
339
|
+
|
|
340
|
+
const base = typeof obj.base === "string" ? obj.base.trim() : ""
|
|
341
|
+
|
|
342
|
+
const variants: Record<string, Record<string, string>> = {}
|
|
343
|
+
const rawVariants = obj.variants
|
|
344
|
+
if (rawVariants && typeof rawVariants === "object" && !Array.isArray(rawVariants)) {
|
|
345
|
+
for (const [variantName, variantValues] of Object.entries(rawVariants as ParsedObject)) {
|
|
346
|
+
if (typeof variantValues === "object" && !Array.isArray(variantValues)) {
|
|
347
|
+
variants[variantName] = {}
|
|
348
|
+
for (const [valueName, cls] of Object.entries(variantValues as ParsedObject)) {
|
|
349
|
+
if (typeof cls === "string") variants[variantName][valueName] = cls.trim()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const compounds: Array<{ class: string; [key: string]: any }> = []
|
|
356
|
+
const rawCompounds = obj.compoundVariants
|
|
357
|
+
if (Array.isArray(rawCompounds)) {
|
|
358
|
+
for (const item of rawCompounds as ParsedObject[]) {
|
|
359
|
+
if (item && typeof item.class === "string") compounds.push(item as any)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const defaults: Record<string, string> = {}
|
|
364
|
+
const rawDefaults = obj.defaultVariants
|
|
365
|
+
if (rawDefaults && typeof rawDefaults === "object" && !Array.isArray(rawDefaults)) {
|
|
366
|
+
for (const [k, v] of Object.entries(rawDefaults as ParsedObject)) {
|
|
367
|
+
if (typeof v === "string") defaults[k] = v
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { base, variants, compounds, defaults }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
375
|
+
// Public API — oxc-parser primary, tokenizer fallback
|
|
376
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parse a ComponentConfig object literal string.
|
|
380
|
+
*
|
|
381
|
+
* PRIMARY: oxc-parser (Rust, napi-rs) — ~10x faster, full TS/JS support.
|
|
382
|
+
* FALLBACK: bracket-counting tokenizer — transparan, zero breakage.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* parseComponentConfig(`{
|
|
386
|
+
* base: "px-4 py-2",
|
|
387
|
+
* variants: { size: { sm: "text-sm", lg: "text-lg" } },
|
|
388
|
+
* defaultVariants: { size: "sm" }
|
|
389
|
+
* }`)
|
|
390
|
+
*/
|
|
391
|
+
export function parseComponentConfig(objectStr: string): ParsedComponentConfig {
|
|
392
|
+
const oxcResult = parseWithOxc(objectStr)
|
|
393
|
+
if (oxcResult !== null) return oxcResult
|
|
394
|
+
|
|
395
|
+
// Fallback: original tokenizer
|
|
396
|
+
return parseComponentConfigFallback(objectStr)
|
|
397
|
+
}
|