Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ WORKSPACE_DIR = { value = "rolldown", relative = true }

[build]
rustflags = ["--cfg", "tokio_unstable"] # also update .github/workflows/ci.yml

# fix sqlite build error on linux
[target.'cfg(target_os = "linux")']
rustflags = ["--cfg", "tokio_unstable", "-C", "link-args=-Wl,--warn-unresolved-symbols"]

# Increase stack size on Windows to avoid stack overflow
Comment thread
fengmk2 marked this conversation as resolved.
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["--cfg", "tokio_unstable", "-C", "link-arg=/STACK:8388608"]
[target.'cfg(all(windows, target_env = "gnu"))']
rustflags = ["--cfg", "tokio_unstable", "-C", "link-arg=-Wl,--stack,8388608"]

[unstable]
bindeps = true

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ jobs:
# Test all crates/* packages. New crates are automatically included.
# Also test vite-plus-cli (lives outside crates/) to catch type sync issues.
- run: cargo test $(for d in crates/*/; do echo -n "-p $(basename $d) "; done) -p vite-plus-cli
env:
RUST_MIN_STACK: 8388608

lint:
needs: detect-changes
Expand Down Expand Up @@ -399,6 +401,8 @@ jobs:
git diff
exit 1
fi
env:
RUST_MIN_STACK: 8388608

# Upgrade tests (merged from separate job to avoid duplicate build)
- name: Test upgrade (bash)
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ brush-parser = "0.3.0"
blake3 = "1.8.2"
chrono = { version = "0.4", features = ["serde"] }
clap = "4.5.40"
clap_complete = "4.6.0"
commondir = "1.0.0"
cow-utils = "0.1.3"
criterion = { version = "0.7", features = ["html_reports"] }
Expand Down
1 change: 1 addition & 0 deletions crates/vite_global_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ path = "src/main.rs"
base64-simd = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }
directories = { workspace = true }
flate2 = { workspace = true }
serde = { workspace = true }
Expand Down
175 changes: 163 additions & 12 deletions crates/vite_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@

use std::process::ExitStatus;

use clap::CommandFactory;
use owo_colors::OwoColorize;

use super::config::{get_bin_dir, get_vite_plus_home};
use crate::{error::Error, help};
use crate::{cli::Args, error::Error, help};

/// Tools to create shims for (node, npm, npx, vpx)
const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx"];
Expand All @@ -40,6 +41,9 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
// Ensure home directory exists (env files are written here)
tokio::fs::create_dir_all(&vite_plus_home).await?;

// Generate completion scripts
generate_completion_scripts(&vite_plus_home).await?;

// Create env files with PATH guard (prevents duplicate PATH entries)
create_env_files(&vite_plus_home).await?;

Expand Down Expand Up @@ -270,6 +274,39 @@ async fn create_windows_shim(
Ok(())
}

/// Creates completion scripts in `~/.vite-plus/completion/`:
/// - `vp.bash` (bash)
/// - `_vp` (zsh, following zsh convention)
/// - `vp.fish` (fish shell)
/// - `vp.ps1` (PowerShell)
async fn generate_completion_scripts(
vite_plus_home: &vite_path::AbsolutePath,
) -> Result<(), Error> {
let mut cmd = Args::command();

// Create completion directory
let completion_dir = vite_plus_home.join("completion");
tokio::fs::create_dir_all(&completion_dir).await?;

// Generate shell completion scripts
let completions = [
(clap_complete::Shell::Bash, "vp.bash"),
(clap_complete::Shell::Zsh, "_vp"),
(clap_complete::Shell::Fish, "vp.fish"),
(clap_complete::Shell::PowerShell, "vp.ps1"),
];

for (shell, filename) in completions {
let path = completion_dir.join(filename);
let mut file = std::fs::File::create(&path)?;
clap_complete::generate(shell, &mut cmd, "vp", &mut file);
}

tracing::debug!("Generated completion scripts in {:?}", completion_dir);

Ok(())
}

/// Get the path to the trampoline template binary (vp-shim.exe).
///
/// The trampoline binary is distributed alongside vp.exe in the same directory.
Expand Down Expand Up @@ -378,23 +415,28 @@ pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vite_path::AbsolutePat
/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`)
async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> {
let bin_path = vite_plus_home.join("bin");
let completion_path = vite_plus_home.join("completion");

// Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env)
// This makes the env file portable across sessions where HOME may differ
let bin_path_ref = if let Some(home_dir) = vite_shared::EnvConfig::get().user_home {
if let Ok(suffix) = bin_path.as_path().strip_prefix(&home_dir) {
format!("$HOME/{}", suffix.display())
} else {
bin_path.as_path().display().to_string()
}
} else {
bin_path.as_path().display().to_string()
let home_dir = vite_shared::EnvConfig::get().user_home;
let to_ref = |path: &vite_path::AbsolutePath| -> String {
home_dir
.as_ref()
.and_then(|h| path.as_path().strip_prefix(h).ok())
.map(|s| {
// Normalize to forward slashes for $HOME/... paths (POSIX-style)
format!("$HOME/{}", s.display().to_string().replace('\\', "/"))
})
.unwrap_or_else(|| path.as_path().display().to_string())
};
let bin_path_ref = to_ref(&bin_path);

// POSIX env file (bash/zsh)
// When sourced multiple times, removes existing entry and re-prepends to front
// Uses parameter expansion to split PATH around the bin entry in O(1) operations
// Includes vp() shell function wrapper for `vp env use` (evals stdout)
// Includes shell completion support
let env_content = r#"#!/bin/sh
# Vite+ environment setup (https://viteplus.dev)
__vp_bin="__VP_BIN__"
Expand Down Expand Up @@ -425,8 +467,29 @@ vp() {
command vp "$@"
fi
}

# Shell completion for bash/zsh
# Source appropriate completion script based on current shell
# Only load completion in interactive shells with required builtins
if [ -n "$BASH_VERSION" ] && type complete >/dev/null 2>&1; then
# Bash shell with completion support
__vp_completion="__VP_COMPLETION_BASH__"
if [ -f "$__vp_completion" ]; then
. "$__vp_completion"
fi
unset __vp_completion
elif [ -n "$ZSH_VERSION" ] && type compdef >/dev/null 2>&1; then
# Zsh shell with completion support
__vp_completion="__VP_COMPLETION_ZSH__"
if [ -f "$__vp_completion" ]; then
. "$__vp_completion"
Comment thread
fengmk2 marked this conversation as resolved.
Comment thread
nekomoyi marked this conversation as resolved.
fi
unset __vp_completion
fi
"#
.replace("__VP_BIN__", &bin_path_ref);
.replace("__VP_BIN__", &bin_path_ref)
.replace("__VP_COMPLETION_BASH__", &to_ref(&completion_path.join("vp.bash")))
.replace("__VP_COMPLETION_ZSH__", &to_ref(&completion_path.join("_vp")));
let env_file = vite_plus_home.join("env");
tokio::fs::write(&env_file, env_content).await?;

Expand All @@ -450,8 +513,15 @@ function vp
command vp $argv
end
end

# Shell completion for fish
set -l __vp_completion "__VP_COMPLETION_FISH__"
if test -f "$__vp_completion"
source "$__vp_completion"
Comment thread
fengmk2 marked this conversation as resolved.
Outdated
Comment thread
nekomoyi marked this conversation as resolved.
Outdated
end
"#
.replace("__VP_BIN__", &bin_path_ref);
.replace("__VP_BIN__", &bin_path_ref)
.replace("__VP_COMPLETION_FISH__", &to_ref(&completion_path.join("vp.fish")));
let env_fish_file = vite_plus_home.join("env.fish");
tokio::fs::write(&env_fish_file, env_fish_content).await?;

Expand Down Expand Up @@ -485,11 +555,20 @@ function vp {
& (Join-Path $__vp_bin "vp.exe") @args
}
}

# Shell completion for PowerShell
$__vp_completion = "__VP_COMPLETION_PS1__"
if (Test-Path $__vp_completion) {
. $__vp_completion
}
"#;

// For PowerShell, use the actual absolute path (not $HOME-relative)
let bin_path_win = bin_path.as_path().display().to_string();
let env_ps1_content = env_ps1_content.replace("__VP_BIN_WIN__", &bin_path_win);
let completion_ps1_win = completion_path.join("vp.ps1").as_path().display().to_string();
let env_ps1_content = env_ps1_content
.replace("__VP_BIN_WIN__", &bin_path_win)
.replace("__VP_COMPLETION_PS1__", &completion_ps1_win);
let env_ps1_file = vite_plus_home.join("env.ps1");
tokio::fs::write(&env_ps1_file, env_ps1_content).await?;

Expand Down Expand Up @@ -820,4 +899,76 @@ mod tests {
assert!(fresh_home.join("env.fish").exists(), "env.fish file should be created");
assert!(fresh_home.join("env.ps1").exists(), "env.ps1 file should be created");
}

#[tokio::test]
async fn test_generate_completion_scripts_creates_all_files() {
let temp_dir = TempDir::new().unwrap();
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();

generate_completion_scripts(&home).await.unwrap();

let completion_dir = home.join("completion");

// Verify all completion scripts are created
let bash_completion = completion_dir.join("vp.bash");
let zsh_completion = completion_dir.join("_vp");
let fish_completion = completion_dir.join("vp.fish");
let ps1_completion = completion_dir.join("vp.ps1");

assert!(bash_completion.as_path().exists(), "bash completion (vp.bash) should be created");
assert!(zsh_completion.as_path().exists(), "zsh completion (_vp) should be created");
assert!(fish_completion.as_path().exists(), "fish completion (vp.fish) should be created");
assert!(
ps1_completion.as_path().exists(),
"PowerShell completion (vp.ps1) should be created"
);
}

#[tokio::test]
async fn test_create_env_files_contains_completion() {
let temp_dir = TempDir::new().unwrap();
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
let _guard = home_guard(temp_dir.path());

create_env_files(&home).await.unwrap();

let env_content = tokio::fs::read_to_string(home.join("env")).await.unwrap();
let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap();
let ps1_content = tokio::fs::read_to_string(home.join("env.ps1")).await.unwrap();

assert!(
env_content.contains("Shell completion")
&& env_content.contains("/completion/vp.bash\""),
"env file should contain bash completion"
);
assert!(
fish_content.contains("Shell completion")
&& fish_content.contains("/completion/vp.fish\""),
"env.fish file should contain fish completion"
);
assert!(
ps1_content.contains("Shell completion")
&& ps1_content.contains(&format!(
"{}completion{}vp.ps1\"",
std::path::MAIN_SEPARATOR_STR,
std::path::MAIN_SEPARATOR_STR
)),
"env.ps1 file should contain PowerShell completion"
);

// Verify placeholders are replaced
assert!(
!env_content.contains("__VP_COMPLETION_BASH__")
&& !env_content.contains("__VP_COMPLETION_ZSH__"),
"env file should not contain __VP_COMPLETION_* placeholders"
);
assert!(
!fish_content.contains("__VP_COMPLETION_FISH__"),
"env.fish file should not contain __VP_COMPLETION_FISH__ placeholder"
);
assert!(
!ps1_content.contains("__VP_COMPLETION_PS1__"),
"env.ps1 file should not contain __VP_COMPLETION_PS1__ placeholder"
);
}
}
Loading