Skip to content

Commit 1b14737

Browse files
authored
perf(experimental): add file system cache (#9026)
1 parent 8508296 commit 1b14737

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1585
-271
lines changed

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,42 @@ jobs:
127127
path: test/ui/test-results/
128128
retention-days: 30
129129

130+
test-cached:
131+
needs: changed
132+
name: 'Cache&Test: node-${{ matrix.node_version }}, ${{ matrix.os }}'
133+
if: needs.changed.outputs.should_skip != 'true'
134+
runs-on: ${{ matrix.os }}
135+
136+
timeout-minutes: 30
137+
138+
strategy:
139+
matrix:
140+
node_version: [24]
141+
os:
142+
- macos-latest
143+
- windows-latest
144+
fail-fast: false
145+
146+
steps:
147+
- uses: actions/checkout@v5
148+
149+
- uses: ./.github/actions/setup-and-cache
150+
with:
151+
node-version: ${{ matrix.node_version }}
152+
153+
- uses: browser-actions/setup-chrome@b94431e051d1c52dcbe9a7092a4f10f827795416 # v2.1.0
154+
155+
- name: Install
156+
run: pnpm i
157+
158+
- uses: ./.github/actions/setup-playwright
159+
160+
- name: Build
161+
run: pnpm run build
162+
163+
- name: Test
164+
run: pnpm run test:ci:cache
165+
130166
test-browser:
131167
needs: changed
132168
name: 'Browsers: node-${{ matrix.node_version }}, ${{ matrix.os }}'

docs/.vitepress/scripts/cli-generator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const skipConfig = new Set([
4141
'ui',
4242
'browser.name',
4343
'browser.fileParallelism',
44+
'clearCache',
4445
])
4546

4647
function resolveOptions(options: CLIOptions<any>, parentName?: string) {

docs/api/advanced/plugin.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,51 @@ The project's `configFile` can be accessed in Vite's config: `project.vite.confi
123123

124124
Note that this will also inherit the `name` - Vitest doesn't allow multiple projects with the same name, so this will throw an error. Make sure you specified a different name. You can access the current name via the `project.name` property and all used names are available in the `vitest.projects` array.
125125
:::
126+
127+
### experimental_defineCacheKeyGenerator <Version type="experimental">4.0.11</Version> <Experimental /> {#definecachekeygenerator}
128+
129+
```ts
130+
interface CacheKeyIdGeneratorContext {
131+
environment: DevEnvironment
132+
id: string
133+
sourceCode: string
134+
}
135+
136+
function experimental_defineCacheKeyGenerator(
137+
callback: (context: CacheKeyIdGeneratorContext) => string | undefined | null | false
138+
): void
139+
```
140+
141+
Define a generator that will be applied before hashing the cache key.
142+
143+
Use this to make sure Vitest generates correct hash. It is a good idea to define this function if your plugin can be registered with different options.
144+
145+
This is called only if [`experimental.fsModuleCache`](/config/experimental#fsmodulecache) is defined.
146+
147+
```ts
148+
interface PluginOptions {
149+
replacePropertyKey: string
150+
replacePropertyValue: string
151+
}
152+
153+
export function plugin(options: PluginOptions) {
154+
return {
155+
name: 'plugin-that-replaces-property',
156+
transform(code) {
157+
return code.replace(
158+
options.replacePropertyKey,
159+
options.replacePropertyValue
160+
)
161+
},
162+
configureVitest({ experimental_defineCacheKeyGenerator }) {
163+
experimental_defineCacheKeyGenerator(() => {
164+
// since these options affect the transform result,
165+
// return them together as a unique string
166+
return options.replacePropertyKey + options.replacePropertyValue
167+
})
168+
}
169+
}
170+
}
171+
```
172+
173+
If the `false` is returned, the module will not be cached on the file system.

docs/api/advanced/vitest.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,11 @@ function experimental_parseSpecifications(
607607
```
608608

609609
This method will [collect tests](#parsespecification) from an array of specifications. By default, Vitest will run only `os.availableParallelism()` number of specifications at a time to reduce the potential performance degradation. You can specify a different number in a second argument.
610+
611+
## experimental_clearCache <Version type="experimental">4.0.11</Version> <Badge type="warning">experimental</Badge> {#clearcache}
612+
613+
```ts
614+
function experimental_clearCache(): Promise<void>
615+
```
616+
617+
Deletes all Vitest caches, including [`experimental.fsModuleCache`](/config/experimental#fsmodulecache).

docs/config/experimental.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,75 @@ outline: deep
55

66
# experimental
77

8-
## openTelemetry <Version type="experimental">4.0.10</Version>
8+
## experimental.fsModuleCache <Version type="experimental">4.0.11</Version> {#experimental-fsmodulecache}
9+
10+
- **Type:** `boolean`
11+
- **Default:** `false`
12+
13+
Enabling this option allows Vitest to keep cached modules on the file system, making tests run faster between reruns.
14+
15+
You can delete the old cache by running [`vitest --clearCache`](/guide/cli#clearcache).
16+
17+
::: warning BROWSER SUPPORT
18+
At the moment, this option does not affect [the browser](/guide/browser/).
19+
:::
20+
21+
You can debug if your modules are cached by running vitest with a `DEBUG=vitest:cache:fs` environment variable:
22+
23+
```shell
24+
DEBUG=vitest:cache:fs vitest --experimental.fsModuleCache
25+
```
26+
27+
### Known Issues
28+
29+
Vitest creates persistent file hash based on file content, its id, vite's environment configuration and coverage status. Vitest tries to use as much information it has about the configuration, but it is still incomplete. At the moment, it is not possible to track your plugin options because there is no standard interface for it.
30+
31+
If you have a plugin that relies on things outside the file content or the public configuration (like reading another file or a folder), it's possible that the cache will get stale. To workaround that, you can define a [cache key generator](/api/advanced/plugin#definecachekeygenerator) to specify dynamic option or to opt-out of caching for that module:
32+
33+
```js [vitest.config.js]
34+
import { defineConfig } from 'vitest/config'
35+
36+
export default defineConfig({
37+
plugins: [
38+
{
39+
name: 'vitest-cache',
40+
configureVitest({ experimental_defineCacheKeyGenerator }) {
41+
experimental_defineCacheKeyGenerator(({ id, sourceCode }) => {
42+
// never cache this id
43+
if (id.includes('do-not-cache')) {
44+
return false
45+
}
46+
47+
// cache this file based on the value of a dynamic variable
48+
if (sourceCode.includes('myDynamicVar')) {
49+
return process.env.DYNAMIC_VAR_VALUE
50+
}
51+
})
52+
}
53+
}
54+
],
55+
test: {
56+
experimental: {
57+
fsModuleCache: true,
58+
},
59+
},
60+
})
61+
```
62+
63+
If you are a plugin author, consider defining a [cache key generator](/api/advanced/plugin#definecachekeygenerator) in your plugin if it can be registered with different options that affect the transform result.
64+
65+
## experimental.fsModuleCachePath <Version type="experimental">4.0.11</Version> {#experimental-fsmodulecachepath}
66+
67+
- **Type:** `string`
68+
- **Default:** `'node_modules/.experimental-vitest-cache'`
69+
70+
Directory where the file system cache is located.
71+
72+
By default, Vitest will try to find the workspace root and store the cache inside the `node_modules` folder. The root is based on your package manager's lockfile (for example, `.package-lock.json`, `.yarn-state.yml`, `.pnpm/lock.yaml` and so on).
73+
74+
At the moment, Vitest ignores the [test.cache.dir](/config/cache) or [cacheDir](https://vite.dev/config/shared-options#cachedir) options completely and creates a separate folder.
75+
76+
## experimental.openTelemetry <Version type="experimental">4.0.11</Version> {#experimental-opentelemetry}
977

1078
- **Type:**
1179

docs/config/server.md

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,3 @@ If a `RegExp` is provided, it is matched against the full file path.
7171
When a dependency is a valid ESM package, try to guess the cjs version based on the path. This might be helpful, if a dependency has the wrong ESM file.
7272

7373
This might potentially cause some misalignment if a package has different logic in ESM and CJS mode.
74-
75-
## debug
76-
77-
### dump
78-
79-
- **Type:** `string | boolean`
80-
- **Default:** `false`
81-
82-
The folder where Vitest stores the contents of inlined test files that can be inspected manually.
83-
84-
If set to `true`, Vitest dumps the files inside the `.vitest-dump` folder relative to the root of the project.
85-
86-
You can also use `VITEST_DEBUG_DUMP` env variable to enable this conditionally.
87-
88-
### load
89-
90-
- **Type:** `boolean`
91-
- **Default:** `false`
92-
93-
Read files from the dump instead of transforming them. If dump is disabled, this does nothing.
94-
95-
You can also use `VITEST_DEBUG_LOAD_DUMP` env variable to enable this conditionally.

docs/guide/cli-generated.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,3 +798,16 @@ Use `bundle` to bundle the config with esbuild or `runner` (experimental) to pro
798798
- **CLI:** `--standalone`
799799

800800
Start Vitest without running tests. Tests will be running only on change. This option is ignored when CLI file filters are passed. (default: `false`)
801+
802+
### clearCache
803+
804+
- **CLI:** `--clearCache`
805+
806+
Delete all Vitest caches, including `experimental.fsModuleCache`, without running any tests. This will reduce the performance in the subsequent test run.
807+
808+
### experimental.fsModuleCache
809+
810+
- **CLI:** `--experimental.fsModuleCache`
811+
- **Config:** [experimental.fsModuleCache](/config/experimental#experimental-fsmodulecache)
812+
813+
Enable caching of modules on the file system between reruns.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"release": "tsx scripts/release.ts",
2626
"test": "pnpm --filter test-core test:threads",
2727
"test:ci": "CI=true pnpm -r --reporter-hide-prefix --stream --sequential --filter '@vitest/test-*' --filter !test-browser run test",
28+
"test:ci:cache": "CI=true pnpm -r --reporter-hide-prefix --stream --sequential --filter '@vitest/test-*' --filter !test-browser --filter !test-dts-config --filter !test-dts-fixture --filter !test-dts-playwright --filter !test-ui --filter !test-cache run test --experimental.fsModuleCache",
2829
"test:examples": "CI=true pnpm -r --reporter-hide-prefix --stream --filter '@vitest/example-*' run test",
2930
"test:ecosystem-ci": "ECOSYSTEM_CI=true pnpm test:ci",
3031
"typebuild": "tsx ./scripts/explain-types.ts",

packages/coverage-istanbul/src/provider.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,23 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider<ResolvedCover
5050
})
5151
}
5252

53-
onFileTransform(sourceCode: string, id: string, pluginCtx: any): { code: string; map: any } | undefined {
53+
requiresTransform(id: string): boolean {
5454
// Istanbul/babel cannot instrument CSS - e.g. Vue imports end up here.
5555
// File extension itself is .vue, but it contains CSS.
5656
// e.g. "Example.vue?vue&type=style&index=0&scoped=f7f04e08&lang.css"
5757
if (isCSSRequest(id)) {
58-
return
58+
return false
5959
}
6060

6161
if (!this.isIncluded(removeQueryParameters(id))) {
62+
return false
63+
}
64+
65+
return true
66+
}
67+
68+
onFileTransform(sourceCode: string, id: string, pluginCtx: Vite.Rollup.TransformPluginContext): { code: string; map: any } | undefined {
69+
if (!this.requiresTransform(id)) {
6270
return
6371
}
6472

0 commit comments

Comments
 (0)