@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.
@@ -0,0 +1,155 @@
1
+ import { type StoryExecutionOptions } from './execution.js';
2
+ export type { Collected, Collector, CollectorConfig, CollectorContext, Plan, PlannedSubject, } from './contract.js';
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
+ /**
136
+ * Build the module a `subjects.collector` path should default-export.
137
+ *
138
+ * A factory rather than a plugin: the config still names a module, the module
139
+ * still exports a function, and the extension mechanism is unchanged. What the
140
+ * adopter writes shrinks to the two facts only they hold.
141
+ *
142
+ * ```js
143
+ * import { storybookCollector } from '@variance-authority/storybook-collector';
144
+ *
145
+ * export default storybookCollector({
146
+ * ready: { 'surface--deferred': '[data-testid="ready"]' },
147
+ * source: { dirs: ['src'] },
148
+ * });
149
+ * ```
150
+ */
151
+ export declare function storybookCollector(options?: StorybookCollectorOptions): (context: CollectorContext) => Promise<Collector>;
152
+ export type { StoryExecutionOptions } from './execution.js';
153
+ export type { SourceScan } from './source.js';
154
+ export type { AcquireRequest, Acquired } from './page-agent.js';
155
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,270 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { createCallSiteResolver, matchesGlob, normalize as normalizeCapture, } from '@variance-authority/core';
6
+ import { acquireFromAgent, createHarness, fetchModules, observeNetwork, unresizable, } from '@variance-authority/playwright';
7
+ import { suspenseRefusal } from '@variance-authority/react';
8
+ import { collectStory, harnessPage } from '@variance-authority/storybook';
9
+ import { createStoryRecorder } from './execution.js';
10
+ import { operatorError } from './operator.js';
11
+ /** Said in two places, because a mismatched kind can be caught at either. */
12
+ const WRONG_KIND = 'this collector expects `subjects.kind: "storybook"`, which is what supplies the plan';
13
+ import { serveStatic } from './serve.js';
14
+ import { scanSource } from './source.js';
15
+ const STORY_ROOTS = ['#storybook-root', '#root'];
16
+ /**
17
+ * Two candidate paths, because this module is imported from two places.
18
+ *
19
+ * A consumer imports `dist/index.js` and the bundle sits beside it. This
20
+ * repository's own tests import `src/index.ts`, where it does not — and a
21
+ * collector that only worked when nobody in the project tested it would be a
22
+ * collector nobody in the project tested.
23
+ */
24
+ async function pageAgentBundle() {
25
+ const candidates = [
26
+ new URL('./page-agent.bundle.js', import.meta.url),
27
+ new URL('../dist/page-agent.bundle.js', import.meta.url),
28
+ ];
29
+ for (const path of candidates) {
30
+ try {
31
+ return await readFile(path, 'utf8');
32
+ }
33
+ catch {
34
+ // Next candidate. The throw below names both, so a genuinely missing
35
+ // bundle still fails with somewhere to look rather than a bare ENOENT.
36
+ }
37
+ }
38
+ throw new Error(`the storybook page agent bundle is missing (looked in ` +
39
+ `${candidates.map((path) => fileURLToPath(path)).join(' and ')}); ` +
40
+ 'it is produced by this repository’s build, not at collection time');
41
+ }
42
+ /**
43
+ * Build the module a `subjects.collector` path should default-export.
44
+ *
45
+ * A factory rather than a plugin: the config still names a module, the module
46
+ * still exports a function, and the extension mechanism is unchanged. What the
47
+ * adopter writes shrinks to the two facts only they hold.
48
+ *
49
+ * ```js
50
+ * import { storybookCollector } from '@variance-authority/storybook-collector';
51
+ *
52
+ * export default storybookCollector({
53
+ * ready: { 'surface--deferred': '[data-testid="ready"]' },
54
+ * source: { dirs: ['src'] },
55
+ * });
56
+ * ```
57
+ */
58
+ export function storybookCollector(options = {}) {
59
+ return async function createCollector(context) {
60
+ const { config, plan } = context;
61
+ const roots = options.roots ?? STORY_ROOTS;
62
+ // Checked before anything reads it. `index` is what `subjects.kind:
63
+ // "storybook"` supplies, so a config naming another kind arrives here with
64
+ // nothing to serve — and the guard further down in `plan()` is too late,
65
+ // because this line runs first.
66
+ const index = config.subjects.index;
67
+ if (index === undefined)
68
+ throw operatorError(WRONG_KIND);
69
+ // The build the config named, not a sibling this file assumed. Serving the
70
+ // directory that holds the planned index is the only arrangement in which the
71
+ // stories driven and the stories planned are guaranteed to be the same ones.
72
+ const staticDir = dirname(index);
73
+ if (options.baseUrl === undefined && !existsSync(join(staticDir, 'index.json'))) {
74
+ throw operatorError(`${staticDir} has no index.json; build the Storybook first, or point ` +
75
+ '`baseUrl` at one that is already served');
76
+ }
77
+ const source = options.source === undefined ? undefined : scanSource(process.cwd(), options.source);
78
+ // What each story executed, if anybody asked. The index it feeds is the one
79
+ // the Vitest seam writes: same probe recipe, same block ordinals, one index
80
+ // across every origin.
81
+ const recorder = options.tests === undefined || options.tests === false
82
+ ? undefined
83
+ : await createStoryRecorder(index, options.tests === true ? {} : options.tests);
84
+ const bundle = await pageAgentBundle();
85
+ let served;
86
+ if (options.baseUrl === undefined)
87
+ served = await serveStatic(staticDir);
88
+ const baseUrl = options.baseUrl ?? served.baseUrl;
89
+ let harness;
90
+ // Installed in `prepare`, which runs before the first navigation: an
91
+ // observation that starts afterwards has already missed the assets the
92
+ // preview loaded on its way up, and a URL nobody saw is a hole in the key.
93
+ let network;
94
+ try {
95
+ // Pointed at the preview, so `collectStory` finds itself already there and
96
+ // does not navigate: one navigation for a whole run is the saving ADR-0009
97
+ // rests on, and it is easiest to keep by never taking a second one.
98
+ harness = await createHarness({
99
+ url: `${baseUrl}/iframe.html`,
100
+ bundle,
101
+ viewport: config.viewport,
102
+ ...(config.fonts !== undefined ? { fonts: config.fonts } : {}),
103
+ ...(options.headless !== undefined ? { headless: options.headless } : {}),
104
+ ...(options.network === false
105
+ ? {}
106
+ : {
107
+ prepare: async (page) => {
108
+ network = await observeNetwork(page, config.blank !== undefined ? { blank: config.blank } : {});
109
+ },
110
+ }),
111
+ });
112
+ }
113
+ catch (error) {
114
+ await served?.close();
115
+ throw error;
116
+ }
117
+ const page = harness.page;
118
+ const engine = harness.engine;
119
+ // One per run, not one per story. Every story in a Storybook is written in
120
+ // the same handful of modules, so the second story onward resolves its call
121
+ // sites out of this cache without a single fetch.
122
+ const callSites = createCallSiteResolver(fetchModules(page));
123
+ return {
124
+ async plan() {
125
+ if (plan === undefined) {
126
+ throw operatorError(WRONG_KIND);
127
+ }
128
+ return plan;
129
+ },
130
+ async collect(planned) {
131
+ const storyId = planned.subject.id.replace(/^story:/, '');
132
+ const viewport = planned.viewport ?? config.viewport;
133
+ const readySelector = options.ready?.[storyId];
134
+ // Applied, not merely recorded. A story that declares its own viewport
135
+ // was being painted at the run's width while its environment key said
136
+ // otherwise — a baseline whose key describes a render that never
137
+ // happened, and a verdict over it that is green for the wrong reason.
138
+ // The resize precedes the mount, so a component that reads `matchMedia`
139
+ // when it mounts reads the width it is about to be shown at.
140
+ const fixed = unresizable(viewport, config.viewport);
141
+ if (fixed !== undefined)
142
+ return { ok: false, because: fixed };
143
+ const current = page.viewportSize();
144
+ if (current?.width !== viewport.width || current?.height !== viewport.height) {
145
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
146
+ }
147
+ const outcome = await collectStory(harnessPage({ page }), storyId, {
148
+ baseUrl,
149
+ ...(options.readyTimeoutMs !== undefined ? { timeoutMs: options.readyTimeoutMs } : {}),
150
+ ...(readySelector !== undefined ? { readySelector } : {}),
151
+ });
152
+ // A story that did not render is a hole in this run's coverage, and it
153
+ // travels as a value rather than an exception: one component that throws
154
+ // must not cost the others their observations, and must not be silently
155
+ // absent either.
156
+ if (outcome.status !== 'rendered') {
157
+ // A story that rendered a fallback still executed code, and counters left
158
+ // in the page would be handed to whichever story drained next.
159
+ await recorder?.note(page, planned.subject.id, false);
160
+ return {
161
+ ok: false,
162
+ because: `the story did not render (${outcome.status}, readiness ${outcome.readiness})` +
163
+ (outcome.error === undefined ? '' : `: ${outcome.error.message}`),
164
+ };
165
+ }
166
+ // After the story has mounted and before it is read. A story whose
167
+ // image is still in flight is a story whose bytes nobody hashed, and the
168
+ // wait is the driver's because the page cannot see its own requests.
169
+ // Never a failure: a page that keeps fetching is reported, not refused.
170
+ await network?.settle();
171
+ // Either form matches. A subject is `story:case-surface--x` and a story
172
+ // is `case-surface--x`, and `ready` above is keyed by the second — so a
173
+ // declaration written the way the neighbouring option is written has to
174
+ // work, or the escape hatch fails silently and the story it was written
175
+ // for is refused anyway.
176
+ const declaredLoading = (options.loading ?? []).some((pattern) => matchesGlob(planned.subject.id, pattern) || matchesGlob(storyId, pattern));
177
+ const worn = new Set(planned.tags ?? []);
178
+ const selectable = (config.ignore ?? []).flatMap((rule) => rule.select === undefined ||
179
+ (rule.subjects !== undefined &&
180
+ !rule.subjects.some((pattern) => matchesGlob(planned.subject.id, pattern))) ||
181
+ (rule.tags !== undefined && !rule.tags.some((tag) => worn.has(tag)))
182
+ ? []
183
+ : [{ id: rule.id, select: rule.select }]);
184
+ const request = {
185
+ subjectId: planned.subject.id,
186
+ viewport,
187
+ engine,
188
+ ...(config.fonts !== undefined ? { fonts: config.fonts } : {}),
189
+ // Only the rules that name a selector cross into the page. A
190
+ // fingerprint rule has nothing for a document to resolve, and sending
191
+ // one would put a digest in a browser that cannot use it.
192
+ ...(selectable.length > 0 ? { ignore: selectable } : {}),
193
+ // A story declared as a loading capture waits for nothing: the whole
194
+ // point of it is the fallback, and paying the timeout to be told the
195
+ // boundary is still there would cost five seconds per story to learn
196
+ // what the declaration already said.
197
+ ...(declaredLoading
198
+ ? { suspense: { timeoutMs: 0 } }
199
+ : options.suspenseTimeoutMs !== undefined
200
+ ? { suspense: { timeoutMs: options.suspenseTimeoutMs } }
201
+ : {}),
202
+ roots,
203
+ };
204
+ // The page's whole observed set goes in there, narrowed inside the page
205
+ // to the URLs this story references: the driver cannot know which of
206
+ // them the story uses, and the page cannot know what the bytes were.
207
+ // Never reset between stories — one page serves the whole run, and a
208
+ // story whose asset was fetched during an earlier story still
209
+ // references it.
210
+ const raw = await acquireFromAgent(page, network, request);
211
+ const acquired = JSON.parse(raw);
212
+ // The symptom of a page agent older than this driver, named rather than
213
+ // read past: `undefined` here would make `suspenseRefusal` throw inside
214
+ // one story and take the run with it, and defaulting it to "settled"
215
+ // would silently restore the behaviour this exists to end.
216
+ if (acquired.suspense === undefined) {
217
+ await recorder?.note(page, planned.subject.id, false);
218
+ return {
219
+ ok: false,
220
+ because: 'the page agent returned no Suspense reading, which means the bundle in ' +
221
+ '`@variance-authority/storybook-collector` predates it — rebuild the package',
222
+ };
223
+ }
224
+ // The forced decision. A story still showing a fallback is refused with
225
+ // the boundary named, unless somebody declared that this is the subject.
226
+ const unsettled = suspenseRefusal(acquired.suspense, {
227
+ subjectId: planned.subject.id,
228
+ declaredLoading,
229
+ });
230
+ if (unsettled !== undefined) {
231
+ await recorder?.note(page, planned.subject.id, false);
232
+ return { ok: false, because: unsettled };
233
+ }
234
+ await recorder?.note(page, planned.subject.id, true);
235
+ // No frames are spent here, and that is deliberate: a story that settles
236
+ // on its document digest has nobody to hand a location to. They ride the
237
+ // snapshot as provenance — which no hash projects — and `locateSites`
238
+ // spends them for the few nodes a region or a finding names.
239
+ return {
240
+ ok: true,
241
+ document: acquired.document,
242
+ // Normalized here rather than in the page: the ruleset is the one the
243
+ // jsdom path uses, and running it in the browser would make the two
244
+ // profiles two implementations of it.
245
+ // `sourceRoot` for the same reason `scanSource` is rooted at the cwd
246
+ // above: both answers name files, and a report that mixes a
247
+ // repository-relative declaration with an absolute call site is one
248
+ // nobody can paste into anything.
249
+ snapshot: normalizeCapture(acquired.capture, { sourceRoot: process.cwd() }),
250
+ ...(acquired.stabilization !== undefined && acquired.stabilization.length > 0
251
+ ? { stabilization: acquired.stabilization }
252
+ : {}),
253
+ ...(source !== undefined ? { source } : {}),
254
+ // No `causes`. Naming the roots of a change needs the *previous*
255
+ // snapshot, and a durable run has a baseline image without one — so
256
+ // ranking falls back to area, which `rankRegions` documents as honest
257
+ // and not good.
258
+ };
259
+ },
260
+ callSites,
261
+ async close() {
262
+ await recorder?.close();
263
+ await network?.close();
264
+ await harness?.close();
265
+ await served?.close();
266
+ },
267
+ };
268
+ };
269
+ }
270
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { dirname, join } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { fileURLToPath } from 'node:url';\nimport {\n createCallSiteResolver,\n matchesGlob,\n normalize as normalizeCapture,\n} from '@variance-authority/core';\nimport {\n acquireFromAgent,\n createHarness,\n fetchModules,\n observeNetwork,\n unresizable,\n type Harness,\n type NetworkObservation,\n} from '@variance-authority/playwright';\nimport { suspenseRefusal } from '@variance-authority/react';\nimport { collectStory, harnessPage } from '@variance-authority/storybook';\nimport { createStoryRecorder, type StoryExecutionOptions } from './execution.js';\nimport type { AcquireRequest, Acquired } from './page-agent.js';\nexport type {\n Collected,\n Collector,\n CollectorConfig,\n CollectorContext,\n Plan,\n PlannedSubject,\n} from './contract.js';\nimport type { Collected, Collector, CollectorContext, Plan, PlannedSubject } from './contract.js';\nimport { operatorError } from './operator.js';\n\n/** Said in two places, because a mismatched kind can be caught at either. */\nconst WRONG_KIND =\n 'this collector expects `subjects.kind: \"storybook\"`, which is what supplies the plan';\nimport { serveStatic, type StaticServer } from './serve.js';\nimport { scanSource, type SourceScan } from './source.js';\n\n/**\n * The Storybook collector, shipped.\n *\n * ## Why this package exists, stated as the thing it retracts\n *\n * This repository's position was that mounting is the adopter's, once, per\n * project — and it measured the cost honestly at 341 lines rather than the\n * \"about thirty\" the source had guessed. That position is right about React\n * applications and wrong about Storybook, for a reason the measurement itself\n * shows: of those 341 lines, the parts that were genuinely *this project's* were\n * a map of three ready selectors, a directory to scan for components, and a\n * static server. Everything else was Storybook's own contract being re-typed.\n *\n * A story index is a documented artifact. A preview owns its own mount and\n * exposes a channel for switching stories. So for this one subject source the\n * mounting problem is already solved by somebody else, and asking every adopter\n * to re-solve it was charging them for our boundary rather than for their\n * project.\n *\n * **What stays theirs.** Readiness per story, because Storybook's `storyRendered`\n * fires when the story function returns — which for a component that defers work\n * is *before the component exists*. That is knowledge about a component, and a\n * project-wide default would be a guess about all of them.\n *\n * ## What it does not make generic\n *\n * Nothing outside Storybook. A Playwright suite, a route table or a bespoke\n * mount is still a collector somebody writes — see\n * [`@variance-authority/playwright-test`](../playwright-test) for the surface\n * where the adopter's own test body plays that part instead.\n */\n\n\nexport interface StorybookCollectorOptions {\n /**\n * Story id to the selector that says it is ready.\n *\n * Per story, not per project, and that is the design rather than an\n * ergonomics gap. A button needs no marker and demanding one from it would\n * time out every story to solve a problem one of them has. A story that\n * declares a marker and never attaches it times out saying which selector it\n * waited for — there is no fallback, because falling back is how you\n * photograph a spinner and call it a component.\n */\n readonly ready?: Readonly<Record<string, string>>;\n\n /**\n * Where the components live, for `file:line` attribution.\n *\n * Omitted, the report names components and no files — which is still ahead of\n * every product in the category and is not what this is for.\n */\n readonly source?: SourceScan;\n\n /**\n * A Storybook that is already served, e.g. `http://localhost:6006`.\n *\n * Preferred when it exists: then nothing here has an opinion about how the\n * build is hosted. Omitted, the directory holding `subjects.index` is served\n * on a loopback port for the life of the run.\n */\n readonly baseUrl?: string;\n\n /** Defaults to `true`. Set false to watch a run by hand. */\n readonly headless?: boolean;\n\n /**\n * Watch the wire: hash asset bodies into the environment key, and serve\n * animated GIFs as their first frame. Defaults to `true`.\n *\n * On, this is what closes a false `unchanged` that a page cannot see about\n * itself. A logo re-exported at the same URL is the same markup, the same CSS\n * and the same document — so every tier settles, and the run reports that\n * nothing moved while the image on the page is different bytes. Only the party\n * that saw the response knows otherwise.\n *\n * The assets are narrowed **per story** before they reach a key, from the URLs\n * that story's own subtree references (`assetsFor`). Without that, a run that\n * reads three hundred stories out of one page would give story 200 a key that\n * depends on which stories ran before it, and sharding the suite would change\n * every baseline's identity.\n *\n * Off is a position for a build whose asset URLs already contain their own\n * content hash: the URL is then the identity, and hashing the bytes again buys\n * a read and nothing else.\n */\n readonly network?: boolean;\n\n /** Overrides the roots the story is read from. Tightest first. */\n readonly roots?: readonly string[];\n\n /** Milliseconds for Storybook or a declared marker to report readiness. Defaults to 15000. */\n readonly readyTimeoutMs?: number;\n\n /**\n * Milliseconds to wait for a story's Suspense boundaries. Defaults to 5000.\n *\n * Paid only by stories that are actually waiting: a subtree with no boundary\n * in it returns on the first read. `0` turns the wait off and keeps the\n * reading, which is a position for a project whose readiness markers already\n * cover its data — the refusal below still fires, so the boundary is reported\n * rather than photographed.\n */\n readonly suspenseTimeoutMs?: number;\n\n /**\n * Record what each story executed, into the shared test-selection index.\n *\n * Off unless asked for, and it asks something of the build rather than of this\n * package: the Storybook preview has to have been built with\n * `testSelectionProbes()` from `@variance-authority/sense/journal`, which is\n * what puts probes in the source and writes down what their ordinals mean.\n * Without it a run records nothing and says so on stderr — the next selection\n * then runs everything, which is the direction every uncertainty here\n * resolves.\n *\n * A story is its own owner in that index. Storybook is an execution surface\n * this tool drives one subject at a time, so unlike a test runner — where the\n * file is the smallest thing a runner can be asked to execute — the crossings\n * of one story belong to that story and to nothing else.\n */\n readonly tests?: boolean | StoryExecutionOptions;\n\n /**\n * Stories whose *loading* state is the subject, as id globs.\n *\n * The escape hatch, and the only one. A story left showing its fallback is\n * otherwise refused, because a subject that records a skeleton on a slow\n * machine and a component on a fast one is a flake nobody wrote — so a\n * skeleton somebody *does* want a baseline over has to be said out loud.\n *\n * Declared here rather than sensed, and checked in both directions: a story\n * named by this that turns out to settle is refused too. A declaration that\n * outlived its subject is the same nondeterminism arriving from the other side.\n *\n * Matched against the story id and the subject id both, so\n * `case-surface--feed` and `story:case-surface--feed` name the same story —\n * `ready` above is keyed by the first.\n */\n readonly loading?: readonly string[];\n}\n\nconst STORY_ROOTS = ['#storybook-root', '#root'];\n\n/**\n * Two candidate paths, because this module is imported from two places.\n *\n * A consumer imports `dist/index.js` and the bundle sits beside it. This\n * repository's own tests import `src/index.ts`, where it does not — and a\n * collector that only worked when nobody in the project tested it would be a\n * collector nobody in the project tested.\n */\nasync function pageAgentBundle(): Promise<string> {\n const candidates = [\n new URL('./page-agent.bundle.js', import.meta.url),\n new URL('../dist/page-agent.bundle.js', import.meta.url),\n ];\n\n for (const path of candidates) {\n try {\n return await readFile(path, 'utf8');\n } catch {\n // Next candidate. The throw below names both, so a genuinely missing\n // bundle still fails with somewhere to look rather than a bare ENOENT.\n }\n }\n\n throw new Error(\n `the storybook page agent bundle is missing (looked in ` +\n `${candidates.map((path) => fileURLToPath(path)).join(' and ')}); ` +\n 'it is produced by this repository’s build, not at collection time',\n );\n}\n\n/**\n * Build the module a `subjects.collector` path should default-export.\n *\n * A factory rather than a plugin: the config still names a module, the module\n * still exports a function, and the extension mechanism is unchanged. What the\n * adopter writes shrinks to the two facts only they hold.\n *\n * ```js\n * import { storybookCollector } from '@variance-authority/storybook-collector';\n *\n * export default storybookCollector({\n * ready: { 'surface--deferred': '[data-testid=\"ready\"]' },\n * source: { dirs: ['src'] },\n * });\n * ```\n */\nexport function storybookCollector(\n options: StorybookCollectorOptions = {},\n): (context: CollectorContext) => Promise<Collector> {\n return async function createCollector(context: CollectorContext): Promise<Collector> {\n const { config, plan } = context;\n const roots = options.roots ?? STORY_ROOTS;\n\n // Checked before anything reads it. `index` is what `subjects.kind:\n // \"storybook\"` supplies, so a config naming another kind arrives here with\n // nothing to serve — and the guard further down in `plan()` is too late,\n // because this line runs first.\n const index = config.subjects.index;\n if (index === undefined) throw operatorError(WRONG_KIND);\n\n // The build the config named, not a sibling this file assumed. Serving the\n // directory that holds the planned index is the only arrangement in which the\n // stories driven and the stories planned are guaranteed to be the same ones.\n const staticDir = dirname(index);\n\n if (options.baseUrl === undefined && !existsSync(join(staticDir, 'index.json'))) {\n throw operatorError(\n `${staticDir} has no index.json; build the Storybook first, or point ` +\n '`baseUrl` at one that is already served',\n );\n }\n\n const source = options.source === undefined ? undefined : scanSource(process.cwd(), options.source);\n\n // What each story executed, if anybody asked. The index it feeds is the one\n // the Vitest seam writes: same probe recipe, same block ordinals, one index\n // across every origin.\n const recorder =\n options.tests === undefined || options.tests === false\n ? undefined\n : await createStoryRecorder(index, options.tests === true ? {} : options.tests);\n const bundle = await pageAgentBundle();\n\n let served: StaticServer | undefined;\n if (options.baseUrl === undefined) served = await serveStatic(staticDir);\n const baseUrl = options.baseUrl ?? served!.baseUrl;\n\n let harness: Harness | undefined;\n // Installed in `prepare`, which runs before the first navigation: an\n // observation that starts afterwards has already missed the assets the\n // preview loaded on its way up, and a URL nobody saw is a hole in the key.\n let network: NetworkObservation | undefined;\n try {\n // Pointed at the preview, so `collectStory` finds itself already there and\n // does not navigate: one navigation for a whole run is the saving ADR-0009\n // rests on, and it is easiest to keep by never taking a second one.\n harness = await createHarness({\n url: `${baseUrl}/iframe.html`,\n bundle,\n viewport: config.viewport,\n ...(config.fonts !== undefined ? { fonts: config.fonts } : {}),\n ...(options.headless !== undefined ? { headless: options.headless } : {}),\n ...(options.network === false\n ? {}\n : {\n prepare: async (page): Promise<void> => {\n network = await observeNetwork(\n page,\n config.blank !== undefined ? { blank: config.blank } : {},\n );\n },\n }),\n });\n } catch (error) {\n await served?.close();\n throw error;\n }\n\n const page = harness.page;\n const engine = harness.engine;\n\n // One per run, not one per story. Every story in a Storybook is written in\n // the same handful of modules, so the second story onward resolves its call\n // sites out of this cache without a single fetch.\n const callSites = createCallSiteResolver(fetchModules(page));\n\n return {\n async plan(): Promise<Plan> {\n if (plan === undefined) {\n throw operatorError(WRONG_KIND);\n }\n return plan;\n },\n\n async collect(planned: PlannedSubject): Promise<Collected> {\n const storyId = planned.subject.id.replace(/^story:/, '');\n\n const viewport = planned.viewport ?? config.viewport;\n const readySelector = options.ready?.[storyId];\n\n // Applied, not merely recorded. A story that declares its own viewport\n // was being painted at the run's width while its environment key said\n // otherwise — a baseline whose key describes a render that never\n // happened, and a verdict over it that is green for the wrong reason.\n // The resize precedes the mount, so a component that reads `matchMedia`\n // when it mounts reads the width it is about to be shown at.\n const fixed = unresizable(viewport, config.viewport);\n if (fixed !== undefined) return { ok: false, because: fixed };\n\n const current = page.viewportSize();\n if (current?.width !== viewport.width || current?.height !== viewport.height) {\n await page.setViewportSize({ width: viewport.width, height: viewport.height });\n }\n\n const outcome = await collectStory(harnessPage({ page }), storyId, {\n baseUrl,\n ...(options.readyTimeoutMs !== undefined ? { timeoutMs: options.readyTimeoutMs } : {}),\n ...(readySelector !== undefined ? { readySelector } : {}),\n });\n\n // A story that did not render is a hole in this run's coverage, and it\n // travels as a value rather than an exception: one component that throws\n // must not cost the others their observations, and must not be silently\n // absent either.\n if (outcome.status !== 'rendered') {\n // A story that rendered a fallback still executed code, and counters left\n // in the page would be handed to whichever story drained next.\n await recorder?.note(page, planned.subject.id, false);\n return {\n ok: false,\n because:\n `the story did not render (${outcome.status}, readiness ${outcome.readiness})` +\n (outcome.error === undefined ? '' : `: ${outcome.error.message}`),\n };\n }\n\n // After the story has mounted and before it is read. A story whose\n // image is still in flight is a story whose bytes nobody hashed, and the\n // wait is the driver's because the page cannot see its own requests.\n // Never a failure: a page that keeps fetching is reported, not refused.\n await network?.settle();\n\n // Either form matches. A subject is `story:case-surface--x` and a story\n // is `case-surface--x`, and `ready` above is keyed by the second — so a\n // declaration written the way the neighbouring option is written has to\n // work, or the escape hatch fails silently and the story it was written\n // for is refused anyway.\n const declaredLoading = (options.loading ?? []).some(\n (pattern) => matchesGlob(planned.subject.id, pattern) || matchesGlob(storyId, pattern),\n );\n\n const worn = new Set(planned.tags ?? []);\n const selectable = (config.ignore ?? []).flatMap((rule) =>\n rule.select === undefined ||\n (rule.subjects !== undefined &&\n !rule.subjects.some((pattern) => matchesGlob(planned.subject.id, pattern))) ||\n (rule.tags !== undefined && !rule.tags.some((tag) => worn.has(tag)))\n ? []\n : [{ id: rule.id, select: rule.select }],\n );\n\n const request: AcquireRequest = {\n subjectId: planned.subject.id,\n viewport,\n engine,\n ...(config.fonts !== undefined ? { fonts: config.fonts } : {}),\n // Only the rules that name a selector cross into the page. A\n // fingerprint rule has nothing for a document to resolve, and sending\n // one would put a digest in a browser that cannot use it.\n ...(selectable.length > 0 ? { ignore: selectable } : {}),\n // A story declared as a loading capture waits for nothing: the whole\n // point of it is the fallback, and paying the timeout to be told the\n // boundary is still there would cost five seconds per story to learn\n // what the declaration already said.\n ...(declaredLoading\n ? { suspense: { timeoutMs: 0 } }\n : options.suspenseTimeoutMs !== undefined\n ? { suspense: { timeoutMs: options.suspenseTimeoutMs } }\n : {}),\n roots,\n };\n\n // The page's whole observed set goes in there, narrowed inside the page\n // to the URLs this story references: the driver cannot know which of\n // them the story uses, and the page cannot know what the bytes were.\n // Never reset between stories — one page serves the whole run, and a\n // story whose asset was fetched during an earlier story still\n // references it.\n const raw = await acquireFromAgent(page, network, request);\n\n const acquired = JSON.parse(raw) as Acquired;\n\n // The symptom of a page agent older than this driver, named rather than\n // read past: `undefined` here would make `suspenseRefusal` throw inside\n // one story and take the run with it, and defaulting it to \"settled\"\n // would silently restore the behaviour this exists to end.\n if (acquired.suspense === undefined) {\n await recorder?.note(page, planned.subject.id, false);\n return {\n ok: false,\n because:\n 'the page agent returned no Suspense reading, which means the bundle in ' +\n '`@variance-authority/storybook-collector` predates it — rebuild the package',\n };\n }\n\n // The forced decision. A story still showing a fallback is refused with\n // the boundary named, unless somebody declared that this is the subject.\n const unsettled = suspenseRefusal(acquired.suspense, {\n subjectId: planned.subject.id,\n declaredLoading,\n });\n if (unsettled !== undefined) {\n await recorder?.note(page, planned.subject.id, false);\n return { ok: false, because: unsettled };\n }\n\n await recorder?.note(page, planned.subject.id, true);\n\n // No frames are spent here, and that is deliberate: a story that settles\n // on its document digest has nobody to hand a location to. They ride the\n // snapshot as provenance — which no hash projects — and `locateSites`\n // spends them for the few nodes a region or a finding names.\n return {\n ok: true,\n document: acquired.document,\n // Normalized here rather than in the page: the ruleset is the one the\n // jsdom path uses, and running it in the browser would make the two\n // profiles two implementations of it.\n // `sourceRoot` for the same reason `scanSource` is rooted at the cwd\n // above: both answers name files, and a report that mixes a\n // repository-relative declaration with an absolute call site is one\n // nobody can paste into anything.\n snapshot: normalizeCapture(acquired.capture, { sourceRoot: process.cwd() }),\n ...(acquired.stabilization !== undefined && acquired.stabilization.length > 0\n ? { stabilization: acquired.stabilization }\n : {}),\n ...(source !== undefined ? { source } : {}),\n // No `causes`. Naming the roots of a change needs the *previous*\n // snapshot, and a durable run has a baseline image without one — so\n // ranking falls back to area, which `rankRegions` documents as honest\n // and not good.\n };\n },\n\n callSites,\n\n async close(): Promise<void> {\n await recorder?.close();\n await network?.close();\n await harness?.close();\n await served?.close();\n },\n };\n };\n}\n\nexport type { StoryExecutionOptions } from './execution.js';\nexport type { SourceScan } from './source.js';\nexport type { AcquireRequest, Acquired } from './page-agent.js';\n"]}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Say that a failure is the operator's to fix, not a bug in the tool.
3
+ *
4
+ * A collector is loaded by dynamic import from the adopter's own
5
+ * `node_modules`, so it cannot share an error *class* with whatever runs it —
6
+ * `instanceof` across two installed copies is `false`, and the misconfigured
7
+ * suite would be told it has found a defect. The contract is therefore a
8
+ * property name, which survives the boundary the way a header name does.
9
+ *
10
+ * Use it for anything the reader can act on: a config that names two plans, a
11
+ * sitemap that answered 404, a route with no id. Leave a bare `Error` where the
12
+ * cause really is this package — a missing bundle it was supposed to build is
13
+ * not something an adopter can repair, and dressing it up as their problem
14
+ * sends them to edit a config that was never wrong.
15
+ */
16
+ export declare function operatorError(message: string, options?: ErrorOptions): Error;
17
+ //# sourceMappingURL=operator.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Say that a failure is the operator's to fix, not a bug in the tool.
3
+ *
4
+ * A collector is loaded by dynamic import from the adopter's own
5
+ * `node_modules`, so it cannot share an error *class* with whatever runs it —
6
+ * `instanceof` across two installed copies is `false`, and the misconfigured
7
+ * suite would be told it has found a defect. The contract is therefore a
8
+ * property name, which survives the boundary the way a header name does.
9
+ *
10
+ * Use it for anything the reader can act on: a config that names two plans, a
11
+ * sitemap that answered 404, a route with no id. Leave a bare `Error` where the
12
+ * cause really is this package — a missing bundle it was supposed to build is
13
+ * not something an adopter can repair, and dressing it up as their problem
14
+ * sends them to edit a config that was never wrong.
15
+ */
16
+ export function operatorError(message, options) {
17
+ return Object.assign(new Error(message, options), { varianceOperatorError: true });
18
+ }
19
+ //# sourceMappingURL=operator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operator.js","sourceRoot":"","sources":["../src/operator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,OAAsB;IACnE,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAC,CAAC;AACrF,CAAC","sourcesContent":["/**\n * Say that a failure is the operator's to fix, not a bug in the tool.\n *\n * A collector is loaded by dynamic import from the adopter's own\n * `node_modules`, so it cannot share an error *class* with whatever runs it —\n * `instanceof` across two installed copies is `false`, and the misconfigured\n * suite would be told it has found a defect. The contract is therefore a\n * property name, which survives the boundary the way a header name does.\n *\n * Use it for anything the reader can act on: a config that names two plans, a\n * sitemap that answered 404, a route with no id. Leave a bare `Error` where the\n * cause really is this package — a missing bundle it was supposed to build is\n * not something an adopter can repair, and dressing it up as their problem\n * sends them to edit a config that was never wrong.\n */\nexport function operatorError(message: string, options?: ErrorOptions): Error {\n return Object.assign(new Error(message, options), { varianceOperatorError: true });\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=page-agent-entry.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { AGENT_GLOBAL } from '@variance-authority/playwright/agent';
2
+ import { AGENT_VERSION, acquire } from './page-agent.js';
3
+ /**
4
+ * The bundle's entry point, and the only module here with a side effect.
5
+ *
6
+ * It installs at `AGENT_GLOBAL` because `createHarness` refuses a bundle that
7
+ * does not — a check worth keeping, since a broken bundle otherwise surfaces as
8
+ * a timeout with no cause. So the object satisfies `PageAgent` *and* carries the
9
+ * `acquire` a collector needs.
10
+ *
11
+ * The two are not the same call. `PageAgent.capture` answers the semantic tiers
12
+ * and returns a capture; a collector additionally needs the `RenderDocument` the
13
+ * raster tier paints, from the same mount. `capture` is implemented in terms of
14
+ * `acquire` rather than beside it, so there is no arrangement in which the two
15
+ * read different DOM.
16
+ */
17
+ const STORY_ROOTS = ['#storybook-root', '#root'];
18
+ async function acquireFor(request) {
19
+ return await acquire({
20
+ subjectId: request.subjectId,
21
+ viewport: request.viewport,
22
+ engine: request.engine,
23
+ ...(request.fonts !== undefined ? { fonts: request.fonts } : {}),
24
+ roots: STORY_ROOTS,
25
+ });
26
+ }
27
+ globalThis[AGENT_GLOBAL] = {
28
+ version: AGENT_VERSION,
29
+ acquire: (request) => acquire(request),
30
+ capture: async (request) => JSON.stringify(JSON.parse(await acquireFor(request)).capture),
31
+ };
32
+ //# sourceMappingURL=page-agent-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-agent-entry.js","sourceRoot":"","sources":["../src/page-agent-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,sCAAsC,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,OAAO,EAAuB,MAAM,iBAAiB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAU,CAAC;AAE1D,KAAK,UAAU,UAAU,CAAC,OAAuB;IAC/C,OAAO,MAAM,OAAO,CAAC;QACnB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,KAAK,EAAE,WAAW;KACnB,CAAC,CAAC;AACL,CAAC;AAEA,UAAiD,CAAC,YAAY,CAAC,GAAG;IACjE,OAAO,EAAE,aAAa;IACtB,OAAO,EAAE,CAAC,OAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;IACtD,OAAO,EAAE,KAAK,EAAE,OAAuB,EAAE,EAAE,CACzC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;CAChE,CAAC","sourcesContent":["import { AGENT_GLOBAL, type CaptureRequest } from '@variance-authority/playwright/agent';\nimport { AGENT_VERSION, acquire, type AcquireRequest } from './page-agent.js';\n\n/**\n * The bundle's entry point, and the only module here with a side effect.\n *\n * It installs at `AGENT_GLOBAL` because `createHarness` refuses a bundle that\n * does not — a check worth keeping, since a broken bundle otherwise surfaces as\n * a timeout with no cause. So the object satisfies `PageAgent` *and* carries the\n * `acquire` a collector needs.\n *\n * The two are not the same call. `PageAgent.capture` answers the semantic tiers\n * and returns a capture; a collector additionally needs the `RenderDocument` the\n * raster tier paints, from the same mount. `capture` is implemented in terms of\n * `acquire` rather than beside it, so there is no arrangement in which the two\n * read different DOM.\n */\n\nconst STORY_ROOTS = ['#storybook-root', '#root'] as const;\n\nasync function acquireFor(request: CaptureRequest): Promise<string> {\n return await acquire({\n subjectId: request.subjectId,\n viewport: request.viewport,\n engine: request.engine,\n ...(request.fonts !== undefined ? { fonts: request.fonts } : {}),\n roots: STORY_ROOTS,\n });\n}\n\n(globalThis as unknown as Record<string, unknown>)[AGENT_GLOBAL] = {\n version: AGENT_VERSION,\n acquire: (request: AcquireRequest) => acquire(request),\n capture: async (request: CaptureRequest) =>\n JSON.stringify(JSON.parse(await acquireFor(request)).capture),\n};\n"]}