Skip to content

Commit e186685

Browse files
feat(mfe): inject local proxy (#9356)
### Description This PR adds all support necessary for starting a MFE local proxy: - Adds new `CommandProvider` to allow for multiple strategies for constructing a command depending on the type - Adds parsing logic for `micro-frontends.jsonc` - Parses all `micro-frontends.jsonc` found in the package graph* - Adding a task definition for the generated `proxy` task if MFE configs are found - Explicitly include the `proxy` tasks in engine building step if a MFE dev task is found in the product of the package `--filter` and the tasks specified in `turbo run` - Adds a `CommandProvider` that construct a command that invokes the MFE local proxy ⚠️ This currently only support MFE configs that are located in shared packages and doesn't respect a loose MFE configs. Follow up work: - [ ] Handle configurable MFE config location ### Testing Instructions In a repository with MFE, run a dev task for one of the configured MFE. This should result in a `#proxy` task being injected for that MFE and correctly configured to route traffic to the dev tasks or a remote host.
1 parent 06fef5f commit e186685

File tree

20 files changed

+741
-26
lines changed

20 files changed

+741
-26
lines changed

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ turborepo-errors = { path = "crates/turborepo-errors" }
5757
turborepo-fs = { path = "crates/turborepo-fs" }
5858
turborepo-lib = { path = "crates/turborepo-lib", default-features = false }
5959
turborepo-lockfiles = { path = "crates/turborepo-lockfiles" }
60+
turborepo-micro-frontend = { path = "crates/turborepo-micro-frontend" }
6061
turborepo-repository = { path = "crates/turborepo-repository" }
6162
turborepo-ui = { path = "crates/turborepo-ui" }
6263
turborepo-unescape = { path = "crates/turborepo-unescape" }

crates/turborepo-lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ turborepo-filewatch = { path = "../turborepo-filewatch" }
133133
turborepo-fs = { path = "../turborepo-fs" }
134134
turborepo-graph-utils = { path = "../turborepo-graph-utils" }
135135
turborepo-lockfiles = { workspace = true }
136+
turborepo-micro-frontend = { workspace = true }
136137
turborepo-repository = { path = "../turborepo-repository" }
137138
turborepo-scm = { workspace = true }
138139
turborepo-telemetry = { path = "../turborepo-telemetry" }

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,10 @@ impl Engine<Built> {
404404
.filter_map(|task| {
405405
let pkg_name = PackageName::from(task.package());
406406
let json = pkg_graph.package_json(&pkg_name)?;
407-
json.command(task.task()).map(|_| task.to_string())
407+
// TODO: delegate to command factory to filter down tasks to those that will
408+
// have a runnable command.
409+
(task.task() == "proxy" || json.command(task.task()).is_some())
410+
.then(|| task.to_string())
408411
})
409412
.collect()
410413
}

crates/turborepo-lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod framework;
2323
mod gitignore;
2424
pub(crate) mod globwatcher;
2525
mod hash;
26+
mod micro_frontends;
2627
mod opts;
2728
mod package_changes_watcher;
2829
mod panic_handler;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::collections::{HashMap, HashSet};
2+
3+
use turbopath::AbsoluteSystemPath;
4+
use turborepo_micro_frontend::{Config as MFEConfig, Error, DEFAULT_MICRO_FRONTENDS_CONFIG};
5+
use turborepo_repository::package_graph::PackageGraph;
6+
7+
use crate::run::task_id::TaskId;
8+
9+
#[derive(Debug, Clone)]
10+
pub struct MicroFrontendsConfigs {
11+
configs: HashMap<String, HashSet<TaskId<'static>>>,
12+
}
13+
14+
impl MicroFrontendsConfigs {
15+
pub fn new(
16+
repo_root: &AbsoluteSystemPath,
17+
package_graph: &PackageGraph,
18+
) -> Result<Option<Self>, Error> {
19+
let mut configs = HashMap::new();
20+
for (package_name, package_info) in package_graph.packages() {
21+
let config_path = repo_root
22+
.resolve(package_info.package_path())
23+
.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG);
24+
let Some(config) = MFEConfig::load(&config_path)? else {
25+
continue;
26+
};
27+
let tasks = config
28+
.applications
29+
.iter()
30+
.map(|(application, options)| {
31+
let dev_task = options.development.task.as_deref().unwrap_or("dev");
32+
TaskId::new(application, dev_task).into_owned()
33+
})
34+
.collect();
35+
configs.insert(package_name.to_string(), tasks);
36+
}
37+
38+
Ok((!configs.is_empty()).then_some(Self { configs }))
39+
}
40+
41+
pub fn contains_package(&self, package_name: &str) -> bool {
42+
self.configs.contains_key(package_name)
43+
}
44+
45+
pub fn configs(&self) -> impl Iterator<Item = (&String, &HashSet<TaskId<'static>>)> {
46+
self.configs.iter()
47+
}
48+
49+
pub fn get(&self, package_name: &str) -> Option<&HashSet<TaskId<'static>>> {
50+
self.configs.get(package_name)
51+
}
52+
}

