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