diff --git a/packages/vitest/src/node/projects/resolveProjects.ts b/packages/vitest/src/node/projects/resolveProjects.ts index 5d61bccdb..38484ef37 100644 --- a/packages/vitest/src/node/projects/resolveProjects.ts +++ b/packages/vitest/src/node/projects/resolveProjects.ts @@ -5,6 +5,7 @@ import type { } from 'vite' import type { PluginHarness } from '../config/pluginHarness' import type { Vitest } from '../core' +import type { Logger } from '../logger' import type { BrowserInstanceOption, ConfigResolutionCaptures, @@ -31,6 +32,7 @@ import { BrowserLoaderPlugin, createClusterServer } from '../plugins/browserLoad import { resolveTestOptions, TestConfigPlugin } from '../plugins/testConfig' import { WorkspaceVitestPlugin } from '../plugins/workspace' import { TestProject } from '../project' +import { withLabel } from '../reporters/renderers/utils' import { globProjectTestFiles } from './globProjectFiles' const debug = createDebugger('vitest:projects') @@ -565,6 +567,46 @@ function hasNoPlugins(plugins: unknown): boolean { && plugins.every(plugin => !plugin || (Array.isArray(plugin) && hasNoPlugins(plugin))) } +function countPluginNames(plugins: ResolvedViteConfig['plugins']): Map { + const counts = new Map() + for (const plugin of plugins) { + if (plugin.name) { + counts.set(plugin.name, (counts.get(plugin.name) || 0) + 1) + } + } + return counts +} + +function warnDuplicateInheritedPlugins( + logger: Logger, + projectConfig: ResolvedConfig, + projectViteConfig: ResolvedViteConfig, + parentViteConfig: ResolvedViteConfig, + configFile: string, + root: string, +): void { + const parentCounts = countPluginNames(parentViteConfig.plugins) + const duplicates: string[] = [] + for (const [name, count] of countPluginNames(projectViteConfig.plugins)) { + // a duplicate already present in the declaring config is not caused by `extends` + const parentCount = parentCounts.get(name) || 0 + if (count > 1 && parentCount > 0 && count > parentCount) { + duplicates.push(name) + } + } + if (!duplicates.length) { + return + } + logger.warn( + withLabel('yellow', 'Vitest', [ + `The "${projectConfig.name}" project applies the same plugin multiple times: ${duplicates.map(name => `"${name}"`).join(', ')}. `, + `Since Vitest 5, an inline project extends the config file that declares it by default, so the plugins from "${relative(root, configFile)}" already apply to this project and don't need to be listed again.\n`, + `Remove the duplicated plugins from the project, or set \`extends: false\` to not inherit the declaring config. `, + `Setting the \`extends\` option explicitly hides this warning.`, + ].join('')), + ) +} + // `deleteDefineConfig` always drops these const DROPPED_DEFINE_KEYS = ['process.env', 'process', 'global'] @@ -766,6 +808,17 @@ async function resolveSingleProjectEntry( projectViteConfig.test = projectConfig + if (inheritsParentConfig && options.extends === undefined && configFile) { + warnDuplicateInheritedPlugins( + harness.logger, + projectConfig, + projectViteConfig, + parentViteConfig, + configFile, + rootConfig.root, + ) + } + // The browser provider's contribution (captured during this resolution by the // `vitest:browser:loader` plugin) is carried on the resolved config + entry so // server creation can build the single shared Vite server. diff --git a/test/e2e/test/projects.test.ts b/test/e2e/test/projects.test.ts index c5848deeb..8b5d79d16 100644 --- a/test/e2e/test/projects.test.ts +++ b/test/e2e/test/projects.test.ts @@ -235,6 +235,75 @@ describe('the root config inheritance', () => { 'inherited-tags': 2, }) }) + + it('warns when a project without an explicit `extends` duplicates an inherited plugin', async () => { + const { stderr } = await runInlineTests({ + 'vitest.config.js': ts` + import { defineConfig } from 'vitest/config' + + const myPlugin = () => ({ name: 'my-plugin' }) + + export default defineConfig({ + plugins: [myPlugin()], + test: { + projects: [ + { plugins: [myPlugin()], test: { name: 'duplicated' } }, + ], + }, + }) + `, + 'basic.test.js': basicTest, + }) + expect(stderr).toMatchInlineSnapshot(` + " Vitest The "duplicated" project applies the same plugin multiple times: "my-plugin". Since Vitest 5, an inline project extends the config file that declares it by default, so the plugins from "vitest.config.js" already apply to this project and don't need to be listed again. + Remove the duplicated plugins from the project, or set \`extends: false\` to not inherit the declaring config. Setting the \`extends\` option explicitly hides this warning. + " + `) + }) + + it('does not warn about duplicated plugins when `extends` is set explicitly', async () => { + const { stderr } = await runInlineTests({ + 'vitest.config.js': ts` + import { defineConfig } from 'vitest/config' + + const myPlugin = () => ({ name: 'my-plugin' }) + + export default defineConfig({ + plugins: [myPlugin()], + test: { + projects: [ + { extends: true, plugins: [myPlugin()], test: { name: 'duplicated' } }, + { extends: false, plugins: [myPlugin()], test: { name: 'isolated' } }, + ], + }, + }) + `, + 'basic.test.js': basicTest, + }) + expect(stderr).toBe('') + }) + + it('does not warn when the declaring config itself registers a plugin twice', async () => { + const { stderr } = await runInlineTests({ + 'vitest.config.js': ts` + import { defineConfig } from 'vitest/config' + + const myPlugin = () => ({ name: 'my-plugin' }) + const otherPlugin = () => ({ name: 'other-plugin' }) + + export default defineConfig({ + plugins: [myPlugin(), myPlugin()], + test: { + projects: [ + { plugins: [otherPlugin()], test: { name: 'inherited' } }, + ], + }, + }) + `, + 'basic.test.js': basicTest, + }) + expect(stderr).toBe('') + }) }) it('fails if workspace is empty', async () => {