From 1ffafc0878db61c92ace581b11d926e4699ca079 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 24 Jul 2026 13:47:12 +0200 Subject: [PATCH] refactor: deduplicate run-level option propagation and document the resolution model (#10823) --- docs/guide/advanced/index.md | 25 +++++++++++++++++++ .../vitest/src/node/config/resolveConfig.ts | 21 ++++++++++------ packages/vitest/src/node/plugins/workspace.ts | 3 --- .../src/node/projects/resolveProjects.ts | 1 - 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/docs/guide/advanced/index.md b/docs/guide/advanced/index.md index affae6a01..ac83e0b53 100644 --- a/docs/guide/advanced/index.md +++ b/docs/guide/advanced/index.md @@ -117,6 +117,31 @@ This is the same method Vitest uses internally to resolve the config before crea You can pass a shared [`PluginHarness`](#pluginharness) as the third argument to reuse a logger and package installer across calls. ::: +## Project Configuration Resolution + +This section describes how the arguments of `startVitest`, `createVitest`, and `resolveConfig` interact with [test projects](/guide/projects). Without projects, all resolved options apply to the single root project and none of this matters. + +The root configuration is resolved from three inputs, in ascending priority: + +1. the root config file +2. `viteOverrides`, merged on top of the config file values +3. CLI options (`options`), applied on top of everything else + +Every project then resolves its own Vite config independently: + +- A project referenced as a config file or a directory resolves only its own file. It does not inherit any options from the root configuration. +- An inline project with the [`extends`](/guide/projects#configuration) option re-executes the extended config file and merges the project's own options on top. Only the config **file** participates in this: `viteOverrides` and CLI options are not part of the file, so `extends` does not carry them into projects. + +Independently of `extends`, several groups of options reach every project: + +- A fixed subset of CLI options that configure how tests run (`--testTimeout`, `--retry`, `--pool`, and similar) is applied to every project at the highest priority, mirroring the root resolution. +- Run-level options only make sense for the test run as a whole: every project receives the root's resolved `coverage`, `attachmentsDir`, and `mergeReportsLabel` values. +- The root's `fsModuleCache`, `fsModuleCachePath`, `experimental.viteModuleRunner`, `experimental.nodeLoader`, and `experimental.importDurations` values are applied as defaults to every project that doesn't define them. + +The project's own `tags` always replace the `tags` array merged from an extended config instead of being concatenated with it, so the same tag names can be redefined. + +Note that `plugins` reach a project only through a config file: the file is re-executed for every project, which creates fresh plugin instances. Plugin instances passed in `viteOverrides` belong to the root Vite server and are never shared with project servers. + ## parseCLI ```ts diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 31467e22b..18a050ebe 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -208,11 +208,13 @@ export function resolveTestConfig( const resolved = deepMerge({}, configDefaults, options) as ResolvedConfig resolved.root = viteConfig.root - // Coverage is collected once for the whole run using the root config, so projects - // share its resolved coverage. Each project's setup/test/config files are then + // These options are resolved once for the whole run using the root config. + // Coverage is shared by reference: each project's setup/test/config files are // appended to the same exclude list below, keeping them out of the report. if (globalConfig) { resolved.coverage = globalConfig.coverage + resolved.attachmentsDir = globalConfig.attachmentsDir + resolved.mergeReportsLabel = globalConfig.mergeReportsLabel } const rootStats = statSync(resolved.root, { throwIfNoEntry: false }) @@ -757,12 +759,15 @@ export function resolveTestConfig( .map(reporter => [reporter, configReportersMap.get(reporter) || {}]) } - resolved.mergeReportsLabel = process.env.VITEST_BLOB_LABEL - for (const reporter of resolved.reporters) { - if (Array.isArray(reporter) && reporter[0] === 'blob') { - const options = reporter[1] as any - if (options && typeof options.label === 'string') { - resolved.mergeReportsLabel = options.label + // only the root resolves the label; projects receive the root's value above + if (!globalConfig) { + resolved.mergeReportsLabel = process.env.VITEST_BLOB_LABEL + for (const reporter of resolved.reporters) { + if (Array.isArray(reporter) && reporter[0] === 'blob') { + const options = reporter[1] as any + if (options && typeof options.label === 'string') { + resolved.mergeReportsLabel = options.label + } } } } diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index c1b205a34..8ff92afd1 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -70,9 +70,6 @@ export function WorkspaceVitestPlugin( return config }, configResolved(config) { - // Projects always inherit non-project config options - config.test.coverage = globalConfig.coverage - config.test.attachmentsDir = globalConfig.attachmentsDir // project servers never watch; the top-level server owns the watcher config.server.watch = null }, diff --git a/packages/vitest/src/node/projects/resolveProjects.ts b/packages/vitest/src/node/projects/resolveProjects.ts index 87ea2bfb5..262de3eaa 100644 --- a/packages/vitest/src/node/projects/resolveProjects.ts +++ b/packages/vitest/src/node/projects/resolveProjects.ts @@ -412,7 +412,6 @@ async function resolveSingleProjectEntry( projectViteConfig, globalConfig, ) - projectConfig.mergeReportsLabel = globalConfig.mergeReportsLabel projectViteConfig.test = projectConfig -- 2.51.2