@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/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/operator.js
ADDED
|
@@ -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,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"]}
|