sfo_js/
js_engine.rs

1use std::cell::RefCell;
2use std::fs::read_to_string;
3use std::path::{Path, PathBuf};
4use std::rc::Rc;
5use std::sync::{Arc, Mutex};
6use boa_engine::{js_string, Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsValue, Module, NativeFunction, Source};
7use boa_engine::class::Class;
8use boa_engine::module::{resolve_module_specifier, ModuleLoader, Referrer};
9use boa_engine::object::builtins::JsArray;
10use boa_engine::parser::source::ReadChar;
11use boa_engine::property::{Attribute, PropertyKey};
12use rustc_hash::FxHashMap;
13use crate::errors::{into_js_err, js_err, JSErrorCode, JSResult};
14use crate::gc::GcRefCell;
15use crate::JsString;
16
17struct SfoModuleLoader {
18    roots: Mutex<Vec<PathBuf>>,
19    module_map: GcRefCell<FxHashMap<PathBuf, Module>>,
20    commonjs_module_map: GcRefCell<FxHashMap<PathBuf, (Module, JsValue)>>,
21}
22
23impl SfoModuleLoader {
24    pub fn new(roots: Vec<PathBuf>) -> JSResult<Self> {
25        if !roots.is_empty() {
26            if cfg!(target_family = "wasm") {
27                return Err(js_err!(JSErrorCode::JsFailed, "cannot resolve a relative path in WASM targets"));
28            }
29        }
30        Ok(Self {
31            roots: Mutex::new(vec![]),
32            module_map: GcRefCell::new(FxHashMap::default()),
33            commonjs_module_map: GcRefCell::new(FxHashMap::default()),
34        })
35    }
36
37    #[inline]
38    pub fn insert(&self, path: PathBuf, module: Module) {
39        self.module_map.borrow_mut().insert(path, module);
40    }
41
42    #[inline]
43    pub fn get(&self, path: &Path) -> Option<Module> {
44        self.module_map.borrow().get(path).cloned()
45    }
46
47    #[inline]
48    pub fn insert_commonjs(&self, path: PathBuf, module: Module, module_obj: JsValue) {
49        self.commonjs_module_map.borrow_mut().insert(path, (module, module_obj));
50    }
51
52    #[inline]
53    pub fn get_commonjs(&self, path: &Path) -> Option<(Module, JsValue)> {
54        self.commonjs_module_map.borrow().get(path).cloned()
55    }
56
57    pub fn add_module_path(&self, module_path: &Path) -> JSResult<()> {
58        self.roots.lock().unwrap().push(module_path.canonicalize()
59            .map_err(into_js_err!(JSErrorCode::InvalidPath, "Invalid path {:?}", module_path))?);
60        Ok(())
61    }
62
63    pub fn commonjs_resolve_module(&self, module_name: &str) -> JsResult<PathBuf> {
64        let roots = {
65            self.roots.lock().unwrap().clone()
66        };
67        for root in roots.iter() {
68            let mut path = root.join(module_name);
69            if path.exists() && path.is_dir() {
70                let index = path.join("index.js");
71                if index.exists() && index.is_file() {
72                    if let Some(parent) = index.parent() {
73                        if parent != root {
74                            let _ = self.add_module_path(parent);
75                        }
76                    }
77                    return Ok(index);
78                }
79            }
80            if path.exists() && path.is_file() {
81                if let Some(parent) = path.parent() {
82                    if parent != root {
83                        let _ = self.add_module_path(parent);
84                    }
85                }
86                return Ok(path);
87            }
88            let mut js_path = path.to_path_buf();
89            js_path.add_extension("js");
90            if js_path.exists() && js_path.is_file() {
91                if let Some(parent) = js_path.parent() {
92                    if parent != root {
93                        let _ = self.add_module_path(parent);
94                    }
95                }
96                return Ok(js_path);
97            }
98            path.add_extension("mjs");
99            if path.exists() && path.is_file() {
100                if let Some(parent) = path.parent() {
101                    if parent != root {
102                        let _ = self.add_module_path(parent);
103                    }
104                }
105                return Ok(path);
106            }
107        }
108        Err(JsError::from_native(JsNativeError::typ().with_message(format!("module {} not found", module_name))))
109    }
110}
111
112impl ModuleLoader for SfoModuleLoader {
113    async fn load_imported_module(self: Rc<Self>, referrer: Referrer, specifier: JsString, context: &RefCell<&mut Context>) -> JsResult<Module> {
114        let roots = {
115            self.roots.lock().unwrap().clone()
116        };
117        for root in roots.iter() {
118            let short_path = specifier.to_std_string_escaped();
119            let path = resolve_module_specifier(
120                Some(root),
121                &specifier,
122                referrer.path(),
123                &mut context.borrow_mut(),
124            )?;
125            if let Some(module) = self.get(&path) {
126                return Ok(module);
127            }
128
129            let mut path = path.to_path_buf();
130            let source = match Source::from_filepath(&path) {
131                Ok(source) => source,
132                Err(_) => {
133                    if !path.ends_with(".js") {
134                        path.add_extension("js");
135                        match Source::from_filepath(&path) {
136                            Ok(source) => source,
137                            Err(_) => continue,
138                        }
139                    } else {
140                        continue;
141                    }
142                }
143            };
144            let module = Module::parse(source, None, &mut context.borrow_mut()).map_err(|err| {
145                JsNativeError::syntax()
146                    .with_message(format!("could not parse module `{short_path}`"))
147                    .with_cause(err)
148            })?;
149            self.insert(path.clone(), module.clone());
150            if let Some(parent) = path.parent() {
151                if parent != root {
152                    let _ = self.add_module_path(parent);
153                }
154            }
155            return Ok(module);
156        }
157
158        Err(
159            JsError::from_native(JsNativeError::typ()
160                .with_message(format!("could not find module `{:?}`", specifier))))
161    }
162}
163
164pub struct JsEngine {
165    loader: Rc<SfoModuleLoader>,
166    context: Context,
167    module: Option<Module>,
168}
169
170unsafe impl Send for JsEngine {}
171unsafe impl Sync for JsEngine {}
172
173impl JsEngine {
174    pub fn new() -> JSResult<Self> {
175        let loader = Rc::new(SfoModuleLoader::new(vec![])?);
176        let mut context = Context::builder()
177            .module_loader(loader.clone())
178            .can_block(true)
179            .build()
180            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
181
182        boa_runtime::register(
183            (
184                boa_runtime::extensions::ConsoleExtension::default(),
185                boa_runtime::extensions::FetchExtension(
186                    boa_runtime::fetch::BlockingReqwestFetcher::default()
187                ),
188            ),
189            None,
190            &mut context,
191        ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
192
193        context.register_global_callable("require".into(), 0, NativeFunction::from_fn_ptr(require))
194            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
195
196        // Adding custom object that mimics 'module.exports'
197        let moduleobj = JsObject::default(context.intrinsics());
198        moduleobj.set(js_string!("exports"), js_string!(" "), false, &mut context)
199            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
200
201        context.register_global_property(
202            js_string!("module"),
203            JsValue::from(moduleobj),
204            Attribute::default(),
205        ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
206
207        Ok(JsEngine {
208            loader,
209            context,
210            module: None,
211        })
212    }
213
214    pub fn context(&mut self) -> &mut Context {
215        &mut self.context
216    }
217
218    pub fn add_module_path(&mut self, module_path: &Path) -> JSResult<()> {
219        self.loader.add_module_path(module_path)
220    }
221
222    pub fn register_global_property<K, V>(
223        &mut self,
224        key: K,
225        value: V,
226        attribute: Attribute,
227    ) -> JSResult<()>
228    where
229        K: Into<PropertyKey>,
230        V: Into<JsValue>, {
231        self.context.register_global_property(key, value, attribute)
232            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
233        Ok(())
234    }
235
236    pub fn register_global_callable(
237        &mut self,
238        name: String,
239        length: usize,
240        body: NativeFunction,
241    ) -> JSResult<()> {
242        self.context.register_global_callable(JsString::from(name), length, body)
243            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
244        Ok(())
245    }
246
247    pub fn register_global_builtin_callable(
248        &mut self,
249        name: String,
250        length: usize,
251        body: NativeFunction,
252    ) -> JSResult<()> {
253        self.context.register_global_builtin_callable(JsString::from(name), length, body)
254            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
255        Ok(())
256    }
257
258    pub fn register_global_class<C: Class>(&mut self) -> JSResult<()> {
259        self.context.register_global_class::<C>()
260            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
261        Ok(())
262    }
263
264    pub fn eval_file(&mut self, path: &Path) -> JSResult<()> {
265        let path = path.canonicalize()
266            .map_err(into_js_err!(JSErrorCode::InvalidPath, "Invalid path {:?}", path))?;
267        if let Some(parent) = path.parent() {
268            self.add_module_path(parent)?;
269        } else {
270            self.add_module_path(std::env::current_dir()
271                .map_err(into_js_err!(JSErrorCode::InvalidPath))?.as_path())?;
272        }
273        let source = Source::from_filepath(path.as_path())
274            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
275        self.eval(source)
276    }
277
278    pub fn eval_file_with_args(&mut self, path: &Path, args: &str) -> JSResult<()> {
279        if let Some(params) = shlex::split(args) {
280            let process_obj = JsObject::default(self.context.intrinsics());
281            let params: Vec<_> = params.iter().map(|param| {
282                JsValue::from(JsString::from(param.as_str()))
283            }).collect();
284            let params = JsArray::from_iter(params.into_iter(), &mut self.context);
285            process_obj.set(js_string!("argv"), params, false, &mut self.context)
286                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
287            self.context.register_global_property(
288                js_string!("process"),
289                JsValue::from(process_obj),
290                Attribute::default(),
291            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
292        }
293        self.eval_file(path)
294    }
295
296    pub fn eval_string(&mut self, code: &str) -> JSResult<()> {
297        let source = Source::from_bytes(code.as_bytes());
298        self.eval(source)
299    }
300
301    pub fn eval_string_with_args(&mut self, code: &str, args: &str) -> JSResult<()> {
302        if let Some(params) = shlex::split(args) {
303            let process_obj = JsObject::default(self.context.intrinsics());
304            let params: Vec<_> = params.iter().map(|param| {
305                JsValue::from(JsString::from(param.as_str()))
306            }).collect();
307            let params = JsArray::from_iter(params.into_iter(), &mut self.context);
308            process_obj.set(js_string!("argv"), params, false, &mut self.context)
309                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
310            self.context.register_global_property(
311                js_string!("process"),
312                JsValue::from(process_obj),
313                Attribute::default(),
314            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
315        }
316        self.eval_string(code)
317    }
318
319    fn eval<'path, R: ReadChar>(&mut self, source: Source<'path, R>) -> JSResult<()> {
320        if self.module.is_some() {
321            return Err(js_err!(JSErrorCode::JsFailed, "Already loaded a module"));
322        }
323
324        let module = Module::parse(source, None, &mut self.context)
325            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
326
327        let promise_result = module.load_link_evaluate(&mut self.context);
328
329        let _ = promise_result.await_blocking(&mut self.context)
330            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
331
332        self.module = Some(module);
333        Ok(())
334    }
335
336    pub fn call(&mut self, name: &str, args: Vec<JsValue>) -> JSResult<JsValue> {
337        if self.module.is_none() {
338            return Err(js_err!(JSErrorCode::JsFailed, "module didn't execute!"));
339        }
340
341        let fun = self.module.as_mut().unwrap().get_value(JsString::from(name), &mut self.context)
342            .map_err(|e| js_err!(JSErrorCode::JsFailed, "can't find {name} failed: {}", e))?;
343
344        if let Some(fun) = fun.as_callable() {
345            let result = fun.call(&JsValue::null(), args.as_slice(), &mut self.context)
346                .map_err(|e| js_err!(JSErrorCode::JsFailed, "call {name} failed: {}", e))?;
347            Ok(result)
348        } else {
349            Err(js_err!(JSErrorCode::JsFailed, "can't call {name}"))
350        }
351    }
352}
353
354pub struct AsyncJsEngine {
355    inner: Arc<Mutex<JsEngine>>,
356}
357
358impl AsyncJsEngine {
359    pub async fn new() -> JSResult<Self> {
360        let inner = tokio::task::spawn_blocking(|| JsEngine::new())
361            .await
362            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))??;
363        Ok(AsyncJsEngine {
364            inner: Arc::new(Mutex::new(inner)),
365        })
366    }
367
368    pub fn add_module_path(&self, module_path: &Path) -> JSResult<()> {
369        let mut inner = self.inner.lock().unwrap();
370        inner.add_module_path(module_path)
371    }
372
373    pub fn register_global_property<K, V>(
374        &self,
375        key: K,
376        value: V,
377        attribute: Attribute,
378    ) -> JSResult<()>
379    where
380        K: Into<PropertyKey>,
381        V: Into<JsValue>, {
382        self.inner.lock().unwrap().register_global_property(key, value, attribute)
383    }
384
385    pub fn register_global_callable(
386        &self,
387        name: impl Into<String>,
388        length: usize,
389        body: NativeFunction,
390    ) -> JSResult<()> {
391        self.inner.lock().unwrap().register_global_callable(name.into(), length, body)
392    }
393
394    pub fn register_global_builtin_callable(
395        &self,
396        name: String,
397        length: usize,
398        body: NativeFunction,
399    ) -> JSResult<()> {
400        self.inner.lock().unwrap().register_global_builtin_callable(name, length, body)
401    }
402
403    pub fn register_global_class<C: Class>(&self) -> JSResult<()> {
404        self.inner.lock().unwrap().register_global_class::<C>()
405    }
406
407    pub async fn eval_string(&self, code: impl Into<String>) -> JSResult<()> {
408        let inner = self.inner.clone();
409        let code = code.into();
410        tokio::task::spawn_blocking(move || {
411            let mut inner = inner.lock().unwrap();
412            inner.eval_string(code.as_str())
413        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
414    }
415
416    pub async fn eval_string_with_args(&self, code: impl Into<String>, args: impl Into<String>) -> JSResult<()> {
417        let inner = self.inner.clone();
418        let code = code.into();
419        let params = args.into();
420        tokio::task::spawn_blocking(move || {
421            let mut inner = inner.lock().unwrap();
422            inner.eval_string_with_args(code.as_str(), params.as_str())
423        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
424    }
425
426    pub async fn eval_file(&self, path: impl AsRef<Path>) -> JSResult<()> {
427        let inner = self.inner.clone();
428        let path = path.as_ref().to_path_buf();
429        tokio::task::spawn_blocking(move || {
430            let mut inner = inner.lock().unwrap();
431            inner.eval_file(path.as_path())
432        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
433    }
434
435    pub async fn eval_file_with_args(&self, path: impl AsRef<Path>, args: impl Into<String>) -> JSResult<()> {
436        let inner = self.inner.clone();
437        let path = path.as_ref().to_path_buf();
438        let params = args.into();
439        tokio::task::spawn_blocking(move || {
440            let mut inner = inner.lock().unwrap();
441            inner.eval_file_with_args(path.as_path(), params.as_str())
442        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
443    }
444
445    pub async fn call(&self, name: impl Into<String>, args: Vec<serde_json::Value>) -> JSResult<Option<serde_json::Value>> {
446        let inner = self.inner.clone();
447        let name = name.into();
448        tokio::task::spawn_blocking(move || {
449            let mut inner = inner.lock().unwrap();
450            let mut new_args = Vec::with_capacity(args.len());
451            for v in args.iter() {
452                new_args.push(JsValue::from_json(v, &mut inner.context)
453                    .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?);
454            }
455            let result = inner.call(name.as_str(), new_args)?;
456            let result = result.to_json(&mut inner.context)
457                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
458            Ok(result)
459        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
460    }
461}
462
463fn require(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
464    let arg = args.get_or_undefined(0);
465
466    // BUG: Dev branch seems to be passing string arguments along with quotes
467    let libfile = arg.to_string(ctx)?.to_std_string_escaped();
468    let module_loader = ctx.downcast_module_loader::<SfoModuleLoader>().unwrap();
469    let libfile = module_loader.commonjs_resolve_module(libfile.as_str())?;
470
471    if let Some((_, module_obj)) = module_loader.get_commonjs(libfile.as_path()) {
472        let exports = module_obj.as_object().unwrap().get(js_string!("exports"), ctx)?;
473        return Ok(exports)
474    }
475
476    let buffer = read_to_string(libfile.clone())
477        .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
478
479    let wrapper_code = format!(
480        r#"export function cjs_module(exports, requireInner, module, __filename, __dirname) {{ {}
481        }}"#,
482        buffer
483    );
484
485    let module = Module::parse(Source::from_reader(wrapper_code.as_bytes(), Some(libfile.as_path())), None, ctx)?;
486    let promise_result = module.load_link_evaluate(ctx);
487    promise_result.await_blocking(ctx)?;
488
489    let module_obj = JsObject::default(ctx.intrinsics());
490    let exports_obj = JsObject::default(ctx.intrinsics());
491    module_obj.set(js_string!("exports"), exports_obj.clone(), false, ctx)?;
492    module_loader.insert_commonjs(libfile.clone(), module.clone(), JsValue::from(module_obj.clone()));
493
494    let require = NativeFunction::from_fn_ptr(require).to_js_function(ctx.realm());
495    let filename = libfile.to_string_lossy().to_string();
496    let dirname = libfile.parent().unwrap().to_string_lossy().to_string();
497
498    let commonjs_module = module.get_value(JsString::from("cjs_module"), ctx)?;
499    if let Some(args) = commonjs_module.as_callable() {
500        let result = args.call(
501            &JsValue::null(),
502            &[
503                JsValue::from(exports_obj.clone()),
504                JsValue::from(require),
505                JsValue::from(module_obj.clone()),
506                JsValue::from(JsString::from(filename)),
507                JsValue::from(JsString::from(dirname)),
508            ],
509            ctx
510        );
511        if result.is_err() {
512            let err = result.as_ref().err().unwrap();
513            log::error!("{}", err);
514            return result;
515        }
516        let exports = module_obj.get(js_string!("exports"), ctx)?;
517        Ok(exports)
518    } else {
519        unreachable!()
520    }
521}