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/dist/bin.js ADDED
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { resolve } from "path";
5
+ import { Command } from "commander";
6
+
7
+ // src/color.ts
8
+ function wrap(open, close) {
9
+ return (text) => `\x1B[${open}m${text}\x1B[${close}m`;
10
+ }
11
+ var identity = (text) => text;
12
+ function createColors(enabled) {
13
+ if (!enabled) {
14
+ return { green: identity, red: identity, dim: identity, cyan: identity, bold: identity };
15
+ }
16
+ return {
17
+ green: wrap(32, 39),
18
+ red: wrap(31, 39),
19
+ dim: wrap(2, 22),
20
+ cyan: wrap(36, 39),
21
+ bold: wrap(1, 22)
22
+ };
23
+ }
24
+
25
+ // src/format.ts
26
+ var UNITS = ["B", "KB", "MB", "GB", "TB"];
27
+ var SIZE_RE = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i;
28
+ var MULTIPLIERS = {
29
+ b: 1,
30
+ kb: 1024,
31
+ mb: 1024 ** 2,
32
+ gb: 1024 ** 3,
33
+ tb: 1024 ** 4
34
+ };
35
+ function formatBytes(bytes) {
36
+ if (bytes < 1) return "0 B";
37
+ const exponent = Math.min(
38
+ Math.floor(Math.log(bytes) / Math.log(1024)),
39
+ UNITS.length - 1
40
+ );
41
+ const value = bytes / 1024 ** exponent;
42
+ const rendered = exponent === 0 ? String(Math.round(value)) : value.toFixed(2);
43
+ return `${rendered} ${UNITS[exponent]}`;
44
+ }
45
+ function formatDelta(bytes) {
46
+ if (bytes === 0) return "0 B";
47
+ const sign = bytes > 0 ? "+" : "-";
48
+ return `${sign}${formatBytes(Math.abs(bytes))}`;
49
+ }
50
+ function parseSize(input) {
51
+ const match = SIZE_RE.exec(input.trim());
52
+ if (!match) {
53
+ throw new Error(
54
+ `Invalid size: "${input}" (try "50kb", "1.5mb", or a raw byte count)`
55
+ );
56
+ }
57
+ const value = Number(match[1]);
58
+ const unit = (match[2] ?? "b").toLowerCase();
59
+ return Math.round(value * MULTIPLIERS[unit]);
60
+ }
61
+
62
+ // src/render.ts
63
+ var plainCell = (text) => ({ plain: text, colored: text });
64
+ function colorDelta(delta, metric, colors) {
65
+ if (delta === null) return { plain: "new", colored: colors.cyan("new") };
66
+ const value = delta[metric];
67
+ const text = formatDelta(value);
68
+ if (value < 0) return { plain: text, colored: colors.green(text) };
69
+ if (value > 0) return { plain: text, colored: colors.red(text) };
70
+ return { plain: text, colored: colors.dim(text) };
71
+ }
72
+ function buildCells(label, size, delta, metric, hasDelta, options, bold) {
73
+ const cells = [
74
+ { plain: label, colored: bold ? options.colors.bold(label) : label },
75
+ plainCell(formatBytes(size.raw))
76
+ ];
77
+ if (options.gzip) cells.push(plainCell(formatBytes(size.gzip)));
78
+ if (options.brotli) cells.push(plainCell(formatBytes(size.brotli)));
79
+ if (hasDelta) cells.push(colorDelta(delta, metric, options.colors));
80
+ return cells;
81
+ }
82
+ function renderJson(report) {
83
+ return JSON.stringify(
84
+ {
85
+ files: report.rows.map((row) => ({
86
+ path: row.path,
87
+ raw: row.size.raw,
88
+ gzip: row.size.gzip,
89
+ brotli: row.size.brotli,
90
+ delta: row.delta
91
+ })),
92
+ total: {
93
+ raw: report.total.size.raw,
94
+ gzip: report.total.size.gzip,
95
+ brotli: report.total.size.brotli,
96
+ delta: report.total.delta
97
+ }
98
+ },
99
+ null,
100
+ 2
101
+ );
102
+ }
103
+ function renderTable(report, options) {
104
+ const metric = options.gzip ? "gzip" : "raw";
105
+ const hasDelta = report.rows.some((row) => row.delta !== null) || report.total.delta !== null;
106
+ const headers = ["File", "Raw"];
107
+ if (options.gzip) headers.push("Gzip");
108
+ if (options.brotli) headers.push("Brotli");
109
+ if (hasDelta) headers.push(`\u0394 ${metric}`);
110
+ const headerCells = headers.map((header) => ({
111
+ plain: header,
112
+ colored: options.colors.dim(header)
113
+ }));
114
+ const bodyRows = report.rows.map(
115
+ (row) => buildCells(row.path, row.size, row.delta, metric, hasDelta, options, false)
116
+ );
117
+ const totalRow = buildCells(
118
+ "total",
119
+ report.total.size,
120
+ report.total.delta,
121
+ metric,
122
+ hasDelta,
123
+ options,
124
+ true
125
+ );
126
+ const matrix = [headerCells, ...bodyRows, totalRow];
127
+ const widths = headers.map(
128
+ (_, column) => Math.max(...matrix.map((row) => row[column].plain.length))
129
+ );
130
+ return matrix.map(
131
+ (row) => row.map((cell, column) => {
132
+ const padding = " ".repeat(widths[column] - cell.plain.length);
133
+ return column === 0 ? cell.colored + padding : padding + cell.colored;
134
+ }).join(" ")
135
+ ).join("\n");
136
+ }
137
+ function renderReport(report, options) {
138
+ return options.json ? renderJson(report) : renderTable(report, options);
139
+ }
140
+
141
+ // src/report.ts
142
+ function subtract(next, prev) {
143
+ return {
144
+ raw: next.raw - prev.raw,
145
+ gzip: next.gzip - prev.gzip,
146
+ brotli: next.brotli - prev.brotli
147
+ };
148
+ }
149
+ function sum(sizes) {
150
+ return sizes.reduce(
151
+ (acc, size) => ({
152
+ raw: acc.raw + size.raw,
153
+ gzip: acc.gzip + size.gzip,
154
+ brotli: acc.brotli + size.brotli
155
+ }),
156
+ { raw: 0, gzip: 0, brotli: 0 }
157
+ );
158
+ }
159
+ function buildReport(measurements, previous) {
160
+ const rows = measurements.map((measurement) => {
161
+ const prev = previous?.get(measurement.path);
162
+ return {
163
+ path: measurement.path,
164
+ size: measurement.size,
165
+ delta: prev ? subtract(measurement.size, prev) : null
166
+ };
167
+ });
168
+ const totalSize = sum(measurements.map((m) => m.size));
169
+ const previousTotal = previous ? sum([...previous.values()]) : void 0;
170
+ return {
171
+ rows,
172
+ total: {
173
+ size: totalSize,
174
+ delta: previousTotal ? subtract(totalSize, previousTotal) : null
175
+ }
176
+ };
177
+ }
178
+
179
+ // src/sizes.ts
180
+ import { readFile } from "fs/promises";
181
+ import { brotliCompressSync, gzipSync } from "zlib";
182
+ function measureBuffer(buffer) {
183
+ return {
184
+ raw: buffer.byteLength,
185
+ gzip: gzipSync(buffer).byteLength,
186
+ brotli: brotliCompressSync(buffer).byteLength
187
+ };
188
+ }
189
+ async function measureFile(path) {
190
+ const buffer = await readFile(path);
191
+ return measureBuffer(buffer);
192
+ }
193
+
194
+ // src/watch.ts
195
+ import * as fs from "fs";
196
+ var defaultImpl = (path, onChange) => {
197
+ const watcher = fs.watch(path, () => onChange());
198
+ return { close: () => watcher.close() };
199
+ };
200
+ function watchPaths(paths, onChange, impl = defaultImpl) {
201
+ const watchers = paths.map((path) => impl(path, onChange));
202
+ return () => {
203
+ for (const watcher of watchers) watcher.close();
204
+ };
205
+ }
206
+
207
+ // src/cli.ts
208
+ var DESCRIPTION = "Print the gzipped bundle cost of your files \u2014 and the delta on every save.";
209
+ var HELP_EXAMPLES = `
210
+ Examples:
211
+ $ bundle-cost dist/index.js
212
+ $ bundle-cost dist/*.js --brotli
213
+ $ bundle-cost dist/index.js --limit 50kb
214
+ $ bundle-cost dist/index.js --watch
215
+ $ bundle-cost dist/index.js --json
216
+ `;
217
+ var trimTrailingNewline = (text) => text.replace(/\n+$/, "");
218
+ async function measureAll(targets) {
219
+ return Promise.all(
220
+ targets.map(async (target) => ({ ...target, size: await measureFile(target.path) }))
221
+ );
222
+ }
223
+ var toMeasurements = (measured) => measured.map((m) => ({ path: m.label, size: m.size }));
224
+ var snapshot = (measured) => new Map(measured.map((m) => [m.label, m.size]));
225
+ function startWatch(targets, renderOptions, initial, ctx) {
226
+ let previous = snapshot(initial);
227
+ ctx.log(renderOptions.colors.dim("watching for changes\u2026 (ctrl-c to stop)"));
228
+ ctx.watch(
229
+ targets.map((target) => target.path),
230
+ async () => {
231
+ let next;
232
+ try {
233
+ next = await measureAll(targets);
234
+ } catch (err) {
235
+ ctx.error(renderOptions.colors.red(`Could not read file: ${err.message}`));
236
+ return;
237
+ }
238
+ ctx.log(renderReport(buildReport(toMeasurements(next), previous), renderOptions));
239
+ previous = snapshot(next);
240
+ }
241
+ );
242
+ }
243
+ function reportBudget(totalGzip, limit, colors, ctx) {
244
+ const headline = `${formatBytes(totalGzip)} gzip`;
245
+ if (totalGzip > limit) {
246
+ ctx.error(colors.red(`\u2717 ${headline} exceeds the ${formatBytes(limit)} budget`));
247
+ return 1;
248
+ }
249
+ ctx.log(colors.green(`\u2713 ${headline} is within the ${formatBytes(limit)} budget`));
250
+ return 0;
251
+ }
252
+ async function execute(files, options, ctx) {
253
+ const colorsEnabled = options.color !== false && !ctx.env.NO_COLOR;
254
+ const renderOptions = {
255
+ gzip: options.gzip !== false,
256
+ brotli: options.brotli === true,
257
+ json: options.json === true,
258
+ colors: createColors(colorsEnabled)
259
+ };
260
+ let limit;
261
+ if (options.limit !== void 0) {
262
+ try {
263
+ limit = parseSize(options.limit);
264
+ } catch (err) {
265
+ ctx.error(renderOptions.colors.red(err.message));
266
+ return 1;
267
+ }
268
+ }
269
+ const targets = files.map((file) => ({ label: file, path: resolve(ctx.cwd, file) }));
270
+ let measured;
271
+ try {
272
+ measured = await measureAll(targets);
273
+ } catch (err) {
274
+ ctx.error(renderOptions.colors.red(`Could not read file: ${err.message}`));
275
+ return 1;
276
+ }
277
+ const report = buildReport(toMeasurements(measured));
278
+ ctx.log(renderReport(report, renderOptions));
279
+ if (options.watch) {
280
+ startWatch(targets, renderOptions, measured, ctx);
281
+ return 0;
282
+ }
283
+ if (limit !== void 0) {
284
+ return reportBudget(report.total.size.gzip, limit, renderOptions.colors, ctx);
285
+ }
286
+ return 0;
287
+ }
288
+ async function run(argv, deps = {}) {
289
+ const log = deps.log ?? ((message) => process.stdout.write(`${message}
290
+ `));
291
+ const error = deps.error ?? ((message) => process.stderr.write(`${message}
292
+ `));
293
+ const ctx = {
294
+ log,
295
+ error,
296
+ cwd: deps.cwd ?? process.cwd(),
297
+ env: deps.env ?? process.env,
298
+ watch: deps.watch ?? watchPaths
299
+ };
300
+ const program = new Command();
301
+ 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({
302
+ writeOut: (text) => log(trimTrailingNewline(text)),
303
+ writeErr: (text) => error(trimTrailingNewline(text))
304
+ });
305
+ let exitCode = 0;
306
+ program.action(async (files, options) => {
307
+ exitCode = await execute(files, options, ctx);
308
+ });
309
+ try {
310
+ await program.parseAsync(argv, { from: "user" });
311
+ } catch (err) {
312
+ return err.exitCode;
313
+ }
314
+ return exitCode;
315
+ }
316
+
317
+ // src/bin.ts
318
+ run(process.argv.slice(2)).then((code) => {
319
+ if (code !== 0) process.exitCode = code;
320
+ });
package/dist/index.cjs ADDED
@@ -0,0 +1,369 @@
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 src_exports = {};
32
+ __export(src_exports, {
33
+ buildReport: () => buildReport,
34
+ createColors: () => createColors,
35
+ formatBytes: () => formatBytes,
36
+ formatDelta: () => formatDelta,
37
+ measureBuffer: () => measureBuffer,
38
+ measureFile: () => measureFile,
39
+ parseSize: () => parseSize,
40
+ renderReport: () => renderReport,
41
+ run: () => run,
42
+ watchPaths: () => watchPaths
43
+ });
44
+ module.exports = __toCommonJS(src_exports);
45
+
46
+ // src/color.ts
47
+ function wrap(open, close) {
48
+ return (text) => `\x1B[${open}m${text}\x1B[${close}m`;
49
+ }
50
+ var identity = (text) => text;
51
+ function createColors(enabled) {
52
+ if (!enabled) {
53
+ return { green: identity, red: identity, dim: identity, cyan: identity, bold: identity };
54
+ }
55
+ return {
56
+ green: wrap(32, 39),
57
+ red: wrap(31, 39),
58
+ dim: wrap(2, 22),
59
+ cyan: wrap(36, 39),
60
+ bold: wrap(1, 22)
61
+ };
62
+ }
63
+
64
+ // src/format.ts
65
+ var UNITS = ["B", "KB", "MB", "GB", "TB"];
66
+ var SIZE_RE = /^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i;
67
+ var MULTIPLIERS = {
68
+ b: 1,
69
+ kb: 1024,
70
+ mb: 1024 ** 2,
71
+ gb: 1024 ** 3,
72
+ tb: 1024 ** 4
73
+ };
74
+ function formatBytes(bytes) {
75
+ if (bytes < 1) return "0 B";
76
+ const exponent = Math.min(
77
+ Math.floor(Math.log(bytes) / Math.log(1024)),
78
+ UNITS.length - 1
79
+ );
80
+ const value = bytes / 1024 ** exponent;
81
+ const rendered = exponent === 0 ? String(Math.round(value)) : value.toFixed(2);
82
+ return `${rendered} ${UNITS[exponent]}`;
83
+ }
84
+ function formatDelta(bytes) {
85
+ if (bytes === 0) return "0 B";
86
+ const sign = bytes > 0 ? "+" : "-";
87
+ return `${sign}${formatBytes(Math.abs(bytes))}`;
88
+ }
89
+ function parseSize(input) {
90
+ const match = SIZE_RE.exec(input.trim());
91
+ if (!match) {
92
+ throw new Error(
93
+ `Invalid size: "${input}" (try "50kb", "1.5mb", or a raw byte count)`
94
+ );
95
+ }
96
+ const value = Number(match[1]);
97
+ const unit = (match[2] ?? "b").toLowerCase();
98
+ return Math.round(value * MULTIPLIERS[unit]);
99
+ }
100
+
101
+ // src/sizes.ts
102
+ var import_promises = require("fs/promises");
103
+ var import_node_zlib = require("zlib");
104
+ function measureBuffer(buffer) {
105
+ return {
106
+ raw: buffer.byteLength,
107
+ gzip: (0, import_node_zlib.gzipSync)(buffer).byteLength,
108
+ brotli: (0, import_node_zlib.brotliCompressSync)(buffer).byteLength
109
+ };
110
+ }
111
+ async function measureFile(path) {
112
+ const buffer = await (0, import_promises.readFile)(path);
113
+ return measureBuffer(buffer);
114
+ }
115
+
116
+ // src/report.ts
117
+ function subtract(next, prev) {
118
+ return {
119
+ raw: next.raw - prev.raw,
120
+ gzip: next.gzip - prev.gzip,
121
+ brotli: next.brotli - prev.brotli
122
+ };
123
+ }
124
+ function sum(sizes) {
125
+ return sizes.reduce(
126
+ (acc, size) => ({
127
+ raw: acc.raw + size.raw,
128
+ gzip: acc.gzip + size.gzip,
129
+ brotli: acc.brotli + size.brotli
130
+ }),
131
+ { raw: 0, gzip: 0, brotli: 0 }
132
+ );
133
+ }
134
+ function buildReport(measurements, previous) {
135
+ const rows = measurements.map((measurement) => {
136
+ const prev = previous?.get(measurement.path);
137
+ return {
138
+ path: measurement.path,
139
+ size: measurement.size,
140
+ delta: prev ? subtract(measurement.size, prev) : null
141
+ };
142
+ });
143
+ const totalSize = sum(measurements.map((m) => m.size));
144
+ const previousTotal = previous ? sum([...previous.values()]) : void 0;
145
+ return {
146
+ rows,
147
+ total: {
148
+ size: totalSize,
149
+ delta: previousTotal ? subtract(totalSize, previousTotal) : null
150
+ }
151
+ };
152
+ }
153
+
154
+ // src/render.ts
155
+ var plainCell = (text) => ({ plain: text, colored: text });
156
+ function colorDelta(delta, metric, colors) {
157
+ if (delta === null) return { plain: "new", colored: colors.cyan("new") };
158
+ const value = delta[metric];
159
+ const text = formatDelta(value);
160
+ if (value < 0) return { plain: text, colored: colors.green(text) };
161
+ if (value > 0) return { plain: text, colored: colors.red(text) };
162
+ return { plain: text, colored: colors.dim(text) };
163
+ }
164
+ function buildCells(label, size, delta, metric, hasDelta, options, bold) {
165
+ const cells = [
166
+ { plain: label, colored: bold ? options.colors.bold(label) : label },
167
+ plainCell(formatBytes(size.raw))
168
+ ];
169
+ if (options.gzip) cells.push(plainCell(formatBytes(size.gzip)));
170
+ if (options.brotli) cells.push(plainCell(formatBytes(size.brotli)));
171
+ if (hasDelta) cells.push(colorDelta(delta, metric, options.colors));
172
+ return cells;
173
+ }
174
+ function renderJson(report) {
175
+ return JSON.stringify(
176
+ {
177
+ files: report.rows.map((row) => ({
178
+ path: row.path,
179
+ raw: row.size.raw,
180
+ gzip: row.size.gzip,
181
+ brotli: row.size.brotli,
182
+ delta: row.delta
183
+ })),
184
+ total: {
185
+ raw: report.total.size.raw,
186
+ gzip: report.total.size.gzip,
187
+ brotli: report.total.size.brotli,
188
+ delta: report.total.delta
189
+ }
190
+ },
191
+ null,
192
+ 2
193
+ );
194
+ }
195
+ function renderTable(report, options) {
196
+ const metric = options.gzip ? "gzip" : "raw";
197
+ const hasDelta = report.rows.some((row) => row.delta !== null) || report.total.delta !== null;
198
+ const headers = ["File", "Raw"];
199
+ if (options.gzip) headers.push("Gzip");
200
+ if (options.brotli) headers.push("Brotli");
201
+ if (hasDelta) headers.push(`\u0394 ${metric}`);
202
+ const headerCells = headers.map((header) => ({
203
+ plain: header,
204
+ colored: options.colors.dim(header)
205
+ }));
206
+ const bodyRows = report.rows.map(
207
+ (row) => buildCells(row.path, row.size, row.delta, metric, hasDelta, options, false)
208
+ );
209
+ const totalRow = buildCells(
210
+ "total",
211
+ report.total.size,
212
+ report.total.delta,
213
+ metric,
214
+ hasDelta,
215
+ options,
216
+ true
217
+ );
218
+ const matrix = [headerCells, ...bodyRows, totalRow];
219
+ const widths = headers.map(
220
+ (_, column) => Math.max(...matrix.map((row) => row[column].plain.length))
221
+ );
222
+ return matrix.map(
223
+ (row) => row.map((cell, column) => {
224
+ const padding = " ".repeat(widths[column] - cell.plain.length);
225
+ return column === 0 ? cell.colored + padding : padding + cell.colored;
226
+ }).join(" ")
227
+ ).join("\n");
228
+ }
229
+ function renderReport(report, options) {
230
+ return options.json ? renderJson(report) : renderTable(report, options);
231
+ }
232
+
233
+ // src/watch.ts
234
+ var fs = __toESM(require("fs"), 1);
235
+ var defaultImpl = (path, onChange) => {
236
+ const watcher = fs.watch(path, () => onChange());
237
+ return { close: () => watcher.close() };
238
+ };
239
+ function watchPaths(paths, onChange, impl = defaultImpl) {
240
+ const watchers = paths.map((path) => impl(path, onChange));
241
+ return () => {
242
+ for (const watcher of watchers) watcher.close();
243
+ };
244
+ }
245
+
246
+ // src/cli.ts
247
+ var import_node_path = require("path");
248
+ var import_commander = require("commander");
249
+ var DESCRIPTION = "Print the gzipped bundle cost of your files \u2014 and the delta on every save.";
250
+ var HELP_EXAMPLES = `
251
+ Examples:
252
+ $ bundle-cost dist/index.js
253
+ $ bundle-cost dist/*.js --brotli
254
+ $ bundle-cost dist/index.js --limit 50kb
255
+ $ bundle-cost dist/index.js --watch
256
+ $ bundle-cost dist/index.js --json
257
+ `;
258
+ var trimTrailingNewline = (text) => text.replace(/\n+$/, "");
259
+ async function measureAll(targets) {
260
+ return Promise.all(
261
+ targets.map(async (target) => ({ ...target, size: await measureFile(target.path) }))
262
+ );
263
+ }
264
+ var toMeasurements = (measured) => measured.map((m) => ({ path: m.label, size: m.size }));
265
+ var snapshot = (measured) => new Map(measured.map((m) => [m.label, m.size]));
266
+ function startWatch(targets, renderOptions, initial, ctx) {
267
+ let previous = snapshot(initial);
268
+ ctx.log(renderOptions.colors.dim("watching for changes\u2026 (ctrl-c to stop)"));
269
+ ctx.watch(
270
+ targets.map((target) => target.path),
271
+ async () => {
272
+ let next;
273
+ try {
274
+ next = await measureAll(targets);
275
+ } catch (err) {
276
+ ctx.error(renderOptions.colors.red(`Could not read file: ${err.message}`));
277
+ return;
278
+ }
279
+ ctx.log(renderReport(buildReport(toMeasurements(next), previous), renderOptions));
280
+ previous = snapshot(next);
281
+ }
282
+ );
283
+ }
284
+ function reportBudget(totalGzip, limit, colors, ctx) {
285
+ const headline = `${formatBytes(totalGzip)} gzip`;
286
+ if (totalGzip > limit) {
287
+ ctx.error(colors.red(`\u2717 ${headline} exceeds the ${formatBytes(limit)} budget`));
288
+ return 1;
289
+ }
290
+ ctx.log(colors.green(`\u2713 ${headline} is within the ${formatBytes(limit)} budget`));
291
+ return 0;
292
+ }
293
+ async function execute(files, options, ctx) {
294
+ const colorsEnabled = options.color !== false && !ctx.env.NO_COLOR;
295
+ const renderOptions = {
296
+ gzip: options.gzip !== false,
297
+ brotli: options.brotli === true,
298
+ json: options.json === true,
299
+ colors: createColors(colorsEnabled)
300
+ };
301
+ let limit;
302
+ if (options.limit !== void 0) {
303
+ try {
304
+ limit = parseSize(options.limit);
305
+ } catch (err) {
306
+ ctx.error(renderOptions.colors.red(err.message));
307
+ return 1;
308
+ }
309
+ }
310
+ const targets = files.map((file) => ({ label: file, path: (0, import_node_path.resolve)(ctx.cwd, file) }));
311
+ let measured;
312
+ try {
313
+ measured = await measureAll(targets);
314
+ } catch (err) {
315
+ ctx.error(renderOptions.colors.red(`Could not read file: ${err.message}`));
316
+ return 1;
317
+ }
318
+ const report = buildReport(toMeasurements(measured));
319
+ ctx.log(renderReport(report, renderOptions));
320
+ if (options.watch) {
321
+ startWatch(targets, renderOptions, measured, ctx);
322
+ return 0;
323
+ }
324
+ if (limit !== void 0) {
325
+ return reportBudget(report.total.size.gzip, limit, renderOptions.colors, ctx);
326
+ }
327
+ return 0;
328
+ }
329
+ async function run(argv, deps = {}) {
330
+ const log = deps.log ?? ((message) => process.stdout.write(`${message}
331
+ `));
332
+ const error = deps.error ?? ((message) => process.stderr.write(`${message}
333
+ `));
334
+ const ctx = {
335
+ log,
336
+ error,
337
+ cwd: deps.cwd ?? process.cwd(),
338
+ env: deps.env ?? process.env,
339
+ watch: deps.watch ?? watchPaths
340
+ };
341
+ const program = new import_commander.Command();
342
+ 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({
343
+ writeOut: (text) => log(trimTrailingNewline(text)),
344
+ writeErr: (text) => error(trimTrailingNewline(text))
345
+ });
346
+ let exitCode = 0;
347
+ program.action(async (files, options) => {
348
+ exitCode = await execute(files, options, ctx);
349
+ });
350
+ try {
351
+ await program.parseAsync(argv, { from: "user" });
352
+ } catch (err) {
353
+ return err.exitCode;
354
+ }
355
+ return exitCode;
356
+ }
357
+ // Annotate the CommonJS export names for ESM import in node:
358
+ 0 && (module.exports = {
359
+ buildReport,
360
+ createColors,
361
+ formatBytes,
362
+ formatDelta,
363
+ measureBuffer,
364
+ measureFile,
365
+ parseSize,
366
+ renderReport,
367
+ run,
368
+ watchPaths
369
+ });