@electron-stagewright/plugin-budget 0.0.0-stage → 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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/dist/budget.d.ts +134 -0
  4. package/dist/budget.d.ts.map +1 -0
  5. package/dist/budget.js +129 -0
  6. package/dist/budget.js.map +1 -0
  7. package/dist/bundle.d.ts +15 -0
  8. package/dist/bundle.d.ts.map +1 -0
  9. package/dist/bundle.js +64 -0
  10. package/dist/bundle.js.map +1 -0
  11. package/dist/check.d.ts +43 -0
  12. package/dist/check.d.ts.map +1 -0
  13. package/dist/check.js +172 -0
  14. package/dist/check.js.map +1 -0
  15. package/dist/cli.d.ts +77 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +401 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +17 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/measure.d.ts +47 -0
  24. package/dist/measure.d.ts.map +1 -0
  25. package/dist/measure.js +139 -0
  26. package/dist/measure.js.map +1 -0
  27. package/dist/memory.d.ts +43 -0
  28. package/dist/memory.d.ts.map +1 -0
  29. package/dist/memory.js +159 -0
  30. package/dist/memory.js.map +1 -0
  31. package/dist/plugin.d.ts +13 -0
  32. package/dist/plugin.d.ts.map +1 -0
  33. package/dist/plugin.js +456 -0
  34. package/dist/plugin.js.map +1 -0
  35. package/dist/probe.d.ts +64 -0
  36. package/dist/probe.d.ts.map +1 -0
  37. package/dist/probe.js +161 -0
  38. package/dist/probe.js.map +1 -0
  39. package/dist/render.d.ts +21 -0
  40. package/dist/render.d.ts.map +1 -0
  41. package/dist/render.js +63 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/run.d.ts +218 -0
  44. package/dist/run.d.ts.map +1 -0
  45. package/dist/run.js +218 -0
  46. package/dist/run.js.map +1 -0
  47. package/dist/session.d.ts +54 -0
  48. package/dist/session.d.ts.map +1 -0
  49. package/dist/session.js +167 -0
  50. package/dist/session.js.map +1 -0
  51. package/package.json +60 -3
