@variance-authority/storybook-collector 0.15.0 → 0.19.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,5 +1,184 @@
1
1
  # @variance-authority/storybook-collector
2
2
 
3
+ ## 0.19.0
4
+
5
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
6
+
7
+ ## 0.18.0
8
+
9
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
10
+
11
+ ## 0.17.1
12
+
13
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
14
+
15
+ ## 0.17.0
16
+
17
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
18
+
19
+ ## 0.16.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 402ce9f: A story's parameters are read when the story is reached, so no story file is imported before its story comes up
24
+
25
+ Your Storybook run used to read every story's `parameters.variance` before the
26
+ first story, through the preview's `extract()`. That imported every story file
27
+ up front: on a large Storybook you paid for the whole build before seeing one
28
+ story, and each file's first story then timed its import as 0 in `collect`.
29
+
30
+ The run now plans from `index.json` alone. When it reaches a story, it asks the
31
+ preview for that story's merged parameters, which imports only that story's
32
+ file, the import showing the story would have made anyway. That import is
33
+ charged to the story's `collect`, so the first story of each file carries what
34
+ its file costs. A story that excludes itself is still skipped and listed in the
35
+ report with its reason, and a story whose parameters cannot be read is read at
36
+ your run's `viewport` with a warning naming it. A preview too old to read one
37
+ story's parameters says so once per run, not once per story.
38
+
39
+ A story that declares `widths` is still read as `<story id>@<width>` at each
40
+ width, with its own baseline, and all its widths are read on the same page in
41
+ one visit: nothing is opened, booted or loaded again for a second width. A
42
+ run that does not read such a story, because `--subjects`, `--since` or a shard
43
+ left it out, because its visit failed, or because its parameters could not be read, still counts its width baselines as
44
+ planned rather than naming them as baselines your plan no longer holds. A
45
+ story that excludes itself when it is reached holds no subject of the run, so
46
+ every baseline it left, under its own id and at each width, is named. A
47
+ shard prices the story at the sum of the widths its last visit read.
48
+
49
+ If you write a collector, `collect` may now return a `Visit` instead of a
50
+ `Collected`: every subject the visit to the planned one produced, each with its own
51
+ result, each id the planned id or the planned id at a width (`<id>@<width>`),
52
+ and any warnings the visit raised. A visit that could not learn every subject
53
+ it realizes, such as a story read without its parameters, says `incomplete: true`.
54
+ A visit that produced no subject is
55
+ reported as a failed one, and so is a visit that produced any other id. The run and `variance collect` report each subject
56
+ at the position the plan gave the planned subject, and evidence parts record
57
+ the planned id as `of`; a merge refuses a part whose subject is neither. A
58
+ `--subjects` glob that names one width, such as `*@375`, now reaches the story
59
+ it comes from, and only a width suffix extends a story id: `button--primary`
60
+ no longer reaches `button--primary-large`.
61
+
62
+ `@variance-authority/core/format` exports `atWidth`, which writes a subject's
63
+ id and name at a width, and `widenedFrom`, which reads the planned id back
64
+ from a width id, and `realizes`, which says whether a visit to one id may
65
+ produce another.
66
+
67
+ `readParameters(page)` is now `readParameters(page, storyId)` and returns one
68
+ story's parameters; the up-front read of every story is gone.
69
+ - d7f8b72: A story's execution record now says where the browser spent it, timed by the page: `prepare` for the preview's load, paid by the first story read on a document and 0 for every later one; `collect` for importing the story's file and unmounting the story before it; `cases` for the render through `afterEach`. `collectStory` returns them as `timing`, and `watchStoryTiming` starts the page's watch; the collector's page agent runs it.
70
+ - bd4243a: An image, font or media request still unanswered when the network wait runs
71
+ out refuses the subject, naming the URL. The subject used to be read without
72
+ it, with a `network-not-quiet` warning. `NetworkObservation.settle()` now
73
+ resolves that refusal, or `undefined` once nothing is in flight; a caller that
74
+ ignores it reads a subject whose bytes nobody hashed.
75
+ Only the requests a subject started count against it: one still open from an
76
+ earlier subject on the same page was named in that subject's refusal.
77
+ One the read itself asks for and never gets makes `acquireFromAgent` throw
78
+ `UnansweredAssets`, which both collectors turn into that subject's refusal.
79
+ - 8b189b9: Place the probes ahead of your transforms with `placement: 'before-transforms'`
80
+
81
+ The probes go in after your transforms by default, so a compiler that reads
82
+ syntax never reads one. That costs a second parse and the map back to each
83
+ file. A unit-test run whose transforms only strip types or JSX can skip both:
84
+ `placement: 'before-transforms'` places the probes in the file as read, with
85
+ one parse and no map. It means the same on the Vitest, Jest and Rstest
86
+ `withTestSelection`, on `testSelectionProbes` and `testSelectionRule`, and as an
87
+ option of the Storybook addon. With Compiled, React Compiler or another compiler
88
+ that reads syntax in the run, keep the default: nothing detects them for you.
89
+ - 7ff6d4b: Record what each story executed under Rspack and Webpack too
90
+
91
+ Add `@variance-authority/storybook-collector` to your Storybook's `addons`, and
92
+ the preview build carries the probes `tests` reads, whichever builder runs it:
93
+ `testSelectionProbes()` under Vite, the `testSelectionRule()` loader under
94
+ Rspack and Webpack 5. The probes go in after every compiler the build runs, so a
95
+ Storybook built by Rspack with `@compiled/react` records its stories. The
96
+ addon's options are the probes' own: `root`, `mode`, `include` and `placement`.
97
+
98
+ The addon probes only the builds the collector records: `storybook build
99
+ --test` and a dev server. Any other `storybook build`, the one a team
100
+ publishes, gets nothing from it, so build the recorded Storybook with `--test`.
101
+ The collector's `tests` is read in a later process, after the build, so the
102
+ build cannot ask it; Storybook's own `--test` is how a build says it is for
103
+ testing.
104
+
105
+ Under Rspack and Webpack, where the build writes no source map the loaders can
106
+ read, as `storybook build --test` does, the addon hands the loaders their maps
107
+ and the build still writes no `.map` file. `devtool` stays as you set it.
108
+
109
+ Storybook runs an addon's builder hooks before the ones in your `main.js`, so
110
+ the addon places the rule from a plugin, once the config is final: a post
111
+ loader your own `webpackFinal` or `rsbuildFinal` adds still runs ahead of the
112
+ probes. If you added `testSelectionProbes()` or `testSelectionRule()` to the
113
+ Storybook's builder config yourself, remove it and add the addon instead: a
114
+ second one stops the build with "remove your testSelectionRule(); the addon
115
+ places it", or the same of `testSelectionProbes()`, wherever the rule sits,
116
+ in `oneOf` or nested `rules` too, and whether it names the loader by its path
117
+ or as `@variance-authority/sense/probes-loader`. One your `vite.config` lists is
118
+ kept, and the addon adds no second one; if the addon was also given a `mode`,
119
+ `include` or `placement` that differs from it, the build stops and names the
120
+ option. The Vite config `@storybook/addon-vitest` loads your presets for,
121
+ inside Vitest, gets nothing from the addon. Under Rsbuild, an `rsbuildFinal` of
122
+ yours that assigns `tools.rspack` outright, rather than adding to it, drops the
123
+ addon's plugin, and the build runs unprobed.
124
+
125
+ When `tests` is on and the preview has no probes, the run's stderr now says to
126
+ add the addon and build with `storybook build --test`, where it named
127
+ `testSelectionProbes()`. The `tests` option's documentation says the same.
128
+
129
+ ### Patch Changes
130
+
131
+ - 8d1bcc1: A link in a doc comment opens from `node_modules`
132
+
133
+ Doc comments linked to documentation by repository paths such as
134
+ `../../../../docs/flakiness.md`, which resolve to nothing in an installed
135
+ package. They now link to the page on variance-authority.dev, or to the file on
136
+ GitHub when the site does not publish it, so the link works on hover in your
137
+ editor and in what `variance ask symbol` prints.
138
+
139
+ The MCP server's instructions and its observability answer linked to
140
+ `/start-cli` and `/presentation`, which the site does not serve. They now link
141
+ to `/start/cli` and `/docs/presentation`, and the `@variance-authority/cli`,
142
+ `@variance-authority/help`, `@variance-authority/mcp` and
143
+ `@variance-authority/vitest-browser` READMEs link to the guides where the site
144
+ publishes them.
145
+
146
+ `variancePrecondition`, `wiringOf`, `tapCommits` and `provenanceOf` carry an
147
+ `@see` link to the section of the guide that documents them.
148
+ - 0e77166: A Vite Storybook with a `.storybook/preview` file builds with the addon
149
+
150
+ From Storybook 9.1 on, Storybook rewrites each `sb.mock(import('../lib/api'))`
151
+ in the preview file to `sb.mock('../lib/api')` by printing the whole file
152
+ again, with no map back to your lines, so `storybook build --test` stopped at
153
+ the probes on any preview, whether it mocked a module or not. The addon now has
154
+ that rewrite edit the file in place: `import(` and its closing parenthesis
155
+ become spaces, every other character keeps its line and column, and the
156
+ preview's decorators are recorded where you wrote them. Storybook 10 builds the
157
+ same way. A Storybook that rewrites more calls than the edit in place makes
158
+ stops the build with an error that names `storybook:mock-loader-preview` and
159
+ the preview, rather than shipping a mock that no longer mocks.
160
+ - c3c8e61: An absolute `source.dirs` entry is walked
161
+
162
+ Both collectors joined every `source.dirs` entry onto the working directory, so
163
+ an absolute path such as `/repo/packages/ui/src` was looked for under that
164
+ directory, matched nothing, and the run was refused for having no source files.
165
+ An absolute entry is now walked as written, and a relative one is still resolved
166
+ against the working directory. File names in the index stay relative to that
167
+ directory either way. When no entry matches any file, the refusal lists each
168
+ directory by the absolute path it walked, in place of the entries as spelled
169
+ followed by `(from <root>)`.
170
+ - 52d87e4: A run starts one Chromium. Harnesses and the renderer lease one browser per launch recipe and the last to close it closes it, so a Storybook or route run with four workers launches one browser instead of five. A Storybook run's workers also share one browser context, and with it the HTTP cache: a worker's preview boots from what the first page fetched instead of downloading the bundle again (at four workers on the storybook case, 75 fetches and 29 MB became 17 fetches and 6 MB).
171
+
172
+ New options: `createHarness({ warm })` opens the page in a context shared with other warm harnesses, `createHarness({ isolated })` launches a browser of the harness's own, and `observeNetwork(page, { cache })` watches the wire through the DevTools protocol with the HTTP cache left on (Chromium only). The `alone` reading in both collectors stays in a browser of its own.
173
+ - e6afc58: One recorder for every visual subject
174
+
175
+ `@variance-authority/sense/journal` exports `createSubjectRecorder`, which keeps
176
+ one row per subject of a visual run with the page and every head joined, and
177
+ `driveJourneys`, the driver's listener, cookies and settling wait. The
178
+ Storybook collector records through the first, the Playwright fixture through
179
+ the second, so a story, a page and a test file record their journeys the same
180
+ way.
181
+
3
182
  ## 0.15.0
