@variance-authority/storybook-collector 0.1.0 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # @variance-authority/storybook-collector
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8efba76: A run that changed ten files stops rewriting the whole selection index
8
+
9
+ Every run after the first reads the selection index, lays its own recording over
10
+ it, and writes it back. At a repository's scale almost all of that was spent
11
+ making objects nobody reads: a run that re-records ten modules of twenty thousand
12
+ decoded six hundred thousand regions into a model, merged ten of them, and
13
+ encoded the model back — the other 99.95% of the index materialized and
14
+ re-serialized to arrive at the bytes it was read from.
15
+
16
+ `layerTestCoverage` does the merge and the encode as one pass over the columns
17
+ the previous snapshot is already stored in. A module the run did not touch is
18
+ never made an object: its rows are copied column to column as integers, its
19
+ strings blob to blob as bytes, and the only thing that happens to either is the
20
+ renumbering the new dictionary implies. Objects are made for what the merge has
21
+ to reason about — the tests, the modules this run re-recorded, and the carried
22
+ modules whose text moved on disk. Over twenty thousand modules that is 1855 ms to
23
+ 616, and it is byte for byte the same file, which a gate asserts across every
24
+ case the merge distinguishes.
25
+
26
+ The columns are now zstd rather than brotli, at two levels, because the runs are
27
+ two kinds of data. A varint run is a dense stream of small integers and answers
28
+ to a long search; a run of the string blob is file paths and hex digests, which
29
+ zstd finds most of at level 1 and nothing more of above it. Against the brotli
30
+ quality 4 it replaces, over the same snapshot: 319 ms to compress became 120, and
31
+ the file got 86 KB smaller.
32
+
33
+ `zlib.zstdCompressSync` arrived in Node 22.15, so that is the floor these
34
+ packages declare. The snapshot's layout version moved with the codec, which means
35
+ an index written by an earlier build is refused at its header and rebuilt — one
36
+ full run, and nothing a reader has to think about.
37
+
38
+ ### Patch Changes
39
+
40
+ - e546e21: Where the JSX settings live under Vite 8
41
+
42
+ Vite 8 transforms with oxc, so `esbuild: { jsx, jsxDev, jsxImportSource }` becomes
43
+ `oxc: { jsx: { runtime, development, importSource } }`, and `esbuild.keepNames`
44
+ becomes `build.rolldownOptions.output.keepNames`. A config keeps whichever keys it
45
+ is given and reads only the ones its own major knows, so the wrong block is not an
46
+ error, not a warning and not a log line: the plugin installs, the bundle runs,
47
+ every subject renders, and every report names the line a component is declared on
48
+ instead of the line that wrote the element. It fails in the direction that looks
49
+ like it worked.
50
+
51
+ The `jsx-source` README carries a table of where the two settings live per
52
+ transform, both spellings on every copyable snippet, and the reminder to read
53
+ `provenanceOf`'s result rather than the config — the config cannot tell you.
54
+ `storybook-collector` gets the same for `keepNames`, on the symptom it produces: a
55
+ confident report naming a component that appears nowhere in your source.
56
+ - Updated dependencies [e546e21]
57
+ - Updated dependencies [8efba76]
58
+ - Updated dependencies [e546e21]
59
+ - Updated dependencies [d50e020]
60
+ - Updated dependencies [f4b328b]
61
+ - Updated dependencies [e546e21]
62
+ - Updated dependencies [e546e21]
63
+ - Updated dependencies [f4b328b]
64
+ - Updated dependencies [0718464]
65
+ - Updated dependencies [0718464]
66
+ - Updated dependencies [0718464]
67
+ - Updated dependencies [e546e21]
68
+ - Updated dependencies [e546e21]
69
+ - Updated dependencies [0718464]
70
+ - @variance-authority/core@0.2.0
71
+ - @variance-authority/sense@0.2.0
72
+ - @variance-authority/playwright@0.2.0
73
+ - @variance-authority/dom@0.2.0
74
+ - @variance-authority/react@0.2.0
75
+ - @variance-authority/storybook@0.2.0
76
+
77
+ ## 0.1.1
78
+
79
+ ### Patch Changes
80
+
81
+ - db08866: Keep the run when the execution journal cannot be recorded.
82
+
83
+ `close()` already held the position that a journal is not the artefact under
84
+ review: a recorder that declines to record is written to stderr and the run
85
+ continues, because failing the run over it would cost every subject in it the
86
+ baselines it just captured. Only the declining half was handled. A throw out of
87
+ the same call — `duplicate test coverage observation` was the one reached in
88
+ practice — went straight up through `close()` and took the run down with it.
89
+ Both outcomes now end the same way, in a sentence on stderr and a run that keeps
90
+ its images.
91
+ - Updated dependencies [fe578c8]
92
+ - @variance-authority/sense@0.1.1
93
+ - @variance-authority/core@0.1.1
94
+ - @variance-authority/dom@0.1.1
95
+ - @variance-authority/playwright@0.1.1
96
+ - @variance-authority/react@0.1.1
97
+ - @variance-authority/storybook@0.1.1
98
+
3
99
  ## 0.1.0
