Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions crates/vite_install/src/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{
collections::HashMap,
env, fmt,
fs::{self, File},
io::{self, BufReader, IsTerminal, Write},
io::{self, BufRead, BufReader, IsTerminal, Write},
path::Path,
};

Expand Down Expand Up @@ -291,10 +291,20 @@ pub fn get_package_manager_type_and_version(
return Ok((PackageManagerType::Pnpm, version, None, source));
}

// if yarn.lock or .yarnrc.yml exists, use yarn@latest
// if yarn.lock exists, detect version from lockfile
let yarn_lock_path = workspace_root.path.join("yarn.lock");
if is_exists_file(&yarn_lock_path)? {
let version = if is_yarn_lockfile_v1(&yarn_lock_path) {
Str::from(">=1.0.0 <2.0.0")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve Yarn v1 lockfiles to the latest 1.x release

When a project has only a Yarn v1 yarn.lock and the user's VP cache already contains an older Yarn 1.x install, returning this range flows into download_package_manager, whose range resolver checks find_cached_package_manager_version before the registry and returns that cached version. Because this detection source is LockfileOrConfig, build() then auto-pins the stale cached version into devEngines.packageManager, so a lockfile-only project can be stuck on an arbitrary old Yarn 1.x instead of the latest compatible Yarn v1 release.

Useful? React with 👍 / 👎.

Comment on lines +296 to +298

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor Berry config before v1 lockfile detection

When a workspace has both .yarnrc.yml and an existing v1-format yarn.lock (for example right after configuring Yarn Berry before regenerating the lockfile), this branch classifies the project as Yarn v1 before the .yarnrc.yml check runs. The repo's detection docs identify .yarnrc.yml as a Yarn Berry (v2+) configuration signal, so this would install/pin Yarn 1 and ignore the project's Berry config instead of using the intended Yarn 2+ CLI.

Useful? React with 👍 / 👎.

} else {
version // "latest"
};
return Ok((PackageManagerType::Yarn, version, None, source));
}

// if .yarnrc.yml exists, use yarn@latest
let yarnrc_yml_path = workspace_root.path.join(".yarnrc.yml");
if is_exists_file(&yarn_lock_path)? || is_exists_file(&yarnrc_yml_path)? {
if is_exists_file(&yarnrc_yml_path)? {
return Ok((PackageManagerType::Yarn, version, None, source));
}

Expand Down Expand Up @@ -619,6 +629,21 @@ fn is_exists_file(path: impl AsRef<Path>) -> Result<bool, Error> {
}
}

/// Detect whether a `yarn.lock` file is for yarn v1 or not.
///
/// See https://github.com/yarnpkg/yarn/blob/c2dda503f3759b5be5f0e24ecd9cf5c97a540147/src/lockfile/parse.js#L28
fn is_yarn_lockfile_v1(path: impl AsRef<Path>) -> bool {
let Ok(file) = File::open(path.as_ref()) else {
return false;
};
let reader = BufReader::new(file);
reader
.lines()
.take(2)
.filter_map(|line| line.ok())
.any(|line| line.trim() == "# yarn lockfile v1")
}

/// Whether a managed package manager install is complete (usable on the current
/// platform).
///
Expand Down Expand Up @@ -2045,10 +2070,57 @@ mod tests {
let entry = &package_json["devEngines"]["packageManager"];
assert_eq!(entry["name"].as_str().unwrap(), "yarn");
assert_eq!(entry["onFail"].as_str().unwrap(), "download");
// resolved to yarn v1
assert!(
entry["version"].as_str().unwrap().starts_with("1."),
"expected yarn v1, got version {:?}",
entry["version"]
);
// keep other fields
assert_eq!(package_json["name"].as_str().unwrap(), "test-package");
}

#[tokio::test]
async fn test_detect_package_manager_with_yarn_berry_lock() {
let temp_dir = create_temp_dir();
let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
let package_content = r#"{"name": "test-package"}"#;
create_package_json(&temp_dir_path, package_content);

// Create yarn.lock
fs::write(
temp_dir_path.join("yarn.lock"),
"__metadata:\n version: 10\n cacheKey: 10c0\n",
)
.expect("Failed to write berry yarn.lock");

let result = PackageManager::builder(temp_dir_path.to_absolute_path_buf())
.build()
.await
.expect("Should detect yarn");
assert_eq!(result.bin_name, "yarn");
assert!(
result.get_bin_prefix().ends_with("yarn/bin"),
"bin_prefix should end with yarn/bin, but got {:?}",
result.get_bin_prefix()
);
// auto-pin writes devEngines.packageManager
let package_json_path = temp_dir_path.join("package.json");
let package_json: serde_json::Value =
serde_json::from_slice(&fs::read(&package_json_path).unwrap()).unwrap();
println!("package_json: {package_json:?}");
let entry = &package_json["devEngines"]["packageManager"];
assert_eq!(entry["name"].as_str().unwrap(), "yarn");
assert_eq!(entry["onFail"].as_str().unwrap(), "download");
// resolved to latest yarn berry (2.x+), not v1
assert!(
!entry["version"].as_str().unwrap().starts_with("1."),
"expected yarn berry, got version {:?}",
entry["version"]
);
assert_eq!(package_json["name"].as_str().unwrap(), "test-package");
}

#[tokio::test]
#[cfg(not(windows))] // FIXME
async fn test_detect_package_manager_with_package_lock_json() {
Expand Down
Loading