@absolutejs/absolute 0.18.7 → 0.18.8-beta.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.
@@ -107,7 +107,7 @@ var handleReactPageRequest = async (PageComponent, index, ...props) => {
107
107
  const element = maybeProps !== undefined ? createElement(PageComponent, maybeProps) : createElement(PageComponent);
108
108
  const propsScript = maybeProps ? `window.__INITIAL_PROPS__=${JSON.stringify(maybeProps)}` : "";
109
109
  const stream = await renderToReadableStream(element, {
110
- bootstrapModules: [index],
110
+ bootstrapScripts: [index],
111
111
  bootstrapScriptContent: propsScript || undefined,
112
112
  onError(error) {
113
113
  console.error("[SSR] React streaming error:", error);
@@ -128,5 +128,5 @@ export {
128
128
  handleReactPageRequest
129
129
  };
130
130
 
131
- //# debugId=EAAD0B18629FAAB864756E2164756E21
131
+ //# debugId=F8273F0992C13A3B64756E2164756E21
132
132
  //# sourceMappingURL=index.js.map
@@ -3,9 +3,9 @@
3
3
  "sources": ["../src/utils/ssrErrorPage.ts", "../src/react/pageHandler.ts"],
4
4
  "sourcesContent": [
5
5
  "export const ssrErrorPage = (framework: string, error: unknown) => {\n\tconst frameworkColors: Record<string, string> = {\n\t\tangular: '#dd0031',\n\t\thtml: '#e34c26',\n\t\thtmx: '#1a365d',\n\t\treact: '#61dafb',\n\t\tsvelte: '#ff3e00',\n\t\tvue: '#42b883'\n\t};\n\n\tconst accent = frameworkColors[framework] ?? '#94a3b8';\n\tconst label = framework.charAt(0).toUpperCase() + framework.slice(1);\n\tconst message = error instanceof Error ? error.message : String(error);\n\n\treturn `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>SSR Error - AbsoluteJS</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{min-height:100vh;background:linear-gradient(135deg,rgba(15,23,42,0.98) 0%,rgba(30,41,59,0.98) 100%);color:#e2e8f0;font-family:\"JetBrains Mono\",\"Fira Code\",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:14px;line-height:1.6;display:flex;align-items:flex-start;justify-content:center;padding:32px}\n.card{max-width:720px;width:100%;background:rgba(30,41,59,0.6);border:1px solid rgba(71,85,105,0.5);border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.5),0 0 0 1px rgba(255,255,255,0.05);overflow:hidden}\n.header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px;background:rgba(15,23,42,0.5);border-bottom:1px solid rgba(71,85,105,0.4)}\n.brand{font-weight:700;font-size:20px;color:#fff;letter-spacing:-0.02em}\n.badge{padding:5px 10px;border-radius:8px;font-size:12px;font-weight:600;background:${accent};color:#fff;opacity:0.95;box-shadow:0 2px 4px rgba(0,0,0,0.2)}\n.kind{color:#94a3b8;font-size:13px;font-weight:500}\n.content{padding:24px}\n.label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin-bottom:8px}\n.message{margin:0;padding:16px 20px;background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.25);border-radius:10px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;color:#fca5a5;font-size:13px;line-height:1.5}\n.hint{margin-top:20px;padding:12px 20px;background:rgba(71,85,105,0.3);border-radius:10px;border:1px solid rgba(71,85,105,0.4);color:#cbd5e1;font-size:13px}\n</style>\n</head>\n<body>\n<div class=\"card\">\n<div class=\"header\">\n<div style=\"display:flex;align-items:center;gap:12px\">\n<span class=\"brand\">AbsoluteJS</span>\n<span class=\"badge\">${label}</span>\n</div>\n<span class=\"kind\">Server Render Error</span>\n</div>\n<div class=\"content\">\n<div class=\"label\">What went wrong</div>\n<pre class=\"message\">${message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>\n<div class=\"hint\">A component threw during server-side rendering. Check the terminal for the full stack trace.</div>\n</div>\n</div>\n</body>\n</html>`;\n};\n",
6
- "import type { ComponentType as ReactComponent } from 'react';\nimport { ssrErrorPage } from '../utils/ssrErrorPage';\n\nexport const handleReactPageRequest = async <\n\tProps extends Record<string, unknown> = Record<never, never>\n>(\n\tPageComponent: ReactComponent<Props>,\n\tindex: string,\n\t...props: keyof Props extends never ? [] : [props: NoInfer<Props>]\n) => {\n\ttry {\n\t\tconst [maybeProps] = props;\n\t\tconst { createElement } = await import('react');\n\t\tconst { renderToReadableStream } = await import('react-dom/server');\n\n\t\tconst element =\n\t\t\tmaybeProps !== undefined\n\t\t\t\t? createElement(PageComponent, maybeProps)\n\t\t\t\t: createElement(PageComponent);\n\n\t\tconst propsScript = maybeProps\n\t\t\t? `window.__INITIAL_PROPS__=${JSON.stringify(maybeProps)}`\n\t\t\t: '';\n\n\t\tconst stream = await renderToReadableStream(element, {\n\t\t\tbootstrapModules: [index],\n\t\t\tbootstrapScriptContent: propsScript || undefined,\n\t\t\tonError(error: unknown) {\n\t\t\t\tconsole.error('[SSR] React streaming error:', error);\n\t\t\t}\n\t\t});\n\n\t\treturn new Response(stream, {\n\t\t\theaders: { 'Content-Type': 'text/html' }\n\t\t});\n\t} catch (error) {\n\t\tconsole.error('[SSR] React render error:', error);\n\n\t\treturn new Response(ssrErrorPage('react', error), {\n\t\t\theaders: { 'Content-Type': 'text/html' },\n\t\t\tstatus: 500\n\t\t});\n\t}\n};\n"
6
+ "import type { ComponentType as ReactComponent } from 'react';\nimport { ssrErrorPage } from '../utils/ssrErrorPage';\n\nexport const handleReactPageRequest = async <\n\tProps extends Record<string, unknown> = Record<never, never>\n>(\n\tPageComponent: ReactComponent<Props>,\n\tindex: string,\n\t...props: keyof Props extends never ? [] : [props: NoInfer<Props>]\n) => {\n\ttry {\n\t\tconst [maybeProps] = props;\n\t\tconst { createElement } = await import('react');\n\t\tconst { renderToReadableStream } = await import('react-dom/server');\n\n\t\tconst element =\n\t\t\tmaybeProps !== undefined\n\t\t\t\t? createElement(PageComponent, maybeProps)\n\t\t\t\t: createElement(PageComponent);\n\n\t\tconst propsScript = maybeProps\n\t\t\t? `window.__INITIAL_PROPS__=${JSON.stringify(maybeProps)}`\n\t\t\t: '';\n\n\t\tconst stream = await renderToReadableStream(element, {\n\t\t\tbootstrapScripts: [index],\n\t\t\tbootstrapScriptContent: propsScript || undefined,\n\t\t\tonError(error: unknown) {\n\t\t\t\tconsole.error('[SSR] React streaming error:', error);\n\t\t\t}\n\t\t});\n\n\t\treturn new Response(stream, {\n\t\t\theaders: { 'Content-Type': 'text/html' }\n\t\t});\n\t} catch (error) {\n\t\tconsole.error('[SSR] React render error:', error);\n\n\t\treturn new Response(ssrErrorPage('react', error), {\n\t\t\theaders: { 'Content-Type': 'text/html' },\n\t\t\tstatus: 500\n\t\t});\n\t}\n};\n"
7
7
  ],
8
8
  "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAAa,eAAe,CAAC,WAAmB,UAAmB;AAAA,EAClE,MAAM,kBAA0C;AAAA,IAC/C,SAAS;AAAA,IACT,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,KAAK;AAAA,EACN;AAAA,EAEA,MAAM,SAAS,gBAAgB,cAAc;AAAA,EAC7C,MAAM,QAAQ,UAAU,OAAO,CAAC,EAAE,YAAY,IAAI,UAAU,MAAM,CAAC;AAAA,EACnE,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,EAErE,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sFAY8E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAahE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAMC,QAAQ,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC1CzF,IAAM,yBAAyB,OAGrC,eACA,UACG,UACC;AAAA,EACJ,IAAI;AAAA,IACH,OAAO,cAAc;AAAA,IACrB,QAAQ,kBAAkB,MAAa;AAAA,IACvC,QAAQ,2BAA2B,MAAa;AAAA,IAEhD,MAAM,UACL,eAAe,YACZ,cAAc,eAAe,UAAU,IACvC,cAAc,aAAa;AAAA,IAE/B,MAAM,cAAc,aACjB,4BAA4B,KAAK,UAAU,UAAU,MACrD;AAAA,IAEH,MAAM,SAAS,MAAM,uBAAuB,SAAS;AAAA,MACpD,kBAAkB,CAAC,KAAK;AAAA,MACxB,wBAAwB,eAAe;AAAA,MACvC,OAAO,CAAC,OAAgB;AAAA,QACvB,QAAQ,MAAM,gCAAgC,KAAK;AAAA;AAAA,IAErD,CAAC;AAAA,IAED,OAAO,IAAI,SAAS,QAAQ;AAAA,MAC3B,SAAS,EAAE,gBAAgB,YAAY;AAAA,IACxC,CAAC;AAAA,IACA,OAAO,OAAO;AAAA,IACf,QAAQ,MAAM,6BAA6B,KAAK;AAAA,IAEhD,OAAO,IAAI,SAAS,aAAa,SAAS,KAAK,GAAG;AAAA,MACjD,SAAS,EAAE,gBAAgB,YAAY;AAAA,MACvC,QAAQ;AAAA,IACT,CAAC;AAAA;AAAA;",
9
- "debugId": "EAAD0B18629FAAB864756E2164756E21",
9
+ "debugId": "F8273F0992C13A3B64756E2164756E21",
10
10
  "names": []
11
11
  }
package/package.json CHANGED
@@ -151,5 +151,5 @@
151
151
  "typecheck": "bun run vue-tsc --noEmit"
152
152
  },
153
153
  "types": "./dist/src/index.d.ts",
154
- "version": "0.18.7"
154
+ "version": "0.18.8-beta.1"
155
155
  }
package/speed-test.ts ADDED
@@ -0,0 +1,445 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ const PORT = 4444;
5
+ const SERVER_ENTRY = 'example/server.ts';
6
+ const CONFIG_PATH = 'example/absolute.config.ts';
7
+
8
+ // ── Config ──
9
+ // Change these to control sample size. More rounds = more reliable averages.
10
+ const WARM_UP_ROUNDS = 3;
11
+ const BUILD_ROUNDS = 10;
12
+ const DEV_START_ROUNDS = 10;
13
+ const HMR_ROUNDS = 20;
14
+
15
+ const FRAMEWORKS = ['react', 'svelte', 'vue', 'angular', 'html', 'htmx'] as const;
16
+
17
+ const HMR_TARGETS: Record<string, { path: string; marker: string; replacement: (i: number) => string }> = {
18
+ react: {
19
+ path: 'example/react/pages/ReactExample.tsx',
20
+ marker: '<App initialCount={initialCount} />',
21
+ replacement: (i) => `<App initialCount={initialCount} /> {/* speed-test-${i} */}`
22
+ },
23
+ svelte: { path: 'example/svelte/pages/SvelteExample.svelte', marker: '', replacement: () => '' },
24
+ vue: { path: 'example/vue/pages/VueExample.vue', marker: '', replacement: () => '' },
25
+ angular: { path: 'example/angular/pages/angular-example.ts', marker: '', replacement: () => '' },
26
+ html: { path: 'example/html/pages/HTMLExample.html', marker: '', replacement: () => '' },
27
+ htmx: { path: 'example/htmx/pages/HTMXExample.html', marker: '', replacement: () => '' }
28
+ };
29
+
30
+ // ── Helpers ──
31
+
32
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
33
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
34
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
35
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
36
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
37
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
38
+ const magenta = (s: string) => `\x1b[35m${s}\x1b[0m`;
39
+
40
+ const fmtMs = (n: number) => `${n.toFixed(0)}ms`;
41
+ const pad = (s: string, len: number) => s.padEnd(len);
42
+ const sleep = (t: number) => new Promise<void>((r) => setTimeout(r, t));
43
+
44
+ const avg = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;
45
+ const p50 = (arr: number[]) => [...arr].sort((a, b) => a - b)[Math.floor(arr.length / 2)];
46
+ const p95 = (arr: number[]) => [...arr].sort((a, b) => a - b)[Math.floor(arr.length * 0.95)];
47
+ const minVal = (arr: number[]) => Math.min(...arr);
48
+ const maxVal = (arr: number[]) => Math.max(...arr);
49
+
50
+ const log = (msg: string) => console.log(` ${msg}`);
51
+ const header = (msg: string) => console.log(`\n${bold(cyan(`▸ ${msg}`))}`);
52
+ const separator = () => console.log(dim('─'.repeat(72)));
53
+
54
+ // ── Types ──
55
+
56
+ type HmrTiming = { total: number; detection: number; build: number };
57
+ type HmrFrameworkResult = { cold: HmrTiming[]; warm: HmrTiming[] };
58
+ type DevStartTiming = { total: number; boot: number; build: number };
59
+
60
+ const UPDATE_TYPES = new Set([
61
+ 'react-update', 'svelte-update', 'vue-update', 'angular-update',
62
+ 'html-update', 'htmx-update', 'full-reload', 'module-update', 'script-update'
63
+ ]);
64
+
65
+ // ── Detect HMR markers from actual files ──
66
+
67
+ const initHmrTargets = () => {
68
+ for (const [fw, target] of Object.entries(HMR_TARGETS)) {
69
+ if (target.marker) continue;
70
+ const content = readFileSync(resolve(target.path), 'utf-8');
71
+ const lines = content.split('\n');
72
+
73
+ const searchTerms: Record<string, string[]> = {
74
+ svelte: ['<h1', '<main'],
75
+ vue: ['<h1', '<template'],
76
+ angular: ['template:', 'templateUrl'],
77
+ html: ['<title', '<h1'],
78
+ htmx: ['<title', '<h1']
79
+ };
80
+
81
+ const terms = searchTerms[fw];
82
+ if (!terms) continue;
83
+ const idx = lines.findIndex((l) => terms.some((t) => l.includes(t)));
84
+ if (idx === -1) continue;
85
+
86
+ target.marker = lines[idx];
87
+ target.replacement = fw === 'angular'
88
+ ? (i) => `${lines[idx]} // speed-test-${i}`
89
+ : (i) => `${lines[idx]}<!-- speed-test-${i} -->`;
90
+ }
91
+ };
92
+
93
+ const killPort = async (port: number) => {
94
+ try {
95
+ const proc = Bun.spawn(['lsof', '-i', `:${port}`, '-t'], { stdout: 'pipe', stderr: 'pipe' });
96
+ const text = await new Response(proc.stdout).text();
97
+ const pids = text.trim().split('\n').filter(Boolean);
98
+ for (const pid of pids) {
99
+ try { process.kill(parseInt(pid), 'SIGKILL'); } catch { /* already dead */ }
100
+ }
101
+ if (pids.length) await sleep(500);
102
+ } catch { /* lsof not found or no processes */ }
103
+ };
104
+
105
+ // ── Build speed ──
106
+
107
+ const measureBuild = async () => {
108
+ header('Build (production bundle)');
109
+ const times: number[] = [];
110
+
111
+ for (let i = 0; i < BUILD_ROUNDS; i++) {
112
+ const start = performance.now();
113
+ const proc = Bun.spawn(
114
+ ['bun', 'run', 'src/cli/index.ts', 'start', SERVER_ENTRY, '--outdir', 'example/dist', '--config', CONFIG_PATH],
115
+ {
116
+ cwd: process.cwd(),
117
+ env: { ...process.env, TELEMETRY_OFF: '1', NODE_ENV: 'production', PORT: String(PORT + 100) },
118
+ stdout: 'pipe', stderr: 'pipe'
119
+ }
120
+ );
121
+
122
+ const reader = proc.stdout.getReader();
123
+ let ready = false;
124
+ while (!ready) {
125
+ const { done, value } = await reader.read();
126
+ if (done) break;
127
+ if (Buffer.from(value).toString().includes('Local:')) ready = true;
128
+ }
129
+ const elapsed = performance.now() - start;
130
+ proc.kill();
131
+ await proc.exited;
132
+ times.push(elapsed);
133
+ log(` run ${i + 1}/${BUILD_ROUNDS}: ${fmtMs(elapsed)}`);
134
+ }
135
+
136
+ return times;
137
+ };
138
+
139
+ // ── Dev server start ──
140
+
141
+ const measureDevStart = async () => {
142
+ header('Dev server cold start');
143
+ const timings: DevStartTiming[] = [];
144
+
145
+ for (let i = 0; i < DEV_START_ROUNDS; i++) {
146
+ await killPort(PORT);
147
+ const start = performance.now();
148
+ let reportedBuildMs = 0;
149
+
150
+ const proc = Bun.spawn(
151
+ ['bun', '--hot', '--no-clear-screen', SERVER_ENTRY],
152
+ {
153
+ cwd: process.cwd(),
154
+ env: {
155
+ ...process.env, TELEMETRY_OFF: '1', NODE_ENV: 'development',
156
+ PORT: String(PORT), ABSOLUTE_CONFIG: CONFIG_PATH
157
+ },
158
+ stdout: 'pipe', stderr: 'pipe', stdin: 'ignore'
159
+ }
160
+ );
161
+
162
+ const reader = proc.stdout.getReader();
163
+ let ready = false;
164
+ while (!ready) {
165
+ const { done, value } = await reader.read();
166
+ if (done) break;
167
+ const chunk = Buffer.from(value).toString().replace(/\x1b\[[0-9;]*m/g, '');
168
+ const m = chunk.match(/ready in\s+(?:(\d+(?:\.\d+)?)s|(\d+)ms)/);
169
+ if (m) reportedBuildMs = m[1] ? parseFloat(m[1]) * 1000 : parseInt(m[2], 10);
170
+ if (chunk.includes('Local:')) ready = true;
171
+ }
172
+ const total = performance.now() - start;
173
+ proc.kill();
174
+ await proc.exited;
175
+
176
+ const build = reportedBuildMs || total;
177
+ const boot = total - build;
178
+ timings.push({ total, boot: Math.max(0, boot), build });
179
+ log(` run ${i + 1}/${DEV_START_ROUNDS}: ${fmtMs(total)} ${dim(`(boot ${fmtMs(Math.max(0, boot))} + build ${fmtMs(build)})`)}`);
180
+ }
181
+
182
+ return timings;
183
+ };
184
+
185
+ // ── HMR measurement ──
186
+
187
+ const connectWs = (port: number) =>
188
+ new Promise<WebSocket>((resolve, reject) => {
189
+ const ws = new WebSocket(`ws://localhost:${port}/hmr`);
190
+ const timer = setTimeout(() => { ws.close(); reject(new Error('WS timeout')); }, 10_000);
191
+ ws.onopen = () => { clearTimeout(timer); ws.send(JSON.stringify({ type: 'ready', framework: 'react' })); resolve(ws); };
192
+ ws.onerror = () => { clearTimeout(timer); reject(new Error('WS failed')); };
193
+ });
194
+
195
+ // Wait for one complete HMR rebuild cycle. Resolves with timing breakdown.
196
+ // fileWriteTime should be set to performance.now() right before writing the file.
197
+ const waitForRebuild = (ws: WebSocket, fileWriteTime: number, timeoutMs = 15_000) =>
198
+ new Promise<HmrTiming>((resolve, reject) => {
199
+ let rebuildStartAt = 0;
200
+ let doneAt = 0;
201
+
202
+ const timer = setTimeout(() => {
203
+ ws.removeEventListener('message', onMsg);
204
+ reject(new Error('HMR timeout'));
205
+ }, timeoutMs);
206
+
207
+ const onMsg = (event: MessageEvent) => {
208
+ const now = performance.now();
209
+ const data = JSON.parse(String(event.data));
210
+
211
+ if (data.type === 'rebuild-start' && !rebuildStartAt) {
212
+ rebuildStartAt = now;
213
+ }
214
+
215
+ // Fast paths: rebuild-start → framework-update (no rebuild-complete)
216
+ // Full rebuilds: rebuild-start → rebuild-complete → framework-updates
217
+ // Either way, the first update/complete after rebuild-start marks the end.
218
+ if (rebuildStartAt && (UPDATE_TYPES.has(data.type) || data.type === 'rebuild-complete')) {
219
+ if (!doneAt) {
220
+ doneAt = now;
221
+ clearTimeout(timer);
222
+ ws.removeEventListener('message', onMsg);
223
+ resolve({
224
+ total: doneAt - fileWriteTime,
225
+ detection: rebuildStartAt - fileWriteTime,
226
+ build: doneAt - rebuildStartAt
227
+ });
228
+ }
229
+ }
230
+ };
231
+
232
+ ws.addEventListener('message', onMsg);
233
+ });
234
+
235
+ const measureHmr = async () => {
236
+ header('HMR (hot module replacement)');
237
+
238
+ await killPort(PORT);
239
+ log('Starting dev server...');
240
+ const proc = Bun.spawn(
241
+ ['bun', '--hot', '--no-clear-screen', SERVER_ENTRY],
242
+ {
243
+ cwd: process.cwd(),
244
+ env: {
245
+ ...process.env, TELEMETRY_OFF: '1', NODE_ENV: 'development',
246
+ PORT: String(PORT), ABSOLUTE_CONFIG: CONFIG_PATH
247
+ },
248
+ stdout: 'pipe', stderr: 'pipe', stdin: 'ignore'
249
+ }
250
+ );
251
+
252
+ const reader = proc.stdout.getReader();
253
+ let serverReady = false;
254
+ while (!serverReady) {
255
+ const { done, value } = await reader.read();
256
+ if (done) break;
257
+ if (Buffer.from(value).toString().includes('Local:')) serverReady = true;
258
+ }
259
+
260
+ if (!serverReady) {
261
+ log(red('Dev server failed to start'));
262
+ proc.kill(); await proc.exited;
263
+ return {};
264
+ }
265
+
266
+ log(green('Dev server ready'));
267
+ await sleep(500);
268
+
269
+ let ws: WebSocket;
270
+ try {
271
+ ws = await connectWs(PORT);
272
+ log(green('WebSocket connected'));
273
+ } catch {
274
+ log(red('Failed to connect WebSocket'));
275
+ proc.kill(); await proc.exited;
276
+ return {};
277
+ }
278
+
279
+ await sleep(800);
280
+
281
+ const results: Record<string, HmrFrameworkResult> = {};
282
+
283
+ for (const fw of FRAMEWORKS) {
284
+ const target = HMR_TARGETS[fw];
285
+ if (!target.marker) {
286
+ log(yellow(` ${fw}: skipped (no marker found)`));
287
+ continue;
288
+ }
289
+
290
+ const originalContent = readFileSync(resolve(target.path), 'utf-8');
291
+ const coldTimings: HmrTiming[] = [];
292
+ const warmTimings: HmrTiming[] = [];
293
+
294
+ log(dim(` ${fw}: ${WARM_UP_ROUNDS} warm-up + ${HMR_ROUNDS} measured...`));
295
+
296
+ const totalRounds = WARM_UP_ROUNDS + HMR_ROUNDS;
297
+
298
+ for (let i = 0; i < totalRounds; i++) {
299
+ const isWarmUp = i < WARM_UP_ROUNDS;
300
+
301
+ try {
302
+ // ── Modify file ──
303
+ const modifiedContent = originalContent.replace(
304
+ target.marker,
305
+ target.replacement(Date.now())
306
+ );
307
+ const writeTime = performance.now();
308
+ writeFileSync(resolve(target.path), modifiedContent);
309
+ const timing = await waitForRebuild(ws, writeTime);
310
+
311
+ // ── Restore file and wait for that rebuild too ──
312
+ const restoreTime = performance.now();
313
+ writeFileSync(resolve(target.path), originalContent);
314
+ await waitForRebuild(ws, restoreTime);
315
+ await sleep(80);
316
+
317
+ if (!isWarmUp) {
318
+ if (coldTimings.length === 0) {
319
+ coldTimings.push(timing);
320
+ } else {
321
+ warmTimings.push(timing);
322
+ }
323
+ }
324
+ } catch {
325
+ log(red(` ${fw} round ${i}: timed out`));
326
+ // Ensure file is restored
327
+ writeFileSync(resolve(target.path), originalContent);
328
+ await sleep(500);
329
+ }
330
+ }
331
+
332
+ writeFileSync(resolve(target.path), originalContent);
333
+ results[fw] = { cold: coldTimings, warm: warmTimings };
334
+
335
+ if (coldTimings.length > 0 && warmTimings.length > 0) {
336
+ log(` ${fw} cold: ${fmtMs(coldTimings[0].total)} ${dim(`(detect ${fmtMs(coldTimings[0].detection)} + build ${fmtMs(coldTimings[0].build)})`)}`);
337
+ log(` ${fw} warm avg: ${fmtMs(avg(warmTimings.map((t) => t.total)))} ${dim(`(detect ${fmtMs(avg(warmTimings.map((t) => t.detection)))} + build ${fmtMs(avg(warmTimings.map((t) => t.build)))})`)}`);
338
+ }
339
+ }
340
+
341
+ ws.close();
342
+ proc.kill();
343
+ await proc.exited;
344
+
345
+ return results;
346
+ };
347
+
348
+ // ── Summary ──
349
+
350
+ const printSummary = (
351
+ buildTimes: number[],
352
+ devStartTimings: DevStartTiming[],
353
+ hmrResults: Record<string, HmrFrameworkResult>
354
+ ) => {
355
+ console.log('\n');
356
+ console.log(bold('╔════════════════════════════════════════════════════════════════════════╗'));
357
+ console.log(bold('║ SPEED TEST RESULTS ║'));
358
+ console.log(bold('╚════════════════════════════════════════════════════════════════════════╝'));
359
+
360
+ const row = (label: string, times: number[]) => {
361
+ if (!times.length) return;
362
+ console.log(
363
+ ` ${pad(label, 28)} ${pad(fmtMs(avg(times)), 8)} ${pad(fmtMs(p50(times)), 8)} ${pad(fmtMs(p95(times)), 8)} ${pad(fmtMs(minVal(times)), 8)} ${fmtMs(maxVal(times))}`
364
+ );
365
+ };
366
+
367
+ const headerRow = () => {
368
+ console.log(bold(` ${pad('Metric', 28)} ${pad('Avg', 8)} ${pad('p50', 8)} ${pad('p95', 8)} ${pad('Min', 8)} Max`));
369
+ };
370
+
371
+ separator();
372
+ console.log(bold(magenta(' Production Build')));
373
+ separator();
374
+ headerRow();
375
+ row('Total', buildTimes);
376
+
377
+ separator();
378
+ console.log(bold(magenta(' Dev Server Cold Start')));
379
+ separator();
380
+ headerRow();
381
+ row('Total', devStartTimings.map((t) => t.total));
382
+ row(' Bun boot (overhead)', devStartTimings.map((t) => t.boot));
383
+ row(' Build + vendors', devStartTimings.map((t) => t.build));
384
+
385
+ separator();
386
+ console.log(bold(magenta(' HMR by Framework (warm)')));
387
+ separator();
388
+ headerRow();
389
+
390
+ for (const fw of FRAMEWORKS) {
391
+ const r = hmrResults[fw];
392
+ if (!r?.warm?.length) continue;
393
+
394
+ console.log(bold(` ${fw}`));
395
+ row(' Total (cold)', r.cold.map((t) => t.total));
396
+ row(' Total (warm)', r.warm.map((t) => t.total));
397
+ row(' Detection (fs→ws)', r.warm.map((t) => t.detection));
398
+ row(' Build (compile+bundle)', r.warm.map((t) => t.build));
399
+ }
400
+
401
+ separator();
402
+ console.log(bold(magenta(' Bottleneck Analysis')));
403
+ separator();
404
+
405
+ for (const fw of FRAMEWORKS) {
406
+ const r = hmrResults[fw];
407
+ if (!r?.warm?.length) continue;
408
+ const warmTotal = avg(r.warm.map((t) => t.total));
409
+ const warmDetect = avg(r.warm.map((t) => t.detection));
410
+ const warmBuild = avg(r.warm.map((t) => t.build));
411
+ const detectPct = ((warmDetect / warmTotal) * 100).toFixed(0);
412
+ const buildPct = ((warmBuild / warmTotal) * 100).toFixed(0);
413
+
414
+ const bottleneck = warmDetect >= warmBuild ? 'detection' : 'build';
415
+
416
+ console.log(
417
+ ` ${pad(fw, 12)} detect ${pad(detectPct + '%', 5)} build ${pad(buildPct + '%', 5)} ${dim(`bottleneck: ${bottleneck}`)}`
418
+ );
419
+ }
420
+
421
+ separator();
422
+ console.log('');
423
+ };
424
+
425
+ // ── Main ──
426
+
427
+ const main = async () => {
428
+ console.log(bold(cyan('\n AbsoluteJS Speed Test\n')));
429
+ console.log(dim(` Build: ${BUILD_ROUNDS} rounds | Dev start: ${DEV_START_ROUNDS} rounds | HMR: ${HMR_ROUNDS} rounds (+${WARM_UP_ROUNDS} warm-up)`));
430
+ console.log(dim(` Port: ${PORT}`));
431
+ console.log('');
432
+
433
+ initHmrTargets();
434
+
435
+ const buildTimes = await measureBuild();
436
+ const devStartTimings = await measureDevStart();
437
+ const hmrResults = await measureHmr();
438
+
439
+ printSummary(buildTimes, devStartTimings, hmrResults);
440
+ };
441
+
442
+ main().catch((err) => {
443
+ console.error(red(`Speed test failed: ${err.message}`));
444
+ process.exit(1);
445
+ });