crates/turborepo-lib/src/process/command.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ impl Command {
101101
pub fn will_open_stdin(&self) -> bool {
102102
self.open_stdin
103103
}
104+
105+
/// Program for the command
106+
pub fn program(&self) -> &OsStr {
107+
&self.program
108+
}
104109
}
105110

106111
impl From<Command> for tokio::process::Command {

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

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
};
77

88
use chrono::Local;
9+
use itertools::Itertools;
910
use tracing::debug;
1011
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf};
1112
use turborepo_analytics::{start_analytics, AnalyticsHandle, AnalyticsSender};
@@ -37,10 +38,12 @@ use {
3738
},
3839
};
3940

41+
use super::task_id::TaskId;
4042
use crate::{
4143
cli::DryRunMode,
4244
commands::CommandBase,
4345
engine::{Engine, EngineBuilder},
46+
micro_frontends::MicroFrontendsConfigs,
4447
opts::Opts,
4548
process::ProcessManager,
4649
run::{scope, task_access::TaskAccess, task_id::TaskName, Error, Run, RunCache},
@@ -370,6 +373,7 @@ impl RunBuilder {
370373
repo_telemetry.track_package_manager(pkg_dep_graph.package_manager().to_string());
371374
repo_telemetry.track_size(pkg_dep_graph.len());
372375
run_telemetry.track_run_type(self.opts.run_opts.dry_run.is_some());
376+
let micro_frontend_configs = MicroFrontendsConfigs::new(&self.repo_root, &pkg_dep_graph)?;
373377

374378
let scm = scm.await.expect("detecting scm panicked");
375379
let async_cache = AsyncCache::new(
@@ -401,6 +405,13 @@ impl RunBuilder {
401405
self.repo_root.clone(),
402406
pkg_dep_graph.packages(),
403407
)
408+
} else if let Some(micro_frontends) = &micro_frontend_configs {
409+
TurboJsonLoader::workspace_with_microfrontends(
410+
self.repo_root.clone(),
411+
self.root_turbo_json_path.clone(),
412+
pkg_dep_graph.packages(),
413+
micro_frontends.clone(),
414+
)
404415
} else {
405416
TurboJsonLoader::workspace(
406417
self.repo_root.clone(),
@@ -427,6 +438,7 @@ impl RunBuilder {
427438
&root_turbo_json,
428439
filtered_pkgs.keys(),
429440
turbo_json_loader.clone(),
441+
micro_frontend_configs.as_ref(),
430442
)?;
431443

432444
if self.opts.run_opts.parallel {
@@ -436,6 +448,7 @@ impl RunBuilder {
436448
&root_turbo_json,
437449
filtered_pkgs.keys(),
438450
turbo_json_loader,
451+
micro_frontend_configs.as_ref(),
439452
)?;
440453
}
441454

@@ -476,6 +489,7 @@ impl RunBuilder {
476489
signal_handler: signal_handler.clone(),
477490
daemon,
478491
should_print_prelude,
492+
micro_frontend_configs,
479493
})
480494
}
481495

@@ -485,7 +499,52 @@ impl RunBuilder {
485499
root_turbo_json: &TurboJson,
486500
filtered_pkgs: impl Iterator<Item = &'a PackageName>,
487501
turbo_json_loader: TurboJsonLoader,
502+
micro_frontends_configs: Option<&MicroFrontendsConfigs>,
488503
) -> Result<Engine, Error> {
504+
let mut tasks = self
505+
.opts
506+
.run_opts
507+
.tasks
508+
.iter()
509+
.map(|task| {
510+
// TODO: Pull span info from command
511+
Spanned::new(TaskName::from(task.as_str()).into_owned())
512+
})
513+
.collect::<Vec<_>>();
514+
let mut workspace_packages = filtered_pkgs.cloned().collect::<Vec<_>>();
515+
if let Some(micro_frontends_configs) = micro_frontends_configs {
516+
// TODO: this logic is very similar to what happens inside engine builder and
517+
// could probably be exposed
518+
let tasks_from_filter = workspace_packages
519+
.iter()
520+
.map(|package| package.as_str())
521+
.cartesian_product(tasks.iter())
522+
.map(|(package, task)| {
523+
task.task_id().map_or_else(
524+
|| TaskId::new(package, task.task()).into_owned(),
525+
|task_id| task_id.into_owned(),
526+
)
527+
})
528+
.collect::<HashSet<_>>();
529+
// we need to add the MFE config packages into the scope here to make sure the
530+
// proxy gets run
531+
// TODO(olszewski): We are relying on the fact that persistent tasks must be
532+
// entry points to the task graph so we can get away with only
533+
// checking the entrypoint tasks.
534+
for (mfe_config_package, dev_tasks) in micro_frontends_configs.configs() {
535+
for dev_task in dev_tasks {
536+
if tasks_from_filter.contains(dev_task) {
537+
workspace_packages.push(PackageName::from(mfe_config_package.as_str()));
538+
tasks.push(Spanned::new(
539+
TaskId::new(mfe_config_package, "proxy")
540+
.as_task_name()
541+
.into_owned(),
542+
));
543+
break;
544+
}
545+
}
546+
}
547+
}
489548
let mut builder = EngineBuilder::new(
490549
&self.repo_root,
491550
pkg_dep_graph,
@@ -494,11 +553,8 @@ impl RunBuilder {
494553
)
495554
.with_root_tasks(root_turbo_json.tasks.keys().cloned())
496555
.with_tasks_only(self.opts.run_opts.only)
497-
.with_workspaces(filtered_pkgs.cloned().collect())
498-
.with_tasks(self.opts.run_opts.tasks.iter().map(|task| {
499-
// TODO: Pull span info from command
500-
Spanned::new(TaskName::from(task.as_str()).into_owned())
501-
}));
556+
.with_workspaces(workspace_packages)
557+
.with_tasks(tasks);
502558

503559
if self.add_all_tasks {
504560
builder = builder.add_all_tasks();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,6 @@ pub enum Error {
6060
UI(#[from] turborepo_ui::Error),
6161
#[error(transparent)]
6262
Tui(#[from] tui::Error),
63+
#[error("Error reading micro frontends configuration: {0}")]
64+
MicroFrontends(#[from] turborepo_micro_frontend::Error),
6365
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub use crate::run::error::Error;
4141
use crate::{
4242
cli::EnvMode,
4343
engine::Engine,
44+
micro_frontends::MicroFrontendsConfigs,
4445
opts::Opts,
4546
process::ProcessManager,
4647
run::{global_hash::get_global_hash_inputs, summary::RunTracker, task_access::TaskAccess},
@@ -73,6 +74,7 @@ pub struct Run {
7374
task_access: TaskAccess,
7475
daemon: Option<DaemonClient<DaemonConnector>>,
7576
should_print_prelude: bool,
77+
micro_frontend_configs: Option<MicroFrontendsConfigs>,
7678
}
7779

7880
type UIResult<T> = Result<Option<(T, JoinHandle<Result<(), turborepo_ui::Error>>)>, Error>;
@@ -460,6 +462,7 @@ impl Run {
460462
global_env,
461463
ui_sender,
462464
is_watch,
465+
self.micro_frontend_configs.as_ref(),
463466
)
464467
.await;
465468

0 commit comments

Comments
 (0)