Skip to content

Commit db99bd6

Browse files
committed
feat: replace lazy field with vitePlugins() helper for conditional plugin loading
Replace the `lazy` field approach in `defineConfig` with a standalone `vitePlugins()` function that checks the `VP_COMMAND` environment variable to conditionally load plugins only for commands that need them (dev, build, test, preview). This is simpler, more explicit, and doesn't require modifying defineConfig internals. The VP_COMMAND env var is set automatically by vp for every command. Refs vitejs/vite#22085
1 parent cc8352b commit db99bd6

7 files changed

Lines changed: 84 additions & 250 deletions

File tree

docs/config/troubleshooting.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,31 @@ 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 `lazy` field in `defineConfig` to defer heavy plugin loading. Plugins provided through `lazy` are only resolved when Vite actually needs them:
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`):
1010

1111
```ts
12-
import { defineConfig } from 'vite-plus';
12+
import { defineConfig, vitePlugins } from 'vite-plus';
1313

1414
export default defineConfig({
15-
lazy: async () => {
16-
const { default: heavyPlugin } = await import('vite-plugin-heavy');
17-
return { plugins: [heavyPlugin()] };
18-
},
15+
plugins: vitePlugins(() => [myPlugin()]),
1916
});
2017
```
2118

22-
You can keep lightweight plugins inline and defer only the expensive ones. Plugins from `lazy` are appended after existing plugins:
19+
For heavy plugins that should be lazily imported, combine with dynamic `import()`:
2320

2421
```ts
25-
import { defineConfig } from 'vite-plus';
26-
import lightPlugin from 'vite-plugin-light';
22+
import { defineConfig, vitePlugins } from 'vite-plus';
2723

