char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
513fcd4
main
1use std:: cell:: RefCell ; 2use std:: collections::{ BTreeSet , HashMap , HashSet }; 3use std:: io:: Read ; 4use std:: path::{ Component , Path , PathBuf }; 5 6use crate :: authority:: SourceAuthority ; 7use crate :: caps:: PathTarget ; 8use crate :: package_config:: check_jsx_option; 9use avril_core:: Error ; 10use oxc:: transformer::{ JsxOptions , JsxRuntime , TransformOptions }; 11use serde_json::{ Map , Value }; 12 13use crate :: local_packages:: trusted; 14 15# [ derive ( Clone , Default )] 16pub ( crate ) struct JsxConfig ( Map < String , Value >); 17 18impl JsxConfig { 19pub fn compiler_options ( & self ) ->Map < String , Value > { 20let mut options =self . 0 . clone (); 21 options. entry ( "jsx" ). or_insert_with ( ||"react-jsx" . into ()); 22 options 23} 24 25pub fn preserves_jsx ( & self ) ->bool { 26matches! ( 27self . 0 . get ( "jsx" ). and_then ( Value :: as_str), 28Some ( "preserve" |"react-native" ) 29) 30} 31 32pub fn transform_options ( & self ) ->Result < TransformOptions , Error > { 33if self . preserves_jsx () { 34return Err ( Error :: Module ( "avril cannot execute preserved JSX; set compilerOptions.jsx to react-jsx, react-jsxdev, or react" . into ())); 35} 36let string = |key|self . 0 . get ( key). and_then ( Value :: as_str). map ( str:: to_owned); 37let mut options =TransformOptions { 38jsx : JsxOptions { 39runtime : if string ( "jsx" ). as_deref () ==Some ( "react" ) { 40JsxRuntime :: Classic 41} else { 42JsxRuntime :: Automatic 43}, 44development : string ( "jsx" ). as_deref () ==Some ( "react-jsxdev" ), 45import_source : string ( "jsxImportSource" ), 46pragma : string ( "jsxFactory" ), 47pragma_frag : string ( "jsxFragmentFactory" ), 48 ..JsxOptions :: default () 49}, 50 ..TransformOptions :: default () 51}; 52// TypeScript import elision must retain factories used only by JSX. 53if let Some ( factory) =& options. jsx . pragma { 54 options. typescript . jsx_pragma = factory. clone (). into (); 55} 56if let Some ( fragment) =& options. jsx . pragma_frag { 57 options. typescript . jsx_pragma_frag = fragment. clone (). into (); 58} 59Ok ( options) 60} 61} 62 63/// The nearest applicable `tsconfig.json` overrides `defaults`, the owning 64/// package's `avril.conf` options. 65pub ( crate ) fn jsx_config ( 66authority : & SourceAuthority , 67module : & Path , 68boundary : Option < & Path >, 69defaults : & Map < String , Value >, 70inputs : & RefCell < BTreeSet < PathBuf >>, 71cache : & RefCell < HashMap < PathBuf , JsxConfig >>, 72) ->Result < JsxConfig , Error > { 73let mut directory = module. parent (); 74while let Some ( path) = directory{ 75let target =PathTarget :: resolve ( path). map_err ( |error|Error :: Module ( error. to_string ())) ?; 76if target. as_path () != path || authority. check_local ( & target). is_err () { 77break ; 78} 79let handle =crate :: sources:: open_directory ( path) 80. map_err ( |error|Error :: Module ( error. to_string ())) ?; 81if !trusted ( 82& handle 83. dir_metadata () 84. map_err ( |error|Error :: Module ( error. to_string ())) ?, 85) { 86break ; 87} 88let package =PathTarget :: for_entry ( path. join ( "avril.conf" )) 89. map_err ( |error|Error :: Module ( error. to_string ())) ?; 90if authority. check_local ( & package). is_err () { 91break ; 92} 93let config = path. join ( "tsconfig.json" ); 94let target = 95PathTarget :: for_entry ( & config). map_err ( |error|Error :: Module ( error. to_string ())) ?; 96// Do not probe the existence of configs outside source authority. 97if authority. check_local ( & target). is_err () { 98break ; 99} 100 inputs. borrow_mut (). insert ( config. clone ()); 101match handle. symlink_metadata ( "tsconfig.json" ) { 102Ok ( _) =>{ 103let found =ConfigLoader { 104 authority, 105 inputs, 106active : HashSet :: new (), 107 cache, 108} 109. load ( & config, false ) ?; 110let mut options = defaults. clone (); 111 options. extend ( found. 0 ); 112return Ok ( JsxConfig ( options)); 113} 114Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{} 115Err ( error) =>return Err ( Error :: Module ( format! ( "{}: {error}" , config. display ()))), 116} 117if Some ( path) == boundary{ 118break ; 119} 120 directory = path. parent (); 121} 122Ok ( JsxConfig ( defaults. clone ())) 123} 124 125struct ConfigLoader < ' a > { 126authority : &' a SourceAuthority , 127inputs : &' a RefCell < BTreeSet < PathBuf >>, 128active : HashSet < PathBuf >, 129cache : &' a RefCell < HashMap < PathBuf , JsxConfig >>, 130} 131 132impl ConfigLoader < ' _ > { 133fn load ( & mut self , path : & Path , append_json : bool ) ->Result < JsxConfig , Error > { 134let target = 135PathTarget :: for_entry ( path). map_err ( |error|Error :: Module ( error. to_string ())) ?; 136self . authority 137. check_local ( & target) 138. map_err ( |error|Error :: Module ( error. to_string ())) ?; 139let path = target. as_path (); 140self . inputs . borrow_mut (). insert ( path. to_owned ()); 141let error = |message : & str |Error :: Module ( format! ( "{}: {message}" , path. display ())); 142if self . active . len () >=64 ||self . cache . borrow (). len () >=512 { 143return Err ( error ( "tsconfig extends graph is too large" )); 144} 145if !self . active . insert ( path. to_owned ()) { 146return Err ( error ( "Cyclic tsconfig extends" )); 147} 148if let Some ( config) =self . cache . borrow (). get ( path). cloned () { 149self . active . remove ( path); 150return Ok ( config); 151} 152let directory = path 153. parent () 154. ok_or_else ( ||error ( "Expected a config directory" )) ?; 155let handle =crate :: sources:: open_directory ( directory) 156. map_err ( |_|error ( "Cannot open config directory" )) ?; 157if !trusted ( 158& handle 159. dir_metadata () 160. map_err ( |_|error ( "Cannot inspect config directory" )) ?, 161) { 162return Err ( error ( "Untrusted config directory" )); 163} 164let name = path 165. file_name () 166. ok_or_else ( ||error ( "Expected a config filename" )) ?; 167let metadata =match handle. symlink_metadata ( name) { 168Ok ( metadata) => metadata, 169Err ( err) 170if append_json 171 && path. extension (). is_none_or ( |ext| ext !="json" ) 172 && err. kind () == std:: io:: ErrorKind :: NotFound => 173{ 174let mut name = name. to_os_string (); 175 name. push ( ".json" ); 176self . active . remove ( path); 177return self . load ( & path. with_file_name ( name), false ); 178} 179Err ( _) =>return Err ( error ( "Cannot find tsconfig" )), 180}; 181if !metadata. is_file () || metadata. is_symlink () { 182return Err ( error ( "Config must be a regular non-symlink file" )); 183} 184if !trusted ( & metadata) { 185return Err ( error ( "Untrusted config" )); 186} 187let file = 188crate :: sources:: open_regular ( & handle, name). map_err ( |_|error ( "Cannot read config" )) ?; 189if !trusted ( 190& file 191. metadata () 192. map_err ( |_|error ( "Cannot inspect config" )) ?, 193) { 194return Err ( error ( "Untrusted config" )); 195} 196let mut source =String :: new (); 197 file. take ( 32 * 1024 * 1024 +1 ) 198. read_to_string ( & mut source) 199. map_err ( |_|error ( "Cannot read config" )) ?; 200if source. len () >32 * 1024 * 1024 { 201return Err ( error ( "Config is too large" )); 202} 203self . authority 204. check_local ( & target) 205. map_err ( |error|Error :: Module ( error. to_string ())) ?; 206let value: Value = jsonc_parser:: parse_to_serde_value ( 207 source. trim_start_matches ( '\u{feff}' ), 208& jsonc_parser:: ParseOptions { 209allow_comments : true , 210allow_trailing_commas : true , 211allow_loose_object_property_names : false , 212allow_missing_commas : false , 213allow_single_quoted_strings : false , 214allow_hexadecimal_numbers : false , 215allow_unary_plus_numbers : false , 216allow_bare_decimal_point_numbers : false , 217allow_non_finite_numbers : false , 218allow_extended_string_escapes : false , 219}, 220) 221. map_err ( |err|error ( & err. to_string ())) ?; 222let object = value 223. as_object () 224. ok_or_else ( ||error ( "tsconfig must be an object" )) ?; 225let mut config =JsxConfig :: default (); 226if let Some ( extends) = object. get ( "extends" ) { 227let bases =match extends{ 228Value :: String ( _) => std:: slice:: from_ref ( extends), 229Value :: Array ( bases) => bases. as_slice (), 230 _ =>return Err ( error ( "extends must be a path or an array of paths" )), 231}; 232for basein bases{ 233let base = base 234. as_str () 235. ok_or_else ( ||error ( "extends entries must be paths" )) ?; 236if !Path :: new ( base). is_absolute () 237 && !matches! ( 238Path :: new ( base). components (). next (), 239Some ( Component :: CurDir |Component :: ParentDir ) 240) 241{ 242return Err ( error ( 243"extends must use relative or absolute file paths; package and URL configs are not supported" , 244)); 245} 246 config. 0 . extend ( self . load ( & directory. join ( base), true ) ?. 0 ); 247} 248} 249if let Some ( options) = object. get ( "compilerOptions" ) { 250let options = options 251. as_object () 252. ok_or_else ( ||error ( "compilerOptions must be an object" )) ?; 253for ( key, value) in options{ 254check_jsx_option ( key, value). map_err ( |message|error ( & message)) ?; 255 config. 0 . insert ( key. to_owned (), value. clone ()); 256} 257} 258self . active . remove ( path); 259self . cache 260. borrow_mut () 261. insert ( path. to_owned (), config. clone ()); 262Ok ( config) 263} 264}