@@ -26,8 +26,6 @@ mod configv1;
2626mod error;
2727mod schema;
2828
29- use std:: io;
30-
3129use configv1:: ConfigV1 ;
3230pub use configv1:: PathGroup ;
3331pub use error:: Error ;
@@ -43,6 +41,7 @@ pub const DEFAULT_MICROFRONTENDS_CONFIG_V1: &str = "microfrontends.json";
4341pub const DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT : & str = "microfrontends.jsonc" ;
4442pub const MICROFRONTENDS_PACKAGE : & str = "@vercel/microfrontends" ;
4543pub const SUPPORTED_VERSIONS : & [ & str ] = [ "1" ] . as_slice ( ) ;
44+ pub const CUSTOM_CONFIG_ENV_VAR : & str = "VC_MICROFRONTENDS_CONFIG_FILE_NAME" ;
4645
4746/// Strict Turborepo-only configuration for the microfrontends proxy.
4847/// This configuration parser only accepts fields that Turborepo's native proxy
@@ -57,6 +56,60 @@ pub struct TurborepoMfeConfig {
5756}
5857
5958impl TurborepoMfeConfig {
59+ /// Validates a custom config filename from environment variable.
60+ /// Returns error if:
61+ /// - Path contains ".." (attempting to traverse up)
62+ /// - Path does not end with ".json" or ".jsonc"
63+ /// - Path starts with "/" (must be relative)
64+ /// - Path contains "/" or "\" (no subdirectories allowed)
65+ fn validate_custom_config_name ( filename : & str ) -> Result < ( ) , Error > {
66+ // Must end with .json or .jsonc
67+ if !filename. ends_with ( ".json" ) && !filename. ends_with ( ".jsonc" ) {
68+ return Err ( Error :: InvalidCustomConfigPath ( format ! (
69+ "{filename}: must be a JSON file ending with .json or .jsonc"
70+ ) ) ) ;
71+ }
72+
73+ // Must not contain directory separators (no subdirectories)
74+ if filename. contains ( '/' ) || filename. contains ( '\\' ) {
75+ return Err ( Error :: InvalidCustomConfigPath ( format ! (
76+ "{filename}: subdirectories not allowed, file must be in package root"
77+ ) ) ) ;
78+ }
79+
80+ // Must not contain path traversal
81+ if filename. contains ( ".." ) {
82+ return Err ( Error :: InvalidCustomConfigPath ( format ! (
83+ "{filename}: path traversal not allowed"
84+ ) ) ) ;
85+ }
86+
87+ // Must be relative (not start with /)
88+ if filename. starts_with ( '/' ) {
89+ return Err ( Error :: InvalidCustomConfigPath ( format ! (
90+ "{filename}: must be relative to package root"
91+ ) ) ) ;
92+ }
93+
94+ Ok ( ( ) )
95+ }
96+
97+ /// Gets the custom config filename from environment variable if set.
98+ /// Returns None if not set, or Error if invalid.
99+ fn get_custom_config_name ( ) -> Result < Option < String > , Error > {
100+ match std:: env:: var ( CUSTOM_CONFIG_ENV_VAR ) {
101+ Ok ( filename) if !filename. is_empty ( ) => {
102+ Self :: validate_custom_config_name ( & filename) ?;
103+ Ok ( Some ( filename) )
104+ }
105+ Ok ( _) => Ok ( None ) , // Empty string means not set
106+ Err ( std:: env:: VarError :: NotPresent ) => Ok ( None ) ,
107+ Err ( std:: env:: VarError :: NotUnicode ( _) ) => Err ( Error :: InvalidCustomConfigPath (
108+ "environment variable contains invalid UTF-8" . to_string ( ) ,
109+ ) ) ,
110+ }
111+ }
112+
60113 /// Reads config from given path using strict Turborepo schema.
61114 /// Returns `Ok(None)` if the file does not exist
62115 pub fn load ( config_path : & AbsoluteSystemPath ) -> Result < Option < Self > , Error > {
@@ -89,10 +142,9 @@ impl TurborepoMfeConfig {
89142
90143 Config :: validate_package_path ( repo_root, & absolute_dir) ?;
91144
92- let Some ( ( contents, path) ) = Self :: load_v1_dir ( & absolute_dir) else {
145+ let Some ( ( contents, path) ) = Self :: load_v1_dir ( & absolute_dir) ? else {
93146 return Ok ( None ) ;
94147 } ;
95- let contents = contents?;
96148 let mut config = Self :: from_str_with_mfe_dep ( & contents, path. as_str ( ) , has_mfe_dependency) ?;
97149 config. filename = path
98150 . file_name ( )
@@ -200,15 +252,36 @@ impl TurborepoMfeConfig {
200252
201253 fn load_v1_dir (
202254 dir : & AbsoluteSystemPath ,
203- ) -> Option < ( Result < String , io :: Error > , AbsoluteSystemPathBuf ) > {
255+ ) -> Result < Option < ( String , AbsoluteSystemPathBuf ) > , Error > {
204256 let load_config =
205- |filename : & str | -> Option < ( Result < String , io :: Error > , AbsoluteSystemPathBuf ) > {
257+ |filename : & str | -> Option < ( Result < String , Error > , AbsoluteSystemPathBuf ) > {
206258 let path = dir. join_component ( filename) ;
207- let contents = path. read_existing_to_string ( ) . transpose ( ) ?;
259+ let contents = path
260+ . read_existing_to_string ( )
261+ . transpose ( ) ?
262+ . map_err ( Error :: from) ;
208263 Some ( ( contents, path) )
209264 } ;
210- load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1 )
211- . or_else ( || load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT ) )
265+
266+ // First check if custom config is specified via environment variable
267+ match Self :: get_custom_config_name ( ) ? {
268+ Some ( custom_name) => {
269+ // If environment variable is set, only try that path
270+ let Some ( ( contents, path) ) = load_config ( & custom_name) else {
271+ return Ok ( None ) ;
272+ } ;
273+ Ok ( Some ( ( contents?, path) ) )
274+ }
275+ None => {
276+ // Otherwise use default config file names
277+ let Some ( ( contents, path) ) = load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1 )
278+ . or_else ( || load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT ) )
279+ else {
280+ return Ok ( None ) ;
281+ } ;
282+ Ok ( Some ( ( contents?, path) ) )
283+ }
284+ }
212285 }
213286
214287 pub fn set_path ( & mut self , dir : & AnchoredSystemPath ) {
@@ -300,10 +373,9 @@ impl Config {
300373 Self :: validate_package_path ( repo_root, & absolute_dir) ?;
301374
302375 // we want to try different paths and then do `from_str`
303- let Some ( ( contents, path) ) = Self :: load_v1_dir ( & absolute_dir) else {
376+ let Some ( ( contents, path) ) = Self :: load_v1_dir ( & absolute_dir) ? else {
304377 return Ok ( None ) ;
305378 } ;
306- let contents = contents?;
307379 let mut config = Config :: from_str ( & contents, path. as_str ( ) ) ?;
308380 config. filename = path
309381 . file_name ( )
@@ -390,15 +462,36 @@ impl Config {
390462
391463 fn load_v1_dir (
392464 dir : & AbsoluteSystemPath ,
393- ) -> Option < ( Result < String , io :: Error > , AbsoluteSystemPathBuf ) > {
465+ ) -> Result < Option < ( String , AbsoluteSystemPathBuf ) > , Error > {
394466 let load_config =
395- |filename : & str | -> Option < ( Result < String , io :: Error > , AbsoluteSystemPathBuf ) > {
467+ |filename : & str | -> Option < ( Result < String , Error > , AbsoluteSystemPathBuf ) > {
396468 let path = dir. join_component ( filename) ;
397- let contents = path. read_existing_to_string ( ) . transpose ( ) ?;
469+ let contents = path
470+ . read_existing_to_string ( )
471+ . transpose ( ) ?
472+ . map_err ( Error :: from) ;
398473 Some ( ( contents, path) )
399474 } ;
400- load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1 )
401- . or_else ( || load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT ) )
475+
476+ // First check if custom config is specified via environment variable
477+ match TurborepoMfeConfig :: get_custom_config_name ( ) ? {
478+ Some ( custom_name) => {
479+ // If environment variable is set, only try that path
480+ let Some ( ( contents, path) ) = load_config ( & custom_name) else {
481+ return Ok ( None ) ;
482+ } ;
483+ Ok ( Some ( ( contents?, path) ) )
484+ }
485+ None => {
486+ // Otherwise use default config file names
487+ let Some ( ( contents, path) ) = load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1 )
488+ . or_else ( || load_config ( DEFAULT_MICROFRONTENDS_CONFIG_V1_ALT ) )
489+ else {
490+ return Ok ( None ) ;
491+ } ;
492+ Ok ( Some ( ( contents?, path) ) )
493+ }
494+ }
402495 }
403496
404497 /// Sets the path the configuration was loaded from
0 commit comments