debugger: Fix issues with debugging scripts from package.json (#32995)

- [x] Pass in cwd
- [x] Use the appropriate package manager
- [x] Don't mix up package.json and composer.json

Release Notes:

- debugger: Fixed wrong arguments being passed to the DAP when debugging
scripts from package.json.
This commit is contained in:
Cole Miller 2025-06-19 10:33:24 -04:00 committed by GitHub
parent e914d84f00
commit 0b228ad12c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 182 additions and 153 deletions

View file

@ -31,6 +31,8 @@ use std::{
use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
use crate::PackageJsonData;
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@ -47,12 +49,14 @@ impl ContextProvider for JsonTaskProvider {
file: Option<Arc<dyn language::File>>,
cx: &App,
) -> gpui::Task<Option<TaskTemplates>> {
let Some(file) = project::File::from_dyn(file.as_ref())
.filter(|file| file.path.file_name() == Some("package.json".as_ref()))
.cloned()
else {
let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
return Task::ready(None);
};
let is_package_json = file.path.ends_with("package.json");
let is_composer_json = file.path.ends_with("composer.json");
if !is_package_json && !is_composer_json {
return Task::ready(None);
}
cx.spawn(async move |cx| {
let contents = file
@ -62,21 +66,48 @@ impl ContextProvider for JsonTaskProvider {
.await
.ok()?;
let as_json = serde_json_lenient::Value::from_str(&contents.text).ok()?;
let gutter_tasks = [
TaskTemplate {
let task_templates = if is_package_json {
let package_json = serde_json_lenient::from_str::<
HashMap<String, serde_json_lenient::Value>,
>(&contents.text)
.ok()?;
let package_json = PackageJsonData::new(file.path.clone(), package_json);
let command = package_json.package_manager.unwrap_or("npm").to_owned();
package_json
.scripts
.into_iter()
.map(|(_, key)| TaskTemplate {
label: format!("run {key}"),
command: command.clone(),
args: vec!["run".into(), key],
cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
})
.chain([TaskTemplate {
label: "package script $ZED_CUSTOM_script".to_owned(),
command: "npm".to_owned(),
command: command.clone(),
args: vec![
"--prefix".into(),
"$ZED_DIRNAME".into(),
"run".into(),
VariableName::Custom("script".into()).template_value(),
],
cwd: Some(VariableName::Dirname.template_value()),
tags: vec!["package-script".into()],
..TaskTemplate::default()
},
TaskTemplate {
}])
.collect()
} else if is_composer_json {
serde_json_lenient::Value::from_str(&contents.text)
.ok()?
.get("scripts")?
.as_object()?
.keys()
.map(|key| TaskTemplate {
label: format!("run {key}"),
command: "composer".to_owned(),
args: vec!["-d".into(), "$ZED_DIRNAME".into(), key.into()],
..TaskTemplate::default()
})
.chain([TaskTemplate {
label: "composer script $ZED_CUSTOM_script".to_owned(),
command: "composer".to_owned(),
args: vec![
@ -86,29 +117,17 @@ impl ContextProvider for JsonTaskProvider {
],
tags: vec!["composer-script".into()],
..TaskTemplate::default()
},
];
let tasks = as_json
.get("scripts")?
.as_object()?
.keys()
.map(|key| TaskTemplate {
label: format!("run {key}"),
command: "npm".to_owned(),
args: vec![
"--prefix".into(),
"$ZED_DIRNAME".into(),
"run".into(),
key.into(),
],
..TaskTemplate::default()
})
.chain(gutter_tasks)
.collect();
Some(TaskTemplates(tasks))
}])
.collect()
} else {
vec![]
};
Some(TaskTemplates(task_templates))
})
}
}
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}

View file

@ -18,6 +18,7 @@ mod c;
mod css;
mod go;
mod json;
mod package_json;
mod python;
mod rust;
mod tailwind;
@ -25,6 +26,8 @@ mod typescript;
mod vtsls;
mod yaml;
pub(crate) use package_json::{PackageJson, PackageJsonData};
#[derive(RustEmbed)]
#[folder = "src/"]
#[exclude = "*.rs"]

View file

@ -0,0 +1,106 @@
use chrono::{DateTime, Local};
use collections::{BTreeSet, HashMap};
use serde_json_lenient::Value;
use std::{path::Path, sync::Arc};
#[derive(Clone, Debug)]
pub struct PackageJson {
pub mtime: DateTime<Local>,
pub data: PackageJsonData,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PackageJsonData {
pub jest_package_path: Option<Arc<Path>>,
pub mocha_package_path: Option<Arc<Path>>,
pub vitest_package_path: Option<Arc<Path>>,
pub jasmine_package_path: Option<Arc<Path>>,
pub scripts: BTreeSet<(Arc<Path>, String)>,
pub package_manager: Option<&'static str>,
}
impl PackageJsonData {
pub fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
let mut scripts = BTreeSet::new();
if let Some(Value::Object(package_json_scripts)) = package_json.get("scripts") {
scripts.extend(
package_json_scripts
.keys()
.cloned()
.map(|name| (path.clone(), name)),
);
}
let mut jest_package_path = None;
let mut mocha_package_path = None;
let mut vitest_package_path = None;
let mut jasmine_package_path = None;
if let Some(Value::Object(dependencies)) = package_json.get("devDependencies") {
if dependencies.contains_key("jest") {
jest_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("mocha") {
mocha_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("vitest") {
vitest_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
}
if let Some(Value::Object(dev_dependencies)) = package_json.get("dependencies") {
if dev_dependencies.contains_key("jest") {
jest_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("mocha") {
mocha_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("vitest") {
vitest_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
}
let package_manager = package_json
.get("packageManager")
.and_then(|value| value.as_str())
.and_then(|value| {
if value.starts_with("pnpm") {
Some("pnpm")
} else if value.starts_with("yarn") {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
} else {
None
}
});
Self {
jest_package_path,
mocha_package_path,
vitest_package_path,
jasmine_package_path,
scripts,
package_manager,
}
}
pub fn merge(&mut self, other: Self) {
self.jest_package_path = self.jest_package_path.take().or(other.jest_package_path);
self.mocha_package_path = self.mocha_package_path.take().or(other.mocha_package_path);
self.vitest_package_path = self
.vitest_package_path
.take()
.or(other.vitest_package_path);
self.jasmine_package_path = self
.jasmine_package_path
.take()
.or(other.jasmine_package_path);
self.scripts.extend(other.scripts);
self.package_manager = self.package_manager.or(other.package_manager);
}
}

View file

@ -18,7 +18,6 @@ use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
use std::{
any::Any,
borrow::Cow,
collections::BTreeSet,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
@ -28,6 +27,8 @@ use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use crate::{PackageJson, PackageJsonData};
#[derive(Debug)]
pub(crate) struct TypeScriptContextProvider {
last_package_json: PackageJsonContents,
@ -57,108 +58,7 @@ const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
#[derive(Clone, Debug, Default)]
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
#[derive(Clone, Debug)]
struct PackageJson {
mtime: DateTime<Local>,
data: PackageJsonData,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct PackageJsonData {
jest_package_path: Option<Arc<Path>>,
mocha_package_path: Option<Arc<Path>>,
vitest_package_path: Option<Arc<Path>>,
jasmine_package_path: Option<Arc<Path>>,
scripts: BTreeSet<(Arc<Path>, String)>,
package_manager: Option<&'static str>,
}
impl PackageJsonData {
fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
let mut scripts = BTreeSet::new();
if let Some(serde_json::Value::Object(package_json_scripts)) = package_json.get("scripts") {
scripts.extend(
package_json_scripts
.keys()
.cloned()
.map(|name| (path.clone(), name)),
);
}
let mut jest_package_path = None;
let mut mocha_package_path = None;
let mut vitest_package_path = None;
let mut jasmine_package_path = None;
if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
if dependencies.contains_key("jest") {
jest_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("mocha") {
mocha_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("vitest") {
vitest_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
}
if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
{
if dev_dependencies.contains_key("jest") {
jest_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("mocha") {
mocha_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("vitest") {
vitest_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
}
let package_manager = package_json
.get("packageManager")
.and_then(|value| value.as_str())
.and_then(|value| {
if value.starts_with("pnpm") {
Some("pnpm")
} else if value.starts_with("yarn") {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
} else {
None
}
});
Self {
jest_package_path,
mocha_package_path,
vitest_package_path,
jasmine_package_path,
scripts,
package_manager,
}
}
fn merge(&mut self, other: Self) {
self.jest_package_path = self.jest_package_path.take().or(other.jest_package_path);
self.mocha_package_path = self.mocha_package_path.take().or(other.mocha_package_path);
self.vitest_package_path = self
.vitest_package_path
.take()
.or(other.vitest_package_path);
self.jasmine_package_path = self
.jasmine_package_path
.take()
.or(other.jasmine_package_path);
self.scripts.extend(other.scripts);
self.package_manager = self.package_manager.or(other.package_manager);
}
fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
if self.jest_package_path.is_some() {
task_templates.0.push(TaskTemplate {
@ -400,8 +300,8 @@ impl TypeScriptContextProvider {
fs.load(&package_json_path).await.with_context(|| {
format!("loading package.json from {package_json_path:?}")
})?;
let package_json: HashMap<String, serde_json::Value> =
serde_json::from_str(&package_json_string).with_context(|| {
let package_json: HashMap<String, serde_json_lenient::Value> =
serde_json_lenient::from_str(&package_json_string).with_context(|| {
format!("parsing package.json from {package_json_path:?}")
})?;
let new_data =

View file

@ -29,8 +29,9 @@ impl DapLocator for NodeLocator {
return None;
}
if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value()
&& build_config.command != "composer"
&& build_config.command != "npm"
&& build_config.command != "pnpm"
&& build_config.command != "yarn"
{
return None;
}