Skip to content

Commit fb32965

Browse files
feat: add JSONC support (#10083)
### Description JSONC allows the use of comments in JSON files. Until now, our configuration parser allowed comments in JSON by accident, which made for awkwardness talked about in #3793. With this PR, we're now able to support `turbo.jsonc` in both the root configuration and in Package Configurations. The file will be properly used for hashing and users can use `.jsonc` that will work with their editors and other tooling. ### Testing Instructions Tests have been added in this PR and I've hand-tested with `create-turbo`, both `turbo` and some packages. Would appreciate some more hand-testing, just in case, along with your code review. --------- Co-authored-by: Chris Olszewski <[email protected]>
1 parent 4cc60f8 commit fb32965

File tree

26 files changed

+612
-99
lines changed

26 files changed

+612
-99
lines changed

crates/turborepo-filewatch/src/package_watcher.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -449,13 +449,15 @@ impl Subscriber {
449449
tracing::debug!("handling change to workspace {path_workspace}");
450450
let package_json = path_workspace.join_component("package.json");
451451
let turbo_json = path_workspace.join_component("turbo.json");
452+
let turbo_jsonc = path_workspace.join_component("turbo.jsonc");
452453

453-
let (package_exists, turbo_exists) = join!(
454+
let (package_exists, turbo_json_exists, turbo_jsonc_exists) = join!(
454455
// It's possible that an IO error could occur other than the file not existing, but
455456
// we will treat it like the file doesn't exist. It's possible we'll need to
456457
// revisit this, depending on what kind of errors occur.
457458
tokio::fs::try_exists(&package_json).map(|result| result.unwrap_or(false)),
458-
tokio::fs::try_exists(&turbo_json)
459+
tokio::fs::try_exists(&turbo_json),
460+
tokio::fs::try_exists(&turbo_jsonc)
459461
);
460462

461463
changed |= if package_exists {
@@ -464,7 +466,14 @@ impl Subscriber {
464466
path_workspace.to_owned(),
465467
WorkspaceData {
466468
package_json,
467-
turbo_json: turbo_exists.unwrap_or_default().then_some(turbo_json),
469+
turbo_json: turbo_json_exists
470+
.unwrap_or_default()
471+
.then_some(turbo_json)
472+
.or_else(|| {
473+
turbo_jsonc_exists
474+
.unwrap_or_default()
475+
.then_some(turbo_jsonc)
476+
}),
468477
},
469478
)
470479
.is_none()

crates/turborepo-lib/src/commands/login/manual.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,8 @@ pub async fn login_manual(base: &mut CommandBase, force: bool) -> Result<(), Err
4444
// update global config with token
4545
write_token(base, token)?;
4646
// ensure api url & team id/slug are present in turbo.json
47-
write_remote(
48-
&base.root_turbo_json_path(),
49-
api_client.base_url(),
50-
team_identifier,
51-
)?;
47+
let turbo_json_path = base.root_turbo_json_path()?;
48+
write_remote(&turbo_json_path, api_client.base_url(), team_identifier)?;
5249
Ok(())
5350
}
5451

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

+22-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
cli,
1111
config::{ConfigurationOptions, Error as ConfigError, TurborepoConfigBuilder},
1212
opts::Opts,
13+
turbo_json::{CONFIG_FILE, CONFIG_FILE_JSONC},
1314
Args,
1415
};
1516

@@ -139,8 +140,27 @@ impl CommandBase {
139140
fn root_package_json_path(&self) -> AbsoluteSystemPathBuf {
140141
self.repo_root.join_component("package.json")
141142
}
142-
fn root_turbo_json_path(&self) -> AbsoluteSystemPathBuf {
143-
self.repo_root.join_component("turbo.json")
143+
fn root_turbo_json_path(&self) -> Result<AbsoluteSystemPathBuf, ConfigError> {
144+
let turbo_json_path = self.repo_root.join_component(CONFIG_FILE);
145+
let turbo_jsonc_path = self.repo_root.join_component(CONFIG_FILE_JSONC);
146+
147+
let turbo_json_exists = turbo_json_path.exists();
148+
let turbo_jsonc_exists = turbo_jsonc_path.exists();
149+
150+
if turbo_json_exists && turbo_jsonc_exists {
151+
return Err(ConfigError::MultipleTurboConfigs {
152+
directory: self.repo_root.to_string(),
153+
});
154+
}
155+
156+
if turbo_json_exists {
157+
Ok(turbo_json_path)
158+
} else if turbo_jsonc_exists {
159+
Ok(turbo_jsonc_path)
160+
} else {
161+
Ok(turbo_json_path) // Default to turbo.json path even if it doesn't
162+
// exist
163+
}
144164
}
145165

146166
pub fn api_auth(&self) -> Result<Option<APIAuth>, ConfigError> {

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

+99-20
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use turborepo_repository::package_graph::PackageName;
2626
pub use crate::turbo_json::{RawTurboJson, UIMode};
2727
use crate::{
2828
cli::{EnvMode, LogOrder},
29-
turbo_json::CONFIG_FILE,
29+
turbo_json::{CONFIG_FILE, CONFIG_FILE_JSONC},
3030
};
3131

3232
#[derive(Debug, Error, Diagnostic)]
@@ -74,10 +74,15 @@ pub enum Error {
7474
#[error(transparent)]
7575
PackageJson(#[from] turborepo_repository::package_json::Error),
7676
#[error(
77-
"Could not find turbo.json.\nFollow directions at https://turbo.build/repo/docs to create \
77+
"Could not find turbo.json or turbo.jsonc.\nFollow directions at https://turbo.build/repo/docs to create \
7878
one."
7979
)]
8080
NoTurboJSON,
81+
#[error(
82+
"Found both turbo.json and turbo.jsonc in the same directory: {directory}\nRemove either \
83+
turbo.json or turbo.jsonc so there is only one."
84+
)]
85+
MultipleTurboConfigs { directory: String },
8186
#[error(transparent)]
8287
SerdeJson(#[from] serde_json::Error),
8388
#[error(transparent)]
@@ -396,10 +401,29 @@ impl ConfigurationOptions {
396401
self.run_summary.unwrap_or_default()
397402
}
398403

399-
pub fn root_turbo_json_path(&self, repo_root: &AbsoluteSystemPath) -> AbsoluteSystemPathBuf {
400-
self.root_turbo_json_path
401-
.clone()
402-
.unwrap_or_else(|| repo_root.join_component(CONFIG_FILE))
404+
pub fn root_turbo_json_path(
405+
&self,
406+
repo_root: &AbsoluteSystemPath,
407+
) -> Result<AbsoluteSystemPathBuf, Error> {
408+
if let Some(path) = &self.root_turbo_json_path {
409+
return Ok(path.clone());
410+
}
411+
412+
// Check if both files exist
413+
let turbo_json_path = repo_root.join_component(CONFIG_FILE);
414+
let turbo_jsonc_path = repo_root.join_component(CONFIG_FILE_JSONC);
415+
let turbo_json_exists = turbo_json_path.try_exists()?;
416+
let turbo_jsonc_exists = turbo_jsonc_path.try_exists()?;
417+
418+
match (turbo_json_exists, turbo_jsonc_exists) {
419+
(true, true) => Err(Error::MultipleTurboConfigs {
420+
directory: repo_root.to_string(),
421+
}),
422+
(true, false) => Ok(turbo_json_path),
423+
(false, true) => Ok(turbo_jsonc_path),
424+
// Default to turbo.json if neither exists
425+
(false, false) => Ok(turbo_json_path),
426+
}
403427
}
404428

405429
pub fn allow_no_turbo_json(&self) -> bool {
@@ -450,16 +474,6 @@ impl TurborepoConfigBuilder {
450474
self
451475
}
452476

453-
// Getting all of the paths.
454-
#[allow(dead_code)]
455-
fn root_package_json_path(&self) -> AbsoluteSystemPathBuf {
456-
self.repo_root.join_component("package.json")
457-
}
458-
#[allow(dead_code)]
459-
fn root_turbo_json_path(&self) -> AbsoluteSystemPathBuf {
460-
self.repo_root.join_component("turbo.json")
461-
}
462-
463477
fn get_environment(&self) -> HashMap<OsString, OsString> {
464478
self.environment
465479
.clone()
@@ -517,9 +531,12 @@ mod test {
517531
use tempfile::TempDir;
518532
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf};
519533

520-
use crate::config::{
521-
ConfigurationOptions, TurborepoConfigBuilder, DEFAULT_API_URL, DEFAULT_LOGIN_URL,
522-
DEFAULT_TIMEOUT,
534+
use crate::{
535+
config::{
536+
ConfigurationOptions, TurborepoConfigBuilder, DEFAULT_API_URL, DEFAULT_LOGIN_URL,
537+
DEFAULT_TIMEOUT,
538+
},
539+
turbo_json::{CONFIG_FILE, CONFIG_FILE_JSONC},
523540
};
524541

525542
#[test]
@@ -542,7 +559,7 @@ mod test {
542559
})
543560
.unwrap();
544561
assert_eq!(
545-
defaults.root_turbo_json_path(repo_root),
562+
defaults.root_turbo_json_path(repo_root).unwrap(),
546563
repo_root.join_component("turbo.json")
547564
)
548565
}
@@ -635,4 +652,66 @@ mod test {
635652
assert!(!config.preflight());
636653
assert_eq!(config.timeout(), 123);
637654
}
655+
656+
#[test]
657+
fn test_multiple_turbo_configs() {
658+
let tmp_dir = TempDir::new().unwrap();
659+
let repo_root = AbsoluteSystemPath::from_std_path(tmp_dir.path()).unwrap();
660+
661+
// Create both turbo.json and turbo.jsonc
662+
let turbo_json_path = repo_root.join_component(CONFIG_FILE);
663+
let turbo_jsonc_path = repo_root.join_component(CONFIG_FILE_JSONC);
664+
665+
turbo_json_path.create_with_contents("{}").unwrap();
666+
turbo_jsonc_path.create_with_contents("{}").unwrap();
667+
668+
// Test ConfigurationOptions.root_turbo_json_path
669+
let config = ConfigurationOptions::default();
670+
let result = config.root_turbo_json_path(repo_root);
671+
assert!(result.is_err());
672+
}
673+
674+
#[test]
675+
fn test_only_turbo_json() {
676+
let tmp_dir = TempDir::new().unwrap();
677+
let repo_root = AbsoluteSystemPath::from_std_path(tmp_dir.path()).unwrap();
678+
679+
// Create only turbo.json
680+
let turbo_json_path = repo_root.join_component(CONFIG_FILE);
681+
turbo_json_path.create_with_contents("{}").unwrap();
682+
683+
// Test ConfigurationOptions.root_turbo_json_path
684+
let config = ConfigurationOptions::default();
685+
let result = config.root_turbo_json_path(repo_root);
686+
687+
assert_eq!(result.unwrap(), turbo_json_path);
688+
}
689+
690+
#[test]
691+
fn test_only_turbo_jsonc() {
692+
let tmp_dir = TempDir::new().unwrap();
693+
let repo_root = AbsoluteSystemPath::from_std_path(tmp_dir.path()).unwrap();
694+
695+
// Create only turbo.jsonc
696+
let turbo_jsonc_path = repo_root.join_component(CONFIG_FILE_JSONC);
697+
turbo_jsonc_path.create_with_contents("{}").unwrap();
698+
699+
// Test ConfigurationOptions.root_turbo_json_path
700+
let config = ConfigurationOptions::default();
701+
let result = config.root_turbo_json_path(repo_root);
702+
703+
assert_eq!(result.unwrap(), turbo_jsonc_path);
704+
}
705+
706+
#[test]
707+
fn test_no_turbo_config() {
708+
let tmp_dir = TempDir::new().unwrap();
709+
let repo_root = AbsoluteSystemPath::from_std_path(tmp_dir.path()).unwrap();
710+
711+
// Test ConfigurationOptions.root_turbo_json_path
712+
let config = ConfigurationOptions::default();
713+
let result = config.root_turbo_json_path(repo_root);
714+
715+
assert_eq!(result.unwrap(), repo_root.join_component(CONFIG_FILE));
716+
}
638717
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl<'a> ResolvedConfigurationOptions for TurboJsonReader<'a> {
5252
&self,
5353
existing_config: &ConfigurationOptions,
5454
) -> Result<ConfigurationOptions, Error> {
55-
let turbo_json_path = existing_config.root_turbo_json_path(self.repo_root);
55+
let turbo_json_path = existing_config.root_turbo_json_path(self.repo_root)?;
5656
let turbo_json = RawTurboJson::read(self.repo_root, &turbo_json_path).or_else(|e| {
5757
if let Error::Io(e) = &e {
5858
if matches!(e.kind(), std::io::ErrorKind::NotFound) {
@@ -72,6 +72,7 @@ mod test {
7272
use tempfile::tempdir;
7373

7474
use super::*;
75+
use crate::turbo_json::CONFIG_FILE;
7576

7677
#[test]
7778
fn test_reads_from_default() {
@@ -82,7 +83,7 @@ mod test {
8283
..Default::default()
8384
};
8485
repo_root
85-
.join_component("turbo.json")
86+
.join_component(CONFIG_FILE)
8687
.create_with_contents(
8788
serde_json::to_string_pretty(&serde_json::json!({
8889
"daemon": false

crates/turborepo-lib/src/opts.rs

+13-8
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
},
1515
config::ConfigurationOptions,
1616
run::task_id::TaskId,
17-
turbo_json::UIMode,
17+
turbo_json::{UIMode, CONFIG_FILE},
1818
Args,
1919
};
2020

@@ -287,7 +287,10 @@ pub enum ResolvedLogPrefix {
287287

288288
impl<'a> From<OptsInputs<'a>> for RepoOpts {
289289
fn from(inputs: OptsInputs<'a>) -> Self {
290-
let root_turbo_json_path = inputs.config.root_turbo_json_path(inputs.repo_root);
290+
let root_turbo_json_path = inputs
291+
.config
292+
.root_turbo_json_path(inputs.repo_root)
293+
.unwrap_or_else(|_| inputs.repo_root.join_component(CONFIG_FILE));
291294
let allow_no_package_manager = inputs.config.allow_no_package_manager();
292295
let allow_no_turbo_json = inputs.config.allow_no_turbo_json();
293296

@@ -553,7 +556,7 @@ mod test {
553556
commands::CommandBase,
554557
config::ConfigurationOptions,
555558
opts::{Opts, RunCacheOpts, ScopeOpts},
556-
turbo_json::UIMode,
559+
turbo_json::{UIMode, CONFIG_FILE},
557560
Args,
558561
};
559562

@@ -694,7 +697,9 @@ mod test {
694697
.map(|(base, head)| (Some(base), Some(head))),
695698
};
696699
let config = ConfigurationOptions::default();
697-
let root_turbo_json_path = config.root_turbo_json_path(&AbsoluteSystemPathBuf::default());
700+
let root_turbo_json_path = config
701+
.root_turbo_json_path(&AbsoluteSystemPathBuf::default())
702+
.unwrap_or_else(|_| AbsoluteSystemPathBuf::default().join_component(CONFIG_FILE));
698703

699704
let opts = Opts {
700705
repo_opts: RepoOpts {
@@ -801,11 +806,11 @@ mod test {
801806
let tmpdir = TempDir::new()?;
802807
let repo_root = AbsoluteSystemPathBuf::try_from(tmpdir.path())?;
803808

804-
repo_root
805-
.join_component("turbo.json")
806-
.create_with_contents(serde_json::to_string_pretty(&serde_json::json!({
809+
repo_root.join_component(CONFIG_FILE).create_with_contents(
810+
serde_json::to_string_pretty(&serde_json::json!({
807811
"remoteCache": { "enabled": true }
808-
}))?)?;
812+
}))?,
813+
)?;
809814

810815
let mut args = Args::default();
811816
args.command = Some(Command::Run {

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -383,19 +383,21 @@ impl RunBuilder {
383383
let task_access = TaskAccess::new(self.repo_root.clone(), async_cache.clone(), &scm);
384384
task_access.restore_config().await;
385385

386+
let root_turbo_json_path = self.opts.repo_opts.root_turbo_json_path.clone();
387+
386388
let turbo_json_loader = if task_access.is_enabled() {
387389
TurboJsonLoader::task_access(
388390
self.repo_root.clone(),
389-
self.opts.repo_opts.root_turbo_json_path.clone(),
391+
root_turbo_json_path.clone(),
390392
root_package_json.clone(),
391393
)
392394
} else if is_single_package {
393395
TurboJsonLoader::single_package(
394396
self.repo_root.clone(),
395-
self.opts.repo_opts.root_turbo_json_path.clone(),
397+
root_turbo_json_path.clone(),
396398
root_package_json.clone(),
397399
)
398-
} else if !self.opts.repo_opts.root_turbo_json_path.exists() &&
400+
} else if !root_turbo_json_path.exists() &&
399401
// Infer a turbo.json if allowing no turbo.json is explicitly allowed or if MFE configs are discovered
400402
(self.opts.repo_opts.allow_no_turbo_json || micro_frontend_configs.is_some())
401403
{
@@ -407,14 +409,14 @@ impl RunBuilder {
407409
} else if let Some(micro_frontends) = &micro_frontend_configs {
408410
TurboJsonLoader::workspace_with_microfrontends(
409411
self.repo_root.clone(),
410-
self.opts.repo_opts.root_turbo_json_path.clone(),
412+
root_turbo_json_path.clone(),
411413
pkg_dep_graph.packages(),
412414
micro_frontends.clone(),
413415
)
414416
} else {
415417
TurboJsonLoader::workspace(
416418
self.repo_root.clone(),
417-
self.opts.repo_opts.root_turbo_json_path.clone(),
419+
root_turbo_json_path.clone(),
418420
pkg_dep_graph.packages(),
419421
)
420422
};

crates/turborepo-lib/src/run/watch.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ impl WatchClient {
120120
let signal = get_signal()?;
121121
let handler = SignalHandler::new(signal);
122122

123-
if base.opts.repo_opts.root_turbo_json_path != base.repo_root.join_component(CONFIG_FILE) {
123+
// Check if the turbo.json path is the standard one
124+
let standard_path = base.repo_root.join_component(CONFIG_FILE);
125+
if base.opts.repo_opts.root_turbo_json_path != standard_path {
124126
return Err(Error::NonStandardTurboJsonPath(
125127
base.opts.repo_opts.root_turbo_json_path.to_string(),
126128
));

0 commit comments

Comments
 (0)