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::builtins::promise::PromiseState;
8use boa_engine::class::Class;
9use boa_engine::module::{resolve_module_specifier, ModuleLoader, Referrer};
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 add_module_path(&mut self, module_path: &Path) -> JSResult<()> {
215        self.loader.add_module_path(module_path)
216    }
217
218    pub fn register_global_property<K, V>(
219        &mut self,
220        key: K,
221        value: V,
222        attribute: Attribute,
223    ) -> JSResult<()>
224    where
225        K: Into<PropertyKey>,
226        V: Into<JsValue>, {
227        self.context.register_global_property(key, value, attribute)
228            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
229        Ok(())
230    }
231
232    pub fn register_global_callable(
233        &mut self,
234        name: String,
235        length: usize,
236        body: NativeFunction,
237    ) -> JSResult<()> {
238        self.context.register_global_callable(JsString::from(name), length, body)
239            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
240        Ok(())
241    }
242
243    pub fn register_global_builtin_callable(
244        &mut self,
245        name: String,
246        length: usize,
247        body: NativeFunction,
248    ) -> JSResult<()> {
249        self.context.register_global_builtin_callable(JsString::from(name), length, body)
250            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
251        Ok(())
252    }
253
254    pub fn register_global_class<C: Class>(&mut self) -> JSResult<()> {
255        self.context.register_global_class::<C>()
256            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
257        Ok(())
258    }
259
260    pub fn eval_file(&mut self, path: &Path) -> JSResult<()> {
261        let path = path.canonicalize()
262            .map_err(into_js_err!(JSErrorCode::InvalidPath, "Invalid path {:?}", path))?;
263        if let Some(parent) = path.parent() {
264            self.add_module_path(parent)?;
265        } else {
266            self.add_module_path(std::env::current_dir()
267                .map_err(into_js_err!(JSErrorCode::InvalidPath))?.as_path())?;
268        }
269        let source = Source::from_filepath(path.as_path())
270            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
271        self.eval(source)
272    }
273
274    pub fn eval_string(&mut self, code: &str) -> JSResult<()> {
275        let source = Source::from_bytes(code.as_bytes());
276        self.eval(source)
277    }
278
279    fn eval<'path, R: ReadChar>(&mut self, source: Source<'path, R>) -> JSResult<()> {
280        if self.module.is_some() {
281            return Err(js_err!(JSErrorCode::JsFailed, "Already loaded a module"));
282        }
283
284        let module = Module::parse(source, None, &mut self.context)
285            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
286
287        let promise_result = module.load(&mut self.context)
288            .then(
289                Some(
290                    NativeFunction::from_copy_closure_with_captures(
291                        |_, _, module, context| {
292                            // After loading, link all modules by resolving the imports
293                            // and exports on the full module graph, initializing module
294                            // environments. This returns a plain `Err` since all modules
295                            // must link at the same time.
296                            module.link(context)?;
297                            Ok(JsValue::undefined())
298                        },
299                        module.clone(),
300                    )
301                        .to_js_function(self.context.realm()),
302                ),
303                None,
304                &mut self.context,
305            )
306            .then(
307                Some(
308                    NativeFunction::from_copy_closure_with_captures(
309                        // Finally, evaluate the root module.
310                        // This returns a `JsPromise` since a module could have
311                        // top-level await statements, which defers module execution to the
312                        // job queue.
313                        |_, _, module, context| {
314                            let result = module.evaluate(context);
315                            Ok(result.into())
316                        },
317                        module.clone(),
318                    )
319                        .to_js_function(self.context.realm()),
320                ),
321                None,
322                &mut self.context,
323            );
324
325        self.context.run_jobs()
326            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
327
328        match promise_result.state() {
329            PromiseState::Pending => return Err(js_err!(JSErrorCode::JsFailed, "module didn't execute!")),
330            PromiseState::Fulfilled(v) => {
331                assert_eq!(v, JsValue::undefined());
332            }
333            PromiseState::Rejected(err) => {
334                log::error!("module {:?} execution failed: {:?}", module.path(), err.to_string(&mut self.context));
335                let err = JsError::from_opaque(err).into_erased(&mut self.context);
336                return Err(js_err!(JSErrorCode::JsFailed, "{err}"));
337            }
338        }
339
340        self.module = Some(module);
341
342        Ok(())
343    }
344
345    fn call(&mut self, name: &str, args: Vec<JsValue>) -> JSResult<JsValue> {
346        if self.module.is_none() {
347            return Err(js_err!(JSErrorCode::JsFailed, "module didn't execute!"));
348        }
349
350        let fun = self.module.as_mut().unwrap().get_value(JsString::from(name), &mut self.context)
351            .map_err(|e| js_err!(JSErrorCode::JsFailed, "can't find {name} failed: {}", e))?;
352
353        if let Some(fun) = fun.as_callable() {
354            let result = fun.call(&JsValue::null(), args.as_slice(), &mut self.context)
355                .map_err(|e| js_err!(JSErrorCode::JsFailed, "call {name} failed: {}", e))?;
356            Ok(result)
357        } else {
358            Err(js_err!(JSErrorCode::JsFailed, "can't call {name}"))
359        }
360    }
361}
362
363pub struct AsyncJsEngine {
364    inner: Arc<Mutex<JsEngine>>,
365}
366
367impl AsyncJsEngine {
368    pub async fn new() -> JSResult<Self> {
369        let inner = tokio::task::spawn_blocking(|| JsEngine::new())
370            .await
371            .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))??;
372        Ok(AsyncJsEngine {
373            inner: Arc::new(Mutex::new(inner)),
374        })
375    }
376
377    pub fn add_module_path(&self, module_path: &Path) -> JSResult<()> {
378        let mut inner = self.inner.lock().unwrap();
379        inner.add_module_path(module_path)
380    }
381
382    pub fn register_global_property<K, V>(
383        &self,
384        key: K,
385        value: V,
386        attribute: Attribute,
387    ) -> JSResult<()>
388    where
389        K: Into<PropertyKey>,
390        V: Into<JsValue>, {
391        self.inner.lock().unwrap().register_global_property(key, value, attribute)
392    }
393
394    pub fn register_global_callable(
395        &self,
396        name: impl Into<String>,
397        length: usize,
398        body: NativeFunction,
399    ) -> JSResult<()> {
400        self.inner.lock().unwrap().register_global_callable(name.into(), length, body)
401    }
402
403    pub fn register_global_builtin_callable(
404        &self,
405        name: String,
406        length: usize,
407        body: NativeFunction,
408    ) -> JSResult<()> {
409        self.inner.lock().unwrap().register_global_builtin_callable(name, length, body)
410    }
411
412    pub fn register_global_class<C: Class>(&self) -> JSResult<()> {
413        self.inner.lock().unwrap().register_global_class::<C>()
414    }
415
416    pub async fn eval_string(&self, code: impl Into<String>) -> JSResult<()> {
417        let inner = self.inner.clone();
418        let code = code.into();
419        tokio::task::spawn_blocking(move || {
420            let mut inner = inner.lock().unwrap();
421            inner.eval_string(code.as_str())
422        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
423    }
424
425    pub async fn eval_file(&self, path: impl AsRef<Path>) -> JSResult<()> {
426        let inner = self.inner.clone();
427        let path = path.as_ref().to_path_buf();
428        tokio::task::spawn_blocking(move || {
429            let mut inner = inner.lock().unwrap();
430            inner.eval_file(path.as_path())
431        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
432    }
433
434    pub async fn call(&self, name: impl Into<String>, args: Vec<serde_json::Value>) -> JSResult<Option<serde_json::Value>> {
435        let inner = self.inner.clone();
436        let name = name.into();
437        tokio::task::spawn_blocking(move || {
438            let mut inner = inner.lock().unwrap();
439            let mut new_args = Vec::with_capacity(args.len());
440            for v in args.iter() {
441                new_args.push(JsValue::from_json(v, &mut inner.context)
442                    .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?);
443            }
444            let result = inner.call(name.as_str(), new_args)?;
445            let result = result.to_json(&mut inner.context)
446                .map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?;
447            Ok(result)
448        }).await.map_err(|e| js_err!(JSErrorCode::JsFailed, "{e}"))?
449    }
450}
451
452fn require(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
453    let arg = args.get_or_undefined(0);
454
455    // BUG: Dev branch seems to be passing string arguments along with quotes
456    let libfile = arg.to_string(ctx)?.to_std_string_escaped();
457    let module_loader = ctx.downcast_module_loader::<SfoModuleLoader>().unwrap();
458    let libfile = module_loader.commonjs_resolve_module(libfile.as_str())?;
459
460    if let Some((_, module_obj)) = module_loader.get_commonjs(libfile.as_path()) {
461        let exports = module_obj.as_object().unwrap().get(js_string!("exports"), ctx)?;
462        return Ok(exports)
463    }
464
465    let buffer = read_to_string(libfile.clone())
466        .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
467
468    let wrapper_code = format!(
469        r#"export function cjs_module(exports, requireInner, module, __filename, __dirname) {{ {}
470        }}"#,
471        buffer
472    );
473
474    let module = Module::parse(Source::from_reader(wrapper_code.as_bytes(), Some(libfile.as_path())), None, ctx)?;
475    let promise_result = module.load(ctx)
476        .then(
477            Some(
478                NativeFunction::from_copy_closure_with_captures(
479                    |_, _, module, context| {
480                        // After loading, link all modules by resolving the imports
481                        // and exports on the full module graph, initializing module
482                        // environments. This returns a plain `Err` since all modules
483                        // must link at the same time.
484                        module.link(context)?;
485                        Ok(JsValue::undefined())
486                    },
487                    module.clone(),
488                )
489                    .to_js_function(ctx.realm()),
490            ),
491            None,
492            ctx,
493        )
494        .then(
495            Some(
496                NativeFunction::from_copy_closure_with_captures(
497                    // Finally, evaluate the root module.
498                    // This returns a `JsPromise` since a module could have
499                    // top-level await statements, which defers module execution to the
500                    // job queue.
501                    |_, _, module, context| Ok(module.evaluate(context).into()),
502                    module.clone(),
503                )
504                    .to_js_function(ctx.realm()),
505            ),
506            None,
507            ctx,
508        );
509    ctx.run_jobs()?;
510
511    match promise_result.state() {
512        PromiseState::Pending => return Err(JsError::from_native(JsNativeError::typ().with_message("module didn't execute!"))),
513        PromiseState::Fulfilled(v) => {
514            assert_eq!(v, JsValue::undefined());
515        }
516        PromiseState::Rejected(err) => {
517            let stacks = ctx.stack_trace();
518            for stack in stacks {
519                println!("{:?}", stack);
520            }
521
522            let err = JsError::from_opaque(err).try_native(ctx).unwrap();
523            return Err(JsError::from_native(err));
524        }
525    }
526
527    // let wrapper_func = ctx.eval(Source::from_bytes(&wrapper_code))?;
528
529    // Adding custom object that mimics 'module.exports'
530    let module_obj = JsObject::default(ctx.intrinsics());
531    let exports_obj = JsObject::default(ctx.intrinsics());
532    module_obj.set(js_string!("exports"), exports_obj.clone(), false, ctx)?;
533    module_loader.insert_commonjs(libfile.clone(), module.clone(), JsValue::from(module_obj.clone()));
534
535    let require = NativeFunction::from_fn_ptr(require).to_js_function(ctx.realm());
536    let filename = libfile.to_string_lossy().to_string();
537    let dirname = libfile.parent().unwrap().to_string_lossy().to_string();
538
539    let commonjs_module = module.get_value(JsString::from("cjs_module"), ctx)?;
540    if let Some(args) = commonjs_module.as_callable() {
541        let result = args.call(
542            &JsValue::null(),
543            &[
544                JsValue::from(exports_obj.clone()),
545                JsValue::from(require),
546                JsValue::from(module_obj.clone()),
547                JsValue::from(JsString::from(filename)),
548                JsValue::from(JsString::from(dirname)),
549            ],
550            ctx
551        );
552        if result.is_err() {
553            let err = result.as_ref().err().unwrap();
554            log::error!("{}", err);
555            return result;
556        }
557        let exports = module_obj.get(js_string!("exports"), ctx)?;
558        Ok(exports)
559    } else {
560        unreachable!()
561    }
562
563
564    // let wrapper_func = ctx.eval(Source::from_bytes(&wrapper_code))?;
565    //
566    // // Adding custom object that mimics 'module.exports'
567    // let module_obj = JsObject::default(ctx.intrinsics());
568    // let exports_obj = JsObject::default(ctx.intrinsics());
569    // exports_obj.set(js_string!("__esModule"), JsValue::new(true), false, ctx)?;
570    // module_obj.set(js_string!("exports"), exports_obj.clone(), false, ctx)?;
571    //
572    // let require = NativeFunction::from_fn_ptr(require).to_js_function(ctx.realm());
573    // let filename = libfile.to_string_lossy().to_string();
574    // let dirname = libfile.parent().unwrap().to_string_lossy().to_string();
575    //
576    // if let Some(args) = wrapper_func.as_callable() {
577    //     args.call(
578    //         &JsValue::null(),
579    //         &[
580    //             JsValue::from(exports_obj.clone()),
581    //             JsValue::from(module_obj),
582    //             JsValue::from(JsString::from(filename)),
583    //             JsValue::from(JsString::from(dirname)),
584    //         ],
585    //         ctx
586    //     )?;
587    //     Ok(JsValue::from(exports_obj))
588    // } else {
589    //     unreachable!()
590    // }
591}