use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::host::{self, with_host, JsObj};
use fusevm::Value;
thread_local! {
static CACHE: RefCell<HashMap<PathBuf, Value>> = RefCell::new(HashMap::new());
static ENTRY_DIR: RefCell<PathBuf> = RefCell::new(std::env::current_dir().unwrap_or_default());
static FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
static CALLSITE_FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
static SEQ: RefCell<u64> = const { RefCell::new(0) };
}
pub fn reset() {
CACHE.with(|c| c.borrow_mut().clear());
FACTORY.with(|f| *f.borrow_mut() = None);
CALLSITE_FACTORY.with(|f| *f.borrow_mut() = None);
SEQ.with(|s| *s.borrow_mut() = 0);
ENTRY_DIR.with(|d| *d.borrow_mut() = std::env::current_dir().unwrap_or_default());
}
pub fn set_entry_dir(dir: PathBuf) {
ENTRY_DIR.with(|d| *d.borrow_mut() = dir);
}
pub fn entry_dir() -> PathBuf {
ENTRY_DIR.with(|d| d.borrow().clone())
}
fn add_ext(p: &Path, ext: &str) -> PathBuf {
let mut s = p.as_os_str().to_owned();
s.push(".");
s.push(ext);
PathBuf::from(s)
}
fn load_as_file(p: &Path) -> Option<PathBuf> {
if p.is_file() {
return Some(p.to_path_buf());
}
for ext in ["js", "json"] {
let cand = add_ext(p, ext);
if cand.is_file() {
return Some(cand);
}
}
None
}
fn load_as_dir(p: &Path) -> Option<PathBuf> {
let pkg = p.join("package.json");
if pkg.is_file() {
if let Some(main) = pkg_entry(&pkg) {
let mp = p.join(&main);
if let Some(f) = load_as_file(&mp).or_else(|| load_index(&mp)) {
return Some(f);
}
}
}
load_index(p)
}
fn load_index(p: &Path) -> Option<PathBuf> {
for name in ["index.js", "index.json"] {
let cand = p.join(name);
if cand.is_file() {
return Some(cand);
}
}
None
}
fn pkg_entry(pkg: &Path) -> Option<String> {
let text = std::fs::read_to_string(pkg).ok()?;
let json: serde_json::Value = serde_json::from_str(&text).ok()?;
if let Some(e) = exports_main(json.get("exports")) {
return Some(strip_dot_slash(&e));
}
json.get("main")
.and_then(|m| m.as_str())
.map(strip_dot_slash)
}
fn exports_main(exports: Option<&serde_json::Value>) -> Option<String> {
let exports = exports?;
if let Some(s) = exports.as_str() {
return Some(s.to_string());
}
let obj = exports.as_object()?;
let target = obj.get(".").unwrap_or(exports);
condition_target(target)
}
fn condition_target(target: &serde_json::Value) -> Option<String> {
if let Some(s) = target.as_str() {
return Some(s.to_string());
}
let obj = target.as_object()?;
for cond in ["require", "node", "default"] {
if let Some(v) = obj.get(cond) {
if let Some(s) = condition_target(v) {
return Some(s);
}
}
}
None
}
fn strip_dot_slash(s: &str) -> String {
s.strip_prefix("./").unwrap_or(s).to_string()
}
fn resolve_bare(spec: &str, from_dir: &Path) -> Option<PathBuf> {
let mut dir = Some(from_dir);
while let Some(d) = dir {
if d.file_name().is_some_and(|n| n == "node_modules") {
dir = d.parent();
continue;
}
let candidate = d.join("node_modules").join(spec);
if let Some(f) = load_as_file(&candidate).or_else(|| load_as_dir(&candidate)) {
return Some(f);
}
dir = d.parent();
}
None
}
pub fn resolve(spec: &str, from_dir: &Path) -> Option<PathBuf> {
let is_relative =
spec.starts_with("./") || spec.starts_with("../") || spec == "." || spec == "..";
let is_absolute = spec.starts_with('/');
if is_relative || is_absolute {
let base = if is_absolute {
PathBuf::from(spec)
} else {
from_dir.join(spec)
};
return load_as_file(&base).or_else(|| load_as_dir(&base));
}
resolve_bare(spec, from_dir)
}
pub fn require(spec: &str, from_dir: &Path) -> Result<Value, String> {
if let Some(ns) = crate::stdlib::resolve(spec) {
return Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string()))));
}
let path =
resolve(spec, from_dir).ok_or_else(|| format!("Error: Cannot find module '{spec}'"))?;
let path = std::fs::canonicalize(&path).unwrap_or(path);
load_file(&path)
}
fn load_file(path: &Path) -> Result<Value, String> {
if let Some(cached) = CACHE.with(|c| c.borrow().get(path).cloned()) {
return Ok(module_exports(&cached));
}
if path.extension().is_some_and(|e| e == "json") {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let src = with_host(|h| h.new_str(text));
let val = crate::builtins::call_builtin_function("JSON.parse", vec![src])?;
let module = new_module(val.clone());
CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module));
return Ok(val);
}
let source = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
let wrapper = compile_wrapper(&source)
.map_err(|e| format!("{e}\n while loading {}", path.display()))?;
let exports = with_host(|h| h.new_object(indexmap::IndexMap::new()));
let module = new_module(exports.clone());
let require_fn = make_require(&dir)?;
let (dirname, filename) = with_host(|h| {
(
h.new_str(dir.to_string_lossy().to_string()),
h.new_str(path.to_string_lossy().to_string()),
)
});
CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module.clone()));
host::invoke(
&wrapper,
vec![exports, require_fn, module.clone(), dirname, filename],
None,
)?;
Ok(module_exports(&module))
}
fn new_module(exports: Value) -> Value {
with_host(|h| {
let mut props = indexmap::IndexMap::new();
props.insert("exports".to_string(), exports);
h.new_object(props)
})
}
fn module_exports(module: &Value) -> Value {
with_host(|h| match h.get(module) {
Some(JsObj::Object(p)) => p.get("exports").cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
})
}
fn compile_wrapper(source: &str) -> Result<Value, String> {
let n = SEQ.with(|s| {
let mut b = s.borrow_mut();
*b += 1;
*b
});
let var = format!("__cjs_w{n}");
let wrapped = format!(
"var {var} = (function (exports, require, module, __dirname, __filename) {{\n{source}\n}});"
);
eval_binding(&wrapped, &var)
}
fn eval_binding(src: &str, name: &str) -> Result<Value, String> {
let prog = crate::compile(src)?;
let main = crate::load_merged(prog);
host::run_chunk_on(main)?;
with_host(|h| h.read_name(name))
.ok_or_else(|| format!("module loader: failed to capture '{name}'"))
}
fn make_require(dir: &Path) -> Result<Value, String> {
let factory = factory()?;
let dir_str = with_host(|h| h.new_str(dir.to_string_lossy().to_string()));
host::invoke(&factory, vec![dir_str], None)
}
fn factory() -> Result<Value, String> {
if let Some(f) = FACTORY.with(|f| f.borrow().clone()) {
return Ok(f);
}
let src = "var __cjs_factory = (function (__cjs_dir) {\n\
var req = function (spec) { return __cjs_require(spec, __cjs_dir); };\n\
req.resolve = function (spec) { return __cjs_resolve(spec, __cjs_dir); };\n\
req.cache = {};\n\
req.main = undefined;\n\
req.extensions = {};\n\
return req;\n\
});";
let f = eval_binding(src, "__cjs_factory")?;
FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
Ok(f)
}
pub fn callsite_stack(depth: usize) -> Result<Value, String> {
let factory = if let Some(f) = CALLSITE_FACTORY.with(|f| f.borrow().clone()) {
f
} else {
let src = "var __cjs_callsites = (function (n) {\n\
var a = [];\n\
for (var i = 0; i < n; i++) {\n\
a.push({\n\
getFileName: function () { return null; },\n\
getLineNumber: function () { return 0; },\n\
getColumnNumber: function () { return 0; },\n\
getFunctionName: function () { return null; },\n\
getMethodName: function () { return null; },\n\
getTypeName: function () { return null; },\n\
getThis: function () { return undefined; },\n\
isNative: function () { return false; },\n\
isEval: function () { return false; },\n\
toString: function () { return '<anonymous>'; }\n\
});\n\
}\n\
return a;\n\
});";
let f = eval_binding(src, "__cjs_callsites")?;
CALLSITE_FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
f
};
host::invoke(&factory, vec![Value::Float(depth as f64)], None)
}