1use std::collections::hash_map::{Entry, HashMap};
8use std::ffi::OsStr;
9use std::fs::read_to_string;
10use std::path::{Path, PathBuf};
11
12use dunce::canonicalize;
13use ion::module::{Module, ModuleData, ModuleLoader, ModuleRequest, ModuleType};
14use ion::{Context, Error, Local, Object, Result, Value};
15use mozjs::jsapi::JSObject;
16use url::Url;
17
18use crate::cache::locate_in_cache;
19use crate::cache::map::save_sourcemap;
20use crate::config::Config;
21
22#[derive(Debug, Eq, Hash, PartialEq)]
23pub enum Specifier {
24 Path(PathBuf),
25}
26
27#[derive(Debug, Eq, Hash, PartialEq)]
28pub struct ModuleKey {
29 pub(crate) specifier: Specifier,
30 pub(crate) kind: ModuleType,
31}
32
33#[derive(Default)]
34pub struct Loader {
35 modules: HashMap<ModuleKey, *mut JSObject>,
36}
37
38impl Loader {
39 fn resolve_specifier(specifier: &str, data: Option<&ModuleData>) -> Specifier {
40 let path = if specifier.starts_with("./") || specifier.starts_with("../") {
41 Path::new(data.and_then(|d| d.path.as_ref()).unwrap()).parent().unwrap().join(specifier)
42 } else {
43 Path::new(specifier).to_path_buf()
44 };
45 Specifier::Path(path)
46 }
47}
48
49impl ModuleLoader for Loader {
50 fn resolve<'cx>(&mut self, cx: &'cx Context, private: &Value, request: &ModuleRequest) -> Result<Module<'cx>> {
51 let specifier = request.specifier(cx).to_owned(cx)?;
52 let data = ModuleData::from_private(cx, private);
53
54 let specifier = Loader::resolve_specifier(&specifier, data.as_ref());
55 let kind = request.kind(cx);
56
57 let key = ModuleKey { specifier, kind };
58 let Specifier::Path(path) = &key.specifier;
59 let name = path.to_str().unwrap();
60
61 if let Some(module) = self.modules.get(&key) {
62 return Ok(Module(Object::from(unsafe { Local::from_marked(module) })));
63 }
64
65 let script = read_to_string(path).map_err(|_| Error::new(format!("Unable to read module: {key:?}"), None))?;
66
67 let module = match kind {
68 ModuleType::JavaScript => {
69 let is_typescript = Config::global().typescript && path.extension() == Some(OsStr::new("ts"));
70 let (script, sourcemap) = is_typescript
71 .then(|| locate_in_cache(path, &script))
72 .flatten()
73 .map_or_else(|| (script, None), |(s, sm)| (s, Some(sm)));
74 if let Some(sourcemap) = sourcemap {
75 save_sourcemap(path, sourcemap);
76 }
77
78 Module::compile_and_evaluate(cx, name, Some(path.as_path()), &script).map(|(module, _)| module)
79 }
80 ModuleType::Json => {
81 Module::compile_and_evaluate(cx, name, Some(path.as_path()), &script).map(|(module, _)| module)
82 }
83 };
84
85 match module {
86 Ok(module) => {
87 let request = ModuleRequest::new(cx, name, kind);
88 self.register(cx, module.0.handle().get(), &request)?;
89 Ok(module)
90 }
91 Err(_) => Err(Error::new(format!("Unable to compile module: {key:?}"), None)),
92 }
93 }
94
95 fn register(&mut self, cx: &Context, module: *mut JSObject, request: &ModuleRequest) -> Result<()> {
96 let specifier = Specifier::Path(PathBuf::from(request.specifier(cx).to_owned(cx)?));
97 let kind = ModuleType::JavaScript;
98 let key = ModuleKey { specifier, kind };
99 match self.modules.entry(key) {
100 Entry::Vacant(v) => {
101 v.insert(module);
102 Ok(())
103 }
104 Entry::Occupied(_) => Err(Error::new("Module already exists", None)),
105 }
106 }
107
108 fn metadata(&self, cx: &Context, private: &Value, meta: &Object) -> Result<()> {
109 let data = ModuleData::from_private(cx, private);
110
111 if let Some(data) = data
112 && let Some(path) = data.path.as_ref()
113 {
114 let url = Url::from_file_path(canonicalize(path)?).unwrap();
115 if !meta.set_as(cx, "url", url.as_str()) {
116 return Err(Error::none());
117 }
118 }
119 Ok(())
120 }
121}