From 705f6615ca7e35dd38562c1a8621f8b3b40de30c Mon Sep 17 00:00:00 2001 From: Anthony Shew Date: Thu, 30 Jul 2026 18:09:07 -0700 Subject: [PATCH] refactor: Compose repository graphs for optional toolchains --- crates/turborepo-lib/src/commands/config.rs | 13 ++- crates/turborepo-lib/src/commands/daemon.rs | 5 +- .../src/commands/get_mfe_port.rs | 17 ++-- crates/turborepo-lib/src/devtools.rs | 18 ++-- crates/turborepo-lib/src/lib.rs | 1 + .../src/package_changes_watcher.rs | 83 +++++++------------ crates/turborepo-lib/src/repository_graph.rs | 53 ++++++++++++ crates/turborepo-lib/src/run/builder.rs | 33 ++------ crates/turborepo-lib/src/run/watch.rs | 5 +- 9 files changed, 114 insertions(+), 114 deletions(-) create mode 100644 crates/turborepo-lib/src/repository_graph.rs diff --git a/crates/turborepo-lib/src/commands/config.rs b/crates/turborepo-lib/src/commands/config.rs index b8b617465a081..af0c87dd00ddb 100644 --- a/crates/turborepo-lib/src/commands/config.rs +++ b/crates/turborepo-lib/src/commands/config.rs @@ -5,7 +5,7 @@ use turborepo_repository::package_graph::PackageGraph; use turborepo_types::{EnvMode, UIMode}; use crate::{ - cli, config::resolve_configuration_from_args, run::builder::load_root_package_json, + cli, config::resolve_configuration_from_args, repository_graph::RepositoryGraphFeatures, turbo_json::RawTurboJson, Args, }; @@ -40,15 +40,12 @@ pub async fn run(repo_root: AbsoluteSystemPathBuf, args: Args) -> Result<(), cli let future_flags = RawTurboJson::read(&repo_root, &root_turbo_json_path, true)? .and_then(|raw| raw.future_flags.map(|flags| flags.into_inner())) .unwrap_or_default(); - let cargo_enabled = future_flags.experimental_cargo_workspaces; - let root_package_json = load_root_package_json(&repo_root, cargo_enabled)?; + let features = RepositoryGraphFeatures::new(&future_flags); + let root_package_json = features.load_root_package_json(&repo_root)?; - let mut builder = PackageGraph::builder_optional(&repo_root, root_package_json) + let builder = PackageGraph::builder_optional(&repo_root, root_package_json) .with_allow_no_package_manager(config.allow_no_package_manager()); - if cargo_enabled { - builder = builder.with_cargo(); - } - let package_graph = builder.build().await?; + let package_graph = features.configure(builder).build().await?; let package_manager = package_graph.package_manager().map(|pm| pm.name()); diff --git a/crates/turborepo-lib/src/commands/daemon.rs b/crates/turborepo-lib/src/commands/daemon.rs index 7401865f4dbcd..ec5012f8b9c48 100644 --- a/crates/turborepo-lib/src/commands/daemon.rs +++ b/crates/turborepo-lib/src/commands/daemon.rs @@ -344,7 +344,8 @@ pub async fn daemon_server( custom_turbo_json_path, allow_no_package_manager, { - let cargo_enabled = crate::run::builder::cargo_enabled(&base.opts().future_flags); + let graph_features = + crate::repository_graph::RepositoryGraphFeatures::new(&base.opts().future_flags); move |args| { PackageChangesWatcher::new( args.repo_root, @@ -353,7 +354,7 @@ pub async fn daemon_server( args.custom_turbo_json_path, false, args.allow_no_package_manager, - cargo_enabled, + graph_features, ) } }, diff --git a/crates/turborepo-lib/src/commands/get_mfe_port.rs b/crates/turborepo-lib/src/commands/get_mfe_port.rs index fec08631ccd2e..1e3968a0d9188 100644 --- a/crates/turborepo-lib/src/commands/get_mfe_port.rs +++ b/crates/turborepo-lib/src/commands/get_mfe_port.rs @@ -7,11 +7,7 @@ use turborepo_repository::package_graph::{ PackageGraph, PackageGraphNodeKind, PackageName, PackageNode, }; -use crate::{ - commands::CommandBase, - microfrontends::MicrofrontendsConfigs, - run::builder::{cargo_enabled, load_root_package_json}, -}; +use crate::{commands::CommandBase, microfrontends::MicrofrontendsConfigs}; #[derive(Debug, Error)] pub enum Error { @@ -52,17 +48,14 @@ async fn get_port_for_current_package(base: &CommandBase) -> Result async fn build_package_graph(base: &CommandBase) -> Result { let repo_root = &base.repo_root; - let cargo_enabled = cargo_enabled(&base.opts().future_flags); - let root_package_json = load_root_package_json(repo_root, cargo_enabled)?; + let features = crate::repository_graph::RepositoryGraphFeatures::new(&base.opts().future_flags); + let root_package_json = features.load_root_package_json(repo_root)?; - let mut builder = PackageGraph::builder_optional(repo_root, root_package_json) + let builder = PackageGraph::builder_optional(repo_root, root_package_json) .with_single_package_mode(base.opts().run_opts.single_package) .with_allow_no_package_manager(base.opts().repo_opts.allow_no_package_manager); - if cargo_enabled { - builder = builder.with_cargo(); - } - Ok(builder.build().await?) + Ok(features.configure(builder).build().await?) } /// Resolve directory ownership from authoritative repository knowledge. The diff --git a/crates/turborepo-lib/src/devtools.rs b/crates/turborepo-lib/src/devtools.rs index d42f19b86a92c..b9cde462828d9 100644 --- a/crates/turborepo-lib/src/devtools.rs +++ b/crates/turborepo-lib/src/devtools.rs @@ -18,7 +18,7 @@ use crate::{ commands::CommandBase, engine::{EngineBuilder, TaskNode as EngineTaskNode}, opts::Opts, - run::builder::{cargo_enabled, load_root_package_json}, + repository_graph::RepositoryGraphFeatures, turbo_json::{TurboJsonReader, UnifiedTurboJsonLoader}, Args, }; @@ -57,19 +57,17 @@ impl ProperTaskGraphBuilder { /// Build the package graph for the repository async fn build_package_graph(&self, opts: &Opts) -> Result { - let root_package_json = - load_root_package_json(&self.repo_root, cargo_enabled(&opts.future_flags)).map_err( - |e| TaskGraphError::BuildError(format!("Failed to load package.json: {e}")), - )?; + let features = RepositoryGraphFeatures::new(&opts.future_flags); + let root_package_json = features + .load_root_package_json(&self.repo_root) + .map_err(|e| TaskGraphError::BuildError(format!("Failed to load package.json: {e}")))?; - let mut builder = PackageGraphBuilder::new_optional(&self.repo_root, root_package_json) + let builder = PackageGraphBuilder::new_optional(&self.repo_root, root_package_json) .with_single_package_mode(opts.run_opts.single_package) .with_allow_no_package_manager(opts.repo_opts.allow_no_package_manager); - if cargo_enabled(&opts.future_flags) { - builder = builder.with_cargo(); - } - builder + features + .configure(builder) .build() .await .map_err(|e| TaskGraphError::BuildError(format!("Failed to build package graph: {e}"))) diff --git a/crates/turborepo-lib/src/lib.rs b/crates/turborepo-lib/src/lib.rs index 457dfc82473a5..5a7dfa3166f1d 100644 --- a/crates/turborepo-lib/src/lib.rs +++ b/crates/turborepo-lib/src/lib.rs @@ -23,6 +23,7 @@ mod opts; mod package_changes_watcher; mod panic_handler; mod rayon_compat; +mod repository_graph; mod run; mod shim; mod task_change_detector; diff --git a/crates/turborepo-lib/src/package_changes_watcher.rs b/crates/turborepo-lib/src/package_changes_watcher.rs index a346dd2862710..dd70dcde89329 100644 --- a/crates/turborepo-lib/src/package_changes_watcher.rs +++ b/crates/turborepo-lib/src/package_changes_watcher.rs @@ -1,7 +1,6 @@ use std::{ cell::RefCell, collections::{HashMap, HashSet}, - io::ErrorKind, ops::DerefMut, sync::{ atomic::{AtomicBool, Ordering}, @@ -23,13 +22,13 @@ use turborepo_repository::{ ChangeMapper, GlobalDepsPackageChangeMapper, LockfileContents, PackageChanges, }, package_graph::{PackageGraph, PackageName, WorkspacePackage}, - package_json::{self, PackageJson}, toolchain::WatchSpec, }; use turborepo_scm::GitHashes; use crate::{ config::{resolve_turbo_config_path, CONFIG_FILE, CONFIG_FILE_JSONC}, + repository_graph::RepositoryGraphFeatures, turbo_json::{TurboJson, TurboJsonReader, UnifiedTurboJsonLoader}, }; @@ -70,7 +69,7 @@ impl PackageChangesWatcher { custom_turbo_json_path: Option, single_package: bool, allow_no_package_manager: bool, - cargo_enabled: bool, + graph_features: RepositoryGraphFeatures, ) -> Self { let (exit_tx, exit_rx) = oneshot::channel(); let (package_change_events_tx, package_change_events_rx) = @@ -83,7 +82,7 @@ impl PackageChangesWatcher { custom_turbo_json_path, single_package, allow_no_package_manager, - cargo_enabled, + graph_features, ); let _handle = tokio::spawn(subscriber.watch(exit_rx)); @@ -134,8 +133,7 @@ struct Subscriber { custom_turbo_json_path: Option, single_package: bool, allow_no_package_manager: bool, - /// Mirrors the run builder so the watcher observes the same Cargo scopes. - cargo_enabled: bool, + graph_features: RepositoryGraphFeatures, } fn is_in_git_folder(path: &AnchoredSystemPath) -> bool { @@ -162,21 +160,6 @@ struct PackageHashBaseline { hashes: Arc, } -fn load_root_package_json( - repo_root: &AbsoluteSystemPathBuf, - allow_missing_for_cargo: bool, -) -> Result, package_json::Error> { - match PackageJson::load(&repo_root.join_component("package.json")) { - Ok(package_json) => Ok(Some(package_json)), - Err(package_json::Error::Io(error)) - if error.kind() == ErrorKind::NotFound && allow_missing_for_cargo => - { - Ok(None) - } - Err(error) => Err(error), - } -} - fn baseline_matches( baseline: Option<&PackageHashBaseline>, path: &AnchoredSystemPath, @@ -370,7 +353,7 @@ impl Subscriber { custom_turbo_json_path: Option, single_package: bool, allow_no_package_manager: bool, - cargo_enabled: bool, + graph_features: RepositoryGraphFeatures, ) -> Self { // Try to canonicalize the custom path to match what the file watcher reports let normalized_custom_path = custom_turbo_json_path.map(|path| { @@ -422,35 +405,25 @@ impl Subscriber { custom_turbo_json_path: normalized_custom_path, single_package, allow_no_package_manager, - cargo_enabled, + graph_features, } } async fn initialize_repo_state(&self) -> Option { - let allow_missing_for_cargo = self.cargo_enabled - && self - .repo_root - .join_component(turborepo_repository::cargo::CARGO_TOML) - .exists(); - let root_package_json = - match load_root_package_json(&self.repo_root, allow_missing_for_cargo) { - Ok(package_json) => package_json, - Err(error) => { - tracing::debug!( - ?error, - "root package.json not available, package watcher not available" - ); - return None; - } - }; - let mut builder = - PackageGraph::builder_optional(&self.repo_root, root_package_json.clone()) - .with_single_package_mode(self.single_package) - .with_allow_no_package_manager(self.allow_no_package_manager); - if self.cargo_enabled { - builder = builder.with_cargo(); - } - let Ok(pkg_dep_graph) = builder.build().await else { + let root_package_json = match self.graph_features.load_root_package_json(&self.repo_root) { + Ok(package_json) => package_json, + Err(error) => { + tracing::debug!( + ?error, + "root package.json not available, package watcher not available" + ); + return None; + } + }; + let builder = PackageGraph::builder_optional(&self.repo_root, root_package_json.clone()) + .with_single_package_mode(self.single_package) + .with_allow_no_package_manager(self.allow_no_package_manager); + let Ok(pkg_dep_graph) = self.graph_features.configure(builder).build().await else { tracing::debug!("package graph not available, package watcher not available"); return None; }; @@ -889,10 +862,10 @@ mod test { use super::{ ancestors_is_ignored, baseline_matches, classify_changed_files, hash_scopes, - is_in_git_folder, load_root_package_json, ChangedFiles, FileChangeAction, - PackageChangeEvent, PackageChangesWatcher, PackageHashBaseline, RepositoryIgnore, - Subscriber, CONFIG_FILE, + is_in_git_folder, ChangedFiles, FileChangeAction, PackageChangeEvent, + PackageChangesWatcher, PackageHashBaseline, RepositoryIgnore, Subscriber, CONFIG_FILE, }; + use crate::repository_graph::RepositoryGraphFeatures; fn anchored(s: &str) -> AnchoredSystemPathBuf { AnchoredSystemPathBuf::try_from(s).unwrap() @@ -993,7 +966,9 @@ mod test { None, single_package, false, - cargo_enabled, + RepositoryGraphFeatures { + cargo: cargo_enabled, + }, ) } @@ -1130,7 +1105,9 @@ mod test { .join_component("package.json") .create_with_contents(b"{") .unwrap(); - assert!(load_root_package_json(&repo_root, true).is_err()); + assert!(RepositoryGraphFeatures { cargo: true } + .load_root_package_json(&repo_root) + .is_err()); assert!(initialize_test_state(&repo_root, true).await.is_none()); repo_root.join_component("package.json").remove().unwrap(); @@ -1816,7 +1793,7 @@ mod test { None, single_package, allow_no_package_manager, - false, + RepositoryGraphFeatures { cargo: false }, ); TestWatcherHandle { diff --git a/crates/turborepo-lib/src/repository_graph.rs b/crates/turborepo-lib/src/repository_graph.rs new file mode 100644 index 0000000000000..f099ee5b2876e --- /dev/null +++ b/crates/turborepo-lib/src/repository_graph.rs @@ -0,0 +1,53 @@ +use std::io::ErrorKind; + +use turbopath::AbsoluteSystemPath; +use turborepo_repository::{ + package_graph::PackageGraphBuilder, + package_json::{self, PackageJson}, +}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct RepositoryGraphFeatures { + pub(crate) cargo: bool, +} + +impl RepositoryGraphFeatures { + pub(crate) fn new(future_flags: &turborepo_turbo_json::FutureFlags) -> Self { + Self { + cargo: future_flags.experimental_cargo_workspaces, + } + } + + pub(crate) fn cargo_enabled(self) -> bool { + self.cargo + } + + pub(crate) fn load_root_package_json( + self, + repo_root: &AbsoluteSystemPath, + ) -> Result, package_json::Error> { + match PackageJson::load(&repo_root.join_component("package.json")) { + Ok(package_json) => Ok(Some(package_json)), + Err(package_json::Error::Io(io)) + if io.kind() == ErrorKind::NotFound + && (self.cargo + && repo_root + .join_component(turborepo_repository::cargo::CARGO_TOML) + .exists()) => + { + Ok(None) + } + Err(error) => Err(error), + } + } + + pub(crate) fn configure<'a, T>( + self, + mut builder: PackageGraphBuilder<'a, T>, + ) -> PackageGraphBuilder<'a, T> { + if self.cargo { + builder = builder.with_cargo(); + } + builder + } +} diff --git a/crates/turborepo-lib/src/run/builder.rs b/crates/turborepo-lib/src/run/builder.rs index 8665fd1e57839..97a3720272aa5 100644 --- a/crates/turborepo-lib/src/run/builder.rs +++ b/crates/turborepo-lib/src/run/builder.rs @@ -18,7 +18,6 @@ use turborepo_repository::{ change_mapper::PackageInclusionReason, package_graph::{PackageGraph, PackageName, TaskEntrypointPreference}, package_json, - package_json::PackageJson, }; use turborepo_run_summary::observability; use turborepo_scm::SCM; @@ -56,6 +55,7 @@ use crate::{ engine::{task_has_command, Engine, EngineBuilder, EngineExt}, microfrontends::MicrofrontendsConfigs, opts::Opts, + repository_graph::RepositoryGraphFeatures, run::{ scope, task_access::TaskAccess, Error, RemoteCacheStatus, RemoteCacheUnavailableReason, Run, RunCache, @@ -525,8 +525,8 @@ impl RunBuilder { // package.json) has no JavaScript root manifest. A *missing* file is // only tolerated in that mode; a malformed one always fails, and a // missing one without Cargo support keeps the original hard error. - let root_package_json = - load_root_package_json(&self.repo_root, cargo_enabled(&self.opts.future_flags))?; + let graph_features = RepositoryGraphFeatures::new(&self.opts.future_flags); + let root_package_json = graph_features.load_root_package_json(&self.repo_root)?; let run_telemetry = GenericEventBuilder::new().with_parent(&telemetry); let repo_telemetry = RepoEventBuilder::new(&self.repo_root.to_string()).with_parent(&telemetry); @@ -563,13 +563,11 @@ impl RunBuilder { } let mut pkg_dep_graph = { - let mut builder = + let builder = PackageGraph::builder_optional(&self.repo_root, root_package_json.clone()) .with_single_package_mode(self.opts.run_opts.single_package) .with_allow_no_package_manager(self.opts.repo_opts.allow_no_package_manager); - if cargo_enabled(&self.opts.future_flags) { - builder = builder.with_cargo(); - } + let builder = graph_features.configure(builder); let graph = builder .build() @@ -1467,26 +1465,7 @@ impl RunBuilder { /// future flag is the only switch: it is repo-level configuration, so every /// invoker sees the same package graph. pub(crate) fn cargo_enabled(future_flags: &turborepo_turbo_json::FutureFlags) -> bool { - future_flags.experimental_cargo_workspaces -} - -pub(crate) fn load_root_package_json( - repo_root: &AbsoluteSystemPath, - cargo_enabled: bool, -) -> Result, package_json::Error> { - match PackageJson::load(&repo_root.join_component("package.json")) { - Ok(package_json) => Ok(Some(package_json)), - Err(package_json::Error::Io(io)) - if io.kind() == ErrorKind::NotFound - && cargo_enabled - && repo_root - .join_component(turborepo_repository::cargo::CARGO_TOML) - .exists() => - { - Ok(None) - } - Err(error) => Err(error), - } + RepositoryGraphFeatures::new(future_flags).cargo_enabled() } fn origins_match(url1: &str, url2: &str) -> bool { diff --git a/crates/turborepo-lib/src/run/watch.rs b/crates/turborepo-lib/src/run/watch.rs index 7f1011f18167d..eb7fea2cffc4b 100644 --- a/crates/turborepo-lib/src/run/watch.rs +++ b/crates/turborepo-lib/src/run/watch.rs @@ -301,7 +301,8 @@ impl WatchClient { scm, )); // The watcher builds its own graph and must enable the same ecosystems. - let cargo_enabled = crate::run::builder::cargo_enabled(&base.opts().future_flags); + let graph_features = + crate::repository_graph::RepositoryGraphFeatures::new(&base.opts().future_flags); let package_changes_watcher = PackageChangesWatcher::new( base.repo_root.clone(), watcher.source(), @@ -309,7 +310,7 @@ impl WatchClient { custom_turbo_json_path, base.opts().run_opts.single_package, base.opts().repo_opts.allow_no_package_manager, - cargo_enabled, + graph_features, ); // Subscribe before building the Run so we don't miss the initial