Skip to content

Commit 0d33abb

Browse files
authored
Merge branch 'main' into rfc/vp-hooks
2 parents 173a85d + 8300a50 commit 0d33abb

File tree

12 files changed

+681
-667
lines changed

12 files changed

+681
-667
lines changed

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ async fn cleanup_legacy_completion_dir(vite_plus_home: &vite_path::AbsolutePath)
388388
/// Creates:
389389
/// - `~/.vite-plus/env` (POSIX shell — bash/zsh) with `vp()` wrapper function
390390
/// - `~/.vite-plus/env.fish` (fish shell) with `vp` wrapper function
391+
/// - `~/.vite-plus/env.nu` (Nushell) with `vp env use` wrapper function
391392
/// - `~/.vite-plus/env.ps1` (PowerShell) with PATH setup + `vp` function
392393
/// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`)
393394
async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> {
@@ -407,6 +408,9 @@ async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<()
407408
.unwrap_or_else(|| path.as_path().display().to_string())
408409
};
409410
let bin_path_ref = to_ref(&bin_path);
411+
// Nushell requires `~` instead of `$HOME` in string literals — `$HOME` is not expanded
412+
// at parse time, so PATH entries would contain a literal "$HOME/..." segment.
413+
let bin_path_ref_nu = bin_path_ref.replace("$HOME/", "~/");
410414

411415
// POSIX env file (bash/zsh)
412416
// When sourced multiple times, removes existing entry and re-prepends to front
@@ -499,6 +503,72 @@ complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)"
499503
let env_fish_file = vite_plus_home.join("env.fish");
500504
tokio::fs::write(&env_fish_file, env_fish_content).await?;
501505

