From 80ae408c254d1383a9bd2c577e84d7aecf2dd09f Mon Sep 17 00:00:00 2001 From: tknickman Date: Thu, 30 Jan 2025 10:03:22 -0500 Subject: [PATCH 1/3] feat(turbo): framework conditionals --- crates/turborepo-lib/src/framework.rs | 106 +++++++++++++++++- crates/turborepo-lib/src/task_hash.rs | 3 + packages/turbo-types/src/json/frameworks.json | 6 + packages/turbo-types/src/types/frameworks.ts | 6 + 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/crates/turborepo-lib/src/framework.rs b/crates/turborepo-lib/src/framework.rs index 59d22c51586d8..f5d6a5bf3ed15 100644 --- a/crates/turborepo-lib/src/framework.rs +++ b/crates/turborepo-lib/src/framework.rs @@ -1,27 +1,42 @@ -use std::sync::OnceLock; +use std::{collections::HashMap, sync::OnceLock}; use serde::Deserialize; use turborepo_repository::package_graph::PackageInfo; -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Deserialize, Clone)] #[serde(rename_all = "camelCase")] enum Strategy { All, Some, } -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Deserialize, Clone)] #[serde(rename_all = "camelCase")] struct Matcher { strategy: Strategy, dependencies: Vec, } -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct EnvConditionKey { + key: String, + value: Option, +} + +#[derive(Debug, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +struct EnvConditional { + when: EnvConditionKey, + include: Vec, +} + +#[derive(Debug, PartialEq, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Framework { slug: String, env_wildcards: Vec, + env_conditionals: Option>, dependency_match: Matcher, } @@ -33,6 +48,27 @@ impl Framework { pub fn env_wildcards(&self) -> &[String] { &self.env_wildcards } + + pub fn resolved_env_conditionals( + &self, + env_at_execution_start: &HashMap, + ) -> Vec { + let mut conditional_env_vars = Vec::new(); + + if let Some(env_conditionals) = &self.env_conditionals { + for conditional in env_conditionals { + let (key, expected_value) = (&conditional.when.key, &conditional.when.value); + + if let Some(actual_value) = env_at_execution_start.get(key) { + if expected_value.is_none() || expected_value.as_ref() == Some(actual_value) { + conditional_env_vars.extend(conditional.include.iter().cloned()); + } + } + } + } + + conditional_env_vars + } } static FRAMEWORKS: OnceLock> = OnceLock::new(); @@ -80,6 +116,8 @@ pub fn infer_framework(workspace: &PackageInfo, is_monorepo: bool) -> Option<&Fr #[cfg(test)] mod tests { + use std::collections::HashMap; + use test_case::test_case; use turborepo_repository::{package_graph::PackageInfo, package_json::PackageJson}; @@ -199,4 +237,64 @@ mod tests { let framework = infer_framework(&workspace_info, is_monorepo); assert_eq!(framework, expected); } + + #[test] + fn test_resolved_env_conditionals_for_nextjs() { + let framework = get_framework_by_slug("nextjs"); + + // Case 1: Condition NOT met (no env var set) + let env_at_execution_start = HashMap::new(); + let resolved_vars = framework.resolved_env_conditionals(&env_at_execution_start); + assert!( + resolved_vars.is_empty(), + "Expected no conditional env vars when condition is not met" + ); + + // Case 2: Condition met with correct value + let mut env_at_execution_start = HashMap::new(); + env_at_execution_start.insert( + "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), + "1".to_string(), + ); + + let resolved_vars = framework.resolved_env_conditionals(&env_at_execution_start); + assert_eq!( + resolved_vars, + vec!["VERCEL_DEPLOYMENT_ID".to_string()], + "Expected VERCEL_DEPLOYMENT_ID to be included when condition is met" + ); + + // Case 3: Condition NOT met (wrong value) + let mut env_at_execution_start = HashMap::new(); + env_at_execution_start.insert( + "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), + "0".to_string(), + ); + + let resolved_vars = framework.resolved_env_conditionals(&env_at_execution_start); + assert!( + resolved_vars.is_empty(), + "Expected no conditional env vars when condition has wrong value" + ); + + // Case 4: Condition met (no value required) + let mut framework_no_value = framework.clone(); + if let Some(env_conditionals) = framework_no_value.env_conditionals.as_mut() { + env_conditionals[0].when.value = None; + } + + let mut env_at_execution_start = HashMap::new(); + env_at_execution_start.insert( + "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), + "any".to_string(), + ); + + let resolved_vars = framework_no_value.resolved_env_conditionals(&env_at_execution_start); + assert_eq!( + resolved_vars, + vec!["VERCEL_DEPLOYMENT_ID".to_string()], + "Expected VERCEL_DEPLOYMENT_ID to be included when condition key exists, regardless \ + of value" + ); + } } diff --git a/crates/turborepo-lib/src/task_hash.rs b/crates/turborepo-lib/src/task_hash.rs index 4c5ead250022a..e1807326f9a56 100644 --- a/crates/turborepo-lib/src/task_hash.rs +++ b/crates/turborepo-lib/src/task_hash.rs @@ -303,6 +303,9 @@ impl<'a> TaskHasher<'a> { let env_vars = if let Some(framework) = framework { let mut computed_wildcards = framework.env_wildcards().to_vec(); + let conditional_env_vars = + framework.resolved_env_conditionals(&self.env_at_execution_start); + computed_wildcards.extend(conditional_env_vars); if let Some(exclude_prefix) = self .env_at_execution_start diff --git a/packages/turbo-types/src/json/frameworks.json b/packages/turbo-types/src/json/frameworks.json index 7dcf893ccc817..2fc0fbfa82d25 100644 --- a/packages/turbo-types/src/json/frameworks.json +++ b/packages/turbo-types/src/json/frameworks.json @@ -39,6 +39,12 @@ "slug": "nextjs", "name": "Next.js", "envWildcards": ["NEXT_PUBLIC_*"], + "envConditionals": [ + { + "when": { "key": "VERCEL_SKEW_PROTECTION_ENABLED", "value": "1" }, + "include": ["VERCEL_DEPLOYMENT_ID"] + } + ], "dependencyMatch": { "strategy": "all", "dependencies": ["next"] diff --git a/packages/turbo-types/src/types/frameworks.ts b/packages/turbo-types/src/types/frameworks.ts index 1f91e4f958474..339cf1e1153b2 100644 --- a/packages/turbo-types/src/types/frameworks.ts +++ b/packages/turbo-types/src/types/frameworks.ts @@ -1,9 +1,15 @@ export type FrameworkStrategy = "all" | "some"; +export interface EnvConditional { + when: { key: string; value?: string }; + include: Array; +} + export interface Framework { slug: string; name: string; envWildcards: Array; + envConditionals?: Array; dependencyMatch: { strategy: FrameworkStrategy; dependencies: Array; From 39c71b79a10e495e478330b1820095a8037f8d65 Mon Sep 17 00:00:00 2001 From: tknickman Date: Fri, 7 Feb 2025 10:53:02 -0500 Subject: [PATCH 2/3] Combine method --- crates/turborepo-lib/src/framework.rs | 113 ++++++++++++++++++-------- crates/turborepo-lib/src/task_hash.rs | 9 +- 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/crates/turborepo-lib/src/framework.rs b/crates/turborepo-lib/src/framework.rs index f5d6a5bf3ed15..9b2f2da2cbe59 100644 --- a/crates/turborepo-lib/src/framework.rs +++ b/crates/turborepo-lib/src/framework.rs @@ -45,15 +45,8 @@ impl Framework { self.slug.clone() } - pub fn env_wildcards(&self) -> &[String] { - &self.env_wildcards - } - - pub fn resolved_env_conditionals( - &self, - env_at_execution_start: &HashMap, - ) -> Vec { - let mut conditional_env_vars = Vec::new(); + pub fn env(&self, env_at_execution_start: &HashMap) -> Vec { + let mut env_vars = self.env_wildcards.clone(); if let Some(env_conditionals) = &self.env_conditionals { for conditional in env_conditionals { @@ -61,13 +54,13 @@ impl Framework { if let Some(actual_value) = env_at_execution_start.get(key) { if expected_value.is_none() || expected_value.as_ref() == Some(actual_value) { - conditional_env_vars.extend(conditional.include.iter().cloned()); + env_vars.extend(conditional.include.iter().cloned()); } } } } - conditional_env_vars + env_vars } } @@ -239,62 +232,116 @@ mod tests { } #[test] - fn test_resolved_env_conditionals_for_nextjs() { + fn test_env_with_no_conditions() { let framework = get_framework_by_slug("nextjs"); - // Case 1: Condition NOT met (no env var set) let env_at_execution_start = HashMap::new(); - let resolved_vars = framework.resolved_env_conditionals(&env_at_execution_start); - assert!( - resolved_vars.is_empty(), - "Expected no conditional env vars when condition is not met" + let env_vars = framework.env(&env_at_execution_start); + + assert_eq!( + env_vars, + framework.env_wildcards.clone(), + "Expected env_wildcards when no conditionals exist" ); + } + + #[test] + fn test_env_with_matching_condition() { + let framework = get_framework_by_slug("nextjs"); - // Case 2: Condition met with correct value let mut env_at_execution_start = HashMap::new(); env_at_execution_start.insert( "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), "1".to_string(), ); - let resolved_vars = framework.resolved_env_conditionals(&env_at_execution_start); + let env_vars = framework.env(&env_at_execution_start); + + let mut expected_vars = framework.env_wildcards.clone(); + expected_vars.push("VERCEL_DEPLOYMENT_ID".to_string()); + assert_eq!( - resolved_vars, - vec!["VERCEL_DEPLOYMENT_ID".to_string()], + env_vars, expected_vars, "Expected VERCEL_DEPLOYMENT_ID to be included when condition is met" ); + } + + #[test] + fn test_env_with_non_matching_condition() { + let framework = get_framework_by_slug("nextjs"); - // Case 3: Condition NOT met (wrong value) let mut env_at_execution_start = HashMap::new(); env_at_execution_start.insert( "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), "0".to_string(), ); - let resolved_vars = framework.resolved_env_conditionals(&env_at_execution_start); - assert!( - resolved_vars.is_empty(), - "Expected no conditional env vars when condition has wrong value" + let env_vars = framework.env(&env_at_execution_start); + + assert_eq!( + env_vars, + framework.env_wildcards.clone(), + "Expected only env_wildcards when condition is not met" ); + } + + #[test] + fn test_env_with_condition_without_value_requirement() { + let mut framework = get_framework_by_slug("nextjs").clone(); - // Case 4: Condition met (no value required) - let mut framework_no_value = framework.clone(); - if let Some(env_conditionals) = framework_no_value.env_conditionals.as_mut() { + if let Some(env_conditionals) = framework.env_conditionals.as_mut() { env_conditionals[0].when.value = None; } let mut env_at_execution_start = HashMap::new(); env_at_execution_start.insert( "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), - "any".to_string(), + "random".to_string(), ); - let resolved_vars = framework_no_value.resolved_env_conditionals(&env_at_execution_start); + let env_vars = framework.env(&env_at_execution_start); + + let mut expected_vars = framework.env_wildcards.clone(); + expected_vars.push("VERCEL_DEPLOYMENT_ID".to_string()); + assert_eq!( - resolved_vars, - vec!["VERCEL_DEPLOYMENT_ID".to_string()], + env_vars, expected_vars, "Expected VERCEL_DEPLOYMENT_ID to be included when condition key exists, regardless \ of value" ); } + + #[test] + fn test_env_with_multiple_conditions() { + let mut framework = get_framework_by_slug("nextjs").clone(); + + if let Some(env_conditionals) = framework.env_conditionals.as_mut() { + env_conditionals.push(crate::framework::EnvConditional { + when: crate::framework::EnvConditionKey { + key: "ANOTHER_CONDITION".to_string(), + value: Some("true".to_string()), + }, + include: vec!["ADDITIONAL_ENV_VAR".to_string()], + }); + } + + let mut env_at_execution_start = HashMap::new(); + env_at_execution_start.insert( + "VERCEL_SKEW_PROTECTION_ENABLED".to_string(), + "1".to_string(), + ); + env_at_execution_start.insert("ANOTHER_CONDITION".to_string(), "true".to_string()); + + let env_vars = framework.env(&env_at_execution_start); + + let mut expected_vars = framework.env_wildcards.clone(); + expected_vars.push("VERCEL_DEPLOYMENT_ID".to_string()); + expected_vars.push("ADDITIONAL_ENV_VAR".to_string()); + + assert_eq!( + env_vars, expected_vars, + "Expected both VERCEL_DEPLOYMENT_ID and ADDITIONAL_ENV_VAR when both conditions are \ + met" + ); + } } diff --git a/crates/turborepo-lib/src/task_hash.rs b/crates/turborepo-lib/src/task_hash.rs index e1807326f9a56..42ce3797e7083 100644 --- a/crates/turborepo-lib/src/task_hash.rs +++ b/crates/turborepo-lib/src/task_hash.rs @@ -286,7 +286,7 @@ impl<'a> TaskHasher<'a> { .hashes .get(task_id) .ok_or_else(|| Error::MissingPackageFileHash(task_id.to_string()))?; - // See if we infer a framework + // See if we can infer a framework let framework = do_framework_inference .then(|| infer_framework(workspace, is_monorepo)) .flatten() @@ -295,17 +295,14 @@ impl<'a> TaskHasher<'a> { debug!( "framework: {}, env_prefix: {:?}", framework.slug(), - framework.env_wildcards() + framework.env(&self.env_at_execution_start) ); telemetry.track_framework(framework.slug()); }); let framework_slug = framework.map(|f| f.slug().to_string()); let env_vars = if let Some(framework) = framework { - let mut computed_wildcards = framework.env_wildcards().to_vec(); - let conditional_env_vars = - framework.resolved_env_conditionals(&self.env_at_execution_start); - computed_wildcards.extend(conditional_env_vars); + let mut computed_wildcards = framework.env(&self.env_at_execution_start); if let Some(exclude_prefix) = self .env_at_execution_start From 68c07ecae6ca59e9d5031a9a91c8384da0094505 Mon Sep 17 00:00:00 2001 From: tknickman Date: Fri, 7 Feb 2025 16:47:35 -0500 Subject: [PATCH 3/3] fix lint --- crates/turborepo-lib/src/task_hash.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/turborepo-lib/src/task_hash.rs b/crates/turborepo-lib/src/task_hash.rs index 42ce3797e7083..1d0d36ae28a7c 100644 --- a/crates/turborepo-lib/src/task_hash.rs +++ b/crates/turborepo-lib/src/task_hash.rs @@ -295,14 +295,14 @@ impl<'a> TaskHasher<'a> { debug!( "framework: {}, env_prefix: {:?}", framework.slug(), - framework.env(&self.env_at_execution_start) + framework.env(self.env_at_execution_start) ); telemetry.track_framework(framework.slug()); }); let framework_slug = framework.map(|f| f.slug().to_string()); let env_vars = if let Some(framework) = framework { - let mut computed_wildcards = framework.env(&self.env_at_execution_start); + let mut computed_wildcards = framework.env(self.env_at_execution_start); if let Some(exclude_prefix) = self .env_at_execution_start