FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(cli): aim the guard at Vite's selected root · voidzero-dev/vite-plus@8e0b89b · GitHub

Repository navigation

Commit 8e0b89b

Browse files
committed
fix(cli): aim the guard at Vite's selected root
vp build apps/web (or an explicit -c apps/web/vite.config.ts) rebases Vite's config lookup onto the selected root while the process cwd stays put, so the guard checked the wrong directory: a skewed app alias could pass and a skewed root alias could reject an aligned app. The vite resolver arms now derive the guard directory from the args with the same cac/mri walk app_target already uses for elicitation: the parent of an explicit -c/--config file wins, else the [root] positional, else the command cwd. The snapshot case gains a positional-root step that vendors a real-vite-shaped package in app/, so it only passes when the guard checks the selected root instead of the workspace cwd.
1 parent bc56e4d commit 8e0b89b

6 files changed

Lines changed: 146 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<!doctype html>
2+
<html>
3+
<body>
4+
<h1>positional root app</h1>
5+
</body>
6+
</html>

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_version_guard/app/node_modules/vite/package.json‎

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# The vendored node_modules/vite/package.json shadows the runner's run-root
22
# `vite` -> core link with a core at a version the CLI never shipped with,
33
# simulating a dependency-bot bump of the `vite` alias without the matching
4-
# vite-plus bump (issue #2356).
4+
# vite-plus bump (issue #2356). The app/ subdir vendors a real-vite-shaped
5+
# package, so the positional-root step passes only when the guard checks the
6+
# selected root instead of the workspace cwd.
57
[[case]]
68
name = "core_version_guard"
79
vp = "local"
810
steps = [
911
{ argv = ["vp", "build"], continue-on-failure = true },
1012
{ argv = ["vp", "test"], continue-on-failure = true },
13+
{ argv = ["vp", "build", "app"], comment = "the guard checks the positional root, where vite is real Vite" },
1114
{ argv = ["vp", "build"], comment = "VP_SKIP_CORE_VERSION_CHECK=1 skips the guard", envs = [["VP_SKIP_CORE_VERSION_CHECK", "1"]] },
1215
]

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_version_guard/snapshots/core_version_guard.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ error: Failed to resolve vite command: GenericFailure, Error: The project's `vit
1616
error: Failed to resolve test command: GenericFailure, Error: The project's `vite` alias resolves to @voidzero-dev/vite-plus-core@<version>, but this vite-plus CLI requires @voidzero-dev/vite-plus-core@<version>: the two packages are published in lockstep and other pairings are untested. A dependency bot usually causes this by updating vite-plus and the `vite` alias in separate PRs. Update the `vite` alias to npm:@voidzero-dev/vite-plus-core@<version> where it is declared (catalog, overrides, resolutions, or dependencies), or run `vp migrate` to realign it. Set VP_SKIP_CORE_VERSION_CHECK=1 to skip this check.
1717
```
1818

19+
## `vp build app`
20+
21+
the guard checks the positional root, where vite is real Vite
22+
23+
```
24+
note: `vp build app` sets Vite's root without changing the working directory. To run as if started there, use `vp -C app build`.
25+
✓ 2 modules transformed.
26+
computing gzip size...
27+
app/dist/index.html <size> kB │ gzip: <size> kB
28+
29+
✓ built in <duration>
30+
```
31+
1932
## `VP_SKIP_CORE_VERSION_CHECK=1 vp build`
2033

2134
VP_SKIP_CORE_VERSION_CHECK=1 skips the guard

‎packages/cli/binding/src/cli/app_target.rs‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,62 @@ fn classify_args<'a>(command: &str, args: &'a [String]) -> ArgTarget<'a> {
153153
ArgTarget::Bare
154154
}
155155

156+
/// The directory Vite loads the app config from, when the args select one:
157+
/// the parent of an explicit `-c`/`--config` file (which wins over a
158+
/// positional), else the `[root]` positional. `None` means the command's cwd.
159+
/// Walks the args like [`classify_args`] (cac/mri value consumption), but
160+
/// scans them all instead of returning at the first target, because Vite
161+
/// accepts `--config` and `[root]` in either order.
162+
///
163+
/// Used to aim the core-version guard at the copy of `vite` the app's config
164+
/// and plugins will import: Vite keeps the process cwd unchanged and rebases
165+
/// config lookup onto the selected root.
166+
pub(super) fn vite_config_dir(args: &[String], cwd: &AbsolutePath) -> Option<AbsolutePathBuf> {
167+
let mut positional: Option<&str> = None;
168+
let mut config: Option<&str> = None;
169+
let mut iter = args.iter().peekable();
170+
while let Some(arg) = iter.next() {
171+
if !arg.starts_with('-') {
172+
positional.get_or_insert(arg);
173+
continue;
174+
}
175+
// `--` terminates options: the first following token is the
176+
// positional, and nothing after it can be a flag.
177+
if arg == "--" {
178+
if positional.is_none() {
179+
positional = iter.next().map(String::as_str);
180+
}
181+
break;
182+
}
183+
if arg == "-c" || arg == "--config" {
184+
if let Some(next) = iter.peek() {
185+
if !next.starts_with('-') {
186+
config = iter.next().map(String::as_str);
187+
}
188+
}
189+
continue;
190+
}
191+
if let Some(value) = arg.strip_prefix("-c=").or_else(|| arg.strip_prefix("--config=")) {
192+
config = Some(value);
193+
continue;
194+
}
195+
let is_boolean = VITE_BOOLEAN_FLAGS.contains(&arg.as_str()) || arg.starts_with("--no-");
196+
if !is_boolean
197+
&& !arg.contains('=')
198+
&& iter.peek().is_some_and(|next| !next.starts_with('-'))
199+
{
200+
iter.next();
201+
}
202+
}
203+
if let Some(config) = config {
204+
// The config's parent dir, resolved like Vite resolves `--config`
205+
// (relative to cwd). An empty parent means the config sits in cwd.
206+
let parent = std::path::Path::new(config).parent().unwrap_or(std::path::Path::new(""));
207+
return Some(cwd.join(parent).clean());
208+
}
209+
positional.map(|root| cwd.join(root).clean())
210+
}
211+
156212
/// Heuristic ranking signal: does a directory look runnable for `command`?
157213
/// Used for ordering and single-candidate auto-selection, never for hiding.
158214
/// The rules are documented in rfcs/cwd-flag.md ("The likely-runnable
@@ -513,6 +569,44 @@ pub(super) fn resolve_app_target(
513569
mod tests {
514570
use super::*;
515571

572+
#[test]
573+
fn vite_config_dir_selects_the_app_dir() {
574+
let to_args = |args: &[&str]| args.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();
575+
let root = if cfg!(windows) { "C:\\ws" } else { "/ws" };
576+
let cwd = AbsolutePath::new(root).unwrap();
577+
let dir = |rel: &str| Some(cwd.join(rel).clean());
578+
579+
// No target selection: fall back to the command's cwd.
580+
assert_eq!(vite_config_dir(&to_args(&[]), cwd), None);
581+
assert_eq!(vite_config_dir(&to_args(&["--mode", "production"]), cwd), None);
582+
583+
// The `[root]` positional, wherever cac would see one.
584+
assert_eq!(vite_config_dir(&to_args(&["apps/web"]), cwd), dir("apps/web"));
585+
assert_eq!(
586+
vite_config_dir(&to_args(&["--mode", "production", "apps/web"]), cwd),
587+
dir("apps/web")
588+
);
589+
assert_eq!(vite_config_dir(&to_args(&["-w", "apps/web"]), cwd), dir("apps/web"));
590+
assert_eq!(vite_config_dir(&to_args(&["--", "apps/web"]), cwd), dir("apps/web"));
591+
592+
// An explicit config wins over the positional in either order; its
593+
// parent is where the config's imports resolve from.
594+
assert_eq!(
595+
vite_config_dir(&to_args(&["-c", "apps/web/vite.config.ts"]), cwd),
596+
dir("apps/web")
597+
);
598+
assert_eq!(
599+
vite_config_dir(&to_args(&["apps/web", "--config=conf/vite.config.ts"]), cwd),
600+
dir("conf")
601+
);
602+
assert_eq!(
603+
vite_config_dir(&to_args(&["-c", "conf/vite.config.ts", "apps/web"]), cwd),
604+
dir("conf")
605+
);
606+
// A config in the cwd itself keeps the cwd as the guard dir.
607+
assert_eq!(vite_config_dir(&to_args(&["-c", "vite.config.ts"]), cwd), dir(""));
608+
}
609+
516610
#[test]
517611
fn bare_means_no_positional_target_and_no_help() {
518612
let to_args = |args: &[&str]| args.iter().map(|s| (*s).to_string()).collect::<Vec<_>>();

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ use super::{
1212
types::{CliOptions, ResolvedSubcommand, ResolvedUniversalViteConfig, SynthesizableSubcommand},
1313
};
1414

15+
/// The directory string handed to the JS `vite` resolver: the Vite app dir
16+
/// selected by a `[root]` positional or an explicit `-c`/`--config` file
17+
/// (where the app's config and plugins resolve `vite` from), falling back to
18+
/// the command's cwd.
19+
fn vite_resolver_dir(
20+
args: &[String],
21+
cwd: &AbsolutePath,
22+
cwd_string: &str,
23+
) -> anyhow::Result<String> {
24+
match super::app_target::vite_config_dir(args, cwd) {
25+
Some(dir) => Ok(dir
26+
.as_path()
27+
.to_str()
28+
.ok_or_else(|| anyhow::anyhow!("vite root is not valid UTF-8"))?
29+
.to_string()),
30+
None => Ok(cwd_string.to_string()),
31+
}
32+
}
33+
1534
/// Resolves synthesizable subcommands to concrete programs and arguments.
1635
/// Used by both direct CLI execution and CommandHandler.
1736
pub struct SubcommandResolver {
@@ -162,7 +181,8 @@ impl SubcommandResolver {
162181
}
163182
SynthesizableSubcommand::Build { args } => {
164183
let cli_options = self.cli_options()?;
165-
let resolved = (cli_options.vite)(cwd_string.clone()).await?;
184+
let resolved =
185+
(cli_options.vite)(vite_resolver_dir(&args, cwd, &cwd_string)?).await?;
166186
let js_path = resolved.bin_path;
167187
let js_path_str = js_path
168188
.to_str()
@@ -245,7 +265,8 @@ impl SubcommandResolver {
245265
}
246266
SynthesizableSubcommand::Dev { args } => {
247267
let cli_options = self.cli_options()?;
248-
let resolved = (cli_options.vite)(cwd_string.clone()).await?;
268+
let resolved =
269+
(cli_options.vite)(vite_resolver_dir(&args, cwd, &cwd_string)?).await?;
249270
let js_path = resolved.bin_path;
250271
let js_path_str = js_path
251272
.to_str()
@@ -263,7 +284,8 @@ impl SubcommandResolver {
263284
}
264285
SynthesizableSubcommand::Preview { args } => {
265286
let cli_options = self.cli_options()?;
266-
let resolved = (cli_options.vite)(cwd_string.clone()).await?;
287+
let resolved =
288+
(cli_options.vite)(vite_resolver_dir(&args, cwd, &cwd_string)?).await?;
267289
let js_path = resolved.bin_path;
268290
let js_path_str = js_path
269291
.to_str()

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL