@ozsarman/clarityjs 0.6.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 +178 -0
- package/package.json +168 -0
- package/src/analyze.js +534 -0
- package/src/async-state.js +555 -0
- package/src/bundle-runtime.js +35 -0
- package/src/clarity-bundle.js +332 -0
- package/src/clarity-test.js +622 -0
- package/src/cli.js +453 -0
- package/src/codegen.js +1934 -0
- package/src/dev-server.js +362 -0
- package/src/devtools.js +765 -0
- package/src/edge.js +606 -0
- package/src/error-overlay.js +535 -0
- package/src/file-conventions.js +472 -0
- package/src/font.js +513 -0
- package/src/game-loop.js +106 -0
- package/src/head.js +393 -0
- package/src/hydrate.js +292 -0
- package/src/i18n.js +403 -0
- package/src/image.js +352 -0
- package/src/index.js +193 -0
- package/src/islands.js +284 -0
- package/src/isr.js +306 -0
- package/src/layout.js +342 -0
- package/src/lexer.js +572 -0
- package/src/linter.js +547 -0
- package/src/pages-router.js +229 -0
- package/src/parser.js +1108 -0
- package/src/router.js +732 -0
- package/src/runtime.js +1465 -0
- package/src/scoped-css.js +641 -0
- package/src/server-actions.js +439 -0
- package/src/server-data.js +225 -0
- package/src/sourcemap.js +130 -0
- package/src/ssg.js +310 -0
- package/src/ssr.js +621 -0
- package/src/store.js +276 -0
- package/src/transitions.js +438 -0
- package/src/ts-plugin.js +613 -0
- package/src/typegen.js +240 -0
- package/src/vite-plugin.js +447 -0
- package/types/index.d.ts +366 -0
package/src/analyze.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clarity.js — Bundle Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Reads a Vite/Rollup build output directory and the emitted stats
|
|
5
|
+
* (stats.json or the route manifest) to generate:
|
|
6
|
+
* - An interactive HTML sunburst/treemap chart
|
|
7
|
+
* - A JSON report
|
|
8
|
+
* - A terminal summary
|
|
9
|
+
*
|
|
10
|
+
* Usage (programmatic):
|
|
11
|
+
* import { analyzeBundle } from '@ozsarman/clarityjs/analyze';
|
|
12
|
+
* const report = await analyzeBundle({ distDir: 'dist', open: true });
|
|
13
|
+
*
|
|
14
|
+
* Usage (CLI — called from cli.js):
|
|
15
|
+
* clarity analyze [dist-dir] [--json] [--open] [--out path]
|
|
16
|
+
*
|
|
17
|
+
* Author: Claude (Anthropic) + Özdemir Sarman
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { resolve, relative, extname, basename, join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Analyse a Vite/Rollup dist directory.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.distDir - Path to the dist folder (default: 'dist')
|
|
30
|
+
* @param {string} [opts.outDir] - Where to write the HTML report (default: distDir)
|
|
31
|
+
* @param {string} [opts.outFile]- Report filename (default: 'clarity-analyze.html')
|
|
32
|
+
* @param {boolean} [opts.open] - Open in browser after generation (default: false)
|
|
33
|
+
* @param {boolean} [opts.json] - Also write a .json report
|
|
34
|
+
* @param {boolean} [opts.verbose]
|
|
35
|
+
* @returns {Promise<BundleReport>}
|
|
36
|
+
*/
|
|
37
|
+
export async function analyzeBundle({
|
|
38
|
+
distDir = 'dist',
|
|
39
|
+
outDir,
|
|
40
|
+
outFile = 'clarity-analyze.html',
|
|
41
|
+
open = false,
|
|
42
|
+
json = false,
|
|
43
|
+
verbose = false,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const distAbs = resolve(distDir);
|
|
46
|
+
|
|
47
|
+
if (!existsSync(distAbs)) {
|
|
48
|
+
throw new Error(`[clarity analyze] dist directory not found: ${distAbs}\nRun "clarity build" first.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Scan all .js files in dist ──────────────────────────────────────────────
|
|
52
|
+
const files = _scanFiles(distAbs);
|
|
53
|
+
|
|
54
|
+
if (files.length === 0) {
|
|
55
|
+
throw new Error(`[clarity analyze] No .js files found in ${distAbs}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Build tree ──────────────────────────────────────────────────────────────
|
|
59
|
+
const tree = _buildTree(files, distAbs);
|
|
60
|
+
const report = _buildReport(tree, files, distAbs);
|
|
61
|
+
|
|
62
|
+
if (verbose) {
|
|
63
|
+
_printSummary(report);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Write JSON ──────────────────────────────────────────────────────────────
|
|
67
|
+
const outBase = resolve(outDir ?? distAbs);
|
|
68
|
+
|
|
69
|
+
if (json) {
|
|
70
|
+
const jsonPath = join(outBase, outFile.replace(/\.html?$/, '.json'));
|
|
71
|
+
writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf8');
|
|
72
|
+
if (verbose) console.log(`[clarity analyze] JSON → ${jsonPath}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Write HTML chart ────────────────────────────────────────────────────────
|
|
76
|
+
const html = _renderHTML(report);
|
|
77
|
+
const htmlPath = join(outBase, outFile);
|
|
78
|
+
writeFileSync(htmlPath, html, 'utf8');
|
|
79
|
+
|
|
80
|
+
if (verbose) console.log(`[clarity analyze] Report → ${htmlPath}`);
|
|
81
|
+
|
|
82
|
+
// ── Open in browser ─────────────────────────────────────────────────────────
|
|
83
|
+
if (open) {
|
|
84
|
+
await _openBrowser(htmlPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return report;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── File scanning ────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function _scanFiles(dir) {
|
|
93
|
+
const results = [];
|
|
94
|
+
const IGNORE = new Set(['node_modules', '.git']);
|
|
95
|
+
|
|
96
|
+
function walk(d) {
|
|
97
|
+
for (const name of readdirSync(d)) {
|
|
98
|
+
if (IGNORE.has(name)) continue;
|
|
99
|
+
const full = join(d, name);
|
|
100
|
+
const st = statSync(full);
|
|
101
|
+
if (st.isDirectory()) {
|
|
102
|
+
walk(full);
|
|
103
|
+
} else if (st.isFile()) {
|
|
104
|
+
const ext = extname(name).toLowerCase();
|
|
105
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.css') {
|
|
106
|
+
results.push({ path: full, size: st.size, ext });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
walk(dir);
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Tree builder ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function _buildTree(files, root) {
|
|
119
|
+
const tree = { name: basename(root), children: [], size: 0 };
|
|
120
|
+
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
const rel = relative(root, file.path);
|
|
123
|
+
const parts = rel.split(/[\\/]/);
|
|
124
|
+
let node = tree;
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
127
|
+
let child = node.children.find(c => c.name === parts[i] && c.children);
|
|
128
|
+
if (!child) {
|
|
129
|
+
child = { name: parts[i], children: [], size: 0 };
|
|
130
|
+
node.children.push(child);
|
|
131
|
+
}
|
|
132
|
+
node = child;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const leaf = {
|
|
136
|
+
name: parts[parts.length - 1],
|
|
137
|
+
size: file.size,
|
|
138
|
+
ext: file.ext,
|
|
139
|
+
path: rel,
|
|
140
|
+
children: null,
|
|
141
|
+
};
|
|
142
|
+
node.children.push(leaf);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Roll up sizes
|
|
146
|
+
function rollup(n) {
|
|
147
|
+
if (n.children === null) return n.size;
|
|
148
|
+
n.size = n.children.reduce((sum, c) => sum + rollup(c), 0);
|
|
149
|
+
return n.size;
|
|
150
|
+
}
|
|
151
|
+
rollup(tree);
|
|
152
|
+
|
|
153
|
+
return tree;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Report builder ───────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @typedef {{ total: number, files: FileEntry[], tree: object, chunks: ChunkGroup[] }} BundleReport
|
|
160
|
+
*/
|
|
161
|
+
function _buildReport(tree, files, root) {
|
|
162
|
+
const jsFiles = files.filter(f => f.ext === '.js' || f.ext === '.mjs');
|
|
163
|
+
const cssFiles = files.filter(f => f.ext === '.css');
|
|
164
|
+
|
|
165
|
+
const totalJS = jsFiles.reduce((s, f) => s + f.size, 0);
|
|
166
|
+
const totalCSS = cssFiles.reduce((s, f) => s + f.size, 0);
|
|
167
|
+
const total = files.reduce((s, f) => s + f.size, 0);
|
|
168
|
+
|
|
169
|
+
// Identify chunk types by naming convention
|
|
170
|
+
function chunkType(name) {
|
|
171
|
+
if (name.startsWith('route-')) return 'route';
|
|
172
|
+
if (name.includes('vendor')) return 'vendor';
|
|
173
|
+
if (name.includes('clarity')) return 'framework';
|
|
174
|
+
if (name.includes('index')) return 'entry';
|
|
175
|
+
return 'chunk';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const chunks = jsFiles
|
|
179
|
+
.map(f => ({
|
|
180
|
+
name: basename(f.path, extname(f.path)),
|
|
181
|
+
path: relative(root, f.path),
|
|
182
|
+
size: f.size,
|
|
183
|
+
type: chunkType(basename(f.path)),
|
|
184
|
+
}))
|
|
185
|
+
.sort((a, b) => b.size - a.size);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
generatedAt: new Date().toISOString(),
|
|
189
|
+
distDir: root,
|
|
190
|
+
total,
|
|
191
|
+
totalJS,
|
|
192
|
+
totalCSS,
|
|
193
|
+
fileCount: files.length,
|
|
194
|
+
chunks,
|
|
195
|
+
tree,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Terminal summary ─────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function _printSummary(report) {
|
|
202
|
+
const fmt = n => `${(n / 1024).toFixed(1)} KB`;
|
|
203
|
+
console.log(`\n Bundle Analysis`);
|
|
204
|
+
console.log(` ──────────────────────────────────`);
|
|
205
|
+
console.log(` Total: ${fmt(report.total)} (JS: ${fmt(report.totalJS)}, CSS: ${fmt(report.totalCSS)})`);
|
|
206
|
+
console.log(` Files: ${report.fileCount}`);
|
|
207
|
+
console.log(`\n Largest chunks:`);
|
|
208
|
+
for (const c of report.chunks.slice(0, 8)) {
|
|
209
|
+
const bar = '█'.repeat(Math.max(1, Math.round((c.size / (report.chunks[0]?.size || 1)) * 20)));
|
|
210
|
+
console.log(` ${bar.padEnd(20)} ${fmt(c.size).padStart(9)} ${c.name}`);
|
|
211
|
+
}
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── HTML report renderer ─────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function _renderHTML(report) {
|
|
218
|
+
const dataJson = JSON.stringify(report);
|
|
219
|
+
|
|
220
|
+
return `<!DOCTYPE html>
|
|
221
|
+
<html lang="en">
|
|
222
|
+
<head>
|
|
223
|
+
<meta charset="UTF-8">
|
|
224
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
225
|
+
<title>Clarity Bundle Analyzer</title>
|
|
226
|
+
<style>
|
|
227
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
228
|
+
:root {
|
|
229
|
+
--bg: #1e1e2e; --surface: #181825; --border: #313244;
|
|
230
|
+
--text: #cdd6f4; --muted: #6c7086; --accent: #7c3aed;
|
|
231
|
+
--blue: #89b4fa; --green: #a6e3a1; --yellow: #f9e2af;
|
|
232
|
+
--red: #f38ba8; --pink: #cba6f7; --teal: #94e2d5;
|
|
233
|
+
--orange: #fab387;
|
|
234
|
+
}
|
|
235
|
+
body { background: var(--bg); color: var(--text); font-family: system-ui, sans-serif; font-size: 14px; min-height: 100vh; }
|
|
236
|
+
|
|
237
|
+
.header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; gap: 16px; }
|
|
238
|
+
.header h1 { font-size: 18px; color: var(--green); font-weight: 700; }
|
|
239
|
+
.header .meta { color: var(--muted); font-size: 12px; }
|
|
240
|
+
.header .total { margin-left: auto; font-weight: 700; color: var(--blue); }
|
|
241
|
+
|
|
242
|
+
.stats { display: flex; gap: 12px; padding: 16px 24px; flex-wrap: wrap; }
|
|
243
|
+
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; min-width: 140px; }
|
|
244
|
+
.stat-label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
|
|
245
|
+
.stat-value { font-size: 20px; font-weight: 700; color: var(--blue); margin-top: 4px; }
|
|
246
|
+
|
|
247
|
+
.main { display: grid; grid-template-columns: 1fr 360px; gap: 0; height: calc(100vh - 160px); }
|
|
248
|
+
|
|
249
|
+
/* Sunburst/Treemap */
|
|
250
|
+
.chart-panel { padding: 16px 24px; overflow: hidden; display: flex; flex-direction: column; gap: 12px; }
|
|
251
|
+
.chart-title { font-size: 13px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
|
|
252
|
+
#chart-svg { flex: 1; width: 100%; }
|
|
253
|
+
.tooltip {
|
|
254
|
+
position: fixed; background: var(--surface); border: 1px solid var(--border);
|
|
255
|
+
border-radius: 8px; padding: 10px 14px; font-size: 12px; pointer-events: none;
|
|
256
|
+
box-shadow: 0 4px 20px rgba(0,0,0,.4); z-index: 9999; max-width: 280px;
|
|
257
|
+
}
|
|
258
|
+
.tooltip .tt-name { font-weight: 700; color: var(--text); margin-bottom: 4px; word-break: break-all; }
|
|
259
|
+
.tooltip .tt-size { color: var(--blue); }
|
|
260
|
+
.tooltip .tt-pct { color: var(--muted); }
|
|
261
|
+
|
|
262
|
+
/* Chunks list */
|
|
263
|
+
.list-panel { background: var(--surface); border-left: 1px solid var(--border); overflow-y: auto; padding: 12px 16px; }
|
|
264
|
+
.list-panel h2 { font-size: 12px; color: var(--muted); text-transform: uppercase; margin-bottom: 12px; letter-spacing: .05em; }
|
|
265
|
+
.chunk-item { display: flex; align-items: center; gap: 8px; padding: 7px 0; border-bottom: 1px solid var(--border); }
|
|
266
|
+
.chunk-item:last-child { border-bottom: none; }
|
|
267
|
+
.chunk-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
268
|
+
.chunk-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
269
|
+
.chunk-type { font-size: 10px; color: var(--muted); padding: 1px 6px; border-radius: 10px; background: var(--border); flex-shrink: 0; }
|
|
270
|
+
.chunk-size { font-size: 11px; color: var(--blue); font-weight: 600; flex-shrink: 0; }
|
|
271
|
+
.chunk-bar { display: block; height: 3px; background: var(--accent); border-radius: 2px; margin-top: 3px; }
|
|
272
|
+
|
|
273
|
+
.filter-row { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
|
274
|
+
.filter-btn {
|
|
275
|
+
padding: 3px 10px; border-radius: 10px; border: 1px solid var(--border);
|
|
276
|
+
background: none; color: var(--muted); font-size: 11px; cursor: pointer;
|
|
277
|
+
}
|
|
278
|
+
.filter-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
279
|
+
</style>
|
|
280
|
+
</head>
|
|
281
|
+
<body>
|
|
282
|
+
<div class="header">
|
|
283
|
+
<h1>⚡ Clarity Bundle Analyzer</h1>
|
|
284
|
+
<div class="meta">Generated ${new Date(report.generatedAt).toLocaleString()}</div>
|
|
285
|
+
<div class="total">Total: ${_fmtKB(report.total)}</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="stats">
|
|
289
|
+
<div class="stat"><div class="stat-label">JavaScript</div><div class="stat-value">${_fmtKB(report.totalJS)}</div></div>
|
|
290
|
+
<div class="stat"><div class="stat-label">CSS</div><div class="stat-value">${_fmtKB(report.totalCSS)}</div></div>
|
|
291
|
+
<div class="stat"><div class="stat-label">Files</div><div class="stat-value">${report.fileCount}</div></div>
|
|
292
|
+
<div class="stat"><div class="stat-label">Chunks</div><div class="stat-value">${report.chunks.length}</div></div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div class="main">
|
|
296
|
+
<div class="chart-panel">
|
|
297
|
+
<div class="chart-title">Treemap — click to drill down</div>
|
|
298
|
+
<svg id="chart-svg"></svg>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="list-panel">
|
|
302
|
+
<h2>Chunks</h2>
|
|
303
|
+
<div class="filter-row" id="filters"></div>
|
|
304
|
+
<div id="chunk-list"></div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<div class="tooltip" id="tooltip" style="display:none"></div>
|
|
309
|
+
|
|
310
|
+
<script>
|
|
311
|
+
const DATA = ${dataJson};
|
|
312
|
+
|
|
313
|
+
// ── Palette ──
|
|
314
|
+
const PALETTE = {
|
|
315
|
+
route: '#7c3aed',
|
|
316
|
+
vendor: '#89b4fa',
|
|
317
|
+
framework: '#a6e3a1',
|
|
318
|
+
entry: '#fab387',
|
|
319
|
+
chunk: '#6c7086',
|
|
320
|
+
css: '#94e2d5',
|
|
321
|
+
};
|
|
322
|
+
function colourFor(type) { return PALETTE[type] || PALETTE.chunk; }
|
|
323
|
+
function fmtKB(n) { return n >= 1024 ? (n/1024).toFixed(1)+' KB' : n+' B'; }
|
|
324
|
+
function fmtPct(n, total) { return (n/total*100).toFixed(1)+'%'; }
|
|
325
|
+
|
|
326
|
+
// ── Chunk list ──────────────────────────────────────────────────────────────
|
|
327
|
+
let _activeFilter = 'all';
|
|
328
|
+
const maxSize = DATA.chunks[0]?.size || 1;
|
|
329
|
+
|
|
330
|
+
function renderChunkList(filter) {
|
|
331
|
+
const list = document.getElementById('chunk-list');
|
|
332
|
+
const items = filter === 'all' ? DATA.chunks : DATA.chunks.filter(c => c.type === filter);
|
|
333
|
+
list.innerHTML = items.map(c => \`
|
|
334
|
+
<div class="chunk-item">
|
|
335
|
+
<span class="chunk-dot" style="background:\${colourFor(c.type)}"></span>
|
|
336
|
+
<div style="flex:1;overflow:hidden">
|
|
337
|
+
<div class="chunk-name" title="\${c.path}">\${c.name}</div>
|
|
338
|
+
<span class="chunk-bar" style="width:\${Math.max(2, Math.round(c.size/maxSize*200))}px"></span>
|
|
339
|
+
</div>
|
|
340
|
+
<span class="chunk-type">\${c.type}</span>
|
|
341
|
+
<span class="chunk-size">\${fmtKB(c.size)}</span>
|
|
342
|
+
</div>
|
|
343
|
+
\`).join('');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function renderFilters() {
|
|
347
|
+
const types = ['all', ...new Set(DATA.chunks.map(c => c.type))];
|
|
348
|
+
const el = document.getElementById('filters');
|
|
349
|
+
el.innerHTML = types.map(t => \`
|
|
350
|
+
<button class="filter-btn\${t === _activeFilter ? ' active' : ''}" data-type="\${t}">\${t}</button>
|
|
351
|
+
\`).join('');
|
|
352
|
+
el.querySelectorAll('.filter-btn').forEach(btn => {
|
|
353
|
+
btn.addEventListener('click', () => {
|
|
354
|
+
_activeFilter = btn.dataset.type;
|
|
355
|
+
renderFilters();
|
|
356
|
+
renderChunkList(_activeFilter);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
renderFilters();
|
|
362
|
+
renderChunkList('all');
|
|
363
|
+
|
|
364
|
+
// ── Treemap (squarified) ────────────────────────────────────────────────────
|
|
365
|
+
const svg = document.getElementById('chart-svg');
|
|
366
|
+
const tt = document.getElementById('tooltip');
|
|
367
|
+
|
|
368
|
+
function getRect() {
|
|
369
|
+
const r = svg.getBoundingClientRect();
|
|
370
|
+
return { x: 0, y: 0, w: r.width || 800, h: r.height || 500 };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Squarified treemap algorithm
|
|
374
|
+
function squarify(items, rect) {
|
|
375
|
+
if (!items.length) return [];
|
|
376
|
+
items = [...items].sort((a, b) => b.size - a.size);
|
|
377
|
+
|
|
378
|
+
const totalSize = items.reduce((s, i) => s + i.size, 0);
|
|
379
|
+
const rects = [];
|
|
380
|
+
let remaining = [...items];
|
|
381
|
+
let { x, y, w, h } = rect;
|
|
382
|
+
|
|
383
|
+
while (remaining.length) {
|
|
384
|
+
const row = [];
|
|
385
|
+
let rowSize = 0;
|
|
386
|
+
let i = 0;
|
|
387
|
+
|
|
388
|
+
while (i < remaining.length) {
|
|
389
|
+
row.push(remaining[i]);
|
|
390
|
+
rowSize += remaining[i].size;
|
|
391
|
+
const ratio = _worstRatio(row, rowSize, w, h, totalSize);
|
|
392
|
+
if (i > 0 && ratio > _worstRatio(row.slice(0, -1), rowSize - remaining[i].size, w, h, totalSize)) {
|
|
393
|
+
row.pop();
|
|
394
|
+
rowSize -= remaining[i].size;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
i++;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const rowFrac = rowSize / totalSize;
|
|
401
|
+
const shorter = Math.min(w, h);
|
|
402
|
+
const longer = (w <= h ? w : h) * (rowFrac / (shorter / Math.max(w, h)));
|
|
403
|
+
|
|
404
|
+
let off = w <= h ? x : y;
|
|
405
|
+
for (const item of row) {
|
|
406
|
+
const frac = item.size / rowSize;
|
|
407
|
+
const len = shorter * (rowFrac * (w <= h ? w : h) / shorter) * frac;
|
|
408
|
+
if (w <= h) {
|
|
409
|
+
rects.push({ item, x: off, y, w: len, h: shorter * rowFrac });
|
|
410
|
+
off += len;
|
|
411
|
+
} else {
|
|
412
|
+
rects.push({ item, x, y: off, w: shorter * rowFrac, h: len });
|
|
413
|
+
off += len;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (w <= h) { y += shorter * rowFrac; h -= shorter * rowFrac; }
|
|
418
|
+
else { x += shorter * rowFrac; w -= shorter * rowFrac; }
|
|
419
|
+
|
|
420
|
+
totalSize && (totalSize - rowSize);
|
|
421
|
+
remaining = remaining.filter(r => !row.includes(r));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return rects;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function _worstRatio(row, rowSize, w, h, total) {
|
|
428
|
+
if (!row.length) return Infinity;
|
|
429
|
+
const shorter = Math.min(w, h);
|
|
430
|
+
const rowW = Math.max(w, h) * (rowSize / total);
|
|
431
|
+
let worst = 0;
|
|
432
|
+
for (const item of row) {
|
|
433
|
+
const cellH = shorter * (rowSize / total) * (item.size / rowSize);
|
|
434
|
+
const r = Math.max(rowW / cellH, cellH / rowW);
|
|
435
|
+
if (r > worst) worst = r;
|
|
436
|
+
}
|
|
437
|
+
return worst;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderTreemap() {
|
|
441
|
+
svg.innerHTML = '';
|
|
442
|
+
const { w, h } = getRect();
|
|
443
|
+
svg.setAttribute('viewBox', \`0 0 \${w} \${h}\`);
|
|
444
|
+
|
|
445
|
+
const leaves = DATA.chunks.map(c => ({ ...c, size: c.size }));
|
|
446
|
+
const rects = squarify(leaves, { x: 0, y: 0, w, h });
|
|
447
|
+
|
|
448
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
449
|
+
const PAD = 2;
|
|
450
|
+
|
|
451
|
+
for (const { item, x, y, w: rw, h: rh } of rects) {
|
|
452
|
+
if (rw < 2 || rh < 2) continue;
|
|
453
|
+
|
|
454
|
+
const g = document.createElementNS(ns, 'g');
|
|
455
|
+
|
|
456
|
+
const rect = document.createElementNS(ns, 'rect');
|
|
457
|
+
rect.setAttribute('x', x + PAD);
|
|
458
|
+
rect.setAttribute('y', y + PAD);
|
|
459
|
+
rect.setAttribute('width', Math.max(0, rw - PAD * 2));
|
|
460
|
+
rect.setAttribute('height', Math.max(0, rh - PAD * 2));
|
|
461
|
+
rect.setAttribute('rx', 4);
|
|
462
|
+
rect.setAttribute('fill', colourFor(item.type));
|
|
463
|
+
rect.setAttribute('fill-opacity', '0.85');
|
|
464
|
+
rect.setAttribute('stroke', '#1e1e2e');
|
|
465
|
+
rect.setAttribute('stroke-width', '1');
|
|
466
|
+
rect.style.cursor = 'pointer';
|
|
467
|
+
g.appendChild(rect);
|
|
468
|
+
|
|
469
|
+
if (rw > 50 && rh > 24) {
|
|
470
|
+
const label = document.createElementNS(ns, 'text');
|
|
471
|
+
label.setAttribute('x', x + PAD + 6);
|
|
472
|
+
label.setAttribute('y', y + PAD + 15);
|
|
473
|
+
label.setAttribute('font-size', Math.min(13, rw / 7));
|
|
474
|
+
label.setAttribute('fill', '#fff');
|
|
475
|
+
label.setAttribute('font-family', 'monospace');
|
|
476
|
+
label.setAttribute('font-weight', '600');
|
|
477
|
+
label.setAttribute('clip-path', \`url(#clip-\${item.name})\`);
|
|
478
|
+
label.textContent = item.name;
|
|
479
|
+
g.appendChild(label);
|
|
480
|
+
|
|
481
|
+
if (rh > 38) {
|
|
482
|
+
const sub = document.createElementNS(ns, 'text');
|
|
483
|
+
sub.setAttribute('x', x + PAD + 6);
|
|
484
|
+
sub.setAttribute('y', y + PAD + 30);
|
|
485
|
+
sub.setAttribute('font-size', 10);
|
|
486
|
+
sub.setAttribute('fill', 'rgba(255,255,255,0.7)');
|
|
487
|
+
sub.setAttribute('font-family', 'system-ui');
|
|
488
|
+
sub.textContent = fmtKB(item.size);
|
|
489
|
+
g.appendChild(sub);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Tooltip events
|
|
494
|
+
g.addEventListener('mouseenter', (e) => {
|
|
495
|
+
tt.style.display = 'block';
|
|
496
|
+
tt.innerHTML = \`
|
|
497
|
+
<div class="tt-name">\${item.name}</div>
|
|
498
|
+
<div class="tt-size">\${fmtKB(item.size)}</div>
|
|
499
|
+
<div class="tt-pct">\${fmtPct(item.size, DATA.totalJS)} of JS</div>
|
|
500
|
+
<div style="color:#6c7086;font-size:10px;margin-top:4px">\${item.path}</div>
|
|
501
|
+
\`;
|
|
502
|
+
});
|
|
503
|
+
g.addEventListener('mousemove', (e) => {
|
|
504
|
+
tt.style.left = (e.clientX + 16) + 'px';
|
|
505
|
+
tt.style.top = (e.clientY - 10) + 'px';
|
|
506
|
+
});
|
|
507
|
+
g.addEventListener('mouseleave', () => { tt.style.display = 'none'; });
|
|
508
|
+
|
|
509
|
+
svg.appendChild(g);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
renderTreemap();
|
|
514
|
+
window.addEventListener('resize', renderTreemap);
|
|
515
|
+
</script>
|
|
516
|
+
</body>
|
|
517
|
+
</html>`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function _fmtKB(n) {
|
|
521
|
+
if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
522
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
523
|
+
return `${n} B`;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function _openBrowser(filePath) {
|
|
527
|
+
const { platform } = process;
|
|
528
|
+
const url = `file://${filePath}`;
|
|
529
|
+
const cmd = platform === 'darwin' ? 'open'
|
|
530
|
+
: platform === 'win32' ? 'start'
|
|
531
|
+
: 'xdg-open';
|
|
532
|
+
const { exec } = await import('node:child_process');
|
|
533
|
+
exec(`${cmd} "${url}"`);
|
|
534
|
+
}
|