char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somexperiment: automatic types for npm packages51266f9

main
10.8 KiB316 linesraw
1mod add;
2mod bundle_cli;
3mod check;
4mod cli;
5mod commands;
6mod env_file;
7mod fragments;
8mod lsp;
9mod progress;
10mod repl;
11mod snapshots;
12mod task;
13mod test;
14
15use std::io::IsTerminal;
16use std::path::PathBuf;
17use std::process::ExitCode;
18use std::rc::Rc;
19
20use avril_core::ModuleLoader;
21use avril_runtime::modules::ModuleGraph;
22use avril_runtime::stdlib;
23use std::sync::Arc;
24
25type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
26
27fn main() -> ExitCode {
28    // A watched task re-executes avril as its process-group anchor.
29    if std::env::args_os()
30        .nth(1)
31        .is_some_and(|arg| arg == avril_subprocess::ANCHOR_ARG)
32    {
33        avril_subprocess::anchor();
34    }
35    let result = (|| -> Result<ExitCode> {
36        // Snapshot inherited signal dispositions before the supervisor,
37        // listener feeds, or signal owners install anything.
38        #[cfg(any(target_os = "linux", target_os = "macos"))]
39        avril_subprocess::signals::snapshot_originals();
40        let mut args = std::env::args_os()
41            .skip(1)
42            .map(|arg| {
43                arg.into_string()
44                    .map_err(|_| "Arguments must be valid UTF-8")
45            })
46            .collect::<std::result::Result<Vec<_>, _>>()?;
47        // A named task owns everything after its name, including --help.
48        // The name may be preceded by `task`'s own --watch flag.
49        if args.first().is_some_and(|arg| arg == "task") {
50            let rest = &args[1..];
51            let name = if rest.first().is_some_and(|arg| arg == "--watch") {
52                rest.get(1)
53            } else {
54                rest.first()
55            };
56            if name.is_some_and(|arg| !arg.starts_with('-')) {
57                return task::run(task::parse(rest)?);
58            }
59        }
60        if let Some(help) = cli::help(&args)? {
61            println!("{help}");
62            return Ok(ExitCode::SUCCESS);
63        }
64        if args.first().is_some_and(|arg| arg == "lsp") {
65            lsp::run(lsp::parse(&args[1..])?)?;
66            return Ok(ExitCode::SUCCESS);
67        }
68        if args.first().is_some_and(|arg| arg == "check") {
69            return check::run(check::parse(&args[1..])?);
70        }
71        if args.first().is_some_and(|arg| arg == "add") {
72            return add::run(add::parse(&args[1..])?).map(|()| ExitCode::SUCCESS);
73        }
74        if args.first().is_some_and(|arg| arg == "task") {
75            return task::run(task::parse(&args[1..])?);
76        }
77        let script = args.split_off(cli::script_args_start(&args));
78        let cwd = fragments::working_dir()?;
79        let tail = |args: &[String]| args[1..].to_vec();
80        match args.first().map(String::as_str) {
81            Some("repl") => {
82                let parsed = commands::EntryArgs::parse_repl(tail(&args), script, cwd)?;
83                run_command(parsed, Mode::Repl)
84            }
85            Some("run") => {
86                let parsed = commands::EntryArgs::parse_run(tail(&args), script, cwd)?;
87                run_command(parsed, Mode::Run)
88            }
89            Some("serve") => {
90                let parsed = commands::ServeArgs::parse(tail(&args), script, cwd)?;
91                run_command(parsed.common, Mode::Serve(parsed.listener))
92            }
93            Some("test") => {
94                let mut parsed = test::parse(tail(&args), script, cwd)?;
95                test::run(&mut parsed).map(|()| ExitCode::SUCCESS)
96            }
97            Some("cache") => {
98                inspect_command(commands::parse_cache(tail(&args), cwd)?, After::Cache)
99            }
100            Some("info") => inspect_command(commands::parse_info(tail(&args), cwd)?, After::Info),
101            Some("update") => {
102                let parsed = commands::UpdateArgs::parse(tail(&args), cwd)?;
103                let after = if parsed.prune {
104                    After::Update
105                } else {
106                    After::Cache
107                };
108                inspect_command(parsed.entry, after)
109            }
110            Some("vendor") => {
111                let parsed = commands::VendorArgs::parse(tail(&args), cwd)?;
112                inspect_command(parsed.entry, After::Vendor(parsed.output))
113            }
114            Some("bundle") => bundle_command(bundle_cli::parse(tail(&args), cwd)?),
115            None => {
116                let parsed = commands::EntryArgs::parse_repl(args, script, cwd)?;
117                run_command(parsed, Mode::Repl)
118            }
119            Some(arg) if arg.starts_with('-') => {
120                let parsed = commands::EntryArgs::parse_repl(args, script, cwd)?;
121                run_command(parsed, Mode::Repl)
122            }
123            Some(arg) => Err(format!(
124                "Unknown command: {arg}\nRun a module with `avril run <module>`"
125            )
126            .into()),
127        }
128    })();
129
130    match result {
131        Ok(code) => code,
132        Err(error) => {
133            eprintln!("{error}");
134            ExitCode::FAILURE
135        }
136    }
137}
138
139enum Mode {
140    Run,
141    Serve(avril_http::Address),
142    Repl,
143}
144
145enum After {
146    Cache,
147    Update,
148    Info,
149    Vendor(Option<PathBuf>),
150}
151
152fn open_graph(
153    authority: avril_runtime::authority::SourceAuthority,
154    packages: avril_runtime::registry::Options,
155    entry: &fragments::Entry,
156    prepare: bool,
157    progress: Option<avril_runtime::registry::ProgressSink>,
158) -> Result<(avril_host::Host, Rc<ModuleGraph>)> {
159    // Declared before the JS runtime so it outlives the extensions' pending host work.
160    let host = avril_host::Host::new()?;
161    let graph = fragments::prepare_graph(
162        host.handle(),
163        authority,
164        packages,
165        &entry.module,
166        &entry.includes,
167        prepare,
168        progress,
169    )?;
170    Ok((host, graph))
171}
172
173fn bundle_command(args: bundle_cli::BundleArgs) -> Result<ExitCode> {
174    let (authority, _root, packages) = args.entry.project(false)?;
175    // Declared before the JS runtime so it outlives the extensions' pending host work.
176    let host = avril_host::Host::new()?;
177    let graph = ModuleGraph::open_browser(host.handle(), authority, packages)?;
178    bundle_cli::run(graph, &args.entries, &args.output, &args.bundle)?;
179    Ok(ExitCode::SUCCESS)
180}
181
182fn inspect_command(entry: fragments::Entry, after: After) -> Result<ExitCode> {
183    let name = entry.module.clone();
184    let (authority, root, packages) = entry.project(false)?;
185    let reporter = if matches!(after, After::Cache) {
186        progress::Reporter::terminal()
187    } else {
188        None
189    };
190    let sink = reporter
191        .as_ref()
192        .map(|reporter| reporter.sink(packages.jsr_registry.clone()));
193    let (_host, graph) = open_graph(authority, packages, &entry, true, sink)?;
194    graph.set_automatic_types(entry.automatic_types);
195    graph.set_typescript_version(avril_lsp::BACKEND_VERSION)?;
196    if matches!(after, After::Cache) {
197        // Existing corruption takes precedence over expanding the graph with type packages.
198        graph.audit_npm()?;
199    }
200    if matches!(after, After::Cache | After::Update | After::Vendor(_)) {
201        futures::executor::block_on(graph.prepare_types(std::slice::from_ref(&name)))?;
202        for (include, from) in &entry.includes {
203            let referrer = match from {
204                Some(from) => {
205                    futures::executor::block_on(graph.prepare(from.clone(), name.clone()))?
206                }
207                None => name.clone(),
208            };
209            futures::executor::block_on(graph.prepare_types_for_request(include, &referrer))?;
210        }
211    }
212    match after {
213        After::Cache => graph.audit_npm()?,
214        After::Update => {
215            graph.store().prune()?;
216        }
217        After::Info => {
218            println!("{}", serde_json::to_string_pretty(&graph.info(&name)?)?)
219        }
220        After::Vendor(output) => graph
221            .store()
222            .vendor(&output.unwrap_or_else(|| root.join("vendor")))?,
223    }
224    Ok(ExitCode::SUCCESS)
225}
226
227fn run_command(parsed: commands::EntryArgs, mode: Mode) -> Result<ExitCode> {
228    let commands::EntryArgs { entry, script } = parsed;
229    let repl = matches!(mode, Mode::Repl);
230    let name = entry.module.clone();
231    let (authority, root, packages) = entry.project(repl)?;
232    let anchor = entry
233        .path
234        .to_str()
235        .ok_or("Module paths must be valid UTF-8")?;
236    // A REPL belongs to its project's config wherever it was started
237    // from, so its entry component is fixed rather than the cwd-bound
238    // synthetic module path; without a config it keys by that root.
239    let (storage_config, storage_entry) = if repl {
240        match ModuleGraph::project_config_for_repl(&authority, anchor)? {
241            Some(config) => (config, "repl"),
242            None => (root.clone(), name.as_str()),
243        }
244    } else {
245        (root.join("avril.conf"), name.as_str())
246    };
247    let storage =
248        avril_webstorage::Persistent::new(&packages.cache, &storage_config, storage_entry);
249    let reporter = if matches!(mode, Mode::Run) || (repl && std::io::stdin().is_terminal()) {
250        progress::Reporter::terminal()
251    } else {
252        None
253    };
254    if repl && let Some(reporter) = &reporter {
255        reporter.scope(false);
256    }
257    let sink = reporter
258        .as_ref()
259        .map(|reporter| reporter.sink(packages.jsr_registry.clone()));
260    let (host, graph) = open_graph(authority, packages, &entry, !repl, sink)?;
261    let listener = match &mode {
262        Mode::Serve(address) => Some(address.clone()),
263        _ => None,
264    };
265    let caps = entry.caps;
266    let environment = entry.environment;
267    let stdio = if repl {
268        avril_ext_stdio::Stdio::new_for_repl()?
269    } else {
270        avril_ext_stdio::Stdio::new()?
271    };
272    graph.set_entry(&name);
273    let mut runtime = avril_runtime::assembly::runtime(
274        graph.clone(),
275        avril_runtime::assembly::Settings {
276            snapshot: Some(snapshots::embedded()),
277            storage,
278            caps,
279            environment,
280            args: script,
281            store: graph.store().clone(),
282            runtime: host.handle(),
283            broker: Arc::default(),
284            process_signal: match mode {
285                Mode::Repl => avril_ext_process::SignalMode::Repl,
286                Mode::Serve(_) => avril_ext_process::SignalMode::Serve,
287                Mode::Run => avril_ext_process::SignalMode::Run,
288            },
289        },
290        stdio.clone(),
291        listener,
292        None,
293    )?;
294    if repl {
295        let result = repl::run(
296            &mut runtime,
297            &name,
298            &stdio,
299            &host.handle(),
300            reporter.as_ref(),
301        );
302        let flushed = stdio.flush();
303        result?;
304        flushed?;
305        return Ok(ExitCode::SUCCESS);
306    }
307    let result = if matches!(mode, Mode::Serve(_)) {
308        runtime.execute_module_with_bootstrap(&name, "ext:http/main.js", "startCLI")
309    } else {
310        runtime.execute_module(&name)
311    };
312    let flushed = stdio.flush();
313    result?;
314    flushed?;
315    Ok(ExitCode::SUCCESS)
316}