diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/assert-runtime.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/assert-runtime.cjs new file mode 100644 index 0000000000..fab425d07f --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/assert-runtime.cjs @@ -0,0 +1,23 @@ +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const { chmodSync, copyFileSync, mkdirSync } = require('node:fs'); +const path = require('node:path'); + +const runtimeDir = path.resolve('custom-runtime'); +mkdirSync(runtimeDir); +const runtime = path.join(runtimeDir, process.platform === 'win32' ? 'custom node.exe' : 'custom node'); +copyFileSync(process.execPath, runtime); +chmodSync(runtime, 0o755); +const vp = path.join(path.dirname(require.resolve('vite-plus/package.json')), 'bin', 'vp'); +const env = { ...process.env }; +for (const key of Object.keys(env)) { + if (key.toUpperCase() === 'PATH') delete env[key]; +} +env.PATH = runtimeDir; + +for (const tool of ['lint', 'fmt']) { + const child = spawnSync(runtime, [vp, tool, '--version'], { env, encoding: 'utf8', timeout: 30000 }); + assert.equal(child.status, 0, child.stderr || child.error?.message); + assert.match(child.stdout, /\d+\.\d+\.\d+/); + console.log(`${tool} reused the current runtime without node on PATH`); +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/package.json new file mode 100644 index 0000000000..5782eb19e3 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/package.json @@ -0,0 +1,4 @@ +{ + "name": "current-node-runtime", + "private": true +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/snapshots.toml new file mode 100644 index 0000000000..dc36d01ad7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/snapshots.toml @@ -0,0 +1,6 @@ +[[case]] +name = "builtin_tools_without_node_on_path" +vp = "local" +steps = [ + { argv = ["node", "assert-runtime.cjs"], comment = "Built-in tools reuse the current runtime even when its filename is not node and PATH has no node executable." }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/snapshots/builtin_tools_without_node_on_path.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/snapshots/builtin_tools_without_node_on_path.md new file mode 100644 index 0000000000..ed0e438ac7 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/current_node_runtime/snapshots/builtin_tools_without_node_on_path.md @@ -0,0 +1,10 @@ +# builtin_tools_without_node_on_path + +## `node assert-runtime.cjs` + +Built-in tools reuse the current runtime even when its filename is not node and PATH has no node executable. + +``` +lint reused the current runtime without node on PATH +fmt reused the current runtime without node on PATH +``` diff --git a/packages/cli/binding/index.d.cts b/packages/cli/binding/index.d.cts index 7e637156df..d49516cf0b 100644 --- a/packages/cli/binding/index.d.cts +++ b/packages/cli/binding/index.d.cts @@ -3456,6 +3456,8 @@ export interface BatchRewriteResult { /** Configuration options passed from JavaScript to Rust. */ export interface CliOptions { + /** The current JavaScript runtime (`process.execPath`). */ + nodeExecPath: string; lint: (err: Error | null, arg: JsCommandContext) => Promise; fmt: (err: Error | null, arg: JsCommandContext) => Promise; vite: (err: Error | null, arg: JsCommandContext) => Promise; diff --git a/packages/cli/binding/src/cli/resolver.rs b/packages/cli/binding/src/cli/resolver.rs index 0063540873..7d29aa4910 100644 --- a/packages/cli/binding/src/cli/resolver.rs +++ b/packages/cli/binding/src/cli/resolver.rs @@ -93,7 +93,7 @@ impl SubcommandResolver { } Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from("--disable-warning=MODULE_TYPELESS_PACKAGE_JSON")) .chain(iter::once(Str::from(js_path_str))) .chain(args.into_iter().map(Str::from)) @@ -130,7 +130,7 @@ impl SubcommandResolver { } Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)) .chain(args.into_iter().map(Str::from)) .collect(), @@ -152,7 +152,7 @@ impl SubcommandResolver { .ok_or_else(|| anyhow::anyhow!("vite JS path is not valid UTF-8"))?; Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)) .chain(iter::once(Str::from("build"))) .chain(args.into_iter().map(Str::from)) @@ -187,7 +187,7 @@ impl SubcommandResolver { }; Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)).chain(vitest_args).collect(), cache_config: UserCacheConfig::with_config(EnabledCacheConfig { env: None, @@ -213,7 +213,7 @@ impl SubcommandResolver { .ok_or_else(|| anyhow::anyhow!("pack JS path is not valid UTF-8"))?; Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)) .chain(args.into_iter().map(Str::from)) .collect(), @@ -235,7 +235,7 @@ impl SubcommandResolver { .ok_or_else(|| anyhow::anyhow!("vite JS path is not valid UTF-8"))?; Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)) .chain(iter::once(Str::from("dev"))) .chain(args.into_iter().map(Str::from)) @@ -253,7 +253,7 @@ impl SubcommandResolver { .ok_or_else(|| anyhow::anyhow!("vite JS path is not valid UTF-8"))?; Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)) .chain(iter::once(Str::from("preview"))) .chain(args.into_iter().map(Str::from)) @@ -271,7 +271,7 @@ impl SubcommandResolver { .ok_or_else(|| anyhow::anyhow!("doc JS path is not valid UTF-8"))?; Ok(ResolvedSubcommand { - program: Arc::from(OsStr::new("node")), + program: Arc::clone(&cli_options.node_exec_path), args: iter::once(Str::from(js_path_str)) .chain(args.into_iter().map(Str::from)) .collect(), @@ -343,3 +343,55 @@ fn merge_resolved_envs_with_version( .or_insert_with(|| Arc::from(OsStr::new(env!("CARGO_PKG_VERSION")))); merged } + +#[cfg(test)] +mod tests { + use vt_path::AbsolutePathBuf; + + use super::*; + use crate::cli::types::{BoxedResolverFn, ResolveCommandResult}; + + fn tool_resolver() -> BoxedResolverFn { + Box::new(|_, _| { + Box::pin(async { + Ok(ResolveCommandResult { + bin_path: Arc::from(OsStr::new("tool.js")), + envs: Vec::new(), + }) + }) + }) + } + + #[tokio::test] + async fn builtins_reuse_the_calling_node_runtime() { + let temp = tempfile::tempdir().unwrap(); + let cwd = AbsolutePathBuf::new(temp.path().to_path_buf()).unwrap(); + let runtime: Arc = Arc::from(cwd.join("custom runtime").as_path().as_os_str()); + let resolver = SubcommandResolver::new(cwd.clone().into()).with_cli_options(CliOptions { + node_exec_path: Arc::clone(&runtime), + lint: tool_resolver(), + fmt: tool_resolver(), + vite: tool_resolver(), + test: tool_resolver(), + pack: tool_resolver(), + doc: tool_resolver(), + toolchain_manifest_path: String::new(), + vite_plus_package_path: String::new(), + resolve_universal_vite_config: Arc::new(|_| Box::pin(async { Ok("{}".to_string()) })), + }); + let envs = Arc::new(FxHashMap::default()); + for command in [ + SynthesizableSubcommand::Lint { args: vec![] }, + SynthesizableSubcommand::Fmt { args: vec![] }, + SynthesizableSubcommand::Build { args: vec![] }, + SynthesizableSubcommand::Test { args: vec![] }, + SynthesizableSubcommand::Pack { args: vec![] }, + SynthesizableSubcommand::Dev { args: vec![] }, + SynthesizableSubcommand::Preview { args: vec![] }, + SynthesizableSubcommand::Doc { args: vec![] }, + ] { + let resolved = resolver.resolve(command, None, &envs, &cwd).await.unwrap(); + assert_eq!(resolved.program, runtime); + } + } +} diff --git a/packages/cli/binding/src/cli/types.rs b/packages/cli/binding/src/cli/types.rs index bae2eb3969..612ae29339 100644 --- a/packages/cli/binding/src/cli/types.rs +++ b/packages/cli/binding/src/cli/types.rs @@ -156,6 +156,7 @@ pub type ViteConfigResolverFn = Arc< /// CLI options containing JavaScript resolver functions (using boxed futures for simplicity) pub struct CliOptions { + pub node_exec_path: Arc, pub lint: BoxedResolverFn, pub fmt: BoxedResolverFn, pub vite: BoxedResolverFn, diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index 632ca5ba1c..e195bfb55b 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -67,6 +67,8 @@ pub fn ensure_blocking_stdio() { /// Configuration options passed from JavaScript to Rust. #[napi(object, object_to_js = false)] pub struct CliOptions { + /// The current JavaScript runtime (`process.execPath`). + pub node_exec_path: String, pub lint: Arc>>, pub fmt: Arc>>, pub vite: Arc>>, @@ -199,6 +201,7 @@ pub async fn run(options: CliOptions) -> Result { let explicit_chdir = options.explicit_chdir.unwrap_or(false); let toolchain_manifest_path = options.toolchain_manifest_path; let vite_plus_package_path = options.vite_plus_package_path; + let node_exec_path = Arc::from(OsStr::new(&options.node_exec_path)); // Create a channel to receive the result from the worker thread let (tx, rx) = tokio::sync::oneshot::channel(); @@ -209,6 +212,7 @@ pub async fn run(options: CliOptions) -> Result { std::thread::spawn(move || { // Create the resolvers inside the thread (BoxedResolverFn is not Send) let cli_options = ViteTaskCliOptions { + node_exec_path, lint: create_resolver(lint_tsf, "Failed to resolve lint command"), fmt: create_resolver(fmt_tsf, "Failed to resolve fmt command"), vite: create_resolver(vite_tsf, "Failed to resolve vite command"), diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 8f5865b8ad..38484ab1d4 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -146,6 +146,7 @@ if (maybePrintCommandHelp(args)) { } const exitCode = await run({ + nodeExecPath: process.execPath, lint, pack, fmt,