1use 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")]
35pub type Bundle = bundle::Bundle<bundle::PhfBundleData<&'static [u8]>>;
37
38#[cfg(not(feature = "phf"))]
39pub type Bundle = bundle::Bundle<bundle::ScaBundleData<&'static [u8]>>;
41
42#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
44pub trait Resolver {
45 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#[derive(Clone, Debug)]
75pub struct ImportAttributes<'js>(Object<'js>);
76
77impl<'js> ImportAttributes<'js> {
78 pub fn get(&self, key: &str) -> Result<Option<String>> {
80 self.0.get(key)
81 }
82
83 pub fn get_type(&self) -> Result<Option<String>> {
85 self.get("type")
86 }
87
88 pub fn keys(&self) -> ObjectKeysIter<'js, String> {
90 self.0.keys()
91 }
92}
93
94#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "loader")))]
96pub trait Loader {
97 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, 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 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 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 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 Err($crate::Error::Resolving { message, .. }) => {
261 message.map(|message| messages.push(message));
262 },
263 result => return result,
264 }
265 )*
266 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 Err($crate::Error::Loading { message, .. }) => {
293 message.map(|message| messages.push(message));
294 },
295 result => return result,
296 }
297 )*
298 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}