这是indexloc提供的服务,不要输入任何密码
Skip to content

feat(boundaries): package rules #10160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/turborepo-lib/src/boundaries/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ pub struct BoundariesConfig {
pub tags: Option<Spanned<RulesMap>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub implicit_dependencies: Option<Spanned<Vec<Spanned<String>>>>,
/// If in a package `turbo.json`, the following two keys define
/// boundaries rules for that package
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Spanned<Permissions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependents: Option<Spanned<Permissions>>,
}

pub type RulesMap = HashMap<String, Spanned<Rule>>;

#[derive(Serialize, Default, Debug, Clone, Iterable, Deserializable, PartialEq)]
pub struct Rule {
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Spanned<Permissions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependents: Option<Spanned<Permissions>>,
}

Expand Down
34 changes: 14 additions & 20 deletions crates/turborepo-lib/src/boundaries/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ use turborepo_ui::{color, ColorConfig, BOLD_GREEN, BOLD_RED};
use crate::{
boundaries::{imports::DependencyLocations, tags::ProcessedRulesMap, tsconfig::TsConfigLoader},
run::Run,
turbo_json::TurboJson,
};

#[derive(Clone, Debug, Error, Diagnostic)]
Expand Down Expand Up @@ -67,6 +66,13 @@ pub enum SecondaryDiagnostic {

#[derive(Clone, Debug, Error, Diagnostic)]
pub enum BoundariesDiagnostic {
#[error("Package boundaries rules cannot have `tags` key")]
PackageBoundariesHasTags {
#[label("tags defined here")]
span: Option<SourceSpan>,
#[source_code]
text: NamedSource<String>,
},
#[error("Tag `{tag}` cannot share the same name as package `{package}`")]
TagSharesPackageName {
tag: String,
Expand Down Expand Up @@ -332,25 +338,13 @@ impl Run {
)
.await?;

if let Ok(TurboJson {
tags: Some(tags), ..
}) = self.turbo_json_loader().load(package_name)
{
if let Some(tag_rules) = tag_rules {
result.diagnostics.extend(self.check_package_tags(
PackageNode::Workspace(package_name.clone()),
&package_info.package_json,
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
);
}
if let Ok(turbo_json) = self.turbo_json_loader().load(package_name) {
result.diagnostics.extend(self.check_package_tags(
PackageNode::Workspace(package_name.clone()),
&package_info.package_json,
turbo_json.tags.as_ref(),
tag_rules.as_ref(),
)?);
}

result.packages_checked += 1;
Expand Down
173 changes: 116 additions & 57 deletions crates/turborepo-lib/src/boundaries/tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,73 +177,132 @@ impl Run {
Ok(None)
}

pub(crate) fn check_tag(
&self,
diagnostics: &mut Vec<BoundariesDiagnostic>,
dependencies: Option<&ProcessedPermissions>,
dependents: Option<&ProcessedPermissions>,
pkg: &PackageNode,
package_json: &PackageJson,
) -> Result<(), Error> {
if let Some(dependency_permissions) = dependencies {
for dependency in self.pkg_dep_graph().dependencies(pkg) {
if matches!(dependency, PackageNode::Root) {
continue;
}

let dependency_tags = self.load_package_tags(dependency.as_package_name());

diagnostics.extend(self.validate_relation(
pkg.as_package_name(),
package_json,
dependency.as_package_name(),
dependency_tags,
dependency_permissions.allow.as_ref(),
dependency_permissions.deny.as_ref(),
)?);
}
}

if let Some(dependent_permissions) = dependents {
for dependent in self.pkg_dep_graph().ancestors(pkg) {
if matches!(dependent, PackageNode::Root) {
continue;
}
let dependent_tags = self.load_package_tags(dependent.as_package_name());
diagnostics.extend(self.validate_relation(
pkg.as_package_name(),
package_json,
dependent.as_package_name(),
dependent_tags,
dependent_permissions.allow.as_ref(),
dependent_permissions.deny.as_ref(),
)?)
}
}

Ok(())
}

fn check_if_package_name_is_tag(
tags_rules: &ProcessedRulesMap,
pkg: &PackageNode,
package_json: &PackageJson,
) -> Option<BoundariesDiagnostic> {
let rule = tags_rules.get(pkg.as_package_name().as_str())?;
let (tag_span, tag_text) = rule.span.span_and_text("turbo.json");
let (package_span, package_text) = package_json
.name
.as_ref()
.map(|name| name.span_and_text("package.json"))
.unwrap_or_else(|| (None, NamedSource::new("package.json", "".into())));
Some(BoundariesDiagnostic::TagSharesPackageName {
tag: pkg.as_package_name().to_string(),
package: pkg.as_package_name().to_string(),
tag_span,
tag_text,
secondary: [SecondaryDiagnostic::PackageDefinedHere {
package: pkg.as_package_name().to_string(),
package_span,
package_text,
}],
})
}

pub(crate) fn check_package_tags(
&self,
pkg: PackageNode,
package_json: &PackageJson,
current_package_tags: &Spanned<Vec<Spanned<String>>>,
tags_rules: &ProcessedRulesMap,
current_package_tags: Option<&Spanned<Vec<Spanned<String>>>>,
tags_rules: Option<&ProcessedRulesMap>,
) -> Result<Vec<BoundariesDiagnostic>, Error> {
let mut diagnostics = Vec::new();
let package_turbo_json = self.turbo_json_loader().load(pkg.as_package_name());
let package_boundaries = package_turbo_json
.ok()
.and_then(|turbo_json| turbo_json.boundaries.as_ref());

// We don't allow tags to share the same name as the package because
// we allow package names to be used as a tag
if let Some(rule) = tags_rules.get(pkg.as_package_name().as_str()) {
let (tag_span, tag_text) = rule.span.span_and_text("turbo.json");
let (package_span, package_text) = package_json
.name
.as_ref()
.map(|name| name.span_and_text("package.json"))
.unwrap_or_else(|| (None, NamedSource::new("package.json", "".into())));
diagnostics.push(BoundariesDiagnostic::TagSharesPackageName {
tag: pkg.as_package_name().to_string(),
package: pkg.as_package_name().to_string(),
tag_span,
tag_text,
secondary: [SecondaryDiagnostic::PackageDefinedHere {
package: pkg.as_package_name().to_string(),
package_span,
package_text,
}],
});
if let Some(boundaries) = package_boundaries {
if let Some(tags) = &boundaries.tags {
let (span, text) = tags.span_and_text("turbo.json");
diagnostics.push(BoundariesDiagnostic::PackageBoundariesHasTags { span, text });
}
let dependencies = boundaries
.dependencies
.clone()
.map(|deps| deps.into_inner().into());
let dependents = boundaries
.dependents
.clone()
.map(|deps| deps.into_inner().into());

self.check_tag(
&mut diagnostics,
dependencies.as_ref(),
dependents.as_ref(),
&pkg,
package_json,
)?;
}

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 = self.load_package_tags(dependency.as_package_name());

diagnostics.extend(self.validate_relation(
pkg.as_package_name(),
package_json,
dependency.as_package_name(),
dependency_tags,
dependency_permissions.allow.as_ref(),
dependency_permissions.deny.as_ref(),
)?);
}
}
if let Some(tags_rules) = tags_rules {
// We don't allow tags to share the same name as the package
// because we allow package names to be used as a tag
diagnostics.extend(Self::check_if_package_name_is_tag(
tags_rules,
&pkg,
package_json,
));

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 = self.load_package_tags(dependent.as_package_name());
diagnostics.extend(self.validate_relation(
pkg.as_package_name(),
package_json,
dependent.as_package_name(),
dependent_tags,
dependent_permissions.allow.as_ref(),
dependent_permissions.deny.as_ref(),
)?)
}
for tag in current_package_tags.into_iter().flatten().flatten() {
if let Some(rule) = tags_rules.get(tag.as_inner()) {
self.check_tag(
&mut diagnostics,
rule.dependencies.as_ref(),
rule.dependents.as_ref(),
&pkg,
package_json,
)?;
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/turborepo-lib/src/query/boundaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ impl From<BoundariesDiagnostic> for Diagnostic {
import: None,
reason: Some(tag),
},
BoundariesDiagnostic::PackageBoundariesHasTags { span, text: _ } => Diagnostic {
message,
path: None,
start: span.map(|span| span.offset()),
end: span.map(|span| span.offset() + span.len()),
import: None,
reason: None,
},
}
}
}
8 changes: 8 additions & 0 deletions crates/turborepo-lib/src/turbo_json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,14 @@ mod tests {
}"#,
"implicit dependencies and tags"
)]
#[test_case(
r#"{
"dependencies": {
"allow": ["my-package"]
}
}"#,
"package rule"
)]
fn test_deserialize_boundaries(json: &str, name: &str) {
let deserialized_result = deserialize_from_json_str(
json,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ expression: raw_boundaries_config
{
"tags": {
"my-tag": {
"dependencies": null,
"dependents": {
"allow": [
"my-package"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: crates/turborepo-lib/src/turbo_json/mod.rs
expression: raw_boundaries_config
---
{
"dependencies": {
"allow": [
"my-package"
],
"deny": null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ expression: raw_boundaries_config
"my-package"
],
"deny": null
},
"dependents": null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ expression: raw_boundaries_config
"deny": [
"my-other-package"
]
},
"dependents": null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ expression: raw_boundaries_config
{
"tags": {
"my-tag": {
"dependencies": null,
"dependents": {
"allow": [
"my-package"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ expression: query_output
"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-dependency` found with tag listed in denylist for `@vercel/package-with-boundaries-rules`: `@vercel/not-allowed-dependency`",
"import": "@vercel/not-allowed-dependency"
},
{
"message": "Package `@vercel/not-allowed-dependent` found without any tag listed in allowlist for `@vercel/allowed-and-denied-tag`",
"import": "@vercel/not-allowed-dependent"
Expand Down
Loading
Loading