@describe-me/vitest 0.6.0 → 0.7.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.
@@ -8,6 +8,7 @@ export const browserProfile = {
8
8
  },
9
9
  },
10
10
  setupFile: '@describe-me/vitest/setup',
11
+ env: {},
11
12
  configFor({ renderModule }) {
12
13
  return {
13
14
  config: {
@@ -0,0 +1,7 @@
1
+ import type { StoriesMode } from './stories-mode.js';
2
+ /**
3
+ * Test tags arrived in Vitest 4.1. Without them `stories: 'tagged'` would
4
+ * record nothing, so an older Vitest fails before any test runs. A version
5
+ * that cannot be read passes.
6
+ */
7
+ export declare function checkTagsSupported(stories: StoriesMode, root: string): void;
@@ -0,0 +1,16 @@
1
+ import { readVitestVersion } from './read-vitest-version.js';
2
+ /**
3
+ * Test tags arrived in Vitest 4.1. Without them `stories: 'tagged'` would
4
+ * record nothing, so an older Vitest fails before any test runs. A version
5
+ * that cannot be read passes.
6
+ */
7
+ export function checkTagsSupported(stories, root) {
8
+ if (stories !== 'tagged') {
9
+ return;
10
+ }
11
+ const version = readVitestVersion(root);
12
+ if (version === null || version.major > 4 || (version.major === 4 && version.minor >= 1)) {
13
+ return;
14
+ }
15
+ throw new Error(`describe-me: stories: 'tagged' needs test tags, which arrived in Vitest 4.1. This project resolves Vitest ${version.major}.${version.minor}.`);
16
+ }
@@ -0,0 +1,9 @@
1
+ import type { ViteUserConfig } from 'vitest/config';
2
+ type TagDefinition = NonNullable<NonNullable<ViteUserConfig['test']>['tags']>[number];
3
+ /**
4
+ * The definitions of `docs` and `no-docs` that the user's config lacks.
5
+ * Vitest throws on a tag it has no definition for, and Vite concatenates this
6
+ * list with the user's, so a tag the user defines is left to them.
7
+ */
8
+ export declare function docsTagDefinitions(userConfig: ViteUserConfig): TagDefinition[];
9
+ export {};
@@ -0,0 +1,14 @@
1
+ import { DOCS_TAG, NO_DOCS_TAG } from './stories-mode.js';
2
+ const DEFINITIONS = [
3
+ { name: DOCS_TAG, description: "describe-me: a story, when stories is 'tagged'" },
4
+ { name: NO_DOCS_TAG, description: 'describe-me: never a story' },
5
+ ];
6
+ /**
7
+ * The definitions of `docs` and `no-docs` that the user's config lacks.
8
+ * Vitest throws on a tag it has no definition for, and Vite concatenates this
9
+ * list with the user's, so a tag the user defines is left to them.
10
+ */
11
+ export function docsTagDefinitions(userConfig) {
12
+ const defined = new Set((userConfig.test?.tags ?? []).map((tag) => tag.name));
13
+ return DEFINITIONS.filter((tag) => !defined.has(tag.name));
14
+ }
@@ -9,16 +9,15 @@ export const domProfile = {
9
9
  },
10
10
  },
11
11
  setupFile: '@describe-me/vitest/setup-dom',
12
+ // Testing Library would otherwise unmount in its own afterEach, before the
13
+ // closing frame is taken. The adapter unmounts through the recorder instead.
14
+ env: { RTL_SKIP_AUTO_CLEANUP: 'true' },
12
15
  configFor({ userConfig, options }) {
13
16
  const build = options.styledComponentsBrowserBuild === false
14
17
  ? undefined
15
18
  : styledComponentsBrowserBuild(userConfig.root ?? process.cwd());
16
19
  const userTest = userConfig.test;
17
- const test = {
18
- // Testing Library would otherwise unmount in its own afterEach, before the
19
- // closing frame is taken. The adapter unmounts through the recorder instead.
20
- env: { RTL_SKIP_AUTO_CLEANUP: 'true' },
21
- };
20
+ const test = {};
22
21
  // Vitest stubs CSS imports by default, which would leave every imported
23
22
  // stylesheet out of the snapshots. An explicit choice by the user wins.
24
23
  if (userTest?.css === undefined) {
@@ -30,6 +30,12 @@ export interface EnvironmentProfile {
30
30
  renderModules: Record<NonNullable<DescribeMeOptions['framework']>, RenderModule>;
31
31
  /** The setup file that patches interactions and registers the recording hooks. */
32
32
  setupFile: string;
33
+ /**
34
+ * Variables the tests of a project with the setup file need. The plugin sets
35
+ * them per project and never in the root's `test.env`, which Vitest hands to
36
+ * every project, also to one without describe-me.
37
+ */
38
+ env: Record<string, string>;
33
39
  /** The rest of the environment's config, built in the plugin's `config` hook. */
34
40
  configFor(context: ProfileContext): ProfileConfig;
35
41
  }
package/dist/plugin.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ViteUserConfig } from 'vitest/config';
2
2
  import { type RenderFrameMode } from '@describe-me/core/types';
3
+ import type { StoriesMode } from './stories-mode.js';
3
4
  /** Where the tests run: Vitest browser mode, or a simulated DOM such as jsdom. */
4
5
  export type DescribeMeEnvironment = 'browser' | 'dom';
5
6
  export interface DescribeMeOptions {
@@ -39,6 +40,13 @@ export interface DescribeMeOptions {
39
40
  include?: string | string[];
40
41
  /** Test files never to record, as globs like `include`. Wins over `include`. Default: none. */
41
42
  exclude?: string | string[];
43
+ /**
44
+ * Which tests of the recorded files become stories. `'all'` records every
45
+ * test, `'tagged'` only those tagged `docs`, on the test, a `describe` or the
46
+ * file (`@module-tag docs`). A `no-docs` tag leaves a test out in both modes.
47
+ * The plugin defines both tags, which need Vitest 4.1. Default: `'all'`.
48
+ */
49
+ stories?: StoriesMode;
42
50
  /**
43
51
  * HTML the viewer adds to the start of the `<head>` of every replayed frame,
44
52
  * like Storybook's `preview-head.html`. It never reaches the test page.
package/dist/plugin.js CHANGED
@@ -1,9 +1,12 @@
1
+ import { resolve } from 'node:path';
1
2
  import { LIVE_PROTOCOL } from '@describe-me/core/types';
2
3
  import { adapterPackageRoot } from './adapter-package-root.js';
4
+ import { checkTagsSupported } from './check-tags-supported.js';
3
5
  import { compileGlobs } from './compile-globs.js';
4
6
  import { compileInclude } from './compile-include.js';
5
7
  import { defaultReporters } from './default-reporters.js';
6
8
  import { detectEnvironment } from './detect-environment.js';
9
+ import { docsTagDefinitions } from './docs-tag-definitions.js';
7
10
  import { isAdapterModule } from './is-adapter-module.js';
8
11
  import { LIVE_ENV } from './live-env.js';
9
12
  import { livePlugin } from './live-plugin.js';
@@ -13,7 +16,9 @@ import { projectModulePath } from './project-module-path.js';
13
16
  import { registerExports } from './register-exports.js';
14
17
  import DescribeMeReporter from './reporter.js';
15
18
  import { RUNTIME_OPTIONS_KEY } from './runtime-options.js';
19
+ import { setUpProject } from './set-up-project.js';
16
20
  import { validateRenderFrame } from './validate-render-frame.js';
21
+ import { validateStories } from './validate-stories.js';
17
22
  /**
18
23
  * One-line integration: `plugins: [describeMe()]`.
19
24
  *
@@ -32,6 +37,8 @@ export function describeMe(options = {}) {
32
37
  // Set by `config()`, which Vite runs before `configResolved` and `resolveId`.
33
38
  // Stays null while the plugin is disabled.
34
39
  let renderModule = null;
40
+ // The chosen profile, also set by `config()`, for the projects that skip it.
41
+ let projectProfile = null;
35
42
  // Known once Vite has resolved its config. Vitest always serves; a production
36
43
  // build that shares the config must not carry the export registration.
37
44
  let root = '';
@@ -55,15 +62,19 @@ export function describeMe(options = {}) {
55
62
  include: compileInclude(options.include),
56
63
  exclude: compileGlobs(options.exclude),
57
64
  renderFrame: validateRenderFrame(options.renderFrame),
65
+ stories: validateStories(options.stories),
58
66
  };
67
+ checkTagsSupported(runtimeOptions.stories, resolve(userConfig.root ?? process.cwd()));
59
68
  const provide = { [RUNTIME_OPTIONS_KEY]: runtimeOptions };
60
69
  const profile = await loadProfile(environment);
61
70
  renderModule = profile.renderModules[framework];
71
+ projectProfile = profile;
62
72
  const { config, setupWarnings } = profile.configFor({ userConfig, options, renderModule });
63
73
  const reporter = new DescribeMeReporter({
64
74
  outDir,
65
75
  include: options.include,
66
76
  exclude: options.exclude,
77
+ stories: runtimeOptions.stories,
67
78
  previewHead: options.previewHead,
68
79
  setupWarnings,
69
80
  });
@@ -73,9 +84,22 @@ export function describeMe(options = {}) {
73
84
  const reporters = userConfig.test?.reporters ? [reporter] : [...defaultReporters(), reporter];
74
85
  return {
75
86
  ...config,
76
- test: { setupFiles: [profile.setupFile], ...config.test, reporters, provide },
87
+ test: {
88
+ setupFiles: [profile.setupFile],
89
+ ...config.test,
90
+ reporters,
91
+ provide,
92
+ tags: docsTagDefinitions(userConfig),
93
+ },
77
94
  };
78
95
  },
96
+ // Runs for every project the plugin's Vite server serves, also those that
97
+ // share the root's server and so never reach `config()` (see setUpProject).
98
+ configureVitest({ project }) {
99
+ if (projectProfile !== null) {
100
+ setUpProject(project, projectProfile);
101
+ }
102
+ },
79
103
  async resolveId(source, importer) {
80
104
  if (!enabled || renderModule === null || source !== renderModule.original) {
81
105
  return null;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The major version of the `vitest` that the root resolves, whose rules the
3
- * live page names `each` and `for` rows by. Vitest exports its
4
- * `package.json`. Falls back to 5 when it cannot be read.
3
+ * live page names `each` and `for` rows by. Falls back to 5 when it cannot be
4
+ * read.
5
5
  */
6
6
  export declare function readLiveVitestMajor(root: string): number;
@@ -1,20 +1,10 @@
1
- import { readFileSync } from 'node:fs';
2
- import { createRequire } from 'node:module';
3
- import { join } from 'node:path';
1
+ import { readVitestVersion } from './read-vitest-version.js';
4
2
  const DEFAULT_MAJOR = 5;
5
3
  /**
6
4
  * The major version of the `vitest` that the root resolves, whose rules the
7
- * live page names `each` and `for` rows by. Vitest exports its
8
- * `package.json`. Falls back to 5 when it cannot be read.
5
+ * live page names `each` and `for` rows by. Falls back to 5 when it cannot be
6
+ * read.
9
7
  */
10
8
  export function readLiveVitestMajor(root) {
11
- try {
12
- const file = createRequire(join(root, 'package.json')).resolve('vitest/package.json');
13
- const { version } = JSON.parse(readFileSync(file, 'utf8'));
14
- const major = typeof version === 'string' ? Number.parseInt(version, 10) : Number.NaN;
15
- return Number.isInteger(major) && major > 0 ? major : DEFAULT_MAJOR;
16
- }
17
- catch {
18
- return DEFAULT_MAJOR;
19
- }
9
+ return readVitestVersion(root)?.major ?? DEFAULT_MAJOR;
20
10
  }
@@ -1,3 +1,6 @@
1
1
  import { type RuntimeOptions } from './runtime-options.js';
2
- /** Without the plugin nothing is provided, so a hand-wired setup keeps recording every test. */
2
+ /**
3
+ * Without the plugin nothing is provided, so a hand-wired setup keeps recording
4
+ * every test. The defaults also fill what an older plugin does not provide.
5
+ */
3
6
  export declare function readRuntimeOptions(): RuntimeOptions;
@@ -1,10 +1,16 @@
1
1
  import { inject } from 'vitest';
2
2
  import { RUNTIME_OPTIONS_KEY } from './runtime-options.js';
3
- /** Without the plugin nothing is provided, so a hand-wired setup keeps recording every test. */
3
+ const DEFAULTS = {
4
+ include: null,
5
+ exclude: [],
6
+ renderFrame: 'eager',
7
+ stories: 'all',
8
+ };
9
+ /**
10
+ * Without the plugin nothing is provided, so a hand-wired setup keeps recording
11
+ * every test. The defaults also fill what an older plugin does not provide.
12
+ */
4
13
  export function readRuntimeOptions() {
5
- return (inject(RUNTIME_OPTIONS_KEY) ?? {
6
- include: null,
7
- exclude: [],
8
- renderFrame: 'eager',
9
- });
14
+ const provided = inject(RUNTIME_OPTIONS_KEY);
15
+ return { ...DEFAULTS, ...provided };
10
16
  }
@@ -0,0 +1,10 @@
1
+ /** A Vitest release, as far as describe-me needs to tell them apart. */
2
+ export interface VitestVersion {
3
+ major: number;
4
+ minor: number;
5
+ }
6
+ /**
7
+ * The version of the `vitest` that the root resolves, or `null` when it cannot
8
+ * be read. Vitest exports its `package.json`.
9
+ */
10
+ export declare function readVitestVersion(root: string): VitestVersion | null;
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * The version of the `vitest` that the root resolves, or `null` when it cannot
6
+ * be read. Vitest exports its `package.json`.
7
+ */
8
+ export function readVitestVersion(root) {
9
+ try {
10
+ const file = createRequire(join(root, 'package.json')).resolve('vitest/package.json');
11
+ const { version } = JSON.parse(readFileSync(file, 'utf8'));
12
+ const [major, minor] = typeof version === 'string' ? version.split('.').map(Number) : [];
13
+ if (!Number.isInteger(major) || major <= 0 || !Number.isInteger(minor)) {
14
+ return null;
15
+ }
16
+ return { major, minor };
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
@@ -3,7 +3,8 @@ import type { RuntimeOptions } from './runtime-options.js';
3
3
  /**
4
4
  * The per-test lifecycle shared by every environment: begin a recording, take
5
5
  * a deferred render frame or else the closing frame, hand the frames to the
6
- * reporter via task.meta, and only then let adapters unmount. Teardown runs
6
+ * reporter via task.meta, and only then let adapters unmount. Which tests
7
+ * record follows the file globs and then the test's tags. Teardown runs
7
8
  * for unrecorded tests too, because Testing Library's auto-cleanup is off in
8
9
  * jsdom. Concurrent tests would share the one recorder, so they are not
9
10
  * recorded. The hooks destructure the context, because Vitest rejects any
@@ -1,10 +1,12 @@
1
1
  import { afterEach, beforeEach } from 'vitest';
2
2
  import { META_KEY, recorder } from '@describe-me/core';
3
3
  import { fileFilter } from './file-filter.js';
4
+ import { tagFilter } from './tag-filter.js';
4
5
  /**
5
6
  * The per-test lifecycle shared by every environment: begin a recording, take
6
7
  * a deferred render frame or else the closing frame, hand the frames to the
7
- * reporter via task.meta, and only then let adapters unmount. Teardown runs
8
+ * reporter via task.meta, and only then let adapters unmount. Which tests
9
+ * record follows the file globs and then the test's tags. Teardown runs
8
10
  * for unrecorded tests too, because Testing Library's auto-cleanup is off in
9
11
  * jsdom. Concurrent tests would share the one recorder, so they are not
10
12
  * recorded. The hooks destructure the context, because Vitest rejects any
@@ -12,6 +14,7 @@ import { fileFilter } from './file-filter.js';
12
14
  */
13
15
  export function registerRecordingHooks(options, aroundWait) {
14
16
  const isRecordedFile = fileFilter(options);
17
+ const isRecordedTags = tagFilter(options.stories);
15
18
  let warnedConcurrent = false;
16
19
  recorder.configure({ renderFrame: options.renderFrame, aroundWait });
17
20
  beforeEach(({ task }) => {
@@ -22,7 +25,8 @@ export function registerRecordingHooks(options, aroundWait) {
22
25
  }
23
26
  return;
24
27
  }
25
- if (isRecordedFile(task.file.name)) {
28
+ // Vitest before 4.1 has no tags, so every test counts as untagged.
29
+ if (isRecordedFile(task.file.name) && isRecordedTags(task.tags ?? [])) {
26
30
  recorder.begin();
27
31
  }
28
32
  });
@@ -1,4 +1,5 @@
1
1
  import type { Reporter, TestModule, Vitest } from 'vitest/node';
2
+ import type { StoriesMode } from './stories-mode.js';
2
3
  export interface DescribeMeReporterOptions {
3
4
  /** Output directory, relative to the Vitest root. Default: `.describe-me` */
4
5
  outDir?: string;
@@ -13,6 +14,11 @@ export interface DescribeMeReporterOptions {
13
14
  * `include`. Default: none.
14
15
  */
15
16
  exclude?: string | string[];
17
+ /**
18
+ * Which tests appear in the manifest: `'all'`, or only those tagged `docs`.
19
+ * A test tagged `no-docs` never does. Default: `'all'`.
20
+ */
21
+ stories?: StoriesMode;
16
22
  /**
17
23
  * HTML the viewer adds to the start of the `<head>` of every replayed frame,
18
24
  * like Storybook's `preview-head.html`. It never reaches the test page.
@@ -25,6 +31,7 @@ export interface DescribeMeReporterOptions {
25
31
  export default class DescribeMeReporter implements Reporter {
26
32
  private readonly outDirOption;
27
33
  private readonly isRecordedFile;
34
+ private readonly isRecordedTags;
28
35
  private readonly emptyInclude;
29
36
  private readonly previewHead;
30
37
  private readonly generator;
@@ -44,7 +51,8 @@ export default class DescribeMeReporter implements Reporter {
44
51
  * A filtered run (`vitest run Button.test.tsx`) must not wipe the other
45
52
  * modules from the manifest, so start from whatever the last run wrote.
46
53
  * Modules whose file is gone or no longer selected by `include` / `exclude`
47
- * are dropped, and so are the empty tests that 0.4 still wrote.
54
+ * are dropped, and so are tests that `stories` no longer selects and the
55
+ * empty tests that 0.4 still wrote.
48
56
  */
49
57
  private seedFromPreviousRun;
50
58
  /**
@@ -54,8 +62,8 @@ export default class DescribeMeReporter implements Reporter {
54
62
  */
55
63
  private withStableIds;
56
64
  /**
57
- * A test skipped in this run keeps its previous entry, so a filtered run does
58
- * not erase documentation.
65
+ * A test skipped in this run keeps its previous entry with its current tags,
66
+ * so a filtered run does not erase documentation.
59
67
  */
60
68
  onTestModuleEnd(module: TestModule): void;
61
69
  onTestRunEnd(): Promise<void>;
package/dist/reporter.js CHANGED
@@ -18,6 +18,7 @@ import { readGenerator } from './read-generator.js';
18
18
  import { SnapshotStore } from './snapshot-store.js';
19
19
  import { stableTestIds } from './stable-test-ids.js';
20
20
  import { StyleStore } from './style-store.js';
21
+ import { tagFilter } from './tag-filter.js';
21
22
  import { testPath } from './test-path.js';
22
23
  function toPosix(moduleId) {
23
24
  return moduleId.split(sep).join('/');
@@ -25,9 +26,16 @@ function toPosix(moduleId) {
25
26
  function recordOf(tc) {
26
27
  return tc.meta()[META_KEY];
27
28
  }
29
+ /** The test's tags for the manifest, where `undefined` leaves them out of the JSON. */
30
+ function tagsOf(tc) {
31
+ // Vitest before 4.1 has no tags.
32
+ const tags = tc.tags ?? [];
33
+ return tags.length > 0 ? tags : undefined;
34
+ }
28
35
  export default class DescribeMeReporter {
29
36
  outDirOption;
30
37
  isRecordedFile;
38
+ isRecordedTags;
31
39
  emptyInclude;
32
40
  previewHead;
33
41
  generator;
@@ -44,6 +52,7 @@ export default class DescribeMeReporter {
44
52
  this.outDirOption = options.outDir ?? '.describe-me';
45
53
  const include = compileInclude(options.include);
46
54
  this.isRecordedFile = fileFilter({ include, exclude: compileGlobs(options.exclude) });
55
+ this.isRecordedTags = tagFilter(options.stories ?? 'all');
47
56
  this.emptyInclude = include !== null && include.length === 0;
48
57
  // A blank head, such as an empty preview-head.html, is the same as none.
49
58
  this.previewHead = options.previewHead?.trim() ? options.previewHead : undefined;
@@ -76,7 +85,8 @@ export default class DescribeMeReporter {
76
85
  * A filtered run (`vitest run Button.test.tsx`) must not wipe the other
77
86
  * modules from the manifest, so start from whatever the last run wrote.
78
87
  * Modules whose file is gone or no longer selected by `include` / `exclude`
79
- * are dropped, and so are the empty tests that 0.4 still wrote.
88
+ * are dropped, and so are tests that `stories` no longer selects and the
89
+ * empty tests that 0.4 still wrote.
80
90
  */
81
91
  seedFromPreviousRun() {
82
92
  const file = join(this.outDir, 'manifest.json');
@@ -86,7 +96,7 @@ export default class DescribeMeReporter {
86
96
  try {
87
97
  const previous = JSON.parse(readFileSync(file, 'utf8'));
88
98
  for (const mod of previous.modules ?? []) {
89
- const tests = this.withStableIds(mod).filter((test) => isRecordedTest(test));
99
+ const tests = this.withStableIds(mod).filter((test) => isRecordedTest(test) && this.isRecordedTags(test.tags ?? []));
90
100
  if (tests.length > 0 &&
91
101
  this.isRecordedModule(mod.id) &&
92
102
  existsSync(resolve(this.root, mod.id))) {
@@ -113,8 +123,8 @@ export default class DescribeMeReporter {
113
123
  });
114
124
  }
115
125
  /**
116
- * A test skipped in this run keeps its previous entry, so a filtered run does
117
- * not erase documentation.
126
+ * A test skipped in this run keeps its previous entry with its current tags,
127
+ * so a filtered run does not erase documentation.
118
128
  */
119
129
  onTestModuleEnd(module) {
120
130
  const id = relative(this.root, module.moduleId);
@@ -198,6 +208,11 @@ export default class DescribeMeReporter {
198
208
  }
199
209
  /** Decided before any snapshot is written, so a dropped test writes no file. */
200
210
  manifestTestOf(tc, stableId, previous) {
211
+ // Before the skipped branch, so a test tagged out since the last run does
212
+ // not survive a filtered run on its old frames. Vitest before 4.1 has no tags.
213
+ if (!this.isRecordedTags(tc.tags ?? [])) {
214
+ return undefined;
215
+ }
201
216
  const record = recordOf(tc);
202
217
  if (record && isRecordedTest(record)) {
203
218
  return this.toManifestTest(tc, stableId, record);
@@ -205,7 +220,13 @@ export default class DescribeMeReporter {
205
220
  if (tc.result().state !== 'skipped') {
206
221
  return undefined;
207
222
  }
208
- return previous?.tests.find((test) => test.id === stableId && isRecordedTest(test));
223
+ const kept = previous?.tests.find((test) => test.id === stableId && isRecordedTest(test));
224
+ if (kept === undefined) {
225
+ return undefined;
226
+ }
227
+ // Tags belong to the test, not to the run, and Vitest knows them for a
228
+ // skipped test too. The viewer shows and filters by them.
229
+ return { ...kept, tags: tagsOf(tc) };
209
230
  }
210
231
  toManifestTest(tc, stableId, record) {
211
232
  const result = tc.result();
@@ -223,6 +244,7 @@ export default class DescribeMeReporter {
223
244
  name: tc.name,
224
245
  path: testPath(tc),
225
246
  fullName: tc.fullName,
247
+ tags: tagsOf(tc),
226
248
  state: result.state ?? 'pending',
227
249
  duration: tc.diagnostic()?.duration,
228
250
  errors: result.errors?.map((error) => ({ message: error.message, stack: error.stack })),
@@ -5,6 +5,7 @@
5
5
  * only types.
6
6
  */
7
7
  import type { RenderFrameMode } from '@describe-me/core/types';
8
+ import type { StoriesMode } from './stories-mode.js';
8
9
  export interface CompiledGlob {
9
10
  source: string;
10
11
  flags: string;
@@ -18,6 +19,7 @@ export interface RuntimeOptions {
18
19
  /** Wins over `include`. */
19
20
  exclude: CompiledGlob[];
20
21
  renderFrame: RenderFrameMode;
22
+ stories: StoriesMode;
21
23
  }
22
24
  export declare const RUNTIME_OPTIONS_KEY: "describe-me";
23
25
  declare module 'vitest' {
@@ -0,0 +1,12 @@
1
+ import type { TestProject } from 'vitest/node';
2
+ import type { EnvironmentProfile } from './environment-profile.js';
3
+ /**
4
+ * Gives a project the profile's setup file, when it lacks it, and the
5
+ * profile's variables. In Vitest 5 a project that shares the root's Vite
6
+ * server inherits the root config as it was before the plugin's `config`
7
+ * hook, so it gets the render redirect but not the setup file, and records
8
+ * nothing. The file goes last, as in the root config. Only projects with the
9
+ * setup file get the variables, so one without describe-me keeps Testing
10
+ * Library's own cleanup.
11
+ */
12
+ export declare function setUpProject(project: TestProject, profile: EnvironmentProfile): void;
@@ -0,0 +1,39 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'node:path';
4
+ function resolveFrom(root, specifier) {
5
+ try {
6
+ return createRequire(join(root, 'package.json')).resolve(specifier);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ function realPathOf(file) {
13
+ try {
14
+ return realpathSync(file);
15
+ }
16
+ catch {
17
+ return file;
18
+ }
19
+ }
20
+ /**
21
+ * Gives a project the profile's setup file, when it lacks it, and the
22
+ * profile's variables. In Vitest 5 a project that shares the root's Vite
23
+ * server inherits the root config as it was before the plugin's `config`
24
+ * hook, so it gets the render redirect but not the setup file, and records
25
+ * nothing. The file goes last, as in the root config. Only projects with the
26
+ * setup file get the variables, so one without describe-me keeps Testing
27
+ * Library's own cleanup.
28
+ */
29
+ export function setUpProject(project, profile) {
30
+ const file = resolveFrom(project.config.root, profile.setupFile);
31
+ if (file === null) {
32
+ return;
33
+ }
34
+ const real = realPathOf(file);
35
+ if (!project.config.setupFiles.some((entry) => realPathOf(entry) === real)) {
36
+ project.config.setupFiles.push(file);
37
+ }
38
+ project.config.env = { ...project.config.env, ...profile.env };
39
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `stories` option and its two tags. Kept apart from `runtime-options.ts`,
3
+ * because the public types import this file and must not carry that file's
4
+ * augmentation of Vitest's `ProvidedContext`. The test runtime imports it, so
5
+ * no Node imports. The tags live in core, which the viewer reads too.
6
+ */
7
+ export { DOCS_TAG, NO_DOCS_TAG } from '@describe-me/core/types';
8
+ /** Which tests of the recorded files become stories: every one, or only those tagged `docs`. */
9
+ export type StoriesMode = 'all' | 'tagged';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The `stories` option and its two tags. Kept apart from `runtime-options.ts`,
3
+ * because the public types import this file and must not carry that file's
4
+ * augmentation of Vitest's `ProvidedContext`. The test runtime imports it, so
5
+ * no Node imports. The tags live in core, which the viewer reads too.
6
+ */
7
+ export { DOCS_TAG, NO_DOCS_TAG } from '@describe-me/core/types';
@@ -0,0 +1,7 @@
1
+ import { type StoriesMode } from './stories-mode.js';
2
+ /**
3
+ * Decides from a test's tags, its own and those it inherits from its suites
4
+ * and its file, whether it becomes a story. `no-docs` wins over `docs`. The
5
+ * test runtime imports this file, so no Node imports.
6
+ */
7
+ export declare function tagFilter(stories: StoriesMode): (tags: readonly string[]) => boolean;
@@ -0,0 +1,14 @@
1
+ import { DOCS_TAG, NO_DOCS_TAG } from './stories-mode.js';
2
+ /**
3
+ * Decides from a test's tags, its own and those it inherits from its suites
4
+ * and its file, whether it becomes a story. `no-docs` wins over `docs`. The
5
+ * test runtime imports this file, so no Node imports.
6
+ */
7
+ export function tagFilter(stories) {
8
+ return (tags) => {
9
+ if (tags.includes(NO_DOCS_TAG)) {
10
+ return false;
11
+ }
12
+ return stories === 'all' || tags.includes(DOCS_TAG);
13
+ };
14
+ }
@@ -0,0 +1,3 @@
1
+ import type { StoriesMode } from './stories-mode.js';
2
+ /** The plugin's `stories`, checked before any test runs. */
3
+ export declare function validateStories(value: unknown): StoriesMode;
@@ -0,0 +1,11 @@
1
+ import { inspect } from 'node:util';
2
+ /** The plugin's `stories`, checked before any test runs. */
3
+ export function validateStories(value) {
4
+ if (value === undefined) {
5
+ return 'all';
6
+ }
7
+ if (value === 'all' || value === 'tagged') {
8
+ return value;
9
+ }
10
+ throw new Error(`describe-me: stories expects 'all' or 'tagged', got ${inspect(value)}`);
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@describe-me/vitest",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Living component documentation from your Vitest tests: the Vitest plugin, setup files and reporter of describe-me",
5
5
  "keywords": [
6
6
  "vitest",
@@ -71,13 +71,13 @@
71
71
  "dependencies": {
72
72
  "picomatch": "^4.0.3",
73
73
  "std-env": "^4.0.0",
74
- "@describe-me/core": "^0.6.0"
74
+ "@describe-me/core": "^0.7.0"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "vitest": "^4.0.0 || ^5.0.0",
78
78
  "typescript": "^5.0.0 || ^6.0.0",
79
79
  "@testing-library/user-event": "^14.0.0",
80
- "describe-me": "^0.6.0"
80
+ "describe-me": "^0.7.0"
81
81
  },
82
82
  "peerDependenciesMeta": {
83
83
  "typescript": {