|
1 | 1 | use std::collections::HashSet;
|
2 | 2 |
|
3 | 3 | use node_semver::{Range, Version};
|
| 4 | +use serde::Deserialize; |
| 5 | +use tracing::debug; |
4 | 6 | use turbopath::{AbsoluteSystemPath, RelativeUnixPath};
|
5 | 7 |
|
| 8 | +use super::npmrc; |
6 | 9 | use crate::{
|
7 | 10 | package_json::PackageJson,
|
8 | 11 | package_manager::{Error, PackageManager},
|
9 | 12 | };
|
10 | 13 |
|
11 | 14 | pub const LOCKFILE: &str = "pnpm-lock.yaml";
|
| 15 | +pub const WORKSPACE_CONFIGURATION_PATH: &str = "pnpm-workspace.yaml"; |
| 16 | + |
| 17 | +/// A representation of the pnpm versions have different treatment by turbo. |
| 18 | +/// |
| 19 | +/// Not all behaviors are gated by this enum, lockfile interpretations are |
| 20 | +/// decided by `lockfileVersion` in `pnpm-lock.yaml`. In the future, this would |
| 21 | +/// be better represented by the semver to allow better gating of behavior |
| 22 | +/// based on when it changed in pnpm. |
| 23 | +pub enum PnpmVersion { |
| 24 | + Pnpm6, |
| 25 | + Pnpm7And8, |
| 26 | + Pnpm9, |
| 27 | +} |
12 | 28 |
|
13 | 29 | pub struct PnpmDetector<'a> {
|
14 | 30 | found: bool,
|
@@ -69,6 +85,88 @@ pub(crate) fn prune_patches<R: AsRef<RelativeUnixPath>>(
|
69 | 85 | pruned_json
|
70 | 86 | }
|
71 | 87 |
|
| 88 | +pub fn link_workspace_packages(pnpm_version: PnpmVersion, repo_root: &AbsoluteSystemPath) -> bool { |
| 89 | + let npmrc_config = npmrc::NpmRc::from_file(repo_root) |
| 90 | + .inspect_err(|e| debug!("unable to read npmrc: {e}")) |
| 91 | + .unwrap_or_default(); |
| 92 | + let workspace_config = matches!(pnpm_version, PnpmVersion::Pnpm9) |
| 93 | + .then(|| { |
| 94 | + PnpmWorkspace::from_file(repo_root) |
| 95 | + .inspect_err(|e| debug!("unable to read {WORKSPACE_CONFIGURATION_PATH}: {e}")) |
| 96 | + .ok() |
| 97 | + }) |
| 98 | + .flatten() |
| 99 | + .and_then(|config| config.link_workspace_packages); |
| 100 | + workspace_config |
| 101 | + .or(npmrc_config.link_workspace_packages) |
| 102 | + // The default for pnpm 9 is false if not explicitly set |
| 103 | + // All previous versions had a default of true |
| 104 | + .unwrap_or(match pnpm_version { |
| 105 | + PnpmVersion::Pnpm6 | PnpmVersion::Pnpm7And8 => true, |
| 106 | + PnpmVersion::Pnpm9 => false, |
| 107 | + }) |
| 108 | +} |
| 109 | + |
| 110 | +pub fn get_configured_workspace_globs(repo_root: &AbsoluteSystemPath) -> Option<Vec<String>> { |
| 111 | + let pnpm_workspace = PnpmWorkspace::from_file(repo_root).ok()?; |
| 112 | + if pnpm_workspace.packages.is_empty() { |
| 113 | + None |
| 114 | + } else { |
| 115 | + Some(pnpm_workspace.packages) |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +pub fn get_default_exclusions() -> &'static [&'static str] { |
| 120 | + ["**/node_modules/**", "**/bower_components/**"].as_slice() |
| 121 | +} |
| 122 | + |
| 123 | +#[derive(Debug, Deserialize)] |
| 124 | +#[serde(rename_all = "camelCase")] |
| 125 | +struct PnpmWorkspace { |
| 126 | + pub packages: Vec<String>, |
| 127 | + link_workspace_packages: Option<bool>, |
| 128 | +} |
| 129 | + |
| 130 | +impl PnpmWorkspace { |
| 131 | + pub fn from_file(repo_root: &AbsoluteSystemPath) -> Result<Self, Error> { |
| 132 | + let workspace_yaml_path = repo_root.join_component(WORKSPACE_CONFIGURATION_PATH); |
| 133 | + let workspace_yaml = workspace_yaml_path.read_to_string()?; |
| 134 | + Ok(serde_yaml::from_str(&workspace_yaml)?) |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +#[derive(Debug)] |
| 139 | +pub struct NotPnpmError { |
| 140 | + package_manager: PackageManager, |
| 141 | +} |
| 142 | + |
| 143 | +impl std::fmt::Display for NotPnpmError { |
| 144 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 145 | + f.write_fmt(format_args!( |
| 146 | + "Package managers other than pnpm cannot have pnpm version: {:?}", |
| 147 | + self.package_manager |
| 148 | + )) |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +impl TryFrom<&'_ PackageManager> for PnpmVersion { |
| 153 | + type Error = NotPnpmError; |
| 154 | + |
| 155 | + fn try_from(value: &PackageManager) -> Result<Self, Self::Error> { |
| 156 | + match value { |
| 157 | + PackageManager::Pnpm9 => Ok(Self::Pnpm9), |
| 158 | + PackageManager::Pnpm => Ok(Self::Pnpm7And8), |
| 159 | + PackageManager::Pnpm6 => Ok(Self::Pnpm6), |
| 160 | + PackageManager::Berry |
| 161 | + | PackageManager::Yarn |
| 162 | + | PackageManager::Npm |
| 163 | + | PackageManager::Bun => Err(NotPnpmError { |
| 164 | + package_manager: value.clone(), |
| 165 | + }), |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | + |
72 | 170 | #[cfg(test)]
|
73 | 171 | mod test {
|
74 | 172 | use std::{collections::BTreeMap, fs::File};
|
@@ -133,4 +231,76 @@ mod test {
|
133 | 231 | expected
|
134 | 232 | );
|
135 | 233 | }
|
| 234 | + |
| 235 | + #[test] |
| 236 | + fn test_workspace_parsing() { |
| 237 | + let config: PnpmWorkspace = |
| 238 | + serde_yaml::from_str("linkWorkspacePackages: true\npackages:\n - \"apps/*\"\n") |
| 239 | + .unwrap(); |
| 240 | + assert_eq!(config.link_workspace_packages, Some(true)); |
| 241 | + assert_eq!(config.packages, vec!["apps/*".to_string()]); |
| 242 | + } |
| 243 | + |
| 244 | + #[test_case(PnpmVersion::Pnpm6, None, true)] |
| 245 | + #[test_case(PnpmVersion::Pnpm7And8, None, true)] |
| 246 | + #[test_case(PnpmVersion::Pnpm7And8, Some(false), false)] |
| 247 | + #[test_case(PnpmVersion::Pnpm7And8, Some(true), true)] |
| 248 | + #[test_case(PnpmVersion::Pnpm9, None, false)] |
| 249 | + #[test_case(PnpmVersion::Pnpm9, Some(true), true)] |
| 250 | + #[test_case(PnpmVersion::Pnpm9, Some(false), false)] |
| 251 | + fn test_link_workspace_packages(version: PnpmVersion, enabled: Option<bool>, expected: bool) { |
| 252 | + let tmpdir = tempfile::tempdir().unwrap(); |
| 253 | + let repo_root = AbsoluteSystemPath::from_std_path(tmpdir.path()).unwrap(); |
| 254 | + if let Some(enabled) = enabled { |
| 255 | + repo_root |
| 256 | + .join_component(npmrc::NPMRC_FILENAME) |
| 257 | + .create_with_contents(format!("link-workspace-packages={enabled}")) |
| 258 | + .unwrap(); |
| 259 | + } |
| 260 | + let actual = link_workspace_packages(version, repo_root); |
| 261 | + assert_eq!(actual, expected); |
| 262 | + } |
| 263 | + |
| 264 | + #[test_case(PnpmVersion::Pnpm6, None, true)] |
| 265 | + #[test_case(PnpmVersion::Pnpm7And8, None, true)] |
| 266 | + // Pnpm <9 doesn't use workspace config |
| 267 | + #[test_case(PnpmVersion::Pnpm7And8, Some(false), true)] |
| 268 | + #[test_case(PnpmVersion::Pnpm7And8, Some(true), true)] |
| 269 | + #[test_case(PnpmVersion::Pnpm9, None, false)] |
| 270 | + #[test_case(PnpmVersion::Pnpm9, Some(true), true)] |
| 271 | + #[test_case(PnpmVersion::Pnpm9, Some(false), false)] |
| 272 | + fn test_link_workspace_packages_via_workspace( |
| 273 | + version: PnpmVersion, |
| 274 | + enabled: Option<bool>, |
| 275 | + expected: bool, |
| 276 | + ) { |
| 277 | + let tmpdir = tempfile::tempdir().unwrap(); |
| 278 | + let repo_root = AbsoluteSystemPath::from_std_path(tmpdir.path()).unwrap(); |
| 279 | + if let Some(enabled) = enabled { |
| 280 | + repo_root |
| 281 | + .join_component(WORKSPACE_CONFIGURATION_PATH) |
| 282 | + .create_with_contents(format!( |
| 283 | + "linkWorkspacePackages: {enabled}\npackages:\n - \"apps/*\"\n" |
| 284 | + )) |
| 285 | + .unwrap(); |
| 286 | + } |
| 287 | + let actual = link_workspace_packages(version, repo_root); |
| 288 | + assert_eq!(actual, expected); |
| 289 | + } |
| 290 | + |
| 291 | + #[test] |
| 292 | + fn test_workspace_yaml_wins_over_npmrc() { |
| 293 | + let tmpdir = tempfile::tempdir().unwrap(); |
| 294 | + let repo_root = AbsoluteSystemPath::from_std_path(tmpdir.path()).unwrap(); |
| 295 | + repo_root |
| 296 | + .join_component(WORKSPACE_CONFIGURATION_PATH) |
| 297 | + .create_with_contents("linkWorkspacePackages: true\npackages:\n - \"apps/*\"\n") |
| 298 | + .unwrap(); |
| 299 | + repo_root |
| 300 | + .join_component(npmrc::NPMRC_FILENAME) |
| 301 | + .create_with_contents("link-workspace-packages=false") |
| 302 | + .unwrap(); |
| 303 | + let actual = link_workspace_packages(PnpmVersion::Pnpm9, repo_root); |
| 304 | + assert!(actual); |
| 305 | + } |
136 | 306 | }
|
0 commit comments