Skip to content

Commit 91ba5cf

Browse files
committed
fix(cli): propagate vite-plus/test rewrite plugin into test.projects
The mocker rewrite plugin was only injected into the root vite config, so projects defined under `test.projects` (each spinning up an isolated Vite pipeline) never got the rewrite — `vi.mock()` failed with "problems in resolving the mocks API" for source that imports from `'vite-plus/test'` inside a project.
1 parent 261787e commit 91ba5cf

2 files changed

Lines changed: 158 additions & 3 deletions

File tree

packages/cli/src/__tests__/define-config-mocker-rewrite.spec.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1+
import type { Plugin } from '@voidzero-dev/vite-plus-core';
12
import { describe, expect, it } from 'vitest';
23

3-
import { rewriteVitePlusTestSpecifier } from '../define-config.ts';
4+
import { defineConfig, rewriteVitePlusTestSpecifier } from '../define-config.ts';
5+
6+
const REWRITE_PLUGIN_NAME = 'vite-plus:vitest-specifier-rewrite';
7+
8+
function pluginName(p: unknown): string | undefined {
9+
if (p && typeof p === 'object' && 'name' in p && typeof (p as { name: unknown }).name === 'string') {
10+
return (p as { name: string }).name;
11+
}
12+
return undefined;
13+
}
414

