char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
51266f9
main
1use std:: cell:: RefCell ; 2use std:: collections:: HashSet ; 3use std:: rc:: Rc ; 4use std:: sync:: Arc ; 5 6use avril_runtime:: caps::{ Capabilities , Initial }; 7use avril_runtime:: config_edit::{ self , Addition , Change }; 8use avril_runtime:: import_map:: ImportMap ; 9use avril_runtime:: package_config; 10use avril_runtime:: registry::{ 11Options , PackageStore , ProgressEvent , ProgressSink , npm:: resolution:: NpmResolver , 12 parse_jsr_request, 13}; 14use futures:: executor:: block_on; 15 16use crate ::{ cli, cli:: ParseError }; 17use avril_runtime:: modules:: ModuleGraph ; 18 19pub struct Args { 20config : Option < String >, 21pre : bool , 22offline : bool , 23automatic_types : bool , 24jsr_registry : url:: Url , 25npm_registry : url:: Url , 26cache_dir : Option < String >, 27values : Vec < String >, 28} 29 30/// `avril add` argument parsing failed. 31# [ derive ( Debug , thiserror :: Error )] 32pub enum AddParseError { 33# [ error ( transparent )] 34Generic ( # [ from ] ParseError ), 35# [ error ( "Expected one config path" )] 36DuplicateConfig , 37# [ error ( "{0}" )] 38Url ( # [ from ] url:: ParseError ), 39} 40 41pub fn parse ( args : & [ String ]) -> std:: result:: Result < Args , AddParseError > { 42let command = cli:: command ( "add" ). ok_or_else ( ||ParseError :: Usage ( cli:: usage ())) ?; 43let mut parsed =Args { 44config : None , 45pre : false , 46offline : false , 47automatic_types : true , 48jsr_registry : url:: Url :: parse ( "https://jsr.io/" ). unwrap (), 49npm_registry : url:: Url :: parse ( "https://registry.npmjs.org/" ). unwrap (), 50cache_dir : None , 51values : Vec :: new (), 52}; 53for argin args{ 54if arg. starts_with ( '-' ) { 55 command. check ( arg). map_err ( ParseError :: Check ) ?; 56} 57match arg. as_str () { 58"--pre" => parsed. pre =true , 59"--offline" => parsed. offline =true , 60"--no-types-packages" => parsed. automatic_types =false , 61 _if arg. starts_with ( '-' ) =>{ 62let ( flag, value) =match arg. split_once ( '=' ) { 63Some (( flag, value)) if !value. is_empty () =>{ 64( flag. strip_prefix ( "--" ). unwrap_or ( flag). to_owned (), value) 65} 66Some (( flag, _)) =>{ 67return Err ( ParseError :: MissingValue { 68flag : flag. strip_prefix ( "--" ). unwrap_or ( flag). to_owned (), 69equals : "=" , 70} 71. into ()); 72} 73None =>{ 74return Err ( ParseError :: MissingValue { 75flag : arg. strip_prefix ( "--" ). unwrap_or ( arg). to_owned (), 76equals : "" , 77} 78. into ()); 79} 80}; 81match flag. as_str () { 82"config" =>{ 83if parsed. config . replace ( value. to_owned ()). is_some () { 84return Err ( AddParseError :: DuplicateConfig ); 85} 86} 87"jsr-registry" => parsed. jsr_registry = url:: Url :: parse ( value) ?, 88"npm-registry" => parsed. npm_registry = url:: Url :: parse ( value) ?, 89"cache-dir" => parsed. cache_dir =Some ( value. to_owned ()), 90 _ =>return Err ( ParseError :: UnexpectedValue { flag}. into ()), 91} 92} 93 _ => parsed. values . push ( arg. clone ()), 94} 95} 96Ok ( parsed) 97} 98 99/// `avril add`: pins unversioned targets to the newest release, edits the config, then 100/// caches what the new mapping can import, as `avril cache` would. 101pub fn run ( args : Args ) ->Result <(), Box < dyn std:: error:: Error >> { 102let cwd = std:: env:: current_dir () ?. canonicalize () ?; 103let Args { 104 config, 105 pre, 106 offline, 107 automatic_types, 108 jsr_registry, 109 npm_registry, 110 cache_dir, 111 values, 112} = args; 113let cache = cache_dir 114. map ( |dir| cwd. join ( dir)) 115. unwrap_or_else ( || cli:: default_cache ( & cwd)); 116let config = config. map ( |config| cwd. join ( config)); 117let command = cli:: command ( "add" ). ok_or_else ( cli:: usage) ?; 118let ( alias, target) =match values. as_slice () { 119[ target] =>( None , target. as_str ()), 120[ alias, target] =>( Some ( alias. as_str ()), target. as_str ()), 121 _ =>return Err ( command. usage (). into ()), 122}; 123let caps =Capabilities :: new ( Initial :: new ( false ), cwd. clone ()); 124let authority = avril_runtime:: authority:: SourceAuthority :: execute ( caps. clone ()); 125let config =match config{ 126Some ( config) => config, 127None =>ModuleGraph :: project_root_for_add ( 128& authority, 129 cwd. join ( "avril.conf" ) 130. to_str () 131. ok_or ( "Paths must be UTF-8" ) ?, 132) ? 133. join ( "avril.conf" ), 134}; 135let directory = config. parent (). ok_or ( "Config needs a parent directory" ) ?; 136let host = avril_host:: Host :: new () ?; 137let store =Arc :: new ( 138PackageStore :: new_with_authority ( 139 host. handle (), 140Options { 141 jsr_registry, 142 npm_registry, 143 cache, 144lock : directory. join ( "avril.lock" ), 145project_root : directory. to_owned (), 146 offline, 147frozen : false , 148update : false , 149ephemeral_lock : false , 150vendor : None , 151}, 152 authority. clone (), 153) ? 154. with_progress ( progress_sink ()), 155); 156let npm_target = target. starts_with ( "npm:" ); 157let target =match config_edit:: unversioned ( target) ?{ 158Some ( name) =>{ 159if npm_target{ 160if pre{ 161return Err ( 162"--pre does not apply to npm: targets; select a prerelease tag explicitly" 163. into (), 164); 165} 166phase ( format! ( "Selecting version for {target:?}..." )); 167let version =block_on ( store. npm_select_version ( target)) ?; 168 config_edit:: with_caret ( target, & name, & version) 169} else { 170phase ( format! ( "Selecting version for {target:?}..." )); 171let version =block_on ( store. latest ( & name, pre)) ?. ok_or_else ( ||{ 172if pre{ 173format! ( "{name} has no non-yanked versions" ) 174} else { 175format! ( 176"{name} has no non-yanked stable versions; --pre allows prereleases" 177) 178} 179}) ?; 180 config_edit:: with_caret ( target, & name, & version) 181} 182} 183None if pre =>{ 184if npm_target{ 185return Err ( 186"--pre does not apply to npm: targets; select a prerelease tag explicitly" 187. into (), 188); 189} 190return Err ( "--pre only applies to targets without a version" . into ()); 191} 192None => target. to_owned (), 193}; 194let package = config_edit:: whole_package ( & target) ?; 195// Packages belong in deps; only an alias for a subpath or prefix needs imports. 196let addition =match alias{ 197Some ( alias) 198if !config_edit:: selects_whole_package ( & target) ? 199 || !package_config:: valid_name ( alias) => 200{ 201Addition :: Import { alias, target : & target} 202} 203 name =>Addition :: Dependency { name, package : & package}, 204}; 205let binding = addition. binding () ?; 206let change = config_edit:: add_file ( & config, & addition) ?; 207report ( & change, & binding, addition. target (), & config, & cwd); 208let prefix =match addition{ 209Addition :: Import { alias, target} if alias. ends_with ( '/' ) =>{ 210Some (( alias. to_owned (), target. to_owned ())) 211} 212Addition :: Import { ..} =>None , 213Addition :: Dependency { package, ..} =>{ 214Some (( format! ( "{binding}/" ), format! ( "{package}/" ))) 215} 216}; 217let type_target = target. clone (); 218let requests =if npm_target{ 219if prefix. is_some () { 220phase ( format! ( "Preparing exports for {target:?}..." )); 221} 222npm_add_requests ( & store, & target, prefix. as_ref ()) ? 223} else if let Some (( alias, target)) = prefix{ 224let base = url:: Url :: from_file_path ( & config). map_err ( |_|"Paths must be absolute" ) ?; 225let map =ImportMap :: parse ( 226& format! ( 227"imports: {{{}: {}}}" , 228 serde_json:: to_string ( & alias) ?, 229 serde_json:: to_string ( & target) ? 230), 231& base, 232) ?; 233let referrer = base. join ( ".avril-add.js" ) ?; 234let export = 235 |request : & str |parse_jsr_request ( request). map ( |request| request. export_name ()); 236phase ( format! ( "Preparing exports for {target:?}..." )); 237// The prefix's own normalized subpath, found by probing beneath it. 238let probe =export ( & format! ( "{target}x" )) ?; 239let prefix =& probe[ "./" . len () ..probe. len () -1 ]; 240let mut requests =Vec :: new (); 241for keyin block_on ( store. exports ( & target)) ?{ 242// Each export the prefix itself or a suffix beneath it reaches under the 243// runtime's import map rules, which reject escapes and encoded separators. 244// Normalization drops one trailing slash, so a key ending in '/' needs another. 245let suffix = key 246. strip_prefix ( "./" ) 247. and_then ( |key| key. strip_prefix ( prefix)); 248let mut suffixes =[ String :: new ()]. into_iter (). chain ( 249 suffix 250. into_iter () 251. flat_map ( |suffix|[ suffix. to_owned (), format! ( "{suffix}/" )]), 252); 253if let Some ( request) = suffixes. find_map ( |suffix|{ 254let request = map. resolve ( & format! ( "{alias}{suffix}" ), & referrer). ok () ??; 255( export ( & request). ok () ? == key). then_some ( request) 256}) { 257 requests. push ( request); 258} 259} 260 requests 261} else { 262vec! [ target] 263}; 264let graph =ModuleGraph :: open_shared ( authority, store. clone ()); 265 graph. set_automatic_types ( automatic_types); 266 graph. set_typescript_version ( avril_lsp:: BACKEND_VERSION ) ?; 267// Warm through the updated config so joint root contexts match what 268// the runtime prepares: registry-referrer contexts would pin different 269// roots than a frozen run looks up once other deps or peers exist. 270let referrer =if npm_target{ 271 config. to_str (). ok_or ( "Paths must be UTF-8" ) ?. to_owned () 272} else { 273 store. options (). jsr_registry . to_string () 274}; 275let warn_unsupported =Rc :: new ({ 276let warned =RefCell :: new ( HashSet ::< String >:: new ()); 277move |specifier : & str , referrer : & str |{ 278if warned. borrow_mut (). insert ( specifier. to_owned ()) { 279use std:: io:: Write ; 280let _ =writeln! ( 281 std:: io:: stderr (), 282"Warning: skipping unsupported Node.js builtin import {specifier:?} from {referrer:?}; builtin modules remain unsupported at runtime" , 283); 284} 285} 286}); 287let type_requests =if requests. is_empty () { 288vec! [ type_target] 289} else { 290 requests. clone () 291}; 292for requestin requests{ 293phase ( format! ( "Caching {request:?}..." )); 294block_on ( graph. prepare_for_add ( request, referrer. clone (), warn_unsupported. clone ())) ?; 295} 296for requestin type_requests{ 297phase ( format! ( "Caching types for {request:?}..." )); 298block_on ( graph. prepare_types_for_request ( & request, & referrer)) ?; 299} 300Ok (()) 301} 302 303/// Cacheable requests for a whole npm package or the export prefix a 304/// prefix target names: exact subpaths resolve as a single request, 305/// anything else enumerates ESM exports beneath that scope only. 306fn npm_add_requests ( 307store : & Arc < PackageStore >, 308target : & str , 309prefix : Option < & ( String , String )>, 310) ->Result < Vec < String >, Box < dyn std:: error:: Error >> { 311let Some (( _, scope)) = prefixelse { 312return Ok ( vec! [ target. to_owned ()]); 313}; 314let resolver =NpmResolver :: new ( store. clone ()); 315Ok ( block_on ( resolver. export_requests ( scope)) ?) 316} 317 318fn phase ( message : String ) { 319use std:: io:: Write ; 320let message =crate :: progress:: color_action ( message, avril_ext_console:: colors ( true )); 321let _ =writeln! ( std:: io:: stderr (), "{message}" ); 322} 323 324fn progress_sink () ->ProgressSink { 325Arc :: new ( |event : & ProgressEvent |match event{ 326ProgressEvent :: NpmMetadata { name} =>{ 327phase ( format! ( "Fetching npm metadata for {name:?}..." )); 328} 329ProgressEvent :: NpmArchive { name, version} =>{ 330phase ( format! ( 331"Downloading npm archive for {:?}..." , 332format! ( "{name}@{version}" ), 333)); 334} 335ProgressEvent :: NpmMaterialized { name, version} =>{ 336phase ( format! ( 337"Materialized npm package {:?}..." , 338format! ( "{name}@{version}" ), 339)); 340} 341ProgressEvent :: SourceDownload { ..} 342 |ProgressEvent :: NpmMaterializing { ..} 343 |ProgressEvent :: NpmResolving =>{} 344}) 345} 346 347fn report ( 348change : & Change , 349alias : & str , 350target : & str , 351config : & std:: path:: Path , 352cwd : & std:: path:: Path , 353) { 354let paint = |stderr : bool , style : & str , text : & str |{ 355if avril_ext_console:: colors ( stderr) { 356format! ( "\x1b[{style}m{text}\x1b[0m" ) 357} else { 358 text. to_owned () 359} 360}; 361let string = |text : & str |paint ( false , "32" , & serde_json:: to_string ( text). unwrap ()); 362let ( status, style, previous) =match change{ 363Change :: Added =>( "Added" , "1;32" , String :: new ()), 364Change :: Updated { previous} =>( 365"Updated" , 366"1;33" , 367format! ( "{} → " , previous. as_deref (). map_or ( "null" . into (), string)), 368), 369Change :: Unchanged =>( "Unchanged" , "1" , String :: new ()), 370}; 371let mut path =String :: new (); 372// Paths may contain terminal controls, like the strings escaped as JSON above. 373for chin config 374. strip_prefix ( cwd) 375. unwrap_or ( config) 376. to_string_lossy () 377. chars () 378{ 379if ch. is_control () { 380 path. extend ( ch. escape_default ()); 381} else { 382 path. push ( ch); 383} 384} 385// The status is decoration on stderr; stdout keeps just the resulting mapping. 386eprint! ( "{} " , paint ( true , style, status)); 387println! ( 388"{}: {previous}{} in {path}" , 389paint ( false , "1" , & serde_json:: to_string ( alias). unwrap ()), 390string ( target), 391); 392}