@variance-authority/storybook-collector 0.6.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @variance-authority/storybook-collector
2
2
 
3
+ ## 0.8.1
4
+
5
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
6
+
7
+ ## 0.8.0
8
+
9
+ ### Minor Changes
10
+
11
+ - 4250eda: Every run records which case entered each region. The `cases` option is removed
12
+ from `withTestSelection` for Vitest, Jest and Rstest, from the Playwright
13
+ recorder and reporter, and from the Storybook recorder: each writes
14
+ `<coverage file>.cases.bin` beside the file-level snapshot, or `executionFile`
15
+ when you name one. A test file that runs in a page is still recorded per file,
16
+ and says so.
17
+
18
+ Without `continuations: true`, a file whose cases overlap no longer fails the
19
+ run. It is recorded as a whole, so a change it reaches runs every case in it, and
20
+ the run names the two cases that were open at once.
21
+
22
+ ### Patch Changes
23
+
24
+ - e4ee0da: The repository names its cache. Set `cacheRoot` in the `variance.config.json` at the repository root, for example `".variance/cache"`, and every command, every test runner integration and every function that takes a `cacheRoot` option uses that directory. The path resolves against the repository root. Without the key the cache is `$XDG_CACHE_HOME/variance-authority`, or `~/.cache/variance-authority`, as before, so an existing recording stays where it is. The key is read before the environment, so a sandboxed agent that sets `XDG_CACHE_HOME` to a temporary directory no longer splits the recording away from your own runs. [The cache](https://variance-authority.dev/docs/cache) page describes the location, what is in it, worktrees and CI.
25
+
26
+ `@variance-authority/sense` exports `cacheRootFor(root)`, which returns that answer, and `CACHE_CONFIG`. `defaultCacheRoot` is removed; call `cacheRootFor(root)`. A `cacheRoot` option now names the variance-authority directory itself, and `test-selection/` is created under it. `testCoverageFile`, `seedTestCoverage`, `readableTestCoverage`, `moduleNamesFile`, `openModuleNames`, `recordStore`, `recordStores` and `repositoryLayers` take an optional `cacheRoot`. An empty or relative `XDG_CACHE_HOME` is ignored rather than resolved against the working directory. A root `variance.config.json` that is not JSON, or whose `cacheRoot` is not a non-empty string, is an error.
27
+
28
+ `@variance-authority/cli` accepts `cacheRoot` in the config and in the schema, and refuses it in a `variance.config.json` below the repository root. `variance run`'s render cache and suite indexes are under it. `renderCacheRoot` and `suiteIndexPath` take the config.
29
+
30
+ ## 0.7.0
31
+
32
+ ### Patch Changes
33
+
34
+ - 8ae7195: A relative `coverageFile` or `executionFile` is read from `root`
35
+
36
+ The Vitest, Rstest and Jest integrations resolve these two paths from their own
37
+ root. The Playwright reporter and the Storybook recorder resolved them from the
38
+ root of the repository instead. In a workspace, where `root` is a package
39
+ directory, that caused two problems:
40
+
41
+ - The Playwright workers staged their results beside one coverage index, and the
42
+ reporter merged them into a different one.
43
+ - A run where the fixture merged for itself, without the reporter, wrote its
44
+ execution index to a third place.
45
+
46
+ Both paths are now read from `root`, as the fixture already read `coverageFile`.
47
+ An absolute path is unchanged.
48
+ - 17d7f7e: A story file is recorded under its path in the repository
49
+
50
+ Storybook writes each story's `importPath` relative to the directory it ran
51
+ in, with a `./` in front. The recorder resolved that path against the
52
+ repository root. In a workspace, where Storybook runs in a package directory,
53
+ this had two effects: the story file's precondition read a file that does not
54
+ exist and was dropped without a message, and the per-story index named a path
55
+ no diff contains. So a commit that edited only a `.stories` file selected none
56
+ of its stories.
57
+
58
+ The recorder now resolves `importPath` from `tests.root`, which defaults to the
59
+ current directory, and records the path relative to the repository root.
60
+
3
61
  ## 0.6.0
4
62
 
5
63
  ### Minor Changes
package/README.md CHANGED
@@ -372,9 +372,11 @@ export default storybookCollector({
372
372
  ```
373
373
 
374
374
  `tests` accepts `true` or an options object: `label` must match the one the
375
- preview's `testSelectionProbes()` was given, `root` is the repository root the
376
- recorded paths are relative to, and `cacheRoot` and `coverageFile` override the
377
- repository-keyed cache paths for the block records and the coverage index.
375
+ preview's `testSelectionProbes()` was given, and `cacheRoot` and `coverageFile`
376
+ override the repository-keyed cache paths for the block records and the coverage
377
+ index. `root` is the directory Storybook ran in, the cwd by default: story paths
378
+ and a relative `coverageFile` or `executionFile` are read from it, and every
379
+ path the run records is relative to the repository that contains it.
378
380
  Without a collector in the page, the run says so on stderr and records nothing.
379
381
 
380
382
  `mode` is the probe recipe, and it has to be the one the preview was built
@@ -392,19 +394,10 @@ A story is its own owner in the recorded index — Storybook is an execution
392
394
  surface this tool drives one subject at a time — and a story that did not render
393
395
  still contributes its crossings while never justifying a later skip.
394
396
 
395
- Set `cases` to also write the execution index: one row per story per region,
397
+ Every run also writes the execution index: one row per story per region,
396
398
  beside the coverage index, or wherever `executionFile` points. That is the file
397
399
  `variance covering` and `@variance-authority/distill` read to answer *which
398
- stories walk this branch* — the question a component library is worth asking and
399
- a preview of ten thousand stories usually is not, which is why it is off by
400
- default.
401
-
402
- ```js
403
- export default storybookCollector({
404
- source: { dirs: ['src'] },
405
- tests: { label: 'storybook', cases: true },
406
- });
407
- ```
400
+ stories walk this branch*.
408
401
 
409
402
  A story is the case this costs nothing to name. The driver shows one at a time,
410
403
  so the per-story grain a unit runner needs an async scope for is already here:
@@ -17,31 +17,24 @@ import { type EvaluatingPage, type InstrumentMode } from '@variance-authority/se
17
17
  export interface StoryExecutionOptions {
18
18
  /**
19
19
  * A directory inside the repository; defaults to the cwd. Recorded paths are
20
- * relative to the checkout it sits in, never to it.
20
+ * relative to the checkout it sits in, never to it. A story's `importPath` is
21
+ * read from here, because it is relative to the directory Storybook ran in.
21
22
  */
22
23
  readonly root?: string;
23
24
  /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */
24
25
  readonly label?: string;
25
- /** Where the build wrote its block records. Defaults to the user cache. */
26
+ /** Where the build wrote its block records. Defaults to the repository's cache. */
26
27
  readonly cacheRoot?: string;
27
28
  /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */
28
29
  readonly coverageFile?: string;
29
30
  /**
30
- * Also write the execution index: which individual story entered which region.
31
- *
32
- * Off by default, and the default is the point. The index is what answers
33
- * *which stories walk this branch* — the question `variance covering` and
34
- * `@variance-authority/distill` are asked, and the one a component library
35
- * with a few hundred stories is worth asking. It is a row per story per
36
- * region either way, so a preview that indexes ten thousand stories to answer
37
- * a question nobody asks has bought a large file and nothing else.
31
+ * Where the execution index — which individual story entered which region —
32
+ * goes. Defaults beside the snapshot, as the Vitest seam's does.
38
33
  *
39
34
  * A story is the case this costs nothing to name: the driver shows one at a
40
- * time, so the per-case grain a unit runner needs an async scope and a custom
41
- * runner for is already here.
35
+ * time, so the per-case grain a unit runner needs a custom runner for is
36
+ * already here.
42
37
  */
43
- readonly cases?: boolean;
44
- /** Where that index goes. Defaults beside the snapshot, as the Vitest seam's does. */
45
38
  readonly executionFile?: string;
46
39
  /**
47
40
  * The probe recipe the preview was built with, matching
package/dist/execution.js CHANGED
@@ -13,12 +13,14 @@
13
13
  * belong to that story and to nothing else.
14
14
  */
15
15
  import { readFile } from 'node:fs/promises';
16
+ import { relative, resolve, sep } from 'node:path';
16
17
  import { drainExecution, preconditionOf, recordExecution, } from '@variance-authority/sense/journal';
17
18
  import { parseStoryIndex } from '@variance-authority/storybook';
18
19
  import { repositoryRoot } from '@variance-authority/sense/test-selection';
19
20
  export async function createStoryRecorder(index, options) {
20
- const root = repositoryRoot(options.root ?? process.cwd());
21
- const storyFiles = await storyFilesFrom(index);
21
+ const ran = resolve(options.root ?? process.cwd());
22
+ const root = repositoryRoot(ran);
23
+ const storyFiles = await storyFilesFrom(index, ran, root);
22
24
  const observed = [];
23
25
  const cases = [];
24
26
  let seen = false;
@@ -42,7 +44,7 @@ export async function createStoryRecorder(index, options) {
42
44
  // touches. A story the driving index does not know — a subject named on
43
45
  // the command line, a preview rebuilt since — has no file to be a case
44
46
  // in, and contributes to the file-level record only.
45
- if (options.cases === true && story !== undefined) {
47
+ if (story !== undefined) {
46
48
  cases.push({ file: story.file, name: `${story.title}/${story.name}`, id: storyId, journal });
47
49
  }
48
50
  },
@@ -73,7 +75,10 @@ export async function createStoryRecorder(index, options) {
73
75
  subjects: observed,
74
76
  ...(options.label === undefined ? {} : { label: options.label }),
75
77
  ...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),
76
- ...(options.coverageFile === undefined ? {} : { coverageFile: options.coverageFile }),
78
+ // Against `root`, as the runner seams resolve them, never the checkout.
79
+ ...(options.coverageFile === undefined
80
+ ? {}
81
+ : { coverageFile: resolve(ran, options.coverageFile) }),
77
82
  ...(options.mode === undefined ? {} : { mode: options.mode }),
78
83
  ...(options.preconditions === undefined
79
84
  ? {}
@@ -85,7 +90,7 @@ export async function createStoryRecorder(index, options) {
85
90
  ...(cases.length === 0 ? {} : { cases }),
86
91
  ...(options.executionFile === undefined
87
92
  ? {}
88
- : { executionFile: options.executionFile }),
93
+ : { executionFile: resolve(ran, options.executionFile) }),
89
94
  });
90
95
  }
91
96
  catch (error) {
@@ -108,12 +113,20 @@ export async function createStoryRecorder(index, options) {
108
113
  * enters a module, so without this a commit that edits one selects nothing. The
109
114
  * title and the name come along because the execution index names a case by its
110
115
  * declaration rather than by the id Storybook slugged from it.
116
+ *
117
+ * Storybook writes `importPath` from the directory it ran in, `./` and all. A
118
+ * diff names files from the checkout, so the path is resolved from `ran` and
119
+ * named from `root`; in a workspace the two are a package apart.
111
120
  */
112
- async function storyFilesFrom(index) {
121
+ async function storyFilesFrom(index, ran, root) {
113
122
  const parsed = parseStoryIndex(JSON.parse(await readFile(index, 'utf8')), index);
114
123
  return new Map(parsed.stories.map((story) => [
115
124
  story.id,
116
- { file: story.importPath, title: story.title, name: story.name },
125
+ {
126
+ file: relative(root, resolve(ran, story.importPath)).split(sep).join('/'),
127
+ title: story.title,
128
+ name: story.name,
129
+ },
117
130
  ]));
118
131
  }
119
132
  //# sourceMappingURL=execution.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"execution.js","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each story executed, recorded for the next run's selection.\n *\n * The instrument is the one `@variance-authority/sense` already uses; only the\n * transport differs, which is the rule\n * [spec 0028](../../../docs/specs/0028-the-instrument.md) states. The preview is\n * built with `testSelectionProbes()`, the page counts block crossings, and this\n * closes one window per story and joins them at the end of the run.\n *\n * A story is its own owner. Storybook is an execution surface this tool drives\n * one subject at a time, so — unlike a test runner, where the file is the\n * smallest thing a runner can be asked to execute — the crossings of one story\n * belong to that story and to nothing else.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport {\n drainExecution,\n preconditionOf,\n recordExecution,\n type EvaluatingPage,\n type InstrumentMode,\n type ObservedCase,\n type ObservedSubject,\n} from '@variance-authority/sense/journal';\nimport { parseStoryIndex } from '@variance-authority/storybook';\nimport { repositoryRoot } from '@variance-authority/sense/test-selection';\n\n/** Where a story run's execution evidence is joined, read and kept. */\nexport interface StoryExecutionOptions {\n /**\n * A directory inside the repository; defaults to the cwd. Recorded paths are\n * relative to the checkout it sits in, never to it.\n */\n readonly root?: string;\n /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */\n readonly label?: string;\n /** Where the build wrote its block records. Defaults to the user cache. */\n readonly cacheRoot?: string;\n /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */\n readonly coverageFile?: string;\n /**\n * Also write the execution index: which individual story entered which region.\n *\n * Off by default, and the default is the point. The index is what answers\n * *which stories walk this branch* — the question `variance covering` and\n * `@variance-authority/distill` are asked, and the one a component library\n * with a few hundred stories is worth asking. It is a row per story per\n * region either way, so a preview that indexes ten thousand stories to answer\n * a question nobody asks has bought a large file and nothing else.\n *\n * A story is the case this costs nothing to name: the driver shows one at a\n * time, so the per-case grain a unit runner needs an async scope and a custom\n * runner for is already here.\n */\n readonly cases?: boolean;\n /** Where that index goes. Defaults beside the snapshot, as the Vitest seam's does. */\n readonly executionFile?: string;\n /**\n * The probe recipe the preview was built with, matching\n * `testSelectionProbes()`'s `mode`. `presence` when absent, as it is there.\n *\n * Both sides answer the same or neither works: a journal cut by one recipe\n * and folded as another is refused, and a snapshot a runner seam also writes\n * under a different recipe has each run retire the other's evidence.\n */\n readonly mode?: InstrumentMode;\n /**\n * Files whose contents are preconditions of every story this run recorded.\n *\n * A `.storybook/preview` file, a theme module every decorator reads — a\n * story *file* is already a precondition of its own stories, and this is for\n * the ones no story declares and nothing enters.\n */\n readonly preconditions?: readonly string[];\n /**\n * Other builds this same run drove, by the label each instrumented under.\n *\n * A preview and the application behind it are two builds of overlapping\n * source, and a story that reaches both is one observation. Their stores\n * join this recording rather than getting one of their own.\n */\n readonly heads?: readonly string[];\n /**\n * Where this recording stands. Defaults to the checkout's `HEAD`, which is\n * the answer in every case except a caller that already knows better.\n */\n readonly commit?: string;\n}\n\n/** One run's accumulation: a window per story, written once at the end. */\nexport interface StoryRecorder {\n /**\n * Close one story's window.\n *\n * `complete` is false for a story this run refused. A refused story still\n * executed code, so its crossings are kept — what it may never do is justify\n * excluding itself from a later run.\n */\n readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean) => Promise<void>;\n /** Merge the run into the index, or say on stderr why it could not. */\n readonly close: () => Promise<void>;\n}\n\nexport async function createStoryRecorder(\n index: string,\n options: StoryExecutionOptions,\n): Promise<StoryRecorder> {\n const root = repositoryRoot(options.root ?? process.cwd());\n const storyFiles = await storyFilesFrom(index);\n const observed: ObservedSubject[] = [];\n const cases: ObservedCase[] = [];\n let seen = false;\n\n return {\n note: async (page, subjectId, complete) => {\n const journal = await drainExecution(page);\n if (journal === undefined) return;\n seen = true;\n const storyId = subjectId.replace(/^story:/, '');\n const story = storyFiles.get(storyId);\n const precondition =\n story === undefined ? undefined : await preconditionOf(root, story.file);\n observed.push({\n owner: subjectId,\n journal,\n complete,\n ...(precondition === undefined ? {} : { preconditions: [precondition] }),\n });\n // The index names a story by where it is declared, which is the same\n // coordinate a person reads in the sidebar and the same one a diff\n // touches. A story the driving index does not know — a subject named on\n // the command line, a preview rebuilt since — has no file to be a case\n // in, and contributes to the file-level record only.\n if (options.cases === true && story !== undefined) {\n cases.push({ file: story.file, name: `${story.title}/${story.name}`, id: storyId, journal });\n }\n },\n\n close: async () => {\n if (!seen) {\n process.stderr.write(\n 'variance-authority: `tests` is on and the Storybook preview has no execution ' +\n 'collector in it — build it with `testSelectionProbes()` from ' +\n '`@variance-authority/sense/journal`, or the next `--since` will run ' +\n 'everything\\n',\n );\n return;\n }\n // A refusal is written and the run continues. Recording is an addition to\n // a visual run: losing it costs the *next* selection its narrowing, which\n // is a full suite — the safe direction — while failing the run over it\n // would cost this one its baselines, which is not.\n //\n // That holds for a throw as much as for a refusal. The recorder reports\n // what it declined in `because`, but it is a file format, a lock and a\n // filesystem underneath, and any of them can raise something it has no\n // sentence for. Letting that escape ends the run *after* every subject\n // has been rendered and compared — the most expensive possible moment to\n // lose a set of baselines, and over the half of the work that was only\n // ever an addition.\n let record: Awaited<ReturnType<typeof recordExecution>>;\n try {\n record = await recordExecution({\n root,\n subjects: observed,\n ...(options.label === undefined ? {} : { label: options.label }),\n ...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),\n ...(options.coverageFile === undefined ? {} : { coverageFile: options.coverageFile }),\n ...(options.mode === undefined ? {} : { mode: options.mode }),\n ...(options.preconditions === undefined\n ? {}\n : { preconditions: options.preconditions }),\n ...(options.heads === undefined || options.heads.length === 0\n ? {}\n : { heads: options.heads }),\n ...(options.commit === undefined ? {} : { commit: options.commit }),\n ...(cases.length === 0 ? {} : { cases }),\n ...(options.executionFile === undefined\n ? {}\n : { executionFile: options.executionFile }),\n });\n } catch (error) {\n process.stderr.write(\n 'variance-authority: recorded no story execution — the recorder failed with ' +\n `${error instanceof Error ? error.message : String(error)}; this run is ` +\n 'unaffected and the next `--since` will run everything\\n',\n );\n return;\n }\n if (!record.recorded) {\n process.stderr.write(\n `variance-authority: recorded no story execution — ${record.because}\\n`,\n );\n }\n },\n };\n}\n\n/**\n * Story id to where it is declared, from the index the run is driving.\n *\n * The one route from a subject back to a file, and the reason a story's own\n * `.stories` file can select it: nothing *enters* a story file the way execution\n * enters a module, so without this a commit that edits one selects nothing. The\n * title and the name come along because the execution index names a case by its\n * declaration rather than by the id Storybook slugged from it.\n */\nasync function storyFilesFrom(index: string): Promise<Map<string, StoryCoordinate>> {\n const parsed = parseStoryIndex(JSON.parse(await readFile(index, 'utf8')), index);\n return new Map(\n parsed.stories.map((story) => [\n story.id,\n { file: story.importPath, title: story.title, name: story.name },\n ]),\n );\n}\n\n/** Where a story is declared and what it is called there. */\ninterface StoryCoordinate {\n readonly file: string;\n readonly title: string;\n readonly name: string;\n}\n"]}
1
+ {"version":3,"file":"execution.js","sourceRoot":"","sources":["../src/execution.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each story executed, recorded for the next run's selection.\n *\n * The instrument is the one `@variance-authority/sense` already uses; only the\n * transport differs, which is the rule\n * [spec 0028](../../../docs/specs/0028-the-instrument.md) states. The preview is\n * built with `testSelectionProbes()`, the page counts block crossings, and this\n * closes one window per story and joins them at the end of the run.\n *\n * A story is its own owner. Storybook is an execution surface this tool drives\n * one subject at a time, so — unlike a test runner, where the file is the\n * smallest thing a runner can be asked to execute — the crossings of one story\n * belong to that story and to nothing else.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { relative, resolve, sep } from 'node:path';\nimport {\n drainExecution,\n preconditionOf,\n recordExecution,\n type EvaluatingPage,\n type InstrumentMode,\n type ObservedCase,\n type ObservedSubject,\n} from '@variance-authority/sense/journal';\nimport { parseStoryIndex } from '@variance-authority/storybook';\nimport { repositoryRoot } from '@variance-authority/sense/test-selection';\n\n/** Where a story run's execution evidence is joined, read and kept. */\nexport interface StoryExecutionOptions {\n /**\n * A directory inside the repository; defaults to the cwd. Recorded paths are\n * relative to the checkout it sits in, never to it. A story's `importPath` is\n * read from here, because it is relative to the directory Storybook ran in.\n */\n readonly root?: string;\n /** Matches the `label` the build's `testSelectionProbes()` used. Defaults to `build`. */\n readonly label?: string;\n /** Where the build wrote its block records. Defaults to the repository's cache. */\n readonly cacheRoot?: string;\n /** Coverage index. Defaults to the repository-keyed cache the runner seams share. */\n readonly coverageFile?: string;\n /**\n * Where the execution index — which individual story entered which region —\n * goes. Defaults beside the snapshot, as the Vitest seam's does.\n *\n * A story is the case this costs nothing to name: the driver shows one at a\n * time, so the per-case grain a unit runner needs a custom runner for is\n * already here.\n */\n readonly executionFile?: string;\n /**\n * The probe recipe the preview was built with, matching\n * `testSelectionProbes()`'s `mode`. `presence` when absent, as it is there.\n *\n * Both sides answer the same or neither works: a journal cut by one recipe\n * and folded as another is refused, and a snapshot a runner seam also writes\n * under a different recipe has each run retire the other's evidence.\n */\n readonly mode?: InstrumentMode;\n /**\n * Files whose contents are preconditions of every story this run recorded.\n *\n * A `.storybook/preview` file, a theme module every decorator reads — a\n * story *file* is already a precondition of its own stories, and this is for\n * the ones no story declares and nothing enters.\n */\n readonly preconditions?: readonly string[];\n /**\n * Other builds this same run drove, by the label each instrumented under.\n *\n * A preview and the application behind it are two builds of overlapping\n * source, and a story that reaches both is one observation. Their stores\n * join this recording rather than getting one of their own.\n */\n readonly heads?: readonly string[];\n /**\n * Where this recording stands. Defaults to the checkout's `HEAD`, which is\n * the answer in every case except a caller that already knows better.\n */\n readonly commit?: string;\n}\n\n/** One run's accumulation: a window per story, written once at the end. */\nexport interface StoryRecorder {\n /**\n * Close one story's window.\n *\n * `complete` is false for a story this run refused. A refused story still\n * executed code, so its crossings are kept — what it may never do is justify\n * excluding itself from a later run.\n */\n readonly note: (page: EvaluatingPage, subjectId: string, complete: boolean) => Promise<void>;\n /** Merge the run into the index, or say on stderr why it could not. */\n readonly close: () => Promise<void>;\n}\n\nexport async function createStoryRecorder(\n index: string,\n options: StoryExecutionOptions,\n): Promise<StoryRecorder> {\n const ran = resolve(options.root ?? process.cwd());\n const root = repositoryRoot(ran);\n const storyFiles = await storyFilesFrom(index, ran, root);\n const observed: ObservedSubject[] = [];\n const cases: ObservedCase[] = [];\n let seen = false;\n\n return {\n note: async (page, subjectId, complete) => {\n const journal = await drainExecution(page);\n if (journal === undefined) return;\n seen = true;\n const storyId = subjectId.replace(/^story:/, '');\n const story = storyFiles.get(storyId);\n const precondition =\n story === undefined ? undefined : await preconditionOf(root, story.file);\n observed.push({\n owner: subjectId,\n journal,\n complete,\n ...(precondition === undefined ? {} : { preconditions: [precondition] }),\n });\n // The index names a story by where it is declared, which is the same\n // coordinate a person reads in the sidebar and the same one a diff\n // touches. A story the driving index does not know — a subject named on\n // the command line, a preview rebuilt since — has no file to be a case\n // in, and contributes to the file-level record only.\n if (story !== undefined) {\n cases.push({ file: story.file, name: `${story.title}/${story.name}`, id: storyId, journal });\n }\n },\n\n close: async () => {\n if (!seen) {\n process.stderr.write(\n 'variance-authority: `tests` is on and the Storybook preview has no execution ' +\n 'collector in it — build it with `testSelectionProbes()` from ' +\n '`@variance-authority/sense/journal`, or the next `--since` will run ' +\n 'everything\\n',\n );\n return;\n }\n // A refusal is written and the run continues. Recording is an addition to\n // a visual run: losing it costs the *next* selection its narrowing, which\n // is a full suite — the safe direction — while failing the run over it\n // would cost this one its baselines, which is not.\n //\n // That holds for a throw as much as for a refusal. The recorder reports\n // what it declined in `because`, but it is a file format, a lock and a\n // filesystem underneath, and any of them can raise something it has no\n // sentence for. Letting that escape ends the run *after* every subject\n // has been rendered and compared — the most expensive possible moment to\n // lose a set of baselines, and over the half of the work that was only\n // ever an addition.\n let record: Awaited<ReturnType<typeof recordExecution>>;\n try {\n record = await recordExecution({\n root,\n subjects: observed,\n ...(options.label === undefined ? {} : { label: options.label }),\n ...(options.cacheRoot === undefined ? {} : { cacheRoot: options.cacheRoot }),\n // Against `root`, as the runner seams resolve them, never the checkout.\n ...(options.coverageFile === undefined\n ? {}\n : { coverageFile: resolve(ran, options.coverageFile) }),\n ...(options.mode === undefined ? {} : { mode: options.mode }),\n ...(options.preconditions === undefined\n ? {}\n : { preconditions: options.preconditions }),\n ...(options.heads === undefined || options.heads.length === 0\n ? {}\n : { heads: options.heads }),\n ...(options.commit === undefined ? {} : { commit: options.commit }),\n ...(cases.length === 0 ? {} : { cases }),\n ...(options.executionFile === undefined\n ? {}\n : { executionFile: resolve(ran, options.executionFile) }),\n });\n } catch (error) {\n process.stderr.write(\n 'variance-authority: recorded no story execution — the recorder failed with ' +\n `${error instanceof Error ? error.message : String(error)}; this run is ` +\n 'unaffected and the next `--since` will run everything\\n',\n );\n return;\n }\n if (!record.recorded) {\n process.stderr.write(\n `variance-authority: recorded no story execution — ${record.because}\\n`,\n );\n }\n },\n };\n}\n\n/**\n * Story id to where it is declared, from the index the run is driving.\n *\n * The one route from a subject back to a file, and the reason a story's own\n * `.stories` file can select it: nothing *enters* a story file the way execution\n * enters a module, so without this a commit that edits one selects nothing. The\n * title and the name come along because the execution index names a case by its\n * declaration rather than by the id Storybook slugged from it.\n *\n * Storybook writes `importPath` from the directory it ran in, `./` and all. A\n * diff names files from the checkout, so the path is resolved from `ran` and\n * named from `root`; in a workspace the two are a package apart.\n */\nasync function storyFilesFrom(\n index: string,\n ran: string,\n root: string,\n): Promise<Map<string, StoryCoordinate>> {\n const parsed = parseStoryIndex(JSON.parse(await readFile(index, 'utf8')), index);\n return new Map(\n parsed.stories.map((story) => [\n story.id,\n {\n file: relative(root, resolve(ran, story.importPath)).split(sep).join('/'),\n title: story.title,\n name: story.name,\n },\n ]),\n );\n}\n\n/** Where a story is declared and what it is called there. */\ninterface StoryCoordinate {\n readonly file: string;\n readonly title: string;\n readonly name: string;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variance-authority/storybook-collector",
3
- "version": "0.6.0",
3
+ "version": "0.8.1",
4
4
  "description": "Turn a built or already-served Storybook into Variance Authority subjects.",
5
5
  "keywords": [
6
6
  "variance-authority",
@@ -40,11 +40,11 @@
40
40
  "build": "tsc --build"
41
41
  },
42
42
  "dependencies": {
43
- "@variance-authority/core": "^0.6.0",
44
- "@variance-authority/dom": "^0.6.0",
45
- "@variance-authority/playwright": "^0.6.0",
46
- "@variance-authority/react": "^0.6.0",
47
- "@variance-authority/sense": "^0.6.0",
48
- "@variance-authority/storybook": "^0.6.0"
43
+ "@variance-authority/core": "^0.8.1",
44
+ "@variance-authority/dom": "^0.8.1",
45
+ "@variance-authority/playwright": "^0.8.1",
46
+ "@variance-authority/react": "^0.8.1",
47
+ "@variance-authority/sense": "^0.8.1",
48
+ "@variance-authority/storybook": "^0.8.1"
49
49
  }
50
50
  }