Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mfe): support MFE config v2 #9471

Merged
merged 14 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ turborepo-errors = { path = "crates/turborepo-errors" }
turborepo-fs = { path = "crates/turborepo-fs" }
turborepo-lib = { path = "crates/turborepo-lib", default-features = false }
turborepo-lockfiles = { path = "crates/turborepo-lockfiles" }
turborepo-micro-frontend = { path = "crates/turborepo-micro-frontend" }
turborepo-microfrontends = { path = "crates/turborepo-microfrontends" }
turborepo-repository = { path = "crates/turborepo-repository" }
turborepo-ui = { path = "crates/turborepo-ui" }
turborepo-unescape = { path = "crates/turborepo-unescape" }
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ turborepo-filewatch = { path = "../turborepo-filewatch" }
turborepo-fs = { path = "../turborepo-fs" }
turborepo-graph-utils = { path = "../turborepo-graph-utils" }
turborepo-lockfiles = { workspace = true }
turborepo-micro-frontend = { workspace = true }
turborepo-microfrontends = { workspace = true }
turborepo-repository = { path = "../turborepo-repository" }
turborepo-scm = { workspace = true }
turborepo-telemetry = { path = "../turborepo-telemetry" }
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ mod framework;
mod gitignore;
pub(crate) mod globwatcher;
mod hash;
mod micro_frontends;
mod microfrontends;
mod opts;
mod package_changes_watcher;
mod panic_handler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ use std::collections::{HashMap, HashSet};
use itertools::Itertools;
use tracing::warn;
use turbopath::AbsoluteSystemPath;
use turborepo_micro_frontend::{
Config as MFEConfig, Error, DEFAULT_MICRO_FRONTENDS_CONFIG, MICRO_FRONTENDS_PACKAGES,
};
use turborepo_microfrontends::{Config as MFEConfig, Error, MICROFRONTENDS_PACKAGES};
use turborepo_repository::package_graph::{PackageGraph, PackageName};

