tailwind-styled-v4 4.0.0 → 5.0.1

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.
Files changed (194) hide show
  1. package/CHANGELOG.md +398 -0
  2. package/LICENSE +21 -0
  3. package/README.md +532 -0
  4. package/dist/analyzer.d.mts +114 -0
  5. package/dist/analyzer.d.ts +114 -0
  6. package/dist/analyzer.js +1555 -0
  7. package/dist/analyzer.js.map +1 -0
  8. package/dist/analyzer.mjs +1544 -0
  9. package/dist/analyzer.mjs.map +1 -0
  10. package/dist/animate.d.mts +46 -0
  11. package/dist/animate.d.ts +41 -112
  12. package/dist/animate.js +792 -235
  13. package/dist/animate.js.map +1 -1
  14. package/dist/animate.mjs +782 -0
  15. package/dist/animate.mjs.map +1 -0
  16. package/dist/atomic.d.mts +18 -0
  17. package/dist/atomic.d.ts +18 -0
  18. package/dist/atomic.js +191 -0
  19. package/dist/atomic.js.map +1 -0
  20. package/dist/atomic.mjs +185 -0
  21. package/dist/atomic.mjs.map +1 -0
  22. package/dist/cli.d.mts +1 -0
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +6063 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/cli.mjs +6053 -0
  27. package/dist/cli.mjs.map +1 -0
  28. package/dist/{compiler.d.cts → compiler.d.mts} +503 -210
  29. package/dist/compiler.d.ts +503 -210
  30. package/dist/compiler.js +1549 -566
  31. package/dist/compiler.js.map +1 -1
  32. package/dist/{compiler.cjs → compiler.mjs} +1476 -627
  33. package/dist/compiler.mjs.map +1 -0
  34. package/dist/dashboard.d.mts +272 -0
  35. package/dist/dashboard.d.ts +272 -0
  36. package/dist/dashboard.js +249 -0
  37. package/dist/dashboard.js.map +1 -0
  38. package/dist/dashboard.mjs +239 -0
  39. package/dist/dashboard.mjs.map +1 -0
  40. package/dist/devtools.js +336 -211
  41. package/dist/devtools.js.map +1 -1
  42. package/dist/{devtools.cjs → devtools.mjs} +331 -220
  43. package/dist/devtools.mjs.map +1 -0
  44. package/dist/engine.d.mts +84 -0
  45. package/dist/engine.d.ts +84 -0
  46. package/dist/engine.js +3014 -0
  47. package/dist/engine.js.map +1 -0
  48. package/dist/engine.mjs +3005 -0
  49. package/dist/engine.mjs.map +1 -0
  50. package/dist/{index.d.cts → index.d.mts} +75 -4
  51. package/dist/index.d.ts +75 -4
  52. package/dist/index.js +1341 -149
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.mjs +2162 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/liveTokenEngine-DYN3Zale.d.mts +34 -0
  57. package/dist/liveTokenEngine-DYN3Zale.d.ts +34 -0
  58. package/dist/next.d.mts +55 -0
  59. package/dist/next.d.ts +30 -20
  60. package/dist/next.js +6947 -149
  61. package/dist/next.js.map +1 -1
  62. package/dist/next.mjs +7050 -0
  63. package/dist/next.mjs.map +1 -0
  64. package/dist/plugin.d.mts +90 -0
  65. package/dist/plugin.d.ts +90 -0
  66. package/dist/plugin.js +185 -0
  67. package/dist/plugin.js.map +1 -0
  68. package/dist/plugin.mjs +174 -0
  69. package/dist/plugin.mjs.map +1 -0
  70. package/dist/pluginRegistry.d.mts +83 -0
  71. package/dist/pluginRegistry.d.ts +83 -0
  72. package/dist/pluginRegistry.js +303 -0
  73. package/dist/pluginRegistry.js.map +1 -0
  74. package/dist/pluginRegistry.mjs +298 -0
  75. package/dist/pluginRegistry.mjs.map +1 -0
  76. package/dist/{preset.d.cts → preset.d.mts} +29 -2
  77. package/dist/preset.d.ts +29 -2
  78. package/dist/preset.js +318 -21
  79. package/dist/preset.js.map +1 -1
  80. package/dist/preset.mjs +414 -0
  81. package/dist/preset.mjs.map +1 -0
  82. package/dist/rspack.d.mts +33 -0
  83. package/dist/rspack.d.ts +33 -0
  84. package/dist/rspack.js +55 -0
  85. package/dist/rspack.js.map +1 -0
  86. package/dist/rspack.mjs +45 -0
  87. package/dist/rspack.mjs.map +1 -0
  88. package/dist/runtime.d.mts +62 -0
  89. package/dist/runtime.d.ts +62 -0
  90. package/dist/runtime.js +207 -0
  91. package/dist/runtime.js.map +1 -0
  92. package/dist/runtime.mjs +188 -0
  93. package/dist/runtime.mjs.map +1 -0
  94. package/dist/runtimeCss.d.mts +65 -0
  95. package/dist/runtimeCss.d.ts +65 -0
  96. package/dist/runtimeCss.js +188 -0
  97. package/dist/runtimeCss.js.map +1 -0
  98. package/dist/runtimeCss.mjs +173 -0
  99. package/dist/runtimeCss.mjs.map +1 -0
  100. package/dist/scanner.d.mts +25 -0
  101. package/dist/scanner.d.ts +25 -0
  102. package/dist/scanner.js +717 -0
  103. package/dist/scanner.js.map +1 -0
  104. package/dist/scanner.mjs +703 -0
  105. package/dist/scanner.mjs.map +1 -0
  106. package/dist/shared.d.mts +85 -0
  107. package/dist/shared.d.ts +85 -0
  108. package/dist/shared.js +255 -0
  109. package/dist/shared.js.map +1 -0
  110. package/dist/shared.mjs +233 -0
  111. package/dist/shared.mjs.map +1 -0
  112. package/dist/storybookAddon.d.mts +108 -0
  113. package/dist/storybookAddon.d.ts +108 -0
  114. package/dist/storybookAddon.js +95 -0
  115. package/dist/storybookAddon.js.map +1 -0
  116. package/dist/storybookAddon.mjs +88 -0
  117. package/dist/storybookAddon.mjs.map +1 -0
  118. package/dist/svelte.d.mts +114 -0
  119. package/dist/svelte.d.ts +114 -0
  120. package/dist/svelte.js +67 -0
  121. package/dist/svelte.js.map +1 -0
  122. package/dist/svelte.mjs +59 -0
  123. package/dist/svelte.mjs.map +1 -0
  124. package/dist/testing.d.mts +185 -0
  125. package/dist/testing.d.ts +185 -0
  126. package/dist/testing.js +173 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/testing.mjs +158 -0
  129. package/dist/testing.mjs.map +1 -0
  130. package/dist/{theme.d.cts → theme.d.mts} +18 -11
  131. package/dist/theme.d.ts +18 -11
  132. package/dist/theme.js +205 -19
  133. package/dist/theme.js.map +1 -1
  134. package/dist/theme.mjs +311 -0
  135. package/dist/theme.mjs.map +1 -0
  136. package/dist/types-DXr2PmGP.d.mts +31 -0
  137. package/dist/types-DXr2PmGP.d.ts +31 -0
  138. package/dist/vite.d.mts +51 -0
  139. package/dist/vite.d.ts +35 -6
  140. package/dist/vite.js +4254 -57
  141. package/dist/vite.js.map +1 -1
  142. package/dist/vite.mjs +4281 -0
  143. package/dist/vite.mjs.map +1 -0
  144. package/dist/vue.d.mts +89 -0
  145. package/dist/vue.d.ts +89 -0
  146. package/dist/vue.js +104 -0
  147. package/dist/vue.js.map +1 -0
  148. package/dist/vue.mjs +96 -0
  149. package/dist/vue.mjs.map +1 -0
  150. package/package.json +173 -67
  151. package/dist/animate.cjs +0 -252
  152. package/dist/animate.cjs.map +0 -1
  153. package/dist/animate.d.cts +0 -117
  154. package/dist/astTransform-ua-eapqs.d.cts +0 -41
  155. package/dist/astTransform-ua-eapqs.d.ts +0 -41
  156. package/dist/compiler.cjs.map +0 -1
  157. package/dist/css.cjs +0 -71
  158. package/dist/css.cjs.map +0 -1
  159. package/dist/css.d.cts +0 -45
  160. package/dist/css.d.ts +0 -45
  161. package/dist/css.js +0 -62
  162. package/dist/css.js.map +0 -1
  163. package/dist/devtools.cjs.map +0 -1
  164. package/dist/index.cjs +0 -1058
  165. package/dist/index.cjs.map +0 -1
  166. package/dist/next.cjs +0 -268
  167. package/dist/next.cjs.map +0 -1
  168. package/dist/next.d.cts +0 -45
  169. package/dist/plugins.cjs +0 -396
  170. package/dist/plugins.cjs.map +0 -1
  171. package/dist/plugins.d.cts +0 -231
  172. package/dist/plugins.d.ts +0 -231
  173. package/dist/plugins.js +0 -381
  174. package/dist/plugins.js.map +0 -1
  175. package/dist/preset.cjs +0 -129
  176. package/dist/preset.cjs.map +0 -1
  177. package/dist/theme.cjs +0 -154
  178. package/dist/theme.cjs.map +0 -1
  179. package/dist/turbopackLoader.cjs +0 -2689
  180. package/dist/turbopackLoader.cjs.map +0 -1
  181. package/dist/turbopackLoader.d.cts +0 -22
  182. package/dist/turbopackLoader.d.ts +0 -22
  183. package/dist/turbopackLoader.js +0 -2681
  184. package/dist/turbopackLoader.js.map +0 -1
  185. package/dist/vite.cjs +0 -105
  186. package/dist/vite.cjs.map +0 -1
  187. package/dist/vite.d.cts +0 -22
  188. package/dist/webpackLoader.cjs +0 -2670
  189. package/dist/webpackLoader.cjs.map +0 -1
  190. package/dist/webpackLoader.d.cts +0 -24
  191. package/dist/webpackLoader.d.ts +0 -24
  192. package/dist/webpackLoader.js +0 -2662
  193. package/dist/webpackLoader.js.map +0 -1
  194. /package/dist/{devtools.d.cts → devtools.d.mts} +0 -0
