@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 +20 -0
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/contract.d.ts +135 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/execution.d.ts +41 -0
- package/dist/execution.js +76 -0
- package/dist/execution.js.map +1 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/operator.d.ts +17 -0
- package/dist/operator.js +19 -0
- package/dist/operator.js.map +1 -0
- package/dist/page-agent-entry.d.ts +2 -0
- package/dist/page-agent-entry.js +32 -0
- package/dist/page-agent-entry.js.map +1 -0
- package/dist/page-agent.bundle.js +3996 -0
- package/dist/page-agent.d.ts +96 -0
- package/dist/page-agent.js +60 -0
- package/dist/page-agent.js.map +1 -0
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +66 -0
- package/dist/serve.js.map +1 -0
- package/dist/source.d.ts +25 -0
- package/dist/source.js +43 -0
- package/dist/source.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +50 -0
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
|
package/dist/contract.js
ADDED
|
@@ -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"]}
|