@variance-authority/storybook-collector 0.1.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,11 +1,69 @@
1
1
  # @variance-authority/storybook-collector
2
2
 
3
+ ## 0.3.0
4
+
5
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
6
+
7
+ ## 0.2.0
8
+
9
+ ### Minor Changes
10
+
11
+ - 8efba76: A run that changed ten files stops rewriting the whole selection index
12
+
13
+ Every run after the first reads the selection index, lays its own recording over
14
+ it, and writes it back. At a repository's scale almost all of that was spent
15
+ making objects nobody reads: a run that re-records ten modules of twenty thousand
16
+ decoded six hundred thousand regions into a model, merged ten of them, and
17
+ encoded the model back — the other 99.95% of the index materialized and
18
+ re-serialized to arrive at the bytes it was read from.
19
+
20
+ `layerTestCoverage` does the merge and the encode as one pass over the columns
21
+ the previous snapshot is already stored in. A module the run did not touch is
22
+ never made an object: its rows are copied column to column as integers, its
23
+ strings blob to blob as bytes, and the only thing that happens to either is the
24
+ renumbering the new dictionary implies. Objects are made for what the merge has
25
+ to reason about — the tests, the modules this run re-recorded, and the carried
26
+ modules whose text moved on disk. Over twenty thousand modules that is 1855 ms to
27
+ 616, and it is byte for byte the same file, which a gate asserts across every
28
+ case the merge distinguishes.
29
+
30
+ The columns are now zstd rather than brotli, at two levels, because the runs are
31
+ two kinds of data. A varint run is a dense stream of small integers and answers
32
+ to a long search; a run of the string blob is file paths and hex digests, which
33
+ zstd finds most of at level 1 and nothing more of above it. Against the brotli
34
+ quality 4 it replaces, over the same snapshot: 319 ms to compress became 120, and
35
+ the file got 86 KB smaller.
36
+
37
+ `zlib.zstdCompressSync` arrived in Node 22.15, so that is the floor these
38
+ packages declare. The snapshot's layout version moved with the codec, which means
39
+ an index written by an earlier build is refused at its header and rebuilt — one
40
+ full run, and nothing a reader has to think about.
41
+
42
+ ### Patch Changes
43
+
44
+ - e546e21: Where the JSX settings live under Vite 8
45
+
46
+ Vite 8 transforms with oxc, so `esbuild: { jsx, jsxDev, jsxImportSource }` becomes
47
+ `oxc: { jsx: { runtime, development, importSource } }`, and `esbuild.keepNames`
48
+ becomes `build.rolldownOptions.output.keepNames`. A config keeps whichever keys it
49
+ is given and reads only the ones its own major knows, so the wrong block is not an
50
+ error, not a warning and not a log line: the plugin installs, the bundle runs,
51
+ every subject renders, and every report names the line a component is declared on
52
+ instead of the line that wrote the element. It fails in the direction that looks
53
+ like it worked.
54
+
55
+ The `jsx-source` README carries a table of where the two settings live per
56
+ transform, both spellings on every copyable snippet, and the reminder to read
57
+ `provenanceOf`'s result rather than the config — the config cannot tell you.
58
+ `storybook-collector` gets the same for `keepNames`, on the symptom it produces: a
59
+ confident report naming a component that appears nowhere in your source.
60
+
3
61
  ## 0.1.1
4
62
 
5
63
  ### Patch Changes
6
64
 
7
65
  - db08866: Keep the run when the execution journal cannot be recorded.
8
-
66
+
9
67
  `close()` already held the position that a journal is not the artefact under
10
68
  review: a recorder that declines to record is written to stderr and the run
11
69
  continues, because failing the run over it would cost every subject in it the
@@ -14,29 +72,7 @@
14
72
  practice — went straight up through `close()` and took the run down with it.
15
73
  Both outcomes now end the same way, in a sentence on stderr and a run that keeps
16
74
  its images.
17
- - Updated dependencies [fe578c8]
18
- - @variance-authority/sense@0.1.1
19
- - @variance-authority/core@0.1.1
20
- - @variance-authority/dom@0.1.1
21
- - @variance-authority/playwright@0.1.1
22
- - @variance-authority/react@0.1.1
23
- - @variance-authority/storybook@0.1.1
24
75
 
