Skip to content

Commit ee5dc12

Browse files
authored
feat(turbo): framework conditionals (#9853)
### Description Support framework conditional env vars ### Testing Instructions <!-- Give a quick description of steps to test your changes. -->
1 parent 4115d5c commit ee5dc12

File tree

4 files changed

+166
-9
lines changed

4 files changed

+166
-9
lines changed

crates/turborepo-lib/src/framework.rs

+151-6
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
1-
use std::sync::OnceLock;
1+
use std::{collections::HashMap, sync::OnceLock};
22

33
use serde::Deserialize;
44
use turborepo_repository::package_graph::PackageInfo;
55

6-
#[derive(Debug, PartialEq, Deserialize)]
6+
#[derive(Debug, PartialEq, Deserialize, Clone)]
77
#[serde(rename_all = "camelCase")]
88
enum Strategy {
99
All,
1010
Some,
1111
}
1212

13-
#[derive(Debug, PartialEq, Deserialize)]
13+
#[derive(Debug, PartialEq, Deserialize, Clone)]
1414
#[serde(rename_all = "camelCase")]
1515
struct Matcher {
1616
strategy: Strategy,
1717
dependencies: Vec<String>,
1818
}
1919

20-
#[derive(Debug, PartialEq, Deserialize)]
20+
#[derive(Debug, PartialEq, Deserialize, Clone)]
21+
#[serde(rename_all = "camelCase")]
22+
struct EnvConditionKey {
23+
key: String,
24+
value: Option<String>,
25+
}
26+
27+
#[derive(Debug, PartialEq, Deserialize, Clone)]
28+
#[serde(rename_all = "camelCase")]
29+
struct EnvConditional {
30+
when: EnvConditionKey,
31+
include: Vec<String>,
32+
}
33+
34+
#[derive(Debug, PartialEq, Deserialize, Clone)]
2135
#[serde(rename_all = "camelCase")]
2236
pub struct Framework {
2337
slug: String,
2438
env_wildcards: Vec<String>,
39+
env_conditionals: Option<Vec<EnvConditional>>,
2540
dependency_match: Matcher,
2641
}
2742

@@ -30,8 +45,22 @@ impl Framework {
3045
self.slug.clone()
3146
}
3247

33-
pub fn env_wildcards(&self) -> &[String] {
34-
&self.env_wildcards
48+
pub fn env(&self, env_at_execution_start: &HashMap<String, String>) -> Vec<String> {
49+
let mut env_vars = self.env_wildcards.clone();
50+
51+
if let Some(env_conditionals) = &self.env_conditionals {
52+
for conditional in env_conditionals {
53+
let (key, expected_value) = (&conditional.when.key, &conditional.when.value);
54+
55+
if let Some(actual_value) = env_at_execution_start.get(key) {
56+
if expected_value.is_none() || expected_value.as_ref() == Some(actual_value) {
57+
env_vars.extend(conditional.include.iter().cloned());
58+
}
59+
}
60+
}
61+
}
62+
63+
env_vars
3564
}
3665
}
3766

@@ -80,6 +109,8 @@ pub fn infer_framework(workspace: &PackageInfo, is_monorepo: bool) -> Option<&Fr
80109

81110
#[cfg(test)]
82111
mod tests {
112+
use std::collections::HashMap;
113+
83114
use test_case::test_case;
84115
use turborepo_repository::{package_graph::PackageInfo, package_json::PackageJson};
85116

@@ -199,4 +230,118 @@ mod tests {
199230
let framework = infer_framework(&workspace_info, is_monorepo);
200231
assert_eq!(framework, expected);
201232
}
233+
234+
#[test]
235+
fn test_env_with_no_conditions() {
236+
let framework = get_framework_by_slug("nextjs");
237+
238+
let env_at_execution_start = HashMap::new();
239+
let env_vars = framework.env(&env_at_execution_start);
240+
241+
assert_eq!(
242+
env_vars,
243+
framework.env_wildcards.clone(),
244+
"Expected env_wildcards when no conditionals exist"
245+
);
246+
}
247+
248+
#[test]
249+
fn test_env_with_matching_condition() {
250+
let framework = get_framework_by_slug("nextjs");
251+
252+
let mut env_at_execution_start = HashMap::new();
253+
env_at_execution_start.insert(
254+
"VERCEL_SKEW_PROTECTION_ENABLED".to_string(),
255+
"1".to_string(),
256+
);
257+
258+
let env_vars = framework.env(&env_at_execution_start);
259+
260+
let mut expected_vars = framework.env_wildcards.clone();
261+
expected_vars.push("VERCEL_DEPLOYMENT_ID".to_string());
262+
263+
assert_eq!(
264+
env_vars, expected_vars,
265+
"Expected VERCEL_DEPLOYMENT_ID to be included when condition is met"
266+
);
267+
}
268+
269+
#[test]
270+
fn test_env_with_non_matching_condition() {
271+
let framework = get_framework_by_slug("nextjs");
272+
273+
let mut env_at_execution_start = HashMap::new();
274+
env_at_execution_start.insert(
275+
"VERCEL_SKEW_PROTECTION_ENABLED".to_string(),
276+
"0".to_string(),
277+
);
278+
279+
let env_vars = framework.env(&env_at_execution_start);
280+
281+
assert_eq!(
282+
env_vars,
283+
framework.env_wildcards.clone(),
284+
"Expected only env_wildcards when condition is not met"
285+
);
286+
}
287+
288+
#[test]
289+
fn test_env_with_condition_without_value_requirement() {
290+
let mut framework = get_framework_by_slug("nextjs").clone();
291+
292+
if let Some(env_conditionals) = framework.env_conditionals.as_mut() {
293+
env_conditionals[0].when.value = None;
294+
}
295+
296+
let mut env_at_execution_start = HashMap::new();
297+
env_at_execution_start.insert(
298+
"VERCEL_SKEW_PROTECTION_ENABLED".to_string(),
299+
"random".to_string(),
300+
);
301+
302+
let env_vars = framework.env(&env_at_execution_start);
303+
304+
let mut expected_vars = framework.env_wildcards.clone();
305+
expected_vars.push("VERCEL_DEPLOYMENT_ID".to_string());
306+
307+
assert_eq!(
308+
env_vars, expected_vars,
309+
"Expected VERCEL_DEPLOYMENT_ID to be included when condition key exists, regardless \
310+
of value"
311+
);
312+
}
313+
314+
#[test]
315+
fn test_env_with_multiple_conditions() {
316+
let mut framework = get_framework_by_slug("nextjs").clone();
317+
318+
if let Some(env_conditionals) = framework.env_conditionals.as_mut() {
319+
env_conditionals.push(crate::framework::EnvConditional {
320+
when: crate::framework::EnvConditionKey {
321+
key: "ANOTHER_CONDITION".to_string(),
322+
value: Some("true".to_string()),
323+
},
324+
include: vec!["ADDITIONAL_ENV_VAR".to_string()],
325+
});
326+
}
327+
328+
let mut env_at_execution_start = HashMap::new();
329+
env_at_execution_start.insert(
330+
"VERCEL_SKEW_PROTECTION_ENABLED".to_string(),
331+
"1".to_string(),
332+
);
333+
env_at_execution_start.insert("ANOTHER_CONDITION".to_string(), "true".to_string());
334+
335+
let env_vars = framework.env(&env_at_execution_start);
336+
337+
let mut expected_vars = framework.env_wildcards.clone();
338+
expected_vars.push("VERCEL_DEPLOYMENT_ID".to_string());
339+
expected_vars.push("ADDITIONAL_ENV_VAR".to_string());
340+
341+
assert_eq!(
342+
env_vars, expected_vars,
343+
"Expected both VERCEL_DEPLOYMENT_ID and ADDITIONAL_ENV_VAR when both conditions are \
344+
met"
345+
);
346+
}
202347
}

crates/turborepo-lib/src/task_hash.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ impl<'a> TaskHasher<'a> {
286286
.hashes
287287
.get(task_id)
288288
.ok_or_else(|| Error::MissingPackageFileHash(task_id.to_string()))?;
289-
// See if we infer a framework
289+
// See if we can infer a framework
290290
let framework = do_framework_inference
291291
.then(|| infer_framework(workspace, is_monorepo))
292292
.flatten()
@@ -295,14 +295,14 @@ impl<'a> TaskHasher<'a> {
295295
debug!(
296296
"framework: {}, env_prefix: {:?}",
297297
framework.slug(),
298-
framework.env_wildcards()
298+
framework.env(self.env_at_execution_start)
299299
);
300300
telemetry.track_framework(framework.slug());
301301
});
302302
let framework_slug = framework.map(|f| f.slug().to_string());
303303

304304
let env_vars = if let Some(framework) = framework {
305-
let mut computed_wildcards = framework.env_wildcards().to_vec();
305+
let mut computed_wildcards = framework.env(self.env_at_execution_start);
306306

307307
if let Some(exclude_prefix) = self
308308
.env_at_execution_start

packages/turbo-types/src/json/frameworks.json

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
"slug": "nextjs",
4040
"name": "Next.js",
4141
"envWildcards": ["NEXT_PUBLIC_*"],
42+
"envConditionals": [
43+
{
44+
"when": { "key": "VERCEL_SKEW_PROTECTION_ENABLED", "value": "1" },
45+
"include": ["VERCEL_DEPLOYMENT_ID"]
46+
}
47+
],
4248
"dependencyMatch": {
4349
"strategy": "all",
4450
"dependencies": ["next"]

packages/turbo-types/src/types/frameworks.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
export type FrameworkStrategy = "all" | "some";
22

3+
export interface EnvConditional {
4+
when: { key: string; value?: string };
5+
include: Array<string>;
6+
}
7+
38
export interface Framework {
49
slug: string;
510
name: string;
611
envWildcards: Array<string>;
12+
envConditionals?: Array<EnvConditional>;
713
dependencyMatch: {
814
strategy: FrameworkStrategy;
915
dependencies: Array<string>;

0 commit comments

Comments
 (0)