@hydration-audit/dashboard 0.2.0
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/README.md +67 -0
- package/dist/index.cjs +124 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +86 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @hydration-audit/dashboard
|
|
2
|
+
|
|
3
|
+
> Interactive web dashboard for visualizing JavaScript hydration costs.
|
|
4
|
+
|
|
5
|
+
Part of the [Hydration Cost Visibility Platform](../../README.md).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Treemap Visualization** — Islands sized by gzip cost, colored by severity
|
|
10
|
+
- **Sortable Island Table** — Click any column header to sort
|
|
11
|
+
- **Budget Gauges** — Visual meters for page and site-wide budgets
|
|
12
|
+
- **Issue List** — All detected issues with actionable recommendations
|
|
13
|
+
- **Live Reload** — Dashboard updates automatically via WebSocket when the report file changes
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Via CLI (Recommended)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @hydration-audit/cli dashboard
|
|
21
|
+
npx @hydration-audit/cli dashboard --port 3000
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Programmatic API
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { startDashboard } from '@hydration-audit/dashboard';
|
|
28
|
+
|
|
29
|
+
const { url, close } = await startDashboard(
|
|
30
|
+
'.hydration-audit-report.json', // path to report
|
|
31
|
+
4173, // port
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
console.log(`Dashboard at ${url}`);
|
|
35
|
+
|
|
36
|
+
// Later...
|
|
37
|
+
close();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How It Works
|
|
41
|
+
|
|
42
|
+
1. Reads the `.hydration-audit-report.json` file generated by the analyzer
|
|
43
|
+
2. Serves a lightweight SPA (vanilla JS, no framework runtime) on a local HTTP server
|
|
44
|
+
3. Opens a WebSocket connection to watch for report file changes
|
|
45
|
+
4. When the report changes (e.g., after a rebuild), all connected browsers update instantly
|
|
46
|
+
|
|
47
|
+
## Dashboard Views
|
|
48
|
+
|
|
49
|
+
### Treemap
|
|
50
|
+
Islands are displayed as rectangles proportional to their gzip size. Colors indicate severity:
|
|
51
|
+
- **Green** — Under 20KB, no issues
|
|
52
|
+
- **Blue** — 20-50KB, within budget
|
|
53
|
+
- **Yellow** — 50-100KB, warning
|
|
54
|
+
- **Red** — Over 100KB or has errors
|
|
55
|
+
|
|
56
|
+
### Island Table
|
|
57
|
+
Sortable table with columns:
|
|
58
|
+
- Name, Directive, Framework, Gzip size, Brotli size, Issues, Pages
|
|
59
|
+
|
|
60
|
+
### Budget Gauges
|
|
61
|
+
Visual progress bars showing:
|
|
62
|
+
- Per-page budget usage
|
|
63
|
+
- Total site budget usage
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
startDashboard: () => startDashboard
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/server.ts
|
|
38
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
39
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
40
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
41
|
+
var import_ws = require("ws");
|
|
42
|
+
var import_meta = {};
|
|
43
|
+
var MIME_TYPES = {
|
|
44
|
+
".html": "text/html",
|
|
45
|
+
".js": "application/javascript",
|
|
46
|
+
".css": "text/css",
|
|
47
|
+
".json": "application/json",
|
|
48
|
+
".svg": "image/svg+xml"
|
|
49
|
+
};
|
|
50
|
+
async function startDashboard(reportPath, port = 4173) {
|
|
51
|
+
const absoluteReportPath = import_node_path.default.resolve(reportPath);
|
|
52
|
+
let currentReport = null;
|
|
53
|
+
try {
|
|
54
|
+
const content = import_node_fs.default.readFileSync(absoluteReportPath, "utf-8");
|
|
55
|
+
currentReport = JSON.parse(content);
|
|
56
|
+
} catch {
|
|
57
|
+
console.warn(`Could not read report at ${absoluteReportPath}`);
|
|
58
|
+
}
|
|
59
|
+
const staticDir = import_node_path.default.resolve(
|
|
60
|
+
import_node_path.default.dirname(new URL(import_meta.url).pathname),
|
|
61
|
+
"app"
|
|
62
|
+
);
|
|
63
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
64
|
+
const url = req.url ?? "/";
|
|
65
|
+
if (url === "/api/report") {
|
|
66
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
67
|
+
res.end(JSON.stringify(currentReport ?? { error: "No report loaded" }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let filePath = import_node_path.default.join(staticDir, url === "/" ? "index.html" : url);
|
|
71
|
+
const ext = import_node_path.default.extname(filePath);
|
|
72
|
+
if (!ext) {
|
|
73
|
+
filePath = import_node_path.default.join(staticDir, "index.html");
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const content = import_node_fs.default.readFileSync(filePath);
|
|
77
|
+
const mime = MIME_TYPES[import_node_path.default.extname(filePath)] ?? "text/plain";
|
|
78
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
79
|
+
res.end(content);
|
|
80
|
+
} catch {
|
|
81
|
+
res.writeHead(404);
|
|
82
|
+
res.end("Not found");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const wss = new import_ws.WebSocketServer({ server });
|
|
86
|
+
let watcher = null;
|
|
87
|
+
try {
|
|
88
|
+
watcher = import_node_fs.default.watch(absoluteReportPath, () => {
|
|
89
|
+
try {
|
|
90
|
+
const content = import_node_fs.default.readFileSync(absoluteReportPath, "utf-8");
|
|
91
|
+
currentReport = JSON.parse(content);
|
|
92
|
+
for (const client of wss.clients) {
|
|
93
|
+
if (client.readyState === 1) {
|
|
94
|
+
client.send(JSON.stringify({ type: "update", report: currentReport }));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
console.warn("Could not watch report file for changes");
|
|
102
|
+
}
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
server.listen(port, () => {
|
|
105
|
+
const url = `http://localhost:${port}`;
|
|
106
|
+
console.log(`
|
|
107
|
+
Dashboard running at ${url}
|
|
108
|
+
`);
|
|
109
|
+
resolve({
|
|
110
|
+
url,
|
|
111
|
+
close: () => {
|
|
112
|
+
watcher?.close();
|
|
113
|
+
wss.close();
|
|
114
|
+
server.close();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
121
|
+
0 && (module.exports = {
|
|
122
|
+
startDashboard
|
|
123
|
+
});
|
|
124
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/server.ts"],"sourcesContent":["export { startDashboard } from './server.js';\n","import http from 'node:http';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { WebSocketServer } from 'ws';\nimport type { AnalysisReport } from '@hydration-audit/core';\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.svg': 'image/svg+xml',\n};\n\n/**\n * Start the dashboard server.\n * Serves a static SPA and provides a WebSocket for live report updates.\n */\nexport async function startDashboard(\n reportPath: string,\n port = 4173,\n): Promise<{ url: string; close: () => void }> {\n const absoluteReportPath = path.resolve(reportPath);\n\n // Read the initial report\n let currentReport: AnalysisReport | null = null;\n try {\n const content = fs.readFileSync(absoluteReportPath, 'utf-8');\n currentReport = JSON.parse(content);\n } catch {\n console.warn(`Could not read report at ${absoluteReportPath}`);\n }\n\n const staticDir = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n 'app',\n );\n\n const server = http.createServer((req, res) => {\n const url = req.url ?? '/';\n\n // API endpoint for report data\n if (url === '/api/report') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(currentReport ?? { error: 'No report loaded' }));\n return;\n }\n\n // Serve static files\n let filePath = path.join(staticDir, url === '/' ? 'index.html' : url);\n const ext = path.extname(filePath);\n\n // SPA fallback — serve index.html for non-file routes\n if (!ext) {\n filePath = path.join(staticDir, 'index.html');\n }\n\n try {\n const content = fs.readFileSync(filePath);\n const mime = MIME_TYPES[path.extname(filePath)] ?? 'text/plain';\n res.writeHead(200, { 'Content-Type': mime });\n res.end(content);\n } catch {\n res.writeHead(404);\n res.end('Not found');\n }\n });\n\n // WebSocket for live reload\n const wss = new WebSocketServer({ server });\n\n // Watch report file for changes\n let watcher: fs.FSWatcher | null = null;\n try {\n watcher = fs.watch(absoluteReportPath, () => {\n try {\n const content = fs.readFileSync(absoluteReportPath, 'utf-8');\n currentReport = JSON.parse(content);\n // Notify all connected clients\n for (const client of wss.clients) {\n if (client.readyState === 1) {\n client.send(JSON.stringify({ type: 'update', report: currentReport }));\n }\n }\n } catch {\n // Ignore parse errors during write\n }\n });\n } catch {\n console.warn('Could not watch report file for changes');\n }\n\n return new Promise((resolve) => {\n server.listen(port, () => {\n const url = `http://localhost:${port}`;\n console.log(`\\n Dashboard running at ${url}\\n`);\n resolve({\n url,\n close: () => {\n watcher?.close();\n wss.close();\n server.close();\n },\n });\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,uBAAiB;AACjB,qBAAe;AACf,uBAAiB;AACjB,gBAAgC;AAHhC;AAMA,IAAM,aAAqC;AAAA,EACzC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AACV;AAMA,eAAsB,eACpB,YACA,OAAO,MACsC;AAC7C,QAAM,qBAAqB,iBAAAA,QAAK,QAAQ,UAAU;AAGlD,MAAI,gBAAuC;AAC3C,MAAI;AACF,UAAM,UAAU,eAAAC,QAAG,aAAa,oBAAoB,OAAO;AAC3D,oBAAgB,KAAK,MAAM,OAAO;AAAA,EACpC,QAAQ;AACN,YAAQ,KAAK,4BAA4B,kBAAkB,EAAE;AAAA,EAC/D;AAEA,QAAM,YAAY,iBAAAD,QAAK;AAAA,IACrB,iBAAAA,QAAK,QAAQ,IAAI,IAAI,YAAY,GAAG,EAAE,QAAQ;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,SAAS,iBAAAE,QAAK,aAAa,CAAC,KAAK,QAAQ;AAC7C,UAAM,MAAM,IAAI,OAAO;AAGvB,QAAI,QAAQ,eAAe;AACzB,UAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,UAAI,IAAI,KAAK,UAAU,iBAAiB,EAAE,OAAO,mBAAmB,CAAC,CAAC;AACtE;AAAA,IACF;AAGA,QAAI,WAAW,iBAAAF,QAAK,KAAK,WAAW,QAAQ,MAAM,eAAe,GAAG;AACpE,UAAM,MAAM,iBAAAA,QAAK,QAAQ,QAAQ;AAGjC,QAAI,CAAC,KAAK;AACR,iBAAW,iBAAAA,QAAK,KAAK,WAAW,YAAY;AAAA,IAC9C;AAEA,QAAI;AACF,YAAM,UAAU,eAAAC,QAAG,aAAa,QAAQ;AACxC,YAAM,OAAO,WAAW,iBAAAD,QAAK,QAAQ,QAAQ,CAAC,KAAK;AACnD,UAAI,UAAU,KAAK,EAAE,gBAAgB,KAAK,CAAC;AAC3C,UAAI,IAAI,OAAO;AAAA,IACjB,QAAQ;AACN,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AAAA,IACrB;AAAA,EACF,CAAC;AAGD,QAAM,MAAM,IAAI,0BAAgB,EAAE,OAAO,CAAC;AAG1C,MAAI,UAA+B;AACnC,MAAI;AACF,cAAU,eAAAC,QAAG,MAAM,oBAAoB,MAAM;AAC3C,UAAI;AACF,cAAM,UAAU,eAAAA,QAAG,aAAa,oBAAoB,OAAO;AAC3D,wBAAgB,KAAK,MAAM,OAAO;AAElC,mBAAW,UAAU,IAAI,SAAS;AAChC,cAAI,OAAO,eAAe,GAAG;AAC3B,mBAAO,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,QAAQ,cAAc,CAAC,CAAC;AAAA,UACvE;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,YAAQ,KAAK,yCAAyC;AAAA,EACxD;AAEA,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,WAAO,OAAO,MAAM,MAAM;AACxB,YAAM,MAAM,oBAAoB,IAAI;AACpC,cAAQ,IAAI;AAAA,yBAA4B,GAAG;AAAA,CAAI;AAC/C,cAAQ;AAAA,QACN;AAAA,QACA,OAAO,MAAM;AACX,mBAAS,MAAM;AACf,cAAI,MAAM;AACV,iBAAO,MAAM;AAAA,QACf;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH,CAAC;AACH;","names":["path","fs","http"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the dashboard server.
|
|
3
|
+
* Serves a static SPA and provides a WebSocket for live report updates.
|
|
4
|
+
*/
|
|
5
|
+
declare function startDashboard(reportPath: string, port?: number): Promise<{
|
|
6
|
+
url: string;
|
|
7
|
+
close: () => void;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export { startDashboard };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the dashboard server.
|
|
3
|
+
* Serves a static SPA and provides a WebSocket for live report updates.
|
|
4
|
+
*/
|
|
5
|
+
declare function startDashboard(reportPath: string, port?: number): Promise<{
|
|
6
|
+
url: string;
|
|
7
|
+
close: () => void;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export { startDashboard };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import http from "http";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
6
|
+
var MIME_TYPES = {
|
|
7
|
+
".html": "text/html",
|
|
8
|
+
".js": "application/javascript",
|
|
9
|
+
".css": "text/css",
|
|
10
|
+
".json": "application/json",
|
|
11
|
+
".svg": "image/svg+xml"
|
|
12
|
+
};
|
|
13
|
+
async function startDashboard(reportPath, port = 4173) {
|
|
14
|
+
const absoluteReportPath = path.resolve(reportPath);
|
|
15
|
+
let currentReport = null;
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(absoluteReportPath, "utf-8");
|
|
18
|
+
currentReport = JSON.parse(content);
|
|
19
|
+
} catch {
|
|
20
|
+
console.warn(`Could not read report at ${absoluteReportPath}`);
|
|
21
|
+
}
|
|
22
|
+
const staticDir = path.resolve(
|
|
23
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
24
|
+
"app"
|
|
25
|
+
);
|
|
26
|
+
const server = http.createServer((req, res) => {
|
|
27
|
+
const url = req.url ?? "/";
|
|
28
|
+
if (url === "/api/report") {
|
|
29
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
30
|
+
res.end(JSON.stringify(currentReport ?? { error: "No report loaded" }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let filePath = path.join(staticDir, url === "/" ? "index.html" : url);
|
|
34
|
+
const ext = path.extname(filePath);
|
|
35
|
+
if (!ext) {
|
|
36
|
+
filePath = path.join(staticDir, "index.html");
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(filePath);
|
|
40
|
+
const mime = MIME_TYPES[path.extname(filePath)] ?? "text/plain";
|
|
41
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
42
|
+
res.end(content);
|
|
43
|
+
} catch {
|
|
44
|
+
res.writeHead(404);
|
|
45
|
+
res.end("Not found");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const wss = new WebSocketServer({ server });
|
|
49
|
+
let watcher = null;
|
|
50
|
+
try {
|
|
51
|
+
watcher = fs.watch(absoluteReportPath, () => {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(absoluteReportPath, "utf-8");
|
|
54
|
+
currentReport = JSON.parse(content);
|
|
55
|
+
for (const client of wss.clients) {
|
|
56
|
+
if (client.readyState === 1) {
|
|
57
|
+
client.send(JSON.stringify({ type: "update", report: currentReport }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
console.warn("Could not watch report file for changes");
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
server.listen(port, () => {
|
|
68
|
+
const url = `http://localhost:${port}`;
|
|
69
|
+
console.log(`
|
|
70
|
+
Dashboard running at ${url}
|
|
71
|
+
`);
|
|
72
|
+
resolve({
|
|
73
|
+
url,
|
|
74
|
+
close: () => {
|
|
75
|
+
watcher?.close();
|
|
76
|
+
wss.close();
|
|
77
|
+
server.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
startDashboard
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"sourcesContent":["import http from 'node:http';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { WebSocketServer } from 'ws';\nimport type { AnalysisReport } from '@hydration-audit/core';\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.svg': 'image/svg+xml',\n};\n\n/**\n * Start the dashboard server.\n * Serves a static SPA and provides a WebSocket for live report updates.\n */\nexport async function startDashboard(\n reportPath: string,\n port = 4173,\n): Promise<{ url: string; close: () => void }> {\n const absoluteReportPath = path.resolve(reportPath);\n\n // Read the initial report\n let currentReport: AnalysisReport | null = null;\n try {\n const content = fs.readFileSync(absoluteReportPath, 'utf-8');\n currentReport = JSON.parse(content);\n } catch {\n console.warn(`Could not read report at ${absoluteReportPath}`);\n }\n\n const staticDir = path.resolve(\n path.dirname(new URL(import.meta.url).pathname),\n 'app',\n );\n\n const server = http.createServer((req, res) => {\n const url = req.url ?? '/';\n\n // API endpoint for report data\n if (url === '/api/report') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(currentReport ?? { error: 'No report loaded' }));\n return;\n }\n\n // Serve static files\n let filePath = path.join(staticDir, url === '/' ? 'index.html' : url);\n const ext = path.extname(filePath);\n\n // SPA fallback — serve index.html for non-file routes\n if (!ext) {\n filePath = path.join(staticDir, 'index.html');\n }\n\n try {\n const content = fs.readFileSync(filePath);\n const mime = MIME_TYPES[path.extname(filePath)] ?? 'text/plain';\n res.writeHead(200, { 'Content-Type': mime });\n res.end(content);\n } catch {\n res.writeHead(404);\n res.end('Not found');\n }\n });\n\n // WebSocket for live reload\n const wss = new WebSocketServer({ server });\n\n // Watch report file for changes\n let watcher: fs.FSWatcher | null = null;\n try {\n watcher = fs.watch(absoluteReportPath, () => {\n try {\n const content = fs.readFileSync(absoluteReportPath, 'utf-8');\n currentReport = JSON.parse(content);\n // Notify all connected clients\n for (const client of wss.clients) {\n if (client.readyState === 1) {\n client.send(JSON.stringify({ type: 'update', report: currentReport }));\n }\n }\n } catch {\n // Ignore parse errors during write\n }\n });\n } catch {\n console.warn('Could not watch report file for changes');\n }\n\n return new Promise((resolve) => {\n server.listen(port, () => {\n const url = `http://localhost:${port}`;\n console.log(`\\n Dashboard running at ${url}\\n`);\n resolve({\n url,\n close: () => {\n watcher?.close();\n wss.close();\n server.close();\n },\n });\n });\n });\n}\n"],"mappings":";AAAA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,uBAAuB;AAGhC,IAAM,aAAqC;AAAA,EACzC,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AACV;AAMA,eAAsB,eACpB,YACA,OAAO,MACsC;AAC7C,QAAM,qBAAqB,KAAK,QAAQ,UAAU;AAGlD,MAAI,gBAAuC;AAC3C,MAAI;AACF,UAAM,UAAU,GAAG,aAAa,oBAAoB,OAAO;AAC3D,oBAAgB,KAAK,MAAM,OAAO;AAAA,EACpC,QAAQ;AACN,YAAQ,KAAK,4BAA4B,kBAAkB,EAAE;AAAA,EAC/D;AAEA,QAAM,YAAY,KAAK;AAAA,IACrB,KAAK,QAAQ,IAAI,IAAI,YAAY,GAAG,EAAE,QAAQ;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,SAAS,KAAK,aAAa,CAAC,KAAK,QAAQ;AAC7C,UAAM,MAAM,IAAI,OAAO;AAGvB,QAAI,QAAQ,eAAe;AACzB,UAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,UAAI,IAAI,KAAK,UAAU,iBAAiB,EAAE,OAAO,mBAAmB,CAAC,CAAC;AACtE;AAAA,IACF;AAGA,QAAI,WAAW,KAAK,KAAK,WAAW,QAAQ,MAAM,eAAe,GAAG;AACpE,UAAM,MAAM,KAAK,QAAQ,QAAQ;AAGjC,QAAI,CAAC,KAAK;AACR,iBAAW,KAAK,KAAK,WAAW,YAAY;AAAA,IAC9C;AAEA,QAAI;AACF,YAAM,UAAU,GAAG,aAAa,QAAQ;AACxC,YAAM,OAAO,WAAW,KAAK,QAAQ,QAAQ,CAAC,KAAK;AACnD,UAAI,UAAU,KAAK,EAAE,gBAAgB,KAAK,CAAC;AAC3C,UAAI,IAAI,OAAO;AAAA,IACjB,QAAQ;AACN,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AAAA,IACrB;AAAA,EACF,CAAC;AAGD,QAAM,MAAM,IAAI,gBAAgB,EAAE,OAAO,CAAC;AAG1C,MAAI,UAA+B;AACnC,MAAI;AACF,cAAU,GAAG,MAAM,oBAAoB,MAAM;AAC3C,UAAI;AACF,cAAM,UAAU,GAAG,aAAa,oBAAoB,OAAO;AAC3D,wBAAgB,KAAK,MAAM,OAAO;AAElC,mBAAW,UAAU,IAAI,SAAS;AAChC,cAAI,OAAO,eAAe,GAAG;AAC3B,mBAAO,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,QAAQ,cAAc,CAAC,CAAC;AAAA,UACvE;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,YAAQ,KAAK,yCAAyC;AAAA,EACxD;AAEA,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,WAAO,OAAO,MAAM,MAAM;AACxB,YAAM,MAAM,oBAAoB,IAAI;AACpC,cAAQ,IAAI;AAAA,yBAA4B,GAAG;AAAA,CAAI;AAC/C,cAAQ;AAAA,QACN;AAAA,QACA,OAAO,MAAM;AACX,mBAAS,MAAM;AACf,cAAI,MAAM;AACV,iBAAO,MAAM;AAAA,QACf;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH,CAAC;AACH;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydration-audit/dashboard",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Web dashboard for visualizing JavaScript hydration costs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"require": "./dist/index.cjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@hydration-audit/core": "workspace:*",
|
|
29
|
+
"preact": "^10.24.0",
|
|
30
|
+
"d3-hierarchy": "^3.1.2",
|
|
31
|
+
"ws": "^8.18.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@preact/preset-vite": "^2.9.0",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"@types/ws": "^8.5.0",
|
|
37
|
+
"vite": "^6.0.0",
|
|
38
|
+
"tsup": "^8.3.0",
|
|
39
|
+
"typescript": "^5.7.0",
|
|
40
|
+
"vitest": "^2.1.0"
|
|
41
|
+
}
|
|
42
|
+
}
|