Skip to content

Commit 4a788d9

Browse files
feat(mfe): support MFE config v2 (#9471)
### Description Add support for MFE v2 configuration to `turbo` along with supporting multiple default configuration file names. A few highlights of this PR: - Generalize MFE config interface to support multiple schemas - Add support for MFE v2 schema - Refactor MFE configuration loading by `turbo` so it can be unit tested - Rename of any casing permutation to `Microfrontends` Reviewing each commit individually should be helpful as I did code moves to ensure that we follow `Microfrontends` as the casing scheme throughout the code. ### Testing Instructions Added unit tests for configuration loading. Manual test in a MFE v2 enabled monorepo.
1 parent 274a6cf commit 4a788d9

File tree

18 files changed

+667
-177
lines changed

18 files changed

+667
-177
lines changed

Cargo.lock

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +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" }
60+
turborepo-microfrontends = { path = "crates/turborepo-microfrontends" }
6161
turborepo-repository = { path = "crates/turborepo-repository" }
6262
turborepo-ui = { path = "crates/turborepo-ui" }
6363
turborepo-unescape = { path = "crates/turborepo-unescape" }

crates/turborepo-lib/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +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 }
136+
turborepo-microfrontends = { workspace = true }
137137
turborepo-repository = { path = "../turborepo-repository" }
138138
turborepo-scm = { workspace = true }
139139
turborepo-telemetry = { path = "../turborepo-telemetry" }

crates/turborepo-lib/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod framework;
2323
mod gitignore;
2424
pub(crate) mod globwatcher;
2525
mod hash;
26-
mod micro_frontends;
26+
mod microfrontends;
2727
mod opts;
2828
mod package_changes_watcher;
2929
mod panic_handler;

crates/turborepo-lib/src/micro_frontends.rs renamed to crates/turborepo-lib/src/microfrontends.rs

+203-43
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ use std::collections::{HashMap, HashSet};
33
use itertools::Itertools;
44
use tracing::warn;
55
use turbopath::AbsoluteSystemPath;
6-
use turborepo_micro_frontend::{
7-
Config as MFEConfig, Error, DEFAULT_MICRO_FRONTENDS_CONFIG, MICRO_FRONTENDS_PACKAGES,
8-
};
6+
use turborepo_microfrontends::{Config as MFEConfig, Error, MICROFRONTENDS_PACKAGES};
97
use turborepo_repository::package_graph::{PackageGraph, PackageName};
108