4
100
 
5
101
  ### Patch Changes
package/README.md CHANGED
@@ -56,26 +56,24 @@ still name components, but it cannot point to their declarations.
56
56
 
57
57
  Against a built Storybook those names arrive minified — a run reports the cause
58
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.
59
+ remembers otherwise. Preserve component names when the report must use them.
60
+
61
+ That scan names where a component is *declared* — one line however many times
62
+ that component is rendered.
63
+
64
+ For the line the changed element is actually written on, a **development**
65
+ Storybook needs no instrumentation. A **built** Storybook can opt into that
66
+ extra precision with `@variance-authority/jsx-source`; collection, comparison
67
+ and declaration-level attribution work without it. The build must emit
68
+ automatic development JSX, and the plugin belongs in `viteFinal`:
74
69
 
75
70
  ```bash
76
71
  npm install --save-dev @variance-authority/jsx-source
77
72
  ```
78
73
 
74
+ See [`@variance-authority/jsx-source`](../jsx-source) for the Vite configuration
75
+ and for builds that already use Emotion, theme-ui or another custom JSX runtime.
76
+
79
77
  ### 3. Point the CLI at the Storybook index and collector
80
78
 
81
79
  In `variance.config.json`, the index is the built file and `collector` is the
@@ -107,20 +105,19 @@ npx variance run --config variance.config.json
107
105
 
108
106
  Each run writes `.variance/report.json`, the artifact everything else reads.
109
107
  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
