@describe-me/vitest 0.4.0 → 0.5.0-next.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 +22 -5
- package/dist/adapter-package-root.d.ts +7 -0
- package/dist/adapter-package-root.js +40 -0
- package/dist/asset-store.d.ts +62 -12
- package/dist/asset-store.js +226 -23
- package/dist/compile-globs.d.ts +6 -0
- package/dist/compile-globs.js +20 -0
- package/dist/css-asset-references.d.ts +5 -0
- package/dist/css-asset-references.js +17 -0
- package/dist/file-filter.d.ts +7 -0
- package/dist/file-filter.js +23 -0
- package/dist/is-recorded-test.d.ts +6 -0
- package/dist/is-recorded-test.js +3 -0
- package/dist/mirror-react-act-environment.d.ts +8 -0
- package/dist/mirror-react-act-environment.js +27 -0
- package/dist/patch-testing-library-user-event.d.ts +3 -2
- package/dist/patch-testing-library-user-event.js +11 -4
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.js +41 -7
- package/dist/read-runtime-options.d.ts +3 -0
- package/dist/read-runtime-options.js +6 -0
- package/dist/register-recording-hooks.d.ts +7 -4
- package/dist/register-recording-hooks.js +33 -9
- package/dist/reporter.d.ts +34 -2
- package/dist/reporter.js +83 -14
- package/dist/rewrite-css-references.d.ts +5 -0
- package/dist/rewrite-css-references.js +18 -0
- package/dist/rewrite-head-references.d.ts +8 -0
- package/dist/rewrite-head-references.js +49 -0
- package/dist/runtime-options.d.ts +22 -0
- package/dist/runtime-options.js +7 -0
- package/dist/setup-dom.js +9 -5
- package/dist/setup.js +4 -2
- package/dist/split-stylesheet.d.ts +7 -0
- package/dist/split-stylesheet.js +114 -0
- package/dist/style-store.d.ts +25 -0
- package/dist/style-store.js +90 -0
- package/package.json +8 -6
- package/dist/element-label.d.ts +0 -6
- package/dist/element-label.js +0 -34
package/README.md
CHANGED
|
@@ -19,10 +19,13 @@ plugin detects where the tests run:
|
|
|
19
19
|
- **Browser mode** (`test.browser.enabled`): redirects `vitest-browser-react`,
|
|
20
20
|
records a frame after every `Locator` action and keyboard-level `userEvent`.
|
|
21
21
|
- **DOM environments** such as jsdom (anything else): redirects
|
|
22
|
-
`@testing-library/react`, records a frame after every
|
|
22
|
+
`@testing-library/react`, records a frame after every `fireEvent` and every
|
|
23
23
|
`@testing-library/user-event` call, turns on `test.css` so imported
|
|
24
24
|
stylesheets reach the snapshots, and sets `RTL_SKIP_AUTO_CLEANUP` so the
|
|
25
|
-
component is unmounted only after the closing frame.
|
|
25
|
+
component is unmounted only after the closing frame. The setup file sets
|
|
26
|
+
React's `IS_REACT_ACT_ENVIRONMENT` where Testing Library would, so act
|
|
27
|
+
warnings match a run without describe-me. It also works without user-event
|
|
28
|
+
installed.
|
|
26
29
|
|
|
27
30
|
```ts
|
|
28
31
|
import { describeMe } from '@describe-me/vitest/plugin'
|
|
@@ -44,9 +47,23 @@ export default defineConfig({
|
|
|
44
47
|
|
|
45
48
|
Pass `describeMe({ environment: 'browser' | 'dom' })` to override the detection.
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
A test that records no frame before the closing one (no recording `render`,
|
|
51
|
+
no interaction, no `step()`) costs no snapshot and is left out of the
|
|
52
|
+
manifest. `describeMe({ include, exclude })` chooses which test files are
|
|
53
|
+
recorded, with picomatch globs relative to the Vitest root. `exclude` wins.
|
|
54
|
+
Tests in other files still run, but record nothing. The reporter takes the
|
|
55
|
+
same two options, but on its own it only filters the manifest: without the
|
|
56
|
+
plugin the setup file does not know them.
|
|
57
|
+
|
|
58
|
+
`describeMe({ previewHead })` takes HTML that the viewer adds to the start of
|
|
59
|
+
every frame's `<head>`, like Storybook's `preview-head.html`: typically the
|
|
60
|
+
font links that the app shell loads and tests never do. It never reaches the
|
|
61
|
+
test page, and the local files it links are copied into the output directory
|
|
62
|
+
([Fonts](https://github.com/grzehub/describe-me#fonts) in the root README).
|
|
63
|
+
|
|
64
|
+
The pieces can also be wired by hand. Tests then import from the adapter
|
|
65
|
+
themselves: `render` from `@describe-me/react` in browser mode, and `render`,
|
|
66
|
+
`fireEvent` and `cleanup` from `@describe-me/react/testing-library` in jsdom:
|
|
50
67
|
|
|
51
68
|
```ts
|
|
52
69
|
import DescribeMeReporter from '@describe-me/vitest/reporter'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory of the adapter package the project resolves, e.g.
|
|
3
|
+
* `/repo/node_modules/@describe-me/react/`, with posix separators and a
|
|
4
|
+
* trailing slash the way Vite ids look, or null when it cannot be resolved.
|
|
5
|
+
* It is the real path, so a workspace link points at its source directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare function adapterPackageRoot(root: string, packageName: string): string | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join, sep } from 'node:path';
|
|
4
|
+
/** The `name` in a directory's package.json, if it has a readable one. */
|
|
5
|
+
function packageNameIn(directory) {
|
|
6
|
+
const manifest = join(directory, 'package.json');
|
|
7
|
+
if (!existsSync(manifest)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).name;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The directory of the adapter package the project resolves, e.g.
|
|
19
|
+
* `/repo/node_modules/@describe-me/react/`, with posix separators and a
|
|
20
|
+
* trailing slash the way Vite ids look, or null when it cannot be resolved.
|
|
21
|
+
* It is the real path, so a workspace link points at its source directory.
|
|
22
|
+
*/
|
|
23
|
+
export function adapterPackageRoot(root, packageName) {
|
|
24
|
+
let entry;
|
|
25
|
+
// The package does not export `./package.json`, so resolve its entry and walk up.
|
|
26
|
+
try {
|
|
27
|
+
entry = createRequire(join(root, 'package.json')).resolve(packageName);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
let directory = dirname(entry);
|
|
33
|
+
while (directory !== dirname(directory)) {
|
|
34
|
+
if (packageNameIn(directory) === packageName) {
|
|
35
|
+
return `${directory.split(sep).join('/')}/`;
|
|
36
|
+
}
|
|
37
|
+
directory = dirname(directory);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
package/dist/asset-store.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content-addressed store for the project files snapshots
|
|
3
|
-
* fonts, `url()` targets of stylesheets), living in
|
|
2
|
+
* Content-addressed store for the project files that snapshots and the preview
|
|
3
|
+
* head point at (images, fonts, `url()` targets of stylesheets), living in
|
|
4
|
+
* `<outDir>/assets`.
|
|
4
5
|
*
|
|
5
6
|
* rrweb makes most URLs absolute against the test page, whose server is gone
|
|
6
|
-
* once the tests finish
|
|
7
|
+
* once the tests finish. A few stay root-relative, which the viewer would
|
|
7
8
|
* resolve against itself. `rewrite()` copies each file it finds in the project
|
|
8
9
|
* and swaps its URL for `ASSET_URL_PREFIX` plus the stored name, which the
|
|
9
|
-
* viewer resolves against its own data directory.
|
|
10
|
+
* viewer resolves against its own data directory. `rewriteHead()` does the
|
|
11
|
+
* same for the preview head. A CSS file is stored with its own `url()` and
|
|
12
|
+
* `@import` targets copied too and written as bare sibling names, so it keeps
|
|
13
|
+
* working from `assets/`.
|
|
10
14
|
*/
|
|
11
15
|
export declare class AssetStore {
|
|
12
16
|
private readonly outDir;
|
|
@@ -15,8 +19,17 @@ export declare class AssetStore {
|
|
|
15
19
|
/** Stored name per URL pathname in this run, or null when it is not a project file. */
|
|
16
20
|
private readonly names;
|
|
17
21
|
private readonly notFound;
|
|
18
|
-
/**
|
|
19
|
-
private readonly
|
|
22
|
+
/** Stored name per absolute CSS file path in this run. */
|
|
23
|
+
private readonly cssNames;
|
|
24
|
+
/** CSS files being copied right now, so an `@import` cycle ends. */
|
|
25
|
+
private readonly copying;
|
|
26
|
+
/**
|
|
27
|
+
* Asset names per snapshot or style chunk. Both are content-addressed, so an
|
|
28
|
+
* entry never goes stale.
|
|
29
|
+
*/
|
|
30
|
+
private readonly referencesByFile;
|
|
31
|
+
/** Asset names per stored CSS asset, which is content-addressed too. */
|
|
32
|
+
private readonly referencesByCssAsset;
|
|
20
33
|
constructor(outDir: string, root: string);
|
|
21
34
|
/** Forget what the previous run found: files may have changed or appeared since. */
|
|
22
35
|
startRun(): void;
|
|
@@ -28,21 +41,58 @@ export declare class AssetStore {
|
|
|
28
41
|
* handled on their own. A `#fragment` is kept, for SVG sprites.
|
|
29
42
|
*/
|
|
30
43
|
rewrite(json: string, origin: string | undefined): string;
|
|
31
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Replace every URL in the preview head that points at a project file with
|
|
46
|
+
* an asset URL. A relative URL counts from the project root, the page the
|
|
47
|
+
* head is written for.
|
|
48
|
+
*/
|
|
49
|
+
rewriteHead(html: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Paths that snapshots, the preview head or copied CSS files in this run
|
|
52
|
+
* pointed at but that do not exist: root-relative inside the project root,
|
|
53
|
+
* `/@fs/<absolute path>` outside it.
|
|
54
|
+
*/
|
|
32
55
|
missing(): string[];
|
|
33
56
|
/**
|
|
34
|
-
* Drop the assets no kept snapshot
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
57
|
+
* Drop the assets that no kept snapshot or style chunk, no inline text such
|
|
58
|
+
* as the preview head, and no kept CSS asset refers to. Paths are relative
|
|
59
|
+
* to the output directory, as the manifest stores them. Every kept file is
|
|
60
|
+
* scanned, not only the ones written now: snapshots of modules that were not
|
|
61
|
+
* re-run were rewritten by an earlier run.
|
|
38
62
|
*/
|
|
39
|
-
collectGarbage(
|
|
63
|
+
collectGarbage(files: Iterable<string>, inlineTexts?: Iterable<string>): void;
|
|
64
|
+
/** Add what each kept CSS asset refers to, and what those refer to in turn. */
|
|
65
|
+
private keepCssTargets;
|
|
66
|
+
private cssAssetReferencesOf;
|
|
40
67
|
private referencesIn;
|
|
41
68
|
/** The asset URL for a root-relative URL path, or null when it is not a project file. */
|
|
42
69
|
private assetUrl;
|
|
43
70
|
/** The stored name for a URL pathname, copying the file on first sight in this run. */
|
|
44
71
|
private nameFor;
|
|
45
72
|
private copyIntoStore;
|
|
73
|
+
/** Store a project file under its content hash, and return its stored name. */
|
|
74
|
+
private store;
|
|
75
|
+
/**
|
|
76
|
+
* Store a CSS file with its `url()` and `@import` targets stored too. They
|
|
77
|
+
* are written as bare sibling names, which the browser resolves against the
|
|
78
|
+
* stylesheet's own URL, so the viewer has nothing to substitute. The name is
|
|
79
|
+
* the hash of the rewritten text.
|
|
80
|
+
*/
|
|
81
|
+
private storeCss;
|
|
82
|
+
/**
|
|
83
|
+
* The stored name, with its fragment, for a target of the CSS file
|
|
84
|
+
* `cssFile`, or null to leave the target as written: a remote URL, a
|
|
85
|
+
* file type outside the allowlist, a missing file or a file being copied.
|
|
86
|
+
*/
|
|
87
|
+
private siblingName;
|
|
88
|
+
/**
|
|
89
|
+
* The file behind a target path of a CSS file, reported when missing. A
|
|
90
|
+
* root-relative path is looked up like a snapshot URL, a relative one next
|
|
91
|
+
* to the CSS file on disk.
|
|
92
|
+
*/
|
|
93
|
+
private cssTargetFile;
|
|
94
|
+
/** How Vite serves a file: root-relative inside the project root, `/@fs/<absolute path>` outside it. */
|
|
95
|
+
private servedPath;
|
|
46
96
|
/** Where Vite serves a URL pathname from: `/@fs/`, the project root, or its `public/` folder. */
|
|
47
97
|
private fileFor;
|
|
48
98
|
}
|
package/dist/asset-store.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, } from 'node:fs';
|
|
3
|
-
import { extname, join, resolve, sep } from 'node:path';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { dirname, extname, join, resolve, sep } from 'node:path';
|
|
4
4
|
import { ASSET_URL_PREFIX } from '@describe-me/core/types';
|
|
5
|
+
import { cssAssetReferences } from './css-asset-references.js';
|
|
6
|
+
import { rewriteCssReferences } from './rewrite-css-references.js';
|
|
7
|
+
import { rewriteHeadReferences } from './rewrite-head-references.js';
|
|
5
8
|
/** A URL path inside serialized JSON runs until a quote, whitespace, `)`, `\`, `<` or `>`. */
|
|
6
9
|
const URL_PATH = String.raw `/[^"'\s)\\<>]*`;
|
|
7
10
|
/** A root-relative path: one slash, not the two of a protocol-relative URL. */
|
|
@@ -43,12 +46,66 @@ const MODULE_EXTENSIONS = new Set([
|
|
|
43
46
|
'.vue',
|
|
44
47
|
'.svelte',
|
|
45
48
|
]);
|
|
49
|
+
/** A URL with a scheme (`https:`, `data:`, `mailto:`, `describe-me-asset:`) is never a project path. */
|
|
50
|
+
const URL_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
51
|
+
/** The preview head's URLs resolve as if the page were served from the project root. */
|
|
52
|
+
const PROJECT_ROOT_URL = 'http://project-root.invalid/';
|
|
53
|
+
/**
|
|
54
|
+
* What a copied CSS file may pull into the store: stylesheets, fonts, images
|
|
55
|
+
* and cursors. Any other target stays as written, so a stylesheet cannot
|
|
56
|
+
* publish source files or secrets.
|
|
57
|
+
*/
|
|
58
|
+
const CSS_TARGET_EXTENSIONS = new Set([
|
|
59
|
+
'.css',
|
|
60
|
+
'.woff2',
|
|
61
|
+
'.woff',
|
|
62
|
+
'.ttf',
|
|
63
|
+
'.otf',
|
|
64
|
+
'.eot',
|
|
65
|
+
'.svg',
|
|
66
|
+
'.png',
|
|
67
|
+
'.jpg',
|
|
68
|
+
'.jpeg',
|
|
69
|
+
'.gif',
|
|
70
|
+
'.webp',
|
|
71
|
+
'.avif',
|
|
72
|
+
'.ico',
|
|
73
|
+
'.bmp',
|
|
74
|
+
'.cur',
|
|
75
|
+
]);
|
|
46
76
|
function escapeRegExp(text) {
|
|
47
77
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
78
|
}
|
|
49
79
|
function isFile(path) {
|
|
50
80
|
return existsSync(path) && statSync(path).isFile();
|
|
51
81
|
}
|
|
82
|
+
function contentHash(content) {
|
|
83
|
+
return createHash('sha1').update(content).digest('hex').slice(0, 16);
|
|
84
|
+
}
|
|
85
|
+
function assetNamesIn(text) {
|
|
86
|
+
return Array.from(text.matchAll(ASSET_REFERENCE), (match) => match[1]);
|
|
87
|
+
}
|
|
88
|
+
/** Empty values, `#fragment`s, protocol-relative and scheme URLs never name a project file. */
|
|
89
|
+
function isExternalUrl(url) {
|
|
90
|
+
return url === '' || url.startsWith('#') || url.startsWith('//') || URL_SCHEME.test(url);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A preview head URL as a root-relative path, or null when it cannot be read
|
|
94
|
+
* or leaves the project root, as `\\host\x` does.
|
|
95
|
+
*/
|
|
96
|
+
function rootRelativePath(url) {
|
|
97
|
+
let resolved;
|
|
98
|
+
try {
|
|
99
|
+
resolved = new URL(url, PROJECT_ROOT_URL);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (!resolved.href.startsWith(PROJECT_ROOT_URL)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return resolved.pathname + resolved.search + resolved.hash;
|
|
108
|
+
}
|
|
52
109
|
function decodePath(pathname) {
|
|
53
110
|
try {
|
|
54
111
|
return decodeURIComponent(pathname);
|
|
@@ -78,14 +135,18 @@ function fsPathFromUrl(pathname) {
|
|
|
78
135
|
return /^[A-Za-z]:/.test(path) ? path : `/${path}`;
|
|
79
136
|
}
|
|
80
137
|
/**
|
|
81
|
-
* Content-addressed store for the project files snapshots
|
|
82
|
-
* fonts, `url()` targets of stylesheets), living in
|
|
138
|
+
* Content-addressed store for the project files that snapshots and the preview
|
|
139
|
+
* head point at (images, fonts, `url()` targets of stylesheets), living in
|
|
140
|
+
* `<outDir>/assets`.
|
|
83
141
|
*
|
|
84
142
|
* rrweb makes most URLs absolute against the test page, whose server is gone
|
|
85
|
-
* once the tests finish
|
|
143
|
+
* once the tests finish. A few stay root-relative, which the viewer would
|
|
86
144
|
* resolve against itself. `rewrite()` copies each file it finds in the project
|
|
87
145
|
* and swaps its URL for `ASSET_URL_PREFIX` plus the stored name, which the
|
|
88
|
-
* viewer resolves against its own data directory.
|
|
146
|
+
* viewer resolves against its own data directory. `rewriteHead()` does the
|
|
147
|
+
* same for the preview head. A CSS file is stored with its own `url()` and
|
|
148
|
+
* `@import` targets copied too and written as bare sibling names, so it keeps
|
|
149
|
+
* working from `assets/`.
|
|
89
150
|
*/
|
|
90
151
|
export class AssetStore {
|
|
91
152
|
outDir;
|
|
@@ -94,8 +155,17 @@ export class AssetStore {
|
|
|
94
155
|
/** Stored name per URL pathname in this run, or null when it is not a project file. */
|
|
95
156
|
names = new Map();
|
|
96
157
|
notFound = new Set();
|
|
97
|
-
/**
|
|
98
|
-
|
|
158
|
+
/** Stored name per absolute CSS file path in this run. */
|
|
159
|
+
cssNames = new Map();
|
|
160
|
+
/** CSS files being copied right now, so an `@import` cycle ends. */
|
|
161
|
+
copying = new Set();
|
|
162
|
+
/**
|
|
163
|
+
* Asset names per snapshot or style chunk. Both are content-addressed, so an
|
|
164
|
+
* entry never goes stale.
|
|
165
|
+
*/
|
|
166
|
+
referencesByFile = new Map();
|
|
167
|
+
/** Asset names per stored CSS asset, which is content-addressed too. */
|
|
168
|
+
referencesByCssAsset = new Map();
|
|
99
169
|
constructor(outDir, root) {
|
|
100
170
|
this.outDir = outDir;
|
|
101
171
|
this.dir = join(outDir, 'assets');
|
|
@@ -106,6 +176,7 @@ export class AssetStore {
|
|
|
106
176
|
startRun() {
|
|
107
177
|
this.names.clear();
|
|
108
178
|
this.notFound.clear();
|
|
179
|
+
this.cssNames.clear();
|
|
109
180
|
}
|
|
110
181
|
/**
|
|
111
182
|
* Replace every URL in a serialized snapshot that points at a project file
|
|
@@ -132,45 +203,96 @@ export class AssetStore {
|
|
|
132
203
|
return asset === null ? poster : `"poster":"${asset}`;
|
|
133
204
|
});
|
|
134
205
|
}
|
|
135
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Replace every URL in the preview head that points at a project file with
|
|
208
|
+
* an asset URL. A relative URL counts from the project root, the page the
|
|
209
|
+
* head is written for.
|
|
210
|
+
*/
|
|
211
|
+
rewriteHead(html) {
|
|
212
|
+
return rewriteHeadReferences(html, (url) => {
|
|
213
|
+
if (isExternalUrl(url)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const path = rootRelativePath(url);
|
|
217
|
+
return path === null ? null : this.assetUrl(path);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Paths that snapshots, the preview head or copied CSS files in this run
|
|
222
|
+
* pointed at but that do not exist: root-relative inside the project root,
|
|
223
|
+
* `/@fs/<absolute path>` outside it.
|
|
224
|
+
*/
|
|
136
225
|
missing() {
|
|
137
226
|
return Array.from(this.notFound).sort();
|
|
138
227
|
}
|
|
139
228
|
/**
|
|
140
|
-
* Drop the assets no kept snapshot
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
229
|
+
* Drop the assets that no kept snapshot or style chunk, no inline text such
|
|
230
|
+
* as the preview head, and no kept CSS asset refers to. Paths are relative
|
|
231
|
+
* to the output directory, as the manifest stores them. Every kept file is
|
|
232
|
+
* scanned, not only the ones written now: snapshots of modules that were not
|
|
233
|
+
* re-run were rewritten by an earlier run.
|
|
144
234
|
*/
|
|
145
|
-
collectGarbage(
|
|
235
|
+
collectGarbage(files, inlineTexts = []) {
|
|
146
236
|
const stored = readdirSync(this.dir);
|
|
147
237
|
if (stored.length === 0) {
|
|
148
238
|
return;
|
|
149
239
|
}
|
|
150
240
|
const keep = new Set();
|
|
151
|
-
for (const
|
|
152
|
-
for (const name of this.referencesIn(
|
|
241
|
+
for (const file of files) {
|
|
242
|
+
for (const name of this.referencesIn(file)) {
|
|
243
|
+
keep.add(name);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const text of inlineTexts) {
|
|
247
|
+
for (const name of assetNamesIn(text)) {
|
|
153
248
|
keep.add(name);
|
|
154
249
|
}
|
|
155
250
|
}
|
|
251
|
+
this.keepCssTargets(keep);
|
|
156
252
|
for (const file of stored) {
|
|
157
253
|
if (!keep.has(file)) {
|
|
158
254
|
rmSync(join(this.dir, file), { force: true });
|
|
159
255
|
}
|
|
160
256
|
}
|
|
161
257
|
}
|
|
162
|
-
|
|
163
|
-
|
|
258
|
+
/** Add what each kept CSS asset refers to, and what those refer to in turn. */
|
|
259
|
+
keepCssTargets(keep) {
|
|
260
|
+
const pending = Array.from(keep).filter((name) => name.endsWith('.css'));
|
|
261
|
+
let name = pending.pop();
|
|
262
|
+
while (name !== undefined) {
|
|
263
|
+
for (const target of this.cssAssetReferencesOf(name)) {
|
|
264
|
+
if (!keep.has(target)) {
|
|
265
|
+
keep.add(target);
|
|
266
|
+
pending.push(target);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
name = pending.pop();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
cssAssetReferencesOf(name) {
|
|
273
|
+
const cached = this.referencesByCssAsset.get(name);
|
|
274
|
+
if (cached) {
|
|
275
|
+
return cached;
|
|
276
|
+
}
|
|
277
|
+
const path = join(this.dir, name);
|
|
278
|
+
if (!name.endsWith('.css') || !isFile(path)) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
const names = cssAssetReferences(readFileSync(path, 'utf8'));
|
|
282
|
+
this.referencesByCssAsset.set(name, names);
|
|
283
|
+
return names;
|
|
284
|
+
}
|
|
285
|
+
referencesIn(file) {
|
|
286
|
+
const cached = this.referencesByFile.get(file);
|
|
164
287
|
if (cached) {
|
|
165
288
|
return cached;
|
|
166
289
|
}
|
|
167
|
-
const path = join(this.outDir,
|
|
290
|
+
const path = join(this.outDir, file);
|
|
168
291
|
if (!isFile(path)) {
|
|
169
292
|
return [];
|
|
170
293
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
this.referencesBySnapshot.set(snapshotFile, names);
|
|
294
|
+
const names = assetNamesIn(readFileSync(path, 'utf8'));
|
|
295
|
+
this.referencesByFile.set(file, names);
|
|
174
296
|
return names;
|
|
175
297
|
}
|
|
176
298
|
/** The asset URL for a root-relative URL path, or null when it is not a project file. */
|
|
@@ -205,8 +327,15 @@ export class AssetStore {
|
|
|
205
327
|
}
|
|
206
328
|
return null;
|
|
207
329
|
}
|
|
208
|
-
|
|
330
|
+
return this.store(file);
|
|
331
|
+
}
|
|
332
|
+
/** Store a project file under its content hash, and return its stored name. */
|
|
333
|
+
store(file) {
|
|
209
334
|
const extension = extname(file).toLowerCase();
|
|
335
|
+
if (extension === '.css') {
|
|
336
|
+
return this.storeCss(file);
|
|
337
|
+
}
|
|
338
|
+
const hash = contentHash(readFileSync(file));
|
|
210
339
|
const name = /^\.[a-z0-9]+$/.test(extension) ? `${hash}${extension}` : hash;
|
|
211
340
|
const target = join(this.dir, name);
|
|
212
341
|
if (!existsSync(target)) {
|
|
@@ -214,6 +343,80 @@ export class AssetStore {
|
|
|
214
343
|
}
|
|
215
344
|
return name;
|
|
216
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Store a CSS file with its `url()` and `@import` targets stored too. They
|
|
348
|
+
* are written as bare sibling names, which the browser resolves against the
|
|
349
|
+
* stylesheet's own URL, so the viewer has nothing to substitute. The name is
|
|
350
|
+
* the hash of the rewritten text.
|
|
351
|
+
*/
|
|
352
|
+
storeCss(file) {
|
|
353
|
+
const known = this.cssNames.get(file);
|
|
354
|
+
if (known !== undefined) {
|
|
355
|
+
return known;
|
|
356
|
+
}
|
|
357
|
+
let css;
|
|
358
|
+
this.copying.add(file);
|
|
359
|
+
try {
|
|
360
|
+
css = rewriteCssReferences(readFileSync(file, 'utf8'), (url) => this.siblingName(url, file));
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
this.copying.delete(file);
|
|
364
|
+
}
|
|
365
|
+
const name = `${contentHash(css)}.css`;
|
|
366
|
+
const target = join(this.dir, name);
|
|
367
|
+
if (!existsSync(target)) {
|
|
368
|
+
writeFileSync(target, css);
|
|
369
|
+
}
|
|
370
|
+
this.cssNames.set(file, name);
|
|
371
|
+
return name;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The stored name, with its fragment, for a target of the CSS file
|
|
375
|
+
* `cssFile`, or null to leave the target as written: a remote URL, a
|
|
376
|
+
* file type outside the allowlist, a missing file or a file being copied.
|
|
377
|
+
*/
|
|
378
|
+
siblingName(url, cssFile) {
|
|
379
|
+
if (isExternalUrl(url)) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const { pathname, fragment } = splitUrlPath(url);
|
|
383
|
+
const path = decodePath(pathname);
|
|
384
|
+
if (!CSS_TARGET_EXTENSIONS.has(extname(path).toLowerCase())) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const file = this.cssTargetFile(path, cssFile);
|
|
388
|
+
if (!file || this.copying.has(file)) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return `${this.store(file)}${fragment}`;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The file behind a target path of a CSS file, reported when missing. A
|
|
395
|
+
* root-relative path is looked up like a snapshot URL, a relative one next
|
|
396
|
+
* to the CSS file on disk.
|
|
397
|
+
*/
|
|
398
|
+
cssTargetFile(path, cssFile) {
|
|
399
|
+
if (path.startsWith('/')) {
|
|
400
|
+
const file = this.fileFor(path);
|
|
401
|
+
if (!file) {
|
|
402
|
+
this.notFound.add(path);
|
|
403
|
+
}
|
|
404
|
+
return file;
|
|
405
|
+
}
|
|
406
|
+
const file = resolve(dirname(cssFile), path);
|
|
407
|
+
if (!isFile(file)) {
|
|
408
|
+
this.notFound.add(this.servedPath(file));
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
return file;
|
|
412
|
+
}
|
|
413
|
+
/** How Vite serves a file: root-relative inside the project root, `/@fs/<absolute path>` outside it. */
|
|
414
|
+
servedPath(file) {
|
|
415
|
+
const path = file.startsWith(this.root + sep)
|
|
416
|
+
? file.slice(this.root.length)
|
|
417
|
+
: `/@fs/${file.replace(/^[\\/]/, '')}`;
|
|
418
|
+
return path.split(sep).join('/');
|
|
419
|
+
}
|
|
217
420
|
/** Where Vite serves a URL pathname from: `/@fs/`, the project root, or its `public/` folder. */
|
|
218
421
|
fileFor(pathname) {
|
|
219
422
|
if (pathname.startsWith('/@fs/')) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CompiledGlob } from './runtime-options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Plain `{ source, flags }` pairs, because the globs travel to the test
|
|
4
|
+
* runtime as JSON. Invalid input fails here, before any test runs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function compileGlobs(patterns: string | string[] | undefined): CompiledGlob[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import picomatch from 'picomatch';
|
|
3
|
+
/**
|
|
4
|
+
* Plain `{ source, flags }` pairs, because the globs travel to the test
|
|
5
|
+
* runtime as JSON. Invalid input fails here, before any test runs.
|
|
6
|
+
*/
|
|
7
|
+
export function compileGlobs(patterns) {
|
|
8
|
+
if (patterns === undefined) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
12
|
+
return list.map((pattern) => {
|
|
13
|
+
// picomatch throws its own unprefixed TypeError for an empty string.
|
|
14
|
+
if (typeof pattern !== 'string' || pattern === '') {
|
|
15
|
+
throw new Error(`describe-me: include and exclude expect non-empty glob strings, got ${inspect(pattern)}`);
|
|
16
|
+
}
|
|
17
|
+
const regExp = picomatch.makeRe(pattern, { dot: true, windows: false });
|
|
18
|
+
return { source: regExp.source, flags: regExp.flags };
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cssReferences } from '@describe-me/core/css-references';
|
|
2
|
+
/** A stored asset's name: a content hash and, when the file has one, its extension. */
|
|
3
|
+
const ASSET_NAME = /^[0-9a-f]{16}(\.[a-z0-9]+)?$/;
|
|
4
|
+
/**
|
|
5
|
+
* The stored asset names a stored CSS asset refers to. The asset store writes
|
|
6
|
+
* them as bare sibling names, so garbage collection can follow them.
|
|
7
|
+
*/
|
|
8
|
+
export function cssAssetReferences(css) {
|
|
9
|
+
const names = [];
|
|
10
|
+
for (const { url } of cssReferences(css)) {
|
|
11
|
+
const name = url.split(/[?#]/)[0];
|
|
12
|
+
if (ASSET_NAME.test(name)) {
|
|
13
|
+
names.push(name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return names;
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RuntimeOptions } from './runtime-options.js';
|
|
2
|
+
/**
|
|
3
|
+
* Takes a path relative to the root, with posix separators. Build it once and
|
|
4
|
+
* reuse the returned function, which memoizes per file name. The test runtime
|
|
5
|
+
* imports this file, so no Node imports.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fileFilter(options: Pick<RuntimeOptions, 'include' | 'exclude'>): (fileName: string) => boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function toRegExps(globs) {
|
|
2
|
+
return globs.map((glob) => new RegExp(glob.source, glob.flags));
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Takes a path relative to the root, with posix separators. Build it once and
|
|
6
|
+
* reuse the returned function, which memoizes per file name. The test runtime
|
|
7
|
+
* imports this file, so no Node imports.
|
|
8
|
+
*/
|
|
9
|
+
export function fileFilter(options) {
|
|
10
|
+
const include = toRegExps(options.include);
|
|
11
|
+
const exclude = toRegExps(options.exclude);
|
|
12
|
+
const answers = new Map();
|
|
13
|
+
return (fileName) => {
|
|
14
|
+
const known = answers.get(fileName);
|
|
15
|
+
if (known !== undefined) {
|
|
16
|
+
return known;
|
|
17
|
+
}
|
|
18
|
+
const included = include.length === 0 || include.some((glob) => glob.test(fileName));
|
|
19
|
+
const answer = included && !exclude.some((glob) => glob.test(fileName));
|
|
20
|
+
answers.set(fileName, answer);
|
|
21
|
+
return answer;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set React's `IS_REACT_ACT_ENVIRONMENT` flag for each test file, the way
|
|
3
|
+
* Testing Library does. `RTL_SKIP_AUTO_CLEANUP` skips its whole setup block,
|
|
4
|
+
* the flag included, which silences React's act warnings. The flag is set
|
|
5
|
+
* only under Testing Library's own condition (Vitest globals), so the
|
|
6
|
+
* warnings match a run without describe-me.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mirrorReactActEnvironment(): void;
|