char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
ab5df08
main
1use std:: cell:: RefCell ; 2use std:: collections::{ BTreeMap , BTreeSet , HashMap }; 3use std:: path::{ Path , PathBuf }; 4use std:: rc:: Rc ; 5use std:: sync:: Arc ; 6 7use avril_core::{ Error , ModuleFormat , ModuleLoader }; 8use oxc:: span:: SourceType ; 9 10use crate :: authority:: SourceAuthority ; 11use crate :: caps:: PathTarget ; 12use crate :: data_modules:: DataModule ; 13use crate :: local_packages:: ResolveMode ; 14pub use crate :: registry:: npm:: resolution:: ResolutionMode ; 15use crate :: registry:: npm:: resolution::{ 16NpmResolver , NpmRootRequest , PreparedNpmRoots , ResolutionError , ResolvedNpmModule , 17}; 18 19pub use crate :: loader:: BuiltinWarning ; 20pub ( crate ) use crate :: loader::{ compile, transpile}; 21pub use crate :: type_graph:: TypeGraph ; 22 23# [ derive ( Debug , thiserror :: Error )] 24pub ( crate ) enum ResolutionFailure { 25# [ error ( "Unsupported module specifier: {0}" )] 26UnboundSpecifier ( String ), 27# [ error ( transparent )] 28Npm ( # [ from ] ResolutionError ), 29# [ error ( transparent )] 30Other ( # [ from ] Error ), 31} 32 33impl ResolutionFailure { 34pub ( crate ) fn can_be_ambient ( & self ) ->bool { 35matches! ( 36self , 37Self :: UnboundSpecifier ( _) |Self :: Npm ( ResolutionError :: NpmPackageNotDeclared { ..}) 38) 39} 40} 41 42impl From < ResolutionFailure > for Error { 43fn from ( failure : ResolutionFailure ) ->Self { 44match failure{ 45ResolutionFailure :: UnboundSpecifier ( specifier) =>{ 46Error :: Module ( format! ( "Unsupported module specifier: {specifier}" )) 47} 48ResolutionFailure :: Npm ( error) =>Error :: Module ( error. to_string ()), 49ResolutionFailure :: Other ( error) => error, 50} 51} 52} 53 54pub ( crate ) enum ResolutionTarget { 55Module ( String ), 56Jsr ( String ), 57Npm ( String ), 58Remote ( String ), 59} 60 61pub ( crate ) struct ResolvedRequest { 62pub ( crate ) name : String , 63pub ( crate ) tracked : Option < String >, 64// The npm export still eligible for type overrides and automatic discovery. 65pub ( crate ) type_subpath : Option < String >, 66} 67 68impl ResolvedRequest { 69fn module ( name : String , tracked : Option < String >) ->Self { 70Self { 71 name, 72 tracked, 73type_subpath : None , 74} 75} 76 77fn npm ( 78module : ResolvedNpmModule , 79specifier : & str , 80tracked : Option < String >, 81) ->Result < Self , Error > { 82let subpath =if specifier. starts_with ( "npm:" ) { 83crate :: registry:: npm:: parse_npm_request ( specifier) 84. map_err ( |error|Error :: Module ( error. to_string ())) ? 85. subpath 86} else if !specifier. starts_with ([ '.' , '/' , '#' ]) && !specifier. contains ( ':' ) { 87crate :: registry:: npm:: package:: split_bare ( specifier) 88. 1 89. map ( str:: to_owned) 90} else { 91return Ok ( Self :: module ( 92 module. path . to_string_lossy (). into_owned (), 93 tracked, 94)); 95}; 96Ok ( Self { 97name : module. path . to_string_lossy (). into_owned (), 98 tracked, 99type_subpath : Some ( subpath. unwrap_or_default ()), 100}) 101} 102} 103 104/// Normalized name, local path, and remote/npm flags for one referrer. 105pub ( crate ) struct Referrer { 106pub ( crate ) name : String , 107pub ( crate ) path : Option < PathBuf >, 108pub ( crate ) remote : bool , 109pub ( crate ) inline : bool , 110pub ( crate ) npm : bool , 111} 112 113# [ derive ( Clone )] 114pub struct ModuleGraph { 115pub ( crate ) authority : SourceAuthority , 116pub ( crate ) packages : Rc < crate :: local_packages:: LocalPackages >, 117pub ( crate ) store : Arc < crate :: registry:: PackageStore >, 118pub ( crate ) remote : Rc < crate :: remote_packages:: RemotePackages >, 119pub ( crate ) npm : Rc < NpmResolver >, 120pub ( crate ) npm_handles : Rc < RefCell < HashMap < PathBuf , PreparedNpmRoots >>>, 121pub ( crate ) types : Rc < crate :: type_graph:: TypeState >, 122pub ( crate ) loader : Rc < RefCell < crate :: loader:: LoaderState >>, 123} 124 125impl ModuleGraph { 126/// A graph with internally owned package loaders sharing one authority. 127/// The store is built with [`crate::registry::PackageStore::new_with_authority`]. 128pub fn open ( 129runtime : tokio:: runtime:: Handle , 130authority : SourceAuthority , 131options : crate :: registry:: Options , 132) ->Result < Self , Error > { 133let store =Arc :: new ( 134crate :: registry:: PackageStore :: new_with_authority ( runtime, options, authority. clone ()) 135. map_err ( |error|Error :: Module ( error. to_string ())) ?, 136); 137Ok ( Self :: assemble ( authority, store, false )) 138} 139 140/// Like [`ModuleGraph::open`], resolving npm packages for browser bundles. 141pub fn open_browser ( 142runtime : tokio:: runtime:: Handle , 143authority : SourceAuthority , 144options : crate :: registry:: Options , 145) ->Result < Self , Error > { 146let store =Arc :: new ( 147crate :: registry:: PackageStore :: new_with_authority ( runtime, options, authority. clone ()) 148. map_err ( |error|Error :: Module ( error. to_string ())) ?, 149); 150Ok ( Self :: assemble ( authority, store, true )) 151} 152 153/// Reuse an execution store, as workers do with their parent's store. 154/// Graph checks retain the caller's authority; acquisition uses the store's. 155/// Read graphs should use `open` or `open_browser` so acquisition shares 156/// their read/network policy. 157pub fn open_shared ( 158authority : SourceAuthority , 159store : Arc < crate :: registry:: PackageStore >, 160) ->Self { 161Self :: assemble ( authority, store, false ) 162} 163 164/// The store this graph acquires through. 165pub fn store ( & self ) ->& Arc < crate :: registry:: PackageStore > { 166& self . store 167} 168 169pub fn set_typescript_version ( & self , version : & str ) ->Result <(), Error > { 170self . npm 171. set_typescript_version ( version) 172. map_err ( |error|Error :: Module ( error. to_string ())) 173} 174 175pub fn audit_npm ( & self ) ->Result <(), Error > { 176self . npm 177. audit () 178. map_err ( |error|Error :: Module ( error. to_string ())) 179} 180 181/// The project root owning `anchor`, for store and lock bootstrap. 182pub fn project_root ( authority : & SourceAuthority , anchor : & str ) ->Result < PathBuf , Error > { 183crate :: local_packages:: LocalPackages :: new ( authority. clone ()). root ( anchor) 184} 185 186/// The trusted config a REPL session belongs to, for storage bootstrap. 187pub fn project_root_for_repl ( 188authority : & SourceAuthority , 189anchor : & str , 190) ->Result < PathBuf , Error > { 191crate :: local_packages:: LocalPackages :: new ( authority. clone ()). root_for_repl ( anchor) 192} 193 194/// The owning config path of a REPL session, if it has one. 195pub fn project_config_for_repl ( 196authority : & SourceAuthority , 197anchor : & str , 198) ->Result < Option < PathBuf >, Error > { 199crate :: local_packages:: LocalPackages :: new ( authority. clone ()). config_for_repl ( anchor) 200} 201 202/// Locate the project root for `add`, rejecting untrusted configs. 203pub fn project_root_for_add ( 204authority : & SourceAuthority , 205anchor : & str , 206) ->Result < PathBuf , Error > { 207crate :: local_packages:: LocalPackages :: new ( authority. clone ()). root_for_add ( anchor) 208} 209 210fn assemble ( 211authority : SourceAuthority , 212store : Arc < crate :: registry:: PackageStore >, 213browser : bool , 214) ->Self { 215let packages =Rc :: new ( crate :: local_packages:: LocalPackages :: new ( authority. clone ())); 216let remote =Rc :: new ( crate :: remote_packages:: RemotePackages :: new ( 217 authority. clone (), 218 store. clone (), 219)); 220let npm =Rc :: new ( if browser{ 221NpmResolver :: for_browser ( store. clone ()) 222} else { 223NpmResolver :: new ( store. clone ()) 224}); 225Self { 226 packages, 227 store, 228 remote, 229 npm, 230npm_handles : Rc :: default (), 231types : Rc :: default (), 232 authority, 233loader : Rc :: default (), 234} 235} 236 237pub ( crate ) fn side_effects ( & self , name : & str ) ->Option < bool > { 238self . npm . side_effects ( Path :: new ( name)) 239} 240 241pub async fn initialize_package ( & self , name : & str ) ->Result <(), Error > { 242if data_url ( name). is_some () { 243return Ok (()); 244} 245if http_url ( name). is_some () { 246self . remote . initialize ( name). await 247} else if self . npm . is_reserved_npm_path ( Path :: new ( name)) { 248self . npm 249. check_access ( Path :: new ( name)) 250. map ( |_|()) 251. map_err ( |error|Error :: Module ( error. to_string ())) 252} else { 253self . packages . initialize ( name, & self . remote ). await 254} 255} 256 257pub async fn original_source ( & self , name : & str ) ->Result < String , Error > { 258self . original_source_for ( name, ResolutionMode :: Import ). await 259} 260 261pub ( crate ) async fn original_source_for ( 262& self , 263name : & str , 264mode : ResolutionMode , 265) ->Result < String , Error > { 266self . check_source_access ( name, mode) ?; 267let source =if data_url ( name). is_some () { 268DataModule :: parse ( name) ?. source () ? 269} else if http_url ( name). is_some () { 270self . store 271. remote_source ( name) 272. await 273. map_err ( |error|Error :: Module ( error. to_string ())) ? 274} else if self . npm . owning_instance ( Path :: new ( name)). is_some () { 275self . npm 276. npm_source ( Path :: new ( name)) 277. map_err ( |error|Error :: Module ( error. to_string ())) ? 278} else { 279crate :: sources:: read_text ( & self . target ( name) ?, & self . authority ) 280. map_err ( |error|self . path_error ( name, error)) ? 281}; 282self . check_source_access ( name, mode) ?; 283Ok ( source) 284} 285 286pub ( crate ) fn check_source_access ( 287& self , 288name : & str , 289mode : ResolutionMode , 290) ->Result <(), Error > { 291if mode. is_type () &&self . npm . is_reserved_npm_path ( Path :: new ( name)) { 292self . npm 293. check_access ( Path :: new ( name)) 294. map ( |_|()) 295. map_err ( |error|Error :: Module ( error. to_string ())) 296} else { 297self . check_access ( name) 298} 299} 300 301pub ( crate ) fn package_metadata ( & self , name : & str ) ->Result < Option < serde_json:: Value >, Error > { 302if data_url ( name). is_some () { 303return Ok ( None ); 304} 305if Path :: new ( name). is_absolute () { 306if let Some ( instance) =self . npm . owning_instance ( Path :: new ( name)) { 307return self . npm_package_metadata ( & instance, name); 308} 309self . target ( name) ?; 310self . packages . metadata ( name) 311} else { 312self . remote . metadata ( name) 313} 314} 315 316fn npm_package_metadata ( 317& self , 318instance : & str , 319name : & str , 320) ->Result < Option < serde_json:: Value >, Error > { 321let identity =self 322. npm 323. describe_instance ( instance) 324. map_err ( |error|Error :: Module ( error. to_string ())) ?; 325Ok ( Some ( serde_json:: json!({ 326"registry" : self . store . options (). npm_registry . as_str (), 327"name" : identity. name , 328"version" : identity. version , 329"instance" : instance, 330"file" : name, 331}))) 332} 333 334/// Workspace ownership, not guest write permission. Package-store bytes are 335/// immutable inputs even when their materialized identity is an absolute path. 336pub fn is_workspace_source ( & self , name : & str ) ->bool { 337let path =Path :: new ( name); 338let options =self . store . options (); 339 path. is_absolute () 340 && !self . npm . is_reserved_npm_path ( path) 341 && !path. starts_with ( & options. cache ) 342 && options 343. vendor 344. as_ref () 345. is_none_or ( |vendor| !path. starts_with ( vendor)) 346} 347 348pub fn config_inputs ( & self ) ->BTreeSet < std:: path:: PathBuf > { 349self . packages . config_inputs . borrow (). clone () 350} 351 352pub ( crate ) fn jsx_config ( & self , name : & str ) ->Result < crate :: project:: JsxConfig , Error > { 353if http_url ( name). is_some () 354 ||data_url ( name). is_some () 355 ||self . npm . is_reserved_npm_path ( Path :: new ( name)) 356{ 357Ok ( crate :: project:: JsxConfig :: default ()) 358} else { 359self . packages . jsx_config ( name) 360} 361} 362 363pub ( crate ) fn source_type ( & self , name : & str ) ->Result < SourceType , Error > { 364if data_url ( name). is_some () { 365return Ok ( DataModule :: parse ( name) ?. source_type ); 366} 367if self . npm . owning_instance ( Path :: new ( name)). is_some () { 368// Descriptive only; execution policy lives in check_access and 369// the resolver, so type tooling keeps working on declarations. 370return Ok ( SourceType :: from_path ( name). unwrap_or_else ( |_|SourceType :: mjs ())); 371} 372let base =if http_url ( name). is_some () { 373self . store 374. remote_base ( name) 375. map_err ( |error|Error :: Module ( error. to_string ())) ? 376} else { 377 name. to_owned () 378}; 379let url =http_url ( & base); 380Ok ( 381SourceType :: from_path ( url. as_ref (). map_or ( base. as_str (), url:: Url :: path)) 382. unwrap_or_else ( |_|SourceType :: mjs ()), 383) 384} 385 386pub ( crate ) fn target ( & self , name : & str ) ->Result < PathTarget , Error > { 387let target =PathTarget :: resolve ( name). map_err ( |error|self . path_error ( name, error)) ?; 388if target. as_path () !=Path :: new ( name) { 389return Err ( self . path_error ( name, "Module path changed after resolution" )); 390} 391self . authority 392. check_local ( & target) 393. map_err ( |error|self . path_error ( name, error)) ?; 394Ok ( target) 395} 396 397fn resolve_local_target ( 398& self , 399path : & Path , 400specifier : & str , 401allow_missing : bool , 402) ->Result < String , Error > { 403// A cache symlink must not turn a registry-owned URL into an unchecked local import. 404let logical =lexical_absolute ( path) ?; 405if self . npm . is_reserved_npm_path ( & logical) { 406self . npm 407. check_access ( & logical) 408. map_err ( |error|Error :: Module ( error. to_string ())) ?; 409return logical 410. into_os_string () 411. into_string () 412. map_err ( |_|Error :: Module ( "Module paths must be valid UTF-8" . into ())); 413} 414let target =if allow_missing{ 415PathTarget :: for_creation ( path) 416} else { 417PathTarget :: resolve ( path) 418} 419. map_err ( |error|self . path_error ( specifier, error)) ?; 420self . authority 421. check_local ( & target) 422. map_err ( |error|self . path_error ( specifier, error)) ?; 423 target 424. into_path () 425. into_os_string () 426. into_string () 427. map_err ( |_|Error :: Module ( "Module paths must be valid UTF-8" . into ())) 428} 429 430pub ( crate ) fn path_error ( & self , name : & str , error : impl std:: fmt:: Display ) ->Error { 431if self . authority . unrestricted_local () { 432Error :: Module ( format! ( "{name}: {error}" )) 433} else if self . authority . is_read () { 434Error :: Module ( format! ( "PermissionDenied: read {name}" )) 435} else { 436Error :: Module ( format! ( "PermissionDenied: js-exec {name}" )) 437} 438} 439} 440 441impl ModuleGraph { 442pub ( crate ) async fn resolve_tracked ( 443& self , 444specifier : & str , 445referrer : & str , 446) ->Result <( String , Option < String >), Error > { 447let resolved =self 448. resolve_tracked_for ( specifier, referrer, ResolutionMode :: Import , false ) 449. await 450. map_err ( Error :: from) ?; 451Ok (( resolved. name , resolved. tracked )) 452} 453 454pub ( crate ) async fn resolve_tracked_for ( 455& self , 456specifier : & str , 457referrer : & str , 458mode : ResolutionMode , 459overrides : bool , 460) ->Result < ResolvedRequest , ResolutionFailure > { 461let referrer =self . classify_referrer ( referrer) ?; 462if mode. is_type () && specifier. starts_with ( "node:" ) { 463return Ok ( ResolvedRequest :: module ( specifier. to_owned (), None )); 464} 465if let Some ( name) =Self :: early_scheme ( specifier) ?{ 466return Ok ( ResolvedRequest :: module ( name, None )); 467} 468if overrides &&let Some ( target) =self . type_override ( specifier, & referrer. name ) ?{ 469let anchor =self . type_config_referrer ( & referrer. name ); 470let mut resolved =Box :: pin ( self . resolve_tracked_for ( & target, & anchor, mode, false )) 471. await 472. map_err ( |failure|ResolutionFailure :: Other ( failure. into ())) ?; 473 resolved. type_subpath =None ; 474return Ok ( resolved); 475} 476if let Some ( path) = referrer. path . as_deref () 477 && referrer. npm 478{ 479if let Some ( url) =data_url ( specifier) { 480return Ok ( ResolvedRequest :: module ( url. to_string (), None )); 481} 482if self . npm . owning_instance ( path). is_none () { 483return Err ( Error :: Module ( format! ( 484"npm cache path is not owned by a prepared instance: {}" , 485 path. display () 486)) 487. into ()); 488} 489return self . resolve_npm_from_file ( specifier, path, mode). await ; 490} 491let mapped =if referrer. inline { 492None 493} else if referrer. remote { 494self . remote . resolve ( specifier, & referrer. name ). await ? 495} else { 496self . packages 497. resolve ( specifier, & referrer. name , & self . remote ) 498. await ? 499}; 500let effective =match & mapped{ 501Some ( crate :: local_packages:: Resolution :: Specifier ( target)) => target. as_str (), 502 _ => specifier, 503}; 504if mapped. is_none () && !effective. contains ( ':' ) && !effective. starts_with ([ '.' , '/' , '#' ]) { 505return Err ( ResolutionFailure :: UnboundSpecifier ( effective. to_owned ())); 506} 507if overrides 508 && effective != specifier 509 &&let Some ( target) =self . type_override ( effective, & referrer. name ) ? 510{ 511let mut resolved = 512Box :: pin ( self . resolve_tracked_for ( & target, & referrer. name , mode, false )) 513. await 514. map_err ( |failure|ResolutionFailure :: Other ( failure. into ())) ?; 515 resolved. type_subpath =None ; 516return Ok ( resolved); 517} 518if effective. starts_with ( "npm:" ) { 519return self 520. resolve_npm_specifier ( effective, & referrer, mode, !overrides) 521. await ; 522} 523match self . resolve_target ( specifier, & referrer, mapped, ResolveMode :: Acquire ) ?{ 524ResolutionTarget :: Module ( name) =>Ok ( ResolvedRequest :: module ( name, None )), 525ResolutionTarget :: Npm ( specifier) =>{ 526self . resolve_npm_specifier ( & specifier, & referrer, mode, !overrides) 527. await 528} 529ResolutionTarget :: Jsr ( specifier) =>{ 530let request =crate :: registry:: parse_jsr_request ( & specifier) 531. map_err ( |error|Error :: Module ( error. to_string ())) ?; 532let key = request. request_key (); 533let name =self 534. store 535. resolve_request ( & request) 536. await 537. map_err ( |error|Error :: Module ( error. to_string ())) ?; 538Ok ( ResolvedRequest :: module ( name, Some ( key))) 539} 540ResolutionTarget :: Remote ( name) =>{ 541let declarations =if referrer. remote || referrer. inline { 542Vec :: new () 543} else { 544self . packages . remote_dependencies ( & referrer. name ) ? 545}; 546self . remote 547. import ( & name, & referrer. name , & declarations) 548. await ?; 549self . check_access ( & name) ?; 550Ok ( ResolvedRequest :: module ( name, None )) 551} 552} 553} 554 555/// Classify the referrer once per resolution. The local path reuses 556/// `require_referrer_path`; npm/remote are checked here, ownership at 557/// use sites. Invalid file URLs and non-UTF8 paths error. 558pub ( crate ) fn classify_referrer ( & self , referrer : & str ) ->Result < Referrer , Error > { 559let path =Self :: require_referrer_path ( referrer); 560if path. is_none () && url:: Url :: parse ( referrer). is_ok_and ( |url| url. scheme () =="file" ) { 561return Err ( Error :: Module ( format! ( "Invalid file referrer: {referrer}" ))); 562} 563let npm = path 564. as_deref () 565. is_some_and ( |path|self . npm . is_reserved_npm_path ( path)); 566let name =match & path{ 567Some ( path) => path 568. to_str () 569. ok_or_else ( ||Error :: Module ( "Module paths must be valid UTF-8" . into ())) ? 570. to_owned (), 571None => referrer. to_owned (), 572}; 573Ok ( Referrer { 574remote : http_url ( referrer). is_some (), 575inline : data_url ( referrer). is_some (), 576 name, 577 path, 578 npm, 579}) 580} 581 582/// `ext:` rejects, `avril:` passes through untouched, `node:` rejects. 583/// Returns `Some` when the specifier decides the resolution before any 584/// mapping; `None` continues the tree. 585pub ( crate ) fn early_scheme ( specifier : & str ) ->Result < Option < String >, Error > { 586if specifier. starts_with ( "ext:" ) { 587return Err ( Error :: Module ( "Extension modules are private" . into ())); 588} 589if specifier. starts_with ( "avril:" ) { 590return Ok ( Some ( specifier. to_owned ())); 591} 592if specifier. starts_with ( "node:" ) { 593return Err ( Error :: Module ( format! ( 594"node: builtin modules are not supported: {specifier}" 595))); 596} 597Ok ( None ) 598} 599 600/// Scheme dispatch over the (possibly mapped) specifier. 601/// `mode.allow_missing()` controls target existence probing at every call 602/// site: `Acquire` requires the target, `PreparedOnly` only forms it. 603/// Every other branch is mode-independent. 604pub ( crate ) fn resolve_target ( 605& self , 606specifier : & str , 607referrer : & Referrer , 608mapped : Option < crate :: local_packages:: Resolution >, 609mode : ResolveMode , 610) ->Result < ResolutionTarget , Error > { 611let allow_missing = mode. allow_missing (); 612let mapped =match mapped{ 613Some ( crate :: local_packages:: Resolution :: Module ( name)) =>{ 614let logical =lexical_absolute ( Path :: new ( & name)) ?; 615if self . npm . is_reserved_npm_path ( & logical) { 616self . npm 617. check_access ( & logical) 618. map_err ( |error|Error :: Module ( error. to_string ())) ?; 619let name = logical 620. into_os_string () 621. into_string () 622. map_err ( |_|Error :: Module ( "Module paths must be valid UTF-8" . into ())) ?; 623return Ok ( ResolutionTarget :: Module ( name)); 624} 625if !allow_missing{ 626self . target ( & name) ?; 627} 628return Ok ( ResolutionTarget :: Module ( name)); 629} 630Some ( crate :: local_packages:: Resolution :: Remote ( name)) =>{ 631self . check_access ( & name) ?; 632return Ok ( ResolutionTarget :: Module ( name)); 633} 634Some ( crate :: local_packages:: Resolution :: Specifier ( target)) =>Some ( target), 635None =>None , 636}; 637let specifier = mapped. as_deref (). unwrap_or ( specifier); 638if let Some ( name) =Self :: early_scheme ( specifier) ?{ 639return Ok ( ResolutionTarget :: Module ( name)); 640} 641if let Some ( url) =data_url ( specifier) { 642return Ok ( ResolutionTarget :: Module ( url. to_string ())); 643} 644if referrer. inline && url:: Url :: parse ( specifier). is_err () { 645return Err ( Error :: Module ( format! ( 646"Data modules require absolute import URLs: {specifier}" 647))); 648} 649if specifier. starts_with ( "jsr:" ) { 650return Ok ( ResolutionTarget :: Jsr ( specifier. to_owned ())); 651} 652if specifier. starts_with ( "npm:" ) { 653return Ok ( ResolutionTarget :: Npm ( specifier. to_owned ())); 654} 655if referrer 656. name 657. starts_with ( self . store . options (). jsr_registry . as_str ()) 658{ 659if specifier. starts_with ( "./" ) 660 || specifier. starts_with ( "../" ) 661 || specifier. starts_with ( '/' ) 662 || specifier. starts_with ( self . store . options (). jsr_registry . as_str ()) 663{ 664let name =self 665. store 666. resolve_relative ( specifier, & referrer. name ) 667. map_err ( |error|Error :: Module ( error. to_string ())) ?; 668self . check_access ( & name) ?; 669return Ok ( ResolutionTarget :: Module ( name)); 670} 671return Err ( Error :: Module ( format! ( 672"Unsupported module specifier: {specifier}" 673))); 674} 675let remote =if let Some ( url) =http_url ( specifier) { 676Some ( url) 677} else if referrer. remote 678 &&( specifier. starts_with ( "./" ) 679 || specifier. starts_with ( "../" ) 680 || specifier. starts_with ( '/' ) 681 || specifier. starts_with ( '?' ) 682 || specifier. starts_with ( '#' )) 683{ 684let base =self . store . remote_base ( & referrer. name ); 685let base = base. map_err ( |error|Error :: Module ( error. to_string ())) ?; 686Some ( 687 url:: Url :: parse ( & base) 688. and_then ( |url| url. join ( specifier)) 689. map_err ( |error|Error :: Module ( error. to_string ())) ?, 690) 691} else { 692None 693}; 694if let Some ( url) = remote{ 695let name = url. to_string (); 696self . check_access ( & name) ?; 697return Ok ( ResolutionTarget :: Remote ( name)); 698} 699if referrer. remote { 700return Err ( Error :: Module ( format! ( 701"Unsupported module specifier: {specifier}" 702))); 703} 704let path =Path :: new ( specifier); 705let path =if specifier 706. get ( ..5 ) 707. is_some_and ( |scheme| scheme. eq_ignore_ascii_case ( "file:" )) 708{ 709let url = url:: Url :: parse ( specifier) 710. map_err ( |_|Error :: Module ( format! ( "Invalid file URL: {specifier}" ))) ?; 711if url. query (). is_some () || url. fragment (). is_some () { 712return Err ( Error :: Module ( 713"File URL queries and fragments are not supported yet" . into (), 714)); 715} 716 url. to_file_path () 717. map_err ( |_|Error :: Module ( format! ( "Invalid file URL: {specifier}" ))) ? 718} else if path. is_absolute () { 719 path. to_owned () 720} else if specifier. starts_with ( "./" ) || specifier. starts_with ( "../" ) { 721Path :: new ( & referrer. name ). parent (). unwrap (). join ( path) 722} else { 723return Err ( Error :: Module ( format! ( 724"Unsupported module specifier: {specifier}" 725))); 726}; 727self . resolve_local_target ( & path, specifier, allow_missing) 728. map ( ResolutionTarget :: Module ) 729} 730 731/// Prepare every declared npm dependency of the owning config together 732/// with on-demand requests, keeping one resolution handle per config 733/// root so different peer environments never share bindings. 734pub ( crate ) async fn prepare_npm_for_config ( 735& self , 736referrer : & str , 737extra : & [ String ], 738) ->Result < PreparedNpmRoots , Error > { 739// Reserved npm paths are never owned by a local config: resolve 740// them through the instance tree instead of config discovery. 741let canonical =Path :: new ( referrer) 742. canonicalize () 743. unwrap_or_else ( |_|Path :: new ( referrer). to_owned ()); 744if self . npm . is_reserved_npm_path ( & canonical) { 745return Err ( Error :: Module ( format! ( 746"npm cache paths resolve through their owning instance, not a config: {referrer}" 747))); 748} 749let root =self 750. packages 751. root ( referrer) 752. map_err ( |error|Error :: Module ( error. to_string ())) ?; 753if let Some ( handle) =self . npm_handles . borrow (). get ( & root) 754 && extra. iter (). all ( |request|{ 755self . npm 756. resolve_in ( handle, request, ResolutionMode :: Import ) 757. is_ok () 758}) 759{ 760return Ok ( handle. clone ()); 761} 762let mut requests =self 763. packages 764. npm_dependencies ( referrer) 765. map_err ( |error|Error :: Module ( error. to_string ())) ?; 766 requests. extend ( extra. iter (). cloned ()); 767 requests. sort (); 768 requests. dedup (); 769let handle =self 770. npm 771. prepare ( 772& requests 773. into_iter () 774. map ( |request|NpmRootRequest { 775 request, 776context : BTreeMap :: new (), 777}) 778. collect ::< Vec < _ >>(), 779) 780. await 781. map_err ( |error|Error :: Module ( error. to_string ())) ?; 782let mut handles =self . npm_handles . borrow_mut (); 783let entry = handles. entry ( root). or_default (); 784for ( key, instance) in & handle. roots { 785let request = key. split ( '\0' ). next (). unwrap (); 786if entry. keys_for_request ( request). is_empty () { 787 entry. roots . insert ( key. clone (), instance. clone ()); 788} 789} 790Ok ( entry. clone ()) 791} 792 793/// Prepares the npm roots a local module can reach before its edges 794/// resolve, returning lock keys to record for CommonJS `require`. 795pub ( crate ) async fn prepare_npm_roots ( 796& self , 797name : & str , 798format : ModuleFormat , 799specifiers : & [ String ], 800) ->Result < Vec < String >, Error > { 801// Remote and npm-owned modules resolve on demand instead. 802if self . npm . is_reserved_npm_path ( Path :: new ( name)) 803 ||http_url ( name). is_some () 804 ||data_url ( name). is_some () 805{ 806return Ok ( Vec :: new ()); 807} 808if format ==ModuleFormat :: CommonJs { 809let handle =self . prepare_npm_for_config ( name, & []). await ?; 810return Ok ( handle. roots . into_keys (). collect ()); 811} 812// Literal npm: imports of one local module resolve together 813// so their peer environments preselect jointly. Mapped-away 814// literals never become roots: only what the import map 815// still selects as npm: is prepared. 816let mut literal =Vec :: new (); 817for specifierin specifiers 818. iter () 819. filter ( |specifier| specifier. starts_with ( "npm:" )) 820{ 821match self . packages . resolve ( specifier, name, & self . remote ). await ?{ 822Some ( crate :: local_packages:: Resolution :: Specifier ( target)) 823if target. starts_with ( "npm:" ) => 824{ 825 literal. push ( target); 826} 827Some ( _) =>{} 828None => literal. push ( specifier. clone ()), 829} 830} 831if !literal. is_empty () { 832self . prepare_npm_for_config ( name, & literal). await ?; 833} 834Ok ( Vec :: new ()) 835} 836 837async fn resolve_npm_from_file ( 838& self , 839specifier : & str , 840path : & Path , 841mode : ResolutionMode , 842) ->Result < ResolvedRequest , ResolutionFailure > { 843if specifier. starts_with ( "node:" ) { 844return Err ( Error :: Module ( format! ( 845"node: builtin modules are not supported: {specifier}" 846)) 847. into ()); 848} 849if specifier. starts_with ( "npm:" ) { 850let request =crate :: registry:: npm:: parse_npm_request ( specifier) 851. map_err ( |error|Error :: Module ( error. to_string ())) ?; 852let handle =self 853. npm 854. prepare ( & [ NpmRootRequest { 855request : request. request_key (), 856context : BTreeMap :: new (), 857}]) 858. await 859. map_err ( |error|Error :: Module ( error. to_string ())) ?; 860let resolved =self . npm . resolve_in ( & handle, specifier, mode) ?; 861let key = handle 862. roots 863. iter () 864. find ( |( _, instance) |* instance ==& resolved. instance ) 865. map ( |( key, _) | key. clone ()) 866. ok_or_else ( ||{ 867Error :: Module ( format! ( "npm package is not prepared: {specifier}" )) 868}) ?; 869return ResolvedRequest :: npm ( resolved, specifier, Some ( key)) 870. map_err ( ResolutionFailure :: Other ); 871} 872let resolved =self . npm . resolve_from_file ( specifier, path, mode) ?; 873ResolvedRequest :: npm ( resolved, specifier, None ). map_err ( ResolutionFailure :: Other ) 874} 875 876async fn resolve_npm_specifier ( 877& self , 878specifier : & str , 879referrer : & Referrer , 880mode : ResolutionMode , 881type_root : bool , 882) ->Result < ResolvedRequest , ResolutionFailure > { 883if referrer. path . is_some () && !referrer. remote && !( type_root && mode. is_type ()) { 884let handle =self 885. prepare_npm_for_config ( & referrer. name , std:: slice:: from_ref ( & specifier. to_owned ())) 886. await ?; 887let resolved =self . npm . resolve_in ( & handle, specifier, mode) ?; 888let key =self . npm_root_key ( & handle, specifier, & resolved); 889return ResolvedRequest :: npm ( resolved, specifier, Some ( key)) 890. map_err ( ResolutionFailure :: Other ); 891} 892let request =crate :: registry:: npm:: parse_npm_request ( specifier) 893. map_err ( |error|Error :: Module ( error. to_string ())) ?; 894if !mode. is_type () { 895let resolved =self 896. npm 897. resolve_request_parsed ( & request, mode) 898. await 899. map_err ( |error|Error :: Module ( error. to_string ())) ?; 900let tracked =self 901. npm 902. tracking_key ( & request. request_key (), & resolved. instance ) 903. unwrap_or_else ( || request. request_key ()); 904return ResolvedRequest :: npm ( resolved, specifier, Some ( tracked)) 905. map_err ( ResolutionFailure :: Other ); 906} 907let context =self . types . context . borrow (). clone (); 908let handle =self 909. npm 910. prepare ( & [ NpmRootRequest { 911request : request. request_key (), 912 context, 913}]) 914. await 915. map_err ( |error|Error :: Module ( error. to_string ())) ?; 916let resolved =self . npm . resolve_in ( & handle, specifier, mode) ?; 917let tracked =self . npm_root_key ( & handle, specifier, & resolved); 918ResolvedRequest :: npm ( resolved, specifier, Some ( tracked)). map_err ( ResolutionFailure :: Other ) 919} 920 921/// The exact recorded root key behind a handle resolution, so lock 922/// tracking keeps successful contexts only. 923fn npm_root_key ( 924& self , 925handle : & PreparedNpmRoots , 926specifier : & str , 927resolved : & ResolvedNpmModule , 928) ->String { 929let request = 930crate :: registry:: npm:: parse_npm_request ( specifier). map ( |request| request. request_key ()); 931let request = request. unwrap_or_else ( |_| specifier. to_owned ()); 932 handle 933. keys_for_request ( & request) 934. into_iter () 935. find ( |key|{ 936 handle 937. roots 938. get ( key) 939. is_some_and ( |instance| instance ==& resolved. instance ) 940}) 941. unwrap_or ( request) 942} 943 944/// Synchronous metadata-only npm resolution: prepared handles first, 945/// then globally prepared roots. Never performs I/O. 946pub ( crate ) fn resolve_import_meta_npm ( 947& self , 948specifier : & str , 949name : & str , 950remote : bool , 951) ->Result < String , Error > { 952let request =crate :: registry:: npm:: parse_npm_request ( specifier) 953. map_err ( |error|Error :: Module ( error. to_string ())) ?; 954if !remote 955 &&data_url ( name). is_none () 956 &&let Ok ( root) =self . packages . root ( name) 957 &&let Some ( handle) =self . npm_handles . borrow (). get ( & root) 958 &&let Ok ( resolved) =self 959. npm 960. resolve_in ( handle, specifier, ResolutionMode :: Import ) 961{ 962let path = resolved 963. path 964. to_str () 965. ok_or_else ( ||Error :: Module ( "Invalid module path" . into ())) ?; 966return self 967. module_url ( path) 968. ok_or_else ( ||Error :: Module ( "Invalid module path" . into ())); 969} 970let path =self 971. npm 972. resolve_import_meta_parsed ( & request) 973. map_err ( |error|Error :: Module ( error. to_string ())) ?; 974self . module_url ( & path) 975. ok_or_else ( ||Error :: Module ( "Invalid module path" . into ())) 976} 977} 978 979pub ( crate ) fn lexical_absolute ( path : & Path ) ->Result < PathBuf , Error > { 980if !path. is_absolute () { 981return Err ( Error :: Module ( "Expected an absolute module path" . into ())); 982} 983let mut normalized =PathBuf :: new (); 984for componentin path. components () { 985match component{ 986 std:: path:: Component :: Prefix ( prefix) => normalized. push ( prefix. as_os_str ()), 987 std:: path:: Component :: RootDir => normalized. push ( component. as_os_str ()), 988 std:: path:: Component :: CurDir =>{} 989 std:: path:: Component :: ParentDir =>{ 990 normalized. pop (); 991} 992 std:: path:: Component :: Normal ( part) => normalized. push ( part), 993} 994} 995Ok ( normalized) 996} 997 998pub fn data_url ( name : & str ) ->Option < url:: Url > { 999 url:: Url :: parse ( name) 1000. ok () 1001. filter ( |url| url. scheme () =="data" ) 1002} 1003 1004pub fn http_url ( name : & str ) ->Option < url:: Url > { 1005 url:: Url :: parse ( name) 1006. ok () 1007. filter ( |url|matches! ( url. scheme (), "http" |"https" )) 1008} 1009 1010/// An offline graph over a scratch project directory, for tests that never 1011/// touch the network: registries point at `.invalid`, the lock is never 1012/// written, and the runtime is returned so it outlives the graph. 1013# [ cfg ( test )] 1014pub ( crate ) fn offline_graph ( dir : & Path ) ->( tokio:: runtime:: Runtime , ModuleGraph ) { 1015let caps =crate :: caps:: Capabilities :: new ( crate :: caps:: Initial :: new ( false ), dir. to_owned ()); 1016let authority =crate :: authority:: SourceAuthority :: execute ( caps); 1017let runtime = tokio:: runtime:: Builder :: new_multi_thread () 1018. worker_threads ( 2 ) 1019. enable_all () 1020. build () 1021. unwrap (); 1022let graph =ModuleGraph :: open ( 1023 runtime. handle (). clone (), 1024 authority, 1025crate :: registry:: Options { 1026jsr_registry : url:: Url :: parse ( "https://example.invalid/" ). unwrap (), 1027npm_registry : url:: Url :: parse ( "https://example.invalid/" ). unwrap (), 1028cache : dir. join ( "cache" ), 1029lock : dir. join ( "avril.lock" ), 1030project_root : dir. to_owned (), 1031offline : true , 1032frozen : false , 1033update : false , 1034ephemeral_lock : false , 1035vendor : None , 1036}, 1037) 1038. unwrap (); 1039( runtime, graph) 1040}