Skip to main content

nodejs/
module.rs

1//! CommonJS module loader.
2//!
3//! Node's `require()` semantics, layered on the existing engine — no bespoke VM
4//! primitive. A `.js` file is wrapped in the canonical Node module wrapper
5//! `(function (exports, require, module, __dirname, __filename) { … })`, compiled
6//! through the ordinary `compile` → `load_merged` path to obtain the wrapper
7//! FUNCTION value, then `host::invoke`d with a fresh `module = { exports: {} }`.
8//! Whatever the body assigns to `module.exports` (or hangs off `exports`) is the
9//! module's value; it is cached by resolved absolute path so a second `require`
10//! of the same file returns the identical object and circular requires observe
11//! the partially-filled `exports`.
12//!
13//! Core modules (`fs`, `path`, `http`, …) short-circuit to their native
14//! `JsObj::Builtin` namespace (see `stdlib::resolve`) and are never read from
15//! disk. Everything else — relative paths, JSON files, and bare `node_modules`
16//! packages with their `package.json` `"exports"`/`"main"` and `index.js`
17//! fallbacks — resolves on the real filesystem and runs the genuine, unmodified
18//! source.
19//!
20//! Per-module `require` is a real JS closure that bakes in the defining module's
21//! directory, so a `require(...)` deferred inside a function called much later
22//! still resolves against the module that defined it (a single global
23//! "current dir" would resolve against the wrong module). The closure is minted
24//! by a one-time compiled factory (`FACTORY`) invoked with the directory string;
25//! it dispatches back into this loader through the `__cjs_require` /
26//! `__cjs_resolve` global native builtins.
27
28use 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    /// Require cache: resolved absolute path → the `module` object (its `.exports`
37    /// is re-read on every hit, matching Node — `module.exports = X` reassignment
38    /// is observed by later requires).
39    static CACHE: RefCell<HashMap<PathBuf, Value>> = RefCell::new(HashMap::new());
40    /// Base directory the ENTRY script's top-level `require` resolves against
41    /// (the dir of `node app.js`, or cwd for `node -e`).
42    static ENTRY_DIR: RefCell<PathBuf> = RefCell::new(std::env::current_dir().unwrap_or_default());
43    /// The compiled per-module `require`-closure factory (see module docs),
44    /// minted once per host and reused for every module.
45    static FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
46    /// The compiled synthetic-CallSite-array factory (for `Error.captureStackTrace`
47    /// under a custom `Error.prepareStackTrace`), minted once per host.
48    static CALLSITE_FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
49    /// Monotonic counter feeding the temporary capture-variable name used to read
50    /// a compiled expression's value out of the shared module scope.
51    static SEQ: RefCell<u64> = const { RefCell::new(0) };
52}
53
54/// Clear all per-host loader state. Called from `host::reset_host` so a fresh
55/// eval (which rebuilds the heap) never reuses a stale heap handle.
56pub 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
64/// Set the base directory the ENTRY script's `require` resolves against.
65pub fn set_entry_dir(dir: PathBuf) {
66    ENTRY_DIR.with(|d| *d.borrow_mut() = dir);
67}
68
69/// The ENTRY script's base directory.
70pub fn entry_dir() -> PathBuf {
71    ENTRY_DIR.with(|d| d.borrow().clone())
72}
73
74// ── resolution ───────────────────────────────────────────────────────────────
75
76/// Append `.ext` to a path (Node appends the extension, it does not replace an
77/// existing one — `foo.min` → `foo.min.js`, not `foo.js`).
78fn 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
85/// `require`-as-a-file: `p`, then `p.js`, then `p.json`. `.node` native addons
86/// are skipped (unsupported), matching the resolution order minus that step.
87fn 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
100/// `require`-as-a-directory: honor `package.json` `"exports"`/`"main"`, else
101/// `index.js` / `index.json`.
102fn 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
115/// `index.js` / `index.json` inside directory `p`.
116fn 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
126/// The relative entry path a `package.json` declares: the `"."` (or main-string)
127/// `"exports"` target if present, else `"main"`. Only the common `"exports"`
128/// shapes are handled — a bare string, or an object whose `"."` maps to a string
129/// or to `{ "require"/"default"/"node": "…" }`. Anything more exotic falls back
130/// to `"main"`, then to the directory's `index.js`.
131fn 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
142/// Resolve the `"exports"` field down to a single relative path for the `"."`
143/// (package root) entry, across the shapes CommonJS packages commonly ship.
144fn exports_main(exports: Option<&serde_json::Value>) -> Option<String> {
145    let exports = exports?;
146    // `"exports": "./index.js"` — a bare string is the `"."` target.
147    if let Some(s) = exports.as_str() {
148        return Some(s.to_string());
149    }
150    let obj = exports.as_object()?;
151    // Either a subpath map keyed by `"."`, or a bare conditions map at the root.
152    let target = obj.get(".").unwrap_or(exports);
153    condition_target(target)
154}
155
156/// Reduce an `"exports"` target — a string, or a conditions object — to a path,
157/// preferring the CommonJS-relevant conditions (`require`/`node`/`default`).
158fn 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
173/// Drop a leading `./` from a package-relative path.
174fn strip_dot_slash(s: &str) -> String {
175    s.strip_prefix("./").unwrap_or(s).to_string()
176}
177
178/// Resolve `spec` (already known to be a bare specifier) by walking parent
179/// directories from `from_dir`, checking `<dir>/node_modules/<spec>` at each
180/// level with the file-then-directory rules.
181fn resolve_bare(spec: &str, from_dir: &Path) -> Option<PathBuf> {
182    let mut dir = Some(from_dir);
183    while let Some(d) = dir {
184        // Skip a `node_modules/node_modules` descent.
185        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
198/// Resolve `spec` relative to `from_dir` to an absolute file path, or `None` if
199/// no file matches (core modules are handled earlier, by the caller).
200pub 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
215// ── loading / execution ──────────────────────────────────────────────────────
216
217/// `require(spec)` from `from_dir`: the single entry point shared by the
218/// top-level `require` builtin and the per-module `__cjs_require`. Returns the
219/// module's exports value.
220pub fn require(spec: &str, from_dir: &Path) -> Result<Value, String> {
221    // Core module: the native namespace value, never a file (mirrors the legacy
222    // `require` path — `require('events')` yields the EventEmitter ctor, etc.).
223    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    // A canonical absolute key so the same file required via different relative
229    // specifiers shares one cache entry.
230    let path = std::fs::canonicalize(&path).unwrap_or(path);
231    load_file(&path)
232}
233
234/// Load the resolved absolute file `path` (`.json` parses to its value; `.js`
235/// runs through the module wrapper) and return its exports, caching by path.
236fn load_file(path: &Path) -> Result<Value, String> {
237    if let Some(cached) = CACHE.with(|c| c.borrow().get(path).cloned()) {
238        // Re-read `.exports` — a cached module may have reassigned it.
239        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        // A JSON module's value IS the parsed data; cache a synthetic wrapper so
247        // repeated requires share it.
248        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    // Compile the Node module wrapper to obtain the wrapper FUNCTION value.
258    // A compile error is annotated with the offending file (Node does likewise).
259    let wrapper = compile_wrapper(&source)
260        .map_err(|e| format!("{e}\n    while loading {}", path.display()))?;
261
262    // `module = { exports: {} }`, plus the aliases the wrapper receives.
263    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 BEFORE running so a circular `require` back to this module observes
274    // the partial `exports`.
275    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
286/// A fresh `module` object holding `exports`.
287fn 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
295/// Read `module.exports` (falls back to `undefined` for a malformed module).
296fn 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
303/// Compile `<source>` wrapped in the Node module wrapper and return the wrapper
304/// FUNCTION value. Uses a fresh unique capture variable so a nested load in
305/// progress cannot clobber the value before it is read.
306fn 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    // A trailing newline before `})` guards a source ending in a `//` comment.
314    let wrapped = format!(
315        "var {var} = (function (exports, require, module, __dirname, __filename) {{\n{source}\n}});"
316    );
317    eval_binding(&wrapped, &var)
318}
319
320/// Compile+run a `var <name> = <expr>;` statement (expression statements pop
321/// their value, so a binding is how we capture an expression's result out of the
322/// shared module scope) and return the bound value. Runs on the LIVE host — no
323/// reset, no event-loop drain.
324fn 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
332/// Build a per-module `require` closure bound to `dir` (see module docs).
333fn 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
339/// The one-time compiled `require`-closure factory. `require.resolve` /
340/// `require.cache` are provided since some packages read them.
341fn 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
358/// An array of `depth` synthetic V8 CallSite objects for `Error.captureStackTrace`.
359/// Stack-introspection packages (e.g. `depd`) set `Error.prepareStackTrace` to a
360/// function that receives this array; the getters return neutral placeholders (no
361/// real frame info is available), which is enough for those packages to build
362/// their deprecation sites without throwing.
363pub 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}