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 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 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}