diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml new file mode 100644 index 0000000000..22cad4aa8c --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots.toml @@ -0,0 +1,161 @@ +[[case]] +name = "pnpm_runtime_disabled_for_managed_node" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "Vite+ owns Node.js runtime management independently from the selected pnpm binary." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct system-first pnpm disables its duplicate Node.js runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same system-first policy" }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm", "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf '11.1.0\\n'; else printf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"; fi\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx", "#!/bin/sh\nprintf 'PNPM_CONFIG_RUNTIME=%s\\n' \"${PNPM_CONFIG_RUNTIME-unset}\"\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx"], snapshot = false }, + { argv = ["vpt", "mkdir", "other"], snapshot = false }, + { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "child"], snapshot = false }, + { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct managed pnpm disables its duplicate Node.js runtime" }, + { argv = ["pnpx", "package"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the pnpx alias receives the same child-only override" }, + { argv = ["pnpx", "package", "--dir", "other"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpx forwards --dir to the downloaded command without changing runtime ownership" }, + { argv = ["pnpm", "exec", "package", "--dir", "other"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm exec forwards --dir after the command without changing runtime ownership" }, + { argv = ["pnpm", "runtime", "set", "deno", "2"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "an explicit runtime command keeps pnpm runtime management enabled" }, + { argv = ["pnpm", "add", "bun@runtime:1.2.0"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "an explicit runtime dependency keeps pnpm runtime management enabled" }, + { argv = ["vp", "install"], envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "pnpm-backed vp install applies the same managed-pnpm policy" }, + { argv = ["vp", "install"], cwd = "child", envs = [["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install from a child directory uses the parent package's managed Node.js" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_mixed_runtimes" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "Vite+ must not disable pnpm runtimes that it cannot manage." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":[{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"},{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}]}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves runtime management for a mixed declaration" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same mixed-runtime setting" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_system_first_node" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "pnpm keeps runtime ownership when Vite+ does not manage Node.js." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "off", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves runtime management with system-first Node.js" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same system-first Node.js policy" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_engines_runtime" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "pnpm runtime declarations in engines are part of the same process-wide policy." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}},\"engines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the Deno runtime declared in engines.runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same engines.runtime setting" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_workspace_member" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "A runtime required by any workspace member keeps pnpm runtime management enabled." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "pnpm-workspace.yaml", "packages:\n - packages/*\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "packages/member"], snapshot = false }, + { argv = ["vpt", "write-file", "packages/member/package.json", "{\"name\":\"member\",\"version\":\"1.0.0\",\"devEngines\":{\"runtime\":{\"name\":\"bun\",\"version\":\"1.2.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the member's Bun runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same workspace-wide setting" }, +] + +[[case]] +name = "pnpm_runtime_uses_explicit_working_directory" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "Direct pnpm evaluates runtime ownership in the project selected by -C." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"20.18.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "other"], snapshot = false }, + { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"deno\",\"version\":\"2.0.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "-C", "other", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the target project's Deno runtime remains managed by pnpm" }, + { argv = ["pnpm", "-C=other", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the short equals form resolves the same target project" }, +] + +[[case]] +name = "pnpm_runtime_preserved_for_conflicting_node_versions" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "pnpm keeps runtime ownership when Vite+ selects a different Node.js version." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\",\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"22.11.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", ".node-version", "20.18.0\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "system-bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "off", "pnpm"], snapshot = false }, + { argv = ["pnpm", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "direct pnpm preserves the conflicting Node.js runtime" }, + { argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "vp install preserves the same conflicting runtime" }, +] + +[[case]] +name = "pnpm_runtime_uses_target_node_version" +vp = "global" +seed-runtime = false +skip-platforms = ["windows"] +comment = "The managed Node.js selected for direct pnpm matches the project inspected for runtime ownership." +steps = [ + { argv = ["vpt", "write-file", "package.json", "{\"name\":\"pnpm-runtime-management\",\"private\":true,\"packageManager\":\"pnpm@11.1.0\"}\n"], snapshot = false }, + { argv = ["vpt", "write-file", ".node-version", "20.18.0\n"], snapshot = false }, + { argv = ["vpt", "mkdir", "other"], snapshot = false }, + { argv = ["vpt", "write-file", "other/package.json", "{\"name\":\"other\",\"private\":true,\"devEngines\":{\"runtime\":{\"name\":\"node\",\"version\":\"22.11.0\",\"onFail\":\"download\"}}}\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/20.18.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/js_runtime/node/22.11.0/bin/node", "#!/bin/sh\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/20.18.0/bin/node"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/js_runtime/node/22.11.0/bin/node"], snapshot = false }, + { argv = ["vpt", "write-file", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm", "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf '11.1.0\\n'; else printf 'VP_ACTIVE_NODE=%s\\nPNPM_CONFIG_RUNTIME=%s\\n' \"${VP_ACTIVE_NODE-unset}\" \"${PNPM_CONFIG_RUNTIME-unset}\"; fi\n"], snapshot = false }, + { argv = ["vpt", "chmod", "+x", "$VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm"], snapshot = false }, + { argv = ["vp", "env", "on", "node"], snapshot = false }, + { argv = ["vp", "env", "on", "pnpm"], snapshot = false }, + { argv = ["pnpm", "-C", "other", "install"], envs = [["VP_DEBUG_SHIM", "1"], ["PNPM_CONFIG_RUNTIME", "from-user"]], comment = "the target Node.js version and runtime opt-out are applied together" }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md new file mode 100644 index 0000000000..adaca1b2e6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_disabled_for_managed_node.md @@ -0,0 +1,138 @@ +# pnpm_runtime_disabled_for_managed_node + +Vite+ owns Node.js runtime management independently from the selected pnpm binary. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct system-first pnpm disables its duplicate Node.js runtime + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +pnpm-backed vp install applies the same system-first policy + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=false +``` + +## `vpt write-file $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm '#'\!'/bin/sh +if [ "$1" = "--version" ]; then printf '\''11.1.0\n'\''; else printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}"; fi +'` + + +## `vpt write-file $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx '#'\!'/bin/sh +printf '\''PNPM_CONFIG_RUNTIME=%s\n'\'' "${PNPM_CONFIG_RUNTIME-unset}" +'` + + +## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm` + + +## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpx` + + +## `vpt mkdir other` + + +## `vpt write-file other/package.json '{"name":"other","private":true,"devEngines":{"runtime":{"name":"deno","version":"2.0.0","onFail":"download"}}} +'` + + +## `vpt mkdir child` + + +## `vp env on pnpm` + + +## `PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct managed pnpm disables its duplicate Node.js runtime + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpx package` + +the pnpx alias receives the same child-only override + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpx package --dir other` + +pnpx forwards --dir to the downloaded command without changing runtime ownership + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpm exec package --dir other` + +pnpm exec forwards --dir after the command without changing runtime ownership + +``` +PNPM_CONFIG_RUNTIME=false +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpm runtime set deno 2` + +an explicit runtime command keeps pnpm runtime management enabled + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PNPM_CONFIG_RUNTIME=from-user pnpm add bun@runtime:1.2.0` + +an explicit runtime dependency keeps pnpm runtime management enabled + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PNPM_CONFIG_RUNTIME=from-user vp install` + +pnpm-backed vp install applies the same managed-pnpm policy + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=false +``` + +## `cd child && PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install from a child directory uses the parent package's managed Node.js + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=false +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md new file mode 100644 index 0000000000..302c0f46fa --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_conflicting_node_versions.md @@ -0,0 +1,46 @@ +# pnpm_runtime_preserved_for_conflicting_node_versions + +pnpm keeps runtime ownership when Vite+ selects a different Node.js version. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"22.11.0","onFail":"download"}}} +'` + + +## `vpt write-file .node-version '20.18.0 +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves the conflicting Node.js runtime + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same conflicting runtime + +``` +VITE+ - The Unified Toolchain for the Web + +warning: Node.js version 20.18.0 (from .node-version) does not satisfy devEngines.runtime constraint '22.11.0' +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md new file mode 100644 index 0000000000..4d8540bfd7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_engines_runtime.md @@ -0,0 +1,41 @@ +# pnpm_runtime_preserved_for_engines_runtime + +pnpm runtime declarations in engines are part of the same process-wide policy. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}},"engines":{"runtime":{"name":"deno","version":"2.0.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves the Deno runtime declared in engines.runtime + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same engines.runtime setting + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md new file mode 100644 index 0000000000..467ea77646 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_mixed_runtimes.md @@ -0,0 +1,41 @@ +# pnpm_runtime_preserved_for_mixed_runtimes + +Vite+ must not disable pnpm runtimes that it cannot manage. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":[{"name":"node","version":"20.18.0","onFail":"download"},{"name":"deno","version":"2.0.0","onFail":"download"}]}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves runtime management for a mixed declaration + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same mixed-runtime setting + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md new file mode 100644 index 0000000000..bda7460ffa --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_system_first_node.md @@ -0,0 +1,37 @@ +# pnpm_runtime_preserved_for_system_first_node + +pnpm keeps runtime ownership when Vite+ does not manage Node.js. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt chmod +x system-bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env off node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm install` + +direct pnpm preserves runtime management with system-first Node.js + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user vp install` + +vp install preserves the same system-first Node.js policy + +``` +VITE+ - The Unified Toolchain for the Web + +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md new file mode 100644 index 0000000000..e09562bc51 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_preserved_for_workspace_member.md @@ -0,0 +1,22 @@ +# pnpm_runtime_preserved_for_workspace_member + +A runtime required by any workspace member keeps pnpm runtime management enabled. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt write-file pnpm-workspace.yaml 'packages: + - packages/* +'` + + +## `vpt mkdir packages/member` + +**Exit code:** 1 + +``` +No such file or directory (os error 2) +``` + +*(remaining steps skipped: step failed)* diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md new file mode 100644 index 0000000000..355be2cbe6 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_explicit_working_directory.md @@ -0,0 +1,46 @@ +# pnpm_runtime_uses_explicit_working_directory + +Direct pnpm evaluates runtime ownership in the project selected by -C. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0","devEngines":{"runtime":{"name":"node","version":"20.18.0","onFail":"download"}}} +'` + + +## `vpt mkdir other` + + +## `vpt write-file other/package.json '{"name":"other","private":true,"devEngines":{"runtime":{"name":"deno","version":"2.0.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x system-bin/pnpm` + + +## `vp env on node` + + +## `vp env off pnpm` + + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm -C other install` + +the target project's Deno runtime remains managed by pnpm + +``` +PNPM_CONFIG_RUNTIME=from-user +``` + +## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} PNPM_CONFIG_RUNTIME=from-user pnpm -C=other install` + +the short equals form resolves the same target project + +``` +PNPM_CONFIG_RUNTIME=from-user +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md new file mode 100644 index 0000000000..0426534731 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/snapshots/pnpm_runtime_uses_target_node_version.md @@ -0,0 +1,55 @@ +# pnpm_runtime_uses_target_node_version + +The managed Node.js selected for direct pnpm matches the project inspected for runtime ownership. + +## `vpt write-file package.json '{"name":"pnpm-runtime-management","private":true,"packageManager":"pnpm@11.1.0"} +'` + + +## `vpt write-file .node-version '20.18.0 +'` + + +## `vpt mkdir other` + + +## `vpt write-file other/package.json '{"name":"other","private":true,"devEngines":{"runtime":{"name":"node","version":"22.11.0","onFail":"download"}}} +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/20.18.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt write-file $VP_HOME/js_runtime/node/22.11.0/bin/node '#'\!'/bin/sh +'` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/20.18.0/bin/node` + + +## `vpt chmod +x $VP_HOME/js_runtime/node/22.11.0/bin/node` + + +## `vpt write-file $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm '#'\!'/bin/sh +if [ "$1" = "--version" ]; then printf '\''11.1.0\n'\''; else printf '\''VP_ACTIVE_NODE=%s\nPNPM_CONFIG_RUNTIME=%s\n'\'' "${VP_ACTIVE_NODE-unset}" "${PNPM_CONFIG_RUNTIME-unset}"; fi +'` + + +## `vpt chmod +x $VP_HOME/package_manager/pnpm/11.1.0/pnpm/bin/pnpm` + + +## `vp env on node` + + +## `vp env on pnpm` + + +## `VP_DEBUG_SHIM=1 PNPM_CONFIG_RUNTIME=from-user pnpm -C other install` + +the target Node.js version and runtime opt-out are applied together + +``` +VP_ACTIVE_NODE=22.11.0 +PNPM_CONFIG_RUNTIME=false +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node new file mode 100644 index 0000000000..c743bd241e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/node @@ -0,0 +1,2 @@ +#!/bin/sh +printf 'v20.18.0\n' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm new file mode 100644 index 0000000000..4ccecfce59 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/pnpm_runtime_management/system-bin/pnpm @@ -0,0 +1,6 @@ +#!/bin/sh +if [ "$1" = "--version" ]; then + printf '10.18.0\n' +else + printf 'PNPM_CONFIG_RUNTIME=%s\n' "${PNPM_CONFIG_RUNTIME-unset}" +fi diff --git a/crates/vp_global_cli/src/cli.rs b/crates/vp_global_cli/src/cli.rs index cf61829a7e..05af145040 100644 --- a/crates/vp_global_cli/src/cli.rs +++ b/crates/vp_global_cli/src/cli.rs @@ -3,7 +3,11 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{collections::HashSet, ffi::OsStr, process::ExitStatus}; +use std::{ + collections::{BTreeMap, HashSet}, + ffi::OsStr, + process::ExitStatus, +}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; @@ -675,22 +679,45 @@ async fn run_package_manager_command( commands::prepend_js_runtime_to_path_env(&cwd).await?; let selected = commands::env::package_manager::resolve_current_spec(&cwd).await?; + let config = commands::env::config::load_config().await?; + let mut package_manager_env = BTreeMap::new(); + if matches!(&command, PackageManagerCommand::Install(_)) + && selected.as_ref().is_some_and(|selected| { + selected.package_manager_type == vp_pm_cli::PackageManagerType::Pnpm + }) + && commands::pnpm_runtime::should_disable(&cwd, config.node_shim_mode).await? + { + package_manager_env.insert( + commands::pnpm_runtime::PNPM_CONFIG_RUNTIME.to_string(), + commands::pnpm_runtime::PNPM_CONFIG_RUNTIME_DISABLED.to_string(), + ); + } let result = if let Some(selected) = selected.as_ref() - && commands::env::config::load_config() - .await? - .package_manager_shim_mode_for(selected.package_manager_type) + && config.package_manager_shim_mode_for(selected.package_manager_type) == commands::env::config::ShimMode::SystemFirst && let Some(system_path) = crate::shim::dispatch::find_system_tool(&selected.package_manager_type.to_string()) && let Some(manager) = system_package_manager(selected.package_manager_type, &system_path).await { - vp_pm_cli::dispatch_with_resolved_package_manager(&cwd, command, manager).await? + vp_pm_cli::dispatch_with_resolved_package_manager_and_env( + &cwd, + command, + manager, + &package_manager_env, + ) + .await? } else { let selected = commands::env::package_manager::resolve_current(&cwd).await?; match selected { Some(selected) => { - vp_pm_cli::dispatch_with_package_manager(&cwd, command, &selected).await? + vp_pm_cli::dispatch_with_package_manager_and_env( + &cwd, + command, + &selected, + &package_manager_env, + ) + .await? } None => vp_pm_cli::dispatch_with_metadata(&cwd, command).await?, } diff --git a/crates/vp_global_cli/src/commands/mod.rs b/crates/vp_global_cli/src/commands/mod.rs index 007d351332..717b82eaa3 100644 --- a/crates/vp_global_cli/src/commands/mod.rs +++ b/crates/vp_global_cli/src/commands/mod.rs @@ -111,8 +111,8 @@ pub(crate) fn warn_missing_local_cli_if_project(cwd: &AbsolutePath) { /// Select the configured JS runtime and prepend its bin directory to PATH. /// This should be called before executing any package manager command. /// -/// If `project_path` contains a package.json, uses the project's runtime -/// (based on devEngines.runtime). Otherwise, falls back to the CLI's runtime. +/// If `project_path` is inside a package, uses the nearest package's runtime. +/// Otherwise, falls back to the CLI's runtime. pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Result<(), Error> { let config = env::config::load_config().await?; let mut executor = JsExecutor::new(None); @@ -123,12 +123,12 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu { bin_dir.to_absolute_path_buf() } else { - // Use project runtime if package.json exists, otherwise use CLI runtime - let package_json_path = project_path.join("package.json"); - let runtime = if package_json_path.as_path().exists() { - executor.ensure_project_runtime(project_path).await? - } else { - executor.ensure_cli_runtime().await? + let runtime = match vt_workspace::find_package_root(project_path) { + Ok(package) => executor.ensure_project_runtime(package.path).await?, + Err(vt_workspace::Error::PackageJsonNotFound(_)) => { + executor.ensure_cli_runtime().await? + } + Err(error) => return Err(error.into()), }; runtime.get_bin_prefix() }; @@ -176,6 +176,7 @@ pub mod config; pub mod create; pub mod hooks; pub mod migrate; +pub(crate) mod pnpm_runtime; pub mod staged; pub mod toolchain; pub mod version; diff --git a/crates/vp_global_cli/src/commands/pnpm_runtime.rs b/crates/vp_global_cli/src/commands/pnpm_runtime.rs new file mode 100644 index 0000000000..f4738d3f9e --- /dev/null +++ b/crates/vp_global_cli/src/commands/pnpm_runtime.rs @@ -0,0 +1,110 @@ +use std::path::Path; + +use vt_path::{AbsolutePath, AbsolutePathBuf}; + +use super::env::config::{self, ShimMode}; +use crate::error::Error; + +pub(crate) const PNPM_CONFIG_RUNTIME: &str = "PNPM_CONFIG_RUNTIME"; +pub(crate) const PNPM_CONFIG_RUNTIME_DISABLED: &str = "false"; + +const FORWARDED_ARGUMENT_COMMANDS: &[&str] = + &["create", "dlx", "exec", "restart", "run", "start", "stop", "test"]; + +pub(crate) fn command_cwd( + tool: &str, + cwd: &AbsolutePath, + args: &[String], +) -> Option { + // pnpx forwards every argument to the downloaded command. + if tool != "pnpm" { + return Some(cwd.to_absolute_path_buf()); + } + + let mut dir = None; + let mut args = args.iter(); + while let Some(arg) = args.next() { + if arg == "--" { + break; + } + if FORWARDED_ARGUMENT_COMMANDS.contains(&arg.as_str()) { + break; + } + if arg == "-C" || arg == "--dir" { + dir = Some(args.next()?.as_str()); + } else if let Some(value) = arg.strip_prefix("-C=") { + dir = Some(value); + } else if let Some(value) = arg.strip_prefix("--dir=") { + dir = Some(value); + } + } + + let Some(dir) = dir else { return Some(cwd.to_absolute_path_buf()) }; + if dir.is_empty() { + return None; + } + let path = Path::new(dir); + if path.is_absolute() { AbsolutePathBuf::new(path.to_path_buf()) } else { Some(cwd.join(path)) } +} + +pub(crate) fn explicitly_manages_runtime(args: &[String]) -> bool { + args.windows(2).any(|args| args[0] == "runtime" && args[1] == "set") + || args.iter().any(|arg| arg.contains("@runtime:")) +} + +pub(crate) async fn should_disable( + cwd: &AbsolutePath, + node_shim_mode: ShimMode, +) -> Result { + if node_shim_mode != ShimMode::Managed { + return Ok(false); + } + + let workspace = match vt_workspace::find_workspace_root(cwd) { + Ok((workspace, _)) => workspace, + Err(vt_workspace::Error::PackageJsonNotFound(_)) => return Ok(false), + Err(error) => return Err(error.into()), + }; + let packages = vt_workspace::load_package_graph(&workspace)?; + let mut has_managed_node = false; + let mut node_requirements = Vec::new(); + + // pnpm converts both fields from every workspace manifest into runtime dependencies. + for package in packages.node_weights() { + let content = tokio::fs::read_to_string(package.absolute_path.join("package.json")).await?; + let package_json: serde_json::Value = serde_json::from_str(&content)?; + for engines in ["devEngines", "engines"] { + let Some(runtime) = package_json.get(engines).and_then(|value| value.get("runtime")) + else { + continue; + }; + let entries = + runtime.as_array().map_or_else(|| std::slice::from_ref(runtime), Vec::as_slice); + for entry in entries { + let Some(name) = entry.get("name").and_then(serde_json::Value::as_str) else { + continue; + }; + if name != "node" { + return Ok(false); + } + has_managed_node = has_managed_node || engines == "devEngines"; + if let Some(version) = entry.get("version").and_then(serde_json::Value::as_str) { + node_requirements.push(version.to_string()); + } + } + } + } + + // The opt-out filters runtime entries for every workspace importer, so the + // selected Node.js must satisfy each workspace declaration that it replaces. + if !has_managed_node { + return Ok(false); + } + let selected = config::resolve_version(cwd).await?; + let Ok(selected) = node_semver::Version::parse(&selected.version) else { + return Ok(false); + }; + Ok(node_requirements.iter().all(|requirement| { + node_semver::Range::parse(requirement).is_ok_and(|range| range.satisfies(&selected)) + })) +} diff --git a/crates/vp_global_cli/src/shim/dispatch.rs b/crates/vp_global_cli/src/shim/dispatch.rs index 0c67e85c1b..5802d9a664 100644 --- a/crates/vp_global_cli/src/shim/dispatch.rs +++ b/crates/vp_global_cli/src/shim/dispatch.rs @@ -23,6 +23,7 @@ use crate::{ package_metadata::PackageMetadata, }, global::install::is_protected_shim, + pnpm_runtime, }, error::Error, }; @@ -737,12 +738,53 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { // Check shim mode from config let shim_mode = load_shim_mode(tool).await; + let (disable_pnpm_runtime, pnpm_cwd) = + if PackageManagerType::from_tool(tool) == Some(PackageManagerType::Pnpm) { + let cwd = match current_dir() { + Ok(path) => path, + Err(error) => { + eprintln!("vp: Failed to get current directory: {error}"); + return 1; + } + }; + let node_shim_mode = match config::load_config().await { + Ok(config) => config.node_shim_mode, + Err(error) => { + eprintln!("vp: Failed to load Node.js shim mode: {error}"); + return 1; + } + }; + let command_cwd = pnpm_runtime::command_cwd(tool, &cwd, args); + let disable = if pnpm_runtime::explicitly_manages_runtime(args) { + Ok(false) + } else if let Some(cwd) = command_cwd.as_deref() { + pnpm_runtime::should_disable(cwd, node_shim_mode).await + } else { + Ok(false) + }; + match disable { + Ok(disable) => (disable, command_cwd), + Err(error) => { + eprintln!("vp: Failed to resolve pnpm runtime management: {error}"); + return 1; + } + } + } else { + (false, None) + }; + let pnpm_node_cwd = pnpm_cwd.as_deref().filter(|_| disable_pnpm_runtime); + let pnpm_runtime_env = if disable_pnpm_runtime { + &[(pnpm_runtime::PNPM_CONFIG_RUNTIME, pnpm_runtime::PNPM_CONFIG_RUNTIME_DISABLED)][..] + } else { + &[] + }; if shim_mode == ShimMode::SystemFirst { tracing::debug!("system-first mode enabled"); // In system-first mode, try to find system tool first if let Some(system_path) = find_system_tool(tool) { if PackageManagerType::from_tool(tool).is_some() - && let Err(error) = prepare_node_path_for_system_package_manager().await + && let Err(error) = + prepare_node_path_for_system_package_manager(pnpm_node_cwd).await { eprintln!("vp: Failed to prepare Node.js for system package manager: {error}"); return 1; @@ -764,7 +806,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { std::env::set_var(env_vars::VP_BYPASS, bypass_val); } } - return exec::exec_tool(&system_path, args); + return exec::exec_tool_with_env(&system_path, args, pnpm_runtime_env); } // Fall through to managed if system not found } @@ -783,6 +825,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { return 1; } }; + let node_cwd = pnpm_node_cwd.map_or_else(|| cwd.clone(), AbsolutePath::to_absolute_path_buf); // Ensure Node.js is installed and locate its binary for PATH preparation. // Package-manager shims can use their own declared version, but JS-based @@ -802,7 +845,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { None }; let resolution = if system_node.is_none() { - match resolve_with_cache(&cwd).await { + match resolve_with_cache(&node_cwd).await { Ok(resolution) => Some(resolution), Err(error) => { eprintln!("vp: Failed to resolve Node version: {error}"); @@ -930,7 +973,7 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 { } // Execute the tool (normal path — exec replaces process on Unix) - exec::exec_tool(&tool_path, args) + exec::exec_tool_with_env(&tool_path, args, pnpm_runtime_env) } fn node_prefix_from_binary(node_path: &AbsolutePath) -> AbsolutePathBuf { @@ -949,7 +992,9 @@ fn read_node_version(node_path: &AbsolutePath) -> Option { .then(|| String::from_utf8_lossy(&output.stdout).trim().trim_start_matches('v').to_string()) } -async fn prepare_node_path_for_system_package_manager() -> Result<(), Error> { +async fn prepare_node_path_for_system_package_manager( + pnpm_cwd: Option<&AbsolutePath>, +) -> Result<(), Error> { let config = config::load_config().await?; if config.node_shim_mode == ShimMode::SystemFirst && let Some(node) = find_system_tool("node") @@ -959,7 +1004,10 @@ async fn prepare_node_path_for_system_package_manager() -> Result<(), Error> { return Ok(()); } - let cwd = current_dir()?; + let cwd = match pnpm_cwd { + Some(cwd) => cwd.to_absolute_path_buf(), + None => current_dir()?, + }; let resolution = resolve_with_cache(&cwd).await.map_err(|error| Error::Other(error.into()))?; let node = ensure_installed(&resolution.version).await.map_err(|error| Error::Other(error.into()))?; diff --git a/crates/vp_global_cli/src/shim/exec.rs b/crates/vp_global_cli/src/shim/exec.rs index 2fe5d0b7f9..fdca5e7b2e 100644 --- a/crates/vp_global_cli/src/shim/exec.rs +++ b/crates/vp_global_cli/src/shim/exec.rs @@ -24,8 +24,12 @@ fn sync_child_pwd(cmd: &mut std::process::Command) { /// Unlike `exec_tool()`, this does NOT replace the current process on Unix, /// allowing the caller to run code after the tool exits. pub fn spawn_tool(path: &AbsolutePath, args: &[String]) -> i32 { + spawn_tool_with_env(path, args, &[]) +} + +fn spawn_tool_with_env(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { let mut cmd = std::process::Command::new(path.as_path()); - cmd.args(args); + cmd.args(args).envs(env.iter().copied()); sync_child_pwd(&mut cmd); match cmd.status() { Ok(status) => exit_code_from_status(status), @@ -40,24 +44,29 @@ pub fn spawn_tool(path: &AbsolutePath, args: &[String]) -> i32 { /// /// Returns an exit code on Windows or if exec fails on Unix. pub fn exec_tool(path: &AbsolutePath, args: &[String]) -> i32 { + exec_tool_with_env(path, args, &[]) +} + +/// Execute a tool with child-only environment overrides. +pub fn exec_tool_with_env(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { #[cfg(unix)] { - exec_unix(path, args) + exec_unix(path, args, env) } #[cfg(windows)] { - exec_windows(path, args) + exec_windows(path, args, env) } } /// Unix: Use exec to replace the current process. #[cfg(unix)] -fn exec_unix(path: &AbsolutePath, args: &[String]) -> i32 { +fn exec_unix(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { use std::os::unix::process::CommandExt; let mut cmd = std::process::Command::new(path.as_path()); - cmd.args(args); + cmd.args(args).envs(env.iter().copied()); sync_child_pwd(&mut cmd); // exec replaces the current process - this only returns on error @@ -68,6 +77,6 @@ fn exec_unix(path: &AbsolutePath, args: &[String]) -> i32 { /// Windows: Spawn the process and wait for completion. #[cfg(windows)] -fn exec_windows(path: &AbsolutePath, args: &[String]) -> i32 { - spawn_tool(path, args) +fn exec_windows(path: &AbsolutePath, args: &[String], env: &[(&str, &str)]) -> i32 { + spawn_tool_with_env(path, args, env) } diff --git a/crates/vp_pm_cli/src/dispatch.rs b/crates/vp_pm_cli/src/dispatch.rs index be89973c4e..e9e6d7176c 100644 --- a/crates/vp_pm_cli/src/dispatch.rs +++ b/crates/vp_pm_cli/src/dispatch.rs @@ -3,7 +3,7 @@ //! Callers must perform any environment setup (PATH adjustments, runtime //! download) before invoking [`dispatch`]. -use std::process::ExitStatus; +use std::{collections::BTreeMap, process::ExitStatus}; use vt_path::AbsolutePath; @@ -13,7 +13,7 @@ use crate::{ download_package_manager, error::Error, helpers::build_package_manager_or_npm_default, - resolution::{DlxArgs, run_resolution}, + resolution::{DlxArgs, Resolution, run_resolution}, }; #[derive(Debug)] @@ -39,7 +39,7 @@ pub async fn dispatch_with_metadata( cwd: &AbsolutePath, command: PackageManagerCommand, ) -> Result { - dispatch_with_manager(cwd, command, ManagerSource::Detect).await + dispatch_with_manager(cwd, command, ManagerSource::Detect, None).await } pub async fn dispatch_with_package_manager( @@ -47,7 +47,17 @@ pub async fn dispatch_with_package_manager( command: PackageManagerCommand, package_manager: &EnvironmentPackageManagerResolution, ) -> Result { - dispatch_with_manager(cwd, command, ManagerSource::Environment(package_manager)).await + dispatch_with_manager(cwd, command, ManagerSource::Environment(package_manager), None).await +} + +pub async fn dispatch_with_package_manager_and_env( + cwd: &AbsolutePath, + command: PackageManagerCommand, + package_manager: &EnvironmentPackageManagerResolution, + env: &BTreeMap, +) -> Result { + dispatch_with_manager(cwd, command, ManagerSource::Environment(package_manager), Some(env)) + .await } pub async fn dispatch_with_resolved_package_manager( @@ -55,25 +65,38 @@ pub async fn dispatch_with_resolved_package_manager( command: PackageManagerCommand, manager: PackageManager, ) -> Result { - dispatch_with_manager(cwd, command, ManagerSource::Resolved(manager)).await + dispatch_with_manager(cwd, command, ManagerSource::Resolved(manager), None).await +} + +pub async fn dispatch_with_resolved_package_manager_and_env( + cwd: &AbsolutePath, + command: PackageManagerCommand, + manager: PackageManager, + env: &BTreeMap, +) -> Result { + dispatch_with_manager(cwd, command, ManagerSource::Resolved(manager), Some(env)).await } async fn dispatch_with_manager( cwd: &AbsolutePath, command: PackageManagerCommand, source: ManagerSource<'_>, + env: Option<&BTreeMap>, ) -> Result { let render_diagnostics = command.should_render_diagnostics(); let command = match command { PackageManagerCommand::Dlx(args) => { let manager = match source { - ManagerSource::Detect => return dispatch_dlx(cwd, args, render_diagnostics).await, + ManagerSource::Detect => { + return dispatch_dlx(cwd, args, render_diagnostics, env).await; + } ManagerSource::Environment(package_manager) => { build_selected_package_manager(package_manager).await? } ManagerSource::Resolved(manager) => manager, }; - let resolution = PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?; + let resolution = + with_env(PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?, env); let status = run_resolution(cwd, resolution, render_diagnostics).await?; return Ok(DispatchResult { status, why_hint_packages: None }); } @@ -89,7 +112,7 @@ async fn dispatch_with_manager( }; let package_manager = manager.client; let why_hint_packages = command.why_hint_packages(package_manager).map(<[String]>::to_vec); - let resolution = command.resolve_for_manager(&manager)?; + let resolution = with_env(command.resolve_for_manager(&manager)?, env); let status = run_resolution(cwd, resolution, render_diagnostics).await?; Ok(DispatchResult { status, why_hint_packages }) } @@ -107,20 +130,29 @@ async fn build_selected_package_manager( Ok(PackageManager::from_install_dir(package_manager.package_manager_type, version, install_dir)) } +fn with_env(mut resolution: Resolution, env: Option<&BTreeMap>) -> Resolution { + if let Some(env) = env { + resolution.extend_env(env); + } + resolution +} + async fn dispatch_dlx( cwd: &AbsolutePath, args: DlxArgs, render_diagnostics: bool, + env: Option<&BTreeMap>, ) -> Result { match PackageManager::builder(cwd).build_with_default().await { Ok(manager) => { - let resolution = PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?; + let resolution = + with_env(PackageManagerCommand::Dlx(args).resolve_for_manager(&manager)?, env); let status = run_resolution(cwd, resolution, render_diagnostics).await?; Ok(DispatchResult { status, why_hint_packages: None }) } Err(vp_error::Error::WorkspaceError(vt_workspace::Error::PackageJsonNotFound(_))) => { - let status = - run_resolution(cwd, args.resolve_npx_fallback(), render_diagnostics).await?; + let resolution = with_env(args.resolve_npx_fallback(), env); + let status = run_resolution(cwd, resolution, render_diagnostics).await?; Ok(DispatchResult { status, why_hint_packages: None }) } Err(error) => Err(Error::Install(error)), diff --git a/crates/vp_pm_cli/src/lib.rs b/crates/vp_pm_cli/src/lib.rs index fe0ec60df7..9fe6707b11 100644 --- a/crates/vp_pm_cli/src/lib.rs +++ b/crates/vp_pm_cli/src/lib.rs @@ -21,7 +21,8 @@ pub use cli::{ManagedGlobalCommand, PackageManagerCommand, PmCommand}; pub use config::npm_registry; pub use dispatch::{ DispatchResult, dispatch, dispatch_with_metadata, dispatch_with_package_manager, - dispatch_with_resolved_package_manager, + dispatch_with_package_manager_and_env, dispatch_with_resolved_package_manager, + dispatch_with_resolved_package_manager_and_env, }; pub use error::Error; pub use package_manager::{ diff --git a/crates/vp_pm_cli/src/resolution/resolve.rs b/crates/vp_pm_cli/src/resolution/resolve.rs index 896d1a461f..6c342f416c 100644 --- a/crates/vp_pm_cli/src/resolution/resolve.rs +++ b/crates/vp_pm_cli/src/resolution/resolve.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use semver::Version; use crate::{ @@ -13,6 +15,14 @@ pub(crate) struct Resolution { pub(crate) diagnostics: Diagnostics, } +impl Resolution { + pub(crate) fn extend_env(&mut self, env: &BTreeMap) { + if let CommandResolution::Run(command) = &mut self.outcome { + command.env.extend(env.clone()); + } + } +} + pub(crate) trait Resolve: PackageManagerDialect { fn resolve(&self, args: &A, diag: &mut Diagnostics) -> CommandResolution; } diff --git a/docs/guide/env.md b/docs/guide/env.md index 4a7e5d6142..32950f1c61 100644 --- a/docs/guide/env.md +++ b/docs/guide/env.md @@ -74,6 +74,8 @@ vp env off This switches both components to system-first mode. Vite+ prefers system tools and falls back to managed installations. Mixed configurations compose: a system package-manager launcher receives the Node.js selected by the Node mode. +When Node.js is managed and a pnpm project declares only Node.js in `devEngines.runtime`, Vite+ passes `PNPM_CONFIG_RUNTIME=false` to pnpm so it does not install a duplicate runtime. This also applies when pnpm is system-first because the Node.js and package-manager modes are independent. Vite+ leaves pnpm runtime management unchanged for Bun, Deno, and mixed runtime declarations. + Using `pm` records the selected mode for all currently supported package managers and replaces their individual choices. An unscoped `on` or `off` does the same while also changing Node.js. A family without a recorded mode remains undecided until its shim is first used or an `on` / `off` command configures it. ## Commands