Resolves https://github.com/zed-industries/zed/issues/58086 This diff fixes `package.json` runnables using npm for projects that declare their package manager through `devEngines.packageManager`. The `package.json` parser only looked at the top-level `packageManager` field, so projects using the newer `devEngines` shape fell back to `npm` even when they specified `pnpm`, `yarn`, or `bun`. With this change, package manager detection also reads `devEngines.packageManager` in both object and array forms. The top-level `packageManager` field still takes precedence when both are present, and the shared parser means the fix applies to `package.json` gutter runnables as well as TypeScript package-script tasks. Release Notes: - Fixed `package.json` runnables ignoring package managers declared via `devEngines.packageManager`. --------- Co-authored-by: dino <dinojoaocosta@gmail.com>
203 lines
6.9 KiB
Rust
203 lines
6.9 KiB
Rust
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 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>,
|
|
}
|
|
|
|
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;
|
|
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());
|
|
}
|
|
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 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") {
|
|
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());
|
|
}
|
|
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_manager_from_package_json(&package_json);
|
|
|
|
Self {
|
|
jest_package_path,
|
|
mocha_package_path,
|
|
vitest_package_path,
|
|
jasmine_package_path,
|
|
bun_package_path,
|
|
node_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.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);
|
|
}
|
|
}
|
|
|
|
fn package_manager_from_package_json(
|
|
package_json: &HashMap<String, Value>,
|
|
) -> Option<&'static str> {
|
|
package_json
|
|
.get("packageManager")
|
|
.and_then(|value| value.as_str())
|
|
.and_then(package_manager_name)
|
|
.or_else(|| {
|
|
package_json
|
|
.get("devEngines")
|
|
.and_then(|value| value.as_object())
|
|
.and_then(|dev_engines| dev_engines.get("packageManager"))
|
|
.and_then(package_manager_from_dev_engine)
|
|
})
|
|
}
|
|
|
|
fn package_manager_from_dev_engine(value: &Value) -> Option<&'static str> {
|
|
match value {
|
|
Value::Object(package_manager) => package_manager
|
|
.get("name")
|
|
.and_then(|name| name.as_str())
|
|
.and_then(package_manager_name),
|
|
Value::Array(package_managers) => package_managers
|
|
.iter()
|
|
.find_map(package_manager_from_dev_engine),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn package_manager_name(value: &str) -> Option<&'static str> {
|
|
let value = value.split_once('@').map(|(name, _)| name).unwrap_or(value);
|
|
match value {
|
|
"pnpm" => Some("pnpm"),
|
|
"yarn" => Some("yarn"),
|
|
"npm" => Some("npm"),
|
|
"bun" => Some("bun"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::Path;
|
|
|
|
use super::PackageJsonData;
|
|
|
|
#[test]
|
|
fn package_manager_detection() {
|
|
fn package_manager(source: &str) -> Option<&'static str> {
|
|
PackageJsonData::new(
|
|
Path::new("/root/package.json").into(),
|
|
serde_json_lenient::from_str(source).expect("provided source should be valid JSON"),
|
|
)
|
|
.package_manager
|
|
}
|
|
|
|
assert_eq!(
|
|
package_manager(r#"{"packageManager": "pnpm@11.1.3"}"#),
|
|
Some("pnpm")
|
|
);
|
|
|
|
assert_eq!(
|
|
package_manager(
|
|
r#"{"devEngines": {"packageManager": {"name": "pnpm", "version": "^11.1.3", "onFail": "download"}}}"#
|
|
),
|
|
Some("pnpm"),
|
|
);
|
|
|
|
assert_eq!(
|
|
package_manager(
|
|
r#"{"devEngines": {"packageManager": [{"name": "foo"}, {"name": "yarn", "version": "^4.0.0"}]}}"#
|
|
),
|
|
Some("yarn"),
|
|
);
|
|
|
|
assert_eq!(
|
|
package_manager(
|
|
r#"{"packageManager": "npm@10.0.0", "devEngines": {"packageManager": {"name": "pnpm"}}}"#
|
|
),
|
|
Some("npm"),
|
|
);
|
|
|
|
assert_eq!(
|
|
package_manager(r#"{"devEngines": {"packageManager": {"version": "^11.1.3"}}}"#),
|
|
None,
|
|
);
|
|
}
|
|
}
|