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