typescript: Runners support for bun:test & node:test (#39238)

Closes #21132

Release Notes:

- JavaScript/TypeScript: Added support for detecting `node:test` and `bun:test` test runners
This commit is contained in:
versecafe
2025-10-13 09:05:04 +02:00
committed by GitHub
parent 92e765b5d2
commit 95780e5baf
4 changed files with 226 additions and 2 deletions
+22
View File
@@ -15,6 +15,8 @@ pub struct PackageJsonData {
pub mocha_package_path: Option<Arc<Path>>,
pub vitest_package_path: Option<Arc<Path>>,
pub jasmine_package_path: Option<Arc<Path>>,
pub bun_package_path: Option<Arc<Path>>,
pub node_package_path: Option<Arc<Path>>,
pub scripts: BTreeSet<(Arc<Path>, String)>,
pub package_manager: Option<&'static str>,
}
@@ -35,6 +37,8 @@ impl PackageJsonData {
let mut mocha_package_path = None;
let mut vitest_package_path = None;
let mut jasmine_package_path = None;
let mut bun_package_path = None;
let mut node_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());
@@ -48,6 +52,12 @@ impl PackageJsonData {
if dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("@types/bun") {
bun_package_path.get_or_insert_with(|| path.clone());
}
if dependencies.contains_key("@types/node") {
node_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") {
@@ -62,6 +72,12 @@ impl PackageJsonData {
if dev_dependencies.contains_key("jasmine") {
jasmine_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("@types/bun") {
bun_package_path.get_or_insert_with(|| path.clone());
}
if dev_dependencies.contains_key("@types/node") {
node_package_path.get_or_insert_with(|| path.clone());
}
}
let package_manager = package_json
@@ -74,6 +90,8 @@ impl PackageJsonData {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
} else if value.starts_with("bun") {
Some("bun")
} else {
None
}
@@ -84,6 +102,8 @@ impl PackageJsonData {
mocha_package_path,
vitest_package_path,
jasmine_package_path,
bun_package_path,
node_package_path,
scripts,
package_manager,
}
@@ -100,6 +120,8 @@ impl PackageJsonData {
.jasmine_package_path
.take()
.or(other.jasmine_package_path);
self.bun_package_path = self.bun_package_path.take().or(other.bun_package_path);
self.node_package_path = self.node_package_path.take().or(other.node_package_path);
self.scripts.extend(other.scripts);
self.package_manager = self.package_manager.or(other.package_manager);
}
+194
View File
@@ -54,6 +54,12 @@ const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
const TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_BUN_PACKAGE_PATH"));
const TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_NODE_PACKAGE_PATH"));
#[derive(Clone, Debug, Default)]
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
@@ -220,6 +226,65 @@ impl PackageJsonData {
});
}
if self.bun_package_path.is_some() {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "bun test".to_owned()),
command: "bun".to_owned(),
args: vec!["test".to_owned(), VariableName::File.template_value()],
cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!("bun test {}", VariableName::Symbol.template_value(),),
command: "bun".to_owned(),
args: vec![
"test".to_owned(),
"--test-name-pattern".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::File.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
}
if self.node_package_path.is_some() {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "node test".to_owned()),
command: "node".to_owned(),
args: vec!["--test".to_owned(), VariableName::File.template_value()],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!("node test {}", VariableName::Symbol.template_value()),
command: "node".to_owned(),
args: vec![
"--test".to_owned(),
"--test-name-pattern".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::File.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
}
let script_name_counts: HashMap<_, usize> =
self.scripts
.iter()
@@ -493,6 +558,26 @@ impl ContextProvider for TypeScriptContextProvider {
.to_string(),
);
}
if let Some(path) = package_json_data.bun_package_path {
vars.insert(
TYPESCRIPT_BUN_PACKAGE_PATH_VARIABLE,
path.parent()
.unwrap_or(Path::new(""))
.to_string_lossy()
.to_string(),
);
}
if let Some(path) = package_json_data.node_package_path {
vars.insert(
TYPESCRIPT_NODE_PACKAGE_PATH_VARIABLE,
path.parent()
.unwrap_or(Path::new(""))
.to_string_lossy()
.to_string(),
);
}
}
}
Ok(vars)
@@ -1178,6 +1263,8 @@ mod tests {
mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
jasmine_package_path: None,
bun_package_path: None,
node_package_path: None,
scripts: [
(
Path::new(path!("/root/package.json")).into(),
@@ -1231,6 +1318,7 @@ mod tests {
]
);
}
#[test]
fn test_escaping_name() {
let cases = [
@@ -1264,4 +1352,110 @@ mod tests {
assert_eq!(replace_test_name_parameters(input), expected);
}
}
// The order of test runner tasks is based on inferred user preference:
// 1. Dedicated test runners (e.g., Jest, Vitest, Mocha, Jasmine) are prioritized.
// 2. Bun's built-in test runner (`bun test`) comes next.
// 3. Node.js's built-in test runner (`node --test`) is last.
// This hierarchy assumes that if a dedicated test framework is installed, it is the
// preferred testing mechanism. Between runtime-specific options, `bun test` is
// typically preferred over `node --test` when @types/bun is present.
#[gpui::test]
async fn test_task_ordering_with_multiple_test_runners(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
cx.update(|cx| {
settings::init(cx);
Project::init_settings(cx);
language_settings::init(cx);
});
// Test case with all test runners present
let package_json_all_runners = json!({
"devDependencies": {
"@types/bun": "1.0.0",
"@types/node": "^20.0.0",
"jest": "29.0.0",
"mocha": "10.0.0",
"vitest": "1.0.0",
"jasmine": "5.0.0",
},
"scripts": {
"test": "jest"
}
})
.to_string();
let fs = FakeFs::new(executor);
fs.insert_tree(
path!("/root"),
json!({
"package.json": package_json_all_runners,
"file.js": "",
}),
)
.await;
let provider = TypeScriptContextProvider::new(fs.clone());
let package_json_data = cx
.update(|cx| {
provider.combined_package_json_data(
fs.clone(),
path!("/root").as_ref(),
rel_path("file.js"),
cx,
)
})
.await
.unwrap();
assert!(package_json_data.jest_package_path.is_some());
assert!(package_json_data.mocha_package_path.is_some());
assert!(package_json_data.vitest_package_path.is_some());
assert!(package_json_data.jasmine_package_path.is_some());
assert!(package_json_data.bun_package_path.is_some());
assert!(package_json_data.node_package_path.is_some());
let mut task_templates = TaskTemplates::default();
package_json_data.fill_task_templates(&mut task_templates);
let test_tasks: Vec<_> = task_templates
.0
.iter()
.filter(|template| {
template.tags.contains(&"ts-test".to_owned())
|| template.tags.contains(&"js-test".to_owned())
})
.map(|template| &template.label)
.collect();
let node_test_index = test_tasks
.iter()
.position(|label| label.contains("node test"));
let jest_test_index = test_tasks.iter().position(|label| label.contains("jest"));
let bun_test_index = test_tasks
.iter()
.position(|label| label.contains("bun test"));
assert!(
node_test_index.is_some(),
"Node test tasks should be present"
);
assert!(
jest_test_index.is_some(),
"Jest test tasks should be present"
);
assert!(bun_test_index.is_some(), "Bun test tasks should be present");
assert!(
jest_test_index.unwrap() < bun_test_index.unwrap(),
"Jest should come before Bun"
);
assert!(
bun_test_index.unwrap() < node_test_index.unwrap(),
"Bun should come before Node"
);
}
}
+5 -1
View File
@@ -179,10 +179,14 @@ Zed supports debugging JavaScript code out of the box.
The following can be debugged without writing additional configuration:
- Tasks from `package.json`
- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine)
- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine, Bun, Node)
Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these predefined debug tasks.
> **Note:** Bun test is automatically detected when `@types/bun` is present in `package.json`.
>
> **Note:** Node test is automatically detected when `@types/node` is present in `package.json` (requires Node.js 20+).
As for all languages, configurations from `.vscode/launch.json` are also available for debugging in Zed.
If your use-case isn't covered by any of these, you can take full control by adding debug configurations to `.zed/debug.json`. See below for example configurations.
+5 -1
View File
@@ -162,10 +162,14 @@ Zed supports debugging TypeScript code out of the box.
The following can be debugged without writing additional configuration:
- Tasks from `package.json`
- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine)
- Tests written using several popular frameworks (Jest, Mocha, Vitest, Jasmine, Bun, Node)
Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list of these predefined debug tasks.
> **Note:** Bun test is automatically detected when `@types/bun` is present in `package.json`.
>
> **Note:** Node test is automatically detected when `@types/node` is present in `package.json` (requires Node.js 20+).
As for all languages, configurations from `.vscode/launch.json` are also available for debugging in Zed.
If your use-case isn't covered by any of these, you can take full control by adding debug configurations to `.zed/debug.json`. See below for example configurations.