119
use crate::{
@@ -15,58 +13,44 @@ use crate::{
1513
};
1614

1715
#[derive(Debug, Clone)]
18-
pub struct MicroFrontendsConfigs {
16+
pub struct MicrofrontendsConfigs {
1917
configs: HashMap<String, HashSet<TaskId<'static>>>,
18+
config_filenames: HashMap<String, String>,
2019
mfe_package: Option<&'static str>,
2120
}
2221

23-
impl MicroFrontendsConfigs {
22+
impl MicrofrontendsConfigs {
2423
pub fn new(
2524
repo_root: &AbsoluteSystemPath,
2625
package_graph: &PackageGraph,
2726
) -> Result<Option<Self>, Error> {
28-
let mut configs = HashMap::new();
29-
for (package_name, package_info) in package_graph.packages() {
30-
let config_path = repo_root
31-
.resolve(package_info.package_path())
32-
.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG);
33-
let Some(config) = MFEConfig::load(&config_path).or_else(|err| {
34-
if matches!(err, turborepo_micro_frontend::Error::UnsupportedVersion(_)) {
35-
warn!("Ignoring {config_path}: {err}");
36-
Ok(None)
37-
} else {
38-
Err(err)
39-
}
40-
})?
41-
else {
42-
continue;
43-
};
44-
let tasks = config
45-
.applications
46-
.iter()
47-
.map(|(application, options)| {
48-
let dev_task = options.development.task.as_deref().unwrap_or("dev");
49-
TaskId::new(application, dev_task).into_owned()
50-
})
51-
.collect();
52-
configs.insert(package_name.to_string(), tasks);
27+
let PackageGraphResult {
28+
configs,
29+
config_filenames,
30+
missing_default_apps,
31+
unsupported_version,
32+
mfe_package,
33+
} = PackageGraphResult::new(package_graph.packages().map(|(name, info)| {
34+
(
35+
name.as_str(),
36+
MFEConfig::load_from_dir(&repo_root.resolve(info.package_path())),
37+
)
38+
}))?;
39+
40+
for (package, err) in unsupported_version {
41+
warn!("Ignoring {package}: {err}");
5342
}
5443

55-
let mfe_package = package_graph
56-
.packages()
57-
.map(|(pkg, _)| pkg.as_str())
58-
.sorted()
59-
// We use `find_map` here instead of a simple `find` so we get the &'static str
60-
// instead of the &str tied to the lifetime of the package graph.
61-
.find_map(|pkg| {
62-
MICRO_FRONTENDS_PACKAGES
63-
.iter()
64-
.find(|static_pkg| pkg == **static_pkg)
65-
})
66-
.copied();
44+
if !missing_default_apps.is_empty() {
45+
warn!(
46+
"Missing default applications: {}",
47+
missing_default_apps.join(", ")
48+
);
49+
}
6750

6851
Ok((!configs.is_empty()).then_some(Self {
6952
configs,
53+
config_filenames,
7054
mfe_package,
7155
}))
7256
}
@@ -89,6 +73,11 @@ impl MicroFrontendsConfigs {
8973
.any(|dev_tasks| dev_tasks.contains(task_id))
9074
}
9175

76+
pub fn config_filename(&self, package_name: &str) -> Option<&str> {
77+
let filename = self.config_filenames.get(package_name)?;
78+
Some(filename.as_str())
79+
}
80+
9281
pub fn update_turbo_json(
9382
&self,
9483
package_name: &PackageName,
@@ -145,6 +134,74 @@ impl MicroFrontendsConfigs {
145134
}
146135
}
147136

137+
// Internal struct used to capture the results of checking the package graph
138+
struct PackageGraphResult {
139+
configs: HashMap<String, HashSet<TaskId<'static>>>,
140+
config_filenames: HashMap<String, String>,
141+
missing_default_apps: Vec<String>,
142+
unsupported_version: Vec<(String, String)>,
143+
mfe_package: Option<&'static str>,
144+
}
145+
146+
impl PackageGraphResult {
147+
fn new<'a>(
148+
packages: impl Iterator<Item = (&'a str, Result<Option<MFEConfig>, Error>)>,
149+
) -> Result<Self, Error> {
150+
let mut configs = HashMap::new();
151+
let mut config_filenames = HashMap::new();
152+
let mut referenced_default_apps = HashSet::new();
153+
let mut unsupported_version = Vec::new();
154+
let mut mfe_package = None;
155+
// We sort packages to ensure deterministic behavior
156+
let sorted_packages = packages.sorted_by(|(a, _), (b, _)| a.cmp(b));
157+
for (package_name, config) in sorted_packages {
158+
if let Some(pkg) = MICROFRONTENDS_PACKAGES
159+
.iter()
160+
.find(|static_pkg| package_name == **static_pkg)
161+
{
162+
mfe_package = Some(*pkg);
163+
}
164+
165+
let Some(config) = config.or_else(|err| match err {
166+
turborepo_microfrontends::Error::UnsupportedVersion(_) => {
167+
unsupported_version.push((package_name.to_string(), err.to_string()));
168+
Ok(None)
169+
}
170+
turborepo_microfrontends::Error::ChildConfig { reference } => {
171+
referenced_default_apps.insert(reference);
172+
Ok(None)
173+
}
174+
err => Err(err),
175+
})?
176+
else {
177+
continue;
178+
};
179+
let tasks = config
180+
.development_tasks()
181+
.map(|(application, options)| {
182+
let dev_task = options.unwrap_or("dev");
183+
TaskId::new(application, dev_task).into_owned()
184+
})
185+
.collect();
186+
configs.insert(package_name.to_string(), tasks);
187+
config_filenames.insert(package_name.to_string(), config.filename().to_owned());
188+
}
189+
let default_apps_found = configs.keys().cloned().collect();
190+
let mut missing_default_apps = referenced_default_apps
191+
.difference(&default_apps_found)
192+
.cloned()
193+
.collect::<Vec<_>>();
194+
missing_default_apps.sort();
195+
Ok(Self {
196+
configs,
197+
config_filenames,
198+
missing_default_apps,
199+
unsupported_version,
200+
mfe_package,
201+
})
202+
}
203+
}
204+
148205
#[derive(Debug, PartialEq, Eq)]
149206
struct FindResult<'a> {
150207
dev: Option<TaskId<'a>>,
@@ -153,7 +210,11 @@ struct FindResult<'a> {
153210

154211
#[cfg(test)]
155212
mod test {
213+
use serde_json::json;
156214
use test_case::test_case;
215+
use turborepo_microfrontends::{
216+
MICROFRONTENDS_PACKAGE_EXTERNAL, MICROFRONTENDS_PACKAGE_INTERNAL,
217+
};
157218

158219
use super::*;
159220

@@ -253,13 +314,112 @@ mod test {
253314
"mfe-config-pkg" => ["web#dev", "docs#dev"],
254315
"mfe-web" => ["mfe-web#dev", "mfe-docs#serve"]
255316
);
256-
let mfe = MicroFrontendsConfigs {
317+
let mfe = MicrofrontendsConfigs {
257318
configs,
319+
config_filenames: HashMap::new(),
258320
mfe_package: None,
259321
};
260322
assert_eq!(
261323
mfe.package_turbo_json_update(&test.package_name()),
262324
test.expected()
263325
);
264326
}
327+
328+
#[test]
329+
fn test_mfe_package_is_found() {
330+
let result = PackageGraphResult::new(
331+
vec![
332+
// These should never be present in the same graph, but if for some reason they
333+
// are, we defer to the external variant.
334+
(MICROFRONTENDS_PACKAGE_EXTERNAL, Ok(None)),
335+
(MICROFRONTENDS_PACKAGE_INTERNAL, Ok(None)),
336+
]
337+
.into_iter(),
338+
)
339+
.unwrap();
340+
assert_eq!(result.mfe_package, Some(MICROFRONTENDS_PACKAGE_EXTERNAL));
341+
}
342+
343+
#[test]
344+
fn test_no_mfe_package() {
345+
let result =
346+
PackageGraphResult::new(vec![("foo", Ok(None)), ("bar", Ok(None))].into_iter())
347+
.unwrap();
348+
assert_eq!(result.mfe_package, None);
349+
}
350+
351+
#[test]
352+
fn test_unsupported_versions_ignored() {
353+
let result = PackageGraphResult::new(
354+
vec![("foo", Err(Error::UnsupportedVersion("bad version".into())))].into_iter(),
355+
)
356+
.unwrap();
357+
assert_eq!(result.configs, HashMap::new());
358+
}
359+
360+
#[test]
361+
fn test_child_configs_with_missing_default() {
362+
let result = PackageGraphResult::new(
363+
vec![(
364+
"child",
365+
Err(Error::ChildConfig {
366+
reference: "main".into(),
367+
}),
368+
)]
369+
.into_iter(),
370+
)
371+
.unwrap();
372+
assert_eq!(result.configs, HashMap::new());
373+
assert_eq!(result.missing_default_apps, &["main".to_string()]);
374+
}
375+
376+
#[test]
377+
fn test_io_err_stops_traversal() {
378+
let result = PackageGraphResult::new(
379+
vec![
380+
(
381+
"a",
382+
Err(Error::Io(std::io::Error::new(
383+
std::io::ErrorKind::Other,
384+
"something",
385+
))),
386+
),
387+
(
388+
"b",
389+
Err(Error::ChildConfig {
390+
reference: "main".into(),
391+
}),
392+
),
393+
]
394+
.into_iter(),
395+
);
396+
assert!(result.is_err());
397+
}
398+
399+
#[test]
400+
fn test_dev_task_collection() {
401+
let config = MFEConfig::from_str(
402+
&serde_json::to_string_pretty(&json!({
403+
"version": "2",
404+
"applications": {
405+
"web": {},
406+
"docs": {
407+
"development": {
408+
"task": "serve"
409+
}
410+
}
411+
}
412+
}))
413+
.unwrap(),
414+
"something.txt",
415+
)
416+
.unwrap();
417+
let result = PackageGraphResult::new(vec![("web", Ok(Some(config)))].into_iter()).unwrap();
418+
assert_eq!(
419+
result.configs,
420+
mfe_configs!(
421+
"web" => ["web#dev", "docs#serve"]
422+
)
423+
)
424+
}
265425
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::{
4141
cli::DryRunMode,
4242
commands::CommandBase,
4343
engine::{Engine, EngineBuilder},
44-
micro_frontends::MicroFrontendsConfigs,
44+
microfrontends::MicrofrontendsConfigs,
4545
opts::Opts,
4646
process::ProcessManager,
4747
run::{scope, task_access::TaskAccess, task_id::TaskName, Error, Run, RunCache},
@@ -371,7 +371,7 @@ impl RunBuilder {
371371
repo_telemetry.track_package_manager(pkg_dep_graph.package_manager().name().to_string());
372372
repo_telemetry.track_size(pkg_dep_graph.len());
373373
run_telemetry.track_run_type(self.opts.run_opts.dry_run.is_some());
374-
let micro_frontend_configs = MicroFrontendsConfigs::new(&self.repo_root, &pkg_dep_graph)?;
374+
let micro_frontend_configs = MicrofrontendsConfigs::new(&self.repo_root, &pkg_dep_graph)?;
375375

376376
let scm = scm.await.expect("detecting scm panicked");
377377
let async_cache = AsyncCache::new(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,5 @@ pub enum Error {
6161
#[error(transparent)]
6262
Tui(#[from] tui::Error),
6363
#[error("Error reading micro frontends configuration: {0}")]
64-
MicroFrontends(#[from] turborepo_micro_frontend::Error),
64+
MicroFrontends(#[from] turborepo_microfrontends::Error),
6565
}

0 commit comments

Comments
 (0)