Skip to content

Commit 20a5d4b

Browse files
feat: add "configureVitest" plugin hook (#7349)
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
1 parent ba9b51c commit 20a5d4b

File tree

16 files changed

+489
-31
lines changed

16 files changed

+489
-31
lines changed

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ export default ({ mode }: { mode: string }) => {
350350
},
351351
],
352352
},
353+
{
354+
text: 'Plugin API',
355+
link: '/advanced/api/plugin',
356+
},
353357
{
354358
text: 'Runner API',
355359
link: '/advanced/runner',

docs/advanced/api/plugin.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
title: Plugin API
3+
outline: deep
4+
---
5+
6+
# Plugin API <Version>3.1.0</Version> {#plugin-api}
7+
8+
::: warning
9+
This is an advanced API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors.
10+
11+
This guide assumes you know how to work with [Vite plugins](https://vite.dev/guide/api-plugin.html).
12+
:::
13+
14+
Vitest supports an experimental `configureVitest` [plugin](https://vite.dev/guide/api-plugin.html) hook hook since version 3.1. Any feedback regarding this API is welcome in [GitHub](https://github.com/vitest-dev/vitest/discussions/7104).
15+
16+
::: code-group
17+
```ts [only vitest]
18+
import type { Vite, VitestPluginContext } from 'vitest/node'
19+
20+
export function plugin(): Vite.Plugin {
21+
return {
22+
name: 'vitest:my-plugin',
23+
configureVitest(context: VitestPluginContext) {
24+
// ...
25+
}
26+
}
27+
}
28+
```
29+
```ts [vite and vitest]
30+
/// <reference types="vitest/config" />
31+
32+
import type { Plugin } from 'vite'
33+
34+
export function plugin(): Plugin {
35+
return {
36+
name: 'vitest:my-plugin',
37+
transform() {
38+
// ...
39+
},
40+
configureVitest(context) {
41+
// ...
42+
}
43+
}
44+
}
45+
```
46+
:::
47+
48+
::: tip TypeScript
49+
Vitest re-exports all Vite type-only imports via a `Vite` namespace, which you can use to keep your versions in sync. However, if you are writing a plugin for both Vite and Vitest, you can continue using the `Plugin` type from the `vite` entrypoint. Just make sure you have `vitest/config` referenced somewhere so that `configureVitest` is augmented correctly:
50+
51+
```ts
52+
/// <reference types="vitest/config" />
53+
```
54+
:::
55+
56+
Unlike [`reporter.onInit`](/advanced/api/reporters#oninit), this hooks runs early in Vitest lifecycle allowing you to make changes to configuration like `coverage` and `reporters`. A more notable change is that you can manipulate the global config from a [workspace project](/guide/workspace) if your plugin is defined in the project and not in the global config.
57+
58+
## Context
59+
60+
### project
61+
62+
The current [test project](./test-project) that the plugin belongs to.
63+
64+
::: warning Browser Mode
65+
Note that if you are relying on a browser feature, the `project.browser` field is not set yet. Use [`reporter.onBrowserInit`](./reporters#onbrowserinit) event instead.
66+
:::
67+
68+
### vitest
69+
70+
The global [Vitest](./vitest) instance. You can change the global configuration by directly mutating the `vitest.config` property:
71+
72+
```ts
73+
vitest.config.coverage.enabled = false
74+
vitest.config.reporters.push([['my-reporter', {}]])
75+
```
76+
77+
::: warning Config is Resolved
78+
Note that Vitest already resolved the config, so some types might be different from the usual user configuration. This also means that some properties will not be resolved again, like `setupFile`. If you are adding new files, make sure to resolve it first.
79+
80+
At this point reporters are not created yet, so modifying `vitest.reporters` will have no effect because it will be overwritten. If you need to inject your own reporter, modify the config instead.
81+
:::
82+
83+
### injectTestProjects
84+
85+
```ts
86+
function injectTestProjects(
87+
config: TestProjectConfiguration | TestProjectConfiguration[]
88+
): Promise<TestProject[]>
89+
```
90+
91+
This methods accepts a config glob pattern, a filepath to the config or an inline configuration. It returns an array of resolved [test projects](./test-project).
92+
93+
```ts
94+
// inject a single project with a custom alias
95+
const newProjects = await injectTestProjects({
96+
// you can inherit the current project config by referencing `configFile`
97+
// note that you cannot have a project with the name that already exists,
98+
// so it's a good practice to define a custom name
99+
configFile: project.vite.config.configFile,
100+
test: {
101+
name: 'my-custom-alias',
102+
alias: {
103+
customAlias: resolve('./custom-path.js'),
104+
},
105+
},
106+
})
107+
```
108+
109+
::: warning Projects are Filtered
110+
Vitest filters projects during the config resolution, so if the user defined a filter, injected project might not be resolved unless it [matches the filter](./vitest#matchesprojectfilter). You can update the filter via the `vitest.config.project` option to always include your workspace project:
111+
112+
```ts
113+
vitest.config.project.push('my-project-name')
114+
```
115+
116+
Note that this will only affect projects injected with [`injectTestProjects`](#injecttestprojects) method.
117+
:::
118+
119+
::: tip Referencing the Current Config
120+
If you want to keep the user configuration, you can specify the `configFile` property. All other properties will be merged with the user defined config.
121+
122+
The project's `configFile` can be accessed in Vite's config: `project.vite.config.configFile`.
123+
124+
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.
125+
:::

docs/advanced/api/vitest.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,13 @@ vitest.onFilterWatchedSpecification(specification =>
518518
```
519519

520520
Vitest can create different specifications for the same file depending on the `pool` or `locations` options, so do not rely on the reference. Vitest can also return cached specification from [`vitest.getModuleSpecifications`](#getmodulespecifications) - the cache is based on the `moduleId` and `pool`. Note that [`project.createSpecification`](/advanced/api/test-project#createspecification) always returns a new instance.
521+
522+
## matchesProjectFilter <Version>3.1.0</Version> {#matchesprojectfilter}
523+
524+
```ts
525+
function matchesProjectFilter(name: string): boolean
526+
```
527+
528+
Check if the name matches the current [project filter](/guide/cli#project). If there is no project filter, this will always return `true`.
529+
530+
It is not possible to programmatically change the `--project` CLI option.

packages/browser/src/node/pool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
169169
async close() {
170170
await Promise.all([...providers].map(provider => provider.close()))
171171
providers.clear()
172-
vitest.resolvedProjects.forEach((project) => {
172+
vitest.projects.forEach((project) => {
173173
project.browser?.state.orchestrators.forEach((orchestrator) => {
174174
orchestrator.$close()
175175
})

packages/vitest/src/api/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
8888
return ctx.getRootProject().serializedConfig
8989
},
9090
getResolvedProjectNames(): string[] {
91-
return ctx.resolvedProjects.map(p => p.name)
91+
return ctx.projects.map(p => p.name)
9292
},
9393
async getTransformResult(projectName: string, id, browser = false) {
9494
const project = ctx.getProjectByName(projectName)

packages/vitest/src/node/config/resolveConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ function isPlaywrightChromiumOnly(vitest: Vitest, config: ResolvedConfig) {
913913
for (const instance of browser.instances) {
914914
const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser)
915915
// browser config is filtered out
916-
if (!vitest._matchesProjectFilter(name)) {
916+
if (!vitest.matchesProjectFilter(name)) {
917917
continue
918918
}
919919
if (instance.browser !== 'chromium') {

packages/vitest/src/node/core.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { SerializedCoverageConfig } from '../runtime/config'
77
import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general'
88
import type { ProcessPool, WorkspaceSpec } from './pool'
99
import type { TestSpecification } from './spec'
10-
import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config'
10+
import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config'
1111
import type { CoverageProvider } from './types/coverage'
1212
import type { Reporter } from './types/reporter'
1313
import type { TestRunResult } from './types/tests'
@@ -98,11 +98,10 @@ export class Vitest {
9898
/** @internal */ _browserLastPort = defaultBrowserPort
9999
/** @internal */ _browserSessions = new BrowserSessions()
100100
/** @internal */ _options: UserConfig = {}
101-
/** @internal */ reporters: Reporter[] = undefined!
101+
/** @internal */ reporters: Reporter[] = []
102102
/** @internal */ vitenode: ViteNodeServer = undefined!
103103
/** @internal */ runner: ViteNodeRunner = undefined!
104104
/** @internal */ _testRun: TestRun = undefined!
105-
/** @internal */ _projectFilters: RegExp[] = []
106105

107106
private isFirstRun = true
108107
private restartsCount = 0
@@ -216,7 +215,6 @@ export class Vitest {
216215
this.specifications.clearCache()
217216
this._onUserTestsRerun = []
218217

219-
this._projectFilters = toArray(options.project || []).map(project => wildcardPatternToRegExp(project))
220218
this._vite = server
221219

222220
const resolved = resolveConfig(this, options, server.config)
@@ -259,7 +257,7 @@ export class Vitest {
259257
server.watcher.on('change', async (file) => {
260258
file = normalize(file)
261259
const isConfig = file === server.config.configFile
262-
|| this.resolvedProjects.some(p => p.vite.config.configFile === file)
260+
|| this.projects.some(p => p.vite.config.configFile === file)
263261
|| file === this._workspaceConfigPath
264262
if (isConfig) {
265263
await Promise.all(this._onRestartListeners.map(fn => fn('config')))
@@ -279,6 +277,16 @@ export class Vitest {
279277
const projects = await this.resolveWorkspace(cliOptions)
280278
this.resolvedProjects = projects
281279
this.projects = projects
280+
281+
await Promise.all(projects.flatMap((project) => {
282+
const hooks = project.vite.config.getSortedPluginHooks('configureVitest')
283+
return hooks.map(hook => hook({
284+
project,
285+
vitest: this,
286+
injectTestProjects: this.injectTestProject,
287+
}))
288+
}))
289+
282290
if (!this.projects.length) {
283291
throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`)
284292
}
@@ -297,6 +305,24 @@ export class Vitest {
297305
await Promise.all(this._onSetServer.map(fn => fn()))
298306
}
299307

308+
/**
309+
* Inject new test projects into the workspace.
310+
* @param config Glob, config path or a custom config options.
311+
* @returns An array of new test projects. Can be empty if the name was filtered out.
312+
*/
313+
private injectTestProject = async (config: TestProjectConfiguration | TestProjectConfiguration[]): Promise<TestProject[]> => {
314+
const currentNames = new Set(this.projects.map(p => p.name))
315+
const workspace = await resolveWorkspace(
316+
this,
317+
this._options,
318+
undefined,
319+
Array.isArray(config) ? config : [config],
320+
currentNames,
321+
)
322+
this.projects.push(...workspace)
323+
return workspace
324+
}
325+
300326
/**
301327
* Provide a value to the test context. This value will be available to all tests with `inject`.
302328
*/
@@ -385,12 +411,15 @@ export class Vitest {
385411
}
386412

387413
private async resolveWorkspace(cliOptions: UserConfig): Promise<TestProject[]> {
414+
const names = new Set<string>()
415+
388416
if (Array.isArray(this.config.workspace)) {
389417
return resolveWorkspace(
390418
this,
391419
cliOptions,
392420
undefined,
393421
this.config.workspace,
422+
names,
394423
)
395424
}
396425

@@ -406,7 +435,7 @@ export class Vitest {
406435
if (!project) {
407436
return []
408437
}
409-
return resolveBrowserWorkspace(this, new Set(), [project])
438+
return resolveBrowserWorkspace(this, new Set([project.name]), [project])
410439
}
411440

412441
const workspaceModule = await this.import<{
@@ -422,6 +451,7 @@ export class Vitest {
422451
cliOptions,
423452
workspaceConfigPath,
424453
workspaceModule.default,
454+
names,
425455
)
426456
}
427457

@@ -861,11 +891,9 @@ export class Vitest {
861891
async changeProjectName(pattern: string): Promise<void> {
862892
if (pattern === '') {
863893
this.configOverride.project = undefined
864-
this._projectFilters = []
865894
}
866895
else {
867896
this.configOverride.project = [pattern]
868-
this._projectFilters = [wildcardPatternToRegExp(pattern)]
869897
}
870898

871899
await this.vite.restart()
@@ -1096,10 +1124,10 @@ export class Vitest {
10961124
await project._teardownGlobalSetup()
10971125
}
10981126

1099-
const closePromises: unknown[] = this.resolvedProjects.map(w => w.close())
1127+
const closePromises: unknown[] = this.projects.map(w => w.close())
11001128
// close the core workspace server only once
11011129
// it's possible that it's not initialized at all because it's not running any tests
1102-
if (this.coreWorkspaceProject && !this.resolvedProjects.includes(this.coreWorkspaceProject)) {
1130+
if (this.coreWorkspaceProject && !this.projects.includes(this.coreWorkspaceProject)) {
11031131
closePromises.push(this.coreWorkspaceProject.close().then(() => this._vite = undefined as any))
11041132
}
11051133

@@ -1136,7 +1164,7 @@ export class Vitest {
11361164
this.state.getProcessTimeoutCauses().forEach(cause => console.warn(cause))
11371165

11381166
if (!this.pool) {
1139-
const runningServers = [this._vite, ...this.resolvedProjects.map(p => p._vite)].filter(Boolean).length
1167+
const runningServers = [this._vite, ...this.projects.map(p => p._vite)].filter(Boolean).length
11401168

11411169
if (runningServers === 1) {
11421170
console.warn('Tests closed successfully but something prevents Vite server from exiting')
@@ -1252,20 +1280,23 @@ export class Vitest {
12521280

12531281
/**
12541282
* Check if the project with a given name should be included.
1255-
* @internal
12561283
*/
1257-
_matchesProjectFilter(name: string): boolean {
1284+
matchesProjectFilter(name: string): boolean {
1285+
const projects = this._config?.project || this._options?.project
12581286
// no filters applied, any project can be included
1259-
if (!this._projectFilters.length) {
1287+
if (!projects || !projects.length) {
12601288
return true
12611289
}
1262-
return this._projectFilters.some(filter => filter.test(name))
1290+
return toArray(projects).some((project) => {
1291+
const regexp = wildcardPatternToRegExp(project)
1292+
return regexp.test(name)
1293+
})
12631294
}
12641295
}
12651296

12661297
function assert(condition: unknown, property: string, name: string = property): asserts condition {
12671298
if (!condition) {
1268-
throw new Error(`The ${name} was not set. It means that \`vitest.${property}\` was called before the Vite server was established. Either await the Vitest promise or check that it is initialized with \`vitest.ready()\` before accessing \`vitest.${property}\`.`)
1299+
throw new Error(`The ${name} was not set. It means that \`vitest.${property}\` was called before the Vite server was established. Await the Vitest promise before accessing \`vitest.${property}\`.`)
12691300
}
12701301
}
12711302

packages/vitest/src/node/plugins/workspace.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite'
22
import type { TestProject } from '../project'
3-
import type { ResolvedConfig, UserWorkspaceConfig } from '../types/config'
3+
import type { ResolvedConfig, TestProjectInlineConfiguration } from '../types/config'
44
import { existsSync, readFileSync } from 'node:fs'
55
import { deepMerge } from '@vitest/utils'
66
import { basename, dirname, relative, resolve } from 'pathe'
@@ -21,7 +21,7 @@ import {
2121
} from './utils'
2222
import { VitestProjectResolver } from './vitestResolver'
2323

24-
interface WorkspaceOptions extends UserWorkspaceConfig {
24+
interface WorkspaceOptions extends TestProjectInlineConfiguration {
2525
root?: string
2626
workspacePath: string | number
2727
}
@@ -85,7 +85,7 @@ export function WorkspaceVitestPlugin(
8585
// if some of them match, they will later be filtered again by `resolveWorkspace`
8686
if (filters.length) {
8787
const hasProject = workspaceNames.some((name) => {
88-
return project.vitest._matchesProjectFilter(name)
88+
return project.vitest.matchesProjectFilter(name)
8989
})
9090
if (!hasProject) {
9191
throw new VitestFilteredOutProjectError()

packages/vitest/src/node/project.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import type { ParentProjectBrowser, ProjectBrowser } from './types/browser'
1616
import type {
1717
ResolvedConfig,
1818
SerializedConfig,
19+
TestProjectInlineConfiguration,
1920
UserConfig,
20-
UserWorkspaceConfig,
2121
} from './types/config'
2222
import { promises as fs, readFileSync } from 'node:fs'
2323
import { rm } from 'node:fs/promises'
@@ -726,7 +726,7 @@ export interface SerializedTestProject {
726726
context: ProvidedContext
727727
}
728728

729-
interface InitializeProjectOptions extends UserWorkspaceConfig {
729+
interface InitializeProjectOptions extends TestProjectInlineConfiguration {
730730
configFile: string | false
731731
}
732732

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy