Skip to content

Commit 39f0ef9

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/publish-support
2 parents 1f1fdb1 + d304770 commit 39f0ef9

89 files changed

Lines changed: 792 additions & 79 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ All user-facing output must go through shared output modules instead of raw prin
106106
- Run `cargo test` to execute all tests
107107
- You never need to run `pnpm install` in the test fixtures dir, vite-plus should able to load and parse the workspace without `pnpm install`.
108108

109+
### Environment Variables in Tests
110+
111+
- **Prefer `EnvConfig::test_scope()`**: For tests needing custom config values (VP_HOME, npm registry, etc.), use thread-local `EnvConfig::test_scope()` or `EnvConfig::test_guard()` from `vite_shared` — no `unsafe`, no `#[serial]`, full parallelism
112+
- **`#[serial]` required for `std::env::set_var`/`remove_var`**: Any test that directly modifies process env vars (PATH, VP_SHIM_TOOL, etc.) MUST have `#[serial_test::serial]` to prevent concurrent access races
113+
- **Clean up ALL related env vars**: When clearing env vars before a test, clear ALL vars that the function under test reads — not just the one being tested
114+
109115
## Build
110116

111117
- Run `pnpm bootstrap-cli` from the project root to build all packages and install the global CLI

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_global_cli/completion-register.bash

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,18 @@ _clap_complete_vp() {
5757
fi
5858
_clap_trim_completions
5959
}
60+
61+
_clap_complete_vpr() {
62+
local COMP_WORDS=("vp" "run" "${COMP_WORDS[@]:1}")
63+
local COMP_CWORD=$((COMP_CWORD + 1))
64+
local COMP_LINE="vp run ${COMP_LINE#vpr}"
65+
_clap_complete_vp
66+
}
67+
6068
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
6169
complete -o nospace -o bashdefault -o nosort -F _clap_complete_vp vp
70+
complete -o nospace -o bashdefault -o nosort -F _clap_complete_vpr vpr
6271
else
6372
complete -o nospace -o bashdefault -F _clap_complete_vp vp
73+
complete -o nospace -o bashdefault -F _clap_complete_vpr vpr
6474
fi

