From e69350148a85ede47bb3d10c80a28113749e21a2 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 15 Nov 2024 14:26:46 -0500 Subject: [PATCH 01/14] chore(mfe): hide inner fields for mfe config --- crates/turborepo-lib/src/micro_frontends.rs | 3 +-- crates/turborepo-micro-frontend/src/lib.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index b4ef6a8212437..1f78bac493fe2 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -42,8 +42,7 @@ impl MicroFrontendsConfigs { continue; }; let tasks = config - .applications - .iter() + .applications() .map(|(application, options)| { let dev_task = options.development.task.as_deref().unwrap_or("dev"); TaskId::new(application, dev_task).into_owned() diff --git a/crates/turborepo-micro-frontend/src/lib.rs b/crates/turborepo-micro-frontend/src/lib.rs index 50b7c67fbf128..1392b9c54da14 100644 --- a/crates/turborepo-micro-frontend/src/lib.rs +++ b/crates/turborepo-micro-frontend/src/lib.rs @@ -26,8 +26,8 @@ pub const SUPPORTED_VERSIONS: &[&str] = ["1"].as_slice(); /// proxy server for microfrontends #[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] pub struct Config { - pub version: String, - pub applications: BTreeMap, + version: String, + applications: BTreeMap, } impl Config { @@ -71,6 +71,10 @@ impl Config { Err(Error::biome_error(errs)) } } + + pub fn applications(&self) -> impl Iterator { + self.applications.iter() + } } #[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] From 17016f3ee6ae871bb0eb5f8870bf5f8789986400 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 15 Nov 2024 14:43:33 -0500 Subject: [PATCH 02/14] chore(mfe): prefactor for multiple versions --- .../turborepo-micro-frontend/src/configv1.rs | 35 ++++++++++++++++ crates/turborepo-micro-frontend/src/lib.rs | 40 +++++++++---------- 2 files changed, 53 insertions(+), 22 deletions(-) create mode 100644 crates/turborepo-micro-frontend/src/configv1.rs diff --git a/crates/turborepo-micro-frontend/src/configv1.rs b/crates/turborepo-micro-frontend/src/configv1.rs new file mode 100644 index 0000000000000..20f788b9dc92e --- /dev/null +++ b/crates/turborepo-micro-frontend/src/configv1.rs @@ -0,0 +1,35 @@ +use std::collections::BTreeMap; + +use biome_deserialize_macros::Deserializable; +use biome_json_parser::JsonParserOptions; +use serde::Serialize; + +use crate::{Application, Error}; + +/// The minimal amount of information Turborepo needs to correctly start a local +/// proxy server for microfrontends +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +pub struct ConfigV1 { + pub version: String, + pub applications: BTreeMap, +} + +impl ConfigV1 { + pub fn from_str(input: &str, source: &str) -> Result { + let (config, errs) = biome_deserialize::json::deserialize_from_json_str( + input, + JsonParserOptions::default().with_allow_comments(), + source, + ) + .consume(); + if let Some(config) = config { + Ok(config) + } else { + Err(Error::biome_error(errs)) + } + } + + pub fn applications(&self) -> impl Iterator { + self.applications.iter() + } +} diff --git a/crates/turborepo-micro-frontend/src/lib.rs b/crates/turborepo-micro-frontend/src/lib.rs index 1392b9c54da14..c605abf0841a2 100644 --- a/crates/turborepo-micro-frontend/src/lib.rs +++ b/crates/turborepo-micro-frontend/src/lib.rs @@ -1,10 +1,12 @@ #![deny(clippy::all)] +mod configv1; mod error; use std::collections::BTreeMap; use biome_deserialize_macros::Deserializable; use biome_json_parser::JsonParserOptions; +use configv1::ConfigV1; pub use error::Error; use serde::Serialize; use turbopath::AbsoluteSystemPath; @@ -24,10 +26,9 @@ pub const SUPPORTED_VERSIONS: &[&str] = ["1"].as_slice(); /// The minimal amount of information Turborepo needs to correctly start a local /// proxy server for microfrontends -#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] -pub struct Config { - version: String, - applications: BTreeMap, +#[derive(Debug, PartialEq, Eq)] +pub enum Config { + V1(ConfigV1), } impl Config { @@ -52,28 +53,23 @@ impl Config { source, ) .consume(); - // If parsing just the version fails, fallback to full schema to provide better - // error message - if let Some(VersionOnly { version }) = version_only { - if !SUPPORTED_VERSIONS.contains(&version.as_str()) { - return Err(Error::UnsupportedVersion(version)); - } - } - let (config, errs) = biome_deserialize::json::deserialize_from_json_str( - input, - JsonParserOptions::default().with_allow_comments(), - source, - ) - .consume(); - if let Some(config) = config { - Ok(config) - } else { - Err(Error::biome_error(errs)) + + let version = match version_only { + Some(VersionOnly { version }) => version, + // Default to version 1 if no version found + None => "1".to_string(), + }; + + match version.as_str() { + "1" => ConfigV1::from_str(input, source).map(Config::V1), + version => Err(Error::UnsupportedVersion(version.to_string())), } } pub fn applications(&self) -> impl Iterator { - self.applications.iter() + match self { + Config::V1(config_v1) => config_v1.applications(), + } } } From 158d55d7e04c140607e30d82aa7f1f8cd7966899 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 18 Nov 2024 17:05:14 -0500 Subject: [PATCH 03/14] chore(mfe): avoid leaking application type --- crates/turborepo-lib/src/micro_frontends.rs | 6 +++--- crates/turborepo-micro-frontend/src/lib.rs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index 1f78bac493fe2..2f545376626f0 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -42,9 +42,9 @@ impl MicroFrontendsConfigs { continue; }; let tasks = config - .applications() - .map(|(application, options)| { - let dev_task = options.development.task.as_deref().unwrap_or("dev"); + .development_tasks() + .map(|(application, task)| { + let dev_task = task.unwrap_or("dev"); TaskId::new(application, dev_task).into_owned() }) .collect(); diff --git a/crates/turborepo-micro-frontend/src/lib.rs b/crates/turborepo-micro-frontend/src/lib.rs index c605abf0841a2..dc3697a8b8f37 100644 --- a/crates/turborepo-micro-frontend/src/lib.rs +++ b/crates/turborepo-micro-frontend/src/lib.rs @@ -1,9 +1,8 @@ #![deny(clippy::all)] mod configv1; +mod configv2; mod error; -use std::collections::BTreeMap; - use biome_deserialize_macros::Deserializable; use biome_json_parser::JsonParserOptions; use configv1::ConfigV1; @@ -66,9 +65,11 @@ impl Config { } } - pub fn applications(&self) -> impl Iterator { + pub fn development_tasks(&self) -> impl Iterator)> { match self { - Config::V1(config_v1) => config_v1.applications(), + Config::V1(config_v1) => config_v1 + .applications() + .map(|(name, config)| (name.as_str(), config.development.task.as_deref())), } } } From b9f0be0c1196d8b97342c792bf0d19bf24b4e8c7 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 19 Nov 2024 15:41:24 -0500 Subject: [PATCH 04/14] chore(mfe): add functionality to load multiple versions of mfe config --- Cargo.lock | 1 + crates/turborepo-lib/src/micro_frontends.rs | 11 +- .../src/task_graph/visitor/mod.rs | 4 +- crates/turborepo-micro-frontend/Cargo.toml | 1 + .../turborepo-micro-frontend/src/configv1.rs | 30 ++++- .../turborepo-micro-frontend/src/configv2.rs | 120 ++++++++++++++++++ crates/turborepo-micro-frontend/src/error.rs | 7 + crates/turborepo-micro-frontend/src/lib.rs | 116 ++++++++++++++--- 8 files changed, 262 insertions(+), 28 deletions(-) create mode 100644 crates/turborepo-micro-frontend/src/configv2.rs diff --git a/Cargo.lock b/Cargo.lock index 46054e239253e..ccf637fb4ea74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6551,6 +6551,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", + "tempfile", "thiserror", "turbopath", "turborepo-errors", diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index 2f545376626f0..743820397fdb8 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -4,7 +4,7 @@ use itertools::Itertools; use tracing::warn; use turbopath::AbsoluteSystemPath; use turborepo_micro_frontend::{ - Config as MFEConfig, Error, DEFAULT_MICRO_FRONTENDS_CONFIG, MICRO_FRONTENDS_PACKAGES, + Config as MFEConfig, Error, DEFAULT_MICRO_FRONTENDS_CONFIG_V1, MICRO_FRONTENDS_PACKAGES, }; use turborepo_repository::package_graph::{PackageGraph, PackageName}; @@ -29,7 +29,7 @@ impl MicroFrontendsConfigs { 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); + .join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V1); let Some(config) = MFEConfig::load(&config_path).or_else(|err| { if matches!(err, turborepo_micro_frontend::Error::UnsupportedVersion(_)) { warn!("Ignoring {config_path}: {err}"); @@ -42,9 +42,10 @@ impl MicroFrontendsConfigs { continue; }; let tasks = config - .development_tasks() - .map(|(application, task)| { - let dev_task = task.unwrap_or("dev"); + .applications + .iter() + .map(|(application, options)| { + let dev_task = options.development.task.as_deref().unwrap_or("dev"); TaskId::new(application, dev_task).into_owned() }) .collect(); diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index 6dd29bce6ba96..16b8c85ab06e7 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -23,7 +23,7 @@ use tracing::{debug, error, warn, Span}; use turbopath::{AbsoluteSystemPath, AnchoredSystemPath}; use turborepo_ci::{Vendor, VendorBehavior}; use turborepo_env::{platform::PlatformEnv, EnvironmentVariableMap}; -use turborepo_micro_frontend::DEFAULT_MICRO_FRONTENDS_CONFIG; +use turborepo_micro_frontend::DEFAULT_MICRO_FRONTENDS_CONFIG_V1; use turborepo_repository::package_graph::{PackageGraph, PackageName, ROOT_PKG_NAME}; use turborepo_telemetry::events::{ generic::GenericEventBuilder, task::PackageTaskEventBuilder, EventBuilder, TrackedErrors, @@ -101,7 +101,7 @@ pub enum Error { #[error("unable to find package manager binary: {0}")] Which(#[from] which::Error), #[error( - "'{package}' is configured with a {DEFAULT_MICRO_FRONTENDS_CONFIG}, but doesn't have \ + "'{package}' is configured with a {DEFAULT_MICRO_FRONTENDS_CONFIG_V1}, but doesn't have \ '@vercel/microfrontends' listed as a dependency" )] MissingMFEDependency { package: String }, diff --git a/crates/turborepo-micro-frontend/Cargo.toml b/crates/turborepo-micro-frontend/Cargo.toml index d08ccd33d425f..c34ca3d66e21c 100644 --- a/crates/turborepo-micro-frontend/Cargo.toml +++ b/crates/turborepo-micro-frontend/Cargo.toml @@ -19,6 +19,7 @@ turborepo-errors = { workspace = true } [dev-dependencies] insta = { workspace = true } pretty_assertions = { workspace = true } +tempfile = { workspace = true } [lints] workspace = true diff --git a/crates/turborepo-micro-frontend/src/configv1.rs b/crates/turborepo-micro-frontend/src/configv1.rs index 20f788b9dc92e..9c2962c937fe1 100644 --- a/crates/turborepo-micro-frontend/src/configv1.rs +++ b/crates/turborepo-micro-frontend/src/configv1.rs @@ -4,7 +4,7 @@ use biome_deserialize_macros::Deserializable; use biome_json_parser::JsonParserOptions; use serde::Serialize; -use crate::{Application, Error}; +use crate::Error; /// The minimal amount of information Turborepo needs to correctly start a local /// proxy server for microfrontends @@ -14,16 +14,34 @@ pub struct ConfigV1 { pub applications: BTreeMap, } +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +pub struct Application { + pub development: Development, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +pub struct Development { + #[serde(skip_serializing_if = "Option::is_none")] + pub task: Option, +} + impl ConfigV1 { pub fn from_str(input: &str, source: &str) -> Result { - let (config, errs) = biome_deserialize::json::deserialize_from_json_str( + let (config, errs) = biome_deserialize::json::deserialize_from_json_str::( input, JsonParserOptions::default().with_allow_comments(), source, ) .consume(); if let Some(config) = config { - Ok(config) + if config.version == "1" { + Ok(config) + } else { + Err(Error::InvalidVersion { + expected: "1", + actual: config.version, + }) + } } else { Err(Error::biome_error(errs)) } @@ -32,4 +50,10 @@ impl ConfigV1 { pub fn applications(&self) -> impl Iterator { self.applications.iter() } + + pub fn development_tasks(&self) -> impl Iterator)> { + self.applications + .iter() + .map(|(application, config)| (application.as_str(), config.development.task.as_deref())) + } } diff --git a/crates/turborepo-micro-frontend/src/configv2.rs b/crates/turborepo-micro-frontend/src/configv2.rs new file mode 100644 index 0000000000000..744f741b4e967 --- /dev/null +++ b/crates/turborepo-micro-frontend/src/configv2.rs @@ -0,0 +1,120 @@ +use std::collections::BTreeMap; + +use biome_deserialize_macros::Deserializable; +use biome_json_parser::JsonParserOptions; +use serde::Serialize; + +use crate::Error; + +pub enum ParseResult { + Actual(ConfigV2), + Reference(String), +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +pub struct ConfigV2 { + version: String, + applications: BTreeMap, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +struct ChildConfig { + part_of: String, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +struct Application { + development: Option, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] +struct Development { + task: Option, +} + +impl ConfigV2 { + pub fn from_str(input: &str, source: &str) -> Result { + // attempt to parse a child, ignoring any errors + let (config, errs) = biome_deserialize::json::deserialize_from_json_str::( + input, + JsonParserOptions::default().with_allow_comments(), + source, + ) + .consume(); + + if let Some(ChildConfig { part_of }) = errs.is_empty().then_some(config).flatten() { + return Ok(ParseResult::Reference(part_of)); + } + // attempt to parse a real one + let (config, errs) = biome_deserialize::json::deserialize_from_json_str::( + input, + JsonParserOptions::default().with_allow_comments(), + source, + ) + .consume(); + + if let Some(config) = config { + if config.version == "2" { + Ok(ParseResult::Actual(config)) + } else { + Err(Error::InvalidVersion { + expected: "2", + actual: config.version, + }) + } + } else { + Err(Error::biome_error(errs)) + } + } + + pub fn development_tasks(&self) -> impl Iterator)> { + self.applications + .iter() + .map(|(application, config)| (application.as_str(), config.task())) + } +} + +impl Application { + fn task(&self) -> Option<&str> { + self.development.as_ref()?.task.as_deref() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_child_config_parse() { + let input = r#"{"partOf": "web"}"#; + let config = ConfigV2::from_str(input, "somewhere").unwrap(); + match config { + ParseResult::Actual(_config_v2) => panic!("expected to get reference to default app"), + ParseResult::Reference(default_app) => { + assert_eq!(default_app, "web"); + } + } + } + + #[test] + fn test_root_config_parse() { + let input = r#"{ + "version": "2", + "applications": { + "web": {}, + "docs": {"development": {"task": "serve"}} + } + }"#; + let config = ConfigV2::from_str(input, "somewhere").unwrap(); + match config { + ParseResult::Actual(config_v2) => { + assert_eq!(config_v2.applications.get("web").unwrap().task(), None); + assert_eq!( + config_v2.applications.get("docs").unwrap().task(), + Some("serve") + ); + } + ParseResult::Reference(_) => panic!("expected to get main config"), + } + } +} diff --git a/crates/turborepo-micro-frontend/src/error.rs b/crates/turborepo-micro-frontend/src/error.rs index 36354aeec2fb4..90984181bb09a 100644 --- a/crates/turborepo-micro-frontend/src/error.rs +++ b/crates/turborepo-micro-frontend/src/error.rs @@ -13,6 +13,13 @@ pub enum Error { {SUPPORTED_VERSIONS:?}" )] UnsupportedVersion(String), + #[error("Configuration references config located in package {reference}")] + ChildConfig { reference: String }, + #[error("Cannot parse config with version '{actual}' as version '{expected}'")] + InvalidVersion { + expected: &'static str, + actual: String, + }, } impl Error { diff --git a/crates/turborepo-micro-frontend/src/lib.rs b/crates/turborepo-micro-frontend/src/lib.rs index dc3697a8b8f37..5fe42ce786313 100644 --- a/crates/turborepo-micro-frontend/src/lib.rs +++ b/crates/turborepo-micro-frontend/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(assert_matches)] #![deny(clippy::all)] mod configv1; mod configv2; @@ -6,14 +7,15 @@ mod error; use biome_deserialize_macros::Deserializable; use biome_json_parser::JsonParserOptions; use configv1::ConfigV1; +use configv2::ConfigV2; pub use error::Error; -use serde::Serialize; use turbopath::AbsoluteSystemPath; /// Currently the default path for a package that provides a configuration. /// /// This is subject to change at any time. -pub const DEFAULT_MICRO_FRONTENDS_CONFIG: &str = "micro-frontends.jsonc"; +pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V1: &str = "micro-frontends.jsonc"; +pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V2: &str = "microfrontends.json"; pub const MICRO_FRONTENDS_PACKAGES: &[&str] = [ MICRO_FRONTENDS_PACKAGE_EXTERNAL, MICRO_FRONTENDS_PACKAGE_INTERNAL, @@ -21,13 +23,14 @@ pub const MICRO_FRONTENDS_PACKAGES: &[&str] = [ .as_slice(); pub const MICRO_FRONTENDS_PACKAGE_INTERNAL: &str = "@vercel/micro-frontends-internal"; pub const MICRO_FRONTENDS_PACKAGE_EXTERNAL: &str = "@vercel/microfrontends"; -pub const SUPPORTED_VERSIONS: &[&str] = ["1"].as_slice(); +pub const SUPPORTED_VERSIONS: &[&str] = ["1", "2"].as_slice(); /// The minimal amount of information Turborepo needs to correctly start a local /// proxy server for microfrontends #[derive(Debug, PartialEq, Eq)] pub enum Config { V1(ConfigV1), + V2(ConfigV2), } impl Config { @@ -41,6 +44,14 @@ impl Config { Ok(Some(config)) } + pub fn load_from_dir(dir: &AbsoluteSystemPath) -> Result, Error> { + if let Some(config) = Self::load_v2_dir(dir)? { + Ok(Some(config)) + } else { + Self::load_v1_dir(dir) + } + } + pub fn from_str(input: &str, source: &str) -> Result { #[derive(Deserializable, Default)] struct VersionOnly { @@ -56,38 +67,62 @@ impl Config { let version = match version_only { Some(VersionOnly { version }) => version, // Default to version 1 if no version found - None => "1".to_string(), + None => "2".to_string(), }; match version.as_str() { "1" => ConfigV1::from_str(input, source).map(Config::V1), + "2" => ConfigV2::from_str(input, source).and_then(|result| match result { + configv2::ParseResult::Actual(config_v2) => Ok(Config::V2(config_v2)), + configv2::ParseResult::Reference(default_app) => Err(Error::ChildConfig { + reference: default_app, + }), + }), version => Err(Error::UnsupportedVersion(version.to_string())), } } - pub fn development_tasks(&self) -> impl Iterator)> { + pub fn development_tasks<'a>(&'a self) -> Box)> + 'a> { match self { - Config::V1(config_v1) => config_v1 - .applications() - .map(|(name, config)| (name.as_str(), config.development.task.as_deref())), + Config::V1(config_v1) => Box::new(config_v1.development_tasks()), + Config::V2(config_v2) => Box::new(config_v2.development_tasks()), } } -} -#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] -pub struct Application { - pub development: Development, -} + fn load_v2_dir(dir: &AbsoluteSystemPath) -> Result, Error> { + let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V2); + let Some(contents) = path.read_existing_to_string()? else { + return Ok(None); + }; -#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default)] -pub struct Development { - #[serde(skip_serializing_if = "Option::is_none")] - pub task: Option, + ConfigV2::from_str(&contents, path.as_str()) + .and_then(|result| match result { + configv2::ParseResult::Actual(config_v2) => Ok(Config::V2(config_v2)), + configv2::ParseResult::Reference(default_app) => Err(Error::ChildConfig { + reference: default_app, + }), + }) + .map(Some) + } + + fn load_v1_dir(dir: &AbsoluteSystemPath) -> Result, Error> { + let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V1); + let Some(contents) = path.read_existing_to_string()? else { + return Ok(None); + }; + + ConfigV1::from_str(&contents, path.as_str()) + .map(Self::V1) + .map(Some) + } } #[cfg(test)] mod test { + use std::assert_matches::assert_matches; + use insta::assert_snapshot; + use tempfile::TempDir; use super::*; @@ -102,6 +137,51 @@ mod test { fn test_unsupported_version() { let input = r#"{"version": "yolo"}"#; let err = Config::from_str(input, "something.json").unwrap_err(); - assert_snapshot!(err, @r###"Unsupported micro-frontends configuration version: yolo. Supported versions: ["1"]"###); + assert_snapshot!(err, @r###"Unsupported micro-frontends configuration version: yolo. Supported versions: ["1", "2"]"###); + } + + fn add_v1_config(dir: &AbsoluteSystemPath) -> Result<(), std::io::Error> { + let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V1); + path.create_with_contents(r#"{"version": "1", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) + } + + fn add_v2_config(dir: &AbsoluteSystemPath) -> Result<(), std::io::Error> { + let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V2); + path.create_with_contents(r#"{"version": "2", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) + } + + #[test] + fn test_load_dir_v1() { + let dir = TempDir::new().unwrap(); + let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); + add_v1_config(path).unwrap(); + let config = Config::load_from_dir(path).unwrap(); + assert_matches!(config, Some(Config::V1(_))); + } + + #[test] + fn test_load_dir_v2() { + let dir = TempDir::new().unwrap(); + let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); + add_v2_config(path).unwrap(); + let config = Config::load_from_dir(path).unwrap(); + assert_matches!(config, Some(Config::V2(_))); + } + + #[test] + fn test_load_dir_both() { + let dir = TempDir::new().unwrap(); + let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); + add_v1_config(path).unwrap(); + add_v2_config(path).unwrap(); + let config = Config::load_from_dir(path).unwrap(); + assert_matches!(config, Some(Config::V2(_))); + } + + #[test] + fn test_load_dir_none() { + let dir = TempDir::new().unwrap(); + let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); + assert!(Config::load_from_dir(path).unwrap().is_none()); } } From 77710e7667e629f0473249e7e42448da79f62c4d Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 19 Nov 2024 16:53:21 -0500 Subject: [PATCH 05/14] feat(mfe): use mfe version 2 --- crates/turborepo-lib/src/micro_frontends.rs | 35 ++++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index 743820397fdb8..3b7a180574657 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -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_V1, MICRO_FRONTENDS_PACKAGES, -}; +use turborepo_micro_frontend::{Config as MFEConfig, Error, MICRO_FRONTENDS_PACKAGES}; use turborepo_repository::package_graph::{PackageGraph, PackageName}; use crate::{ @@ -26,17 +24,19 @@ impl MicroFrontendsConfigs { package_graph: &PackageGraph, ) -> Result, Error> { let mut configs = HashMap::new(); + let mut referenced_default_apps = HashSet::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_V1); - let Some(config) = MFEConfig::load(&config_path).or_else(|err| { - if matches!(err, turborepo_micro_frontend::Error::UnsupportedVersion(_)) { - warn!("Ignoring {config_path}: {err}"); + let package_dir = repo_root.resolve(package_info.package_path()); + let Some(config) = MFEConfig::load_from_dir(&package_dir).or_else(|err| match err { + turborepo_micro_frontend::Error::UnsupportedVersion(_) => { + warn!("Ignoring {package_dir}: {err}"); + Ok(None) + } + turborepo_micro_frontend::Error::ChildConfig { reference } => { + referenced_default_apps.insert(reference); Ok(None) - } else { - Err(err) } + err => Err(err), })? else { continue; @@ -51,7 +51,18 @@ impl MicroFrontendsConfigs { .collect(); configs.insert(package_name.to_string(), tasks); } - + let default_apps_found = configs.keys().cloned().collect(); + let mut missing_default_apps = referenced_default_apps + .difference(&default_apps_found) + .map(|s| s.as_str()) + .collect::>(); + if !missing_default_apps.is_empty() { + missing_default_apps.sort(); + warn!( + "Missing default applications: {}", + missing_default_apps.join(", ") + ); + } let mfe_package = package_graph .packages() .map(|(pkg, _)| pkg.as_str()) From a895d1808a33df46ab233f21405aaccebdd8bdef Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 25 Nov 2024 16:03:30 -0600 Subject: [PATCH 06/14] chore: fix rebase --- crates/turborepo-lib/src/micro_frontends.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index 3b7a180574657..8061ced0e9360 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -42,10 +42,9 @@ impl MicroFrontendsConfigs { continue; }; let tasks = config - .applications - .iter() + .development_tasks() .map(|(application, options)| { - let dev_task = options.development.task.as_deref().unwrap_or("dev"); + let dev_task = options.unwrap_or("dev"); TaskId::new(application, dev_task).into_owned() }) .collect(); From 73710bc9dc51886c65c763c11d5ac58db1602d0e Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 25 Nov 2024 16:44:18 -0600 Subject: [PATCH 07/14] chore(mfe): add support for jsonc extension --- crates/turborepo-micro-frontend/src/lib.rs | 31 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/turborepo-micro-frontend/src/lib.rs b/crates/turborepo-micro-frontend/src/lib.rs index 5fe42ce786313..1cd0ae4f579f7 100644 --- a/crates/turborepo-micro-frontend/src/lib.rs +++ b/crates/turborepo-micro-frontend/src/lib.rs @@ -4,18 +4,21 @@ mod configv1; mod configv2; mod error; +use std::io; + use biome_deserialize_macros::Deserializable; use biome_json_parser::JsonParserOptions; use configv1::ConfigV1; use configv2::ConfigV2; pub use error::Error; -use turbopath::AbsoluteSystemPath; +use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf}; /// Currently the default path for a package that provides a configuration. /// /// This is subject to change at any time. pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V1: &str = "micro-frontends.jsonc"; pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V2: &str = "microfrontends.json"; +pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V2_ALT: &str = "microfrontends.jsonc"; pub const MICRO_FRONTENDS_PACKAGES: &[&str] = [ MICRO_FRONTENDS_PACKAGE_EXTERNAL, MICRO_FRONTENDS_PACKAGE_INTERNAL, @@ -90,10 +93,18 @@ impl Config { } fn load_v2_dir(dir: &AbsoluteSystemPath) -> Result, Error> { - let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V2); - let Some(contents) = path.read_existing_to_string()? else { + let load_config = + |filename: &str| -> Option<(Result, AbsoluteSystemPathBuf)> { + let path = dir.join_component(filename); + let contents = path.read_existing_to_string().transpose()?; + Some((contents, path)) + }; + let Some((contents, path)) = load_config(DEFAULT_MICRO_FRONTENDS_CONFIG_V2) + .or_else(|| load_config(DEFAULT_MICRO_FRONTENDS_CONFIG_V2_ALT)) + else { return Ok(None); }; + let contents = contents?; ConfigV2::from_str(&contents, path.as_str()) .and_then(|result| match result { @@ -150,6 +161,11 @@ mod test { path.create_with_contents(r#"{"version": "2", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) } + fn add_v2_alt_config(dir: &AbsoluteSystemPath) -> Result<(), std::io::Error> { + let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V2_ALT); + path.create_with_contents(r#"{"version": "2", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) + } + #[test] fn test_load_dir_v1() { let dir = TempDir::new().unwrap(); @@ -178,6 +194,15 @@ mod test { assert_matches!(config, Some(Config::V2(_))); } + #[test] + fn test_load_dir_v2_alt() { + let dir = TempDir::new().unwrap(); + let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); + add_v2_alt_config(path).unwrap(); + let config = Config::load_from_dir(path).unwrap(); + assert_matches!(config, Some(Config::V2(_))); + } + #[test] fn test_load_dir_none() { let dir = TempDir::new().unwrap(); From 0e975d9c3a8b1e1a9963ea7ece643fe2ff0b3b58 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 25 Nov 2024 17:01:19 -0600 Subject: [PATCH 08/14] chore(mfe): rename crate --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/turborepo-lib/Cargo.toml | 2 +- crates/turborepo-lib/src/micro_frontends.rs | 6 +++--- crates/turborepo-lib/src/run/error.rs | 2 +- crates/turborepo-lib/src/task_graph/visitor/command.rs | 2 +- crates/turborepo-lib/src/task_graph/visitor/mod.rs | 2 +- .../Cargo.toml | 2 +- .../fixtures/sample.jsonc | 0 .../src/configv1.rs | 0 .../src/configv2.rs | 0 .../src/error.rs | 0 .../src/lib.rs | 0 13 files changed, 11 insertions(+), 11 deletions(-) rename crates/{turborepo-micro-frontend => turborepo-microfrontends}/Cargo.toml (94%) rename crates/{turborepo-micro-frontend => turborepo-microfrontends}/fixtures/sample.jsonc (100%) rename crates/{turborepo-micro-frontend => turborepo-microfrontends}/src/configv1.rs (100%) rename crates/{turborepo-micro-frontend => turborepo-microfrontends}/src/configv2.rs (100%) rename crates/{turborepo-micro-frontend => turborepo-microfrontends}/src/error.rs (100%) rename crates/{turborepo-micro-frontend => turborepo-microfrontends}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index ccf637fb4ea74..32e1a2c97ede5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6483,7 +6483,7 @@ dependencies = [ "turborepo-fs", "turborepo-graph-utils", "turborepo-lockfiles", - "turborepo-micro-frontend", + "turborepo-microfrontends", "turborepo-repository", "turborepo-scm", "turborepo-telemetry", @@ -6539,7 +6539,7 @@ dependencies = [ ] [[package]] -name = "turborepo-micro-frontend" +name = "turborepo-microfrontends" version = "0.1.0" dependencies = [ "biome_deserialize", diff --git a/Cargo.toml b/Cargo.toml index 4b0a20b9e9a20..46f694f059679 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/turborepo-lib/Cargo.toml b/crates/turborepo-lib/Cargo.toml index 644cc96eecac6..fe968a728abea 100644 --- a/crates/turborepo-lib/Cargo.toml +++ b/crates/turborepo-lib/Cargo.toml @@ -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" } diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index 8061ced0e9360..df1f54c7d7288 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use itertools::Itertools; use tracing::warn; use turbopath::AbsoluteSystemPath; -use turborepo_micro_frontend::{Config as MFEConfig, Error, MICRO_FRONTENDS_PACKAGES}; +use turborepo_microfrontends::{Config as MFEConfig, Error, MICRO_FRONTENDS_PACKAGES}; use turborepo_repository::package_graph::{PackageGraph, PackageName}; use crate::{ @@ -28,11 +28,11 @@ impl MicroFrontendsConfigs { for (package_name, package_info) in package_graph.packages() { let package_dir = repo_root.resolve(package_info.package_path()); let Some(config) = MFEConfig::load_from_dir(&package_dir).or_else(|err| match err { - turborepo_micro_frontend::Error::UnsupportedVersion(_) => { + turborepo_microfrontends::Error::UnsupportedVersion(_) => { warn!("Ignoring {package_dir}: {err}"); Ok(None) } - turborepo_micro_frontend::Error::ChildConfig { reference } => { + turborepo_microfrontends::Error::ChildConfig { reference } => { referenced_default_apps.insert(reference); Ok(None) } diff --git a/crates/turborepo-lib/src/run/error.rs b/crates/turborepo-lib/src/run/error.rs index e4ba94be2a5af..75fc695598251 100644 --- a/crates/turborepo-lib/src/run/error.rs +++ b/crates/turborepo-lib/src/run/error.rs @@ -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), } diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index 17b2b315dff46..23c14efc18e2c 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, path::PathBuf}; use turbopath::AbsoluteSystemPath; use turborepo_env::EnvironmentVariableMap; -use turborepo_micro_frontend::MICRO_FRONTENDS_PACKAGES; +use turborepo_microfrontends::MICRO_FRONTENDS_PACKAGES; use turborepo_repository::package_graph::{PackageGraph, PackageInfo, PackageName}; use super::Error; diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index 16b8c85ab06e7..b9e0903d0fe02 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -23,7 +23,7 @@ use tracing::{debug, error, warn, Span}; use turbopath::{AbsoluteSystemPath, AnchoredSystemPath}; use turborepo_ci::{Vendor, VendorBehavior}; use turborepo_env::{platform::PlatformEnv, EnvironmentVariableMap}; -use turborepo_micro_frontend::DEFAULT_MICRO_FRONTENDS_CONFIG_V1; +use turborepo_microfrontends::DEFAULT_MICRO_FRONTENDS_CONFIG_V1; use turborepo_repository::package_graph::{PackageGraph, PackageName, ROOT_PKG_NAME}; use turborepo_telemetry::events::{ generic::GenericEventBuilder, task::PackageTaskEventBuilder, EventBuilder, TrackedErrors, diff --git a/crates/turborepo-micro-frontend/Cargo.toml b/crates/turborepo-microfrontends/Cargo.toml similarity index 94% rename from crates/turborepo-micro-frontend/Cargo.toml rename to crates/turborepo-microfrontends/Cargo.toml index c34ca3d66e21c..209cd30805caf 100644 --- a/crates/turborepo-micro-frontend/Cargo.toml +++ b/crates/turborepo-microfrontends/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "turborepo-micro-frontend" +name = "turborepo-microfrontends" version = "0.1.0" edition = "2021" license = "MIT" diff --git a/crates/turborepo-micro-frontend/fixtures/sample.jsonc b/crates/turborepo-microfrontends/fixtures/sample.jsonc similarity index 100% rename from crates/turborepo-micro-frontend/fixtures/sample.jsonc rename to crates/turborepo-microfrontends/fixtures/sample.jsonc diff --git a/crates/turborepo-micro-frontend/src/configv1.rs b/crates/turborepo-microfrontends/src/configv1.rs similarity index 100% rename from crates/turborepo-micro-frontend/src/configv1.rs rename to crates/turborepo-microfrontends/src/configv1.rs diff --git a/crates/turborepo-micro-frontend/src/configv2.rs b/crates/turborepo-microfrontends/src/configv2.rs similarity index 100% rename from crates/turborepo-micro-frontend/src/configv2.rs rename to crates/turborepo-microfrontends/src/configv2.rs diff --git a/crates/turborepo-micro-frontend/src/error.rs b/crates/turborepo-microfrontends/src/error.rs similarity index 100% rename from crates/turborepo-micro-frontend/src/error.rs rename to crates/turborepo-microfrontends/src/error.rs diff --git a/crates/turborepo-micro-frontend/src/lib.rs b/crates/turborepo-microfrontends/src/lib.rs similarity index 100% rename from crates/turborepo-micro-frontend/src/lib.rs rename to crates/turborepo-microfrontends/src/lib.rs From 109ec17d2ae5b663b13aec5fd13f678f4f69f208 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 25 Nov 2024 17:05:37 -0600 Subject: [PATCH 09/14] chore(mfe): rename constants --- crates/turborepo-lib/src/micro_frontends.rs | 4 +-- .../src/task_graph/visitor/command.rs | 4 +-- .../src/task_graph/visitor/mod.rs | 4 +-- crates/turborepo-microfrontends/src/lib.rs | 28 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index df1f54c7d7288..78176294f2aee 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use itertools::Itertools; use tracing::warn; use turbopath::AbsoluteSystemPath; -use turborepo_microfrontends::{Config as MFEConfig, Error, MICRO_FRONTENDS_PACKAGES}; +use turborepo_microfrontends::{Config as MFEConfig, Error, MICROFRONTENDS_PACKAGES}; use turborepo_repository::package_graph::{PackageGraph, PackageName}; use crate::{ @@ -69,7 +69,7 @@ impl MicroFrontendsConfigs { // 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 + MICROFRONTENDS_PACKAGES .iter() .find(|static_pkg| pkg == **static_pkg) }) diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index 23c14efc18e2c..e8e1308b22515 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, path::PathBuf}; use turbopath::AbsoluteSystemPath; use turborepo_env::EnvironmentVariableMap; -use turborepo_microfrontends::MICRO_FRONTENDS_PACKAGES; +use turborepo_microfrontends::MICROFRONTENDS_PACKAGES; use turborepo_repository::package_graph::{PackageGraph, PackageInfo, PackageName}; use super::Error; @@ -206,7 +206,7 @@ impl<'a> CommandProvider for MicroFrontendProxyProvider<'a> { let has_mfe_dependency = package_info .package_json .all_dependencies() - .any(|(package, _version)| MICRO_FRONTENDS_PACKAGES.contains(&package.as_str())); + .any(|(package, _version)| MICROFRONTENDS_PACKAGES.contains(&package.as_str())); if !has_mfe_dependency { return Err(Error::MissingMFEDependency { package: task_id.package().into(), diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index b9e0903d0fe02..7ce6b20b93c5b 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -23,7 +23,7 @@ use tracing::{debug, error, warn, Span}; use turbopath::{AbsoluteSystemPath, AnchoredSystemPath}; use turborepo_ci::{Vendor, VendorBehavior}; use turborepo_env::{platform::PlatformEnv, EnvironmentVariableMap}; -use turborepo_microfrontends::DEFAULT_MICRO_FRONTENDS_CONFIG_V1; +use turborepo_microfrontends::DEFAULT_MICROFRONTENDS_CONFIG_V1; use turborepo_repository::package_graph::{PackageGraph, PackageName, ROOT_PKG_NAME}; use turborepo_telemetry::events::{ generic::GenericEventBuilder, task::PackageTaskEventBuilder, EventBuilder, TrackedErrors, @@ -101,7 +101,7 @@ pub enum Error { #[error("unable to find package manager binary: {0}")] Which(#[from] which::Error), #[error( - "'{package}' is configured with a {DEFAULT_MICRO_FRONTENDS_CONFIG_V1}, but doesn't have \ + "'{package}' is configured with a {DEFAULT_MICROFRONTENDS_CONFIG_V1}, but doesn't have \ '@vercel/microfrontends' listed as a dependency" )] MissingMFEDependency { package: String }, diff --git a/crates/turborepo-microfrontends/src/lib.rs b/crates/turborepo-microfrontends/src/lib.rs index 1cd0ae4f579f7..7bb4a427e80fb 100644 --- a/crates/turborepo-microfrontends/src/lib.rs +++ b/crates/turborepo-microfrontends/src/lib.rs @@ -16,16 +16,16 @@ use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf}; /// Currently the default path for a package that provides a configuration. /// /// This is subject to change at any time. -pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V1: &str = "micro-frontends.jsonc"; -pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V2: &str = "microfrontends.json"; -pub const DEFAULT_MICRO_FRONTENDS_CONFIG_V2_ALT: &str = "microfrontends.jsonc"; -pub const MICRO_FRONTENDS_PACKAGES: &[&str] = [ - MICRO_FRONTENDS_PACKAGE_EXTERNAL, - MICRO_FRONTENDS_PACKAGE_INTERNAL, +pub const DEFAULT_MICROFRONTENDS_CONFIG_V1: &str = "micro-frontends.jsonc"; +pub const DEFAULT_MICROFRONTENDS_CONFIG_V2: &str = "microfrontends.json"; +pub const DEFAULT_MICROFRONTENDS_CONFIG_V2_ALT: &str = "microfrontends.jsonc"; +pub const MICROFRONTENDS_PACKAGES: &[&str] = [ + MICROFRONTENDS_PACKAGE_EXTERNAL, + MICROFRONTENDS_PACKAGE_INTERNAL, ] .as_slice(); -pub const MICRO_FRONTENDS_PACKAGE_INTERNAL: &str = "@vercel/micro-frontends-internal"; -pub const MICRO_FRONTENDS_PACKAGE_EXTERNAL: &str = "@vercel/microfrontends"; +pub const MICROFRONTENDS_PACKAGE_INTERNAL: &str = "@vercel/micro-frontends-internal"; +pub const MICROFRONTENDS_PACKAGE_EXTERNAL: &str = "@vercel/microfrontends"; pub const SUPPORTED_VERSIONS: &[&str] = ["1", "2"].as_slice(); /// The minimal amount of information Turborepo needs to correctly start a local @@ -99,8 +99,8 @@ impl Config { let contents = path.read_existing_to_string().transpose()?; Some((contents, path)) }; - let Some((contents, path)) = load_config(DEFAULT_MICRO_FRONTENDS_CONFIG_V2) - .or_else(|| load_config(DEFAULT_MICRO_FRONTENDS_CONFIG_V2_ALT)) + let Some((contents, path)) = load_config(DEFAULT_MICROFRONTENDS_CONFIG_V2) + .or_else(|| load_config(DEFAULT_MICROFRONTENDS_CONFIG_V2_ALT)) else { return Ok(None); }; @@ -117,7 +117,7 @@ impl Config { } fn load_v1_dir(dir: &AbsoluteSystemPath) -> Result, Error> { - let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V1); + let path = dir.join_component(DEFAULT_MICROFRONTENDS_CONFIG_V1); let Some(contents) = path.read_existing_to_string()? else { return Ok(None); }; @@ -152,17 +152,17 @@ mod test { } fn add_v1_config(dir: &AbsoluteSystemPath) -> Result<(), std::io::Error> { - let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V1); + let path = dir.join_component(DEFAULT_MICROFRONTENDS_CONFIG_V1); path.create_with_contents(r#"{"version": "1", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) } fn add_v2_config(dir: &AbsoluteSystemPath) -> Result<(), std::io::Error> { - let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V2); + let path = dir.join_component(DEFAULT_MICROFRONTENDS_CONFIG_V2); path.create_with_contents(r#"{"version": "2", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) } fn add_v2_alt_config(dir: &AbsoluteSystemPath) -> Result<(), std::io::Error> { - let path = dir.join_component(DEFAULT_MICRO_FRONTENDS_CONFIG_V2_ALT); + let path = dir.join_component(DEFAULT_MICROFRONTENDS_CONFIG_V2_ALT); path.create_with_contents(r#"{"version": "2", "applications": {"web": {"development": {"task": "serve"}}, "docs": {}}}"#) } From f5a4597051519effacd5025678ea94840215a5ae Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 25 Nov 2024 17:07:34 -0600 Subject: [PATCH 10/14] chore(mfe): rename struct --- crates/turborepo-lib/src/micro_frontends.rs | 6 +++--- crates/turborepo-lib/src/run/builder.rs | 4 ++-- crates/turborepo-lib/src/run/mod.rs | 4 ++-- crates/turborepo-lib/src/task_graph/visitor/command.rs | 10 +++++----- crates/turborepo-lib/src/task_graph/visitor/mod.rs | 6 +++--- crates/turborepo-lib/src/turbo_json/loader.rs | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/micro_frontends.rs index 78176294f2aee..5788da64128db 100644 --- a/crates/turborepo-lib/src/micro_frontends.rs +++ b/crates/turborepo-lib/src/micro_frontends.rs @@ -13,12 +13,12 @@ use crate::{ }; #[derive(Debug, Clone)] -pub struct MicroFrontendsConfigs { +pub struct MicrofrontendsConfigs { configs: HashMap>>, mfe_package: Option<&'static str>, } -impl MicroFrontendsConfigs { +impl MicrofrontendsConfigs { pub fn new( repo_root: &AbsoluteSystemPath, package_graph: &PackageGraph, @@ -263,7 +263,7 @@ mod test { "mfe-config-pkg" => ["web#dev", "docs#dev"], "mfe-web" => ["mfe-web#dev", "mfe-docs#serve"] ); - let mfe = MicroFrontendsConfigs { + let mfe = MicrofrontendsConfigs { configs, mfe_package: None, }; diff --git a/crates/turborepo-lib/src/run/builder.rs b/crates/turborepo-lib/src/run/builder.rs index f2cdd39b96755..650a2d0a68810 100644 --- a/crates/turborepo-lib/src/run/builder.rs +++ b/crates/turborepo-lib/src/run/builder.rs @@ -41,7 +41,7 @@ use crate::{ cli::DryRunMode, commands::CommandBase, engine::{Engine, EngineBuilder}, - micro_frontends::MicroFrontendsConfigs, + micro_frontends::MicrofrontendsConfigs, opts::Opts, process::ProcessManager, run::{scope, task_access::TaskAccess, task_id::TaskName, Error, Run, RunCache}, @@ -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( diff --git a/crates/turborepo-lib/src/run/mod.rs b/crates/turborepo-lib/src/run/mod.rs index 448b8c02d4726..869b0bb785ee9 100644 --- a/crates/turborepo-lib/src/run/mod.rs +++ b/crates/turborepo-lib/src/run/mod.rs @@ -41,7 +41,7 @@ pub use crate::run::error::Error; use crate::{ cli::EnvMode, engine::Engine, - micro_frontends::MicroFrontendsConfigs, + micro_frontends::MicrofrontendsConfigs, opts::Opts, process::ProcessManager, run::{global_hash::get_global_hash_inputs, summary::RunTracker, task_access::TaskAccess}, @@ -74,7 +74,7 @@ pub struct Run { task_access: TaskAccess, daemon: Option>, should_print_prelude: bool, - micro_frontend_configs: Option, + micro_frontend_configs: Option, } type UIResult = Result>)>, Error>; diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index e8e1308b22515..cb7770bb0033a 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -7,7 +7,7 @@ use turborepo_repository::package_graph::{PackageGraph, PackageInfo, PackageName use super::Error; use crate::{ - engine::Engine, micro_frontends::MicroFrontendsConfigs, opts::TaskArgs, process::Command, + engine::Engine, micro_frontends::MicrofrontendsConfigs, opts::TaskArgs, process::Command, run::task_id::TaskId, }; @@ -61,7 +61,7 @@ pub struct PackageGraphCommandProvider<'a> { package_graph: &'a PackageGraph, package_manager_binary: Result, task_args: TaskArgs<'a>, - mfe_configs: Option<&'a MicroFrontendsConfigs>, + mfe_configs: Option<&'a MicrofrontendsConfigs>, } impl<'a> PackageGraphCommandProvider<'a> { @@ -69,7 +69,7 @@ impl<'a> PackageGraphCommandProvider<'a> { repo_root: &'a AbsoluteSystemPath, package_graph: &'a PackageGraph, task_args: TaskArgs<'a>, - mfe_configs: Option<&'a MicroFrontendsConfigs>, + mfe_configs: Option<&'a MicrofrontendsConfigs>, ) -> Self { let package_manager_binary = which::which(package_graph.package_manager().command()); Self { @@ -151,7 +151,7 @@ pub struct MicroFrontendProxyProvider<'a> { repo_root: &'a AbsoluteSystemPath, package_graph: &'a PackageGraph, tasks_in_graph: HashSet>, - mfe_configs: &'a MicroFrontendsConfigs, + mfe_configs: &'a MicrofrontendsConfigs, } impl<'a> MicroFrontendProxyProvider<'a> { @@ -159,7 +159,7 @@ impl<'a> MicroFrontendProxyProvider<'a> { repo_root: &'a AbsoluteSystemPath, package_graph: &'a PackageGraph, engine: &Engine, - micro_frontends_configs: &'a MicroFrontendsConfigs, + micro_frontends_configs: &'a MicrofrontendsConfigs, ) -> Self { let tasks_in_graph = engine .tasks() diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index 7ce6b20b93c5b..df3100224d7c1 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -35,7 +35,7 @@ use turborepo_ui::{ use crate::{ cli::EnvMode, engine::{Engine, ExecutionOptions}, - micro_frontends::MicroFrontendsConfigs, + micro_frontends::MicrofrontendsConfigs, opts::RunOpts, process::ProcessManager, run::{ @@ -67,7 +67,7 @@ pub struct Visitor<'a> { is_watch: bool, ui_sender: Option, warnings: Arc>>, - micro_frontends_configs: Option<&'a MicroFrontendsConfigs>, + micro_frontends_configs: Option<&'a MicrofrontendsConfigs>, } #[derive(Debug, thiserror::Error, Diagnostic)] @@ -127,7 +127,7 @@ impl<'a> Visitor<'a> { global_env: EnvironmentVariableMap, ui_sender: Option, is_watch: bool, - micro_frontends_configs: Option<&'a MicroFrontendsConfigs>, + micro_frontends_configs: Option<&'a MicrofrontendsConfigs>, ) -> Self { let task_hasher = TaskHasher::new( package_inputs_hashes, diff --git a/crates/turborepo-lib/src/turbo_json/loader.rs b/crates/turborepo-lib/src/turbo_json/loader.rs index a37018fd46e77..eb8f92690a0a4 100644 --- a/crates/turborepo-lib/src/turbo_json/loader.rs +++ b/crates/turborepo-lib/src/turbo_json/loader.rs @@ -12,7 +12,7 @@ use super::{Pipeline, RawTaskDefinition, TurboJson, CONFIG_FILE}; use crate::{ cli::EnvMode, config::Error, - micro_frontends::MicroFrontendsConfigs, + micro_frontends::MicrofrontendsConfigs, run::{task_access::TASK_ACCESS_CONFIG_PATH, task_id::TaskName}, }; @@ -35,7 +35,7 @@ enum Strategy { Workspace { // Map of package names to their package specific turbo.json packages: HashMap, - micro_frontends_configs: Option, + micro_frontends_configs: Option, }, WorkspaceNoTurboJson { // Map of package names to their scripts @@ -71,7 +71,7 @@ impl TurboJsonLoader { repo_root: AbsoluteSystemPathBuf, root_turbo_json_path: AbsoluteSystemPathBuf, packages: impl Iterator, - micro_frontends_configs: MicroFrontendsConfigs, + micro_frontends_configs: MicrofrontendsConfigs, ) -> Self { let packages = package_turbo_jsons(&repo_root, root_turbo_json_path, packages); Self { From 7d147e4179c3589741eb2884c54feace98c822d2 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Mon, 25 Nov 2024 17:10:46 -0600 Subject: [PATCH 11/14] chore(mfe): rename module --- crates/turborepo-lib/src/lib.rs | 2 +- .../turborepo-lib/src/{micro_frontends.rs => microfrontends.rs} | 0 crates/turborepo-lib/src/run/builder.rs | 2 +- crates/turborepo-lib/src/run/mod.rs | 2 +- crates/turborepo-lib/src/task_graph/visitor/command.rs | 2 +- crates/turborepo-lib/src/task_graph/visitor/mod.rs | 2 +- crates/turborepo-lib/src/turbo_json/loader.rs | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename crates/turborepo-lib/src/{micro_frontends.rs => microfrontends.rs} (100%) diff --git a/crates/turborepo-lib/src/lib.rs b/crates/turborepo-lib/src/lib.rs index 4bda1819c4407..5584fa4ba0c83 100644 --- a/crates/turborepo-lib/src/lib.rs +++ b/crates/turborepo-lib/src/lib.rs @@ -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; diff --git a/crates/turborepo-lib/src/micro_frontends.rs b/crates/turborepo-lib/src/microfrontends.rs similarity index 100% rename from crates/turborepo-lib/src/micro_frontends.rs rename to crates/turborepo-lib/src/microfrontends.rs diff --git a/crates/turborepo-lib/src/run/builder.rs b/crates/turborepo-lib/src/run/builder.rs index 650a2d0a68810..59f13c71fb73c 100644 --- a/crates/turborepo-lib/src/run/builder.rs +++ b/crates/turborepo-lib/src/run/builder.rs @@ -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}, diff --git a/crates/turborepo-lib/src/run/mod.rs b/crates/turborepo-lib/src/run/mod.rs index 869b0bb785ee9..834da32767b73 100644 --- a/crates/turborepo-lib/src/run/mod.rs +++ b/crates/turborepo-lib/src/run/mod.rs @@ -41,7 +41,7 @@ pub use crate::run::error::Error; use crate::{ cli::EnvMode, engine::Engine, - micro_frontends::MicrofrontendsConfigs, + microfrontends::MicrofrontendsConfigs, opts::Opts, process::ProcessManager, run::{global_hash::get_global_hash_inputs, summary::RunTracker, task_access::TaskAccess}, diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index cb7770bb0033a..98427e2a900c1 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -7,7 +7,7 @@ use turborepo_repository::package_graph::{PackageGraph, PackageInfo, PackageName use super::Error; use crate::{ - engine::Engine, micro_frontends::MicrofrontendsConfigs, opts::TaskArgs, process::Command, + engine::Engine, microfrontends::MicrofrontendsConfigs, opts::TaskArgs, process::Command, run::task_id::TaskId, }; diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index df3100224d7c1..d014c10d4d412 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -35,7 +35,7 @@ use turborepo_ui::{ use crate::{ cli::EnvMode, engine::{Engine, ExecutionOptions}, - micro_frontends::MicrofrontendsConfigs, + microfrontends::MicrofrontendsConfigs, opts::RunOpts, process::ProcessManager, run::{ diff --git a/crates/turborepo-lib/src/turbo_json/loader.rs b/crates/turborepo-lib/src/turbo_json/loader.rs index eb8f92690a0a4..caab0d837b939 100644 --- a/crates/turborepo-lib/src/turbo_json/loader.rs +++ b/crates/turborepo-lib/src/turbo_json/loader.rs @@ -12,7 +12,7 @@ use super::{Pipeline, RawTaskDefinition, TurboJson, CONFIG_FILE}; use crate::{ cli::EnvMode, config::Error, - micro_frontends::MicrofrontendsConfigs, + microfrontends::MicrofrontendsConfigs, run::{task_access::TASK_ACCESS_CONFIG_PATH, task_id::TaskName}, }; From c0299c3d49e4caf372d903e9e7cd834eafd7c320 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 26 Nov 2024 09:15:10 -0600 Subject: [PATCH 12/14] chore(mfe): include correct config filename in error message --- crates/turborepo-lib/src/microfrontends.rs | 10 +++ .../src/task_graph/visitor/command.rs | 2 + .../src/task_graph/visitor/mod.rs | 8 ++- .../turborepo-microfrontends/src/configv1.rs | 4 -- crates/turborepo-microfrontends/src/lib.rs | 68 ++++++++++++++----- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/crates/turborepo-lib/src/microfrontends.rs b/crates/turborepo-lib/src/microfrontends.rs index 5788da64128db..3b2cc3374eeb4 100644 --- a/crates/turborepo-lib/src/microfrontends.rs +++ b/crates/turborepo-lib/src/microfrontends.rs @@ -15,6 +15,7 @@ use crate::{ #[derive(Debug, Clone)] pub struct MicrofrontendsConfigs { configs: HashMap>>, + config_filenames: HashMap, mfe_package: Option<&'static str>, } @@ -24,6 +25,7 @@ impl MicrofrontendsConfigs { package_graph: &PackageGraph, ) -> Result, Error> { let mut configs = HashMap::new(); + let mut config_filenames = HashMap::new(); let mut referenced_default_apps = HashSet::new(); for (package_name, package_info) in package_graph.packages() { let package_dir = repo_root.resolve(package_info.package_path()); @@ -49,6 +51,7 @@ impl MicrofrontendsConfigs { }) .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 @@ -77,6 +80,7 @@ impl MicrofrontendsConfigs { Ok((!configs.is_empty()).then_some(Self { configs, + config_filenames, mfe_package, })) } @@ -99,6 +103,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, @@ -265,6 +274,7 @@ mod test { ); let mfe = MicrofrontendsConfigs { configs, + config_filenames: HashMap::new(), mfe_package: None, }; assert_eq!( diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index 98427e2a900c1..868fb5eca58cd 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -208,8 +208,10 @@ impl<'a> CommandProvider for MicroFrontendProxyProvider<'a> { .all_dependencies() .any(|(package, _version)| MICROFRONTENDS_PACKAGES.contains(&package.as_str())); if !has_mfe_dependency { + let mfe_config_filename = self.mfe_configs.config_filename(task_id.package()); return Err(Error::MissingMFEDependency { package: task_id.package().into(), + mfe_config_filename: mfe_config_filename.unwrap_or_default().to_owned(), }); } let local_apps = dev_tasks diff --git a/crates/turborepo-lib/src/task_graph/visitor/mod.rs b/crates/turborepo-lib/src/task_graph/visitor/mod.rs index d014c10d4d412..8bb69aad75f74 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/mod.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/mod.rs @@ -23,7 +23,6 @@ use tracing::{debug, error, warn, Span}; use turbopath::{AbsoluteSystemPath, AnchoredSystemPath}; use turborepo_ci::{Vendor, VendorBehavior}; use turborepo_env::{platform::PlatformEnv, EnvironmentVariableMap}; -use turborepo_microfrontends::DEFAULT_MICROFRONTENDS_CONFIG_V1; use turborepo_repository::package_graph::{PackageGraph, PackageName, ROOT_PKG_NAME}; use turborepo_telemetry::events::{ generic::GenericEventBuilder, task::PackageTaskEventBuilder, EventBuilder, TrackedErrors, @@ -101,10 +100,13 @@ pub enum Error { #[error("unable to find package manager binary: {0}")] Which(#[from] which::Error), #[error( - "'{package}' is configured with a {DEFAULT_MICROFRONTENDS_CONFIG_V1}, but doesn't have \ + "'{package}' is configured with a {mfe_config_filename}, but doesn't have \ '@vercel/microfrontends' listed as a dependency" )] - MissingMFEDependency { package: String }, + MissingMFEDependency { + package: String, + mfe_config_filename: String, + }, } impl<'a> Visitor<'a> { diff --git a/crates/turborepo-microfrontends/src/configv1.rs b/crates/turborepo-microfrontends/src/configv1.rs index 9c2962c937fe1..83a98b6fb5a5c 100644 --- a/crates/turborepo-microfrontends/src/configv1.rs +++ b/crates/turborepo-microfrontends/src/configv1.rs @@ -47,10 +47,6 @@ impl ConfigV1 { } } - pub fn applications(&self) -> impl Iterator { - self.applications.iter() - } - pub fn development_tasks(&self) -> impl Iterator)> { self.applications .iter() diff --git a/crates/turborepo-microfrontends/src/lib.rs b/crates/turborepo-microfrontends/src/lib.rs index 7bb4a427e80fb..f93068d01defd 100644 --- a/crates/turborepo-microfrontends/src/lib.rs +++ b/crates/turborepo-microfrontends/src/lib.rs @@ -31,7 +31,13 @@ pub const SUPPORTED_VERSIONS: &[&str] = ["1", "2"].as_slice(); /// The minimal amount of information Turborepo needs to correctly start a local /// proxy server for microfrontends #[derive(Debug, PartialEq, Eq)] -pub enum Config { +pub struct Config { + inner: ConfigInner, + filename: String, +} + +#[derive(Debug, PartialEq, Eq)] +enum ConfigInner { V1(ConfigV1), V2(ConfigV2), } @@ -73,25 +79,34 @@ impl Config { None => "2".to_string(), }; - match version.as_str() { - "1" => ConfigV1::from_str(input, source).map(Config::V1), + let inner = match version.as_str() { + "1" => ConfigV1::from_str(input, source).map(ConfigInner::V1), "2" => ConfigV2::from_str(input, source).and_then(|result| match result { - configv2::ParseResult::Actual(config_v2) => Ok(Config::V2(config_v2)), + configv2::ParseResult::Actual(config_v2) => Ok(ConfigInner::V2(config_v2)), configv2::ParseResult::Reference(default_app) => Err(Error::ChildConfig { reference: default_app, }), }), version => Err(Error::UnsupportedVersion(version.to_string())), - } + }?; + Ok(Self { + inner, + filename: source.to_owned(), + }) } pub fn development_tasks<'a>(&'a self) -> Box)> + 'a> { - match self { - Config::V1(config_v1) => Box::new(config_v1.development_tasks()), - Config::V2(config_v2) => Box::new(config_v2.development_tasks()), + match &self.inner { + ConfigInner::V1(config_v1) => Box::new(config_v1.development_tasks()), + ConfigInner::V2(config_v2) => Box::new(config_v2.development_tasks()), } } + /// Filename of the loaded configuration + pub fn filename(&self) -> &str { + &self.filename + } + fn load_v2_dir(dir: &AbsoluteSystemPath) -> Result, Error> { let load_config = |filename: &str| -> Option<(Result, AbsoluteSystemPathBuf)> { @@ -108,7 +123,13 @@ impl Config { ConfigV2::from_str(&contents, path.as_str()) .and_then(|result| match result { - configv2::ParseResult::Actual(config_v2) => Ok(Config::V2(config_v2)), + configv2::ParseResult::Actual(config_v2) => Ok(Config { + inner: ConfigInner::V2(config_v2), + filename: path + .file_name() + .expect("microfrontends config should not be root") + .to_owned(), + }), configv2::ParseResult::Reference(default_app) => Err(Error::ChildConfig { reference: default_app, }), @@ -123,7 +144,10 @@ impl Config { }; ConfigV1::from_str(&contents, path.as_str()) - .map(Self::V1) + .map(|config_v1| Self { + inner: ConfigInner::V1(config_v1), + filename: DEFAULT_MICROFRONTENDS_CONFIG_V1.to_owned(), + }) .map(Some) } } @@ -171,8 +195,10 @@ mod test { let dir = TempDir::new().unwrap(); let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); add_v1_config(path).unwrap(); - let config = Config::load_from_dir(path).unwrap(); - assert_matches!(config, Some(Config::V1(_))); + let config = Config::load_from_dir(path) + .unwrap() + .map(|config| config.inner); + assert_matches!(config, Some(ConfigInner::V1(_))); } #[test] @@ -180,8 +206,10 @@ mod test { let dir = TempDir::new().unwrap(); let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); add_v2_config(path).unwrap(); - let config = Config::load_from_dir(path).unwrap(); - assert_matches!(config, Some(Config::V2(_))); + let config = Config::load_from_dir(path) + .unwrap() + .map(|config| config.inner); + assert_matches!(config, Some(ConfigInner::V2(_))); } #[test] @@ -190,8 +218,10 @@ mod test { let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); add_v1_config(path).unwrap(); add_v2_config(path).unwrap(); - let config = Config::load_from_dir(path).unwrap(); - assert_matches!(config, Some(Config::V2(_))); + let config = Config::load_from_dir(path) + .unwrap() + .map(|config| config.inner); + assert_matches!(config, Some(ConfigInner::V2(_))); } #[test] @@ -199,8 +229,10 @@ mod test { let dir = TempDir::new().unwrap(); let path = AbsoluteSystemPath::new(dir.path().to_str().unwrap()).unwrap(); add_v2_alt_config(path).unwrap(); - let config = Config::load_from_dir(path).unwrap(); - assert_matches!(config, Some(Config::V2(_))); + let config = Config::load_from_dir(path) + .unwrap() + .map(|config| config.inner); + assert_matches!(config, Some(ConfigInner::V2(_))); } #[test] From 9b5d104d0692d517f9eed043a23e3e74fde3a94c Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 26 Nov 2024 17:24:13 -0600 Subject: [PATCH 13/14] chore(mfe): break out mfe config loading to be testable --- crates/turborepo-lib/src/microfrontends.rs | 134 ++++++++++++++------- 1 file changed, 88 insertions(+), 46 deletions(-) diff --git a/crates/turborepo-lib/src/microfrontends.rs b/crates/turborepo-lib/src/microfrontends.rs index 3b2cc3374eeb4..b487414fda077 100644 --- a/crates/turborepo-lib/src/microfrontends.rs +++ b/crates/turborepo-lib/src/microfrontends.rs @@ -24,59 +24,35 @@ impl MicrofrontendsConfigs { repo_root: &AbsoluteSystemPath, package_graph: &PackageGraph, ) -> Result, Error> { - let mut configs = HashMap::new(); - let mut config_filenames = HashMap::new(); - let mut referenced_default_apps = HashSet::new(); - for (package_name, package_info) in package_graph.packages() { - let package_dir = repo_root.resolve(package_info.package_path()); - let Some(config) = MFEConfig::load_from_dir(&package_dir).or_else(|err| match err { - turborepo_microfrontends::Error::UnsupportedVersion(_) => { - warn!("Ignoring {package_dir}: {err}"); - 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 PackageGraphResult { + configs, + config_filenames, + missing_default_apps, + unsupported_version, + mfe_package, + } = PackageGraphResult::new( + package_graph + .packages() + // We sort packages to make sure we have a deterministic ordering for any warnings + .sorted_by(|(a, _), (b, _)| a.cmp(b)) + .map(|(name, info)| { + ( + name, + MFEConfig::load_from_dir(&repo_root.resolve(info.package_path())), + ) + }), + )?; + + for (package, err) in unsupported_version { + warn!("Ignoring {package}: {err}"); } - let default_apps_found = configs.keys().cloned().collect(); - let mut missing_default_apps = referenced_default_apps - .difference(&default_apps_found) - .map(|s| s.as_str()) - .collect::>(); + if !missing_default_apps.is_empty() { - missing_default_apps.sort(); warn!( "Missing default applications: {}", missing_default_apps.join(", ") ); } - 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| { - MICROFRONTENDS_PACKAGES - .iter() - .find(|static_pkg| pkg == **static_pkg) - }) - .copied(); Ok((!configs.is_empty()).then_some(Self { configs, @@ -164,6 +140,72 @@ impl MicrofrontendsConfigs { } } +// Internal struct used to capture the results of checking the package graph +struct PackageGraphResult { + configs: HashMap>>, + config_filenames: HashMap, + missing_default_apps: Vec, + unsupported_version: Vec<(String, String)>, + mfe_package: Option<&'static str>, +} + +impl PackageGraphResult { + fn new<'a>( + packages: impl Iterator, Error>)>, + ) -> Result { + 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; + for (package_name, config) in packages { + if let Some(pkg) = MICROFRONTENDS_PACKAGES + .iter() + .find(|static_pkg| package_name.as_str() == **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::>(); + 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>, From 18f976ff4b065be0191adc5e4dfeb45b63282e5b Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Tue, 3 Dec 2024 16:05:17 -0500 Subject: [PATCH 14/14] chore(mfe): add config loading tests --- crates/turborepo-lib/src/microfrontends.rs | 128 ++++++++++++++++-- .../src/task_graph/visitor/command.rs | 6 +- crates/turborepo-microfrontends/src/lib.rs | 2 +- 3 files changed, 119 insertions(+), 17 deletions(-) diff --git a/crates/turborepo-lib/src/microfrontends.rs b/crates/turborepo-lib/src/microfrontends.rs index b487414fda077..77f0917fc1705 100644 --- a/crates/turborepo-lib/src/microfrontends.rs +++ b/crates/turborepo-lib/src/microfrontends.rs @@ -30,18 +30,12 @@ impl MicrofrontendsConfigs { missing_default_apps, unsupported_version, mfe_package, - } = PackageGraphResult::new( - package_graph - .packages() - // We sort packages to make sure we have a deterministic ordering for any warnings - .sorted_by(|(a, _), (b, _)| a.cmp(b)) - .map(|(name, info)| { - ( - name, - MFEConfig::load_from_dir(&repo_root.resolve(info.package_path())), - ) - }), - )?; + } = 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}"); @@ -151,17 +145,19 @@ struct PackageGraphResult { impl PackageGraphResult { fn new<'a>( - packages: impl Iterator, Error>)>, + packages: impl Iterator, Error>)>, ) -> Result { 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; - for (package_name, config) in packages { + // 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.as_str() == **static_pkg) + .find(|static_pkg| package_name == **static_pkg) { mfe_package = Some(*pkg); } @@ -214,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::*; @@ -324,4 +324,102 @@ mod test { 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"] + ) + ) + } } diff --git a/crates/turborepo-lib/src/task_graph/visitor/command.rs b/crates/turborepo-lib/src/task_graph/visitor/command.rs index 868fb5eca58cd..2eaf2d6f05d44 100644 --- a/crates/turborepo-lib/src/task_graph/visitor/command.rs +++ b/crates/turborepo-lib/src/task_graph/visitor/command.rs @@ -219,7 +219,11 @@ impl<'a> CommandProvider for MicroFrontendProxyProvider<'a> { .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 mfe_config_filename = self + .mfe_configs + .config_filename(task_id.package()) + .expect("every microfrontends default application should have configuration path"); + let mfe_path = package_dir.join_component(mfe_config_filename); let mut args = vec!["proxy", mfe_path.as_str(), "--names"]; args.extend(local_apps); diff --git a/crates/turborepo-microfrontends/src/lib.rs b/crates/turborepo-microfrontends/src/lib.rs index f93068d01defd..58581faf6e273 100644 --- a/crates/turborepo-microfrontends/src/lib.rs +++ b/crates/turborepo-microfrontends/src/lib.rs @@ -75,7 +75,7 @@ impl Config { let version = match version_only { Some(VersionOnly { version }) => version, - // Default to version 1 if no version found + // Default to version 2 if no version found None => "2".to_string(), };