From 9da276bdd037f32b82d80740d772848b78ee4a6e Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 14 Aug 2026 22:18:20 +0800 Subject: [PATCH 1/7] feat(cli): refuse vp dev/build in Nuxt and Astro projects vp dev and vp build run the bundled Vite CLI, which cannot serve or build a Nuxt or Astro project: dev answers every URL with 404 and build fails on the missing index.html entry. Detect nuxt.config.* or astro.config.* next to the nearest package.json and stop with an error that points at vp run . An explicit --config/-c flag skips the refusal. Refs #1506 --- .../astro/astro.config.mjs | 1 + .../astro/package.json | 10 ++ .../builtin_framework_guard/nuxt.config.ts | 1 + .../builtin_framework_guard/package.json | 10 ++ .../builtin_framework_guard/snapshots.toml | 17 ++ .../builtin_framework_guard.global.md | 78 ++++++++++ .../builtin_framework_guard.local.md | 66 ++++++++ .../builtin_framework_guard/src/valid.js | 1 + .../builtin_framework_guard/vite.config.ts | 1 + docs/guide/run.md | 2 +- docs/guide/troubleshooting.md | 2 + .../cli/binding/src/cli/framework_guard.rs | 145 ++++++++++++++++++ packages/cli/binding/src/cli/mod.rs | 11 ++ 13 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/astro.config.mjs create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/src/valid.js create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/vite.config.ts create mode 100644 packages/cli/binding/src/cli/framework_guard.rs diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/astro.config.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/astro.config.mjs new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/astro.config.mjs @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/package.json new file mode 100644 index 0000000000..c35a149312 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/astro/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/builtin-framework-guard-astro", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vpt print astro build script", + "dev": "vpt print astro dev script" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json new file mode 100644 index 0000000000..d1c048bc2d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/builtin-framework-guard", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vpt print nuxt build script", + "dev": "vpt print nuxt dev script" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml new file mode 100644 index 0000000000..0d002d507f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -0,0 +1,17 @@ +# `vp dev` and `vp build` run the bundled Vite CLI, which cannot serve or +# build a project whose framework wraps Vite behind its own CLI. A Nuxt or +# Astro config file next to the nearest `package.json` turns both commands +# into an error that points at `vp run `. This project is Nuxt-shaped +# at the root and holds an Astro package in `astro/`. +[[case]] +name = "builtin_framework_guard" +vp = ["local", "global"] +skip-platforms = ["windows"] +steps = [ + { argv = ["vp", "dev"], comment = "`vp dev` refuses in a Nuxt project and points at the dev script", continue-on-failure = true }, + { argv = ["vp", "build"], comment = "`vp build` refuses the same way", continue-on-failure = true }, + { argv = ["vp", "dev"], cwd = "src", comment = "the refusal reaches the enclosing package from a subdirectory, like `vp run` does", continue-on-failure = true }, + { argv = ["vp", "dev"], cwd = "astro", comment = "an Astro config triggers the same refusal", continue-on-failure = true }, + { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (invalid port exits the server immediately)", continue-on-failure = true }, + { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script the refusal points at" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md new file mode 100644 index 0000000000..3e85d9bf93 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -0,0 +1,78 @@ +# builtin_framework_guard + +## `vp dev` + +`vp dev` refuses in a Nuxt project and points at the dev script + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run dev`? +``` + +## `vp build` + +`vp build` refuses the same way + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this is a Nuxt project (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run build`? +``` + +## `cd src && vp dev` + +the refusal reaches the enclosing package from a subdirectory, like `vp run` does + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run dev`? +``` + +## `cd astro && vp dev` + +an Astro config triggers the same refusal + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this is an Astro project (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. +hint: did you mean `vp run dev`? +``` + +## `vp dev --config vite.config.ts --port 12312312312` + +an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (invalid port exits the server immediately) + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +note: You are running `vp dev` as a Vite+ built-in command. If you meant to run the dev npm script, use `vpr dev` instead. +error when starting dev server: +Error: No available ports found between 12312312312 and 65535 +``` + +## `vp run dev` + +`vp run dev` runs the dev script the refusal points at + +``` +VITE+ - The Unified Toolchain for the Web + +$ vpt print nuxt dev script ⊘ cache disabled +nuxt dev script +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md new file mode 100644 index 0000000000..df8c1b6631 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -0,0 +1,66 @@ +# builtin_framework_guard + +## `vp dev` + +`vp dev` refuses in a Nuxt project and points at the dev script + +**Exit code:** 1 + +``` +error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run dev`? +``` + +## `vp build` + +`vp build` refuses the same way + +**Exit code:** 1 + +``` +error: this is a Nuxt project (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run build`? +``` + +## `cd src && vp dev` + +the refusal reaches the enclosing package from a subdirectory, like `vp run` does + +**Exit code:** 1 + +``` +error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run dev`? +``` + +## `cd astro && vp dev` + +an Astro config triggers the same refusal + +**Exit code:** 1 + +``` +error: this is an Astro project (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. +hint: did you mean `vp run dev`? +``` + +## `vp dev --config vite.config.ts --port 12312312312` + +an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (invalid port exits the server immediately) + +**Exit code:** 1 + +``` +note: You are running `vp dev` as a Vite+ built-in command. If you meant to run the dev npm script, use `vpr dev` instead. +error when starting dev server: +Error: No available ports found between 12312312312 and 65535 +``` + +## `vp run dev` + +`vp run dev` runs the dev script the refusal points at + +``` +$ vpt print nuxt dev script ⊘ cache disabled +nuxt dev script +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/src/valid.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/src/valid.js new file mode 100644 index 0000000000..9e1015feaf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/src/valid.js @@ -0,0 +1 @@ +export const valid = true; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/vite.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/vite.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/docs/guide/run.md b/docs/guide/run.md index 5752752078..be3de66021 100644 --- a/docs/guide/run.md +++ b/docs/guide/run.md @@ -51,7 +51,7 @@ Select a task (↑/↓, Enter to run, Esc to clear): `build`, `preview`, `lint`, `fmt`, `check`, and `pack` work the same way. -If the project defines that script or task, run it with `vp run `. For example, with a `"dev": "astro dev"` script, `vp run dev` starts Astro, while `vp dev` ignores the script and starts Vite. +If the project defines that script or task, run it with `vp run `. For example, with a `"dev": "astro dev"` script, `vp run dev` starts Astro. In a Nuxt or Astro project, `vp dev` and `vp build` stop with an error that points at `vp run `: those frameworks run Vite through their own CLI, so the bundled Vite CLI cannot serve or build them. ## Caching diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index ce88a329c5..324dbc3f0c 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -45,6 +45,8 @@ For example: - `vp test` always runs the built-in Vitest command - `vp run dev`, `vp run build`, and `vp run test` run the matching `package.json` scripts instead +In a Nuxt or Astro project (a `nuxt.config.*` or `astro.config.*` file next to `package.json`), `vp dev` and `vp build` stop with an error that points at `vp run `. Those frameworks run Vite through their own CLI, so the bundled Vite CLI cannot serve or build them. + See [Built-in Commands vs Scripts](/guide/run#built-in-commands-vs-scripts) for when to prefer each path. ::: info diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs new file mode 100644 index 0000000000..429bed9fde --- /dev/null +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -0,0 +1,145 @@ +//! Refusal for framework projects whose own CLI wraps Vite. +//! +//! `vp dev` and `vp build` run the bundled Vite CLI. Nuxt and Astro use Vite +//! under the hood but only through their own CLIs, so the bundled Vite CLI +//! cannot serve or build them: dev answers every URL with 404 and build stops +//! on the missing `index.html` entry. A framework config file next to the +//! nearest `package.json` turns both commands into an error that points at +//! `vp run ` (voidzero-dev/vite-plus#1506). An explicit +//! `--config`/`-c` flag skips the refusal: it selects a Vite config on +//! purpose, so the bundled Vite CLI stays reachable. + +use owo_colors::OwoColorize; +use vp_shared::output; +use vt::ExitStatus; +use vt_path::AbsolutePath; + +use super::types::SynthesizableSubcommand; + +/// Frameworks with a Vite-wrapping CLI, marked by the config file next to +/// `package.json` that their own CLI loads. +const FRAMEWORKS: &[Framework] = &[ + Framework { name: "Nuxt", article: "a", config_stem: "nuxt.config" }, + Framework { name: "Astro", article: "an", config_stem: "astro.config" }, +]; + +/// Config extensions both framework loaders accept. +const CONFIG_EXTENSIONS: &[&str] = &["ts", "js", "mjs", "cjs", "mts", "cts"]; + +struct Framework { + name: &'static str, + article: &'static str, + config_stem: &'static str, +} + +/// Refuse `vp dev` / `vp build` in a Nuxt or Astro project. +/// +/// Returns the exit status to stop with once the refusal printed, or `None` +/// when the command should proceed. +pub(super) fn check( + subcommand: &SynthesizableSubcommand, + cwd: &AbsolutePath, +) -> Option { + let (command, args) = match subcommand { + SynthesizableSubcommand::Dev { args } => ("dev", args), + SynthesizableSubcommand::Build { args } => ("build", args), + _ => return None, + }; + if has_explicit_config(args) { + return None; + } + // The nearest `package.json` is the package `vp run` resolves the task + // from, so the refusal and its hint stay consistent from a subdirectory. + let package = vt_workspace::find_package_root(cwd).ok()?; + let (framework, config_file) = detect(package.path)?; + + let built_in = format!("`vp {command}`").bright_blue().to_string(); + let via_run = format!("`vp run {command}`").bright_blue().to_string(); + output::error(&format!( + "this is {article} {name} project ({config_file}), but {built_in} runs the bundled \ + Vite CLI, not the {name} CLI.", + article = framework.article, + name = framework.name, + )); + output::raw_stderr(&format!("hint: did you mean {via_run}?")); + Some(ExitStatus(1)) +} + +/// The first framework config file present in `dir`. +fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, String)> { + for framework in FRAMEWORKS { + for extension in CONFIG_EXTENSIONS { + let config_file = format!("{}.{extension}", framework.config_stem); + if dir.join(config_file.as_str()).as_path().is_file() { + return Some((framework, config_file)); + } + } + } + None +} + +/// Whether the forwarded Vite args select a config file explicitly. The +/// capital `-C` retarget flag is a different flag and does not count. +fn has_explicit_config(args: &[String]) -> bool { + args.iter().any(|arg| { + arg == "-c" || arg == "--config" || arg.starts_with("--config=") || arg.starts_with("-c=") + }) +} + +#[cfg(test)] +mod tests { + use vt_path::AbsolutePathBuf; + + use super::{detect, has_explicit_config}; + + fn temp_dir(label: &str) -> AbsolutePathBuf { + let unique = format!( + "vp-framework-guard-{label}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("clock after epoch") + .as_nanos() + ); + let dir = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&dir).expect("create temp dir"); + AbsolutePathBuf::new(dir).expect("temp dir is absolute") + } + + #[test] + fn detects_nuxt_before_astro_across_extensions() { + let dir = temp_dir("detect"); + std::fs::write(dir.as_path().join("astro.config.mjs"), "export default {}") + .expect("write astro config"); + std::fs::write(dir.as_path().join("nuxt.config.mts"), "export default {}") + .expect("write nuxt config"); + + let (framework, config_file) = detect(&dir).expect("framework detected"); + assert_eq!(framework.name, "Nuxt"); + assert_eq!(config_file, "nuxt.config.mts"); + + std::fs::remove_dir_all(dir.as_path()).expect("remove temp dir"); + } + + #[test] + fn ignores_directories_and_unrelated_files() { + let dir = temp_dir("ignore"); + std::fs::create_dir_all(dir.as_path().join("nuxt.config.ts")).expect("create dir"); + std::fs::write(dir.as_path().join("vite.config.ts"), "export default {}") + .expect("write vite config"); + + assert!(detect(&dir).is_none()); + + std::fs::remove_dir_all(dir.as_path()).expect("remove temp dir"); + } + + #[test] + fn explicit_config_flags_skip_the_refusal() { + let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); + assert!(has_explicit_config(&owned(&["--config", "vite.config.ts"]))); + assert!(has_explicit_config(&owned(&["--config=vite.config.ts"]))); + assert!(has_explicit_config(&owned(&["-c", "vite.config.ts"]))); + assert!(!has_explicit_config(&owned(&["--port", "5000"]))); + assert!(!has_explicit_config(&owned(&["-C", "apps/web"]))); + } +} diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 1954d24daa..1ca583a27b 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -5,6 +5,7 @@ mod app_target; mod execution; +mod framework_guard; mod handler; mod help; mod resolver; @@ -58,6 +59,11 @@ async fn execute_direct_subcommand( app_target::AppTarget::Dir(dir) => dir, app_target::AppTarget::CurrentDir => cwd, }; + // Elicitation can land in a package the invocation-directory check never + // saw, such as a `defaultPackage` that holds a Nuxt or Astro app. + if retargeted && let Some(exit) = framework_guard::check(&subcommand, cwd) { + return Ok(exit); + } // The resolver hands back the workspace root it already found whenever the // command runs in the unchanged cwd (never after a -C/elicitation @@ -387,6 +393,11 @@ pub async fn main( match cli_args { CLIArgs::Synthesizable(subcmd) => { + // A Nuxt/Astro project cannot run through the bundled Vite CLI, + // so refuse before the script note recommends anything else. + if let Some(exit) = framework_guard::check(&subcmd, &cwd) { + return Ok(exit); + } // Only the built-ins can be mistaken for a script. `run`/`cache` // below are the script path itself; `install` and friends // legitimately trigger a project's `install` lifecycle scripts From 47a282149ec6decc9ce44cdd49cf8d77789cb90e Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 14 Aug 2026 22:34:29 +0800 Subject: [PATCH 2/7] fix(cli): align framework config detection with upstream loaders Astro only searches astro.config.{mjs,js,ts,mts} (configPaths in withastro/astro), so drop the .cjs/.cts names it never loads. Nuxt resolves nuxt.config through c12, which accepts all six script extensions. Cite the pinned upstream sources in the framework table. Also reword the refusal to 'this project uses ' and drop the article field it needed. --- .../builtin_framework_guard.global.md | 8 +-- .../builtin_framework_guard.local.md | 8 +-- .../cli/binding/src/cli/framework_guard.rs | 67 ++++++++++++++----- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md index 3e85d9bf93..5cf9b60931 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -9,7 +9,7 @@ ``` VITE+ - The Unified Toolchain for the Web -error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -22,7 +22,7 @@ hint: did you mean `vp run dev`? ``` VITE+ - The Unified Toolchain for the Web -error: this is a Nuxt project (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run build`? ``` @@ -35,7 +35,7 @@ the refusal reaches the enclosing package from a subdirectory, like `vp run` doe ``` VITE+ - The Unified Toolchain for the Web -error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -48,7 +48,7 @@ an Astro config triggers the same refusal ``` VITE+ - The Unified Toolchain for the Web -error: this is an Astro project (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. +error: this project uses Astro (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. hint: did you mean `vp run dev`? ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md index df8c1b6631..735bc6707a 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -7,7 +7,7 @@ **Exit code:** 1 ``` -error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -18,7 +18,7 @@ hint: did you mean `vp run dev`? **Exit code:** 1 ``` -error: this is a Nuxt project (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run build`? ``` @@ -29,7 +29,7 @@ the refusal reaches the enclosing package from a subdirectory, like `vp run` doe **Exit code:** 1 ``` -error: this is a Nuxt project (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -40,7 +40,7 @@ an Astro config triggers the same refusal **Exit code:** 1 ``` -error: this is an Astro project (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. +error: this project uses Astro (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. hint: did you mean `vp run dev`? ``` diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs index 429bed9fde..1e2f0c686c 100644 --- a/packages/cli/binding/src/cli/framework_guard.rs +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -17,19 +17,45 @@ use vt_path::AbsolutePath; use super::types::SynthesizableSubcommand; /// Frameworks with a Vite-wrapping CLI, marked by the config file next to -/// `package.json` that their own CLI loads. +/// `package.json` that their own CLI loads. Each list mirrors that loader's +/// own file names, in its resolution order. const FRAMEWORKS: &[Framework] = &[ - Framework { name: "Nuxt", article: "a", config_stem: "nuxt.config" }, - Framework { name: "Astro", article: "an", config_stem: "astro.config" }, + // Nuxt resolves `nuxt.config` through c12: `loadNuxtConfig` passes + // `configFile: "nuxt.config"` + // (https://github.com/nuxt/nuxt/blob/v4.5.2/packages/kit/src/loader/config.ts) + // and c12 tries the script extensions of `SUPPORTED_EXTENSIONS` + // (https://github.com/unjs/c12/blob/v3.3.4/src/loader.ts). c12 also + // accepts data configs (`.json`, `.jsonc`, `.json5`, `.yaml`, `.yml`, + // `.toml`) and rc files; those are rare enough for the guard to leave + // alone. + Framework { + name: "Nuxt", + config_files: &[ + "nuxt.config.js", + "nuxt.config.ts", + "nuxt.config.mjs", + "nuxt.config.cjs", + "nuxt.config.mts", + "nuxt.config.cts", + ], + }, + // Astro searches exactly these four names: `configPaths` in + // https://github.com/withastro/astro/blob/astro@7.2.2/packages/astro/src/core/config/config.ts. + // Astro loads no `.cjs`/`.cts` config. + Framework { + name: "Astro", + config_files: &[ + "astro.config.mjs", + "astro.config.js", + "astro.config.ts", + "astro.config.mts", + ], + }, ]; -/// Config extensions both framework loaders accept. -const CONFIG_EXTENSIONS: &[&str] = &["ts", "js", "mjs", "cjs", "mts", "cts"]; - struct Framework { name: &'static str, - article: &'static str, - config_stem: &'static str, + config_files: &'static [&'static str], } /// Refuse `vp dev` / `vp build` in a Nuxt or Astro project. @@ -56,9 +82,8 @@ pub(super) fn check( let built_in = format!("`vp {command}`").bright_blue().to_string(); let via_run = format!("`vp run {command}`").bright_blue().to_string(); output::error(&format!( - "this is {article} {name} project ({config_file}), but {built_in} runs the bundled \ - Vite CLI, not the {name} CLI.", - article = framework.article, + "this project uses {name} ({config_file}), but {built_in} runs the bundled Vite CLI, \ + not the {name} CLI.", name = framework.name, )); output::raw_stderr(&format!("hint: did you mean {via_run}?")); @@ -66,11 +91,10 @@ pub(super) fn check( } /// The first framework config file present in `dir`. -fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, String)> { +fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, &'static str)> { for framework in FRAMEWORKS { - for extension in CONFIG_EXTENSIONS { - let config_file = format!("{}.{extension}", framework.config_stem); - if dir.join(config_file.as_str()).as_path().is_file() { + for &config_file in framework.config_files { + if dir.join(config_file).as_path().is_file() { return Some((framework, config_file)); } } @@ -133,6 +157,19 @@ mod tests { std::fs::remove_dir_all(dir.as_path()).expect("remove temp dir"); } + #[test] + fn ignores_config_names_astro_does_not_load() { + let dir = temp_dir("astro-cjs"); + std::fs::write(dir.as_path().join("astro.config.cjs"), "module.exports = {}") + .expect("write cjs config"); + std::fs::write(dir.as_path().join("astro.config.cts"), "module.exports = {}") + .expect("write cts config"); + + assert!(detect(&dir).is_none()); + + std::fs::remove_dir_all(dir.as_path()).expect("remove temp dir"); + } + #[test] fn explicit_config_flags_skip_the_refusal() { let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); From 63e3cb9032ff82850ebaaff3e386292dc01ee29e Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 14 Aug 2026 22:49:55 +0800 Subject: [PATCH 3/7] refactor(cli): simplify refusal wording to short sentences Split the refusal error into two sentences and rewrite the guard comments, docs, and fixture comments in short active sentences. Re-record the fixture snapshots for the new wording. --- .../builtin_framework_guard/snapshots.toml | 16 +++--- .../builtin_framework_guard.global.md | 14 +++--- .../builtin_framework_guard.local.md | 14 +++--- docs/guide/run.md | 2 +- docs/guide/troubleshooting.md | 2 +- .../cli/binding/src/cli/framework_guard.rs | 49 ++++++++++--------- packages/cli/binding/src/cli/mod.rs | 9 ++-- 7 files changed, 54 insertions(+), 52 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml index 0d002d507f..a6f0c4c69a 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -1,8 +1,8 @@ -# `vp dev` and `vp build` run the bundled Vite CLI, which cannot serve or -# build a project whose framework wraps Vite behind its own CLI. A Nuxt or -# Astro config file next to the nearest `package.json` turns both commands -# into an error that points at `vp run `. This project is Nuxt-shaped -# at the root and holds an Astro package in `astro/`. +# `vp dev` and `vp build` run the bundled Vite CLI. That CLI cannot serve or +# build a project whose framework wraps Vite behind its own CLI. When a Nuxt +# or Astro config file is next to the nearest `package.json`, the two +# commands stop with an error that points at `vp run `. The root of +# this project looks like a Nuxt app, and `astro/` holds an Astro package. [[case]] name = "builtin_framework_guard" vp = ["local", "global"] @@ -10,8 +10,8 @@ skip-platforms = ["windows"] steps = [ { argv = ["vp", "dev"], comment = "`vp dev` refuses in a Nuxt project and points at the dev script", continue-on-failure = true }, { argv = ["vp", "build"], comment = "`vp build` refuses the same way", continue-on-failure = true }, - { argv = ["vp", "dev"], cwd = "src", comment = "the refusal reaches the enclosing package from a subdirectory, like `vp run` does", continue-on-failure = true }, + { argv = ["vp", "dev"], cwd = "src", comment = "the refusal finds the enclosing package from a subdirectory, with the same walk `vp run` uses", continue-on-failure = true }, { argv = ["vp", "dev"], cwd = "astro", comment = "an Astro config triggers the same refusal", continue-on-failure = true }, - { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (invalid port exits the server immediately)", continue-on-failure = true }, - { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script the refusal points at" }, + { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately)", continue-on-failure = true }, + { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script that the refusal points at" }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md index 5cf9b60931..3e009c6a0d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -9,7 +9,7 @@ ``` VITE+ - The Unified Toolchain for the Web -error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -22,20 +22,20 @@ hint: did you mean `vp run dev`? ``` VITE+ - The Unified Toolchain for the Web -error: this project uses Nuxt (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run build`? ``` ## `cd src && vp dev` -the refusal reaches the enclosing package from a subdirectory, like `vp run` does +the refusal finds the enclosing package from a subdirectory, with the same walk `vp run` uses **Exit code:** 1 ``` VITE+ - The Unified Toolchain for the Web -error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -48,13 +48,13 @@ an Astro config triggers the same refusal ``` VITE+ - The Unified Toolchain for the Web -error: this project uses Astro (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. +error: this project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vite CLI, not the Astro CLI. hint: did you mean `vp run dev`? ``` ## `vp dev --config vite.config.ts --port 12312312312` -an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (invalid port exits the server immediately) +an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately) **Exit code:** 1 @@ -68,7 +68,7 @@ Error: No available ports found between 12312312312 and 65535 ## `vp run dev` -`vp run dev` runs the dev script the refusal points at +`vp run dev` runs the dev script that the refusal points at ``` VITE+ - The Unified Toolchain for the Web diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md index 735bc6707a..603a430276 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -7,7 +7,7 @@ **Exit code:** 1 ``` -error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -18,18 +18,18 @@ hint: did you mean `vp run dev`? **Exit code:** 1 ``` -error: this project uses Nuxt (nuxt.config.ts), but `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run build`? ``` ## `cd src && vp dev` -the refusal reaches the enclosing package from a subdirectory, like `vp run` does +the refusal finds the enclosing package from a subdirectory, with the same walk `vp run` uses **Exit code:** 1 ``` -error: this project uses Nuxt (nuxt.config.ts), but `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. hint: did you mean `vp run dev`? ``` @@ -40,13 +40,13 @@ an Astro config triggers the same refusal **Exit code:** 1 ``` -error: this project uses Astro (astro.config.mjs), but `vp dev` runs the bundled Vite CLI, not the Astro CLI. +error: this project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vite CLI, not the Astro CLI. hint: did you mean `vp run dev`? ``` ## `vp dev --config vite.config.ts --port 12312312312` -an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (invalid port exits the server immediately) +an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately) **Exit code:** 1 @@ -58,7 +58,7 @@ Error: No available ports found between 12312312312 and 65535 ## `vp run dev` -`vp run dev` runs the dev script the refusal points at +`vp run dev` runs the dev script that the refusal points at ``` $ vpt print nuxt dev script ⊘ cache disabled diff --git a/docs/guide/run.md b/docs/guide/run.md index be3de66021..5f36e00a94 100644 --- a/docs/guide/run.md +++ b/docs/guide/run.md @@ -51,7 +51,7 @@ Select a task (↑/↓, Enter to run, Esc to clear): `build`, `preview`, `lint`, `fmt`, `check`, and `pack` work the same way. -If the project defines that script or task, run it with `vp run `. For example, with a `"dev": "astro dev"` script, `vp run dev` starts Astro. In a Nuxt or Astro project, `vp dev` and `vp build` stop with an error that points at `vp run `: those frameworks run Vite through their own CLI, so the bundled Vite CLI cannot serve or build them. +If the project defines that script or task, run it with `vp run `. For example, with a `"dev": "astro dev"` script, `vp run dev` starts Astro. In a Nuxt or Astro project, `vp dev` and `vp build` stop with an error that points at `vp run `. Those frameworks run Vite through their own CLI. The bundled Vite CLI cannot serve or build them. ## Caching diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index 324dbc3f0c..bf0be7b9c2 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -45,7 +45,7 @@ For example: - `vp test` always runs the built-in Vitest command - `vp run dev`, `vp run build`, and `vp run test` run the matching `package.json` scripts instead -In a Nuxt or Astro project (a `nuxt.config.*` or `astro.config.*` file next to `package.json`), `vp dev` and `vp build` stop with an error that points at `vp run `. Those frameworks run Vite through their own CLI, so the bundled Vite CLI cannot serve or build them. +In a Nuxt or Astro project (a `nuxt.config.*` or `astro.config.*` file next to `package.json`), `vp dev` and `vp build` stop with an error that points at `vp run `. Those frameworks run Vite through their own CLI. The bundled Vite CLI cannot serve or build them. See [Built-in Commands vs Scripts](/guide/run#built-in-commands-vs-scripts) for when to prefer each path. diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs index 1e2f0c686c..3a8b09e384 100644 --- a/packages/cli/binding/src/cli/framework_guard.rs +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -1,13 +1,13 @@ //! Refusal for framework projects whose own CLI wraps Vite. //! -//! `vp dev` and `vp build` run the bundled Vite CLI. Nuxt and Astro use Vite -//! under the hood but only through their own CLIs, so the bundled Vite CLI -//! cannot serve or build them: dev answers every URL with 404 and build stops -//! on the missing `index.html` entry. A framework config file next to the -//! nearest `package.json` turns both commands into an error that points at -//! `vp run ` (voidzero-dev/vite-plus#1506). An explicit -//! `--config`/`-c` flag skips the refusal: it selects a Vite config on -//! purpose, so the bundled Vite CLI stays reachable. +//! `vp dev` and `vp build` run the bundled Vite CLI. Nuxt and Astro run Vite +//! only through their own CLIs. The bundled Vite CLI cannot serve or build +//! these projects: dev answers every URL with 404, and build stops on the +//! missing `index.html` entry. When a framework config file is next to the +//! nearest `package.json`, the two commands stop with an error that points +//! at `vp run ` (voidzero-dev/vite-plus#1506). An explicit +//! `--config`/`-c` flag selects a Vite config on purpose, so it skips the +//! refusal. use owo_colors::OwoColorize; use vp_shared::output; @@ -16,18 +16,18 @@ use vt_path::AbsolutePath; use super::types::SynthesizableSubcommand; -/// Frameworks with a Vite-wrapping CLI, marked by the config file next to -/// `package.json` that their own CLI loads. Each list mirrors that loader's -/// own file names, in its resolution order. +/// Frameworks that wrap Vite behind their own CLI. Each entry lists the +/// config files that the framework's loader resolves, in its resolution +/// order. const FRAMEWORKS: &[Framework] = &[ - // Nuxt resolves `nuxt.config` through c12: `loadNuxtConfig` passes + // Nuxt resolves `nuxt.config` through c12. `loadNuxtConfig` passes // `configFile: "nuxt.config"` - // (https://github.com/nuxt/nuxt/blob/v4.5.2/packages/kit/src/loader/config.ts) - // and c12 tries the script extensions of `SUPPORTED_EXTENSIONS` + // (https://github.com/nuxt/nuxt/blob/v4.5.2/packages/kit/src/loader/config.ts), + // and c12 tries the script extensions in `SUPPORTED_EXTENSIONS` // (https://github.com/unjs/c12/blob/v3.3.4/src/loader.ts). c12 also // accepts data configs (`.json`, `.jsonc`, `.json5`, `.yaml`, `.yml`, - // `.toml`) and rc files; those are rare enough for the guard to leave - // alone. + // `.toml`) and rc files. Those are rare, so the guard does not check + // them. Framework { name: "Nuxt", config_files: &[ @@ -39,9 +39,9 @@ const FRAMEWORKS: &[Framework] = &[ "nuxt.config.cts", ], }, - // Astro searches exactly these four names: `configPaths` in + // Astro searches only these four names: `configPaths` in // https://github.com/withastro/astro/blob/astro@7.2.2/packages/astro/src/core/config/config.ts. - // Astro loads no `.cjs`/`.cts` config. + // Astro does not load a `.cjs` or `.cts` config. Framework { name: "Astro", config_files: &[ @@ -60,8 +60,8 @@ struct Framework { /// Refuse `vp dev` / `vp build` in a Nuxt or Astro project. /// -/// Returns the exit status to stop with once the refusal printed, or `None` -/// when the command should proceed. +/// Returns the exit status after it prints the refusal. Returns `None` when +/// the command can proceed. pub(super) fn check( subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath, @@ -74,15 +74,15 @@ pub(super) fn check( if has_explicit_config(args) { return None; } - // The nearest `package.json` is the package `vp run` resolves the task - // from, so the refusal and its hint stay consistent from a subdirectory. + // `vp run` resolves the task from the nearest `package.json`. The same + // walk here keeps the hint correct from a subdirectory. let package = vt_workspace::find_package_root(cwd).ok()?; let (framework, config_file) = detect(package.path)?; let built_in = format!("`vp {command}`").bright_blue().to_string(); let via_run = format!("`vp run {command}`").bright_blue().to_string(); output::error(&format!( - "this project uses {name} ({config_file}), but {built_in} runs the bundled Vite CLI, \ + "this project uses {name} ({config_file}). {built_in} runs the bundled Vite CLI, \ not the {name} CLI.", name = framework.name, )); @@ -103,7 +103,8 @@ fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, &'static str)> { } /// Whether the forwarded Vite args select a config file explicitly. The -/// capital `-C` retarget flag is a different flag and does not count. +/// capital `-C` flag retargets the directory. It is a different flag and +/// does not count. fn has_explicit_config(args: &[String]) -> bool { args.iter().any(|arg| { arg == "-c" || arg == "--config" || arg.starts_with("--config=") || arg.starts_with("-c=") diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 1ca583a27b..57c28ff74f 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -59,8 +59,8 @@ async fn execute_direct_subcommand( app_target::AppTarget::Dir(dir) => dir, app_target::AppTarget::CurrentDir => cwd, }; - // Elicitation can land in a package the invocation-directory check never - // saw, such as a `defaultPackage` that holds a Nuxt or Astro app. + // Elicitation can select a package that the first check never saw, for + // example a `defaultPackage` that holds a Nuxt or Astro app. if retargeted && let Some(exit) = framework_guard::check(&subcommand, cwd) { return Ok(exit); } @@ -393,8 +393,9 @@ pub async fn main( match cli_args { CLIArgs::Synthesizable(subcmd) => { - // A Nuxt/Astro project cannot run through the bundled Vite CLI, - // so refuse before the script note recommends anything else. + // A Nuxt or Astro project cannot run through the bundled Vite + // CLI. Refuse before the script note recommends a different + // path. if let Some(exit) = framework_guard::check(&subcmd, &cwd) { return Ok(exit); } From 8abcf9a6b5d159852dcc348d3b7cea0efe79a948 Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 15 Aug 2026 00:20:52 +0800 Subject: [PATCH 4/7] feat(cli): adapt the refusal hint to the package scripts Without a script named after the command, the old hint pointed at a vp run task that does not exist. The hint now picks the first path that works: the same-name script, a script that runs the framework command under another name (matched on whitespace boundaries against the nuxt/nuxi/astro bins), or the framework CLI through vp exec. Add snapshot coverage for the no-scripts and renamed-script cases. --- .../no-scripts/nuxt.config.ts | 1 + .../no-scripts/package.json | 6 + .../renamed-script/nuxt.config.ts | 1 + .../renamed-script/package.json | 11 ++ .../builtin_framework_guard/snapshots.toml | 4 + .../builtin_framework_guard.global.md | 52 ++++++++ .../builtin_framework_guard.local.md | 44 +++++++ .../cli/binding/src/cli/framework_guard.rs | 116 ++++++++++++++++-- 8 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/package.json diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/package.json new file mode 100644 index 0000000000..6d0f11097a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/no-scripts/package.json @@ -0,0 +1,6 @@ +{ + "name": "@test/builtin-framework-guard-no-scripts", + "version": "1.0.0", + "private": true, + "type": "module" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/package.json new file mode 100644 index 0000000000..6239701e01 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/renamed-script/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/builtin-framework-guard-renamed-script", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "devtools": "nuxt devtools enable", + "make": "nuxt build", + "start": "nuxi dev --host" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml index a6f0c4c69a..28c63e6580 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -12,6 +12,10 @@ steps = [ { argv = ["vp", "build"], comment = "`vp build` refuses the same way", continue-on-failure = true }, { argv = ["vp", "dev"], cwd = "src", comment = "the refusal finds the enclosing package from a subdirectory, with the same walk `vp run` uses", continue-on-failure = true }, { argv = ["vp", "dev"], cwd = "astro", comment = "an Astro config triggers the same refusal", continue-on-failure = true }, + { argv = ["vp", "dev"], cwd = "no-scripts", comment = "without scripts, the hint points at the framework CLI through `vp exec`", continue-on-failure = true }, + { argv = ["vp", "build"], cwd = "no-scripts", comment = "`vp build` gets the same fallback hint", continue-on-failure = true }, + { argv = ["vp", "dev"], cwd = "renamed-script", comment = "a script that runs the framework dev command under another name becomes the hint target", continue-on-failure = true }, + { argv = ["vp", "build"], cwd = "renamed-script", comment = "the build hint finds the renamed build script the same way", continue-on-failure = true }, { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately)", continue-on-failure = true }, { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script that the refusal points at" }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md index 3e009c6a0d..b63e610639 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -52,6 +52,58 @@ error: this project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vit hint: did you mean `vp run dev`? ``` +## `cd no-scripts && vp dev` + +without scripts, the hint points at the framework CLI through `vp exec` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: run the Nuxt CLI with `vp exec nuxt dev`. +``` + +## `cd no-scripts && vp build` + +`vp build` gets the same fallback hint + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: run the Nuxt CLI with `vp exec nuxt build`. +``` + +## `cd renamed-script && vp dev` + +a script that runs the framework dev command under another name becomes the hint target + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run start`? The start script runs `nuxi dev`. +``` + +## `cd renamed-script && vp build` + +the build hint finds the renamed build script the same way + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run make`? The make script runs `nuxt build`. +``` + ## `vp dev --config vite.config.ts --port 12312312312` an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md index 603a430276..b6b1092c9c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -44,6 +44,50 @@ error: this project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vit hint: did you mean `vp run dev`? ``` +## `cd no-scripts && vp dev` + +without scripts, the hint points at the framework CLI through `vp exec` + +**Exit code:** 1 + +``` +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: run the Nuxt CLI with `vp exec nuxt dev`. +``` + +## `cd no-scripts && vp build` + +`vp build` gets the same fallback hint + +**Exit code:** 1 + +``` +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: run the Nuxt CLI with `vp exec nuxt build`. +``` + +## `cd renamed-script && vp dev` + +a script that runs the framework dev command under another name becomes the hint target + +**Exit code:** 1 + +``` +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run start`? The start script runs `nuxi dev`. +``` + +## `cd renamed-script && vp build` + +the build hint finds the renamed build script the same way + +**Exit code:** 1 + +``` +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run make`? The make script runs `nuxt build`. +``` + ## `vp dev --config vite.config.ts --port 12312312312` an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately) diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs index 3a8b09e384..9ecf6c709d 100644 --- a/packages/cli/binding/src/cli/framework_guard.rs +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -4,10 +4,11 @@ //! only through their own CLIs. The bundled Vite CLI cannot serve or build //! these projects: dev answers every URL with 404, and build stops on the //! missing `index.html` entry. When a framework config file is next to the -//! nearest `package.json`, the two commands stop with an error that points -//! at `vp run ` (voidzero-dev/vite-plus#1506). An explicit -//! `--config`/`-c` flag selects a Vite config on purpose, so it skips the -//! refusal. +//! nearest `package.json`, the two commands stop with an error and a hint +//! (voidzero-dev/vite-plus#1506). The hint points at the `package.json` +//! script that runs the framework command, or at the framework CLI through +//! `vp exec` when no script matches. An explicit `--config`/`-c` flag +//! selects a Vite config on purpose, so it skips the refusal. use owo_colors::OwoColorize; use vp_shared::output; @@ -38,6 +39,9 @@ const FRAMEWORKS: &[Framework] = &[ "nuxt.config.mts", "nuxt.config.cts", ], + // The nuxt package ships the `nuxt` and `nuxi` bins (its `bin` + // field, verified against nuxt 4.5.2). + bins: &["nuxt", "nuxi"], }, // Astro searches only these four names: `configPaths` in // https://github.com/withastro/astro/blob/astro@7.2.2/packages/astro/src/core/config/config.ts. @@ -50,12 +54,18 @@ const FRAMEWORKS: &[Framework] = &[ "astro.config.ts", "astro.config.mts", ], + // The astro package ships the `astro` bin (its `bin` field, + // verified against astro 7.2.2). + bins: &["astro"], }, ]; struct Framework { name: &'static str, config_files: &'static [&'static str], + /// Executable names of the framework CLI. The first one is the name the + /// `vp exec` hint shows. + bins: &'static [&'static str], } /// Refuse `vp dev` / `vp build` in a Nuxt or Astro project. @@ -80,16 +90,67 @@ pub(super) fn check( let (framework, config_file) = detect(package.path)?; let built_in = format!("`vp {command}`").bright_blue().to_string(); - let via_run = format!("`vp run {command}`").bright_blue().to_string(); output::error(&format!( "this project uses {name} ({config_file}). {built_in} runs the bundled Vite CLI, \ not the {name} CLI.", name = framework.name, )); - output::raw_stderr(&format!("hint: did you mean {via_run}?")); + let manifest = serde_json::from_slice::(package.package_json.content()) + .unwrap_or(serde_json::Value::Null); + output::raw_stderr(&format!("hint: {}", run_hint(&manifest, framework, command))); Some(ExitStatus(1)) } +/// The hint that follows the refusal. It points at the first path that works +/// in this package: +/// +/// 1. the `package.json` script with the command's name, +/// 2. a script that runs the framework command under another name, +/// 3. the framework CLI through `vp exec`. +/// +/// The check reads `package.json` scripts only. A `run.tasks` entry in +/// `vite.config.ts` with the command's name also works with `vp run`, but +/// the guard does not load that config. +fn run_hint(manifest: &serde_json::Value, framework: &Framework, command: &str) -> String { + if let Some(scripts) = manifest.get("scripts").and_then(serde_json::Value::as_object) { + if scripts.get(command).is_some_and(serde_json::Value::is_string) { + let via_run = format!("`vp run {command}`").bright_blue().to_string(); + return format!("did you mean {via_run}?"); + } + for (name, value) in scripts { + let Some(value) = value.as_str() else { continue }; + for &bin in framework.bins { + let framework_command = format!("{bin} {command}"); + if contains_word(value, &framework_command) { + let via_run = format!("`vp run {name}`").bright_blue().to_string(); + return format!( + "did you mean {via_run}? The {name} script runs `{framework_command}`." + ); + } + } + } + } + let via_exec = format!("`vp exec {} {command}`", framework.bins[0]).bright_blue().to_string(); + format!("run the {} CLI with {via_exec}.", framework.name) +} + +/// Whether `text` contains `pattern` between whitespace boundaries, so +/// `nuxt dev` does not match inside `nuxt devtools`. +fn contains_word(text: &str, pattern: &str) -> bool { + let mut search_from = 0; + while let Some(found) = text[search_from..].find(pattern) { + let start = search_from + found; + let end = start + pattern.len(); + let boundary_before = start == 0 || text[..start].ends_with(char::is_whitespace); + let boundary_after = end == text.len() || text[end..].starts_with(char::is_whitespace); + if boundary_before && boundary_after { + return true; + } + search_from = start + 1; + } + false +} + /// The first framework config file present in `dir`. fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, &'static str)> { for framework in FRAMEWORKS { @@ -115,7 +176,11 @@ fn has_explicit_config(args: &[String]) -> bool { mod tests { use vt_path::AbsolutePathBuf; - use super::{detect, has_explicit_config}; + use super::{FRAMEWORKS, contains_word, detect, has_explicit_config, run_hint}; + + fn framework(name: &str) -> &'static super::Framework { + FRAMEWORKS.iter().find(|framework| framework.name == name).expect("known framework") + } fn temp_dir(label: &str) -> AbsolutePathBuf { let unique = format!( @@ -171,6 +236,43 @@ mod tests { std::fs::remove_dir_all(dir.as_path()).expect("remove temp dir"); } + #[test] + fn hint_prefers_the_script_with_the_command_name() { + let manifest = serde_json::json!({ "scripts": { "dev": "nuxt dev" } }); + let hint = run_hint(&manifest, framework("Nuxt"), "dev"); + assert!(hint.contains("vp run dev"), "hint was: {hint}"); + } + + #[test] + fn hint_finds_a_renamed_script_that_runs_the_framework_command() { + let manifest = serde_json::json!({ "scripts": { + "devtools": "nuxt devtools enable", + "start": "NODE_OPTIONS=--inspect nuxi dev --host", + } }); + let hint = run_hint(&manifest, framework("Nuxt"), "dev"); + assert!(hint.contains("vp run start"), "hint was: {hint}"); + assert!(hint.contains("nuxi dev"), "hint was: {hint}"); + } + + #[test] + fn hint_falls_back_to_vp_exec_without_a_matching_script() { + let empty = serde_json::json!({}); + let hint = run_hint(&empty, framework("Nuxt"), "dev"); + assert!(hint.contains("vp exec nuxt dev"), "hint was: {hint}"); + + let unrelated = serde_json::json!({ "scripts": { "lint": "oxlint ." } }); + let hint = run_hint(&unrelated, framework("Astro"), "build"); + assert!(hint.contains("vp exec astro build"), "hint was: {hint}"); + } + + #[test] + fn contains_word_needs_whitespace_boundaries() { + assert!(contains_word("nuxt dev", "nuxt dev")); + assert!(contains_word("NODE_OPTIONS=--inspect nuxt dev --host", "nuxt dev")); + assert!(!contains_word("nuxt devtools enable", "nuxt dev")); + assert!(!contains_word("pnpm-nuxt dev", "nuxt dev")); + } + #[test] fn explicit_config_flags_skip_the_refusal() { let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); From 19eb0e535ce3b7b5890ce64c5ecdad49b70e6d8c Mon Sep 17 00:00:00 2001 From: MK Date: Sat, 15 Aug 2026 00:32:38 +0800 Subject: [PATCH 5/7] fix(cli): skip the framework guard for script-spawned commands A package script can run vp dev or vp build itself. The guard now checks direct invocations only, with the same task and npm_lifecycle_event gate the script note uses (extracted as spawned_from_script). Add task-spawned and npm-spawned snapshot cases that build through a nested vp build without a refusal. --- .../builtin_framework_guard/index.html | 1 + .../builtin_framework_guard/package.json | 3 ++- .../builtin_framework_guard/snapshots.toml | 19 +++++++++++++++++++ ...iltin_framework_guard_inside_npm_script.md | 18 ++++++++++++++++++ .../builtin_framework_guard_inside_task.md | 15 +++++++++++++++ .../cli/binding/src/cli/framework_guard.rs | 11 +++++++++-- packages/cli/binding/src/cli/script_note.rs | 14 +++++++++----- 7 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/index.html create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_npm_script.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_task.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/index.html b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/index.html new file mode 100644 index 0000000000..13ee08c5d6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/index.html @@ -0,0 +1 @@ + diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json index d1c048bc2d..783e243f04 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "build": "vpt print nuxt build script", - "dev": "vpt print nuxt dev script" + "dev": "vpt print nuxt dev script", + "nested-build": "vp build" } } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml index 28c63e6580..3f535aebbb 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -19,3 +19,22 @@ steps = [ { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately)", continue-on-failure = true }, { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script that the refusal points at" }, ] + +# A task or package script can spawn `vp dev` / `vp build` itself, for +# example a `"nested-build": "vp build"` script. The guard checks direct +# invocations only, so the nested command runs as invoked. +[[case]] +name = "builtin_framework_guard_inside_task" +vp = "local" +skip-platforms = ["windows"] +steps = [ + { argv = ["vp", "run", "nested-build"], comment = "no refusal: a task-spawned `vp build` runs the bundled Vite build as invoked" }, +] + +[[case]] +name = "builtin_framework_guard_inside_npm_script" +vp = "local" +skip-platforms = ["windows"] +steps = [ + { argv = ["npm", "run", "nested-build"], comment = "no refusal: an npm-spawned `vp build` runs as invoked" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_npm_script.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_npm_script.md new file mode 100644 index 0000000000..314c7efde8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_npm_script.md @@ -0,0 +1,18 @@ +# builtin_framework_guard_inside_npm_script + +## `npm run nested-build` + +no refusal: an npm-spawned `vp build` runs as invoked + +``` + +> @test/builtin-framework-guard@1.0.0 nested-build +> vp build + +✓ 4 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB +dist/assets/index-.js kB │ gzip: kB + +✓ built in +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_task.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_task.md new file mode 100644 index 0000000000..598d905f1d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard_inside_task.md @@ -0,0 +1,15 @@ +# builtin_framework_guard_inside_task + +## `vp run nested-build` + +no refusal: a task-spawned `vp build` runs the bundled Vite build as invoked + +``` +$ vp build ⊘ cache disabled +✓ 4 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB +dist/assets/index-.js kB │ gzip: kB + +✓ built in +``` diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs index 9ecf6c709d..8f273dc39b 100644 --- a/packages/cli/binding/src/cli/framework_guard.rs +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -7,8 +7,10 @@ //! nearest `package.json`, the two commands stop with an error and a hint //! (voidzero-dev/vite-plus#1506). The hint points at the `package.json` //! script that runs the framework command, or at the framework CLI through -//! `vp exec` when no script matches. An explicit `--config`/`-c` flag -//! selects a Vite config on purpose, so it skips the refusal. +//! `vp exec` when no script matches. The guard checks direct invocations +//! only: a command spawned from a task or package script, for example a +//! `"dev": "vp dev"` script, runs as invoked. An explicit `--config`/`-c` +//! flag selects a Vite config on purpose, so it skips the refusal. use owo_colors::OwoColorize; use vp_shared::output; @@ -81,6 +83,11 @@ pub(super) fn check( SynthesizableSubcommand::Build { args } => ("build", args), _ => return None, }; + // A task or package script can spawn `vp dev` itself, for example a + // `"dev": "vp dev"` script. The nested command runs as invoked. + if super::script_note::spawned_from_script() { + return None; + } if has_explicit_config(args) { return None; } diff --git a/packages/cli/binding/src/cli/script_note.rs b/packages/cli/binding/src/cli/script_note.rs index ec1d812302..a7fb9a5c1a 100644 --- a/packages/cli/binding/src/cli/script_note.rs +++ b/packages/cli/binding/src/cli/script_note.rs @@ -21,11 +21,8 @@ const NPM_LIFECYCLE_EVENT_ENV_NAME: &str = "npm_lifecycle_event"; pub(super) fn print(command: Option<&str>, cwd: &AbsolutePath) { let Some(command) = command else { return }; // A task spawned this command, so the user is already on a script-running - // path. npm-compatible runners set `npm_lifecycle_event`; Vite Task uses - // its own marker. - if std::env::var_os(MARKER_ENV_NAME).is_some() - || std::env::var_os(NPM_LIFECYCLE_EVENT_ENV_NAME).is_some_and(|event| !event.is_empty()) - { + // path. + if spawned_from_script() { return; } if !has_package_json_script(cwd, command) { @@ -40,6 +37,13 @@ pub(super) fn print(command: Option<&str>, cwd: &AbsolutePath) { )); } +/// Whether a task or a package script spawned this command. npm-compatible +/// runners set `npm_lifecycle_event`; Vite Task uses its own marker. +pub(super) fn spawned_from_script() -> bool { + std::env::var_os(MARKER_ENV_NAME).is_some() + || std::env::var_os(NPM_LIFECYCLE_EVENT_ENV_NAME).is_some_and(|event| !event.is_empty()) +} + /// Whether the package enclosing `cwd` defines a `` script. /// /// Walks up to the nearest `package.json`, which is the package `vp run` From dad5d88aec87f12a3b969c16d3ed0406a34c7eb4 Mon Sep 17 00:00:00 2001 From: MK Date: Sun, 16 Aug 2026 16:02:00 +0800 Subject: [PATCH 6/7] fix(cli): apply the framework guard to the resolved target Run the refusal after resolve_app_target, so defaultPackage keeps acting as an implicit -C and the hint comes from the target package. The script note asks the guard first (print-free applies) and stays silent right before a refusal. Let help and version requests reach the tool: vp dev --help prints the Vite help again. Tokens after the -- terminator stay positionals, for the config bypass too. Qualify the docs: the hint points at the matching script, or at vp exec when no script matches. --- .../builtin_framework_guard/snapshots.toml | 1 + .../builtin_framework_guard.global.md | 39 ++++++++ .../builtin_framework_guard.local.md | 39 ++++++++ .../apps/web/index.html | 1 + .../apps/web/package.json | 1 + .../apps/web/src/valid.js | 1 + .../nuxt.config.ts | 1 + .../package.json | 1 + .../snapshots.toml | 18 ++++ ..._framework_guard_default_package.global.md | 26 ++++++ ...n_framework_guard_default_package.local.md | 24 +++++ ...rk_guard_default_package_to_nuxt.global.md | 15 ++++ ...ork_guard_default_package_to_nuxt.local.md | 13 +++ .../to_nuxt/app/nuxt.config.ts | 1 + .../to_nuxt/app/package.json | 1 + .../to_nuxt/package.json | 1 + .../to_nuxt/vite.config.ts | 3 + .../vite.config.ts | 3 + docs/guide/run.md | 2 +- docs/guide/troubleshooting.md | 2 +- .../cli/binding/src/cli/framework_guard.rs | 90 ++++++++++++++----- packages/cli/binding/src/cli/mod.rs | 24 ++--- 22 files changed, 272 insertions(+), 35 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/index.html create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/src/valid.js create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/vite.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/vite.config.ts diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml index 3f535aebbb..bbd3747fa3 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -17,6 +17,7 @@ steps = [ { argv = ["vp", "dev"], cwd = "renamed-script", comment = "a script that runs the framework dev command under another name becomes the hint target", continue-on-failure = true }, { argv = ["vp", "build"], cwd = "renamed-script", comment = "the build hint finds the renamed build script the same way", continue-on-failure = true }, { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately)", continue-on-failure = true }, + { argv = ["vp", "dev", "--help"], comment = "a help request reaches the tool, so the guard steps aside" }, { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script that the refusal points at" }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md index b63e610639..d3945162dd 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -118,6 +118,45 @@ error when starting dev server: Error: No available ports found between 12312312312 and 65535 ``` +## `vp dev --help` + +a help request reaches the tool, so the guard steps aside + +``` +VITE+ - The Unified Toolchain for the Web + +Usage: vp dev [ROOT] [OPTIONS] + +Run the development server. +Options are forwarded to Vite. + +Arguments: + [ROOT] Project root directory (default: current directory) + +Options: + --host [host] [string] specify hostname + --port [number] specify port + --open [path] [boolean | string] open browser on startup + --cors [boolean] enable CORS + --strictPort [boolean] exit if specified port is already in use + --force [boolean] force the optimizer to ignore the cache and re-bundle + --experimentalBundle [boolean] use experimental full bundle mode (this is highly experimental) + --base [string] public base path (default: /) + -l, --logLevel [string] info | warn | error | silent + --clearScreen [boolean] allow/disable clear screen when logging + -d, --debug [feat] [string | boolean] show debug logs + -f, --filter [string] filter debug logs + -m, --mode [string] set env mode + -h, --help Display this message + +Examples: + vp dev + vp dev --open + vp dev --host localhost --port 5173 + +Documentation: https://viteplus.dev/guide/dev +``` + ## `vp run dev` `vp run dev` runs the dev script that the refusal points at diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md index b6b1092c9c..e157baa3d0 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -100,6 +100,45 @@ error when starting dev server: Error: No available ports found between 12312312312 and 65535 ``` +## `vp dev --help` + +a help request reaches the tool, so the guard steps aside + +``` +VITE+ - The Unified Toolchain for the Web + +Usage: vp dev [ROOT] [OPTIONS] + +Run the development server. +Options are forwarded to Vite. + +Arguments: + [ROOT] Project root directory (default: current directory) + +Options: + --host [host] [string] specify hostname + --port [number] specify port + --open [path] [boolean | string] open browser on startup + --cors [boolean] enable CORS + --strictPort [boolean] exit if specified port is already in use + --force [boolean] force the optimizer to ignore the cache and re-bundle + --experimentalBundle [boolean] use experimental full bundle mode (this is highly experimental) + --base [string] public base path (default: /) + -l, --logLevel [string] info | warn | error | silent + --clearScreen [boolean] allow/disable clear screen when logging + -d, --debug [feat] [string | boolean] show debug logs + -f, --filter [string] filter debug logs + -m, --mode [string] set env mode + -h, --help Display this message + +Examples: + vp dev + vp dev --open + vp dev --host localhost --port 5173 + +Documentation: https://viteplus.dev/guide/dev +``` + ## `vp run dev` `vp run dev` runs the dev script that the refusal points at diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/index.html b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/index.html new file mode 100644 index 0000000000..13ee08c5d6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/index.html @@ -0,0 +1 @@ + diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/package.json new file mode 100644 index 0000000000..b9775517b4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/package.json @@ -0,0 +1 @@ +{ "name": "@test/builtin-framework-guard-web", "private": true } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/src/valid.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/src/valid.js new file mode 100644 index 0000000000..9e1015feaf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/apps/web/src/valid.js @@ -0,0 +1 @@ +export const valid = true; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/package.json new file mode 100644 index 0000000000..3e235c4f0d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/package.json @@ -0,0 +1 @@ +{ "name": "@test/builtin-framework-guard-default-package", "private": true } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml new file mode 100644 index 0000000000..a7d800c9f2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml @@ -0,0 +1,18 @@ +# The framework refusal runs on the resolved target package, not on the +# invocation directory. `defaultPackage` acts as an implicit -C, so it can +# move a bare `vp build` out of a framework root into a plain Vite app, or +# into a framework app that the invocation directory never saw. +[[case]] +name = "builtin_framework_guard_default_package" +vp = ["local", "global"] +steps = [ + { argv = ["vp", "build"], comment = "the root holds a nuxt.config.ts, but defaultPackage retargets the build into apps/web, so it runs" }, + { argv = ["vpt", "list-dir", "apps/web/dist"], comment = "output lands in the configured package" }, +] + +[[case]] +name = "builtin_framework_guard_default_package_to_nuxt" +vp = ["local", "global"] +steps = [ + { argv = ["vp", "build"], cwd = "to_nuxt", comment = "defaultPackage points at a Nuxt app, so the refusal fires for the resolved target", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.global.md new file mode 100644 index 0000000000..390e2d8e3e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.global.md @@ -0,0 +1,26 @@ +# builtin_framework_guard_default_package + +## `vp build` + +the root holds a nuxt.config.ts, but defaultPackage retargets the build into apps/web, so it runs + +``` +VITE+ - The Unified Toolchain for the Web + +note: vp build: using ./apps/web (defaultPackage in vite.config.ts) +✓ 4 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB +dist/assets/index-.js kB │ gzip: kB + +✓ built in +``` + +## `vpt list-dir apps/web/dist` + +output lands in the configured package + +``` +assets +index.html +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.local.md new file mode 100644 index 0000000000..9ed2e2699e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package.local.md @@ -0,0 +1,24 @@ +# builtin_framework_guard_default_package + +## `vp build` + +the root holds a nuxt.config.ts, but defaultPackage retargets the build into apps/web, so it runs + +``` +note: vp build: using ./apps/web (defaultPackage in vite.config.ts) +✓ 4 modules transformed. +computing gzip size... +dist/index.html kB │ gzip: kB +dist/assets/index-.js kB │ gzip: kB + +✓ built in +``` + +## `vpt list-dir apps/web/dist` + +output lands in the configured package + +``` +assets +index.html +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md new file mode 100644 index 0000000000..69fc118fe1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md @@ -0,0 +1,15 @@ +# builtin_framework_guard_default_package_to_nuxt + +## `cd to_nuxt && vp build` + +defaultPackage points at a Nuxt app, so the refusal fires for the resolved target + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +note: vp build: using ./app (defaultPackage in vite.config.ts) +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: run the Nuxt CLI with `vp exec nuxt build`. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md new file mode 100644 index 0000000000..69b02dfbc5 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md @@ -0,0 +1,13 @@ +# builtin_framework_guard_default_package_to_nuxt + +## `cd to_nuxt && vp build` + +defaultPackage points at a Nuxt app, so the refusal fires for the resolved target + +**Exit code:** 1 + +``` +note: vp build: using ./app (defaultPackage in vite.config.ts) +error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. +hint: run the Nuxt CLI with `vp exec nuxt build`. +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/package.json new file mode 100644 index 0000000000..dc4dd92c51 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/app/package.json @@ -0,0 +1 @@ +{ "name": "@test/builtin-framework-guard-to-nuxt-app", "private": true } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/package.json new file mode 100644 index 0000000000..22ac2051d2 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/package.json @@ -0,0 +1 @@ +{ "name": "@test/builtin-framework-guard-to-nuxt", "private": true } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/vite.config.ts new file mode 100644 index 0000000000..1c15cee28a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/to_nuxt/vite.config.ts @@ -0,0 +1,3 @@ +export default { + defaultPackage: './app', +}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/vite.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/vite.config.ts new file mode 100644 index 0000000000..f3f5a014a7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/vite.config.ts @@ -0,0 +1,3 @@ +export default { + defaultPackage: './apps/web', +}; diff --git a/docs/guide/run.md b/docs/guide/run.md index 5f36e00a94..cf49a9d6c2 100644 --- a/docs/guide/run.md +++ b/docs/guide/run.md @@ -51,7 +51,7 @@ Select a task (↑/↓, Enter to run, Esc to clear): `build`, `preview`, `lint`, `fmt`, `check`, and `pack` work the same way. -If the project defines that script or task, run it with `vp run `. For example, with a `"dev": "astro dev"` script, `vp run dev` starts Astro. In a Nuxt or Astro project, `vp dev` and `vp build` stop with an error that points at `vp run `. Those frameworks run Vite through their own CLI. The bundled Vite CLI cannot serve or build them. +If the project defines that script or task, run it with `vp run `. For example, with a `"dev": "astro dev"` script, `vp run dev` starts Astro. In a Nuxt or Astro project, `vp dev` and `vp build` stop with an error. The hint points at the script that runs the framework command, or at the framework CLI through `vp exec` when no script matches. Those frameworks run Vite through their own CLI. The bundled Vite CLI cannot serve or build them. ## Caching diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index bf0be7b9c2..664a80f382 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -45,7 +45,7 @@ For example: - `vp test` always runs the built-in Vitest command - `vp run dev`, `vp run build`, and `vp run test` run the matching `package.json` scripts instead -In a Nuxt or Astro project (a `nuxt.config.*` or `astro.config.*` file next to `package.json`), `vp dev` and `vp build` stop with an error that points at `vp run `. Those frameworks run Vite through their own CLI. The bundled Vite CLI cannot serve or build them. +In a Nuxt or Astro project (a `nuxt.config.*` or `astro.config.*` file next to `package.json`), `vp dev` and `vp build` stop with an error. The hint points at the script that runs the framework command, or at the framework CLI through `vp exec` when no script matches. Those frameworks run Vite through their own CLI. The bundled Vite CLI cannot serve or build them. See [Built-in Commands vs Scripts](/guide/run#built-in-commands-vs-scripts) for when to prefer each path. diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs index 8f273dc39b..84592d8a10 100644 --- a/packages/cli/binding/src/cli/framework_guard.rs +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -9,8 +9,9 @@ //! script that runs the framework command, or at the framework CLI through //! `vp exec` when no script matches. The guard checks direct invocations //! only: a command spawned from a task or package script, for example a -//! `"dev": "vp dev"` script, runs as invoked. An explicit `--config`/`-c` -//! flag selects a Vite config on purpose, so it skips the refusal. +//! `"dev": "vp dev"` script, runs as invoked. Help and version requests +//! reach the tool, and an explicit `--config`/`-c` flag selects a Vite +//! config on purpose, so both skip the refusal. use owo_colors::OwoColorize; use vp_shared::output; @@ -78,6 +79,30 @@ pub(super) fn check( subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath, ) -> Option { + let (command, manifest, framework, config_file) = find_refusal(subcommand, cwd)?; + let built_in = format!("`vp {command}`").bright_blue().to_string(); + output::error(&format!( + "this project uses {name} ({config_file}). {built_in} runs the bundled Vite CLI, \ + not the {name} CLI.", + name = framework.name, + )); + output::raw_stderr(&format!("hint: {}", run_hint(&manifest, framework, command))); + Some(ExitStatus(1)) +} + +/// Whether `check` would refuse in `cwd`, without output. The script note +/// calls this before target resolution: a note that recommends `vpr` must +/// not print right before a refusal. +pub(super) fn applies(subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath) -> bool { + find_refusal(subcommand, cwd).is_some() +} + +/// The refusal for `cwd`, or `None` when the command can proceed. The +/// returned manifest is the enclosing `package.json`, for the hint. +fn find_refusal( + subcommand: &SynthesizableSubcommand, + cwd: &AbsolutePath, +) -> Option<(&'static str, serde_json::Value, &'static Framework, &'static str)> { let (command, args) = match subcommand { SynthesizableSubcommand::Dev { args } => ("dev", args), SynthesizableSubcommand::Build { args } => ("build", args), @@ -88,24 +113,16 @@ pub(super) fn check( if super::script_note::spawned_from_script() { return None; } - if has_explicit_config(args) { + if asks_help_or_version(args) || selects_explicit_config(args) { return None; } // `vp run` resolves the task from the nearest `package.json`. The same // walk here keeps the hint correct from a subdirectory. let package = vt_workspace::find_package_root(cwd).ok()?; let (framework, config_file) = detect(package.path)?; - - let built_in = format!("`vp {command}`").bright_blue().to_string(); - output::error(&format!( - "this project uses {name} ({config_file}). {built_in} runs the bundled Vite CLI, \ - not the {name} CLI.", - name = framework.name, - )); let manifest = serde_json::from_slice::(package.package_json.content()) .unwrap_or(serde_json::Value::Null); - output::raw_stderr(&format!("hint: {}", run_hint(&manifest, framework, command))); - Some(ExitStatus(1)) + Some((command, manifest, framework, config_file)) } /// The hint that follows the refusal. It points at the first path that works @@ -170,11 +187,23 @@ fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, &'static str)> { None } -/// Whether the forwarded Vite args select a config file explicitly. The -/// capital `-C` flag retargets the directory. It is a different flag and -/// does not count. -fn has_explicit_config(args: &[String]) -> bool { - args.iter().any(|arg| { +/// The args the tool would parse as flags: everything before the `--` +/// terminator. Tokens after the terminator are positionals. +fn forwarded_flags(args: &[String]) -> impl Iterator { + args.iter().map(String::as_str).take_while(|arg| *arg != "--") +} + +/// Whether the forwarded args ask the tool for help or its version. These +/// requests must reach the tool: `vp dev --help` prints the Vite help. +fn asks_help_or_version(args: &[String]) -> bool { + forwarded_flags(args).any(super::help::is_app_tool_help_or_version_flag) +} + +/// Whether the forwarded args select a config file explicitly. The capital +/// `-C` flag retargets the directory. It is a different flag and does not +/// count. +fn selects_explicit_config(args: &[String]) -> bool { + forwarded_flags(args).any(|arg| { arg == "-c" || arg == "--config" || arg.starts_with("--config=") || arg.starts_with("-c=") }) } @@ -183,7 +212,9 @@ fn has_explicit_config(args: &[String]) -> bool { mod tests { use vt_path::AbsolutePathBuf; - use super::{FRAMEWORKS, contains_word, detect, has_explicit_config, run_hint}; + use super::{ + FRAMEWORKS, asks_help_or_version, contains_word, detect, run_hint, selects_explicit_config, + }; fn framework(name: &str) -> &'static super::Framework { FRAMEWORKS.iter().find(|framework| framework.name == name).expect("known framework") @@ -283,10 +314,23 @@ mod tests { #[test] fn explicit_config_flags_skip_the_refusal() { let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); - assert!(has_explicit_config(&owned(&["--config", "vite.config.ts"]))); - assert!(has_explicit_config(&owned(&["--config=vite.config.ts"]))); - assert!(has_explicit_config(&owned(&["-c", "vite.config.ts"]))); - assert!(!has_explicit_config(&owned(&["--port", "5000"]))); - assert!(!has_explicit_config(&owned(&["-C", "apps/web"]))); + assert!(selects_explicit_config(&owned(&["--config", "vite.config.ts"]))); + assert!(selects_explicit_config(&owned(&["--config=vite.config.ts"]))); + assert!(selects_explicit_config(&owned(&["-c", "vite.config.ts"]))); + assert!(!selects_explicit_config(&owned(&["--port", "5000"]))); + assert!(!selects_explicit_config(&owned(&["-C", "apps/web"]))); + assert!(!selects_explicit_config(&owned(&["--", "--config"]))); + } + + #[test] + fn help_and_version_requests_skip_the_refusal() { + let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); + assert!(asks_help_or_version(&owned(&["--help"]))); + assert!(asks_help_or_version(&owned(&["-h"]))); + assert!(asks_help_or_version(&owned(&["--version"]))); + assert!(asks_help_or_version(&owned(&["-v"]))); + assert!(asks_help_or_version(&owned(&["--port", "5000", "--help"]))); + assert!(!asks_help_or_version(&owned(&["--port", "5000"]))); + assert!(!asks_help_or_version(&owned(&["--", "--help"]))); } } diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index 57c28ff74f..c23d0f03db 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -59,9 +59,11 @@ async fn execute_direct_subcommand( app_target::AppTarget::Dir(dir) => dir, app_target::AppTarget::CurrentDir => cwd, }; - // Elicitation can select a package that the first check never saw, for - // example a `defaultPackage` that holds a Nuxt or Astro app. - if retargeted && let Some(exit) = framework_guard::check(&subcommand, cwd) { + // The refusal belongs to the resolved target: elicitation or + // `defaultPackage` can select a package that the invocation directory + // never saw, or move the command out of a framework root into a plain + // Vite app. + if let Some(exit) = framework_guard::check(&subcommand, cwd) { return Ok(exit); } @@ -393,18 +395,20 @@ pub async fn main( match cli_args { CLIArgs::Synthesizable(subcmd) => { - // A Nuxt or Astro project cannot run through the bundled Vite - // CLI. Refuse before the script note recommends a different - // path. - if let Some(exit) = framework_guard::check(&subcmd, &cwd) { - return Ok(exit); - } // Only the built-ins can be mistaken for a script. `run`/`cache` // below are the script path itself; `install` and friends // legitimately trigger a project's `install` lifecycle scripts // through the package manager, so redirecting those to `vpr` would // be wrong; and `exec` names a binary rather than a task. - script_note::print(raw_subcommand.as_deref(), &cwd); + // + // The framework refusal itself runs after target resolution in + // `execute_direct_subcommand`, because `defaultPackage` can move + // the command out of a framework root. The note still must not + // recommend `vpr` right before a refusal, so a refusal in the + // invocation package silences it. + if !framework_guard::applies(&subcmd, &cwd) { + script_note::print(raw_subcommand.as_deref(), &cwd); + } execute_direct_subcommand(subcmd, &cwd, options).await } CLIArgs::ViteTask(command) => execute_vite_task_command(command, cwd, options).await, From ae9bdba8ab884491315d1571ada679a9b915b0c3 Mon Sep 17 00:00:00 2001 From: MK Date: Sun, 16 Aug 2026 19:34:39 +0800 Subject: [PATCH 7/7] fix(cli): guard the directory Vite would use Classify the args with the app-target walk: a positional is the Vite root and becomes the guarded directory, and explicit invocations (help/version, --config) reach the tool. This replaces the hand-rolled flag scans. Validate the same-named script's command before recommending it: an unrelated "dev": "storybook dev" no longer becomes the hint. Carry the resolved target into the hints as -C after a defaultPackage or picker retarget, and for positional roots. Refuse at a framework workspace root instead of falling into package elicitation. An explicit defaultPackage still wins. --- .../mismatched-script/nuxt.config.ts | 1 + .../mismatched-script/package.json | 10 + .../builtin_framework_guard/snapshots.toml | 3 + .../builtin_framework_guard.global.md | 44 ++++ .../builtin_framework_guard.local.md | 38 +++ .../builtin_framework_guard/web/index.html | 1 + .../builtin_framework_guard/web/package.json | 1 + .../builtin_framework_guard/web/src/valid.js | 1 + .../framework_root_ws/nuxt.config.ts | 1 + .../framework_root_ws/package.json | 9 + .../packages/lib/package.json | 1 + .../framework_root_ws/pnpm-workspace.yaml | 2 + .../snapshots.toml | 11 +- ...rk_guard_default_package_to_nuxt.global.md | 4 +- ...ork_guard_default_package_to_nuxt.local.md | 4 +- ...n_framework_guard_workspace_root.global.md | 14 + ...in_framework_guard_workspace_root.local.md | 12 + packages/cli/binding/src/cli/app_target.rs | 10 +- .../cli/binding/src/cli/framework_guard.rs | 239 ++++++++++++------ packages/cli/binding/src/cli/mod.rs | 22 +- 20 files changed, 332 insertions(+), 96 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/index.html create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/src/valid.js create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/nuxt.config.ts create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/packages/lib/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/pnpm-workspace.yaml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.global.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.local.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/package.json new file mode 100644 index 0000000000..7cf640d27c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/mismatched-script/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/builtin-framework-guard-mismatched-script", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "storybook dev", + "serve": "nuxt dev --host" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml index bbd3747fa3..d8ed9bfd53 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -16,6 +16,9 @@ steps = [ { argv = ["vp", "build"], cwd = "no-scripts", comment = "`vp build` gets the same fallback hint", continue-on-failure = true }, { argv = ["vp", "dev"], cwd = "renamed-script", comment = "a script that runs the framework dev command under another name becomes the hint target", continue-on-failure = true }, { argv = ["vp", "build"], cwd = "renamed-script", comment = "the build hint finds the renamed build script the same way", continue-on-failure = true }, + { argv = ["vp", "dev"], cwd = "mismatched-script", comment = "an unrelated same-named script does not become the hint; the script that runs nuxt dev does", continue-on-failure = true }, + { argv = ["vp", "build", "web"], comment = "a positional root is inspected instead of the invocation package: the plain Vite child builds" }, + { argv = ["vp", "dev", "astro"], comment = "a positional root inside an Astro package refuses, and the hint carries -C", continue-on-failure = true }, { argv = ["vp", "dev", "--config", "vite.config.ts", "--port", "12312312312"], comment = "an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately)", continue-on-failure = true }, { argv = ["vp", "dev", "--help"], comment = "a help request reaches the tool, so the guard steps aside" }, { argv = ["vp", "run", "dev"], comment = "`vp run dev` runs the dev script that the refusal points at" }, diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md index d3945162dd..79b531ff1c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -104,6 +104,50 @@ error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite hint: did you mean `vp run make`? The make script runs `nuxt build`. ``` +## `cd mismatched-script && vp dev` + +an unrelated same-named script does not become the hint; the script that runs nuxt dev does + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run serve`? The serve script runs `nuxt dev`. +``` + +## `vp build web` + +a positional root is inspected instead of the invocation package: the plain Vite child builds + +``` +VITE+ - The Unified Toolchain for the Web + +note: You are running `vp build` as a Vite+ built-in command. If you meant to run the build npm script, use `vpr build` instead. +note: `vp build web` sets Vite's root without changing the working directory. To run as if started there, use `vp -C web build`. +✓ 4 modules transformed. +computing gzip size... +web/dist/index.html kB │ gzip: kB +web/dist/assets/index-.js kB │ gzip: kB + +✓ built in +``` + +## `vp dev astro` + +a positional root inside an Astro package refuses, and the hint carries -C + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +note: `vp dev astro` sets Vite's root without changing the working directory. To run as if started there, use `vp -C astro dev`. +error: this project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vite CLI, not the Astro CLI. +hint: did you mean `vp -C astro run dev`? +``` + ## `vp dev --config vite.config.ts --port 12312312312` an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md index e157baa3d0..e1e6a1da79 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -88,6 +88,44 @@ error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite hint: did you mean `vp run make`? The make script runs `nuxt build`. ``` +## `cd mismatched-script && vp dev` + +an unrelated same-named script does not become the hint; the script that runs nuxt dev does + +**Exit code:** 1 + +``` +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run serve`? The serve script runs `nuxt dev`. +``` + +## `vp build web` + +a positional root is inspected instead of the invocation package: the plain Vite child builds + +``` +note: You are running `vp build` as a Vite+ built-in command. If you meant to run the build npm script, use `vpr build` instead. +note: `vp build web` sets Vite's root without changing the working directory. To run as if started there, use `vp -C web build`. +✓ 4 modules transformed. +computing gzip size... +web/dist/index.html kB │ gzip: kB +web/dist/assets/index-.js kB │ gzip: kB + +✓ built in +``` + +## `vp dev astro` + +a positional root inside an Astro package refuses, and the hint carries -C + +**Exit code:** 1 + +``` +note: `vp dev astro` sets Vite's root without changing the working directory. To run as if started there, use `vp -C astro dev`. +error: this project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vite CLI, not the Astro CLI. +hint: did you mean `vp -C astro run dev`? +``` + ## `vp dev --config vite.config.ts --port 12312312312` an explicit --config selects the bundled Vite CLI on purpose, so only the script note prints (the invalid port stops the server immediately) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/index.html b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/index.html new file mode 100644 index 0000000000..13ee08c5d6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/index.html @@ -0,0 +1 @@ + diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/package.json new file mode 100644 index 0000000000..af039894f4 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/package.json @@ -0,0 +1 @@ +{ "name": "@test/builtin-framework-guard-web-child", "private": true } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/src/valid.js b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/src/valid.js new file mode 100644 index 0000000000..9e1015feaf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/web/src/valid.js @@ -0,0 +1 @@ +export const valid = true; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/nuxt.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/nuxt.config.ts new file mode 100644 index 0000000000..ff8b4c5632 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/nuxt.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/package.json new file mode 100644 index 0000000000..1f81e92bec --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/builtin-framework-guard-ws-root", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "nuxt dev" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/packages/lib/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/packages/lib/package.json new file mode 100644 index 0000000000..4be061b7bb --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/packages/lib/package.json @@ -0,0 +1 @@ +{ "name": "@test/builtin-framework-guard-ws-lib", "private": true } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/pnpm-workspace.yaml new file mode 100644 index 0000000000..dee51e928d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/framework_root_ws/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml index a7d800c9f2..822ac840cd 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml @@ -14,5 +14,14 @@ steps = [ name = "builtin_framework_guard_default_package_to_nuxt" vp = ["local", "global"] steps = [ - { argv = ["vp", "build"], cwd = "to_nuxt", comment = "defaultPackage points at a Nuxt app, so the refusal fires for the resolved target", continue-on-failure = true }, + { argv = ["vp", "build"], cwd = "to_nuxt", comment = "defaultPackage points at a Nuxt app, so the refusal fires for the resolved target and the hint carries -C", continue-on-failure = true }, +] + +# A framework app can itself be a workspace root with member packages. The +# refusal owns that root: the command must not fall into the package picker. +[[case]] +name = "builtin_framework_guard_workspace_root" +vp = ["local", "global"] +steps = [ + { argv = ["vp", "dev"], cwd = "framework_root_ws", comment = "a framework app at a workspace root refuses instead of listing member packages", continue-on-failure = true }, ] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md index 69fc118fe1..a99922768e 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.global.md @@ -2,7 +2,7 @@ ## `cd to_nuxt && vp build` -defaultPackage points at a Nuxt app, so the refusal fires for the resolved target +defaultPackage points at a Nuxt app, so the refusal fires for the resolved target and the hint carries -C **Exit code:** 1 @@ -11,5 +11,5 @@ VITE+ - The Unified Toolchain for the Web note: vp build: using ./app (defaultPackage in vite.config.ts) error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. -hint: run the Nuxt CLI with `vp exec nuxt build`. +hint: run the Nuxt CLI with `vp -C app exec nuxt build`. ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md index 69b02dfbc5..b8a9e86e11 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_default_package_to_nuxt.local.md @@ -2,12 +2,12 @@ ## `cd to_nuxt && vp build` -defaultPackage points at a Nuxt app, so the refusal fires for the resolved target +defaultPackage points at a Nuxt app, so the refusal fires for the resolved target and the hint carries -C **Exit code:** 1 ``` note: vp build: using ./app (defaultPackage in vite.config.ts) error: this project uses Nuxt (nuxt.config.ts). `vp build` runs the bundled Vite CLI, not the Nuxt CLI. -hint: run the Nuxt CLI with `vp exec nuxt build`. +hint: run the Nuxt CLI with `vp -C app exec nuxt build`. ``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.global.md new file mode 100644 index 0000000000..e0cf6242f9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.global.md @@ -0,0 +1,14 @@ +# builtin_framework_guard_workspace_root + +## `cd framework_root_ws && vp dev` + +a framework app at a workspace root refuses instead of listing member packages + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run dev`? +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.local.md new file mode 100644 index 0000000000..f70d96278d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots/builtin_framework_guard_workspace_root.local.md @@ -0,0 +1,12 @@ +# builtin_framework_guard_workspace_root + +## `cd framework_root_ws && vp dev` + +a framework app at a workspace root refuses instead of listing member packages + +**Exit code:** 1 + +``` +error: this project uses Nuxt (nuxt.config.ts). `vp dev` runs the bundled Vite CLI, not the Nuxt CLI. +hint: did you mean `vp run dev`? +``` diff --git a/packages/cli/binding/src/cli/app_target.rs b/packages/cli/binding/src/cli/app_target.rs index 6c687ba7cd..4587887c95 100644 --- a/packages/cli/binding/src/cli/app_target.rs +++ b/packages/cli/binding/src/cli/app_target.rs @@ -85,7 +85,7 @@ const PACK_BOOLEAN_FLAGS: &[&str] = &[ /// How an app command's arguments target it, per the walk in /// [`classify_args`]. -enum ArgTarget<'a> { +pub(super) enum ArgTarget<'a> { /// No positional target and no help-like flag: elicitation territory. Bare, /// The first token the tool would treat as a positional (a Vite `[root]` @@ -108,7 +108,7 @@ fn is_bare(command: &str, args: &[String]) -> bool { /// and disables elicitation. pack's workspace selectors already define their /// own target set and disable elicitation outright. Help/version requests /// are answered by the underlying tool and must never be redirected. -fn classify_args<'a>(command: &str, args: &'a [String]) -> ArgTarget<'a> { +pub(super) fn classify_args<'a>(command: &str, args: &'a [String]) -> ArgTarget<'a> { /// `arg` is one of `flags`, exactly or in inline `flag=value` form. fn matches_flag(arg: &str, flags: &[&str]) -> bool { flags.iter().any(|f| arg == *f || arg.strip_prefix(f).is_some_and(|r| r.starts_with('='))) @@ -405,6 +405,12 @@ fn classify(subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath) -> Classif if root_looks_runnable(&root_config, &workspace_root.path, command) { return Classification::RunInPlace(Some(workspace_root)); } + // A framework root is not runnable by the config heuristic, but the + // framework refusal owns it: a package listing must not hide the + // refusal. An explicit `defaultPackage` already won above. + if super::framework_guard::applies(subcommand, cwd) { + return Classification::RunInPlace(Some(workspace_root)); + } Classification::Elicit(command, Elicitation::WorkspaceRoot(workspace_root)) } diff --git a/packages/cli/binding/src/cli/framework_guard.rs b/packages/cli/binding/src/cli/framework_guard.rs index 84592d8a10..40675c14e0 100644 --- a/packages/cli/binding/src/cli/framework_guard.rs +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -9,9 +9,11 @@ //! script that runs the framework command, or at the framework CLI through //! `vp exec` when no script matches. The guard checks direct invocations //! only: a command spawned from a task or package script, for example a -//! `"dev": "vp dev"` script, runs as invoked. Help and version requests -//! reach the tool, and an explicit `--config`/`-c` flag selects a Vite -//! config on purpose, so both skip the refusal. +//! `"dev": "vp dev"` script, runs as invoked. The guarded directory is the +//! one Vite would use: the invocation directory, a positional root, or the +//! target that `defaultPackage`/elicitation resolved. Help and version +//! requests reach the tool, and an explicit `--config`/`-c` flag selects a +//! Vite config on purpose, so both skip the refusal. use owo_colors::OwoColorize; use vp_shared::output; @@ -73,36 +75,53 @@ struct Framework { /// Refuse `vp dev` / `vp build` in a Nuxt or Astro project. /// -/// Returns the exit status after it prints the refusal. Returns `None` when -/// the command can proceed. +/// `retarget` is the path that elicitation or `defaultPackage` resolved for +/// a bare command, relative to the invocation directory; hints then carry +/// the matching `-C ` so they run in the refused package. Returns +/// the exit status after it prints the refusal, or `None` when the command +/// can proceed. pub(super) fn check( subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath, + retarget: Option<&str>, ) -> Option { - let (command, manifest, framework, config_file) = find_refusal(subcommand, cwd)?; - let built_in = format!("`vp {command}`").bright_blue().to_string(); + let refusal = find_refusal(subcommand, cwd, retarget)?; + let built_in = format!("`vp {}`", refusal.command).bright_blue().to_string(); output::error(&format!( "this project uses {name} ({config_file}). {built_in} runs the bundled Vite CLI, \ not the {name} CLI.", - name = framework.name, + name = refusal.framework.name, + config_file = refusal.config_file, )); - output::raw_stderr(&format!("hint: {}", run_hint(&manifest, framework, command))); + output::raw_stderr(&format!("hint: {}", run_hint(&refusal))); Some(ExitStatus(1)) } -/// Whether `check` would refuse in `cwd`, without output. The script note -/// calls this before target resolution: a note that recommends `vpr` must -/// not print right before a refusal. +/// Whether `check` would refuse, without output. The script note and +/// workspace elicitation ask this before they act: a note must not +/// recommend `vpr` right before a refusal, and a package listing must not +/// hide one. pub(super) fn applies(subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath) -> bool { - find_refusal(subcommand, cwd).is_some() + find_refusal(subcommand, cwd, None).is_some() } -/// The refusal for `cwd`, or `None` when the command can proceed. The -/// returned manifest is the enclosing `package.json`, for the hint. +struct Refusal { + command: &'static str, + framework: &'static Framework, + config_file: &'static str, + /// The enclosing `package.json` of the refused directory, for the hint. + manifest: serde_json::Value, + /// `-C` target for the hint when the refused directory is not the + /// invocation directory (a positional root or a resolved retarget). + target: Option, +} + +/// The refusal for this invocation, or `None` when the command can proceed. fn find_refusal( subcommand: &SynthesizableSubcommand, cwd: &AbsolutePath, -) -> Option<(&'static str, serde_json::Value, &'static Framework, &'static str)> { + retarget: Option<&str>, +) -> Option { let (command, args) = match subcommand { SynthesizableSubcommand::Dev { args } => ("dev", args), SynthesizableSubcommand::Build { args } => ("build", args), @@ -113,51 +132,77 @@ fn find_refusal( if super::script_note::spawned_from_script() { return None; } - if asks_help_or_version(args) || selects_explicit_config(args) { - return None; - } + // The tool's own arg walk decides what the guard inspects: a positional + // is the Vite root, and an explicit invocation (a help/version request, + // a `-c`/`--config` file) must reach the tool untouched. + let positional_root; + let (dir, target): (&AbsolutePath, Option<&str>) = + match super::app_target::classify_args(command, args) { + super::app_target::ArgTarget::Explicit => return None, + super::app_target::ArgTarget::Positional(root) => { + positional_root = cwd.join(root).clean(); + (&positional_root, Some(root)) + } + super::app_target::ArgTarget::Bare => (cwd, retarget), + }; // `vp run` resolves the task from the nearest `package.json`. The same // walk here keeps the hint correct from a subdirectory. - let package = vt_workspace::find_package_root(cwd).ok()?; + let package = vt_workspace::find_package_root(dir).ok()?; let (framework, config_file) = detect(package.path)?; let manifest = serde_json::from_slice::(package.package_json.content()) .unwrap_or(serde_json::Value::Null); - Some((command, manifest, framework, config_file)) + Some(Refusal { command, framework, config_file, manifest, target: target.map(str::to_string) }) } /// The hint that follows the refusal. It points at the first path that works -/// in this package: +/// in the refused package: /// -/// 1. the `package.json` script with the command's name, -/// 2. a script that runs the framework command under another name, +/// 1. the `package.json` script with the command's name, when its command +/// runs the framework (a `"dev": "storybook dev"` must not become the +/// hint), +/// 2. another script that runs the framework command, /// 3. the framework CLI through `vp exec`. /// /// The check reads `package.json` scripts only. A `run.tasks` entry in /// `vite.config.ts` with the command's name also works with `vp run`, but /// the guard does not load that config. -fn run_hint(manifest: &serde_json::Value, framework: &Framework, command: &str) -> String { +fn run_hint(refusal: &Refusal) -> String { + let Refusal { command, framework, manifest, target, .. } = refusal; + let vp = match target { + Some(target) => format!("vp -C {target}"), + None => "vp".to_string(), + }; if let Some(scripts) = manifest.get("scripts").and_then(serde_json::Value::as_object) { - if scripts.get(command).is_some_and(serde_json::Value::is_string) { - let via_run = format!("`vp run {command}`").bright_blue().to_string(); + let named = scripts.get(*command).and_then(serde_json::Value::as_str); + if named.is_some_and(|script| framework_invocation(script, framework, command).is_some()) { + let via_run = format!("`{vp} run {command}`").bright_blue().to_string(); return format!("did you mean {via_run}?"); } - for (name, value) in scripts { - let Some(value) = value.as_str() else { continue }; - for &bin in framework.bins { - let framework_command = format!("{bin} {command}"); - if contains_word(value, &framework_command) { - let via_run = format!("`vp run {name}`").bright_blue().to_string(); - return format!( - "did you mean {via_run}? The {name} script runs `{framework_command}`." - ); - } + for (name, script) in scripts { + if name == command { + continue; + } + let Some(script) = script.as_str() else { continue }; + if let Some(invocation) = framework_invocation(script, framework, command) { + let via_run = format!("`{vp} run {name}`").bright_blue().to_string(); + return format!("did you mean {via_run}? The {name} script runs `{invocation}`."); } } } - let via_exec = format!("`vp exec {} {command}`", framework.bins[0]).bright_blue().to_string(); + let via_exec = format!("`{vp} exec {} {command}`", framework.bins[0]).bright_blue().to_string(); format!("run the {} CLI with {via_exec}.", framework.name) } +/// The ` ` invocation inside `script`, when the script runs +/// the framework command. +fn framework_invocation(script: &str, framework: &Framework, command: &str) -> Option { + framework + .bins + .iter() + .map(|bin| format!("{bin} {command}")) + .find(|invocation| contains_word(script, invocation)) +} + /// Whether `text` contains `pattern` between whitespace boundaries, so /// `nuxt dev` does not match inside `nuxt devtools`. fn contains_word(text: &str, pattern: &str) -> bool { @@ -187,33 +232,13 @@ fn detect(dir: &AbsolutePath) -> Option<(&'static Framework, &'static str)> { None } -/// The args the tool would parse as flags: everything before the `--` -/// terminator. Tokens after the terminator are positionals. -fn forwarded_flags(args: &[String]) -> impl Iterator { - args.iter().map(String::as_str).take_while(|arg| *arg != "--") -} - -/// Whether the forwarded args ask the tool for help or its version. These -/// requests must reach the tool: `vp dev --help` prints the Vite help. -fn asks_help_or_version(args: &[String]) -> bool { - forwarded_flags(args).any(super::help::is_app_tool_help_or_version_flag) -} - -/// Whether the forwarded args select a config file explicitly. The capital -/// `-C` flag retargets the directory. It is a different flag and does not -/// count. -fn selects_explicit_config(args: &[String]) -> bool { - forwarded_flags(args).any(|arg| { - arg == "-c" || arg == "--config" || arg.starts_with("--config=") || arg.starts_with("-c=") - }) -} - #[cfg(test)] mod tests { use vt_path::AbsolutePathBuf; use super::{ - FRAMEWORKS, asks_help_or_version, contains_word, detect, run_hint, selects_explicit_config, + super::app_target::{ArgTarget, classify_args}, + FRAMEWORKS, Refusal, contains_word, detect, run_hint, }; fn framework(name: &str) -> &'static super::Framework { @@ -274,35 +299,74 @@ mod tests { std::fs::remove_dir_all(dir.as_path()).expect("remove temp dir"); } + fn refusal( + name: &str, + command: &'static str, + manifest: serde_json::Value, + target: Option<&str>, + ) -> Refusal { + Refusal { + command, + framework: framework(name), + config_file: "nuxt.config.ts", + manifest, + target: target.map(str::to_string), + } + } + #[test] fn hint_prefers_the_script_with_the_command_name() { let manifest = serde_json::json!({ "scripts": { "dev": "nuxt dev" } }); - let hint = run_hint(&manifest, framework("Nuxt"), "dev"); + let hint = run_hint(&refusal("Nuxt", "dev", manifest, None)); assert!(hint.contains("vp run dev"), "hint was: {hint}"); } + #[test] + fn hint_skips_a_same_named_script_that_runs_something_else() { + let manifest = serde_json::json!({ "scripts": { + "dev": "storybook dev", + "serve": "nuxt dev --host", + } }); + let hint = run_hint(&refusal("Nuxt", "dev", manifest, None)); + assert!(hint.contains("vp run serve"), "hint was: {hint}"); + assert!(!hint.contains("vp run dev"), "hint was: {hint}"); + + let only_unrelated = serde_json::json!({ "scripts": { "dev": "storybook dev" } }); + let hint = run_hint(&refusal("Nuxt", "dev", only_unrelated, None)); + assert!(hint.contains("vp exec nuxt dev"), "hint was: {hint}"); + } + #[test] fn hint_finds_a_renamed_script_that_runs_the_framework_command() { let manifest = serde_json::json!({ "scripts": { "devtools": "nuxt devtools enable", "start": "NODE_OPTIONS=--inspect nuxi dev --host", } }); - let hint = run_hint(&manifest, framework("Nuxt"), "dev"); + let hint = run_hint(&refusal("Nuxt", "dev", manifest, None)); assert!(hint.contains("vp run start"), "hint was: {hint}"); assert!(hint.contains("nuxi dev"), "hint was: {hint}"); } #[test] fn hint_falls_back_to_vp_exec_without_a_matching_script() { - let empty = serde_json::json!({}); - let hint = run_hint(&empty, framework("Nuxt"), "dev"); + let hint = run_hint(&refusal("Nuxt", "dev", serde_json::json!({}), None)); assert!(hint.contains("vp exec nuxt dev"), "hint was: {hint}"); let unrelated = serde_json::json!({ "scripts": { "lint": "oxlint ." } }); - let hint = run_hint(&unrelated, framework("Astro"), "build"); + let hint = run_hint(&refusal("Astro", "build", unrelated, None)); assert!(hint.contains("vp exec astro build"), "hint was: {hint}"); } + #[test] + fn hint_carries_the_resolved_target() { + let manifest = serde_json::json!({ "scripts": { "dev": "nuxt dev" } }); + let hint = run_hint(&refusal("Nuxt", "dev", manifest, Some("apps/web"))); + assert!(hint.contains("vp -C apps/web run dev"), "hint was: {hint}"); + + let hint = run_hint(&refusal("Nuxt", "build", serde_json::json!({}), Some("app"))); + assert!(hint.contains("vp -C app exec nuxt build"), "hint was: {hint}"); + } + #[test] fn contains_word_needs_whitespace_boundaries() { assert!(contains_word("nuxt dev", "nuxt dev")); @@ -312,25 +376,32 @@ mod tests { } #[test] - fn explicit_config_flags_skip_the_refusal() { - let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); - assert!(selects_explicit_config(&owned(&["--config", "vite.config.ts"]))); - assert!(selects_explicit_config(&owned(&["--config=vite.config.ts"]))); - assert!(selects_explicit_config(&owned(&["-c", "vite.config.ts"]))); - assert!(!selects_explicit_config(&owned(&["--port", "5000"]))); - assert!(!selects_explicit_config(&owned(&["-C", "apps/web"]))); - assert!(!selects_explicit_config(&owned(&["--", "--config"]))); - } - - #[test] - fn help_and_version_requests_skip_the_refusal() { + fn arg_classification_drives_the_guarded_directory() { let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); - assert!(asks_help_or_version(&owned(&["--help"]))); - assert!(asks_help_or_version(&owned(&["-h"]))); - assert!(asks_help_or_version(&owned(&["--version"]))); - assert!(asks_help_or_version(&owned(&["-v"]))); - assert!(asks_help_or_version(&owned(&["--port", "5000", "--help"]))); - assert!(!asks_help_or_version(&owned(&["--port", "5000"]))); - assert!(!asks_help_or_version(&owned(&["--", "--help"]))); + // Explicit invocations reach the tool untouched. + for args in [ + vec!["--help"], + vec!["-h"], + vec!["--version"], + vec!["-v"], + vec!["--port", "5000", "--help"], + vec!["--config", "vite.config.ts"], + vec!["--config=vite.config.ts"], + vec!["-c", "vite.config.ts"], + ] { + assert!( + matches!(classify_args("dev", &owned(&args)), ArgTarget::Explicit), + "expected Explicit for {args:?}" + ); + } + // A positional is the Vite root and becomes the guarded directory. + assert!(matches!(classify_args("dev", &owned(&["web"])), ArgTarget::Positional("web"))); + assert!(matches!( + classify_args("dev", &owned(&["--cors", "web"])), + ArgTarget::Positional("web") + )); + // A flag value is not a positional; a bare command guards the + // invocation directory. + assert!(matches!(classify_args("dev", &owned(&["--port", "5000"])), ArgTarget::Bare)); } } diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index c23d0f03db..aa9fd82c43 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -42,6 +42,17 @@ use self::{ types::CLIArgs, }; +/// The `-C` spelling of a resolved target for user-facing hints: relative to +/// the invocation directory when the target is inside it, absolute otherwise. +fn display_target(invocation: &AbsolutePath, target: &AbsolutePath) -> String { + if let Ok(Some(rel)) = target.strip_prefix(invocation) + && !rel.as_str().is_empty() + { + return rel.as_str().to_string(); + } + target.as_path().display().to_string() +} + /// Execute a synthesizable subcommand directly (not through vite-task Session). /// No caching, no task graph, no dependency resolution. async fn execute_direct_subcommand( @@ -54,16 +65,17 @@ async fn execute_direct_subcommand( // in the resolved directory (rfcs/cwd-flag.md). let (target, workspace_root_hint) = app_target::resolve_app_target(&subcommand, cwd)?; let retargeted = matches!(&target, app_target::AppTarget::Dir(_)); - let cwd = match &target { + let (cwd, retarget) = match &target { app_target::AppTarget::Exit(status) => return Ok(*status), - app_target::AppTarget::Dir(dir) => dir, - app_target::AppTarget::CurrentDir => cwd, + app_target::AppTarget::Dir(dir) => (dir, Some(display_target(cwd, dir))), + app_target::AppTarget::CurrentDir => (cwd, None), }; // The refusal belongs to the resolved target: elicitation or // `defaultPackage` can select a package that the invocation directory // never saw, or move the command out of a framework root into a plain - // Vite app. - if let Some(exit) = framework_guard::check(&subcommand, cwd) { + // Vite app. A retarget rides into the hints as `-C `, so they + // run in the refused package. + if let Some(exit) = framework_guard::check(&subcommand, cwd, retarget.as_deref()) { return Ok(exit); }