feat(run): choose the Node interpreter of an npm configuration (#1120)

npm configurations get a "Node interpreter" field; the script and the package manager run on that node (its directory first on the PATH). Empty keeps the project's interpreter from Settings › Languages & Frameworks › Node.js. Saving drops the PATH settings derived before the choice, so a stale project PATH isn't kept.

Closes #997
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-04 08:11:43 +08:00
1 джерело 6bf1d25711
коміт 4c5b6de9ad
4 files changed
+49 -5

No files matched your search

+9 -4
Переглянути файл
@@ -703,8 +703,10 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
{
c.command = pm.to_owned();
}
if let Some(dir) = &node {
path_dirs.push(dir.clone());
// The configuration's own Node interpreter, else the project's.
let own = c.extras.params.get("nodeInterpreter").and_then(|p| setting_path(root, p));
if let Some(dir) = own.and_then(|p| p.parent().map(Path::to_path_buf)).or_else(|| node.clone()) {
path_dirs.push(dir);
}
}
"php" | "phpunit" | "composer" => {
@@ -825,14 +827,17 @@ pub fn without_derived(config: &RunConfig, root: &Path, s: &Settings) -> RunConf
};
let mut out = config.clone();
// Until nothing changes: some entries are derived only once others are gone
// (the virtualenv's `PATH` is added only without `VIRTUAL_ENV`).
// (the virtualenv's `PATH` is added only without `VIRTUAL_ENV`). An entry derived before
// the configuration's own choice (an npm script's Node interpreter) is derived too.
while let Some(key) = out
.env
.iter()
.find(|(key, value)| {
let mut probe = out.clone();
probe.env.remove(*key);
applied(&probe).env.get(*key) == Some(*value)
let mut plain = probe.clone();
plain.extras.params.clear();
applied(&probe).env.get(*key) == Some(*value) || applied(&plain).env.get(*key) == Some(*value)
})
.map(|(key, _)| key.clone())
{
+29
Переглянути файл
@@ -173,6 +173,35 @@ fn run_configs_use_selected_interpreter_and_package_manager() {
assert_eq!(configs[6].args, ["manage.py", "test", "polls"]);
}
#[test]
fn npm_config_runs_on_its_own_node_interpreter() {
if cfg!(windows) {
return; // Unix paths
}
let tmp = tempfile::tempdir().unwrap();
let mut s = Settings::default();
s.languages.node.interpreter = "/opt/node22/bin/node".into();
let mut own = run("npm", "npm", &["run", "dev"]);
own.extras.params.insert("nodeInterpreter".into(), "/opt/node18/bin/node".into());
let mut configs = vec![own, run("npm", "npm", &["run", "build"])];
apply_to_run_configs(&mut configs, tmp.path(), &s);
let first = |c: &RunConfig| c.env["PATH"].split(':').next().unwrap().to_owned();
assert_eq!(first(&configs[0]), "/opt/node18/bin");
assert_eq!(first(&configs[1]), "/opt/node22/bin", "the project's Node.js without one");
// Saved without the derived PATH, keeping the choice itself.
let saved = without_derived(&configs[0], tmp.path(), &s);
assert!(!saved.env.contains_key("PATH"));
assert_eq!(saved.extras.params["nodeInterpreter"], "/opt/node18/bin/node");
// Chosen on a configuration loaded with the project's PATH: that PATH isn't kept either.
let mut edited = configs[1].clone();
edited.extras.params.insert("nodeInterpreter".into(), "/opt/node18/bin/node".into());
let saved = without_derived(&edited, tmp.path(), &s);
assert!(!saved.env.contains_key("PATH"));
let mut reloaded = vec![saved];
apply_to_run_configs(&mut reloaded, tmp.path(), &s);
assert_eq!(first(&reloaded[0]), "/opt/node18/bin");
}
fn fake_jdk(dir: &Path) {
std::fs::create_dir_all(dir.join("bin")).unwrap();
std::fs::write(dir.join("bin").join(exe("java")), "").unwrap();
+8
Переглянути файл
@@ -232,6 +232,14 @@ const npm: RunConfigTypeContribution = {
control: "select",
options: PACKAGE_MANAGERS.map((v) => ({ value: v, label: v })),
},
{
key: "nodeInterpreter",
label: "Node interpreter",
mono: true,
browse: "file",
placeholder: "Project default",
hint: "The node executable this script runs on; empty uses Settings › Languages & Frameworks › Node.js.",
},
],
commandLine(p) {
const command = p.command || "run";
+3 -1
Переглянути файл
@@ -5802,5 +5802,7 @@
"Not in Local History: {names}. They are in the Trash.": "本地历史中没有:{names}。它们在回收站中。",
"Show All Commits": "显示所有提交",
"Pin": "固定",
"Unpin": "取消固定"
"Unpin": "取消固定",
"Project default": "项目默认",
"The node executable this script runs on; empty uses Settings › Languages & Frameworks › Node.js.": "运行此脚本的 node 可执行文件;留空则使用“设置 › 语言和框架 › Node.js”中的解释器。"
}