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