From 01842dc3e9fb16a7852ce0ae1a16f1447f2d173e Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Wed, 2 Jul 2025 18:35:40 -0600 Subject: [PATCH 1/4] refactor: add spans to some turbo.json fields --- crates/turborepo-lib/src/config/turbo_json.rs | 10 +- crates/turborepo-lib/src/turbo_json/loader.rs | 4 +- crates/turborepo-lib/src/turbo_json/mod.rs | 181 +++++++++++++++--- crates/turborepo-lib/src/turbo_json/parser.rs | 48 ++++- 4 files changed, 205 insertions(+), 38 deletions(-) diff --git a/crates/turborepo-lib/src/config/turbo_json.rs b/crates/turborepo-lib/src/config/turbo_json.rs index 9250195bf0dc1..7f4c87a0cbe3d 100644 --- a/crates/turborepo-lib/src/config/turbo_json.rs +++ b/crates/turborepo-lib/src/config/turbo_json.rs @@ -38,12 +38,14 @@ impl<'a> TurboJsonReader<'a> { // Don't allow token to be set for shared config. opts.token = None; - opts.ui = turbo_json.ui; - opts.allow_no_package_manager = turbo_json.allow_no_package_manager; + opts.ui = turbo_json.ui.map(|ui| *ui.as_inner()); + opts.allow_no_package_manager = turbo_json + .allow_no_package_manager + .map(|allow| *allow.as_inner()); opts.daemon = turbo_json.daemon.map(|daemon| *daemon.as_inner()); - opts.env_mode = turbo_json.env_mode; + opts.env_mode = turbo_json.env_mode.map(|mode| *mode.as_inner()); opts.cache_dir = cache_dir; - opts.concurrency = turbo_json.concurrency; + opts.concurrency = turbo_json.concurrency.map(|c| c.as_inner().clone()); Ok(opts) } } diff --git a/crates/turborepo-lib/src/turbo_json/loader.rs b/crates/turborepo-lib/src/turbo_json/loader.rs index 8741acd9bb40f..69c12074c3330 100644 --- a/crates/turborepo-lib/src/turbo_json/loader.rs +++ b/crates/turborepo-lib/src/turbo_json/loader.rs @@ -372,7 +372,7 @@ fn root_turbo_json_from_scripts(scripts: &[String]) -> Result task_name, Spanned::new(RawTaskDefinition { cache: Some(Spanned::new(false)), - env_mode: Some(EnvMode::Loose), + env_mode: Some(Spanned::new(EnvMode::Loose)), ..Default::default() }), ); @@ -391,7 +391,7 @@ fn workspace_turbo_json_from_scripts(scripts: &[String]) -> Result, + api_url: Option>, #[serde(skip_serializing_if = "Option::is_none")] - login_url: Option, + login_url: Option>, #[serde(skip_serializing_if = "Option::is_none")] - team_slug: Option, + team_slug: Option>, #[serde(skip_serializing_if = "Option::is_none")] - team_id: Option, + team_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] - signature: Option, + signature: Option>, #[serde(skip_serializing_if = "Option::is_none")] - preflight: Option, + preflight: Option>, #[serde(skip_serializing_if = "Option::is_none")] - timeout: Option, + timeout: Option>, #[serde(skip_serializing_if = "Option::is_none")] - enabled: Option, + enabled: Option>, #[serde(skip_serializing_if = "Option::is_none")] - upload_timeout: Option, + upload_timeout: Option>, } impl From<&RawRemoteCacheOptions> for ConfigurationOptions { fn from(remote_cache_opts: &RawRemoteCacheOptions) -> Self { Self { - api_url: remote_cache_opts.api_url.clone(), - login_url: remote_cache_opts.login_url.clone(), - team_slug: remote_cache_opts.team_slug.clone(), - team_id: remote_cache_opts.team_id.clone(), - signature: remote_cache_opts.signature, - preflight: remote_cache_opts.preflight, - timeout: remote_cache_opts.timeout, - upload_timeout: remote_cache_opts.upload_timeout, - enabled: remote_cache_opts.enabled, + api_url: remote_cache_opts + .api_url + .as_ref() + .map(|s| s.as_inner().clone()), + login_url: remote_cache_opts + .login_url + .as_ref() + .map(|s| s.as_inner().clone()), + team_slug: remote_cache_opts + .team_slug + .as_ref() + .map(|s| s.as_inner().clone()), + team_id: remote_cache_opts + .team_id + .as_ref() + .map(|s| s.as_inner().clone()), + signature: remote_cache_opts.signature.as_ref().map(|s| *s.as_inner()), + preflight: remote_cache_opts.preflight.as_ref().map(|s| *s.as_inner()), + timeout: remote_cache_opts.timeout.as_ref().map(|s| *s.as_inner()), + upload_timeout: remote_cache_opts + .upload_timeout + .as_ref() + .map(|s| *s.as_inner()), + enabled: remote_cache_opts.enabled.as_ref().map(|s| *s.as_inner()), ..Self::default() } } @@ -139,21 +154,21 @@ pub struct RawTurboJson { #[serde(skip_serializing_if = "Option::is_none")] pub(crate) remote_cache: Option, #[serde(skip_serializing_if = "Option::is_none", rename = "ui")] - pub ui: Option, + pub ui: Option>, #[serde( skip_serializing_if = "Option::is_none", rename = "dangerouslyDisablePackageManagerCheck" )] - pub allow_no_package_manager: Option, + pub allow_no_package_manager: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub daemon: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub env_mode: Option, + pub env_mode: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub cache_dir: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub no_update_notifier: Option, + pub no_update_notifier: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tags: Option>>>, @@ -162,7 +177,7 @@ pub struct RawTurboJson { pub boundaries: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub concurrency: Option, + pub concurrency: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub future_flags: Option>, @@ -270,7 +285,7 @@ pub struct RawTaskDefinition { // TODO: Remove this once we have the ability to load task definitions directly // instead of deriving them from a TurboJson #[serde(skip)] - env_mode: Option, + env_mode: Option>, // This can currently only be set internally and isn't a part of turbo.json #[serde(skip_serializing_if = "Option::is_none")] with: Option>>, @@ -307,7 +322,9 @@ impl RawTaskDefinition { set_field!(self, other, env); set_field!(self, other, pass_through_env); set_field!(self, other, interactive); - set_field!(self, other, env_mode); + if let Some(env_mode) = other.env_mode { + self.env_mode = env_mode.into(); + } set_field!(self, other, with); } } @@ -478,7 +495,7 @@ impl TaskDefinition { persistent, interruptible: *interruptible, interactive, - env_mode: raw_task.env_mode, + env_mode: raw_task.env_mode.map(|mode| *mode.as_inner()), with, }) } @@ -701,7 +718,7 @@ impl TurboJson { )))]) }), persistent: Some(Spanned::new(true)), - env_mode: Some(EnvMode::Loose), + env_mode: Some(Spanned::new(EnvMode::Loose)), ..Default::default() }), ); @@ -1243,7 +1260,7 @@ mod tests { #[test_case(r#"{}"#, None ; "missing")] fn test_ui(json: &str, expected: Option) { let json = RawTurboJson::parse(json, "").unwrap(); - assert_eq!(json.ui, expected); + assert_eq!(json.ui.as_ref().map(|ui| *ui.as_inner()), expected); } #[test_case(r#"{ "experimentalSpaces": { "id": "hello-world" } }"#, Some(SpacesJson { id: Some("hello-world".to_string().into()) }))] @@ -1275,7 +1292,12 @@ mod tests { #[test_case(r#"{}"#, None ; "missing")] fn test_allow_no_package_manager_serde(json_str: &str, expected: Option) { let json = RawTurboJson::parse(json_str, "").unwrap(); - assert_eq!(json.allow_no_package_manager, expected); + assert_eq!( + json.allow_no_package_manager + .as_ref() + .map(|allow| *allow.as_inner()), + expected + ); let serialized = serde_json::to_string(&json).unwrap(); assert_eq!(serialized, json_str); } @@ -1459,4 +1481,105 @@ mod tests { "`with` cannot use dependency relationships." ); } + + #[test] + fn test_spanned_fields_have_span_info() { + let json = r#"{ + "ui": "tui", + "dangerouslyDisablePackageManagerCheck": true, + "envMode": "strict", + "noUpdateNotifier": true, + "concurrency": "50%" + }"#; + + let parsed = RawTurboJson::parse(json, "turbo.json").unwrap(); + + // Verify that all the new spanned fields have span information + assert!(parsed.ui.is_some()); + if let Some(ui) = &parsed.ui { + assert!(ui.range.is_some(), "ui field should have span info"); + } + + assert!(parsed.allow_no_package_manager.is_some()); + if let Some(allow_no_package_manager) = &parsed.allow_no_package_manager { + assert!( + allow_no_package_manager.range.is_some(), + "allow_no_package_manager field should have span info" + ); + } + + assert!(parsed.env_mode.is_some()); + if let Some(env_mode) = &parsed.env_mode { + assert!( + env_mode.range.is_some(), + "env_mode field should have span info" + ); + } + + assert!(parsed.no_update_notifier.is_some()); + if let Some(no_update_notifier) = &parsed.no_update_notifier { + assert!( + no_update_notifier.range.is_some(), + "no_update_notifier field should have span info" + ); + } + + assert!(parsed.concurrency.is_some()); + if let Some(concurrency) = &parsed.concurrency { + assert!( + concurrency.range.is_some(), + "concurrency field should have span info" + ); + } + } + + #[test] + fn test_remote_cache_options_have_span_info() { + let json = r#"{ + "remoteCache": { + "apiUrl": "https://api.example.com", + "teamSlug": "my-team", + "signature": true, + "timeout": 30, + "enabled": true + } + }"#; + + let parsed = RawTurboJson::parse(json, "turbo.json").unwrap(); + + // Verify that remote cache options have span information + assert!(parsed.remote_cache.is_some()); + if let Some(remote_cache) = &parsed.remote_cache { + if let Some(api_url) = &remote_cache.api_url { + assert!( + api_url.range.is_some(), + "api_url field should have span info" + ); + } + if let Some(team_slug) = &remote_cache.team_slug { + assert!( + team_slug.range.is_some(), + "team_slug field should have span info" + ); + } + if let Some(signature) = &remote_cache.signature { + assert!( + signature.range.is_some(), + "signature field should have span info" + ); + } + if let Some(timeout) = &remote_cache.timeout { + assert!( + timeout.range.is_some(), + "timeout field should have span info" + ); + } + if let Some(enabled) = &remote_cache.enabled { + assert!( + enabled.range.is_some(), + "enabled field should have span info" + ); + } + } + } } diff --git a/crates/turborepo-lib/src/turbo_json/parser.rs b/crates/turborepo-lib/src/turbo_json/parser.rs index c5dac9d2046f0..402111eb17dab 100644 --- a/crates/turborepo-lib/src/turbo_json/parser.rs +++ b/crates/turborepo-lib/src/turbo_json/parser.rs @@ -18,7 +18,7 @@ use turborepo_unescape::UnescapedString; use crate::{ boundaries::{BoundariesConfig, Permissions, Rule}, run::task_id::TaskName, - turbo_json::{Pipeline, RawTaskDefinition, RawTurboJson, Spanned}, + turbo_json::{Pipeline, RawRemoteCacheOptions, RawTaskDefinition, RawTurboJson, Spanned}, }; #[derive(Debug, Error, Diagnostic)] @@ -116,7 +116,15 @@ impl WithMetadata for RawTurboJson { self.tasks.add_text(text.clone()); self.cache_dir.add_text(text.clone()); - self.pipeline.add_text(text); + self.pipeline.add_text(text.clone()); + self.remote_cache.add_text(text.clone()); + self.ui.add_text(text.clone()); + self.allow_no_package_manager.add_text(text.clone()); + self.daemon.add_text(text.clone()); + self.env_mode.add_text(text.clone()); + self.no_update_notifier.add_text(text.clone()); + self.concurrency.add_text(text.clone()); + self.future_flags.add_text(text); } fn add_path(&mut self, path: Arc) { @@ -135,7 +143,15 @@ impl WithMetadata for RawTurboJson { } self.tasks.add_path(path.clone()); self.cache_dir.add_path(path.clone()); - self.pipeline.add_path(path); + self.pipeline.add_path(path.clone()); + self.remote_cache.add_path(path.clone()); + self.ui.add_path(path.clone()); + self.allow_no_package_manager.add_path(path.clone()); + self.daemon.add_path(path.clone()); + self.env_mode.add_path(path.clone()); + self.no_update_notifier.add_path(path.clone()); + self.concurrency.add_path(path.clone()); + self.future_flags.add_path(path); } } @@ -275,6 +291,32 @@ impl WithMetadata for RawTaskDefinition { } } +impl WithMetadata for RawRemoteCacheOptions { + fn add_text(&mut self, text: Arc) { + self.api_url.add_text(text.clone()); + self.login_url.add_text(text.clone()); + self.team_slug.add_text(text.clone()); + self.team_id.add_text(text.clone()); + self.signature.add_text(text.clone()); + self.preflight.add_text(text.clone()); + self.timeout.add_text(text.clone()); + self.enabled.add_text(text.clone()); + self.upload_timeout.add_text(text); + } + + fn add_path(&mut self, path: Arc) { + self.api_url.add_path(path.clone()); + self.login_url.add_path(path.clone()); + self.team_slug.add_path(path.clone()); + self.team_id.add_path(path.clone()); + self.signature.add_path(path.clone()); + self.preflight.add_path(path.clone()); + self.timeout.add_path(path.clone()); + self.enabled.add_path(path.clone()); + self.upload_timeout.add_path(path); + } +} + impl RawTurboJson { // A simple helper for tests #[cfg(test)] From d6dcf1473e780e57788bf65250d2e1725083fcc2 Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Wed, 2 Jul 2025 18:37:08 -0600 Subject: [PATCH 2/4] WIP ab2ac --- crates/turborepo-lib/src/turbo_json/mod.rs | 101 --------------------- 1 file changed, 101 deletions(-) diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index 6a25bf037e1b3..cd335bcb275b6 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -1481,105 +1481,4 @@ mod tests { "`with` cannot use dependency relationships." ); } - - #[test] - fn test_spanned_fields_have_span_info() { - let json = r#"{ - "ui": "tui", - "dangerouslyDisablePackageManagerCheck": true, - "envMode": "strict", - "noUpdateNotifier": true, - "concurrency": "50%" - }"#; - - let parsed = RawTurboJson::parse(json, "turbo.json").unwrap(); - - // Verify that all the new spanned fields have span information - assert!(parsed.ui.is_some()); - if let Some(ui) = &parsed.ui { - assert!(ui.range.is_some(), "ui field should have span info"); - } - - assert!(parsed.allow_no_package_manager.is_some()); - if let Some(allow_no_package_manager) = &parsed.allow_no_package_manager { - assert!( - allow_no_package_manager.range.is_some(), - "allow_no_package_manager field should have span info" - ); - } - - assert!(parsed.env_mode.is_some()); - if let Some(env_mode) = &parsed.env_mode { - assert!( - env_mode.range.is_some(), - "env_mode field should have span info" - ); - } - - assert!(parsed.no_update_notifier.is_some()); - if let Some(no_update_notifier) = &parsed.no_update_notifier { - assert!( - no_update_notifier.range.is_some(), - "no_update_notifier field should have span info" - ); - } - - assert!(parsed.concurrency.is_some()); - if let Some(concurrency) = &parsed.concurrency { - assert!( - concurrency.range.is_some(), - "concurrency field should have span info" - ); - } - } - - #[test] - fn test_remote_cache_options_have_span_info() { - let json = r#"{ - "remoteCache": { - "apiUrl": "https://api.example.com", - "teamSlug": "my-team", - "signature": true, - "timeout": 30, - "enabled": true - } - }"#; - - let parsed = RawTurboJson::parse(json, "turbo.json").unwrap(); - - // Verify that remote cache options have span information - assert!(parsed.remote_cache.is_some()); - if let Some(remote_cache) = &parsed.remote_cache { - if let Some(api_url) = &remote_cache.api_url { - assert!( - api_url.range.is_some(), - "api_url field should have span info" - ); - } - if let Some(team_slug) = &remote_cache.team_slug { - assert!( - team_slug.range.is_some(), - "team_slug field should have span info" - ); - } - if let Some(signature) = &remote_cache.signature { - assert!( - signature.range.is_some(), - "signature field should have span info" - ); - } - if let Some(timeout) = &remote_cache.timeout { - assert!( - timeout.range.is_some(), - "timeout field should have span info" - ); - } - if let Some(enabled) = &remote_cache.enabled { - assert!( - enabled.range.is_some(), - "enabled field should have span info" - ); - } - } - } } From d6d848c390d196bdc9de4433745270883dae559a Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Thu, 3 Jul 2025 07:44:13 -0600 Subject: [PATCH 3/4] Update crates/turborepo-lib/src/turbo_json/mod.rs Co-authored-by: Chris Olszewski --- crates/turborepo-lib/src/turbo_json/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index cd335bcb275b6..6199910912a09 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -322,9 +322,7 @@ impl RawTaskDefinition { set_field!(self, other, env); set_field!(self, other, pass_through_env); set_field!(self, other, interactive); - if let Some(env_mode) = other.env_mode { - self.env_mode = env_mode.into(); - } + set_field!(self, other, env_mode) set_field!(self, other, with); } } From 85738b130cd66e125d96975af5a6711b7c238962 Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Thu, 3 Jul 2025 07:47:10 -0600 Subject: [PATCH 4/4] Update crates/turborepo-lib/src/turbo_json/mod.rs --- crates/turborepo-lib/src/turbo_json/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index 6199910912a09..3eb6795bf4c8c 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -322,7 +322,7 @@ impl RawTaskDefinition { set_field!(self, other, env); set_field!(self, other, pass_through_env); set_field!(self, other, interactive); - set_field!(self, other, env_mode) + set_field!(self, other, env_mode); set_field!(self, other, with); } }