506+
// Nushell env file with vp wrapper function.
507+
// Completions delegate to Fish dynamically (VP_COMPLETE=fish) because clap_complete_nushell
508+
// generates multiple rest params (e.g. for `vp install`), which Nushell does not support.
509+
let env_nu_content = r#"# Vite+ environment setup (https://viteplus.dev)
510+
$env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__")
511+
512+
# Shell function wrapper: intercepts `vp env use` to parse its stdout,
513+
# which sets/unsets VP_NODE_VERSION in the current shell session.
514+
def --env --wrapped vp [...args: string@"nu-complete vp"] {
515+
if ($args | length) >= 2 and $args.0 == "env" and $args.1 == "use" {
516+
if ("-h" in $args) or ("--help" in $args) {
517+
^vp ...$args
518+
return
519+
}
520+
let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL_NU: "1" } {
521+
^vp ...$args
522+
})
523+
let lines = ($out | lines)
524+
let exports = ($lines | where { $in =~ '^\$env\.' } | parse '$env.{key} = "{value}"')
525+
let export_keys = ($exports | get key? | default [])
526+
# Exclude keys that also appear in exports: when vp emits `hide-env X` then
527+
# `$env.X = "v"` (e.g. `vp env use` with no args resolving from .node-version),
528+
# the set should win.
529+
let unsets = ($lines | where { $in =~ '^hide-env ' } | parse 'hide-env {key}' | get key? | default [] | where { $in not-in $export_keys })
530+
if ($exports | is-not-empty) {
531+
load-env ($exports | reduce -f {} {|it, acc| $acc | insert $it.key $it.value})
532+
}
533+
for key in $unsets {
534+
if ($key in $env) { hide-env $key }
535+
}
536+
} else {
537+
^vp ...$args
538+
}
539+
}
540+
541+
# Shell completion for nushell (delegates to fish completions dynamically)
542+
def "nu-complete vp" [context: string] {
543+
let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($context)'"
544+
fish --command $fish_cmd | from tsv --flexible --noheaders --no-infer | rename value description | update value {|row|
545+
let value = $row.value
546+
let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | any {$in in $value}
547+
if ($need_quote and ($value | path exists)) {
548+
let expanded_path = if ($value starts-with ~) {$value | path expand --no-symlink} else {$value}
549+
$'"($expanded_path | str replace --all "\"" "\\\"")"'
550+
} else {$value}
551+
}
552+
}
553+
# Completion logic for vpr (translates context to 'vp run ...')
554+
def "nu-complete vpr" [context: string] {
555+
let modified_context = ($context | str replace -r '^vpr' 'vp run')
556+
let fish_cmd = $"VP_COMPLETE=fish command vp | source; complete '--do-complete=($modified_context)'"
557+
fish --command $fish_cmd | from tsv --flexible --noheaders --no-infer | rename value description | update value {|row|
558+
let value = $row.value
559+
let need_quote = ['\' ',' '[' ']' '(' ')' ' ' '\t' "'" '"' "`"] | any {$in in $value}
560+
if ($need_quote and ($value | path exists)) {
561+
let expanded_path = if ($value starts-with ~) {$value | path expand --no-symlink} else {$value}
562+
$'"($expanded_path | str replace --all "\"" "\\\"")"'
563+
} else {$value}
564+
}
565+
}
566+
export extern "vpr" [...args: string@"nu-complete vpr"]
567+
"#
568+
.replace("__VP_BIN__", &bin_path_ref_nu);
569+
let env_nu_file = vite_plus_home.join("env.nu");
570+
tokio::fs::write(&env_nu_file, env_nu_content).await?;
571+
502572
// PowerShell env file
503573
let env_ps1_content = r#"# Vite+ environment setup (https://viteplus.dev)
504574
$__vp_bin = "__VP_BIN_WIN__"
@@ -582,14 +652,16 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) {
582652
.parent()
583653
.map(|p| p.as_path().display().to_string())
584654
.unwrap_or_else(|| bin_dir.as_path().display().to_string());
585-
let home_path = if let Ok(home_dir) = std::env::var("HOME") {
655+
let (home_path, nu_home_path) = if let Ok(home_dir) = std::env::var("HOME") {
586656
if let Some(suffix) = home_path.strip_prefix(&home_dir) {
587-
format!("$HOME{suffix}")
657+
// POSIX/Fish use $HOME; Nushell's `source` is a parse-time keyword
658+
// that cannot expand $HOME (a runtime env var), so use ~ instead.
659+
(format!("$HOME{suffix}"), format!("~{suffix}"))
588660
} else {
589-
home_path
661+
(home_path.clone(), home_path)
590662
}
591663
} else {
592-
home_path
664+
(home_path.clone(), home_path)
593665
};
594666

595667
println!("{}", help::render_heading("Next Steps"));
@@ -601,6 +673,10 @@ fn print_path_instructions(bin_dir: &vite_path::AbsolutePath) {
601673
println!();
602674
println!(" source \"{home_path}/env.fish\"");
603675
println!();
676+
println!(" For Nushell, add to ~/.config/nushell/config.nu:");
677+
println!();
678+
println!(" source \"{nu_home_path}/env.nu\"");
679+
println!();
604680
println!(" For PowerShell, add to your $PROFILE:");
605681
println!();
606682
println!(" . \"{home_path}/env.ps1\"");
@@ -654,12 +730,46 @@ mod tests {
654730

655731
let env_path = home.join("env");
656732
let env_fish_path = home.join("env.fish");
733+
let env_nu_path = home.join("env.nu");
657734
let env_ps1_path = home.join("env.ps1");
658735
assert!(env_path.as_path().exists(), "env file should be created");
659736
assert!(env_fish_path.as_path().exists(), "env.fish file should be created");
737+
assert!(env_nu_path.as_path().exists(), "env.nu file should be created");
660738
assert!(env_ps1_path.as_path().exists(), "env.ps1 file should be created");
661739
}
662740

741+
#[tokio::test]
742+
async fn test_create_env_files_nu_contains_path_guard() {
743+
let temp_dir = TempDir::new().unwrap();
744+
let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
745+
let _guard = home_guard(temp_dir.path());
746+
747+
create_env_files(&home).await.unwrap();
748+
749+
let nu_content = tokio::fs::read_to_string(home.join("env.nu")).await.unwrap();
750+
assert!(
751+
!nu_content.contains("__VP_BIN__"),
752+
"env.nu should not contain __VP_BIN__ placeholder"
753+
);
754+
assert!(
755+
nu_content.contains("~/bin"),
756+
"env.nu should reference ~/bin (not $HOME/bin — Nushell does not expand $HOME in string literals)"
757+
);
758+
assert!(
759+
nu_content.contains("VP_ENV_USE_EVAL_ENABLE"),
760+
"env.nu should set VP_ENV_USE_EVAL_ENABLE"
761+
);
762+
assert!(
763+
nu_content.contains("VP_COMPLETE=fish"),
764+
"env.nu should use dynamic Fish completion delegation"
765+
);
766+
assert!(
767+
nu_content.contains("VP_SHELL_NU"),
768+
"env.nu should use VP_SHELL_NU explicit marker instead of inherited NU_VERSION"
769+
);
770+
assert!(nu_content.contains("load-env"), "env.nu should use load-env to apply exports");
771+
}
772+
663773
#[tokio::test]
664774
async fn test_create_env_files_replaces_placeholder_with_home_relative_path() {
665775
let temp_dir = TempDir::new().unwrap();

crates/vite_global_cli/src/commands/env/use.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ enum Shell {
2525
PowerShell,
2626
/// Windows cmd.exe
2727
Cmd,
28+
/// Nushell
29+
NuShell,
2830
}
2931

3032
/// Detect the current shell from environment variables.
3133
fn detect_shell() -> Shell {
3234
let config = vite_shared::EnvConfig::get();
3335
if config.fish_version.is_some() {
3436
Shell::Fish
37+
} else if config.vp_shell_nu {
38+
Shell::NuShell
3539
} else if cfg!(windows) && config.ps_module_path.is_some() {
3640
Shell::PowerShell
3741
} else if cfg!(windows) {
@@ -48,6 +52,7 @@ fn format_export(shell: &Shell, value: &str) -> String {
4852
Shell::Fish => format!("set -gx {VERSION_ENV_VAR} {value}"),
4953
Shell::PowerShell => format!("$env:{VERSION_ENV_VAR} = \"{value}\""),
5054
Shell::Cmd => format!("set {VERSION_ENV_VAR}={value}"),
55+
Shell::NuShell => format!("$env.{VERSION_ENV_VAR} = \"{value}\""),
5156
}
5257
}
5358

@@ -60,6 +65,7 @@ fn format_unset(shell: &Shell) -> String {
6065
format!("Remove-Item Env:{VERSION_ENV_VAR} -ErrorAction SilentlyContinue")
6166
}
6267
Shell::Cmd => format!("set {VERSION_ENV_VAR}="),
68+
Shell::NuShell => format!("hide-env {VERSION_ENV_VAR}"),
6369
}
6470
}
6571

@@ -194,6 +200,18 @@ mod tests {
194200
assert!(matches!(shell, Shell::Fish));
195201
}
196202

203+
#[test]
204+
fn test_detect_shell_fish_and_nushell() {
205+
// Fish takes priority over Nu shell signal
206+
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
207+
fish_version: Some("3.7.0".into()),
208+
vp_shell_nu: true,
209+
..vite_shared::EnvConfig::for_test()
210+
});
211+
let shell = detect_shell();
212+
assert!(matches!(shell, Shell::Fish));
213+
}
214+
197215
#[test]
198216
fn test_detect_shell_posix_default() {
199217
// All shell detection fields None → defaults
@@ -205,6 +223,32 @@ mod tests {
205223
assert!(matches!(shell, Shell::Cmd));
206224
}
207225

226+
#[test]
227+
fn test_detect_shell_nushell() {
228+
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
229+
vp_shell_nu: true,
230+
..vite_shared::EnvConfig::for_test()
231+
});
232+
let shell = detect_shell();
233+
assert!(matches!(shell, Shell::NuShell));
234+
}
235+
236+
#[test]
237+
fn test_detect_shell_inherited_nu_version_is_posix() {
238+
// NU_VERSION alone (inherited from parent Nushell) must NOT trigger Nu detection.
239+
// Only the explicit VP_SHELL_NU marker set by env.nu wrapper counts.
240+
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
241+
nu_version: Some("0.111.0".into()),
242+
vp_shell_nu: false,
243+
..vite_shared::EnvConfig::for_test()
244+
});
245+
let shell = detect_shell();
246+
#[cfg(not(windows))]
247+
assert!(matches!(shell, Shell::Posix));
248+
#[cfg(windows)]
249+
let _ = shell;
250+
}
251+
208252
#[test]
209253
fn test_format_export_posix() {
210254
let result = format_export(&Shell::Posix, "20.18.0");
@@ -252,4 +296,15 @@ mod tests {
252296
let result = format_unset(&Shell::Cmd);
253297
assert_eq!(result, "set VP_NODE_VERSION=");
254298
}
299+
#[test]
300+
fn test_format_export_nushell() {
301+
let result = format_export(&Shell::NuShell, "20.18.0");
302+
assert_eq!(result, "$env.VP_NODE_VERSION = \"20.18.0\"");
303+
}
304+
305+
#[test]
306+
fn test_format_unset_nushell() {
307+
let result = format_unset(&Shell::NuShell);
308+
assert_eq!(result, "hide-env VP_NODE_VERSION");
309+
}
255310
}

