char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
ac70d72
main
1use std:: path::{ Path , PathBuf }; 2 3use avril_core::{ Error , ModuleLoader }; 4 5use crate :: caps:: PathTarget ; 6use crate :: modules::{ ModuleGraph , ResolutionMode , lexical_absolute}; 7 8use oxc:: allocator:: Allocator ; 9use oxc:: parser:: Parser ; 10use oxc:: span:: SourceType ; 11 12/// Static CommonJS named exports. 13pub ( crate ) fn named_exports ( source : & str ) ->Vec < String > { 14use oxc:: ast:: ast::{ 15AssignmentTarget , CallExpression , Expression , ObjectPropertyKind , PropertyKey , 16}; 17use oxc:: ast_visit::{ Visit , walk}; 18use oxc:: syntax:: identifier:: is_identifier_name; 19 20fn valid_name ( name : & str ) ->bool { 21// Synthetic ESM wrappers need importable identifiers; string-keyed 22// properties like `exports["foo-bar"]` stay available via `default`. 23 !name. is_empty () &&is_identifier_name ( name) 24} 25 26fn string_key ( key : & PropertyKey < ' _ >) ->Option < String > { 27match key{ 28PropertyKey :: StaticIdentifier ( identifier) =>Some ( identifier. name . to_string ()), 29PropertyKey :: StringLiteral ( literal) =>Some ( literal. value . to_string ()), 30 _ =>None , 31} 32} 33 34fn expr_is_exports ( expr : & Expression < ' _ >) ->bool { 35matches! ( expr, Expression :: Identifier ( identifier) if identifier. name =="exports" ) 36} 37 38fn expr_is_module_exports ( expr : & Expression < ' _ >) ->bool { 39match expr{ 40Expression :: StaticMemberExpression ( member) =>{ 41matches! ( & member. object , Expression :: Identifier ( object) if object. name =="module" ) 42 && member. property . name =="exports" 43} 44 _ =>false , 45} 46} 47 48# [ derive ( Default )] 49struct Collector { 50names : std:: collections:: BTreeSet < String >, 51} 52 53impl < ' a > Visit < ' a > for Collector { 54fn visit_assignment_expression ( & mut self , expr : & oxc:: ast:: ast:: AssignmentExpression < ' a >) { 55let mut direct: Option < String > =None ; 56let mut object_assign: Option < & Expression < ' a >> =None ; 57match & expr. left { 58AssignmentTarget :: StaticMemberExpression ( member) =>{ 59let prop = member. property . name . to_string (); 60if expr_is_exports ( & member. object ) ||expr_is_module_exports ( & member. object ) { 61 direct =Some ( prop); 62} else if matches! ( & member. object , Expression :: Identifier ( object) if object. name =="module" ) 63 && member. property . name =="exports" 64{ 65// `module.exports = …` itself; object literal handled below. 66 object_assign =Some ( & expr. right ); 67} 68} 69AssignmentTarget :: ComputedMemberExpression ( member) =>{ 70let prop =match & member. expression { 71Expression :: StringLiteral ( literal) =>Some ( literal. value . to_string ()), 72 _ =>None , 73}; 74if let Some ( prop) = prop 75 &&( expr_is_exports ( & member. object ) 76 ||expr_is_module_exports ( & member. object )) 77{ 78 direct =Some ( prop); 79} 80// `module.exports = …` via computed `module["exports"]` is rare; skip. 81} 82 _ =>{} 83} 84if let Some ( name) = direct 85 &&valid_name ( & name) 86{ 87self . names . insert ( name); 88} 89// `module.exports = { a, b: …, "c": … }`. 90if let Some ( right) = object_assign 91 &&let Expression :: ObjectExpression ( object) = right 92{ 93for propertyin & object. properties { 94match property{ 95ObjectPropertyKind :: ObjectProperty ( property) =>{ 96if property. computed { 97continue ; 98} 99if let Some ( name) =string_key ( & property. key ) 100 &&valid_name ( & name) 101{ 102self . names . insert ( name); 103} 104} 105ObjectPropertyKind :: SpreadProperty ( _) =>{} 106} 107} 108} 109 walk:: walk_assignment_expression ( self , expr); 110} 111 112fn visit_call_expression ( & mut self , expr : & CallExpression < ' a >) { 113if let Expression :: StaticMemberExpression ( callee) =& expr. callee 114 &&matches! ( & callee. object , Expression :: Identifier ( object) if object. name =="Object" ) 115 && callee. property . name =="defineProperty" 116 && expr. arguments . len () >=2 117{ 118let target = expr. arguments . first (). and_then ( |arg| arg. as_expression ()); 119let name = expr. arguments . get ( 1 ). and_then ( |arg| arg. as_expression ()); 120let is_exports_target = target. is_some_and ( |target|{ 121expr_is_exports ( target) ||expr_is_module_exports ( target) 122}); 123if is_exports_target &&let Some ( Expression :: StringLiteral ( literal)) = name{ 124let name = literal. value . to_string (); 125if valid_name ( & name) { 126self . names . insert ( name); 127} 128} 129} 130 walk:: walk_call_expression ( self , expr); 131} 132} 133 134let allocator =Allocator :: default (); 135let parsed =Parser :: new ( & allocator, source, SourceType :: cjs ()). parse (); 136// On syntax errors return what was found; execution reports the error. 137let mut collector =Collector :: default (); 138 collector. visit_program ( & parsed. program ); 139 collector. names . into_iter (). collect () 140} 141 142enum LocalLookup { 143Missing , 144Err ( Error ), 145} 146 147impl ModuleGraph { 148/// Shared synchronous source for `require` demand loads: verified npm 149/// bytes via `npm_source` (virtual-URL authority + integrity) or local 150/// files via `read_text` (source authority, no broader grant needed). 151/// Never performs network I/O. 152pub ( crate ) fn sync_source ( & self , name : & str ) ->Result < String , Error > { 153self . check_access ( name) ?; 154let source =if self . npm . owning_instance ( Path :: new ( name)). is_some () { 155self . npm 156. npm_source ( Path :: new ( name)) 157. map_err ( |error|Error :: Module ( error. to_string ())) ? 158} else if self . npm . is_reserved_npm_path ( Path :: new ( name)) { 159return Err ( Error :: Module ( format! ( 160"npm cache path is not owned by a prepared instance: {name}" 161))); 162} else { 163crate :: sources:: read_text ( & self . target ( name) ?, & self . authority ) 164. map_err ( |error|self . path_error ( name, error)) ? 165}; 166self . check_access ( name) ?; 167Ok ( source) 168} 169 170pub ( crate ) fn require_referrer_path ( referrer : & str ) ->Option < PathBuf > { 171let path =Path :: new ( referrer); 172if path. is_absolute () { 173return lexical_absolute ( path). ok (); 174} 175if let Ok ( url) = url:: Url :: parse ( referrer) 176 && url. scheme () =="file" 177{ 178return lexical_absolute ( & url. to_file_path (). ok () ?). ok (); 179} 180None 181} 182 183fn resolve_npm_for_anchor ( 184& self , 185anchor : & str , 186npm_specifier : & str , 187original : & str , 188) ->Result < String , Error > { 189let root =self 190. packages 191. root ( anchor) 192. map_err ( |error|Error :: Module ( error. to_string ())) ?; 193let handles =self . npm_handles . borrow (); 194let Some ( handle) = handles. get ( & root) else { 195return Err ( Error :: Module ( format! ( 196"npm package is not prepared in this context (sync require cannot acquire new packages): {original}" 197))); 198}; 199self . npm 200. resolve_in ( handle, npm_specifier, ResolutionMode :: Require ) 201. map ( |resolved|{ 202 resolved 203. path 204. to_str () 205. expect ( "npm paths are UTF-8" ) 206. to_owned () 207}) 208. map_err ( |error|Error :: Module ( error. to_string ())) 209} 210 211fn resolve_local_require ( 212& self , 213specifier : & str , 214referrer_path : & Path , 215) ->Result < String , Error > { 216let path =if specifier 217. get ( ..5 ) 218. is_some_and ( |scheme| scheme. eq_ignore_ascii_case ( "file:" )) 219{ 220let url = url:: Url :: parse ( specifier) 221. map_err ( |_|Error :: Module ( format! ( "Invalid file URL: {specifier}" ))) ?; 222if url. query (). is_some () || url. fragment (). is_some () { 223return Err ( Error :: Module ( 224"File URL queries and fragments are not supported yet" . into (), 225)); 226} 227 url. to_file_path () 228. map_err ( |_|Error :: Module ( format! ( "Invalid file URL: {specifier}" ))) ? 229} else { 230let path =Path :: new ( specifier); 231if path. is_absolute () { 232 path. to_owned () 233} else if specifier. starts_with ( "./" ) || specifier. starts_with ( "../" ) { 234 referrer_path. parent (). unwrap_or ( Path :: new ( "." )). join ( path) 235} else { 236return Err ( Error :: Module ( format! ( 237"Unsupported module specifier for sync require (use import() instead): {specifier}" 238))); 239} 240}; 241match self . local_file_require ( & path, 0 ) { 242Ok ( name) =>Ok ( name), 243Err ( LocalLookup :: Missing ) =>{ 244Err ( self . path_error ( specifier, format! ( "Module not found: {}" , path. display ()))) 245} 246Err ( LocalLookup :: Err ( error)) =>Err ( error), 247} 248} 249 250fn local_file_require ( & self , path : & Path , depth : usize ) ->Result < String , LocalLookup > { 251if depth >5 { 252return Err ( LocalLookup :: Err ( Error :: Module ( 253"Local require lookup is too deep" . into (), 254))); 255} 256match self . local_candidate ( path) { 257Ok ( name) =>return Self :: check_native ( name). map_err ( LocalLookup :: Err ), 258Err ( LocalLookup :: Missing ) =>{} 259Err ( other) =>return Err ( other), 260} 261for extensionin [ ".js" , ".json" , ".node" ] { 262let mut candidate = path. as_os_str (). to_owned (); 263 candidate. push ( extension); 264let candidate_path =PathBuf :: from ( candidate); 265match self . local_candidate ( & candidate_path) { 266Ok ( name) =>return Self :: check_native ( name). map_err ( LocalLookup :: Err ), 267Err ( LocalLookup :: Missing ) =>continue , 268Err ( other) =>return Err ( other), 269} 270} 271if self . local_is_dir ( path) { 272match self . local_dir_main ( path) { 273Ok ( Some ( main)) =>{ 274let candidate = path. join ( main); 275match self . local_file_require ( & candidate, depth +1 ) { 276Ok ( name) =>return Ok ( name), 277Err ( LocalLookup :: Missing ) =>{} 278Err ( other) =>return Err ( other), 279} 280} 281Ok ( None ) =>{} 282Err ( error) =>return Err ( LocalLookup :: Err ( error)), 283} 284for indexin [ "index.js" , "index.json" , "index.cjs" , "index.node" ] { 285match self . local_candidate ( & path. join ( index)) { 286Ok ( name) =>return Self :: check_native ( name). map_err ( LocalLookup :: Err ), 287Err ( LocalLookup :: Missing ) =>continue , 288Err ( other) =>return Err ( other), 289} 290} 291} 292Err ( LocalLookup :: Missing ) 293} 294 295fn check_native ( name : String ) ->Result < String , Error > { 296if name. to_ascii_lowercase (). ends_with ( ".node" ) { 297return Err ( Error :: Module ( format! ( 298"Native addons are not supported: {name}" 299))); 300} 301Ok ( name) 302} 303 304fn local_candidate ( & self , path : & Path ) ->Result < String , LocalLookup > { 305let logical =lexical_absolute ( path). map_err ( LocalLookup :: Err ) ?; 306if self . npm . is_reserved_npm_path ( & logical) { 307return self . npm_local_candidate ( & logical); 308} 309let target =match PathTarget :: resolve ( path) { 310Ok ( target) => target, 311Err ( _) =>return Err ( LocalLookup :: Missing ), 312}; 313if self . npm . is_reserved_npm_path ( target. as_path ()) { 314return self . npm_local_candidate ( target. as_path ()); 315} 316if let Err ( error) =self . authority . check_local ( & target) { 317return Err ( LocalLookup :: Err ( 318self . path_error ( & path. display (). to_string (), error), 319)); 320} 321let resolved = target. as_path (). to_owned (); 322if !std:: fs:: symlink_metadata ( & resolved). is_ok_and ( |meta| meta. is_file ()) { 323return Err ( LocalLookup :: Missing ); 324} 325 resolved 326. into_os_string () 327. into_string () 328. map_err ( |_|LocalLookup :: Err ( Error :: Module ( "Module paths must be valid UTF-8" . into ()))) 329} 330 331fn npm_local_candidate ( & self , path : & Path ) ->Result < String , LocalLookup > { 332match self . npm . resolve_require_path ( path) { 333Ok ( Some ( module)) => module. path . into_os_string (). into_string (). map_err ( |_|{ 334LocalLookup :: Err ( Error :: Module ( "Module paths must be valid UTF-8" . into ())) 335}), 336Ok ( None ) =>Err ( LocalLookup :: Missing ), 337Err ( error) =>Err ( LocalLookup :: Err ( Error :: Module ( error. to_string ()))), 338} 339} 340 341fn local_is_dir ( & self , path : & Path ) ->bool { 342PathTarget :: resolve ( path) 343. ok () 344. filter ( |target|self . authority . check_local ( target). is_ok ()) 345. is_some_and ( |target| target. as_path (). is_dir ()) 346} 347 348fn local_dir_main ( & self , dir : & Path ) ->Result < Option < PathBuf >, Error > { 349let package = dir. join ( "package.json" ); 350let target =match PathTarget :: resolve ( & package) { 351Ok ( target) => target, 352Err ( _) =>return Ok ( None ), 353}; 354let Some ( name) = target. as_path (). to_str () else { 355return Ok ( None ); 356}; 357let source =self . sync_source ( name) ?; 358let value: serde_json:: Value = serde_json:: from_str ( & source). map_err ( |error|{ 359Error :: Module ( format! ( 360"{}: invalid package.json: {error}" , 361 package. display () 362)) 363}) ?; 364let Some ( main) = value. get ( "main" ). and_then ( |entry| entry. as_str ()) else { 365return Ok ( None ); 366}; 367if main. is_empty () || main. contains ( '\0' ) { 368return Err ( Error :: Module ( format! ( 369"{}: invalid package.json main" , 370 package. display () 371))); 372} 373if main. contains ( "../" ) || main. starts_with ( ".." ) { 374return Ok ( None ); 375} 376let main = main. strip_prefix ( "./" ). unwrap_or ( main); 377if main. is_empty () 378 || main 379. split ( '/' ) 380. any ( |part| part. is_empty () || part =="." || part ==".." ) 381{ 382return Ok ( None ); 383} 384Ok ( Some ( PathBuf :: from ( main))) 385} 386pub ( crate ) fn require_resolve_sync ( 387& self , 388specifier : & str , 389referrer : & str , 390) ->Result < String , Error > { 391if specifier. starts_with ( "ext:" ) { 392return Err ( Error :: Module ( "Extension modules are private" . into ())); 393} 394if specifier. starts_with ( "node:" ) || specifier =="node" { 395return Err ( Error :: Module ( format! ( 396"node: builtin modules are not supported: {specifier}" 397))); 398} 399if specifier. starts_with ( "avril:" ) { 400return Ok ( specifier. to_owned ()); 401} 402// Never expose internal extension modules via any path. 403// `require_resolve` itself performs no I/O beyond verified metadata 404// and canonicalized filesystem probes; capability checks happen in 405// the core `require` wrapper (and `require.resolve`) around this. 406if let Some ( referrer_path) =Self :: require_referrer_path ( referrer) { 407if self . npm . is_reserved_npm_path ( & referrer_path) { 408if self . npm . owning_instance ( & referrer_path). is_none () { 409return Err ( Error :: Module ( format! ( 410"npm cache path is not owned by a prepared instance: {}" , 411 referrer_path. display () 412))); 413} 414return self 415. npm 416. resolve_from_file ( specifier, & referrer_path, ResolutionMode :: Require ) 417. map ( |resolved|{ 418 resolved 419. path 420. to_str () 421. expect ( "npm paths are UTF-8" ) 422. to_owned () 423}) 424. map_err ( |error|Error :: Module ( error. to_string ())); 425} 426let anchor = referrer_path 427. to_str () 428. ok_or_else ( ||Error :: Module ( "Module paths must be valid UTF-8" . into ())) ?; 429if let Some ( mapped) = 430self . packages 431. resolve_import_meta ( specifier, anchor, & self . remote ) ? 432{ 433match mapped{ 434crate :: local_packages:: Resolution :: Specifier ( target) =>{ 435if target. starts_with ( "npm:" ) { 436return self . resolve_npm_for_anchor ( anchor, & target, specifier); 437} 438if target. starts_with ( "ext:" ) { 439return Err ( Error :: Module ( "Extension modules are private" . into ())); 440} 441if target. starts_with ( "node:" ) || target =="node" { 442return Err ( Error :: Module ( format! ( 443"node: builtin modules are not supported: {specifier}" 444))); 445} 446if target. starts_with ( "avril:" ) 447 || target. starts_with ( "jsr:" ) 448 || target. starts_with ( "http:" ) 449 || target. starts_with ( "https:" ) 450{ 451return Err ( Error :: Module ( format! ( 452"Unsupported module specifier for sync require (use import() instead): {specifier}" 453))); 454} 455return self . resolve_local_require ( & target, & referrer_path); 456} 457crate :: local_packages:: Resolution :: Module ( name) =>{ 458if name. starts_with ( "ext:" ) { 459return Err ( Error :: Module ( "Extension modules are private" . into ())); 460} 461return Ok ( name); 462} 463crate :: local_packages:: Resolution :: Remote ( _) =>{ 464return Err ( Error :: Module ( format! ( 465"Unsupported module specifier for sync require (use import() instead): {specifier}" 466))); 467} 468} 469} 470if specifier. starts_with ( "npm:" ) { 471return self . resolve_npm_for_anchor ( anchor, specifier, specifier); 472} 473return self . resolve_local_require ( specifier, & referrer_path); 474} 475Err ( Error :: Module ( format! ( 476"Unsupported module specifier for sync require (use import() instead): {specifier}" 477))) 478} 479} 480 481# [ cfg ( test )] 482mod require_referrer_tests{ 483use super :: ModuleGraph ; 484 485# [ test ] 486fn referrer_paths_are_portable () { 487assert! ( ModuleGraph :: require_referrer_path ( "/tmp/a.cjs" ). is_some ()); 488assert! ( ModuleGraph :: require_referrer_path ( "file:///tmp/a.cjs" ). is_some ()); 489assert! ( ModuleGraph :: require_referrer_path ( "./a.cjs" ). is_none ()); 490assert! ( ModuleGraph :: require_referrer_path ( "https://example.test/a.js" ). is_none ()); 491assert! ( ModuleGraph :: require_referrer_path ( "avril:std" ). is_none ()); 492} 493 494# [ cfg ( windows )] 495# [ test ] 496fn windows_absolute_paths_resolve () { 497assert! ( ModuleGraph :: require_referrer_path ( r"C:\proj\a.cjs" ). is_some ()); 498} 499}