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 JsEngineBuilder {
165    enable_fetch: bool,
166    enable_console: bool,
167    enable_commonjs: bool,
168}
169
170impl Default for JsEngineBuilder {
171    fn default() -> Self {
172        Self {
173            enable_fetch: true,
174            enable_console: true,
175            enable_commonjs: true,
176        }
177    }
178}
179
180impl JsEngineBuilder {
181    pub fn enable_fetch(mut self, enable: bool) -> Self {
182        self.enable_fetch = enable;
183        self
184    }
185
186    pub fn enable_console(mut self, enable: bool) -> Self {
187        self.enable_console = enable;
188        self
189    }
190
191    pub fn enable_commonjs(mut self, enable: bool) -> Self {
192        self.enable_commonjs = enable;
193        self
194    }
195
196    pub fn build(self) -> JSResult<JsEngine> {
197        JsEngine::create(self)
198    }
199
200    pub async fn build_async(self) -> JSResult<AsyncJsEngine> {
201        AsyncJsEngine::create(self).await
202    }
203}
204
205pub struct JsEngine {
206    loader: Rc<SfoModuleLoader>,
207    context: Context,
208    module: Option<Module>,
209}
210
211unsafe impl Send for JsEngine {}
212unsafe impl Sync for JsEngine {}
213
214impl JsEngine {
215    pub fn new() -> JSResult<Self> {
216        Self::create(JsEngineBuilder::default())
217    }
218
219    pub fn builder() -> JsEngineBuilder {
220        JsEngineBuilder::default()
221    }
222
223    fn create(builder: JsEngineBuilder) -> JSResult<Self> {
224        let loader = Rc::new(SfoModuleLoader::new(vec![])?);
225        let mut context = Context::builder()
226            .module_loader(loader.clone())
227            .can_block(true)
228            .build()
229            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
230
231        if builder.enable_fetch && builder.enable_console {
232            boa_runtime::register((
233                                      boa_runtime::extensions::ConsoleExtension::default(),
234                                      boa_runtime::extensions::FetchExtension(
235                                          boa_runtime::fetch::BlockingReqwestFetcher::default()
236                                      ),
237                                  ),
238                                  None,
239                                  &mut context,
240            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
241        } else if builder.enable_console {
242            boa_runtime::register(
243                boa_runtime::extensions::ConsoleExtension::default(),
244                None,
245                &mut context,
246            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
247        } else if builder.enable_fetch {
248            boa_runtime::register(
249                boa_runtime::extensions::FetchExtension(
250                    boa_runtime::fetch::BlockingReqwestFetcher::default()
251                ),
252                None,
253                &mut context,
254            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
255        }
256
257        if builder.enable_commonjs {
258            context.register_global_callable("require".into(), 0, NativeFunction::from_fn_ptr(require))
259                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
260
261            // Adding custom object that mimics 'module.exports'
262            let moduleobj = JsObject::default(context.intrinsics());
263            moduleobj.set(js_string!("exports"), js_string!(" "), false, &mut context)
264                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
265
266            context.register_global_property(
267                js_string!("module"),
268                JsValue::from(moduleobj),
269                Attribute::default(),
270            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
271        }
272
273        Ok(JsEngine {
274            loader,
275            context,
276            module: None,
277        })
278    }
279
280    pub fn context(&mut self) -> &mut Context {
281        &mut self.context
282    }
283
284    pub fn add_module_path(&mut self, module_path: &Path) -> JSResult<()> {
285        self.loader.add_module_path(module_path)
286    }
287
288    pub fn register_global_property<K, V>(
289        &mut self,
290        key: K,
291        value: V,
292        attribute: Attribute,
293    ) -> JSResult<()>
294    where
295        K: Into<PropertyKey>,
296        V: Into<JsValue>, {
297        self.context.register_global_property(key, value, attribute)
298            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
299        Ok(())
300    }
301
302    pub fn register_global_callable(
303        &mut self,
304        name: String,
305        length: usize,
306        body: NativeFunction,
307    ) -> JSResult<()> {
308        self.context.register_global_callable(JsString::from(name), length, body)
309            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
310        Ok(())
311    }
312
313    pub fn register_global_builtin_callable(
314        &mut self,
315        name: String,
316        length: usize,
317        body: NativeFunction,
318    ) -> JSResult<()> {
319        self.context.register_global_builtin_callable(JsString::from(name), length, body)
320            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
321        Ok(())
322    }
323
324    pub fn register_global_class<C: Class>(&mut self) -> JSResult<()> {
325        self.context.register_global_class::<C>()
326            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
327        Ok(())
328    }
329
330    pub fn eval_file(&mut self, path: &Path) -> JSResult<()> {
331        let path = path.canonicalize()
332            .map_err(into_js_err!(JSErrorCode::InvalidPath, "Invalid path {:?}", path))?;
333        if let Some(parent) = path.parent() {
334            self.add_module_path(parent)?;
335        } else {
336            self.add_module_path(std::env::current_dir()
337                .map_err(into_js_err!(JSErrorCode::InvalidPath))?.as_path())?;
338        }
339        let source = Source::from_filepath(path.as_path())
340            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
341        self.eval(source)
342    }
343
344    pub fn eval_file_with_args(&mut self, path: &Path, args: &str) -> JSResult<()> {
345        if let Some(params) = shlex::split(args) {
346            let process_obj = JsObject::default(self.context.intrinsics());
347            let params: Vec<_> = params.iter().map(|param| {
348                JsValue::from(JsString::from(param.as_str()))
349            }).collect();
350            let params = JsArray::from_iter(params.into_iter(), &mut self.context);
351            process_obj.set(js_string!("argv"), params, false, &mut self.context)
352                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
353            self.context.register_global_property(
354                js_string!("process"),
355                JsValue::from(process_obj),
356                Attribute::default(),
357            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
358        }
359        self.eval_file(path)
360    }
361
362    pub fn eval_string(&mut self, code: &str) -> JSResult<()> {
363        let source = Source::from_bytes(code.as_bytes());
364        self.eval(source)
365    }
366
367    pub fn eval_string_with_args(&mut self, code: &str, args: &str) -> JSResult<()> {
368        if let Some(params) = shlex::split(args) {
369            let process_obj = JsObject::default(self.context.intrinsics());
370            let params: Vec<_> = params.iter().map(|param| {
371                JsValue::from(JsString::from(param.as_str()))
372            }).collect();
373            let params = JsArray::from_iter(params.into_iter(), &mut self.context);
374            process_obj.set(js_string!("argv"), params, false, &mut self.context)
375                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
376            self.context.register_global_property(
377                js_string!("process"),
378                JsValue::from(process_obj),
379                Attribute::default(),
380            ).map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
381        }
382        self.eval_string(code)
383    }
384
385    fn eval<'path, R: ReadChar>(&mut self, source: Source<'path, R>) -> JSResult<()> {
386        if self.module.is_some() {
387            return Err(js_err!(JSErrorCode::JsFailed, "Already loaded a module"));
388        }
389
390        let module = Module::parse(source, None, &mut self.context)
391            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
392
393        let promise_result = module.load_link_evaluate(&mut self.context);
394
395        let _ = promise_result.await_blocking(&mut self.context)
396            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
397
398        self.module = Some(module);
399        Ok(())
400    }
401
402    pub fn call(&mut self, name: &str, args: Vec<JsValue>) -> JSResult<JsValue> {
403        if self.module.is_none() {
404            return Err(js_err!(JSErrorCode::JsFailed, "module didn't execute!"));
405        }
406
407        let fun = self.module.as_mut().unwrap().get_value(JsString::from(name), &mut self.context)
408            .map_err(|e| js_err!(JSErrorCode::JsFailed, "can't find {name} failed: {}", e))?;
409
410        if let Some(fun) = fun.as_callable() {
411            let result = fun.call(&JsValue::null(), args.as_slice(), &mut self.context)
412                .map_err(|e| js_err!(JSErrorCode::JsFailed, "call {name} failed: {}", e))?;
413            Ok(result)
414        } else {
415            Err(js_err!(JSErrorCode::JsFailed, "can't call {name}"))
416        }
417    }
418}
419
420pub struct AsyncJsEngine {
421    inner: Arc<Mutex<JsEngine>>,
422}
423
424impl AsyncJsEngine {
425    async fn create(builder: JsEngineBuilder) -> JSResult<AsyncJsEngine> {
426        let inner = tokio::task::spawn_blocking(|| JsEngine::create(builder))
427            .await
428            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))??;
429        Ok(AsyncJsEngine {
430            inner: Arc::new(Mutex::new(inner)),
431        })
432    }
433
434    pub fn add_module_path(&self, module_path: &Path) -> JSResult<()> {
435        let mut inner = self.inner.lock().unwrap();
436        inner.add_module_path(module_path)
437    }
438
439    pub fn register_global_property<K, V>(
440        &self,
441        key: K,
442        value: V,
443        attribute: Attribute,
444    ) -> JSResult<()>
445    where
446        K: Into<PropertyKey>,
447        V: Into<JsValue>, {
448        self.inner.lock().unwrap().register_global_property(key, value, attribute)
449    }
450
451    pub fn register_global_callable(
452        &self,
453        name: impl Into<String>,
454        length: usize,
455        body: NativeFunction,
456    ) -> JSResult<()> {
457        self.inner.lock().unwrap().register_global_callable(name.into(), length, body)
458    }
459
460    pub fn register_global_builtin_callable(
461        &self,
462        name: String,
463        length: usize,
464        body: NativeFunction,
465    ) -> JSResult<()> {
466        self.inner.lock().unwrap().register_global_builtin_callable(name, length, body)
467    }
468
469    pub fn register_global_class<C: Class>(&self) -> JSResult<()> {
470        self.inner.lock().unwrap().register_global_class::<C>()
471    }
472
473    pub async fn eval_string(&self, code: impl Into<String>) -> JSResult<()> {
474        let inner = self.inner.clone();
475        let code = code.into();
476        tokio::task::spawn_blocking(move || {
477            let mut inner = inner.lock().unwrap();
478            inner.eval_string(code.as_str())
479        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
480    }
481
482    pub async fn eval_string_with_args(&self, code: impl Into<String>, args: impl Into<String>) -> JSResult<()> {
483        let inner = self.inner.clone();
484        let code = code.into();
485        let params = args.into();
486        tokio::task::spawn_blocking(move || {
487            let mut inner = inner.lock().unwrap();
488            inner.eval_string_with_args(code.as_str(), params.as_str())
489        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
490    }
491
492    pub async fn eval_file(&self, path: impl AsRef<Path>) -> JSResult<()> {
493        let inner = self.inner.clone();
494        let path = path.as_ref().to_path_buf();
495        tokio::task::spawn_blocking(move || {
496            let mut inner = inner.lock().unwrap();
497            inner.eval_file(path.as_path())
498        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
499    }
500
501    pub async fn eval_file_with_args(&self, path: impl AsRef<Path>, args: impl Into<String>) -> JSResult<()> {
502        let inner = self.inner.clone();
503        let path = path.as_ref().to_path_buf();
504        let params = args.into();
505        tokio::task::spawn_blocking(move || {
506            let mut inner = inner.lock().unwrap();
507            inner.eval_file_with_args(path.as_path(), params.as_str())
508        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
509    }
510
511    pub async fn call(&self, name: impl Into<String>, args: Vec<serde_json::Value>) -> JSResult<Option<serde_json::Value>> {
512        let inner = self.inner.clone();
513        let name = name.into();
514        tokio::task::spawn_blocking(move || {
515            let mut inner = inner.lock().unwrap();
516            let mut new_args = Vec::with_capacity(args.len());
517            for v in args.iter() {
518                new_args.push(JsValue::from_json(v, &mut inner.context)
519                    .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?);
520            }
521            let result = inner.call(name.as_str(), new_args)?;
522            let result = result.to_json(&mut inner.context)
523                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
524            Ok(result)
525        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
526    }
527}
528
529fn require(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
530    let arg = args.get_or_undefined(0);
531
532    // BUG: Dev branch seems to be passing string arguments along with quotes
533    let libfile = arg.to_string(ctx)?.to_std_string_escaped();
534    let module_loader = ctx.downcast_module_loader::<SfoModuleLoader>().unwrap();
535    let libfile = module_loader.commonjs_resolve_module(libfile.as_str())?;
536
537    if let Some((_, module_obj)) = module_loader.get_commonjs(libfile.as_path()) {
538        let exports = module_obj.as_object().unwrap().get(js_string!("exports"), ctx)?;
539        return Ok(exports)
540    }
541
542    let buffer = read_to_string(libfile.clone())
543        .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
544
545    let wrapper_code = format!(
546        r#"export function cjs_module(exports, requireInner, module, __filename, __dirname) {{ {}
547        }}"#,
548        buffer
549    );
550
551    let module = Module::parse(Source::from_reader(wrapper_code.as_bytes(), Some(libfile.as_path())), None, ctx)?;
552    let promise_result = module.load_link_evaluate(ctx);
553    promise_result.await_blocking(ctx)?;
554
555    let module_obj = JsObject::default(ctx.intrinsics());
556    let exports_obj = JsObject::default(ctx.intrinsics());
557    module_obj.set(js_string!("exports"), exports_obj.clone(), false, ctx)?;
558    module_loader.insert_commonjs(libfile.clone(), module.clone(), JsValue::from(module_obj.clone()));
559
560    let require = NativeFunction::from_fn_ptr(require).to_js_function(ctx.realm());
561    let filename = libfile.to_string_lossy().to_string();
562    let dirname = libfile.parent().unwrap().to_string_lossy().to_string();
563
564    let commonjs_module = module.get_value(JsString::from("cjs_module"), ctx)?;
565    if let Some(args) = commonjs_module.as_callable() {
566        let result = args.call(
567            &JsValue::null(),
568            &[
569                JsValue::from(exports_obj.clone()),
570                JsValue::from(require),
571                JsValue::from(module_obj.clone()),
572                JsValue::from(JsString::from(filename)),
573                JsValue::from(JsString::from(dirname)),
574            ],
575            ctx
576        );
577        if result.is_err() {
578            let err = result.as_ref().err().unwrap();
579            log::error!("{}", err);
580            return result;
581        }
582        let exports = module_obj.get(js_string!("exports"), ctx)?;
583        Ok(exports)
584    } else {
585        unreachable!()
586    }
587}