Skip to content

Commit 49d17df

Browse files
committed
docs: add lazy loading plugins documentation and tests
Document the `lazy` field in config page with usage examples showing how to defer heavy plugin imports using async/await dynamic imports. Add unit tests for async/await lazy loading patterns and a snap test that verifies lazy-loaded plugins are applied during vp build.
1 parent a3c80d5 commit 49d17df

8 files changed

Lines changed: 165 additions & 0 deletions

File tree

docs/config/index.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,66 @@ Vite+ extends the basic Vite configuration with these additions:
2929
- [`run`](/config/run) for Vite Task
3030
- [`pack`](/config/pack) for tsdown
3131
- [`staged`](/config/staged) for staged-file checks
32+
33+
## Lazy Loading Plugins
34+
35+
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.
36+
37+
The `lazy` field solves this by letting you defer plugin loading into an async function. Plugins provided through `lazy` are only resolved when actually needed:
38+
39+
```ts
40+
import { defineConfig } from 'vite-plus';
41+
42+
export default defineConfig({
43+
lazy: async () => {
44+
const { default: myHeavyPlugins } = await import('./my-heavy-plugins');
45+
return { plugins: myHeavyPlugins };
46+
},
47+
});
48+
```
49+
50+
### Type Signature
51+
52+
```ts
53+
lazy?: () => Promise<{
54+
plugins?: Plugin[];
55+
}>;
56+
```
57+
58+
### Merging with Existing Plugins
59+
60+
Plugins returned from `lazy` are appended after any plugins already in the `plugins` array. This lets you keep lightweight plugins inline and defer only the expensive ones:
61+
62+
```ts
63+
import { defineConfig } from 'vite-plus';
64+
import lightPlugin from 'vite-plugin-light';
65+
66+
export default defineConfig({
67+
plugins: [lightPlugin()],
68+
lazy: async () => {
69+
const { default: heavyPlugin } = await import('vite-plugin-heavy');
70+
return { plugins: [heavyPlugin()] };
71+
},
72+
});
73+
```
74+
75+
The resulting plugin order is: `[lightPlugin(), heavyPlugin()]`.
76+
77+
### Function Config
78+
79+
`lazy` also works with function-style and async function-style configs:
80+
81+
```ts
82+
import { defineConfig } from 'vite-plus';
83+
84+
export default defineConfig(async () => ({
85+
lazy: async () => {
86+
const { default: heavyPlugin } = await import('vite-plugin-heavy');
87+
return { plugins: [heavyPlugin()] };
88+
},
89+
}));
90+
```
91+
92+
::: info
93+
The `lazy` field is a temporary Vite+ extension. We plan to support this in upstream Vite in the future.
94+
:::
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<html>
3+
<body>
4+
<script type="module">
5+
console.log('hello');
6+
</script>
7+
</body>
8+
</html>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function myLazyPlugin() {
2+
return {
3+
name: 'my-lazy-plugin',
4+
transformIndexHtml(html: string) {
5+
return html.replace('</body>', '<!-- lazy-plugin-injected --></body>');
6+
},
7+
};
8+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "lazy-loading-plugins-test",
3+
"private": true
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
> # Test that plugins loaded via lazy field are applied during build
2+
> vp build
3+
> cat dist/index.html | grep 'lazy-plugin-injected'
4+
<!-- lazy-plugin-injected --></body>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"commands": [
3+
"# Test that plugins loaded via lazy field are applied during build",
4+
{
5+
"command": "vp build",
6+
"ignoreOutput": true
7+
},
8+
"cat dist/index.html | grep 'lazy-plugin-injected'"
9+
]
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from 'vite-plus';
2+
3+
export default defineConfig({
4+
lazy: async () => {
5+
const { default: myLazyPlugin } = await import('./my-plugin');
6+
return { plugins: [myLazyPlugin()] };
7+
},
8+
});

packages/cli/src/__tests__/index.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,63 @@ test('should handle async function config without lazy', async () => {
141141
expect(config.plugins?.length).toBe(1);
142142
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
143143
});
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+
},
163+
});
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');
167+
});
168+
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+
},
176+
});
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');
203+
});

0 commit comments

Comments
 (0)