Skip to content

Commit 181e81d

Browse files
authored
fix(cli): skip lazyPlugins only while resolving config metadata (#1939)
## Problem `lazyPlugins` decided whether to run the user's plugin factory from `VP_COMMAND`. That signal is: - **overloaded** — `vp run`'s task-discovery config read and a verbatim build child both see `run`; - **unnormalized** — the `vp format` alias is `format`, not `fmt`; - **inherited** by spawned children. So 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. (Both were raised in review.) ## Fix Decide from the resolution intent instead of the command name. 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, regardless of which command launched them. The marker is scoped to the resolution, so it never leaks to task children, and it's immune to aliases and not-yet-known commands. `VP_COMMAND` had no other reader, so it's removed. Adds snap tests for the two fixed cases (verbatim `vp run` build loads plugins; `vp format` skips them) plus updated unit tests and docs.
1 parent 1bbf3f1 commit 181e81d

33 files changed

Lines changed: 288 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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
// `vp exec` does not load the config for metadata, so the config-metadata
4+
// marker is unset here; `lazyPlugins` must load the plugins for the build to
5+
// produce a usable index.html.
6+
import { build } from 'vite-plus';
7+
8+
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)