@@ -145,15 +145,34 @@ pub async fn save_config(config: &Config) -> Result<(), Error> {
145145 Ok ( ( ) )
146146}
147147
148+ /// Environment variable for per-shell session Node.js version override.
149+ /// Set by `vp env use` command.
150+ pub const VERSION_ENV_VAR : & str = "VITE_PLUS_NODE_VERSION" ;
151+
148152/// Resolve Node.js version for a directory.
149153///
150154/// Resolution order:
155+ /// 0. `VITE_PLUS_NODE_VERSION` env var (session override from `vp env use`)
151156/// 1. `.node-version` file in current or parent directories
152157/// 2. `package.json#engines.node` in current or parent directories
153158/// 3. `package.json#devEngines.runtime` in current or parent directories
154159/// 4. User default from config.json
155160/// 5. Latest LTS version
156161pub async fn resolve_version ( cwd : & AbsolutePath ) -> Result < VersionResolution , Error > {
162+ // Session override via environment variable (set by `vp env use`)
163+ if let Ok ( env_version) = std:: env:: var ( VERSION_ENV_VAR ) {
164+ let env_version = env_version. trim ( ) ;
165+ if !env_version. is_empty ( ) {
166+ return Ok ( VersionResolution {
167+ version : env_version. to_string ( ) ,
168+ source : VERSION_ENV_VAR . into ( ) ,
169+ source_path : None ,
170+ project_root : None ,
171+ is_range : false ,
172+ } ) ;
173+ }
174+ }
175+
157176 let provider = NodeProvider :: new ( ) ;
158177
159178 // Use shared version resolution with directory walking
@@ -291,11 +310,13 @@ async fn resolve_version_string(version: &str, provider: &NodeProvider) -> Resul
291310}
292311
293312/// Resolve version alias (lts, latest) to an exact version.
313+ ///
314+ /// Wraps resolution errors with a user-friendly message showing valid examples.
294315pub async fn resolve_version_alias (
295316 version : & str ,
296317 provider : & NodeProvider ,
297318) -> Result < String , Error > {
298- match version. to_lowercase ( ) . as_str ( ) {
319+ let result = match version. to_lowercase ( ) . as_str ( ) {
299320 "lts" => {
300321 let resolved = provider. resolve_latest_version ( ) . await ?;
301322 Ok ( resolved. to_string ( ) )
@@ -306,7 +327,24 @@ pub async fn resolve_version_alias(
306327 Ok ( resolved. to_string ( ) )
307328 }
308329 _ => resolve_version_string ( version, provider) . await ,
309- }
330+ } ;
331+ result. map_err ( |e| match e {
332+ Error :: RuntimeDownload (
333+ vite_js_runtime:: Error :: SemverRange ( _)
334+ | vite_js_runtime:: Error :: NoMatchingVersion { .. } ,
335+ ) => Error :: Other (
336+ format ! (
337+ "Invalid Node.js version: \" {version}\" \n \n \
338+ Valid examples:\n \
339+ vp env use 20 # Latest Node.js 20.x\n \
340+ vp env use 20.18.0 # Exact version\n \
341+ vp env use lts # Latest LTS version\n \
342+ vp env use latest # Latest version"
343+ )
344+ . into ( ) ,
345+ ) ,
346+ other => other,
347+ } )
310348}
311349
312350#[ cfg( test) ]
@@ -723,4 +761,83 @@ mod tests {
723761 resolution. version
724762 ) ;
725763 }
764+
765+ #[ tokio:: test]
766+ #[ serial]
767+ async fn test_resolve_version_env_var_takes_priority ( ) {
768+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
769+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
770+
771+ // Create .node-version file
772+ tokio:: fs:: write ( temp_path. join ( ".node-version" ) , "20.18.0\n " ) . await . unwrap ( ) ;
773+
774+ // SAFETY: This test runs in isolation with serial_test
775+ unsafe {
776+ std:: env:: set_var ( VERSION_ENV_VAR , "22.0.0" ) ;
777+ }
778+
779+ let resolution = resolve_version ( & temp_path) . await . unwrap ( ) ;
780+
781+ // VITE_PLUS_NODE_VERSION should take priority over .node-version
782+ assert_eq ! ( resolution. version, "22.0.0" ) ;
783+ assert_eq ! ( resolution. source, VERSION_ENV_VAR ) ;
784+ assert ! ( resolution. source_path. is_none( ) ) ;
785+ assert ! ( !resolution. is_range) ;
786+
787+ unsafe {
788+ std:: env:: remove_var ( VERSION_ENV_VAR ) ;
789+ }
790+ }
791+
792+ #[ tokio:: test]
793+ #[ serial]
794+ async fn test_resolve_version_empty_env_var_is_ignored ( ) {
795+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
796+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
797+
798+ // Create .node-version file
799+ tokio:: fs:: write ( temp_path. join ( ".node-version" ) , "20.18.0\n " ) . await . unwrap ( ) ;
800+
801+ // Set empty env var - should be ignored
802+ // SAFETY: This test runs in isolation with serial_test
803+ unsafe {
804+ std:: env:: set_var ( VERSION_ENV_VAR , "" ) ;
805+ }
806+
807+ let resolution = resolve_version ( & temp_path) . await . unwrap ( ) ;
808+
809+ // Empty env var should be ignored, should fall through to .node-version
810+ assert_eq ! ( resolution. version, "20.18.0" ) ;
811+ assert_eq ! ( resolution. source, ".node-version" ) ;
812+
813+ unsafe {
814+ std:: env:: remove_var ( VERSION_ENV_VAR ) ;
815+ }
816+ }
817+
818+ #[ tokio:: test]
819+ #[ serial]
820+ async fn test_resolve_version_whitespace_env_var_is_ignored ( ) {
821+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
822+ let temp_path = AbsolutePathBuf :: new ( temp_dir. path ( ) . to_path_buf ( ) ) . unwrap ( ) ;
823+
824+ // Create .node-version file
825+ tokio:: fs:: write ( temp_path. join ( ".node-version" ) , "20.18.0\n " ) . await . unwrap ( ) ;
826+
827+ // Set whitespace-only env var - should be ignored
828+ // SAFETY: This test runs in isolation with serial_test
829+ unsafe {
830+ std:: env:: set_var ( VERSION_ENV_VAR , " " ) ;
831+ }
832+
833+ let resolution = resolve_version ( & temp_path) . await . unwrap ( ) ;
834+
835+ // Whitespace env var should be ignored, should fall through to .node-version
836+ assert_eq ! ( resolution. version, "20.18.0" ) ;
837+ assert_eq ! ( resolution. source, ".node-version" ) ;
838+
839+ unsafe {
840+ std:: env:: remove_var ( VERSION_ENV_VAR ) ;
841+ }
842+ }
726843}
0 commit comments