Skip to content

Commit 3202891

Browse files
anthonyshewchris-olszewski
authored andcommitted
feat(turbo_json): $TURBO_ROOT$
1 parent 3f02771 commit 3202891

File tree

4 files changed

+190
-14
lines changed

4 files changed

+190
-14
lines changed

crates/turborepo-lib/src/config/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ pub enum Error {
204204
},
205205
#[error("Cannot load turbo.json for {0} in single package mode.")]
206206
InvalidTurboJsonLoad(PackageName),
207+
#[error("Cannot use '$TURBO_ROOT$' anywhere besides start of string.")]
208+
InvalidTurboRootUse {
209+
#[label("must be at start")]
210+
span: Option<SourceSpan>,
211+
#[source_code]
212+
text: NamedSource<String>,
213+
},
207214
}
208215

209216
const DEFAULT_API_URL: &str = "https://vercel.com/api";

crates/turborepo-lib/src/engine/builder.rs

+63-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
33
use convert_case::{Case, Casing};
44
use itertools::Itertools;
55
use miette::{Diagnostic, NamedSource, SourceSpan};
6-
use turbopath::AbsoluteSystemPath;
6+
use turbopath::{AbsoluteSystemPath, AnchoredSystemPathBuf, RelativeUnixPathBuf};
77
use turborepo_errors::{Spanned, TURBO_SITE};
88
use turborepo_graph_utils as graph;
99
use turborepo_repository::package_graph::{PackageGraph, PackageName, PackageNode, ROOT_PKG_NAME};
@@ -568,8 +568,11 @@ impl<'a> EngineBuilder<'a> {
568568
task_id,
569569
task_name,
570570
)?);
571-
572-
Ok(TaskDefinition::try_from(raw_task_definition)?)
571+
let path_to_root = self.path_to_root(task_id.as_inner())?;
572+
Ok(TaskDefinition::from_raw(
573+
raw_task_definition,
574+
&path_to_root,
575+
)?)
573576
}
574577

575578
fn task_definition_chain(
@@ -638,6 +641,22 @@ impl<'a> EngineBuilder<'a> {
638641

639642
Ok(task_definitions)
640643
}
644+
645+
// Returns that path from a task's package directory to the repo root
646+
fn path_to_root(&self, task_id: &TaskId) -> Result<RelativeUnixPathBuf, Error> {
647+
let package_name = PackageName::from(task_id.package());
648+
let pkg_path = self
649+
.package_graph
650+
.package_dir(&package_name)
651+
.ok_or_else(|| Error::MissingPackageJson {
652+
workspace: package_name,
653+
})?;
654+
Ok(AnchoredSystemPathBuf::relative_path_between(
655+
&self.repo_root.resolve(pkg_path),
656+
self.repo_root,
657+
)
658+
.to_unix())
659+
}
641660
}
642661

643662
impl Error {
@@ -1595,4 +1614,45 @@ mod test {
15951614
"only the root task node should be present"
15961615
);
15971616
}
1617+
1618+
#[test]
1619+
fn test_path_to_root() {
1620+
let repo_root_dir = TempDir::with_prefix("repo").unwrap();
1621+
let repo_root = AbsoluteSystemPathBuf::new(repo_root_dir.path().to_str().unwrap()).unwrap();
1622+
let package_graph = mock_package_graph(
1623+
&repo_root,
1624+
package_jsons! {
1625+
repo_root,
1626+
"app1" => ["libA"],
1627+
"libA" => []
1628+
},
1629+
);
1630+
let turbo_jsons = vec![(
1631+
PackageName::Root,
1632+
turbo_json(json!({
1633+
"tasks": {
1634+
"build": { "dependsOn": ["^build"] },
1635+
}
1636+
})),
1637+
)]
1638+
.into_iter()
1639+
.collect();
1640+
let loader = TurboJsonLoader::noop(turbo_jsons);
1641+
let engine = EngineBuilder::new(&repo_root, &package_graph, &loader, false);
1642+
assert_eq!(
1643+
engine
1644+
.path_to_root(&TaskId::new("//", "build"))
1645+
.unwrap()
1646+
.as_str(),
1647+
"."
1648+
);
1649+
// libA is located at packages/libA
1650+
assert_eq!(
1651+
engine
1652+
.path_to_root(&TaskId::new("libA", "build"))
1653+
.unwrap()
1654+
.as_str(),
1655+
"../.."
1656+
);
1657+
}
15981658
}

