@progen-labs/spectra 0.1.2 → 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/README.md CHANGED
@@ -8,10 +8,14 @@ command and it starts your dev server, auto-discovers the actionable elements on
8
8
  of **golden-path journeys**, records each one, and reports a ranked list of culprits — each with the
9
9
  exact function, file and line, the evidence, and a suggested fix.
10
10
 
11
- Deep component-level attribution for **React, Next.js and Vue** (component names, source files, render
12
- counts and per-component render time). For **Svelte, Angular and any other build**, a source-mapped
13
- generic profile still pinpoints the slow files and functions. A [Progen Labs](https://github.com/progen-labs)
14
- project — open source, non-profit, no telemetry, no account required.
11
+ Deep component-level attribution for **React, Next.js, Vue and Angular** (component names, render counts
12
+ and per-component render/change-detection time; source files too, where the framework exposes them). For
13
+ **Svelte and any other build**, a source-mapped generic profile still pinpoints the slow files and
14
+ functions. A [Progen Labs](https://github.com/progen-labs) project — open source, non-profit, no
15
+ telemetry, no account required.
16
+
17
+ **Documentation:** the full guide, with annotated screenshots of every screen, is at
18
+ [progen-labs.github.io/website](https://progen-labs.github.io/website/#spectra/docs/introduction).
15
19
 
16
20
  ## Install
17
21
 
@@ -51,15 +55,23 @@ Then open the visual dashboard — ranked culprits, React re-renders, long tasks
51
55
  **PDF export** and **commit-to-commit compare**:
52
56
 
53
57
  ```bash
54
- npx spectra dashboard
58
+ npx spectra dashboard --open
55
59
  ```
56
60
 
61
+ ![The Spectra dashboard: the top culprit of a recording, burn() in src/heavy.js, with its time and a suggested fix](docs/dashboard.webp)
62
+
63
+ A [tour of the dashboard](https://progen-labs.github.io/website/#spectra/docs/dashboard), with every part
64
+ labelled, is in the docs.
65
+
57
66
  ## Commands
58
67
 
59
68
  ```
60
- spectra performance [options] Golden-path evaluation (default)
61
- spectra dashboard [--port N] Open the dashboard UI
62
- spectra --help Full option list
69
+ spectra performance [options] Golden-path evaluation (default)
70
+ spectra performance --quick One page-load profile (about 15 s)
71
+ spectra compare <before> <after> Did it get faster? Noise-aware before/after
72
+ spectra reports List saved reports
73
+ spectra dashboard [--port N] [--open] Open the dashboard UI
74
+ spectra --help Full option list
63
75
  spectra --version
64
76
  ```
65
77
 
@@ -73,7 +85,70 @@ spectra --version
73
85
  | `--boot-timeout <s>` | How long to wait for the dev server (default 60) |
74
86
  | `--headed` | Show the browser window |
75
87
  | `--sign-in <url>` | Sign-in URL template, e.g. `http://host/login/?redirect={url}` |
88
+ | `--quick` | One page-load profile instead of the golden-path suite: the fast loop |
89
+ | `--runs <n>` | Record n times and report the median (with `--quick` or `--mode`) |
76
90
  | `--mode live\|load` | Classic single-page profile instead of the golden-path suite |
91
+ | `--json` | Print a machine-readable result on stdout (tables go to stderr) |
92
+ | `--out <file>` | Also write that result to a file (an input for `compare`) |
93
+ | `--config <file>` | Budgets file (default: `spectra.config.json` in your project) |
94
+
95
+ ## Measure, change, verify
96
+
97
+ Performance numbers are noisy, so Spectra never trusts a single recording when you ask it whether a change
98
+ helped. Record a few runs before and after, then compare:
99
+
100
+ ```bash
101
+ npx spectra performance --quick --runs 5 --out before.json
102
+ # …make your change…
103
+ npx spectra performance --quick --runs 5 --out after.json
104
+ npx spectra compare before.json after.json
105
+ ```
106
+
107
+ ```
108
+ Spectra compare — IMPROVED (confidence: medium)
109
+ ✓ JavaScript CPU time: 1.52 s → 92.1 ms (−94%)
110
+ ✓ Row (src/Row.jsx) — render time: 1.44 s → 7.4 ms (−99%)
111
+
112
+ Findings: 3 resolved · 2 new · 10 still present
113
+ resolved [critical] Row is slow to render: 1444ms total, 0.2ms per render (7200×)
114
+ ```
115
+
116
+ Each side is reduced to its median, and a difference only counts when it is larger than the run-to-run
117
+ spread. A side can be a `--out` result file, a report id from `spectra reports`, a report file, or several
118
+ ids joined by commas.
119
+
120
+ ## Budgets and CI
121
+
122
+ Put limits in `spectra.config.json` at your project root:
123
+
124
+ ```json
125
+ {
126
+ "$schema": "./node_modules/@progen-labs/spectra/schema/config.schema.json",
127
+ "budgets": { "tbtMs": 200, "lcpMs": 2500, "cls": 0.1, "criticalFindings": 0 }
128
+ }
129
+ ```
130
+
131
+ Every `spectra performance` run checks them: the median for `--quick`/`--mode`, every journey for the
132
+ golden-path suite. Metrics a recording did not measure are skipped, never failed. Available budgets:
133
+ `tbtMs`, `lcpMs`, `fcpMs`, `cls`, `inpMs`, `ttfbMs`, `longTaskCount`, `maxLongTaskMs`, `scriptingMs`,
134
+ `forcedLayoutCount`, `domNodes`, `heapUsedMb`, `transferKb`, `criticalFindings`, `findingCount`.
135
+
136
+ Exit codes make it a CI gate:
137
+
138
+ | Code | Meaning |
139
+ | --- | --- |
140
+ | `0` | Done, and within budget |
141
+ | `1` | Could not run (no Chrome, server down, bad config) |
142
+ | `2` | Ran, but a budget was exceeded, or `compare --fail-on-regression` found a regression |
143
+
144
+ ## Machine-readable output
145
+
146
+ `--json` prints a small, versioned result on stdout and sends every human-readable line to stderr, so it can
147
+ be piped into `jq`, an editor or an AI agent. Each result has `schemaVersion` and a `kind` (`profile`,
148
+ `evaluation`, `compare` or `reports`) and points at the full reports on disk rather than embedding them.
149
+ Findings carry the file, line, cost in ms and a suggested fix. The format is described by a JSON Schema
150
+ shipped with the package (`schema/result.schema.json`); additive changes keep `schemaVersion`, breaking
151
+ changes bump it. Saved reports carry their own `schemaVersion` too.
77
152
 
78
153
  ## Safe by default
79
154
 
@@ -96,6 +171,18 @@ that cost the most main-thread time. See the
96
171
  [deep-dive docs](https://github.com/progen-labs/spectra/blob/main/DOCS.md) for how capture, correlation
97
172
  and report generation work.
98
173
 
174
+ ## Documentation
175
+
176
+ - **[The guide](https://progen-labs.github.io/website/#spectra/docs/introduction)**: every feature, with
177
+ annotated screenshots. Start with
178
+ [Quick start](https://progen-labs.github.io/website/#spectra/docs/quick-start),
179
+ [Compare before and after](https://progen-labs.github.io/website/#spectra/docs/compare),
180
+ [Budgets and CI](https://progen-labs.github.io/website/#spectra/docs/budgets),
181
+ [JSON output](https://progen-labs.github.io/website/#spectra/docs/json-output), the
182
+ [Findings reference](https://progen-labs.github.io/website/#spectra/docs/findings) and
183
+ [Troubleshooting](https://progen-labs.github.io/website/#spectra/docs/troubleshooting).
184
+ - **[DOCS.md](DOCS.md)**: the in-repo manual, the technical deep dive, and how to contribute.
185
+
99
186
  ## License
100
187
 
101
188
  MIT © Progen Labs
package/bin/spectra.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * Spectra CLI — a Progen Labs project.
4
4
  *
5
5
  * spectra performance [--url <page>] [profiler options] profile a frontend
6
+ * spectra compare <before> <after> before vs after, noise-aware
7
+ * spectra reports list saved reports
6
8
  * spectra dashboard open the dashboard UI
7
9
  * spectra --help show this help
8
10
  * spectra --version print the version
@@ -12,7 +14,7 @@
12
14
  * root is used for source lookup, unless the matching PERF_* env var overrides it.
13
15
  */
14
16
  import { spawn } from 'node:child_process';
15
- import { readFileSync } from 'node:fs';
17
+ import { existsSync, readFileSync } from 'node:fs';
16
18
  import path from 'node:path';
17
19
  import { fileURLToPath } from 'node:url';
18
20
  import { bootDevServer } from '../server/explore/boot.js';
@@ -33,6 +35,10 @@ Spectra ${pkg.version} — frontend performance profiler (Progen Labs)
33
35
 
34
36
  Usage:
35
37
  spectra performance [options] Profile a frontend build and report the culprits
38
+ spectra compare <before> <after> [options]
39
+ Compare two profiles; a change counts only when
40
+ it is bigger than the noise between runs
41
+ spectra reports [--json] List saved reports (ids for compare / dashboard)
36
42
  spectra dashboard Open the Spectra dashboard UI
37
43
  spectra --help, -h Show this help
38
44
  spectra --version, -v Print the version
@@ -52,18 +58,40 @@ performance options:
52
58
  --headed Show the browser window
53
59
  --sign-in <url> Sign-in URL template, e.g. "http://host/login/?redirect={url}"
54
60
 
55
- classic single-page profile (opt in with --mode or --scenario):
61
+ quick single-page profile (opt in with --quick, --mode or --scenario):
62
+ --quick One page-load profile (about 15 s): the fast loop
63
+ --runs <n> Record n times and report the median (with --quick/--mode)
56
64
  --mode live|load Live session or page-load recording
57
65
  --duration <seconds> Live recording length (default 8)
58
66
  --throttle <n> CPU throttle multiplier (default 1)
59
67
  --scenario <file.mjs> Script a live session: export default async page => {…}
60
68
  --save-trace Keep the raw Chrome trace alongside the report
61
69
 
70
+ output and checks (any performance run):
71
+ --json Print a machine-readable result on stdout (progress and
72
+ tables go to stderr)
73
+ --out <file> Also write that result to a file (input for compare)
74
+ --config <file> Budgets file (default: spectra.config.json in the project)
75
+
76
+ compare options:
77
+ <before> <after> Each: a report id, a report file, a --out result file,
78
+ or several ids joined by commas
79
+ --json, --out <file> As above
80
+ --fail-on-regression Exit with code 2 when a key metric got worse
81
+
82
+ Budgets (spectra.config.json in your project root):
83
+ { "budgets": { "tbtMs": 200, "lcpMs": 2500, "cls": 0.1, "criticalFindings": 0 } }
84
+
85
+ Exit codes: 0 done · 1 could not run · 2 over budget / regression
86
+
62
87
  Examples:
63
88
  spectra performance # self-boot + golden-path suite
64
89
  spectra performance --paths 8 # cap the suite at 8 journeys
65
90
  spectra performance --url http://localhost:3000/ # evaluate a running server
66
- spectra performance --mode load # classic single page-load profile
91
+ spectra performance --quick --runs 5 --out before.json
92
+ spectra performance --quick --runs 5 --out after.json
93
+ spectra compare before.json after.json # did the change help?
94
+ spectra performance --json | jq .summary # pipe it anywhere
67
95
  `;
68
96
 
69
97
  function runNodeAsync(scriptRelPath, args, extraEnv = {}) {
@@ -91,6 +119,19 @@ function takeOption(args, name) {
91
119
  return { value, rest: copy };
92
120
  }
93
121
 
122
+ /**
123
+ * The scripts run with the package as their working directory, so path
124
+ * options the user typed relative to their own directory are made absolute
125
+ * here, before they cross over.
126
+ */
127
+ function absolutize(args, names) {
128
+ return args.map((a, i) =>
129
+ i > 0 && names.includes(args[i - 1].replace(/^--/, '')) && args[i - 1].startsWith('--') && !a.startsWith('--')
130
+ ? path.resolve(process.cwd(), a)
131
+ : a,
132
+ );
133
+ }
134
+
94
135
  /**
95
136
  * Default the profiled project and the reports location to the user's current
96
137
  * directory, so an installed Spectra profiles the host project and writes
@@ -114,20 +155,38 @@ async function runProfile() {
114
155
  return 0;
115
156
  }
116
157
  const env = projectEnv();
158
+ let args = absolutize(rest, ['scenario', 'out', 'config']);
159
+
160
+ // --quick is shorthand for a single page-load profile.
161
+ if (args.includes('--quick')) {
162
+ const mode = takeOption(args, 'mode').value;
163
+ if ((mode && mode !== 'load') || args.includes('--scenario')) {
164
+ console.error('spectra: --quick is a page-load profile; it cannot be combined with --mode live or --scenario.');
165
+ return 1;
166
+ }
167
+ args = args.filter(a => a !== '--quick');
168
+ if (!mode) args.push('--mode', 'load');
169
+ }
117
170
 
118
171
  // Golden-path evaluation is the default. --mode/--scenario opt into the
119
172
  // classic single-page profile instead.
120
- const classic = rest.includes('--mode') || rest.includes('--scenario');
173
+ const classic = args.includes('--mode') || args.includes('--scenario');
121
174
  const script = classic ? 'server/cli.js' : 'server/explore/evaluate-cli.js';
175
+ if (!classic && args.includes('--runs')) {
176
+ console.error(
177
+ 'spectra: --runs works with single-page profiles (--quick, --mode or --scenario); the golden-path suite records each journey once.',
178
+ );
179
+ return 1;
180
+ }
122
181
 
123
182
  // An explicit --url means the server is already running: run against it.
124
- if (rest.includes('--url')) {
125
- return runNodeAsync(script, rest, env);
183
+ if (args.includes('--url')) {
184
+ return runNodeAsync(script, args, env);
126
185
  }
127
186
 
128
187
  // No --url: self-boot the target project's own dev server, run, stop.
129
188
  const cwd = process.cwd();
130
- const { value: command, rest: afterCmd } = takeOption(rest, 'command');
189
+ const { value: command, rest: afterCmd } = takeOption(args, 'command');
131
190
  const { value: bootTimeout, rest: profileArgs } = takeOption(afterCmd, 'boot-timeout');
132
191
  const fw = detectFramework(cwd);
133
192
  const label =
@@ -164,6 +223,16 @@ async function runProfile() {
164
223
  return code;
165
224
  }
166
225
 
226
+ function runCompare() {
227
+ // Sides may be files relative to the user's directory (ids pass through).
228
+ const fixToken = t =>
229
+ t.endsWith('.json') || existsSync(path.resolve(process.cwd(), t)) ? path.resolve(process.cwd(), t) : t;
230
+ const args = absolutize(rest, ['out']).map((a, i, all) =>
231
+ a.startsWith('--') || all[i - 1] === '--out' ? a : a.split(',').map(fixToken).join(','),
232
+ );
233
+ return runNodeAsync('server/compare-cli.js', args, projectEnv());
234
+ }
235
+
167
236
  function runDashboard() {
168
237
  // Standalone server: serves the prebuilt dashboard and the backend API, so an
169
238
  // installed Spectra needs no dev toolchain. projectEnv() points it at this
@@ -194,6 +263,17 @@ switch (command) {
194
263
  process.exit(1);
195
264
  });
196
265
  break;
266
+ case 'compare':
267
+ case 'reports': {
268
+ const run = command === 'compare' ? runCompare() : runNodeAsync('server/reports-cli.js', rest, projectEnv());
269
+ run
270
+ .then(code => process.exit(code ?? 0))
271
+ .catch(err => {
272
+ console.error(`spectra: ${err.message}`);
273
+ process.exit(1);
274
+ });
275
+ break;
276
+ }
197
277
  case 'dashboard':
198
278
  case 'ui':
199
279
  runDashboard();