@describe-me/vitest 0.5.0 → 0.5.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/README.md CHANGED
@@ -10,6 +10,9 @@ reporter that writes `.describe-me/`.
10
10
  pnpm add -D @describe-me/vitest @describe-me/react describe-me
11
11
  ```
12
12
 
13
+ All describe-me packages must be on the same version
14
+ ([Add it to your project](https://github.com/grzehub/describe-me#add-it-to-your-project)).
15
+
13
16
  ## Usage
14
17
 
15
18
  One line in the Vitest config registers the setup file and the reporter, and
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
3
  import { dirname, extname, join, resolve, sep } from 'node:path';
4
4
  import { ASSET_URL_PREFIX } from '@describe-me/core/types';
5
+ import { collectableFiles } from './collectable-files.js';
5
6
  import { cssAssetReferences } from './css-asset-references.js';
6
7
  import { rewriteCssReferences } from './rewrite-css-references.js';
7
8
  import { rewriteHeadReferences } from './rewrite-head-references.js';
@@ -233,7 +234,7 @@ export class AssetStore {
233
234
  * re-run were rewritten by an earlier run.
234
235
  */
235
236
  collectGarbage(files, inlineTexts = []) {
236
- const stored = readdirSync(this.dir);
237
+ const stored = collectableFiles(this.dir);
237
238
  if (stored.length === 0) {
238
239
  return;
239
240
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The names of the entries in `dir` that garbage collection may delete. No
3
+ * store writes a folder, so a folder belongs to someone else and is left out.
4
+ */
5
+ export declare function collectableFiles(dir: string): string[];
@@ -0,0 +1,10 @@
1
+ import { readdirSync } from 'node:fs';
2
+ /**
3
+ * The names of the entries in `dir` that garbage collection may delete. No
4
+ * store writes a folder, so a folder belongs to someone else and is left out.
5
+ */
6
+ export function collectableFiles(dir) {
7
+ return readdirSync(dir, { withFileTypes: true })
8
+ .filter((entry) => !entry.isDirectory())
9
+ .map((entry) => entry.name);
10
+ }
package/dist/plugin.d.ts CHANGED
@@ -25,7 +25,9 @@ export interface DescribeMeOptions {
25
25
  * DOM environments only. Load the browser build of styled-components (v5+)
26
26
  * instead of the Node one, whose `createGlobalStyle` never inserts its CSS on
27
27
  * the client: global resets and fonts would be missing from every snapshot.
28
- * Applied when styled-components is installed. Default: true.
28
+ * Applied when styled-components is installed. For 6.0 to 6.3 it also points
29
+ * `tslib` at the `tslib.es6.mjs` they resolve, so the optimizer keeps its
30
+ * exports. Default: true.
29
31
  */
30
32
  styledComponentsBrowserBuild?: boolean;
31
33
  /**
package/dist/plugin.js CHANGED
@@ -4,7 +4,7 @@ import { projectModulePath } from './project-module-path.js';
4
4
  import { registerExports } from './register-exports.js';
5
5
  import DescribeMeReporter from './reporter.js';
6
6
  import { RUNTIME_OPTIONS_KEY } from './runtime-options.js';
7
- import { styledComponentsBrowserBuild } from './styled-components-browser-build.js';
7
+ import { styledComponentsBrowserBuild, } from './styled-components-browser-build.js';
8
8
  import { validateRenderFrame } from './validate-render-frame.js';
9
9
  /** For each framework and environment: the render module tests import, and our adapter that wraps it. */
10
10
  const RENDER_MODULES = {
@@ -49,7 +49,7 @@ function browserConfig(renderModule) {
49
49
  },
50
50
  };
51
51
  }
52
- function domConfig(userConfig, options) {
52
+ function domConfig(userConfig, styledComponents) {
53
53
  const userTest = userConfig.test;
54
54
  const test = {
55
55
  setupFiles: ['@describe-me/vitest/setup-dom'],
@@ -65,14 +65,19 @@ function domConfig(userConfig, options) {
65
65
  else if (userTest.css === false) {
66
66
  console.warn('describe-me: `test.css` is false, so imported stylesheets are stubbed and will be missing from the snapshots.');
67
67
  }
68
- const styledComponents = options.styledComponentsBrowserBuild === false
69
- ? undefined
70
- : styledComponentsBrowserBuild(userConfig.root ?? process.cwd());
71
68
  if (!styledComponents) {
72
69
  return { test };
73
70
  }
74
71
  console.info('describe-me: using the browser build of styled-components, for createGlobalStyle');
75
- return { resolve: styledComponents.resolve, test: { ...test, ...styledComponents.test } };
72
+ const { config } = styledComponents;
73
+ return { resolve: config.resolve, test: { ...test, ...config.test } };
74
+ }
75
+ /** Looked up before the reporter is created, which needs its warning. */
76
+ function styledComponentsFor(environment, userConfig, options) {
77
+ if (environment !== 'dom' || options.styledComponentsBrowserBuild === false) {
78
+ return undefined;
79
+ }
80
+ return styledComponentsBrowserBuild(userConfig.root ?? process.cwd());
76
81
  }
77
82
  /**
78
83
  * One-line integration: `plugins: [describeMe()]`.
@@ -110,16 +115,21 @@ export function describeMe(options = {}) {
110
115
  renderFrame: validateRenderFrame(options.renderFrame),
111
116
  };
112
117
  const provide = { [RUNTIME_OPTIONS_KEY]: runtimeOptions };
118
+ const styledComponents = styledComponentsFor(environment, userConfig, options);
119
+ const setupWarnings = styledComponents?.warning ? [styledComponents.warning] : [];
113
120
  const reporter = new DescribeMeReporter({
114
121
  outDir,
115
122
  include: options.include,
116
123
  exclude: options.exclude,
117
124
  previewHead: options.previewHead,
125
+ setupWarnings,
118
126
  });
119
127
  // Vite concatenates arrays when merging, so only add `default` when the
120
128
  // user has not chosen their own reporters; otherwise they would lose it.
121
129
  const reporters = userConfig.test?.reporters ? [reporter] : ['default', reporter];
122
- const config = environment === 'browser' ? browserConfig(renderModule) : domConfig(userConfig, options);
130
+ const config = environment === 'browser'
131
+ ? browserConfig(renderModule)
132
+ : domConfig(userConfig, styledComponents);
123
133
  return { ...config, test: { ...config.test, reporters, provide } };
124
134
  },
125
135
  async resolveId(source, importer) {
@@ -1,8 +1,9 @@
1
1
  import type { Manifest } from '@describe-me/core/types';
2
2
  /**
3
- * Warn, one line per kind, about what the manifest could not document. Prints
4
- * nothing when everything is named, documented and loadable, every font is
5
- * loaded and no stylesheet comes from a remote host. The viewer shows the same
6
- * list in its header.
3
+ * Warn about what the manifest could not document, one line per kind. Setup
4
+ * warnings come first, one line each, because they can explain failing tests.
5
+ * Prints nothing when the setup is sound, everything is named, documented and
6
+ * loadable, every font is loaded and no stylesheet comes from a remote host.
7
+ * The viewer shows the same list in its header.
7
8
  */
8
9
  export declare function printDiagnostics(manifest: Manifest): void;
@@ -13,13 +13,17 @@ function plural(count, one, many) {
13
13
  return `${count} ${count === 1 ? one : many}`;
14
14
  }
15
15
  /**
16
- * Warn, one line per kind, about what the manifest could not document. Prints
17
- * nothing when everything is named, documented and loadable, every font is
18
- * loaded and no stylesheet comes from a remote host. The viewer shows the same
19
- * list in its header.
16
+ * Warn about what the manifest could not document, one line per kind. Setup
17
+ * warnings come first, one line each, because they can explain failing tests.
18
+ * Prints nothing when the setup is sound, everything is named, documented and
19
+ * loadable, every font is loaded and no stylesheet comes from a remote host.
20
+ * The viewer shows the same list in its header.
20
21
  */
21
22
  export function printDiagnostics(manifest) {
22
- const { anonymous, undocumented, assetsMissing, fontsMissing, remoteStylesheets } = manifestDiagnostics(manifest);
23
+ const { setupWarnings, anonymous, undocumented, assetsMissing, fontsMissing, remoteStylesheets } = manifestDiagnostics(manifest);
24
+ for (const warning of setupWarnings) {
25
+ console.warn(`describe-me: ${warning}`);
26
+ }
23
27
  if (anonymous.length > 0) {
24
28
  console.warn(`describe-me: ${plural(anonymous.length, 'test renders', 'tests render')} an anonymous component; export it from a project module or give it a displayName`);
25
29
  }
@@ -0,0 +1,7 @@
1
+ import type { ManifestGenerator } from '@describe-me/core/types';
2
+ /**
3
+ * The name and version of this package, for the manifest's `generator`, or
4
+ * `undefined` when its package.json cannot be read, so the manifest is still
5
+ * written.
6
+ */
7
+ export declare function readGenerator(): ManifestGenerator | undefined;
@@ -0,0 +1,21 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /**
3
+ * The name and version of this package, for the manifest's `generator`, or
4
+ * `undefined` when its package.json cannot be read, so the manifest is still
5
+ * written.
6
+ */
7
+ export function readGenerator() {
8
+ try {
9
+ // The compiled file sits in `dist/`, one level below the package root, and
10
+ // `exports` does not list `./package.json`.
11
+ const file = new URL('../package.json', import.meta.url);
12
+ const { name, version } = JSON.parse(readFileSync(file, 'utf8'));
13
+ if (typeof name !== 'string' || typeof version !== 'string') {
14
+ return undefined;
15
+ }
16
+ return { name, version };
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
@@ -18,11 +18,15 @@ export interface DescribeMeReporterOptions {
18
18
  * Project files it links are copied into the output directory. Default: none.
19
19
  */
20
20
  previewHead?: string;
21
+ /** Problems in the project's setup, filled by the plugin. Default: none. */
22
+ setupWarnings?: string[];
21
23
  }
22
24
  export default class DescribeMeReporter implements Reporter {
23
25
  private readonly outDirOption;
24
26
  private readonly isRecordedFile;
25
27
  private readonly previewHead;
28
+ private readonly generator;
29
+ private readonly setupWarnings;
26
30
  private root;
27
31
  private outDir;
28
32
  private snapshots;
package/dist/reporter.js CHANGED
@@ -13,6 +13,7 @@ import { fileFilter } from './file-filter.js';
13
13
  import { FontAudit } from './font-audit.js';
14
14
  import { isRecordedTest } from './is-recorded-test.js';
15
15
  import { printDiagnostics } from './print-diagnostics.js';
16
+ import { readGenerator } from './read-generator.js';
16
17
  import { SnapshotStore } from './snapshot-store.js';
17
18
  import { stableTestIds } from './stable-test-ids.js';
18
19
  import { StyleStore } from './style-store.js';
@@ -27,6 +28,8 @@ export default class DescribeMeReporter {
27
28
  outDirOption;
28
29
  isRecordedFile;
29
30
  previewHead;
31
+ generator;
32
+ setupWarnings;
30
33
  root = process.cwd();
31
34
  outDir = '';
32
35
  snapshots;
@@ -42,6 +45,8 @@ export default class DescribeMeReporter {
42
45
  });
43
46
  // A blank head, such as an empty preview-head.html, is the same as none.
44
47
  this.previewHead = options.previewHead?.trim() ? options.previewHead : undefined;
48
+ this.generator = readGenerator();
49
+ this.setupWarnings = options.setupWarnings ?? [];
45
50
  }
46
51
  onInit(vitest) {
47
52
  this.root = vitest.config.root;
@@ -136,6 +141,7 @@ export default class DescribeMeReporter {
136
141
  const assetsMissing = this.assets.missing();
137
142
  const manifest = {
138
143
  version: 1,
144
+ generator: this.generator,
139
145
  generatedAt: new Date().toISOString(),
140
146
  root: this.root,
141
147
  modules,
@@ -150,6 +156,10 @@ export default class DescribeMeReporter {
150
156
  const styleFiles = this.styles.collectGarbage(snapshotFiles);
151
157
  this.assets.collectGarbage([...snapshotFiles, ...styleFiles], head === undefined ? [] : [head]);
152
158
  this.auditFonts(manifest);
159
+ // Never seeded from the last run: the plugin checks the setup on every start.
160
+ if (this.setupWarnings.length > 0) {
161
+ manifest.setupWarnings = this.setupWarnings;
162
+ }
153
163
  writeFileSync(join(this.outDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
154
164
  printDiagnostics(manifest);
155
165
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { collectableFiles } from './collectable-files.js';
4
5
  /**
5
6
  * Content-addressed store for the serialized DOM of every frame, living in
6
7
  * `<outDir>/snapshots`. Identical DOM across tests or runs is written once.
@@ -27,7 +28,7 @@ export class SnapshotStore {
27
28
  for (const path of referenced) {
28
29
  keep.add(path.replace('snapshots/', ''));
29
30
  }
30
- for (const file of readdirSync(this.dir)) {
31
+ for (const file of collectableFiles(this.dir)) {
31
32
  if (!keep.has(file)) {
32
33
  rmSync(join(this.dir, file), { force: true });
33
34
  }
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { STYLE_URL_PREFIX } from '@describe-me/core/types';
5
+ import { collectableFiles } from './collectable-files.js';
5
6
  import { splitStylesheet } from './split-stylesheet.js';
6
7
  /** Shorter sheets stay inline, where they cost less than a reference and a file of their own. */
7
8
  const MIN_EXTRACTED_LENGTH = 256;
@@ -54,7 +55,7 @@ export class StyleStore {
54
55
  }
55
56
  }
56
57
  const kept = [];
57
- for (const file of readdirSync(this.dir)) {
58
+ for (const file of collectableFiles(this.dir)) {
58
59
  if (keep.has(file)) {
59
60
  kept.push(`styles/${file}`);
60
61
  }
@@ -1,4 +1,10 @@
1
1
  import type { ViteUserConfig } from 'vitest/config';
2
+ /** The config that loads the browser build, and a setup problem found on the way. */
3
+ export interface StyledComponentsBrowserBuild {
4
+ config: ViteUserConfig;
5
+ /** Set when the build's tslib cannot be aliased, so it may fail to load. */
6
+ warning?: string;
7
+ }
2
8
  /**
3
9
  * The Vite and Vitest config that makes a DOM environment load the browser
4
10
  * build of styled-components, or undefined when the project does not use
@@ -11,5 +17,9 @@ import type { ViteUserConfig } from 'vitest/config';
11
17
  * styled-components through Node, which ignores the alias, and would get a
12
18
  * second instance whose styles its serializer cannot see. Bundling both
13
19
  * together gives them one.
20
+ *
21
+ * The optimizer bundles tslib's UMD file without its default export, which the
22
+ * browser build of 6.0 to 6.3 destructures. An alias is the only resolution the
23
+ * optimizer applies, so `tslib` is aliased to its ES module for the whole config.
14
24
  */
15
- export declare function styledComponentsBrowserBuild(root: string): ViteUserConfig | undefined;
25
+ export declare function styledComponentsBrowserBuild(root: string): StyledComponentsBrowserBuild | undefined;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { dirname, join, resolve } from 'node:path';
4
+ import { styledComponentsTslib } from './styled-components-tslib.js';
4
5
  function resolverFor(root) {
5
6
  // createRequire wants an absolute file inside the directory; it does not have to exist.
6
7
  const requireFromRoot = createRequire(join(resolve(root), 'package.json'));
@@ -41,6 +42,10 @@ function browserModule(pkg) {
41
42
  * styled-components through Node, which ignores the alias, and would get a
42
43
  * second instance whose styles its serializer cannot see. Bundling both
43
44
  * together gives them one.
45
+ *
46
+ * The optimizer bundles tslib's UMD file without its default export, which the
47
+ * browser build of 6.0 to 6.3 destructures. An alias is the only resolution the
48
+ * optimizer applies, so `tslib` is aliased to its ES module for the whole config.
44
49
  */
45
50
  export function styledComponentsBrowserBuild(root) {
46
51
  const resolveFromRoot = resolverFor(root);
@@ -62,8 +67,17 @@ export function styledComponentsBrowserBuild(root) {
62
67
  if (resolveFromRoot('jest-styled-components')) {
63
68
  include.push('jest-styled-components');
64
69
  }
65
- return {
66
- resolve: { alias: [{ find: /^styled-components$/, replacement }] },
70
+ const alias = [{ find: /^styled-components$/, replacement }];
71
+ const tslib = styledComponentsTslib(manifest);
72
+ if (tslib.kind === 'alias') {
73
+ alias.push({ find: /^tslib$/, replacement: tslib.replacement });
74
+ }
75
+ const config = {
76
+ resolve: { alias },
67
77
  test: { deps: { optimizer: { client: { enabled: true, include } } } },
68
78
  };
79
+ if (tslib.kind === 'warning') {
80
+ return { config, warning: tslib.warning };
81
+ }
82
+ return { config };
69
83
  }
@@ -0,0 +1,16 @@
1
+ /** How the browser build of styled-components gets its tslib helpers. */
2
+ export type StyledComponentsTslib = {
3
+ kind: 'untouched';
4
+ } | {
5
+ kind: 'alias';
6
+ replacement: string;
7
+ } | {
8
+ kind: 'warning';
9
+ warning: string;
10
+ };
11
+ /**
12
+ * Where `tslib` should point for the styled-components whose `package.json`
13
+ * is given. Only 6.0 to 6.3 import tslib, and only tslib 2.5.3 and later ship
14
+ * the `tslib.es6.mjs` that keeps its exports intact through the optimizer.
15
+ */
16
+ export declare function styledComponentsTslib(styledComponentsManifest: string): StyledComponentsTslib;
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ function readPackage(manifest) {
5
+ return JSON.parse(readFileSync(manifest, 'utf8'));
6
+ }
7
+ function resolveTslib(styledComponentsManifest) {
8
+ try {
9
+ return createRequire(styledComponentsManifest).resolve('tslib/package.json');
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ /**
16
+ * Where `tslib` should point for the styled-components whose `package.json`
17
+ * is given. Only 6.0 to 6.3 import tslib, and only tslib 2.5.3 and later ship
18
+ * the `tslib.es6.mjs` that keeps its exports intact through the optimizer.
19
+ */
20
+ export function styledComponentsTslib(styledComponentsManifest) {
21
+ const styledComponents = readPackage(styledComponentsManifest);
22
+ if (!styledComponents.dependencies?.tslib) {
23
+ return { kind: 'untouched' };
24
+ }
25
+ // Resolved from styled-components itself, so a nested or pnpm-linked tslib
26
+ // wins over whatever the project hoists.
27
+ const tslibManifest = resolveTslib(styledComponentsManifest);
28
+ if (!tslibManifest) {
29
+ return { kind: 'untouched' };
30
+ }
31
+ const replacement = join(dirname(tslibManifest), 'tslib.es6.mjs');
32
+ if (existsSync(replacement)) {
33
+ return { kind: 'alias', replacement };
34
+ }
35
+ const tslib = readPackage(tslibManifest);
36
+ return {
37
+ kind: 'warning',
38
+ warning: `styled-components ${styledComponents.version} resolves tslib ${tslib.version}, which has no tslib.es6.mjs, so its browser build may fail to load ("Cannot destructure property '__extends'"). Upgrade styled-components to 6.1.10 or later, or override tslib to 2.5.3 or later.`,
39
+ };
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@describe-me/vitest",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Vitest integration for describe-me: plugin, setup file and reporter",
5
5
  "keywords": [
6
6
  "vitest",
@@ -59,12 +59,13 @@
59
59
  ],
60
60
  "dependencies": {
61
61
  "picomatch": "^4.0.3",
62
- "@describe-me/core": "^0.5.0"
62
+ "@describe-me/core": "^0.5.1"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "vitest": "^4.0.0 || ^5.0.0",
66
66
  "typescript": "^5.0.0 || ^6.0.0",
67
- "@testing-library/user-event": "^14.0.0"
67
+ "@testing-library/user-event": "^14.0.0",
68
+ "describe-me": "^0.5.1"
68
69
  },
69
70
  "peerDependenciesMeta": {
70
71
  "typescript": {
@@ -72,6 +73,9 @@
72
73
  },
73
74
  "@testing-library/user-event": {
74
75
  "optional": true
76
+ },
77
+ "describe-me": {
78
+ "optional": true
75
79
  }
76
80
  },
77
81
  "devDependencies": {