@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/src/index.ts ADDED
@@ -0,0 +1,100 @@
1
+ // ── Core transform pipeline ──────────────────────────────────────────────────
2
+
3
+ export type { TransformOptions, TransformResult } from "./astTransform"
4
+ export { shouldProcess, transformSource } from "./astTransform"
5
+ export type { AtomicRule } from "./atomicCss"
6
+ // ── Atomic CSS ────────────────────────────────────────────────────────────────
7
+ export {
8
+ clearAtomicRegistry,
9
+ generateAtomicCss,
10
+ getAtomicRegistry,
11
+ parseAtomicClass,
12
+ toAtomicClasses,
13
+ } from "./atomicCss"
14
+ // ── Class utilities ───────────────────────────────────────────────────────────
15
+ export { extractAllClasses } from "./classExtractor"
16
+ export { mergeClassesStatic, normalizeClasses } from "./classMerger"
17
+ export type { HoistResult } from "./componentHoister"
18
+ // ── Component hoisting ────────────────────────────────────────────────────────
19
+ export { hoistComponents } from "./componentHoister"
20
+ export type {
21
+ EliminationReport,
22
+ VariantUsage,
23
+ } from "./deadStyleEliminator"
24
+ // ── Dead Style Eliminator ─────────────────────────────────────────────────────
25
+ export {
26
+ extractComponentUsage,
27
+ runElimination,
28
+ scanProjectUsage,
29
+ } from "./deadStyleEliminator"
30
+ export type {
31
+ CssDiff,
32
+ FileDependencyGraph,
33
+ IncrementalEngineOptions,
34
+ IncrementalStats,
35
+ ProcessResult,
36
+ StyleNode,
37
+ } from "./incrementalEngine"
38
+ // ── Incremental CSS Compiler ──────────────────────────────────────────────────
39
+ export {
40
+ getIncrementalEngine,
41
+ IncrementalEngine,
42
+ parseClassesToNodes,
43
+ resetIncrementalEngine,
44
+ } from "./incrementalEngine"
45
+ // ── Zero-config Tailwind config loader ───────────────────────────────────────
46
+ export {
47
+ bootstrapZeroConfig,
48
+ getContentPaths,
49
+ invalidateConfigCache,
50
+ isZeroConfig,
51
+ loadTailwindConfig,
52
+ } from "./loadTailwindConfig"
53
+ export type { RouteClassMap } from "./routeCssCollector"
54
+ // ── Route CSS collector ────────────────────────────────────────────────────────
55
+ export {
56
+ fileToRoute,
57
+ getAllRoutes,
58
+ getCollector,
59
+ getCollectorSummary,
60
+ getRouteClasses,
61
+ registerFileClasses,
62
+ registerGlobalClasses,
63
+ resetCollector,
64
+ } from "./routeCssCollector"
65
+ export type { ComponentEnv, RscAnalysis, StaticVariantUsage } from "./rscAnalyzer"
66
+ // ── RSC-Aware ─────────────────────────────────────────────────────────────────
67
+ export {
68
+ analyzeFile,
69
+ analyzeVariantUsage,
70
+ injectClientDirective,
71
+ injectServerOnlyComment,
72
+ resolveServerVariant,
73
+ } from "./rscAnalyzer"
74
+ // ── Safelist ──────────────────────────────────────────────────────────────────
75
+ export { generateSafelist, generateSafelistCss, loadSafelist } from "./safelistGenerator"
76
+ export type {
77
+ BucketStats,
78
+ ConflictWarning,
79
+ StyleBucket,
80
+ } from "./styleBucketSystem"
81
+ // ── Style Bucket System ───────────────────────────────────────────────────────
82
+ export {
83
+ BucketEngine,
84
+ bucketSort,
85
+ classifyNode,
86
+ detectConflicts,
87
+ getBucketEngine,
88
+ resetBucketEngine,
89
+ } from "./styleBucketSystem"
90
+ export type { CssGenerateResult, TailwindEngineOptions } from "./tailwindEngine"
91
+ // ── Embedded Tailwind engine ──────────────────────────────────────────────────
92
+ export { generateAllRouteCss, generateCssForClasses } from "./tailwindEngine"
93
+ // ── Detectors ─────────────────────────────────────────────────────────────────
94
+ export {
95
+ hasInteractiveFeatures,
96
+ hasTwUsage,
97
+ isDynamic,
98
+ isServerComponent,
99
+ } from "./twDetector"
100
+ export { compileVariants } from "./variantCompiler"
@@ -0,0 +1,144 @@
1
+ /**
2
+ * tailwind-styled-v4 — Tailwind Config Loader
3
+ *
4
+ * Auto-load tailwind config dari project.
5
+ * Jika tidak ada → fallback ke defaultPreset (zero-config mode).
6
+ *
7
+ * Priority:
8
+ * 1. tailwind.config.ts (TypeScript)
9
+ * 2. tailwind.config.js (JavaScript)
10
+ * 3. tailwind.config.mjs (ESM)
11
+ * 4. defaultPreset (fallback — zero-config)
12
+ */
13
+
14
+ import fs from "node:fs"
15
+ import path from "node:path"
16
+
17
+ export type TailwindConfig = Record<string, any>
18
+
19
+ const CONFIG_FILES = [
20
+ "tailwind.config.ts",
21
+ "tailwind.config.js",
22
+ "tailwind.config.mjs",
23
+ "tailwind.config.cjs",
24
+ ]
25
+
26
+ let _cachedConfig: TailwindConfig | null = null
27
+ let _cachedCwd: string = ""
28
+
29
+ /**
30
+ * Load tailwind config. Cached per process.
31
+ * Returns defaultPreset if no config found (zero-config mode).
32
+ */
33
+ export function loadTailwindConfig(cwd = process.cwd()): TailwindConfig {
34
+ // Cache invalidation
35
+ if (_cachedConfig && _cachedCwd === cwd) return _cachedConfig
36
+
37
+ _cachedCwd = cwd
38
+
39
+ // Try each config file
40
+ for (const file of CONFIG_FILES) {
41
+ const fullPath = path.join(cwd, file)
42
+ if (fs.existsSync(fullPath)) {
43
+ try {
44
+ // For .ts files, we need ts-node or pre-compiled version
45
+ // In practice, Next.js/Vite already handle this via their config system
46
+ const mod = require(fullPath)
47
+ const config = mod.default ?? mod
48
+ _cachedConfig = config
49
+ console.log(`[tailwind-styled-v4] Using config: ${file}`)
50
+ return config
51
+ } catch {}
52
+ }
53
+ }
54
+
55
+ // Zero-config fallback
56
+ console.log("[tailwind-styled-v4] No tailwind config found → using built-in preset")
57
+ const { defaultPreset } = require("../../preset/src/defaultPreset")
58
+ _cachedConfig = defaultPreset
59
+ return defaultPreset
60
+ }
61
+
62
+ /**
63
+ * Get content paths dari config (atau default paths)
64
+ */
65
+ export function getContentPaths(config: TailwindConfig, cwd = process.cwd()): string[] {
66
+ const paths: string[] = []
67
+
68
+ if (Array.isArray(config.content)) {
69
+ for (const item of config.content) {
70
+ if (typeof item === "string") paths.push(item)
71
+ else if (typeof item === "object" && item.raw) {
72
+ // inline content object — skip
73
+ }
74
+ }
75
+ return paths
76
+ }
77
+
78
+ if (config.content?.files) {
79
+ return config.content.files.filter((f: any) => typeof f === "string")
80
+ }
81
+
82
+ // Fallback: scan standard dirs
83
+ return ["src", "app", "pages", "components"]
84
+ .filter((d) => fs.existsSync(path.join(cwd, d)))
85
+ .map((d) => `./${d}/**/*.{tsx,ts,jsx,js}`)
86
+ }
87
+
88
+ /**
89
+ * Invalidate config cache (useful for watch mode)
90
+ */
91
+ export function invalidateConfigCache(): void {
92
+ _cachedConfig = null
93
+ _cachedCwd = ""
94
+ }
95
+
96
+ /**
97
+ * Check if project has zero-config setup (no user tailwind config)
98
+ */
99
+ export function isZeroConfig(cwd = process.cwd()): boolean {
100
+ return !CONFIG_FILES.some((f) => fs.existsSync(path.join(cwd, f)))
101
+ }
102
+
103
+ /**
104
+ * Auto-generate tailwind.config.ts dan globals.css jika tidak ada
105
+ * (dipanggil oleh CLI dan withTailwindStyled pada first run)
106
+ */
107
+ export function bootstrapZeroConfig(cwd = process.cwd()): {
108
+ generatedConfig: boolean
109
+ generatedCss: boolean
110
+ } {
111
+ let generatedConfig = false
112
+ let generatedCss = false
113
+
114
+ // Tailwind v4: CSS-first — tidak perlu tailwind.config.ts
115
+ // Config dilakukan via CSS (@source, @theme, dsb.)
116
+ generatedConfig = false
117
+
118
+ // Generate globals.css if missing
119
+ const cssPaths = [
120
+ "src/app/globals.css",
121
+ "app/globals.css",
122
+ "src/index.css",
123
+ "src/styles/globals.css",
124
+ ]
125
+ const hasGlobalCss = cssPaths.some((p) => fs.existsSync(path.join(cwd, p)))
126
+
127
+ if (!hasGlobalCss) {
128
+ const { defaultGlobalCss } = require("../../preset/src/defaultPreset")
129
+ // Try to find app directory
130
+ const appDir = fs.existsSync(path.join(cwd, "src/app"))
131
+ ? "src/app"
132
+ : fs.existsSync(path.join(cwd, "app"))
133
+ ? "app"
134
+ : "src"
135
+ const cssPath = path.join(cwd, appDir, "globals.css")
136
+ if (fs.existsSync(path.dirname(cssPath))) {
137
+ fs.writeFileSync(cssPath, defaultGlobalCss)
138
+ generatedCss = true
139
+ console.log(`[tailwind-styled-v4] Generated ${cssPath}`)
140
+ }
141
+ }
142
+
143
+ return { generatedConfig, generatedCss }
144
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * tailwind-styled-v4 — Route CSS Collector
3
+ *
4
+ * Mengumpulkan Tailwind classes per-route sehingga setiap halaman
5
+ * hanya memuat CSS yang benar-benar dipakai.
6
+ *
7
+ * Tailwind default: ~300kb global CSS
8
+ * Route CSS: ~2–10kb per halaman
9
+ *
10
+ * Cara kerja:
11
+ * 1. Setiap file yang di-transform oleh compiler melaporkan classnya
12
+ * 2. Collector memetakan file → route
13
+ * 3. Di akhir build, CSS di-generate per route
14
+ *
15
+ * File structure output:
16
+ * .next/static/css/
17
+ * _global.css ← base + reset (sekali load)
18
+ * app/page.css ← hanya class yang dipakai di /
19
+ * app/about/page.css ← hanya class untuk /about
20
+ * app/dashboard/...
21
+ */
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Types
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ export interface RouteClassMap {
28
+ /** filepath → array of tw classes */
29
+ files: Map<string, Set<string>>
30
+ /** route → Set of files yang dipakai */
31
+ routes: Map<string, Set<string>>
32
+ /** Global classes (di-load semua route) */
33
+ global: Set<string>
34
+ }
35
+
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ // Singleton collector (per build)
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+
40
+ let _collector: RouteClassMap = {
41
+ files: new Map(),
42
+ routes: new Map(),
43
+ global: new Set(),
44
+ }
45
+
46
+ /**
47
+ * Register classes dari sebuah file setelah compiler transform.
48
+ * Dipanggil oleh turbopackLoader/webpackLoader setelah setiap file di-transform.
49
+ */
50
+ export function registerFileClasses(filepath: string, classes: string[]): void {
51
+ if (!_collector.files.has(filepath)) {
52
+ _collector.files.set(filepath, new Set())
53
+ }
54
+ const fileSet = _collector.files.get(filepath)!
55
+ classes.forEach((c) => fileSet.add(c))
56
+
57
+ // Auto-detect route dari filepath
58
+ const route = fileToRoute(filepath)
59
+ if (route) {
60
+ if (!_collector.routes.has(route)) {
61
+ _collector.routes.set(route, new Set())
62
+ }
63
+ _collector.routes.get(route)!.add(filepath)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Register global classes (base styles, layout, dsb.)
69
+ * Global classes dimuat di semua route.
70
+ */
71
+ export function registerGlobalClasses(classes: string[]): void {
72
+ classes.forEach((c) => _collector.global.add(c))
73
+ }
74
+
75
+ /**
76
+ * Get all classes for a specific route (termasuk global)
77
+ */
78
+ export function getRouteClasses(route: string): Set<string> {
79
+ const result = new Set<string>(_collector.global)
80
+
81
+ // Tambahkan classes dari semua file yang terkait route ini
82
+ const routeFiles = _collector.routes.get(route) ?? new Set()
83
+ for (const filepath of routeFiles) {
84
+ const fileClasses = _collector.files.get(filepath) ?? new Set()
85
+ fileClasses.forEach((c) => result.add(c))
86
+ }
87
+
88
+ return result
89
+ }
90
+
91
+ /**
92
+ * Get all routes yang sudah ter-register
93
+ */
94
+ export function getAllRoutes(): string[] {
95
+ return Array.from(_collector.routes.keys()).sort()
96
+ }
97
+
98
+ /**
99
+ * Get complete map (untuk build-time generation)
100
+ */
101
+ export function getCollector(): RouteClassMap {
102
+ return _collector
103
+ }
104
+
105
+ /**
106
+ * Reset collector (start of each build)
107
+ */
108
+ export function resetCollector(): void {
109
+ _collector = {
110
+ files: new Map(),
111
+ routes: new Map(),
112
+ global: new Set(),
113
+ }
114
+ }
115
+
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ // File → Route mapping
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Konversi filepath ke Next.js App Router route.
122
+ *
123
+ * /src/app/page.tsx → /
124
+ * /src/app/about/page.tsx → /about
125
+ * /src/app/dashboard/page.tsx → /dashboard
126
+ * /src/components/Button.tsx → null (shared component, goes to global)
127
+ * /src/app/layout.tsx → __layout (global)
128
+ */
129
+ export function fileToRoute(filepath: string): string | null {
130
+ const normalized = filepath.replace(/\\/g, "/")
131
+
132
+ // Layout files → global
133
+ if (
134
+ normalized.includes("/layout.") ||
135
+ normalized.includes("/loading.") ||
136
+ normalized.includes("/error.")
137
+ ) {
138
+ return "__global"
139
+ }
140
+
141
+ // Page files in App Router
142
+ const pageMatch = normalized.match(/\/app\/(.+?)\/page\.[tj]sx?$/)
143
+ if (pageMatch) return `/${pageMatch[1]}`
144
+
145
+ const rootPage = normalized.match(/\/app\/page\.[tj]sx?$/)
146
+ if (rootPage) return "/"
147
+
148
+ // Pages Router
149
+ const pagesMatch = normalized.match(/\/pages\/(.+?)\.[tj]sx?$/)
150
+ if (pagesMatch) {
151
+ const route = pagesMatch[1].replace(/\/index$/, "")
152
+ return `/${route}`
153
+ }
154
+
155
+ // Shared components → global
156
+ if (
157
+ normalized.includes("/components/") ||
158
+ normalized.includes("/ui/") ||
159
+ normalized.includes("/shared/")
160
+ ) {
161
+ return "__global"
162
+ }
163
+
164
+ return null
165
+ }
166
+
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+ // Summary for logging
169
+ // ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ export function getCollectorSummary(): string {
172
+ const routes = getAllRoutes()
173
+ const totalFiles = _collector.files.size
174
+ const totalGlobal = _collector.global.size
175
+
176
+ const lines = [
177
+ `[tailwind-styled-v4] Route CSS Summary:`,
178
+ ` Files processed: ${totalFiles}`,
179
+ ` Global classes: ${totalGlobal}`,
180
+ ` Routes found: ${routes.length}`,
181
+ ...routes.map((r) => {
182
+ const cls = getRouteClasses(r).size
183
+ return ` ${r} → ${cls} classes`
184
+ }),
185
+ ]
186
+
187
+ return lines.join("\n")
188
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * tailwind-styled-v4 — RSC Analyzer
3
+ *
4
+ * Inti dari RSC-Aware upgrade.
5
+ * Menganalisis setiap file untuk menentukan:
6
+ * - Server Component atau Client Component
7
+ * - Variant mana yang bisa di-resolve di server
8
+ * - Class mana yang membutuhkan client runtime
9
+ * - Auto client boundary injection
10
+ *
11
+ * Hasilnya:
12
+ * - Server Component → pure static className, zero JS ke client
13
+ * - Client Component → tetap seperti biasa dengan lookup table
14
+ */
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // RSC Analysis Types
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ export type ComponentEnv = "server" | "client" | "auto"
21
+
22
+ export interface RscAnalysis {
23
+ /** File ini adalah server component */
24
+ isServer: boolean
25
+ /** File ini butuh "use client" directive */
26
+ needsClientDirective: boolean
27
+ /** Alasan butuh client */
28
+ clientReasons: string[]
29
+ /** Classes yang membutuhkan client interaction */
30
+ interactiveClasses: string[]
31
+ /** Apakah semua variants bisa di-resolve statically di server */
32
+ canStaticResolveVariants: boolean
33
+ }
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Interactive class patterns — butuh client-side event handling
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ /** CSS classes yang TIDAK butuh JS — tetap bisa di server */
40
+ const CSS_INTERACTIVE_OK = [
41
+ /^hover:/, // CSS :hover — no JS needed
42
+ /^focus:/, // CSS :focus — no JS needed
43
+ /^focus-within:/, // CSS :focus-within
44
+ /^focus-visible:/, // CSS :focus-visible
45
+ /^active:/, // CSS :active
46
+ /^group-hover:/, // Tailwind group variant — CSS only
47
+ /^group-focus:/, // CSS only
48
+ /^peer-/, // Tailwind peer — CSS only
49
+ /^first:/, // CSS :first-child
50
+ /^last:/, // CSS :last-child
51
+ /^odd:/, // CSS :nth-child(odd)
52
+ /^even:/, // CSS :nth-child(even)
53
+ /^disabled:/, // CSS :disabled
54
+ /^placeholder:/, // CSS ::placeholder
55
+ /^dark:/, // CSS @media prefers-color-scheme
56
+ /^print:/, // CSS @media print
57
+ /^md:|^sm:|^lg:|^xl:|^2xl:/, // Responsive breakpoints — CSS only
58
+ ]
59
+
60
+ /** Patterns yang BENAR-BENAR butuh JS runtime */
61
+ const REQUIRES_JS_PATTERNS = [
62
+ // React hooks
63
+ /\buseState\b/,
64
+ /\buseEffect\b/,
65
+ /\buseRef\b/,
66
+ /\buseCallback\b/,
67
+ /\buseMemo\b/,
68
+ /\buseReducer\b/,
69
+ /\buseContext\b/,
70
+ // Event handlers
71
+ /\bon[A-Z][a-zA-Z]+\s*[=:]/, // onClick=, onMouseEnter:, etc.
72
+ // Browser APIs
73
+ /\bwindow\./,
74
+ /\bdocument\./,
75
+ /\blocalStorage\b/,
76
+ /\bsessionStorage\b/,
77
+ // Dynamic imports
78
+ /import\s*\(/,
79
+ ]
80
+
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Main analyzer
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+
85
+ export function analyzeFile(source: string, _filename = ""): RscAnalysis {
86
+ const clientReasons: string[] = []
87
+ const interactiveClasses: string[] = []
88
+
89
+ // 1. Explicit "use client" directive
90
+ const hasClientDirective =
91
+ source.trimStart().startsWith('"use client"') || source.trimStart().startsWith("'use client'")
92
+
93
+ if (hasClientDirective) {
94
+ clientReasons.push("explicit 'use client' directive")
95
+ }
96
+
97
+ // 2. React hooks → needs client
98
+ for (const pattern of REQUIRES_JS_PATTERNS) {
99
+ if (pattern.test(source)) {
100
+ const match = source.match(pattern)
101
+ if (match) clientReasons.push(`uses ${match[0].trim()}`)
102
+ }
103
+ }
104
+
105
+ // 3. Check for tw.server.* usage — force server
106
+ const hasServerMarker = source.includes("tw.server.")
107
+
108
+ // 4. Collect interactive classes from tw templates
109
+ const templateRe = /\btw\.(?:server\.)?(\w+)`((?:[^`\\]|\\.)*)`/g
110
+ const _objectRe = /\btw\.(?:server\.)?(\w+)\(\s*(\{[\s\S]*?\})\s*\)/g
111
+ let m: RegExpExecArray | null
112
+
113
+ while ((m = templateRe.exec(source)) !== null) {
114
+ const classes = m[2]
115
+ // CSS-only interaction is fine for server, skip
116
+ // But collect them for reference
117
+ const parts = classes.split(/\s+/).filter(Boolean)
118
+ for (const cls of parts) {
119
+ const isOk = CSS_INTERACTIVE_OK.some((re) => re.test(cls))
120
+ if (!isOk && /^[a-z-]+:/.test(cls)) {
121
+ interactiveClasses.push(cls)
122
+ clientReasons.push(`uses JS-interactive class: ${cls}`)
123
+ }
124
+ }
125
+ }
126
+
127
+ const needsClientDirective = !hasServerMarker && (hasClientDirective || clientReasons.length > 0)
128
+
129
+ const isServer = !needsClientDirective || hasServerMarker
130
+
131
+ return {
132
+ isServer,
133
+ needsClientDirective,
134
+ clientReasons: [...new Set(clientReasons)],
135
+ interactiveClasses: [...new Set(interactiveClasses)],
136
+ canStaticResolveVariants: isServer,
137
+ }
138
+ }
139
+
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ // Server-side static variant resolution
142
+ //
143
+ // Ketika sebuah file adalah server component dan variant value diketahui
144
+ // di compile time (literal string), compiler bisa langsung inline className.
145
+ // ─────────────────────────────────────────────────────────────────────────────
146
+
147
+ export interface StaticVariantUsage {
148
+ /** Variant prop values yang ditemukan di JSX — bisa di-resolve di server */
149
+ resolved: Record<string, string>
150
+ /** Variant props yang dinamis — butuh runtime */
151
+ dynamic: string[]
152
+ }
153
+
154
+ /**
155
+ * Deteksi penggunaan variant dalam JSX:
156
+ * <Button variant="primary"/> → dapat di-resolve statically
157
+ * <Button variant={userVariant}/> → dinamis, butuh runtime
158
+ */
159
+ export function analyzeVariantUsage(
160
+ source: string,
161
+ componentName: string,
162
+ variantKeys: string[]
163
+ ): StaticVariantUsage {
164
+ const resolved: Record<string, string> = {}
165
+ const dynamic: string[] = []
166
+
167
+ for (const key of variantKeys) {
168
+ // Static: variant="primary"
169
+ const staticRe = new RegExp(`<${componentName}[^>]*\\b${key}=["']([^"']+)["'][^>]*>`, "g")
170
+ // Dynamic: variant={someVar}
171
+ const dynamicRe = new RegExp(`<${componentName}[^>]*\\b${key}=\\{[^"'][^}]*\\}[^>]*>`, "g")
172
+
173
+ const staticMatch = source.match(staticRe)
174
+ const dynamicMatch = source.match(dynamicRe)
175
+
176
+ if (dynamicMatch) {
177
+ dynamic.push(key)
178
+ } else if (staticMatch) {
179
+ const valMatch = staticMatch[0].match(new RegExp(`${key}=["']([^"']+)["']`))
180
+ if (valMatch) resolved[key] = valMatch[1]
181
+ }
182
+ }
183
+
184
+ return { resolved, dynamic }
185
+ }
186
+
187
+ /**
188
+ * Untuk server component dengan variant usage statically known,
189
+ * resolve langsung ke className string — nol runtime.
190
+ *
191
+ * Input:
192
+ * base = "px-4 py-2"
193
+ * table = { variant: { primary: "px-4 py-2 bg-blue-500" } }
194
+ * resolved = { variant: "primary" }
195
+ *
196
+ * Output:
197
+ * "px-4 py-2 bg-blue-500" ← langsung inline di server
198
+ */
199
+ export function resolveServerVariant(
200
+ base: string,
201
+ table: Record<string, Record<string, string>>,
202
+ defaults: Record<string, string>,
203
+ resolved: Record<string, string>
204
+ ): string {
205
+ const parts: string[] = [base]
206
+
207
+ for (const key in table) {
208
+ const val = resolved[key] ?? defaults[key]
209
+ if (val && table[key][val]) {
210
+ parts.push(table[key][val])
211
+ }
212
+ }
213
+
214
+ // Dedupe dengan last-wins
215
+ const seen = new Map<string, string>()
216
+ for (const part of parts) {
217
+ for (const cls of part.split(/\s+/).filter(Boolean)) {
218
+ const prefix = cls.replace(/^(?:[\w-]+:)*/, "").split("-")[0]
219
+ seen.set(prefix, cls)
220
+ }
221
+ }
222
+
223
+ return Array.from(seen.values()).join(" ")
224
+ }
225
+
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+ // Client boundary auto-injection helpers
228
+ // ─────────────────────────────────────────────────────────────────────────────
229
+
230
+ export function injectClientDirective(code: string): string {
231
+ if (code.startsWith('"use client"') || code.startsWith("'use client'")) {
232
+ return code
233
+ }
234
+ return `"use client";\n${code}`
235
+ }
236
+
237
+ export function injectServerOnlyComment(code: string): string {
238
+ // Hint untuk bundler — RSC optimizer bisa tree-shake client deps
239
+ return `/* @tw-server-only */\n${code}`
240
+ }