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