25
76
  ## 0.1.0
26
77
 
27
- ### Patch Changes
28
-
29
- - Updated dependencies [9587133]
30
- - Updated dependencies [f800342]
31
- - Updated dependencies [26ae9ed]
32
- - Updated dependencies [f09528d]
33
- - Updated dependencies [cfb333d]
34
- - Updated dependencies [e8fee66]
35
- - Updated dependencies [48d32eb]
36
- - Updated dependencies [5c34e6d]
37
- - @variance-authority/sense@0.1.0
38
- - @variance-authority/dom@0.1.0
39
- - @variance-authority/react@0.1.0
40
- - @variance-authority/core@0.1.0
41
- - @variance-authority/playwright@0.1.0
42
- - @variance-authority/storybook@0.1.0
78
+ First release.
package/README.md CHANGED
@@ -1,85 +1,90 @@
1
- <p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
1
+ <p align="center"><img src="https://variance-authority.dev/mark.svg" alt="Variance Authority mark" width="72"></p>
2
2
 
3
3
  # @variance-authority/storybook-collector
4
4
 
5
5
  > Turn a built or already-served Storybook into Variance Authority subjects.
6
6
 
7
- Turn a built or already-served Storybook into subjects that `variance run` can
8
- observe — one subject per story, the unit a comparison runs against. Use this
9
- package when Storybook already owns component mounting and you want
10
- source-attributed visual, semantic, accessibility, and localization findings
11
- without writing a browser harness.
7
+ Part of [Variance Authority](https://variance-authority.dev).
12
8
 
13
- This is the Storybook adapter, not the `variance` binary. Pair it with
14
- `@variance-authority/cli`.
9
+ ## What this is for
10
+
11
+ You already have a Storybook. Each story is a UI state somebody deliberately
12
+ named, mounted and made ready — which is the expensive half of visual regression
13
+ testing, and you have paid for it.
14
+
15
+ This package hands that catalogue to `npx variance run` without a browser harness of
16
+ your own. It reads the `index.json` your Storybook build writes, opens the
17
+ preview once, switches stories over Storybook's own channel, and produces one
18
+ **subject** per story — one named UI state you asked for and can ask for again,
19
+ and the unit a comparison runs against. A story's subject id is its Storybook
20
+ story id with a `story:` prefix: `story:checkout--empty`.
21
+
22
+ What you get back is not only a pixel diff. Each changed region resolves to the
23
+ React component that rendered it and to the `file:line` where that component is
24
+ declared, so a red run names `Button` in `src/ds.tsx:41` rather than a rectangle.
25
+
26
+ This is the Storybook adapter, not the `variance` binary. The binary lives in
27
+ `@variance-authority/cli`, and both are installed below.
28
+
29
+ ## Requirements
30
+
31
+ | Requirement | Detail |
32
+ | --- | --- |
33
+ | Node | 22.15 or newer. |
34
+ | Module format | ESM only. Every `@variance-authority/*` package sets `"type": "module"`; `require()` will not load them. |
35
+ | Browser | Chromium, installed through Playwright. The binaries do not arrive with an `npm install`. |
36
+ | Storybook | Exercised against Storybook 10 with `@storybook/react-vite`. Index versions `3`, `4` and `5` are read, and the story root is looked for at `#storybook-root` (Storybook 7 and later) and then `#root` (before it). Storybook 8.3 and later emit `storyFinished`, which this package waits for; it falls back to `storyRendered` when the event is absent. |
37
+ | Builder | Collection reads `index.json` and drives the preview channel, so it does not know or care whether Vite or Webpack 5 built the Storybook. Two optional precision features are Vite plugins with no Webpack equivalent, and are named as such where they appear below: `@variance-authority/jsx-source` and the `tests` option. |
38
+ | React | Required for component attribution, the `wiring` band and the Suspense wait. The reading is of the expando `react-dom` writes on host nodes — `__reactFiber$` on React 17 and later, `__reactInternalInstance$` on React 16 — so there is no React version to match and none is imported. The suite runs against React 19. |
39
+
40
+ A Storybook whose renderer is not React still collects: documents, pixels,
41
+ comparison and verdicts all work, and the fiber walk reports nothing rather than
42
+ failing. Set `wiring: false` so it does not visit every node to find that out.
43
+
44
+ ## Install
15
45
 
16
46
  ```bash
17
47
  npm install --save-dev @variance-authority/cli @variance-authority/storybook-collector
18
48
  npx playwright install chromium
19
49
  ```
20
50
 
21
- The second command is there because Playwright's browser binaries do not arrive
22
- with an `npm install`. The built `index.json` is the input, whether you build the
23
- Storybook or already serve it; `.storybook` configuration is not read.
51
+ ## Run the first loop
52
+
53
+ Four steps. Nothing here reads your `.storybook` directory — the built
54
+ `index.json` is the input, whether you build the Storybook or already serve one.
24
55
 
25
- ## Integrate a Storybook
56
+ ### 1. Build the Storybook
26
57
 
27
- ### 1. Build the artifact you want to observe
58
+ ```bash
59
+ npx storybook build -o storybook-static
60
+ ```
28
61
 
29
- The collector — the module you create in step 2 below — reads the artifact
30
- Storybook produced, so run your ordinary Storybook build first. A development
31
- server also works when you pass `baseUrl` below.
62
+ A development server works too; pass its origin as `baseUrl` in step 2 and skip
63
+ the build.
32
64
 
33
- ### 2. Add a collector module
65
+ ### 2. Write the collector module
34
66
 
35
- Create a module in your project and default-export the collector factory. The
36
- only project-specific facts in the common path are where components live and
37
- which stories need an explicit readiness marker.
67
+ The collector is the module that tells the run how to find and read your
68
+ stories. Create it anywhere in your project and default-export the factory.
38
69
 
39
70
  ```js
40
71
  // variance/storybook.mjs
41
72
  import { storybookCollector } from '@variance-authority/storybook-collector';
42
73
 
43
74
  export default storybookCollector({
44
- ready: { 'checkout--deferred': '[data-testid="checkout-ready"]' },
45
75
  source: { dirs: ['src'] },
46
76
  });
47
77
  ```
48
78
 
49
- Omit `ready` for stories that are complete when Storybook emits
50
- `storyRendered`. Add an entry only when a story continues mounting or fetching
51
- after that signal. If the selector never appears, collection fails with the
52
- story id and selector instead of silently capturing a spinner.
53
-
54
- `source` enables component-to-`file:line` attribution. Without it the report can
55
- still name components, but it cannot point to their declarations.
56
-
57
- Against a built Storybook those names arrive minified — a run reports the cause
58
- as `Ce`, not `Button`, because the bundler renamed it and nothing in the browser
59
- remembers otherwise. `file:line` is the identifier that survives minification,
60
- which is what makes the plugin below load-bearing there rather than an
61
- enhancement.
62
-
63
- It names where a component is *declared* — one line however many times that
64
- component is rendered.
65
-
66
- For the line the changed element is actually written on: against a
67
- **development** Storybook, source locations work automatically — nothing to
68
- configure. Against a **built, minified** Storybook it needs the
69
- `@variance-authority/jsx-source` plugin in your `viteFinal` with
70
- `esbuild.jsxDev` on; a report prefers the exact location wherever it comes
71
- from, and this collector makes it repository-relative. The plugin does not
72
- take `jsxImportSource`, so a Storybook already compiling against Emotion or
73
- theme-ui keeps doing exactly that.
74
-
75
- ```bash
76
- npm install --save-dev @variance-authority/jsx-source
77
- ```
78
-
79
- ### 3. Point the CLI at the Storybook index and collector
79
+ `source.dirs` is a list of directory paths — not globs — resolved against the
80
+ directory you run `variance` from. The scan reads `.tsx`, `.jsx`, `.ts` and
81
+ `.js`, skips `node_modules`, `dist`, `storybook-static` and any path containing
82
+ `.test.`, `.spec.` or `.stories.`, and indexes where each component is
83
+ *declared*. Without it the report still names components and resolves none of
84
+ them to a file. A `dirs` list that matches no source file at all is refused,
85
+ naming the directories it walked.
80
86
 
81
- In `variance.config.json`, the index is the built file and `collector` is the
82
- module you created:
87
+ ### 3. Write the run config
83
88
 
84
89
  ```json
85
90
  {
@@ -98,33 +103,101 @@ module you created:
98
103
  }
99
104
  ```
100
105
 
101
- ### 4. Check the machine, then run
106
+ Save it as `variance.config.json`. Paths resolve against this file's directory
107
+ rather than the working directory, and unknown keys are refused by name.
108
+ `profile` is what the run is capable of observing — `chromium` resolves computed
109
+ style, layout and pixels, and a Storybook loop paints, so it is `chromium`.
110
+
111
+ Track the approved images. `.variance/` also collects per-run junk, so exclude
112
+ the contents rather than the directory:
113
+
114
+ ```gitignore
115
+ .variance/*
116
+ !.variance/baselines/
117
+ ```
118
+
119
+ ### 4. Check the machine, run, accept, run again
102
120
 
103
121
  ```bash
104
122
  npx variance doctor --config variance.config.json
105
123
  npx variance run --config variance.config.json
106
124
  ```
107
125
 
108
- Each run writes `.variance/report.json`, the artifact everything else reads.
109
- Run `npx variance report --config variance.config.json --format html >
110
- .variance/report.html` and open the result in a browser to see before/diff/after
111
- images per changed story, grouped by cause rather than by story. The page
112
- references its images relatively, so it belongs beside the `report` path this
113
- config declares; written anywhere else it shows broken images.
126
+ `doctor` opens a browser, measures the fonts your config asserts, and lists
127
+ which **identities** the baseline root stores — an identity being the digest of
128
+ renderer, engine, platform, device scale factor and fonts that an approved image
129
+ is stored under. It exits `2` when no browser opens or when the root has no
130
+ images this machine could compare against.
131
+
132
+ The first durable run exits `1` and reports every story `new`. An image nobody
133
+ approved is not a pass. Promote the candidates the run already produced:
134
+
135
+ ```bash
136
+ npx variance accept --config variance.config.json --all
137
+ npx variance run --config variance.config.json
138
+ ```
139
+
140
+ The second run exits `0` and reports every story `unchanged`.
141
+
142
+ `accept` never renders again — it promotes an image this run produced — and its
143
+ full form is:
144
+
145
+ ```
146
+ npx variance accept [--config <path>] <subject>... | --all | --shape <fingerprint>[,...] [--message-file <path> [--message <text>]]
147
+ ```
148
+
149
+ Keep `--all` for the first run and for deliberate re-baselines; it cannot tell a
150
+ story nobody looked at from one that changed. After setup, name subject ids:
151
+
152
+ ```bash
153
+ npx variance accept --config variance.config.json story:checkout--empty story:checkout--one-item
154
+ ```
155
+
156
+ `--shape` takes a **fingerprint** — an identifier the report gives every changed
157
+ region, naming *what* changed rather than which screenshots it landed in.
158
+ Accepting one covers every subject where that shape is the whole change, and
159
+ refuses by name any subject where something else changed too.
160
+
161
+ ### What you get
162
+
163
+ `npx variance run` prints its summary to the terminal and writes the same facts to
164
+ `.variance/report.json`, which every other command reads. Abridged, a run after
165
+ one edit to `Button`:
166
+
167
+ ```
168
+ 12 of 14 subject(s) observed, durable run at 2026-01-14T09:22:41.108Z
169
+ rendered by playwright-chromium (chromium@131, darwin/arm64, 1x)
170
+ 7 unchanged, 5 changed, 2 not observed
171
+
172
+ [changed] story:checkout--empty — Button: 1356 pixel(s) differ across 2 region(s) in Button
173
+ [changed] story:checkout--one-item — Button: 1356 pixel(s) differ across 2 region(s) in Button
174
+
175
+ not observed: 2 subject(s) — 0 the run could not see, 2 excluded by configuration, 0 not reached by this change
176
+ [excluded] story:checkout--docs-only: excluded by tag `no-variance`
177
+ ```
178
+
179
+ The second list is the point of the first number. A run that planned fourteen
180
+ subjects, observed twelve and found five changes is a different fact from a run
181
+ of twelve, so every subject that was never observed is named with the reason.
182
+
183
+ For the before, diff and after images per changed story, grouped by cause rather
184
+ than by story:
114
185
 
115
- The first successful run exits `1` and reports each story as `new`; a
116
- baseline — the stored snapshot a subject is compared against — nobody
117
- approved is not a pass. Review the candidates, accept the intended subjects,
118
- and run again. The next unchanged run exits `0` without re-rendering
119
- (*painting*) subjects whose stored document digest already proves they did
120
- not move.
186
+ ```bash
187
+ npx variance report --config variance.config.json --format html > .variance/report.html
188
+ ```
121
189
 
122
- The executable `storybook-case` demonstrates the
123
- complete cycle against a Storybook-built artifact: new → accept → unchanged → a
124
- `Button` edit changing exactly the five stories that render it.
190
+ The page references its images relatively, so write it beside the `report` path
191
+ the config declares. Written anywhere else it shows broken images.
192
+
193
+ An unchanged rerun does not repaint a story whose stored document digest already
194
+ proves it did not change.
125
195
 
126
196
  ## Options
127
197
 
198
+ Everything below is passed to `storybookCollector(...)` in the module from step
199
+ 2.
200
+
128
201
  | Option | Use it when | Default and boundary |
129
202
  | --- | --- | --- |
130
203
  | `ready` | A particular story finishes after Storybook's render signal. | No additional wait. Keys are Storybook story ids. |
@@ -132,52 +205,68 @@ complete cycle against a Storybook-built artifact: new → accept → unchanged
132
205
  | `loading` | A story's *fallback* is the state you intend to review. | Omitted. Id globs, matched against the story id and the subject id. |
133
206
  | `suspenseTimeoutMs` | A story legitimately needs longer than five seconds to arrive. | `5000`. `0` skips the wait and keeps the reading. |
134
207
  | `source` | Reports should resolve component names to `file:line`. | Omitted; component names remain available. An empty or mistyped scan is refused. |
135
- | `baseUrl` | Storybook is already running. | Omitted; the directory containing `subjects.index` is served on loopback for the run. |
136
- | `headless` | You need to watch collection while debugging. | `true`; set `false` locally. |
208
+ | `baseUrl` | Storybook is already running, e.g. `http://localhost:6006`. | Omitted; the directory containing `subjects.index` is served on loopback for the run. |
209
+ | `headless` | You need to watch collection while debugging. | `true`. |
137
210
  | `network` | Asset bytes at stable URLs must participate in render identity. | `true`; set `false` only when asset URLs are already content-addressed. |
211
+ | `hashAssets` | Your asset URLs already contain their own content hash. | `true`. Read only while `network` is on, so GIF freezing and blanking survive it — this, not `network: false`, is the setting for a content-addressed build. |
212
+ | `wiring` | Your preview's renderer is not React. | `true`. Reads props, context, hook cells and keys into a band of its own; turning it off changes no stored digest. |
213
+ | `holdings` | Application values behind the nodes are evidence you want read. | `false`. Changes `structureHash` — an inert wrapper survives the collapse — so both sides of a comparison must be read the same way. |
138
214
  | `roots` | Your preview mounts somewhere other than the standard roots. | `['#storybook-root', '#root']`, tightest match first. |
139
- | `tests` | The next run should be able to skip stories whose code nothing touched. | `false`. Requires a preview built with `testSelectionProbes()` from `@variance-authority/sense/journal`; without a collector in the page the run says so on stderr and records nothing. |
215
+ | `tests` | The next run should be able to skip stories whose code nothing touched. | `false`. Requires a Vite-built preview that includes `testSelectionProbes()`; see below. |
216
+
217
+ A **band** is the severity category a change is filed under. Loudest first:
218
+ `a11y`, `geometry`, `token`, `content`, `texture`. A role or accessible name that
219
+ changed outranks a box that moved, which outranks a style value, which outranks
220
+ text, which outranks sub-pixel noise.
140
221
 
141
222
  The package exports `StorybookCollectorOptions` plus the collector contract
142
- types (`Collector`, `CollectorContext`, `Plan`, `PlannedSubject`, and
223
+ types (`Collector`, `CollectorContext`, `Plan`, `PlannedSubject` and
143
224
  `Collected`) for callers that need to wrap the factory without re-declaring its
144
- boundary. `SourceScan`, `AcquireRequest`, and `Acquired` expose the corresponding
225
+ boundary. `SourceScan`, `AcquireRequest` and `Acquired` expose the corresponding
145
226
  source and page-agent shapes.
146
227
 
147
- `tests` accepts `true` or `StoryExecutionOptions`: `root` is the repository root
148
- the recorded paths are relative to, `label` must match the one the preview's
149
- `testSelectionProbes()` was given, and `modulesFile` and `coverageFile` override
150
- the repository-keyed cache paths for the block inventory and the coverage index. A
151
- story is its own owner in the recorded index — Storybook is an execution surface
152
- this tool drives one subject at a time — and a story that did not render still
153
- contributes its crossings while never justifying a later skip.
154
-
155
- The collector reads a built Storybook index and switches stories over
156
- Storybook's own preview channel, so a suite pays for one navigation rather than
157
- one navigation per story. Addon chrome remains outside the selected story root
158
- and does not enter the subject document.
228
+ One navigation serves the whole suite rather than one navigation per story, and
229
+ addon chrome stays outside the selected story root, so it never enters the
230
+ subject document.
159
231
 
160
232
  ## Readiness and loading
161
233
 
162
- Before each story is read, the collector waits for every React Suspense boundary
163
- under the story root to settle. This runs first, ahead of stabilization — the
164
- step that pins in-flight CSS animations and transitions to a settled frame
165
- before anything is read — because content that arrives late brings its own
166
- images and fonts.
234
+ Most stories are complete when Storybook emits `storyRendered`. Add a `ready`
235
+ entry only when a story continues mounting or fetching after that signal, keyed
236
+ by Storybook story id:
167
237
 
168
- `ready` cannot cover this case and no marker can: a component that suspends
169
- renders no markup for a selector to attach to, and Storybook's `storyRendered`
170
- has already fired — the story function returned. A story still showing a
171
- fallback when the wait runs out is reported as **not collected**, naming the open
172
- boundaries and the components that wrote them, rather than recorded as a
173
- baseline. A skeleton on a slow machine and the component on a fast one is a
174
- difference no one authored, and every band — the severity category, from
175
- accessibility down to sub-pixel noise, a change is filed under — agrees with
176
- both.
238
+ ```js
239
+ // variance/storybook.mjs
240
+ import { storybookCollector } from '@variance-authority/storybook-collector';
241
+
242
+ export default storybookCollector({
243
+ ready: { 'checkout--deferred': '[data-testid="checkout-ready"]' },
244
+ source: { dirs: ['src'] },
245
+ });
246
+ ```
247
+
248
+ If the selector never appears, collection fails naming the story and the
249
+ selector instead of silently capturing a spinner.
250
+
251
+ React Suspense is handled without a marker, and needs to be. Before each story is
252
+ read, every Suspense boundary under the story root is waited on — ahead of
253
+ stabilization, the step that pins in-flight CSS animations and transitions to a
254
+ settled frame, because content that arrives late brings its own images and fonts.
255
+ A marker cannot cover this case: a component that suspends renders no markup for
256
+ a selector to attach to, and `storyRendered` has already fired because the story
257
+ function returned.
258
+
259
+ A story still showing a fallback when the wait runs out is reported as **not
260
+ collected**, naming the open boundaries and the components that wrote them. A
261
+ skeleton on a slow machine and the component on a fast one is a difference no one
262
+ authored.
177
263
 
178
- Declare the exception when the fallback is the subject:
264
+ Declare the exception when the fallback *is* the subject:
179
265
 
180
266
  ```js
267
+ // variance/storybook.mjs
268
+ import { storybookCollector } from '@variance-authority/storybook-collector';
269
+
181
270
  export default storybookCollector({
182
271
  loading: ['inbox--empty-state'],
183
272
  source: { dirs: ['src'] },
@@ -188,6 +277,141 @@ A declared story waits for nothing, and is refused if it turns out to have
188
277
  settled — a declaration that outlived its subject is the same nondeterminism from
189
278
  the other side.
190
279
 
280
+ ## Sharper source lines from a built Storybook
281
+
282
+ Two things get lost in a production Storybook build, and both are recovered in
283
+ `.storybook/main.js`. Skip this section entirely against a development
284
+ Storybook, where names and lines survive by default.
285
+
286
+ **Component names.** A bundler renames functions, React reads a component's
287
+ display name off the function, and a run then reports the cause as `Ce` rather
288
+ than `Button` — a complete, confident answer naming something that appears
289
+ nowhere in your source. Keep function names in whatever your builder spells that
290
+ setting.
291
+
292
+ **Element lines.** With names alone, a changed element resolves to the line its
293
+ component is *declared* on — one line however many times that component is
294
+ rendered. `@variance-authority/jsx-source` makes it resolve to the line the
295
+ element itself is written on. It is a Vite plugin.
296
+
297
+ ```bash
298
+ npm install --save-dev @variance-authority/jsx-source
299
+ ```
300
+
301
+ Added to an existing Vite config rather than replacing one. On Vite 8 the two
302
+ settings sit in two different sections — the JSX transform under `oxc`, keeping
303
+ names under `build.rolldownOptions.output`:
304
+
305
+ ```js
306
+ // .storybook/main.js
307
+ import { jsxSource } from '@variance-authority/jsx-source/vite';
308
+
309
+ export default {
310
+ stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
311
+ framework: { name: '@storybook/react-vite', options: {} },
312
+ viteFinal: async (config) => ({
313
+ ...config,
314
+ plugins: [...(config.plugins ?? []), jsxSource()],
315
+ oxc: { ...config.oxc, jsx: { runtime: 'automatic', development: true } },
316
+ build: {
317
+ ...config.build,
318
+ rolldownOptions: {
319
+ ...config.build?.rolldownOptions,
320
+ output: { ...config.build?.rolldownOptions?.output, keepNames: true },
321
+ },
322
+ },
323
+ }),
324
+ };
325
+ ```
326
+
327
+ `development: true` is what makes the transform emit each element's file, line
328
+ and column at all; without it there is no location for the plugin to keep, and a
329
+ changed element reports the line its component is declared on. `keepNames` is a
330
+ Rolldown output option and does nothing inside the `oxc` block — it belongs in
331
+ the `build` section, not beside the JSX keys.
332
+
333
+ Vite 7 and below spell all three under `esbuild` — `jsx: 'automatic'`,
334
+ `jsxDev: true` and `keepNames: true` — in place of both sections above. A config
335
+ that uses the other major's key is read by nothing and warns about nothing: the
336
+ build succeeds, and the report names `Ce`. See
337
+ [`@variance-authority/jsx-source`](https://variance-authority.dev/reference/packages/jsx-source)
338
+ for builds that already use Emotion, theme-ui or another custom JSX runtime.
339
+
340
+ ## Skipping stories nothing touched
341
+
342
+ With `tests`, a run records which blocks of your code each story crossed, so the
343
+ next run can skip a story whose code your diff did not reach. The recording is
344
+ done by a Vite plugin in the preview build:
345
+
346
+ ```bash
347
+ npm install --save-dev @variance-authority/sense
348
+ ```
349
+
350
+ ```js
351
+ // .storybook/main.js
352
+ import { testSelectionProbes } from '@variance-authority/sense/journal';
353
+
354
+ export default {
355
+ stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
356
+ framework: { name: '@storybook/react-vite', options: {} },
357
+ viteFinal: async (config) => ({
358
+ ...config,
359
+ plugins: [...(config.plugins ?? []), testSelectionProbes({ label: 'storybook' })],
360
+ }),
361
+ };
362
+ ```
363
+
364
+ ```js
365
+ // variance/storybook.mjs
366
+ import { storybookCollector } from '@variance-authority/storybook-collector';
367
+
368
+ export default storybookCollector({
369
+ source: { dirs: ['src'] },
370
+ tests: { label: 'storybook' },
371
+ });
372
+ ```
373
+
374
+ `tests` accepts `true` or an options object: `label` must match the one the
375
+ preview's `testSelectionProbes()` was given, `root` is the repository root the
376
+ recorded paths are relative to, and `cacheRoot` and `coverageFile` override the
377
+ repository-keyed cache paths for the block records and the coverage index.
378
+ Without a collector in the page, the run says so on stderr and records nothing.
379
+
380
+ `mode` is the probe recipe, and it has to be the one the preview was built
381
+ under: a snapshot names the recipe its ordinals were cut by, and a merge
382
+ discards a layer cut by another one, so a preview probing under `entries` and a
383
+ run folding under `presence` would each wipe the other. `preconditions` names
384
+ files whose contents are a precondition of every story's observation — a
385
+ `preview.js` that decides what renders belongs there, because a change to it
386
+ invalidates every reading and no crossing will ever say so. `heads` and
387
+ `commit` stamp the snapshot with the branch tips this recording stands on and
388
+ the commit it was taken at, which is what lets a later run tell a stale layer
389
+ from a current one.
390
+
391
+ A story is its own owner in the recorded index — Storybook is an execution
392
+ surface this tool drives one subject at a time — and a story that did not render
393
+ still contributes its crossings while never justifying a later skip.
394
+
395
+ Set `cases` to also write the execution index: one row per story per region,
396
+ beside the coverage index, or wherever `executionFile` points. That is the file
397
+ `variance covering` and `@variance-authority/distill` read to answer *which
398
+ stories walk this branch* — the question a component library is worth asking and
399
+ a preview of ten thousand stories usually is not, which is why it is off by
400
+ default.
401
+
402
+ ```js
403
+ export default storybookCollector({
404
+ source: { dirs: ['src'] },
405
+ tests: { label: 'storybook', cases: true },
406
+ });
407
+ ```
408
+
409
+ A story is the case this costs nothing to name. The driver shows one at a time,
410
+ so the per-story grain a unit runner needs an async scope for is already here:
411
+ the index names each row by where the story is declared — its file, its
412
+ `title/name` — which is the coordinate a person reads in the sidebar and the one
413
+ a diff touches.
414
+
191
415
  ## When integration fails
192
416
 
193
417
  - **The index cannot be read:** build Storybook first and check
@@ -199,26 +423,30 @@ the other side.
199
423
  resolved. Fix what it awaits, or add the story to `loading` if the fallback is
200
424
  what you intend to review.
201
425
  - **Components have names but no source lines:** add or correct `source.dirs`.
202
- Production minification must also preserve component function names; the
203
- worked case uses `esbuild.keepNames: true` for this reason.
426
+ - **The report names a component that is nowhere in your source:** minification
427
+ renamed it. See the `keepNames` setting above.
204
428
  - **Elements report their component's declaration rather than their own line:**
205
- against a built Storybook, the `jsx-source` plugin is not installed or
206
- `esbuild.jsxDev` is off in the build that produced this artifact. Locations
207
- survive minification, so it is worth turning on. Against a development
208
- Storybook nothing needs installing — if lines are missing there, React is not
209
- its development build, the dev server is emitting no source maps, or this is
210
- React 18 compiled with the classic transform, which records no location for
211
- either mechanism to read.
429
+ this is the expected fallback for an uninstrumented built Storybook. Install
430
+ `jsx-source` only when the report must distinguish the exact element instance.
431
+ Against a development Storybook nothing normally needs installing; missing
432
+ lines there mean React is not its development build, the dev server emits no
433
+ source maps, or React 18 is still compiled to classic `createElement` calls.
212
434
  - **Images change without a document change:** leave `network` enabled so asset
213
435
  response bodies participate in the environment key. Disable it only when the
214
436
  URL already identifies the bytes.
215
437
  - **The browser is missing:** run `npx playwright install chromium`, then
216
- `variance doctor` again.
438
+ `npx variance doctor` again.
439
+ - **A rerun reports `incomparable`:** the approved image was stored under a
440
+ different identity than this machine produces. `npx variance doctor` lists the
441
+ identities the baseline root stores.
217
442
 
218
443
  ## Boundaries
219
444
 
220
445
  This package handles Storybook only. For pages your application already serves,
221
- use `@variance-authority/route-collector`. For an existing
222
- Playwright test, use
223
- `@variance-authority/playwright-test`, where the test body
224
- already performs navigation, mounting, and readiness.
446
+ use `@variance-authority/route-collector`. For an existing Playwright test, use
447
+ `@variance-authority/playwright-test`, where the test body already performs
448
+ navigation, mounting and readiness.
449
+
450
+ ---
451
+
452
+ **[@variance-authority/storybook-collector](https://variance-authority.dev/reference/packages/storybook-collector)** is part of [Variance Authority](https://variance-authority.dev) — [documentation](https://variance-authority.dev/docs) · MIT