108
+ .variance/report.html` and open the result in a browser to see the before, diff and
109
+ after images per changed story, grouped by cause rather than by story. The page
112
110
  references its images relatively, so it belongs beside the `report` path this
113
- config declares; written anywhere else it shows broken images.
111
+ config declares; written anywhere else, it shows broken images.
114
112
 
115
113
  The first successful run exits `1` and reports each story as `new`; a
116
- baseline — the stored snapshot a subject is compared against — nobody
114
+ baseline — the stored snapshot a subject is compared against — that nobody
117
115
  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.
116
+ and run again. The next unchanged run exits `0` without repainting the subjects
117
+ whose stored document digest already proves they did not change.
121
118
 
122
- The executable `storybook-case` demonstrates the
123
- complete cycle against a Storybook-built artifact: new → accept → unchanged → a
119
+ The executable `storybook-case` demonstrates the complete cycle against a
120
+ Storybook-built artifact: new → accept → unchanged → a
124
121
  `Button` edit changing exactly the five stories that render it.
125
122
 
126
123
  ## Options
@@ -135,6 +132,9 @@ complete cycle against a Storybook-built artifact: new → accept → unchanged
135
132
  | `baseUrl` | Storybook is already running. | Omitted; the directory containing `subjects.index` is served on loopback for the run. |
136
133
  | `headless` | You need to watch collection while debugging. | `true`; set `false` locally. |
137
134
  | `network` | Asset bytes at stable URLs must participate in render identity. | `true`; set `false` only when asset URLs are already content-addressed. |
135
+ | `hashAssets` | Your asset URLs already carry 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. |
136
+ | `wiring` | Your preview's renderer is not React, so the fiber walk buys an absent band. | `true`. A band of its own; turning it off changes no stored digest. |
137
+ | `holdings` | Application values behind the nodes are evidence you want carried. | `false`. Changes `structureHash` — an inert wrapper survives the collapse — so both sides of a comparison must be read the same way. |
138
138
  | `roots` | Your preview mounts somewhere other than the standard roots. | `['#storybook-root', '#root']`, tightest match first. |
139
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. |
140
140
 
@@ -146,8 +146,8 @@ source and page-agent shapes.
146
146
 
147
147
  `tests` accepts `true` or `StoryExecutionOptions`: `root` is the repository root
148
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
149
+ `testSelectionProbes()` was given, and `cacheRoot` and `coverageFile` override
150
+ the repository-keyed cache paths for the block records and the coverage index. A
151
151
  story is its own owner in the recorded index — Storybook is an execution surface
152
152
  this tool drives one subject at a time — and a story that did not render still
153
153
  contributes its crossings while never justifying a later skip.
@@ -171,9 +171,8 @@ has already fired — the story function returned. A story still showing a
171
171
  fallback when the wait runs out is reported as **not collected**, naming the open
172
172
  boundaries and the components that wrote them, rather than recorded as a
173
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.
174
+ difference no one authored, and every band — the severity category a change is filed
175
+ under, from accessibility down to sub-pixel noise — agrees with both.
177
176
 
178
177
  Declare the exception when the fallback is the subject:
179
178
 
@@ -199,16 +198,19 @@ the other side.
199
198
  resolved. Fix what it awaits, or add the story to `loading` if the fallback is
200
199
  what you intend to review.
201
200
  - **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.
201
+ Production minification must also preserve component function names. Vite 8
202
+ spells that `build.rolldownOptions.output.keepNames: true`; Vite 7 and below,
203
+ including the worked case, spell it `esbuild.keepNames: true`. A config
204
+ carrying the other major's key is read by nothing and warns about nothing, so
205
+ the symptom is a confident report naming a component that appears nowhere in
206
+ your source.
204
207
  - **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.
208
+ this is the expected fallback for an uninstrumented built Storybook. Install
209
+ `jsx-source` only when the report must distinguish the exact element instance,
210
+ and make that build emit automatic development JSX. Against a development
211
+ Storybook nothing normally needs installing; missing lines there mean React
212
+ is not its development build, the dev server emits no source maps, or React 18
213
+ is still compiled to classic `createElement` calls.
212
214
  - **Images change without a document change:** leave `network` enabled so asset
213
215
  response bodies participate in the environment key. Disable it only when the
214
216
  URL already identifies the bytes.
@@ -1,4 +1,5 @@
1
- import type { CallSiteResolver, RenderDocument, SemanticSnapshot, SourceIndex, SubjectRef, Viewport } from '@variance-authority/core';
1
+ import type { CallSiteResolver, SourceIndex } from '@variance-authority/core/attribute';
2
+ import type { RenderDocument, SemanticSnapshot, SubjectRef, Viewport } from '@variance-authority/core/format';
2
3
  /**
3
4
  * The contract a collector satisfies, restated here rather than imported.
4
5
  *
@@ -130,6 +131,27 @@ export interface Collector {
130
131
  * this bounded, and a suite's subjects share their modules.
131
132
  */
132
133
  readonly callSites?: CallSiteResolver;
134
+ /**
135
+ * Collect this subject again, in a world nothing else has touched.
136
+ *
137
+ * The saving this whole tool is built on is that the world is *not* rebuilt
138
+ * between subjects (ADR-0009): one browser, one page, one Storybook, for the
139
+ * length of a run. What it buys is the possibility that story B renders
140
+ * differently because story A ran first — and a comparison cannot tell that
141
+ * apart from a regression, because both arrive as "the pixels moved".
142
+ *
143
+ * One clean collection settles it. If the difference is gone with nothing else
144
+ * in the world, the baseline was right and the session moved this subject, and
145
+ * the run reports `order-dependent` rather than asking anybody to accept it.
146
+ *
147
+ * Optional in the contract the CLI declares, because a collector holding a
148
+ * single page open has no clean world to offer and the run says so rather than
149
+ * reading silence as "nothing leaked". This collector implements it: a
150
+ * Storybook preview can be opened twice, and the second one costs a browser
151
+ * launch — which is why the run only spends it on subjects it already called
152
+ * `changed`, up to `alone.limit`.
153
+ */
154
+ collectAlone?(subject: PlannedSubject): Promise<Collected>;
133
155
  close(): Promise<void>;
134
156
  }
135
157
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n CallSiteResolver,\n RenderDocument,\n SemanticSnapshot,\n SourceIndex,\n SubjectRef,\n Viewport,\n} from '@variance-authority/core';\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/** The subset of a run's configuration this collector reads. */\nexport interface CollectorConfig {\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 only per-story declaration that survives a\n * build — 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 *\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 makes a\n * differently-stabilized baseline `incomparable`; this is the half a\n * person reads.\n */\n readonly stabilization?: readonly string[];\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 close(): 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 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/** The subset of a run's configuration this collector reads. */\nexport interface CollectorConfig {\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 only per-story declaration that survives a\n * build — 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 *\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 makes a\n * differently-stabilized baseline `incomparable`; this is the half a\n * person reads.\n */\n readonly stabilization?: readonly string[];\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 close(): Promise<void>;\n}\n"]}
@@ -19,8 +19,8 @@ export interface StoryExecutionOptions {
19
19
  readonly root?: string;
20
20
  /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */
21
21
  readonly label?: string;
22
- /** Block inventory the build wrote. Defaults to the label's repository-keyed cache. */
23
- readonly modulesFile?: string;
22
+ /** Where the build wrote its block records. Defaults to the user cache. */
23
+ readonly cacheRoot?: string;
24
24
  /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */
25
25
  readonly coverageFile?: string;
26
26
  }
package/dist/execution.js CHANGED
@@ -49,13 +49,30 @@ export async function createStoryRecorder(index, options) {
49
49
  // a visual run: losing it costs the *next* selection its narrowing, which
50
50
  // is a full suite — the safe direction — while failing the run over it
51
51
  // would cost this one its baselines, which is not.
52
- const record = await recordExecution({
53
- root,
54
- subjects: observed,
55
- ...(options.label === undefined ? {} : { label: options.label }),
56
- ...(options.modulesFile === undefined ? {} : { modulesFile: options.modulesFile }),
57
- ...(options.coverageFile === undefined ? {} : { coverageFile: options.coverageFile }),
58
- });
52
+ //
53
+ // That holds for a throw as much as for a refusal. The recorder reports
54
+ // what it declined in `because`, but it is a file format, a lock and a
55
+ // filesystem underneath, and any of them can raise something it has no
56
+ // sentence for. Letting that escape ends the run *after* every subject
57
+ // has been rendered and compared — the most expensive possible moment to
58
+ // lose a set of baselines, and over the half of the work that was only
59
+ // ever an addition.
60
+ let record;
61
+ try {
62
+ record = await recordExecution({
63
+ root,
64
+ subjects: observed,
65
+ ...(options.label === undefined ? {} : { label: options.label }),
66
+ ...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),
67
+ ...(options.coverageFile === undefined ? {} : { coverageFile: options.coverageFile }),
68
+ });
69
+ }
70
+ catch (error) {
71
+ process.stderr.write('variance-authority: recorded no story execution — the recorder failed with ' +
72
+ `${error instanceof Error ? error.message : String(error)}; this run is ` +
73
+ 'unaffected and the next `--since` will run everything\n');
74
+ return;
75
+ }
59
76
  if (!record.recorded) {
60
77
  process.stderr.write(`variance-authority: recorded no story execution — ${record.because}\n`);
61
78
  }
@@ -1 +1 @@
1
- {"version":3,"file":"execution.js","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each story executed, recorded for the next run's selection.\n *\n * The instrument is the one `@variance-authority/sense` already uses; only the\n * transport differs, which is the rule\n * [spec 0028](../../../docs/specs/0028-the-instrument.md) states. The preview is\n * built with `testSelectionProbes()`, the page counts block crossings, and this\n * closes one window per story and joins them at the end of the run.\n *\n * A story is its own owner. Storybook is an execution surface this tool drives\n * one subject at a time, so — unlike a test runner, where the file is the\n * smallest thing a runner can be asked to execute — the crossings of one story\n * belong to that story and to nothing else.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport {\n drainExecution,\n preconditionOf,\n recordExecution,\n type EvaluatingPage,\n type ObservedSubject,\n} from '@variance-authority/sense/journal';\nimport { parseStoryIndex } from '@variance-authority/storybook';\nimport { resolve } from 'node:path';\n\n/** Where a story run's execution evidence is joined, read and kept. */\nexport interface StoryExecutionOptions {\n /** Repository root the recorded paths are relative to. Defaults to the cwd. */\n readonly root?: string;\n /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */\n readonly label?: string;\n /** Block inventory the build wrote. Defaults to the label's repository-keyed cache. */\n readonly modulesFile?: string;\n /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */\n readonly coverageFile?: string;\n}\n\n/** One run's accumulation: a window per story, written once at the end. */\nexport interface StoryRecorder {\n /**\n * Close one story's window.\n *\n * `complete` is false for a story this run refused. A refused story still\n * executed code, so its crossings are kept — what it may never do is justify\n * excluding itself from a later run.\n */\n readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean) => Promise<void>;\n /** Merge the run into the index, or say on stderr why it could not. */\n readonly close: () => Promise<void>;\n}\n\nexport async function createStoryRecorder(\n index: string,\n options: StoryExecutionOptions,\n): Promise<StoryRecorder> {\n const root = resolve(options.root ?? process.cwd());\n const storyFiles = await storyFilesFrom(index);\n const observed: ObservedSubject[] = [];\n let seen = false;\n\n return {\n note: async (page, subjectId, complete) => {\n const journal = await drainExecution(page);\n if (journal === undefined) return;\n seen = true;\n const storyId = subjectId.replace(/^story:/, '');\n const file = storyFiles.get(storyId);\n const precondition = file === undefined ? undefined : await preconditionOf(root, file);\n observed.push({\n owner: subjectId,\n journal,\n complete,\n ...(precondition === undefined ? {} : { preconditions: [precondition] }),\n });\n },\n\n close: async () => {\n if (!seen) {\n process.stderr.write(\n 'variance-authority: `tests` is on and the Storybook preview has no execution ' +\n 'collector in it — build it with `testSelectionProbes()` from ' +\n '`@variance-authority/sense/journal`, or the next `--since` will run ' +\n 'everything\\n',\n );\n return;\n }\n // A refusal is written and the run continues. Recording is an addition to\n // a visual run: losing it costs the *next* selection its narrowing, which\n // is a full suite — the safe direction — while failing the run over it\n // would cost this one its baselines, which is not.\n const record = await recordExecution({\n root,\n subjects: observed,\n ...(options.label === undefined ? {} : { label: options.label }),\n ...(options.modulesFile === undefined ? {} : { modulesFile: options.modulesFile }),\n ...(options.coverageFile === undefined ? {} : { coverageFile: options.coverageFile }),\n });\n if (!record.recorded) {\n process.stderr.write(\n `variance-authority: recorded no story execution — ${record.because}\\n`,\n );\n }\n },\n };\n}\n\n/**\n * Story id to the file that declares it, from the index the run is driving.\n *\n * The one route from a subject back to a file, and the reason a story's own\n * `.stories` file can select it: nothing *enters* a story file the way execution\n * enters a module, so without this a commit that edits one selects nothing.\n */\nasync function storyFilesFrom(index: string): Promise<Map<string, string>> {\n const parsed = parseStoryIndex(JSON.parse(await readFile(index, 'utf8')), index);\n return new Map(parsed.stories.map((story) => [story.id, story.importPath]));\n}\n"]}
1
+ {"version":3,"file":"execution.js","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each story executed, recorded for the next run's selection.\n *\n * The instrument is the one `@variance-authority/sense` already uses; only the\n * transport differs, which is the rule\n * [spec 0028](../../../docs/specs/0028-the-instrument.md) states. The preview is\n * built with `testSelectionProbes()`, the page counts block crossings, and this\n * closes one window per story and joins them at the end of the run.\n *\n * A story is its own owner. Storybook is an execution surface this tool drives\n * one subject at a time, so — unlike a test runner, where the file is the\n * smallest thing a runner can be asked to execute — the crossings of one story\n * belong to that story and to nothing else.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport {\n drainExecution,\n preconditionOf,\n recordExecution,\n type EvaluatingPage,\n type ObservedSubject,\n} from '@variance-authority/sense/journal';\nimport { parseStoryIndex } from '@variance-authority/storybook';\nimport { resolve } from 'node:path';\n\n/** Where a story run's execution evidence is joined, read and kept. */\nexport interface StoryExecutionOptions {\n /** Repository root the recorded paths are relative to. Defaults to the cwd. */\n readonly root?: string;\n /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */\n readonly label?: string;\n /** Where the build wrote its block records. Defaults to the user cache. */\n readonly cacheRoot?: string;\n /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */\n readonly coverageFile?: string;\n}\n\n/** One run's accumulation: a window per story, written once at the end. */\nexport interface StoryRecorder {\n /**\n * Close one story's window.\n *\n * `complete` is false for a story this run refused. A refused story still\n * executed code, so its crossings are kept — what it may never do is justify\n * excluding itself from a later run.\n */\n readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean) => Promise<void>;\n /** Merge the run into the index, or say on stderr why it could not. */\n readonly close: () => Promise<void>;\n}\n\nexport async function createStoryRecorder(\n index: string,\n options: StoryExecutionOptions,\n): Promise<StoryRecorder> {\n const root = resolve(options.root ?? process.cwd());\n const storyFiles = await storyFilesFrom(index);\n const observed: ObservedSubject[] = [];\n let seen = false;\n\n return {\n note: async (page, subjectId, complete) => {\n const journal = await drainExecution(page);\n if (journal === undefined) return;\n seen = true;\n const storyId = subjectId.replace(/^story:/, '');\n const file = storyFiles.get(storyId);\n const precondition = file === undefined ? undefined : await preconditionOf(root, file);\n observed.push({\n owner: subjectId,\n journal,\n complete,\n ...(precondition === undefined ? {} : { preconditions: [precondition] }),\n });\n },\n\n close: async () => {\n if (!seen) {\n process.stderr.write(\n 'variance-authority: `tests` is on and the Storybook preview has no execution ' +\n 'collector in it — build it with `testSelectionProbes()` from ' +\n '`@variance-authority/sense/journal`, or the next `--since` will run ' +\n 'everything\\n',\n );\n return;\n }\n // A refusal is written and the run continues. Recording is an addition to\n // a visual run: losing it costs the *next* selection its narrowing, which\n // is a full suite — the safe direction — while failing the run over it\n // would cost this one its baselines, which is not.\n //\n // That holds for a throw as much as for a refusal. The recorder reports\n // what it declined in `because`, but it is a file format, a lock and a\n // filesystem underneath, and any of them can raise something it has no\n // sentence for. Letting that escape ends the run *after* every subject\n // has been rendered and compared — the most expensive possible moment to\n // lose a set of baselines, and over the half of the work that was only\n // ever an addition.\n let record: Awaited<ReturnType<typeof recordExecution>>;\n try {\n record = await recordExecution({\n root,\n subjects: observed,\n ...(options.label === undefined ? {} : { label: options.label }),\n ...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),\n ...(options.coverageFile === undefined ? {} : { coverageFile: options.coverageFile }),\n });\n } catch (error) {\n process.stderr.write(\n 'variance-authority: recorded no story execution — the recorder failed with ' +\n `${error instanceof Error ? error.message : String(error)}; this run is ` +\n 'unaffected and the next `--since` will run everything\\n',\n );\n return;\n }\n if (!record.recorded) {\n process.stderr.write(\n `variance-authority: recorded no story execution — ${record.because}\\n`,\n );\n }\n },\n };\n}\n\n/**\n * Story id to the file that declares it, from the index the run is driving.\n *\n * The one route from a subject back to a file, and the reason a story's own\n * `.stories` file can select it: nothing *enters* a story file the way execution\n * enters a module, so without this a commit that edits one selects nothing.\n */\nasync function storyFilesFrom(index: string): Promise<Map<string, string>> {\n const parsed = parseStoryIndex(JSON.parse(await readFile(index, 'utf8')), index);\n return new Map(parsed.stories.map((story) => [story.id, story.importPath]));\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,137 +1,6 @@
1
- import { type StoryExecutionOptions } from './execution.js';
1
+ import type { StorybookCollectorOptions } from './options.js';
2
2
  export type { Collected, Collector, CollectorConfig, CollectorContext, Plan, PlannedSubject, } from './contract.js';
3
3
  import type { Collector, CollectorContext } from './contract.js';
4
- import { type SourceScan } from './source.js';
5
- /**
6
- * The Storybook collector, shipped.
7
- *
8
- * ## Why this package exists, stated as the thing it retracts
9
- *
10
- * This repository's position was that mounting is the adopter's, once, per
11
- * project — and it measured the cost honestly at 341 lines rather than the
12
- * "about thirty" the source had guessed. That position is right about React
13
- * applications and wrong about Storybook, for a reason the measurement itself
14
- * shows: of those 341 lines, the parts that were genuinely *this project's* were
15
- * a map of three ready selectors, a directory to scan for components, and a
16
- * static server. Everything else was Storybook's own contract being re-typed.
17
- *
18
- * A story index is a documented artifact. A preview owns its own mount and
19
- * exposes a channel for switching stories. So for this one subject source the
20
- * mounting problem is already solved by somebody else, and asking every adopter
21
- * to re-solve it was charging them for our boundary rather than for their
22
- * project.
23
- *
24
- * **What stays theirs.** Readiness per story, because Storybook's `storyRendered`
25
- * fires when the story function returns — which for a component that defers work
26
- * is *before the component exists*. That is knowledge about a component, and a
27
- * project-wide default would be a guess about all of them.
28
- *
29
- * ## What it does not make generic
30
- *
31
- * Nothing outside Storybook. A Playwright suite, a route table or a bespoke
32
- * mount is still a collector somebody writes — see
33
- * [`@variance-authority/playwright-test`](../playwright-test) for the surface
34
- * where the adopter's own test body plays that part instead.
35
- */
36
- export interface StorybookCollectorOptions {
37
- /**
38
- * Story id to the selector that says it is ready.
39
- *
40
- * Per story, not per project, and that is the design rather than an
41
- * ergonomics gap. A button needs no marker and demanding one from it would
42
- * time out every story to solve a problem one of them has. A story that
43
- * declares a marker and never attaches it times out saying which selector it
44
- * waited for — there is no fallback, because falling back is how you
45
- * photograph a spinner and call it a component.
46
- */
47
- readonly ready?: Readonly<Record<string, string>>;
48
- /**
49
- * Where the components live, for `file:line` attribution.
50
- *
51
- * Omitted, the report names components and no files — which is still ahead of
52
- * every product in the category and is not what this is for.
53
- */
54
- readonly source?: SourceScan;
55
- /**
56
- * A Storybook that is already served, e.g. `http://localhost:6006`.
57
- *
58
- * Preferred when it exists: then nothing here has an opinion about how the
59
- * build is hosted. Omitted, the directory holding `subjects.index` is served
60
- * on a loopback port for the life of the run.
61
- */
62
- readonly baseUrl?: string;
63
- /** Defaults to `true`. Set false to watch a run by hand. */
64
- readonly headless?: boolean;
65
- /**
66
- * Watch the wire: hash asset bodies into the environment key, and serve
67
- * animated GIFs as their first frame. Defaults to `true`.
68
- *
69
- * On, this is what closes a false `unchanged` that a page cannot see about
70
- * itself. A logo re-exported at the same URL is the same markup, the same CSS
71
- * and the same document — so every tier settles, and the run reports that
72
- * nothing moved while the image on the page is different bytes. Only the party
73
- * that saw the response knows otherwise.
74
- *
75
- * The assets are narrowed **per story** before they reach a key, from the URLs
76
- * that story's own subtree references (`assetsFor`). Without that, a run that
77
- * reads three hundred stories out of one page would give story 200 a key that
78
- * depends on which stories ran before it, and sharding the suite would change
79
- * every baseline's identity.
80
- *
81
- * Off is a position for a build whose asset URLs already contain their own
82
- * content hash: the URL is then the identity, and hashing the bytes again buys
83
- * a read and nothing else.
84
- */
85
- readonly network?: boolean;
86
- /** Overrides the roots the story is read from. Tightest first. */
87
- readonly roots?: readonly string[];
88
- /** Milliseconds for Storybook or a declared marker to report readiness. Defaults to 15000. */
89
- readonly readyTimeoutMs?: number;
90
- /**
91
- * Milliseconds to wait for a story's Suspense boundaries. Defaults to 5000.
92
- *
93
- * Paid only by stories that are actually waiting: a subtree with no boundary
94
- * in it returns on the first read. `0` turns the wait off and keeps the
95
- * reading, which is a position for a project whose readiness markers already
96
- * cover its data — the refusal below still fires, so the boundary is reported
97
- * rather than photographed.
98
- */
99
- readonly suspenseTimeoutMs?: number;
100
- /**
101
- * Record what each story executed, into the shared test-selection index.
102
- *
103
- * Off unless asked for, and it asks something of the build rather than of this
104
- * package: the Storybook preview has to have been built with
105
- * `testSelectionProbes()` from `@variance-authority/sense/journal`, which is
106
- * what puts probes in the source and writes down what their ordinals mean.
107
- * Without it a run records nothing and says so on stderr — the next selection
108
- * then runs everything, which is the direction every uncertainty here
109
- * resolves.
110
- *
111
- * A story is its own owner in that index. Storybook is an execution surface
112
- * this tool drives one subject at a time, so unlike a test runner — where the
113
- * file is the smallest thing a runner can be asked to execute — the crossings
114
- * of one story belong to that story and to nothing else.
115
- */
116
- readonly tests?: boolean | StoryExecutionOptions;
117
- /**
118
- * Stories whose *loading* state is the subject, as id globs.
119
- *
120
- * The escape hatch, and the only one. A story left showing its fallback is
121
- * otherwise refused, because a subject that records a skeleton on a slow
122
- * machine and a component on a fast one is a flake nobody wrote — so a
123
- * skeleton somebody *does* want a baseline over has to be said out loud.
124
- *
125
- * Declared here rather than sensed, and checked in both directions: a story
126
- * named by this that turns out to settle is refused too. A declaration that
127
- * outlived its subject is the same nondeterminism arriving from the other side.
128
- *
129
- * Matched against the story id and the subject id both, so
130
- * `case-surface--feed` and `story:case-surface--feed` name the same story —
131
- * `ready` above is keyed by the first.
132
- */
133
- readonly loading?: readonly string[];
134
- }
135
4
  /**
136
5
  * Build the module a `subjects.collector` path should default-export.
137
6
  *
@@ -149,6 +18,7 @@ export interface StorybookCollectorOptions {
149
18
  * ```
150
19
  */
151
20
  export declare function storybookCollector(options?: StorybookCollectorOptions): (context: CollectorContext) => Promise<Collector>;
21
+ export type { StorybookCollectorOptions } from './options.js';
152
22
  export type { StoryExecutionOptions } from './execution.js';
153
23
  export type { SourceScan } from './source.js';
154
24
  export type { AcquireRequest, Acquired } from './page-agent.js';