crates/turborepo-lib/src/turbo_json/loader.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ mod test {
434434
use insta::assert_snapshot;
435435
use tempfile::tempdir;
436436
use test_case::test_case;
437+
use turbopath::RelativeUnixPath;
437438
use turborepo_unescape::UnescapedString;
438439

439440
use super::*;
@@ -616,7 +617,7 @@ mod test {
616617

617618
assert_eq!(
618619
expected_root_build,
619-
TaskDefinition::try_from(root_build.clone())?
620+
TaskDefinition::from_raw(root_build.clone(), RelativeUnixPath::new(".").unwrap())?
620621
);
621622

622623
Ok(())

crates/turborepo-lib/src/turbo_json/mod.rs

+118-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use clap::ValueEnum;
1111
use miette::{NamedSource, SourceSpan};
1212
use serde::{Deserialize, Serialize};
1313
use struct_iterable::Iterable;
14-
use turbopath::AbsoluteSystemPath;
14+
use turbopath::{AbsoluteSystemPath, RelativeUnixPath};
1515
use turborepo_errors::Spanned;
1616
use turborepo_repository::package_graph::ROOT_PKG_NAME;
1717
use turborepo_unescape::UnescapedString;
@@ -33,6 +33,8 @@ pub use loader::TurboJsonLoader;
3333

3434
use crate::{boundaries::BoundariesConfig, config::UnnecessaryPackageTaskSyntaxError};
3535

36+
const TURBO_ROOT: &str = "$TURBO_ROOT$";
37+
3638
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, Deserializable)]
3739
#[serde(rename_all = "camelCase")]
3840
pub struct SpacesJson {
@@ -343,10 +345,12 @@ impl TryFrom<Vec<Spanned<UnescapedString>>> for TaskOutputs {
343345
}
344346
}
345347