4
183
 
5
184
  ### Minor Changes
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  > Turn a built or already-served Storybook into Variance Authority subjects.
6
6
 
7
7
  Part of [Variance Authority](https://variance-authority.dev).
8
+ Point your coding agent at [`llms.txt`](https://variance-authority.dev/llms.txt).
8
9
 
9
10
  ## What this is for
10
11
 
@@ -34,7 +35,7 @@ This is the Storybook adapter, not the `variance` binary. The binary lives in
34
35
  | Module format | ESM only. Every `@variance-authority/*` package sets `"type": "module"`; `require()` will not load them. |
35
36
  | Browser | Chromium, installed through Playwright. The binaries do not arrive with an `npm install`. |
36
37
  | 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 works the same 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
+ | Builder | Collection reads `index.json` and drives the preview channel, so it works the same whether Vite, Webpack 5 or Rspack built the Storybook. The `tests` option records under all three. `@variance-authority/jsx-source` is a Vite plugin with no Webpack or Rspack equivalent. |
38
39
  | 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
 
40
41
  A Storybook whose renderer is not React still collects: documents, pixels,
@@ -76,8 +77,8 @@ export default storybookCollector({
76
77
  });
77
78
  ```
78
79
 
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
80
+ `source.dirs` is a list of directory paths — not globs — each absolute or
81
+ resolved against the directory you run `variance` from. The scan reads `.tsx`, `.jsx`, `.ts` and
81
82
  `.js`, skips `node_modules`, `dist`, `storybook-static` and any path containing
82
83
  `.test.`, `.spec.` or `.stories.`, and indexes where each component is
83
84
  *declared*. Without it the report still names components and resolves none of
@@ -173,7 +174,7 @@ rendered by playwright-chromium (chromium@131, darwin/arm64, 1x)
173
174
  [changed] story:checkout--empty — Button: 1356 pixel(s) differ across 2 region(s) in Button
174
175
  [changed] story:checkout--one-item — Button: 1356 pixel(s) differ across 2 region(s) in Button
175
176
 
176
- not observed: 2 subject(s) — 0 the run could not see, 2 excluded by configuration, 0 not reached by this change
177
+ not observed: 2 subject(s) — 0 the run could not see, 2 excluded on purpose, 0 not reached by this change
177
178
  [excluded] story:checkout--docs-only: excluded by tag `no-variance`
178
179
  ```
179
180
 
@@ -215,7 +216,7 @@ Everything below is passed to `storybookCollector(...)` in the module from step
215
216
  | `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. |
216
217
  | `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. |
217
218
  | `roots` | Your preview mounts somewhere other than the standard roots. | `['#storybook-root', '#root']`, tightest match first. |
218
- | `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. |
219
+ | `tests` | The next run should be able to skip stories whose code nothing touched. | `false`. Requires this package in the Storybook's `addons` and `storybook build --test`; see below. |
219
220
 
220
221
  A **band** is the severity category a change is filed under. Loudest first:
221
222
  `a11y`, `geometry`, `token`, `content`, `texture`. A role or accessible name that
@@ -232,6 +233,16 @@ One navigation serves the whole suite rather than one navigation per story, and
232
233
  addon chrome stays outside the selected story root, so it never covers the
233
234
  subject document.
234
235
 
236
+ A parallel run starts one Chromium. Each worker is another page in it, in one
237
+ browser context with the first, so its preview boots from the HTTP cache rather
238
+ than fetching the bundle again; the pages share cookies and storage, as stories
239
+ in one preview already share a document. Across workers, what a story finds left
240
+ behind depends on scheduling: which worker read which story before it decides the
241
+ cookies, storage and service workers, msw's among them, that it meets. When you
242
+ let the collector serve the build, it answers with `cache-control: immutable` so
243
+ every asset stays cached for the run. A story read again to rule out its
244
+ neighbours gets a browser of its own.
245
+
235
246
  ## Readiness and loading
236
247
 
237
248
  Most stories are complete when Storybook emits `storyRendered`. Add a `ready`
@@ -344,26 +355,58 @@ for builds that already use Emotion, theme-ui or another custom JSX runtime.
344
355
 
345
356
  With `tests`, a run records which blocks of your code each story crossed, so the
346
357
  next run can skip a story whose code your diff did not reach. The recording is
347
- done by a Vite plugin in the preview build:
348
-
349
- ```bash
350
- npm install --save-dev @variance-authority/sense
351
- ```
358
+ done by probes in the preview build, which this package adds as a Storybook
359
+ addon to `storybook build --test` and a dev server:
352
360
 
353
361
  ```js
354
362
  // .storybook/main.js
355
- import { testSelectionProbes } from '@variance-authority/sense/journal';
356
-
357
363
  export default {
358
364
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
359
- framework: { name: '@storybook/react-vite', options: {} },
360
- viteFinal: async (config) => ({
361
- ...config,
362
- plugins: [...(config.plugins ?? []), testSelectionProbes()],
363
- }),
365
+ framework: { name: 'storybook-react-rsbuild', options: {} },
366
+ addons: ['@variance-authority/storybook-collector'],
364
367
  };
365
368
  ```
366
369
 
370
+ Storybook loads the addon through its preset,
371
+ `@variance-authority/storybook-collector/preset`, which you never import
372
+ yourself. The preset reads the builder Storybook runs. Under Vite it adds
373
+ `testSelectionProbes()` from `@variance-authority/sense/journal`; under Rspack,
374
+ through `rsbuildFinal`, and Webpack 5, through `webpackFinal`, it adds a plugin
375
+ that lists `testSelectionRule()`, a loader at `enforce: 'post'`, first among the
376
+ rules once the build's config is final, so it runs after every post loader of
377
+ yours, the ones your `main.js` hooks add included: Storybook runs an addon's
378
+ hooks before yours. Either way the probes go in after
379
+ every compiler your build runs, so an extractor such as `@compiled/react` reads
380
+ your file as you wrote it, and each probe is placed where the source map says
381
+ that code went. Where the build writes no map the loaders can read, as
382
+ `storybook build --test` does, the preset hands the loaders their maps and the
383
+ build still writes no `.map` file. The preset asks Storybook which build it
384
+ is: `test`, which `--test` sets, or `configType` of `DEVELOPMENT`, which a dev
385
+ server sets. Any other `storybook build`, the one you publish, gets nothing
386
+ from the addon: build it with `--test` for the run that records. If you added
387
+ `testSelectionRule()` to the Storybook's builder config by hand, or
388
+ `testSelectionProbes()` in a `viteFinal` of your own, remove it: two would
389
+ probe each other's probes, so the build stops and says "remove your
390
+ testSelectionRule(); the addon places it", or the same of
391
+ `testSelectionProbes()`. One your `vite.config` lists is already in the config
392
+ when the addon runs, and the addon keeps it rather than add a second; if you
393
+ also gave the addon a `mode`, `include` or `placement` that differs from it, the
394
+ build stops and names the option, since the addon's would be dropped. Under
395
+ Rsbuild the addon adds its plugin as a function at the end of `tools.rspack`:
396
+ an `rsbuildFinal` of yours that assigns `tools.rspack` outright, rather than
397
+ adding to the list, drops it, and the build runs without probes. The Vite
398
+ config `@storybook/addon-vitest` loads your presets for, inside Vitest, gets
399
+ nothing from the addon either: that run is a Vitest run, and the Vitest seam
400
+ records it where you configured one.
401
+
402
+ Pass the probes' options as the addon's: `{ name:
403
+ '@variance-authority/storybook-collector', options: { mode: 'entries' } }`.
404
+ `include` decides which modules are your source, every file outside
405
+ `node_modules` and `dist` that is not a test or a config by default; `root` is a
406
+ directory in the repository, the cwd by default. `placement:
407
+ 'before-transforms'` places the probes in each file as read, ahead of every
408
+ compiler; a Storybook that runs an extractor keeps the default.
409
+
367
410
  ```js
368
411
  // variance/storybook.mjs
369
412
  import { storybookCollector } from '@variance-authority/storybook-collector';
@@ -412,6 +455,23 @@ the index names each row by where the story is declared — its file, its
412
455
  `title/name` — which is the coordinate a person reads in the sidebar and the one
413
456
  a diff touches.
414
457
 
458
+ Each story's row also says where the browser spent it, timed by the page on its
459
+ own clock rather than by the test driving it, under the phase names a test
460
+ file's row uses. `prepare` is the document's start until the preview is ready:
461
+ the navigation, the runtime, the project annotations and the index. A document
462
+ pays it once, so only the first story read on it carries it, and a story the
463
+ preview switched to reads 0, as the second test in one file pays no worker
464
+ start. `collect` is importing the story's file and everything it loads, and then
465
+ unmounting the story shown before it: Storybook does that in between and signals
466
+ nothing for it. No file is imported before its first story comes up — the
467
+ story's parameters are read from the file the preview loads to show it — so the
468
+ first story of each file carries that file's import, and the stories after it
469
+ read only the unmount. `cases` is the render, from loaders through the play function to
470
+ `afterEach`, or to the phase it ended on when it errored or was aborted. Each
471
+ width a story is read at is its own row, with its own figures; the widths share
472
+ one visit on one page, so the first carries the import and the others are the
473
+ same story shown again at another size.
474
+
415
475
  ## When integration fails
416
476
 
417
477
  - **The index cannot be read:** build Storybook first and check
@@ -450,4 +510,4 @@ navigation, mounting and readiness.
450
510
 
451
511
  ---
452
512
 
453
- **[@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
513
+ **[@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) · [GitHub](https://github.com/Variance-Authority/variance-authority/tree/main/packages/storybook-collector) · MIT
@@ -82,8 +82,8 @@ export interface PlannedSubject {
82
82
  * from — and it is the right one anyway: what a subject *is* belongs in its
83
83
  * own name, next to it, rather than in a central file repeating every id.
84
84
  * How a story is *read* — excluded, resized, read at several widths — comes
85
- * from its parameters, which `withParameters` lays over this plan from the
86
- * running preview.
85
+ * from its parameters, which are read when the story is reached: see
86
+ * {@link Visit}.
87
87
  *
88
88
  * Selection lives here; definition lives in the config. A tag is a word a
89
89
  * story wears, and what that word *means* is the operator's to write down
@@ -124,7 +124,39 @@ export type Collected = {
124
124
  } | {
125
125
  readonly ok: false;
126
126
  readonly because: string;
127
+ /**
128
+ * The subject asked not to be read, and was not: an exclusion the run
129
+ * reports with its reason, not a failure. Absent for every other refusal.
130
+ */
131
+ readonly excluded?: true;
127
132
  };
133
+ /** One subject a visit read: the subject as it was realized, and its reading. */
134
+ export interface VisitedSubject {
135
+ readonly planned: PlannedSubject;
136
+ readonly collected: Collected;
137
+ }
138
+ /**
139
+ * What reaching one planned subject produced, when it is not one reading.
140
+ *
141
+ * A plan is made before any subject is visited, and some of what decides how a
142
+ * subject is read is only known at the visit: a story's widths live in its
143
+ * parameters, which are read when its file is loaded. So a visit may realize
144
+ * several subjects — one per width, each the planned id at that width,
145
+ * `<id>@<digits>` — or the planned subject at a viewport it declared, and say
146
+ * what it could not read on the way. A visit that realizes any other id fails
147
+ * its story.
148
+ */
149
+ export interface Visit {
150
+ readonly subjects: readonly VisitedSubject[];
151
+ readonly warnings?: readonly string[];
152
+ /**
153
+ * Present when the visit could not learn every subject the planned one
154
+ * realizes — a story whose parameters were not read, so its widths are
155
+ * unknown. The run then treats what the store holds of the planned subject
156
+ * as it does for a subject it did not visit: planned, not left behind.
157
+ */
158
+ readonly incomplete?: true;
159
+ }
128
160
  export interface CollectorContext {
129
161
  readonly config: CollectorConfig;
130
162
  /** Computed by the run from the story index. Returned unchanged. */
@@ -132,7 +164,7 @@ export interface CollectorContext {
132
164
  }
133
165
  export interface Collector {
134
166
  plan(): Promise<Plan>;
135
- collect(subject: PlannedSubject): Promise<Collected>;
167
+ collect(subject: PlannedSubject): Promise<Collected | Visit>;
136
168
  /**
137
169
  * The frames a snapshot carries, spent on demand.
138
170
  *
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type { CallSiteResolver, SourceIndex } from '@variance-authority/core/attribute';\nimport type {\n Diagnostic,\n PageRegion,\n RenderDocument,\n SemanticSnapshot,\n SubjectRef,\n Viewport,\n} from '@variance-authority/core/format';\n\n/**\n * The contract a collector satisfies, restated here rather than imported.\n *\n * The CLI declares the same shapes and this package does not depend on it — a\n * surface package pulling in the binary would put a config parser, a docket and\n * six commands into an adopter's dependency tree to describe four values\n * (ADR-0024). What keeps the two copies honest is that the CLI imports *this*\n * collector in its own tests, so a drift between them fails a build rather than\n * a run.\n */\n\n/**\n * The subset of a run's configuration this collector reads. A region field it\n * leaves out is pinned to `UTC` or `en-US`, never the host's.\n */\nexport interface CollectorConfig extends PageRegion {\n readonly viewport: Viewport;\n readonly fonts?: readonly string[];\n /**\n * Subtrees this project excludes, from `config.ignore` (spec 0024).\n *\n * Only the two fields a page needs: a rule id to record the mark under, and a\n * selector to find it with. Everything else about a rule — its reason, its\n * expiry, the bands it narrows to — is decided after collection, where a clock\n * and the whole run's diffs are available and a browser is not.\n */\n readonly ignore?: readonly {\n readonly id: string;\n readonly select?: string;\n /**\n * Subjects the rule names. Read here to decide what to send, and never sent.\n *\n * A page can resolve a selector and cannot know which subject it is one of,\n * so scoping has to happen on this side of the boundary. Without it a rule\n * written for one route marks its element in every subject that renders the\n * same shared layout, and a real regression inside that element is absorbed\n * everywhere — an ignore silencing more than it says, which is the one thing\n * the mechanism must not do.\n */\n readonly subjects?: readonly string[];\n\n /** Tags the subject must carry. Read here to decide what to send, never sent. */\n readonly tags?: readonly string[];\n }[];\n\n /**\n * Images to serve as nothing, from `config.blank`.\n *\n * Handed to the driver rather than to the page, because it is enforced on the\n * wire — before a byte is decoded, and therefore before the layout it would\n * have participated in exists. Not scoped by subject, and it cannot be: a\n * request carries no idea which subject will end up using it, which is the\n * same reason the asset map is per page rather than per subject.\n */\n readonly blank?: readonly {\n readonly id: string;\n readonly url?: string;\n readonly minPixels?: number;\n readonly maxPixels?: number;\n }[];\n\n /**\n * The subjects section of the config, as the operator wrote it.\n *\n * `index` is optional because it belongs to `subjects.kind: \"storybook\"`, and\n * a config naming any other kind still reaches this collector — the kind is\n * chosen in the config and the collector is chosen in the config, and nothing\n * makes the operator pick a matching pair. Typed as always present, the first\n * thing to read it hands `undefined` to `node:path` and the adopter gets a\n * TypeError about an argument instead of a sentence naming the wrong word.\n */\n readonly subjects: { readonly index?: string };\n}\n\nexport interface PlannedSubject {\n readonly subject: SubjectRef;\n readonly viewport?: Viewport;\n\n /**\n * What the subject declares itself to be, from the artifact that produced it.\n *\n * Storybook's built index carries `tags` and does not carry a story's\n * `parameters`, so a tag is the one per-story declaration the index can plan\n * from — and it is the right one anyway: what a subject *is* belongs in its\n * own name, next to it, rather than in a central file repeating every id.\n * How a story is *read* — excluded, resized, read at several widths — comes\n * from its parameters, which `withParameters` lays over this plan from the\n * running preview.\n *\n * Selection lives here; definition lives in the config. A tag is a word a\n * story wears, and what that word *means* is the operator's to write down\n * once, where a typo can be refused by name.\n */\n readonly tags?: readonly string[];\n}\n\nexport interface Plan {\n readonly subjects: readonly PlannedSubject[];\n readonly notObserved: readonly unknown[];\n readonly warnings: readonly string[];\n}\n\nexport type Collected =\n | {\n readonly ok: true;\n readonly document: RenderDocument;\n readonly snapshot?: SemanticSnapshot;\n readonly source?: SourceIndex;\n\n /**\n * Stabilization tricks applied to the page before this subject was read.\n *\n * Reported so a run can say what it did to somebody else's page. The\n * digest of the same list is in the environment key, which is what names a\n * differently-stabilized baseline as the cause of a change; this is the\n * half a person reads.\n */\n readonly stabilization?: readonly string[];\n\n /**\n * What the adapter noticed about the *way* this subject was produced.\n *\n * Not about the picture — the document and the snapshot carry that — but\n * about the reading: a story Storybook finished by reporting it failed, a\n * preview that had to be reloaded to show it, a render that never\n * finished. Each of those leaves a real image that a run can compare, and\n * each of them changes what a person should conclude from a green verdict,\n * so they travel with the subject rather than being dropped at the seam.\n */\n readonly diagnostics?: readonly Diagnostic[];\n }\n | { readonly ok: false; readonly because: string };\n\nexport interface CollectorContext {\n readonly config: CollectorConfig;\n /** Computed by the run from the story index. Returned unchanged. */\n readonly plan?: Plan;\n}\n\nexport interface Collector {\n plan(): Promise<Plan>;\n collect(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * The frames a snapshot carries, spent on demand.\n *\n * Present when the collector drives a browser, because resolving a frame means\n * fetching the module it names and the page is the only place that request is\n * already correct. Absent otherwise, and absence is not a degradation: it means\n * either that nothing captured frames, or that the run has no live page to\n * fetch through — and both are a report without call sites rather than a\n * report that is wrong.\n *\n * Handed to `locateSites` with the few nodes a region or a finding names. The\n * resolver is per collector rather than per subject: the cache is what makes\n * this bounded, and a suite's subjects share their modules.\n */\n readonly callSites?: CallSiteResolver;\n\n /**\n * Collect this subject again, in a world nothing else has touched.\n *\n * The saving this whole tool is built on is that the world is *not* rebuilt\n * between subjects (ADR-0009): one browser, one page, one Storybook, for the\n * length of a run. What it buys is the possibility that story B renders\n * differently because story A ran first — and a comparison cannot tell that\n * apart from a regression, because both arrive as \"the pixels moved\".\n *\n * One clean collection settles it. If the difference is gone with nothing else\n * in the world, the baseline was right and the session moved this subject, and\n * the run reports `order-dependent` rather than asking anybody to accept it.\n *\n * Optional in the contract the CLI declares, because a collector holding a\n * single page open has no clean world to offer and the run says so rather than\n * reading silence as \"nothing leaked\". This collector implements it: a\n * Storybook preview can be opened twice, and the second one costs a browser\n * launch — which is why the run only spends it on subjects it already called\n * `changed`, up to `alone.limit`.\n */\n collectAlone?(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * Another standing world from the same recipe and the same server, for a\n * second worker. Optional in the contract the CLI declares; absent, a run\n * configured with `workers` collects in one world and says so.\n */\n openWorker?(): Promise<Collector>;\n\n /**\n * `costs` is what the run timed for each story, first collection to\n * decision, by subject id; the execution record carries it as each story's\n * duration.\n */\n close(costs?: ReadonlyMap<string, number>): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type { CallSiteResolver, SourceIndex } from '@variance-authority/core/attribute';\nimport type {\n Diagnostic,\n PageRegion,\n RenderDocument,\n SemanticSnapshot,\n SubjectRef,\n Viewport,\n} from '@variance-authority/core/format';\n\n/**\n * The contract a collector satisfies, restated here rather than imported.\n *\n * The CLI declares the same shapes and this package does not depend on it — a\n * surface package pulling in the binary would put a config parser, a docket and\n * six commands into an adopter's dependency tree to describe four values\n * (ADR-0024). What keeps the two copies honest is that the CLI imports *this*\n * collector in its own tests, so a drift between them fails a build rather than\n * a run.\n */\n\n/**\n * The subset of a run's configuration this collector reads. A region field it\n * leaves out is pinned to `UTC` or `en-US`, never the host's.\n */\nexport interface CollectorConfig extends PageRegion {\n readonly viewport: Viewport;\n readonly fonts?: readonly string[];\n /**\n * Subtrees this project excludes, from `config.ignore` (spec 0024).\n *\n * Only the two fields a page needs: a rule id to record the mark under, and a\n * selector to find it with. Everything else about a rule — its reason, its\n * expiry, the bands it narrows to — is decided after collection, where a clock\n * and the whole run's diffs are available and a browser is not.\n */\n readonly ignore?: readonly {\n readonly id: string;\n readonly select?: string;\n /**\n * Subjects the rule names. Read here to decide what to send, and never sent.\n *\n * A page can resolve a selector and cannot know which subject it is one of,\n * so scoping has to happen on this side of the boundary. Without it a rule\n * written for one route marks its element in every subject that renders the\n * same shared layout, and a real regression inside that element is absorbed\n * everywhere — an ignore silencing more than it says, which is the one thing\n * the mechanism must not do.\n */\n readonly subjects?: readonly string[];\n\n /** Tags the subject must carry. Read here to decide what to send, never sent. */\n readonly tags?: readonly string[];\n }[];\n\n /**\n * Images to serve as nothing, from `config.blank`.\n *\n * Handed to the driver rather than to the page, because it is enforced on the\n * wire — before a byte is decoded, and therefore before the layout it would\n * have participated in exists. Not scoped by subject, and it cannot be: a\n * request carries no idea which subject will end up using it, which is the\n * same reason the asset map is per page rather than per subject.\n */\n readonly blank?: readonly {\n readonly id: string;\n readonly url?: string;\n readonly minPixels?: number;\n readonly maxPixels?: number;\n }[];\n\n /**\n * The subjects section of the config, as the operator wrote it.\n *\n * `index` is optional because it belongs to `subjects.kind: \"storybook\"`, and\n * a config naming any other kind still reaches this collector — the kind is\n * chosen in the config and the collector is chosen in the config, and nothing\n * makes the operator pick a matching pair. Typed as always present, the first\n * thing to read it hands `undefined` to `node:path` and the adopter gets a\n * TypeError about an argument instead of a sentence naming the wrong word.\n */\n readonly subjects: { readonly index?: string };\n}\n\nexport interface PlannedSubject {\n readonly subject: SubjectRef;\n readonly viewport?: Viewport;\n\n /**\n * What the subject declares itself to be, from the artifact that produced it.\n *\n * Storybook's built index carries `tags` and does not carry a story's\n * `parameters`, so a tag is the one per-story declaration the index can plan\n * from — and it is the right one anyway: what a subject *is* belongs in its\n * own name, next to it, rather than in a central file repeating every id.\n * How a story is *read* — excluded, resized, read at several widths — comes\n * from its parameters, which are read when the story is reached: see\n * {@link Visit}.\n *\n * Selection lives here; definition lives in the config. A tag is a word a\n * story wears, and what that word *means* is the operator's to write down\n * once, where a typo can be refused by name.\n */\n readonly tags?: readonly string[];\n}\n\nexport interface Plan {\n readonly subjects: readonly PlannedSubject[];\n readonly notObserved: readonly unknown[];\n readonly warnings: readonly string[];\n}\n\nexport type Collected =\n | {\n readonly ok: true;\n readonly document: RenderDocument;\n readonly snapshot?: SemanticSnapshot;\n readonly source?: SourceIndex;\n\n /**\n * Stabilization tricks applied to the page before this subject was read.\n *\n * Reported so a run can say what it did to somebody else's page. The\n * digest of the same list is in the environment key, which is what names a\n * differently-stabilized baseline as the cause of a change; this is the\n * half a person reads.\n */\n readonly stabilization?: readonly string[];\n\n /**\n * What the adapter noticed about the *way* this subject was produced.\n *\n * Not about the picture — the document and the snapshot carry that — but\n * about the reading: a story Storybook finished by reporting it failed, a\n * preview that had to be reloaded to show it, a render that never\n * finished. Each of those leaves a real image that a run can compare, and\n * each of them changes what a person should conclude from a green verdict,\n * so they travel with the subject rather than being dropped at the seam.\n */\n readonly diagnostics?: readonly Diagnostic[];\n }\n | {\n readonly ok: false;\n readonly because: string;\n /**\n * The subject asked not to be read, and was not: an exclusion the run\n * reports with its reason, not a failure. Absent for every other refusal.\n */\n readonly excluded?: true;\n };\n\n/** One subject a visit read: the subject as it was realized, and its reading. */\nexport interface VisitedSubject {\n readonly planned: PlannedSubject;\n readonly collected: Collected;\n}\n\n/**\n * What reaching one planned subject produced, when it is not one reading.\n *\n * A plan is made before any subject is visited, and some of what decides how a\n * subject is read is only known at the visit: a story's widths live in its\n * parameters, which are read when its file is loaded. So a visit may realize\n * several subjects — one per width, each the planned id at that width,\n * `<id>@<digits>` — or the planned subject at a viewport it declared, and say\n * what it could not read on the way. A visit that realizes any other id fails\n * its story.\n */\nexport interface Visit {\n readonly subjects: readonly VisitedSubject[];\n readonly warnings?: readonly string[];\n /**\n * Present when the visit could not learn every subject the planned one\n * realizes — a story whose parameters were not read, so its widths are\n * unknown. The run then treats what the store holds of the planned subject\n * as it does for a subject it did not visit: planned, not left behind.\n */\n readonly incomplete?: true;\n}\n\nexport interface CollectorContext {\n readonly config: CollectorConfig;\n /** Computed by the run from the story index. Returned unchanged. */\n readonly plan?: Plan;\n}\n\nexport interface Collector {\n plan(): Promise<Plan>;\n collect(subject: PlannedSubject): Promise<Collected | Visit>;\n\n /**\n * The frames a snapshot carries, spent on demand.\n *\n * Present when the collector drives a browser, because resolving a frame means\n * fetching the module it names and the page is the only place that request is\n * already correct. Absent otherwise, and absence is not a degradation: it means\n * either that nothing captured frames, or that the run has no live page to\n * fetch through — and both are a report without call sites rather than a\n * report that is wrong.\n *\n * Handed to `locateSites` with the few nodes a region or a finding names. The\n * resolver is per collector rather than per subject: the cache is what makes\n * this bounded, and a suite's subjects share their modules.\n */\n readonly callSites?: CallSiteResolver;\n\n /**\n * Collect this subject again, in a world nothing else has touched.\n *\n * The saving this whole tool is built on is that the world is *not* rebuilt\n * between subjects (ADR-0009): one browser, one page, one Storybook, for the\n * length of a run. What it buys is the possibility that story B renders\n * differently because story A ran first — and a comparison cannot tell that\n * apart from a regression, because both arrive as \"the pixels moved\".\n *\n * One clean collection settles it. If the difference is gone with nothing else\n * in the world, the baseline was right and the session moved this subject, and\n * the run reports `order-dependent` rather than asking anybody to accept it.\n *\n * Optional in the contract the CLI declares, because a collector holding a\n * single page open has no clean world to offer and the run says so rather than\n * reading silence as \"nothing leaked\". This collector implements it: a\n * Storybook preview can be opened twice, and the second one costs a browser\n * launch — which is why the run only spends it on subjects it already called\n * `changed`, up to `alone.limit`.\n */\n collectAlone?(subject: PlannedSubject): Promise<Collected>;\n\n /**\n * Another standing world from the same recipe and the same server, for a\n * second worker. Optional in the contract the CLI declares; absent, a run\n * configured with `workers` collects in one world and says so.\n */\n openWorker?(): Promise<Collector>;\n\n /**\n * `costs` is what the run timed for each story, first collection to\n * decision, by subject id; the execution record carries it as each story's\n * duration.\n */\n close(costs?: ReadonlyMap<string, number>): Promise<void>;\n}\n"]}
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * The instrument is the one `@variance-authority/sense` already uses; only the
5
5
  * transport differs, which is the rule
6
- * [spec 0028](../../../docs/specs/0028-the-instrument.md) states. The preview is
7
- * built with `testSelectionProbes()`, the page counts block crossings, and this
6
+ * [spec 0028](https://github.com/Variance-Authority/variance-authority/blob/main/docs/specs/0028-the-instrument.md) states. The preview is
7
+ * built with the package's addon, which adds the probes whichever builder runs
8
+ * it ([`preset.ts`](./preset.ts)), the page counts block crossings, and this
8
9
  * closes one window per story and joins them at the end of the run.
9
10
  *
10
11
  * A story is its own owner. Storybook is an execution surface this tool drives
@@ -12,44 +13,14 @@
12
13
  * smallest thing a runner can be asked to execute — the crossings of one story
13
14
  * belong to that story and to nothing else.
14
15
  */
15
- import { type EvaluatingPage, type InstrumentMode } from '@variance-authority/sense/journal';
16
- /** Where a story run's execution evidence is joined, read and kept. */
17
- export interface StoryExecutionOptions {
18
- /**
19
- * A directory inside the repository; defaults to the cwd. Recorded paths are
20
- * relative to the checkout it sits in, never to it. A story's `importPath` is
21
- * read from here, because it is relative to the directory Storybook ran in.
22
- */
23
- readonly root?: string;
24
- /** Where the coverage layers live. Defaults to the repository's cache. */
25
- readonly cacheRoot?: string;
26
- /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */
27
- readonly coverageFile?: string;
28
- /** The suite this run is, as the root `variance.config.json` declares it under `suites`. */
29
- readonly suite?: string;
30
- /**
31
- * The probe recipe the preview was built with, matching
32
- * `testSelectionProbes()`'s `mode`. `presence` when absent, as it is there.
33
- *
34
- * Both sides answer the same or neither works: a journal cut by one recipe
35
- * and folded as another is refused, and a snapshot a runner seam also writes
36
- * under a different recipe has each run retire the other's evidence.
37
- */
38
- readonly mode?: InstrumentMode;
39
- /**
40
- * Files whose contents are preconditions of every story this run recorded.
41
- *
42
- * A `.storybook/preview` file, a theme module every decorator reads — a
43
- * story *file* is already a precondition of its own stories, and this is for
44
- * the ones no story declares and nothing enters.
45
- */
46
- readonly preconditions?: readonly string[];
47
- /**
48
- * Where this recording stands. Defaults to the checkout's `HEAD`, which is
49
- * the answer in every case except a caller that already knows better.
50
- */
51
- readonly commit?: string;
52
- }
16
+ import { type EvaluatingPage, type SubjectRecording } from '@variance-authority/sense/journal';
17
+ import { type StoryTiming } from '@variance-authority/storybook';
18
+ /**
19
+ * Where a story run's execution evidence is joined, read and kept: the shared
20
+ * recording, with `root` also the directory a story's `importPath` is read
21
+ * from, because it is relative to the directory Storybook ran in.
22
+ */
23
+ export type StoryExecutionOptions = Omit<SubjectRecording, 'heads'>;
53
24
  /** One run's accumulation: a window per story, written once at the end. */
54
25
  export interface StoryRecorder {
55
26
  /**
@@ -58,8 +29,12 @@ export interface StoryRecorder {
58
29
  * `complete` is false for a story this run refused. A refused story still
59
30
  * executed code, so its crossings are kept — what it may never do is justify
60
31
  * excluding itself from a later run.
32
+ *
33
+ * `timing` is where the page spent the story: the preview's load is the
34
+ * story's `prepare`, 0 for a story the preview switched to, its file's
35
+ * import its `collect`, its render its `cases`.
61
36
  */
62
- readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean) => Promise<void>;
37
+ readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean, timing?: StoryTiming) => Promise<void>;
63
38
  /**
64
39
  * Merge the run into the index, or say on stderr why it could not.
65
40
  *