Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ca4cb59
feat: support devEngines for Node.js runtime and package manager sele…
fengmk2 Jun 4, 2026
6faad5a
test: cover npm-install-checks checkDevEngines scenarios
fengmk2 Jun 4, 2026
d5f0ee2
fix: preserve existing devEngines.packageManager entries on auto-pin
fengmk2 Jun 5, 2026
755458b
fix: address code review findings for devEngines support
fengmk2 Jun 6, 2026
e3f4ba0
fix: make inherited devEngines.runtime pins visible to doctor and pin
fengmk2 Jun 6, 2026
ad3a2c2
docs: sync RFCs with implemented review-fix behavior
fengmk2 Jun 6, 2026
7d06fb8
refactor: consolidate devEngines helpers and trim dead code
fengmk2 Jun 6, 2026
1d03175
fix: share the completed-install check for cached package managers
fengmk2 Jun 6, 2026
e19229f
perf: only check Windows shims on Windows in install-complete check
fengmk2 Jun 6, 2026
5fc027a
refactor: tidy package manager install-complete checks
fengmk2 Jun 6, 2026
0c35cec
docs: narrow onFail claims to match the implemented subset
fengmk2 Jun 6, 2026
8ce926e
docs: trim redundant/too-deep prose in env and install guides
fengmk2 Jun 6, 2026
f1791ac
fix: don't treat an npm hyphen range as a prerelease request
fengmk2 Jun 6, 2026
f8a785a
Merge branch 'main' into feat/dev-engines
fengmk2 Jun 8, 2026
95892cf
test: add vp env doctor snap test for devEngines conflict findings
fengmk2 Jun 8, 2026
e0ef400
test: snapshot the doctor devEngines section instead of a pass/fail line
fengmk2 Jun 8, 2026
f8a7b9e
Merge branch 'main' into feat/dev-engines
fengmk2 Jun 8, 2026
5184b9b
docs: record why doctor doesn't flag compatible .node-version + devEn…
fengmk2 Jun 8, 2026
000db85
fix(doctor): name the package.json field in the Version Resolution so…
fengmk2 Jun 8, 2026
a96fbfb
fix(help): update stale pin/unpin summaries in vp env --help
fengmk2 Jun 8, 2026
2565e23
test: make format_version_source test cross-platform
fengmk2 Jun 8, 2026
12ad06a
Merge branch 'main' into feat/dev-engines
fengmk2 Jun 8, 2026
4808cfe
fix(env): default the pin overwrite prompt to yes
fengmk2 Jun 8, 2026
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
3 changes: 3 additions & 0 deletions crates/vite_error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ pub enum Error {
#[error("Unsupported package manager: {0}")]
UnsupportedPackageManager(Str),

#[error("devEngines.packageManager {0:?} is not supported (supported: pnpm, yarn, npm, bun)")]
UnsupportedDevEnginesPackageManager(Str),

#[error("Unrecognized any package manager, please specify the package manager")]
UnrecognizedPackageManager,

Expand Down
37 changes: 30 additions & 7 deletions crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,32 +337,46 @@ Examples:
tool: String,
},

/// Pin a Node.js version in the current directory (creates .node-version)
/// Pin a Node.js version in the current directory
/// (updates .node-version or package.json#devEngines.runtime)
#[command(after_long_help = "\
Examples:
vp env pin lts # Pin to latest LTS
vp env pin --unpin # Remove .node-version
vp env pin \"^20.0.0\" --force # Overwrite existing pin")]
vp env pin --unpin # Remove the pin
vp env pin \"^20.0.0\" --force # Overwrite existing pin
vp env pin 24 --target node-version # Force the .node-version file

The write target follows the compatibility-first rule: an existing .node-version
keeps being updated; otherwise the pin is written to package.json#devEngines.runtime;
.node-version is only created when the directory has no package.json.")]
Pin {
/// Version to pin (e.g., "20.18.0", "lts", "latest", "^20.0.0").
/// If omitted, prints the currently pinned version.
version: Option<String>,

/// Remove the .node-version file from current directory
/// Remove the pin from the current directory
#[arg(long)]
unpin: bool,

/// Skip pre-downloading the pinned version
#[arg(long)]
no_install: bool,

/// Overwrite existing .node-version without confirmation
/// Overwrite an existing pin without confirmation
#[arg(long)]
force: bool,

/// Explicitly choose the write target (overrides the default selection)
#[arg(long, value_enum)]
target: Option<PinTarget>,
},

/// Remove the .node-version file from current directory (alias for `pin --unpin`)
Unpin,
/// Remove the Node.js pin from current directory (alias for `pin --unpin`)
Unpin {
/// Explicitly choose which pin source to remove
#[arg(long, value_enum)]
target: Option<PinTarget>,
},

