bunx-ray 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/PUBLISHING.md +87 -0
- package/README.md +115 -0
- package/dist/bundle.d.ts +21 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +94 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/report.d.ts +17 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +43 -0
- package/meta.json +36 -0
- package/package.json +34 -0
- package/src/bundle.ts +115 -0
- package/src/cli.ts +97 -0
- package/src/index.ts +11 -0
- package/src/report.ts +68 -0
- package/src-esbuild/index.ts +2 -0
- package/src-esbuild/math.ts +1 -0
- package/src-vite/index.ts +2 -0
- package/src-vite/math.ts +1 -0
- package/src-webpack/index.js +5 -0
- package/src-webpack/math.js +1 -0
- package/test/__snapshots__/bundle.test.ts.snap +34 -0
- package/test/bundle.test.ts +31 -0
- package/test/cli.test.ts +28 -0
- package/tsconfig.json +17 -0
- package/vite.sample.js +15 -0
- package/webpack.sample.js +13 -0
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Publishing `bunx-ray` to npm
|
|
2
|
+
|
|
3
|
+
> Quick reference for future releases. Assumes you already ran `npm login` (or `npm adduser`).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Build before publishing
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run build # compiles TypeScript to dist/
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`package.json` already has:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"files": ["dist"]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
So running `npm publish` will automatically rebuild and only ship the compiled JS.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Choose a package name & scope
|
|
25
|
+
|
|
26
|
+
| Package name value | Default visibility | Can be private? |
|
|
27
|
+
| ------------------------- | ------------------ | --------------- |
|
|
28
|
+
| `"name": "bunx-ray"` | Public | **No** |
|
|
29
|
+
| `"name": "@you/bunx-ray"` | Private | Yes |
|
|
30
|
+
|
|
31
|
+
_Change the `name` field if needed._
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. Publish commands
|
|
36
|
+
|
|
37
|
+
### A) Un-scoped, public (most common)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm version patch # bump 0.1.x → 0.1.(x+1)
|
|
41
|
+
npm publish # publishes publicly
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### B) Scoped & public
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm version minor # or patch / major
|
|
48
|
+
npm publish --access public
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### C) Scoped & private (default for scoped)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm version patch
|
|
55
|
+
npm publish --access restricted # or simply: npm publish
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 4. Extras
|
|
61
|
+
|
|
62
|
+
- **Dry-run** – see what would be published:
|
|
63
|
+
```bash
|
|
64
|
+
npm publish --dry-run
|
|
65
|
+
```
|
|
66
|
+
- **Dist-tags** – publish a prerelease:
|
|
67
|
+
```bash
|
|
68
|
+
npm publish --tag next # install with npm i bunx-ray@next
|
|
69
|
+
```
|
|
70
|
+
- **Private registry**:
|
|
71
|
+
```bash
|
|
72
|
+
npm publish --registry https://registry.my-company.com/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 5. Verify
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm view bunx-ray version # expect the new version
|
|
81
|
+
npm i -g bunx-ray # test global install
|
|
82
|
+
bunx-ray --help # ensure binary runs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
Happy shipping! 🚀
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# bunx-ray
|
|
2
|
+
|
|
3
|
+
**ASCII heat-map bundle viewer** – inspect JavaScript bundle composition right in your terminal (CI-friendly, SSH-friendly, browser-free).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Install & run
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# global (recommended)
|
|
11
|
+
npm install -g bunx-ray
|
|
12
|
+
|
|
13
|
+
# or one-off
|
|
14
|
+
npx bunx-ray <stats.json>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
bunx-ray [stats.json] [flags]
|
|
21
|
+
|
|
22
|
+
Flags
|
|
23
|
+
--webpack Treat input as Webpack stats (default auto-detect)
|
|
24
|
+
--vite Treat input as Vite / Rollup stats
|
|
25
|
+
--esbuild Treat input as esbuild metafile
|
|
26
|
+
--cols <n> Terminal columns (default 80)
|
|
27
|
+
--rows <n> Terminal rows (default 24)
|
|
28
|
+
--top <n> Show N largest modules (default 10)
|
|
29
|
+
--grid-only Only print grid (no legend / summary)
|
|
30
|
+
--no-legend Hide legend line
|
|
31
|
+
--no-summary Hide bundle summary
|
|
32
|
+
-h, --help Show help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Quick demo (no bundler needed)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bunx-ray $(npx bunx-ray demo) # renders a built-in sample
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
_(`bunx-ray demo` prints a temp stats file path; handy for first-time tryout.)_
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Real-world recipes
|
|
46
|
+
|
|
47
|
+
### Webpack ≥ 4
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx webpack --stats-json # writes stats.json
|
|
51
|
+
bunx-ray stats.json # view heat-map
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Vite v5 / Rollup
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
vite build --stats.writeTo stats.json
|
|
58
|
+
bunx-ray --vite stats.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### esbuild
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
esbuild src/index.ts --bundle --metafile=meta.json --outfile=/dev/null
|
|
65
|
+
bunx-ray --esbuild meta.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## TypeScript API
|
|
71
|
+
|
|
72
|
+
Install as a normal dependency and import what you need:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { normalizeWebpack, treemap, draw, Mod } from "bunx-ray";
|
|
76
|
+
|
|
77
|
+
const mods: Mod[] = normalizeWebpack(
|
|
78
|
+
JSON.parse(readFileSync("stats.json", "utf8"))
|
|
79
|
+
);
|
|
80
|
+
console.log(draw(treemap(mods, 80, 24)));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
All `.d.ts` files ship with the package—no extra `@types` install required.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Why text over HTML?
|
|
88
|
+
|
|
89
|
+
- Works in CI logs, SSH sessions, Codespaces, headless Docker containers.
|
|
90
|
+
- Diff-friendly → fail PR when a module grows past your budget.
|
|
91
|
+
- Zero browser animations = instant feedback.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Contributing / local playground
|
|
96
|
+
|
|
97
|
+
Clone the repo and:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install # install dev deps
|
|
101
|
+
npm run build # compile TypeScript → dist/
|
|
102
|
+
|
|
103
|
+
# sample bundles
|
|
104
|
+
npm run sample:webpack # outputs dist-webpack/stats.json
|
|
105
|
+
npm run sample:vite # outputs dist-vite/stats.json
|
|
106
|
+
npm run sample:esbuild # outputs dist-esbuild/meta.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Run tests:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm test
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Feel free to tweak the `src-*` sample sources to watch the heat-map change.
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Mod = {
|
|
2
|
+
path: string;
|
|
3
|
+
size: number;
|
|
4
|
+
};
|
|
5
|
+
export interface Cell {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
w: number;
|
|
9
|
+
h: number;
|
|
10
|
+
mod: Mod;
|
|
11
|
+
}
|
|
12
|
+
export declare function treemap(mods: Mod[], W?: number, H?: number): Cell[];
|
|
13
|
+
export declare function draw(cells: Cell[], W?: number, H?: number): string;
|
|
14
|
+
export declare function normalizeWebpack(stats: any): Mod[];
|
|
15
|
+
export declare function normalizeVite(stats: any): Mod[];
|
|
16
|
+
export declare function normalizeEsbuild(meta: any): Mod[];
|
|
17
|
+
export declare const SIZE_THRESHOLDS: number[];
|
|
18
|
+
export declare function formatSize(bytes: number): string;
|
|
19
|
+
export declare function totalSize(mods: Mod[]): number;
|
|
20
|
+
export declare function topModules(mods: Mod[], n?: number): Mod[];
|
|
21
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,GAAG,GAAG;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,WAAW,IAAI;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,EAAE,GAAG,CAAC;CACV;AAGD,wBAAgB,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,SAAK,EAAE,CAAC,SAAK,GAAG,IAAI,EAAE,CAiB3D;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,SAAK,EAAE,CAAC,SAAK,GAAG,MAAM,CAe1D;AAGD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE,CAalD;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE,CAa/C;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,EAAE,CAUjD;AAID,eAAO,MAAM,eAAe,UAAqC,CAAC;AAElE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAE7C;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,SAAK,GAAG,GAAG,EAAE,CAErD"}
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// simple slice-and-dice treemap mapped to character grid
|
|
2
|
+
export function treemap(mods, W = 80, H = 24) {
|
|
3
|
+
const cells = [];
|
|
4
|
+
let x = 0, y = 0, rowH = H;
|
|
5
|
+
const total = mods.reduce((a, m) => a + m.size, 0);
|
|
6
|
+
for (const m of mods) {
|
|
7
|
+
const frac = m.size / total;
|
|
8
|
+
const w = Math.max(1, Math.round((frac * W * H) / rowH));
|
|
9
|
+
if (x + w > W) {
|
|
10
|
+
y += rowH;
|
|
11
|
+
x = 0;
|
|
12
|
+
}
|
|
13
|
+
cells.push({ x, y, w, h: rowH, mod: m });
|
|
14
|
+
x += w;
|
|
15
|
+
}
|
|
16
|
+
return cells;
|
|
17
|
+
}
|
|
18
|
+
export function draw(cells, W = 80, H = 24) {
|
|
19
|
+
const grid = Array.from({ length: H }, () => Array(W).fill(' '));
|
|
20
|
+
const shades = ['░', '▒', '▓', '█'];
|
|
21
|
+
const max = Math.max(...cells.map((c) => c.mod.size));
|
|
22
|
+
for (const c of cells) {
|
|
23
|
+
const shade = shades[Math.floor((c.mod.size / max) * (shades.length - 1))];
|
|
24
|
+
for (let i = 0; i < c.h; i++) {
|
|
25
|
+
for (let j = 0; j < c.w; j++) {
|
|
26
|
+
if (c.y + i < H && c.x + j < W) {
|
|
27
|
+
grid[c.y + i][c.x + j] = shade;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return grid.map((r) => r.join('')).join('\n');
|
|
33
|
+
}
|
|
34
|
+
// Normalize Webpack stats → Mod[]
|
|
35
|
+
export function normalizeWebpack(stats) {
|
|
36
|
+
const mods = [];
|
|
37
|
+
if (Array.isArray(stats.modules)) {
|
|
38
|
+
for (const m of stats.modules) {
|
|
39
|
+
const size = m.size ?? m.parsedSize ?? 0;
|
|
40
|
+
const name = m.name ?? m.identifier ?? '';
|
|
41
|
+
if (size && name)
|
|
42
|
+
mods.push({ path: name, size });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (Array.isArray(stats.children)) {
|
|
46
|
+
for (const child of stats.children)
|
|
47
|
+
mods.push(...normalizeWebpack(child));
|
|
48
|
+
}
|
|
49
|
+
mods.sort((a, b) => b.size - a.size);
|
|
50
|
+
return mods;
|
|
51
|
+
}
|
|
52
|
+
// Vite / Rollup stats (array of outputs with modules)
|
|
53
|
+
export function normalizeVite(stats) {
|
|
54
|
+
const mods = [];
|
|
55
|
+
const outputs = Array.isArray(stats.output) ? stats.output : [stats.output ?? stats];
|
|
56
|
+
for (const out of outputs) {
|
|
57
|
+
if (!out || !out.modules)
|
|
58
|
+
continue;
|
|
59
|
+
const modulesObj = out.modules;
|
|
60
|
+
for (const [p, m] of Object.entries(modulesObj)) {
|
|
61
|
+
const size = m.renderedLength ?? m.renderedSize ?? m.originalLength ?? m.size ?? 0;
|
|
62
|
+
mods.push({ path: p, size });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
mods.sort((a, b) => b.size - a.size);
|
|
66
|
+
return mods;
|
|
67
|
+
}
|
|
68
|
+
// esbuild metafile JSON
|
|
69
|
+
export function normalizeEsbuild(meta) {
|
|
70
|
+
const mods = [];
|
|
71
|
+
if (meta.inputs && typeof meta.inputs === 'object') {
|
|
72
|
+
for (const [p, info] of Object.entries(meta.inputs)) {
|
|
73
|
+
const size = info.bytes ?? 0;
|
|
74
|
+
mods.push({ path: p, size });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
mods.sort((a, b) => b.size - a.size);
|
|
78
|
+
return mods;
|
|
79
|
+
}
|
|
80
|
+
// ---------------- Utilities ------------------
|
|
81
|
+
export const SIZE_THRESHOLDS = [10 * 1024, 50 * 1024, 100 * 1024]; // bytes
|
|
82
|
+
export function formatSize(bytes) {
|
|
83
|
+
if (bytes >= 1024 * 1024)
|
|
84
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
85
|
+
if (bytes >= 1024)
|
|
86
|
+
return (bytes / 1024).toFixed(1) + ' KB';
|
|
87
|
+
return bytes + ' B';
|
|
88
|
+
}
|
|
89
|
+
export function totalSize(mods) {
|
|
90
|
+
return mods.reduce((a, m) => a + m.size, 0);
|
|
91
|
+
}
|
|
92
|
+
export function topModules(mods, n = 10) {
|
|
93
|
+
return [...mods].sort((a, b) => b.size - a.size).slice(0, n);
|
|
94
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ---- CLI -----------------------------------------------------------------
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { normalizeWebpack, normalizeVite, normalizeEsbuild } from './bundle.js';
|
|
7
|
+
import { renderReport } from './report.js';
|
|
8
|
+
function main() {
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('bunx-ray')
|
|
12
|
+
.description('ASCII heat-map bundle viewer')
|
|
13
|
+
.argument('<stats>', 'Build stats JSON file')
|
|
14
|
+
.option('--webpack', 'Input is Webpack stats (default)')
|
|
15
|
+
.option('--vite', 'Input is Vite/Rollup stats')
|
|
16
|
+
.option('--esbuild', 'Input is esbuild metafile')
|
|
17
|
+
.option('--cols <number>', 'Terminal columns (default 80)', '80')
|
|
18
|
+
.option('--rows <number>', 'Terminal rows (default 24)', '24')
|
|
19
|
+
.option('--top <number>', 'Show N largest modules (default 10)', '10')
|
|
20
|
+
.option('--no-legend', 'Hide legend line')
|
|
21
|
+
.option('--no-summary', 'Hide summary line')
|
|
22
|
+
.option('--grid-only', 'Only print grid (implies --no-legend --no-summary)')
|
|
23
|
+
.option('--demo', 'Render built-in demo heat-map')
|
|
24
|
+
.parse(process.argv);
|
|
25
|
+
const opts = program.opts();
|
|
26
|
+
const file = program.args[0];
|
|
27
|
+
const cols = Number(opts.cols);
|
|
28
|
+
const rows = Number(opts.rows);
|
|
29
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows)) {
|
|
30
|
+
console.error(chalk.red('Error: --cols and --rows must be numbers'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const raw = fs.readFileSync(path.resolve(process.cwd(), file), 'utf8');
|
|
34
|
+
let stats;
|
|
35
|
+
try {
|
|
36
|
+
stats = JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error(chalk.red(`Failed to parse JSON from ${file}`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Choose adapter based on flags or auto-detect.
|
|
43
|
+
let mods = [];
|
|
44
|
+
if (opts.webpack) {
|
|
45
|
+
mods = normalizeWebpack(stats);
|
|
46
|
+
}
|
|
47
|
+
else if (opts.vite) {
|
|
48
|
+
mods = normalizeVite(stats);
|
|
49
|
+
}
|
|
50
|
+
else if (opts.esbuild) {
|
|
51
|
+
mods = normalizeEsbuild(stats);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// auto-detect simple heuristics
|
|
55
|
+
if (stats.inputs && stats.outputs)
|
|
56
|
+
mods = normalizeEsbuild(stats);
|
|
57
|
+
else if (stats.modules || stats.children)
|
|
58
|
+
mods = normalizeWebpack(stats);
|
|
59
|
+
else if (stats.output)
|
|
60
|
+
mods = normalizeVite(stats);
|
|
61
|
+
else {
|
|
62
|
+
console.error(chalk.red('Unable to detect stats format; please pass --webpack | --vite | --esbuild'));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (mods.length === 0) {
|
|
67
|
+
console.error(chalk.yellow('No modules found in stats file.'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const report = renderReport(mods, {
|
|
71
|
+
cols,
|
|
72
|
+
rows,
|
|
73
|
+
top: Number(opts.top ?? 10),
|
|
74
|
+
legend: opts.legend !== false && !opts.gridOnly,
|
|
75
|
+
summary: opts.summary !== false && !opts.gridOnly,
|
|
76
|
+
color: true,
|
|
77
|
+
});
|
|
78
|
+
if (report.legendLine)
|
|
79
|
+
console.log(report.legendLine);
|
|
80
|
+
if (report.summaryLine)
|
|
81
|
+
console.log(report.summaryLine);
|
|
82
|
+
console.log('\n' + report.grid + '\n');
|
|
83
|
+
report.tableLines.forEach((l) => console.log(l));
|
|
84
|
+
}
|
|
85
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,OAAO,EACP,IAAI,EACJ,UAAU,EACV,SAAS,EACT,UAAU,GACX,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { normalizeWebpack, normalizeVite, normalizeEsbuild, treemap, draw, formatSize, totalSize, topModules, } from './bundle.js';
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Mod } from './bundle.js';
|
|
2
|
+
export interface ReportOptions {
|
|
3
|
+
cols: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
top: number;
|
|
6
|
+
legend: boolean;
|
|
7
|
+
summary: boolean;
|
|
8
|
+
color: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface RenderedReport {
|
|
11
|
+
legendLine?: string;
|
|
12
|
+
summaryLine?: string;
|
|
13
|
+
grid: string;
|
|
14
|
+
tableLines: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function renderReport(mods: Mod[], opts: ReportOptions): RenderedReport;
|
|
17
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAoD,MAAM,aAAa,CAAC;AAEpF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAaD,MAAM,WAAW,cAAc;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,aAAa,GAAG,cAAc,CAsC7E"}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { treemap, draw, formatSize, totalSize, topModules } from './bundle.js';
|
|
2
|
+
const SHADES = ['░', '▒', '▓', '█'];
|
|
3
|
+
function calcThresholds(max) {
|
|
4
|
+
return [0.25, 0.5, 0.75].map((p) => max * p);
|
|
5
|
+
}
|
|
6
|
+
function shadeForSize(size, max) {
|
|
7
|
+
const idx = Math.floor((size / max) * (SHADES.length - 1));
|
|
8
|
+
return SHADES[idx];
|
|
9
|
+
}
|
|
10
|
+
export function renderReport(mods, opts) {
|
|
11
|
+
const { cols, rows, top, legend, summary } = opts;
|
|
12
|
+
const max = Math.max(...mods.map((m) => m.size));
|
|
13
|
+
const thresholds = calcThresholds(max);
|
|
14
|
+
// grid
|
|
15
|
+
const grid = draw(treemap(mods, cols, rows), cols, rows);
|
|
16
|
+
// legend
|
|
17
|
+
let legendLine;
|
|
18
|
+
if (legend) {
|
|
19
|
+
legendLine =
|
|
20
|
+
'Legend ' +
|
|
21
|
+
[
|
|
22
|
+
`${SHADES[3]} >${formatSize(thresholds[2])}`,
|
|
23
|
+
`${SHADES[2]} ${formatSize(thresholds[1])}-${formatSize(thresholds[2])}`,
|
|
24
|
+
`${SHADES[1]} ${formatSize(thresholds[0])}-${formatSize(thresholds[1])}`,
|
|
25
|
+
`${SHADES[0]} <${formatSize(thresholds[0])}`,
|
|
26
|
+
].join(' ');
|
|
27
|
+
}
|
|
28
|
+
// summary
|
|
29
|
+
let summaryLine;
|
|
30
|
+
if (summary) {
|
|
31
|
+
summaryLine = `Total bundle: ${formatSize(totalSize(mods))} | modules: ${mods.length}`;
|
|
32
|
+
}
|
|
33
|
+
// table
|
|
34
|
+
const list = topModules(mods, top);
|
|
35
|
+
const tableLines = [`Top ${list.length} modules`];
|
|
36
|
+
list.forEach((m, idx) => {
|
|
37
|
+
const shade = shadeForSize(m.size, max);
|
|
38
|
+
const pct = ((m.size / totalSize(mods)) * 100).toFixed(1).padStart(4);
|
|
39
|
+
const name = m.path.length > 28 ? '…' + m.path.slice(-27) : m.path.padEnd(28);
|
|
40
|
+
tableLines.push(`${(idx + 1).toString().padStart(2)} ${shade} ${name} ${formatSize(m.size).padStart(8)} (${pct}%)`);
|
|
41
|
+
});
|
|
42
|
+
return { legendLine, summaryLine, grid, tableLines };
|
|
43
|
+
}
|
package/meta.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"inputs": {
|
|
3
|
+
"src-esbuild/math.ts": {
|
|
4
|
+
"bytes": 60,
|
|
5
|
+
"imports": [],
|
|
6
|
+
"format": "esm"
|
|
7
|
+
},
|
|
8
|
+
"src-esbuild/index.ts": {
|
|
9
|
+
"bytes": 71,
|
|
10
|
+
"imports": [
|
|
11
|
+
{
|
|
12
|
+
"path": "src-esbuild/math.ts",
|
|
13
|
+
"kind": "import-statement",
|
|
14
|
+
"original": "./math"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"format": "esm"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"outputs": {
|
|
21
|
+
"../../../../../dev/null": {
|
|
22
|
+
"imports": [],
|
|
23
|
+
"exports": [],
|
|
24
|
+
"entryPoint": "src-esbuild/index.ts",
|
|
25
|
+
"inputs": {
|
|
26
|
+
"src-esbuild/math.ts": {
|
|
27
|
+
"bytesInOutput": 29
|
|
28
|
+
},
|
|
29
|
+
"src-esbuild/index.ts": {
|
|
30
|
+
"bytesInOutput": 43
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"bytes": 153
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bunx-ray",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ASCII heat-map bundle viewer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bunx-ray": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepublishOnly": "npm run build",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/cli.js",
|
|
14
|
+
"test": "npm run build && vitest run",
|
|
15
|
+
"sample:webpack": "webpack --config webpack.sample.js --mode production --json > dist-webpack/stats.json",
|
|
16
|
+
"sample:vite": "vite build --config vite.sample.js",
|
|
17
|
+
"sample:esbuild": "esbuild src-esbuild/index.ts --bundle --outfile=dist-esbuild/bundle.js --metafile=dist-esbuild/meta.json"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"commander": "^12.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.3.0",
|
|
25
|
+
"@types/node": "^20.8.10",
|
|
26
|
+
"vitest": "^3.0.0",
|
|
27
|
+
"execa": "^8.0.0",
|
|
28
|
+
"webpack": "^5.91.0",
|
|
29
|
+
"webpack-cli": "^5.1.4",
|
|
30
|
+
"vite": "^5.0.0",
|
|
31
|
+
"rollup-plugin-analyzer": "^4.0.0",
|
|
32
|
+
"esbuild": "^0.21.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Core functions for bunx-ray MVP
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export type Mod = {
|
|
5
|
+
path: string;
|
|
6
|
+
size: number; // bytes
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface Cell {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
w: number;
|
|
13
|
+
h: number;
|
|
14
|
+
mod: Mod;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// simple slice-and-dice treemap mapped to character grid
|
|
18
|
+
export function treemap(mods: Mod[], W = 80, H = 24): Cell[] {
|
|
19
|
+
const cells: Cell[] = [];
|
|
20
|
+
let x = 0,
|
|
21
|
+
y = 0,
|
|
22
|
+
rowH = H;
|
|
23
|
+
const total = mods.reduce((a, m) => a + m.size, 0);
|
|
24
|
+
for (const m of mods) {
|
|
25
|
+
const frac = m.size / total;
|
|
26
|
+
const w = Math.max(1, Math.round((frac * W * H) / rowH));
|
|
27
|
+
if (x + w > W) {
|
|
28
|
+
y += rowH;
|
|
29
|
+
x = 0;
|
|
30
|
+
}
|
|
31
|
+
cells.push({ x, y, w, h: rowH, mod: m });
|
|
32
|
+
x += w;
|
|
33
|
+
}
|
|
34
|
+
return cells;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function draw(cells: Cell[], W = 80, H = 24): string {
|
|
38
|
+
const grid: string[][] = Array.from({ length: H }, () => Array(W).fill(' '));
|
|
39
|
+
const shades = ['░', '▒', '▓', '█'];
|
|
40
|
+
const max = Math.max(...cells.map((c) => c.mod.size));
|
|
41
|
+
for (const c of cells) {
|
|
42
|
+
const shade = shades[Math.floor((c.mod.size / max) * (shades.length - 1))];
|
|
43
|
+
for (let i = 0; i < c.h; i++) {
|
|
44
|
+
for (let j = 0; j < c.w; j++) {
|
|
45
|
+
if (c.y + i < H && c.x + j < W) {
|
|
46
|
+
grid[c.y + i][c.x + j] = shade;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return grid.map((r) => r.join('')).join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Normalize Webpack stats → Mod[]
|
|
55
|
+
export function normalizeWebpack(stats: any): Mod[] {
|
|
56
|
+
const mods: Mod[] = [];
|
|
57
|
+
if (Array.isArray(stats.modules)) {
|
|
58
|
+
for (const m of stats.modules) {
|
|
59
|
+
const size = m.size ?? m.parsedSize ?? 0;
|
|
60
|
+
const name = m.name ?? m.identifier ?? '';
|
|
61
|
+
if (size && name) mods.push({ path: name, size });
|
|
62
|
+
}
|
|
63
|
+
} else if (Array.isArray(stats.children)) {
|
|
64
|
+
for (const child of stats.children) mods.push(...normalizeWebpack(child));
|
|
65
|
+
}
|
|
66
|
+
mods.sort((a, b) => b.size - a.size);
|
|
67
|
+
return mods;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Vite / Rollup stats (array of outputs with modules)
|
|
71
|
+
export function normalizeVite(stats: any): Mod[] {
|
|
72
|
+
const mods: Mod[] = [];
|
|
73
|
+
const outputs = Array.isArray(stats.output) ? stats.output : [stats.output ?? stats];
|
|
74
|
+
for (const out of outputs) {
|
|
75
|
+
if (!out || !out.modules) continue;
|
|
76
|
+
const modulesObj = out.modules;
|
|
77
|
+
for (const [p, m] of Object.entries<any>(modulesObj)) {
|
|
78
|
+
const size = (m as any).renderedLength ?? (m as any).renderedSize ?? (m as any).originalLength ?? (m as any).size ?? 0;
|
|
79
|
+
mods.push({ path: p, size });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
mods.sort((a, b) => b.size - a.size);
|
|
83
|
+
return mods;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// esbuild metafile JSON
|
|
87
|
+
export function normalizeEsbuild(meta: any): Mod[] {
|
|
88
|
+
const mods: Mod[] = [];
|
|
89
|
+
if (meta.inputs && typeof meta.inputs === 'object') {
|
|
90
|
+
for (const [p, info] of Object.entries<any>(meta.inputs)) {
|
|
91
|
+
const size = info.bytes ?? 0;
|
|
92
|
+
mods.push({ path: p, size });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
mods.sort((a, b) => b.size - a.size);
|
|
96
|
+
return mods;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------- Utilities ------------------
|
|
100
|
+
|
|
101
|
+
export const SIZE_THRESHOLDS = [10 * 1024, 50 * 1024, 100 * 1024]; // bytes
|
|
102
|
+
|
|
103
|
+
export function formatSize(bytes: number): string {
|
|
104
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
105
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
106
|
+
return bytes + ' B';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function totalSize(mods: Mod[]): number {
|
|
110
|
+
return mods.reduce((a, m) => a + m.size, 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function topModules(mods: Mod[], n = 10): Mod[] {
|
|
114
|
+
return [...mods].sort((a, b) => b.size - a.size).slice(0, n);
|
|
115
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ---- CLI -----------------------------------------------------------------
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Mod,
|
|
9
|
+
normalizeWebpack,
|
|
10
|
+
normalizeVite,
|
|
11
|
+
normalizeEsbuild,
|
|
12
|
+
treemap,
|
|
13
|
+
draw
|
|
14
|
+
} from './bundle.js';
|
|
15
|
+
|
|
16
|
+
import { renderReport } from './report.js';
|
|
17
|
+
|
|
18
|
+
function main() {
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('bunx-ray')
|
|
23
|
+
.description('ASCII heat-map bundle viewer')
|
|
24
|
+
.argument('<stats>', 'Build stats JSON file')
|
|
25
|
+
.option('--webpack', 'Input is Webpack stats (default)')
|
|
26
|
+
.option('--vite', 'Input is Vite/Rollup stats')
|
|
27
|
+
.option('--esbuild', 'Input is esbuild metafile')
|
|
28
|
+
.option('--cols <number>', 'Terminal columns (default 80)', '80')
|
|
29
|
+
.option('--rows <number>', 'Terminal rows (default 24)', '24')
|
|
30
|
+
.option('--top <number>', 'Show N largest modules (default 10)', '10')
|
|
31
|
+
.option('--no-legend', 'Hide legend line')
|
|
32
|
+
.option('--no-summary', 'Hide summary line')
|
|
33
|
+
.option('--grid-only', 'Only print grid (implies --no-legend --no-summary)')
|
|
34
|
+
.option('--demo', 'Render built-in demo heat-map')
|
|
35
|
+
.parse(process.argv);
|
|
36
|
+
|
|
37
|
+
const opts = program.opts();
|
|
38
|
+
const file = program.args[0];
|
|
39
|
+
|
|
40
|
+
const cols = Number(opts.cols);
|
|
41
|
+
const rows = Number(opts.rows);
|
|
42
|
+
if (!Number.isFinite(cols) || !Number.isFinite(rows)) {
|
|
43
|
+
console.error(chalk.red('Error: --cols and --rows must be numbers'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const raw = fs.readFileSync(path.resolve(process.cwd(), file), 'utf8');
|
|
48
|
+
let stats: any;
|
|
49
|
+
try {
|
|
50
|
+
stats = JSON.parse(raw);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(chalk.red(`Failed to parse JSON from ${file}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Choose adapter based on flags or auto-detect.
|
|
57
|
+
let mods: Mod[] = [];
|
|
58
|
+
if (opts.webpack) {
|
|
59
|
+
mods = normalizeWebpack(stats);
|
|
60
|
+
} else if (opts.vite) {
|
|
61
|
+
mods = normalizeVite(stats);
|
|
62
|
+
} else if (opts.esbuild) {
|
|
63
|
+
mods = normalizeEsbuild(stats);
|
|
64
|
+
} else {
|
|
65
|
+
// auto-detect simple heuristics
|
|
66
|
+
if (stats.inputs && stats.outputs) mods = normalizeEsbuild(stats);
|
|
67
|
+
else if (stats.modules || stats.children) mods = normalizeWebpack(stats);
|
|
68
|
+
else if (stats.output) mods = normalizeVite(stats);
|
|
69
|
+
else {
|
|
70
|
+
console.error(chalk.red('Unable to detect stats format; please pass --webpack | --vite | --esbuild'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (mods.length === 0) {
|
|
76
|
+
console.error(chalk.yellow('No modules found in stats file.'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const report = renderReport(mods, {
|
|
81
|
+
cols,
|
|
82
|
+
rows,
|
|
83
|
+
top: Number(opts.top ?? 10),
|
|
84
|
+
legend: opts.legend !== false && !opts.gridOnly,
|
|
85
|
+
summary: opts.summary !== false && !opts.gridOnly,
|
|
86
|
+
color: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (report.legendLine) console.log(report.legendLine);
|
|
90
|
+
if (report.summaryLine) console.log(report.summaryLine);
|
|
91
|
+
|
|
92
|
+
console.log('\n' + report.grid + '\n');
|
|
93
|
+
|
|
94
|
+
report.tableLines.forEach((l) => console.log(l));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main();
|
package/src/index.ts
ADDED
package/src/report.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Mod, treemap, draw, formatSize, totalSize, topModules } from './bundle.js';
|
|
2
|
+
|
|
3
|
+
export interface ReportOptions {
|
|
4
|
+
cols: number;
|
|
5
|
+
rows: number;
|
|
6
|
+
top: number;
|
|
7
|
+
legend: boolean;
|
|
8
|
+
summary: boolean;
|
|
9
|
+
color: boolean; // reserved – color disabled for now
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SHADES = ['░', '▒', '▓', '█'] as const;
|
|
13
|
+
|
|
14
|
+
function calcThresholds(max: number): number[] {
|
|
15
|
+
return [0.25, 0.5, 0.75].map((p) => max * p);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shadeForSize(size: number, max: number): string {
|
|
19
|
+
const idx = Math.floor((size / max) * (SHADES.length - 1));
|
|
20
|
+
return SHADES[idx];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RenderedReport {
|
|
24
|
+
legendLine?: string;
|
|
25
|
+
summaryLine?: string;
|
|
26
|
+
grid: string;
|
|
27
|
+
tableLines: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderReport(mods: Mod[], opts: ReportOptions): RenderedReport {
|
|
31
|
+
const { cols, rows, top, legend, summary } = opts;
|
|
32
|
+
const max = Math.max(...mods.map((m) => m.size));
|
|
33
|
+
const thresholds = calcThresholds(max);
|
|
34
|
+
|
|
35
|
+
// grid
|
|
36
|
+
const grid = draw(treemap(mods, cols, rows), cols, rows);
|
|
37
|
+
|
|
38
|
+
// legend
|
|
39
|
+
let legendLine: string | undefined;
|
|
40
|
+
if (legend) {
|
|
41
|
+
legendLine =
|
|
42
|
+
'Legend ' +
|
|
43
|
+
[
|
|
44
|
+
`${SHADES[3]} >${formatSize(thresholds[2])}`,
|
|
45
|
+
`${SHADES[2]} ${formatSize(thresholds[1])}-${formatSize(thresholds[2])}`,
|
|
46
|
+
`${SHADES[1]} ${formatSize(thresholds[0])}-${formatSize(thresholds[1])}`,
|
|
47
|
+
`${SHADES[0]} <${formatSize(thresholds[0])}`,
|
|
48
|
+
].join(' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// summary
|
|
52
|
+
let summaryLine: string | undefined;
|
|
53
|
+
if (summary) {
|
|
54
|
+
summaryLine = `Total bundle: ${formatSize(totalSize(mods))} | modules: ${mods.length}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// table
|
|
58
|
+
const list = topModules(mods, top);
|
|
59
|
+
const tableLines: string[] = [`Top ${list.length} modules`];
|
|
60
|
+
list.forEach((m, idx) => {
|
|
61
|
+
const shade = shadeForSize(m.size, max);
|
|
62
|
+
const pct = ((m.size / totalSize(mods)) * 100).toFixed(1).padStart(4);
|
|
63
|
+
const name = m.path.length > 28 ? '…' + m.path.slice(-27) : m.path.padEnd(28);
|
|
64
|
+
tableLines.push(`${(idx + 1).toString().padStart(2)} ${shade} ${name} ${formatSize(m.size).padStart(8)} (${pct}%)`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { legendLine, summaryLine, grid, tableLines };
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const add = (a: number, b: number): number => a + b;
|
package/src-vite/math.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const add = (a: number, b: number): number => a + b;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const add = (a, b) => a + b;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`normalize + draw > esbuild fixture renders 1`] = `
|
|
4
|
+
"███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
5
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
6
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
7
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
8
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
9
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
10
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
11
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒"
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
exports[`normalize + draw > vite fixture renders 1`] = `
|
|
15
|
+
"███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
16
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
17
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
18
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
19
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
20
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
21
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒
|
|
22
|
+
███████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒"
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
exports[`normalize + draw > webpack fixture renders 1`] = `
|
|
26
|
+
"█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
27
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
28
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
29
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
30
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
31
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
32
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░
|
|
33
|
+
█████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░"
|
|
34
|
+
`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { normalizeWebpack, normalizeVite, normalizeEsbuild, treemap, draw } from '../src/bundle';
|
|
6
|
+
|
|
7
|
+
const fixturesDir = new URL('../fixtures/', import.meta.url).pathname;
|
|
8
|
+
|
|
9
|
+
function load(name: string) {
|
|
10
|
+
return JSON.parse(readFileSync(`${fixturesDir}${name}`, 'utf8'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('normalize + draw', () => {
|
|
14
|
+
it('webpack fixture renders', () => {
|
|
15
|
+
const mods = normalizeWebpack(load('webpack-sample.json'));
|
|
16
|
+
const ascii = draw(treemap(mods, 40, 8), 40, 8);
|
|
17
|
+
expect(ascii).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('vite fixture renders', () => {
|
|
21
|
+
const mods = normalizeVite(load('vite-sample.json'));
|
|
22
|
+
const ascii = draw(treemap(mods, 40, 8), 40, 8);
|
|
23
|
+
expect(ascii).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('esbuild fixture renders', () => {
|
|
27
|
+
const mods = normalizeEsbuild(load('esbuild-sample.json'));
|
|
28
|
+
const ascii = draw(treemap(mods, 40, 8), 40, 8);
|
|
29
|
+
expect(ascii).toMatchSnapshot();
|
|
30
|
+
});
|
|
31
|
+
});
|
package/test/cli.test.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const cli = path.resolve('dist/cli.js');
|
|
6
|
+
const fixture = path.resolve('fixtures/webpack-sample.json');
|
|
7
|
+
|
|
8
|
+
describe('CLI smoke tests', () => {
|
|
9
|
+
it('prints legend and summary by default', async () => {
|
|
10
|
+
const { stdout } = await execa('node', [cli, fixture, '--cols', '20', '--rows', '6']);
|
|
11
|
+
expect(stdout).toMatch(/Legend/);
|
|
12
|
+
expect(stdout).toMatch(/Total bundle/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('grid-only hides legend and summary', async () => {
|
|
16
|
+
const { stdout } = await execa('node', [cli, fixture, '--grid-only', '--cols', '20', '--rows', '6']);
|
|
17
|
+
expect(stdout).not.toMatch(/Legend/);
|
|
18
|
+
expect(stdout).not.toMatch(/Total bundle/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('respects top flag', async () => {
|
|
22
|
+
const { stdout } = await execa('node', [cli, fixture, '--top', '2', '--cols', '20', '--rows', '6']);
|
|
23
|
+
const lines = stdout.split('\n').filter((l) => l.startsWith(' '));
|
|
24
|
+
// last section lines start with space digit for table
|
|
25
|
+
const tableLines = lines.filter((l) => /\d+ [░▒▓█]/.test(l));
|
|
26
|
+
expect(tableLines.length).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["node", "vitest"],
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|
package/vite.sample.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import analyze from 'rollup-plugin-analyzer';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
build: {
|
|
6
|
+
outDir: 'dist-vite',
|
|
7
|
+
emptyOutDir: true,
|
|
8
|
+
rollupOptions: {
|
|
9
|
+
input: resolve('./src-vite/index.ts'),
|
|
10
|
+
plugins: [
|
|
11
|
+
analyze({ summaryOnly: true, json: true, filename: 'dist-vite/stats.json' })
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|