1use std::any::Any;
2use std::cell::RefCell;
3use std::path::{Component, Path, PathBuf};
4use std::rc::Rc;
5
6use dynify::dynify;
7use rustc_hash::FxHashMap;
8
9use boa_gc::GcRefCell;
10use boa_parser::Source;
11
12use crate::script::Script;
13use crate::{
14 Context, JsError, JsNativeError, JsResult, JsString, js_error, js_string, object::JsObject,
15 realm::Realm, vm::ActiveRunnable,
16};
17
18use super::Module;
19use crate::module::{ImportAttribute, ModuleRequest};
20
21pub mod embedded;
22
23pub fn resolve_module_specifier(
53 base: Option<&Path>,
54 specifier: &JsString,
55 referrer: Option<&Path>,
56 _context: &mut Context,
57) -> JsResult<PathBuf> {
58 let base_path = base.map_or_else(|| PathBuf::from(""), PathBuf::from);
59 let referrer_dir = referrer.and_then(|p| p.parent());
60
61 let specifier = specifier.to_std_string_escaped();
62
63 #[cfg(target_family = "windows")]
65 let specifier = cow_utils::CowUtils::cow_replace(&*specifier, '/', "\\");
66
67 let short_path = Path::new(&*specifier);
68
69 let is_relative = short_path.starts_with(".") || short_path.starts_with("..");
72
73 let long_path = if is_relative {
74 if let Some(r_path) = referrer_dir {
75 base_path.join(r_path).join(short_path)
76 } else {
77 return Err(js_error!(TypeError: "relative path without referrer"));
78 }
79 } else {
80 base_path.join(&*specifier)
81 };
82
83 if long_path.is_relative() && base.is_some() {
84 return Err(js_error!(TypeError: "resolved path is relative"));
85 }
86
87 let path = long_path
90 .components()
91 .filter(|c| c != &Component::CurDir || c == &Component::Normal("".as_ref()))
92 .try_fold(PathBuf::new(), |mut acc, c| {
93 if c == Component::ParentDir {
94 if acc.as_os_str().is_empty() {
95 return Err(js_error!(TypeError: "path is outside the module root"));
96 }
97 acc.pop();
98 } else {
99 acc.push(c);
100 }
101 Ok(acc)
102 })?;
103
104 if path.starts_with(&base_path) {
105 Ok(path)
106 } else {
107 Err(js_error!(TypeError: "path is outside the module root"))
108 }
109}
110
111#[derive(Debug, Clone)]
113pub enum Referrer {
114 Module(Module),
116 Realm(Realm),
118 Script(Script),
120}
121
122impl Referrer {
123 #[must_use]
125 pub fn path(&self) -> Option<&Path> {
126 match self {
127 Self::Module(module) => module.path(),
128 Self::Realm(_) => None,
129 Self::Script(script) => script.path(),
130 }
131 }
132}
133
134impl From<ActiveRunnable> for Referrer {
135 fn from(value: ActiveRunnable) -> Self {
136 match value {
137 ActiveRunnable::Script(script) => Self::Script(script),
138 ActiveRunnable::Module(module) => Self::Module(module),
139 }
140 }
141}
142
143#[dynify]
148pub trait ModuleLoader: Any {
149 #[allow(async_fn_in_trait, reason = "all our APIs are single-threaded")]
172 async fn load_imported_module(
173 self: Rc<Self>,
174 referrer: Referrer,
175 request: ModuleRequest,
176 context: &RefCell<&mut Context>,
177 ) -> JsResult<Module>;
178
179 #[allow(unused_variables, reason = "this should be overridden by implementors")]
192 fn init_import_meta(
193 self: Rc<Self>,
194 import_meta: &JsObject,
195 module: &Module,
196 context: &mut Context,
197 ) {
198 }
199}
200
201#[derive(Debug, Clone, Copy)]
205pub struct IdleModuleLoader;
206
207impl ModuleLoader for IdleModuleLoader {
208 async fn load_imported_module(
209 self: Rc<Self>,
210 _referrer: Referrer,
211 _request: ModuleRequest,
212 _context: &RefCell<&mut Context>,
213 ) -> JsResult<Module> {
214 Err(JsNativeError::typ()
215 .with_message("module resolution is disabled for this context")
216 .into())
217 }
218}
219
220#[derive(Default, Debug, Clone)]
226pub struct MapModuleLoader {
227 inner: RefCell<FxHashMap<PathBuf, Module>>,
228}
229
230impl MapModuleLoader {
231 #[must_use]
233 #[inline]
234 pub fn new() -> Self {
235 Self::default()
236 }
237
238 #[inline]
241 pub fn insert(&self, specifier: impl AsRef<str>, module: Module) -> Option<Module> {
242 self.inner
243 .borrow_mut()
244 .insert(PathBuf::from(specifier.as_ref()), module)
245 }
246
247 pub fn clear(&self) {
249 self.inner.borrow_mut().clear();
250 }
251}
252
253impl FromIterator<(String, Module)> for MapModuleLoader {
254 fn from_iter<T: IntoIterator<Item = (String, Module)>>(iter: T) -> Self {
255 Self {
256 inner: RefCell::new(
257 iter.into_iter()
258 .map(|(k, v)| (PathBuf::from(k), v))
259 .collect(),
260 ),
261 }
262 }
263}
264
265impl ModuleLoader for MapModuleLoader {
266 fn load_imported_module(
267 self: Rc<Self>,
268 referrer: Referrer,
269 request: ModuleRequest,
270 context: &RefCell<&mut Context>,
271 ) -> impl Future<Output = JsResult<Module>> {
272 let result = (|| {
273 let path = resolve_module_specifier(
274 None,
275 request.specifier(),
276 referrer.path(),
277 &mut context.borrow_mut(),
278 )?;
279 if let Some(module) = self.inner.borrow().get(&path) {
280 Ok(module.clone())
281 } else {
282 Err(js_error!(TypeError: "Module could not be found."))
283 }
284 })();
285
286 async { result }
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291struct ModuleCacheKey {
292 path: PathBuf,
293 attributes: Box<[ImportAttribute]>,
294}
295
296impl ModuleCacheKey {
297 fn new(path: PathBuf, attributes: &[ImportAttribute]) -> Self {
298 let mut attributes = attributes.to_vec();
299 attributes.sort_unstable_by(|left, right| left.key().cmp(right.key()));
300 Self {
301 path,
302 attributes: attributes.into_boxed_slice(),
303 }
304 }
305}
306
307#[derive(Debug)]
315pub struct SimpleModuleLoader {
316 root: PathBuf,
317 module_map: GcRefCell<FxHashMap<ModuleCacheKey, Module>>,
318}
319
320impl SimpleModuleLoader {
321 pub fn new<P: AsRef<Path>>(root: P) -> JsResult<Self> {
323 if cfg!(target_family = "wasm") {
324 return Err(JsNativeError::typ()
325 .with_message("cannot resolve a relative path in Wasm targets")
326 .into());
327 }
328 let root = root.as_ref();
329 let absolute = root.canonicalize().map_err(|e| {
330 JsNativeError::typ()
331 .with_message(format!("could not set module root `{}`", root.display()))
332 .with_cause(JsError::from_rust(e))
333 })?;
334 Ok(Self {
335 root: absolute,
336 module_map: GcRefCell::default(),
337 })
338 }
339
340 #[inline]
342 pub fn insert(&self, path: PathBuf, module: Module) {
343 self.insert_with_attributes(path, &[], module);
344 }
345
346 #[inline]
348 pub fn insert_with_attributes(
349 &self,
350 path: PathBuf,
351 attributes: &[ImportAttribute],
352 module: Module,
353 ) {
354 self.module_map
355 .borrow_mut()
356 .insert(ModuleCacheKey::new(path, attributes), module);
357 }
358
359 #[inline]
361 pub fn get(&self, path: &Path) -> Option<Module> {
362 self.get_with_attributes(path, &[])
363 }
364
365 #[inline]
367 pub fn get_with_attributes(
368 &self,
369 path: &Path,
370 attributes: &[ImportAttribute],
371 ) -> Option<Module> {
372 self.module_map
373 .borrow()
374 .get(&ModuleCacheKey::new(path.to_path_buf(), attributes))
375 .cloned()
376 }
377}
378
379impl ModuleLoader for SimpleModuleLoader {
380 fn load_imported_module(
381 self: Rc<Self>,
382 referrer: Referrer,
383 request: ModuleRequest,
384 context: &RefCell<&mut Context>,
385 ) -> impl Future<Output = JsResult<Module>> {
386 let result = (|| {
387 let short_path = request.specifier().to_std_string_escaped();
388 let path = resolve_module_specifier(
389 Some(&self.root),
390 request.specifier(),
391 referrer.path(),
392 &mut context.borrow_mut(),
393 )?;
394
395 if let Some(module) = self.get_with_attributes(&path, request.attributes()) {
396 return Ok(module);
397 }
398
399 let mut module_type = None;
401 let type_key = js_string!("type");
402 for attr in request.attributes() {
403 if attr.key() == &type_key {
404 module_type = Some(attr.value());
405 }
406 }
407
408 if path
409 .extension()
410 .is_some_and(|ext| ext.to_string_lossy() == "json")
411 {
412 let is_json_type = module_type.is_some_and(|t| t == &js_string!("json"));
413 if !is_json_type {
414 return Err(JsNativeError::typ()
415 .with_message(format!(
416 "module `{short_path}` needs an import attribute of type \"json\""
417 ))
418 .into());
419 }
420 }
421 let module = if let Some(ty) = module_type {
422 match ty.to_std_string_escaped().as_str() {
424 "json" => {
425 let json_content = std::fs::read_to_string(&path).map_err(|err| {
427 JsNativeError::typ()
428 .with_message(format!("could not open file `{short_path}`"))
429 .with_cause(JsError::from_rust(err))
430 })?;
431 let json_string = js_string!(json_content.as_str());
432 Module::parse_json(json_string, &mut context.borrow_mut()).map_err(
433 |err| {
434 JsNativeError::syntax()
435 .with_message(format!(
436 "could not parse JSON module `{short_path}`"
437 ))
438 .with_cause(err)
439 },
440 )?
441 }
442 other => {
443 return Err(JsNativeError::typ()
445 .with_message(format!(
446 "unsupported module type `{other}` for module `{short_path}`"
447 ))
448 .into());
449 }
450 }
451 } else {
452 let source = Source::from_filepath(&path).map_err(|err| {
454 JsNativeError::typ()
455 .with_message(format!("could not open file `{short_path}`"))
456 .with_cause(JsError::from_rust(err))
457 })?;
458 Module::parse(source, None, &mut context.borrow_mut()).map_err(|err| {
459 JsNativeError::syntax()
460 .with_message(format!("could not parse module `{short_path}`"))
461 .with_cause(err)
462 })?
463 };
464
465 self.insert_with_attributes(path, request.attributes(), module.clone());
466 Ok(module)
467 })();
468
469 async { result }
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use std::path::PathBuf;
476
477 use test_case::test_case;
478
479 use super::*;
480
481 #[rustfmt::skip]
484 #[cfg(target_family = "unix")]
485 #[test_case(Some("/hello/ref.js"), "a.js", Ok("/base/a.js"))]
486 #[test_case(Some("/base/ref.js"), "./b.js", Ok("/base/b.js"))]
487 #[test_case(Some("/base/other/ref.js"), "./c.js", Ok("/base/other/c.js"))]
488 #[test_case(Some("/base/other/ref.js"), "../d.js", Ok("/base/d.js"))]
489 #[test_case(Some("/base/ref.js"), "e.js", Ok("/base/e.js"))]
490 #[test_case(Some("/base/ref.js"), "./f.js", Ok("/base/f.js"))]
491 #[test_case(Some("./ref.js"), "./g.js", Ok("/base/g.js"))]
492 #[test_case(Some("./other/ref.js"), "./other/h.js", Ok("/base/other/other/h.js"))]
493 #[test_case(Some("./other/ref.js"), "./other/../h1.js", Ok("/base/other/h1.js"))]
494 #[test_case(Some("./other/ref.js"), "./../h2.js", Ok("/base/h2.js"))]
495 #[test_case(None, "./i.js", Err(()))]
496 #[test_case(None, "j.js", Ok("/base/j.js"))]
497 #[test_case(None, "other/k.js", Ok("/base/other/k.js"))]
498 #[test_case(None, "other/../../l.js", Err(()))]
499 #[test_case(Some("/base/ref.js"), "other/../../m.js", Err(()))]
500 #[test_case(None, "../n.js", Err(()))]
501 fn resolve_test(ref_path: Option<&str>, spec: &str, expected: Result<&str, ()>) {
502 let base = PathBuf::from("/base");
503
504 let mut context = Context::default();
505 let spec = js_string!(spec);
506 let ref_path = ref_path.map(PathBuf::from);
507
508 let actual = resolve_module_specifier(
509 Some(&base),
510 &spec,
511 ref_path.as_deref(),
512 &mut context,
513 );
514 assert_eq!(actual.map_err(|_| ()), expected.map(PathBuf::from));
515 }
516
517 #[rustfmt::skip]
519 #[cfg(target_family = "unix")]
520 #[test_case(Some("hello/ref.js"), "a.js", Ok("a.js"))]
521 #[test_case(Some("base/ref.js"), "./b.js", Ok("base/b.js"))]
522 #[test_case(Some("base/other/ref.js"), "./c.js", Ok("base/other/c.js"))]
523 #[test_case(Some("base/other/ref.js"), "../d.js", Ok("base/d.js"))]
524 #[test_case(Some("base/ref.js"), "e.js", Ok("e.js"))]
525 #[test_case(Some("base/ref.js"), "./f.js", Ok("base/f.js"))]
526 #[test_case(Some("./ref.js"), "./g.js", Ok("g.js"))]
527 #[test_case(Some("./other/ref.js"), "./other/h.js", Ok("other/other/h.js"))]
528 #[test_case(Some("./other/ref.js"), "./other/../h1.js", Ok("other/h1.js"))]
529 #[test_case(Some("./other/ref.js"), "./../h2.js", Ok("h2.js"))]
530 #[test_case(None, "./i.js", Err(()))]
531 #[test_case(None, "j.js", Ok("j.js"))]
532 #[test_case(None, "other/k.js", Ok("other/k.js"))]
533 #[test_case(None, "other/../../l.js", Err(()))]
534 #[test_case(Some("/base/ref.js"), "other/../../m.js", Err(()))]
535 #[test_case(None, "../n.js", Err(()))]
536 fn resolve_test_no_base(ref_path: Option<&str>, spec: &str, expected: Result<&str, ()>) {
537 let mut context = Context::default();
538 let spec = js_string!(spec);
539 let ref_path = ref_path.map(PathBuf::from);
540
541 let actual = resolve_module_specifier(
542 None,
543 &spec,
544 ref_path.as_deref(),
545 &mut context,
546 );
547 assert_eq!(actual.map_err(|_| ()), expected.map(PathBuf::from));
548 }
549
550 #[rustfmt::skip]
551 #[cfg(target_family = "windows")]
552 #[test_case(Some("a:\\hello\\ref.js"), "a.js", Ok("a:\\base\\a.js"))]
553 #[test_case(Some("a:\\base\\ref.js"), "./b.js", Ok("a:\\base\\b.js"))]
554 #[test_case(Some("a:\\base\\other\\ref.js"), "./c.js", Ok("a:\\base\\other\\c.js"))]
555 #[test_case(Some("a:\\base\\other\\ref.js"), "../d.js", Ok("a:\\base\\d.js"))]
556 #[test_case(Some("a:\\base\\ref.js"), "e.js", Ok("a:\\base\\e.js"))]
557 #[test_case(Some("a:\\base\\ref.js"), "./f.js", Ok("a:\\base\\f.js"))]
558 #[test_case(Some(".\\ref.js"), "./g.js", Ok("a:\\base\\g.js"))]
559 #[test_case(Some(".\\other\\ref.js"), "./other/h.js", Ok("a:\\base\\other\\other\\h.js"))]
560 #[test_case(Some(".\\other\\ref.js"), "./other/../h1.js", Ok("a:\\base\\other\\h1.js"))]
561 #[test_case(Some(".\\other\\ref.js"), "./../h2.js", Ok("a:\\base\\h2.js"))]
562 #[test_case(None, "./i.js", Err(()))]
563 #[test_case(None, "j.js", Ok("a:\\base\\j.js"))]
564 #[test_case(None, "other/k.js", Ok("a:\\base\\other\\k.js"))]
565 #[test_case(None, "other/../../l.js", Err(()))]
566 #[test_case(Some("\\base\\ref.js"), "other/../../m.js", Err(()))]
567 #[test_case(None, "../n.js", Err(()))]
568 fn resolve_test(ref_path: Option<&str>, spec: &str, expected: Result<&str, ()>) {
569 let base = PathBuf::from("a:\\base");
570
571 let mut context = Context::default();
572 let spec = js_string!(spec);
573 let ref_path = ref_path.map(PathBuf::from);
574
575 let actual = resolve_module_specifier(
576 Some(&base),
577 &spec,
578 ref_path.as_deref(),
579 &mut context,
580 );
581 assert_eq!(actual.map_err(|_| ()), expected.map(PathBuf::from));
582 }
583}