diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1a08d7dfe..566ff22b33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -956,6 +956,9 @@ jobs: - name: Install Global CLI vp run: pnpm bootstrap-cli:ci + - name: Install Fish + uses: fish-actions/install-fish@4bb37a71daebb76c6fb7d83771cff31b8a78c28e # v1.2.0 + # https://github.com/marketplace/actions/setup-nu - name: Install Nushell uses: hustcer/setup-nu@ccd5bb5426b05a32009c2ba967946231f3919c97 # v3.25 @@ -980,6 +983,7 @@ jobs: run: | VP_SNAP_GLOBAL_VP="$HOME/.vite-plus/bin/vp" \ VP_SNAP_JS_RUNTIME_DIR="$HOME/.vite-plus/js_runtime" \ + VP_SNAP_FISH_BIN="$(command -v fish)" \ VP_SNAP_NU_BIN="$(command -v nu)" \ cargo test -p vp_cli_snapshots env: diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md index 0005c5c2e6..dda8f2b3c6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md @@ -63,6 +63,7 @@ Environment overrides, mainly for CI: | `VP_SNAP_GLOBAL_VP` | Path to a prebuilt global `vp` binary (skips the target-dir lookup) | | `VP_SNAP_LOCAL_CLI_BIN_DIR` | Local CLI bin dir (default `/packages/cli/bin`) | | `VP_SNAP_JS_RUNTIME_DIR` | Provisioned managed runtime to seed case homes with | +| `VP_SNAP_FISH_BIN` | Fish binary for cases that execute generated `env.fish` files | | `VP_SNAP_NU_BIN` | Nushell binary for cases that execute generated `env.nu` files | | `VP_SNAP_SKIP_FLAVORS` | Comma-separated flavors to skip registering (e.g. `local`) | @@ -75,7 +76,7 @@ vp = "local" # "local" | "global" | ["local", "global"] comment = "What this proves." # rendered into the snapshot cwd = "packages/app" # optional, relative to the fixture root skip-platforms = ["windows"] # or { os = "linux", libc = "musl" } -requires = ["nu"] # ignore when an optional runner tool is absent +requires = ["fish"] # "fish" | "nu"; ignore when the tool is absent ignore = false # true: only runs with `-- --ignored` seed-runtime = true # false: start from an empty VP_HOME link-node-modules = false # true: expose the run-root node_modules as @@ -118,7 +119,7 @@ A step is a bare argv array or a table: interactions = [ ... ] } ``` -`argv[0]` may be `vpt`, a runner-provisioned tool such as `nu`, or any +`argv[0]` may be `vpt`, a runner-provisioned tool such as `fish` or `nu`, or any executable exposed by the case's Vite+ installation, including default shims such as `vp`, `node`, and `corepack` and globally installed package binaries. There is no shell: no `&&`, no @@ -185,9 +186,10 @@ case-owned tool dirs, then a system tail for child processes and direct `git` st `TERM=xterm-256color`, `VP_CLI_TEST=1`, `VP_EMIT_MILESTONES=1`, a fresh `HOME`, `VP_HOME`, and npm prefix. The runner still rejects direct step tools that resolve outside the case-owned dirs, except for `git`; `vpt` is the only -required runner helper on PATH, while optional tools such as `nu` are linked -there when available. `CI` and `NO_COLOR` are deliberately NOT set: with a PTY -attached, the CLI behaves interactively by default, which is the point. +required runner helper on PATH, while optional tools such as `fish` and `nu` +are linked there when available. `CI` and `NO_COLOR` are deliberately NOT +set: with a PTY attached, the CLI behaves interactively by default, which is +the point. `seed-runtime = true` (default) symlinks a provisioned managed Node runtime into the case `VP_HOME` so commands do not download ~50MB per case. diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/assert_setup.fish b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/assert_setup.fish new file mode 100644 index 0000000000..3f4b8d9418 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/assert_setup.fish @@ -0,0 +1,45 @@ +source env.fish +or begin + echo "failed to source env.fish" + exit 1 +end + +if test "$VP_HOME" != "$EXPECTED_VP_HOME" + echo "VP_HOME mismatch: expected $EXPECTED_VP_HOME, got $VP_HOME" + exit 1 +end + +set -l expected_bin "$EXPECTED_VP_HOME/bin" +if test "$PATH[1]" != "$expected_bin" + echo "PATH mismatch: expected first entry $expected_bin, got $PATH[1]" + exit 1 +end + +set -l bin_count 0 +for entry in $PATH + if test "$entry" = "$expected_bin" + set bin_count (math $bin_count + 1) + end +end +if test $bin_count -ne 1 + echo "PATH contains the Vite+ bin directory $bin_count times" + exit 1 +end + +functions -q vp +or begin + echo "env.fish did not define the vp wrapper" + exit 1 +end + +set -l vp_output (vp --version) +or begin + echo "vp --version failed through the Fish wrapper" + exit 1 +end +if test (count $vp_output) -eq 0 + echo "vp --version returned no output" + exit 1 +end + +echo "Fish environment setup checks passed" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/assert_use.fish b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/assert_use.fish new file mode 100644 index 0000000000..39f65817d0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/assert_use.fish @@ -0,0 +1,65 @@ +source env.fish +or begin + echo "failed to source env.fish" + exit 1 +end + +set -gx VP_NODE_VERSION 18.20.0 +vp env use --help >/dev/null +or begin + echo "vp env use --help failed through the Fish wrapper" + exit 1 +end +if test "$VP_NODE_VERSION" != "18.20.0" + echo "vp env use --help changed VP_NODE_VERSION" + exit 1 +end + +vp env use 20.18.0 --no-install +or begin + echo "vp env use failed through the Fish wrapper" + exit 1 +end +if not set -q VP_NODE_VERSION + echo "vp env use did not set VP_NODE_VERSION" + exit 1 +end +if test "$VP_NODE_VERSION" != "20.18.0" + echo "VP_NODE_VERSION mismatch: expected 20.18.0, got $VP_NODE_VERSION" + exit 1 +end + +vp env use --unset +or begin + echo "vp env use --unset failed through the Fish wrapper" + exit 1 +end +if set -q VP_NODE_VERSION + echo "vp env use --unset did not remove VP_NODE_VERSION" + exit 1 +end + +vp env use --no-install +or begin + echo "vp env use without a version failed through the Fish wrapper" + exit 1 +end +if not set -q VP_NODE_VERSION + echo "vp env use without a version did not set VP_NODE_VERSION" + exit 1 +end +if test "$VP_NODE_VERSION" != "22.18.0" + echo "file-based VP_NODE_VERSION mismatch: expected 22.18.0, got $VP_NODE_VERSION" + exit 1 +end + +if vp env use --invalid-option >/dev/null 2>&1 + echo "vp env use did not preserve a failing command status" + exit 1 +end +if test "$VP_NODE_VERSION" != "22.18.0" + echo "failing vp env use changed VP_NODE_VERSION: $VP_NODE_VERSION" + exit 1 +end + +echo "Fish environment use checks passed" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots.toml new file mode 100644 index 0000000000..644e35176a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots.toml @@ -0,0 +1,22 @@ +[[case]] +name = "command_env_fish_setup" +vp = "global" +skip-platforms = ["windows"] +requires = ["fish"] +steps = [ + { argv = ["vp", "env", "setup", "--refresh"], envs = [["VP_HOME", '${workspace}/vp "$project\with spaces"']], snapshot = false }, + { argv = ["vpt", "cp", "assert_setup.fish", 'vp "$project\with spaces"/assert_setup.fish'], snapshot = false }, + { argv = ["fish", "--no-config", "assert_setup.fish"], cwd = 'vp "$project\with spaces"', comment = "loads the generated env.fish and verifies Fish path setup and command passthrough", envs = [["EXPECTED_VP_HOME", "${workspace}"], ["PATH", "${workspace}/bin:${workspace}/bin:${PATH}"]] }, +] + +[[case]] +name = "command_env_fish_use" +vp = "global" +skip-platforms = ["windows"] +requires = ["fish"] +steps = [ + { argv = ["vp", "env", "setup", "--refresh"], envs = [["VP_HOME", '${workspace}/vp "$project\with spaces"']], snapshot = false }, + { argv = ["vpt", "cp", "assert_use.fish", 'vp "$project\with spaces"/assert_use.fish'], snapshot = false }, + { argv = ["vpt", "write-file", 'vp "$project\with spaces"/.node-version', "22.18.0"], snapshot = false }, + { argv = ["fish", "--no-config", "assert_use.fish"], cwd = 'vp "$project\with spaces"', comment = "verifies the Fish wrapper help, explicit use, unset, file-based use, and failure branches", envs = [["PATH", "${workspace}/bin:${PATH}"]] }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots/command_env_fish_setup.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots/command_env_fish_setup.md new file mode 100644 index 0000000000..ddd2f26791 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots/command_env_fish_setup.md @@ -0,0 +1,15 @@ +# command_env_fish_setup + +## `VP_HOME=${workspace}/vp "$project\with spaces" vp env setup --refresh` + + +## `vpt cp assert_setup.fish 'vp "$project\with spaces"/assert_setup.fish'` + + +## `cd 'vp "$project\with spaces"' && EXPECTED_VP_HOME=${workspace} PATH=${workspace}/bin:${workspace}/bin:${PATH} fish --no-config assert_setup.fish` + +loads the generated env.fish and verifies Fish path setup and command passthrough + +``` +Fish environment setup checks passed +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots/command_env_fish_use.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots/command_env_fish_use.md new file mode 100644 index 0000000000..5e2ae74cfc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_fish/snapshots/command_env_fish_use.md @@ -0,0 +1,24 @@ +# command_env_fish_use + +## `VP_HOME=${workspace}/vp "$project\with spaces" vp env setup --refresh` + + +## `vpt cp assert_use.fish 'vp "$project\with spaces"/assert_use.fish'` + + +## `vpt write-file 'vp "$project\with spaces"/.node-version' 22.18.0` + + +## `cd 'vp "$project\with spaces"' && PATH=${workspace}/bin:${PATH} fish --no-config assert_use.fish` + +verifies the Fish wrapper help, explicit use, unset, file-based use, and failure branches + +``` +Using Node.js (resolved from 20.18.0) +Reverted to file-based Node.js version resolution +Using Node.js (resolved from .node-version) +error: Unexpected argument '--invalid-option' + +Use `-- --invalid-option` to pass the argument as a value +Fish environment use checks passed +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/flavor.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/flavor.rs index 9f5f8455f4..c0ffe88ad6 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/flavor.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/flavor.rs @@ -6,7 +6,7 @@ //! //! Each flavor gets one runner bin directory per run (created under the run //! temp root) for runner-owned helpers. `vpt` always lives there; optional -//! external tools such as Nushell are linked there when available. +//! external tools such as Fish and Nushell are linked there when available. use std::path::{Path, PathBuf}; @@ -30,6 +30,9 @@ impl Flavor { pub struct FlavorRuntime { pub runner_bin_dir: PathBuf, pub vpt: PathBuf, + /// Runner-owned Fish binary used by fixtures that execute generated + /// `env.fish` files. CI supplies it through `VP_SNAP_FISH_BIN`. + pub fish: Option, /// Runner-owned Nushell binary used by fixtures that execute generated /// `env.nu` files. CI supplies it through `VP_SNAP_NU_BIN`. pub nu: Option, @@ -193,21 +196,32 @@ fn vpt_path() -> Result { }) } -/// Resolves an optional Nushell binary for fixtures that exercise generated -/// `env.nu` files. The explicit override keeps CI deterministic; a developer's -/// PATH is the local fallback. -pub fn nushell_path() -> Result, String> { - if let Some(nu) = std::env::var_os("VP_SNAP_NU_BIN") { - let nu = PathBuf::from(nu); - if nu.is_file() { - return std::fs::canonicalize(&nu).map(Some).map_err(|e| { - format!("failed to canonicalize VP_SNAP_NU_BIN {}: {e}", nu.display()) - }); +/// Resolves an optional external tool. The explicit override keeps CI +/// deterministic; a developer's PATH is the local fallback. +fn optional_tool_path(env_var: &str, binary: &str) -> Result, String> { + if let Some(tool) = std::env::var_os(env_var) { + let tool = PathBuf::from(tool); + if tool.is_file() { + return std::fs::canonicalize(&tool) + .map(Some) + .map_err(|e| format!("failed to canonicalize {env_var} {}: {e}", tool.display())); } - return Err(format!("VP_SNAP_NU_BIN is set but {} does not exist", nu.display())); + return Err(format!("{env_var} is set but {} does not exist", tool.display())); } - Ok(which::which("nu").ok()) + Ok(which::which(binary).ok()) +} + +/// Resolves an optional Fish binary for fixtures that exercise generated +/// `env.fish` files. +pub fn fish_path() -> Result, String> { + optional_tool_path("VP_SNAP_FISH_BIN", "fish") +} + +/// Resolves an optional Nushell binary for fixtures that exercise generated +/// `env.nu` files. +pub fn nushell_path() -> Result, String> { + optional_tool_path("VP_SNAP_NU_BIN", "nu") } /// Home-layout names, shared with `CaseHome` in main.rs so the product's @@ -307,6 +321,8 @@ pub fn provision(flavor: Flavor, run_root: &Path) -> Result Result local_cli_package_dir()?, Flavor::Global => repo_root().join("packages/cli"), }; - Ok(FlavorRuntime { runner_bin_dir, vpt, nu, global_vp, cli_package_dir }) + Ok(FlavorRuntime { runner_bin_dir, vpt, fish, nu, global_vp, cli_package_dir }) } diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs b/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs index d3c422cf87..ab22a7da87 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/main.rs @@ -319,6 +319,7 @@ impl PlatformFilter { #[derive(Clone, Copy, serde::Deserialize, Debug)] #[serde(rename_all = "lowercase")] enum RequiredTool { + Fish, Nu, } @@ -327,6 +328,7 @@ impl RequiredTool { /// reports that error instead of silently hiding a bad override. fn is_missing(self) -> bool { match self { + Self::Fish => matches!(flavor::fish_path(), Ok(None)), Self::Nu => matches!(flavor::nushell_path(), Ok(None)), } } @@ -457,6 +459,7 @@ struct CaseInstall { path_env: OsString, tool_dirs: Vec, vpt: PathBuf, + fish: Option, nu: Option, } @@ -472,6 +475,12 @@ impl CaseInstall { if program == "vpt" { return Ok(self.vpt.clone()); } + if program == "fish" { + return self.fish.clone().ok_or_else(|| { + "`fish` is required by this snapshot case; install Fish or set VP_SNAP_FISH_BIN" + .to_owned() + }); + } if program == "nu" { return self.nu.clone().ok_or_else(|| { "`nu` is required by this snapshot case; install Nushell or set VP_SNAP_NU_BIN" @@ -573,6 +582,7 @@ impl CaseHome { path_env: compose_path_env(&path_dirs), tool_dirs, vpt: runtime.vpt.clone(), + fish: runtime.fish.clone(), nu: runtime.nu.clone(), }) } diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 7acb8a8bc1..6c9753693b 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -573,8 +573,9 @@ fi "#; const ENV_TEMPLATE_FISH: &str = r#"# Vite+ environment setup (https://viteplus.dev) -__ENV_EXPORTS__set -l __vp_idx (contains -i -- "__VP_BIN__" $PATH) -and set -e PATH[$__vp_idx] +__ENV_EXPORTS__while set -l __vp_idx (contains -i -- "__VP_BIN__" $PATH) + set -e PATH[$__vp_idx] +end set -gx PATH "__VP_BIN__" $PATH # Shell function wrapper: intercepts `vp env use` to eval its stdout, @@ -587,7 +588,10 @@ function vp set -lx VP_ENV_USE_EVAL_ENABLE 1 set -lx VP_SHELL fish set -l __vp_out (command vp $argv); or return $status - eval (string join ';' $__vp_out) + for __vp_command in $__vp_out + eval $__vp_command; or return $status + end + return 0 else command vp $argv end diff --git a/crates/vp_global_cli/src/commands/env/use.rs b/crates/vp_global_cli/src/commands/env/use.rs index 5f78efc5ca..405d322575 100644 --- a/crates/vp_global_cli/src/commands/env/use.rs +++ b/crates/vp_global_cli/src/commands/env/use.rs @@ -36,7 +36,10 @@ fn format_export(shell: &Shell, value: &str) -> String { fn format_unset(shell: &Shell) -> String { match shell { Shell::Posix => format!("unset {VERSION_ENV_VAR}"), - Shell::Fish => format!("set -e {VERSION_ENV_VAR}"), + // Fish returns a nonzero status when the variable is already absent. + // Keep the unset idempotent so wrappers can continue evaluating any + // following command, such as the project-file export from `vp env use`. + Shell::Fish => format!("set -e {VERSION_ENV_VAR}; or true"), Shell::PowerShell => { format!("Remove-Item Env:{VERSION_ENV_VAR} -ErrorAction SilentlyContinue") } @@ -268,7 +271,7 @@ mod tests { #[test] fn test_format_unset_fish() { let result = format_unset(&Shell::Fish); - assert_eq!(result, "set -e VP_NODE_VERSION"); + assert_eq!(result, "set -e VP_NODE_VERSION; or true"); } #[test]