tailwind-styled-v4 5.0.0 → 5.0.2
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/CHANGELOG.md +398 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/dist/analyzer.d.mts +114 -0
- package/dist/analyzer.d.ts +114 -0
- package/dist/analyzer.js +6808 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/analyzer.mjs +6798 -0
- package/dist/analyzer.mjs.map +1 -0
- package/dist/{animate.d.cts → animate.d.mts} +3 -30
- package/dist/animate.d.ts +3 -30
- package/dist/animate.js +7096 -352
- package/dist/animate.js.map +1 -1
- package/dist/animate.mjs +7482 -0
- package/dist/animate.mjs.map +1 -0
- package/dist/atomic.d.mts +18 -0
- package/dist/atomic.d.ts +18 -0
- package/dist/atomic.js +191 -0
- package/dist/atomic.js.map +1 -0
- package/dist/atomic.mjs +185 -0
- package/dist/atomic.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +45008 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +44980 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/compiler.d.mts +1009 -0
- package/dist/compiler.d.ts +1009 -0
- package/dist/compiler.js +4937 -0
- package/dist/compiler.js.map +1 -0
- package/dist/compiler.mjs +4862 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/dashboard.d.mts +272 -0
- package/dist/dashboard.d.ts +272 -0
- package/dist/dashboard.js +249 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/dashboard.mjs +239 -0
- package/dist/dashboard.mjs.map +1 -0
- package/dist/devtools.js +171 -158
- package/dist/devtools.js.map +1 -1
- package/dist/{devtools.cjs → devtools.mjs} +166 -167
- package/dist/devtools.mjs.map +1 -0
- package/dist/engine.d.mts +398 -0
- package/dist/engine.d.ts +398 -0
- package/dist/engine.js +19264 -0
- package/dist/engine.js.map +1 -0
- package/dist/engine.mjs +19227 -0
- package/dist/engine.mjs.map +1 -0
- package/dist/{index.d.cts → index.d.mts} +12 -5
- package/dist/index.d.ts +12 -5
- package/dist/index.js +7178 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8408 -0
- package/dist/index.mjs.map +1 -0
- package/dist/liveTokenEngine-DYN3Zale.d.mts +34 -0
- package/dist/liveTokenEngine-DYN3Zale.d.ts +34 -0
- package/dist/{next.d.cts → next.d.mts} +2 -1
- package/dist/next.d.ts +2 -1
- package/dist/next.js +24027 -28
- package/dist/next.js.map +1 -1
- package/dist/next.mjs +24232 -0
- package/dist/next.mjs.map +1 -0
- package/dist/plugin.d.mts +90 -0
- package/dist/plugin.d.ts +90 -0
- package/dist/plugin.js +185 -0
- package/dist/plugin.js.map +1 -0
- package/dist/plugin.mjs +174 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/pluginRegistry.d.mts +83 -0
- package/dist/pluginRegistry.d.ts +83 -0
- package/dist/pluginRegistry.js +303 -0
- package/dist/pluginRegistry.js.map +1 -0
- package/dist/pluginRegistry.mjs +298 -0
- package/dist/pluginRegistry.mjs.map +1 -0
- package/dist/preset.js +9 -4
- package/dist/preset.js.map +1 -1
- package/dist/{preset.cjs → preset.mjs} +5 -14
- package/dist/preset.mjs.map +1 -0
- package/dist/rspack.d.mts +33 -0
- package/dist/rspack.d.ts +33 -0
- package/dist/rspack.js +66 -0
- package/dist/rspack.js.map +1 -0
- package/dist/rspack.mjs +55 -0
- package/dist/rspack.mjs.map +1 -0
- package/dist/runtime.d.mts +62 -0
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.js +455 -0
- package/dist/runtime.js.map +1 -0
- package/dist/runtime.mjs +436 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/runtimeCss.d.mts +65 -0
- package/dist/runtimeCss.d.ts +65 -0
- package/dist/{css.cjs → runtimeCss.js} +71 -4
- package/dist/runtimeCss.js.map +1 -0
- package/dist/{css.js → runtimeCss.mjs} +66 -5
- package/dist/runtimeCss.mjs.map +1 -0
- package/dist/scanner.d.mts +25 -0
- package/dist/scanner.d.ts +25 -0
- package/dist/scanner.js +5774 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scanner.mjs +5760 -0
- package/dist/scanner.mjs.map +1 -0
- package/dist/shared.d.mts +85 -0
- package/dist/shared.d.ts +85 -0
- package/dist/shared.js +255 -0
- package/dist/shared.js.map +1 -0
- package/dist/shared.mjs +233 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/storybookAddon.d.mts +108 -0
- package/dist/storybookAddon.d.ts +108 -0
- package/dist/storybookAddon.js +95 -0
- package/dist/storybookAddon.js.map +1 -0
- package/dist/storybookAddon.mjs +88 -0
- package/dist/storybookAddon.mjs.map +1 -0
- package/dist/svelte.d.mts +114 -0
- package/dist/svelte.d.ts +114 -0
- package/dist/svelte.js +67 -0
- package/dist/svelte.js.map +1 -0
- package/dist/svelte.mjs +59 -0
- package/dist/svelte.mjs.map +1 -0
- package/dist/testing.d.mts +185 -0
- package/dist/testing.d.ts +185 -0
- package/dist/testing.js +173 -0
- package/dist/testing.js.map +1 -0
- package/dist/testing.mjs +158 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/theme.d.mts +188 -0
- package/dist/theme.d.ts +188 -0
- package/dist/theme.js +334 -0
- package/dist/theme.js.map +1 -0
- package/dist/theme.mjs +311 -0
- package/dist/theme.mjs.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types-DXr2PmGP.d.mts +31 -0
- package/dist/types-DXr2PmGP.d.ts +31 -0
- package/dist/vite.js +29611 -17
- package/dist/vite.js.map +1 -1
- package/dist/vite.mjs +29712 -0
- package/dist/vite.mjs.map +1 -0
- package/dist/vue.d.mts +89 -0
- package/dist/vue.d.ts +89 -0
- package/dist/vue.js +104 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +96 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +170 -64
- package/dist/animate.cjs +0 -771
- package/dist/animate.cjs.map +0 -1
- package/dist/chunk-VZEJV27B.js +0 -11
- package/dist/chunk-VZEJV27B.js.map +0 -1
- package/dist/chunk-Y5D3E72P.cjs +0 -13
- package/dist/chunk-Y5D3E72P.cjs.map +0 -1
- package/dist/css.cjs.map +0 -1
- package/dist/css.d.cts +0 -30
- package/dist/css.d.ts +0 -30
- package/dist/css.js.map +0 -1
- package/dist/devtools.cjs.map +0 -1
- package/dist/index.cjs +0 -1353
- package/dist/index.cjs.map +0 -1
- package/dist/next.cjs +0 -248
- package/dist/next.cjs.map +0 -1
- package/dist/preset.cjs.map +0 -1
- package/dist/turbopackLoader.cjs +0 -37
- package/dist/turbopackLoader.cjs.map +0 -1
- package/dist/turbopackLoader.d.cts +0 -12
- package/dist/turbopackLoader.d.ts +0 -12
- package/dist/turbopackLoader.js +0 -35
- package/dist/turbopackLoader.js.map +0 -1
- package/dist/vite.cjs +0 -138
- package/dist/vite.cjs.map +0 -1
- package/dist/webpackLoader.cjs +0 -51
- package/dist/webpackLoader.cjs.map +0 -1
- package/dist/webpackLoader.d.cts +0 -17
- package/dist/webpackLoader.d.ts +0 -17
- package/dist/webpackLoader.js +0 -49
- package/dist/webpackLoader.js.map +0 -1
- /package/dist/{devtools.d.cts → devtools.d.mts} +0 -0
- /package/dist/{preset.d.cts → preset.d.mts} +0 -0
- /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 };
|