From 425a7da003c878e72fb3254d2225c3ba3fc5d357 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:57:44 +0800 Subject: [PATCH] feat(run): import the Node interpreter of JetBrains npm configurations (#1144) An npm configuration's own Node interpreter was listed as ignored on import. The import now fills the npm form's Node interpreter field, together with the form's other fields so it shows the configuration as it runs. Jest, Vitest and Node.js configurations still list it as ignored. --- crates/ide-run/src/import/jetbrains.rs | 61 ++++++++++++++++++++---- crates/ide-run/tests/import_jetbrains.rs | 15 +++++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/crates/ide-run/src/import/jetbrains.rs b/crates/ide-run/src/import/jetbrains.rs index 777843a59..6d9dac06b 100644 --- a/crates/ide-run/src/import/jetbrains.rs +++ b/crates/ide-run/src/import/jetbrains.rs @@ -189,6 +189,21 @@ fn json_string(raw: &str) -> String { serde_json::from_str::(raw).unwrap_or_else(|_| raw.to_owned()) } +/// A command line the run configuration forms parse back into `args` (`formatArgs` in the UI): +/// single quotes only where needed. +fn format_args(args: &[String]) -> String { + let quote = |a: &String| { + if a.is_empty() { + "''".to_owned() + } else if a.chars().any(|c| c.is_whitespace() || "'\"\\$`".contains(c)) { + format!("'{}'", a.replace('\'', "'\\''")) + } else { + a.clone() + } + }; + args.iter().map(quote).collect::>().join(" ") +} + /// Regex-escaped for Jest / Vitest `-t`. fn regex_escape(s: &str) -> String { let mut out = String::with_capacity(s.len()); @@ -550,11 +565,20 @@ impl Cx<'_> { // ------------------------------------------------------------- JavaScript - fn node_interpreter(&self, c: &El, ignored: &mut Vec) { - let interpreter = c.value("node-interpreter").or_else(|| c.attr("node-interpreter")); - if interpreter.is_some_and(|i| !i.is_empty() && i != "project") { + /// The configuration's own Node interpreter; `None` for the project's (`project`). + fn node_interpreter(&self, c: &El) -> Res> { + match c.value("node-interpreter").or_else(|| c.attr("node-interpreter")).map(str::trim) { + None | Some("" | "project") => Ok(None), + interpreter => self.text(interpreter), + } + } + + /// Kinds without an interpreter field keep the project's and list the setting as ignored. + fn node_interpreter_ignored(&self, c: &El, ignored: &mut Vec) -> Res<()> { + if self.node_interpreter(c)?.is_some() { ignored.push("node-interpreter".into()); } + Ok(()) } fn npm(&self, name: &str, c: &El, ignored: &mut Vec) -> Res { @@ -590,10 +614,29 @@ impl Cx<'_> { ignored.push("scripts".into()); } } - args.extend(self.words(c.value("arguments"))?); - self.node_interpreter(c, ignored); - let mut rc = config(name, "npm", manager, args, Some("js-debug")); - rc.cwd = working_dir(self.root, &dir.to_string_lossy()).1.map(Into::into); + let arguments = self.words(c.value("arguments"))?; + args.extend(arguments.iter().cloned()); + let interpreter = self.node_interpreter(c)?; + let package_dir = working_dir(self.root, &dir.to_string_lossy()).1; + let mut rc = config(name, "npm", manager.clone(), args, Some("js-debug")); + if let Some(interpreter) = interpreter { + // The npm form's fields, so the interpreter shows there (and the rest still match). + let script = if command == "run" { scripts.first().map(|s| (*s).to_owned()) } else { None }; + let mut params = BTreeMap::from([ + ("packageManager".to_owned(), manager), + ("command".to_owned(), command), + ("script".to_owned(), script.unwrap_or_default()), + ("arguments".to_owned(), format_args(&arguments)), + ( + "packageDir".to_owned(), + package_dir.as_ref().map(|d| d.to_string_lossy().into_owned()).unwrap_or_default(), + ), + ("nodeInterpreter".to_owned(), interpreter), + ]); + params.retain(|_, v| !v.is_empty()); + rc.extras.params = params; + } + rc.cwd = package_dir.map(Into::into); rc.is_test = test; self.envs(c, &mut rc); Ok(rc) @@ -628,7 +671,7 @@ impl Cx<'_> { } _ => {} } - self.node_interpreter(c, ignored); + self.node_interpreter_ignored(c, ignored)?; let mut rc = config(name, "node", "npx".into(), args, Some("js-debug")); rc.cwd = cwd.map(Into::into); rc.is_test = true; @@ -644,7 +687,7 @@ impl Cx<'_> { let mut args = self.words(c.attr("node-parameters"))?; args.push(path_from(self.root, file, &dir)); args.extend(self.args(c.attr("application-parameters"), &dir)?); - self.node_interpreter(c, ignored); + self.node_interpreter_ignored(c, ignored)?; let mut rc = config(name, "node", "node".into(), args, Some("js-debug")); rc.cwd = cwd.map(Into::into); self.envs(c, &mut rc); diff --git a/crates/ide-run/tests/import_jetbrains.rs b/crates/ide-run/tests/import_jetbrains.rs index 9bcee229d..f2f1da94d 100644 --- a/crates/ide-run/tests/import_jetbrains.rs +++ b/crates/ide-run/tests/import_jetbrains.rs @@ -279,7 +279,20 @@ fn workspace_run_manager_skips_templates_and_temporary_configurations() { assert_eq!(dev.args, s(&["run", "dev", "--host", "0.0.0.0"])); assert_eq!(cwd(dev), Some(PathBuf::from("web"))); assert_eq!(dev.env["VITE_API"], "http://localhost:8000"); - assert_eq!(ignored(&r, "dev"), ["node-interpreter", "scripts"]); + // The configuration's Node interpreter goes to the npm form, with the fields it shows. + let params: Vec<(&str, &str)> = dev.extras.params.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); + assert_eq!( + params, + [ + ("arguments", "--host 0.0.0.0"), + ("command", "run"), + ("nodeInterpreter", "/opt/node22/bin/node"), + ("packageDir", "web"), + ("packageManager", "pnpm"), + ("script", "dev"), + ] + ); + assert_eq!(ignored(&r, "dev"), ["scripts"]); // Neither the template nor the temporary `build` is listed as skipped. assert_eq!(r.skipped.len(), 2); }