@variance-authority/storybook-collector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # @variance-authority/storybook-collector
2
+
3
+ ## 0.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [9587133]
8
+ - Updated dependencies [f800342]
9
+ - Updated dependencies [26ae9ed]
10
+ - Updated dependencies [f09528d]
11
+ - Updated dependencies [cfb333d]
12
+ - Updated dependencies [e8fee66]
13
+ - Updated dependencies [48d32eb]
14
+ - Updated dependencies [5c34e6d]
15
+ - @variance-authority/sense@0.1.0
16
+ - @variance-authority/dom@0.1.0
17
+ - @variance-authority/react@0.1.0
18
+ - @variance-authority/core@0.1.0
19
+ - @variance-authority/playwright@0.1.0
20
+ - @variance-authority/storybook@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Machine Garden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ <p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
2
+
3
+ # @variance-authority/storybook-collector
4
+
5
+ > Turn a built or already-served Storybook into Variance Authority subjects.
6
+
7
+ Turn a built or already-served Storybook into subjects that `variance run` can
8
+ observe — one subject per story, the unit a comparison runs against. Use this
9
+ package when Storybook already owns component mounting and you want
10
+ source-attributed visual, semantic, accessibility, and localization findings
11
+ without writing a browser harness.
12
+
13
+ This is the Storybook adapter, not the `variance` binary. Pair it with
14
+ `@variance-authority/cli`.
15
+
16
+ ```bash
17
+ npm install --save-dev @variance-authority/cli @variance-authority/storybook-collector
18
+ npx playwright install chromium
19
+ ```
20
+
21
+ The second command is there because Playwright's browser binaries do not arrive
22
+ with an `npm install`. The built `index.json` is the input, whether you build the
23
+ Storybook or already serve it; `.storybook` configuration is not read.
24
+
25
+ ## Integrate a Storybook
26
+
27
+ ### 1. Build the artifact you want to observe
28
+
29
+ The collector — the module you create in step 2 below — reads the artifact
30
+ Storybook produced, so run your ordinary Storybook build first. A development
31
+ server also works when you pass `baseUrl` below.
32
+
33
+ ### 2. Add a collector module
34
+
35
+ Create a module in your project and default-export the collector factory. The
36
+ only project-specific facts in the common path are where components live and
37
+ which stories need an explicit readiness marker.
38
+
39
+ ```js
40
+ // variance/storybook.mjs
41
+ import { storybookCollector } from '@variance-authority/storybook-collector';
42
+
43
+ export default storybookCollector({
44
+ ready: { 'checkout--deferred': '[data-testid="checkout-ready"]' },
45
+ source: { dirs: ['src'] },
46
+ });
47
+ ```
48
+
49
+ Omit `ready` for stories that are complete when Storybook emits
50
+ `storyRendered`. Add an entry only when a story continues mounting or fetching
51
+ after that signal. If the selector never appears, collection fails with the
52
+ story id and selector instead of silently capturing a spinner.
53
+
54
+ `source` enables component-to-`file:line` attribution. Without it the report can
55
+ still name components, but it cannot point to their declarations.
56
+
57
+ Against a built Storybook those names arrive minified — a run reports the cause
58
+ as `Ce`, not `Button`, because the bundler renamed it and nothing in the browser
59
+ remembers otherwise. `file:line` is the identifier that survives minification,
60
+ which is what makes the plugin below load-bearing there rather than an
61
+ enhancement.
62
+
63
+ It names where a component is *declared* — one line however many times that
64
+ component is rendered.
65
+
66
+ For the line the changed element is actually written on: against a
67
+ **development** Storybook, source locations work automatically — nothing to
68
+ configure. Against a **built, minified** Storybook it needs the
69
+ `@variance-authority/jsx-source` plugin in your `viteFinal` with
70
+ `esbuild.jsxDev` on; a report prefers the exact location wherever it comes
71
+ from, and this collector makes it repository-relative. The plugin does not
72
+ take `jsxImportSource`, so a Storybook already compiling against Emotion or
73
+ theme-ui keeps doing exactly that.
74
+
75
+ ```bash
76
+ npm install --save-dev @variance-authority/jsx-source
77
+ ```
78
+
79
+ ### 3. Point the CLI at the Storybook index and collector
80
+
81
+ In `variance.config.json`, the index is the built file and `collector` is the
82
+ module you created:
83
+
84
+ ```json
85
+ {
86
+ "project": "checkout-ui",
87
+ "profile": "chromium",
88
+ "viewport": { "width": 1280, "height": 800 },
89
+ "retention": "durable",
90
+ "subjects": {
91
+ "kind": "storybook",
92
+ "index": "storybook-static/index.json",
93
+ "collector": "variance/storybook.mjs"
94
+ },
95
+ "baselines": { "kind": "directory", "root": ".variance/baselines" },
96
+ "fonts": [],
97
+ "report": ".variance/report.json"
98
+ }
99
+ ```
100
+
101
+ ### 4. Check the machine, then run
102
+
103
+ ```bash
104
+ npx variance doctor --config variance.config.json
105
+ npx variance run --config variance.config.json
106
+ ```
107
+
108
+ Each run writes `.variance/report.json`, the artifact everything else reads.
109
+ Run `npx variance report --config variance.config.json --format html >
110
+ .variance/report.html` and open the result in a browser to see before/diff/after
111
+ images per changed story, grouped by cause rather than by story. The page
112
+ references its images relatively, so it belongs beside the `report` path this
113
+ config declares; written anywhere else it shows broken images.
114
+
115
+ The first successful run exits `1` and reports each story as `new`; a
116
+ baseline — the stored snapshot a subject is compared against — nobody
117
+ approved is not a pass. Review the candidates, accept the intended subjects,
118
+ and run again. The next unchanged run exits `0` without re-rendering
119
+ (*painting*) subjects whose stored document digest already proves they did
120
+ not move.
121
+
122
+ The executable `storybook-case` demonstrates the
123
+ complete cycle against a Storybook-built artifact: new → accept → unchanged → a
124
+ `Button` edit changing exactly the five stories that render it.
125
+
126
+ ## Options
127
+
128
+ | Option | Use it when | Default and boundary |
129
+ | --- | --- | --- |
130
+ | `ready` | A particular story finishes after Storybook's render signal. | No additional wait. Keys are Storybook story ids. |
131
+ | `readyTimeoutMs` | Storybook or a declared readiness marker legitimately needs longer to answer. | `15000`. A timeout is reported, never replaced by a fallback capture. |
132
+ | `loading` | A story's *fallback* is the state you intend to review. | Omitted. Id globs, matched against the story id and the subject id. |
133
+ | `suspenseTimeoutMs` | A story legitimately needs longer than five seconds to arrive. | `5000`. `0` skips the wait and keeps the reading. |
134
+ | `source` | Reports should resolve component names to `file:line`. | Omitted; component names remain available. An empty or mistyped scan is refused. |
135
+ | `baseUrl` | Storybook is already running. | Omitted; the directory containing `subjects.index` is served on loopback for the run. |
136
+ | `headless` | You need to watch collection while debugging. | `true`; set `false` locally. |
137
+ | `network` | Asset bytes at stable URLs must participate in render identity. | `true`; set `false` only when asset URLs are already content-addressed. |
138
+ | `roots` | Your preview mounts somewhere other than the standard roots. | `['#storybook-root', '#root']`, tightest match first. |
139
+ | `tests` | The next run should be able to skip stories whose code nothing touched. | `false`. Requires a preview built with `testSelectionProbes()` from `@variance-authority/sense/journal`; without a collector in the page the run says so on stderr and records nothing. |
140
+
141
+ The package exports `StorybookCollectorOptions` plus the collector contract
142
+ types (`Collector`, `CollectorContext`, `Plan`, `PlannedSubject`, and
143
+ `Collected`) for callers that need to wrap the factory without re-declaring its
144
+ boundary. `SourceScan`, `AcquireRequest`, and `Acquired` expose the corresponding
145
+ source and page-agent shapes.
146
+
147
+ `tests` accepts `true` or `StoryExecutionOptions`: `root` is the repository root
148
+ the recorded paths are relative to, `label` must match the one the preview's
149
+ `testSelectionProbes()` was given, and `modulesFile` and `coverageFile` override
150
+ the repository-keyed cache paths for the block inventory and the coverage index. A
151
+ story is its own owner in the recorded index — Storybook is an execution surface
152
+ this tool drives one subject at a time — and a story that did not render still
153
+ contributes its crossings while never justifying a later skip.
154
+
155
+ The collector reads a built Storybook index and switches stories over
156
+ Storybook's own preview channel, so a suite pays for one navigation rather than
157
+ one navigation per story. Addon chrome remains outside the selected story root
158
+ and does not enter the subject document.
159
+
160
+ ## Readiness and loading
161
+
162
+ Before each story is read, the collector waits for every React Suspense boundary
163
+ under the story root to settle. This runs first, ahead of stabilization — the
164
+ step that pins in-flight CSS animations and transitions to a settled frame
165
+ before anything is read — because content that arrives late brings its own
166
+ images and fonts.
167
+
168
+ `ready` cannot cover this case and no marker can: a component that suspends
169
+ renders no markup for a selector to attach to, and Storybook's `storyRendered`
170
+ has already fired — the story function returned. A story still showing a
171
+ fallback when the wait runs out is reported as **not collected**, naming the open
172
+ boundaries and the components that wrote them, rather than recorded as a
173
+ baseline. A skeleton on a slow machine and the component on a fast one is a
174
+ difference no one authored, and every band — the severity category, from
175
+ accessibility down to sub-pixel noise, a change is filed under — agrees with
176
+ both.
177
+
178
+ Declare the exception when the fallback is the subject:
179
+
180
+ ```js
181
+ export default storybookCollector({
182
+ loading: ['inbox--empty-state'],
183
+ source: { dirs: ['src'] },
184
+ });
185
+ ```
186
+
187
+ A declared story waits for nothing, and is refused if it turns out to have
188
+ settled — a declaration that outlived its subject is the same nondeterminism from
189
+ the other side.
190
+
191
+ ## When integration fails
192
+
193
+ - **The index cannot be read:** build Storybook first and check
194
+ `subjects.index`. The collector reads `index.json`; it does not discover a
195
+ `.storybook` directory.
196
+ - **A story times out:** verify the `ready` key is the Storybook story id and
197
+ that the selector is attached only after the intended UI is complete.
198
+ - **A story is refused as still waiting:** the named Suspense boundary never
199
+ resolved. Fix what it awaits, or add the story to `loading` if the fallback is
200
+ what you intend to review.
201
+ - **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.
204
+ - **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.
212
+ - **Images change without a document change:** leave `network` enabled so asset
213
+ response bodies participate in the environment key. Disable it only when the
214
+ URL already identifies the bytes.
215
+ - **The browser is missing:** run `npx playwright install chromium`, then
216
+ `variance doctor` again.
217
+
218
+ ## Boundaries
219
+
220
+ This package handles Storybook only. For pages your application already serves,
221
+ use `@variance-authority/route-collector`. For an existing
222
+ Playwright test, use
223
+ `@variance-authority/playwright-test`, where the test body
224
+ already performs navigation, mounting, and readiness.
@@ -0,0 +1,135 @@
1
+ import type { CallSiteResolver, RenderDocument, SemanticSnapshot, SourceIndex, SubjectRef, Viewport } from '@variance-authority/core';
2
+ /**
3
+ * The contract a collector satisfies, restated here rather than imported.
4
+ *
5
+ * The CLI declares the same shapes and this package does not depend on it — a
6
+ * surface package pulling in the binary would put a config parser, a docket and
7
+ * six commands into an adopter's dependency tree to describe four values
8
+ * (ADR-0024). What keeps the two copies honest is that the CLI imports *this*
9
+ * collector in its own tests, so a drift between them fails a build rather than
10
+ * a run.
11
+ */
12
+ /** The subset of a run's configuration this collector reads. */
13
+ export interface CollectorConfig {
14
+ readonly viewport: Viewport;
15
+ readonly fonts?: readonly string[];
16
+ /**
17
+ * Subtrees this project excludes, from `config.ignore` (spec 0024).
18
+ *
19
+ * Only the two fields a page needs: a rule id to record the mark under, and a
20
+ * selector to find it with. Everything else about a rule — its reason, its
21
+ * expiry, the bands it narrows to — is decided after collection, where a clock
22
+ * and the whole run's diffs are available and a browser is not.
23
+ */
24
+ readonly ignore?: readonly {
25
+ readonly id: string;
26
+ readonly select?: string;
27
+ /**
28
+ * Subjects the rule names. Read here to decide what to send, and never sent.
29
+ *
30
+ * A page can resolve a selector and cannot know which subject it is one of,
31
+ * so scoping has to happen on this side of the boundary. Without it a rule
32
+ * written for one route marks its element in every subject that renders the
33
+ * same shared layout, and a real regression inside that element is absorbed
34
+ * everywhere — an ignore silencing more than it says, which is the one thing
35
+ * the mechanism must not do.
36
+ */
37
+ readonly subjects?: readonly string[];
38
+ /** Tags the subject must carry. Read here to decide what to send, never sent. */
39
+ readonly tags?: readonly string[];
40
+ }[];
41
+ /**
42
+ * Images to serve as nothing, from `config.blank`.
43
+ *
44
+ * Handed to the driver rather than to the page, because it is enforced on the
45
+ * wire — before a byte is decoded, and therefore before the layout it would
46
+ * have participated in exists. Not scoped by subject, and it cannot be: a
47
+ * request carries no idea which subject will end up using it, which is the
48
+ * same reason the asset map is per page rather than per subject.
49
+ */
50
+ readonly blank?: readonly {
51
+ readonly id: string;
52
+ readonly url?: string;
53
+ readonly minPixels?: number;
54
+ readonly maxPixels?: number;
55
+ }[];
56
+ /**
57
+ * The subjects section of the config, as the operator wrote it.
58
+ *
59
+ * `index` is optional because it belongs to `subjects.kind: "storybook"`, and
60
+ * a config naming any other kind still reaches this collector — the kind is
61
+ * chosen in the config and the collector is chosen in the config, and nothing
62
+ * makes the operator pick a matching pair. Typed as always present, the first
63
+ * thing to read it hands `undefined` to `node:path` and the adopter gets a
64
+ * TypeError about an argument instead of a sentence naming the wrong word.
65
+ */
66
+ readonly subjects: {
67
+ readonly index?: string;
68
+ };
69
+ }
70
+ export interface PlannedSubject {
71
+ readonly subject: SubjectRef;
72
+ readonly viewport?: Viewport;
73
+ /**
74
+ * What the subject declares itself to be, from the artifact that produced it.
75
+ *
76
+ * Storybook's built index carries `tags` and does not carry a story's
77
+ * `parameters`, so a tag is the only per-story declaration that survives a
78
+ * build — and it is the right one anyway: what a subject *is* belongs in its
79
+ * own name, next to it, rather than in a central file repeating every id.
80
+ *
81
+ * Selection lives here; definition lives in the config. A tag is a word a
82
+ * story wears, and what that word *means* is the operator's to write down
83
+ * once, where a typo can be refused by name.
84
+ */
85
+ readonly tags?: readonly string[];
86
+ }
87
+ export interface Plan {
88
+ readonly subjects: readonly PlannedSubject[];
89
+ readonly notObserved: readonly unknown[];
90
+ readonly warnings: readonly string[];
91
+ }
92
+ export type Collected = {
93
+ readonly ok: true;
94
+ readonly document: RenderDocument;
95
+ readonly snapshot?: SemanticSnapshot;
96
+ readonly source?: SourceIndex;
97
+ /**
98
+ * Stabilization tricks applied to the page before this subject was read.
99
+ *
100
+ * Reported so a run can say what it did to somebody else's page. The
101
+ * digest of the same list is in the environment key, which is what makes a
102
+ * differently-stabilized baseline `incomparable`; this is the half a
103
+ * person reads.
104
+ */
105
+ readonly stabilization?: readonly string[];
106
+ } | {
107
+ readonly ok: false;
108
+ readonly because: string;
109
+ };
110
+ export interface CollectorContext {
111
+ readonly config: CollectorConfig;
112
+ /** Computed by the run from the story index. Returned unchanged. */
113
+ readonly plan?: Plan;
114
+ }
115
+ export interface Collector {
116
+ plan(): Promise<Plan>;
117
+ collect(subject: PlannedSubject): Promise<Collected>;
118
+ /**
119
+ * The frames a snapshot carries, spent on demand.
120
+ *
121
+ * Present when the collector drives a browser, because resolving a frame means
122
+ * fetching the module it names and the page is the only place that request is
123
+ * already correct. Absent otherwise, and absence is not a degradation: it means
124
+ * either that nothing captured frames, or that the run has no live page to
125
+ * fetch through — and both are a report without call sites rather than a
126
+ * report that is wrong.
127
+ *
128
+ * Handed to `locateSites` with the few nodes a region or a finding names. The
129
+ * resolver is per collector rather than per subject: the cache is what makes
130
+ * this bounded, and a suite's subjects share their modules.
131
+ */
132
+ readonly callSites?: CallSiteResolver;
133
+ close(): Promise<void>;
134
+ }
135
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What each story executed, recorded for the next run's selection.
3
+ *
4
+ * The instrument is the one `@variance-authority/sense` already uses; only the
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
8
+ * closes one window per story and joins them at the end of the run.
9
+ *
10
+ * A story is its own owner. Storybook is an execution surface this tool drives
11
+ * one subject at a time, so — unlike a test runner, where the file is the
12
+ * smallest thing a runner can be asked to execute — the crossings of one story
13
+ * belong to that story and to nothing else.
14
+ */
15
+ import { type EvaluatingPage } from '@variance-authority/sense/journal';
16
+ /** Where a story run's execution evidence is joined, read and kept. */
17
+ export interface StoryExecutionOptions {
18
+ /** Repository root the recorded paths are relative to. Defaults to the cwd. */
19
+ readonly root?: string;
20
+ /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */
21
+ readonly label?: string;
22
+ /** Block inventory the build wrote. Defaults to the label's repository-keyed cache. */
23
+ readonly modulesFile?: string;
24
+ /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */
25
+ readonly coverageFile?: string;
26
+ }
27
+ /** One run's accumulation: a window per story, written once at the end. */
28
+ export interface StoryRecorder {
29
+ /**
30
+ * Close one story's window.
31
+ *
32
+ * `complete` is false for a story this run refused. A refused story still
33
+ * executed code, so its crossings are kept — what it may never do is justify
34
+ * excluding itself from a later run.
35
+ */
36
+ readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean) => Promise<void>;
37
+ /** Merge the run into the index, or say on stderr why it could not. */
38
+ readonly close: () => Promise<void>;
39
+ }
40
+ export declare function createStoryRecorder(index: string, options: StoryExecutionOptions): Promise<StoryRecorder>;
41
+ //# sourceMappingURL=execution.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * What each story executed, recorded for the next run's selection.
3
+ *
4
+ * The instrument is the one `@variance-authority/sense` already uses; only the
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
8
+ * closes one window per story and joins them at the end of the run.
9
+ *
10
+ * A story is its own owner. Storybook is an execution surface this tool drives
11
+ * one subject at a time, so — unlike a test runner, where the file is the
12
+ * smallest thing a runner can be asked to execute — the crossings of one story
13
+ * belong to that story and to nothing else.
14
+ */
15
+ import { readFile } from 'node:fs/promises';
16
+ import { drainExecution, preconditionOf, recordExecution, } from '@variance-authority/sense/journal';
17
+ import { parseStoryIndex } from '@variance-authority/storybook';
18
+ import { resolve } from 'node:path';
19
+ export async function createStoryRecorder(index, options) {
20
+ const root = resolve(options.root ?? process.cwd());
21
+ const storyFiles = await storyFilesFrom(index);
22
+ const observed = [];
23
+ let seen = false;
24
+ return {
25
+ note: async (page, subjectId, complete) => {
26
+ const journal = await drainExecution(page);
27
+ if (journal === undefined)
28
+ return;
29
+ seen = true;
30
+ const storyId = subjectId.replace(/^story:/, '');
31
+ const file = storyFiles.get(storyId);
32
+ const precondition = file === undefined ? undefined : await preconditionOf(root, file);
33
+ observed.push({
34
+ owner: subjectId,
35
+ journal,
36
+ complete,
37
+ ...(precondition === undefined ? {} : { preconditions: [precondition] }),
38
+ });
39
+ },
40
+ close: async () => {
41
+ if (!seen) {
42
+ process.stderr.write('variance-authority: `tests` is on and the Storybook preview has no execution ' +
43
+ 'collector in it — build it with `testSelectionProbes()` from ' +
44
+ '`@variance-authority/sense/journal`, or the next `--since` will run ' +
45
+ 'everything\n');
46
+ return;
47
+ }
48
+ // A refusal is written and the run continues. Recording is an addition to
49
+ // a visual run: losing it costs the *next* selection its narrowing, which
50
+ // is a full suite — the safe direction — while failing the run over it
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
+ });
59
+ if (!record.recorded) {
60
+ process.stderr.write(`variance-authority: recorded no story execution — ${record.because}\n`);
61
+ }
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * Story id to the file that declares it, from the index the run is driving.
67
+ *
68
+ * The one route from a subject back to a file, and the reason a story's own
69
+ * `.stories` file can select it: nothing *enters* a story file the way execution
70
+ * enters a module, so without this a commit that edits one selects nothing.
71
+ */
72
+ async function storyFilesFrom(index) {
73
+ const parsed = parseStoryIndex(JSON.parse(await readFile(index, 'utf8')), index);
74
+ return new Map(parsed.stories.map((story) => [story.id, story.importPath]));
75
+ }
76
+ //# sourceMappingURL=execution.js.map
@@ -0,0 +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"]}