Skip to content

Commit 6d1704f

Browse files
committed
strict parsing
1 parent 82cbb80 commit 6d1704f

File tree

7 files changed

+160
-20
lines changed

7 files changed

+160
-20
lines changed

crates/turborepo-lib/src/microfrontends.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ mod test {
661661
#[test]
662662
fn test_use_turborepo_proxy_false_when_package_has_mfe_dependency() {
663663
// Create a microfrontends config
664-
let config = MFEConfig::from_str(
664+
let config = MfeConfig::from_str(
665665
&serde_json::to_string_pretty(&json!({
666666
"applications": {
667667
"web": {},
@@ -770,7 +770,7 @@ mod test {
770770
#[test]
771771
fn test_config_in_correct_package() {
772772
// Config file is in "web" package, and "web" is the root route app (no routing)
773-
let config = MFEConfig::from_str(
773+
let config = MfeConfig::from_str(
774774
&serde_json::to_string_pretty(&json!({
775775
"applications": {
776776
"web": {},
@@ -796,7 +796,7 @@ mod test {
796796
#[test]
797797
fn test_config_in_wrong_package() {
798798
// Config file is in "docs" package, but "web" is the root route app
799-
let config = MFEConfig::from_str(
799+
let config = MfeConfig::from_str(
800800
&serde_json::to_string_pretty(&json!({
801801
"applications": {
802802
"web": {},
@@ -829,7 +829,7 @@ mod test {
829829
fn test_config_with_package_name_mapping() {
830830
// Config file is in "marketing" package, which is where "web" app (root route)
831831
// is actually implemented
832-
let config = MFEConfig::from_str(
832+
let config = MfeConfig::from_str(
833833
&serde_json::to_string_pretty(&json!({
834834
"applications": {
835835
"web": {
@@ -861,7 +861,7 @@ mod test {
861861
fn test_config_with_package_name_mapping_in_wrong_package() {
862862
// Config file is in "docs" package, but "marketing" maps to "web" app (root
863863
// route)
864-
let config = MFEConfig::from_str(
864+
let config = MfeConfig::from_str(
865865
&serde_json::to_string_pretty(&json!({
866866
"applications": {
867867
"web": {
@@ -894,7 +894,7 @@ mod test {
894894

895895
#[test]
896896
fn test_task_uses_turborepo_proxy_when_enabled() {
897-
let config = MFEConfig::from_str(
897+
let config = MfeConfig::from_str(
898898
&serde_json::to_string_pretty(&json!({
899899
"applications": {
900900
"web": {},
@@ -942,7 +942,7 @@ mod test {
942942

943943
#[test]
944944
fn test_turbo_mfe_port_with_port_number() {
945-
let config = MFEConfig::from_str(
945+
let config = MfeConfig::from_str(
946946
&serde_json::to_string_pretty(&json!({
947947
"applications": {
948948
"web": {
@@ -980,7 +980,7 @@ mod test {
980980

981981
#[test]
982982
fn test_turbo_mfe_port_with_url_string() {
983-
let config = MFEConfig::from_str(
983+
let config = MfeConfig::from_str(
984984
&serde_json::to_string_pretty(&json!({
985985
"applications": {
986986
"web": {

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

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

88
use chrono::Local;
9-
use tracing::{debug, warn};
9+
use tracing::debug;
1010
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf};
1111
use turborepo_analytics::{start_analytics, AnalyticsHandle, AnalyticsSender};
1212
use turborepo_api_client::{APIAuth, APIClient};
@@ -377,12 +377,8 @@ impl RunBuilder {
377377
let micro_frontend_configs =
378378
match MicrofrontendsConfigs::from_disk(&self.repo_root, &pkg_dep_graph) {
379379
Ok(configs) => configs,
380-
Err(err @ turborepo_microfrontends::Error::ConfigInWrongPackage { .. }) => {
381-
return Err(Error::MicroFrontends(err));
382-
}
383380
Err(err) => {
384-
warn!("Ignoring invalid microfrontends configuration: {err}");
385-
None
381+
return Err(Error::MicroFrontends(err));
386382
}
387383
};
388384

crates/turborepo-lib/src/task_graph/visitor/command.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ mod test {
305305

306306
use insta::assert_snapshot;
307307
use turbopath::AnchoredSystemPath;
308-
use turborepo_microfrontends::Config;
308+
use turborepo_microfrontends::TurborepoMfeConfig as Config;
309309
use turborepo_repository::package_json::PackageJson;
310310

311311
use super::*;
@@ -421,7 +421,7 @@ mod test {
421421
}
422422
}
423423
}
424-
let mut config = Config::from_str(
424+
let config = Config::from_str(
425425
r#"
426426
{
427427
"applications": {
@@ -437,7 +437,6 @@ mod test {
437437
"microfrontends.json",
438438
)
439439
.unwrap();
440-
config.set_path(AnchoredSystemPath::new("microfrontends.json").unwrap());
441440
let microfrontends_configs = MicrofrontendsConfigs::from_configs(
442441
["web", "docs"].iter().copied().collect(),
443442
std::iter::once(("web", Ok(Some(config)))),

crates/turborepo-lib/src/turbo_json/loader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ mod test {
865865
vec![
866866
(
867867
"web",
868-
turborepo_microfrontends::Config::from_str(
868+
turborepo_microfrontends::TurborepoMfeConfig::from_str(
869869
r#"{"version": "1", "applications": {"web": {}, "docs": {"routing": [{"paths": ["/docs"]}]}}}"#,
870870
"mfe.json",
871871
)

crates/turborepo-microfrontends/src/configv1.rs

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub enum ParseResult {
1313

1414
#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default, Clone)]
1515
pub struct ConfigV1 {
16+
#[serde(rename = "$schema", skip)]
17+
schema: Option<String>,
1618
version: Option<String>,
1719
applications: BTreeMap<String, Application>,
1820
options: Option<Options>,
@@ -47,10 +49,16 @@ pub struct PathGroup {
4749
}
4850

4951
#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default, Clone)]
50-
struct ProductionConfig {}
52+
struct ProductionConfig {
53+
protocol: Option<String>,
54+
host: Option<String>,
55+
}
5156

5257
#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default, Clone)]
53-
struct VercelConfig {}
58+
struct VercelConfig {
59+
#[serde(rename = "projectId")]
60+
project_id: Option<String>,
61+
}
5462

5563
#[derive(Debug, PartialEq, Eq, Serialize, Deserializable, Default, Clone)]
5664
struct Development {
@@ -152,6 +160,10 @@ impl ConfigV1 {
152160
.consume();
153161

154162
if let Some(config) = config {
163+
// Only accept the config if there were no errors during parsing
164+
if !errs.is_empty() {
165+
return Err(Error::biome_error(errs));
166+
}
155167
// Accept any version. This allows the Turborepo proxy to work with
156168
// configurations that have different version numbers than expected,
157169
// as long as the structure is compatible with what Turborepo needs
@@ -200,6 +212,7 @@ impl ConfigV1 {
200212
local_proxy_port: Some(port),
201213
disable_overrides: None,
202214
}),
215+
schema: None,
203216
}
204217
}
205218

@@ -585,4 +598,73 @@ mod test {
585598
ParseResult::Reference(_) => panic!("expected to get main config"),
586599
}
587600
}
601+
602+
#[test]
603+
fn test_malformed_json_unclosed_bracket() {
604+
let input = r#"{"applications": {"web": {"development": {"local": 3000}}"#;
605+
let config = ConfigV1::from_str(input, "microfrontends.json");
606+
assert!(
607+
config.is_err(),
608+
"Parser should reject JSON with unclosed bracket"
609+
);
610+
}
611+
612+
#[test]
613+
fn test_malformed_json_trailing_comma() {
614+
let input = r#"{"applications": {"web": {"development": {"local": 3000,}}}}"#;
615+
let config = ConfigV1::from_str(input, "microfrontends.json");
616+
assert!(
617+
config.is_err(),
618+
"Parser should reject JSON with trailing comma"
619+
);
620+
}
621+
622+
#[test]
623+
fn test_missing_required_applications() {
624+
// Even though applications has defaults, if JSON structure is invalid it should
625+
// fail
626+
let input = r#"{"applications": {, "web": {}}}"#;
627+
let config = ConfigV1::from_str(input, "microfrontends.json");
628+
assert!(
629+
config.is_err(),
630+
"Parser should reject JSON with syntax errors"
631+
);
632+
}
633+
634+
#[test]
635+
fn test_invalid_routing_structure() {
636+
let input = r#"{
637+
"applications": {
638+
"docs": {
639+
"routing": "invalid"
640+
}
641+
}
642+
}"#;
643+
let config = ConfigV1::from_str(input, "microfrontends.json");
644+
assert!(
645+
config.is_err(),
646+
"Parser should reject routing that is not an array"
647+
);
648+
}
649+
650+
#[test]
651+
fn test_invalid_path_group_structure() {
652+
let input = r#"{
653+
"applications": {
654+
"docs": {
655+
"routing": [
656+
{
657+
"group": "docs",
658+
"paths": "should_be_array"
659+
}
660+
]
661+
}
662+
}
663+
}"#;
664+
let config = ConfigV1::from_str(input, "microfrontends.json");
665+
assert!(
666+
config.is_err(),
667+
"Parser should reject paths that is not an array"
668+
);
669+
}
588670
}

crates/turborepo-microfrontends/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ impl Config {
277277
)
278278
.consume();
279279

280+
// If version extraction had errors, we should still try to parse the full
281+
// config, but we won't let those errors be silently ignored in the full
282+
// parse below.
280283
let version = match version_only {
281284
Some(VersionOnly {
282285
version: Some(version),

crates/turborepo-microfrontends/src/turborepo_schema.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ impl TurborepoConfig {
122122
.consume();
123123

124124
if let Some(config) = config {
125+
// Only accept the config if there were no errors during parsing
126+
if !errs.is_empty() {
127+
return Err(Error::biome_error(errs));
128+
}
125129
Ok(config)
126130
} else {
127131
Err(Error::biome_error(errs))
@@ -261,4 +265,60 @@ mod test {
261265
let config = TurborepoConfig::from_str(input, "somewhere").unwrap();
262266
assert_eq!(config.port("web"), Some(3000));
263267
}
268+
269+
#[test]
270+
fn test_malformed_json_unclosed_bracket() {
271+
let input = r#"{"applications": {"web": {"development": {"local": 3000}}"#;
272+
let config = TurborepoConfig::from_str(input, "somewhere");
273+
assert!(
274+
config.is_err(),
275+
"Parser should reject JSON with unclosed bracket"
276+
);
277+
}
278+
279+
#[test]
280+
fn test_malformed_json_trailing_comma() {
281+
let input = r#"{"applications": {"web": {"development": {"local": 3000,}}}}"#;
282+
let config = TurborepoConfig::from_str(input, "somewhere");
283+
assert!(
284+
config.is_err(),
285+
"Parser should reject JSON with trailing comma"
286+
);
287+
}
288+
289+
#[test]
290+
fn test_invalid_routing_type() {
291+
let input = r#"{
292+
"applications": {
293+
"docs": {
294+
"routing": "should_be_array"
295+
}
296+
}
297+
}"#;
298+
let config = TurborepoConfig::from_str(input, "somewhere");
299+
assert!(
300+
config.is_err(),
301+
"Parser should reject routing that is not an array"
302+
);
303+
}
304+
305+
#[test]
306+
fn test_invalid_paths_structure() {
307+
let input = r#"{
308+
"applications": {
309+
"docs": {
310+
"routing": [
311+
{
312+
"paths": "should_be_array"
313+
}
314+
]
315+
}
316+
}
317+
}"#;
318+
let config = TurborepoConfig::from_str(input, "somewhere");
319+
assert!(
320+
config.is_err(),
321+
"Parser should reject paths that is not an array"
322+
);
323+
}
264324
}

0 commit comments

Comments
 (0)