char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1c8ffed
main
1use std:: collections:: HashSet ; 2use std:: path::{ Path , PathBuf }; 3 4use avril_core:: Error ; 5use avril_runtime:: bundle; 6use avril_runtime:: caps:: PathTarget ; 7use avril_runtime:: modules::{ ModuleGraph , http_url}; 8 9use crate :: cli:: ParseError ; 10use crate :: fragments::{ Arg , Parser , flag_value, reject}; 11 12pub struct BundleArgs { 13pub entry : crate :: fragments:: Entry , 14pub entries : Vec < String >, 15pub output : Output , 16pub bundle : bundle:: Options , 17} 18 19/// `avril bundle` argument parsing failed. 20# [ derive ( Debug , thiserror :: Error )] 21pub enum BundleParseError { 22# [ error ( transparent )] 23Generic ( # [ from ] ParseError ), 24# [ error ( "Unsupported --target {text}: expected es2015 to es2025 or esnext" )] 25BadTarget { text : String }, 26# [ error ( "--out and --outdir cannot be combined" )] 27OutAndOutdir , 28# [ error ( "--out requires exactly one entry module; use --outdir for multiple entries" )] 29OutRequiresSingle , 30# [ error ( "--no-splitting requires exactly one entry module" )] 31NoSplittingSingle , 32# [ error ( 33"Expected --define KEY=VALUE, where KEY is a dotted identifier and VALUE an expression" 34)] 35BadDefine , 36} 37 38pub ( crate ) fn target ( text : & str ) ->Result < Option < u32 >, BundleParseError > { 39if text =="esnext" { 40return Ok ( None ); 41} 42 text. strip_prefix ( "es" ) 43. and_then ( |year| year. parse (). ok ()) 44. filter ( |year|( 2015 ..=2025 ). contains ( year)) 45. map ( Some ) 46. ok_or_else ( ||BundleParseError :: BadTarget { text : text. to_owned () }) 47} 48 49pub fn parse ( argv : Vec < String >, cwd : PathBuf ) ->Result < BundleArgs , BundleParseError > { 50let mut parser =Parser :: new ( "bundle" , argv, & cwd) ?; 51let mut bundle = bundle:: Options :: default (); 52let ( mut output, mut outdir, mut raw) =( None , None , Vec :: new ()); 53while let Some ( arg) = parser. next () ?{ 54let arg =match arg{ 55Arg :: Positional ( arg) =>{ 56 raw. push ( arg); 57continue ; 58} 59Arg :: Flag ( arg) 60if matches! ( 61 arg. as_str (), 62"-o" |"--out" |"--outdir" |"--target" |"--define" 63) => 64{ 65let value = parser. value ( ParseError :: SpacedValue { flag : arg. clone () }) ?; 66format! ( "{arg}={value}" ) 67} 68Arg :: Flag ( arg) => arg, 69}; 70match arg. as_str () { 71"--minify" => bundle. minify =true , 72"--sourcemap" => bundle. sourcemap =true , 73"--no-splitting" => bundle. no_splitting =true , 74 _ =>match flag_value ( & arg) ?{ 75( "-o" |"--out" , value) => output =Some ( cwd. join ( value)), 76( "--outdir" , value) => outdir =Some ( cwd. join ( value)), 77( "--target" , value) => bundle. target =target ( value) ?, 78( "--define" , value) =>{ 79let ( key, value) = value 80. split_once ( '=' ) 81. filter ( |( key, value) |{ 82 !value. is_empty () 83 && key 84. split ( '.' ) 85. all ( oxc:: syntax:: identifier:: is_identifier_name) 86}) 87. ok_or ( BundleParseError :: BadDefine ) ?; 88 bundle. defines . push (( key. to_owned (), value. to_owned ())); 89} 90 _ =>{ 91return Err ( reject ( & arg, & [ "--minify" , "--sourcemap" , "--no-splitting" ]). into ()); 92} 93}, 94} 95} 96let output =match ( output, outdir) { 97( Some ( _), Some ( _)) =>return Err ( BundleParseError :: OutAndOutdir ), 98( Some ( path), None ) =>{ 99if raw. len () >1 { 100return Err ( BundleParseError :: OutRequiresSingle ); 101} 102Output :: File ( path) 103} 104( None , Some ( path)) =>Output :: Directory ( path), 105( None , None ) =>return Err ( parser. usage (). into ()), 106}; 107if bundle. no_splitting && raw. len () >1 { 108return Err ( BundleParseError :: NoSplittingSingle ); 109} 110let first = raw. first (). ok_or_else ( || parser. usage ()) ?; 111let resolved = parser. resolve ( first) ?; 112let mut entries =vec! [ resolved. module . clone ()]; 113for extrain raw. iter (). skip ( 1 ) { 114 entries. push ( parser. resolve ( extra) ?. module ); 115} 116let entry = parser 117. common 118. finish ( resolved. path , resolved. module , resolved. lock ) ?; 119Ok ( BundleArgs { entry, entries, output, bundle}) 120} 121 122pub enum Output { 123File ( PathBuf ), 124Directory ( PathBuf ), 125} 126 127pub ( crate ) fn run ( 128graph : ModuleGraph , 129entries : & [ String ], 130output : & Output , 131options : & bundle:: Options , 132) ->Result <(), Error > { 133let ( parent, file_name) =match output{ 134Output :: File ( out) =>{ 135let name = out 136. file_name () 137. and_then ( |name| name. to_str ()) 138. filter ( |name| name. ends_with ( ".js" ) || name. ends_with ( ".mjs" )) 139. ok_or_else ( ||Error :: Module ( "Output must be a .js or .mjs file" . into ())) ?; 140url_safe ( name) ?; 141( out. parent (). unwrap (), Some ( name)) 142} 143Output :: Directory ( out) =>( out. as_path (), None ), 144}; 145let mut entry_files =HashSet :: new (); 146let mut input =Vec :: new (); 147for entryin entries{ 148let name =if let Some ( file_name) = file_name{ 149 entry_files. insert ( file_name. to_owned ()); 150None 151} else if avril_runtime:: modules:: data_url ( entry). is_some () { 152return Err ( Error :: Module ( 153"Data URL entries require an explicit -o output filename" . into (), 154)); 155} else { 156let url =http_url ( entry); 157let path = url. as_ref (). map_or ( entry. as_str (), |url| url. path ()); 158let name =Path :: new ( path) 159. file_stem () 160. and_then ( |name| name. to_str ()) 161. ok_or_else ( ||Error :: Module ( format! ( "Cannot name entry output: {entry}" ))) ?; 162let file_name =format! ( "{name}.js" ); 163url_safe ( & file_name) ?; 164if !entry_files. insert ( file_name. clone ()) { 165return Err ( Error :: Module ( format! ( 166"Duplicate entry output filename: {file_name}" 167))); 168} 169Some ( name. to_owned ()) 170}; 171 input. push ( bundle:: Entry { name, import : entry. to_owned () }); 172} 173// An ambiguous destination, like `..` below a missing directory, fails here. 174let directory =PathTarget :: for_creation ( parent) 175. map_err ( |_|Error :: Module ( format! ( "Invalid output path: {}" , parent. display ()))) ? 176. into_path (); 177let generated = tokio:: runtime:: Builder :: new_current_thread () 178. enable_all () 179. build () 180. map_err ( |error|Error :: Module ( error. to_string ())) ? 181. block_on ( bundle:: generate ( 182& graph, 183& input, 184 file_name. unwrap_or ( "[name].js" ), 185& directory, 186 options, 187)) ?; 188for warningin & generated. warnings { 189eprintln! ( "{warning}" ); 190} 191 graph 192. store () 193. save_graph ( & generated. lock_keys , & generated. modules ) 194. map_err ( |error|Error :: Module ( error. to_string ())) ?; 195let mut inputs: HashSet < PathBuf > = generated 196. modules 197. iter () 198. map ( Path :: new) 199. filter ( |path| path. is_absolute ()) 200. map ( physical) 201. collect (); 202 inputs. extend ( graph. config_inputs (). iter (). map ( |path|physical ( path))); 203 inputs. insert ( physical ( & graph. store (). options (). lock )); 204publish ( & inputs, parent, & directory, & entry_files, generated. files ) 205} 206 207/// Output files become relative URLs in `import()` calls and map references. 208fn url_safe ( file_name : & str ) ->Result <(), Error > { 209if file_name. is_empty () 210 || file_name. starts_with ( '.' ) 211 || file_name. contains ([ '/' , '\\' , '?' , '#' , '%' , '[' , ']' ]) 212 || file_name 213. chars () 214. any ( |c| c. is_control () || c. is_whitespace ()) 215{ 216return Err ( Error :: Module ( format! ( 217"Unsupported output filename: {file_name:?}" 218))); 219} 220Ok (()) 221} 222 223fn physical ( path : & Path ) ->PathBuf { 224 path. canonicalize () 225. or_else ( |_|PathTarget :: for_entry ( path). map ( PathTarget :: into_path)) 226. unwrap_or_else ( |_| path. to_owned ()) 227} 228 229/// Checks every destination before writing any, stages every file, then 230/// renames chunks and maps into place before the entry that loads them. 231fn publish ( 232inputs : & HashSet < PathBuf >, 233parent : & Path , 234directory : & Path , 235entries : & HashSet < String >, 236mut files : Vec <( String , Vec < u8 >)>, 237) ->Result <(), Error > { 238let failed = |path : & Path , error : std:: io:: Error |{ 239Error :: Module ( format! ( "Cannot write {}: {error}" , path. display ())) 240}; 241for ( name, _) in & files{ 242url_safe ( name) ?; 243let path = directory. join ( name); 244if std:: fs:: symlink_metadata ( & path). is_ok_and ( |metadata| !metadata. is_file ()) { 245return Err ( Error :: Module ( format! ( 246"Output {} is a symlink or not a regular file" , 247 path. display () 248))); 249} 250if inputs. contains ( & physical ( & path)) { 251return Err ( Error :: Module ( format! ( 252"Output {} would overwrite a bundle input" , 253 path. display () 254))); 255} 256} 257// Creating through the given spelling refuses dangling symlinked parents. 258 std:: fs:: create_dir_all ( parent). map_err ( |error|failed ( parent, error)) ?; 259if PathTarget :: resolve ( parent) 260. ok () 261. as_ref () 262. map ( PathTarget :: as_path) 263 !=Some ( directory) 264{ 265return Err ( Error :: Module ( format! ( 266"Output directory {} changed while bundling" , 267 parent. display () 268))); 269} 270 files. sort_by_key ( |( name, _) | entries. contains ( name)); 271let mut staged =Vec :: new (); 272for ( name, bytes) in files{ 273let path = directory. join ( name); 274let mut builder = tempfile:: Builder :: new (); 275 builder. prefix ( ".avril-bundle-" ); 276# [ cfg ( unix )] 277 builder. permissions ( std:: os:: unix:: fs:: PermissionsExt :: from_mode ( 0o666 )); 278let mut file = builder 279. tempfile_in ( directory) 280. map_err ( |error|failed ( & path, error)) ?; 281 std:: io:: Write :: write_all ( & mut file, & bytes). map_err ( |error|failed ( & path, error)) ?; 282if let Ok ( previous) = std:: fs:: symlink_metadata ( & path) 283 && previous. is_file () 284{ 285 file. as_file () 286. set_permissions ( previous. permissions ()) 287. map_err ( |error|failed ( & path, error)) ?; 288} 289 staged. push (( file, path)); 290} 291for ( file, path) in staged{ 292 file. persist ( & path) 293. map_err ( |error|failed ( & path, error. error )) ?; 294} 295Ok (()) 296}