char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
05d50bd
main
1use std:: io:: Read ; 2use std:: path:: Path ; 3 4use crate :: config:: ValueKind ; 5use serde_fjson::{ Document , PathSegment }; 6 7/// What `add` writes: an alias in `imports`, or a whole `jsr:` package in `deps`, bound 8/// to its package name unless `name` renames it. 9# [ derive ( Debug , Clone , Copy )] 10pub enum Addition < ' a > { 11Import { 12alias : &' a str , 13target : &' a str , 14}, 15Dependency { 16name : Option < &' a str >, 17package : &' a str , 18}, 19} 20 21impl Addition < ' _ > { 22/// The specifier, or namespace of specifiers, that the addition binds. 23pub fn binding ( & self ) ->Result < String , String > { 24match * self { 25Self :: Import { alias, ..} =>Ok ( alias. to_owned ()), 26Self :: Dependency { name : Some ( name), ..} =>Ok ( name. to_owned ()), 27Self :: Dependency { name : None , package} =>package_name ( package), 28} 29} 30 31pub fn target ( & self ) ->& str { 32match * self { 33Self :: Import { target, ..} => target, 34Self :: Dependency { package, ..} => package, 35} 36} 37} 38 39fn package_name ( target : & str ) ->Result < String , String > { 40if target. starts_with ( "npm:" ) { 41return crate :: registry:: npm:: parse_npm_request ( target) 42. map ( |request| request. name ) 43. map_err ( |error| error. to_string ()); 44} 45crate :: registry:: parse_jsr_request ( target) 46. map ( |request| request. name ) 47. map_err ( |error| error. to_string ()) 48} 49 50/// Whether a registry target names a whole package, without any subpath or prefix. 51pub fn selects_whole_package ( target : & str ) ->Result < bool , String > { 52if target. starts_with ( "npm:" ) { 53let request = 54crate :: registry:: npm:: parse_npm_request ( target). map_err ( |error| error. to_string ()) ?; 55return Ok ( request. subpath . is_none () && !target. ends_with ( '/' )); 56} 57let request =crate :: registry:: parse_jsr_request ( target). map_err ( |error| error. to_string ()) ?; 58Ok ( request. subpath . is_none () && !target. ends_with ( '/' )) 59} 60 61/// The whole package a registry target selects from, keeping its version requirement. 62pub fn whole_package ( target : & str ) ->Result < String , String > { 63if target. starts_with ( "npm:" ) { 64let request = 65crate :: registry:: npm:: parse_npm_request ( target). map_err ( |error| error. to_string ()) ?; 66return Ok ( request. request_key ()); 67} 68let request =crate :: registry:: parse_jsr_request ( target). map_err ( |error| error. to_string ()) ?; 69let name = request. name . clone (); 70let version =after_name ( target, & name) 71. strip_prefix ( '@' ) 72. and_then ( |rest| rest. split ( '/' ). next ()) 73. map_or ( String :: new (), |version|format! ( "@{version}" )); 74Ok ( format! ( "jsr:{name}{version}" )) 75} 76 77# [ derive ( Debug , Clone , PartialEq , Eq )] 78pub enum Change { 79Added , 80/// The alias previously mapped elsewhere, or was blocked when `previous` is `None`. 81Updated { 82previous : Option < String >, 83}, 84Unchanged , 85} 86 87/// The package a registry target names without any version, which `add` fills in. 88pub fn unversioned ( target : & str ) ->Result < Option < String >, String > { 89if target. starts_with ( "npm:" ) { 90let request = 91crate :: registry:: npm:: parse_npm_request ( target). map_err ( |error| error. to_string ()) ?; 92return Ok ( request. range . is_none (). then ( || request. name . clone ())); 93} 94let request =crate :: registry:: parse_jsr_request ( target). map_err ( |error| error. to_string ()) ?; 95let name = request. name . clone (); 96Ok (( !after_name ( target, & name). starts_with ( '@' )). then_some ( name)) 97} 98 99/// Gives a target `unversioned` named `name` a caret range from `version`, keeping any 100/// subpath, as `deno add` does. 101pub fn with_caret ( target : & str , name : & str , version : & str ) ->String { 102if target. starts_with ( "npm:" ) { 103let request =crate :: registry:: npm:: parse_npm_request ( target) 104. map ( |request| request. subpath ) 105. unwrap_or_default (); 106let subpath = request. map_or ( String :: new (), |path|format! ( "/{path}" )); 107return format! ( "npm:{name}@^{version}{subpath}" ); 108} 109format! ( "jsr:{name}@^{version}{}" , after_name ( target, name)) 110} 111 112fn after_name < ' a >( target : &' a str , name : & str ) ->&' a str { 113let rest =& target[ "jsr:" . len () ..]; 114& rest. strip_prefix ( '/' ). unwrap_or ( rest)[ name. len () ..] 115} 116 117pub fn add ( source : & str , addition : & Addition ) ->Result <( String , Change ), String > { 118match * addition{ 119Addition :: Import { alias, target} =>{ 120if alias. is_empty () || alias. starts_with ([ '.' , '/' ]) || alias. contains ([ ':' , '\\' ]) { 121return Err ( "Expected a bare import alias" . into ()); 122} 123package_name ( target) ?; 124if alias. ends_with ( '/' ) != target. ends_with ( '/' ) { 125return Err ( "Slash-prefix aliases and targets must both end with '/'" . into ()); 126} 127} 128Addition :: Dependency { package, ..} =>{ 129if !selects_whole_package ( package) ?{ 130return Err ( "Dependencies are whole packages, without subpaths" . into ()); 131} 132} 133} 134let mut document = 135Document :: parse_object ( source). map_err ( |error| error. render ( "avril.conf" , source)) ?; 136let root = document. root (); 137let base = url:: Url :: parse ( if cfg! ( windows) { 138"file:///C:/package/avril.conf" 139} else { 140"file:///package/avril.conf" 141}) 142. unwrap (); 143crate :: package_config:: PackageConfig :: from_value ( root, & base) 144. map_err ( |error| error. render ( "avril.conf" , source)) ?; 145let ValueKind :: Object ( members) =& root. kind else { 146unreachable! () 147}; 148let field = |key : & str | members. iter (). find ( |member| member. key == key); 149let target = addition. target (); 150let ( existing, path, value) =match * addition{ 151Addition :: Import { alias, ..} =>{ 152let imports =field ( "imports" ); 153let existing = imports. and_then ( |imports|{ 154let ValueKind :: Object ( entries) =& imports. value . kind else { 155unreachable! () 156}; 157 entries 158. iter () 159. find ( |entry| entry. key == alias) 160. map ( |entry|( & entry. value , false )) 161}); 162if imports. is_some () { 163( 164 existing, 165vec! [ "imports" . into (), alias. into ()], 166 serde_json:: json!( target), 167) 168} else { 169( 170None , 171vec! [ "imports" . into ()], 172 serde_json:: json!({( alias): target}), 173) 174} 175} 176Addition :: Dependency { name, package} =>{ 177let implicit =package_name ( package) ?; 178// Naming a package after itself is the implicit form. 179let name = name. filter ( |name|* name != implicit); 180let binding = name. unwrap_or ( & implicit). to_owned (); 181let deps =field ( "deps" ); 182let entries =match deps. map ( |deps|& deps. value . kind ) { 183Some ( ValueKind :: Array ( entries)) => entries. as_slice (), 184None =>& [], 185 _ =>unreachable! (), 186}; 187let existing = 188 entries 189. iter () 190. enumerate () 191. find_map ( |( index, entry) |match & entry. kind { 192// A renamed replacement keeps this binding only as {from, name}. 193ValueKind :: String ( from) =>(( from. starts_with ( "jsr:" ) 194 || from. starts_with ( "npm:" )) 195 &&package_name ( from). as_deref () ==Ok ( binding. as_str ())) 196. then_some (( entry, name. is_some (), vec! [ "deps" . into (), index. into ()])), 197ValueKind :: Object ( fields) =>{ 198let field = |key : & str | fields. iter (). find ( |field| field. key == key); 199field ( "name" ) 200. is_some_and ( |name|{ 201 name. value . kind ==ValueKind :: String ( binding. clone ()) 202}) 203. then ( ||{ 204field ( "from" ). map ( |from|{ 205( 206& from. value , 207false , 208vec! [ "deps" . into (), index. into (), "from" . into ()], 209) 210}) 211}) 212. flatten () 213} 214 _ =>None , 215}); 216let entry =match name{ 217Some ( name) => serde_json:: json!({ "from" : package, "name" : name}), 218None => serde_json:: json!( package), 219}; 220if let Some (( existing, whole, path)) = existing{ 221let value =if whole{ 222 entry 223} else { 224 serde_json:: json!( target) 225}; 226( Some (( existing, whole)), path, value) 227} else if deps. is_some () { 228( 229None , 230vec! [ "deps" . into (), PathSegment :: Index ( entries. len ())], 231 entry, 232) 233} else { 234( None , vec! [ "deps" . into ()], serde_json:: json!([ entry])) 235} 236} 237}; 238let change =if let Some (( existing, whole)) = existing{ 239let previous =match & existing. kind { 240ValueKind :: String ( previous) =>Some ( previous. clone ()), 241 _ =>None , 242}; 243if !whole && previous. as_deref () ==Some ( target) { 244return Ok (( document. into_source (), Change :: Unchanged )); 245} 246Change :: Updated { previous} 247} else { 248Change :: Added 249}; 250 document 251. set ( & path, & value) 252. map_err ( |error| error. render ( "avril.conf" , source)) ?; 253let output = document. into_source (); 254crate :: package_config:: PackageConfig :: parse ( & output, & base) 255. map_err ( |error| error. render ( "avril.conf" , & output)) ?; 256Ok (( output, change)) 257} 258 259pub fn add_file ( path : & Path , addition : & Addition ) ->Result < Change , String > { 260let parent = path. parent (). ok_or ( "Config needs a parent directory" ) ?; 261 std:: fs:: create_dir_all ( parent). map_err ( |error| error. to_string ()) ?; 262let mut change =Change :: Unchanged ; 263crate :: registry:: update_file ( 264 path, 265 ||read ( path), 266 |source|{ 267let source = source. as_deref (). unwrap_or_default (); 268let edited; 269( edited, change) =add ( source, addition) ?; 270Ok (( edited != source). then ( || edited. into_bytes ())) 271}, 272) ?; 273Ok ( change) 274} 275 276fn read ( path : & Path ) ->Result < Option < String >, String > { 277let mut options = std:: fs:: OpenOptions :: new (); 278 options. read ( true ); 279# [ cfg ( unix )] 280{ 281use std:: os:: unix:: fs:: OpenOptionsExt ; 282 options. custom_flags ( libc:: O_NOFOLLOW | libc:: O_NONBLOCK ); 283} 284match options. open ( path) { 285Ok ( file) =>{ 286if !file 287. metadata () 288. map_err ( |error| error. to_string ()) ? 289. is_file () 290{ 291return Err ( "Config must be a regular file" . into ()); 292} 293let mut source =String :: new (); 294 file. take ( 32 * 1024 * 1024 +1 ) 295. read_to_string ( & mut source) 296. map_err ( |error| error. to_string ()) ?; 297if source. len () >32 * 1024 * 1024 { 298return Err ( "Config is too large" . into ()); 299} 300Ok ( Some ( source)) 301} 302Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>Ok ( None ), 303Err ( error) =>Err ( error. to_string ()), 304} 305}