From 0276180f428c6caf4cbbc05ae7053f1a8af94ac3 Mon Sep 17 00:00:00 2001 From: ywenhao <37856060+ywenhao@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:55:00 +0800 Subject: [PATCH 1/3] fix(env): preserve existing .nvmrc pins --- .../fixtures/command_env_pin_nvmrc/.nvmrc | 2 + .../command_env_pin_nvmrc/package.json | 4 + .../command_env_pin_nvmrc/snapshots.toml | 124 ++++++++++ ...vmrc_overwrite_keeps_the_existing_setup.md | 22 ++ .../dev_engines_keeps_priority_over_nvmrc.md | 62 +++++ ...oes_not_make_pin_write_a_shadowed_nvmrc.md | 50 ++++ .../explicit_nvmrc_target_can_create_a_pin.md | 38 ++++ ...xplicit_targets_override_existing_nvmrc.md | 90 ++++++++ .../node_version_keeps_priority_over_nvmrc.md | 47 ++++ ...rget_rejects_package_manager_operations.md | 42 ++++ .../snapshots/pin_and_unpin_existing_nvmrc.md | 91 ++++++++ ...hild_does_not_overwrite_inherited_nvmrc.md | 56 +++++ .../snapshots/pin_nvmrc_without_manifest.md | 33 +++ crates/vp_global_cli/src/cli.rs | 7 +- crates/vp_global_cli/src/commands/env/pin.rs | 215 ++++++++++++++---- docs/guide/env.md | 2 +- rfcs/dev-engines.md | 9 +- 17 files changed, 848 insertions(+), 46 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/.nvmrc create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/package.json create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/declining_nvmrc_overwrite_keeps_the_existing_setup.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/dev_engines_keeps_priority_over_nvmrc.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_does_not_make_pin_write_a_shadowed_nvmrc.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_nvmrc_target_can_create_a_pin.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_targets_override_existing_nvmrc.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/node_version_keeps_priority_over_nvmrc.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/nvmrc_target_rejects_package_manager_operations.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_and_unpin_existing_nvmrc.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_in_child_does_not_overwrite_inherited_nvmrc.md create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_nvmrc_without_manifest.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/.nvmrc b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/.nvmrc new file mode 100644 index 0000000000..f568b0adb8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/.nvmrc @@ -0,0 +1,2 @@ +# Node for local tools and CI +v20.18.0 # keep this comment diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/package.json new file mode 100644 index 0000000000..1ac67c16d8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/package.json @@ -0,0 +1,4 @@ +{ + "name": "env-pin-nvmrc", + "private": true +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml new file mode 100644 index 0000000000..d645ec6ef1 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml @@ -0,0 +1,124 @@ +[[case]] +name = "pin_and_unpin_existing_nvmrc" +vp = "global" +seed-runtime = false +steps = [ + ["vp", "env", "pin"], + ["vp", "env", "pin", "22.13.0", "--no-install", "--force"], + { argv = ["vpt", "print-file", ".nvmrc", "package.json"], comment = "update the existing version token and preserve the manifest and comment" }, + ["vpt", "stat-file", ".node-version", "--assert", "missing"], + ["vp", "env", "pin"], + ["vp", "env", "current", "node", "--json"], + ["vp", "env", "unpin", "node"], + ["vpt", "stat-file", ".nvmrc", "--assert", "missing"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "pin_nvmrc_without_manifest" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "rm", "package.json"], snapshot = false }, + ["vp", "env", "pin", "22.13.0", "--no-install", "--force"], + ["vpt", "print-file", ".nvmrc"], + ["vpt", "stat-file", ".node-version", "--assert", "missing"], + ["vpt", "stat-file", "package.json", "--assert", "missing"], +] + +[[case]] +name = "node_version_keeps_priority_over_nvmrc" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "write-file", ".node-version", "20.18.0\n"], snapshot = false }, + ["vp", "env", "pin", "22.13.0", "--no-install", "--force"], + ["vpt", "print-file", ".node-version", ".nvmrc", "package.json"], + ["vp", "env", "unpin", "node"], + ["vpt", "stat-file", ".node-version", "--assert", "missing"], + ["vpt", "stat-file", ".nvmrc", "--assert", "file"], +] + +[[case]] +name = "dev_engines_keeps_priority_over_nvmrc" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"warn\"}}}\n"], snapshot = false }, + ["vp", "env", "pin", "22.13.0", "--no-install", "--force"], + ["vp", "env", "pin"], + ["vpt", "print-file", ".nvmrc", "package.json"], + ["vp", "env", "unpin", "node"], + ["vpt", "stat-file", ".nvmrc", "--assert", "file"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "engines_node_does_not_make_pin_write_a_shadowed_nvmrc" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"engines\":{\"node\":\">=20.18.0\"}}\n"], snapshot = false }, + ["vp", "env", "pin", "22.13.0", "--no-install", "--force"], + ["vpt", "print-file", ".nvmrc", "package.json"], + ["vp", "env", "current", "node", "--json"], +] + +[[case]] +name = "explicit_targets_override_existing_nvmrc" +vp = "global" +seed-runtime = false +steps = [ + ["vp", "env", "pin", "22.13.0", "--target", "dev-engines", "--no-install", "--force"], + ["vpt", "print-file", ".nvmrc", "package.json"], + { argv = ["vp", "env", "pin", "24.11.0", "--target", "nvmrc", "--no-install", "--force"], comment = "an explicit .nvmrc target warns about the higher-priority manifest pin" }, + ["vpt", "print-file", ".nvmrc", "package.json"], + ["vp", "env", "unpin", "--target", "nvmrc"], + ["vpt", "stat-file", ".nvmrc", "--assert", "missing"], + ["vpt", "print-file", "package.json"], +] + +[[case]] +name = "explicit_nvmrc_target_can_create_a_pin" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "rm", ".nvmrc"], snapshot = false }, + ["vp", "env", "pin", "22.13.0", "--target", "nvmrc", "--no-install"], + ["vpt", "print-file", ".nvmrc", "package.json"], + ["vp", "env", "pin", "--unpin", "node", "--target", "nvmrc"], + ["vpt", "stat-file", ".nvmrc", "--assert", "missing"], +] + +[[case]] +name = "pin_in_child_does_not_overwrite_inherited_nvmrc" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "mkdir", "child"], snapshot = false }, + { argv = ["vpt", "write-file", "child/package.json", "{\"name\":\"child\"}\n"], snapshot = false }, + { argv = ["vp", "env", "pin"], cwd = "child" }, + { argv = ["vp", "env", "pin", "22.13.0", "--no-install", "--force"], cwd = "child" }, + ["vpt", "print-file", ".nvmrc", "package.json", "child/package.json"], + ["vpt", "stat-file", "child/.nvmrc", "--assert", "missing"], +] + +[[case]] +name = "declining_nvmrc_overwrite_keeps_the_existing_setup" +vp = "global" +seed-runtime = false +steps = [ + ["vpt", "pipe-stdin", "n\n", "--", "vp", "env", "pin", "22.13.0", "--no-install"], + ["vpt", "print-file", ".nvmrc", "package.json"], +] + +[[case]] +name = "nvmrc_target_rejects_package_manager_operations" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vp", "env", "pin", "pnpm@10.18.0", "--target", "nvmrc", "--no-install"], continue-on-failure = true }, + { argv = ["vp", "env", "pin", "22.13.0", "pnpm@10.18.0", "--target", "nvmrc", "--no-install"], continue-on-failure = true }, + { argv = ["vp", "env", "unpin", "pm", "--target", "nvmrc"], continue-on-failure = true }, + { argv = ["vpt", "print-file", ".nvmrc", "package.json"], continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/declining_nvmrc_overwrite_keeps_the_existing_setup.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/declining_nvmrc_overwrite_keeps_the_existing_setup.md new file mode 100644 index 0000000000..222cd4b7ce --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/declining_nvmrc_overwrite_keeps_the_existing_setup.md @@ -0,0 +1,22 @@ +# declining_nvmrc_overwrite_keeps_the_existing_setup + +## `vpt pipe-stdin 'n +' -- vp env pin 22.13.0 --no-install` + +``` +VITE+ - The Unified Toolchain for the Web + +.nvmrc already exists with version 20.18.0 +Overwrite with 22.13.0? (Y/n): Cancelled. +``` + +## `vpt print-file .nvmrc package.json` + +``` +# Node for local tools and CI + # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/dev_engines_keeps_priority_over_nvmrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/dev_engines_keeps_priority_over_nvmrc.md new file mode 100644 index 0000000000..805bdf7b04 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/dev_engines_keeps_priority_over_nvmrc.md @@ -0,0 +1,62 @@ +# dev_engines_keeps_priority_over_nvmrc + +## `vpt write-file package.json '{"devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"warn"}}} +'` + + +## `vp env pin 22.13.0 --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated devEngines.runtime in /package.json +note: Version will be downloaded on first use. +``` + +## `vp env pin` + +``` +VITE+ - The Unified Toolchain for the Web + +Pinned version: 22.13.0 + Source: /package.json (devEngines.runtime) + +No package manager pinned. +``` + +## `vpt print-file .nvmrc package.json` + +``` +# Node for local tools and CI + # keep this comment +{ + "devEngines": { + "runtime": { + "name": "node", + "version": "", + "onFail": "warn" + } + } +} +``` + +## `vp env unpin node` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Removed devEngines.runtime node entry from /package.json +``` + +## `vpt stat-file .nvmrc --assert file` + +``` +.nvmrc: file +``` + +## `vpt print-file package.json` + +``` +{} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_does_not_make_pin_write_a_shadowed_nvmrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_does_not_make_pin_write_a_shadowed_nvmrc.md new file mode 100644 index 0000000000..b006ceaa0e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_does_not_make_pin_write_a_shadowed_nvmrc.md @@ -0,0 +1,50 @@ +# engines_node_does_not_make_pin_write_a_shadowed_nvmrc + +## `vpt write-file package.json '{"engines":{"node":">=20.18.0"}} +'` + + +## `vp env pin 22.13.0 --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated devEngines.runtime in /package.json +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc package.json` + +``` +# Node for local tools and CI + # keep this comment +{ + "engines": { + "node": ">=20.18.0" + }, + "devEngines": { + "runtime": { + "name": "node", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vp env current node --json` + +``` +{ + "node": { + "version": "22.13.0", + "source": "devEngines.runtime", + "source_path": "/package.json", + "project_root": "", + "bin_path": "/.vite-plus/js_runtime/node//bin/node", + "installed": false, + "mode": "managed" + } +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_nvmrc_target_can_create_a_pin.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_nvmrc_target_can_create_a_pin.md new file mode 100644 index 0000000000..e80442e0bb --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_nvmrc_target_can_create_a_pin.md @@ -0,0 +1,38 @@ +# explicit_nvmrc_target_can_create_a_pin + +## `vpt rm .nvmrc` + + +## `vp env pin 22.13.0 --target nvmrc --no-install` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated .nvmrc in +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc package.json` + +``` +22.13.0 +{ + "name": "env-pin-nvmrc", + "private": true +} +``` + +## `vp env pin --unpin node --target nvmrc` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Removed .nvmrc from +``` + +## `vpt stat-file .nvmrc --assert missing` + +``` +.nvmrc: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_targets_override_existing_nvmrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_targets_override_existing_nvmrc.md new file mode 100644 index 0000000000..96236cba6b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/explicit_targets_override_existing_nvmrc.md @@ -0,0 +1,90 @@ +# explicit_targets_override_existing_nvmrc + +## `vp env pin 22.13.0 --target dev-engines --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated devEngines.runtime in /package.json +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc package.json` + +``` +# Node for local tools and CI + # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true, + "devEngines": { + "runtime": { + "name": "node", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vp env pin 24.11.0 --target nvmrc --no-install --force` + +an explicit .nvmrc target warns about the higher-priority manifest pin + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 24.11.0 + Updated .nvmrc in +warn: devEngines.runtime still takes precedence over .nvmrc. Run 'vp env doctor' for details. +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc package.json` + +``` +# Node for local tools and CI +24.11.0 # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true, + "devEngines": { + "runtime": { + "name": "node", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vp env unpin --target nvmrc` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Removed .nvmrc from +``` + +## `vpt stat-file .nvmrc --assert missing` + +``` +.nvmrc: missing +``` + +## `vpt print-file package.json` + +``` +{ + "name": "env-pin-nvmrc", + "private": true, + "devEngines": { + "runtime": { + "name": "node", + "version": "", + "onFail": "download" + } + } +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/node_version_keeps_priority_over_nvmrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/node_version_keeps_priority_over_nvmrc.md new file mode 100644 index 0000000000..57ce3ed89b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/node_version_keeps_priority_over_nvmrc.md @@ -0,0 +1,47 @@ +# node_version_keeps_priority_over_nvmrc + +## `vpt write-file .node-version '20.18.0 +'` + + +## `vp env pin 22.13.0 --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Created .node-version in +note: Version will be downloaded on first use. +``` + +## `vpt print-file .node-version .nvmrc package.json` + +``` +22.13.0 +# Node for local tools and CI + # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true +} +``` + +## `vp env unpin node` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Removed .node-version from +``` + +## `vpt stat-file .node-version --assert missing` + +``` +.node-version: missing +``` + +## `vpt stat-file .nvmrc --assert file` + +``` +.nvmrc: file +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/nvmrc_target_rejects_package_manager_operations.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/nvmrc_target_rejects_package_manager_operations.md new file mode 100644 index 0000000000..90e4381f07 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/nvmrc_target_rejects_package_manager_operations.md @@ -0,0 +1,42 @@ +# nvmrc_target_rejects_package_manager_operations + +## `vp env pin pnpm@10.18.0 --target nvmrc --no-install` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: Node.js file targets cannot pin a package manager +``` + +## `vp env pin 22.13.0 pnpm@10.18.0 --target nvmrc --no-install` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: mixed Node.js and package-manager pins require the default targets or --target dev-engines +``` + +## `vp env unpin pm --target nvmrc` + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: Node.js file targets are incompatible with package-manager scope +``` + +## `vpt print-file .nvmrc package.json` + +``` +# Node for local tools and CI + # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_and_unpin_existing_nvmrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_and_unpin_existing_nvmrc.md new file mode 100644 index 0000000000..d90bc8ccc8 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_and_unpin_existing_nvmrc.md @@ -0,0 +1,91 @@ +# pin_and_unpin_existing_nvmrc + +## `vp env pin` + +``` +VITE+ - The Unified Toolchain for the Web + +Pinned version: 20.18.0 + Source: /.nvmrc + +No package manager pinned. +``` + +## `vp env pin 22.13.0 --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated .nvmrc in +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc package.json` + +update the existing version token and preserve the manifest and comment + +``` +# Node for local tools and CI +22.13.0 # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true +} +``` + +## `vpt stat-file .node-version --assert missing` + +``` +.node-version: missing +``` + +## `vp env pin` + +``` +VITE+ - The Unified Toolchain for the Web + +Pinned version: 22.13.0 + Source: /.nvmrc + +No package manager pinned. +``` + +## `vp env current node --json` + +``` +{ + "node": { + "version": "22.13.0", + "source": ".nvmrc", + "source_path": "/.nvmrc", + "project_root": "", + "bin_path": "/.vite-plus/js_runtime/node//bin/node", + "installed": false, + "mode": "managed" + } +} +``` + +## `vp env unpin node` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Removed .nvmrc from +``` + +## `vpt stat-file .nvmrc --assert missing` + +``` +.nvmrc: missing +``` + +## `vpt print-file package.json` + +``` +{ + "name": "env-pin-nvmrc", + "private": true +} +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_in_child_does_not_overwrite_inherited_nvmrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_in_child_does_not_overwrite_inherited_nvmrc.md new file mode 100644 index 0000000000..4d4ffdf0c7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_in_child_does_not_overwrite_inherited_nvmrc.md @@ -0,0 +1,56 @@ +# pin_in_child_does_not_overwrite_inherited_nvmrc + +## `vpt mkdir child` + + +## `vpt write-file child/package.json '{"name":"child"} +'` + + +## `cd child && vp env pin` + +``` +VITE+ - The Unified Toolchain for the Web + +No version pinned in current directory. + Inherited: 20.18.0 from /.nvmrc + +No package manager pinned. +``` + +## `cd child && vp env pin 22.13.0 --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated devEngines.runtime in /child/package.json +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc package.json child/package.json` + +``` +# Node for local tools and CI + # keep this comment +{ + "name": "env-pin-nvmrc", + "private": true +} +{ + "name": "child", + "devEngines": { + "runtime": { + "name": "node", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vpt stat-file child/.nvmrc --assert missing` + +``` +child/.nvmrc: missing +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_nvmrc_without_manifest.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_nvmrc_without_manifest.md new file mode 100644 index 0000000000..e7083ed8e0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/pin_nvmrc_without_manifest.md @@ -0,0 +1,33 @@ +# pin_nvmrc_without_manifest + +## `vpt rm package.json` + + +## `vp env pin 22.13.0 --no-install --force` + +``` +VITE+ - The Unified Toolchain for the Web + +✓ Pinned Node.js version to 22.13.0 + Updated .nvmrc in +note: Version will be downloaded on first use. +``` + +## `vpt print-file .nvmrc` + +``` +# Node for local tools and CI +22.13.0 # keep this comment +``` + +## `vpt stat-file .node-version --assert missing` + +``` +.node-version: missing +``` + +## `vpt stat-file package.json --assert missing` + +``` +package.json: missing +``` diff --git a/crates/vp_global_cli/src/cli.rs b/crates/vp_global_cli/src/cli.rs index da845da82c..b5fc709aca 100644 --- a/crates/vp_global_cli/src/cli.rs +++ b/crates/vp_global_cli/src/cli.rs @@ -377,8 +377,9 @@ Examples: 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.")] +keeps being updated, as does an effective .nvmrc in the current directory. +Otherwise the pin is written to package.json#devEngines.runtime, or .node-version +when the directory has no package.json.")] Pin { /// Versions to pin. Bare versions select Node.js; package managers use name@version. specs: Vec, @@ -534,6 +535,8 @@ impl EnvSubcommands { pub enum PinTarget { /// Pin via the .node-version file NodeVersion, + /// Pin via the .nvmrc file + Nvmrc, /// Pin via package.json#devEngines.runtime DevEngines, /// Pin via the top-level packageManager field diff --git a/crates/vp_global_cli/src/commands/env/pin.rs b/crates/vp_global_cli/src/commands/env/pin.rs index 490c6cffbc..9b9f47276a 100644 --- a/crates/vp_global_cli/src/commands/env/pin.rs +++ b/crates/vp_global_cli/src/commands/env/pin.rs @@ -2,14 +2,14 @@ //! //! Handles `vp env pin [VERSION]` to pin a Node.js version in the current directory. //! The write target follows the compatibility-first rule from rfcs/dev-engines.md: -//! 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. An explicit `--target` flag overrides the selection. +//! an existing `.node-version` or effective `.nvmrc` keeps being updated; otherwise +//! the pin is written to `package.json#devEngines.runtime`, or `.node-version` when +//! the directory has no package.json. An explicit `--target` overrides the selection. //! An existing `engines.node` is never deleted or modified. use std::{io::Write, process::ExitStatus}; -use vp_js_runtime::NodeProvider; +use vp_js_runtime::{NodeProvider, VersionSource, resolve_node_version}; use vp_pm_cli::{ PackageManagerType, download_package_manager, resolve_package_manager_from_package_json, resolve_package_manager_version, @@ -27,6 +27,8 @@ use crate::{cli::PinTarget, error::Error}; /// Node version file name const NODE_VERSION_FILE: &str = ".node-version"; +const NVMRC_FILE: &str = ".nvmrc"; + /// Package manifest file name const PACKAGE_JSON_FILE: &str = "package.json"; @@ -65,7 +67,10 @@ pub async fn execute( }; if specs.node.is_some() && specs.package_manager.is_some() - && matches!(target, Some(PinTarget::NodeVersion | PinTarget::PackageManager)) + && matches!( + target, + Some(PinTarget::NodeVersion | PinTarget::Nvmrc | PinTarget::PackageManager) + ) { return Err(Error::Other( "mixed Node.js and package-manager pins require the default targets or --target dev-engines" @@ -132,6 +137,14 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result { return Ok(ExitStatus::default()); } + if let Some(resolution) = resolve_node_version(cwd, false).await? + && resolution.source == VersionSource::NvmrcFile + { + println!("Pinned version: {}", resolution.version); + println!(" Source: {}", cwd.join(NVMRC_FILE).as_path().display()); + return Ok(ExitStatus::default()); + } + // Check for inherited version from parent directories if let Some((version, source)) = find_inherited_version(cwd).await? { println!("No version pinned in current directory."); @@ -156,11 +169,12 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result { Ok(ExitStatus::default()) } -/// Find an inherited pin (`.node-version` or `package.json#devEngines.runtime`) +/// Find an inherited pin (`.node-version`, `devEngines.runtime`, or `.nvmrc`) /// in parent directories. /// /// Mirrors the resolution order within each directory: `.node-version` first, -/// then the devEngines.runtime node entry. Returns the version and a display +/// then the devEngines.runtime node entry, then an effective `.nvmrc`. +/// Returns the version and a display /// string describing the source. async fn find_inherited_version(cwd: &AbsolutePathBuf) -> Result, Error> { let mut current: Option = cwd.parent().map(|p| p.to_absolute_path_buf()); @@ -180,12 +194,34 @@ async fn find_inherited_version(cwd: &AbsolutePathBuf) -> Result Result, Error> { + if tokio::fs::try_exists(cwd.join(NODE_VERSION_FILE)).await.unwrap_or(false) { + return Ok(Some(PinTarget::NodeVersion)); + } + if let Some(resolution) = resolve_node_version(cwd, false).await? + && resolution.source == VersionSource::NvmrcFile + { + return Ok(Some(PinTarget::Nvmrc)); + } + Ok(None) +} + /// Pin a version to the current directory. async fn do_pin( cwd: &AbsolutePathBuf, @@ -204,19 +240,32 @@ async fn do_pin( let package_json_exists = tokio::fs::try_exists(cwd.join(PACKAGE_JSON_FILE)).await.unwrap_or(false); - // Compatibility-first target selection (rfcs/dev-engines.md): an existing - // .node-version keeps winning; otherwise pin into package.json#devEngines.runtime; - // .node-version is only created when the directory has no package.json. - let target = target.unwrap_or(if node_version_exists || !package_json_exists { - PinTarget::NodeVersion - } else { - PinTarget::DevEngines - }); + let target = match target { + Some(target) => target, + None => existing_node_file_target(cwd).await?.unwrap_or(if package_json_exists { + PinTarget::DevEngines + } else { + PinTarget::NodeVersion + }), + }; let pinned = match target { PinTarget::NodeVersion => { pin_node_version_file(cwd, version, &resolved_version, was_alias, force).await? } + PinTarget::Nvmrc => { + let pinned = pin_nvmrc_file(cwd, version, &resolved_version, was_alias, force).await?; + if pinned + && let Some(resolution) = resolve_node_version(cwd, false).await? + && resolution.source != VersionSource::NvmrcFile + { + output::warn(&format!( + "{} still takes precedence over {NVMRC_FILE}. Run 'vp env doctor' for details.", + resolution.source + )); + } + pinned + } PinTarget::DevEngines => { if !package_json_exists { return Err(Error::Other( @@ -361,6 +410,66 @@ async fn pin_node_version_file( Ok(true) } +/// Locate the version token using the same line rules as read_nvmrc_file. +/// 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(); + } + Ok(span) +} + +async fn pin_nvmrc_file( + cwd: &AbsolutePathBuf, + input_version: &str, + resolved_version: &str, + was_alias: bool, + force: bool, +) -> Result { + let path = cwd.join(NVMRC_FILE); + let mut content = match tokio::fs::read_to_string(&path).await { + Ok(content) => content, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(error) => return Err(error.into()), + }; + if let Some(span) = nvmrc_version_span(&content)? { + let existing = &content[span.clone()]; + if !confirm_overwrite_pin( + ".nvmrc already exists with version", + existing.strip_prefix('v').unwrap_or(existing), + resolved_version, + force, + )? { + return Ok(false); + } + content.replace_range(span, resolved_version); + } else { + let newline = if content.contains("\r\n") { "\r\n" } else { "\n" }; + if !content.is_empty() && !content.ends_with('\n') { + content.push_str(newline); + } + content.push_str(resolved_version); + content.push_str(newline); + } + tokio::fs::write(&path, content).await?; + print_pin_success(input_version, resolved_version, was_alias); + println!(" Updated {NVMRC_FILE} in {}", cwd.as_path().display()); + Ok(true) +} + /// Pin by writing `package.json#devEngines.runtime`. /// /// Returns `true` when the pin was written, `false` when nothing changed @@ -601,39 +710,33 @@ async fn resolve_version_for_pin( /// Remove the Node.js pin from the current directory. /// -/// Removes the same source that `vp env pin` would write: `.node-version` when -/// present, otherwise the node entry from `package.json#devEngines.runtime`. +/// Removes the same source that `vp env pin` would write, including an effective +/// `.nvmrc` in the current directory. /// An explicit `target` overrides the selection. pub async fn do_unpin( cwd: &AbsolutePathBuf, target: Option, ) -> Result { - let node_version_path = cwd.join(NODE_VERSION_FILE); - let node_version_exists = tokio::fs::try_exists(&node_version_path).await.unwrap_or(false); - - let target = target.unwrap_or(if node_version_exists { - PinTarget::NodeVersion - } else { - PinTarget::DevEngines - }); + let target = match target { + Some(target) => target, + None => existing_node_file_target(cwd).await?.unwrap_or(PinTarget::DevEngines), + }; match target { - PinTarget::NodeVersion => { - if !node_version_exists { - println!("No {NODE_VERSION_FILE} file in current directory."); + PinTarget::NodeVersion | PinTarget::Nvmrc => { + let file = if target == PinTarget::Nvmrc { NVMRC_FILE } else { NODE_VERSION_FILE }; + let path = cwd.join(file); + if !tokio::fs::try_exists(&path).await.unwrap_or(false) { + println!("No {file} file in current directory."); return Ok(ExitStatus::default()); } - tokio::fs::remove_file(&node_version_path).await?; + tokio::fs::remove_file(&path).await?; // Invalidate resolve cache so the unpinned version falls back correctly crate::shim::invalidate_cache(); - output::success(&format!( - "Removed {} from {}", - NODE_VERSION_FILE, - cwd.as_path().display() - )); + output::success(&format!("Removed {} from {}", file, cwd.as_path().display())); } PinTarget::DevEngines => { if remove_dev_engines_runtime_node(cwd).await? { @@ -670,10 +773,10 @@ pub async fn do_unpin_scope( } if scope.includes_package_managers() && !scope.includes_node() - && matches!(target, Some(PinTarget::NodeVersion)) + && matches!(target, Some(PinTarget::NodeVersion | PinTarget::Nvmrc)) { return Err(Error::Other( - "--target node-version is incompatible with package-manager scope".into(), + "Node.js file targets are incompatible with package-manager scope".into(), )); } if scope.includes_node() && !matches!(target, Some(PinTarget::PackageManager)) { @@ -694,8 +797,8 @@ async fn pin_package_manager( force: bool, target: Option, ) -> Result { - if matches!(target, Some(PinTarget::NodeVersion)) { - return Err(Error::Other("--target node-version cannot pin a package manager".into())); + if matches!(target, Some(PinTarget::NodeVersion | PinTarget::Nvmrc)) { + return Err(Error::Other("Node.js file targets cannot pin a package manager".into())); } let resolved = resolve_package_manager_version(package_manager, version).await?; package_manager::warn_if_target_differs(cwd, package_manager).await; @@ -838,7 +941,7 @@ async fn unpin_package_manager( scope: EnvScope, target: Option, ) -> Result<(), Error> { - if matches!(target, Some(PinTarget::NodeVersion)) { + if matches!(target, Some(PinTarget::NodeVersion | PinTarget::Nvmrc)) { return Ok(()); } let root = workspace_root(cwd)?.unwrap_or_else(|| cwd.clone()); @@ -955,6 +1058,40 @@ mod tests { dir } + #[tokio::test] + async fn nvmrc_pin_preserves_non_version_content() { + for (before, after) in [ + ( + "# Node for CI\n v20.18.0 # keep\nkey=value\n", + "# Node for CI\n 22.13.0 # keep\nkey=value\n", + ), + ("# Node\r\n20.18.0\r\n", "# Node\r\n22.13.0\r\n"), + ("20.18.0", "22.13.0"), + ("# Node\nkey=value", "# Node\nkey=value\n22.13.0\n"), + ("", "22.13.0\n"), + ] { + let temp = TempDir::new().unwrap(); + let cwd = AbsolutePathBuf::new(temp.path().to_path_buf()).unwrap(); + let path = cwd.join(NVMRC_FILE); + tokio::fs::write(&path, before).await.unwrap(); + assert!(pin_nvmrc_file(&cwd, "22.13.0", "22.13.0", false, true).await.unwrap()); + assert_eq!(tokio::fs::read_to_string(&path).await.unwrap(), after); + assert!(!pin_nvmrc_file(&cwd, "22.13.0", "22.13.0", false, true).await.unwrap()); + assert_eq!(tokio::fs::read_to_string(&path).await.unwrap(), after); + } + } + + #[tokio::test] + async fn nvmrc_pin_does_not_overwrite_ambiguous_declarations() { + let temp = TempDir::new().unwrap(); + let cwd = AbsolutePathBuf::new(temp.path().to_path_buf()).unwrap(); + let path = cwd.join(NVMRC_FILE); + let content = "20.18.0\n22.13.0\n"; + tokio::fs::write(&path, content).await.unwrap(); + assert!(pin_nvmrc_file(&cwd, "24.11.0", "24.11.0", false, true).await.is_err()); + assert_eq!(tokio::fs::read_to_string(&path).await.unwrap(), content); + } + #[tokio::test] async fn package_manager_pin_preserves_matching_integrity_suffix() { let temp_dir = TempDir::new().unwrap(); diff --git a/docs/guide/env.md b/docs/guide/env.md index e2190b6e86..7a7429de48 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -159,7 +159,7 @@ shim calls in the same job use these files to resolve the same environment. ### Manage - `vp env default` shows the global Node.js default and each configured package-manager version. Bare versions set Node.js; qualified specs such as `pnpm@10.18.0` set that package manager's shim default without replacing the defaults for Bun, Yarn, or npm. `--unset` clears all defaults unless scoped. -- `vp env pin` shows or writes project pins. Existing `.node-version` and top-level `packageManager` fields keep being updated for compatibility; otherwise Vite+ writes the matching `devEngines` entry. Use `--target node-version`, `--target dev-engines`, or `--target package-manager` to choose explicitly. +- `vp env pin` shows or writes project pins. Existing `.node-version` and top-level `packageManager` fields keep being updated for compatibility. An existing `.nvmrc` is updated when it is the effective Node source in the current directory; its comments and other non-version content are preserved. Otherwise Vite+ writes the matching `devEngines` entry. Use `--target node-version`, `--target nvmrc`, `--target dev-engines`, or `--target package-manager` to choose explicitly. Pinning in a child directory does not modify an inherited `.nvmrc`. - `vp env unpin` removes both effective pins by default; append a selector to remove one. Lower-priority declarations are not deleted. - `vp env use` activates the complete project environment. Explicit specs override selected components; `--unset` clears both unless scoped. - `vp env install` installs the complete resolved environment, a selected component, or explicit specs. diff --git a/rfcs/dev-engines.md b/rfcs/dev-engines.md index 0e0daf0f24..c984c7ae89 100644 --- a/rfcs/dev-engines.md +++ b/rfcs/dev-engines.md @@ -38,7 +38,7 @@ interface DevEngines { interface DevEngineDependency { name: string; // required version?: string; // semver range, same syntax as engines.node; absent = any - onFail?: 'ignore' | 'warn' | 'error' | 'download'; // default: error + onFail?: "ignore" | "warn" | "error" | "download"; // default: error } ``` @@ -172,13 +172,14 @@ Notes: | State of cwd | Write target | | ------------------------------------------------------------------------ | --------------------------------------------------------------------- | | `.node-version` exists | Update `.node-version` (unchanged behavior) | +| `.nvmrc` is the effective Node source in cwd | Update its version token, preserving comments and other content | | No `.node-version`; `package.json` has a `devEngines.runtime` node entry | Update that entry's `version` (preserve `onFail`, sibling entries) | | No `.node-version`; `package.json` exists without a node runtime entry | Add `devEngines.runtime` node entry with `onFail: "download"` | | No `package.json` in cwd | Create `.node-version` (unchanged behavior; nothing else to write to) | - `engines.node` is **never** a pin target: it is a consumer-facing constraint, and rewriting it would change the published package contract. More broadly, no Vite+ write path (pin, unpin, create, migrate) ever deletes or modifies an existing `engines.node`; it is always kept unchanged. - When updating an existing node entry in array form, only that entry's `version` changes; other runtimes and `onFail` values are preserved. -- An explicit `--target` flag overrides the selection: `vp env pin 24 --target node-version` or `--target dev-engines`. The flag always wins: `--target dev-engines` writes `devEngines.runtime` even when `.node-version` exists, with a note that `.node-version` still takes resolution precedence until removed. +- An explicit `--target` flag overrides the selection: `vp env pin 24 --target node-version`, `--target nvmrc`, or `--target dev-engines`. The flag always wins: `--target dev-engines` writes `devEngines.runtime` even when `.node-version` exists, with a note that `.node-version` still takes resolution precedence until removed. Likewise, an explicit `.nvmrc` target warns when a higher-priority source shadows it. The default `.nvmrc` selection uses local resolution only and does not change the priority of `.node-version`, `devEngines.runtime`, or `engines.node`. Value semantics (matching the implemented `vp env pin` behavior, which resolves every input to an exact version at pin time; identical for both targets): @@ -206,8 +207,8 @@ When `.node-version` is the write target and a `devEngines.runtime` node entry a #### 2.5 `vp env pin` (show) and `vp env unpin` -- `vp env pin` with no argument reports the active pin and its source, now including `devEngines.runtime` as a possible source (the `VersionSource::DevEnginesRuntime` display string already exists). Inherited pins from parent directories are reported for both sources, checking `.node-version` first and then the `devEngines.runtime` node entry per directory (matching the resolution order). -- `vp env unpin` / `vp env pin --unpin` removes the pin from the same target that `vp env pin` would write: delete `.node-version` if present, otherwise remove the node entry from `devEngines.runtime` (removing the `devEngines.runtime` key entirely if it becomes empty, and `devEngines` if it becomes empty). +- `vp env pin` with no argument reports the active pin and its source, including an effective `.nvmrc`. Inherited pins from parent directories are reported in the same order: `.node-version`, the `devEngines.runtime` node entry, then an effective `.nvmrc`. +- `vp env unpin` / `vp env pin --unpin` removes the pin from the same target that `vp env pin` would write: delete `.node-version` if present, delete an effective `.nvmrc` in cwd, or remove the node entry from `devEngines.runtime` (removing the `devEngines.runtime` key entirely if it becomes empty, and `devEngines` if it becomes empty). An explicit `--target nvmrc` removes that file even if another source shadows it. Parent-directory files are never removed. ### 3. Package manager From 7df05c0ae3ba12039615884cf7d9b65687047faa Mon Sep 17 00:00:00 2001 From: ywenhao <37856060+ywenhao@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:22:38 +0800 Subject: [PATCH 2/3] docs: apply repository formatting to devEngines RFC --- rfcs/dev-engines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/dev-engines.md b/rfcs/dev-engines.md index c984c7ae89..30aaa4e028 100644 --- a/rfcs/dev-engines.md +++ b/rfcs/dev-engines.md @@ -38,7 +38,7 @@ interface DevEngines { interface DevEngineDependency { name: string; // required version?: string; // semver range, same syntax as engines.node; absent = any - onFail?: "ignore" | "warn" | "error" | "download"; // default: error + onFail?: 'ignore' | 'warn' | 'error' | 'download'; // default: error } ``` From e05555e14a68f69cdea13db59ad1f6cf837e96ad Mon Sep 17 00:00:00 2001 From: ywenhao <37856060+ywenhao@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:42:28 +0800 Subject: [PATCH 3/3] fix(env): stop inherited pin lookup at nearer runtime constraints --- .../command_env_pin_nvmrc/snapshots.toml | 14 ++++ ...engines_node_stops_inherited_pin_lookup.md | 74 +++++++++++++++++++ crates/vp_global_cli/src/commands/env/pin.rs | 63 ++++++++++++---- rfcs/dev-engines.md | 2 +- 4 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_stops_inherited_pin_lookup.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml index d645ec6ef1..cf04e5170f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml @@ -112,6 +112,20 @@ steps = [ ["vpt", "print-file", ".nvmrc", "package.json"], ] +[[case]] +name = "engines_node_stops_inherited_pin_lookup" +vp = "global" +seed-runtime = false +steps = [ + { argv = ["vpt", "mkdir", "-p", "middle/leaf"], snapshot = false }, + { argv = ["vpt", "write-file", "middle/package.json", "{\"engines\":{\"node\":\"22.13.0\"}}\n"], snapshot = false }, + { argv = ["vp", "env", "pin"], cwd = "middle/leaf", comment = "the nearer engines.node prevents inheriting the root .nvmrc" }, + { argv = ["vp", "env", "current", "node", "--json"], cwd = "middle/leaf" }, + { argv = ["vp", "env", "pin"], cwd = "middle", comment = "a constraint in cwd also prevents reporting an ancestor pin" }, + { argv = ["vp", "env", "current", "node", "--json"], cwd = "middle" }, + ["vpt", "print-file", ".nvmrc", "middle/package.json"], +] + [[case]] name = "nvmrc_target_rejects_package_manager_operations" vp = "global" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_stops_inherited_pin_lookup.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_stops_inherited_pin_lookup.md new file mode 100644 index 0000000000..9a744420f9 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots/engines_node_stops_inherited_pin_lookup.md @@ -0,0 +1,74 @@ +# engines_node_stops_inherited_pin_lookup + +## `vpt mkdir -p middle/leaf` + + +## `vpt write-file middle/package.json '{"engines":{"node":"22.13.0"}} +'` + + +## `cd middle/leaf && vp env pin` + +the nearer engines.node prevents inheriting the root .nvmrc + +``` +VITE+ - The Unified Toolchain for the Web + +No version pinned. + Node.js constraint: 22.13.0 from /middle/package.json (engines.node) + +No package manager pinned. +``` + +## `cd middle/leaf && vp env current node --json` + +``` +{ + "node": { + "version": "22.13.0", + "source": "engines.node", + "source_path": "/middle/package.json", + "project_root": "/middle", + "bin_path": "/.vite-plus/js_runtime/node//bin/node", + "installed": false, + "mode": "managed" + } +} +``` + +## `cd middle && vp env pin` + +a constraint in cwd also prevents reporting an ancestor pin + +``` +VITE+ - The Unified Toolchain for the Web + +No version pinned. + Node.js constraint: 22.13.0 from /middle/package.json (engines.node) + +No package manager pinned. +``` + +## `cd middle && vp env current node --json` + +``` +{ + "node": { + "version": "22.13.0", + "source": "engines.node", + "source_path": "/middle/package.json", + "project_root": "/middle", + "bin_path": "/.vite-plus/js_runtime/node//bin/node", + "installed": false, + "mode": "managed" + } +} +``` + +## `vpt print-file .nvmrc middle/package.json` + +``` +# Node for local tools and CI + # keep this comment +{"engines":{"node":"22.13.0"}} +``` diff --git a/crates/vp_global_cli/src/commands/env/pin.rs b/crates/vp_global_cli/src/commands/env/pin.rs index 9b9f47276a..f0e3ac960b 100644 --- a/crates/vp_global_cli/src/commands/env/pin.rs +++ b/crates/vp_global_cli/src/commands/env/pin.rs @@ -137,12 +137,24 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result { return Ok(ExitStatus::default()); } - if let Some(resolution) = resolve_node_version(cwd, false).await? - && resolution.source == VersionSource::NvmrcFile - { - println!("Pinned version: {}", resolution.version); - println!(" Source: {}", cwd.join(NVMRC_FILE).as_path().display()); - return Ok(ExitStatus::default()); + if let Some(resolution) = resolve_node_version(cwd, true).await? { + if resolution.source == VersionSource::NvmrcFile + && resolution.project_root.as_ref() == Some(cwd) + { + println!("Pinned version: {}", resolution.version); + println!(" Source: {}", cwd.join(NVMRC_FILE).as_path().display()); + return Ok(ExitStatus::default()); + } + if resolution.source == VersionSource::EnginesNode { + let path = resolution.source_path.unwrap_or_else(|| cwd.join(PACKAGE_JSON_FILE)); + println!("No version pinned."); + println!( + " Node.js constraint: {} from {} (engines.node)", + resolution.version, + path.as_path().display() + ); + return Ok(ExitStatus::default()); + } } // Check for inherited version from parent directories @@ -194,13 +206,16 @@ async fn find_inherited_version(cwd: &AbsolutePathBuf) -> Result