@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.
@@ -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
+ }