Skip to content

Commit

Permalink
fix: honour handleTransparentWorkspaces setting in Yarn/Berry
Browse files Browse the repository at this point in the history
  • Loading branch information
romanofski committed Nov 1, 2024
1 parent 1b1f98e commit 8b7166d
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 31 deletions.
27 changes: 23 additions & 4 deletions crates/turborepo-repository/src/package_graph/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use turborepo_graph_utils as graph;
use turborepo_lockfiles::Lockfile;

use super::{
dep_splitter::DependencySplitter, npmrc::NpmRc, PackageGraph, PackageInfo, PackageName,
PackageNode,
dep_splitter::DependencySplitter, npmrc::NpmRc, yarnrc::YarnRc, PackageGraph, PackageInfo,
PackageName, PackageNode,
};
use crate::{
discovery::{
Expand Down Expand Up @@ -362,6 +362,17 @@ impl<'a, T: PackageDiscovery> BuildState<'a, ResolvedWorkspaces, T> {
}
_ => None,
};
let yarnrc = match package_manager {
PackageManager::Berry => {
// HOME?
let yarnrc_path = self.repo_root.join_component(".yarnrc.yml");
match yarnrc_path.read_existing_to_string().ok().flatten() {
Some(contents) => YarnRc::from_reader(contents.as_bytes()).ok(),
None => None,
}
}
_ => None,
};
let split_deps = self
.workspaces
.iter()
Expand All @@ -375,6 +386,7 @@ impl<'a, T: PackageDiscovery> BuildState<'a, ResolvedWorkspaces, T> {
&self.workspaces,
package_manager,
npmrc.as_ref(),
yarnrc.as_ref(),
entry.package_json.all_dependencies(),
),
)
Expand Down Expand Up @@ -567,6 +579,7 @@ impl Dependencies {
workspaces: &HashMap<PackageName, PackageInfo>,
package_manager: PackageManager,
npmrc: Option<&NpmRc>,
yarnrc: Option<&YarnRc>,
dependencies: I,
) -> Self {
let resolved_workspace_json_path = repo_root.resolve(workspace_json_path);
Expand All @@ -575,8 +588,14 @@ impl Dependencies {
.expect("package.json path should have parent");
let mut internal = HashSet::new();
let mut external = BTreeMap::new();
let splitter =
DependencySplitter::new(repo_root, workspace_dir, workspaces, package_manager, npmrc);
let splitter = DependencySplitter::new(
repo_root,
workspace_dir,
workspaces,
package_manager,
npmrc,
yarnrc,
);
for (name, version) in dependencies.into_iter() {
if let Some(workspace) = splitter.is_internal(name, version) {
internal.insert(workspace);
Expand Down
65 changes: 38 additions & 27 deletions crates/turborepo-repository/src/package_graph/dep_splitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use turbopath::{
RelativeUnixPathBuf,
};

use super::{npmrc::NpmRc, PackageInfo, PackageName};
use super::{npmrc::NpmRc, yarnrc::YarnRc, PackageInfo, PackageName};
use crate::package_manager::PackageManager;

pub struct DependencySplitter<'a> {
repo_root: &'a AbsoluteSystemPath,
workspace_dir: &'a AbsoluteSystemPath,
workspaces: &'a HashMap<PackageName, PackageInfo>,
link_workspace_packages: bool,
enable_transparent_workspaces: bool,
}

impl<'a> DependencySplitter<'a> {
Expand All @@ -22,6 +23,7 @@ impl<'a> DependencySplitter<'a> {
workspaces: &'a HashMap<PackageName, PackageInfo>,
package_manager: PackageManager,
npmrc: Option<&'a NpmRc>,
yarnrc: Option<&'a YarnRc>,
) -> Self {
Self {
repo_root,
Expand All @@ -30,6 +32,8 @@ impl<'a> DependencySplitter<'a> {
link_workspace_packages: npmrc
.and_then(|npmrc| npmrc.link_workspace_packages)
.unwrap_or(!matches!(package_manager, PackageManager::Pnpm9)),
enable_transparent_workspaces: yarnrc
.map_or(true, |yarnrc| yarnrc.enable_transparent_workspaces),
}
}

Expand All @@ -48,6 +52,7 @@ impl<'a> DependencySplitter<'a> {
info.package_json.version.as_deref().unwrap_or_default(),
self.workspace_dir,
self.repo_root,
self.enable_transparent_workspaces,
);

match is_internal {
Expand Down Expand Up @@ -137,22 +142,26 @@ impl<'a> DependencyVersion<'a> {
)
}

fn is_external(&self) -> bool {
fn is_external(&self, enable_transparent_workspaces: bool) -> bool {
// The npm protocol for yarn by default still uses the workspace package if the
// workspace version is in a compatible semver range. See https://github.com/yarnpkg/berry/discussions/4015
// For now, we will just assume if the npm protocol is being used and the
// version matches its an internal dependency which matches the existing
// behavior before this additional logic was added.

// TODO: extend this to support the `enableTransparentWorkspaces` yarn option
self.protocol.map_or(false, |p| p != "npm")
let result = self.protocol.map_or(false, |p| {
p != "npm" && enable_transparent_workspaces == false
});
return result;
}

fn matches_workspace_package(
&self,
package_version: &str,
cwd: &AbsoluteSystemPath,
root: &AbsoluteSystemPath,
enable_transparent_workspaces: bool,
) -> bool {
match self.protocol {
Some("workspace") => {
Expand All @@ -167,7 +176,7 @@ impl<'a> DependencyVersion<'a> {
.map(|file_path| cwd.join_unix_path(file_path))
.map_or(true, |dep_path| root.contains(&dep_path))
}
Some(_) if self.is_external() => {
Some(_) if self.is_external(enable_transparent_workspaces) => {
// Other protocols are assumed to be external references ("github:", etc)
false
}
Expand Down Expand Up @@ -211,37 +220,38 @@ mod test {
use super::*;
use crate::package_json::PackageJson;

#[test_case("1.2.3", None, "1.2.3", Some("@scope/foo"), true ; "handles exact match")]
#[test_case("1.2.3", None, "^1.0.0", Some("@scope/foo"), true ; "handles semver range satisfied")]
#[test_case("2.3.4", None, "^1.0.0", None, true ; "handles semver range not satisfied")]
#[test_case("1.2.3", None, "workspace:1.2.3", Some("@scope/foo"), true ; "handles workspace protocol with version")]
#[test_case("1.2.3", None, "workspace:*", Some("@scope/foo"), true ; "handles workspace protocol with no version")]
#[test_case("1.2.3", None, "workspace:../@scope/foo", Some("@scope/foo"), true ; "handles workspace protocol with scoped relative path")]
#[test_case("1.2.3", None, "workspace:packages/@scope/foo", Some("@scope/foo"), true ; "handles workspace protocol with root relative path")]
#[test_case("1.2.3", Some("bar"), "workspace:../baz", Some("baz"), true ; "handles workspace protocol with path to differing package")]
#[test_case("1.2.3", None, "npm:^1.2.3", Some("@scope/foo"), true ; "handles npm protocol with satisfied semver range")]
#[test_case("2.3.4", None, "npm:^1.2.3", None, true ; "handles npm protocol with not satisfied semver range")]
#[test_case("1.2.3", None, "1.2.2-alpha-123abcd.0", None, true ; "handles pre-release versions")]
#[test_case("1.2.3", None, "1.2.3", Some("@scope/foo"), true, true ; "handles exact match")]
#[test_case("1.2.3", None, "^1.0.0", Some("@scope/foo"), true, true ; "handles semver range satisfied")]
#[test_case("2.3.4", None, "^1.0.0", None, true, true ; "handles semver range not satisfied")]
#[test_case("1.2.3", None, "workspace:1.2.3", Some("@scope/foo"), true, true ; "handles workspace protocol with version")]
#[test_case("1.2.3", None, "workspace:*", Some("@scope/foo"), true, true ; "handles workspace protocol with no version")]
#[test_case("1.2.3", None, "workspace:../@scope/foo", Some("@scope/foo"), true, true ; "handles workspace protocol with scoped relative path")]
#[test_case("1.2.3", None, "workspace:packages/@scope/foo", Some("@scope/foo"), true, true ; "handles workspace protocol with root relative path")]
#[test_case("1.2.3", Some("bar"), "workspace:../baz", Some("baz"), true, true ; "handles workspace protocol with path to differing package")]
#[test_case("1.2.3", None, "npm:^1.2.3", Some("@scope/foo"), true, true ; "handles npm protocol with satisfied semver range")]
#[test_case("2.3.4", None, "npm:^1.2.3", None, true, true ; "handles npm protocol with not satisfied semver range")]
#[test_case("1.2.3", None, "1.2.2-alpha-123abcd.0", None, true, true ; "handles pre-release versions")]
// for backwards compatibility with the code before versions were verified
#[test_case("sometag", None, "1.2.3", Some("@scope/foo"), true ; "handles non-semver package version")]
#[test_case("sometag", None, "1.2.3", Some("@scope/foo"), true, true ; "handles non-semver package version")]
// for backwards compatibility with the code before versions were verified
#[test_case("1.2.3", None, "sometag", Some("@scope/foo"), true ; "handles non-semver dependency version")]
#[test_case("1.2.3", None, "file:../libB", Some("@scope/foo"), true ; "handles file:.. inside repo")]
#[test_case("1.2.3", None, "file:../../../otherproject", None, true ; "handles file:.. outside repo")]
#[test_case("1.2.3", None, "link:../libB", Some("@scope/foo"), true ; "handles link:.. inside repo")]
#[test_case("1.2.3", None, "link:../../../otherproject", None, true ; "handles link:.. outside repo")]
#[test_case("0.0.0-development", None, "*", Some("@scope/foo"), true ; "handles development versions")]
#[test_case("1.2.3", Some("foo"), "workspace:@scope/foo@*", Some("@scope/foo"), true ; "handles pnpm alias star")]
#[test_case("1.2.3", Some("foo"), "workspace:@scope/foo@~", Some("@scope/foo"), true ; "handles pnpm alias tilde")]
#[test_case("1.2.3", Some("foo"), "workspace:@scope/foo@^", Some("@scope/foo"), true ; "handles pnpm alias caret")]
#[test_case("1.2.3", None, "1.2.3", None, false ; "no workspace linking")]
#[test_case("1.2.3", None, "workspace:1.2.3", Some("@scope/foo"), false ; "no workspace linking with protocol")]
#[test_case("1.2.3", None, "sometag", Some("@scope/foo"), true, true ; "handles non-semver dependency version")]
#[test_case("1.2.3", None, "file:../libB", Some("@scope/foo"), true, true ; "handles file:.. inside repo")]
#[test_case("1.2.3", None, "file:../../../otherproject", None, true, true ; "handles file:.. outside repo")]
#[test_case("1.2.3", None, "link:../libB", Some("@scope/foo"), true, true ; "handles link:.. inside repo")]
#[test_case("1.2.3", None, "link:../../../otherproject", None, true, true ; "handles link:.. outside repo")]
#[test_case("0.0.0-development", None, "*", Some("@scope/foo"), true, true ; "handles development versions")]
#[test_case("1.2.3", Some("foo"), "workspace:@scope/foo@*", Some("@scope/foo"), true, true ; "handles pnpm alias star")]
#[test_case("1.2.3", Some("foo"), "workspace:@scope/foo@~", Some("@scope/foo"), true, true ; "handles pnpm alias tilde")]
#[test_case("1.2.3", Some("foo"), "workspace:@scope/foo@^", Some("@scope/foo"), true, true ; "handles pnpm alias caret")]
#[test_case("1.2.3", None, "1.2.3", None, false, false ; "no workspace linking")]
#[test_case("1.2.3", None, "workspace:1.2.3", Some("@scope/foo"), false, false ; "no workspace linking with protocol")]
fn test_matches_workspace_package(
package_version: &str,
dependency_name: Option<&str>,
range: &str,
expected: Option<&str>,
link_workspace_packages: bool,
enable_transparent_workspaces: bool,
) {
let root = AbsoluteSystemPathBuf::new(if cfg!(windows) {
"C:\\some\\repo"
Expand Down Expand Up @@ -306,6 +316,7 @@ mod test {
workspace_dir: &pkg_dir,
workspaces: &workspaces,
link_workspace_packages,
enable_transparent_workspaces,
};

assert_eq!(
Expand Down
1 change: 1 addition & 0 deletions crates/turborepo-repository/src/package_graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
pub mod builder;
mod dep_splitter;
mod npmrc;
mod yarnrc;

pub use builder::{Error, PackageGraphBuilder};

Expand Down
49 changes: 49 additions & 0 deletions crates/turborepo-repository/src/package_graph/yarnrc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::io;

use serde::Deserialize;
use serde_yaml;

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("encountered error parsing yarnrc.yml: {0}")]
SerdeYaml(#[from] serde_yaml::Error),
}

/// A yarnrc.yaml file representing settings affecting the package graph.
#[serde(default)]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct YarnRc {
/// TODO description
pub enable_transparent_workspaces: bool,
}

impl Default for YarnRc {
fn default() -> YarnRc {
YarnRc {
enable_transparent_workspaces: true,
}
}
}

impl YarnRc {
pub fn from_reader(mut reader: impl io::Read) -> Result<Self, Error> {
let config: YarnRc = serde_yaml::from_reader(&mut reader)?;
Ok(config)
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_parse_empty_yarnrc() {
let empty = YarnRc::from_reader(b"".as_slice()).unwrap();
assert_eq!(
empty,
YarnRc {
enable_transparent_workspaces: true
}
);
}
}

0 comments on commit 8b7166d

Please sign in to comment.