Skip to content

Commit 7ef3f0e

Browse files
committed
feat: extend defineConfig plugins to accept factory functions
Replace the standalone vitePlugins() helper with built-in support in defineConfig for plugins factory functions. The plugins field now accepts () => PluginOption[] or async () => Promise<PluginOption[]> in addition to the standard PluginOption[] array. The factory is only called for vite commands (dev, build, test, preview) and skipped for non-vite commands (lint, fmt, check, etc.), avoiding unnecessary plugin loading overhead. Refs vitejs/vite#22085
1 parent 5d8c4d2 commit 7ef3f0e

8 files changed

Lines changed: 233 additions & 89 deletions

File tree

docs/config/troubleshooting.md

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,29 @@ Use this page when your Vite+ configuration is not behaving the way you expect.
66

77
When `vite.config.ts` imports heavy plugins at the top level, every `import` is evaluated eagerly, even for commands like `vp lint` or `vp fmt` that don't need those plugins. This can make config loading noticeably slow.
88

9-
Use the `vitePlugins()` helper to conditionally load plugins. It checks which `vp` command is running and skips plugin loading for commands that don't need them (like `lint`, `fmt`, `check`):
9+
Pass a factory function to `plugins` in `defineConfig` to defer plugin loading. The factory is only called for commands that need plugins (`dev`, `build`, `test`, `preview`), and skipped for everything else:
1010

1111
```ts
12-
import { defineConfig, vitePlugins } from 'vite-plus';
13-
14-
import myPlugin from 'vite-plugin-foo';
12+
import { defineConfig } from 'vite-plus';
1513

1614
export default defineConfig({
17-
plugins: [
18-
vitePlugins(() => [myPlugin()]),
19-
],
15+
plugins: () => [myPlugin()],
2016
});
2117
```
2218

2319
For heavy plugins that should be lazily imported, combine with dynamic `import()`:
2420

2521
```ts
26-
import { defineConfig, vitePlugins } from 'vite-plus';
22+
import { defineConfig } from 'vite-plus';
2723

2824
export default defineConfig({
29-
plugins: [
30-
vitePlugins(async () => {
31-
const { default: heavyPlugin } = await import('vite-plugin-heavy');
32-
return [heavyPlugin()];
33-
}),
34-
],
25+
plugins: async () => {
26+
const { default: heavyPlugin } = await import('vite-plugin-heavy');
27+
return [heavyPlugin()];
28+
},
3529
});
3630
```
3731

38-
Plugins load for `dev`, `build`, `test`, and `preview`. They are skipped for `lint`, `fmt`, `check`, and other commands that don't need them.
39-
4032
::: info
41-
`vitePlugins()` works by checking the `VP_COMMAND` environment variable, which is automatically set by `vp` for every command.
33+
The plugins factory works by checking the `VP_COMMAND` environment variable, which is automatically set by `vp` for every command.
4234
:::
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { defineConfig, vitePlugins } from 'vite-plus';
1+
import { defineConfig } from 'vite-plus';
22

33
export default defineConfig({
4-
plugins: [
5-
vitePlugins(async () => {
6-
const { default: myLazyPlugin } = await import('./my-plugin');
7-
return [myLazyPlugin()];
8-
}),
9-
],
4+
plugins: async () => {
5+
const { default: myLazyPlugin } = await import('./my-plugin');
6+
return [myLazyPlugin()];
7+
},
108
});
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { defineConfig, vitePlugins } from 'vite-plus';
1+
import { defineConfig } from 'vite-plus';
22

33
export default defineConfig({
4-
plugins: [
5-
vitePlugins(async () => {
6-
const { default: heavyPlugin } = await import('./heavy-plugin');
7-
return [heavyPlugin()];
8-
}),
9-
],
4+
plugins: async () => {
5+
const { default: heavyPlugin } = await import('./heavy-plugin');
6+
return [heavyPlugin()];
7+
},
108
});
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { defineConfig, vitePlugins } from 'vite-plus';
1+
import { defineConfig } from 'vite-plus';
22

33
import mySyncPlugin from './my-plugin';
44

55
export default defineConfig({
6-
plugins: [vitePlugins(() => [mySyncPlugin()])],
6+
plugins: () => [mySyncPlugin()],
77
});
Lines changed: 147 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterEach, beforeEach, expect, test } from '@voidzero-dev/vite-plus-test';
1+
import { afterEach, beforeEach, expect, test, vi } from '@voidzero-dev/vite-plus-test';
22

