1use std::cell::RefCell;
29use std::collections::HashMap;
30use std::path::{Path, PathBuf};
31
32use crate::host::{self, with_host, JsObj};
33use fusevm::Value;
34
35thread_local! {
36 static CACHE: RefCell<HashMap<PathBuf, Value>> = RefCell::new(HashMap::new());
40 static ENTRY_DIR: RefCell<PathBuf> = RefCell::new(std::env::current_dir().unwrap_or_default());
43 static FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
46 static CALLSITE_FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
49 static SEQ: RefCell<u64> = const { RefCell::new(0) };
52}
53
54pub fn reset() {
57 CACHE.with(|c| c.borrow_mut().clear());
58 FACTORY.with(|f| *f.borrow_mut() = None);
59 CALLSITE_FACTORY.with(|f| *f.borrow_mut() = None);
60 SEQ.with(|s| *s.borrow_mut() = 0);
61 ENTRY_DIR.with(|d| *d.borrow_mut() = std::env::current_dir().unwrap_or_default());
62}
63
64pub fn set_entry_dir(dir: PathBuf) {
66 ENTRY_DIR.with(|d| *d.borrow_mut() = dir);
67}
68
69pub fn entry_dir() -> PathBuf {
71 ENTRY_DIR.with(|d| d.borrow().clone())
72}
73
74fn add_ext(p: &Path, ext: &str) -> PathBuf {
79 let mut s = p.as_os_str().to_owned();
80 s.push(".");
81 s.push(ext);
82 PathBuf::from(s)
83}
84
85fn load_as_file(p: &Path) -> Option<PathBuf> {
88 if p.is_file() {
89 return Some(p.to_path_buf());
90 }
91 for ext in ["js", "json"] {
92 let cand = add_ext(p, ext);
93 if cand.is_file() {
94 return Some(cand);
95 }
96 }
97 None
98}
99
100fn load_as_dir(p: &Path) -> Option<PathBuf> {
103 let pkg = p.join("package.json");
104 if pkg.is_file() {
105 if let Some(main) = pkg_entry(&pkg) {
106 let mp = p.join(&main);
107 if let Some(f) = load_as_file(&mp).or_else(|| load_index(&mp)) {
108 return Some(f);
109 }
110 }
111 }
112 load_index(p)
113}
114
115fn load_index(p: &Path) -> Option<PathBuf> {
117 for name in ["index.js", "index.json"] {
118 let cand = p.join(name);
119 if cand.is_file() {
120 return Some(cand);
121 }
122 }
123 None
124}
125
126fn pkg_entry(pkg: &Path) -> Option<String> {
132 let text = std::fs::read_to_string(pkg).ok()?;
133 let json: serde_json::Value = serde_json::from_str(&text).ok()?;
134 if let Some(e) = exports_main(json.get("exports")) {
135 return Some(strip_dot_slash(&e));
136 }
137 json.get("main")
138 .and_then(|m| m.as_str())
139 .map(strip_dot_slash)
140}
141
142fn exports_main(exports: Option<&serde_json::Value>) -> Option<String> {
145 let exports = exports?;
146 if let Some(s) = exports.as_str() {
148 return Some(s.to_string());
149 }
150 let obj = exports.as_object()?;
151 let target = obj.get(".").unwrap_or(exports);
153 condition_target(target)
154}
155
156fn condition_target(target: &serde_json::Value) -> Option<String> {
159 if let Some(s) = target.as_str() {
160 return Some(s.to_string());
161 }
162 let obj = target.as_object()?;
163 for cond in ["require", "node", "default"] {
164 if let Some(v) = obj.get(cond) {
165 if let Some(s) = condition_target(v) {
166 return Some(s);
167 }
168 }
169 }
170 None
171}
172
173fn strip_dot_slash(s: &str) -> String {
175 s.strip_prefix("./").unwrap_or(s).to_string()
176}
177
178fn resolve_bare(spec: &str, from_dir: &Path) -> Option<PathBuf> {
182 let mut dir = Some(from_dir);
183 while let Some(d) = dir {
184 if d.file_name().is_some_and(|n| n == "node_modules") {
186 dir = d.parent();
187 continue;
188 }
189 let candidate = d.join("node_modules").join(spec);
190 if let Some(f) = load_as_file(&candidate).or_else(|| load_as_dir(&candidate)) {
191 return Some(f);
192 }
193 dir = d.parent();
194 }
195 None
196}
197
198pub fn resolve(spec: &str, from_dir: &Path) -> Option<PathBuf> {
201 let is_relative =
202 spec.starts_with("./") || spec.starts_with("../") || spec == "." || spec == "..";
203 let is_absolute = spec.starts_with('/');
204 if is_relative || is_absolute {
205 let base = if is_absolute {
206 PathBuf::from(spec)
207 } else {
208 from_dir.join(spec)
209 };
210 return load_as_file(&base).or_else(|| load_as_dir(&base));
211 }
212 resolve_bare(spec, from_dir)
213}
214
215pub fn require(spec: &str, from_dir: &Path) -> Result<Value, String> {
221 if let Some(ns) = crate::stdlib::resolve(spec) {
224 return Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string()))));
225 }
226 let path =
227 resolve(spec, from_dir).ok_or_else(|| format!("Error: Cannot find module '{spec}'"))?;
228 let path = std::fs::canonicalize(&path).unwrap_or(path);
231 load_file(&path)
232}
233
234fn load_file(path: &Path) -> Result<Value, String> {
237 if let Some(cached) = CACHE.with(|c| c.borrow().get(path).cloned()) {
238 return Ok(module_exports(&cached));
240 }
241 if path.extension().is_some_and(|e| e == "json") {
242 let text = std::fs::read_to_string(path)
243 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
244 let src = with_host(|h| h.new_str(text));
245 let val = crate::builtins::call_builtin_function("JSON.parse", vec![src])?;
246 let module = new_module(val.clone());
249 CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module));
250 return Ok(val);
251 }
252
253 let source = std::fs::read_to_string(path)
254 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
255 let dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
256
257 let wrapper = compile_wrapper(&source)
260 .map_err(|e| format!("{e}\n while loading {}", path.display()))?;
261
262 let exports = with_host(|h| h.new_object(indexmap::IndexMap::new()));
264 let module = new_module(exports.clone());
265 let require_fn = make_require(&dir)?;
266 let (dirname, filename) = with_host(|h| {
267 (
268 h.new_str(dir.to_string_lossy().to_string()),
269 h.new_str(path.to_string_lossy().to_string()),
270 )
271 });
272
273 CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module.clone()));
276
277 host::invoke(
278 &wrapper,
279 vec![exports, require_fn, module.clone(), dirname, filename],
280 None,
281 )?;
282
283 Ok(module_exports(&module))
284}
285
286fn new_module(exports: Value) -> Value {
288 with_host(|h| {
289 let mut props = indexmap::IndexMap::new();
290 props.insert("exports".to_string(), exports);
291 h.new_object(props)
292 })
293}
294
295fn module_exports(module: &Value) -> Value {
297 with_host(|h| match h.get(module) {
298 Some(JsObj::Object(p)) => p.get("exports").cloned().unwrap_or(Value::Undef),
299 _ => Value::Undef,
300 })
301}
302
303fn compile_wrapper(source: &str) -> Result<Value, String> {
307 let n = SEQ.with(|s| {
308 let mut b = s.borrow_mut();
309 *b += 1;
310 *b
311 });
312 let var = format!("__cjs_w{n}");
313 let wrapped = format!(
315 "var {var} = (function (exports, require, module, __dirname, __filename) {{\n{source}\n}});"
316 );
317 eval_binding(&wrapped, &var)
318}
319
320fn eval_binding(src: &str, name: &str) -> Result<Value, String> {
325 let prog = crate::compile(src)?;
326 let main = crate::load_merged(prog);
327 host::run_chunk_on(main)?;
328 with_host(|h| h.read_name(name))
329 .ok_or_else(|| format!("module loader: failed to capture '{name}'"))
330}
331
332fn make_require(dir: &Path) -> Result<Value, String> {
334 let factory = factory()?;
335 let dir_str = with_host(|h| h.new_str(dir.to_string_lossy().to_string()));
336 host::invoke(&factory, vec![dir_str], None)
337}
338
339fn factory() -> Result<Value, String> {
342 if let Some(f) = FACTORY.with(|f| f.borrow().clone()) {
343 return Ok(f);
344 }
345 let src = "var __cjs_factory = (function (__cjs_dir) {\n\
346 var req = function (spec) { return __cjs_require(spec, __cjs_dir); };\n\
347 req.resolve = function (spec) { return __cjs_resolve(spec, __cjs_dir); };\n\
348 req.cache = {};\n\
349 req.main = undefined;\n\
350 req.extensions = {};\n\
351 return req;\n\
352 });";
353 let f = eval_binding(src, "__cjs_factory")?;
354 FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
355 Ok(f)
356}
357
358pub fn callsite_stack(depth: usize) -> Result<Value, String> {
364 let factory = if let Some(f) = CALLSITE_FACTORY.with(|f| f.borrow().clone()) {
365 f
366 } else {
367 let src = "var __cjs_callsites = (function (n) {\n\
368 var a = [];\n\
369 for (var i = 0; i < n; i++) {\n\
370 a.push({\n\
371 getFileName: function () { return null; },\n\
372 getLineNumber: function () { return 0; },\n\
373 getColumnNumber: function () { return 0; },\n\
374 getFunctionName: function () { return null; },\n\
375 getMethodName: function () { return null; },\n\
376 getTypeName: function () { return null; },\n\
377 getThis: function () { return undefined; },\n\
378 isNative: function () { return false; },\n\
379 isEval: function () { return false; },\n\
380 toString: function () { return '<anonymous>'; }\n\
381 });\n\
382 }\n\
383 return a;\n\
384 });";
385 let f = eval_binding(src, "__cjs_callsites")?;
386 CALLSITE_FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
387 f
388 };
389 host::invoke(&factory, vec![Value::Float(depth as f64)], None)
390}