Skip to content

Commit abc3b2a

Browse files
authored
feat: add lazyPlugins() helper for conditional plugin loading (#1215)
## Summary Add `lazyPlugins()` helper that conditionally loads plugins based on the `VP_COMMAND` environment variable. Plugins load for vite commands (`dev`, `build`, `test`, `preview`) and when `VP_COMMAND` is unset (e.g., running vitest directly or via VS Code extension). Skipped for non-vite commands (`lint`, `fmt`, `check`, etc.). Refs vitejs/vite#22085 ### Usage ```ts import { defineConfig, lazyPlugins } from 'vite-plus' // sync export default defineConfig({ plugins: lazyPlugins(() => [fooPlugin()]), }) // async with dynamic import export default defineConfig({ plugins: lazyPlugins(async () => { const { default: heavyPlugin } = await import('vite-plugin-heavy'); return [heavyPlugin()]; }), }) ``` ### How it works - `bin.ts` sets `VP_COMMAND` env var from the CLI command argument - Rust resolver injects `VP_COMMAND` into child process envs for synthesized subcommands (fixes `vp run build` case) - `lazyPlugins` checks `VP_COMMAND` — executes callback for vite commands, returns `undefined` otherwise - Async callbacks have their Promise wrapped in an array internally for Vite's `asyncFlatten` ## Test plan - [x] Unit tests: 30 tests — lazyPlugins behavior, type compatibility with `plugins` field, defineConfig compatibility, vitest plugin hooks - [x] Snap tests: 6 cases — sync build, async build, vitest plugin, skip-on-lint, `vp run build`, `vp run lint` - [x] `vp check` passes (format + lint) - [x] Windows CI: forward-slash path normalization fix for snap test stability 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent d202f3f commit abc3b2a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+502
-142
lines changed

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,34 @@ 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+
Use `lazyPlugins` to wrap plugin loading. Plugins are only loaded for commands that need them (`dev`, `build`, `test`, `preview`), and skipped for everything else:
97+
98+
```ts
99+
import { defineConfig, lazyPlugins } from 'vite-plus';
100+
import myPlugin from 'vite-plugin-foo';
101+
102+
export default defineConfig({
103+
plugins: lazyPlugins(() => [myPlugin()]),
104+
});
105+
```
106+
107+
For heavy plugins that should be lazily imported, combine with dynamic `import()`:
108+
109+
```ts
110+
import { defineConfig, lazyPlugins } from 'vite-plus';
111+
112+
export default defineConfig({
113+
plugins: lazyPlugins(async () => {
114+
const { default: heavyPlugin } = await import('vite-plugin-heavy');
115+
return [heavyPlugin()];
116+
}),
117+
});
118+
```
119+
92120
## Asking for Help
93121

94122
If you are stuck, please reach out:

packages/cli/binding/src/cli/resolver.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,22 @@ impl SubcommandResolver {
6868
resolved_vite_config: Option<&ResolvedUniversalViteConfig>,
6969
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
7070
cwd: &Arc<AbsolutePath>,
71+
) -> anyhow::Result<ResolvedSubcommand> {
72+
let command_name = subcommand.command_name();
73+
let mut resolved = self.resolve_inner(subcommand, resolved_vite_config, envs, cwd).await?;
74+
// Inject VP_COMMAND so that defineConfig's plugin factory knows which command is running,
75+
// even when the subcommand is synthesized inside `vp run`.
76+
let envs = Arc::make_mut(&mut resolved.envs);
77+
envs.insert(Arc::from(OsStr::new("VP_COMMAND")), Arc::from(OsStr::new(command_name)));
78+
Ok(resolved)
79+
}
80+
81+
async fn resolve_inner(
82+
&self,
83+
subcommand: SynthesizableSubcommand,
84+
resolved_vite_config: Option<&ResolvedUniversalViteConfig>,
85+
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
86+
cwd: &Arc<AbsolutePath>,
7187
) -> anyhow::Result<ResolvedSubcommand> {
7288
match subcommand {
7389
SynthesizableSubcommand::Lint { mut args } => {

packages/cli/binding/src/cli/types.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,24 @@ pub enum SynthesizableSubcommand {
102102
},
103103
}
104104

105+
impl SynthesizableSubcommand {
106+
/// Return the command name string for use in `VP_COMMAND` env var.
107+
pub(super) fn command_name(&self) -> &'static str {
108+
match self {
109+
Self::Lint { .. } => "lint",
110+
Self::Fmt { .. } => "fmt",
111+
Self::Build { .. } => "build",
112+
Self::Test { .. } => "test",
113+
Self::Pack { .. } => "pack",
114+
Self::Dev { .. } => "dev",
115+
Self::Preview { .. } => "preview",
116+
Self::Doc { .. } => "doc",
117+
Self::Install { .. } => "install",
118+
Self::Check { .. } => "check",
119+
}
120+
}
121+
}
122+
105123
/// Top-level CLI argument parser for vite-plus.
106124
#[derive(Debug, Parser)]
107125
#[command(name = "vp", disable_help_subcommand = true)]
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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
import { expect, onTestFinished, 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+
onTestFinished(() => {
9+
fs.rmSync(markerPath, { force: true });
10+
});
11+
expect(fs.existsSync(markerPath)).toBe(true);
12+
expect(fs.readFileSync(markerPath, 'utf-8')).toBe('configureVitest hook executed');
13+
});
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, lazyPlugins } from 'vite-plus';
2+
3+
export default defineConfig({
4+
plugins: lazyPlugins(async () => {
5+
const { default: myVitestPlugin } = await import('./my-vitest-plugin');
6+
return [myVitestPlugin()];
7+
}),
8+
});

0 commit comments

Comments
 (0)