346-
impl TryFrom<RawTaskDefinition> for TaskDefinition {
347-
type Error = Error;
348-
349-
fn try_from(raw_task: RawTaskDefinition) -> Result<Self, Error> {
348+
impl TaskDefinition {
349+
pub fn from_raw(
350+
mut raw_task: RawTaskDefinition,
351+
path_to_repo_root: &RelativeUnixPath,
352+
) -> Result<Self, Error> {
353+
replace_turbo_root_token(&mut raw_task, path_to_repo_root)?;
350354
let outputs = raw_task.outputs.unwrap_or_default().try_into()?;
351355

352356
let cache = raw_task.cache.is_none_or(|c| c.into_inner());
@@ -572,6 +576,8 @@ impl TryFrom<RawTurboJson> for TurboJson {
572576
}
573577
}
574578

579+
let tasks = raw_turbo.tasks.clone().unwrap_or_default();
580+
575581
Ok(TurboJson {
576582
text: raw_turbo.span.text,
577583
path: raw_turbo.span.path,
@@ -598,7 +604,7 @@ impl TryFrom<RawTurboJson> for TurboJson {
598604

599605
global_deps
600606
},
601-
tasks: raw_turbo.tasks.unwrap_or_default(),
607+
tasks,
602608
// copy these over, we don't need any changes here.
603609
extends: raw_turbo
604610
.extends
@@ -629,7 +635,7 @@ impl TurboJson {
629635
path: &AbsoluteSystemPath,
630636
) -> Result<TurboJson, Error> {
631637
let raw_turbo_json = RawTurboJson::read(repo_root, path)?;
632-
raw_turbo_json.try_into()
638+
TurboJson::try_from(raw_turbo_json)
633639
}
634640

635641
pub fn task(&self, task_id: &TaskId, task_name: &TaskName) -> Option<RawTaskDefinition> {
@@ -765,17 +771,75 @@ fn gather_env_vars(
765771
Ok(())
766772
}
767773

774+
// Takes an input/output glob that might start with TURBO_ROOT_PREFIX
775+
// and swap it with the relative path to the turbo root.
776+
fn replace_turbo_root_token_in_string(
777+
input: &mut Spanned<UnescapedString>,
778+
path_to_repo_root: &RelativeUnixPath,
779+
) -> Result<(), Error> {
780+
match input.find(TURBO_ROOT) {
781+
Some(0) => {
782+
// Replace
783+
input
784+
.as_inner_mut()
785+
.replace_range(..TURBO_ROOT.len(), path_to_repo_root.as_str());
786+
Ok(())
787+
}
788+
// Handle negations
789+
Some(1) if input.starts_with('!') => {
790+
input
791+
.as_inner_mut()
792+
.replace_range(1..TURBO_ROOT.len() + 1, path_to_repo_root.as_str());
793+
Ok(())
794+
}
795+
// We do not allow for TURBO_ROOT to be used in the middle of a glob
796+
Some(_) => {
797+
let (span, text) = input.span_and_text("turbo.json");
798+
Err(Error::InvalidTurboRootUse { span, text })
799+
}
800+
None => Ok(()),
801+
}
802+
}
803+
804+
fn replace_turbo_root_token(
805+
task_definition: &mut RawTaskDefinition,
806+
path_to_repo_root: &RelativeUnixPath,
807+
) -> Result<(), Error> {
808+
for input in task_definition
809+
.inputs
810+
.iter_mut()
811+
.flat_map(|inputs| inputs.iter_mut())
812+
{
813+
replace_turbo_root_token_in_string(input, path_to_repo_root)?;
814+
}
815+
816+
for output in task_definition
817+
.outputs
818+
.iter_mut()
819+
.flat_map(|outputs| outputs.iter_mut())
820+
{
821+
replace_turbo_root_token_in_string(output, path_to_repo_root)?;
822+
}
823+
824+
Ok(())
825+
}
826+
768827
#[cfg(test)]
769828
mod tests {
829+
use std::sync::Arc;
830+
770831
use anyhow::Result;
771832
use biome_deserialize::json::deserialize_from_json_str;
772833
use biome_json_parser::JsonParserOptions;
773834
use pretty_assertions::assert_eq;
774835
use serde_json::json;
775836
use test_case::test_case;
837+
use turbopath::RelativeUnixPath;
776838
use turborepo_unescape::UnescapedString;
777839

778-
use super::{RawTurboJson, SpacesJson, Spanned, TurboJson, UIMode};
840+
use super::{
841+
replace_turbo_root_token_in_string, RawTurboJson, SpacesJson, Spanned, TurboJson, UIMode,
842+
};
779843
use crate::{
780844
boundaries::BoundariesConfig,
781845
cli::OutputLogsMode,
@@ -787,7 +851,7 @@ mod tests {
787851
#[test_case("{}", "empty boundaries")]
788852
#[test_case(r#"{"tags": {} }"#, "empty tags")]
789853
#[test_case(
790-
r#"{"tags": { "my-tag": { "dependencies": { "allow": ["my-package"] } } } }"#,
854+
r#"{"tags": { "my-tag": { "dependencies": { "allow": ["my-package"] } } }"#,
791855
"tags and dependencies"
792856
)]
793857
#[test_case(
@@ -954,6 +1018,29 @@ mod tests {
9541018
}
9551019
; "full (windows)"
9561020
)]
1021+
#[test_case(
1022+
r#"{
1023+
"inputs": ["$TURBO_ROOT$/config.txt"],
1024+
"outputs": ["$TURBO_ROOT$/coverage/**", "!$TURBO_ROOT$/coverage/index.html"]
1025+
}"#,
1026+
RawTaskDefinition {
1027+
inputs: Some(vec![Spanned::new(UnescapedString::from("$TURBO_ROOT$/config.txt")).with_range(25..50)]),
1028+
outputs: Some(vec![
1029+
Spanned::new(UnescapedString::from("$TURBO_ROOT$/coverage/**")).with_range(77..103),
1030+
Spanned::new(UnescapedString::from("!$TURBO_ROOT$/coverage/index.html")).with_range(105..140),
1031+
]),
1032+
..RawTaskDefinition::default()
1033+
},
1034+
TaskDefinition {
1035+
inputs: vec!["../../config.txt".to_owned()],
1036+
outputs: TaskOutputs {
1037+
inclusions: vec!["../../coverage/**".to_owned()],
1038+
exclusions: vec!["../../coverage/index.html".to_owned()],
1039+
},
1040+
..TaskDefinition::default()
1041+
}
1042+
; "turbo root"
1043+
)]
9571044
fn test_deserialize_task_definition(
9581045
task_definition_content: &str,
9591046
expected_raw_task_definition: RawTaskDefinition,
@@ -968,7 +1055,8 @@ mod tests {
9681055
deserialized_result.into_deserialized().unwrap();
9691056
assert_eq!(raw_task_definition, expected_raw_task_definition);
9701057

971-
let task_definition: TaskDefinition = raw_task_definition.try_into()?;
1058+
let task_definition =
1059+
TaskDefinition::from_raw(raw_task_definition, RelativeUnixPath::new("../..").unwrap())?;
9721060
assert_eq!(task_definition, expected_task_definition);
9731061

9741062
Ok(())
@@ -1191,4 +1279,24 @@ mod tests {
11911279
&[Spanned::new(UnescapedString::from("api#server"))]
11921280
);
11931281
}
1282+
1283+
#[test_case("index.ts", Ok("index.ts") ; "no token")]
1284+
#[test_case("$TURBO_ROOT$/config.txt", Ok("../../config.txt") ; "valid token")]
1285+
#[test_case("!$TURBO_ROOT$/README.md", Ok("!../../README.md") ; "negation")]
1286+
#[test_case("../$TURBO_ROOT$/config.txt", Err("Cannot use '$TURBO_ROOT$' anywhere besides start of string.") ; "invalid token")]
1287+
fn test_replace_turbo_root(input: &'static str, expected: Result<&str, &str>) {
1288+
let mut spanned_string = Spanned::new(UnescapedString::from(input))
1289+
.with_path(Arc::from("turbo.json"))
1290+
.with_text(format!("\"{input}\""))
1291+
.with_range(1..(input.len()));
1292+
let result = replace_turbo_root_token_in_string(
1293+
&mut spanned_string,
1294+
RelativeUnixPath::new("../..").unwrap(),
1295+
);
1296+
let actual = match result {
1297+
Ok(()) => Ok(spanned_string.as_inner().as_ref()),
1298+
Err(e) => Err(e.to_string()),
1299+
};
1300+
assert_eq!(actual, expected.map_err(|s| s.to_owned()));
1301+
}
11941302
}

0 commit comments

Comments
 (0)