33
import {
44
configDefaults,
@@ -8,7 +8,6 @@ import {
88
defaultBrowserPort,
99
defineConfig,
1010
defineProject,
11-
vitePlugins,
1211
} from '../index.js';
1312

1413
let originalVpCommand: string | undefined;
@@ -28,58 +27,176 @@ afterEach(() => {
2827
test('should keep vitest exports stable', () => {
2928
expect(defineConfig).toBeTypeOf('function');
3029
expect(defineProject).toBeTypeOf('function');
31-
expect(vitePlugins).toBeTypeOf('function');
3230
expect(configDefaults).toBeDefined();
3331
expect(coverageConfigDefaults).toBeDefined();
3432
expect(defaultExclude).toBeDefined();
3533
expect(defaultInclude).toBeDefined();
3634
expect(defaultBrowserPort).toBeDefined();
3735
});
3836

39-
test('vitePlugins returns undefined when VP_COMMAND is unset', () => {
40-
delete process.env.VP_COMMAND;
41-
const result = vitePlugins(() => [{ name: 'test' }]);
42-
expect(result).toBeUndefined();
43-
});
44-
45-
test('vitePlugins returns undefined when VP_COMMAND is empty string', () => {
46-
process.env.VP_COMMAND = '';
47-
const result = vitePlugins(() => [{ name: 'test' }]);
48-
expect(result).toBeUndefined();
37+
test('defineConfig passes through plain plugins array', () => {
38+
process.env.VP_COMMAND = 'build';
39+
const config = defineConfig({
40+
plugins: [{ name: 'test-plugin' }],
41+
});
42+
expect(config.plugins?.length).toBe(1);
4943
});
5044

5145
test.each(['dev', 'build', 'test', 'preview'])(
52-
'vitePlugins executes callback when VP_COMMAND is %s',
46+
'defineConfig executes sync plugins factory when VP_COMMAND is %s',
5347
(cmd) => {
5448
process.env.VP_COMMAND = cmd;
55-
const result = vitePlugins(() => [{ name: 'my-plugin' }]);
56-
expect(result).toEqual([{ name: 'my-plugin' }]);
49+
const config = defineConfig({
50+
plugins: () => [{ name: 'factory-plugin' }],
51+
});
52+
expect(config.plugins?.length).toBe(1);
53+
expect((config.plugins?.[0] as { name: string })?.name).toBe('factory-plugin');
54+
},
55+
);
56+
57+
test.each(['dev', 'build', 'test', 'preview'])(
58+
'defineConfig executes async plugins factory when VP_COMMAND is %s',
59+
async (cmd) => {
60+
process.env.VP_COMMAND = cmd;
61+
const config = defineConfig({
62+
plugins: async () => {
63+
const plugin = await Promise.resolve({ name: 'async-factory-plugin' });
64+
return [plugin];
65+
},
66+
});
67+
// Async factory wraps the promise in an array for asyncFlatten
68+
expect(config.plugins?.length).toBe(1);
69+
const resolved = await (config.plugins?.[0] as Promise<{ name: string }[]>);
70+
expect(resolved.length).toBe(1);
71+
expect(resolved[0].name).toBe('async-factory-plugin');
5772
},
5873
);
5974

6075
test.each(['lint', 'fmt', 'check', 'pack', 'install', 'run'])(
61-
'vitePlugins returns undefined when VP_COMMAND is %s',
76+
'defineConfig skips plugins factory when VP_COMMAND is %s',
6277
(cmd) => {
6378
process.env.VP_COMMAND = cmd;
64-
const result = vitePlugins(() => [{ name: 'my-plugin' }]);
65-
expect(result).toBeUndefined();
79+
const factoryFn = vi.fn(() => [{ name: 'should-not-load' }]);
80+
const config = defineConfig({
81+
plugins: factoryFn,
82+
});
83+
expect(factoryFn).not.toHaveBeenCalled();
84+
expect(config.plugins?.length).toBe(0);
6685
},
6786
);
6887

69-
test('vitePlugins supports async callback', async () => {
88+
test('defineConfig skips plugins factory when VP_COMMAND is unset', () => {
89+
delete process.env.VP_COMMAND;
90+
const factoryFn = vi.fn(() => [{ name: 'should-not-load' }]);
91+
const config = defineConfig({
92+
plugins: factoryFn,
93+
});
94+
expect(factoryFn).not.toHaveBeenCalled();
95+
expect(config.plugins?.length).toBe(0);
96+
});
97+
98+
test('defineConfig handles function config with plugins factory', () => {
99+
process.env.VP_COMMAND = 'build';
100+
const configFn = defineConfig(() => ({
101+
plugins: () => [{ name: 'fn-factory-plugin' }],
102+
}));
103+
const config = configFn({ command: 'build', mode: 'production' });
104+
const plugins = config.plugins as { name: string }[];
105+
expect(plugins?.length).toBe(1);
106+
expect(plugins?.[0]?.name).toBe('fn-factory-plugin');
107+
});
108+
109+
test('defineConfig handles async function config with plugins factory', async () => {
70110
process.env.VP_COMMAND = 'build';
71-
const result = vitePlugins(async () => {
72-
const plugin = await Promise.resolve({ name: 'async-plugin' });
73-
return [plugin];
111+
const configFn = defineConfig(async () => ({
112+
plugins: () => [{ name: 'async-fn-factory-plugin' }],
113+
}));
114+
const config = await configFn({ command: 'build', mode: 'production' });
115+
const plugins = config.plugins as { name: string }[];
116+
expect(plugins?.length).toBe(1);
117+
expect(plugins?.[0]?.name).toBe('async-fn-factory-plugin');
118+
});
119+
120+
test('defineConfig handles Promise config with plugins factory', async () => {
121+
process.env.VP_COMMAND = 'build';
122+
const config = await defineConfig(
123+
Promise.resolve({
124+
plugins: () => [{ name: 'promise-factory-plugin' }],
125+
}),
126+
);
127+
expect(config.plugins?.length).toBe(1);
128+
expect((config.plugins?.[0] as { name: string })?.name).toBe('promise-factory-plugin');
129+
});
130+
131+
// Vite/Vitest PluginOption compatibility tests
132+
// PluginOption = Thenable<Plugin | { name: string } | false | null | undefined | PluginOption[]>
133+
134+
test('defineConfig supports Plugin objects in plugins array', () => {
135+
const config = defineConfig({
136+
plugins: [{ name: 'plugin-a' }, { name: 'plugin-b' }],
137+
});
138+
expect(config.plugins?.length).toBe(2);
139+
});
140+
141+
test('defineConfig supports falsy values in plugins array', () => {
142+
const config = defineConfig({
143+
plugins: [{ name: 'real-plugin' }, false, null, undefined],
144+
});
145+
expect(config.plugins?.length).toBe(4);
146+
});
147+
148+
test('defineConfig supports nested plugin arrays', () => {
149+
const config = defineConfig({
150+
plugins: [[{ name: 'nested-a' }, { name: 'nested-b' }], { name: 'top-level' }],
151+
});
152+
expect(config.plugins?.length).toBe(2);
153+
});
154+
155+
test('defineConfig supports Promise<Plugin> in plugins array', () => {
156+
const config = defineConfig({
157+
plugins: [Promise.resolve({ name: 'async-plugin' })],
74158
});
75-
expect(result).toBeInstanceOf(Promise);
76-
expect(await result).toEqual([{ name: 'async-plugin' }]);
159+
expect(config.plugins?.length).toBe(1);
77160
});
78161

79-
test('vitePlugins returns undefined for async callback when skipped', () => {
80-
process.env.VP_COMMAND = 'lint';
81-
const result = vitePlugins(async () => {
82-
return [{ name: 'async-plugin' }];
162+
test('defineConfig supports mixed PluginOption types in array', () => {
163+
const config = defineConfig({
164+
plugins: [
165+
{ name: 'sync-plugin' },
166+
false,
167+
Promise.resolve({ name: 'promised-plugin' }),
168+
[{ name: 'nested-plugin' }],
169+
null,
170+
undefined,
171+
],
83172
});
84-
expect(result).toBeUndefined();
173+
expect(config.plugins?.length).toBe(6);
174+
});
175+
176+
test('defineConfig supports empty plugins array', () => {
177+
const config = defineConfig({
178+
plugins: [],
179+
});
180+
expect(config.plugins?.length).toBe(0);
181+
});
182+
183+
test('defineConfig supports config without plugins', () => {
184+
const config = defineConfig({});
185+
expect(config.plugins).toBeUndefined();
186+
});
187+
188+
test('defineConfig supports function config with plain plugins array', () => {
189+
const configFn = defineConfig(() => ({
190+
plugins: [{ name: 'fn-plugin' }],
191+
}));
192+
const config = configFn({ command: 'build', mode: 'production' });
193+
expect(config.plugins?.length).toBe(1);
194+
});
195+
196+
test('defineConfig supports async function config with plain plugins array', async () => {
197+
const configFn = defineConfig(async () => ({
198+
plugins: [{ name: 'async-fn-plugin' }],
199+
}));
200+
const config = await configFn({ command: 'build', mode: 'production' });
201+
expect(config.plugins?.length).toBe(1);
85202
});

0 commit comments

Comments
 (0)