tailwind-styled-v4 5.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 (177) 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.cts → animate.d.mts} +3 -30
  11. package/dist/animate.d.ts +3 -30
  12. package/dist/animate.js +149 -99
  13. package/dist/animate.js.map +1 -1
  14. package/dist/{animate.cjs → animate.mjs} +130 -119
  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.mts +1009 -0
  29. package/dist/compiler.d.ts +1009 -0
  30. package/dist/compiler.js +4518 -0
  31. package/dist/compiler.js.map +1 -0
  32. package/dist/compiler.mjs +4443 -0
  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 +170 -157
  41. package/dist/devtools.js.map +1 -1
  42. package/dist/{devtools.cjs → devtools.mjs} +165 -166
  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} +2 -2
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +945 -36
  53. package/dist/index.js.map +1 -1
  54. package/dist/{index.cjs → index.mjs} +899 -90
  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.cts → next.d.mts} +2 -1
  59. package/dist/next.d.ts +2 -1
  60. package/dist/next.js +6853 -35
  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.js +9 -4
  77. package/dist/preset.js.map +1 -1
  78. package/dist/{preset.cjs → preset.mjs} +5 -14
  79. package/dist/preset.mjs.map +1 -0
  80. package/dist/rspack.d.mts +33 -0
  81. package/dist/rspack.d.ts +33 -0
  82. package/dist/rspack.js +55 -0
  83. package/dist/rspack.js.map +1 -0
  84. package/dist/rspack.mjs +45 -0
  85. package/dist/rspack.mjs.map +1 -0
  86. package/dist/runtime.d.mts +62 -0
  87. package/dist/runtime.d.ts +62 -0
  88. package/dist/runtime.js +207 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/runtime.mjs +188 -0
  91. package/dist/runtime.mjs.map +1 -0
  92. package/dist/runtimeCss.d.mts +65 -0
  93. package/dist/runtimeCss.d.ts +65 -0
  94. package/dist/{css.cjs → runtimeCss.js} +71 -4
  95. package/dist/runtimeCss.js.map +1 -0
  96. package/dist/{css.js → runtimeCss.mjs} +66 -5
  97. package/dist/runtimeCss.mjs.map +1 -0
  98. package/dist/scanner.d.mts +25 -0
  99. package/dist/scanner.d.ts +25 -0
  100. package/dist/scanner.js +717 -0
  101. package/dist/scanner.js.map +1 -0
  102. package/dist/scanner.mjs +703 -0
  103. package/dist/scanner.mjs.map +1 -0
  104. package/dist/shared.d.mts +85 -0
  105. package/dist/shared.d.ts +85 -0
  106. package/dist/shared.js +255 -0
  107. package/dist/shared.js.map +1 -0
  108. package/dist/shared.mjs +233 -0
  109. package/dist/shared.mjs.map +1 -0
  110. package/dist/storybookAddon.d.mts +108 -0
  111. package/dist/storybookAddon.d.ts +108 -0
  112. package/dist/storybookAddon.js +95 -0
  113. package/dist/storybookAddon.js.map +1 -0
  114. package/dist/storybookAddon.mjs +88 -0
  115. package/dist/storybookAddon.mjs.map +1 -0
  116. package/dist/svelte.d.mts +114 -0
  117. package/dist/svelte.d.ts +114 -0
  118. package/dist/svelte.js +67 -0
  119. package/dist/svelte.js.map +1 -0
  120. package/dist/svelte.mjs +59 -0
  121. package/dist/svelte.mjs.map +1 -0
  122. package/dist/testing.d.mts +185 -0
  123. package/dist/testing.d.ts +185 -0
  124. package/dist/testing.js +173 -0
  125. package/dist/testing.js.map +1 -0
  126. package/dist/testing.mjs +158 -0
  127. package/dist/testing.mjs.map +1 -0
  128. package/dist/theme.d.mts +188 -0
  129. package/dist/theme.d.ts +188 -0
  130. package/dist/theme.js +334 -0
  131. package/dist/theme.js.map +1 -0
  132. package/dist/theme.mjs +311 -0
  133. package/dist/theme.mjs.map +1 -0
  134. package/dist/types-DXr2PmGP.d.mts +31 -0
  135. package/dist/types-DXr2PmGP.d.ts +31 -0
  136. package/dist/vite.js +4181 -16
  137. package/dist/vite.js.map +1 -1
  138. package/dist/vite.mjs +4281 -0
  139. package/dist/vite.mjs.map +1 -0
  140. package/dist/vue.d.mts +89 -0
  141. package/dist/vue.d.ts +89 -0
  142. package/dist/vue.js +104 -0
  143. package/dist/vue.js.map +1 -0
  144. package/dist/vue.mjs +96 -0
  145. package/dist/vue.mjs.map +1 -0
  146. package/package.json +168 -65
  147. package/dist/animate.cjs.map +0 -1
  148. package/dist/chunk-VZEJV27B.js +0 -11
  149. package/dist/chunk-VZEJV27B.js.map +0 -1
  150. package/dist/chunk-Y5D3E72P.cjs +0 -13
  151. package/dist/chunk-Y5D3E72P.cjs.map +0 -1
  152. package/dist/css.cjs.map +0 -1
  153. package/dist/css.d.cts +0 -30
  154. package/dist/css.d.ts +0 -30
  155. package/dist/css.js.map +0 -1
  156. package/dist/devtools.cjs.map +0 -1
  157. package/dist/index.cjs.map +0 -1
  158. package/dist/next.cjs +0 -248
  159. package/dist/next.cjs.map +0 -1
  160. package/dist/preset.cjs.map +0 -1
  161. package/dist/turbopackLoader.cjs +0 -37
  162. package/dist/turbopackLoader.cjs.map +0 -1
  163. package/dist/turbopackLoader.d.cts +0 -12
  164. package/dist/turbopackLoader.d.ts +0 -12
  165. package/dist/turbopackLoader.js +0 -35
  166. package/dist/turbopackLoader.js.map +0 -1
  167. package/dist/vite.cjs +0 -138
  168. package/dist/vite.cjs.map +0 -1
  169. package/dist/webpackLoader.cjs +0 -51
  170. package/dist/webpackLoader.cjs.map +0 -1
  171. package/dist/webpackLoader.d.cts +0 -17
  172. package/dist/webpackLoader.d.ts +0 -17
  173. package/dist/webpackLoader.js +0 -49
  174. package/dist/webpackLoader.js.map +0 -1
  175. /package/dist/{devtools.d.cts → devtools.d.mts} +0 -0
  176. /package/dist/{preset.d.cts → preset.d.mts} +0 -0
  177. /package/dist/{vite.d.cts → vite.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 };