use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use avril_core::ModuleLoader; use avril_host::environment::Environment; use avril_runtime::authority::SourceAuthority; use avril_runtime::caps::{Capabilities, Category, Initial}; use avril_runtime::modules::ModuleGraph; use avril_runtime::registry::Options; use crate::cli::{Command, ParseError, command, default_cache, usage}; pub fn working_dir() -> Result { Ok(std::env::current_dir()?.canonicalize()?) } fn value( args: &mut impl Iterator, missing: ParseError, ) -> Result { match args.next() { Some(value) if !value.is_empty() && !value.starts_with('-') => Ok(value), _ => Err(missing), } } pub fn flag_value(arg: &str) -> Result<(&str, &str), ParseError> { let (flag, value) = arg .split_once('=') .ok_or_else(|| ParseError::ExpectedEquals { arg: arg.to_owned() })?; if value.is_empty() { return Err(ParseError::EmptyValue { flag: flag.to_owned() }); } Ok((flag, value)) } pub fn reject(arg: &str, bare: &[&str]) -> ParseError { let flag = arg.split_once('=').map_or(arg, |(flag, _)| flag); if bare.contains(&flag) { ParseError::UnexpectedValue { flag: flag.trim_start_matches('-').to_owned(), } } else { ParseError::ExpectedEquals { arg: arg.to_owned() } } } pub struct Common { pub(crate) initial: Initial, pub(crate) offline: bool, pub(crate) frozen: bool, pub(crate) automatic_types: bool, pub(crate) jsr_registry: url::Url, pub(crate) npm_registry: url::Url, pub(crate) cache: PathBuf, pub(crate) lock: Option, pub(crate) vendor: Option, pub(crate) includes: Vec<(String, Option)>, pub(crate) include_from: Option, pub(crate) env_files: Vec, pub(crate) cwd: PathBuf, } impl Common { pub fn new(argv: &[String], cwd: &Path) -> Self { let zero = argv .iter() .take_while(|arg| arg.as_str() != "--") .any(|arg| arg == "-0"); Self { initial: Initial::new(zero), offline: false, frozen: false, automatic_types: true, jsr_registry: url::Url::parse("https://jsr.io/").unwrap(), npm_registry: url::Url::parse("https://registry.npmjs.org/").unwrap(), cache: default_cache(cwd), lock: None, vendor: None, includes: Vec::new(), include_from: None, env_files: Vec::new(), cwd: cwd.to_owned(), } } pub fn offer( &mut self, arg: &str, args: &mut impl Iterator, ) -> Result { match arg { "-0" => return Ok(true), "--offline" => self.offline = true, "--frozen" => self.frozen = true, "--no-types-packages" => self.automatic_types = false, "--env-file" => { let value = value(args, ParseError::EnvFilePath)?; self.env_files.push(self.cwd.join(value)); } _ if arg.starts_with("--allow-") || arg.starts_with("--deny-") => { let (flag, value) = flag_value(arg)?; let (name, deny) = flag .strip_prefix("--allow-") .map(|name| (name, false)) .or_else(|| flag.strip_prefix("--deny-").map(|name| (name, true))) .unwrap(); self.initial .grant(name.parse::()?, deny, value, &self.cwd)?; } _ => { let Some((flag, value)) = arg.split_once('=') else { return Ok(false); }; match flag { "--offline" | "--frozen" | "--no-types-packages" => { return Err(ParseError::UnexpectedValue { flag: flag.trim_start_matches('-').to_owned(), }); } "--jsr-registry" | "--npm-registry" | "--cache-dir" | "--lock" | "--vendor" | "--include" | "--include-from" | "--env-file" if value.is_empty() => { return Err(ParseError::EmptyValue { flag: flag.to_owned() }); } "--jsr-registry" => self.jsr_registry = url::Url::parse(value)?, "--npm-registry" => self.npm_registry = url::Url::parse(value)?, "--cache-dir" => self.cache = self.cwd.join(value), "--lock" => self.lock = Some(self.cwd.join(value)), "--vendor" => self.vendor = Some(self.cwd.join(value)), "--include" => self .includes .push((value.to_owned(), self.include_from.clone())), "--include-from" => { self.include_from = (value != ".").then(|| value.to_owned()) } "--env-file" => self.env_files.push(self.cwd.join(value)), _ => return Ok(false), } } } Ok(true) } pub fn finish(self, path: PathBuf, module: String, lock: PathBuf) -> Result { let explicit_lock = self.lock.is_some(); Ok(Entry { caps: Capabilities::new(self.initial, self.cwd.clone()), packages: Options { jsr_registry: self.jsr_registry, npm_registry: self.npm_registry, cache: self.cache, lock: self.lock.unwrap_or(lock), project_root: self.cwd.clone(), offline: self.offline, frozen: self.frozen, update: false, ephemeral_lock: false, vendor: self.vendor, }, explicit_lock, automatic_types: self.automatic_types, path, module, includes: self.includes, environment: crate::env_file::load(&self.env_files).map_err(ParseError::Env)?, }) } } /// A finished parse: everything a command needs to open the entry's graph. #[derive(Clone)] pub struct Entry { pub caps: Arc, pub packages: Options, pub explicit_lock: bool, pub automatic_types: bool, pub path: PathBuf, pub module: String, pub includes: Vec<(String, Option)>, pub environment: Environment, } pub enum Arg { Flag(String), Positional(String), } /// One command's argv: `--`, the table check, and the shared flags are /// handled here, so `next` yields only what the command itself must handle. pub struct Parser { pub command: &'static Command, pub common: Common, pub cwd: PathBuf, args: std::vec::IntoIter, separator: bool, } impl Parser { pub fn new(name: &str, argv: Vec, cwd: &Path) -> Result { Ok(Self { command: command(name).ok_or_else(|| ParseError::Usage(usage()))?, common: Common::new(&argv, cwd), cwd: cwd.to_owned(), args: argv.into_iter(), separator: false, }) } pub fn next(&mut self) -> Result, ParseError> { while let Some(arg) = self.args.next() { if self.separator || !arg.starts_with('-') { return Ok(Some(Arg::Positional(arg))); } if arg == "--" { self.separator = true; continue; } self.command.check(&arg).map_err(ParseError::Check)?; if !self.common.offer(&arg, &mut self.args)? { return Ok(Some(Arg::Flag(arg))); } } Ok(None) } pub fn value(&mut self, missing: ParseError) -> Result { value(&mut self.args, missing) } pub fn usage(&self) -> ParseError { ParseError::Usage(self.command.usage()) } /// `--flag=PATH` or `--flag PATH`, given at most once. pub fn path_once( &mut self, arg: &str, flag: &str, slot: &mut Option, ) -> Result { let (name, inline) = arg .split_once('=') .map_or((arg, None), |(name, value)| (name, Some(value))); if name != flag { return Ok(false); } let flag = flag.trim_start_matches('-').to_owned(); let value = inline .map(str::to_owned) .or_else(|| self.args.next()) .filter(|value| !value.is_empty() && !value.starts_with('-')) .ok_or_else(|| ParseError::MissingPath { flag: flag.clone() })?; if slot.replace(value).is_some() { return Err(ParseError::Duplicate { flag }); } Ok(true) } pub fn resolve(&mut self, raw: &str) -> Result { if let Some(url) = avril_runtime::modules::data_url(raw) { return Ok(Resolved { path: self.cwd.join(".avril-data.js"), module: url.to_string(), lock: self.cwd.join("avril.lock"), }); } if let Some(url) = avril_runtime::modules::http_url(raw) { self.common.initial.remote_entry(&url)?; return Ok(Resolved { path: self.cwd.join(".avril-remote.js"), module: url.to_string(), lock: self.cwd.join("avril.lock"), }); } let path = self.cwd.join(raw).canonicalize()?; self.common.initial.entry(&path); Ok(Resolved { module: path.to_str().ok_or(ParseError::ModulePath)?.to_owned(), lock: path.parent().unwrap().join("avril.lock"), path, }) } pub fn entry(mut self, raw: &str) -> Result { let resolved = self.resolve(raw)?; self.common .finish(resolved.path, resolved.module, resolved.lock) } /// An entry for commands without a module (repl, lsp). pub fn synthetic(self, file: &str) -> Result { let path = self.cwd.join(file); let module = path.to_str().ok_or(ParseError::ModulePath)?.to_owned(); let lock = self.cwd.join("avril.lock"); let mut entry = self.common.finish(path, module, lock)?; entry.packages.ephemeral_lock = true; Ok(entry) } } impl Entry { /// Resolves the project root for this entry's anchor: the lockfile /// defaults to the entry's directory unless `--lock` was explicit. pub fn project( &self, repl: bool, ) -> Result<(SourceAuthority, PathBuf, Options), Box> { let authority = SourceAuthority::execute(self.caps.clone()); let anchor = self .path .to_str() .ok_or("Module paths must be valid UTF-8")?; let root = if repl { ModuleGraph::project_root_for_repl(&authority, anchor)? } else { ModuleGraph::project_root(&authority, anchor)? }; let mut packages = self.packages.clone(); if !self.explicit_lock { packages.lock = root.join("avril.lock"); } // Explicit --lock never changes the npm project root. packages.project_root = root.clone(); Ok((authority, root, packages)) } } pub struct Resolved { pub path: PathBuf, pub module: String, pub lock: PathBuf, } pub fn prepare_graph( handle: tokio::runtime::Handle, authority: SourceAuthority, packages: Options, name: &str, includes: &[(String, Option)], prepare_entry: bool, progress: Option, ) -> Result, Box> { let store = avril_runtime::registry::PackageStore::new_with_authority( handle, packages, authority.clone(), )?; let store = Arc::new(match progress { Some(progress) => store.with_progress(progress), None => store, }); let graph = Rc::new(ModuleGraph::open_shared(authority, store)); futures::executor::block_on(graph.initialize_package(name))?; if prepare_entry { futures::executor::block_on(graph.prepare(name.to_owned(), String::new()))?; } for (include, from) in includes { let referrer = match from { Some(from) => { futures::executor::block_on(graph.prepare(from.clone(), name.to_owned()))? } None => name.to_owned(), }; if referrer.starts_with("avril:") { return Err("Include referrer must be a source module".into()); } futures::executor::block_on(graph.prepare(include.clone(), referrer))?; } Ok(graph) }