@@ -0,0 +1,272 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { EventEmitter } from 'node:events';
5
+
6
+ /**
7
+ * tailwind-styled-v4 Dashboard Server
8
+ *
9
+ * HTTP server yang expose LIVE build metrics dari engine.
10
+ * Bukan lagi hardcoded — connect ke engine via file-based IPC atau
11
+ * event emitter jika dijalankan dalam proses yang sama.
12
+ *
13
+ * Port default: 3000 (override via PORT env var)
14
+ *
15
+ * Endpoints:
16
+ * GET / → HTML dashboard UI
17
+ * GET /metrics → JSON metrics snapshot (live)
18
+ * GET /history → JSON array dari metrics snapshots (max 100)
19
+ * POST /reset → Reset history
20
+ * GET /health → { ok: true }
21
+ */
22
+
23
+
24
+ const port = Number(process.env.PORT ?? 3000);
25
+ const METRICS_FILE = path.join(process.cwd(), '.tw-cache', 'metrics.json');
26
+ const MAX_HISTORY = 100;
27
+
28
+ // ─── In-memory metrics store ────────────────────────────────────────────────
29
+ let currentMetrics = {
30
+ generatedAt: new Date().toISOString(),
31
+ buildMs: null,
32
+ scanMs: null,
33
+ memoryMb: null,
34
+ classCount: null,
35
+ fileCount: null,
36
+ cssBytes: null,
37
+ mode: 'idle',
38
+ };
39
+
40
+ const history = [];
41
+ const events = new EventEmitter();
42
+
43
+ /**
44
+ * Update metrics — dipanggil dari engine atau via file watch
45
+ */
46
+ function updateMetrics(data) {
47
+ currentMetrics = { ...currentMetrics, ...data, generatedAt: new Date().toISOString() };
48
+ history.push({ ...currentMetrics });
49
+ if (history.length > MAX_HISTORY) history.shift();
50
+ events.emit('update', currentMetrics);
51
+ }
52
+
53
+ // ─── Watch metrics file untuk IPC dari engine ───────────────────────────────
54
+ function watchMetricsFile() {
55
+ const dir = path.dirname(METRICS_FILE);
56
+ if (!fs.existsSync(dir)) {
57
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
58
+ }
59
+
60
+ if (fs.existsSync(METRICS_FILE)) {
61
+ try {
62
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
63
+ updateMetrics(data);
64
+ } catch {}
65
+ }
66
+
67
+ try {
68
+ fs.watch(METRICS_FILE, { persistent: false }, (eventType) => {
69
+ if (eventType === 'change') {
70
+ try {
71
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
72
+ updateMetrics(data);
73
+ } catch {}
74
+ }
75
+ });
76
+ } catch {
77
+ // File doesn't exist yet — poll instead
78
+ setInterval(() => {
79
+ if (fs.existsSync(METRICS_FILE)) {
80
+ try {
81
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
82
+ if (data.generatedAt !== currentMetrics.generatedAt) updateMetrics(data);
83
+ } catch {}
84
+ }
85
+ }, 2000);
86
+ }
87
+ }
88
+
89
+ // ─── HTML Dashboard UI ──────────────────────────────────────────────────────
90
+ const dashboardHtml = `<!doctype html>
91
+ <html lang="en">
92
+ <head>
93
+ <meta charset="utf-8"/>
94
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
95
+ <title>tailwind-styled dashboard</title>
96
+ <style>
97
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
98
+ :root {
99
+ --bg: #0f1117; --surface: #1a1d2e; --border: #2a2d3e;
100
+ --text: #e2e8f0; --muted: #8892a4; --accent: #38bdf8;
101
+ --green: #4ade80; --amber: #fbbf24; --red: #f87171;
102
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
103
+ }
104
+ body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 2rem }
105
+ h1 { font-size: 1.1rem; color: var(--muted); font-weight: 400; letter-spacing: 0.1em;
106
+ text-transform: uppercase; margin-bottom: 2rem; border-bottom: 1px solid var(--border);
107
+ padding-bottom: 1rem }
108
+ h1 span { color: var(--accent) }
109
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem }
110
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
111
+ padding: 1.25rem; transition: border-color .2s }
112
+ .card:hover { border-color: var(--accent) }
113
+ .card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .1em;
114
+ color: var(--muted); margin-bottom: .5rem }
115
+ .card .value { font-size: 1.8rem; font-weight: 700; color: var(--text) }
116
+ .card .value.good { color: var(--green) }
117
+ .card .value.warn { color: var(--amber) }
118
+ .card .value.bad { color: var(--red) }
119
+ .card .unit { font-size: .75rem; color: var(--muted); margin-left: .25rem }
120
+ .section-title { font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;
121
+ color: var(--muted); margin: 2rem 0 .75rem }
122
+ .raw { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
123
+ padding: 1rem; font-size: .8rem; overflow-x: auto; white-space: pre; max-height: 300px;
124
+ overflow-y: auto }
125
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green);
126
+ display: inline-block; margin-right: .5rem; animation: pulse 2s infinite }
127
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
128
+ .idle .status-dot { background: var(--muted); animation: none }
129
+ .building .status-dot { background: var(--amber) }
130
+ .error .status-dot { background: var(--red); animation: none }
131
+ #status { font-size: .8rem; color: var(--muted); margin-bottom: 2rem }
132
+ #last-update { font-size: .75rem; color: var(--muted); margin-top: 2rem }
133
+ .history-mini { display: flex; align-items: flex-end; gap: 2px; height: 40px; margin-top: .5rem }
134
+ .history-mini .bar { flex: 1; background: var(--accent); opacity: .4; border-radius: 2px 2px 0 0;
135
+ min-width: 4px; transition: opacity .2s }
136
+ .history-mini .bar:hover { opacity: 1 }
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <h1><span class="status-dot" id="dot"></span>tailwind-styled <span>dashboard</span></h1>
141
+ <div id="status">Connecting...</div>
142
+
143
+ <div class="grid" id="cards">
144
+ <div class="card"><div class="label">Build time</div><div class="value" id="buildMs">—</div></div>
145
+ <div class="card"><div class="label">Scan time</div><div class="value" id="scanMs">—</div></div>
146
+ <div class="card"><div class="label">Classes</div><div class="value" id="classCount">—</div></div>
147
+ <div class="card"><div class="label">Files</div><div class="value" id="fileCount">—</div></div>
148
+ <div class="card"><div class="label">CSS output</div><div class="value" id="cssBytes">—</div></div>
149
+ <div class="card"><div class="label">Memory (heap)</div><div class="value" id="memoryMb">—</div></div>
150
+ </div>
151
+
152
+ <div class="section-title">Build time history</div>
153
+ <div class="card">
154
+ <div class="history-mini" id="history-chart"></div>
155
+ </div>
156
+
157
+ <div class="section-title">Raw metrics</div>
158
+ <div class="raw" id="raw-json">Loading...</div>
159
+ <div id="last-update"></div>
160
+
161
+ <script>
162
+ let prevGenAt = null
163
+
164
+ function fmt(v, unit, warn, bad) {
165
+ if (v == null) return '—'
166
+ const el = document.createElement('span')
167
+ el.textContent = typeof v === 'number' ? v.toFixed(unit === 'ms' ? 0 : 1) : v
168
+ if (unit) el.insertAdjacentHTML('beforeend', '<span class="unit">' + unit + '</span>')
169
+ if (bad != null && v > bad) el.className = 'bad'
170
+ else if (warn != null && v > warn) el.className = 'warn'
171
+ else if (v != null) el.className = 'good'
172
+ return el.outerHTML
173
+ }
174
+
175
+ function fmtBytes(b) {
176
+ if (b == null) return '—'
177
+ if (b < 1024) return b + '<span class="unit">B</span>'
178
+ if (b < 1024*1024) return (b/1024).toFixed(1) + '<span class="unit">KB</span>'
179
+ return (b/1024/1024).toFixed(2) + '<span class="unit">MB</span>'
180
+ }
181
+
182
+ function renderHistory(history) {
183
+ const chart = document.getElementById('history-chart')
184
+ if (!history.length) return
185
+ const vals = history.map(h => h.buildMs ?? 0).filter(v => v > 0)
186
+ if (!vals.length) return
187
+ const max = Math.max(...vals)
188
+ chart.innerHTML = vals.map(v => {
189
+ const h = max > 0 ? Math.max(4, Math.round((v / max) * 40)) : 4
190
+ return '<div class="bar" style="height:' + h + 'px" title="' + v + 'ms"></div>'
191
+ }).join('')
192
+ }
193
+
194
+ async function fetchAndRender() {
195
+ try {
196
+ const [mRes, hRes] = await Promise.all([fetch('/metrics'), fetch('/history')])
197
+ const m = await mRes.json()
198
+ const h = await hRes.json()
199
+
200
+ if (m.generatedAt === prevGenAt) return
201
+ prevGenAt = m.generatedAt
202
+
203
+ document.getElementById('buildMs').innerHTML = fmt(m.buildMs, 'ms', 500, 2000)
204
+ document.getElementById('scanMs').innerHTML = fmt(m.scanMs, 'ms', 200, 1000)
205
+ document.getElementById('classCount').innerHTML = fmt(m.classCount, null, null, null)
206
+ document.getElementById('fileCount').innerHTML = fmt(m.fileCount, null, null, null)
207
+ document.getElementById('cssBytes').innerHTML = fmtBytes(m.cssBytes)
208
+ document.getElementById('memoryMb').innerHTML = fmt(m.memoryMb?.heapUsed, 'MB', 100, 500)
209
+ document.getElementById('raw-json').textContent = JSON.stringify(m, null, 2)
210
+ document.getElementById('last-update').textContent = 'Last update: ' + new Date(m.generatedAt).toLocaleTimeString()
211
+ document.getElementById('status').textContent = 'Mode: ' + (m.mode ?? 'idle')
212
+
213
+ const dot = document.getElementById('dot')
214
+ dot.parentElement.className = m.mode ?? 'idle'
215
+
216
+ renderHistory(h)
217
+ } catch (e) {
218
+ document.getElementById('status').textContent = 'Error: ' + e.message
219
+ }
220
+ }
221
+
222
+ fetchAndRender()
223
+ setInterval(fetchAndRender, 1500)
224
+ </script>
225
+ </body>
226
+ </html>`;
227
+
228
+ // ─── HTTP server ────────────────────────────────────────────────────────────
229
+ const server = http.createServer((req, res) => {
230
+ const url = new URL(req.url, `http://localhost:${port}`);
231
+
232
+ if (url.pathname === '/health') {
233
+ res.setHeader('content-type', 'application/json');
234
+ res.end(JSON.stringify({ ok: true }));
235
+ return
236
+ }
237
+
238
+ if (url.pathname === '/metrics') {
239
+ res.setHeader('content-type', 'application/json');
240
+ res.setHeader('cache-control', 'no-cache');
241
+ res.end(JSON.stringify(currentMetrics, null, 2));
242
+ return
243
+ }
244
+
245
+ if (url.pathname === '/history') {
246
+ res.setHeader('content-type', 'application/json');
247
+ res.end(JSON.stringify(history));
248
+ return
249
+ }
250
+
251
+ if (url.pathname === '/reset' && req.method === 'POST') {
252
+ history.length = 0;
253
+ res.setHeader('content-type', 'application/json');
254
+ res.end(JSON.stringify({ ok: true, message: 'History cleared' }));
255
+ return
256
+ }
257
+
258
+ // Default: serve dashboard HTML
259
+ res.setHeader('content-type', 'text/html; charset=utf-8');
260
+ res.end(dashboardHtml);
261
+ });
262
+
263
+ // ─── Start ──────────────────────────────────────────────────────────────────
264
+ watchMetricsFile();
265
+
266
+ server.listen(port, () => {
267
+ console.log(`[tailwind-styled] Dashboard: http://localhost:${port}`);
268
+ console.log(`[tailwind-styled] Metrics: http://localhost:${port}/metrics`);
269
+ console.log(`[tailwind-styled] Watching: ${METRICS_FILE}`);
270
+ });
271
+
272
+ export { currentMetrics, events, history, updateMetrics };
@@ -0,0 +1,272 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { EventEmitter } from 'node:events';
5
+
6
+ /**
7
+ * tailwind-styled-v4 Dashboard Server
8
+ *
9
+ * HTTP server yang expose LIVE build metrics dari engine.
10
+ * Bukan lagi hardcoded — connect ke engine via file-based IPC atau
11
+ * event emitter jika dijalankan dalam proses yang sama.
12
+ *
13
+ * Port default: 3000 (override via PORT env var)
14
+ *
15
+ * Endpoints:
16
+ * GET / → HTML dashboard UI
17
+ * GET /metrics → JSON metrics snapshot (live)
18
+ * GET /history → JSON array dari metrics snapshots (max 100)
19
+ * POST /reset → Reset history
20
+ * GET /health → { ok: true }
21
+ */
22
+
23
+
24
+ const port = Number(process.env.PORT ?? 3000);
25
+ const METRICS_FILE = path.join(process.cwd(), '.tw-cache', 'metrics.json');
26
+ const MAX_HISTORY = 100;
27
+
28
+ // ─── In-memory metrics store ────────────────────────────────────────────────
29
+ let currentMetrics = {
30
+ generatedAt: new Date().toISOString(),
31
+ buildMs: null,
32
+ scanMs: null,
33
+ memoryMb: null,
34
+ classCount: null,
35
+ fileCount: null,
36
+ cssBytes: null,
37
+ mode: 'idle',
38
+ };
39
+
40
+ const history = [];
41
+ const events = new EventEmitter();
42
+
43
+ /**
44
+ * Update metrics — dipanggil dari engine atau via file watch
45
+ */
46
+ function updateMetrics(data) {
47
+ currentMetrics = { ...currentMetrics, ...data, generatedAt: new Date().toISOString() };
48
+ history.push({ ...currentMetrics });
49
+ if (history.length > MAX_HISTORY) history.shift();
50
+ events.emit('update', currentMetrics);
51
+ }
52
+
53
+ // ─── Watch metrics file untuk IPC dari engine ───────────────────────────────
54
+ function watchMetricsFile() {
55
+ const dir = path.dirname(METRICS_FILE);
56
+ if (!fs.existsSync(dir)) {
57
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
58
+ }
59
+
60
+ if (fs.existsSync(METRICS_FILE)) {
61
+ try {
62
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
63
+ updateMetrics(data);
64
+ } catch {}
65
+ }
66
+
67
+ try {
68
+ fs.watch(METRICS_FILE, { persistent: false }, (eventType) => {
69
+ if (eventType === 'change') {
70
+ try {
71
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
72
+ updateMetrics(data);
73
+ } catch {}
74
+ }
75
+ });
76
+ } catch {
77
+ // File doesn't exist yet — poll instead
78
+ setInterval(() => {
79
+ if (fs.existsSync(METRICS_FILE)) {
80
+ try {
81
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
82
+ if (data.generatedAt !== currentMetrics.generatedAt) updateMetrics(data);
83
+ } catch {}
84
+ }
85
+ }, 2000);
86
+ }
87
+ }
88
+
89
+ // ─── HTML Dashboard UI ──────────────────────────────────────────────────────
90
+ const dashboardHtml = `<!doctype html>
91
+ <html lang="en">
92
+ <head>
93
+ <meta charset="utf-8"/>
94
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
95
+ <title>tailwind-styled dashboard</title>
96
+ <style>
97
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
98
+ :root {
99
+ --bg: #0f1117; --surface: #1a1d2e; --border: #2a2d3e;
100
+ --text: #e2e8f0; --muted: #8892a4; --accent: #38bdf8;
101
+ --green: #4ade80; --amber: #fbbf24; --red: #f87171;
102
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
103
+ }
104
+ body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 2rem }
105
+ h1 { font-size: 1.1rem; color: var(--muted); font-weight: 400; letter-spacing: 0.1em;
106
+ text-transform: uppercase; margin-bottom: 2rem; border-bottom: 1px solid var(--border);
107
+ padding-bottom: 1rem }
108
+ h1 span { color: var(--accent) }
109
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem }
110
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
111
+ padding: 1.25rem; transition: border-color .2s }
112
+ .card:hover { border-color: var(--accent) }
113
+ .card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .1em;
114
+ color: var(--muted); margin-bottom: .5rem }
115
+ .card .value { font-size: 1.8rem; font-weight: 700; color: var(--text) }
116
+ .card .value.good { color: var(--green) }
117
+ .card .value.warn { color: var(--amber) }
118
+ .card .value.bad { color: var(--red) }
119
+ .card .unit { font-size: .75rem; color: var(--muted); margin-left: .25rem }
120
+ .section-title { font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;
121
+ color: var(--muted); margin: 2rem 0 .75rem }
122
+ .raw { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
123
+ padding: 1rem; font-size: .8rem; overflow-x: auto; white-space: pre; max-height: 300px;
124
+ overflow-y: auto }
125
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green);
126
+ display: inline-block; margin-right: .5rem; animation: pulse 2s infinite }
127
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
128
+ .idle .status-dot { background: var(--muted); animation: none }
129
+ .building .status-dot { background: var(--amber) }
130
+ .error .status-dot { background: var(--red); animation: none }
131
+ #status { font-size: .8rem; color: var(--muted); margin-bottom: 2rem }
132
+ #last-update { font-size: .75rem; color: var(--muted); margin-top: 2rem }
133
+ .history-mini { display: flex; align-items: flex-end; gap: 2px; height: 40px; margin-top: .5rem }
134
+ .history-mini .bar { flex: 1; background: var(--accent); opacity: .4; border-radius: 2px 2px 0 0;
135
+ min-width: 4px; transition: opacity .2s }
136
+ .history-mini .bar:hover { opacity: 1 }
137
+ </style>
138
+ </head>
139
+ <body>
140
+ <h1><span class="status-dot" id="dot"></span>tailwind-styled <span>dashboard</span></h1>
141
+ <div id="status">Connecting...</div>
142
+
143
+ <div class="grid" id="cards">
144
+ <div class="card"><div class="label">Build time</div><div class="value" id="buildMs">—</div></div>
145
+ <div class="card"><div class="label">Scan time</div><div class="value" id="scanMs">—</div></div>
146
+ <div class="card"><div class="label">Classes</div><div class="value" id="classCount">—</div></div>
147
+ <div class="card"><div class="label">Files</div><div class="value" id="fileCount">—</div></div>
148
+ <div class="card"><div class="label">CSS output</div><div class="value" id="cssBytes">—</div></div>
149
+ <div class="card"><div class="label">Memory (heap)</div><div class="value" id="memoryMb">—</div></div>
150
+ </div>
151
+
152
+ <div class="section-title">Build time history</div>
153
+ <div class="card">
154
+ <div class="history-mini" id="history-chart"></div>
155
+ </div>
156
+
157
+ <div class="section-title">Raw metrics</div>
158
+ <div class="raw" id="raw-json">Loading...</div>
159
+ <div id="last-update"></div>
160
+
161
+ <script>
162
+ let prevGenAt = null
163
+
164
+ function fmt(v, unit, warn, bad) {
165
+ if (v == null) return '—'
166
+ const el = document.createElement('span')
167
+ el.textContent = typeof v === 'number' ? v.toFixed(unit === 'ms' ? 0 : 1) : v
168
+ if (unit) el.insertAdjacentHTML('beforeend', '<span class="unit">' + unit + '</span>')
169
+ if (bad != null && v > bad) el.className = 'bad'
170
+ else if (warn != null && v > warn) el.className = 'warn'
171
+ else if (v != null) el.className = 'good'
172
+ return el.outerHTML
173
+ }
174
+
175
+ function fmtBytes(b) {
176
+ if (b == null) return '—'
177
+ if (b < 1024) return b + '<span class="unit">B</span>'
178
+ if (b < 1024*1024) return (b/1024).toFixed(1) + '<span class="unit">KB</span>'
179
+ return (b/1024/1024).toFixed(2) + '<span class="unit">MB</span>'
180
+ }
181
+
182
+ function renderHistory(history) {
183
+ const chart = document.getElementById('history-chart')
184
+ if (!history.length) return
185
+ const vals = history.map(h => h.buildMs ?? 0).filter(v => v > 0)
186
+ if (!vals.length) return
187
+ const max = Math.max(...vals)
188
+ chart.innerHTML = vals.map(v => {
189
+ const h = max > 0 ? Math.max(4, Math.round((v / max) * 40)) : 4
190
+ return '<div class="bar" style="height:' + h + 'px" title="' + v + 'ms"></div>'
191
+ }).join('')
192
+ }
193
+
194
+ async function fetchAndRender() {
195
+ try {
196
+ const [mRes, hRes] = await Promise.all([fetch('/metrics'), fetch('/history')])
197
+ const m = await mRes.json()
198
+ const h = await hRes.json()
199
+
200
+ if (m.generatedAt === prevGenAt) return
201
+ prevGenAt = m.generatedAt
202
+
203
+ document.getElementById('buildMs').innerHTML = fmt(m.buildMs, 'ms', 500, 2000)
204
+ document.getElementById('scanMs').innerHTML = fmt(m.scanMs, 'ms', 200, 1000)
205
+ document.getElementById('classCount').innerHTML = fmt(m.classCount, null, null, null)
206
+ document.getElementById('fileCount').innerHTML = fmt(m.fileCount, null, null, null)
207
+ document.getElementById('cssBytes').innerHTML = fmtBytes(m.cssBytes)
208
+ document.getElementById('memoryMb').innerHTML = fmt(m.memoryMb?.heapUsed, 'MB', 100, 500)
209
+ document.getElementById('raw-json').textContent = JSON.stringify(m, null, 2)
210
+ document.getElementById('last-update').textContent = 'Last update: ' + new Date(m.generatedAt).toLocaleTimeString()
211
+ document.getElementById('status').textContent = 'Mode: ' + (m.mode ?? 'idle')
212
+
213
+ const dot = document.getElementById('dot')
214
+ dot.parentElement.className = m.mode ?? 'idle'
215
+
216
+ renderHistory(h)
217
+ } catch (e) {
218
+ document.getElementById('status').textContent = 'Error: ' + e.message
219
+ }
220
+ }
221
+
222
+ fetchAndRender()
223
+ setInterval(fetchAndRender, 1500)
224
+ </script>
225
+ </body>
226
+ </html>`;
227
+
228
+ // ─── HTTP server ────────────────────────────────────────────────────────────
229
+ const server = http.createServer((req, res) => {
230
+ const url = new URL(req.url, `http://localhost:${port}`);
231
+
232
+ if (url.pathname === '/health') {
233
+ res.setHeader('content-type', 'application/json');
234
+ res.end(JSON.stringify({ ok: true }));
235
+ return
236
+ }
237
+
238
+ if (url.pathname === '/metrics') {
239
+ res.setHeader('content-type', 'application/json');
240
+ res.setHeader('cache-control', 'no-cache');
241
+ res.end(JSON.stringify(currentMetrics, null, 2));
242
+ return
243
+ }
244
+
245
+ if (url.pathname === '/history') {
246
+ res.setHeader('content-type', 'application/json');
247
+ res.end(JSON.stringify(history));
248
+ return
249
+ }
250
+
251
+ if (url.pathname === '/reset' && req.method === 'POST') {
252
+ history.length = 0;
253
+ res.setHeader('content-type', 'application/json');
254
+ res.end(JSON.stringify({ ok: true, message: 'History cleared' }));
255
+ return
256
+ }
257
+
258
+ // Default: serve dashboard HTML
259
+ res.setHeader('content-type', 'text/html; charset=utf-8');
260
+ res.end(dashboardHtml);
261
+ });
262
+
263
+ // ─── Start ──────────────────────────────────────────────────────────────────
264
+ watchMetricsFile();
265
+
266
+ server.listen(port, () => {
267
+ console.log(`[tailwind-styled] Dashboard: http://localhost:${port}`);
268
+ console.log(`[tailwind-styled] Metrics: http://localhost:${port}/metrics`);
269
+ console.log(`[tailwind-styled] Watching: ${METRICS_FILE}`);
270
+ });
271
+
272
+ export { currentMetrics, events, history, updateMetrics };