diff --git a/crates/turborepo-lib/src/config/mod.rs b/crates/turborepo-lib/src/config/mod.rs index 8ed37957b3621..e1e451558972d 100644 --- a/crates/turborepo-lib/src/config/mod.rs +++ b/crates/turborepo-lib/src/config/mod.rs @@ -225,6 +225,16 @@ pub enum Error { #[source_code] text: NamedSource, }, + #[error( + "The \"futureFlags\" key can only be used in the root turbo.json. Please remove it from \ + Package Configurations." + )] + FutureFlagsInPackage { + #[label("futureFlags key found here")] + span: Option, + #[source_code] + text: NamedSource, + }, #[error( "TURBO_TUI_SCROLLBACK_LENGTH: Invalid value. Use a number for how many lines to keep in \ scrollback." diff --git a/crates/turborepo-lib/src/turbo_json/mod.rs b/crates/turborepo-lib/src/turbo_json/mod.rs index 005934e2cc61f..ba378a5ab1699 100644 --- a/crates/turborepo-lib/src/turbo_json/mod.rs +++ b/crates/turborepo-lib/src/turbo_json/mod.rs @@ -164,11 +164,18 @@ pub struct RawTurboJson { #[serde(skip_serializing_if = "Option::is_none")] pub concurrency: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub future_flags: Option>, + #[deserializable(rename = "//")] #[serde(skip)] _comment: Option, } +#[derive(Serialize, Default, Debug, Clone, Iterable, Deserializable, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct FutureFlags {} + #[derive(Serialize, Default, Debug, PartialEq, Clone)] #[serde(transparent)] pub struct Pipeline(BTreeMap, Spanned>); @@ -558,6 +565,15 @@ impl TryFrom for TurboJson { let (span, text) = pipeline.span_and_text("turbo.json"); return Err(Error::PipelineField { span, text }); } + + // `futureFlags` key is only allowed in root turbo.json + let is_workspace_config = raw_turbo.extends.is_some(); + if is_workspace_config { + if let Some(future_flags) = raw_turbo.future_flags { + let (span, text) = future_flags.span_and_text("turbo.json"); + return Err(Error::FutureFlagsInPackage { span, text }); + } + } let mut global_env = HashSet::new(); let mut global_file_dependencies = HashSet::new(); @@ -871,8 +887,8 @@ mod tests { use turborepo_unescape::UnescapedString; use super::{ - replace_turbo_root_token_in_string, validate_with_has_no_topo, Pipeline, RawTurboJson, - SpacesJson, Spanned, TurboJson, UIMode, + replace_turbo_root_token_in_string, validate_with_has_no_topo, FutureFlags, Pipeline, + RawTurboJson, SpacesJson, Spanned, TurboJson, UIMode, }; use crate::{ boundaries::BoundariesConfig, @@ -1359,6 +1375,65 @@ mod tests { assert_eq!(actual, expected.map_err(|s| s.to_owned())); } + #[test] + fn test_future_flags_not_allowed_in_workspace() { + let json = r#"{ + "extends": ["//"], + "tasks": { + "build": {} + }, + "futureFlags": { + "newFeature": true + } + }"#; + + let deserialized_result = deserialize_from_json_str( + json, + JsonParserOptions::default().with_allow_comments(), + "turbo.json", + ); + let raw_turbo_json: RawTurboJson = deserialized_result.into_deserialized().unwrap(); + + // Try to convert to TurboJson - this should fail + let turbo_json_result = TurboJson::try_from(raw_turbo_json); + assert!(turbo_json_result.is_err()); + + let error = turbo_json_result.unwrap_err(); + let error_str = error.to_string(); + assert!( + error_str.contains("The \"futureFlags\" key can only be used in the root turbo.json") + ); + } + + #[test] + fn test_deserialize_future_flags() { + let json = r#"{ + "tasks": { + "build": {} + }, + "futureFlags": { + "bestFeature": true + } + }"#; + + let deserialized_result = deserialize_from_json_str( + json, + JsonParserOptions::default().with_allow_comments(), + "turbo.json", + ); + let raw_turbo_json: RawTurboJson = deserialized_result.into_deserialized().unwrap(); + + // Verify that futureFlags is parsed correctly + assert!(raw_turbo_json.future_flags.is_some()); + let future_flags = raw_turbo_json.future_flags.as_ref().unwrap(); + assert_eq!(future_flags.as_inner(), &FutureFlags {}); + + // Verify that the futureFlags field doesn't cause errors during conversion to + // TurboJson + let turbo_json = TurboJson::try_from(raw_turbo_json); + assert!(turbo_json.is_ok()); + } + #[test] fn test_validate_with_has_no_topo() { let turbo_json = TurboJson { diff --git a/packages/turbo-types/schemas/schema.json b/packages/turbo-types/schemas/schema.json index 8f164b2a8b802..1543b141f5e4b 100644 --- a/packages/turbo-types/schemas/schema.json +++ b/packages/turbo-types/schemas/schema.json @@ -102,6 +102,12 @@ "type": "boolean", "description": "When set to `true`, disables the update notification that appears when a new version of `turbo` is available.\n\nDocumentation: https://turborepo.com/docs/reference/configuration#noupdatenotifier", "default": false + }, + "futureFlags": { + "type": "object", + "additionalProperties": {}, + "description": "Opt into breaking changes prior to major releases, experimental features, and beta features.", + "default": {} } }, "additionalProperties": false, diff --git a/packages/turbo-types/schemas/schema.v2.json b/packages/turbo-types/schemas/schema.v2.json index 8f164b2a8b802..1543b141f5e4b 100644 --- a/packages/turbo-types/schemas/schema.v2.json +++ b/packages/turbo-types/schemas/schema.v2.json @@ -102,6 +102,12 @@ "type": "boolean", "description": "When set to `true`, disables the update notification that appears when a new version of `turbo` is available.\n\nDocumentation: https://turborepo.com/docs/reference/configuration#noupdatenotifier", "default": false + }, + "futureFlags": { + "type": "object", + "additionalProperties": {}, + "description": "Opt into breaking changes prior to major releases, experimental features, and beta features.", + "default": {} } }, "additionalProperties": false, diff --git a/packages/turbo-types/src/types/config-v2.ts b/packages/turbo-types/src/types/config-v2.ts index 54a91a68f6fb6..38ee396f90f2d 100644 --- a/packages/turbo-types/src/types/config-v2.ts +++ b/packages/turbo-types/src/types/config-v2.ts @@ -190,6 +190,13 @@ export interface RootSchema extends BaseSchema { * @defaultValue `false` */ noUpdateNotifier?: boolean; + + /** + * Opt into breaking changes prior to major releases, experimental features, and beta features. + * + * @defaultValue `{}` + */ + futureFlags?: Record; } export interface Pipeline {