bundle-cost-cli 0.1.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/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/bin.cjs +343 -0
- package/dist/bin.js +320 -0
- package/dist/index.cjs +369 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +323 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kea0811
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# bundle-cost-cli
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**🌐 [Live demo →](https://bundle-cost-cli.vercel.app)**
|
|
8
|
+
|
|
9
|
+
> Print the real over-the-wire cost of your build output — and the delta on every save.
|
|
10
|
+
|
|
11
|
+
You already know your bundle is "kinda big." What you don't know, in the moment you add a dependency, is _how much_ bigger it just got. `bundle-cost` answers that in one line: point it at your built files and it prints raw, gzip, and (optionally) brotli sizes. Run it with `--watch` and it reprints a **delta** every time you save — so a stray `import { everything } from 'lodash'` shows up as `+71 KB` before you commit it, not after a user complains.
|
|
12
|
+
|
|
13
|
+
No bundler plugin, no config file, no telemetry. Just bytes.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
**From GitHub** (always works):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add -g github:kea0811/bundle-cost-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**From npm** _(when published to npm)_:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add -g bundle-cost-cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> Using npm or yarn? `npm install -g bundle-cost-cli` / `yarn global add bundle-cost-cli` work too. Or skip the install entirely and run it once with `pnpm dlx bundle-cost-cli dist/index.js` (`npx` works the same way).
|
|
30
|
+
|
|
31
|
+
Requires Node 18+.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
Point it at one or more files:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle-cost dist/index.js dist/index.cjs
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
File Raw Gzip
|
|
43
|
+
dist/index.js 9.89 KB 3.37 KB
|
|
44
|
+
dist/index.cjs 11.87 KB 4.06 KB
|
|
45
|
+
total 21.76 KB 7.43 KB
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Watch mode — a delta on every save
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle-cost dist/index.js --watch
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The first render is your baseline. Every save after that adds a `Δ gzip` column, colored green when you shrank it and red when you grew it:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
File Raw Gzip Δ gzip
|
|
58
|
+
dist/index.js 10.4 KB 3.71 KB +340 B
|
|
59
|
+
total 10.4 KB 3.71 KB +340 B
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pair it with your bundler's own watch mode in another terminal and you get a live cost readout for free.
|
|
63
|
+
|
|
64
|
+
### Set a budget (great for CI)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle-cost dist/index.js --limit 50kb
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If the **total gzip** size is over budget, `bundle-cost` prints a red `✗` line and exits with code `1` — so it fails your pipeline instead of silently shipping a regression. Under budget, it exits `0` with a green `✓`.
|
|
71
|
+
|
|
72
|
+
### Machine-readable output
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bundle-cost dist/index.js --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"files": [
|
|
81
|
+
{ "path": "dist/index.js", "raw": 10123, "gzip": 3451, "brotli": 3063, "delta": null }
|
|
82
|
+
],
|
|
83
|
+
"total": { "raw": 10123, "gzip": 3451, "brotli": 3063, "delta": null }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Pipe it into `jq`, store it as a build artifact, or diff two runs yourself.
|
|
88
|
+
|
|
89
|
+
## Options
|
|
90
|
+
|
|
91
|
+
| Flag | Description |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `-w, --watch` | Watch the files and print the size delta on every save. |
|
|
94
|
+
| `-b, --brotli` | Add a brotli column alongside gzip. |
|
|
95
|
+
| `--no-gzip` | Hide the gzip column (show raw bytes only). |
|
|
96
|
+
| `-j, --json` | Print machine-readable JSON instead of a table. |
|
|
97
|
+
| `-l, --limit <size>` | Fail (exit `1`) if total gzip exceeds this budget, e.g. `50kb`, `1.5mb`. |
|
|
98
|
+
| `--no-color` | Disable ANSI colors (also respects `NO_COLOR`). |
|
|
99
|
+
| `-h, --help` | Show usage and examples. |
|
|
100
|
+
|
|
101
|
+
Sizes accept `b`, `kb`, `mb`, `gb`, `tb` (case-insensitive), or a bare byte count.
|
|
102
|
+
|
|
103
|
+
## Programmatic API
|
|
104
|
+
|
|
105
|
+
The same building blocks ship as a typed ESM/CJS module, so you can wire bundle cost into your own scripts:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { measureFile, buildReport, renderReport, formatBytes, createColors } from 'bundle-cost-cli';
|
|
109
|
+
|
|
110
|
+
const size = await measureFile('dist/index.js');
|
|
111
|
+
console.log(`gzip: ${formatBytes(size.gzip)}`);
|
|
112
|
+
|
|
113
|
+
const report = buildReport([{ path: 'dist/index.js', size }]);
|
|
114
|
+
console.log(renderReport(report, { gzip: true, brotli: false, json: false, colors: createColors(false) }));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`run(argv, deps)` is exported too — the whole CLI with injectable `log`, `error`, `cwd`, `env`, and `watch`, which is exactly how the test suite drives it.
|
|
118
|
+
|
|
119
|
+
## How it works
|
|
120
|
+
|
|
121
|
+
There's no minifier in here and no bundler hook. `bundle-cost` reads each file as-is and runs it through Node's built-in `zlib` — `gzipSync` and `brotliCompressSync` — because gzip and brotli are what a CDN actually serves. That keeps the tool dependency-light and honest: it measures the bytes you ship, not an estimate.
|
|
122
|
+
|
|
123
|
+
Watch mode keeps the previous measurement in memory and diffs against it, so the delta is always "since the last save in this session." All of the I/O — stdout, the filesystem watcher, the clock — is injected, which is why the test suite hits 100% coverage without touching your real terminal.
|
|
124
|
+
|
|
125
|
+
## Live demo
|
|
126
|
+
|
|
127
|
+
See it in action at **[bundle-cost-cli.vercel.app](https://bundle-cost-cli.vercel.app)** — a static landing page with a sample run.
|
|
128
|
+
|
|
129
|
+
## Contributing
|
|
130
|
+
|
|
131
|
+
PRs welcome — especially new output formats and budget ergonomics. To hack on it:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm install
|
|
135
|
+
pnpm test
|
|
136
|
+
pnpm build
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`pnpm test:coverage` enforces 100% coverage, and `pnpm dev` rebuilds on change.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT © [kea0811](https://github.com/kea0811)
|
package/dist/bin.cjs
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_node_path = require("path");
|
|
28
|
+
var import_commander = require("commander");
|
|
29
|
+
|
|
30
|
+
// src/color.ts
|
|
31
|
+
function wrap(open, close) {
|
|
32
|
+
return (text) => `\x1B[${open}m${text}\x1B[${close}m`;
|
|
33
|
+
}
|
|
34
|
+
var identity = (text) => text;
|
|
35
|
+
function createColors(enabled) {
|
|
36
|
+
if (!enabled) {
|
|
37
|
+
return { green: identity, red: identity, dim: identity, cyan: identity, bold: identity };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
green: wrap(32, 39),
|
|
41
|
+
red: wrap(31, 39),
|
|
42
|
+
dim: wrap(2, 22),
|
|
43
|
+
cyan: wrap(36, 39),
|
|
44
|
+
bold: wrap(1, 22)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/format.ts
|
|
49
|
+
var UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
50
|
+
var SIZE_RE = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i;
|
|
51
|
+
var MULTIPLIERS = {
|
|
52
|
+
b: 1,
|
|
53
|
+
kb: 1024,
|
|
54
|
+
mb: 1024 ** 2,
|
|
55
|
+
gb: 1024 ** 3,
|
|
56
|
+
tb: 1024 ** 4
|
|
57
|
+
};
|
|
58
|
+
function formatBytes(bytes) {
|
|
59
|
+
if (bytes < 1) return "0 B";
|
|
60
|
+
const exponent = Math.min(
|
|
61
|
+
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
62
|
+
UNITS.length - 1
|
|
63
|
+
);
|
|
64
|
+
const value = bytes / 1024 ** exponent;
|
|
65
|
+
const rendered = exponent === 0 ? String(Math.round(value)) : value.toFixed(2);
|
|
66
|
+
return `${rendered} ${UNITS[exponent]}`;
|
|
67
|
+
}
|
|
68
|
+
function formatDelta(bytes) {
|
|
69
|
+
if (bytes === 0) return "0 B";
|
|
70
|
+
const sign = bytes > 0 ? "+" : "-";
|
|
71
|
+
return `${sign}${formatBytes(Math.abs(bytes))}`;
|
|
72
|
+
}
|
|
73
|
+
function parseSize(input) {
|
|
74
|
+
const match = SIZE_RE.exec(input.trim());
|
|
75
|
+
if (!match) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Invalid size: "${input}" (try "50kb", "1.5mb", or a raw byte count)`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const value = Number(match[1]);
|
|
81
|
+
const unit = (match[2] ?? "b").toLowerCase();
|
|
82
|
+
return Math.round(value * MULTIPLIERS[unit]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/render.ts
|
|
86
|
+
var plainCell = (text) => ({ plain: text, colored: text });
|
|
87
|
+
function colorDelta(delta, metric, colors) {
|
|
88
|
+
if (delta === null) return { plain: "new", colored: colors.cyan("new") };
|
|
89
|
+
const value = delta[metric];
|
|
90
|
+
const text = formatDelta(value);
|
|
91
|
+
if (value < 0) return { plain: text, colored: colors.green(text) };
|
|
92
|
+
if (value > 0) return { plain: text, colored: colors.red(text) };
|
|
93
|
+
return { plain: text, colored: colors.dim(text) };
|
|
94
|
+
}
|
|
95
|
+
function buildCells(label, size, delta, metric, hasDelta, options, bold) {
|
|
96
|
+
const cells = [
|
|
97
|
+
{ plain: label, colored: bold ? options.colors.bold(label) : label },
|
|
98
|
+
plainCell(formatBytes(size.raw))
|
|
99
|
+
];
|
|
100
|
+
if (options.gzip) cells.push(plainCell(formatBytes(size.gzip)));
|
|
101
|
+
if (options.brotli) cells.push(plainCell(formatBytes(size.brotli)));
|
|
102
|
+
if (hasDelta) cells.push(colorDelta(delta, metric, options.colors));
|
|
103
|
+
return cells;
|
|
104
|
+
}
|
|
105
|
+
function renderJson(report) {
|
|
106
|
+
return JSON.stringify(
|
|
107
|
+
{
|
|
108
|
+
files: report.rows.map((row) => ({
|
|
109
|
+
path: row.path,
|
|
110
|
+
raw: row.size.raw,
|
|
111
|
+
gzip: row.size.gzip,
|
|
112
|
+
brotli: row.size.brotli,
|
|
113
|
+
delta: row.delta
|
|
114
|
+
})),
|
|
115
|
+
total: {
|
|
116
|
+
raw: report.total.size.raw,
|
|
117
|
+
gzip: report.total.size.gzip,
|
|
118
|
+
brotli: report.total.size.brotli,
|
|
119
|
+
delta: report.total.delta
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
null,
|
|
123
|
+
2
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
function renderTable(report, options) {
|
|
127
|
+
const metric = options.gzip ? "gzip" : "raw";
|
|
128
|
+
const hasDelta = report.rows.some((row) => row.delta !== null) || report.total.delta !== null;
|
|
129
|
+
const headers = ["File", "Raw"];
|
|
130
|
+
if (options.gzip) headers.push("Gzip");
|
|
131
|
+
if (options.brotli) headers.push("Brotli");
|
|
132
|
+
if (hasDelta) headers.push(`\u0394 ${metric}`);
|
|
133
|
+
const headerCells = headers.map((header) => ({
|
|
134
|
+
plain: header,
|
|
135
|
+
colored: options.colors.dim(header)
|
|
136
|
+
}));
|
|
137
|
+
const bodyRows = report.rows.map(
|
|
138
|
+
(row) => buildCells(row.path, row.size, row.delta, metric, hasDelta, options, false)
|
|
139
|
+
);
|
|
140
|
+
const totalRow = buildCells(
|
|
141
|
+
"total",
|
|
142
|
+
report.total.size,
|
|
143
|
+
report.total.delta,
|
|
144
|
+
metric,
|
|
145
|
+
hasDelta,
|
|
146
|
+
options,
|
|
147
|
+
true
|
|
148
|
+
);
|
|
149
|
+
const matrix = [headerCells, ...bodyRows, totalRow];
|
|
150
|
+
const widths = headers.map(
|
|
151
|
+
(_, column) => Math.max(...matrix.map((row) => row[column].plain.length))
|
|
152
|
+
);
|
|
153
|
+
return matrix.map(
|
|
154
|
+
(row) => row.map((cell, column) => {
|
|
155
|
+
const padding = " ".repeat(widths[column] - cell.plain.length);
|
|
156
|
+
return column === 0 ? cell.colored + padding : padding + cell.colored;
|
|
157
|
+
}).join(" ")
|
|
158
|
+
).join("\n");
|
|
159
|
+
}
|
|
160
|
+
function renderReport(report, options) {
|
|
161
|
+
return options.json ? renderJson(report) : renderTable(report, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/report.ts
|
|
165
|
+
function subtract(next, prev) {
|
|
166
|
+
return {
|
|
167
|
+
raw: next.raw - prev.raw,
|
|
168
|
+
gzip: next.gzip - prev.gzip,
|
|
169
|
+
brotli: next.brotli - prev.brotli
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function sum(sizes) {
|
|
173
|
+
return sizes.reduce(
|
|
174
|
+
(acc, size) => ({
|
|
175
|
+
raw: acc.raw + size.raw,
|
|
176
|
+
gzip: acc.gzip + size.gzip,
|
|
177
|
+
brotli: acc.brotli + size.brotli
|
|
178
|
+
}),
|
|
179
|
+
{ raw: 0, gzip: 0, brotli: 0 }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function buildReport(measurements, previous) {
|
|
183
|
+
const rows = measurements.map((measurement) => {
|
|
184
|
+
const prev = previous?.get(measurement.path);
|
|
185
|
+
return {
|
|
186
|
+
path: measurement.path,
|
|
187
|
+
size: measurement.size,
|
|
188
|
+
delta: prev ? subtract(measurement.size, prev) : null
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
const totalSize = sum(measurements.map((m) => m.size));
|
|
192
|
+
const previousTotal = previous ? sum([...previous.values()]) : void 0;
|
|
193
|
+
return {
|
|
194
|
+
rows,
|
|
195
|
+
total: {
|
|
196
|
+
size: totalSize,
|
|
197
|
+
delta: previousTotal ? subtract(totalSize, previousTotal) : null
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/sizes.ts
|
|
203
|
+
var import_promises = require("fs/promises");
|
|
204
|
+
var import_node_zlib = require("zlib");
|
|
205
|
+
function measureBuffer(buffer) {
|
|
206
|
+
return {
|
|
207
|
+
raw: buffer.byteLength,
|
|
208
|
+
gzip: (0, import_node_zlib.gzipSync)(buffer).byteLength,
|
|
209
|
+
brotli: (0, import_node_zlib.brotliCompressSync)(buffer).byteLength
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function measureFile(path) {
|
|
213
|
+
const buffer = await (0, import_promises.readFile)(path);
|
|
214
|
+
return measureBuffer(buffer);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/watch.ts
|
|
218
|
+
var fs = __toESM(require("fs"), 1);
|
|
219
|
+
var defaultImpl = (path, onChange) => {
|
|
220
|
+
const watcher = fs.watch(path, () => onChange());
|
|
221
|
+
return { close: () => watcher.close() };
|
|
222
|
+
};
|
|
223
|
+
function watchPaths(paths, onChange, impl = defaultImpl) {
|
|
224
|
+
const watchers = paths.map((path) => impl(path, onChange));
|
|
225
|
+
return () => {
|
|
226
|
+
for (const watcher of watchers) watcher.close();
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/cli.ts
|
|
231
|
+
var DESCRIPTION = "Print the gzipped bundle cost of your files \u2014 and the delta on every save.";
|
|
232
|
+
var HELP_EXAMPLES = `
|
|
233
|
+
Examples:
|
|
234
|
+
$ bundle-cost dist/index.js
|
|
235
|
+
$ bundle-cost dist/*.js --brotli
|
|
236
|
+
$ bundle-cost dist/index.js --limit 50kb
|
|
237
|
+
$ bundle-cost dist/index.js --watch
|
|
238
|
+
$ bundle-cost dist/index.js --json
|
|
239
|
+
`;
|
|
240
|
+
var trimTrailingNewline = (text) => text.replace(/\n+$/, "");
|
|
241
|
+
async function measureAll(targets) {
|
|
242
|
+
return Promise.all(
|
|
243
|
+
targets.map(async (target) => ({ ...target, size: await measureFile(target.path) }))
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
var toMeasurements = (measured) => measured.map((m) => ({ path: m.label, size: m.size }));
|
|
247
|
+
var snapshot = (measured) => new Map(measured.map((m) => [m.label, m.size]));
|
|
248
|
+
function startWatch(targets, renderOptions, initial, ctx) {
|
|
249
|
+
let previous = snapshot(initial);
|
|
250
|
+
ctx.log(renderOptions.colors.dim("watching for changes\u2026 (ctrl-c to stop)"));
|
|
251
|
+
ctx.watch(
|
|
252
|
+
targets.map((target) => target.path),
|
|
253
|
+
async () => {
|
|
254
|
+
let next;
|
|
255
|
+
try {
|
|
256
|
+
next = await measureAll(targets);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
ctx.error(renderOptions.colors.red(`Could not read file: ${err.message}`));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
ctx.log(renderReport(buildReport(toMeasurements(next), previous), renderOptions));
|
|
262
|
+
previous = snapshot(next);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
function reportBudget(totalGzip, limit, colors, ctx) {
|
|
267
|
+
const headline = `${formatBytes(totalGzip)} gzip`;
|
|
268
|
+
if (totalGzip > limit) {
|
|
269
|
+
ctx.error(colors.red(`\u2717 ${headline} exceeds the ${formatBytes(limit)} budget`));
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
ctx.log(colors.green(`\u2713 ${headline} is within the ${formatBytes(limit)} budget`));
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
async function execute(files, options, ctx) {
|
|
276
|
+
const colorsEnabled = options.color !== false && !ctx.env.NO_COLOR;
|
|
277
|
+
const renderOptions = {
|
|
278
|
+
gzip: options.gzip !== false,
|
|
279
|
+
brotli: options.brotli === true,
|
|
280
|
+
json: options.json === true,
|
|
281
|
+
colors: createColors(colorsEnabled)
|
|
282
|
+
};
|
|
283
|
+
let limit;
|
|
284
|
+
if (options.limit !== void 0) {
|
|
285
|
+
try {
|
|
286
|
+
limit = parseSize(options.limit);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
ctx.error(renderOptions.colors.red(err.message));
|
|
289
|
+
return 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const targets = files.map((file) => ({ label: file, path: (0, import_node_path.resolve)(ctx.cwd, file) }));
|
|
293
|
+
let measured;
|
|
294
|
+
try {
|
|
295
|
+
measured = await measureAll(targets);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
ctx.error(renderOptions.colors.red(`Could not read file: ${err.message}`));
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
const report = buildReport(toMeasurements(measured));
|
|
301
|
+
ctx.log(renderReport(report, renderOptions));
|
|
302
|
+
if (options.watch) {
|
|
303
|
+
startWatch(targets, renderOptions, measured, ctx);
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
if (limit !== void 0) {
|
|
307
|
+
return reportBudget(report.total.size.gzip, limit, renderOptions.colors, ctx);
|
|
308
|
+
}
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
async function run(argv, deps = {}) {
|
|
312
|
+
const log = deps.log ?? ((message) => process.stdout.write(`${message}
|
|
313
|
+
`));
|
|
314
|
+
const error = deps.error ?? ((message) => process.stderr.write(`${message}
|
|
315
|
+
`));
|
|
316
|
+
const ctx = {
|
|
317
|
+
log,
|
|
318
|
+
error,
|
|
319
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
320
|
+
env: deps.env ?? process.env,
|
|
321
|
+
watch: deps.watch ?? watchPaths
|
|
322
|
+
};
|
|
323
|
+
const program = new import_commander.Command();
|
|
324
|
+
program.name("bundle-cost").description(DESCRIPTION).argument("<files...>", "one or more files to measure (e.g. dist/index.js)").option("-w, --watch", "watch the files and print the size delta on every save").option("-b, --brotli", "include a brotli column alongside gzip").option("--no-gzip", "hide the gzip column (show raw bytes only)").option("-j, --json", "print machine-readable JSON instead of a table").option("-l, --limit <size>", "fail if the total gzip size exceeds this budget (e.g. 50kb)").option("--no-color", "disable ANSI colors").addHelpText("after", HELP_EXAMPLES).exitOverride().configureOutput({
|
|
325
|
+
writeOut: (text) => log(trimTrailingNewline(text)),
|
|
326
|
+
writeErr: (text) => error(trimTrailingNewline(text))
|
|
327
|
+
});
|
|
328
|
+
let exitCode = 0;
|
|
329
|
+
program.action(async (files, options) => {
|
|
330
|
+
exitCode = await execute(files, options, ctx);
|
|
331
|
+
});
|
|
332
|
+
try {
|
|
333
|
+
await program.parseAsync(argv, { from: "user" });
|
|
334
|
+
} catch (err) {
|
|
335
|
+
return err.exitCode;
|
|
336
|
+
}
|
|
337
|
+
return exitCode;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/bin.ts
|
|
341
|
+
run(process.argv.slice(2)).then((code) => {
|
|
342
|
+
if (code !== 0) process.exitCode = code;
|
|
343
|
+
});
|