package/dist/check.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Compare a run to a budget. Pure: no I/O, no Electron.
3
+ *
4
+ * @module
5
+ */
6
+ import { PROCESS_TYPES } from './budget.js';
7
+ import { summarizeRun } from './run.js';
8
+ const ANY_PHASE_ORDER = ['warm', 'live', 'cold'];
9
+ function expectations(budget, platform, options) {
10
+ const list = [];
11
+ const tolerance = (own) => options.strict === true ? 0 : (own ?? budget.thresholdPercent ?? 0);
12
+ if (budget.startup !== undefined) {
13
+ list.push({
14
+ id: 'startup.firstWindowMs',
15
+ section: 'startup',
16
+ unit: 'ms',
17
+ limit: budget.startup.maxFirstWindowMs,
18
+ thresholdPercent: tolerance(budget.startup.thresholdPercent),
19
+ phase: options.phase ?? budget.startup.phase ?? 'cold',
20
+ });
21
+ }
22
+ const memory = budget.memory?.[platform];
23
+ if (memory !== undefined) {
24
+ const base = {
25
+ section: 'memory',
26
+ unit: 'MB',
27
+ thresholdPercent: tolerance(memory.thresholdPercent),
28
+ phase: options.phase ?? memory.phase ?? 'warm',
29
+ metric: memory.metric,
30
+ };
31
+ for (const type of PROCESS_TYPES) {
32
+ const max = memory.perTypeMb?.[type];
33
+ if (max !== undefined)
34
+ list.push({ ...base, id: `memory.${type}`, limit: max });
35
+ }
36
+ if (memory.totalWithoutGpuMb !== undefined) {
37
+ list.push({ ...base, id: 'memory.totalWithoutGpu', limit: memory.totalWithoutGpuMb });
38
+ }
39
+ if (memory.totalMb !== undefined) {
40
+ list.push({ ...base, id: 'memory.total', limit: memory.totalMb });
41
+ }
42
+ }
43
+ const mainThread = budget.mainThread;
44
+ if (mainThread !== undefined) {
45
+ const base = {
46
+ section: 'mainThread',
47
+ thresholdPercent: tolerance(mainThread.thresholdPercent),
48
+ ...(options.phase === undefined ? {} : { phase: options.phase }),
49
+ };
50
+ if (mainThread.maxBlockedPercent !== undefined) {
51
+ list.push({
52
+ ...base,
53
+ id: 'mainThread.blockedPercent',
54
+ unit: '%',
55
+ limit: mainThread.maxBlockedPercent,
56
+ });
57
+ }
58
+ if (mainThread.maxStallMs !== undefined) {
59
+ list.push({ ...base, id: 'mainThread.maxStallMs', unit: 'ms', limit: mainThread.maxStallMs });
60
+ }
61
+ }
62
+ const ipc = budget.ipc;
63
+ if (ipc !== undefined) {
64
+ const base = {
65
+ section: 'ipc',
66
+ unit: 'ms',
67
+ thresholdPercent: tolerance(ipc.thresholdPercent),
68
+ ...(options.phase === undefined ? {} : { phase: options.phase }),
69
+ };
70
+ if (ipc.maxP95Ms !== undefined)
71
+ list.push({ ...base, id: 'ipc.p95Ms', limit: ipc.maxP95Ms });
72
+ if (ipc.maxP99Ms !== undefined)
73
+ list.push({ ...base, id: 'ipc.p99Ms', limit: ipc.maxP99Ms });
74
+ }
75
+ for (const entry of budget.bundle?.entries ?? []) {
76
+ list.push({
77
+ id: `bundle.${entry.name ?? entry.path}`,
78
+ section: 'bundle',
79
+ unit: 'KB',
80
+ limit: entry.maxKb,
81
+ thresholdPercent: tolerance(budget.bundle?.thresholdPercent),
82
+ });
83
+ }
84
+ return list;
85
+ }
86
+ function findRow(rows, expectation) {
87
+ const candidates = rows.filter((row) => row.id === expectation.id &&
88
+ (expectation.metric === undefined || row.metric === expectation.metric));
89
+ if (expectation.section === 'bundle')
90
+ return candidates[0];
91
+ if (expectation.phase !== undefined) {
92
+ return candidates.find((row) => row.phase === expectation.phase);
93
+ }
94
+ for (const phase of ANY_PHASE_ORDER) {
95
+ const row = candidates.find((candidate) => candidate.phase === phase);
96
+ if (row !== undefined)
97
+ return row;
98
+ }
99
+ return undefined;
100
+ }
101
+ function unmeasuredDetail(run, expectation) {
102
+ if (expectation.section === 'bundle') {
103
+ const id = expectation.id.slice('bundle.'.length);
104
+ const entry = run.bundle?.entries.find((candidate) => candidate.id === id);
105
+ return entry?.unavailable ?? 'the run has no measurement for this bundle entry';
106
+ }
107
+ const rounds = run.rounds.filter((round) => expectation.phase === undefined || round.phase === expectation.phase);
108
+ if (rounds.length === 0) {
109
+ return expectation.phase === undefined
110
+ ? 'the run has no rounds'
111
+ : `the run has no ${expectation.phase} round`;
112
+ }
113
+ if (expectation.section === 'memory') {
114
+ const measured = rounds.map((round) => round.measurement.memory?.metric).find(Boolean);
115
+ // Megabytes in another metric are a different quantity, never a comparable one.
116
+ if (measured !== undefined && measured !== expectation.metric) {
117
+ return `the run measured ${measured}, the budget names ${expectation.metric}`;
118
+ }
119
+ }
120
+ if (expectation.section === 'ipc' && rounds.some((r) => r.measurement.ipc?.count === 0)) {
121
+ return 'the IPC probe completed no round trip';
122
+ }
123
+ const reason = rounds
124
+ .map((round) => round.measurement.unavailable?.[expectation.section])
125
+ .find((value) => value !== undefined);
126
+ return reason ?? `the run did not measure ${expectation.id}`;
127
+ }
128
+ /** Compare `run` to `budget` for the platform the run was measured on. */
129
+ export function checkRun(run, budget, options = {}) {
130
+ const rows = summarizeRun(run);
131
+ const results = expectations(budget, run.platform, options).map((expectation) => {
132
+ // Divide last: 200 * (1 + 15 / 100) is 229.99999999999997, which would fail a 230.
133
+ const exactAllowed = (expectation.limit * (100 + expectation.thresholdPercent)) / 100;
134
+ const allowed = Math.round(exactAllowed * 10) / 10;
135
+ const base = {
136
+ id: expectation.id,
137
+ unit: expectation.unit,
138
+ limit: expectation.limit,
139
+ allowed,
140
+ ...(expectation.phase === undefined ? {} : { phase: expectation.phase }),
141
+ ...(expectation.metric === undefined ? {} : { metric: expectation.metric }),
142
+ };
143
+ const row = findRow(rows, expectation);
144
+ if (row === undefined) {
145
+ return { ...base, status: 'unmeasured', detail: unmeasuredDetail(run, expectation) };
146
+ }
147
+ return {
148
+ ...base,
149
+ ...(row.phase === undefined ? {} : { phase: row.phase }),
150
+ status: row.exact <= exactAllowed ? 'pass' : 'fail',
151
+ actual: row.value,
152
+ };
153
+ });
154
+ const count = (status) => results.filter((result) => result.status === status).length;
155
+ const summary = { pass: count('pass'), fail: count('fail'), unmeasured: count('unmeasured') };
156
+ const reason = results.length === 0
157
+ ? 'NO_EXPECTATIONS'
158
+ : summary.fail > 0
159
+ ? 'OVER_BUDGET'
160
+ : summary.pass === 0
161
+ ? 'NO_MEASUREMENTS'
162
+ : options.requireMeasurement === true && summary.unmeasured > 0
163
+ ? 'UNMEASURED'
164
+ : undefined;
165
+ return {
166
+ passed: reason === undefined,
167
+ ...(reason === undefined ? {} : { reason }),
168
+ summary,
169
+ results,
170
+ };
171
+ }
172
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `electron-stagewright-budget`: the CI face of the budget plugin.
4
+ *
5
+ * Exit codes: 0 within budget, 1 over budget or nothing measured, 2 usage, unreadable input, or any
6
+ * other error.
7
+ *
8
+ * @module
9
+ */
10
+ import { type Phase } from './budget.js';
11
+ import { measureApp, type LaunchPhase } from './measure.js';
12
+ export declare const BUDGET_EXIT_CODES: {
13
+ readonly SUCCESS: 0;
14
+ readonly OVER_BUDGET: 1;
15
+ readonly USAGE: 2;
16
+ };
17
+ export type BudgetExitCode = (typeof BUDGET_EXIT_CODES)[keyof typeof BUDGET_EXIT_CODES];
18
+ export interface BudgetCliIo {
19
+ readonly stdout: (text: string) => void;
20
+ readonly stderr: (text: string) => void;
21
+ }
22
+ export type BudgetCliOptions = {
23
+ readonly command: 'help';
24
+ } | {
25
+ readonly command: 'check';
26
+ readonly runPath: string;
27
+ readonly budgetPath: string;
28
+ readonly strict: boolean;
29
+ readonly requireMeasurement: boolean;
30
+ readonly phase?: Phase;
31
+ readonly json: boolean;
32
+ } | {
33
+ readonly command: 'compare';
34
+ readonly beforePath: string;
35
+ readonly afterPath: string;
36
+ readonly json: boolean;
37
+ } | {
38
+ readonly command: 'bundle';
39
+ readonly budgetPath: string;
40
+ readonly root?: string;
41
+ readonly outPath?: string;
42
+ readonly strict: boolean;
43
+ readonly json: boolean;
44
+ } | MeasureCliOptions;
45
+ export interface MeasureCliOptions {
46
+ readonly command: 'measure';
47
+ readonly main?: string;
48
+ readonly executablePath?: string;
49
+ readonly args: readonly string[];
50
+ readonly env: Readonly<Record<string, string>>;
51
+ readonly budgetPath?: string;
52
+ readonly rounds: number;
53
+ readonly phases: readonly LaunchPhase[];
54
+ readonly settleMs?: number;
55
+ readonly windowMs?: number;
56
+ readonly workloadPath?: string;
57
+ readonly workloadTimeoutMs?: number;
58
+ readonly phase?: Phase;
59
+ readonly label?: string;
60
+ readonly outPath?: string;
61
+ readonly check: boolean;
62
+ readonly strict: boolean;
63
+ readonly requireMeasurement: boolean;
64
+ readonly json: boolean;
65
+ }
66
+ /** Seam for tests: the launch-and-measure step. */
67
+ export interface BudgetCliDependencies {
68
+ readonly measureApp: typeof measureApp;
69
+ }
70
+ export declare function formatBudgetCliHelp(): string;
71
+ /** Strict parser: an unknown flag or a missing value is a usage error. */
72
+ export declare function parseBudgetCliArgs(argv: readonly string[]): BudgetCliOptions;
73
+ /** Execute already-parsed options. Throws the typed file errors; the caller maps them to exit 2. */
74
+ export declare function runBudgetCli(options: BudgetCliOptions, io?: BudgetCliIo, deps?: BudgetCliDependencies): Promise<BudgetExitCode>;
75
+ /** Parse and execute; never throws. */
76
+ export declare function runBudgetCliCommand(argv: readonly string[], io?: BudgetCliIo, deps?: BudgetCliDependencies): Promise<BudgetExitCode>;
77
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.js ADDED
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `electron-stagewright-budget`: the CI face of the budget plugin.
4
+ *
5
+ * Exit codes: 0 within budget, 1 over budget or nothing measured, 2 usage, unreadable input, or any
6
+ * other error.
7
+ *
8
+ * @module
9
+ */
10
+ import { realpathSync } from 'node:fs';
11
+ import { mkdir, writeFile } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { BudgetFileError, PHASES, loadBudget } from './budget.js';
15
+ import { measureBundle } from './bundle.js';
16
+ import { checkRun } from './check.js';
17
+ import { DEFAULT_ROUNDS, measureApp } from './measure.js';
18
+ import { compareRuns, renderCheckReport, renderComparison } from './render.js';
19
+ import { RunFileError, createRun, loadRun } from './run.js';
20
+ export const BUDGET_EXIT_CODES = { SUCCESS: 0, OVER_BUDGET: 1, USAGE: 2 };
21
+ const PROCESS_IO = {
22
+ stdout: (text) => process.stdout.write(text),
23
+ stderr: (text) => process.stderr.write(text),
24
+ };
25
+ class UsageError extends Error {
26
+ }
27
+ export function formatBudgetCliHelp() {
28
+ return [
29
+ 'Usage:',
30
+ ' electron-stagewright-budget measure (--main <main.js> | --executable <path>) [--arg <v>]...',
31
+ ' [--env K=V]... [--budget <file>] [--rounds 3]',
32
+ ' [--phases cold,warm] [--settle-ms 3000] [--window-ms 2000]',
33
+ ' [--workload <script.mjs> [--workload-timeout-ms 120000]]',
34
+ ' [--label <text>] [--out <run.json>] [--json]',
35
+ ' [--check [--strict] [--require-measurement] [--phase cold|warm]]',
36
+ ' electron-stagewright-budget check <run.json> --budget <file> [--strict]',
37
+ ' [--require-measurement] [--phase cold|warm|live] [--json]',
38
+ ' electron-stagewright-budget compare <before.json> <after.json> [--json]',
39
+ ' electron-stagewright-budget bundle --budget <file> [--root <dir>] [--out <run.json>]',
40
+ ' [--strict] [--json]',
41
+ '',
42
+ 'measure Launch the app on a throwaway profile and measure cold and warm rounds.',
43
+ ' --workload is an ES module whose default export, async ({ dispatch, sessionId,',
44
+ ' phase }), drives the app inside the measurement window. Without --out or',
45
+ ' --check the run document goes to stdout. --phase reads every metric from',
46
+ ' that phase; a metric whose phase was not measured is unmeasured.',
47
+ 'check Compare a measured run to a budget file.',
48
+ 'compare Print a before/after table of two runs.',
49
+ 'bundle Measure the bundle entries of a budget and gate them; needs no Electron.',
50
+ '',
51
+ '--strict Ignore thresholdPercent; the limit itself gates.',
52
+ '--require-measurement Fail when a budgeted metric was not measured.',
53
+ '',
54
+ 'Exit codes: 0 within budget, 1 over budget or nothing measured,',
55
+ ' 2 usage, unreadable input, or any other error.',
56
+ ].join('\n');
57
+ }
58
+ function positiveInt(flag, value, min = 1) {
59
+ const parsed = Number(value);
60
+ if (!Number.isSafeInteger(parsed) || parsed < min) {
61
+ throw new UsageError(`${flag} expects an integer of at least ${min}`);
62
+ }
63
+ return parsed;
64
+ }
65
+ function parseMeasureArgs(rest) {
66
+ const single = new Map();
67
+ const args = [];
68
+ const env = {};
69
+ const flags = new Set();
70
+ const valueFlags = [
71
+ '--main',
72
+ '--executable',
73
+ '--budget',
74
+ '--rounds',
75
+ '--phases',
76
+ '--settle-ms',
77
+ '--window-ms',
78
+ '--workload',
79
+ '--workload-timeout-ms',
80
+ '--phase',
81
+ '--label',
82
+ '--out',
83
+ ];
84
+ const booleanFlags = ['--check', '--strict', '--require-measurement', '--json'];
85
+ for (let index = 0; index < rest.length; index += 1) {
86
+ const token = rest[index];
87
+ // Scanned here, not over all of argv, so `--arg --help` is forwarded to the app.
88
+ if (token === '--help' || token === '-h')
89
+ return { command: 'help' };
90
+ if (booleanFlags.includes(token)) {
91
+ flags.add(token);
92
+ continue;
93
+ }
94
+ if (token !== '--arg' && token !== '--env' && !valueFlags.includes(token)) {
95
+ throw new UsageError(`Unknown option for measure: ${token}`);
96
+ }
97
+ const value = rest[index + 1];
98
+ // --arg forwards anything, including empty values and values that start with dashes.
99
+ if (value === undefined ||
100
+ (token !== '--arg' && (value.length === 0 || value.startsWith('--')))) {
101
+ throw new UsageError(`${token} expects a value`);
102
+ }
103
+ index += 1;
104
+ if (token === '--arg') {
105
+ args.push(value);
106
+ }
107
+ else if (token === '--env') {
108
+ const separator = value.indexOf('=');
109
+ if (separator <= 0)
110
+ throw new UsageError('--env expects KEY=VALUE');
111
+ env[value.slice(0, separator)] = value.slice(separator + 1);
112
+ }
113
+ else {
114
+ if (single.has(token))
115
+ throw new UsageError(`${token} may be given only once`);
116
+ single.set(token, value);
117
+ }
118
+ }
119
+ const main = single.get('--main');
120
+ const executablePath = single.get('--executable');
121
+ if (main === undefined && executablePath === undefined) {
122
+ throw new UsageError('measure requires --main <main.js> or --executable <path>');
123
+ }
124
+ const phases = [
125
+ ...new Set((single.get('--phases') ?? 'cold,warm').split(',').map((phase) => phase.trim())),
126
+ ];
127
+ if (phases.some((phase) => phase !== 'cold' && phase !== 'warm')) {
128
+ throw new UsageError('--phases expects cold, warm, or cold,warm');
129
+ }
130
+ const phase = single.get('--phase');
131
+ if (phase !== undefined && !phases.includes(phase)) {
132
+ throw new UsageError('--phase must name a measured phase: cold or warm');
133
+ }
134
+ const check = flags.has('--check');
135
+ const budgetPath = single.get('--budget');
136
+ if (check && budgetPath === undefined)
137
+ throw new UsageError('--check requires --budget <file>');
138
+ if (!check && (flags.has('--strict') || flags.has('--require-measurement') || phase)) {
139
+ throw new UsageError('--strict, --require-measurement, and --phase apply only with --check');
140
+ }
141
+ const optional = (key, value) => value === undefined ? {} : { [key]: value };
142
+ const number = (flag, min) => {
143
+ const value = single.get(flag);
144
+ return value === undefined ? undefined : positiveInt(flag, value, min);
145
+ };
146
+ return {
147
+ command: 'measure',
148
+ ...optional('main', main === undefined ? undefined : path.resolve(main)),
149
+ ...optional('executablePath', executablePath === undefined ? undefined : path.resolve(executablePath)),
150
+ args,
151
+ env,
152
+ ...optional('budgetPath', budgetPath),
153
+ rounds: number('--rounds', 1) ?? DEFAULT_ROUNDS,
154
+ phases: phases,
155
+ ...optional('settleMs', number('--settle-ms', 0)),
156
+ ...optional('windowMs', number('--window-ms', 250)),
157
+ ...optional('workloadPath', single.get('--workload')),
158
+ ...optional('workloadTimeoutMs', number('--workload-timeout-ms', 1000)),
159
+ ...optional('phase', phase),
160
+ ...optional('label', single.get('--label')),
161
+ ...optional('outPath', single.get('--out')),
162
+ check,
163
+ strict: flags.has('--strict'),
164
+ requireMeasurement: flags.has('--require-measurement'),
165
+ json: flags.has('--json'),
166
+ };
167
+ }
168
+ /** Strict parser: an unknown flag or a missing value is a usage error. */
169
+ export function parseBudgetCliArgs(argv) {
170
+ const [command, ...rest] = argv;
171
+ if (command === undefined || command === 'help' || command === '--help' || command === '-h') {
172
+ if (command === undefined)
173
+ throw new UsageError('Expected a subcommand: measure, check, compare, bundle');
174
+ return { command: 'help' };
175
+ }
176
+ if (command === 'measure')
177
+ return parseMeasureArgs(rest);
178
+ if (command !== 'check' && command !== 'compare' && command !== 'bundle') {
179
+ throw new UsageError(`Unknown subcommand: ${command}`);
180
+ }
181
+ if (rest.includes('--help') || rest.includes('-h'))
182
+ return { command: 'help' };
183
+ const valueFlags = {
184
+ check: ['--budget', '--phase'],
185
+ compare: [],
186
+ bundle: ['--budget', '--root', '--out'],
187
+ };
188
+ const booleanFlags = {
189
+ check: ['--strict', '--require-measurement', '--json'],
190
+ compare: ['--json'],
191
+ bundle: ['--strict', '--json'],
192
+ };
193
+ const values = new Map();
194
+ const flags = new Set();
195
+ const positional = [];
196
+ for (let index = 0; index < rest.length; index += 1) {
197
+ const token = rest[index];
198
+ if (valueFlags[command].includes(token)) {
199
+ const value = rest[index + 1];
200
+ if (value === undefined || value.length === 0 || value.startsWith('--')) {
201
+ throw new UsageError(`${token} expects a value`);
202
+ }
203
+ values.set(token, value);
204
+ index += 1;
205
+ }
206
+ else if (booleanFlags[command].includes(token)) {
207
+ flags.add(token);
208
+ }
209
+ else if (token.startsWith('-')) {
210
+ throw new UsageError(`Unknown option for ${command}: ${token}`);
211
+ }
212
+ else {
213
+ positional.push(token);
214
+ }
215
+ }
216
+ const requireBudget = () => {
217
+ const budgetPath = values.get('--budget');
218
+ if (budgetPath === undefined)
219
+ throw new UsageError(`${command} requires --budget <file>`);
220
+ return budgetPath;
221
+ };
222
+ const json = flags.has('--json');
223
+ if (command === 'compare') {
224
+ const [beforePath, afterPath, extra] = positional;
225
+ if (beforePath === undefined || afterPath === undefined || extra !== undefined) {
226
+ throw new UsageError('compare expects exactly two run files');
227
+ }
228
+ return { command, beforePath, afterPath, json };
229
+ }
230
+ if (command === 'bundle') {
231
+ if (positional.length > 0)
232
+ throw new UsageError(`Unexpected argument: ${positional[0]}`);
233
+ const root = values.get('--root');
234
+ const outPath = values.get('--out');
235
+ return {
236
+ command,
237
+ budgetPath: requireBudget(),
238
+ ...(root === undefined ? {} : { root }),
239
+ ...(outPath === undefined ? {} : { outPath }),
240
+ strict: flags.has('--strict'),
241
+ json,
242
+ };
243
+ }
244
+ const [runPath, extra] = positional;
245
+ if (runPath === undefined || extra !== undefined) {
246
+ throw new UsageError('check expects exactly one run file');
247
+ }
248
+ const phase = values.get('--phase');
249
+ if (phase !== undefined && !PHASES.includes(phase)) {
250
+ throw new UsageError(`--phase expects one of ${PHASES.join(', ')}`);
251
+ }
252
+ return {
253
+ command,
254
+ runPath,
255
+ budgetPath: requireBudget(),
256
+ strict: flags.has('--strict'),
257
+ requireMeasurement: flags.has('--require-measurement'),
258
+ ...(phase === undefined ? {} : { phase: phase }),
259
+ json,
260
+ };
261
+ }
262
+ function emitReport(report, json, io) {
263
+ io.stdout(`${json ? JSON.stringify(report) : renderCheckReport(report)}\n`);
264
+ return report.passed ? BUDGET_EXIT_CODES.SUCCESS : BUDGET_EXIT_CODES.OVER_BUDGET;
265
+ }
266
+ async function writeRun(outPath, run) {
267
+ await mkdir(path.dirname(path.resolve(outPath)), { recursive: true });
268
+ await writeFile(outPath, `${JSON.stringify(run, null, 2)}\n`);
269
+ }
270
+ async function loadWorkload(workloadPath) {
271
+ let module;
272
+ try {
273
+ module = (await import(pathToFileURL(path.resolve(workloadPath)).href));
274
+ }
275
+ catch (error) {
276
+ throw new UsageError(`Cannot load workload ${workloadPath}: ${error instanceof Error ? error.message : String(error)}`);
277
+ }
278
+ if (typeof module.default !== 'function') {
279
+ throw new UsageError(`Workload ${workloadPath} must default-export an async function`);
280
+ }
281
+ return module.default;
282
+ }
283
+ async function runMeasure(options, io, deps) {
284
+ const budget = options.budgetPath === undefined ? undefined : await loadBudget(options.budgetPath);
285
+ const workload = options.workloadPath === undefined ? undefined : await loadWorkload(options.workloadPath);
286
+ let run = await deps.measureApp({
287
+ launch: {
288
+ ...(options.main === undefined ? {} : { main: options.main }),
289
+ ...(options.executablePath === undefined ? {} : { executablePath: options.executablePath }),
290
+ args: options.args,
291
+ env: options.env,
292
+ },
293
+ rounds: options.rounds,
294
+ phases: options.phases,
295
+ ...(options.settleMs === undefined ? {} : { settleMs: options.settleMs }),
296
+ ...(options.windowMs === undefined ? {} : { windowMs: options.windowMs }),
297
+ ...(budget === undefined ? {} : { budget }),
298
+ ...(workload === undefined ? {} : { workload }),
299
+ ...(options.workloadTimeoutMs === undefined
300
+ ? {}
301
+ : { workloadTimeoutMs: options.workloadTimeoutMs }),
302
+ ...(options.label === undefined ? {} : { label: options.label }),
303
+ onProgress: (message) => io.stderr(`${message}\n`),
304
+ });
305
+ // A run with no round would later read as "nothing over budget"; never write one.
306
+ if (run.rounds.length === 0) {
307
+ io.stderr('No round produced a measurement; no run was written.\n');
308
+ return BUDGET_EXIT_CODES.OVER_BUDGET;
309
+ }
310
+ if (budget?.bundle !== undefined && options.budgetPath !== undefined) {
311
+ const root = path.dirname(path.resolve(options.budgetPath));
312
+ run = { ...run, bundle: await measureBundle(root, budget.bundle.entries) };
313
+ }
314
+ if (options.outPath !== undefined)
315
+ await writeRun(options.outPath, run);
316
+ if (options.check && budget !== undefined) {
317
+ const report = checkRun(run, budget, {
318
+ strict: options.strict,
319
+ requireMeasurement: options.requireMeasurement,
320
+ ...(options.phase === undefined ? {} : { phase: options.phase }),
321
+ });
322
+ return emitReport(report, options.json, io);
323
+ }
324
+ if (options.outPath === undefined)
325
+ io.stdout(`${JSON.stringify(run, null, 2)}\n`);
326
+ else
327
+ io.stderr(`Wrote ${options.outPath}\n`);
328
+ return BUDGET_EXIT_CODES.SUCCESS;
329
+ }
330
+ /** Execute already-parsed options. Throws the typed file errors; the caller maps them to exit 2. */
331
+ export async function runBudgetCli(options, io = PROCESS_IO, deps = { measureApp }) {
332
+ if (options.command === 'help') {
333
+ io.stdout(`${formatBudgetCliHelp()}\n`);
334
+ return BUDGET_EXIT_CODES.SUCCESS;
335
+ }
336
+ if (options.command === 'measure')
337
+ return runMeasure(options, io, deps);
338
+ if (options.command === 'compare') {
339
+ const rows = compareRuns(await loadRun(options.beforePath), await loadRun(options.afterPath));
340
+ if (rows.length === 0) {
341
+ io.stderr('Neither run holds a measurement; nothing to compare.\n');
342
+ return BUDGET_EXIT_CODES.OVER_BUDGET;
343
+ }
344
+ io.stdout(`${options.json ? JSON.stringify({ rows }) : renderComparison(rows)}\n`);
345
+ return BUDGET_EXIT_CODES.SUCCESS;
346
+ }
347
+ const budget = await loadBudget(options.budgetPath);
348
+ if (options.command === 'check') {
349
+ const report = checkRun(await loadRun(options.runPath), budget, {
350
+ strict: options.strict,
351
+ requireMeasurement: options.requireMeasurement,
352
+ ...(options.phase === undefined ? {} : { phase: options.phase }),
353
+ });
354
+ return emitReport(report, options.json, io);
355
+ }
356
+ if (budget.bundle === undefined) {
357
+ throw new UsageError(`${options.budgetPath} has no "bundle" section to measure`);
358
+ }
359
+ const root = path.resolve(options.root ?? path.dirname(options.budgetPath));
360
+ const run = createRun({ rounds: [], bundle: await measureBundle(root, budget.bundle.entries) });
361
+ if (options.outPath !== undefined)
362
+ await writeRun(options.outPath, run);
363
+ const bundleOnly = {
364
+ ...(budget.thresholdPercent === undefined ? {} : { thresholdPercent: budget.thresholdPercent }),
365
+ bundle: budget.bundle,
366
+ };
367
+ // Every entry was asked for by name here, so one that matched nothing fails the command.
368
+ const report = checkRun(run, bundleOnly, { strict: options.strict, requireMeasurement: true });
369
+ return emitReport(report, options.json, io);
370
+ }
371
+ /** Parse and execute; never throws. */
372
+ export async function runBudgetCliCommand(argv, io = PROCESS_IO, deps = { measureApp }) {
373
+ try {
374
+ return await runBudgetCli(parseBudgetCliArgs(argv), io, deps);
375
+ }
376
+ catch (error) {
377
+ const known = error instanceof UsageError ||
378
+ error instanceof BudgetFileError ||
379
+ error instanceof RunFileError;
380
+ const message = error instanceof Error ? error.message : String(error);
381
+ io.stderr(`${known ? 'usage' : 'fatal'}: ${message}\n`);
382
+ if (error instanceof UsageError)
383
+ io.stderr(`\n${formatBudgetCliHelp()}\n`);
384
+ // Exit 1 means over budget; a crash must never read as a measured verdict.
385
+ return BUDGET_EXIT_CODES.USAGE;
386
+ }
387
+ }
388
+ function isMainEntryPoint(moduleUrl, entryPath) {
389
+ if (entryPath === undefined)
390
+ return false;
391
+ try {
392
+ return moduleUrl === pathToFileURL(realpathSync(entryPath)).href;
393
+ }
394
+ catch {
395
+ return false;
396
+ }
397
+ }
398
+ if (isMainEntryPoint(import.meta.url, process.argv[1])) {
399
+ process.exitCode = await runBudgetCliCommand(process.argv.slice(2));
400
+ }
401
+ //# sourceMappingURL=cli.js.map