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 PATH_CACHE: RefCell<HashMap<(String, PathBuf), PathBuf>> =
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) };
}
pub fn reset() {
CACHE.with(|c| c.borrow_mut().clear());
PATH_CACHE.with(|c| c.borrow_mut().clear());
FACTORY.with(|f| *f.borrow_mut() = None);
CALLSITE_FACTORY.with(|f| *f.borrow_mut() = None);
ENTRY_DIR.with(|d| *d.borrow_mut() = std::env::current_dir().unwrap_or_default());
}
pub fn cache_keys() -> Vec<String> {
CACHE.with(|c| {
c.borrow()
.keys()
.map(|p| p.to_string_lossy().into_owned())
.collect()
})
}
pub fn cache_get(key: &str) -> Option<Value> {
CACHE.with(|c| c.borrow().get(Path::new(key)).cloned())
}
pub fn cache_delete(key: &str) -> bool {
CACHE.with(|c| c.borrow_mut().remove(Path::new(key)).is_some())
}
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())
}
pub fn install_entry_globals(origin: &str) {
let from_file = origin != "[eval]" && origin != "[stdin]";
let (dirname, id) = if from_file {
let dir = Path::new(origin)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|| ".".into());
(dir, ".".to_string())
} else {
(".".to_string(), origin.to_string())
};
let filename = crate::stdlib::path::resolve_one(origin);
let module = new_module(&id, &dirname, &filename);
let exports = module_exports(&module);
with_host(|h| {
let origin_str = h.new_str(origin.to_string());
let dirname = h.new_str(dirname);
h.set_global("__filename", origin_str);
h.set_global("__dirname", dirname);
h.set_global("module", module.clone());
if from_file {
h.set_builtin_static("require", "main", module.clone());
}
h.set_global("exports", exports.clone());
let top = if from_file {
exports
} else {
h.global_object()
};
h.set_top_this(top);
});
}
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 joined = if is_absolute {
spec.to_string()
} else {
from_dir.join(spec).to_string_lossy().into_owned()
};
let base = PathBuf::from(crate::stdlib::path::resolve_one(&joined));
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 key = (spec.to_string(), from_dir.to_path_buf());
if let Some(hit) = PATH_CACHE.with(|c| c.borrow().get(&key).cloned()) {
return load_file(&hit);
}
let path = resolve(spec, from_dir).ok_or_else(|| {
crate::host::plain_coded_error(
"Error",
"MODULE_NOT_FOUND",
&format!("Cannot find module '{spec}'"),
)
})?;
let path = std::fs::canonicalize(&path).unwrap_or(path);
PATH_CACHE.with(|c| c.borrow_mut().insert(key, path.clone()));
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 abs = path.to_string_lossy().into_owned();
let dir = path.parent().unwrap_or(Path::new("")).to_string_lossy();
let module = new_module(&abs, &dir, &abs);
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(&module) {
p.insert("exports".to_string(), val.clone());
p.insert("loaded".to_string(), Value::Bool(true));
}
});
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 abs = path.to_string_lossy().into_owned();
let module = new_module(&abs, &dir.to_string_lossy(), &abs);
let exports = module_exports(&module);
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,
)?;
mark_loaded(&module);
Ok(module_exports(&module))
}
fn new_module(id: &str, dir: &str, filename: &str) -> Value {
let mut node_modules: Vec<String> = Vec::new();
let mut cur = Some(Path::new(dir));
while let Some(d) = cur.filter(|d| !d.as_os_str().is_empty()) {
node_modules.push(d.join("node_modules").to_string_lossy().into_owned());
cur = d.parent();
}
with_host(|h| {
let exports = h.new_object(indexmap::IndexMap::new());
let mut props = indexmap::IndexMap::new();
props.insert("id".to_string(), h.new_str(id.to_string()));
props.insert("path".to_string(), h.new_str(dir.to_string()));
props.insert("exports".to_string(), exports);
props.insert("filename".to_string(), h.new_str(filename.to_string()));
props.insert("loaded".to_string(), Value::Bool(false));
let children = h.new_array(Vec::new());
props.insert("children".to_string(), children);
let paths: Vec<Value> = node_modules.into_iter().map(|p| h.new_str(p)).collect();
let paths = h.new_array(paths);
props.insert("paths".to_string(), paths);
h.new_object(props)
})
}
fn mark_loaded(module: &Value) {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(module) {
p.insert("loaded".to_string(), Value::Bool(true));
}
});
}
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> {
eval_binding(&format!(
"(function (exports, require, module, __dirname, __filename) {{\n{source}\n}})"
))
}
fn eval_binding(src: &str) -> Result<Value, String> {
crate::eval_in_global_scope(src)
}
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()));
let req = host::invoke(&factory, vec![dir_str], None)?;
if let Some(main) = with_host(|h| h.builtin_static("require", "main")) {
with_host(|h| h.set_fn_prop(&req, "main", main));
}
Ok(req)
}
fn factory() -> Result<Value, String> {
if let Some(f) = FACTORY.with(|f| f.borrow().clone()) {
return Ok(f);
}
let src = "(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 = __cjs_cache;\n\
req.main = undefined;\n\
req.extensions = {};\n\
return req;\n\
});";
let f = eval_binding(src)?;
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 = "(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)?;
CALLSITE_FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
f
};
host::invoke(&factory, vec![Value::Float(depth as f64)], None)
}