diff --git a/crates/turborepo-errors/src/lib.rs b/crates/turborepo-errors/src/lib.rs index fe854b744e512..c0c4806765392 100644 --- a/crates/turborepo-errors/src/lib.rs +++ b/crates/turborepo-errors/src/lib.rs @@ -1,6 +1,8 @@ use std::{ fmt::Display, - ops::{Deref, Range}, + iter, + iter::Once, + ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -76,6 +78,24 @@ pub struct Spanned { pub text: Option>, } +impl IntoIterator for Spanned { + type Item = T; + type IntoIter = Once; + + fn into_iter(self) -> Self::IntoIter { + iter::once(self.value) + } +} + +impl<'a, T> IntoIterator for &'a Spanned { + type Item = &'a T; + type IntoIter = Once<&'a T>; + + fn into_iter(self) -> Self::IntoIter { + iter::once(&self.value) + } +} + impl Deserializable for Spanned { fn deserialize( value: &impl DeserializableValue, @@ -172,7 +192,7 @@ impl Spanned { let path = self.path.as_ref().map_or(default_path, |p| p.as_ref()); match self.range.clone().zip(self.text.as_ref()) { Some((range, text)) => (Some(range.into()), NamedSource::new(path, text.to_string())), - None => (None, NamedSource::new(path, String::new())), + None => (None, NamedSource::new(path, Default::default())), } } @@ -204,6 +224,13 @@ impl Deref for Spanned { &self.value } } + +impl DerefMut for Spanned { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + pub trait WithMetadata { fn add_text(&mut self, text: Arc); fn add_path(&mut self, path: Arc); diff --git a/crates/turborepo-lib/src/boundaries/config.rs b/crates/turborepo-lib/src/boundaries/config.rs new file mode 100644 index 0000000000000..6bf20f0438d04 --- /dev/null +++ b/crates/turborepo-lib/src/boundaries/config.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use biome_deserialize_macros::Deserializable; +use serde::Serialize; +use struct_iterable::Iterable; +use turborepo_errors::Spanned; + +#[derive(Serialize, Default, Debug, Clone, Iterable, Deserializable, PartialEq)] +pub struct RootBoundariesConfig { + pub tags: Option>, +} +pub type RulesMap = HashMap>; + +#[derive(Serialize, Default, Debug, Clone, Iterable, Deserializable, PartialEq)] +pub struct Rule { + pub dependencies: Option>, + pub dependents: Option>, +} + +#[derive(Serialize, Default, Debug, Clone, Iterable, Deserializable, PartialEq)] +pub struct Permissions { + pub allow: Option>>>, + pub deny: Option>>>, +} diff --git a/crates/turborepo-lib/src/boundaries/imports.rs b/crates/turborepo-lib/src/boundaries/imports.rs new file mode 100644 index 0000000000000..be3295cae58c9 --- /dev/null +++ b/crates/turborepo-lib/src/boundaries/imports.rs @@ -0,0 +1,193 @@ +use std::collections::{BTreeMap, HashSet}; + +use itertools::Itertools; +use miette::{NamedSource, SourceSpan}; +use oxc_resolver::{ResolveError, Resolver}; +use turbo_trace::ImportType; +use turbopath::{AbsoluteSystemPath, PathRelation, RelativeUnixPath}; +use turborepo_repository::{ + package_graph::{PackageName, PackageNode}, + package_json::PackageJson, +}; + +use crate::{ + boundaries::{BoundariesDiagnostic, Error}, + run::Run, +}; + +impl Run { + pub(crate) fn check_file_import( + &self, + file_path: &AbsoluteSystemPath, + package_path: &AbsoluteSystemPath, + import: &str, + source_span: SourceSpan, + file_content: &str, + ) -> Result, Error> { + let import_path = RelativeUnixPath::new(import)?; + let dir_path = file_path + .parent() + .ok_or_else(|| Error::NoParentDir(file_path.to_owned()))?; + let resolved_import_path = dir_path.join_unix_path(import_path).clean()?; + // We have to check for this case because `relation_to_path` returns `Parent` if + // the paths are equal and there's nothing wrong with importing the + // package you're in. + if resolved_import_path.as_str() == package_path.as_str() { + return Ok(None); + } + // We use `relation_to_path` and not `contains` because `contains` + // panics on invalid paths with too many `..` components + if !matches!( + package_path.relation_to_path(&resolved_import_path), + PathRelation::Parent + ) { + Ok(Some(BoundariesDiagnostic::ImportLeavesPackage { + import: import.to_string(), + span: source_span, + text: NamedSource::new(file_path.as_str(), file_content.to_string()), + })) + } else { + Ok(None) + } + } + + /// Go through all the possible places a package could be declared to see if + /// it's a valid import. We don't use `oxc_resolver` because there are some + /// cases where you can resolve a package that isn't declared properly. + fn is_dependency( + internal_dependencies: &HashSet<&PackageNode>, + package_json: &PackageJson, + unresolved_external_dependencies: Option<&BTreeMap>, + package_name: &PackageNode, + ) -> bool { + internal_dependencies.contains(&package_name) + || unresolved_external_dependencies.is_some_and(|external_dependencies| { + external_dependencies.contains_key(package_name.as_package_name().as_str()) + }) + || package_json + .dependencies + .as_ref() + .is_some_and(|dependencies| { + dependencies.contains_key(package_name.as_package_name().as_str()) + }) + || package_json + .dev_dependencies + .as_ref() + .is_some_and(|dev_dependencies| { + dev_dependencies.contains_key(package_name.as_package_name().as_str()) + }) + || package_json + .peer_dependencies + .as_ref() + .is_some_and(|peer_dependencies| { + peer_dependencies.contains_key(package_name.as_package_name().as_str()) + }) + || package_json + .optional_dependencies + .as_ref() + .is_some_and(|optional_dependencies| { + optional_dependencies.contains_key(package_name.as_package_name().as_str()) + }) + } + + fn get_package_name(import: &str) -> String { + if import.starts_with("@") { + import.split('/').take(2).join("/") + } else { + import + .split_once("/") + .map(|(import, _)| import) + .unwrap_or(import) + .to_string() + } + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn check_package_import( + &self, + import: &str, + import_type: ImportType, + span: SourceSpan, + file_path: &AbsoluteSystemPath, + file_content: &str, + package_json: &PackageJson, + internal_dependencies: &HashSet<&PackageNode>, + unresolved_external_dependencies: Option<&BTreeMap>, + resolver: &Resolver, + ) -> Option { + let package_name = Self::get_package_name(import); + + if package_name.starts_with("@types/") && matches!(import_type, ImportType::Value) { + return Some(BoundariesDiagnostic::NotTypeOnlyImport { + import: import.to_string(), + span, + text: NamedSource::new(file_path.as_str(), file_content.to_string()), + }); + } + let package_name = PackageNode::Workspace(PackageName::Other(package_name)); + let folder = file_path.parent().expect("file_path should have a parent"); + let is_valid_dependency = Self::is_dependency( + internal_dependencies, + package_json, + unresolved_external_dependencies, + &package_name, + ); + + if !is_valid_dependency + && !matches!( + resolver.resolve(folder, import), + Err(ResolveError::Builtin { .. }) + ) + { + // Check the @types package + let types_package_name = PackageNode::Workspace(PackageName::Other(format!( + "@types/{}", + package_name.as_package_name().as_str() + ))); + let is_types_dependency = Self::is_dependency( + internal_dependencies, + package_json, + unresolved_external_dependencies, + &types_package_name, + ); + + if is_types_dependency { + return match import_type { + ImportType::Type => None, + ImportType::Value => Some(BoundariesDiagnostic::NotTypeOnlyImport { + import: import.to_string(), + span, + text: NamedSource::new(file_path.as_str(), file_content.to_string()), + }), + }; + } + + return Some(BoundariesDiagnostic::PackageNotFound { + name: package_name.to_string(), + span, + text: NamedSource::new(file_path.as_str(), file_content.to_string()), + }); + } + + None + } +} + +#[cfg(test)] +mod test { + use test_case::test_case; + + use super::*; + + #[test_case("", ""; "empty")] + #[test_case("ship", "ship"; "basic")] + #[test_case("@types/ship", "@types/ship"; "types")] + #[test_case("@scope/ship", "@scope/ship"; "scoped")] + #[test_case("@scope/foo/bar", "@scope/foo"; "scoped with path")] + #[test_case("foo/bar", "foo"; "regular with path")] + #[test_case("foo/", "foo"; "trailing slash")] + #[test_case("foo/bar/baz", "foo"; "multiple slashes")] + fn test_get_package_name(import: &str, expected: &str) { + assert_eq!(Run::get_package_name(import), expected); + } +} diff --git a/crates/turborepo-lib/src/boundaries.rs b/crates/turborepo-lib/src/boundaries/mod.rs similarity index 55% rename from crates/turborepo-lib/src/boundaries.rs rename to crates/turborepo-lib/src/boundaries/mod.rs index 86923ecc51107..dca94afb477bc 100644 --- a/crates/turborepo-lib/src/boundaries.rs +++ b/crates/turborepo-lib/src/boundaries/mod.rs @@ -1,13 +1,16 @@ +mod config; +mod imports; +mod tags; + use std::{ - collections::{BTreeMap, HashSet}, + collections::{HashMap, HashSet}, sync::{Arc, LazyLock, Mutex}, }; +pub use config::{Permissions, RootBoundariesConfig, Rule}; use git2::Repository; use globwalk::Settings; -use itertools::Itertools; use miette::{Diagnostic, NamedSource, Report, SourceSpan}; -use oxc_resolver::{ResolveError, Resolver}; use regex::Regex; use swc_common::{ comments::SingleThreadedComments, errors::Handler, input::StringInput, FileName, SourceMap, @@ -17,18 +20,67 @@ use swc_ecma_parser::{lexer::Lexer, Capturing, EsSyntax, Parser, Syntax, TsSynta use swc_ecma_visit::VisitWith; use thiserror::Error; use tracing::log::warn; -use turbo_trace::{ImportFinder, ImportType, Tracer}; -use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf, PathRelation, RelativeUnixPath}; -use turborepo_repository::{ - package_graph::{PackageName, PackageNode}, - package_json::PackageJson, -}; +use turbo_trace::{ImportFinder, Tracer}; +use turbopath::AbsoluteSystemPathBuf; +use turborepo_errors::Spanned; +use turborepo_repository::package_graph::{PackageInfo, PackageName, PackageNode}; use turborepo_ui::{color, ColorConfig, BOLD_GREEN, BOLD_RED}; -use crate::run::Run; +use crate::{boundaries::tags::ProcessedRulesMap, run::Run}; + +#[derive(Clone, Debug, Error, Diagnostic)] +pub enum SecondaryDiagnostic { + #[error("consider adding one of the following tags listed here")] + Allowlist { + #[label] + span: Option, + #[source_code] + text: NamedSource, + }, + #[error("denylist defined here")] + Denylist { + #[label] + span: Option, + #[source_code] + text: NamedSource, + }, +} #[derive(Clone, Debug, Error, Diagnostic)] pub enum BoundariesDiagnostic { + #[error( + "Package `{package_name}` found without any tag listed in allowlist for \ + `{source_package_name}`" + )] + NoTagInAllowlist { + // The package that is declaring the allowlist + source_package_name: PackageName, + // The package that is either a dependency or dependent of the source package + package_name: PackageName, + #[label("tag not found here")] + span: Option, + #[help] + help: Option, + #[source_code] + text: NamedSource, + #[related] + secondary: [SecondaryDiagnostic; 1], + }, + #[error( + "Package `{package_name}` found with tag listed in denylist for `{source_package_name}`: \ + `{tag}`" + )] + DeniedTag { + source_package_name: PackageName, + package_name: PackageName, + tag: String, + #[label("tag found here")] + span: Option, + #[source_code] + text: NamedSource, + #[related] + secondary: [SecondaryDiagnostic; 1], + }, #[error( "importing from a type declaration package, but import is not declared as a type-only \ import" @@ -63,6 +115,8 @@ pub enum BoundariesDiagnostic { #[derive(Debug, Error, Diagnostic)] pub enum Error { + #[error(transparent)] + Config(#[from] crate::config::Error), #[error("file `{0}` does not have a parent directory")] NoParentDir(AbsoluteSystemPathBuf), #[error(transparent)] @@ -131,6 +185,8 @@ impl BoundariesResult { impl Run { pub async fn check_boundaries(&self) -> Result { + let package_tags = self.get_package_tags(); + let rules_map = self.get_processed_rules_map(); let packages = self.pkg_dep_graph().packages(); let repo = Repository::discover(self.repo_root()).ok().map(Mutex::new); let mut diagnostics = vec![]; @@ -143,23 +199,14 @@ impl Run { continue; } - let package_root = self.repo_root().resolve(package_info.package_path()); - - let internal_dependencies = self - .pkg_dep_graph() - .immediate_dependencies(&PackageNode::Workspace(package_name.to_owned())) - .unwrap_or_default(); - let unresolved_external_dependencies = - package_info.unresolved_external_dependencies.as_ref(); - let (files_checked, package_diagnostics) = self .check_package( &repo, - &package_root, - &package_info.package_json, - internal_dependencies, - unresolved_external_dependencies, + package_name, + package_info, &source_map, + &package_tags, + &rules_map, ) .await?; @@ -176,23 +223,63 @@ impl Run { }) } + /// Either returns a list of errors and number of files checked or a single, + /// fatal error + async fn check_package( + &self, + repo: &Option>, + package_name: &PackageName, + package_info: &PackageInfo, + source_map: &SourceMap, + all_package_tags: &HashMap>>>, + tag_rules: &Option, + ) -> Result<(usize, Vec), Error> { + let (files_checked, mut diagnostics) = self + .check_package_files(repo, package_name, package_info, source_map) + .await?; + + if let Some(current_package_tags) = all_package_tags.get(package_name) { + if let Some(tag_rules) = tag_rules { + diagnostics.extend(self.check_package_tags( + PackageNode::Workspace(package_name.clone()), + current_package_tags, + all_package_tags, + tag_rules, + )?); + } else { + // NOTE: if we use tags for something other than boundaries, we should remove + // this warning + warn!( + "No boundaries rules found, but package {} has tags", + package_name + ); + } + } + + Ok((files_checked, diagnostics)) + } + fn is_potential_package_name(import: &str) -> bool { PACKAGE_NAME_REGEX.is_match(import) } - /// Either returns a list of errors and number of files checked or a single, - /// fatal error - async fn check_package( + async fn check_package_files( &self, repo: &Option>, - package_root: &AbsoluteSystemPath, - package_json: &PackageJson, - internal_dependencies: HashSet<&PackageNode>, - unresolved_external_dependencies: Option<&BTreeMap>, + package_name: &PackageName, + package_info: &PackageInfo, source_map: &SourceMap, ) -> Result<(usize, Vec), Error> { + let package_root = self.repo_root().resolve(package_info.package_path()); + let internal_dependencies = self + .pkg_dep_graph() + .immediate_dependencies(&PackageNode::Workspace(package_name.to_owned())) + .unwrap_or_default(); + let unresolved_external_dependencies = + package_info.unresolved_external_dependencies.as_ref(); + let files = globwalk::globwalk_with_settings( - package_root, + &package_root, &[ "**/*.js".parse().unwrap(), "**/*.jsx".parse().unwrap(), @@ -284,7 +371,7 @@ impl Run { // We have a file import let check_result = if import.starts_with(".") { - self.check_file_import(&file_path, package_root, import, span, &file_content)? + self.check_file_import(&file_path, &package_root, import, span, &file_content)? } else if Self::is_potential_package_name(import) { self.check_package_import( import, @@ -292,7 +379,7 @@ impl Run { span, &file_path, &file_content, - package_json, + &package_info.package_json, &internal_dependencies, unresolved_external_dependencies, &resolver, @@ -319,179 +406,4 @@ impl Run { Ok((files_checked, diagnostics)) } - - fn check_file_import( - &self, - file_path: &AbsoluteSystemPath, - package_path: &AbsoluteSystemPath, - import: &str, - source_span: SourceSpan, - file_content: &str, - ) -> Result, Error> { - let import_path = RelativeUnixPath::new(import)?; - let dir_path = file_path - .parent() - .ok_or_else(|| Error::NoParentDir(file_path.to_owned()))?; - let resolved_import_path = dir_path.join_unix_path(import_path).clean()?; - // We have to check for this case because `relation_to_path` returns `Parent` if - // the paths are equal and there's nothing wrong with importing the - // package you're in. - if resolved_import_path.as_str() == package_path.as_str() { - return Ok(None); - } - // We use `relation_to_path` and not `contains` because `contains` - // panics on invalid paths with too many `..` components - if !matches!( - package_path.relation_to_path(&resolved_import_path), - PathRelation::Parent - ) { - Ok(Some(BoundariesDiagnostic::ImportLeavesPackage { - import: import.to_string(), - span: source_span, - text: NamedSource::new(file_path.as_str(), file_content.to_string()), - })) - } else { - Ok(None) - } - } - - /// Go through all the possible places a package could be declared to see if - /// it's a valid import. We don't use `oxc_resolver` because there are some - /// cases where you can resolve a package that isn't declared properly. - fn is_dependency( - internal_dependencies: &HashSet<&PackageNode>, - package_json: &PackageJson, - unresolved_external_dependencies: Option<&BTreeMap>, - package_name: &PackageNode, - ) -> bool { - internal_dependencies.contains(&package_name) - || unresolved_external_dependencies.is_some_and(|external_dependencies| { - external_dependencies.contains_key(package_name.as_package_name().as_str()) - }) - || package_json - .dependencies - .as_ref() - .is_some_and(|dependencies| { - dependencies.contains_key(package_name.as_package_name().as_str()) - }) - || package_json - .dev_dependencies - .as_ref() - .is_some_and(|dev_dependencies| { - dev_dependencies.contains_key(package_name.as_package_name().as_str()) - }) - || package_json - .peer_dependencies - .as_ref() - .is_some_and(|peer_dependencies| { - peer_dependencies.contains_key(package_name.as_package_name().as_str()) - }) - || package_json - .optional_dependencies - .as_ref() - .is_some_and(|optional_dependencies| { - optional_dependencies.contains_key(package_name.as_package_name().as_str()) - }) - } - - fn get_package_name(import: &str) -> String { - if import.starts_with("@") { - import.split('/').take(2).join("/") - } else { - import - .split_once("/") - .map(|(import, _)| import) - .unwrap_or(import) - .to_string() - } - } - - #[allow(clippy::too_many_arguments)] - fn check_package_import( - &self, - import: &str, - import_type: ImportType, - span: SourceSpan, - file_path: &AbsoluteSystemPath, - file_content: &str, - package_json: &PackageJson, - internal_dependencies: &HashSet<&PackageNode>, - unresolved_external_dependencies: Option<&BTreeMap>, - resolver: &Resolver, - ) -> Option { - let package_name = Self::get_package_name(import); - - if package_name.starts_with("@types/") && matches!(import_type, ImportType::Value) { - return Some(BoundariesDiagnostic::NotTypeOnlyImport { - import: import.to_string(), - span, - text: NamedSource::new(file_path.as_str(), file_content.to_string()), - }); - } - let package_name = PackageNode::Workspace(PackageName::Other(package_name)); - let folder = file_path.parent().expect("file_path should have a parent"); - let is_valid_dependency = Self::is_dependency( - internal_dependencies, - package_json, - unresolved_external_dependencies, - &package_name, - ); - - if !is_valid_dependency - && !matches!( - resolver.resolve(folder, import), - Err(ResolveError::Builtin { .. }) - ) - { - // Check the @types package - let types_package_name = PackageNode::Workspace(PackageName::Other(format!( - "@types/{}", - package_name.as_package_name().as_str() - ))); - let is_types_dependency = Self::is_dependency( - internal_dependencies, - package_json, - unresolved_external_dependencies, - &types_package_name, - ); - - if is_types_dependency { - return match import_type { - ImportType::Type => None, - ImportType::Value => Some(BoundariesDiagnostic::NotTypeOnlyImport { - import: import.to_string(), - span, - text: NamedSource::new(file_path.as_str(), file_content.to_string()), - }), - }; - } - - return Some(BoundariesDiagnostic::PackageNotFound { - name: package_name.to_string(), - span, - text: NamedSource::new(file_path.as_str(), file_content.to_string()), - }); - } - - None - } -} - -#[cfg(test)] -mod test { - use test_case::test_case; - - use super::*; - - #[test_case("", ""; "empty")] - #[test_case("ship", "ship"; "basic")] - #[test_case("@types/ship", "@types/ship"; "types")] - #[test_case("@scope/ship", "@scope/ship"; "scoped")] - #[test_case("@scope/foo/bar", "@scope/foo"; "scoped with path")] - #[test_case("foo/bar", "foo"; "regular with path")] - #[test_case("foo/", "foo"; "trailing slash")] - #[test_case("foo/bar/baz", "foo"; "multiple slashes")] - fn test_get_package_name(import: &str, expected: &str) { - assert_eq!(Run::get_package_name(import), expected); - } } diff --git a/crates/turborepo-lib/src/boundaries/tags.rs b/crates/turborepo-lib/src/boundaries/tags.rs new file mode 100644 index 0000000000000..c016f8a1150e6 --- /dev/null +++ b/crates/turborepo-lib/src/boundaries/tags.rs @@ -0,0 +1,208 @@ +use std::collections::{HashMap, HashSet}; + +use tracing::warn; +use turborepo_errors::Spanned; +use turborepo_repository::package_graph::{PackageName, PackageNode}; + +use crate::{ + boundaries::{config::Rule, BoundariesDiagnostic, Error, Permissions, SecondaryDiagnostic}, + run::Run, + turbo_json::TurboJson, +}; + +pub type ProcessedRulesMap = HashMap; + +pub struct ProcessedRule { + dependencies: Option, + dependents: Option, +} + +impl From for ProcessedRule { + fn from(rule: Rule) -> Self { + Self { + dependencies: rule + .dependencies + .map(|dependencies| dependencies.into_inner().into()), + dependents: rule + .dependents + .map(|dependents| dependents.into_inner().into()), + } + } +} + +pub struct ProcessedPermissions { + allow: Option>>, + deny: Option>>, +} + +impl From for ProcessedPermissions { + fn from(permissions: Permissions) -> Self { + Self { + allow: permissions + .allow + .map(|allow| allow.map(|allow| allow.into_iter().flatten().collect())), + deny: permissions + .deny + .map(|deny| deny.map(|deny| deny.into_iter().flatten().collect())), + } + } +} + +impl Run { + pub(crate) fn get_package_tags(&self) -> HashMap>>> { + let mut package_tags = HashMap::new(); + let mut turbo_json_loader = self.turbo_json_loader(); + for (package, _) in self.pkg_dep_graph().packages() { + if let Ok(TurboJson { + tags: Some(tags), + boundaries, + .. + }) = turbo_json_loader.load(package) + { + if boundaries.is_some() && !matches!(package, PackageName::Root) { + warn!( + "Boundaries rules can only be defined in the root turbo.json. Any rules \ + defined in a package's turbo.json will be ignored." + ) + } + package_tags.insert(package.clone(), tags.clone()); + } + } + + package_tags + } + + pub(crate) fn get_processed_rules_map(&self) -> Option { + self.root_turbo_json() + .boundaries + .as_ref() + .and_then(|boundaries| boundaries.tags.as_ref()) + .map(|tags| { + tags.as_inner() + .iter() + .map(|(k, v)| (k.clone(), v.as_inner().clone().into())) + .collect() + }) + } + + /// Loops through the tags of a package that is related to `package_name` + /// (i.e. either a dependency or a dependent) and checks if the tag is + /// allowed or denied by the rules in `allow_list` and `deny_list`. + fn validate_relation( + &self, + package_name: &PackageName, + relation_package_name: &PackageName, + tags: Option<&Spanned>>>, + allow_list: Option<&Spanned>>, + deny_list: Option<&Spanned>>, + ) -> Result, Error> { + // If there is no allow list, then we vacuously have a tag in the allow list + let mut has_tag_in_allowlist = allow_list.is_none(); + let tags_span = tags.map(|tags| tags.to(())).unwrap_or_default(); + + for tag in tags.into_iter().flatten().flatten() { + if let Some(allow_list) = allow_list { + if allow_list.contains(tag.as_inner()) { + has_tag_in_allowlist = true; + } + } + + if let Some(deny_list) = deny_list { + if deny_list.contains(tag.as_inner()) { + let (span, text) = tag.span_and_text("turbo.json"); + let deny_list_spanned = deny_list.to(()); + let (deny_list_span, deny_list_text) = + deny_list_spanned.span_and_text("turbo.json"); + + return Ok(Some(BoundariesDiagnostic::DeniedTag { + source_package_name: package_name.clone(), + package_name: relation_package_name.clone(), + tag: tag.as_inner().to_string(), + span, + text, + secondary: [SecondaryDiagnostic::Denylist { + span: deny_list_span, + text: deny_list_text, + }], + })); + } + } + } + + if !has_tag_in_allowlist { + let (span, text) = tags_span.span_and_text("turbo.json"); + let help = span.is_none().then(|| { + format!( + "`{}` doesn't any tags defined in its `turbo.json` file", + relation_package_name + ) + }); + + let allow_list_spanned = allow_list + .map(|allow_list| allow_list.to(())) + .unwrap_or_default(); + let (allow_list_span, allow_list_text) = allow_list_spanned.span_and_text("turbo.json"); + + return Ok(Some(BoundariesDiagnostic::NoTagInAllowlist { + source_package_name: package_name.clone(), + package_name: relation_package_name.clone(), + help, + span, + text, + secondary: [SecondaryDiagnostic::Allowlist { + span: allow_list_span, + text: allow_list_text, + }], + })); + } + + Ok(None) + } + + pub(crate) fn check_package_tags( + &self, + pkg: PackageNode, + current_package_tags: &Spanned>>, + all_package_tags: &HashMap>>>, + tags_rules: &ProcessedRulesMap, + ) -> Result, Error> { + let mut diagnostics = Vec::new(); + for tag in current_package_tags.iter() { + if let Some(rule) = tags_rules.get(tag.as_inner()) { + if let Some(dependency_permissions) = &rule.dependencies { + for dependency in self.pkg_dep_graph().dependencies(&pkg) { + if matches!(dependency, PackageNode::Root) { + continue; + } + let dependency_tags = all_package_tags.get(dependency.as_package_name()); + diagnostics.extend(self.validate_relation( + pkg.as_package_name(), + dependency.as_package_name(), + dependency_tags, + dependency_permissions.allow.as_ref(), + dependency_permissions.deny.as_ref(), + )?); + } + } + + if let Some(dependent_permissions) = &rule.dependents { + for dependent in self.pkg_dep_graph().ancestors(&pkg) { + if matches!(dependent, PackageNode::Root) { + continue; + } + let dependent_tags = all_package_tags.get(dependent.as_package_name()); + diagnostics.extend(self.validate_relation( + pkg.as_package_name(), + dependent.as_package_name(), + dependent_tags, + dependent_permissions.allow.as_ref(), + dependent_permissions.deny.as_ref(), + )?) + } + } + } + } + + Ok(diagnostics) + } +} diff --git a/crates/turborepo-lib/src/query/boundaries.rs b/crates/turborepo-lib/src/query/boundaries.rs index 7ce65808f0d76..675161a0336e2 100644 --- a/crates/turborepo-lib/src/query/boundaries.rs +++ b/crates/turborepo-lib/src/query/boundaries.rs @@ -37,6 +37,37 @@ impl From for Diagnostic { path: None, reason: None, }, + + BoundariesDiagnostic::NoTagInAllowlist { + source_package_name: _, + help: _, + secondary: _, + package_name, + span, + text, + } => Diagnostic { + message, + path: Some(text.name().to_string()), + start: span.map(|span| span.offset()), + end: span.map(|span| span.offset() + span.len()), + import: Some(package_name.to_string()), + reason: None, + }, + BoundariesDiagnostic::DeniedTag { + source_package_name: _, + secondary: _, + package_name, + tag, + span, + text, + } => Diagnostic { + message, + path: Some(text.name().to_string()), + start: span.map(|span| span.offset()), + end: span.map(|span| span.offset() + span.len()), + import: Some(package_name.to_string()), + reason: Some(tag), + }, } } } diff --git a/crates/turborepo-lib/src/query/mod.rs b/crates/turborepo-lib/src/query/mod.rs index 34bdf06103037..c7e3278ab2efa 100644 --- a/crates/turborepo-lib/src/query/mod.rs +++ b/crates/turborepo-lib/src/query/mod.rs @@ -15,6 +15,7 @@ use std::{ use async_graphql::{http::GraphiQLSource, *}; use axum::{response, response::IntoResponse}; use external_package::ExternalPackage; +use itertools::Itertools; use package::Package; use package_graph::{Edge, PackageGraph}; pub use server::run_server; @@ -569,7 +570,12 @@ impl RepositoryQuery { /// Check boundaries for all packages. async fn boundaries(&self) -> Result, Error> { match self.run.check_boundaries().await { - Ok(result) => Ok(result.diagnostics.into_iter().map(|b| b.into()).collect()), + Ok(result) => Ok(result + .diagnostics + .into_iter() + .map(|b| b.into()) + .sorted_by(|a: &Diagnostic, b: &Diagnostic| a.message.cmp(&b.message)) + .collect()), Err(err) => Err(Error::Boundaries(err)), } } diff --git a/crates/turborepo-lib/src/run/builder.rs b/crates/turborepo-lib/src/run/builder.rs index f0a7a46bd319a..f1773daa87785 100644 --- a/crates/turborepo-lib/src/run/builder.rs +++ b/crates/turborepo-lib/src/run/builder.rs @@ -445,7 +445,7 @@ impl RunBuilder { &pkg_dep_graph, &root_turbo_json, filtered_pkgs.keys(), - turbo_json_loader, + turbo_json_loader.clone(), )?; } @@ -480,6 +480,7 @@ impl RunBuilder { env_at_execution_start, filtered_pkgs: filtered_pkgs.keys().cloned().collect(), pkg_dep_graph: Arc::new(pkg_dep_graph), + turbo_json_loader, root_turbo_json, scm, engine: Arc::new(engine), diff --git a/crates/turborepo-lib/src/run/mod.rs b/crates/turborepo-lib/src/run/mod.rs index 7ea0389e711fc..964cefb892760 100644 --- a/crates/turborepo-lib/src/run/mod.rs +++ b/crates/turborepo-lib/src/run/mod.rs @@ -48,7 +48,7 @@ use crate::{ signal::SignalHandler, task_graph::Visitor, task_hash::{get_external_deps_hash, get_internal_deps_hash, PackageInputsHashes}, - turbo_json::{TurboJson, UIMode}, + turbo_json::{TurboJson, TurboJsonLoader, UIMode}, DaemonClient, DaemonConnector, }; @@ -66,6 +66,7 @@ pub struct Run { env_at_execution_start: EnvironmentVariableMap, filtered_pkgs: HashSet, pkg_dep_graph: Arc, + turbo_json_loader: TurboJsonLoader, root_turbo_json: TurboJson, scm: SCM, run_cache: Arc, @@ -122,6 +123,10 @@ impl Run { } } + pub fn turbo_json_loader(&self) -> TurboJsonLoader { + self.turbo_json_loader.clone() + } + pub fn opts(&self) -> &Opts { &self.opts } diff --git a/crates/turborepo-lib/src/shim/mod.rs b/crates/turborepo-lib/src/shim/mod.rs index 570601ffd5406..6ec56fff02f4c 100644 --- a/crates/turborepo-lib/src/shim/mod.rs +++ b/crates/turborepo-lib/src/shim/mod.rs @@ -288,11 +288,20 @@ pub fn run() -> Result { let _ = miette::set_hook(Box::new(|_| { Box::new( miette::MietteHandlerOpts::new() + .show_related_errors_as_nested() .color(false) .unicode(false) .build(), ) })); + } else { + let _ = miette::set_hook(Box::new(|_| { + Box::new( + miette::MietteHandlerOpts::new() + .show_related_errors_as_nested() + .build(), + ) + })); } let subscriber = TurboSubscriber::new_with_verbosity(args.verbosity, &color_config); diff --git a/crates/turborepo-lib/src/turbo_json/loader.rs b/crates/turborepo-lib/src/turbo_json/loader.rs index 01bc26f1e61b5..b4f20fb776e1d 100644 --- a/crates/turborepo-lib/src/turbo_json/loader.rs +++ b/crates/turborepo-lib/src/turbo_json/loader.rs @@ -163,7 +163,7 @@ impl TurboJsonLoader { .expect("just inserted value for this key")) } - fn uncached_load(&self, package: &PackageName) -> Result { + pub fn uncached_load(&self, package: &PackageName) -> Result { match &self.strategy { Strategy::SinglePackage { package_json, diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index 894db94c1d11e..16c0fe8a28f3c 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -31,7 +31,7 @@ pub mod parser; pub use loader::TurboJsonLoader; -use crate::config::UnnecessaryPackageTaskSyntaxError; +use crate::{boundaries::RootBoundariesConfig, config::UnnecessaryPackageTaskSyntaxError}; #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, Deserializable)] #[serde(rename_all = "camelCase")] @@ -53,6 +53,8 @@ pub struct SpacesJson { pub struct TurboJson { text: Option>, path: Option>, + pub(crate) tags: Option>>>, + pub(crate) boundaries: Option>, pub(crate) extends: Spanned>, pub(crate) global_deps: Vec, pub(crate) global_env: Vec, @@ -146,6 +148,12 @@ pub struct RawTurboJson { #[serde(skip_serializing_if = "Option::is_none")] pub cache_dir: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option>>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub boundaries: Option>, + #[deserializable(rename = "//")] #[serde(skip)] _comment: Option, @@ -565,6 +573,7 @@ impl TryFrom for TurboJson { Ok(TurboJson { text: raw_turbo.span.text, path: raw_turbo.span.path, + tags: raw_turbo.tags, global_env: { let mut global_env: Vec<_> = global_env.into_iter().collect(); global_env.sort(); @@ -593,6 +602,7 @@ impl TryFrom for TurboJson { .extends .unwrap_or_default() .map(|s| s.into_iter().map(|s| s.into()).collect()), + boundaries: raw_turbo.boundaries, // Spaces and Remote Cache config is handled through layered config }) } @@ -764,17 +774,61 @@ mod tests { use super::{RawTurboJson, Spanned, TurboJson, UIMode}; use crate::{ + boundaries::RootBoundariesConfig, cli::OutputLogsMode, run::task_id::TaskName, task_graph::{TaskDefinition, TaskOutputs}, turbo_json::RawTaskDefinition, }; + #[test_case("{}", "empty boundaries")] + #[test_case(r#"{"tags": {} }"#, "empty tags")] + #[test_case( + r#"{"tags": { "my-tag": { "dependencies": { "allow": ["my-package"] } } } }"#, + "tags and dependencies" + )] + #[test_case( + r#"{ + "tags": { + "my-tag": { + "dependencies": { + "allow": ["my-package"], + "deny": ["my-other-package"] + } + } + } + }"#, + "tags and dependencies 2" + )] + #[test_case( + r#"{ + "tags": { + "my-tag": { + "dependents": { + "allow": ["my-package"], + "deny": ["my-other-package"] + } + } + } + }"#, + "tags and dependents" + )] + fn test_deserialize_boundaries(json: &str, name: &str) { + let deserialized_result = deserialize_from_json_str( + json, + JsonParserOptions::default().with_allow_comments(), + "turbo.json", + ); + let raw_task_definition: RootBoundariesConfig = + deserialized_result.into_deserialized().unwrap(); + insta::assert_json_snapshot!(name.replace(' ', "_"), raw_task_definition); + } + #[test_case( "{}", RawTaskDefinition::default(), TaskDefinition::default() - ; "empty")] + ; "empty task definition")] #[test_case( r#"{ "persistent": false }"#, RawTaskDefinition { @@ -991,6 +1045,14 @@ mod tests { assert_eq!(actual, expected); } + #[test_case(r#"{ "tags": [] }"#, "empty tags in package")] + #[test_case(r#"{ "tags": ["my-tag"] }"#, "one tag")] + #[test_case(r#"{ "tags": ["my-tag", "my-other-tag"] }"#, "two tags")] + fn test_tags(json: &str, name: &str) { + let json = RawTurboJson::parse(json, "").unwrap(); + insta::assert_json_snapshot!(name.replace(' ', "_"), json.tags); + } + #[test_case(r#"{ "ui": "tui" }"#, Some(UIMode::Tui) ; "tui")] #[test_case(r#"{ "ui": "stream" }"#, Some(UIMode::Stream) ; "stream")] #[test_case(r#"{}"#, None ; "missing")] diff --git a/crates/turborepo-lib/src/turbo_json/parser.rs b/crates/turborepo-lib/src/turbo_json/parser.rs index 456d5a8b21ec3..3f71020964697 100644 --- a/crates/turborepo-lib/src/turbo_json/parser.rs +++ b/crates/turborepo-lib/src/turbo_json/parser.rs @@ -15,6 +15,7 @@ use turborepo_errors::{ParseDiagnostic, WithMetadata}; use turborepo_unescape::UnescapedString; use crate::{ + boundaries::{Permissions, RootBoundariesConfig, Rule}, run::task_id::TaskName, turbo_json::{Pipeline, RawTaskDefinition, RawTurboJson, Spanned}, }; @@ -100,9 +101,18 @@ impl WithMetadata for RawTurboJson { fn add_text(&mut self, text: Arc) { self.span.add_text(text.clone()); self.extends.add_text(text.clone()); + self.tags.add_text(text.clone()); + if let Some(tags) = &mut self.tags { + tags.value.add_text(text.clone()); + } self.global_dependencies.add_text(text.clone()); self.global_env.add_text(text.clone()); self.global_pass_through_env.add_text(text.clone()); + self.boundaries.add_text(text.clone()); + if let Some(boundaries) = &mut self.boundaries { + boundaries.value.add_text(text.clone()); + } + self.tasks.add_text(text.clone()); self.cache_dir.add_text(text.clone()); self.pipeline.add_text(text); @@ -111,9 +121,17 @@ impl WithMetadata for RawTurboJson { fn add_path(&mut self, path: Arc) { self.span.add_path(path.clone()); self.extends.add_path(path.clone()); + self.tags.add_path(path.clone()); + if let Some(tags) = &mut self.tags { + tags.value.add_path(path.clone()); + } self.global_dependencies.add_path(path.clone()); self.global_env.add_path(path.clone()); self.global_pass_through_env.add_path(path.clone()); + self.boundaries.add_path(path.clone()); + if let Some(boundaries) = &mut self.boundaries { + boundaries.value.add_path(path.clone()); + } self.tasks.add_path(path.clone()); self.cache_dir.add_path(path.clone()); self.pipeline.add_path(path); @@ -136,6 +154,80 @@ impl WithMetadata for Pipeline { } } +impl WithMetadata for RootBoundariesConfig { + fn add_text(&mut self, text: Arc) { + self.tags.add_text(text.clone()); + if let Some(tags) = &mut self.tags { + for rule in tags.as_inner_mut().values_mut() { + rule.add_text(text.clone()); + rule.value.add_text(text.clone()); + } + } + } + + fn add_path(&mut self, path: Arc) { + self.tags.add_path(path.clone()); + if let Some(tags) = &mut self.tags { + for rule in tags.as_inner_mut().values_mut() { + rule.add_path(path.clone()); + rule.value.add_path(path.clone()); + } + } + } +} + +impl WithMetadata for Rule { + fn add_text(&mut self, text: Arc) { + self.dependencies.add_text(text.clone()); + if let Some(dependencies) = &mut self.dependencies { + dependencies.value.add_text(text.clone()); + } + + self.dependents.add_text(text.clone()); + if let Some(dependents) = &mut self.dependents { + dependents.value.add_text(text.clone()); + } + } + + fn add_path(&mut self, path: Arc) { + self.dependencies.add_path(path.clone()); + if let Some(dependencies) = &mut self.dependencies { + dependencies.value.add_path(path.clone()); + } + + self.dependents.add_path(path.clone()); + if let Some(dependents) = &mut self.dependents { + dependents.value.add_path(path); + } + } +} + +impl WithMetadata for Permissions { + fn add_text(&mut self, text: Arc) { + self.allow.add_text(text.clone()); + if let Some(allow) = &mut self.allow { + allow.value.add_text(text.clone()); + } + + self.deny.add_text(text.clone()); + if let Some(deny) = &mut self.deny { + deny.value.add_text(text.clone()); + } + } + + fn add_path(&mut self, path: Arc) { + self.allow.add_path(path.clone()); + if let Some(allow) = &mut self.allow { + allow.value.add_path(path.clone()); + } + + self.deny.add_path(path.clone()); + if let Some(deny) = &mut self.deny { + deny.value.add_path(path.clone()); + } + } +} + impl WithMetadata for RawTaskDefinition { fn add_text(&mut self, text: Arc) { self.depends_on.add_text(text.clone()); diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__dependents.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__dependents.snap new file mode 100644 index 0000000000000..5b951aa8f4d10 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__dependents.snap @@ -0,0 +1,16 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": null, + "dependencies": null, + "dependents": { + "allow": [ + "my-package" + ], + "deny": [ + "my-other-package" + ] + } +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__deserialize_boundaries-2.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__deserialize_boundaries-2.snap new file mode 100644 index 0000000000000..5b648fe6e4aaa --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__deserialize_boundaries-2.snap @@ -0,0 +1,11 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": [ + "my-tag" + ], + "dependencies": null, + "dependents": null +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__deserialize_boundaries.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__deserialize_boundaries.snap new file mode 100644 index 0000000000000..36fa44c506a17 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__deserialize_boundaries.snap @@ -0,0 +1,9 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": null, + "dependencies": null, + "dependents": null +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty.snap new file mode 100644 index 0000000000000..e4b2050155490 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty.snap @@ -0,0 +1,7 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": null +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_boundaries.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_boundaries.snap new file mode 100644 index 0000000000000..e4b2050155490 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_boundaries.snap @@ -0,0 +1,7 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": null +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_tags.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_tags.snap new file mode 100644 index 0000000000000..a496ebdb0a319 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_tags.snap @@ -0,0 +1,7 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": {} +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_tags_in_package.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_tags_in_package.snap new file mode 100644 index 0000000000000..1497dfca7fe29 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__empty_tags_in_package.snap @@ -0,0 +1,5 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: json.tags +--- +[] diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__just tags.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__just tags.snap new file mode 100644 index 0000000000000..5b648fe6e4aaa --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__just tags.snap @@ -0,0 +1,11 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": [ + "my-tag" + ], + "dependencies": null, + "dependents": null +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__just_tags.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__just_tags.snap new file mode 100644 index 0000000000000..5b648fe6e4aaa --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__just_tags.snap @@ -0,0 +1,11 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": [ + "my-tag" + ], + "dependencies": null, + "dependents": null +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__one_tag.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__one_tag.snap new file mode 100644 index 0000000000000..a1a184628aafd --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__one_tag.snap @@ -0,0 +1,7 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: json.tags +--- +[ + "my-tag" +] diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap new file mode 100644 index 0000000000000..671335b532098 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies.snap @@ -0,0 +1,17 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": { + "my-tag": { + "dependencies": { + "allow": [ + "my-package" + ], + "deny": null + }, + "dependents": null + } + } +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies_2.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies_2.snap new file mode 100644 index 0000000000000..28c4ef872b5cc --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependencies_2.snap @@ -0,0 +1,19 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": { + "my-tag": { + "dependencies": { + "allow": [ + "my-package" + ], + "deny": [ + "my-other-package" + ] + }, + "dependents": null + } + } +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependents.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependents.snap new file mode 100644 index 0000000000000..4c0600b860a5c --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__tags_and_dependents.snap @@ -0,0 +1,19 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: raw_task_definition +--- +{ + "tags": { + "my-tag": { + "dependencies": null, + "dependents": { + "allow": [ + "my-package" + ], + "deny": [ + "my-other-package" + ] + } + } + } +} diff --git a/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__two_tags.snap b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__two_tags.snap new file mode 100644 index 0000000000000..f105c209a7739 --- /dev/null +++ b/crates/turborepo-lib/src/turbo_json/snapshots/turborepo_lib__turbo_json__tests__two_tags.snap @@ -0,0 +1,8 @@ +--- +source: crates/turborepo-lib/src/turbo_json/mod.rs +expression: json.tags +--- +[ + "my-tag", + "my-other-tag" +] diff --git a/crates/turborepo/tests/boundaries.rs b/crates/turborepo/tests/boundaries.rs index 69a1cedf96ef8..465b22cf5e690 100644 --- a/crates/turborepo/tests/boundaries.rs +++ b/crates/turborepo/tests/boundaries.rs @@ -11,3 +11,27 @@ fn test_boundaries() -> Result<(), anyhow::Error> { Ok(()) } + +#[test] +fn test_boundaries_tags() -> Result<(), anyhow::Error> { + check_json!( + "boundaries_tags", + "npm@10.5.0", + "query", + "get boundaries lints" => "query { boundaries { items { message import } } }", + ); + + Ok(()) +} + +#[test] +fn test_boundaries_on_basic_monorepo() -> Result<(), anyhow::Error> { + check_json!( + "basic_monorepo", + "npm@10.5.0", + "query", + "get boundaries lints" => "query { boundaries { items { message import } } }", + ); + + Ok(()) +} diff --git a/crates/turborepo/tests/snapshots/boundaries__basic_monorepo_get_boundaries_lints_(npm@10.5.0).snap b/crates/turborepo/tests/snapshots/boundaries__basic_monorepo_get_boundaries_lints_(npm@10.5.0).snap new file mode 100644 index 0000000000000..4f81559be0ee9 --- /dev/null +++ b/crates/turborepo/tests/snapshots/boundaries__basic_monorepo_get_boundaries_lints_(npm@10.5.0).snap @@ -0,0 +1,11 @@ +--- +source: crates/turborepo/tests/boundaries.rs +expression: query_output +--- +{ + "data": { + "boundaries": { + "items": [] + } + } +} diff --git a/crates/turborepo/tests/snapshots/boundaries__boundaries_get_boundaries_lints_(npm@10.5.0).snap b/crates/turborepo/tests/snapshots/boundaries__boundaries_get_boundaries_lints_(npm@10.5.0).snap index d1dc57842e99e..c316bd3f40724 100644 --- a/crates/turborepo/tests/snapshots/boundaries__boundaries_get_boundaries_lints_(npm@10.5.0).snap +++ b/crates/turborepo/tests/snapshots/boundaries__boundaries_get_boundaries_lints_(npm@10.5.0).snap @@ -6,10 +6,22 @@ expression: query_output "data": { "boundaries": { "items": [ + { + "message": "Package `@vercel/unsafe-package` found with tag listed in denylist for `my-app`: `unsafe`", + "import": "@vercel/unsafe-package" + }, + { + "message": "Package `module-package` found without any tag listed in allowlist for `my-app`", + "import": "module-package" + }, { "message": "cannot import file `../../packages/another/index.jsx` because it leaves the package", "import": "../../packages/another/index.jsx" }, + { + "message": "cannot import package `module-package` because it is not a dependency", + "import": "module-package" + }, { "message": "importing from a type declaration package, but import is not declared as a type-only import", "import": "ship" @@ -17,10 +29,6 @@ expression: query_output { "message": "importing from a type declaration package, but import is not declared as a type-only import", "import": "@types/ship" - }, - { - "message": "cannot import package `module-package` because it is not a dependency", - "import": "module-package" } ] } diff --git a/crates/turborepo/tests/snapshots/boundaries__boundaries_tags_get_boundaries_lints_(npm@10.5.0).snap b/crates/turborepo/tests/snapshots/boundaries__boundaries_tags_get_boundaries_lints_(npm@10.5.0).snap new file mode 100644 index 0000000000000..1819338c8d9dc --- /dev/null +++ b/crates/turborepo/tests/snapshots/boundaries__boundaries_tags_get_boundaries_lints_(npm@10.5.0).snap @@ -0,0 +1,28 @@ +--- +source: crates/turborepo/tests/boundaries.rs +expression: query_output +--- +{ + "data": { + "boundaries": { + "items": [ + { + "message": "Package `@vercel/allowed-and-denied-tag` found with tag listed in denylist for `@vercel/my-app`: `unsafe`", + "import": "@vercel/allowed-and-denied-tag" + }, + { + "message": "Package `@vercel/not-allowed-dependent` found without any tag listed in allowlist for `@vercel/allowed-and-denied-tag`", + "import": "@vercel/not-allowed-dependent" + }, + { + "message": "Package `@vercel/not-allowed-dependent` found without any tag listed in allowlist for `@vercel/allowed-tag`", + "import": "@vercel/not-allowed-dependent" + }, + { + "message": "Package `@vercel/not-allowed-dependent` found without any tag listed in allowlist for `@vercel/my-app`", + "import": "@vercel/not-allowed-dependent" + } + ] + } + } +} diff --git a/turborepo-tests/integration/fixtures/boundaries/apps/my-app/package.json b/turborepo-tests/integration/fixtures/boundaries/apps/my-app/package.json index 1597bcae5865f..d7d03805d3fa5 100644 --- a/turborepo-tests/integration/fixtures/boundaries/apps/my-app/package.json +++ b/turborepo-tests/integration/fixtures/boundaries/apps/my-app/package.json @@ -5,6 +5,7 @@ "maybefails": "exit 4" }, "dependencies": { - "@types/ship": "*" + "@types/ship": "*", + "@vercel/unsafe-package": "*" } } diff --git a/turborepo-tests/integration/fixtures/boundaries/apps/my-app/turbo.json b/turborepo-tests/integration/fixtures/boundaries/apps/my-app/turbo.json new file mode 100644 index 0000000000000..4748eae5438bf --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries/apps/my-app/turbo.json @@ -0,0 +1,5 @@ +{ + "tags": [ + "web" + ] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries/packages/ship-types/turbo.json b/turborepo-tests/integration/fixtures/boundaries/packages/ship-types/turbo.json new file mode 100644 index 0000000000000..04851af8d9678 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries/packages/ship-types/turbo.json @@ -0,0 +1,5 @@ +{ + "tags": [ + "types" + ] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries/packages/unsafe-package/package.json b/turborepo-tests/integration/fixtures/boundaries/packages/unsafe-package/package.json new file mode 100644 index 0000000000000..e6ec58f2e2b71 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries/packages/unsafe-package/package.json @@ -0,0 +1,3 @@ +{ + "name": "@vercel/unsafe-package" +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries/packages/unsafe-package/turbo.json b/turborepo-tests/integration/fixtures/boundaries/packages/unsafe-package/turbo.json new file mode 100644 index 0000000000000..b1eb6db55ce48 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries/packages/unsafe-package/turbo.json @@ -0,0 +1,5 @@ +{ + "tags": [ + "unsafe" + ] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries/turbo.json b/turborepo-tests/integration/fixtures/boundaries/turbo.json index 9e26dfeeb6e64..a5d3e78857a45 100644 --- a/turborepo-tests/integration/fixtures/boundaries/turbo.json +++ b/turborepo-tests/integration/fixtures/boundaries/turbo.json @@ -1 +1,16 @@ -{} \ No newline at end of file +{ + "boundaries": { + "tags": { + "web": { + "dependencies": { + "allow": [ + "types" + ], + "deny": [ + "unsafe" + ] + } + } + } + } +} diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/.gitignore b/turborepo-tests/integration/fixtures/boundaries_tags/.gitignore new file mode 100644 index 0000000000000..77af9fc60321d --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.turbo +.npmrc diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/apps/my-app/package.json b/turborepo-tests/integration/fixtures/boundaries_tags/apps/my-app/package.json new file mode 100644 index 0000000000000..c0989cf724c93 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/apps/my-app/package.json @@ -0,0 +1,11 @@ +{ + "name": "@vercel/my-app", + "scripts": { + "build": "echo building", + "maybefails": "exit 4" + }, + "dependencies": { + "@vercel/allowed-tag": "*", + "@vercel/allowed-and-denied-tag": "*" + } +} diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/apps/my-app/turbo.json b/turborepo-tests/integration/fixtures/boundaries_tags/apps/my-app/turbo.json new file mode 100644 index 0000000000000..4748eae5438bf --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/apps/my-app/turbo.json @@ -0,0 +1,5 @@ +{ + "tags": [ + "web" + ] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/package.json b/turborepo-tests/integration/fixtures/boundaries_tags/package.json new file mode 100644 index 0000000000000..404d3338e68cf --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/package.json @@ -0,0 +1,14 @@ +{ + "name": "monorepo", + "scripts": { + "something": "turbo run build" + }, + "dependencies": { + "module-package": "*" + }, + "packageManager": "npm@10.5.0", + "workspaces": [ + "apps/*", + "packages/*" + ] +} diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-and-denied-tag/package.json b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-and-denied-tag/package.json new file mode 100644 index 0000000000000..57c9c9dca9557 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-and-denied-tag/package.json @@ -0,0 +1,9 @@ +{ + "name": "@vercel/allowed-and-denied-tag", + "scripts": { + "dev": "echo building" + }, + "dependencies": { + "utils": "*" + } +} diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-and-denied-tag/turbo.json b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-and-denied-tag/turbo.json new file mode 100644 index 0000000000000..bb0d0284ae360 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-and-denied-tag/turbo.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "types", + "unsafe" + ] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-tag/package.json b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-tag/package.json new file mode 100644 index 0000000000000..dc8c1450a92d4 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-tag/package.json @@ -0,0 +1,7 @@ +{ + "name": "@vercel/allowed-tag", + "module": "my-module.mjs", + "dependencies": { + "@vercel/no-allowlist": "*" + } +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-tag/turbo.json b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-tag/turbo.json new file mode 100644 index 0000000000000..04851af8d9678 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/packages/allowed-tag/turbo.json @@ -0,0 +1,5 @@ +{ + "tags": [ + "types" + ] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/packages/not-allowed-dependent/package.json b/turborepo-tests/integration/fixtures/boundaries_tags/packages/not-allowed-dependent/package.json new file mode 100644 index 0000000000000..30e85bfb5cd79 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/packages/not-allowed-dependent/package.json @@ -0,0 +1,6 @@ +{ + "name": "@vercel/not-allowed-dependent", + "dependencies": { + "@vercel/my-app": "*" + } +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/packages/not-allowed-dependent/turbo.json b/turborepo-tests/integration/fixtures/boundaries_tags/packages/not-allowed-dependent/turbo.json new file mode 100644 index 0000000000000..eb25b1905080a --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/packages/not-allowed-dependent/turbo.json @@ -0,0 +1,3 @@ +{ + "tags": [] +} \ No newline at end of file diff --git a/turborepo-tests/integration/fixtures/boundaries_tags/turbo.json b/turborepo-tests/integration/fixtures/boundaries_tags/turbo.json new file mode 100644 index 0000000000000..becf2c42b2707 --- /dev/null +++ b/turborepo-tests/integration/fixtures/boundaries_tags/turbo.json @@ -0,0 +1,26 @@ +{ + "boundaries": { + "tags": { + "web": { + "dependencies": { + "allow": [ + "types" + ], + "deny": [ + "unsafe" + ] + }, + "dependents": { + "allow": [] + } + }, + "types": { + "dependents": { + "allow": [ + "web" + ] + } + } + } + } +} \ No newline at end of file diff --git a/turborepo-tests/integration/tests/bad-turbo-json.t b/turborepo-tests/integration/tests/bad-turbo-json.t index f18f13be76708..221455abacedb 100644 --- a/turborepo-tests/integration/tests/bad-turbo-json.t +++ b/turborepo-tests/integration/tests/bad-turbo-json.t @@ -9,19 +9,20 @@ Run build with package task in non-root turbo.json [1] $ sed 's/\[\([^]]*\)\]/\(\1)/g' < error.txt x Invalid turbo.json configuration - - Error: unnecessary_package_task_syntax (https://turbo.build/messages/unnecessary-package-task-syntax) - - x "my-app#build". Use "build" instead. - ,-\(apps(\/|\\)my-app(\/|\\)turbo.json:8:21\) (re) - 7 | // this comment verifies that turbo can read .json files with comments - 8 | ,-> "my-app#build": { - 9 | | "outputs": ("banana.txt", "apple.json"), - 10 | | "inputs": ("$TURBO_DEFAULT$", ".env.local") - 11 | |-> } - : `---- unnecessary package syntax found here - 12 | } - `---- + `-> unnecessary_package_task_syntax (https://turbo.build/messages/ + unnecessary-package-task-syntax) + + x "my-app#build". Use "build" instead. + ,-\(apps(\/|\\)my-app(\/|\\)turbo.json:8:21\) (re) + 7 | // this comment verifies that turbo can read .json files + with comments + 8 | ,-> "my-app#build": { + 9 | | "outputs": ("banana.txt", "apple.json"), + 10 | | "inputs": ("$TURBO_DEFAULT$", ".env.local") + 11 | |-> } + : `---- unnecessary package syntax found here + 12 | } + `---- @@ -92,32 +93,26 @@ Run build with syntax errors in turbo.json turbo_json_parse_error x Failed to parse turbo.json. - - Error: - x Expected a property but instead found ','. - ,-[turbo.json:2:48] - 1 | { - 2 | "$schema": "https://turbo.build/schema.json",, - : ^ - 3 | "globalDependencies": ["foo.txt"], - `---- - - Error: - x expected `,` but instead found `42` - ,-[turbo.json:12:46] - 11 | "my-app#build": { - 12 | "outputs": ["banana.txt", "apple.json"]42, - : ^^ - 13 | "inputs": [".env.local" - `---- - - Error: - x expected `,` but instead found `}` - ,-[turbo.json:14:5] - 13 | "inputs": [".env.local" - 14 | }, - : ^ - 15 | - `---- + |-> x Expected a property but instead found ','. + | ,-[turbo.json:2:48] + | 1 | { + | 2 | "$schema": "https://turbo.build/schema.json",, + | : ^ + | 3 | "globalDependencies": ["foo.txt"], + | `---- + |-> x expected `,` but instead found `42` + | ,-[turbo.json:12:46] + | 11 | "my-app#build": { + | 12 | "outputs": ["banana.txt", "apple.json"]42, + | : ^^ + | 13 | "inputs": [".env.local" + | `---- + `-> x expected `,` but instead found `}` + ,-[turbo.json:14:5] + 13 | "inputs": [".env.local" + 14 | }, + : ^ + 15 | + `---- [1] diff --git a/turborepo-tests/integration/tests/dry-json/monorepo.t b/turborepo-tests/integration/tests/dry-json/monorepo.t index ac8abddb619bb..cd1646ea5ae95 100644 --- a/turborepo-tests/integration/tests/dry-json/monorepo.t +++ b/turborepo-tests/integration/tests/dry-json/monorepo.t @@ -180,8 +180,6 @@ Run again with NODE_ENV set and see the value in the summary. --filter=util work Tasks that don't exist throw an error $ ${TURBO} run doesnotexist --dry=json x Missing tasks in project - - Error: - x Could not find task `doesnotexist` in project + `-> x Could not find task `doesnotexist` in project [1] diff --git a/turborepo-tests/integration/tests/invalid-package-json.t b/turborepo-tests/integration/tests/invalid-package-json.t index 8338c00c4b3ee..de7fcef5bc1cd 100644 --- a/turborepo-tests/integration/tests/invalid-package-json.t +++ b/turborepo-tests/integration/tests/invalid-package-json.t @@ -69,13 +69,12 @@ Build should fail due to trailing comma (sed replaces square brackets with paren package_json_parse_error x Unable to parse package.json. - - Error: - x Expected a property but instead found '}'. - ,-\(.*package.json:1:21\) (re) - 1 | { "name": "foobar", } - : ^ - `---- + `-> x Expected a property but instead found '}'. + ,-\[.* (re) + .*package.json:1:21\] (re) + 1 | { "name": "foobar", } + : ^ + `---- diff --git a/turborepo-tests/integration/tests/persistent-dependencies/1-topological.t b/turborepo-tests/integration/tests/persistent-dependencies/1-topological.t index 7b960c5a48f2b..5bd7157508685 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/1-topological.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/1-topological.t @@ -14,15 +14,13 @@ // └── pkg-a#dev $ ${TURBO} run dev x Invalid task configuration - - Error: - x "pkg-a#dev" is a persistent task, "app-a#dev" cannot depend on it - ,-[turbo.json:5:21] - 4 | "dev": { - 5 | "dependsOn": ["^dev"], - : ^^^|^^ - : `-- persistent task - 6 | "persistent": true - `---- + `-> x "pkg-a#dev" is a persistent task, "app-a#dev" cannot depend on it + ,-[turbo.json:5:21] + 4 | "dev": { + 5 | "dependsOn": ["^dev"], + : ^^^|^^ + : `-- persistent task + 6 | "persistent": true + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/10-too-many.t b/turborepo-tests/integration/tests/persistent-dependencies/10-too-many.t index ba51006c87abf..329713344fd58 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/10-too-many.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/10-too-many.t @@ -3,19 +3,15 @@ $ ${TURBO} run build --concurrency=1 x Invalid task configuration - - Error: - x You have 2 persistent tasks but `turbo` is configured for concurrency of - | 1. Set --concurrency to at least 3 + `-> x You have 2 persistent tasks but `turbo` is configured for + | concurrency of 1. Set --concurrency to at least 3 [1] $ ${TURBO} run build --concurrency=2 x Invalid task configuration - - Error: - x You have 2 persistent tasks but `turbo` is configured for concurrency of - | 2. Set --concurrency to at least 3 + `-> x You have 2 persistent tasks but `turbo` is configured for + | concurrency of 2. Set --concurrency to at least 3 [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/2-same-workspace.t b/turborepo-tests/integration/tests/persistent-dependencies/2-same-workspace.t index 16e642c8a956a..ab4c462ec8d8a 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/2-same-workspace.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/2-same-workspace.t @@ -14,15 +14,13 @@ // $ ${TURBO} run build x Invalid task configuration - - Error: - x "app-a#dev" is a persistent task, "app-a#build" cannot depend on it - ,-[turbo.json:5:21] - 4 | "build": { - 5 | "dependsOn": ["dev"] - : ^^|^^ - : `-- persistent task - 6 | }, - `---- + `-> x "app-a#dev" is a persistent task, "app-a#build" cannot depend on it + ,-[turbo.json:5:21] + 4 | "build": { + 5 | "dependsOn": ["dev"] + : ^^|^^ + : `-- persistent task + 6 | }, + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/3-workspace-specific.t b/turborepo-tests/integration/tests/persistent-dependencies/3-workspace-specific.t index 94290e828a6a8..b98222754e0ac 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/3-workspace-specific.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/3-workspace-specific.t @@ -18,25 +18,21 @@ # The regex match is liberal, because the build task from either workspace can throw the error $ ${TURBO} run build x Invalid task configuration - - Error: - x "pkg-a#dev" is a persistent task, "app-a#build" cannot depend on it - ,-[turbo.json:5:21] - 4 | "build": { - 5 | "dependsOn": ["pkg-a#dev"] - : ^^^^^|^^^^^ - : `-- persistent task - 6 | }, - `---- - - Error: - x "pkg-a#dev" is a persistent task, "pkg-a#build" cannot depend on it - ,-[turbo.json:5:21] - 4 | "build": { - 5 | "dependsOn": ["pkg-a#dev"] - : ^^^^^|^^^^^ - : `-- persistent task - 6 | }, - `---- + |-> x "pkg-a#dev" is a persistent task, "app-a#build" cannot depend on it + | ,-[turbo.json:5:21] + | 4 | "build": { + | 5 | "dependsOn": ["pkg-a#dev"] + | : ^^^^^|^^^^^ + | : `-- persistent task + | 6 | }, + | `---- + `-> x "pkg-a#dev" is a persistent task, "pkg-a#build" cannot depend on it + ,-[turbo.json:5:21] + 4 | "build": { + 5 | "dependsOn": ["pkg-a#dev"] + : ^^^^^|^^^^^ + : `-- persistent task + 6 | }, + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/4-cross-workspace.t b/turborepo-tests/integration/tests/persistent-dependencies/4-cross-workspace.t index 8042a84ccb6de..578c2100acb05 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/4-cross-workspace.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/4-cross-workspace.t @@ -8,15 +8,13 @@ # └── pkg-a#dev $ ${TURBO} run dev x Invalid task configuration - - Error: - x "pkg-a#dev" is a persistent task, "app-a#dev" cannot depend on it - ,-[turbo.json:5:21] - 4 | "app-a#dev": { - 5 | "dependsOn": ["pkg-a#dev"], - : ^^^^^|^^^^^ - : `-- persistent task - 6 | "persistent": true - `---- + `-> x "pkg-a#dev" is a persistent task, "app-a#dev" cannot depend on it + ,-[turbo.json:5:21] + 4 | "app-a#dev": { + 5 | "dependsOn": ["pkg-a#dev"], + : ^^^^^|^^^^^ + : `-- persistent task + 6 | "persistent": true + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/5-root-workspace.t b/turborepo-tests/integration/tests/persistent-dependencies/5-root-workspace.t index 7ddef2ea8fa88..80987bae54211 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/5-root-workspace.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/5-root-workspace.t @@ -14,15 +14,13 @@ # $ ${TURBO} run build x Invalid task configuration - - Error: - x "//#dev" is a persistent task, "app-a#build" cannot depend on it - ,-[turbo.json:5:21] - 4 | "build": { - 5 | "dependsOn": ["//#dev"], - : ^^^^|^^^ - : `-- persistent task - 6 | "persistent": true - `---- + `-> x "//#dev" is a persistent task, "app-a#build" cannot depend on it + ,-[turbo.json:5:21] + 4 | "build": { + 5 | "dependsOn": ["//#dev"], + : ^^^^|^^^ + : `-- persistent task + 6 | "persistent": true + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/7-topological-nested.t b/turborepo-tests/integration/tests/persistent-dependencies/7-topological-nested.t index 26dee8791b931..9ae94a5f315a0 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/7-topological-nested.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/7-topological-nested.t @@ -21,15 +21,13 @@ # this case. $ ${TURBO} run dev x Invalid task configuration - - Error: - x "pkg-b#dev" is a persistent task, "pkg-a#dev" cannot depend on it - ,-[turbo.json:5:21] - 4 | "dev": { - 5 | "dependsOn": ["^dev"], - : ^^^|^^ - : `-- persistent task - 6 | "persistent": true - `---- + `-> x "pkg-b#dev" is a persistent task, "pkg-a#dev" cannot depend on it + ,-[turbo.json:5:21] + 4 | "dev": { + 5 | "dependsOn": ["^dev"], + : ^^^|^^ + : `-- persistent task + 6 | "persistent": true + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/8-topological-with-extra.t b/turborepo-tests/integration/tests/persistent-dependencies/8-topological-with-extra.t index abbe1fb2ea8b5..96a32d2817487 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/8-topological-with-extra.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/8-topological-with-extra.t @@ -20,15 +20,13 @@ // └── workspace-z#dev // this one is persistent $ ${TURBO} run build x Invalid task configuration - - Error: - x "pkg-z#dev" is a persistent task, "pkg-b#build" cannot depend on it - ,-[turbo.json:8:21] - 7 | "pkg-b#build": { - 8 | "dependsOn": ["pkg-z#dev"] - : ^^^^^|^^^^^ - : `-- persistent task - 9 | }, - `---- + `-> x "pkg-z#dev" is a persistent task, "pkg-b#build" cannot depend on it + ,-[turbo.json:8:21] + 7 | "pkg-b#build": { + 8 | "dependsOn": ["pkg-z#dev"] + : ^^^^^|^^^^^ + : `-- persistent task + 9 | }, + `---- [1] diff --git a/turborepo-tests/integration/tests/persistent-dependencies/9-cross-workspace-nested.t b/turborepo-tests/integration/tests/persistent-dependencies/9-cross-workspace-nested.t index b163ca8bfd59e..e90b9fa4b0a8c 100644 --- a/turborepo-tests/integration/tests/persistent-dependencies/9-cross-workspace-nested.t +++ b/turborepo-tests/integration/tests/persistent-dependencies/9-cross-workspace-nested.t @@ -13,15 +13,13 @@ // $ ${TURBO} run build x Invalid task configuration - - Error: - x "app-z#dev" is a persistent task, "app-c#build" cannot depend on it - ,-[turbo.json:13:21] - 12 | "app-c#build": { - 13 | "dependsOn": ["app-z#dev"] - : ^^^^^|^^^^^ - : `-- persistent task - 14 | }, - `---- + `-> x "app-z#dev" is a persistent task, "app-c#build" cannot depend on it + ,-[turbo.json:13:21] + 12 | "app-c#build": { + 13 | "dependsOn": ["app-z#dev"] + : ^^^^^|^^^^^ + : `-- persistent task + 14 | }, + `---- [1] diff --git a/turborepo-tests/integration/tests/query/validation.t b/turborepo-tests/integration/tests/query/validation.t index 09bb86518ede5..7a48a7720730e 100644 --- a/turborepo-tests/integration/tests/query/validation.t +++ b/turborepo-tests/integration/tests/query/validation.t @@ -4,10 +4,8 @@ Setup Validate that we get an error when we try to run multiple persistent tasks with concurrency 1 $ ${TURBO} run build --concurrency=1 x Invalid task configuration - - Error: - x You have 2 persistent tasks but `turbo` is configured for concurrency of - | 1. Set --concurrency to at least 3 + `-> x You have 2 persistent tasks but `turbo` is configured for + | concurrency of 1. Set --concurrency to at least 3 [1] diff --git a/turborepo-tests/integration/tests/run/missing-tasks.t b/turborepo-tests/integration/tests/run/missing-tasks.t index 370c2dca67e94..ae7c3c035a2ca 100644 --- a/turborepo-tests/integration/tests/run/missing-tasks.t +++ b/turborepo-tests/integration/tests/run/missing-tasks.t @@ -4,30 +4,22 @@ Setup # Running non-existent tasks errors $ ${TURBO} run doesnotexist x Missing tasks in project - - Error: - x Could not find task `doesnotexist` in project + `-> x Could not find task `doesnotexist` in project [1] # Multiple non-existent tasks also error $ ${TURBO} run doesnotexist alsono x Missing tasks in project - - Error: - x Could not find task `alsono` in project - - Error: - x Could not find task `doesnotexist` in project + |-> x Could not find task `alsono` in project + `-> x Could not find task `doesnotexist` in project [1] # One good and one bad task does not error $ ${TURBO} run build doesnotexist x Missing tasks in project - - Error: - x Could not find task `doesnotexist` in project + `-> x Could not find task `doesnotexist` in project [1] diff --git a/turborepo-tests/integration/tests/workspace-configs/persistent.t b/turborepo-tests/integration/tests/workspace-configs/persistent.t index c0a8b8f36dd15..7b717ab722d4a 100644 --- a/turborepo-tests/integration/tests/workspace-configs/persistent.t +++ b/turborepo-tests/integration/tests/workspace-configs/persistent.t @@ -11,17 +11,15 @@ This test covers: # persistent-task-1 is persistent:true in the root workspace, and does NOT get overriden in the workspace $ ${TURBO} run persistent-task-1-parent --filter=persistent x Invalid task configuration - - Error: - x "persistent#persistent-task-1" is a persistent task, - | "persistent#persistent-task-1-parent" cannot depend on it - ,-[turbo.json:89:9] - 88 | "dependsOn": [ - 89 | "persistent-task-1" - : ^^^^^^^^^|^^^^^^^^^ - : `-- persistent task - 90 | ] - `---- + `-> x "persistent#persistent-task-1" is a persistent task, + | "persistent#persistent-task-1-parent" cannot depend on it + ,-[turbo.json:89:9] + 88 | "dependsOn": [ + 89 | "persistent-task-1" + : ^^^^^^^^^|^^^^^^^^^ + : `-- persistent task + 90 | ] + `---- [1] @@ -53,17 +51,15 @@ This test covers: # persistent-task-3 is defined in workspace, but does NOT have the persistent flag $ ${TURBO} run persistent-task-3-parent --filter=persistent x Invalid task configuration - - Error: - x "persistent#persistent-task-3" is a persistent task, - | "persistent#persistent-task-3-parent" cannot depend on it - ,-[turbo.json:99:9] - 98 | "dependsOn": [ - 99 | "persistent-task-3" - : ^^^^^^^^^|^^^^^^^^^ - : `-- persistent task - 100 | ] - `---- + `-> x "persistent#persistent-task-3" is a persistent task, + | "persistent#persistent-task-3-parent" cannot depend on it + ,-[turbo.json:99:9] + 98 | "dependsOn": [ + 99 | "persistent-task-3" + : ^^^^^^^^^|^^^^^^^^^ + : `-- persistent task + 100 | ] + `---- [1] @@ -71,16 +67,14 @@ This test covers: # persistent-task-4 has no config in the root workspace, and is set to true in the workspace $ ${TURBO} run persistent-task-4-parent --filter=persistent x Invalid task configuration - - Error: - x "persistent#persistent-task-4" is a persistent task, - | "persistent#persistent-task-4-parent" cannot depend on it - ,-[turbo.json:104:9] - 103 | "dependsOn": [ - 104 | "persistent-task-4" - : ^^^^^^^^^|^^^^^^^^^ - : `-- persistent task - 105 | ] - `---- + `-> x "persistent#persistent-task-4" is a persistent task, + | "persistent#persistent-task-4-parent" cannot depend on it + ,-[turbo.json:104:9] + 103 | "dependsOn": [ + 104 | "persistent-task-4" + : ^^^^^^^^^|^^^^^^^^^ + : `-- persistent task + 105 | ] + `---- [1]