@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,285 @@
1
+ /**
2
+ * tailwind-styled-v4 — Embedded Tailwind Engine
3
+ *
4
+ * Compiler menjalankan Tailwind internally — tidak perlu tailwind CLI,
5
+ * tidak perlu postcss config manual.
6
+ *
7
+ * Cara kerja:
8
+ * 1. Compiler extract semua class dari source (via classExtractor)
9
+ * 2. Engine generate CSS hanya untuk class tersebut
10
+ * 3. CSS di-output per route (route-level CSS bundling)
11
+ *
12
+ * Ini membuat CSS output jauh lebih kecil:
13
+ * Tailwind normal: ~300kb global
14
+ * Route CSS: ~2–10kb per route
15
+ *
16
+ * Mode operasi:
17
+ * "jit" → generate CSS saat file berubah (dev mode)
18
+ * "build" → generate semua CSS di akhir build (production)
19
+ * "manual" → tidak generate, hanya extract (default untuk kompatibilitas)
20
+ */
21
+
22
+ import path from "node:path"
23
+ import { loadTailwindConfig } from "./loadTailwindConfig"
24
+ import { getAllRoutes, getRouteClasses } from "./routeCssCollector"
25
+
26
+ export type TailwindEngineMode = "jit" | "build" | "manual"
27
+
28
+ export interface TailwindEngineOptions {
29
+ mode?: TailwindEngineMode
30
+ cwd?: string
31
+ outputDir?: string
32
+ config?: Record<string, any>
33
+ minify?: boolean
34
+ }
35
+
36
+ export interface CssGenerateResult {
37
+ route: string
38
+ css: string
39
+ classes: string[]
40
+ sizeBytes: number
41
+ }
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Core engine
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Try to use Tailwind's internal API for CSS generation.
49
+ * Fallback ke manual CSS generation jika Tailwind API tidak tersedia.
50
+ *
51
+ * NOTE: Tailwind v4 mengubah API internal — kita support keduanya.
52
+ */
53
+ export async function generateCssForClasses(
54
+ classes: string[],
55
+ config?: Record<string, any>,
56
+ cwd = process.cwd()
57
+ ): Promise<string> {
58
+ const twConfig = config ?? loadTailwindConfig(cwd)
59
+
60
+ // Strategy 1: Tailwind v4 (@tailwindcss/postcss atau native)
61
+ try {
62
+ return await generateViaTailwindV4(classes, twConfig, cwd)
63
+ } catch {
64
+ // not available
65
+ }
66
+
67
+ // Strategy 2: Tailwind v3 API
68
+ try {
69
+ return await generateViaTailwindV3(classes, twConfig)
70
+ } catch {
71
+ // not available
72
+ }
73
+
74
+ // Strategy 3: Manual atomic CSS generation (always available)
75
+ return generateManualCss(classes)
76
+ }
77
+
78
+ /**
79
+ * Tailwind v4 CSS generation (via @tailwindcss/postcss)
80
+ */
81
+ async function generateViaTailwindV4(
82
+ classes: string[],
83
+ _config: Record<string, any>,
84
+ cwd: string
85
+ ): Promise<string> {
86
+ // Tailwind v4 uses @import "tailwindcss" syntax
87
+ // We generate a virtual CSS file that imports tailwindcss + safelist
88
+ const virtualCss = [
89
+ `@import "tailwindcss";`,
90
+ `@layer utilities {`,
91
+ ` /* Generated by tailwind-styled-v4 */`,
92
+ `}`,
93
+ ].join("\n")
94
+
95
+ const postcss = require("postcss")
96
+ const tailwindcss = require("@tailwindcss/postcss")
97
+
98
+ const result = await postcss([
99
+ tailwindcss({
100
+ optimize: { minify: false },
101
+ }),
102
+ ]).process(virtualCss, {
103
+ from: path.join(cwd, "virtual.css"),
104
+ })
105
+
106
+ // Filter to only include classes we need
107
+ return filterCssForClasses(result.css, classes)
108
+ }
109
+
110
+ /**
111
+ * Tailwind v3 CSS generation (via tailwindcss package API)
112
+ */
113
+ async function generateViaTailwindV3(
114
+ classes: string[],
115
+ config: Record<string, any>
116
+ ): Promise<string> {
117
+ const postcss = require("postcss")
118
+ const tailwindcss = require("tailwindcss")
119
+
120
+ // Create virtual content with only our classes
121
+ const virtualContent = classes.map((c) => `<div class="${c}">`).join("\n")
122
+
123
+ const twConfigWithContent = {
124
+ ...config,
125
+ content: [{ raw: virtualContent, extension: "html" }],
126
+ safelist: classes,
127
+ }
128
+
129
+ const inputCss = `@tailwind base;\n@tailwind components;\n@tailwind utilities;`
130
+
131
+ const result = await postcss([tailwindcss(twConfigWithContent)]).process(inputCss, {
132
+ from: undefined,
133
+ })
134
+
135
+ return result.css
136
+ }
137
+
138
+ /**
139
+ * Manual atomic CSS generation — always available, no Tailwind dependency.
140
+ * Menggunakan AtomicCss module yang sudah ada.
141
+ */
142
+ function generateManualCss(classes: string[]): string {
143
+ const { generateAtomicCss, parseAtomicClass } = require("./atomicCss")
144
+
145
+ const rules = classes.map((c: string) => parseAtomicClass(c)).filter(Boolean)
146
+
147
+ if (rules.length === 0) return ""
148
+
149
+ const header = `/* Generated by tailwind-styled-v4 — ${new Date().toISOString()} */\n`
150
+ return header + generateAtomicCss(rules)
151
+ }
152
+
153
+ /**
154
+ * Filter compiled Tailwind CSS to only include needed class selectors.
155
+ * Dipakai untuk optimize output CSS size.
156
+ */
157
+ function filterCssForClasses(fullCss: string, classes: string[]): string {
158
+ // Keep @base rules + only selectors matching our classes
159
+ const _classSet = new Set(classes)
160
+
161
+ const lines = fullCss.split("\n")
162
+ const kept: string[] = []
163
+ let inBlock = false
164
+ let keepBlock = false
165
+ let braceDepth = 0
166
+
167
+ for (const line of lines) {
168
+ // Always keep @layer base (reset styles)
169
+ if (line.includes("@layer base") || line.includes("*, *::before")) {
170
+ kept.push(line)
171
+ continue
172
+ }
173
+
174
+ // Check if line is a selector for one of our classes
175
+ if (!inBlock) {
176
+ const isOurClass = classes.some((cls) => {
177
+ const escaped = cls.replace(/[:/[\].!%]/g, "\\$&")
178
+ return line.includes(`.${escaped}`) || line.includes(`.${cls}`)
179
+ })
180
+
181
+ if (isOurClass) {
182
+ keepBlock = true
183
+ inBlock = true
184
+ braceDepth = 0
185
+ }
186
+ }
187
+
188
+ if (inBlock) {
189
+ if (keepBlock) kept.push(line)
190
+ if (line.includes("{")) braceDepth++
191
+ if (line.includes("}")) {
192
+ braceDepth--
193
+ if (braceDepth <= 0) {
194
+ inBlock = false
195
+ keepBlock = false
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return kept.join("\n")
202
+ }
203
+
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+ // Build-time CSS generation (dipakai di withTailwindStyled buildEnd hook)
206
+ // ─────────────────────────────────────────────────────────────────────────────
207
+
208
+ export async function generateAllRouteCss(
209
+ opts: TailwindEngineOptions = {}
210
+ ): Promise<CssGenerateResult[]> {
211
+ const { cwd = process.cwd(), outputDir, config, minify = true } = opts
212
+
213
+ const results: CssGenerateResult[] = []
214
+ const routes = getAllRoutes()
215
+ const twConfig = config ?? loadTailwindConfig(cwd)
216
+
217
+ for (const route of routes) {
218
+ const classes = Array.from(getRouteClasses(route))
219
+ if (classes.length === 0) continue
220
+
221
+ try {
222
+ let css = await generateCssForClasses(classes, twConfig, cwd)
223
+
224
+ if (minify) {
225
+ css = minifyCss(css)
226
+ }
227
+
228
+ results.push({
229
+ route,
230
+ css,
231
+ classes,
232
+ sizeBytes: Buffer.byteLength(css, "utf8"),
233
+ })
234
+ } catch (e) {
235
+ console.warn(`[tailwind-styled-v4] CSS generation failed for route ${route}:`, e)
236
+ }
237
+ }
238
+
239
+ // Emit CSS files if outputDir provided
240
+ if (outputDir) {
241
+ const fs = require("node:fs")
242
+ fs.mkdirSync(outputDir, { recursive: true })
243
+
244
+ for (const result of results) {
245
+ const filename = routeToFilename(result.route)
246
+ const filepath = path.join(outputDir, filename)
247
+ fs.mkdirSync(path.dirname(filepath), { recursive: true })
248
+ fs.writeFileSync(filepath, result.css)
249
+ }
250
+
251
+ const totalSize = results.reduce((sum, r) => sum + r.sizeBytes, 0)
252
+ console.log(
253
+ `[tailwind-styled-v4] Route CSS generated: ${results.length} routes, ${formatBytes(totalSize)} total`
254
+ )
255
+ }
256
+
257
+ return results
258
+ }
259
+
260
+ // ─────────────────────────────────────────────────────────────────────────────
261
+ // Helpers
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+
264
+ function routeToFilename(route: string): string {
265
+ if (route === "/") return "index.css"
266
+ if (route === "__global") return "_global.css"
267
+ return `${route.replace(/^\//, "").replace(/\//g, "_")}.css`
268
+ }
269
+
270
+ function minifyCss(css: string): string {
271
+ return css
272
+ .replace(/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g, "") // remove comments
273
+ .replace(/\s+/g, " ") // collapse whitespace
274
+ .replace(/\s*{\s*/g, "{")
275
+ .replace(/\s*}\s*/g, "}")
276
+ .replace(/\s*:\s*/g, ":")
277
+ .replace(/\s*;\s*/g, ";")
278
+ .trim()
279
+ }
280
+
281
+ function formatBytes(bytes: number): string {
282
+ if (bytes < 1024) return `${bytes}B`
283
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
284
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`
285
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * tailwind-styled-v4 — twDetector
3
+ *
4
+ * Regex-based detector untuk semua syntax tw yang valid.
5
+ * Dipakai sebelum transform — jika tidak ada tw usage, skip file.
6
+ *
7
+ * FIXED: trailing space bug di TEMPLATE_RE (#02)
8
+ */
9
+
10
+ /** tw.div`...` — FIX: removed trailing space before /g */
11
+ export const TEMPLATE_RE = /\btw\.(server\.)?(\w+)`((?:[^`\\]|\\.)*)`/g
12
+
13
+ /** tw.div({ base: "...", variants: {...} }) */
14
+ export const OBJECT_RE = /\btw\.(server\.)?(\w+)\(\s*(\{[\s\S]*?\})\s*\)/g
15
+
16
+ /** tw(Component)`...` */
17
+ export const WRAP_RE = /\btw\((\w+)\)`((?:[^`\\]|\\.)*)`/g
18
+
19
+ /** Card.extend`...` */
20
+ export const EXTEND_RE = /(\w+)\.extend`((?:[^`\\]|\\.)*)`/g
21
+
22
+ /** import { tw } from "tailwind-styled-v4" */
23
+ export const IMPORT_RE = /from\s*["']tailwind-styled-v4["']/
24
+
25
+ /** Transform already-applied marker — idempotency guard (#08) */
26
+ export const TRANSFORM_MARKER = "/* @tw-transformed */"
27
+
28
+ export function hasTwUsage(source: string): boolean {
29
+ return IMPORT_RE.test(source) || source.includes("tw.")
30
+ }
31
+
32
+ /** Check if file was already transformed — prevents double processing (#08) */
33
+ export function isAlreadyTransformed(source: string): boolean {
34
+ return source.includes(TRANSFORM_MARKER)
35
+ }
36
+
37
+ export function isTwTemplateLiteral(source: string, index: number): boolean {
38
+ const before = source.slice(Math.max(0, index - 20), index)
39
+ return /\btw\.\w+$/.test(before) || /\btw\(\w+\)$/.test(before)
40
+ }
41
+
42
+ export function isDynamic(content: string): boolean {
43
+ return content.includes("${")
44
+ }
45
+
46
+ export function isServerComponent(source: string): boolean {
47
+ return !source.includes('"use client"') && !source.includes("'use client'")
48
+ }
49
+
50
+ export function hasInteractiveFeatures(content: string): boolean {
51
+ return /\b(hover:|focus:|active:|group-hover:|peer-|on[A-Z]|useState|useEffect|useRef)\b/.test(
52
+ content
53
+ )
54
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * tailwind-styled-v4 — variantCompiler
3
+ *
4
+ * FIXES:
5
+ * #01 — Don't pre-merge base into variant table values (double-merge bug)
6
+ * #06 — Use proper AST parser instead of fragile regex
7
+ *
8
+ * BEFORE (double-merge):
9
+ * compileVariants: table["size"]["sm"] = "px-4 py-2 text-sm" ← base included
10
+ * astTransform: [base, table["size"][...]] ← base AGAIN → DUPE
11
+ *
12
+ * AFTER (correct):
13
+ * compileVariants: table["size"]["sm"] = "text-sm" ← variant only
14
+ * astTransform: [base, table["size"][...], className] ← base once, correct
15
+ *
16
+ * Input:
17
+ * { base: "px-4 py-2", variants: { size: { sm: "text-sm" } } }
18
+ *
19
+ * Output code:
20
+ * const __vt_abc123 = { size: { sm: "text-sm" } }
21
+ * // className = [base, table[variant]] → no duplication
22
+ */
23
+
24
+ import { parseComponentConfig } from "./astParser"
25
+ import { normalizeClasses } from "./classMerger"
26
+
27
+ export interface CompiledVariants {
28
+ base: string
29
+ table: Record<string, Record<string, string>>
30
+ compounds: Array<{ class: string; [key: string]: any }>
31
+ defaults: Record<string, string>
32
+ }
33
+
34
+ /**
35
+ * Compile variant config into lookup table.
36
+ *
37
+ * FIX #01: Do NOT pre-merge base into table values.
38
+ * Table contains variant-specific classes only.
39
+ * Base is always injected separately in the component className array.
40
+ */
41
+ export function compileVariants(
42
+ base: string,
43
+ variants: Record<string, Record<string, string>>,
44
+ compounds: Array<{ class: string; [key: string]: any }> = [],
45
+ defaults: Record<string, string> = {}
46
+ ): CompiledVariants {
47
+ const table: Record<string, Record<string, string>> = {}
48
+
49
+ for (const key in variants) {
50
+ table[key] = {}
51
+ for (const val in variants[key]) {
52
+ // FIX #01: variant classes only — NOT merged with base
53
+ // Base is injected separately in renderVariantComponent
54
+ table[key][val] = normalizeClasses(variants[key][val])
55
+ }
56
+ }
57
+
58
+ return { base, table, compounds, defaults }
59
+ }
60
+
61
+ export function generateVariantCode(id: string, compiled: CompiledVariants): string {
62
+ const { table, compounds, defaults } = compiled
63
+
64
+ const tableJson = JSON.stringify(table, null, 2)
65
+ const compoundsJson = JSON.stringify(compounds, null, 2)
66
+ const defaultsJson = JSON.stringify(defaults, null, 2)
67
+
68
+ return `const __vt_${id} = ${tableJson};
69
+ const __vc_${id} = ${compoundsJson};
70
+ const __vd_${id} = ${defaultsJson};`
71
+ }
72
+
73
+ /**
74
+ * Parse object config string.
75
+ * UPGRADE #4: Uses proper AST parser — handles all edge cases.
76
+ *
77
+ * FIX #02 (indirect): classExtractor no longer needs .slice(0, -1) workaround
78
+ * since TEMPLATE_RE trailing space is fixed in twDetector.ts
79
+ */
80
+ export function parseObjectConfig(objectStr: string): {
81
+ base: string
82
+ variants: Record<string, Record<string, string>>
83
+ compounds: Array<{ class: string; [key: string]: any }>
84
+ defaults: Record<string, string>
85
+ } {
86
+ return parseComponentConfig(objectStr)
87
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "declarationDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }