From 5c0f98a8eecfef18973f25d830023182389ab3ef Mon Sep 17 00:00:00 2001 From: Liang Date: Mon, 19 Oct 2026 22:43:03 +0800 Subject: [PATCH] refactor(env): share .nvmrc line parsing between reading and pinning --- crates/vp_global_cli/src/commands/env/pin.rs | 23 ++----- crates/vp_js_runtime/src/dev_engines.rs | 69 ++++++++++++++------ crates/vp_js_runtime/src/lib.rs | 4 +- 3 files changed, 57 insertions(+), 39 deletions(-) diff --git a/crates/vp_global_cli/src/commands/env/pin.rs b/crates/vp_global_cli/src/commands/env/pin.rs index f5998f7d14..d70eb1fa22 100644 --- a/crates/vp_global_cli/src/commands/env/pin.rs +++ b/crates/vp_global_cli/src/commands/env/pin.rs @@ -9,7 +9,7 @@ use std::{io::Write, process::ExitStatus}; -use vp_js_runtime::{NodeProvider, VersionSource, resolve_node_version}; +use vp_js_runtime::{NodeProvider, VersionSource, nvmrc_version_spans, resolve_node_version}; use vp_pm_cli::{ PackageManagerType, download_package_manager_for_cwd, resolve_package_manager_from_package_json, resolve_package_manager_version_for_cwd, @@ -445,24 +445,13 @@ async fn pin_node_version_file( Ok(true) } -/// Locate the version token using the same line rules as read_nvmrc_file. +/// Locate the version token so pinning replaces only it. /// Keep comments, reserved key/value lines, whitespace, and line endings intact. fn nvmrc_version_span(content: &str) -> Result>, Error> { - let mut span = None; - let mut offset = 0; - for line in content.split_inclusive('\n') { - let value = line.split_once('#').map_or(line, |(value, _)| value); - let trimmed = value.trim(); - if !trimmed.is_empty() && !trimmed.contains('=') { - if span.is_some() { - return Err(Error::Other( - "cannot pin .nvmrc with multiple version declarations".into(), - )); - } - let start = offset + value.len() - value.trim_start().len(); - span = Some(start..start + trimmed.len()); - } - offset += line.len(); + let mut spans = nvmrc_version_spans(content); + let span = spans.next(); + if spans.next().is_some() { + return Err(Error::Other("cannot pin .nvmrc with multiple version declarations".into())); } Ok(span) } diff --git a/crates/vp_js_runtime/src/dev_engines.rs b/crates/vp_js_runtime/src/dev_engines.rs index 418454d280..1956a687ed 100644 --- a/crates/vp_js_runtime/src/dev_engines.rs +++ b/crates/vp_js_runtime/src/dev_engines.rs @@ -5,6 +5,8 @@ //! //! For `PackageJson` types (devEngines, engines), see `vp_shared::package_json`. +use std::ops::Range; + // Re-export shared types for internal use pub use vp_shared::PackageJson; use vt_path::AbsolutePath; @@ -64,12 +66,32 @@ pub async fn read_node_version_file(project_path: &AbsolutePath) -> Option pub async fn read_nvmrc_file(project_path: &AbsolutePath) -> Option { let path = project_path.join(".nvmrc"); let content = tokio::fs::read_to_string(&path).await.ok()?; - let mut versions = content.lines().filter_map(|line| { - let value = line.split_once('#').map_or(line, |(value, _)| value).trim(); - (!value.is_empty() && !value.contains('=')).then_some(value) - }); - let version = versions.next()?; - if versions.next().is_some() { + parse_nvmrc_content(&content) +} + +/// Byte ranges of the version declarations in `.nvmrc` content. +/// +/// Blank lines, `#` comments, and reserved key/value lines are skipped. More +/// than one declaration is ambiguous, so callers reject it. +pub fn nvmrc_version_spans(content: &str) -> impl Iterator> + '_ { + let mut offset = 0; + content.split_inclusive('\n').filter_map(move |line| { + let line_start = offset; + offset += line.len(); + let value = line.split_once('#').map_or(line, |(value, _)| value); + let trimmed = value.trim(); + if trimmed.is_empty() || trimmed.contains('=') { + return None; + } + let start = line_start + value.len() - value.trim_start().len(); + Some(start..start + trimmed.len()) + }) +} + +fn parse_nvmrc_content(content: &str) -> Option { + let mut spans = nvmrc_version_spans(content); + let version = &content[spans.next()?]; + if spans.next().is_some() { return None; } @@ -148,23 +170,28 @@ mod tests { tokio::fs::write(temp_path.join(".nvmrc"), "v22.13.1\n").await.unwrap(); assert_eq!(read_nvmrc_file(&temp_path).await, Some("22.13.1".into())); + } - tokio::fs::write(temp_path.join(".nvmrc"), "node\n").await.unwrap(); - assert_eq!(read_nvmrc_file(&temp_path).await, Some("latest".into())); - - tokio::fs::write( - temp_path.join(".nvmrc"), - "# Use the project version\n\nv22.13.1 # pinned\nNVM_SYMLINK_CURRENT=true\n", - ) - .await - .unwrap(); - assert_eq!(read_nvmrc_file(&temp_path).await, Some("22.13.1".into())); - - tokio::fs::write(temp_path.join(".nvmrc"), "20\n22\n").await.unwrap(); - assert!(read_nvmrc_file(&temp_path).await.is_none()); + #[test] + fn test_parse_nvmrc_content() { + assert_eq!(parse_nvmrc_content("v22.13.1\n"), Some("22.13.1".into())); + assert_eq!(parse_nvmrc_content("node\n"), Some("latest".into())); + assert_eq!( + parse_nvmrc_content( + "# Use the project version\n\nv22.13.1 # pinned\nNVM_SYMLINK_CURRENT=true\n" + ), + Some("22.13.1".into()) + ); + assert!(parse_nvmrc_content("20\n22\n").is_none()); + assert!(parse_nvmrc_content("system\n").is_none()); + assert!(parse_nvmrc_content("# comment only\n").is_none()); + } - tokio::fs::write(temp_path.join(".nvmrc"), "system\n").await.unwrap(); - assert!(read_nvmrc_file(&temp_path).await.is_none()); + #[test] + fn test_nvmrc_version_spans() { + let content = "# Node\r\n v20.18.0 # keep\r\nkey=value\r\n\r\n22"; + let versions: Vec<_> = nvmrc_version_spans(content).map(|span| &content[span]).collect(); + assert_eq!(versions, ["v20.18.0", "22"]); } // ======================================================================== diff --git a/crates/vp_js_runtime/src/lib.rs b/crates/vp_js_runtime/src/lib.rs index fb899f64e5..f7f75d092d 100644 --- a/crates/vp_js_runtime/src/lib.rs +++ b/crates/vp_js_runtime/src/lib.rs @@ -53,7 +53,9 @@ mod provider; mod providers; mod runtime; -pub use dev_engines::{parse_node_version_content, read_node_version_file, read_nvmrc_file}; +pub use dev_engines::{ + nvmrc_version_spans, parse_node_version_content, read_node_version_file, read_nvmrc_file, +}; pub use error::Error; pub use platform::{Arch, Os, Platform}; pub use provider::{