diff --git a/crates/turborepo-lib/src/cli/mod.rs b/crates/turborepo-lib/src/cli/mod.rs index 2f68ff217322c..fc14a368e0d2d 100644 --- a/crates/turborepo-lib/src/cli/mod.rs +++ b/crates/turborepo-lib/src/cli/mod.rs @@ -417,20 +417,6 @@ impl Args { clap_args } - pub fn get_tasks(&self) -> &[String] { - match &self.command { - Some(Command::Run { - run_args: _, - execution_args: box ExecutionArgs { tasks, .. }, - }) => tasks, - _ => self - .execution_args - .as_ref() - .map(|execution_args| execution_args.tasks.as_slice()) - .unwrap_or(&[]), - } - } - pub fn track(&self, tel: &GenericEventBuilder) { // track usage only track_usage!(tel, self.skip_infer, |val| val); diff --git a/crates/turborepo-lib/src/commands/query.rs b/crates/turborepo-lib/src/commands/query.rs index f423cb4882a1f..83cb64fc25ca4 100644 --- a/crates/turborepo-lib/src/commands/query.rs +++ b/crates/turborepo-lib/src/commands/query.rs @@ -68,7 +68,7 @@ pub async fn run( execution_args: Box::default(), }); - let run_builder = RunBuilder::new(base)?; + let run_builder = RunBuilder::new(base)?.add_all_tasks(); let run = run_builder.build(&handler, telemetry).await?; if let Some(query) = query { diff --git a/crates/turborepo-lib/src/engine/builder.rs b/crates/turborepo-lib/src/engine/builder.rs index 484cce98974e5..dd958fcf127c0 100644 --- a/crates/turborepo-lib/src/engine/builder.rs +++ b/crates/turborepo-lib/src/engine/builder.rs @@ -100,6 +100,7 @@ pub struct EngineBuilder<'a> { tasks: Vec>>, root_enabled_tasks: HashSet>, tasks_only: bool, + add_all_tasks: bool, } impl<'a> EngineBuilder<'a> { @@ -118,6 +119,7 @@ impl<'a> EngineBuilder<'a> { tasks: Vec::new(), root_enabled_tasks: HashSet::new(), tasks_only: false, + add_all_tasks: false, } } @@ -148,6 +150,13 @@ impl<'a> EngineBuilder<'a> { self } + /// If set, we will include all tasks in the graph, even if they are not + /// specified + pub fn add_all_tasks(mut self) -> Self { + self.add_all_tasks = true; + self + } + // Returns the set of allowed tasks that can be run if --only is used // The set is exactly the product of the packages in filter and tasks specified // by CLI @@ -185,7 +194,36 @@ impl<'a> EngineBuilder<'a> { let mut missing_tasks: HashMap<&TaskName<'_>, Spanned<()>> = HashMap::from_iter(self.tasks.iter().map(|spanned| spanned.as_ref().split())); let mut traversal_queue = VecDeque::with_capacity(1); - for (workspace, task) in self.workspaces.iter().cartesian_product(self.tasks.iter()) { + let tasks: Vec>> = if self.add_all_tasks { + let mut tasks = Vec::new(); + if let Ok(turbo_json) = turbo_json_loader.load(&PackageName::Root) { + tasks.extend( + turbo_json + .tasks + .keys() + .map(|task| Spanned::new(task.clone())), + ); + } + + for workspace in self.workspaces.iter() { + let Ok(turbo_json) = turbo_json_loader.load(workspace) else { + continue; + }; + + tasks.extend( + turbo_json + .tasks + .keys() + .map(|task| Spanned::new(task.clone())), + ); + } + + tasks + } else { + self.tasks.clone() + }; + + for (workspace, task) in self.workspaces.iter().cartesian_product(tasks.iter()) { let task_id = task .task_id() .unwrap_or_else(|| TaskId::new(workspace.as_ref(), task.task())); diff --git a/crates/turborepo-lib/src/query/mod.rs b/crates/turborepo-lib/src/query/mod.rs index 96dd105e4ab80..a0cc9a7bb15a4 100644 --- a/crates/turborepo-lib/src/query/mod.rs +++ b/crates/turborepo-lib/src/query/mod.rs @@ -1,6 +1,7 @@ mod file; mod package; mod server; +mod task; use std::{io, sync::Arc}; @@ -18,7 +19,7 @@ use turborepo_repository::package_graph::PackageName; use crate::{ get_version, - query::file::File, + query::{file::File, task::Task}, run::{builder::RunBuilder, Run}, signal::SignalHandler, }; @@ -38,8 +39,10 @@ pub enum Error { #[error("failed to serialize result: {0}")] Serde(#[from] serde_json::Error), #[error(transparent)] + #[diagnostic(transparent)] Run(#[from] crate::run::Error), #[error(transparent)] + #[diagnostic(transparent)] Path(#[from] turbopath::PathError), #[error(transparent)] UI(#[from] turborepo_ui::Error), @@ -56,6 +59,8 @@ impl RepositoryQuery { } #[derive(Debug, SimpleObject)] +#[graphql(concrete(name = "Tasks", params(Task)))] +#[graphql(concrete(name = "Packages", params(Package)))] pub struct Array { items: Vec, length: usize, @@ -235,7 +240,7 @@ impl PackagePredicate { fn check_has(pkg: &Package, field: &PackageFields, value: &Any) -> bool { match (field, &value.0) { (PackageFields::Name, Value::String(name)) => pkg.name.as_ref() == name, - (PackageFields::TaskName, Value::String(name)) => pkg.task_names().contains(name), + (PackageFields::TaskName, Value::String(name)) => pkg.get_tasks().contains_key(name), _ => false, } } diff --git a/crates/turborepo-lib/src/query/package.rs b/crates/turborepo-lib/src/query/package.rs index e5092a87a5237..0145fbf475d79 100644 --- a/crates/turborepo-lib/src/query/package.rs +++ b/crates/turborepo-lib/src/query/package.rs @@ -1,25 +1,32 @@ -use std::{collections::HashSet, sync::Arc}; +use std::{collections::HashMap, sync::Arc}; use async_graphql::Object; use itertools::Itertools; +use turborepo_errors::Spanned; use turborepo_repository::package_graph::{PackageName, PackageNode}; use crate::{ - query::{Array, Error}, + query::{task::Task, Array, Error}, run::Run, }; +#[derive(Clone)] pub struct Package { pub run: Arc, pub name: PackageName, } impl Package { - pub fn task_names(&self) -> HashSet { + pub fn get_tasks(&self) -> HashMap> { self.run .pkg_dep_graph() .package_json(&self.name) - .map(|json| json.scripts.keys().cloned().collect()) + .map(|json| { + json.scripts + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect() + }) .unwrap_or_default() } @@ -191,4 +198,15 @@ impl Package { .sorted_by(|a, b| a.name.cmp(&b.name)) .collect()) } + + async fn tasks(&self) -> Array { + self.get_tasks() + .into_iter() + .map(|(name, script)| Task { + name, + package: self.clone(), + script: Some(script), + }) + .collect() + } } diff --git a/crates/turborepo-lib/src/query/task.rs b/crates/turborepo-lib/src/query/task.rs index 8b137891791fe..37dbaaead745f 100644 --- a/crates/turborepo-lib/src/query/task.rs +++ b/crates/turborepo-lib/src/query/task.rs @@ -1 +1,74 @@ +use async_graphql::Object; +use turborepo_errors::Spanned; +use crate::{ + engine::TaskNode, + query::{package::Package, Array}, + run::task_id::TaskId, +}; + +pub struct Task { + pub name: String, + pub package: Package, + pub script: Option>, +} + +#[Object] +impl Task { + async fn name(&self) -> String { + self.name.clone() + } + + async fn package(&self) -> Package { + self.package.clone() + } + + async fn script(&self) -> Option { + self.script.as_ref().map(|script| script.value.to_string()) + } + + async fn direct_dependents(&self) -> Array { + let task_id = TaskId::from_static(self.package.name.to_string(), self.name.clone()); + self.package + .run + .engine() + .dependents(&task_id) + .into_iter() + .flatten() + .filter_map(|task| match task { + TaskNode::Root => None, + TaskNode::Task(task) => Some(Task { + name: task.task().to_string(), + package: Package { + run: self.package.run.clone(), + name: task.package().to_string().into(), + }, + script: self.package.get_tasks().get(task.task()).cloned(), + }), + }) + .collect() + } + + async fn direct_dependencies(&self) -> Array { + let task_id = TaskId::new(self.package.name.as_ref(), &self.name); + + self.package + .run + .engine() + .dependencies(&task_id) + .into_iter() + .flatten() + .filter_map(|task| match task { + TaskNode::Root => None, + TaskNode::Task(task) => Some(Task { + name: task.task().to_string(), + package: Package { + run: self.package.run.clone(), + name: task.package().to_string().into(), + }, + script: self.package.get_tasks().get(task.task()).cloned(), + }), + }) + .collect() + } +} diff --git a/crates/turborepo-lib/src/run/builder.rs b/crates/turborepo-lib/src/run/builder.rs index aee20d56975b5..0169515663b4c 100644 --- a/crates/turborepo-lib/src/run/builder.rs +++ b/crates/turborepo-lib/src/run/builder.rs @@ -66,6 +66,8 @@ pub struct RunBuilder { should_print_prelude_override: Option, allow_missing_package_manager: bool, allow_no_turbo_json: bool, + // If true, we will add all tasks to the graph, even if they are not specified + add_all_tasks: bool, } impl RunBuilder { @@ -108,6 +110,7 @@ impl RunBuilder { allow_missing_package_manager, root_turbo_json_path, allow_no_turbo_json, + add_all_tasks: false, }) } @@ -121,6 +124,11 @@ impl RunBuilder { self } + pub fn add_all_tasks(mut self) -> Self { + self.add_all_tasks = true; + self + } + fn connect_process_manager(&self, signal_subscriber: SignalSubscriber) { let manager = self.processes.clone(); tokio::spawn(async move { @@ -464,7 +472,7 @@ impl RunBuilder { filtered_pkgs: &HashSet, turbo_json_loader: TurboJsonLoader, ) -> Result { - let mut engine = EngineBuilder::new( + let mut builder = EngineBuilder::new( &self.repo_root, pkg_dep_graph, turbo_json_loader, @@ -476,8 +484,13 @@ impl RunBuilder { .with_tasks(self.opts.run_opts.tasks.iter().map(|task| { // TODO: Pull span info from command Spanned::new(TaskName::from(task.as_str()).into_owned()) - })) - .build()?; + })); + + if self.add_all_tasks { + builder = builder.add_all_tasks(); + } + + let mut engine = builder.build()?; // If we have an initial task, we prune out the engine to only // tasks that are reachable from that initial task. diff --git a/crates/turborepo-lib/src/run/mod.rs b/crates/turborepo-lib/src/run/mod.rs index 20e319477f0f2..5b50452c10be5 100644 --- a/crates/turborepo-lib/src/run/mod.rs +++ b/crates/turborepo-lib/src/run/mod.rs @@ -194,6 +194,10 @@ impl Run { &self.pkg_dep_graph } + pub fn engine(&self) -> &Engine { + &self.engine + } + pub fn filtered_pkgs(&self) -> &HashSet { &self.filtered_pkgs } diff --git a/turborepo-tests/integration/tests/command-query.t b/turborepo-tests/integration/tests/command-query.t index 178e83b0c6934..67b4f57c5644a 100644 --- a/turborepo-tests/integration/tests/command-query.t +++ b/turborepo-tests/integration/tests/command-query.t @@ -210,5 +210,7 @@ Run the query $ ${TURBO} query "query { version }" | jq ".data.version" > QUERY_VERSION WARNING query command is experimental and may change in the future + $ VERSION=${MONOREPO_ROOT_DIR}/version.txt $ diff --strip-trailing-cr <(head -n 1 ${VERSION}) <(${TURBO} --version) + diff --git a/turborepo-tests/integration/tests/query/tasks.t b/turborepo-tests/integration/tests/query/tasks.t new file mode 100644 index 0000000000000..c36a481760485 --- /dev/null +++ b/turborepo-tests/integration/tests/query/tasks.t @@ -0,0 +1,70 @@ +Setup + $ . ${TESTDIR}/../../../helpers/setup_integration_test.sh task_dependencies/topological + + $ ${TURBO} query "query { package(name: \"my-app\") { tasks { items { name } } } }" | jq + WARNING query command is experimental and may change in the future + { + "data": { + "package": { + "tasks": { + "items": [ + { + "name": "build" + } + ] + } + } + } + } + + $ ${TURBO} query "query { package(name: \"my-app\") { tasks { items { name directDependencies { items { name package { name } } } } } } }" | jq + WARNING query command is experimental and may change in the future + { + "data": { + "package": { + "tasks": { + "items": [ + { + "name": "build", + "directDependencies": { + "items": [ + { + "name": "build", + "package": { + "name": "util" + } + } + ] + } + } + ] + } + } + } + } + + $ ${TURBO} query "query { package(name: \"util\") { tasks { items { name directDependents { items { name package { name } } } } } } }" | jq + WARNING query command is experimental and may change in the future + { + "data": { + "package": { + "tasks": { + "items": [ + { + "name": "build", + "directDependents": { + "items": [ + { + "name": "build", + "package": { + "name": "my-app" + } + } + ] + } + } + ] + } + } + } + }