crates/vite_global_cli/src/cli.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,11 +1625,7 @@ fn should_force_global_delegate(command: &str, args: &[String]) -> bool {
16251625
/// Delegates to the local vite-plus CLI to run `vp run` without arguments,
16261626
/// which returns a list of available tasks in the format "task_name: description".
16271627
fn run_tasks_completions(current: &OsStr) -> Vec<clap_complete::CompletionCandidate> {
1628-
let Some(cwd) = std::env::current_dir()
1629-
.ok()
1630-
.and_then(AbsolutePathBuf::new)
1631-
.filter(|p| commands::has_vite_plus_dependency(p))
1632-
else {
1628+
let Ok(cwd) = vite_path::current_dir() else {
16331629
return vec![];
16341630
};
16351631

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,13 @@ if [ -n "$BASH_VERSION" ] && type complete >/dev/null 2>&1; then
449449
eval "$(VP_COMPLETE=bash command vp)"
450450
elif [ -n "$ZSH_VERSION" ] && type compdef >/dev/null 2>&1; then
451451
eval "$(VP_COMPLETE=zsh command vp)"
452+
_vpr_complete() {
453+
local -a orig=("${words[@]}")
454+
words=("vp" "run" "${orig[@]:1}")
455+
CURRENT=$((CURRENT + 1))
456+
${=_comps[vp]}
457+
}
458+
compdef _vpr_complete vpr
452459
fi
453460
"#
454461
.replace("__VP_BIN__", &bin_path_ref);
@@ -478,6 +485,13 @@ end
478485
479486
# Dynamic shell completion for fish
480487
VP_COMPLETE=fish command vp | source
488+
489+
function __vpr_complete
490+
set -l tokens (commandline --current-process --tokenize --cut-at-cursor)
491+
set -l current (commandline --current-token)
492+
VP_COMPLETE=fish command vp -- vp run $tokens[2..] $current
493+
end
494+
complete -c vpr --keep-order --exclusive --arguments "(__vpr_complete)"
481495
"#
482496
.replace("__VP_BIN__", &bin_path_ref);
483497
let env_fish_file = vite_plus_home.join("env.fish");
@@ -518,6 +532,27 @@ function vp {
518532
$env:VP_COMPLETE = "powershell"
519533
& (Join-Path $__vp_bin "vp.exe") | Out-String | Invoke-Expression
520534
Remove-Item Env:\VP_COMPLETE -ErrorAction SilentlyContinue
535+
536+
$__vpr_comp = {
537+
param($wordToComplete, $commandAst, $cursorPosition)
538+
$prev = $env:VP_COMPLETE
539+
$env:VP_COMPLETE = "powershell"
540+
$commandLine = $commandAst.Extent.Text
541+
$args = $commandLine.Substring(0, [math]::Min($cursorPosition, $commandLine.Length))
542+
$args = $args -replace '^(vpr\.exe|vpr)\b', 'vp run'
543+
if ($wordToComplete -eq "") { $args += " ''" }
544+
$results = Invoke-Expression @"
545+
& (Join-Path $__vp_bin 'vp.exe') -- $args
546+
"@;
547+
if ($prev) { $env:VP_COMPLETE = $prev } else { Remove-Item Env:\VP_COMPLETE }
548+
$results | ForEach-Object {
549+
$split = $_.Split("`t")
550+
$cmd = $split[0];
551+
if ($split.Length -eq 2) { $help = $split[1] } else { $help = $split[0] }
552+
[System.Management.Automation.CompletionResult]::new($cmd, $cmd, 'ParameterValue', $help)
553+
}
554+
}
555+
Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp
521556
"#;
522557

523558
// For PowerShell, use the actual absolute path (not $HOME-relative)
@@ -866,7 +901,6 @@ mod tests {
866901
let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap();
867902
let ps1_content = tokio::fs::read_to_string(home.join("env.ps1")).await.unwrap();
868903

869-
// Verify completion env is set
870904
assert!(
871905
env_content.contains("VP_COMPLETE=bash") && env_content.contains("VP_COMPLETE=zsh"),
872906
"env file should contain completion for bash and zsh"
@@ -879,5 +913,15 @@ mod tests {
879913
ps1_content.contains("VP_COMPLETE = \"powershell\""),
880914
"env.ps1 file should contain completion for PowerShell"
881915
);
916+
917+
assert!(
918+
env_content.contains("compdef _vpr_complete vpr"),
919+
"env should have vpr completion for zsh"
920+
);
921+
assert!(fish_content.contains("complete -c vpr"), "env.fish should have vpr completion");
922+
assert!(
923+
ps1_content.contains("Register-ArgumentCompleter -Native -CommandName vpr"),
924+
"env.ps1 should have vpr completion"
925+
);
882926
}
883927
}

crates/vite_global_cli/src/shim/mod.rs

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,18 @@ mod tests {
217217
assert!(!is_potential_package_binary("another-fake-tool"));
218218
}
219219

220+
/// Clear both shim env vars to isolate tests.
221+
/// SAFETY: caller must be `#[serial]` since this mutates process-global state.
222+
unsafe fn clear_shim_env_vars() {
223+
unsafe {
224+
std::env::remove_var(SHIM_TOOL_ENV_VAR);
225+
std::env::remove_var(LEGACY_SHIM_TOOL_ENV_VAR);
226+
}
227+
}
228+
220229
#[test]
221230
#[serial]
222231
fn test_detect_shim_tool_from_env_var() {
223-
// SAFETY: We're in a test at startup, no other threads
224232
unsafe {
225233
std::env::set_var(SHIM_TOOL_ENV_VAR, "node");
226234
std::env::remove_var(LEGACY_SHIM_TOOL_ENV_VAR);
@@ -236,7 +244,6 @@ mod tests {
236244
fn test_detect_shim_tool_from_legacy_env_var() {
237245
// When only VITE_PLUS_SHIM_TOOL is set (older trampoline), it should
238246
// fall back to reading the legacy env var.
239-
// SAFETY: We're in a test at startup, no other threads
240247
unsafe {
241248
std::env::remove_var(SHIM_TOOL_ENV_VAR);
242249
std::env::set_var(LEGACY_SHIM_TOOL_ENV_VAR, "npm");
@@ -247,43 +254,28 @@ mod tests {
247254
assert!(std::env::var(LEGACY_SHIM_TOOL_ENV_VAR).is_err());
248255
}
249256

257+
/// Tests that argv0-based tool detection works for a given tool name,
258+
/// including full path and .exe extension variants.
259+
fn assert_detect_shim_tool_from_argv0(tool: &str) {
260+
unsafe { clear_shim_env_vars() };
261+
262+
assert_eq!(detect_shim_tool(tool), Some(tool.to_string()));
263+
assert_eq!(
264+
detect_shim_tool(&vite_str::format!("/home/user/.vite-plus/bin/{tool}")),
265+
Some(tool.to_string()),
266+
);
267+
assert_eq!(detect_shim_tool(&vite_str::format!("{tool}.exe")), Some(tool.to_string()),);
268+
}
269+
250270
#[test]
251271
#[serial]
252272
fn test_detect_shim_tool_vpx() {
253-
// vpx should be detected via the argv0 check, before the env var check
254-
// and before is_shim_tool (which would incorrectly match it as a package binary)
255-
// SAFETY: We're in a test
256-
unsafe {
257-
std::env::remove_var(SHIM_TOOL_ENV_VAR);
258-
}
259-
let result = detect_shim_tool("vpx");
260-
assert_eq!(result, Some("vpx".to_string()));
261-
262-
// Also works with full path
263-
let result = detect_shim_tool("/home/user/.vite-plus/bin/vpx");
264-
assert_eq!(result, Some("vpx".to_string()));
265-
266-
// Also works with .exe extension (Windows)
267-
let result = detect_shim_tool("vpx.exe");
268-
assert_eq!(result, Some("vpx".to_string()));
273+
assert_detect_shim_tool_from_argv0("vpx");
269274
}
270275

271276
#[test]
277+
#[serial]
272278
fn test_detect_shim_tool_vpr() {
273-
// vpr should be detected via the argv0 check, same pattern as vpx
274-
// SAFETY: We're in a test
275-
unsafe {
276-
std::env::remove_var(SHIM_TOOL_ENV_VAR);
277-
}
278-
let result = detect_shim_tool("vpr");
279-
assert_eq!(result, Some("vpr".to_string()));
280-
281-
// Also works with full path
282-
let result = detect_shim_tool("/home/user/.vite-plus/bin/vpr");
283-
assert_eq!(result, Some("vpr".to_string()));
284-
285-
// Also works with .exe extension (Windows)
286-
let result = detect_shim_tool("vpr.exe");
287-
assert_eq!(result, Some("vpr".to_string()));
279+
assert_detect_shim_tool_from_argv0("vpr");
288280
}
289281
}

crates/vite_shared/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ which = { workspace = true }
2323
[target.'cfg(not(target_os = "windows"))'.dependencies]
2424
rustls = { workspace = true }
2525

26+
[dev-dependencies]
27+
serial_test = { workspace = true }
28+
2629
[lints]
2730
workspace = true

crates/vite_shared/src/home.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ mod tests {
6464
}
6565

6666
#[test]
67+
#[serial_test::serial]
6768
fn test_get_vite_plus_without_home() {
6869
use std::path::PathBuf;
6970

crates/vite_shared/src/path_env.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ mod tests {
142142
}
143143

144144
#[test]
145-
#[ignore]
145+
#[serial_test::serial]
146146
fn test_format_path_prepended_always_prepends() {
147147
// Even if the directory exists somewhere in PATH, it should be prepended
148148
let test_dir = "/test/node/bin";

packages/cli/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
ModuleKind,
3939
} from 'typescript';
4040

41-
import { generateLicenseFile } from '../../scripts/generate-license.ts';
41+
import { generateLicenseFile } from '../../scripts/generate-license.js';
4242
import corePkg from '../core/package.json' with { type: 'json' };
4343
import testPkg from '../test/package.json' with { type: 'json' };
4444

@@ -115,7 +115,7 @@ async function buildNapiBinding() {
115115
});
116116

117117
const outputs = await task;
118-
const viteConfig = await import('../../vite.config');
118+
const viteConfig = await import('../../vite.config.js');
119119
for (const output of outputs) {
120120
if (output.kind !== 'node') {
121121
const { code, errors } = await format(output.path, await readFile(output.path, 'utf8'), {

0 commit comments

Comments
 (0)