/// List locally installed Node.js versions
#[command(visible_alias = "ls")]
Expand Down Expand Up @@ -468,6 +482,15 @@ impl EnvSubcommands {
}
}

/// Write target for `vp env pin` / `vp env unpin` (see rfcs/dev-engines.md)
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum PinTarget {
/// Pin via the .node-version file
NodeVersion,
/// Pin via package.json#devEngines.runtime
DevEngines,
}

/// Version sorting order for list-remote command
#[derive(clap::ValueEnum, Clone, Debug, Default)]
pub enum SortingMethod {
Expand Down
54 changes: 26 additions & 28 deletions crates/vite_global_cli/src/commands/env/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ pub async fn delete_session_version() -> Result<(), Error> {
/// 0. `VP_NODE_VERSION` env var (session override from `vp env use`)
/// 1. `.session-node-version` file (session override written by `vp env use` for shell-wrapper-less environments)
/// 2. `.node-version` file in current or parent directories
/// 3. `package.json#engines.node` in current or parent directories
/// 4. `package.json#devEngines.runtime` in current or parent directories
/// 3. `package.json#devEngines.runtime` in current or parent directories
/// 4. `package.json#engines.node` in current or parent directories
/// 5. User default from config.json
/// 6. Latest LTS version
pub async fn resolve_version(cwd: &AbsolutePath) -> Result<VersionResolution, Error> {
Expand Down Expand Up @@ -267,50 +267,48 @@ pub async fn resolve_version_from_files(cwd: &AbsolutePath) -> Result<VersionRes

// Invalid version from a project source - try lower-priority sources in the same directory.
// This mirrors the fallback logic in download_runtime_for_project().
// - NodeVersionFile: try engines.node, then devEngines.runtime
// - EnginesNode: try devEngines.runtime
if matches!(resolution.source, VersionSource::NodeVersionFile | VersionSource::EnginesNode)
{
// - NodeVersionFile: try devEngines.runtime, then engines.node
// - DevEnginesRuntime: try engines.node
if matches!(
resolution.source,
VersionSource::NodeVersionFile | VersionSource::DevEnginesRuntime
) {
if let Some(project_root) = &resolution.project_root {
let package_json_path = project_root.join("package.json");
if let Ok(Some(pkg)) = read_package_json(&package_json_path).await {
// Try engines.node (only when falling back from .node-version)
// Try devEngines.runtime (only when falling back from .node-version)
if matches!(resolution.source, VersionSource::NodeVersionFile) {
if let Some(engines_node) = pkg
.engines
.as_ref()
.and_then(|e| e.node.clone())
.and_then(|v| normalize_version(&v, "engines.node"))
if let Some(dev_engines) = pkg
.dev_engines_runtime("node")
.and_then(|r| r.version.clone())
.and_then(|v| normalize_version(&v, "devEngines.runtime"))
{
let resolved = resolve_version_string(&engines_node, &provider).await?;
let is_range = NodeProvider::is_lts_alias(&engines_node)
|| !NodeProvider::is_exact_version(&engines_node);
let resolved = resolve_version_string(&dev_engines, &provider).await?;
let is_range = NodeProvider::is_lts_alias(&dev_engines)
|| !NodeProvider::is_exact_version(&dev_engines);
return Ok(VersionResolution {
version: resolved,
source: "engines.node".into(),
source: "devEngines.runtime".into(),
source_path: Some(package_json_path),
project_root: Some(project_root.clone()),
is_range,
});
}
}

// Try devEngines.runtime
if let Some(dev_engines) = pkg
.dev_engines
// Try engines.node
if let Some(engines_node) = pkg
.engines
.as_ref()
.and_then(|de| de.runtime.as_ref())
.and_then(|rt| rt.find_by_name("node"))
.map(|r| r.version.clone())
.filter(|v| !v.is_empty())
.and_then(|v| normalize_version(&v, "devEngines.runtime"))
.and_then(|e| e.node.clone())
.and_then(|v| normalize_version(&v, "engines.node"))
{
let resolved = resolve_version_string(&dev_engines, &provider).await?;
let is_range = NodeProvider::is_lts_alias(&dev_engines)
|| !NodeProvider::is_exact_version(&dev_engines);
let resolved = resolve_version_string(&engines_node, &provider).await?;
let is_range = NodeProvider::is_lts_alias(&engines_node)
|| !NodeProvider::is_exact_version(&engines_node);
return Ok(VersionResolution {
version: resolved,
source: "devEngines.runtime".into(),
source: "engines.node".into(),
source_path: Some(package_json_path),
project_root: Some(project_root.clone()),
is_range,
Expand Down
Loading
Loading