Skip to main content

rquickjs_core/
loader.rs

1//! Loaders and resolvers for loading JS modules.
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use core::{ffi::CStr, ptr};
6
7use crate::{module::Declared, object::ObjectKeysIter, qjs, Ctx, Module, Object, Result, Value};
8
9mod builtin_loader;
10mod builtin_resolver;
11pub mod bundle;
12mod compile;
13#[cfg(feature = "std")]
14mod file_resolver;
15mod module_loader;
16mod script_loader;
17mod util;
18
19#[cfg(feature = "dyn-load")]
20mod native_loader;
21
22pub use builtin_loader::BuiltinLoader;
23pub use builtin_resolver::BuiltinResolver;
24pub use compile::Compile;
25#[cfg(feature = "std")]
26pub use file_resolver::FileResolver;
27pub use module_loader::ModuleLoader;
28pub use script_loader::ScriptLoader;
29
30#[cfg(feature = "dyn-load")]
31#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "dyn-load")))]
32pub use native_loader::NativeLoader;
33
34#[cfg(feature = "phf")]
35/// The type of bundle that the `embed!` macro returns
36pub type Bundle = bundle::Bundle<bundle::PhfBundleData<&'static [u8]>>;
37
38#[cfg(not(feature = "phf"))]
39/// The type of bundle that the `embed!` macro returns
40pub type Bundle = bundle::Bundle<bundle::ScaBundleData<&'static [u8]>>;
41
42/// Module resolver interface
43#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
44pub trait Resolver {
45    /// Normalize module name
46    ///
47    /// The resolving may looks like:
48    ///
49    /// ```no_run
50    /// # use rquickjs::{Ctx, Result, Error};
51    /// # fn default_resolve<'js>(_ctx: &Ctx<'js>, base: &str, name: &str) -> Result<String> {
52    /// Ok(if !name.starts_with('.') {
53    ///     name.into()
54    /// } else {
55    ///     let mut split = base.rsplitn(2, '/');
56    ///     let path = match (split.next(), split.next()) {
57    ///         (_, Some(path)) => path,
58    ///         _ => "",
59    ///     };
60    ///     format!("{path}/{name}")
61    /// })
62    /// # }
63    /// ```
64    fn resolve<'js>(
65        &mut self,
66        ctx: &Ctx<'js>,
67        base: &str,
68        name: &str,
69        attributes: Option<ImportAttributes<'js>>,
70    ) -> Result<String>;
71}
72
73/// Import attributes from statements like `import x from "y" with { type: "json" }`
74#[derive(Clone, Debug)]
75pub struct ImportAttributes<'js>(Object<'js>);
76
77impl<'js> ImportAttributes<'js> {
78    /// Get an attribute value by key
79    pub fn get(&self, key: &str) -> Result<Option<String>> {
80        self.0.get(key)
81    }
82
83    /// Get the `type` attribute (shorthand for `get("type")`)
84    pub fn get_type(&self) -> Result<Option<String>> {
85        self.get("type")
86    }
87
88    /// Get an iterator over the attribute keys
89    pub fn keys(&self) -> ObjectKeysIter<'js, String> {
90        self.0.keys()
91    }
92}
93
94/// Module loader interface
95#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
96pub trait Loader {
97    /// Load module by name with import attributes
98    fn load<'js>(
99        &mut self,
100        ctx: &Ctx<'js>,
101        name: &str,
102        attributes: Option<ImportAttributes<'js>>,
103    ) -> Result<Module<'js, Declared>>;
104}
105
106struct LoaderOpaque {
107    resolver: Box<dyn Resolver>,
108    loader: Box<dyn Loader>,
109}
110
111#[derive(Debug)]
112#[repr(transparent)]
113pub(crate) struct LoaderHolder(*mut LoaderOpaque);
114
115impl Drop for LoaderHolder {
116    fn drop(&mut self) {
117        let _opaque = unsafe { Box::from_raw(self.0) };
118    }
119}
120
121impl LoaderHolder {
122    pub fn new<R, L>(resolver: R, loader: L) -> Self
123    where
124        R: Resolver + 'static,
125        L: Loader + 'static,
126    {
127        Self(Box::into_raw(Box::new(LoaderOpaque {
128            resolver: Box::new(resolver),
129            loader: Box::new(loader),
130        })))
131    }
132
133    pub(crate) fn set_to_runtime(&self, rt: *mut qjs::JSRuntime) {
134        unsafe {
135            qjs::JS_SetModuleLoaderFunc2(
136                rt,
137                None,
138                Some(Self::load_raw),
139                None, // No attribute validation
140                self.0 as _,
141            );
142            qjs::JS_SetModuleNormalizeFunc2(rt, Some(Self::normalize_raw));
143        }
144    }
145
146    #[inline]
147    fn normalize<'js>(
148        opaque: &mut LoaderOpaque,
149        ctx: &Ctx<'js>,
150        base: &CStr,
151        name: &CStr,
152        attributes: qjs::JSValue,
153    ) -> Result<*mut qjs::c_char> {
154        let base = base.to_str()?;
155        let name = name.to_str()?;
156
157        // Convert JSValue to Option<ImportAttributes<'js>>
158        let attrs = {
159            let val = unsafe { Value::from_js_value_const(ctx.clone(), attributes) };
160            if val.is_undefined() || val.is_null() {
161                None
162            } else {
163                Some(ImportAttributes(Object(val)))
164            }
165        };
166
167        let name = opaque.resolver.resolve(ctx, base, name, attrs)?;
168
169        // We should transfer ownership of this string to QuickJS
170        Ok(unsafe { qjs::js_strndup(ctx.as_ptr(), name.as_ptr() as _, name.len() as _) })
171    }
172
173    unsafe extern "C" fn normalize_raw(
174        ctx: *mut qjs::JSContext,
175        base: *const qjs::c_char,
176        name: *const qjs::c_char,
177        attributes: qjs::JSValue,
178        opaque: *mut qjs::c_void,
179    ) -> *mut qjs::c_char {
180        let ctx = Ctx::from_ptr(ctx);
181        let base = CStr::from_ptr(base);
182        let name = CStr::from_ptr(name);
183        let loader = &mut *(opaque as *mut LoaderOpaque);
184
185        Self::normalize(loader, &ctx, base, name, attributes).unwrap_or_else(|error| {
186            error.throw(&ctx);
187            ptr::null_mut()
188        })
189    }
190
191    #[inline]
192    unsafe fn load<'js>(
193        opaque: &mut LoaderOpaque,
194        ctx: &Ctx<'js>,
195        name: &CStr,
196        attributes: qjs::JSValue,
197    ) -> Result<*mut qjs::JSModuleDef> {
198        let name = name.to_str()?;
199
200        // Convert JSValue to Option<ImportAttributes<'js>>
201        let attrs = {
202            let val = Value::from_js_value_const(ctx.clone(), attributes);
203            if val.is_undefined() || val.is_null() {
204                None
205            } else {
206                Some(ImportAttributes(Object(val)))
207            }
208        };
209
210        Ok(opaque.loader.load(ctx, name, attrs)?.as_ptr())
211    }
212
213    unsafe extern "C" fn load_raw(
214        ctx: *mut qjs::JSContext,
215        name: *const qjs::c_char,
216        opaque: *mut qjs::c_void,
217        attributes: qjs::JSValue,
218    ) -> *mut qjs::JSModuleDef {
219        let ctx = Ctx::from_ptr(ctx);
220        let name = CStr::from_ptr(name);
221        let loader = &mut *(opaque as *mut LoaderOpaque);
222
223        Self::load(loader, &ctx, name, attributes).unwrap_or_else(|error| {
224            error.throw(&ctx);
225            ptr::null_mut()
226        })
227    }
228}
229
230macro_rules! loader_impls {
231    ($($t:ident)*) => {
232        loader_impls!(@sub @mark $($t)*);
233    };
234    (@sub $($lead:ident)* @mark $head:ident $($rest:ident)*) => {
235        loader_impls!(@impl $($lead)*);
236        loader_impls!(@sub $($lead)* $head @mark $($rest)*);
237    };
238    (@sub $($lead:ident)* @mark) => {
239        loader_impls!(@impl $($lead)*);
240    };
241    (@impl $($t:ident)*) => {
242            impl<$($t,)*> Resolver for ($($t,)*)
243            where
244                $($t: Resolver,)*
245            {
246                #[allow(non_snake_case)]
247                #[allow(unused_mut)]
248                fn resolve<'js>(
249                    &mut self,
250                    _ctx: &Ctx<'js>,
251                    base: &str,
252                    name: &str,
253                    _attributes: Option<ImportAttributes<'js>>,
254                ) -> Result<String> {
255                    let mut messages = alloc::vec::Vec::<alloc::string::String>::new();
256                    let ($($t,)*) = self;
257                    $(
258                        match $t.resolve(_ctx, base, name, _attributes.clone()) {
259                            // Still could try the next resolver
260                            Err($crate::Error::Resolving { message, .. }) => {
261                                message.map(|message| messages.push(message));
262                            },
263                            result => return result,
264                        }
265                    )*
266                    // Unable to resolve module name
267                    Err(if messages.is_empty() {
268                        $crate::Error::new_resolving(base, name)
269                    } else {
270                        $crate::Error::new_resolving_message(base, name, messages.join("\n"))
271                    })
272                }
273            }
274
275            impl< $($t,)*> $crate::loader::Loader for ($($t,)*)
276            where
277                $($t: $crate::loader::Loader,)*
278            {
279                #[allow(non_snake_case)]
280                #[allow(unused_mut)]
281                fn load<'js>(
282                    &mut self,
283                    _ctx: &Ctx<'js>,
284                    name: &str,
285                    _attributes: Option<$crate::loader::ImportAttributes<'js>>,
286                ) -> Result<Module<'js, Declared>> {
287                    let mut messages = alloc::vec::Vec::<alloc::string::String>::new();
288                    let ($($t,)*) = self;
289                    $(
290                        match $t.load(_ctx, name, _attributes.clone()) {
291                            // Still could try the next loader
292                            Err($crate::Error::Loading { message, .. }) => {
293                                message.map(|message| messages.push(message));
294                            },
295                            result => return result,
296                        }
297                    )*
298                    // Unable to load module
299                    Err(if messages.is_empty() {
300                        $crate::Error::new_loading(name)
301                    } else {
302                        $crate::Error::new_loading_message(name, messages.join("\n"))
303                    })
304                }
305            }
306    };
307}
308loader_impls!(A B C D E F G H);
309
310#[cfg(test)]
311mod test {
312    use std::sync::{Arc, Mutex};
313
314    use crate::{CatchResultExt, Context, Ctx, Error, Module, Result, Runtime};
315
316    use super::{ImportAttributes, Loader, Resolver};
317
318    struct TestResolver;
319
320    impl Resolver for TestResolver {
321        fn resolve<'js>(
322            &mut self,
323            _ctx: &Ctx<'js>,
324            base: &str,
325            name: &str,
326            _attributes: Option<ImportAttributes<'js>>,
327        ) -> Result<String> {
328            if base == "loader" && name == "test" {
329                Ok(name.into())
330            } else {
331                Err(Error::new_resolving_message(
332                    base,
333                    name,
334                    "unable to resolve",
335                ))
336            }
337        }
338    }
339
340    struct TestLoader;
341
342    impl Loader for TestLoader {
343        fn load<'js>(
344            &mut self,
345            ctx: &Ctx<'js>,
346            name: &str,
347            _attributes: Option<super::ImportAttributes<'js>>,
348        ) -> Result<Module<'js>> {
349            if name == "test" {
350                Module::declare(
351                    ctx.clone(),
352                    "test",
353                    r#"
354                      export const n = 123;
355                      export const s = "abc";
356                    "#,
357                )
358            } else {
359                Err(Error::new_loading_message(name, "unable to load"))
360            }
361        }
362    }
363
364    #[test]
365    fn custom_loader() {
366        let rt = Runtime::new().unwrap();
367        let ctx = Context::full(&rt).unwrap();
368        rt.set_loader(TestResolver, TestLoader);
369        ctx.with(|ctx| {
370            Module::evaluate(
371                ctx,
372                "loader",
373                r#"
374                      import { n, s } from "test";
375                      export default [n, s];
376                    "#,
377            )
378            .unwrap()
379            .finish::<()>()
380            .unwrap();
381        })
382    }
383
384    #[test]
385    #[should_panic(expected = "Error resolving module")]
386    fn resolving_error() {
387        let rt = Runtime::new().unwrap();
388        let ctx = Context::full(&rt).unwrap();
389        rt.set_loader(TestResolver, TestLoader);
390        ctx.with(|ctx| {
391            Module::evaluate(
392                ctx.clone(),
393                "loader",
394                r#"
395                      import { n, s } from "test_";
396                    "#,
397            )
398            .catch(&ctx)
399            .unwrap()
400            .finish::<()>()
401            .catch(&ctx)
402            .expect("Unable to resolve");
403        })
404    }
405
406    struct AttributeCapturingLoader {
407        captured_type: Arc<Mutex<Option<String>>>,
408    }
409
410    impl Loader for AttributeCapturingLoader {
411        fn load<'js>(
412            &mut self,
413            ctx: &Ctx<'js>,
414            name: &str,
415            attributes: Option<super::ImportAttributes<'js>>,
416        ) -> Result<Module<'js>> {
417            if let Some(attrs) = &attributes {
418                if let Ok(type_val) = attrs.get("type") {
419                    *self.captured_type.lock().unwrap() = type_val;
420                }
421            }
422
423            if name == "data" {
424                Module::declare(ctx.clone(), name, "export default { value: 42 };")
425            } else {
426                Err(Error::new_loading_message(name, "module not found"))
427            }
428        }
429    }
430
431    struct IdentityResolver;
432
433    impl Resolver for IdentityResolver {
434        fn resolve<'js>(
435            &mut self,
436            _ctx: &Ctx<'js>,
437            _base: &str,
438            name: &str,
439            _attributes: Option<ImportAttributes<'js>>,
440        ) -> Result<String> {
441            Ok(name.into())
442        }
443    }
444
445    #[test]
446    fn import_attributes_passed_to_loader() {
447        let captured_type = Arc::new(Mutex::new(None));
448        let loader = AttributeCapturingLoader {
449            captured_type: captured_type.clone(),
450        };
451
452        let rt = Runtime::new().unwrap();
453        let ctx = Context::full(&rt).unwrap();
454        rt.set_loader(IdentityResolver, loader);
455
456        ctx.with(|ctx| {
457            Module::evaluate(
458                ctx,
459                "test",
460                r#"
461                    import data from "data" with { type: "json" };
462                    export default data;
463                "#,
464            )
465            .unwrap()
466            .finish::<()>()
467            .unwrap();
468        });
469
470        assert_eq!(*captured_type.lock().unwrap(), Some("json".to_string()));
471    }
472
473    #[test]
474    fn import_attributes_none_when_not_provided() {
475        let captured_type = Arc::new(Mutex::new(Some("initial".to_string())));
476        let loader = AttributeCapturingLoader {
477            captured_type: captured_type.clone(),
478        };
479
480        let rt = Runtime::new().unwrap();
481        let ctx = Context::full(&rt).unwrap();
482        rt.set_loader(IdentityResolver, loader);
483
484        ctx.with(|ctx| {
485            Module::evaluate(
486                ctx,
487                "test",
488                r#"
489                    import data from "data";
490                    export default data;
491                "#,
492            )
493            .unwrap()
494            .finish::<()>()
495            .unwrap();
496        });
497
498        assert_eq!(*captured_type.lock().unwrap(), Some("initial".to_string()));
499    }
500
501    struct TypeAwareLoader;
502
503    impl Loader for TypeAwareLoader {
504        fn load<'js>(
505            &mut self,
506            ctx: &Ctx<'js>,
507            name: &str,
508            attributes: Option<super::ImportAttributes<'js>>,
509        ) -> Result<Module<'js>> {
510            let module_type = if let Some(attrs) = &attributes {
511                attrs.get_type()?
512            } else {
513                None
514            };
515
516            match (name, module_type.as_deref()) {
517                ("config", Some("json")) => {
518                    Module::declare(ctx.clone(), name, r#"export default {"format": "json"};"#)
519                }
520                ("config", Some("text")) => {
521                    Module::declare(ctx.clone(), name, r#"export default "plain text";"#)
522                }
523                ("config", None) => Err(Error::new_loading_message(
524                    name,
525                    "config requires a type attribute",
526                )),
527                _ => Err(Error::new_loading_message(name, "unknown module")),
528            }
529        }
530    }
531
532    #[test]
533    fn import_attributes_json_type() {
534        let rt = Runtime::new().unwrap();
535        let ctx = Context::full(&rt).unwrap();
536        rt.set_loader(IdentityResolver, TypeAwareLoader);
537
538        ctx.with(|ctx| {
539            Module::evaluate(
540                ctx,
541                "test_json",
542                r#"
543                    import config from "config" with { type: "json" };
544                    if (config.format !== "json") {
545                        throw new Error("Expected format to be json");
546                    }
547                "#,
548            )
549            .unwrap()
550            .finish::<()>()
551            .unwrap();
552        });
553    }
554
555    #[test]
556    fn import_attributes_text_type() {
557        let rt = Runtime::new().unwrap();
558        let ctx = Context::full(&rt).unwrap();
559        rt.set_loader(IdentityResolver, TypeAwareLoader);
560
561        ctx.with(|ctx| {
562            Module::evaluate(
563                ctx,
564                "test_text",
565                r#"
566                    import config from "config" with { type: "text" };
567                    if (config !== "plain text") {
568                        throw new Error("Expected plain text");
569                    }
570                "#,
571            )
572            .unwrap()
573            .finish::<()>()
574            .unwrap();
575        });
576    }
577
578    #[test]
579    #[should_panic(expected = "Error loading module")]
580    fn import_attributes_missing_required() {
581        let rt = Runtime::new().unwrap();
582        let ctx = Context::full(&rt).unwrap();
583        rt.set_loader(IdentityResolver, TypeAwareLoader);
584
585        ctx.with(|ctx| {
586            Module::evaluate(
587                ctx.clone(),
588                "test_missing",
589                r#"
590                    import config from "config";
591                "#,
592            )
593            .catch(&ctx)
594            .unwrap()
595            .finish::<()>()
596            .catch(&ctx)
597            .expect("missing type attribute");
598        });
599    }
600
601    struct KeysCapturingLoader {
602        captured_keys: Arc<Mutex<Vec<String>>>,
603    }
604
605    impl Loader for KeysCapturingLoader {
606        fn load<'js>(
607            &mut self,
608            ctx: &Ctx<'js>,
609            name: &str,
610            attributes: Option<super::ImportAttributes<'js>>,
611        ) -> Result<Module<'js>> {
612            if let Some(attrs) = &attributes {
613                let keys: Vec<String> = attrs.keys().collect::<Result<Vec<_>>>().unwrap();
614                *self.captured_keys.lock().unwrap() = keys;
615            }
616
617            if name == "data" {
618                Module::declare(ctx.clone(), name, "export default { value: 42 };")
619            } else {
620                Err(Error::new_loading_message(name, "module not found"))
621            }
622        }
623    }
624
625    #[test]
626    fn import_attributes_keys() {
627        let captured_keys = Arc::new(Mutex::new(Vec::new()));
628        let loader = KeysCapturingLoader {
629            captured_keys: captured_keys.clone(),
630        };
631
632        let rt = Runtime::new().unwrap();
633        let ctx = Context::full(&rt).unwrap();
634        rt.set_loader(IdentityResolver, loader);
635
636        ctx.with(|ctx| {
637            Module::evaluate(
638                ctx,
639                "test",
640                r#"
641                    import data from "data" with { type: "json", encoding: "utf-8" };
642                    export default data;
643                "#,
644            )
645            .unwrap()
646            .finish::<()>()
647            .unwrap();
648        });
649
650        let keys = captured_keys.lock().unwrap();
651        assert_eq!(keys.len(), 2);
652        assert!(keys.contains(&"type".to_string()));
653        assert!(keys.contains(&"encoding".to_string()));
654    }
655}