@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tailwind-styled-v4 — safelistGenerator
|
|
3
|
+
*
|
|
4
|
+
* Scan semua source files dan extract Tailwind classes untuk safelist.
|
|
5
|
+
* Output: .tailwind-styled-safelist.json
|
|
6
|
+
*
|
|
7
|
+
* Developer tidak perlu manual safelist.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs"
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
import { extractAllClasses } from "./classExtractor"
|
|
13
|
+
|
|
14
|
+
const SCAN_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"]
|
|
15
|
+
|
|
16
|
+
function scanDir(dir: string, files: string[] = []): string[] {
|
|
17
|
+
if (!fs.existsSync(dir)) return files
|
|
18
|
+
|
|
19
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue
|
|
22
|
+
const fullPath = path.join(dir, entry.name)
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
scanDir(fullPath, files)
|
|
25
|
+
} else if (SCAN_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
26
|
+
files.push(fullPath)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return files
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateSafelist(
|
|
33
|
+
scanDirs: string[],
|
|
34
|
+
outputPath = ".tailwind-styled-safelist.json",
|
|
35
|
+
cwd = process.cwd()
|
|
36
|
+
): string[] {
|
|
37
|
+
const allClasses = new Set<string>()
|
|
38
|
+
|
|
39
|
+
for (const dir of scanDirs) {
|
|
40
|
+
const absDir = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir)
|
|
41
|
+
const files = scanDir(absDir)
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
try {
|
|
45
|
+
const source = fs.readFileSync(file, "utf-8")
|
|
46
|
+
const classes = extractAllClasses(source)
|
|
47
|
+
classes.forEach((c) => allClasses.add(c))
|
|
48
|
+
} catch {
|
|
49
|
+
// skip unreadable files
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sorted = Array.from(allClasses).sort()
|
|
55
|
+
const absOutput = path.isAbsolute(outputPath) ? outputPath : path.resolve(cwd, outputPath)
|
|
56
|
+
|
|
57
|
+
fs.writeFileSync(absOutput, JSON.stringify(sorted, null, 2))
|
|
58
|
+
console.log(`[tailwind-styled-v4] Safelist: ${sorted.length} classes → ${absOutput}`)
|
|
59
|
+
|
|
60
|
+
return sorted
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function loadSafelist(safelistPath: string): string[] {
|
|
64
|
+
try {
|
|
65
|
+
const content = fs.readFileSync(safelistPath, "utf-8")
|
|
66
|
+
return JSON.parse(content)
|
|
67
|
+
} catch {
|
|
68
|
+
return []
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Tailwind v4 variant — output CSS dengan @source inline() bukan JSON.
|
|
74
|
+
* Tailwind v4 tidak punya 'safelist' di config — pakai @source inline() di CSS.
|
|
75
|
+
*/
|
|
76
|
+
export function generateSafelistCss(
|
|
77
|
+
scanDirs: string[],
|
|
78
|
+
outputPath = "src/app/__tw-safelist.css",
|
|
79
|
+
cwd = process.cwd()
|
|
80
|
+
): string[] {
|
|
81
|
+
const allClasses = new Set<string>()
|
|
82
|
+
|
|
83
|
+
for (const dir of scanDirs) {
|
|
84
|
+
const absDir = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir)
|
|
85
|
+
const files = scanDir(absDir)
|
|
86
|
+
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
try {
|
|
89
|
+
const source = fs.readFileSync(file, "utf-8")
|
|
90
|
+
const classes = extractAllClasses(source)
|
|
91
|
+
classes.forEach((c) => allClasses.add(c))
|
|
92
|
+
} catch {
|
|
93
|
+
// skip unreadable files
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sorted = Array.from(allClasses).sort()
|
|
99
|
+
const absOutput = path.isAbsolute(outputPath) ? outputPath : path.resolve(cwd, outputPath)
|
|
100
|
+
|
|
101
|
+
fs.mkdirSync(path.dirname(absOutput), { recursive: true })
|
|
102
|
+
|
|
103
|
+
const css =
|
|
104
|
+
sorted.length > 0
|
|
105
|
+
? `/* Auto-generated by tailwind-styled-v4 — DO NOT EDIT */
|
|
106
|
+
@source inline("${sorted.join(" ")}");
|
|
107
|
+
`
|
|
108
|
+
: `/* Auto-generated by tailwind-styled-v4 — DO NOT EDIT */
|
|
109
|
+
/* No safelist classes found */
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
fs.writeFileSync(absOutput, css)
|
|
113
|
+
console.log(`[tailwind-styled-v4] Safelist: ${sorted.length} classes → ${absOutput}`)
|
|
114
|
+
|
|
115
|
+
return sorted
|
|
116
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tailwind-styled-v4 — Static Variant Compiler
|
|
3
|
+
*
|
|
4
|
+
* Upgrade enterprise #3: Static variant compilation.
|
|
5
|
+
* Semua kombinasi variant di-compile saat build → runtime = 0.
|
|
6
|
+
*
|
|
7
|
+
* BEFORE (runtime):
|
|
8
|
+
* <Button size="lg" intent="primary" />
|
|
9
|
+
* → runtime picks classes → [base, variants.size.lg, variants.intent.primary]
|
|
10
|
+
* → Runtime join + twMerge
|
|
11
|
+
* → className computed on every render
|
|
12
|
+
*
|
|
13
|
+
* AFTER (static):
|
|
14
|
+
* <Button size="lg" intent="primary" />
|
|
15
|
+
* → compiler already generated: "tw-btn-lg-primary" at build time
|
|
16
|
+
* → className is a direct lookup: O(1), pure string
|
|
17
|
+
* → Runtime = 0
|
|
18
|
+
*
|
|
19
|
+
* For a Button with 3 sizes × 4 intents = 12 combinations — all pre-compiled.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const compiled = compileAllVariantCombinations({
|
|
23
|
+
* componentId: "Button",
|
|
24
|
+
* base: "px-4 py-2 font-medium rounded",
|
|
25
|
+
* variants: {
|
|
26
|
+
* size: { sm: "h-8 text-sm", md: "h-10 text-base", lg: "h-12 text-lg" },
|
|
27
|
+
* intent: { primary: "bg-blue-500 text-white", ghost: "border text-current" },
|
|
28
|
+
* },
|
|
29
|
+
* defaultVariants: { size: "md", intent: "primary" },
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* // Generates:
|
|
33
|
+
* // compiled["sm|primary"] = "px-4 py-2 font-medium rounded h-8 text-sm bg-blue-500 text-white"
|
|
34
|
+
* // compiled["md|primary"] = "px-4 py-2 font-medium rounded h-10 text-base bg-blue-500 text-white"
|
|
35
|
+
* // ...all 6 combinations
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { twMerge } from "tailwind-merge"
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Types
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface StaticVariantConfig {
|
|
45
|
+
componentId: string
|
|
46
|
+
base: string
|
|
47
|
+
variants: Record<string, Record<string, string>>
|
|
48
|
+
compoundVariants?: Array<{ class: string; [key: string]: any }>
|
|
49
|
+
defaultVariants?: Record<string, string>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CompiledVariantTable {
|
|
53
|
+
/** componentId */
|
|
54
|
+
id: string
|
|
55
|
+
/** Combination key → final merged className */
|
|
56
|
+
table: Record<string, string>
|
|
57
|
+
/** Ordered variant keys (determines key format) */
|
|
58
|
+
keys: string[]
|
|
59
|
+
/** Default variant combination key */
|
|
60
|
+
defaultKey: string
|
|
61
|
+
/** Stats */
|
|
62
|
+
combinations: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Combination key format
|
|
67
|
+
// key = "variant1Value|variant2Value|..." (sorted by variant key name)
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
export function makeCombinationKey(values: Record<string, string>, sortedKeys: string[]): string {
|
|
71
|
+
return sortedKeys.map((k) => values[k] ?? "").join("|")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
// Cartesian product — generate all variant combinations
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function cartesian(variants: Record<string, string[]>): Record<string, string>[] {
|
|
79
|
+
const keys = Object.keys(variants).sort()
|
|
80
|
+
if (keys.length === 0) return [{}]
|
|
81
|
+
|
|
82
|
+
let combinations: Record<string, string>[] = [{}]
|
|
83
|
+
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
const values = variants[key]
|
|
86
|
+
const next: Record<string, string>[] = []
|
|
87
|
+
for (const combo of combinations) {
|
|
88
|
+
for (const val of values) {
|
|
89
|
+
next.push({ ...combo, [key]: val })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
combinations = next
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return combinations
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Compound variant resolution
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function resolveCompound(
|
|
103
|
+
compounds: Array<{ class: string; [key: string]: any }>,
|
|
104
|
+
combination: Record<string, string>
|
|
105
|
+
): string {
|
|
106
|
+
const classes: string[] = []
|
|
107
|
+
for (const compound of compounds) {
|
|
108
|
+
const { class: cls, ...conditions } = compound
|
|
109
|
+
const match = Object.entries(conditions).every(([k, v]) => combination[k] === v)
|
|
110
|
+
if (match) classes.push(cls)
|
|
111
|
+
}
|
|
112
|
+
return classes.join(" ")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
// Main: compile all combinations
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pre-compile all variant combinations into a static lookup table.
|
|
121
|
+
*
|
|
122
|
+
* Called at build time by the compiler.
|
|
123
|
+
* Output is injected as a const into the transformed file.
|
|
124
|
+
*/
|
|
125
|
+
export function compileAllVariantCombinations(config: StaticVariantConfig): CompiledVariantTable {
|
|
126
|
+
const { componentId, base, variants, compoundVariants = [], defaultVariants = {} } = config
|
|
127
|
+
|
|
128
|
+
const variantValueSets: Record<string, string[]> = {}
|
|
129
|
+
for (const [key, values] of Object.entries(variants)) {
|
|
130
|
+
variantValueSets[key] = Object.keys(values)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const sortedKeys = Object.keys(variantValueSets).sort()
|
|
134
|
+
const combinations = cartesian(variantValueSets)
|
|
135
|
+
const table: Record<string, string> = {}
|
|
136
|
+
|
|
137
|
+
for (const combo of combinations) {
|
|
138
|
+
const key = makeCombinationKey(combo, sortedKeys)
|
|
139
|
+
|
|
140
|
+
const variantClasses = sortedKeys.map((k) => variants[k][combo[k]] ?? "").filter(Boolean)
|
|
141
|
+
|
|
142
|
+
const compoundClasses = resolveCompound(compoundVariants, combo)
|
|
143
|
+
|
|
144
|
+
const finalClass = twMerge(base, ...variantClasses, compoundClasses || "").trim()
|
|
145
|
+
|
|
146
|
+
table[key] = finalClass
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const defaultValues = sortedKeys.reduce<Record<string, string>>((acc, k) => {
|
|
150
|
+
acc[k] = defaultVariants[k] ?? variantValueSets[k][0] ?? ""
|
|
151
|
+
return acc
|
|
152
|
+
}, {})
|
|
153
|
+
const defaultKey = makeCombinationKey(defaultValues, sortedKeys)
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
id: componentId,
|
|
157
|
+
table,
|
|
158
|
+
keys: sortedKeys,
|
|
159
|
+
defaultKey,
|
|
160
|
+
combinations: combinations.length,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// Code generation — emit static table as TypeScript const
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate the JavaScript code for a compiled variant table.
|
|
170
|
+
* This code is injected into the transformed file by the AST compiler.
|
|
171
|
+
*
|
|
172
|
+
* @example Output:
|
|
173
|
+
* const __svt_Button = {"sm|primary":"px-4 py-2 h-8 text-sm bg-blue-500 text-white",...}
|
|
174
|
+
* const __svt_Button_keys = ["intent","size"]
|
|
175
|
+
* const __svt_Button_default = "md|primary"
|
|
176
|
+
*/
|
|
177
|
+
export function generateStaticVariantCode(compiled: CompiledVariantTable): string {
|
|
178
|
+
const { id, table, keys, defaultKey } = compiled
|
|
179
|
+
|
|
180
|
+
return [
|
|
181
|
+
`/* @tw-static-variants: ${id} — ${compiled.combinations} combinations */`,
|
|
182
|
+
`const __svt_${id} = ${JSON.stringify(table)};`,
|
|
183
|
+
`const __svt_${id}_keys = ${JSON.stringify(keys)};`,
|
|
184
|
+
`const __svt_${id}_default = ${JSON.stringify(defaultKey)};`,
|
|
185
|
+
].join("\n")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate the runtime lookup function for a statically compiled variant table.
|
|
190
|
+
* This replaces the variant resolver in the component.
|
|
191
|
+
*
|
|
192
|
+
* Runtime code is minimal — just a string lookup from a pre-compiled table.
|
|
193
|
+
*/
|
|
194
|
+
export function generateStaticVariantLookup(id: string): string {
|
|
195
|
+
return `function __svt_${id}_lookup(props) {
|
|
196
|
+
var key = __svt_${id}_keys.map(function(k){ return props[k] || ""; }).join("|");
|
|
197
|
+
return __svt_${id}[key] || __svt_${id}[__svt_${id}_default] || "";
|
|
198
|
+
}`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// Runtime: StaticVariantResolver
|
|
203
|
+
// Used by createComponent when compiler is NOT running (dev mode)
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Runtime fallback for static variant compilation.
|
|
208
|
+
* Creates a lookup table on first use, caches for subsequent renders.
|
|
209
|
+
*
|
|
210
|
+
* In production (with compiler), the component never calls this —
|
|
211
|
+
* it uses the pre-compiled __svt_* table directly.
|
|
212
|
+
*/
|
|
213
|
+
export class StaticVariantResolver {
|
|
214
|
+
private cache: Map<string, string>
|
|
215
|
+
private compiled: CompiledVariantTable
|
|
216
|
+
|
|
217
|
+
constructor(config: StaticVariantConfig) {
|
|
218
|
+
this.compiled = compileAllVariantCombinations(config)
|
|
219
|
+
this.cache = new Map(Object.entries(this.compiled.table))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
resolve(props: Record<string, any>): string {
|
|
223
|
+
const key = makeCombinationKey(
|
|
224
|
+
this.compiled.keys.reduce<Record<string, string>>((acc, k) => {
|
|
225
|
+
acc[k] = String(props[k] ?? "")
|
|
226
|
+
return acc
|
|
227
|
+
}, {}),
|
|
228
|
+
this.compiled.keys
|
|
229
|
+
)
|
|
230
|
+
return this.cache.get(key) ?? this.cache.get(this.compiled.defaultKey) ?? ""
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
get stats() {
|
|
234
|
+
return {
|
|
235
|
+
id: this.compiled.id,
|
|
236
|
+
combinations: this.compiled.combinations,
|
|
237
|
+
keys: this.compiled.keys,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|