crates/vite_shared/src/env_config.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,19 @@ pub struct EnvConfig {
122122
///
123123
/// Env: `PSModulePath`
124124
pub ps_module_path: Option<String>,
125+
126+
/// Nu shell version (indicates running under Nu shell).
127+
///
128+
/// Env: `NU_VERSION`
129+
pub nu_version: Option<String>,
130+
131+
/// Explicit Nu shell eval signal set by the `env.nu` wrapper.
132+
///
133+
/// Unlike `NU_VERSION`, this is not inherited by child processes — it is only
134+
/// present when the Nushell wrapper explicitly passes it via `with-env`.
135+
///
136+
/// Env: `VP_SHELL_NU`
137+
pub vp_shell_nu: bool,
125138
}
126139

127140
impl EnvConfig {
@@ -151,6 +164,8 @@ impl EnvConfig {
151164
.map(PathBuf::from),
152165
fish_version: std::env::var("FISH_VERSION").ok(),
153166
ps_module_path: std::env::var("PSModulePath").ok(),
167+
nu_version: std::env::var("NU_VERSION").ok(),
168+
vp_shell_nu: std::env::var(env_vars::VP_SHELL_NU).is_ok(),
154169
}
155170
}
156171

@@ -233,6 +248,8 @@ impl EnvConfig {
233248
user_home: None,
234249
fish_version: None,
235250
ps_module_path: None,
251+
nu_version: None,
252+
vp_shell_nu: false,
236253
}
237254
}
238255