515
describe('rewriteVitePlusTestSpecifier', () => {
616
it('is a no-op when source does not mention vite-plus/test', () => {
@@ -66,3 +76,82 @@ describe('rewriteVitePlusTestSpecifier', () => {
6676
expect(rewriteVitePlusTestSpecifier(input)).toBe(expected);
6777
});
6878
});
79+
80+
describe('defineConfig project plugin injection', () => {
81+
it('injects rewrite plugin at the root plugins array', () => {
82+
const existing: Plugin = { name: 'user-existing-root-plugin' };
83+
const result = defineConfig({ plugins: [existing] }) as { plugins: unknown[] };
84+
85+
expect(pluginName(result.plugins[0])).toBe(REWRITE_PLUGIN_NAME);
86+
expect(pluginName(result.plugins[1])).toBe('user-existing-root-plugin');
87+
});
88+
89+
it('injects rewrite plugin into an inline-object project entry, preserving existing plugins', () => {
90+
const existing: Plugin = { name: 'user-unit-project-plugin' };
91+
const result = defineConfig({
92+
test: {
93+
projects: [
94+
{
95+
plugins: [existing],
96+
test: { name: 'unit', include: ['test/unit/**/*.spec.ts'], environment: 'node' },
97+
},
98+
],
99+
},
100+
}) as { test: { projects: unknown[] } };
101+
102+
const project = result.test.projects[0] as { plugins: unknown[]; test: { name: string } };
103+
expect(project.test.name).toBe('unit');
104+
expect(pluginName(project.plugins[0])).toBe(REWRITE_PLUGIN_NAME);
105+
expect(pluginName(project.plugins[1])).toBe('user-unit-project-plugin');
106+
// Sanity: the existing plugin reference is preserved (clone shallow-copies the array).
107+
expect(project.plugins[1]).toBe(existing);
108+
});
109+
110+
it('injects rewrite plugin into the return value of a function-shaped project entry', () => {
111+
const existing: Plugin = { name: 'user-fn-project-plugin' };
112+
const projectFn = () => ({
113+
plugins: [existing],
114+
test: { name: 'nuxt', environment: 'happy-dom' as const },
115+
});
116+
const result = defineConfig({
117+
test: { projects: [projectFn] },
118+
}) as { test: { projects: unknown[] } };
119+
120+
const wrapped = result.test.projects[0];
121+
expect(typeof wrapped).toBe('function');
122+
123+
// Vitest passes a `ConfigEnv` to the function; we don't depend on its
124+
// shape here, the wrapper just forwards it.
125+
const fakeEnv = { mode: 'test', command: 'serve' as const };
126+
const resolved = (wrapped as (env: typeof fakeEnv) => { plugins: unknown[] })(fakeEnv);
127+
expect(pluginName(resolved.plugins[0])).toBe(REWRITE_PLUGIN_NAME);
128+
expect(pluginName(resolved.plugins[1])).toBe('user-fn-project-plugin');
129+
});
130+
131+
it('passes string-glob project entries through unchanged', () => {
132+
const result = defineConfig({
133+
test: {
134+
projects: ['./packages/*', './apps/*'],
135+
},
136+
}) as { test: { projects: unknown[] } };
137+
138+
expect(result.test.projects).toEqual(['./packages/*', './apps/*']);
139+
});
140+
141+
it('handles projects with no existing plugins array', () => {
142+
const result = defineConfig({
143+
test: {
144+
projects: [
145+
{
146+
test: { name: 'no-plugins', environment: 'node' },
147+
},
148+
],
149+
},
150+
}) as { test: { projects: unknown[] } };
151+
152+
const project = result.test.projects[0] as { plugins: unknown[]; test: { name: string } };
153+
expect(project.test.name).toBe('no-plugins');
154+
expect(project.plugins).toHaveLength(1);
155+
expect(pluginName(project.plugins[0])).toBe(REWRITE_PLUGIN_NAME);
156+
});
157+
});

packages/cli/src/define-config.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import type { PluginOption, UserConfig } from '@voidzero-dev/vite-plus-core';
22
import type { OxfmtConfig } from 'oxfmt';
33
import type { OxlintConfig } from 'oxlint';
4-
import { defineConfig as viteDefineConfig, type ConfigEnv } from 'vitest/config';
4+
import {
5+
defineConfig as viteDefineConfig,
6+
type ConfigEnv,
7+
type TestProjectConfiguration,
8+
type TestProjectInlineConfiguration,
9+
type UserProjectConfigFn,
10+
} from 'vitest/config';
511
import type { InlineConfig as VitestInlineConfig } from 'vitest/node';
612

713
import type { PackUserConfig } from './pack.ts';
@@ -98,13 +104,73 @@ function vitePlusTestSpecifierRewritePlugin(): PluginOption {
98104
};
99105
}
100106

101-
function injectPlugin(config: UserConfig): UserConfig {
107+
/**
108+
* Inject the rewrite plugin into a single inline project config. Used both
109+
* for root configs and for object-shaped entries inside `test.projects`.
110+
*
111+
* The shapes overlap (both have an optional top-level `plugins` array), so a
112+
* shared helper keeps the wiring consistent.
113+
*/
114+
function injectPluginIntoInlineConfig<T extends { plugins?: UserConfig['plugins'] }>(
115+
config: T,
116+
): T {
102117
return {
103118
...config,
104119
plugins: [vitePlusTestSpecifierRewritePlugin(), ...(config.plugins ?? [])],
105120
};
106121
}
107122

123+
/**
124+
* Walk `config.test?.projects` and inject the rewrite plugin into each
125+
* project entry. Vitest spins up an independent Vite pipeline per project, so
126+
* root-level plugins do NOT propagate — without this, files matched by a
127+
* project's `include` glob never get the `vite-plus/test` → `vitest` rewrite.
128+
*
129+
* Entry shapes (from `TestProjectConfiguration`):
130+
* - string (glob path like `'./packages/*'`) → passed through unchanged.
131+
* - object (inline config with `test: {...}`) → clone and prepend plugin.
132+
* - function (sync or async) → wrap so its result is injected.
133+
* - Promise (resolves to inline config) → chain `.then(injectPlugin)`.
134+
*/
135+
function injectPluginIntoProject(project: TestProjectConfiguration): TestProjectConfiguration {
136+
if (typeof project === 'string') {
137+
return project;
138+
}
139+
if (typeof project === 'function') {
140+
const fn = project as UserProjectConfigFn;
141+
const wrapped: UserProjectConfigFn = (env: ConfigEnv) => {
142+
const result = fn(env);
143+
if (result instanceof Promise) {
144+
return result.then(injectPluginIntoInlineConfig);
145+
}
146+
return injectPluginIntoInlineConfig(result);
147+
};
148+
return wrapped;
149+
}
150+
if (project instanceof Promise) {
151+
return project.then(injectPluginIntoInlineConfig);
152+
}
153+
if (typeof project === 'object' && project !== null) {
154+
return injectPluginIntoInlineConfig(project as TestProjectInlineConfiguration);
155+
}
156+
return project;
157+
}
158+
159+
function injectPlugin(config: UserConfig): UserConfig {
160+
const injected = injectPluginIntoInlineConfig(config);
161+
const projects = injected.test?.projects;
162+
if (!projects || projects.length === 0) {
163+
return injected;
164+
}
165+
return {
166+
...injected,
167+
test: {
168+
...injected.test,
169+
projects: projects.map(injectPluginIntoProject),
170+
},
171+
};
172+
}
173+
108174
function injectPluginIntoConfig(config: ViteUserConfigExport): ViteUserConfigExport {
109175
if (typeof config === 'function') {
110176
return (env: ConfigEnv) => {

0 commit comments

Comments
 (0)