diff --git a/Cargo.lock b/Cargo.lock index bc07b26a4afe6..295637400934d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3496,6 +3496,29 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" +[[package]] +name = "lazy-regex" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff63c423c68ea6814b7da9e88ce585f793c87ddd9e78f646970891769c8235d4" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -9380,6 +9403,7 @@ dependencies = [ "path-slash", "serde", "thiserror", + "wax", ] [[package]] @@ -9477,6 +9501,7 @@ dependencies = [ "indicatif", "is-terminal", "itertools", + "lazy-regex", "lazy_static", "libc", "node-semver", diff --git a/crates/turborepo-lib/Cargo.toml b/crates/turborepo-lib/Cargo.toml index bb3eee815a4db..b35d0dee4a0cd 100644 --- a/crates/turborepo-lib/Cargo.toml +++ b/crates/turborepo-lib/Cargo.toml @@ -85,6 +85,7 @@ url = "2.3.1" const_format = "0.2.30" go-parse-duration = "0.1.1" is-terminal = "0.4.7" +lazy-regex = "2.5.0" node-semver = "2.1.0" num_cpus = "1.15.0" owo-colors.workspace = true diff --git a/crates/turborepo-lib/src/package_manager/mod.rs b/crates/turborepo-lib/src/package_manager/mod.rs index 4a1b675f73f1a..6f3ab803b43dd 100644 --- a/crates/turborepo-lib/src/package_manager/mod.rs +++ b/crates/turborepo-lib/src/package_manager/mod.rs @@ -6,11 +6,10 @@ use std::{ backtrace, fmt::{self, Display}, fs, - path::PathBuf, }; -use anyhow::{anyhow, Result as AnyhowResult}; use itertools::{Either, Itertools}; +use lazy_regex::{lazy_regex, Lazy}; use regex::Regex; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -129,14 +128,15 @@ impl Globs { }) } - pub fn test(&self, root: &AbsoluteSystemPath, target: PathBuf) -> AnyhowResult { - let search_value = target - .strip_prefix(root)? - .to_str() - .ok_or_else(|| anyhow!("The relative path is not UTF8."))?; + pub fn test( + &self, + root: &AbsoluteSystemPath, + target: &AbsoluteSystemPath, + ) -> Result { + let search_value = root.anchor(target)?; - let includes = self.inclusions.is_match(search_value); - let excludes = self.exclusions.is_match(search_value); + let includes = self.inclusions.is_match(&search_value); + let excludes = self.exclusions.is_match(&search_value); Ok(includes && !excludes) } @@ -151,6 +151,8 @@ pub struct MissingWorkspaceError { pub struct NoPackageManager; impl NoPackageManager { + // TODO: determine how to thread through user-friendly error message and apply + // our UI pub fn ui_display(&self, ui: &UI) -> String { let url = ui.apply(UNDERLINE.apply_to("https://nodejs.org/api/packages.html#packagemanager")); @@ -223,8 +225,17 @@ pub enum Error { Which(#[from] which::Error), #[error("invalid utf8: {0}")] Utf8Error(#[from] std::string::FromUtf8Error), + #[error(transparent)] + Path(#[from] turbopath::PathError), + #[error( + "We could not parse the packageManager field in package.json, expected: {0}, received: {1}" + )] + InvalidPackageManager(String, String), } +static PACKAGE_MANAGER_PATTERN: Lazy = + lazy_regex!(r"(?Pnpm|pnpm|yarn)@(?P\d+\.\d+\.\d+(-.+)?)"); + impl PackageManager { /// Returns the set of globs for the workspace. pub fn get_workspace_globs( @@ -283,7 +294,7 @@ impl PackageManager { } // Attempts to read the package manager from the package.json - fn read_package_manager(pkg: &PackageJson) -> AnyhowResult> { + fn read_package_manager(pkg: &PackageJson) -> Result, Error> { let Some(package_manager) = &pkg.package_manager else { return Ok(None) }; @@ -319,19 +330,15 @@ impl PackageManager { } } - pub(crate) fn parse_package_manager_string(manager: &str) -> AnyhowResult<(&str, &str)> { - let package_manager_pattern = - Regex::new(r"(?Pnpm|pnpm|yarn)@(?P\d+\.\d+\.\d+(-.+)?)")?; - if let Some(captures) = package_manager_pattern.captures(manager) { + pub(crate) fn parse_package_manager_string(manager: &str) -> Result<(&str, &str), Error> { + if let Some(captures) = PACKAGE_MANAGER_PATTERN.captures(manager) { let manager = captures.name("manager").unwrap().as_str(); let version = captures.name("version").unwrap().as_str(); Ok((manager, version)) } else { - Err(anyhow!( - "We could not parse packageManager field in package.json, expected: {}, received: \ - {}", - package_manager_pattern, - manager + Err(Error::InvalidPackageManager( + PACKAGE_MANAGER_PATTERN.to_string(), + manager.to_string(), )) } } @@ -435,7 +442,7 @@ mod tests { } #[test] - fn test_read_package_manager() -> AnyhowResult<()> { + fn test_read_package_manager() -> Result<(), Error> { let mut package_json = PackageJson { package_manager: Some("npm@8.19.4".to_string()), }; @@ -462,7 +469,7 @@ mod tests { } #[test] - fn test_detect_multiple_package_managers() -> AnyhowResult<()> { + fn test_detect_multiple_package_managers() -> Result<(), Error> { let repo_root = tempdir()?; let repo_root_path = AbsoluteSystemPathBuf::new(repo_root.path())?; @@ -471,8 +478,7 @@ mod tests { let pnpm_lock_path = repo_root.path().join(pnpm::LOCKFILE); File::create(pnpm_lock_path)?; - let error = - PackageManager::detect_package_manager(repo_root_path.as_absolute_path()).unwrap_err(); + let error = PackageManager::detect_package_manager(&repo_root_path).unwrap_err(); assert_eq!( error.to_string(), "We detected multiple package managers in your repository: pnpm, npm. Please remove \ @@ -481,8 +487,7 @@ mod tests { fs::remove_file(&package_lock_json_path)?; - let package_manager = - PackageManager::detect_package_manager(repo_root_path.as_absolute_path())?; + let package_manager = PackageManager::detect_package_manager(&repo_root_path)?; assert_eq!(package_manager, PackageManager::Pnpm); Ok(()) @@ -497,9 +502,7 @@ mod tests { .unwrap(); let with_yarn = repo_root.join_components(&["examples", "with-yarn"]); let package_manager = PackageManager::Npm; - let globs = package_manager - .get_workspace_globs(&with_yarn) - .unwrap(); + let globs = package_manager.get_workspace_globs(&with_yarn).unwrap(); let expected = Globs::new(vec!["apps/*", "packages/*"], vec![]).unwrap(); assert_eq!(globs, expected); @@ -510,8 +513,8 @@ mod tests { struct TestCase { globs: Globs, root: AbsoluteSystemPathBuf, - target: PathBuf, - output: AnyhowResult, + target: AbsoluteSystemPathBuf, + output: Result, } #[cfg(unix)] @@ -520,9 +523,9 @@ mod tests { let root = AbsoluteSystemPathBuf::new("C:\\a\\b\\c").unwrap(); #[cfg(unix)] - let target = PathBuf::from("/a/b/c/d/e/f"); + let target = AbsoluteSystemPathBuf::new("/a/b/c/d/e/f").unwrap(); #[cfg(windows)] - let target = PathBuf::from("C:\\a\\b\\c\\d\\e\\f"); + let target = AbsoluteSystemPathBuf::new("C:\\a\\b\\c\\d\\e\\f").unwrap(); let tests = [TestCase { globs: Globs::new(vec!["d/**".to_string()], vec![]).unwrap(), @@ -532,7 +535,7 @@ mod tests { }]; for test in tests { - match test.globs.test(&test.root, test.target) { + match test.globs.test(&test.root, &test.target) { Ok(value) => assert_eq!(value, test.output.unwrap()), Err(value) => assert_eq!(value.to_string(), test.output.unwrap_err().to_string()), }; @@ -540,7 +543,7 @@ mod tests { } #[test] - fn test_nested_workspace_globs() -> AnyhowResult<()> { + fn test_nested_workspace_globs() -> Result<(), Error> { let top_level: PackageJsonWorkspaces = serde_json::from_str("{ \"workspaces\": [\"packages/**\"]}")?; assert_eq!(top_level.workspaces.as_ref(), vec!["packages/**"]); diff --git a/crates/turborepo-lib/src/shim.rs b/crates/turborepo-lib/src/shim.rs index c90737b3856ed..d167f62f99354 100644 --- a/crates/turborepo-lib/src/shim.rs +++ b/crates/turborepo-lib/src/shim.rs @@ -445,11 +445,9 @@ impl InferInfo { info.has_turbo_json } - pub fn is_workspace_root_of(&self, target_path: &Path) -> bool { + pub fn is_workspace_root_of(&self, target_path: &AbsoluteSystemPath) -> bool { match &self.workspace_globs { - Some(globs) => globs - .test(&self.path, target_path.to_path_buf()) - .unwrap_or(false), + Some(globs) => globs.test(&self.path, target_path).unwrap_or(false), None => false, } } @@ -469,19 +467,10 @@ impl RepoState { return None; } - // FIXME: This should be based upon detecting the pacakage manager. - // However, we don't have that functionality implemented in Rust yet. - // PackageManager::detect(path).get_workspace_globs().unwrap_or(None) - let workspace_globs = PackageManager::get_package_manager(reference_dir, None) - .and_then(|mgr| mgr.get_workspace_globs(reference_dir)) + // FIXME: We should save this package manager that we detected + let workspace_globs = PackageManager::get_package_manager(path, None) + .and_then(|mgr| mgr.get_workspace_globs(path)) .ok(); - // let workspace_globs = PackageManager::Pnpm - // .get_workspace_globs(path) - // .unwrap_or_else(|_| { - // PackageManager::Npm - // .get_workspace_globs(path) - // .unwrap_or(None) - // }); Some(InferInfo { path: path.to_owned(), @@ -558,7 +547,7 @@ impl RepoState { // Failing that we just choose the closest. } else { for ancestor_infer in check_roots { - if ancestor_infer.is_workspace_root_of(current.path.as_path()) { + if ancestor_infer.is_workspace_root_of(¤t.path) { let local_turbo_state = LocalTurboState::infer(ancestor_infer.path.as_path()); return Ok(Self { diff --git a/crates/turborepo-paths/Cargo.toml b/crates/turborepo-paths/Cargo.toml index 5d4951549bd48..528763529512b 100644 --- a/crates/turborepo-paths/Cargo.toml +++ b/crates/turborepo-paths/Cargo.toml @@ -14,6 +14,7 @@ path-slash = "0.2.1" # TODO: Make this a crate feature serde = { workspace = true } thiserror = { workspace = true } +wax = { workspace = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/crates/turborepo-paths/src/anchored_system_path_buf.rs b/crates/turborepo-paths/src/anchored_system_path_buf.rs index 7b4eaf39b33f5..f96fc3904f01b 100644 --- a/crates/turborepo-paths/src/anchored_system_path_buf.rs +++ b/crates/turborepo-paths/src/anchored_system_path_buf.rs @@ -20,6 +20,13 @@ impl TryFrom<&Path> for AnchoredSystemPathBuf { } } +// TODO: perhaps we ought to be converting to a unix path? +impl<'a> Into> for &'a AnchoredSystemPathBuf { + fn into(self) -> wax::CandidatePath<'a> { + self.as_path().into() + } +} + impl AnchoredSystemPathBuf { pub fn new( root: impl AsRef,