crates/vite_shared/src/env_vars.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ pub const VP_DEBUG_SHIM: &str = "VP_DEBUG_SHIM";
3636
/// Enable eval mode for `vp env use`.
3737
pub const VP_ENV_USE_EVAL_ENABLE: &str = "VP_ENV_USE_EVAL_ENABLE";
3838

39+
/// Explicit signal set by the Nushell wrapper to indicate Nu shell eval context.
40+
///
41+
/// Unlike `NU_VERSION` (which is inherited by child processes), this is only set
42+
/// by the `with-env` block in `env.nu`, so it cannot cause false detection when
43+
/// bash/zsh is launched from a Nushell session.
44+
pub const VP_SHELL_NU: &str = "VP_SHELL_NU";
45+
3946
/// Filter for update task types.
4047
pub const VITE_UPDATE_TASK_TYPES: &str = "VITE_UPDATE_TASK_TYPES";
4148

justfile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,30 @@ _clean_dist:
1616
_clean_dist:
1717
Remove-Item -Path 'packages/*/dist' -Recurse -Force -ErrorAction SilentlyContinue
1818

19-
init: _clean_dist
19+
init: _clean_dist _fix_symlinks
2020
cargo binstall watchexec-cli cargo-insta typos-cli cargo-shear dprint taplo-cli -y
2121
node packages/tools/src/index.ts sync-remote
2222
pnpm install
2323
pnpm -C docs install
2424

25+
[unix]
26+
_fix_symlinks:
27+
#!/usr/bin/env bash
28+
if [ "$(git config --get core.symlinks)" != "true" ]; then \
29+
echo "Enabling core.symlinks and re-checking out symlinks..."; \
30+
git config core.symlinks true; \
31+
git ls-files -s | grep '^120000' | cut -f2 | while read -r f; do git checkout -- "$f"; done; \
32+
fi
33+
34+
[windows]
35+
_fix_symlinks:
36+
$symlinks = git config --get core.symlinks; \
37+
if ($symlinks -ne 'true') { \
38+
Write-Host 'Enabling core.symlinks and re-checking out symlinks...'; \
39+
git config core.symlinks true; \
40+
git ls-files -s | Where-Object { $_ -match '^120000' } | ForEach-Object { ($_ -split "`t", 2)[1] } | ForEach-Object { git checkout -- $_ }; \
41+
}
42+
2543
build:
2644
pnpm install
2745
pnpm --filter @rolldown/pluginutils build

packages/cli/binding/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ vite_static_config = { workspace = true }
3131
vite_str = { workspace = true }
3232
vite_task = { workspace = true }
3333
vite_workspace = { workspace = true }
34-
rolldown_binding = { workspace = true, optional = true }
34+
rolldown_binding = { workspace = true, optional = true, features = ["disable_panic_hook"] }
3535

3636
[build-dependencies]
3737
napi-build = { workspace = true }

0 commit comments

Comments
 (0)