2824
export default defineConfig({
29-
plugins: [lightPlugin()],
30-
lazy: async () => {
25+
plugins: vitePlugins(async () => {
3126
const { default: heavyPlugin } = await import('vite-plugin-heavy');
32-
return { plugins: [heavyPlugin()] };
33-
},
27+
return [heavyPlugin()];
28+
}),
3429
});
3530
```
3631

37-
The resulting plugin order is: `[lightPlugin(), heavyPlugin()]`.
32+
Plugins load for `dev`, `build`, `test`, and `preview`. They are skipped for `lint`, `fmt`, `check`, and other commands that don't need them.
3833

3934
::: info
40-
The `lazy` field is a Vite+ extension. We plan to support this in upstream Vite in the future.
35+
`vitePlugins()` works by checking the `VP_COMMAND` environment variable, which is automatically set by `vp` for every command.
4136
:::
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { defineConfig } from 'vite-plus';
1+
import { defineConfig, vitePlugins } from 'vite-plus';
22

33
export default defineConfig({
4-
lazy: async () => {
4+
plugins: vitePlugins(async () => {
55
const { default: myLazyPlugin } = await import('./my-plugin');
6-
return { plugins: [myLazyPlugin()] };
7-
},
6+
return [myLazyPlugin()];
7+
}),
88
});
Lines changed: 59 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@voidzero-dev/vite-plus-test';
1+
import { afterEach, beforeEach, expect, test } from '@voidzero-dev/vite-plus-test';
22

33
import {
44
configDefaults,
@@ -8,196 +8,78 @@ import {
88
defaultBrowserPort,
99
defineConfig,
1010
defineProject,
11+
vitePlugins,
1112
} from '../index.js';
1213

14+
let originalVpCommand: string | undefined;
15+
16+
beforeEach(() => {
17+
originalVpCommand = process.env.VP_COMMAND;
18+
});
19+
20+
afterEach(() => {
21+
if (originalVpCommand === undefined) {
22+
delete process.env.VP_COMMAND;
23+
} else {
24+
process.env.VP_COMMAND = originalVpCommand;
25+
}
26+
});
27+
1328
test('should keep vitest exports stable', () => {
1429
expect(defineConfig).toBeTypeOf('function');
1530
expect(defineProject).toBeTypeOf('function');
31+
expect(vitePlugins).toBeTypeOf('function');
1632
expect(configDefaults).toBeDefined();
1733
expect(coverageConfigDefaults).toBeDefined();
1834
expect(defaultExclude).toBeDefined();
1935
expect(defaultInclude).toBeDefined();
2036
expect(defaultBrowserPort).toBeDefined();
2137
});
2238

23-
test('should support lazy loading of plugins', async () => {
24-
const config = await defineConfig({
25-
lazy: () => Promise.resolve({ plugins: [{ name: 'test' }] }),
26-
});
27-
expect(config.plugins?.length).toBe(1);
28-
});
29-
30-
test('should merge lazy plugins with existing plugins', async () => {
31-
const config = await defineConfig({
32-
plugins: [{ name: 'existing' }],
33-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
34-
});
35-
expect(config.plugins?.length).toBe(2);
36-
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
37-
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
38-
});
39-
40-
test('should handle lazy with empty plugins array', async () => {
41-
const config = await defineConfig({
42-
lazy: () => Promise.resolve({ plugins: [] }),
43-
});
44-
expect(config.plugins?.length).toBe(0);
45-
});
46-
47-
test('should handle lazy returning undefined plugins', async () => {
48-
const config = await defineConfig({
49-
lazy: () => Promise.resolve({}),
50-
});
51-
expect(config.plugins?.length).toBe(0);
52-
});
53-
54-
test('should handle Promise config with lazy', async () => {
55-
const config = await defineConfig(
56-
Promise.resolve({
57-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-promise' }] }),
58-
}),
59-
);
60-
expect(config.plugins?.length).toBe(1);
61-
expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-promise');
62-
});
63-
64-
test('should handle Promise config with lazy and existing plugins', async () => {
65-
const config = await defineConfig(
66-
Promise.resolve({
67-
plugins: [{ name: 'existing' }],
68-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
69-
}),
70-
);
71-
expect(config.plugins?.length).toBe(2);
72-
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
73-
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
74-
});
75-
76-
test('should handle Promise config without lazy', async () => {
77-
const config = await defineConfig(
78-
Promise.resolve({
79-
plugins: [{ name: 'no-lazy' }],
80-
}),
81-
);
82-
expect(config.plugins?.length).toBe(1);
83-
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
84-
});
85-
86-
test('should handle function config with lazy', async () => {
87-
const configFn = defineConfig(() => ({
88-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-fn' }] }),
89-
}));
90-
expect(typeof configFn).toBe('function');
91-
const config = await configFn({ command: 'build', mode: 'production' });
92-
expect(config.plugins?.length).toBe(1);
93-
expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-fn');
94-
});
95-
96-
test('should handle function config with lazy and existing plugins', async () => {
97-
const configFn = defineConfig(() => ({
98-
plugins: [{ name: 'existing' }],
99-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
100-
}));
101-
const config = await configFn({ command: 'build', mode: 'production' });
102-
expect(config.plugins?.length).toBe(2);
103-
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
104-
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
105-
});
106-
107-
test('should handle function config without lazy', () => {
108-
const configFn = defineConfig(() => ({
109-
plugins: [{ name: 'no-lazy' }],
110-
}));
111-
const config = configFn({ command: 'build', mode: 'production' });
112-
expect(config.plugins?.length).toBe(1);
113-
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
114-
});
115-
116-
test('should handle async function config with lazy', async () => {
117-
const configFn = defineConfig(async () => ({
118-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-async-fn' }] }),
119-
}));
120-
const config = await configFn({ command: 'build', mode: 'production' });
121-
expect(config.plugins?.length).toBe(1);
122-
expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-async-fn');
123-
});
124-
125-
test('should handle async function config with lazy and existing plugins', async () => {
126-
const configFn = defineConfig(async () => ({
127-
plugins: [{ name: 'existing' }],
128-
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
129-
}));
130-
const config = await configFn({ command: 'build', mode: 'production' });
131-
expect(config.plugins?.length).toBe(2);
132-
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
133-
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
134-
});
135-
136-
test('should handle async function config without lazy', async () => {
137-
const configFn = defineConfig(async () => ({
138-
plugins: [{ name: 'no-lazy' }],
139-
}));
140-
const config = await configFn({ command: 'build', mode: 'production' });
141-
expect(config.plugins?.length).toBe(1);
142-
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
143-
});
144-
145-
test('should support async/await lazy loading of plugins', async () => {
146-
const config = await defineConfig({
147-
lazy: async () => {
148-
const plugins = [{ name: 'async-lazy' }];
149-
return { plugins };
150-
},
151-
});
152-
expect(config.plugins?.length).toBe(1);
153-
expect((config.plugins?.[0] as { name: string })?.name).toBe('async-lazy');
154-
});
155-
156-
test('should merge async/await lazy plugins with existing plugins', async () => {
157-
const config = await defineConfig({
158-
plugins: [{ name: 'existing' }],
159-
lazy: async () => {
160-
const plugins = [{ name: 'async-lazy' }];
161-
return { plugins };
162-
},
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();
49+
});
50+
51+
test.each(['dev', 'build', 'test', 'preview'])(
52+
'vitePlugins executes callback when VP_COMMAND is %s',
53+
(cmd) => {
54+
process.env.VP_COMMAND = cmd;
55+
const result = vitePlugins(() => [{ name: 'my-plugin' }]);
56+
expect(result).toEqual([{ name: 'my-plugin' }]);
57+
},
58+
);
59+
60+
test.each(['lint', 'fmt', 'check', 'pack', 'install', 'run'])(
61+
'vitePlugins returns undefined when VP_COMMAND is %s',
62+
(cmd) => {
63+
process.env.VP_COMMAND = cmd;
64+
const result = vitePlugins(() => [{ name: 'my-plugin' }]);
65+
expect(result).toBeUndefined();
66+
},
67+
);
68+
69+
test('vitePlugins supports async callback', async () => {
70+
process.env.VP_COMMAND = 'build';
71+
const result = vitePlugins(async () => {
72+
const plugin = await Promise.resolve({ name: 'async-plugin' });
73+
return [plugin];
16374
});
164-
expect(config.plugins?.length).toBe(2);
165-
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
166-
expect((config.plugins?.[1] as { name: string })?.name).toBe('async-lazy');
75+
expect(result).toBeInstanceOf(Promise);
76+
expect(await result).toEqual([{ name: 'async-plugin' }]);
16777
});
16878

169-
test('should support async/await lazy with dynamic import pattern', async () => {
170-
const config = await defineConfig({
171-
lazy: async () => {
172-
// simulates: const { default: plugin } = await import('heavy-plugin')
173-
const plugin = await Promise.resolve({ name: 'dynamic-import-plugin' });
174-
return { plugins: [plugin] };
175-
},
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' }];
17683
});
177-
expect(config.plugins?.length).toBe(1);
178-
expect((config.plugins?.[0] as { name: string })?.name).toBe('dynamic-import-plugin');
179-
});
180-
181-
test('should support async/await lazy in async function config', async () => {
182-
const configFn = defineConfig(async () => ({
183-
lazy: async () => {
184-
const plugins = [{ name: 'async-fn-async-lazy' }];
185-
return { plugins };
186-
},
187-
}));
188-
const config = await configFn({ command: 'build', mode: 'production' });
189-
expect(config.plugins?.length).toBe(1);
190-
expect((config.plugins?.[0] as { name: string })?.name).toBe('async-fn-async-lazy');
191-
});
192-
193-
test('should support async/await lazy in sync function config', async () => {
194-
const configFn = defineConfig(() => ({
195-
lazy: async () => {
196-
const plugins = [{ name: 'sync-fn-async-lazy' }];
197-
return { plugins };
198-
},
199-
}));
200-
const config = await configFn({ command: 'build', mode: 'production' });
201-
expect(config.plugins?.length).toBe(1);
202-
expect((config.plugins?.[0] as { name: string })?.name).toBe('sync-fn-async-lazy');
84+
expect(result).toBeUndefined();
20385
});

packages/cli/src/bin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ if (args[0] === 'help' && args[1]) {
4545
}
4646

4747
const command = args[0];
48+
process.env.VP_COMMAND = command ?? '';
4849

4950
// Global commands — handled by tsdown-bundled modules in dist/
5051
if (command === 'create') {

0 commit comments

Comments
 (0)