Skip to content

Commit 4c346b9

Browse files
fix(microfrontends): respect packageName field (#10383)
### Description This PR does 2 things: - Adds a warning if a package is referenced in `microfrontends.json` that is not present in the monorepo - Uses `packageName` over the `applications` key if present ### Testing Instructions Added unit tests. Verified warnings now are present if you reference a package that isn't found in the monorepo: ``` [0 olszewski@macbookpro] /Users/olszewski/code/vercel/microfrontends $ turbo_dev --skip-infer dev -F nextjs-app-marketing -F nextjs-app-docs --dry=json > /dev/null turbo 2.5.1 WARNING Unable to find packages referenced in 'microfrontends.json' in workspace.Local proxy will not route to the following applications if they are running locally: nextjs-app-marketing-test ``` Verified that proxy is now started if every application uses a `packageName` that differs from the key in `applications`. --------- Co-authored-by: Thomas Knickman <[email protected]>
1 parent a0970fb commit 4c346b9

File tree

7 files changed

+341
-73
lines changed

7 files changed

+341
-73
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ impl Engine<Built> {
394394
self.task_graph.node_weights()
395395
}
396396

397+
pub fn task_ids(&self) -> impl Iterator<Item = &TaskId<'static>> {
398+
self.tasks().filter_map(|task| match task {
399+
crate::engine::TaskNode::Task(task_id) => Some(task_id),
400+
crate::engine::TaskNode::Root => None,
401+
})
402+
}
403+
397404
/// Return all tasks that have a command to be run
398405
pub fn tasks_with_command(&self, pkg_graph: &PackageGraph) -> Vec<String> {
399406
self.tasks()

crates/turborepo-lib/src/microfrontends.rs

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ pub struct MicrofrontendsConfigs {
2020

2121
#[derive(Debug, Clone, Default, PartialEq)]
2222
struct ConfigInfo {
23-
tasks: HashSet<TaskId<'static>>,
23+
// A map from tasks declared in the configuration to the application that they belong to
24+
tasks: HashMap<TaskId<'static>, String>,
2425
ports: HashMap<TaskId<'static>, u16>,
2526
version: &'static str,
2627
path: Option<RelativeUnixPathBuf>,
@@ -32,24 +33,33 @@ impl MicrofrontendsConfigs {
3233
repo_root: &AbsoluteSystemPath,
3334
package_graph: &PackageGraph,
3435
) -> Result<Option<Self>, Error> {
35-
Self::from_configs(package_graph.packages().map(|(name, info)| {
36-
(
37-
name.as_str(),
38-
MFEConfig::load_from_dir(repo_root, info.package_path()),
39-
)
40-
}))
36+
let package_names = package_graph
37+
.packages()
38+
.map(|(name, _)| name.as_str())
39+
.collect();
40+
Self::from_configs(
41+
package_names,
42+
package_graph.packages().map(|(name, info)| {
43+
(
44+
name.as_str(),
45+
MFEConfig::load_from_dir(repo_root, info.package_path()),
46+
)
47+
}),
48+
)
4149
}
4250

4351
/// Constructs a collection of configurations from a list of configurations
4452
pub fn from_configs<'a>(
53+
package_names: HashSet<&str>,
4554
configs: impl Iterator<Item = (&'a str, Result<Option<MFEConfig>, Error>)>,
4655
) -> Result<Option<Self>, Error> {
4756
let PackageGraphResult {
4857
configs,
4958
missing_default_apps,
59+
missing_applications,
5060
unsupported_version,
5161
mfe_package,
52-
} = PackageGraphResult::new(configs)?;
62+
} = PackageGraphResult::new(package_names, configs)?;
5363

5464
for (package, err) in unsupported_version {
5565
warn!("Ignoring {package}: {err}");
@@ -62,29 +72,34 @@ impl MicrofrontendsConfigs {
6272
);
6373
}
6474

75+
if !missing_applications.is_empty() {
76+
warn!(
77+
"Unable to find packages referenced in 'microfrontends.json' in workspace. Local \
78+
proxy will not route to the following applications if they are running locally: \
79+
{}",
80+
missing_applications.join(", ")
81+
);
82+
}
83+
6584
Ok((!configs.is_empty()).then_some(Self {
6685
configs,
6786
mfe_package,
6887
}))
6988
}
7089

71-
pub fn contains_package(&self, package_name: &str) -> bool {
72-
self.configs.contains_key(package_name)
73-
}
74-
75-
pub fn configs(&self) -> impl Iterator<Item = (&String, &HashSet<TaskId<'static>>)> {
90+
pub fn configs(&self) -> impl Iterator<Item = (&String, &HashMap<TaskId<'static>, String>)> {
7691
self.configs.iter().map(|(pkg, info)| (pkg, &info.tasks))
7792
}
7893

79-
pub fn get(&self, package_name: &str) -> Option<&HashSet<TaskId<'static>>> {
94+
pub fn get(&self, package_name: &str) -> Option<&HashMap<TaskId<'static>, String>> {
8095
let info = self.configs.get(package_name)?;
8196
Some(&info.tasks)
8297
}
8398

8499
pub fn task_has_mfe_proxy(&self, task_id: &TaskId) -> bool {
85100
self.configs
86101
.values()
87-
.any(|info| info.tasks.contains(task_id))
102+
.any(|info| info.tasks.contains_key(task_id))
88103
}
89104

90105
pub fn config_filename(&self, package_name: &str) -> Option<&RelativeUnixPath> {
@@ -152,7 +167,7 @@ impl MicrofrontendsConfigs {
152167
package_name: &PackageName,
153168
) -> Option<FindResult<'a>> {
154169
let results = self.configs.iter().filter_map(|(config, info)| {
155-
let dev_task = info.tasks.iter().find_map(|task| {
170+
let dev_task = info.tasks.iter().find_map(|(task, _)| {
156171
(task.package() == package_name.as_str()).then(|| FindResult {
157172
dev: Some(task.as_borrowed()),
158173
proxy: TaskId::new(config, "proxy"),
@@ -186,16 +201,19 @@ impl MicrofrontendsConfigs {
186201
struct PackageGraphResult {
187202
configs: HashMap<String, ConfigInfo>,
188203
missing_default_apps: Vec<String>,
204+
missing_applications: Vec<String>,
189205
unsupported_version: Vec<(String, String)>,
190206
mfe_package: Option<&'static str>,
191207
}
192208

193209
impl PackageGraphResult {
194210
fn new<'a>(
211+
packages_in_graph: HashSet<&str>,
195212
packages: impl Iterator<Item = (&'a str, Result<Option<MFEConfig>, Error>)>,
196213
) -> Result<Self, Error> {
197214
let mut configs = HashMap::new();
198215
let mut referenced_default_apps = HashSet::new();
216+
let mut referenced_packages = HashSet::new();
199217
let mut unsupported_version = Vec::new();
200218
let mut mfe_package = None;
201219
// We sort packages to ensure deterministic behavior
@@ -223,6 +241,8 @@ impl PackageGraphResult {
223241
if let Some(path) = config.path() {
224242
info.path = Some(path.to_unix());
225243
}
244+
referenced_packages.insert(package_name.to_string());
245+
referenced_packages.extend(info.tasks.keys().map(|task| task.package().to_string()));
226246
configs.insert(package_name.to_string(), info);
227247
}
228248
let default_apps_found = configs.keys().cloned().collect();
@@ -231,9 +251,15 @@ impl PackageGraphResult {
231251
.cloned()
232252
.collect::<Vec<_>>();
233253
missing_default_apps.sort();
254+
let mut missing_applications = referenced_packages
255+
.into_iter()
256+
.filter(|package| !packages_in_graph.contains(package.as_str()))
257+
.collect::<Vec<_>>();
258+
missing_applications.sort();
234259
Ok(Self {
235260
configs,
236261
missing_default_apps,
262+
missing_applications,
237263
unsupported_version,
238264
mfe_package,
239265
})
@@ -250,13 +276,13 @@ struct FindResult<'a> {
250276
impl ConfigInfo {
251277
fn new(config: &MFEConfig) -> Self {
252278
let mut ports = HashMap::new();
253-
let mut tasks = HashSet::new();
254-
for (application, dev_task) in config.development_tasks() {
255-
let task = TaskId::new(application, dev_task.unwrap_or("dev")).into_owned();
256-
if let Some(port) = config.port(application) {
279+
let mut tasks = HashMap::new();
280+
for dev_task in config.development_tasks() {
281+
let task = TaskId::new(dev_task.package, dev_task.task.unwrap_or("dev")).into_owned();
282+
if let Some(port) = config.port(dev_task.application_name) {
257283
ports.insert(task.clone(), port);
258284
}
259-
tasks.insert(task);
285+
tasks.insert(task, dev_task.application_name.to_owned());
260286
}
261287
let version = config.version();
262288

@@ -281,9 +307,11 @@ mod test {
281307
{
282308
let mut _map = std::collections::HashMap::new();
283309
$(
284-
let mut _dev_tasks = std::collections::HashSet::new();
310+
let mut _dev_tasks = std::collections::HashMap::new();
285311
for _dev_task in $dev_tasks.as_slice() {
286-
_dev_tasks.insert(crate::run::task_id::TaskName::from(*_dev_task).task_id().unwrap().into_owned());
312+
let _dev_task_id = crate::run::task_id::TaskName::from(*_dev_task).task_id().unwrap().into_owned();
313+
let _dev_application = _dev_task_id.package().to_owned();
314+
_dev_tasks.insert(_dev_task_id, _dev_application);
287315
}
288316
_map.insert($config_owner.to_string(), ConfigInfo { tasks: _dev_tasks, version: "1", path: None, ports: std::collections::HashMap::new() });
289317
)+
@@ -363,22 +391,28 @@ mod test {
363391

364392
#[test]
365393
fn test_mfe_package_is_found() {
366-
let result =
367-
PackageGraphResult::new(vec![(MICROFRONTENDS_PACKAGE, Ok(None))].into_iter()).unwrap();
394+
let result = PackageGraphResult::new(
395+
HashSet::default(),
396+
vec![(MICROFRONTENDS_PACKAGE, Ok(None))].into_iter(),
397+
)
398+
.unwrap();
368399
assert_eq!(result.mfe_package, Some(MICROFRONTENDS_PACKAGE));
369400
}
370401

371402
#[test]
372403
fn test_no_mfe_package() {
373-
let result =
374-
PackageGraphResult::new(vec![("foo", Ok(None)), ("bar", Ok(None))].into_iter())
375-
.unwrap();
404+
let result = PackageGraphResult::new(
405+
HashSet::default(),
406+
vec![("foo", Ok(None)), ("bar", Ok(None))].into_iter(),
407+
)
408+
.unwrap();
376409
assert_eq!(result.mfe_package, None);
377410
}
378411

379412
#[test]
380413
fn test_unsupported_versions_ignored() {
381414
let result = PackageGraphResult::new(
415+
HashSet::default(),
382416
vec![("foo", Err(Error::UnsupportedVersion("bad version".into())))].into_iter(),
383417
)
384418
.unwrap();
@@ -388,6 +422,7 @@ mod test {
388422
#[test]
389423
fn test_child_configs_with_missing_default() {
390424
let result = PackageGraphResult::new(
425+
HashSet::default(),
391426
vec![(
392427
"child",
393428
Err(Error::ChildConfig {
@@ -404,6 +439,7 @@ mod test {
404439
#[test]
405440
fn test_io_err_stops_traversal() {
406441
let result = PackageGraphResult::new(
442+
HashSet::default(),
407443
vec![
408444
(
409445
"a",
@@ -442,8 +478,11 @@ mod test {
442478
"something.txt",
443479
)
444480
.unwrap();
445-
let mut result =
446-
PackageGraphResult::new(vec![("web", Ok(Some(config)))].into_iter()).unwrap();
481+
let mut result = PackageGraphResult::new(
482+
HashSet::default(),
483+
vec![("web", Ok(Some(config)))].into_iter(),
484+
)
485+
.unwrap();
447486
result
448487
.configs
449488
.values_mut()
@@ -456,6 +495,42 @@ mod test {
456495
)
457496
}
458497

498+
#[test]
499+
fn test_missing_packages() {
500+
let config = MFEConfig::from_str(
501+
&serde_json::to_string_pretty(&json!({
502+
"version": "1",
503+
"applications": {
504+
"web": {},
505+
"docs": {
506+
"development": {
507+
"task": "serve"
508+
}
509+
}
510+
}
511+
}))
512+
.unwrap(),
513+
"something.txt",
514+
)
515+
.unwrap();
516+
let missing_result = PackageGraphResult::new(
517+
HashSet::default(),
518+
vec![("web", Ok(Some(config.clone())))].into_iter(),
519+
)
520+
.unwrap();
521+
assert_eq!(missing_result.missing_applications, vec!["docs", "web"]);
522+
let found_result = PackageGraphResult::new(
523+
HashSet::from_iter(["docs", "web"].iter().copied()),
524+
vec![("web", Ok(Some(config)))].into_iter(),
525+
)
526+
.unwrap();
527+
assert!(
528+
found_result.missing_applications.is_empty(),
529+
"Expected no missing applications: {:?}",
530+
found_result.missing_applications
531+
);
532+
}
533+
459534
#[test]
460535
fn test_port_collection() {
461536
let config = MFEConfig::from_str(
@@ -477,7 +552,11 @@ mod test {
477552
"something.txt",
478553
)
479554
.unwrap();
480-
let result = PackageGraphResult::new(vec![("web", Ok(Some(config)))].into_iter()).unwrap();
555+
let result = PackageGraphResult::new(
556+
HashSet::default(),
557+
vec![("web", Ok(Some(config)))].into_iter(),
558+
)
559+
.unwrap();
481560
let web_ports = result.configs["web"].ports.clone();
482561
assert_eq!(
483562
web_ports.get(&TaskId::new("docs", "serve")).copied(),

0 commit comments

Comments
 (0)