Skip to main content

boa_engine/module/loader/
embedded.rs

1//! Embedded module loader. Creates a `ModuleLoader` instance that contains
2//! files embedded in the binary at build time.
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::path::Path;
7use std::rc::Rc;
8
9use boa_engine::module::{ModuleLoader, Referrer};
10use boa_engine::{Context, JsNativeError, JsResult, JsString, Module, Source};
11
12/// Create a module loader that embeds files from the filesystem at build
13/// time. This is useful for bundling assets with the binary.
14///
15/// By default will error if the total file size exceeds 1MB. This can be
16/// changed by specifying the `max_size` parameter.
17///
18/// The embedded module will only contain files that have the `.js`, `.mjs`,
19/// or `.cjs` extension.
20#[macro_export]
21macro_rules! embed_module {
22    ($($x: expr),*) => {
23        $crate::module::embedded::EmbeddedModuleLoader::from_iter(
24            $crate::__embed_module_inner!($($x),*),
25        )
26    };
27}
28
29#[derive(Debug, Clone)]
30enum EmbeddedModuleEntry {
31    Source(CompressType, JsString, &'static [u8]),
32    Module(Module),
33}
34
35impl EmbeddedModuleEntry {
36    fn from_source(compress_type: CompressType, path: JsString, source: &'static [u8]) -> Self {
37        Self::Source(compress_type, path, source)
38    }
39
40    fn cache(&mut self, context: &mut Context) -> JsResult<&Module> {
41        if let Self::Source(compress, path, source) = self {
42            let mut bytes: &[u8] = match compress {
43                CompressType::None => source,
44
45                #[cfg(feature = "embedded_lz4")]
46                CompressType::Lz4 => &lz4_flex::decompress_size_prepended(source)
47                    .map_err(|e| boa_engine::js_error!("Could not decompress module: {}", e))?,
48            };
49            let path = path.to_std_string_escaped();
50            let source = Source::from_reader(&mut bytes, Some(Path::new(&path)));
51            match Module::parse(source, None, context) {
52                Ok(module) => {
53                    *self = Self::Module(module);
54                }
55                Err(err) => {
56                    return Err(err);
57                }
58            }
59        }
60
61        match self {
62            Self::Module(module) => Ok(module),
63            EmbeddedModuleEntry::Source(_, _, _) => unreachable!(),
64        }
65    }
66
67    fn as_module(&self) -> Option<&Module> {
68        match self {
69            Self::Module(module) => Some(module),
70            Self::Source(_, _, _) => None,
71        }
72    }
73}
74
75/// The type of compression used, if any.
76#[derive(Debug, Copy, Clone)]
77pub enum CompressType {
78    /// No compression used.
79    None,
80
81    #[cfg(feature = "embedded_lz4")]
82    /// LZ4 compression.
83    Lz4,
84}
85
86impl TryFrom<&str> for CompressType {
87    type Error = &'static str;
88
89    fn try_from(value: &str) -> Result<Self, Self::Error> {
90        match value {
91            "none" => Ok(Self::None),
92            #[cfg(feature = "embedded_lz4")]
93            "lz4" => Ok(Self::Lz4),
94            _ => Err("Invalid compression type"),
95        }
96    }
97}
98
99/// The resulting type of creating an embedded module loader.
100#[derive(Debug, Default, Clone)]
101#[allow(clippy::module_name_repetitions)]
102pub struct EmbeddedModuleLoader {
103    map: HashMap<JsString, RefCell<EmbeddedModuleEntry>>,
104}
105
106impl EmbeddedModuleLoader {
107    /// Get a module if it has been parsed and created. If the module is not found or
108    /// was not loaded, this will return `None`.
109    #[must_use]
110    pub fn get_module(&self, name: &JsString) -> Option<Module> {
111        self.map
112            .get(name)
113            .and_then(|module| module.borrow().as_module().cloned())
114    }
115}
116
117impl FromIterator<(&'static str, &'static str, &'static [u8])> for EmbeddedModuleLoader {
118    fn from_iter<T: IntoIterator<Item = (&'static str, &'static str, &'static [u8])>>(
119        iter: T,
120    ) -> Self {
121        Self {
122            map: iter
123                .into_iter()
124                .map(|(compress_type, path, source)| {
125                    let p = JsString::from(path);
126                    (
127                        p.clone(),
128                        RefCell::new(EmbeddedModuleEntry::from_source(
129                            compress_type.try_into().expect("Invalid compress type"),
130                            p,
131                            source,
132                        )),
133                    )
134                })
135                .collect(),
136        }
137    }
138}
139
140impl ModuleLoader for EmbeddedModuleLoader {
141    fn load_imported_module(
142        self: Rc<Self>,
143        referrer: Referrer,
144        request: boa_engine::module::ModuleRequest,
145        context: &RefCell<&mut Context>,
146    ) -> impl Future<Output = JsResult<Module>> {
147        let result = (|| {
148            let specifier_path = boa_engine::module::resolve_module_specifier(
149                None,
150                request.specifier(),
151                referrer.path(),
152                &mut context.borrow_mut(),
153            )
154            .map_err(|e| {
155                JsNativeError::typ()
156                    .with_message(format!(
157                        "could not resolve module specifier `{}`",
158                        request.specifier().display_escaped()
159                    ))
160                    .with_cause(e)
161            })?;
162
163            let module = self
164                .map
165                .get(&JsString::from(specifier_path.to_string_lossy().as_ref()))
166                .ok_or_else(|| {
167                    JsNativeError::typ().with_message(format!(
168                        "could not find module `{}`",
169                        request.specifier().display_escaped()
170                    ))
171                })?;
172
173            let mut embedded = module.borrow_mut();
174            embedded.cache(&mut context.borrow_mut()).cloned()
175        })();
176
177        async { result }
178    }
179}