Skip to content

Commit a09b307

Browse files
committed
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.
1 parent 3dae3a3 commit a09b307

33 files changed

Lines changed: 249 additions & 90 deletions

docs/guide/troubleshooting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default defineConfig({
6969

7070
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.
7171

72-
Use `lazyPlugins` to load plugins only when the Vite pipeline actually runs (`dev`, `build`, `test`, `preview`):
72+
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`):
7373

7474
```ts [vite.config.ts]
7575
import { defineConfig, lazyPlugins } from 'vite-plus';

packages/cli/bin/oxfmt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ const oxfmtBin = join(dirname(dirname(oxfmtMainPath)), 'bin', 'oxfmt');
2323

2424
// This allows oxfmt to load vite.config.ts.
2525
// For `vp check` and `vp fmt`, VP_VERSION is injected by
26-
// `merge_resolved_envs_with_version()` in `cli.rs`, and VP_COMMAND is injected
27-
// by `SubcommandResolver::resolve()`.
26+
// `merge_resolved_envs_with_version()` in `cli.rs`.
2827
process.env.VP_VERSION = pkg.version;
29-
process.env.VP_COMMAND ??= 'fmt';
28+
// oxfmt reads vite.config.ts only for the `fmt` block, so skip the user's
29+
// Vite plugin factory (lazyPlugins) while the config evaluates.
30+
// Literal kept in sync with CONFIG_METADATA_ENV in src/utils/constants.ts
31+
// (this plain-JS bin can't import the bundled constant).
32+
process.env.VP_RESOLVING_CONFIG_METADATA ??= '1';
3033
await import(pathToFileURL(oxfmtBin).href);

packages/cli/bin/oxlint

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ const tsgolintBin = resolveTsgolintExecutable(
2727

2828
// This allows oxlint to load vite.config.ts.
2929
// For `vp check` and `vp lint`, VP_VERSION is injected by
30-
// `merge_resolved_envs_with_version()` in `cli.rs`, and VP_COMMAND is injected
31-
// by `SubcommandResolver::resolve()`.
30+
// `merge_resolved_envs_with_version()` in `cli.rs`.
3231
process.env.VP_VERSION = pkg.version;
33-
process.env.VP_COMMAND ??= 'lint';
32+
// oxlint reads vite.config.ts only for the `lint` block, so skip the user's
33+
// Vite plugin factory (lazyPlugins) while the config evaluates.
34+
// Literal kept in sync with CONFIG_METADATA_ENV in src/utils/constants.ts
35+
// (this plain-JS bin can't import the bundled constant).
36+
process.env.VP_RESOLVING_CONFIG_METADATA ??= '1';
3437
process.env.OXLINT_TSGOLINT_PATH ??= tsgolintBin;
3538
await import(pathToFileURL(oxlintBin).href);

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,7 @@ impl SubcommandResolver {
6868
resolved_vite_config: Option<&ResolvedUniversalViteConfig>,
6969
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
7070
) -> anyhow::Result<ResolvedSubcommand> {
71-
let command_name = subcommand.command_name();
72-
let mut resolved = self.resolve_inner(subcommand, resolved_vite_config, envs).await?;
73-
// Inject VP_COMMAND so that defineConfig's plugin factory knows which command is running,
74-
// even when the subcommand is synthesized inside `vp run`.
75-
let envs = Arc::make_mut(&mut resolved.envs);
76-
envs.insert(Arc::from(OsStr::new("VP_COMMAND")), Arc::from(OsStr::new(command_name)));
77-
Ok(resolved)
71+
self.resolve_inner(subcommand, resolved_vite_config, envs).await
7872
}
7973

8074
async fn resolve_inner(

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,23 +96,6 @@ pub enum SynthesizableSubcommand {
9696
},
9797
}
9898

99-
impl SynthesizableSubcommand {
100-
/// Return the command name string for use in `VP_COMMAND` env var.
101-
pub(super) fn command_name(&self) -> &'static str {
102-
match self {
103-
Self::Lint { .. } => "lint",
104-
Self::Fmt { .. } => "fmt",
105-
Self::Build { .. } => "build",
106-
Self::Test { .. } => "test",
107-
Self::Pack { .. } => "pack",
108-
Self::Dev { .. } => "dev",
109-
Self::Preview { .. } => "preview",
110-
Self::Doc { .. } => "doc",
111-
Self::Check { .. } => "check",
112-
}
113-
}
114-
}
115-
11699
/// Top-level CLI argument parser for vite-plus.
117100
#[derive(Debug, Parser)]
118101
#[command(name = "vp", disable_help_subcommand = true)]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Run a programmatic Vite build, mirroring a downstream framework CLI
2+
// (e.g. `node framework-cli.js build`) that is spawned underneath `vp exec`.
3+
// The child inherits VP_COMMAND=exec, so `lazyPlugins` must still load the
4+
// plugins for the build to produce a usable index.html.
5+
import { build } from 'vite-plus';
6+
7+
await build({ root: import.meta.dirname, logLevel: 'silent' });
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 myPlugin() {
2+
return {
3+
name: 'my-exec-build-plugin',
4+
transformIndexHtml(html: string) {
5+
return html.replace('</body>', '<!-- exec-build-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": "vite-plugins-exec-build-test",
3+
"private": true
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
> vp exec node build.mjs
2+
> cat dist/index.html | grep 'exec-build-plugin-injected' # vp exec should still load vite plugins from factory
3+
<!-- exec-build-plugin-injected --></body>

0 commit comments

Comments
 (0)