char/avril

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

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

Charlotte Somsupport data urls1c8ffed

main
12.8 KiB373 linesraw
1use std::path::{Path, PathBuf};
2use std::rc::Rc;
3use std::sync::Arc;
4
5use avril_core::ModuleLoader;
6use avril_host::environment::Environment;
7use avril_runtime::authority::SourceAuthority;
8use avril_runtime::caps::{Capabilities, Category, Initial};
9use avril_runtime::modules::ModuleGraph;
10use avril_runtime::registry::Options;
11
12use crate::cli::{Command, ParseError, command, default_cache, usage};
13
14pub fn working_dir() -> Result<PathBuf, ParseError> {
15    Ok(std::env::current_dir()?.canonicalize()?)
16}
17
18fn value(
19    args: &mut impl Iterator<Item = String>,
20    missing: ParseError,
21) -> Result<String, ParseError> {
22    match args.next() {
23        Some(value) if !value.is_empty() && !value.starts_with('-') => Ok(value),
24        _ => Err(missing),
25    }
26}
27
28pub fn flag_value(arg: &str) -> Result<(&str, &str), ParseError> {
29    let (flag, value) = arg
30        .split_once('=')
31        .ok_or_else(|| ParseError::ExpectedEquals { arg: arg.to_owned() })?;
32    if value.is_empty() {
33        return Err(ParseError::EmptyValue { flag: flag.to_owned() });
34    }
35    Ok((flag, value))
36}
37
38pub fn reject(arg: &str, bare: &[&str]) -> ParseError {
39    let flag = arg.split_once('=').map_or(arg, |(flag, _)| flag);
40    if bare.contains(&flag) {
41        ParseError::UnexpectedValue {
42            flag: flag.trim_start_matches('-').to_owned(),
43        }
44    } else {
45        ParseError::ExpectedEquals { arg: arg.to_owned() }
46    }
47}
48
49pub struct Common {
50    pub(crate) initial: Initial,
51    pub(crate) offline: bool,
52    pub(crate) frozen: bool,
53    pub(crate) automatic_types: bool,
54    pub(crate) jsr_registry: url::Url,
55    pub(crate) npm_registry: url::Url,
56    pub(crate) cache: PathBuf,
57    pub(crate) lock: Option<PathBuf>,
58    pub(crate) vendor: Option<PathBuf>,
59    pub(crate) includes: Vec<(String, Option<String>)>,
60    pub(crate) include_from: Option<String>,
61    pub(crate) env_files: Vec<PathBuf>,
62    pub(crate) cwd: PathBuf,
63}
64
65impl Common {
66    pub fn new(argv: &[String], cwd: &Path) -> Self {
67        let zero = argv
68            .iter()
69            .take_while(|arg| arg.as_str() != "--")
70            .any(|arg| arg == "-0");
71        Self {
72            initial: Initial::new(zero),
73            offline: false,
74            frozen: false,
75            automatic_types: true,
76            jsr_registry: url::Url::parse("https://jsr.io/").unwrap(),
77            npm_registry: url::Url::parse("https://registry.npmjs.org/").unwrap(),
78            cache: default_cache(cwd),
79            lock: None,
80            vendor: None,
81            includes: Vec::new(),
82            include_from: None,
83            env_files: Vec::new(),
84            cwd: cwd.to_owned(),
85        }
86    }
87
88    pub fn offer(
89        &mut self,
90        arg: &str,
91        args: &mut impl Iterator<Item = String>,
92    ) -> Result<bool, ParseError> {
93        match arg {
94            "-0" => return Ok(true),
95            "--offline" => self.offline = true,
96            "--frozen" => self.frozen = true,
97            "--no-types-packages" => self.automatic_types = false,
98            "--env-file" => {
99                let value = value(args, ParseError::EnvFilePath)?;
100                self.env_files.push(self.cwd.join(value));
101            }
102            _ if arg.starts_with("--allow-") || arg.starts_with("--deny-") => {
103                let (flag, value) = flag_value(arg)?;
104                let (name, deny) = flag
105                    .strip_prefix("--allow-")
106                    .map(|name| (name, false))
107                    .or_else(|| flag.strip_prefix("--deny-").map(|name| (name, true)))
108                    .unwrap();
109                self.initial
110                    .grant(name.parse::<Category>()?, deny, value, &self.cwd)?;
111            }
112            _ => {
113                let Some((flag, value)) = arg.split_once('=') else {
114                    return Ok(false);
115                };
116                match flag {
117                    "--offline" | "--frozen" | "--no-types-packages" => {
118                        return Err(ParseError::UnexpectedValue {
119                            flag: flag.trim_start_matches('-').to_owned(),
120                        });
121                    }
122                    "--jsr-registry" | "--npm-registry" | "--cache-dir" | "--lock" | "--vendor"
123                    | "--include" | "--include-from" | "--env-file"
124                        if value.is_empty() =>
125                    {
126                        return Err(ParseError::EmptyValue { flag: flag.to_owned() });
127                    }
128                    "--jsr-registry" => self.jsr_registry = url::Url::parse(value)?,
129                    "--npm-registry" => self.npm_registry = url::Url::parse(value)?,
130                    "--cache-dir" => self.cache = self.cwd.join(value),
131                    "--lock" => self.lock = Some(self.cwd.join(value)),
132                    "--vendor" => self.vendor = Some(self.cwd.join(value)),
133                    "--include" => self
134                        .includes
135                        .push((value.to_owned(), self.include_from.clone())),
136                    "--include-from" => {
137                        self.include_from = (value != ".").then(|| value.to_owned())
138                    }
139                    "--env-file" => self.env_files.push(self.cwd.join(value)),
140                    _ => return Ok(false),
141                }
142            }
143        }
144        Ok(true)
145    }
146
147    pub fn finish(self, path: PathBuf, module: String, lock: PathBuf) -> Result<Entry, ParseError> {
148        let explicit_lock = self.lock.is_some();
149        Ok(Entry {
150            caps: Capabilities::new(self.initial, self.cwd.clone()),
151            packages: Options {
152                jsr_registry: self.jsr_registry,
153                npm_registry: self.npm_registry,
154                cache: self.cache,
155                lock: self.lock.unwrap_or(lock),
156                project_root: self.cwd.clone(),
157                offline: self.offline,
158                frozen: self.frozen,
159                update: false,
160                ephemeral_lock: false,
161                vendor: self.vendor,
162            },
163            explicit_lock,
164            automatic_types: self.automatic_types,
165            path,
166            module,
167            includes: self.includes,
168            environment: crate::env_file::load(&self.env_files).map_err(ParseError::Env)?,
169        })
170    }
171}
172
173/// A finished parse: everything a command needs to open the entry's graph.
174#[derive(Clone)]
175pub struct Entry {
176    pub caps: Arc<Capabilities>,
177    pub packages: Options,
178    pub explicit_lock: bool,
179    pub automatic_types: bool,
180    pub path: PathBuf,
181    pub module: String,
182    pub includes: Vec<(String, Option<String>)>,
183    pub environment: Environment,
184}
185
186pub enum Arg {
187    Flag(String),
188    Positional(String),
189}
190
191/// One command's argv: `--`, the table check, and the shared flags are
192/// handled here, so `next` yields only what the command itself must handle.
193pub struct Parser {
194    pub command: &'static Command,
195    pub common: Common,
196    pub cwd: PathBuf,
197    args: std::vec::IntoIter<String>,
198    separator: bool,
199}
200
201impl Parser {
202    pub fn new(name: &str, argv: Vec<String>, cwd: &Path) -> Result<Self, ParseError> {
203        Ok(Self {
204            command: command(name).ok_or_else(|| ParseError::Usage(usage()))?,
205            common: Common::new(&argv, cwd),
206            cwd: cwd.to_owned(),
207            args: argv.into_iter(),
208            separator: false,
209        })
210    }
211
212    pub fn next(&mut self) -> Result<Option<Arg>, ParseError> {
213        while let Some(arg) = self.args.next() {
214            if self.separator || !arg.starts_with('-') {
215                return Ok(Some(Arg::Positional(arg)));
216            }
217            if arg == "--" {
218                self.separator = true;
219                continue;
220            }
221            self.command.check(&arg).map_err(ParseError::Check)?;
222            if !self.common.offer(&arg, &mut self.args)? {
223                return Ok(Some(Arg::Flag(arg)));
224            }
225        }
226        Ok(None)
227    }
228
229    pub fn value(&mut self, missing: ParseError) -> Result<String, ParseError> {
230        value(&mut self.args, missing)
231    }
232
233    pub fn usage(&self) -> ParseError {
234        ParseError::Usage(self.command.usage())
235    }
236
237    /// `--flag=PATH` or `--flag PATH`, given at most once.
238    pub fn path_once(
239        &mut self,
240        arg: &str,
241        flag: &str,
242        slot: &mut Option<String>,
243    ) -> Result<bool, ParseError> {
244        let (name, inline) = arg
245            .split_once('=')
246            .map_or((arg, None), |(name, value)| (name, Some(value)));
247        if name != flag {
248            return Ok(false);
249        }
250        let flag = flag.trim_start_matches('-').to_owned();
251        let value = inline
252            .map(str::to_owned)
253            .or_else(|| self.args.next())
254            .filter(|value| !value.is_empty() && !value.starts_with('-'))
255            .ok_or_else(|| ParseError::MissingPath { flag: flag.clone() })?;
256        if slot.replace(value).is_some() {
257            return Err(ParseError::Duplicate { flag });
258        }
259        Ok(true)
260    }
261
262    pub fn resolve(&mut self, raw: &str) -> Result<Resolved, ParseError> {
263        if let Some(url) = avril_runtime::modules::data_url(raw) {
264            return Ok(Resolved {
265                path: self.cwd.join(".avril-data.js"),
266                module: url.to_string(),
267                lock: self.cwd.join("avril.lock"),
268            });
269        }
270        if let Some(url) = avril_runtime::modules::http_url(raw) {
271            self.common.initial.remote_entry(&url)?;
272            return Ok(Resolved {
273                path: self.cwd.join(".avril-remote.js"),
274                module: url.to_string(),
275                lock: self.cwd.join("avril.lock"),
276            });
277        }
278        let path = self.cwd.join(raw).canonicalize()?;
279        self.common.initial.entry(&path);
280        Ok(Resolved {
281            module: path.to_str().ok_or(ParseError::ModulePath)?.to_owned(),
282            lock: path.parent().unwrap().join("avril.lock"),
283            path,
284        })
285    }
286
287    pub fn entry(mut self, raw: &str) -> Result<Entry, ParseError> {
288        let resolved = self.resolve(raw)?;
289        self.common
290            .finish(resolved.path, resolved.module, resolved.lock)
291    }
292
293    /// An entry for commands without a module (repl, lsp).
294    pub fn synthetic(self, file: &str) -> Result<Entry, ParseError> {
295        let path = self.cwd.join(file);
296        let module = path.to_str().ok_or(ParseError::ModulePath)?.to_owned();
297        let lock = self.cwd.join("avril.lock");
298        let mut entry = self.common.finish(path, module, lock)?;
299        entry.packages.ephemeral_lock = true;
300        Ok(entry)
301    }
302}
303
304impl Entry {
305    /// Resolves the project root for this entry's anchor: the lockfile
306    /// defaults to the entry's directory unless `--lock` was explicit.
307    pub fn project(
308        &self,
309        repl: bool,
310    ) -> Result<(SourceAuthority, PathBuf, Options), Box<dyn std::error::Error>> {
311        let authority = SourceAuthority::execute(self.caps.clone());
312        let anchor = self
313            .path
314            .to_str()
315            .ok_or("Module paths must be valid UTF-8")?;
316        let root = if repl {
317            ModuleGraph::project_root_for_repl(&authority, anchor)?
318        } else {
319            ModuleGraph::project_root(&authority, anchor)?
320        };
321        let mut packages = self.packages.clone();
322        if !self.explicit_lock {
323            packages.lock = root.join("avril.lock");
324        }
325        // Explicit --lock never changes the npm project root.
326        packages.project_root = root.clone();
327        Ok((authority, root, packages))
328    }
329}
330
331pub struct Resolved {
332    pub path: PathBuf,
333    pub module: String,
334    pub lock: PathBuf,
335}
336
337pub fn prepare_graph(
338    handle: tokio::runtime::Handle,
339    authority: SourceAuthority,
340    packages: Options,
341    name: &str,
342    includes: &[(String, Option<String>)],
343    prepare_entry: bool,
344    progress: Option<avril_runtime::registry::ProgressSink>,
345) -> Result<Rc<ModuleGraph>, Box<dyn std::error::Error>> {
346    let store = avril_runtime::registry::PackageStore::new_with_authority(
347        handle,
348        packages,
349        authority.clone(),
350    )?;
351    let store = Arc::new(match progress {
352        Some(progress) => store.with_progress(progress),
353        None => store,
354    });
355    let graph = Rc::new(ModuleGraph::open_shared(authority, store));
356    futures::executor::block_on(graph.initialize_package(name))?;
357    if prepare_entry {
358        futures::executor::block_on(graph.prepare(name.to_owned(), String::new()))?;
359    }
360    for (include, from) in includes {
361        let referrer = match from {
362            Some(from) => {
363                futures::executor::block_on(graph.prepare(from.clone(), name.to_owned()))?
364            }
365            None => name.to_owned(),
366        };
367        if referrer.starts_with("avril:") {
368            return Err("Include referrer must be a source module".into());
369        }
370        futures::executor::block_on(graph.prepare(include.clone(), referrer))?;
371    }
372    Ok(graph)
373}