use crate::{
Expand All @@ -15,58 +13,44 @@ use crate::{
};

#[derive(Debug, Clone)]
pub struct MicroFrontendsConfigs {
pub struct MicrofrontendsConfigs {
configs: HashMap<String, HashSet<TaskId<'static>>>,
config_filenames: HashMap<String, String>,
mfe_package: Option<&'static str>,
}

impl MicroFrontendsConfigs {
impl MicrofrontendsConfigs {
pub fn new(
repo_root: &AbsoluteSystemPath,
package_graph: &PackageGraph,
) -> Result<Option<Self>, Error> {
let mut configs = HashMap::new();
for (package_name, package_info) in package_graph.packages() {
let config_path = repo_root
.resolve(package_info.package_path())
.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG);
let Some(config) = MFEConfig::load(&config_path).or_else(|err| {
if matches!(err, turborepo_micro_frontend::Error::UnsupportedVersion(_)) {
warn!("Ignoring {config_path}: {err}");
Ok(None)
} else {
Err(err)
}
})?
else {
continue;
};
let tasks = config
.applications
.iter()
.map(|(application, options)| {
let dev_task = options.development.task.as_deref().unwrap_or("dev");
TaskId::new(application, dev_task).into_owned()
})
.collect();
configs.insert(package_name.to_string(), tasks);
let PackageGraphResult {
configs,
config_filenames,
missing_default_apps,
unsupported_version,
mfe_package,
} = PackageGraphResult::new(package_graph.packages().map(|(name, info)| {
(
name.as_str(),
MFEConfig::load_from_dir(&repo_root.resolve(info.package_path())),
)
}))?;

for (package, err) in unsupported_version {
warn!("Ignoring {package}: {err}");
}

let mfe_package = package_graph
.packages()
.map(|(pkg, _)| pkg.as_str())
.sorted()
// We use `find_map` here instead of a simple `find` so we get the &'static str
// instead of the &str tied to the lifetime of the package graph.
.find_map(|pkg| {
MICRO_FRONTENDS_PACKAGES
.iter()
.find(|static_pkg| pkg == **static_pkg)
})
.copied();
if !missing_default_apps.is_empty() {
warn!(
"Missing default applications: {}",
missing_default_apps.join(", ")
);
}

Ok((!configs.is_empty()).then_some(Self {
configs,
config_filenames,
mfe_package,
}))
}
Expand All @@ -89,6 +73,11 @@ impl MicroFrontendsConfigs {
.any(|dev_tasks| dev_tasks.contains(task_id))
}

pub fn config_filename(&self, package_name: &str) -> Option<&str> {
let filename = self.config_filenames.get(package_name)?;
Some(filename.as_str())
}

pub fn update_turbo_json(
&self,
package_name: &PackageName,
Expand Down Expand Up @@ -145,6 +134,74 @@ impl MicroFrontendsConfigs {
}
}

// Internal struct used to capture the results of checking the package graph
struct PackageGraphResult {
configs: HashMap<String, HashSet<TaskId<'static>>>,
config_filenames: HashMap<String, String>,
missing_default_apps: Vec<String>,
unsupported_version: Vec<(String, String)>,
mfe_package: Option<&'static str>,
}

impl PackageGraphResult {
fn new<'a>(
packages: impl Iterator<Item = (&'a str, Result<Option<MFEConfig>, Error>)>,
) -> Result<Self, Error> {
let mut configs = HashMap::new();
let mut config_filenames = HashMap::new();
let mut referenced_default_apps = HashSet::new();
let mut unsupported_version = Vec::new();
let mut mfe_package = None;
// We sort packages to ensure deterministic behavior
let sorted_packages = packages.sorted_by(|(a, _), (b, _)| a.cmp(b));
for (package_name, config) in sorted_packages {
if let Some(pkg) = MICROFRONTENDS_PACKAGES
.iter()
.find(|static_pkg| package_name == **static_pkg)
{
mfe_package = Some(*pkg);
}

let Some(config) = config.or_else(|err| match err {
turborepo_microfrontends::Error::UnsupportedVersion(_) => {
unsupported_version.push((package_name.to_string(), err.to_string()));
Ok(None)
}
turborepo_microfrontends::Error::ChildConfig { reference } => {
referenced_default_apps.insert(reference);
Ok(None)
}
err => Err(err),
})?
else {
continue;
};
let tasks = config
.development_tasks()
.map(|(application, options)| {
let dev_task = options.unwrap_or("dev");
TaskId::new(application, dev_task).into_owned()
})
.collect();
configs.insert(package_name.to_string(), tasks);
config_filenames.insert(package_name.to_string(), config.filename().to_owned());
}
let default_apps_found = configs.keys().cloned().collect();
let mut missing_default_apps = referenced_default_apps
.difference(&default_apps_found)
.cloned()
.collect::<Vec<_>>();
missing_default_apps.sort();
Ok(Self {
configs,
config_filenames,
missing_default_apps,
unsupported_version,
mfe_package,
})
}
}

#[derive(Debug, PartialEq, Eq)]
struct FindResult<'a> {
dev: Option<TaskId<'a>>,
Expand All @@ -153,7 +210,11 @@ struct FindResult<'a> {

#[cfg(test)]
mod test {
use serde_json::json;
use test_case::test_case;
use turborepo_microfrontends::{
MICROFRONTENDS_PACKAGE_EXTERNAL, MICROFRONTENDS_PACKAGE_INTERNAL,
};

use super::*;

Expand Down Expand Up @@ -253,13 +314,112 @@ mod test {
"mfe-config-pkg" => ["web#dev", "docs#dev"],
"mfe-web" => ["mfe-web#dev", "mfe-docs#serve"]
);
let mfe = MicroFrontendsConfigs {
let mfe = MicrofrontendsConfigs {
configs,
config_filenames: HashMap::new(),
mfe_package: None,
};
assert_eq!(
mfe.package_turbo_json_update(&test.package_name()),
test.expected()
);
}

#[test]
fn test_mfe_package_is_found() {
let result = PackageGraphResult::new(
vec![
// These should never be present in the same graph, but if for some reason they
// are, we defer to the external variant.
(MICROFRONTENDS_PACKAGE_EXTERNAL, Ok(None)),
(MICROFRONTENDS_PACKAGE_INTERNAL, Ok(None)),
]
.into_iter(),
)
.unwrap();
assert_eq!(result.mfe_package, Some(MICROFRONTENDS_PACKAGE_EXTERNAL));
}

#[test]
fn test_no_mfe_package() {
let result =
PackageGraphResult::new(vec![("foo", Ok(None)), ("bar", Ok(None))].into_iter())
.unwrap();
assert_eq!(result.mfe_package, None);
}

#[test]
fn test_unsupported_versions_ignored() {
let result = PackageGraphResult::new(
vec![("foo", Err(Error::UnsupportedVersion("bad version".into())))].into_iter(),
)
.unwrap();
assert_eq!(result.configs, HashMap::new());
}

#[test]
fn test_child_configs_with_missing_default() {
let result = PackageGraphResult::new(
vec![(
"child",
Err(Error::ChildConfig {
reference: "main".into(),
}),
)]
.into_iter(),
)
.unwrap();
assert_eq!(result.configs, HashMap::new());
assert_eq!(result.missing_default_apps, &["main".to_string()]);
}

#[test]
fn test_io_err_stops_traversal() {
let result = PackageGraphResult::new(
vec![
(
"a",
Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"something",
))),
),
(
"b",
Err(Error::ChildConfig {
reference: "main".into(),
}),
),
]
.into_iter(),
);
assert!(result.is_err());
}

#[test]
fn test_dev_task_collection() {
let config = MFEConfig::from_str(
&serde_json::to_string_pretty(&json!({
"version": "2",
"applications": {
"web": {},
"docs": {
"development": {
"task": "serve"
}
}
}
}))
.unwrap(),
"something.txt",
)
.unwrap();
let result = PackageGraphResult::new(vec![("web", Ok(Some(config)))].into_iter()).unwrap();
assert_eq!(
result.configs,
mfe_configs!(
"web" => ["web#dev", "docs#serve"]
)
)
}
}
4 changes: 2 additions & 2 deletions crates/turborepo-lib/src/run/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ use crate::{
cli::DryRunMode,
commands::CommandBase,
engine::{Engine, EngineBuilder},
micro_frontends::MicroFrontendsConfigs,
microfrontends::MicrofrontendsConfigs,
opts::Opts,
process::ProcessManager,
run::{scope, task_access::TaskAccess, task_id::TaskName, Error, Run, RunCache},
Expand Down Expand Up @@ -371,7 +371,7 @@ impl RunBuilder {
repo_telemetry.track_package_manager(pkg_dep_graph.package_manager().to_string());
repo_telemetry.track_size(pkg_dep_graph.len());
run_telemetry.track_run_type(self.opts.run_opts.dry_run.is_some());
let micro_frontend_configs = MicroFrontendsConfigs::new(&self.repo_root, &pkg_dep_graph)?;
let micro_frontend_configs = MicrofrontendsConfigs::new(&self.repo_root, &pkg_dep_graph)?;

let scm = scm.await.expect("detecting scm panicked");
let async_cache = AsyncCache::new(
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/run/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ pub enum Error {
#[error(transparent)]
Tui(#[from] tui::Error),
#[error("Error reading micro frontends configuration: {0}")]
MicroFrontends(#[from] turborepo_micro_frontend::Error),
MicroFrontends(#[from] turborepo_microfrontends::Error),
}
Loading
Loading