mod add; mod bundle_cli; mod check; mod cli; mod commands; mod env_file; mod fragments; mod lsp; mod progress; mod repl; mod snapshots; mod task; mod test; use std::io::IsTerminal; use std::path::PathBuf; use std::process::ExitCode; use std::rc::Rc; use avril_core::ModuleLoader; use avril_runtime::modules::ModuleGraph; use avril_runtime::stdlib; use std::sync::Arc; type Result = std::result::Result>; fn main() -> ExitCode { // A watched task re-executes avril as its process-group anchor. if std::env::args_os() .nth(1) .is_some_and(|arg| arg == avril_subprocess::ANCHOR_ARG) { avril_subprocess::anchor(); } let result = (|| -> Result { // Snapshot inherited signal dispositions before the supervisor, // listener feeds, or signal owners install anything. #[cfg(any(target_os = "linux", target_os = "macos"))] avril_subprocess::signals::snapshot_originals(); let mut args = std::env::args_os() .skip(1) .map(|arg| { arg.into_string() .map_err(|_| "Arguments must be valid UTF-8") }) .collect::, _>>()?; // A named task owns everything after its name, including --help. // The name may be preceded by `task`'s own --watch flag. if args.first().is_some_and(|arg| arg == "task") { let rest = &args[1..]; let name = if rest.first().is_some_and(|arg| arg == "--watch") { rest.get(1) } else { rest.first() }; if name.is_some_and(|arg| !arg.starts_with('-')) { return task::run(task::parse(rest)?); } } if let Some(help) = cli::help(&args)? { println!("{help}"); return Ok(ExitCode::SUCCESS); } if args.first().is_some_and(|arg| arg == "lsp") { lsp::run(lsp::parse(&args[1..])?)?; return Ok(ExitCode::SUCCESS); } if args.first().is_some_and(|arg| arg == "check") { return check::run(check::parse(&args[1..])?); } if args.first().is_some_and(|arg| arg == "add") { return add::run(add::parse(&args[1..])?).map(|()| ExitCode::SUCCESS); } if args.first().is_some_and(|arg| arg == "task") { return task::run(task::parse(&args[1..])?); } let script = args.split_off(cli::script_args_start(&args)); let cwd = fragments::working_dir()?; let tail = |args: &[String]| args[1..].to_vec(); match args.first().map(String::as_str) { Some("repl") => { let parsed = commands::EntryArgs::parse_repl(tail(&args), script, cwd)?; run_command(parsed, Mode::Repl) } Some("run") => { let parsed = commands::EntryArgs::parse_run(tail(&args), script, cwd)?; run_command(parsed, Mode::Run) } Some("serve") => { let parsed = commands::ServeArgs::parse(tail(&args), script, cwd)?; run_command(parsed.common, Mode::Serve(parsed.listener)) } Some("test") => { let mut parsed = test::parse(tail(&args), script, cwd)?; test::run(&mut parsed).map(|()| ExitCode::SUCCESS) } Some("cache") => { inspect_command(commands::parse_cache(tail(&args), cwd)?, After::Cache) } Some("info") => inspect_command(commands::parse_info(tail(&args), cwd)?, After::Info), Some("update") => { let parsed = commands::UpdateArgs::parse(tail(&args), cwd)?; let after = if parsed.prune { After::Update } else { After::Cache }; inspect_command(parsed.entry, after) } Some("vendor") => { let parsed = commands::VendorArgs::parse(tail(&args), cwd)?; inspect_command(parsed.entry, After::Vendor(parsed.output)) } Some("bundle") => bundle_command(bundle_cli::parse(tail(&args), cwd)?), None => { let parsed = commands::EntryArgs::parse_repl(args, script, cwd)?; run_command(parsed, Mode::Repl) } Some(arg) if arg.starts_with('-') => { let parsed = commands::EntryArgs::parse_repl(args, script, cwd)?; run_command(parsed, Mode::Repl) } Some(arg) => Err(format!( "Unknown command: {arg}\nRun a module with `avril run `" ) .into()), } })(); match result { Ok(code) => code, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } } } enum Mode { Run, Serve(avril_http::Address), Repl, } enum After { Cache, Update, Info, Vendor(Option), } fn open_graph( authority: avril_runtime::authority::SourceAuthority, packages: avril_runtime::registry::Options, entry: &fragments::Entry, prepare: bool, progress: Option, ) -> Result<(avril_host::Host, Rc)> { // Declared before the JS runtime so it outlives the extensions' pending host work. let host = avril_host::Host::new()?; let graph = fragments::prepare_graph( host.handle(), authority, packages, &entry.module, &entry.includes, prepare, progress, )?; Ok((host, graph)) } fn bundle_command(args: bundle_cli::BundleArgs) -> Result { let (authority, _root, packages) = args.entry.project(false)?; // Declared before the JS runtime so it outlives the extensions' pending host work. let host = avril_host::Host::new()?; let graph = ModuleGraph::open_browser(host.handle(), authority, packages)?; bundle_cli::run(graph, &args.entries, &args.output, &args.bundle)?; Ok(ExitCode::SUCCESS) } fn inspect_command(entry: fragments::Entry, after: After) -> Result { let name = entry.module.clone(); let (authority, root, packages) = entry.project(false)?; let reporter = if matches!(after, After::Cache) { progress::Reporter::terminal() } else { None }; let sink = reporter .as_ref() .map(|reporter| reporter.sink(packages.jsr_registry.clone())); let (_host, graph) = open_graph(authority, packages, &entry, true, sink)?; graph.set_automatic_types(entry.automatic_types); graph.set_typescript_version(avril_lsp::BACKEND_VERSION)?; if matches!(after, After::Cache) { // Existing corruption takes precedence over expanding the graph with type packages. graph.audit_npm()?; } if matches!(after, After::Cache | After::Update | After::Vendor(_)) { futures::executor::block_on(graph.prepare_types(std::slice::from_ref(&name)))?; for (include, from) in &entry.includes { let referrer = match from { Some(from) => { futures::executor::block_on(graph.prepare(from.clone(), name.clone()))? } None => name.clone(), }; futures::executor::block_on(graph.prepare_types_for_request(include, &referrer))?; } } match after { After::Cache => graph.audit_npm()?, After::Update => { graph.store().prune()?; } After::Info => { println!("{}", serde_json::to_string_pretty(&graph.info(&name)?)?) } After::Vendor(output) => graph .store() .vendor(&output.unwrap_or_else(|| root.join("vendor")))?, } Ok(ExitCode::SUCCESS) } fn run_command(parsed: commands::EntryArgs, mode: Mode) -> Result { let commands::EntryArgs { entry, script } = parsed; let repl = matches!(mode, Mode::Repl); let name = entry.module.clone(); let (authority, root, packages) = entry.project(repl)?; let anchor = entry .path .to_str() .ok_or("Module paths must be valid UTF-8")?; // A REPL belongs to its project's config wherever it was started // from, so its entry component is fixed rather than the cwd-bound // synthetic module path; without a config it keys by that root. let (storage_config, storage_entry) = if repl { match ModuleGraph::project_config_for_repl(&authority, anchor)? { Some(config) => (config, "repl"), None => (root.clone(), name.as_str()), } } else { (root.join("avril.conf"), name.as_str()) }; let storage = avril_webstorage::Persistent::new(&packages.cache, &storage_config, storage_entry); let reporter = if matches!(mode, Mode::Run) || (repl && std::io::stdin().is_terminal()) { progress::Reporter::terminal() } else { None }; if repl && let Some(reporter) = &reporter { reporter.scope(false); } let sink = reporter .as_ref() .map(|reporter| reporter.sink(packages.jsr_registry.clone())); let (host, graph) = open_graph(authority, packages, &entry, !repl, sink)?; let listener = match &mode { Mode::Serve(address) => Some(address.clone()), _ => None, }; let caps = entry.caps; let environment = entry.environment; let stdio = if repl { avril_ext_stdio::Stdio::new_for_repl()? } else { avril_ext_stdio::Stdio::new()? }; graph.set_entry(&name); let mut runtime = avril_runtime::assembly::runtime( graph.clone(), avril_runtime::assembly::Settings { snapshot: Some(snapshots::embedded()), storage, caps, environment, args: script, store: graph.store().clone(), runtime: host.handle(), broker: Arc::default(), process_signal: match mode { Mode::Repl => avril_ext_process::SignalMode::Repl, Mode::Serve(_) => avril_ext_process::SignalMode::Serve, Mode::Run => avril_ext_process::SignalMode::Run, }, }, stdio.clone(), listener, None, )?; if repl { let result = repl::run( &mut runtime, &name, &stdio, &host.handle(), reporter.as_ref(), ); let flushed = stdio.flush(); result?; flushed?; return Ok(ExitCode::SUCCESS); } let result = if matches!(mode, Mode::Serve(_)) { runtime.execute_module_with_bootstrap(&name, "ext:http/main.js", "startCLI") } else { runtime.execute_module(&name) }; let flushed = stdio.flush(); result?; flushed?; Ok(ExitCode::SUCCESS) }