From c7d492f9c181d466b0080992fc5580aed4e9f41f Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 28 Oct 2024 09:47:37 -0400 Subject: [PATCH] feat(mfe): inject local proxy task --- crates/turborepo-lib/src/engine/builder.rs | 1 + crates/turborepo-lib/src/run/builder.rs | 37 +++++++- crates/turborepo-lib/src/run/mod.rs | 1 + .../src/task_graph/visitor/command.rs | 83 ++++++++++++++++- .../src/task_graph/visitor/exec.rs | 12 ++- .../src/task_graph/visitor/mod.rs | 5 +- crates/turborepo-lib/src/turbo_json/loader.rs | 90 +++++++++++++++++-- 7 files changed, 213 insertions(+), 16 deletions(-) diff --git a/crates/turborepo-lib/src/engine/builder.rs b/crates/turborepo-lib/src/engine/builder.rs index acd2dbb4efa3b..2e46fe79b0e2f 100644 --- a/crates/turborepo-lib/src/engine/builder.rs +++ b/crates/turborepo-lib/src/engine/builder.rs @@ -234,6 +234,7 @@ impl<'a> EngineBuilder<'a> { let task_id = task .task_id() .unwrap_or_else(|| TaskId::new(workspace.as_ref(), task.task())); + eprintln!("{task_id:?}"); if Self::has_task_definition(&mut turbo_json_loader, workspace, task, &task_id)? { missing_tasks.remove(task.as_inner()); diff --git a/crates/turborepo-lib/src/run/builder.rs b/crates/turborepo-lib/src/run/builder.rs index 9c13b69f5d9ef..ceb2ae6fb4190 100644 --- a/crates/turborepo-lib/src/run/builder.rs +++ b/crates/turborepo-lib/src/run/builder.rs @@ -37,6 +37,7 @@ use { }, }; +use super::task_id::TaskId; use crate::{ cli::DryRunMode, commands::CommandBase, @@ -403,6 +404,14 @@ impl RunBuilder { self.repo_root.clone(), pkg_dep_graph.packages(), ) + } else if !micro_frontend_configs.is_empty() { + eprintln!("hit this"); + TurboJsonLoader::workspace_with_microfrontends( + self.repo_root.clone(), + self.root_turbo_json_path.clone(), + pkg_dep_graph.packages(), + micro_frontend_configs.clone(), + ) } else { TurboJsonLoader::workspace( self.repo_root.clone(), @@ -429,6 +438,7 @@ impl RunBuilder { &root_turbo_json, filtered_pkgs.keys(), turbo_json_loader.clone(), + µ_frontend_configs, )?; if self.opts.run_opts.parallel { @@ -438,6 +448,7 @@ impl RunBuilder { &root_turbo_json, filtered_pkgs.keys(), turbo_json_loader, + µ_frontend_configs, )?; } @@ -488,7 +499,28 @@ impl RunBuilder { root_turbo_json: &TurboJson, filtered_pkgs: impl Iterator, turbo_json_loader: TurboJsonLoader, + micro_frontends_configs: &HashMap>>, ) -> Result { + let mut tasks = self + .opts + .run_opts + .tasks + .iter() + .map(|task| { + // TODO: Pull span info from command + Spanned::new(TaskName::from(task.as_str()).into_owned()) + }) + .collect::>(); + if !micro_frontends_configs.is_empty() { + tasks.push(Spanned::new(TaskName::from("proxy").into_owned())); + } + /* + tasks.extend( + micro_frontends_configs + .keys() + .map(|pkg| Spanned::new(TaskId::new(pkg, "proxy").as_task_name().into_owned())), + ); + */ let mut builder = EngineBuilder::new( &self.repo_root, pkg_dep_graph, @@ -498,10 +530,7 @@ impl RunBuilder { .with_root_tasks(root_turbo_json.tasks.keys().cloned()) .with_tasks_only(self.opts.run_opts.only) .with_workspaces(filtered_pkgs.cloned().collect()) - .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()) - })); + .with_tasks(tasks); if self.add_all_tasks { builder = builder.add_all_tasks(); diff --git a/crates/turborepo-lib/src/run/mod.rs b/crates/turborepo-lib/src/run/mod.rs index 6eaefb525def6..4d873cba06678 100644 --- a/crates/turborepo-lib/src/run/mod.rs +++ b/crates/turborepo-lib/src/run/mod.rs @@ -462,6 +462,7 @@ impl Run { global_env, ui_sender, is_watch, + &self.micro_frontend_configs, ) .await; diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index 80014df52c110..05bb16245ce25 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -1,11 +1,14 @@ -use std::path::PathBuf; +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; use turbopath::AbsoluteSystemPath; use turborepo_env::EnvironmentVariableMap; use turborepo_repository::package_graph::{PackageGraph, PackageInfo, PackageName}; use super::Error; -use crate::{opts::TaskArgs, process::Command, run::task_id::TaskId}; +use crate::{engine::Engine, opts::TaskArgs, process::Command, run::task_id::TaskId}; pub trait CommandProvider { fn command( @@ -126,6 +129,82 @@ impl<'a> CommandProvider for PackageGraphCommandProvider<'a> { } } +#[derive(Debug)] +pub struct MicroFrontendProxyProvider<'a> { + repo_root: &'a AbsoluteSystemPath, + package_graph: &'a PackageGraph, + tasks_in_graph: HashSet>, + mfe_configs: &'a HashMap>>, +} + +impl<'a> MicroFrontendProxyProvider<'a> { + pub fn new( + repo_root: &'a AbsoluteSystemPath, + package_graph: &'a PackageGraph, + engine: &Engine, + micro_frontends_configs: &'a HashMap>>, + ) -> Self { + let tasks_in_graph = engine + .tasks() + .filter_map(|task| match task { + crate::engine::TaskNode::Task(task_id) => Some(task_id), + crate::engine::TaskNode::Root => None, + }) + .cloned() + .collect(); + Self { + repo_root, + package_graph, + tasks_in_graph, + mfe_configs: micro_frontends_configs, + } + } + + fn dev_tasks(&self, task_id: &TaskId) -> Option<&HashSet>> { + (task_id.task() == "proxy") + .then(|| self.mfe_configs.get(task_id.package())) + .flatten() + } + + fn package_info(&self, task_id: &TaskId) -> Result<&PackageInfo, Error> { + self.package_graph + .package_info(&PackageName::from(task_id.package())) + .ok_or_else(|| Error::MissingPackage { + package_name: task_id.package().into(), + task_id: task_id.clone().into_owned(), + }) + } +} + +impl<'a> CommandProvider for MicroFrontendProxyProvider<'a> { + fn command( + &self, + task_id: &TaskId, + _environment: EnvironmentVariableMap, + ) -> Result, Error> { + let Some(dev_tasks) = self.dev_tasks(task_id) else { + return Ok(None); + }; + let package_info = self.package_info(task_id)?; + let local_apps = dev_tasks + .iter() + .filter(|task| self.tasks_in_graph.contains(task)) + .map(|task| task.package()); + let package_dir = self.repo_root.resolve(package_info.package_path()); + let mfe_path = package_dir.join_component("micro-frontends.jsonc"); + let mut args = vec!["proxy", mfe_path.as_str(), "--names"]; + args.extend(local_apps); + + // TODO: leverage package manager to find the local proxy + let program = package_dir.join_components(&["node_modules", ".bin", "micro-frontends"]); + let mut cmd = Command::new(program.as_std_path()); + cmd.current_dir(package_dir).args(args); + eprintln!("running proxy {}", cmd.label()); + + Ok(Some(cmd)) + } +} + #[cfg(test)] mod test { use std::ffi::OsStr; diff --git a/crates/turborepo-lib/src/task_graph/visitor/exec.rs b/crates/turborepo-lib/src/task_graph/visitor/exec.rs index 8e9c137a1e152..9c961cd662219 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/exec.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/exec.rs @@ -13,7 +13,7 @@ use turborepo_telemetry::events::{task::PackageTaskEventBuilder, TrackedErrors}; use turborepo_ui::{ColorConfig, OutputWriter}; use super::{ - command::{CommandFactory, PackageGraphCommandProvider}, + command::{CommandFactory, MicroFrontendProxyProvider, PackageGraphCommandProvider}, error::{TaskError, TaskErrorCause, TaskWarning}, output::TaskCacheOutput, TaskOutput, Visitor, @@ -51,7 +51,15 @@ impl<'a> ExecContextFactory<'a> { &visitor.package_graph, visitor.run_opts.task_args(), ); - let command_factory = CommandFactory::new().add_provider(pkg_graph_provider); + let mfe_proxy_provider = MicroFrontendProxyProvider::new( + visitor.repo_root, + &visitor.package_graph, + engine, + visitor.micro_frontends_configs, + ); + let command_factory = CommandFactory::new() + .add_provider(mfe_proxy_provider) + .add_provider(pkg_graph_provider); Ok(Self { visitor, errors, diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index 0d8db7fa3cc55..37e50a6ee0a15 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -5,7 +5,7 @@ mod output; use std::{ borrow::Cow, - collections::HashSet, + collections::{HashMap, HashSet}, io::Write, sync::{Arc, Mutex, OnceLock}, }; @@ -65,6 +65,7 @@ pub struct Visitor<'a> { is_watch: bool, ui_sender: Option, warnings: Arc>>, + micro_frontends_configs: &'a HashMap>>, } #[derive(Debug, thiserror::Error, Diagnostic)] @@ -119,6 +120,7 @@ impl<'a> Visitor<'a> { global_env: EnvironmentVariableMap, ui_sender: Option, is_watch: bool, + micro_frontends_configs: &'a HashMap>>, ) -> Self { let task_hasher = TaskHasher::new( package_inputs_hashes, @@ -155,6 +157,7 @@ impl<'a> Visitor<'a> { ui_sender, is_watch, warnings: Default::default(), + micro_frontends_configs, } } diff --git a/crates/turborepo-lib/src/turbo_json/loader.rs b/crates/turborepo-lib/src/turbo_json/loader.rs index b01fb054babfc..dbd3d116f36b9 100644 --- a/crates/turborepo-lib/src/turbo_json/loader.rs +++ b/crates/turborepo-lib/src/turbo_json/loader.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use tracing::debug; use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf}; @@ -12,7 +12,10 @@ use super::{Pipeline, RawTaskDefinition, TurboJson, CONFIG_FILE}; use crate::{ cli::EnvMode, config::Error, - run::{task_access::TASK_ACCESS_CONFIG_PATH, task_id::TaskName}, + run::{ + task_access::TASK_ACCESS_CONFIG_PATH, + task_id::{TaskId, TaskName}, + }, }; /// Structure for loading TurboJson structures. @@ -34,6 +37,7 @@ enum Strategy { Workspace { // Map of package names to their package specific turbo.json packages: HashMap, + micro_frontends_configs: Option>>>, }, WorkspaceNoTurboJson { // Map of package names to their scripts @@ -57,7 +61,28 @@ impl TurboJsonLoader { Self { repo_root, cache: HashMap::new(), - strategy: Strategy::Workspace { packages }, + strategy: Strategy::Workspace { + packages, + micro_frontends_configs: None, + }, + } + } + + /// Create a loader that will load turbo.json files throughout the workspace + pub fn workspace_with_microfrontends<'a>( + repo_root: AbsoluteSystemPathBuf, + root_turbo_json_path: AbsoluteSystemPathBuf, + packages: impl Iterator, + micro_frontends_configs: HashMap>>, + ) -> Self { + let packages = package_turbo_jsons(&repo_root, root_turbo_json_path, packages); + Self { + repo_root, + cache: HashMap::new(), + strategy: Strategy::Workspace { + packages, + micro_frontends_configs: Some(micro_frontends_configs), + }, } } @@ -147,9 +172,24 @@ impl TurboJsonLoader { load_from_root_package_json(&self.repo_root, root_turbo_json, package_json) } } - Strategy::Workspace { packages } => { + Strategy::Workspace { + packages, + micro_frontends_configs, + } => { + eprint!("resolving {package:?}"); + eprintln!("{micro_frontends_configs:?}"); let path = packages.get(package).ok_or_else(|| Error::NoTurboJSON)?; - load_from_file(&self.repo_root, path) + let should_inject_proxy_task = + micro_frontends_configs.as_ref().map_or(false, |configs| { + let pkg = package.to_string(); + eprintln!("{pkg}"); + configs.contains_key(&pkg) + }); + if should_inject_proxy_task { + load_from_file_with_proxy(&self.repo_root, path) + } else { + load_from_file(&self.repo_root, path) + } } Strategy::WorkspaceNoTurboJson { packages } => { let script_names = packages.get(package).ok_or(Error::NoTurboJSON)?; @@ -230,6 +270,35 @@ fn load_from_file( } } +fn load_from_file_with_proxy( + repo_root: &AbsoluteSystemPath, + turbo_json_path: &AbsoluteSystemPath, +) -> Result { + let mut turbo = match TurboJson::read(repo_root, turbo_json_path) { + // If the file didn't exist, use a default so we can inject the proxy task + Err(Error::Io(_)) => Ok(TurboJson::default()), + // There was an error, and we don't have any chance of recovering + // because we aren't synthesizing anything + Err(e) => Err(e), + // We're not synthesizing anything and there was no error, we're done + Ok(turbo) => Ok(turbo), + }?; + + if turbo.extends.is_empty() { + turbo.extends = Spanned::new(vec!["//".into()]); + } + + turbo.tasks.insert( + TaskName::from("proxy"), + Spanned::new(RawTaskDefinition { + cache: Some(Spanned::new(false)), + ..Default::default() + }), + ); + + Ok(turbo) +} + fn load_from_root_package_json( repo_root: &AbsoluteSystemPath, turbo_json_path: &AbsoluteSystemPath, @@ -385,6 +454,7 @@ mod test { packages: vec![(PackageName::Root, root_turbo_json)] .into_iter() .collect(), + micro_frontends_configs: None, }, }; @@ -581,7 +651,10 @@ mod test { let mut loader = TurboJsonLoader { repo_root: repo_root.to_owned(), cache: HashMap::new(), - strategy: Strategy::Workspace { packages }, + strategy: Strategy::Workspace { + packages, + micro_frontends_configs: None, + }, }; let result = loader.load(&PackageName::from("a")); assert!( @@ -610,7 +683,10 @@ mod test { let mut loader = TurboJsonLoader { repo_root: repo_root.to_owned(), cache: HashMap::new(), - strategy: Strategy::Workspace { packages }, + strategy: Strategy::Workspace { + packages, + micro_frontends_configs: None, + }, }; a_turbo_json .create_with_contents(r#"{"tasks": {"build": {}}}"#)