From ec0a6ddf0fc8b41ed4767de0ff3c6fa603045dcf Mon Sep 17 00:00:00 2001
From: MK
Date: Wed, 24 Jun 2026 22:23:43 +0800
Subject: [PATCH] fix(cli): skip lazyPlugins only while resolving config
metadata
lazyPlugins decided whether to run the user's plugin factory from
VP_COMMAND. That signal is overloaded (the `vp run` task-discovery read
and a verbatim build child both see `run`), unnormalized (the `vp format`
alias is `format`, not `fmt`), and inherited by spawned children. As a
result a Vite build spawned under `vp run` (a verbatim `node build.mjs`
task) or `vp exec` built without the user's plugins, while `vp format`
wrongly loaded them.
Decide from the resolution intent instead: the metadata resolvers
(resolveViteConfig, migrate's rolldown-compat check, and the oxlint/oxfmt
subprocess) mark that the config is being loaded only to read a config
block, and lazyPlugins skips the factory only while that marker is set.
The default is to load, so dev/build/test/preview and any spawned build
keep their plugins. VP_COMMAND had no other reader, so it is removed.
---
docs/guide/troubleshooting.md | 2 +-
packages/cli/bin/oxfmt | 9 +-
packages/cli/bin/oxlint | 9 +-
packages/cli/binding/src/cli/resolver.rs | 8 +-
packages/cli/binding/src/cli/types.rs | 17 ---
.../vite-plugins-exec-build/build.mjs | 8 ++
.../vite-plugins-exec-build/index.html | 8 ++
.../vite-plugins-exec-build/my-plugin.ts | 8 ++
.../vite-plugins-exec-build/package.json | 4 +
.../vite-plugins-exec-build/snap.txt | 3 +
.../vite-plugins-exec-build/steps.json | 9 ++
.../vite-plugins-exec-build/vite.config.ts | 8 ++
.../vite-plugins-run-verbatim-build/build.mjs | 8 ++
.../index.html | 8 ++
.../my-plugin.ts | 8 ++
.../package.json | 7 ++
.../vite-plugins-run-verbatim-build/snap.txt | 3 +
.../steps.json | 9 ++
.../vite.config.ts | 8 ++
.../heavy-plugin.ts | 5 +
.../vite-plugins-skip-on-format/package.json | 4 +
.../vite-plugins-skip-on-format/snap.txt | 3 +
.../vite-plugins-skip-on-format/src/index.ts | 1 +
.../vite-plugins-skip-on-format/steps.json | 5 +
.../vite.config.ts | 8 ++
packages/cli/src/__tests__/index.spec.ts | 100 ++++++++++++------
packages/cli/src/bin.ts | 1 -
packages/cli/src/define-config.ts | 60 +++++++++--
packages/cli/src/migration/bin.ts | 8 +-
packages/cli/src/resolve-fmt.ts | 5 +-
packages/cli/src/resolve-lint.ts | 5 +-
packages/cli/src/resolve-vite-config.ts | 22 ++--
packages/cli/src/utils/constants.ts | 7 ++
33 files changed, 288 insertions(+), 90 deletions(-)
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/build.mjs
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/index.html
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/my-plugin.ts
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/package.json
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/snap.txt
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/steps.json
create mode 100644 packages/cli/snap-tests/vite-plugins-exec-build/vite.config.ts
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/build.mjs
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/index.html
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/my-plugin.ts
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/package.json
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/snap.txt
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/steps.json
create mode 100644 packages/cli/snap-tests/vite-plugins-run-verbatim-build/vite.config.ts
create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-format/heavy-plugin.ts
create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-format/package.json
create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-format/snap.txt
create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-format/src/index.ts
create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-format/steps.json
create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-format/vite.config.ts
diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md
index 7ea325d61c..464c600b00 100644
--- a/docs/guide/troubleshooting.md
+++ b/docs/guide/troubleshooting.md
@@ -69,7 +69,7 @@ export default defineConfig({
When `vite.config.ts` imports plugins at the top level, they are evaluated for every command, including `vp lint`, `vp fmt`, editor integrations, and long-lived background processes. This can make config loading slow and may trigger plugin setup side effects, such as reading files, starting watchers, or connecting to services.
-Use `lazyPlugins` to load plugins only when the Vite pipeline actually runs (`dev`, `build`, `test`, `preview`):
+Use `lazyPlugins` to skip the plugin factory when vite-plus loads your config only to read a metadata block (`lint`, `fmt`, `check`, `staged`, `pack`, `create`, the `run`/`cache` task lookup, and editor tooling). The plugins still load whenever Vite actually runs, `dev`, `build`, `test`, `preview`, and any build your own scripts spawn (a `vp run` task, `vp exec`):
```ts [vite.config.ts]
import { defineConfig, lazyPlugins } from 'vite-plus';
diff --git a/packages/cli/bin/oxfmt b/packages/cli/bin/oxfmt
index 142b0989ff..db9da6b699 100755
--- a/packages/cli/bin/oxfmt
+++ b/packages/cli/bin/oxfmt
@@ -23,8 +23,11 @@ const oxfmtBin = join(dirname(dirname(oxfmtMainPath)), 'bin', 'oxfmt');
// This allows oxfmt to load vite.config.ts.
// For `vp check` and `vp fmt`, VP_VERSION is injected by
-// `merge_resolved_envs_with_version()` in `cli.rs`, and VP_COMMAND is injected
-// by `SubcommandResolver::resolve()`.
+// `merge_resolved_envs_with_version()` in `cli.rs`.
process.env.VP_VERSION = pkg.version;
-process.env.VP_COMMAND ??= 'fmt';
+// oxfmt reads vite.config.ts only for the `fmt` block, so skip the user's
+// Vite plugin factory (lazyPlugins) while the config evaluates.
+// Literal kept in sync with CONFIG_METADATA_ENV in src/utils/constants.ts
+// (this plain-JS bin can't import the bundled constant).
+process.env.VP_RESOLVING_CONFIG_METADATA ??= '1';
await import(pathToFileURL(oxfmtBin).href);
diff --git a/packages/cli/bin/oxlint b/packages/cli/bin/oxlint
index e135a09cac..6b08f5afef 100755
--- a/packages/cli/bin/oxlint
+++ b/packages/cli/bin/oxlint
@@ -27,9 +27,12 @@ const tsgolintBin = resolveTsgolintExecutable(
// This allows oxlint to load vite.config.ts.
// For `vp check` and `vp lint`, VP_VERSION is injected by
-// `merge_resolved_envs_with_version()` in `cli.rs`, and VP_COMMAND is injected
-// by `SubcommandResolver::resolve()`.
+// `merge_resolved_envs_with_version()` in `cli.rs`.
process.env.VP_VERSION = pkg.version;
-process.env.VP_COMMAND ??= 'lint';
+// oxlint reads vite.config.ts only for the `lint` block, so skip the user's
+// Vite plugin factory (lazyPlugins) while the config evaluates.
+// Literal kept in sync with CONFIG_METADATA_ENV in src/utils/constants.ts
+// (this plain-JS bin can't import the bundled constant).
+process.env.VP_RESOLVING_CONFIG_METADATA ??= '1';
process.env.OXLINT_TSGOLINT_PATH ??= tsgolintBin;
await import(pathToFileURL(oxlintBin).href);
diff --git a/packages/cli/binding/src/cli/resolver.rs b/packages/cli/binding/src/cli/resolver.rs
index c050801113..578d525834 100644
--- a/packages/cli/binding/src/cli/resolver.rs
+++ b/packages/cli/binding/src/cli/resolver.rs
@@ -68,13 +68,7 @@ impl SubcommandResolver {
resolved_vite_config: Option<&ResolvedUniversalViteConfig>,
envs: &Arc, Arc>>,
) -> anyhow::Result {
- let command_name = subcommand.command_name();
- let mut resolved = self.resolve_inner(subcommand, resolved_vite_config, envs).await?;
- // Inject VP_COMMAND so that defineConfig's plugin factory knows which command is running,
- // even when the subcommand is synthesized inside `vp run`.
- let envs = Arc::make_mut(&mut resolved.envs);
- envs.insert(Arc::from(OsStr::new("VP_COMMAND")), Arc::from(OsStr::new(command_name)));
- Ok(resolved)
+ self.resolve_inner(subcommand, resolved_vite_config, envs).await
}
async fn resolve_inner(
diff --git a/packages/cli/binding/src/cli/types.rs b/packages/cli/binding/src/cli/types.rs
index e19fd9f148..b0b935af02 100644
--- a/packages/cli/binding/src/cli/types.rs
+++ b/packages/cli/binding/src/cli/types.rs
@@ -96,23 +96,6 @@ pub enum SynthesizableSubcommand {
},
}
-impl SynthesizableSubcommand {
- /// Return the command name string for use in `VP_COMMAND` env var.
- pub(super) fn command_name(&self) -> &'static str {
- match self {
- Self::Lint { .. } => "lint",
- Self::Fmt { .. } => "fmt",
- Self::Build { .. } => "build",
- Self::Test { .. } => "test",
- Self::Pack { .. } => "pack",
- Self::Dev { .. } => "dev",
- Self::Preview { .. } => "preview",
- Self::Doc { .. } => "doc",
- Self::Check { .. } => "check",
- }
- }
-}
-
/// Top-level CLI argument parser for vite-plus.
#[derive(Debug, Parser)]
#[command(name = "vp", disable_help_subcommand = true)]
diff --git a/packages/cli/snap-tests/vite-plugins-exec-build/build.mjs b/packages/cli/snap-tests/vite-plugins-exec-build/build.mjs
new file mode 100644
index 0000000000..524f24aca0
--- /dev/null
+++ b/packages/cli/snap-tests/vite-plugins-exec-build/build.mjs
@@ -0,0 +1,8 @@
+// Run a programmatic Vite build, mirroring a downstream framework CLI
+// (e.g. `node framework-cli.js build`) that is spawned underneath `vp exec`.
+// `vp exec` does not load the config for metadata, so the config-metadata
+// marker is unset here; `lazyPlugins` must load the plugins for the build to
+// produce a usable index.html.
+import { build } from 'vite-plus';
+
+await build({ root: import.meta.dirname, logLevel: 'silent' });
diff --git a/packages/cli/snap-tests/vite-plugins-exec-build/index.html b/packages/cli/snap-tests/vite-plugins-exec-build/index.html
new file mode 100644
index 0000000000..7febab8c7e
--- /dev/null
+++ b/packages/cli/snap-tests/vite-plugins-exec-build/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/packages/cli/snap-tests/vite-plugins-exec-build/my-plugin.ts b/packages/cli/snap-tests/vite-plugins-exec-build/my-plugin.ts
new file mode 100644
index 0000000000..562ff320de
--- /dev/null
+++ b/packages/cli/snap-tests/vite-plugins-exec-build/my-plugin.ts
@@ -0,0 +1,8 @@
+export default function myPlugin() {
+ return {
+ name: 'my-exec-build-plugin',
+ transformIndexHtml(html: string) {
+ return html.replace('
+
+
+