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/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/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/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/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..783e243f04 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/package.json @@ -0,0 +1,11 @@ +{ + "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", + "nested-build": "vp build" + } +} 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 new file mode 100644 index 0000000000..d8ed9bfd53 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots.toml @@ -0,0 +1,44 @@ +# `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"] +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 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"], 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" }, +] + +# 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.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..79b531ff1c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.global.md @@ -0,0 +1,213 @@ +# 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 project uses Nuxt (nuxt.config.ts). `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 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 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). `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 project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vite CLI, not the Astro CLI. +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`. +``` + +## `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) + +**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 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 + +``` +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..e1e6a1da79 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard/snapshots/builtin_framework_guard.local.md @@ -0,0 +1,187 @@ +# builtin_framework_guard + +## `vp dev` + +`vp dev` refuses in a Nuxt project and points at the dev script + +**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`? +``` + +## `vp build` + +`vp build` refuses 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 build`? +``` + +## `cd src && vp dev` + +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). `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 project uses Astro (astro.config.mjs). `vp dev` runs the bundled Vite CLI, not the Astro CLI. +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`. +``` + +## `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) + +**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 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 + +``` +$ 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_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/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/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/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/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/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..822ac840cd --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/builtin_framework_guard_default_package/snapshots.toml @@ -0,0 +1,27 @@ +# 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 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.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..a99922768e --- /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 and the hint carries -C + +**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 -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 new file mode 100644 index 0000000000..b8a9e86e11 --- /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 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 -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/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 5752752078..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, 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. 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 ce88a329c5..664a80f382 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. 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. ::: info 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 new file mode 100644 index 0000000000..40675c14e0 --- /dev/null +++ b/packages/cli/binding/src/cli/framework_guard.rs @@ -0,0 +1,407 @@ +//! Refusal for framework projects whose own CLI wraps Vite. +//! +//! `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 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. 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. 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; +use vt::ExitStatus; +use vt_path::AbsolutePath; + +use super::types::SynthesizableSubcommand; + +/// 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 + // `configFile: "nuxt.config"` + // (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, so the guard does not check + // them. + Framework { + name: "Nuxt", + config_files: &[ + "nuxt.config.js", + "nuxt.config.ts", + "nuxt.config.mjs", + "nuxt.config.cjs", + "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. + // Astro does not load a `.cjs` or `.cts` config. + Framework { + name: "Astro", + config_files: &[ + "astro.config.mjs", + "astro.config.js", + "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. +/// +/// `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 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 = refusal.framework.name, + config_file = refusal.config_file, + )); + output::raw_stderr(&format!("hint: {}", run_hint(&refusal))); + Some(ExitStatus(1)) +} + +/// 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, None).is_some() +} + +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, + retarget: Option<&str>, +) -> Option { + let (command, args) = match subcommand { + SynthesizableSubcommand::Dev { args } => ("dev", args), + 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; + } + // 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(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(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 the refused package: +/// +/// 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(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) { + 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, 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(); + 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 { + 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 { + for &config_file in framework.config_files { + if dir.join(config_file).as_path().is_file() { + return Some((framework, config_file)); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use vt_path::AbsolutePathBuf; + + use super::{ + super::app_target::{ArgTarget, classify_args}, + FRAMEWORKS, Refusal, contains_word, detect, 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!( + "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 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"); + } + + 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(&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(&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 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(&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")); + 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 arg_classification_drives_the_guarded_directory() { + let owned = |args: &[&str]| args.iter().map(|arg| (*arg).to_string()).collect::>(); + // 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 1954d24daa..aa9fd82c43 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; @@ -41,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( @@ -53,11 +65,19 @@ 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. 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); + } // The resolver hands back the workspace root it already found whenever the // command runs in the unchanged cwd (never after a -C/elicitation @@ -392,7 +412,15 @@ pub async fn main( // 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, 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`