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:: path::{ Path , PathBuf }; 2 3use avril_runtime:: caps:: CapabilitiesError ; 4 5/// `(syntax, help)`: the flag names are read off `syntax`. 6type Opt =( &' static str , &' static str ); 7 8type Group =( &' static str , &' static [ Opt ]); 9 10/// The flag names an option's syntax spells, expanding `--allow-{a,b}=VALUE`. 11fn names ( syntax : & str ) ->impl Iterator < Item =String > +' _ { 12 syntax. split ( ", " ). flat_map ( |spelling|{ 13let flag = spelling. split ([ ' ' , '=' ]). next (). unwrap (); 14match flag. split_once ( '{' ) { 15Some (( prefix, rest)) => rest 16. trim_end_matches ( '}' ) 17. split ( ',' ) 18. map ( |name|format! ( "{prefix}{name}" )) 19. collect (), 20None =>vec! [ flag. to_owned ()], 21} 22}) 23} 24 25const OFFLINE : Opt =( "--offline" , "Never contact the registry" ); 26const NO_TYPES_PACKAGES : Opt =( 27"--no-types-packages" , 28"Disable automatic DefinitelyTyped package lookup" , 29); 30const JSR_REGISTRY : Opt =( 31"--jsr-registry=URL" , 32"JSR registry base (default https://jsr.io/)" , 33); 34const NPM_REGISTRY : Opt =( 35"--npm-registry=URL" , 36"npm registry base (default https://registry.npmjs.org/)" , 37); 38const CACHE_DIR : Opt =( 39"--cache-dir=PATH" , 40"Package cache (default $AVRIL_CACHE_DIR or user cache)" , 41); 42const LOCK : Opt =( 43"--lock=PATH" , 44"Root graph lockfile (default package-root/avril.lock)" , 45); 46const VENDOR : Opt =( 47"--vendor=PATH" , 48"Read acquired artifacts from a vendor directory" , 49); 50 51const ALLOW : Opt =( 52"--allow-{net,read,write,js-exec,env,sys,run,ffi}=VALUE" , 53"Grant a capability" , 54); 55const DENY : Opt =( 56"--deny-{net,read,write,js-exec,env,sys,run,ffi}=VALUE" , 57"Deny a capability" , 58); 59 60const PERMISSIONS : Group =( 61"Permissions" , 62& [ 63( 64"-0" , 65"Start with no capabilities except running the entry module" , 66), 67ALLOW , 68DENY , 69], 70); 71const TEST_PERMISSIONS : Group =( 72"Permissions" , 73& [ 74( 75"-0" , 76"Start with no capabilities except running the test files" , 77), 78ALLOW , 79DENY , 80], 81); 82const REPL_PERMISSIONS : Group =( 83"Permissions" , 84& [( "-0" , "Start with no capabilities" ), ALLOW , DENY ], 85); 86const PACKAGES : Group =( 87"Packages" , 88& [ 89OFFLINE , 90NO_TYPES_PACKAGES , 91( "--frozen" , "Require existing lock selections and integrity" ), 92JSR_REGISTRY , 93NPM_REGISTRY , 94CACHE_DIR , 95LOCK , 96VENDOR , 97], 98); 99const INCLUDES : Group =( 100"Includes" , 101& [ 102( 103"--include=SPECIFIER" , 104"Also prepare a dynamic dependency; repeatable" , 105), 106( 107"--include-from=REFERRER" , 108"Resolve following includes from this source ('.' resets)" , 109), 110], 111); 112const ENVIRONMENT : Group =( 113"Environment" , 114& [( 115"--env-file=PATH" , 116"Load environment defaults from a file; repeatable" , 117)], 118); 119const TYPESCRIPT : Opt =( 120"--typescript=PATH" , 121"Stock native TypeScript executable (pinned version required)" , 122); 123const LISTENER : Group =( 124"Listener" , 125& [ 126( "--hostname=HOST" , "TCP bind hostname (default 127.0.0.1)" ), 127( 128"--port=PORT" , 129"TCP bind port (default 8000; 0 selects an available port)" , 130), 131( "--unix=PATH" , "Unix socket path, instead of TCP" ), 132], 133); 134const TASK : Group =( 135"Task" , 136& [( 137"--watch" , 138"Rerun the task when its declared input files change" , 139)], 140); 141const TEST : Group =( 142"Test" , 143& [ 144( 145"--filter=PATTERN" , 146"Only run tests whose name contains PATTERN" , 147), 148( 149"--no-run" , 150"Collect tests without running them (file top levels still evaluate)" , 151), 152], 153); 154const ADD : Group =( 155"Add" , 156& [ 157( 158"--config=PATH" , 159"Edit this config instead of the nearest one" , 160), 161( "--pre" , "Let an unversioned target select a prerelease" ), 162], 163); 164const BUNDLE : Group =( 165"Bundle" , 166& [ 167( 168"-o, --out PATH" , 169"Entry output file; chunks and maps go beside it" , 170), 171( 172"--outdir PATH" , 173"Output directory; entries use their basenames with .js extensions" , 174), 175( "--minify" , "Minify the output" ), 176( "--sourcemap" , "Write a source map beside each output file" ), 177( 178"--no-splitting" , 179"Inline dynamic imports instead of emitting lazy chunks" , 180), 181( 182"--target TARGET" , 183"Output syntax level, es2015 to es2025 or esnext (default)" , 184), 185( 186"--define KEY=VALUE" , 187"Replace a global or dotted path with a JS expression; repeatable" , 188), 189], 190); 191const LANGUAGE_SERVER : Group =( 192"Language server" , 193& [ 194TYPESCRIPT , 195( 196"--project=DIR" , 197"Workspace directory (default current directory); open documents are graph roots" , 198), 199], 200); 201 202pub struct Command { 203name : &' static str , 204summary : &' static str , 205usage : &' static [ &' static str ], 206groups : &' static [ Group ], 207} 208 209const COMMAND_GROUPS : & [( & str , & [ Command ])] =& [ 210( 211"Main commands" , 212& [ 213Command { 214name : "repl" , 215summary : "Start an interactive session (the default without a command)" , 216usage : & [ "repl [OPTIONS] [-- args...]" , "[OPTIONS] [-- args...]" ], 217groups : & [ REPL_PERMISSIONS , PACKAGES ], 218}, 219Command { 220name : "run" , 221summary : "Run a module" , 222usage : & [ "run [OPTIONS] <module> [args...]" ], 223groups : & [ PERMISSIONS , PACKAGES , INCLUDES , ENVIRONMENT ], 224}, 225Command { 226name : "serve" , 227summary : "Serve a module's default fetch handler" , 228usage : & [ "serve [OPTIONS] <module> [args...]" ], 229groups : & [ PERMISSIONS , PACKAGES , INCLUDES , LISTENER , ENVIRONMENT ], 230}, 231Command { 232name : "task" , 233summary : "Run a task from avril.conf (without a name, list tasks)" , 234usage : & [ "task [--watch] [<name> [args...]]" ], 235groups : & [ TASK ], 236}, 237Command { 238name : "test" , 239summary : "Run tests" , 240usage : & [ "test [OPTIONS] [files...] [-- args...]" ], 241groups : & [ TEST_PERMISSIONS , PACKAGES , INCLUDES , ENVIRONMENT , TEST ], 242}, 243], 244), 245( 246"Additional commands" , 247& [ 248Command { 249name : "add" , 250summary : "Add a jsr: or npm: package to avril.conf" , 251usage : & [ "add [OPTIONS] [<alias>] <jsr:|npm:specifier>" ], 252groups : & [ 253ADD , 254( 255"Packages" , 256& [ 257OFFLINE , 258NO_TYPES_PACKAGES , 259JSR_REGISTRY , 260NPM_REGISTRY , 261CACHE_DIR , 262], 263), 264], 265}, 266Command { 267name : "bundle" , 268summary : "Bundle a module for the browser" , 269usage : & [ 270"bundle [OPTIONS] <module> -o <output>" , 271"bundle [OPTIONS] <module>... --outdir <directory>" , 272], 273groups : & [ PERMISSIONS , PACKAGES , BUNDLE ], 274}, 275Command { 276name : "cache" , 277summary : "Download and lock a module's dependencies" , 278usage : & [ "cache [OPTIONS] <module>" ], 279groups : & [ PERMISSIONS , PACKAGES , INCLUDES ], 280}, 281Command { 282name : "check" , 283summary : "Type-check a module" , 284usage : & [ "check --typescript=PATH [OPTIONS] <module>" ], 285groups : & [ PERMISSIONS , PACKAGES , ( "Type checking" , & [ TYPESCRIPT ])], 286}, 287Command { 288name : "info" , 289summary : "Print a module's resolved graph as JSON" , 290usage : & [ "info [OPTIONS] <module>" ], 291groups : & [ PERMISSIONS , PACKAGES , INCLUDES ], 292}, 293Command { 294name : "lsp" , 295summary : "Start the native TypeScript-backed language server" , 296usage : & [ "lsp --typescript=PATH [--project=DIR] [OPTIONS]" ], 297groups : & [ REPL_PERMISSIONS , PACKAGES , LANGUAGE_SERVER ], 298}, 299Command { 300name : "update" , 301summary : "Select newer dependency versions and update the lockfile" , 302usage : & [ "update [OPTIONS] <module>" ], 303groups : & [ 304PERMISSIONS , 305( 306"Packages" , 307& [ 308OFFLINE , 309NO_TYPES_PACKAGES , 310JSR_REGISTRY , 311NPM_REGISTRY , 312CACHE_DIR , 313LOCK , 314VENDOR , 315], 316), 317INCLUDES , 318( 319"Update" , 320& [( "--prune" , "Drop pins outside the prepared graph" )], 321), 322], 323}, 324Command { 325name : "vendor" , 326summary : "Copy a module's dependencies into a directory" , 327usage : & [ "vendor [OPTIONS] <module>" ], 328groups : & [ 329PERMISSIONS , 330PACKAGES , 331INCLUDES , 332( 333"Output" , 334& [( 335"--out=PATH" , 336"Vendor directory (default package-root/vendor)" , 337)], 338), 339], 340}, 341], 342), 343]; 344 345fn commands () ->impl Iterator < Item =&' static Command > { 346COMMAND_GROUPS . iter (). flat_map ( |( _, commands) |* commands) 347} 348 349/// Argument parsing failed: the shared vocabulary for generic shape errors. 350/// Each command's own enum carries its specific failures and converts from 351/// this via `#[from]`. 352# [ derive ( Debug , thiserror :: Error )] 353pub enum ParseError { 354# [ error ( "{0}" )] 355Usage ( String ), 356# [ error ( "{0}" )] 357Check ( String ), 358# [ error ( "--{flag} does not take a value" )] 359UnexpectedValue { flag : String }, 360# [ error ( "Expected a value for --{flag}{equals}" )] 361MissingValue { flag : String , equals : &' static str }, 362# [ error ( "Expected an option with '=VALUE': {arg}" )] 363ExpectedEquals { arg : String }, 364# [ error ( "Expected a value for {flag}" )] 365EmptyValue { flag : String }, 366# [ error ( "Expected a path for --env-file (use --env-file=PATH for names starting with '-')" )] 367EnvFilePath , 368# [ error ( "Expected a pattern for --filter (use --filter=PATTERN for names starting with '-')" )] 369FilterPattern , 370# [ error ( "Expected a value for {flag} (use {flag}=VALUE for values starting with '-')" )] 371SpacedValue { flag : String }, 372# [ error ( "Module paths must be valid UTF-8" )] 373ModulePath , 374# [ error ( "--{flag} requires a path" )] 375MissingPath { flag : String }, 376# [ error ( "Duplicate option: --{flag}" )] 377Duplicate { flag : String }, 378# [ error ( transparent )] 379Caps ( # [ from ] CapabilitiesError ), 380# [ error ( transparent )] 381Url ( # [ from ] url:: ParseError ), 382# [ error ( transparent )] 383Io ( # [ from ] std:: io:: Error ), 384# [ error ( transparent )] 385Env ( # [ from ] crate :: env_file:: Error ), 386} 387 388/// Looks up a command by name for a hardcoded caller (task, add, lsp, and 389/// dispatch); a typo surfaces as a usage error instead of a panic. 390pub fn command ( name : & str ) ->Option < &' static Command > { 391commands (). find ( |command| command. name == name) 392} 393 394// Help and capability pre-scans must respect the same guest-argument boundary. 395pub ( crate ) fn script_args_start ( args : & [ String ]) ->usize { 396let command = args. first (). map ( String :: as_str); 397if matches! ( command, Some ( "run" |"serve" )) { 398let mut args_iter = args. iter (). enumerate (). skip ( 1 ); 399while let Some (( index, arg)) = args_iter. next () { 400match arg. as_str () { 401"--" =>return ( index +2 ). min ( args. len ()), 402"--env-file" =>{ 403 args_iter. next (); 404} 405 _if !arg. starts_with ( '-' ) =>return index +1 , 406 _ =>{} 407} 408} 409} else if let Some ( command) = command 410 &&( matches! ( command, "repl" |"test" ) || command. starts_with ( '-' )) 411 &&let Some ( index) = args. iter (). position ( |arg| arg =="--" ) 412{ 413return index +1 ; 414} 415 args. len () 416} 417 418/// Help for `avril --help`, `avril help [<command>]`, or `avril <command> --help`, if requested. 419pub fn help ( args : & [ String ]) ->Result < Option < String >, String > { 420let args =& args[ ..script_args_start ( args)]; 421let requested = |args : & [ String ] |{ 422 args. iter () 423. take_while ( |arg|* arg !="--" ) 424. any ( |arg| arg =="--help" || arg =="-h" ) 425}; 426match args. first (). map ( String :: as_str) { 427Some ( "help" ) =>match & args[ 1 ..] { 428[] =>Ok ( Some ( usage ())), 429[ name] =>command ( name) 430. map ( |command|Some ( command. help ())) 431. ok_or_else ( ||format! ( "Unknown command: {name}\n\n{}" , usage ())), 432 _ =>Err ( "Usage: avril help [<command>]" . into ()), 433}, 434Some ( name) if requested ( & args[ 1 ..]) &&command ( name). is_some () =>{ 435Ok ( command ( name). map ( Command :: help)) 436} 437 _if requested ( args) =>Ok ( Some ( usage ())), 438 _ =>Ok ( None ), 439} 440} 441 442/// General usage: the command overview. This is the fallback wherever a 443/// hardcoded command lookup fails, so a typo shows usage instead of panicking. 444pub fn usage () ->String { 445let width =commands (). map ( |command| command. name . len ()). max (). unwrap () +3 ; 446let mut text =String :: from ( 447"Usage: avril <command> [OPTIONS] [ARGS]\n avril [OPTIONS] [-- args...] (starts the REPL)\n" , 448); 449for ( title, commands) in COMMAND_GROUPS { 450 text. push_str ( & format! ( "\n{title}:\n" )); 451for commandin * commands{ 452 text. push_str ( & format! ( " {:width$}{}\n" , command. name , command. summary )); 453} 454} 455 text. push_str ( "\nRun `avril help <command>` or `avril <command> --help` for its options." ); 456 text 457} 458 459impl Command { 460pub fn usage ( & self ) ->String { 461let mut text =String :: new (); 462for ( index, line) in self . usage . iter (). enumerate () { 463let prefix =if index ==0 { "Usage:" } else { " " }; 464 text. push_str ( & format! ( "{prefix} avril {line}\n" )); 465} 466 text. pop (); 467 text 468} 469 470fn help ( & self ) ->String { 471let width =self 472. groups 473. iter () 474. flat_map ( |( _, options) | options. iter ()) 475. map ( |option| option. 0 . len ()) 476. max () 477. unwrap_or_default () 478 +3 ; 479let mut text =format! ( "{}\n\n{}\n" , self . summary , self . usage ()); 480for ( title, options) in self . groups { 481 text. push_str ( & format! ( "\n{title}:\n" )); 482for optionin * options{ 483 text. push_str ( & format! ( " {:width$}{}\n" , option. 0 , option. 1 )); 484} 485} 486 text. pop (); 487if matches! ( self . name , "run" |"serve" ) { 488 text. push_str ( "\n\nArguments after <module> are passed verbatim as process.args. Put runtime options before <module>." ); 489} else if matches! ( self . name , "repl" |"test" ) { 490 text. push_str ( "\n\nArguments after -- are passed verbatim as process.args; the separating -- is omitted." ); 491} 492 text 493} 494 495/// Rejects an option this command does not take, `--name=VALUE` or a bare flag. 496pub fn check ( & self , arg : & str ) ->Result <(), String > { 497let flag = arg. split_once ( '=' ). map_or ( arg, |( flag, _) | flag); 498let takes = |command : & Command |{ 499 command 500. groups 501. iter () 502. flat_map ( |( _, options) | options. iter ()) 503. any ( |option|names ( option. 0 ). any ( |name| name == flag)) 504}; 505if takes ( self ) { 506Ok (()) 507} else if commands (). any ( takes) { 508Err ( format! ( "{flag} is not an option of `avril {}`" , self . name )) 509} else { 510Err ( format! ( "Unknown option: {flag}" )) 511} 512} 513} 514 515pub fn default_cache ( cwd : & Path ) ->PathBuf { 516 cwd. join ( 517 std:: env:: var_os ( "AVRIL_CACHE_DIR" ) 518. map ( PathBuf :: from) 519. or_else ( ||{ 520 std:: env:: var_os ( "XDG_CACHE_HOME" ). map ( |root|PathBuf :: from ( root). join ( "avril" )) 521}) 522. or_else ( ||{ 523 std:: env:: var_os ( "HOME" ). map ( |root|PathBuf :: from ( root). join ( ".cache/avril" )) 524}) 525. unwrap_or_else ( || cwd. join ( ".avril-cache" )), 526) 527}