char/avril

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

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

Charlotte Sommass: fix clippy lints513fcd4

main
10.1 KiB264 linesraw
1use std::cell::RefCell;
2use std::collections::{BTreeSet, HashMap, HashSet};
3use std::io::Read;
4use std::path::{Component, Path, PathBuf};
5
6use crate::authority::SourceAuthority;
7use crate::caps::PathTarget;
8use crate::package_config::check_jsx_option;
9use avril_core::Error;
10use oxc::transformer::{JsxOptions, JsxRuntime, TransformOptions};
11use serde_json::{Map, Value};
12
13use crate::local_packages::trusted;
14
15#[derive(Clone, Default)]
16pub(crate) struct JsxConfig(Map<String, Value>);
17
18impl JsxConfig {
19    pub fn compiler_options(&self) -> Map<String, Value> {
20        let mut options = self.0.clone();
21        options.entry("jsx").or_insert_with(|| "react-jsx".into());
22        options
23    }
24
25    pub fn preserves_jsx(&self) -> bool {
26        matches!(
27            self.0.get("jsx").and_then(Value::as_str),
28            Some("preserve" | "react-native")
29        )
30    }
31
32    pub fn transform_options(&self) -> Result<TransformOptions, Error> {
33        if self.preserves_jsx() {
34            return Err(Error::Module("avril cannot execute preserved JSX; set compilerOptions.jsx to react-jsx, react-jsxdev, or react".into()));
35        }
36        let string = |key| self.0.get(key).and_then(Value::as_str).map(str::to_owned);
37        let mut options = TransformOptions {
38            jsx: JsxOptions {
39                runtime: if string("jsx").as_deref() == Some("react") {
40                    JsxRuntime::Classic
41                } else {
42                    JsxRuntime::Automatic
43                },
44                development: string("jsx").as_deref() == Some("react-jsxdev"),
45                import_source: string("jsxImportSource"),
46                pragma: string("jsxFactory"),
47                pragma_frag: string("jsxFragmentFactory"),
48                ..JsxOptions::default()
49            },
50            ..TransformOptions::default()
51        };
52        // TypeScript import elision must retain factories used only by JSX.
53        if let Some(factory) = &options.jsx.pragma {
54            options.typescript.jsx_pragma = factory.clone().into();
55        }
56        if let Some(fragment) = &options.jsx.pragma_frag {
57            options.typescript.jsx_pragma_frag = fragment.clone().into();
58        }
59        Ok(options)
60    }
61}
62
63/// The nearest applicable `tsconfig.json` overrides `defaults`, the owning
64/// package's `avril.conf` options.
65pub(crate) fn jsx_config(
66    authority: &SourceAuthority,
67    module: &Path,
68    boundary: Option<&Path>,
69    defaults: &Map<String, Value>,
70    inputs: &RefCell<BTreeSet<PathBuf>>,
71    cache: &RefCell<HashMap<PathBuf, JsxConfig>>,
72) -> Result<JsxConfig, Error> {
73    let mut directory = module.parent();
74    while let Some(path) = directory {
75        let target = PathTarget::resolve(path).map_err(|error| Error::Module(error.to_string()))?;
76        if target.as_path() != path || authority.check_local(&target).is_err() {
77            break;
78        }
79        let handle = crate::sources::open_directory(path)
80            .map_err(|error| Error::Module(error.to_string()))?;
81        if !trusted(
82            &handle
83                .dir_metadata()
84                .map_err(|error| Error::Module(error.to_string()))?,
85        ) {
86            break;
87        }
88        let package = PathTarget::for_entry(path.join("avril.conf"))
89            .map_err(|error| Error::Module(error.to_string()))?;
90        if authority.check_local(&package).is_err() {
91            break;
92        }
93        let config = path.join("tsconfig.json");
94        let target =
95            PathTarget::for_entry(&config).map_err(|error| Error::Module(error.to_string()))?;
96        // Do not probe the existence of configs outside source authority.
97        if authority.check_local(&target).is_err() {
98            break;
99        }
100        inputs.borrow_mut().insert(config.clone());
101        match handle.symlink_metadata("tsconfig.json") {
102            Ok(_) => {
103                let found = ConfigLoader {
104                    authority,
105                    inputs,
106                    active: HashSet::new(),
107                    cache,
108                }
109                .load(&config, false)?;
110                let mut options = defaults.clone();
111                options.extend(found.0);
112                return Ok(JsxConfig(options));
113            }
114            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
115            Err(error) => return Err(Error::Module(format!("{}: {error}", config.display()))),
116        }
117        if Some(path) == boundary {
118            break;
119        }
120        directory = path.parent();
121    }
122    Ok(JsxConfig(defaults.clone()))
123}
124
125struct ConfigLoader<'a> {
126    authority: &'a SourceAuthority,
127    inputs: &'a RefCell<BTreeSet<PathBuf>>,
128    active: HashSet<PathBuf>,
129    cache: &'a RefCell<HashMap<PathBuf, JsxConfig>>,
130}
131
132impl ConfigLoader<'_> {
133    fn load(&mut self, path: &Path, append_json: bool) -> Result<JsxConfig, Error> {
134        let target =
135            PathTarget::for_entry(path).map_err(|error| Error::Module(error.to_string()))?;
136        self.authority
137            .check_local(&target)
138            .map_err(|error| Error::Module(error.to_string()))?;
139        let path = target.as_path();
140        self.inputs.borrow_mut().insert(path.to_owned());
141        let error = |message: &str| Error::Module(format!("{}: {message}", path.display()));
142        if self.active.len() >= 64 || self.cache.borrow().len() >= 512 {
143            return Err(error("tsconfig extends graph is too large"));
144        }
145        if !self.active.insert(path.to_owned()) {
146            return Err(error("Cyclic tsconfig extends"));
147        }
148        if let Some(config) = self.cache.borrow().get(path).cloned() {
149            self.active.remove(path);
150            return Ok(config);
151        }
152        let directory = path
153            .parent()
154            .ok_or_else(|| error("Expected a config directory"))?;
155        let handle = crate::sources::open_directory(directory)
156            .map_err(|_| error("Cannot open config directory"))?;
157        if !trusted(
158            &handle
159                .dir_metadata()
160                .map_err(|_| error("Cannot inspect config directory"))?,
161        ) {
162            return Err(error("Untrusted config directory"));
163        }
164        let name = path
165            .file_name()
166            .ok_or_else(|| error("Expected a config filename"))?;
167        let metadata = match handle.symlink_metadata(name) {
168            Ok(metadata) => metadata,
169            Err(err)
170                if append_json
171                    && path.extension().is_none_or(|ext| ext != "json")
172                    && err.kind() == std::io::ErrorKind::NotFound =>
173            {
174                let mut name = name.to_os_string();
175                name.push(".json");
176                self.active.remove(path);
177                return self.load(&path.with_file_name(name), false);
178            }
179            Err(_) => return Err(error("Cannot find tsconfig")),
180        };
181        if !metadata.is_file() || metadata.is_symlink() {
182            return Err(error("Config must be a regular non-symlink file"));
183        }
184        if !trusted(&metadata) {
185            return Err(error("Untrusted config"));
186        }
187        let file =
188            crate::sources::open_regular(&handle, name).map_err(|_| error("Cannot read config"))?;
189        if !trusted(
190            &file
191                .metadata()
192                .map_err(|_| error("Cannot inspect config"))?,
193        ) {
194            return Err(error("Untrusted config"));
195        }
196        let mut source = String::new();
197        file.take(32 * 1024 * 1024 + 1)
198            .read_to_string(&mut source)
199            .map_err(|_| error("Cannot read config"))?;
200        if source.len() > 32 * 1024 * 1024 {
201            return Err(error("Config is too large"));
202        }
203        self.authority
204            .check_local(&target)
205            .map_err(|error| Error::Module(error.to_string()))?;
206        let value: Value = jsonc_parser::parse_to_serde_value(
207            source.trim_start_matches('\u{feff}'),
208            &jsonc_parser::ParseOptions {
209                allow_comments: true,
210                allow_trailing_commas: true,
211                allow_loose_object_property_names: false,
212                allow_missing_commas: false,
213                allow_single_quoted_strings: false,
214                allow_hexadecimal_numbers: false,
215                allow_unary_plus_numbers: false,
216                allow_bare_decimal_point_numbers: false,
217                allow_non_finite_numbers: false,
218                allow_extended_string_escapes: false,
219            },
220        )
221        .map_err(|err| error(&err.to_string()))?;
222        let object = value
223            .as_object()
224            .ok_or_else(|| error("tsconfig must be an object"))?;
225        let mut config = JsxConfig::default();
226        if let Some(extends) = object.get("extends") {
227            let bases = match extends {
228                Value::String(_) => std::slice::from_ref(extends),
229                Value::Array(bases) => bases.as_slice(),
230                _ => return Err(error("extends must be a path or an array of paths")),
231            };
232            for base in bases {
233                let base = base
234                    .as_str()
235                    .ok_or_else(|| error("extends entries must be paths"))?;
236                if !Path::new(base).is_absolute()
237                    && !matches!(
238                        Path::new(base).components().next(),
239                        Some(Component::CurDir | Component::ParentDir)
240                    )
241                {
242                    return Err(error(
243                        "extends must use relative or absolute file paths; package and URL configs are not supported",
244                    ));
245                }
246                config.0.extend(self.load(&directory.join(base), true)?.0);
247            }
248        }
249        if let Some(options) = object.get("compilerOptions") {
250            let options = options
251                .as_object()
252                .ok_or_else(|| error("compilerOptions must be an object"))?;
253            for (key, value) in options {
254                check_jsx_option(key, value).map_err(|message| error(&message))?;
255                config.0.insert(key.to_owned(), value.clone());
256            }
257        }
258        self.active.remove(path);
259        self.cache
260            .borrow_mut()
261            .insert(path.to_owned(), config.clone());
262        Ok(config)
263    }
264}