Skip to content

Commit ca6266c

Browse files
committed
feat: extend defineConfig to support lazy plugin loading via factory functions
The `plugins` field in `defineConfig` now accepts factory functions (`() => 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. When VP_COMMAND is unset (e.g., running vitest directly or via VS Code extension), plugins load by default. VP_COMMAND is set automatically by `vp` in bin.ts and injected into child process envs by the Rust resolver for synthesized subcommands. Refs vitejs/vite#22085
1 parent e0ac321 commit ca6266c

38 files changed

Lines changed: 515 additions & 149 deletions

docs/config/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ Vite+ extends the basic Vite configuration with these additions:
2828
- [`test`](/config/test) for Vitest
2929
- [`run`](/config/run) for Vite Task
3030
- [`pack`](/config/pack) for tsdown
31-
- [`staged`](/config/staged) for staged-file checks
31+
- [`staged`](/config/staged) for staged-file checks

docs/guide/troubleshooting.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,37 @@ export default defineConfig({
8989
});
9090
```
9191

92+
## Slow config loading caused by heavy plugins
93+
94+
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.
95+
96+
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:
97+
98+
```ts
99+
import { defineConfig } from 'vite-plus';
100+
101+
export default defineConfig({
102+
plugins: () => [myPlugin()],
103+
});
104+
```
105+
106+
For heavy plugins that should be lazily imported, combine with dynamic `import()`:
107+
108+
```ts
109+
import { defineConfig } from 'vite-plus';
110+
111+
export default defineConfig({
112+
plugins: async () => {
113+
const { default: heavyPlugin } = await import('vite-plugin-heavy');
114+
return [heavyPlugin()];
115+
},
116+
});
117+
```
118+
119+
::: warning
120+
The plugins factory requires `defineConfig` from `vite-plus`, not from `vite`. Vite's native `defineConfig` does not support factory functions for the `plugins` field.
121+
:::
122+
92123
## Asking for Help
93124

94125
If you are stuck, please reach out:

packages/cli/binding/src/cli.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,24 @@ pub enum SynthesizableSubcommand {
123123
},
124124
}
125125

126+
impl SynthesizableSubcommand {
127+
/// Return the command name string for use in `VP_COMMAND` env var.
128+
fn command_name(&self) -> &'static str {
129+
match self {
130+
Self::Lint { .. } => "lint",
131+
Self::Fmt { .. } => "fmt",
132+
Self::Build { .. } => "build",
133+
Self::Test { .. } => "test",
134+
Self::Pack { .. } => "pack",
135+
Self::Dev { .. } => "dev",
136+
Self::Preview { .. } => "preview",
137+
Self::Doc { .. } => "doc",
138+
Self::Install { .. } => "install",
139+
Self::Check { .. } => "check",
140+
}
141+
}
142+
}
143+
126144
/// Top-level CLI argument parser for vite-plus.
127145
#[derive(Debug, Parser)]
128146
#[command(name = "vp", disable_help_subcommand = true)]
@@ -233,6 +251,22 @@ impl SubcommandResolver {
233251
resolved_vite_config: Option<&ResolvedUniversalViteConfig>,
234252
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
235253
cwd: &Arc<AbsolutePath>,
254+
) -> anyhow::Result<ResolvedSubcommand> {
255+
let command_name = subcommand.command_name();
256+
let mut resolved = self.resolve_inner(subcommand, resolved_vite_config, envs, cwd).await?;
257+
// Inject VP_COMMAND so that defineConfig's plugin factory knows which command is running,
258+
// even when the subcommand is synthesized inside `vp run`.
259+
let envs = Arc::make_mut(&mut resolved.envs);
260+
envs.insert(Arc::from(OsStr::new("VP_COMMAND")), Arc::from(OsStr::new(command_name)));
261+
Ok(resolved)
262+
}
263+
264+
async fn resolve_inner(
265+
&self,
266+
subcommand: SynthesizableSubcommand,
267+
resolved_vite_config: Option<&ResolvedUniversalViteConfig>,
268+
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
269+
cwd: &Arc<AbsolutePath>,
236270
) -> anyhow::Result<ResolvedSubcommand> {
237271
match subcommand {
238272
SynthesizableSubcommand::Lint { mut args } => {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
export default function myVitestPlugin() {
5+
return {
6+
name: 'my-vitest-plugin',
7+
configureVitest() {
8+
fs.writeFileSync(
9+
path.join(import.meta.dirname, '.vitest-plugin-loaded'),
10+
'configureVitest hook executed',
11+
);
12+
},
13+
};
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "vite-plugins-async-test",
3+
"private": true
4+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
> vp test # async plugins factory should load vitest plugin with configureVitest hook
2+
RUN <cwd>
3+
4+
✓ src/index.test.ts (1 test) <variable>ms
5+
6+
Test Files 1 passed (1)
7+
Tests 1 passed (1)
8+
Start at <date>
9+
Duration <variable>ms (transform <variable>ms, setup <variable>ms, import <variable>ms, tests <variable>ms, environment <variable>ms)
10+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
import { expect, test } from '@voidzero-dev/vite-plus-test';
5+
6+
test('async plugin factory should load vitest plugin with configureVitest hook', () => {
7+
const markerPath = path.join(import.meta.dirname, '..', '.vitest-plugin-loaded');
8+
expect(fs.existsSync(markerPath)).toBe(true);
9+
expect(fs.readFileSync(markerPath, 'utf-8')).toBe('configureVitest hook executed');
10+
fs.unlinkSync(markerPath);
11+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"commands": [
3+
"vp test # async plugins factory should load vitest plugin with configureVitest hook"
4+
]
5+
}
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+
plugins: async () => {
5+
const { default: myVitestPlugin } = await import('./my-vitest-plugin');
6+
return [myVitestPlugin()];
7+
},
8+
});
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>

0 commit comments

Comments
 (0)