Skip to main content

sfo_js/
js_engine.rs

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