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 /// Resolved `(specifier, from_dir)` to the CANONICAL absolute path it named,
41 /// so a repeated `require` of an already-loaded module costs one hash lookup
42 /// instead of walking `node_modules` and calling `canonicalize` again.
43 ///
44 /// Node keeps the same table (`Module._pathCache`) with the same
45 /// consequence: a file that appears after a specifier has already resolved
46 /// is not picked up by a later `require` of that specifier.
47 static PATH_CACHE: RefCell<HashMap<(String, PathBuf), PathBuf>> =
48 RefCell::new(HashMap::new());
49 /// Base directory the ENTRY script's top-level `require` resolves against
50 /// (the dir of `node app.js`, or cwd for `node -e`).
51 static ENTRY_DIR: RefCell<PathBuf> = RefCell::new(std::env::current_dir().unwrap_or_default());
52 /// The compiled per-module `require`-closure factory (see module docs),
53 /// minted once per host and reused for every module.
54 static FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
55 /// The compiled synthetic-CallSite-array factory (for `Error.captureStackTrace`
56 /// under a custom `Error.prepareStackTrace`), minted once per host.
57 static CALLSITE_FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
58}
59
60/// Clear all per-host loader state. Called from `host::reset_host` so a fresh
61/// eval (which rebuilds the heap) never reuses a stale heap handle.
62pub fn reset() {
63 CACHE.with(|c| c.borrow_mut().clear());
64 PATH_CACHE.with(|c| c.borrow_mut().clear());
65 FACTORY.with(|f| *f.borrow_mut() = None);
66 CALLSITE_FACTORY.with(|f| *f.borrow_mut() = None);
67 ENTRY_DIR.with(|d| *d.borrow_mut() = std::env::current_dir().unwrap_or_default());
68}
69
70/// The resolved filenames of every currently loaded module, in load order —
71/// the keys `require.cache` exposes.
72pub fn cache_keys() -> Vec<String> {
73 CACHE.with(|c| {
74 c.borrow()
75 .keys()
76 .map(|p| p.to_string_lossy().into_owned())
77 .collect()
78 })
79}
80
81/// The module object cached under the resolved filename `key`, if any.
82pub fn cache_get(key: &str) -> Option<Value> {
83 CACHE.with(|c| c.borrow().get(Path::new(key)).cloned())
84}
85
86/// Drop `key` from the module cache, so the next `require` of that file runs it
87/// again. This is what `delete require.cache[id]` must do to mean anything.
88pub fn cache_delete(key: &str) -> bool {
89 CACHE.with(|c| c.borrow_mut().remove(Path::new(key)).is_some())
90}
91
92/// Set the base directory the ENTRY script's `require` resolves against.
93pub fn set_entry_dir(dir: PathBuf) {
94 ENTRY_DIR.with(|d| *d.borrow_mut() = dir);
95}
96
97/// The ENTRY script's base directory.
98pub fn entry_dir() -> PathBuf {
99 ENTRY_DIR.with(|d| d.borrow().clone())
100}
101
102/// Install the CJS wrapper variables the ENTRY script sees.
103///
104/// A `require`d module already receives `exports`/`require`/`module`/`__dirname`
105/// /`__filename` as wrapper parameters (see `compile_wrapper`); the entry
106/// script used to receive none of them, so `typeof module` was `"undefined"`
107/// there and every UMD header took its browser branch. Node gives the entry
108/// script the same five names, with values that DEPEND ON THE ENTRY POINT:
109///
110/// | | `node f.js` | `node -e` | `node -` / piped |
111/// | --- | --- | --- | --- |
112/// | `__filename` | resolved abs path | `[eval]` | `[stdin]` |
113/// | `__dirname` | its directory | `.` | `.` |
114/// | `module.id` | `.` | `[eval]` | `[stdin]` |
115/// | `module.path` | its directory | `.` | `.` |
116///
117/// `module.filename` is `path.resolve(__filename)` in every case, so under `-e`
118/// it is `<cwd>/[eval]` — a path that does not exist, which is Node's own
119/// value. Measured on node v26.7.0.
120///
121/// `origin` is the `__filename` value: an absolute script path, or `[eval]` /
122/// `[stdin]` for the two source-on-the-command-line entry points.
123pub fn install_entry_globals(origin: &str) {
124 let from_file = origin != "[eval]" && origin != "[stdin]";
125 let (dirname, id) = if from_file {
126 let dir = Path::new(origin)
127 .parent()
128 .map(|p| p.to_string_lossy().into_owned())
129 .unwrap_or_else(|| ".".into());
130 (dir, ".".to_string())
131 } else {
132 (".".to_string(), origin.to_string())
133 };
134 let filename = crate::stdlib::path::resolve_one(origin);
135 let module = new_module(&id, &dirname, &filename);
136 let exports = module_exports(&module);
137 with_host(|h| {
138 let origin_str = h.new_str(origin.to_string());
139 let dirname = h.new_str(dirname);
140 h.set_global("__filename", origin_str);
141 h.set_global("__dirname", dirname);
142 h.set_global("module", module.clone());
143 // `require.main === module` in the entry script is the canonical
144 // "am I the program" test, and it read `undefined === <module>`
145 // because nothing ever set `main`. The ENTRY module is the value for
146 // every `require` in the process, not just this one, so it is recorded
147 // for the per-module closures too (`make_require` installs it).
148 // …but only when the program IS a module. `node -e` and a script on
149 // stdin run as a Script, not a CommonJS module, and node reports
150 // `require.main` as `undefined` for both.
151 if from_file {
152 h.set_builtin_static("require", "main", module.clone());
153 }
154 h.set_global("exports", exports.clone());
155 // Top-level `this`: the module's `exports` from a file (CommonJS
156 // module), `globalThis` from `-e` and from stdin (a Script). See
157 // `JsHost::set_top_this` for the measurements.
158 let top = if from_file {
159 exports
160 } else {
161 h.global_object()
162 };
163 h.set_top_this(top);
164 });
165}
166
167// ── resolution ───────────────────────────────────────────────────────────────
168
169/// Append `.ext` to a path (Node appends the extension, it does not replace an
170/// existing one — `foo.min` → `foo.min.js`, not `foo.js`).
171fn add_ext(p: &Path, ext: &str) -> PathBuf {
172 let mut s = p.as_os_str().to_owned();
173 s.push(".");
174 s.push(ext);
175 PathBuf::from(s)
176}
177
178/// `require`-as-a-file: `p`, then `p.js`, then `p.json`. `.node` native addons
179/// are skipped (unsupported), matching the resolution order minus that step.
180fn load_as_file(p: &Path) -> Option<PathBuf> {
181 if p.is_file() {
182 return Some(p.to_path_buf());
183 }
184 for ext in ["js", "json"] {
185 let cand = add_ext(p, ext);
186 if cand.is_file() {
187 return Some(cand);
188 }
189 }
190 None
191}
192
193/// `require`-as-a-directory: honor `package.json` `"exports"`/`"main"`, else
194/// `index.js` / `index.json`.
195fn load_as_dir(p: &Path) -> Option<PathBuf> {
196 let pkg = p.join("package.json");
197 if pkg.is_file() {
198 if let Some(main) = pkg_entry(&pkg) {
199 let mp = p.join(&main);
200 if let Some(f) = load_as_file(&mp).or_else(|| load_index(&mp)) {
201 return Some(f);
202 }
203 }
204 }
205 load_index(p)
206}
207
208/// `index.js` / `index.json` inside directory `p`.
209fn load_index(p: &Path) -> Option<PathBuf> {
210 for name in ["index.js", "index.json"] {
211 let cand = p.join(name);
212 if cand.is_file() {
213 return Some(cand);
214 }
215 }
216 None
217}
218
219/// The relative entry path a `package.json` declares: the `"."` (or main-string)
220/// `"exports"` target if present, else `"main"`. Only the common `"exports"`
221/// shapes are handled — a bare string, or an object whose `"."` maps to a string
222/// or to `{ "require"/"default"/"node": "…" }`. Anything more exotic falls back
223/// to `"main"`, then to the directory's `index.js`.
224fn pkg_entry(pkg: &Path) -> Option<String> {
225 let text = std::fs::read_to_string(pkg).ok()?;
226 let json: serde_json::Value = serde_json::from_str(&text).ok()?;
227 if let Some(e) = exports_main(json.get("exports")) {
228 return Some(strip_dot_slash(&e));
229 }
230 json.get("main")
231 .and_then(|m| m.as_str())
232 .map(strip_dot_slash)
233}
234
235/// Resolve the `"exports"` field down to a single relative path for the `"."`
236/// (package root) entry, across the shapes CommonJS packages commonly ship.
237fn exports_main(exports: Option<&serde_json::Value>) -> Option<String> {
238 let exports = exports?;
239 // `"exports": "./index.js"` — a bare string is the `"."` target.
240 if let Some(s) = exports.as_str() {
241 return Some(s.to_string());
242 }
243 let obj = exports.as_object()?;
244 // Either a subpath map keyed by `"."`, or a bare conditions map at the root.
245 let target = obj.get(".").unwrap_or(exports);
246 condition_target(target)
247}
248
249/// Reduce an `"exports"` target — a string, or a conditions object — to a path,
250/// preferring the CommonJS-relevant conditions (`require`/`node`/`default`).
251fn condition_target(target: &serde_json::Value) -> Option<String> {
252 if let Some(s) = target.as_str() {
253 return Some(s.to_string());
254 }
255 let obj = target.as_object()?;
256 for cond in ["require", "node", "default"] {
257 if let Some(v) = obj.get(cond) {
258 if let Some(s) = condition_target(v) {
259 return Some(s);
260 }
261 }
262 }
263 None
264}
265
266/// Drop a leading `./` from a package-relative path.
267fn strip_dot_slash(s: &str) -> String {
268 s.strip_prefix("./").unwrap_or(s).to_string()
269}
270
271/// Resolve `spec` (already known to be a bare specifier) by walking parent
272/// directories from `from_dir`, checking `<dir>/node_modules/<spec>` at each
273/// level with the file-then-directory rules.
274fn resolve_bare(spec: &str, from_dir: &Path) -> Option<PathBuf> {
275 let mut dir = Some(from_dir);
276 while let Some(d) = dir {
277 // Skip a `node_modules/node_modules` descent.
278 if d.file_name().is_some_and(|n| n == "node_modules") {
279 dir = d.parent();
280 continue;
281 }
282 let candidate = d.join("node_modules").join(spec);
283 if let Some(f) = load_as_file(&candidate).or_else(|| load_as_dir(&candidate)) {
284 return Some(f);
285 }
286 dir = d.parent();
287 }
288 None
289}
290
291/// Resolve `spec` relative to `from_dir` to an absolute file path, or `None` if
292/// no file matches (core modules are handled earlier, by the caller).
293pub fn resolve(spec: &str, from_dir: &Path) -> Option<PathBuf> {
294 let is_relative =
295 spec.starts_with("./") || spec.starts_with("../") || spec == "." || spec == "..";
296 let is_absolute = spec.starts_with('/');
297 if is_relative || is_absolute {
298 // NORMALIZED, not merely joined: `Path::join` keeps the `.` in
299 // `<dir>/./d.js`, and that string is what `require.resolve` returns and
300 // what keys the module cache — so `./d.js` and `d.js` from the same
301 // directory would be two cache entries of one file.
302 let joined = if is_absolute {
303 spec.to_string()
304 } else {
305 from_dir.join(spec).to_string_lossy().into_owned()
306 };
307 let base = PathBuf::from(crate::stdlib::path::resolve_one(&joined));
308 return load_as_file(&base).or_else(|| load_as_dir(&base));
309 }
310 resolve_bare(spec, from_dir)
311}
312
313// ── loading / execution ──────────────────────────────────────────────────────
314
315/// `require(spec)` from `from_dir`: the single entry point shared by the
316/// top-level `require` builtin and the per-module `__cjs_require`. Returns the
317/// module's exports value.
318pub fn require(spec: &str, from_dir: &Path) -> Result<Value, String> {
319 // Core module: the native namespace value, never a file (mirrors the legacy
320 // `require` path — `require('events')` yields the EventEmitter ctor, etc.).
321 if let Some(ns) = crate::stdlib::resolve(spec) {
322 return Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string()))));
323 }
324 // Resolution is filesystem work — a `node_modules` walk, a set of extension
325 // probes, then `canonicalize` — and a program that requires the same
326 // specifier in a loop paid all of it on every call even though the module
327 // itself was already loaded and cached. The answer is memoized per
328 // `(specifier, from_dir)`.
329 let key = (spec.to_string(), from_dir.to_path_buf());
330 if let Some(hit) = PATH_CACHE.with(|c| c.borrow().get(&key).cloned()) {
331 return load_file(&hit);
332 }
333 let path = resolve(spec, from_dir).ok_or_else(|| {
334 crate::host::plain_coded_error(
335 "Error",
336 "MODULE_NOT_FOUND",
337 &format!("Cannot find module '{spec}'"),
338 )
339 })?;
340 // A canonical absolute key so the same file required via different relative
341 // specifiers shares one cache entry.
342 let path = std::fs::canonicalize(&path).unwrap_or(path);
343 PATH_CACHE.with(|c| c.borrow_mut().insert(key, path.clone()));
344 load_file(&path)
345}
346
347/// Load the resolved absolute file `path` (`.json` parses to its value; `.js`
348/// runs through the module wrapper) and return its exports, caching by path.
349fn load_file(path: &Path) -> Result<Value, String> {
350 if let Some(cached) = CACHE.with(|c| c.borrow().get(path).cloned()) {
351 // Re-read `.exports` — a cached module may have reassigned it.
352 return Ok(module_exports(&cached));
353 }
354 if path.extension().is_some_and(|e| e == "json") {
355 let text = std::fs::read_to_string(path)
356 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
357 let src = with_host(|h| h.new_str(text));
358 let val = crate::builtins::call_builtin_function("JSON.parse", vec![src])?;
359 // A JSON module's value IS the parsed data; cache a synthetic wrapper so
360 // repeated requires share it.
361 let abs = path.to_string_lossy().into_owned();
362 let dir = path.parent().unwrap_or(Path::new("")).to_string_lossy();
363 let module = new_module(&abs, &dir, &abs);
364 with_host(|h| {
365 if let Some(JsObj::Object(p)) = h.get_mut(&module) {
366 p.insert("exports".to_string(), val.clone());
367 p.insert("loaded".to_string(), Value::Bool(true));
368 }
369 });
370 CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module));
371 return Ok(val);
372 }
373
374 let source = std::fs::read_to_string(path)
375 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
376 let dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
377
378 // Compile the Node module wrapper to obtain the wrapper FUNCTION value.
379 // A compile error is annotated with the offending file (Node does likewise).
380 let wrapper = compile_wrapper(&source)
381 .map_err(|e| format!("{e}\n while loading {}", path.display()))?;
382
383 // The `module` object, plus the aliases the wrapper receives. A required
384 // module's `id` IS its absolute filename (only the entry module's is `.`).
385 let abs = path.to_string_lossy().into_owned();
386 let module = new_module(&abs, &dir.to_string_lossy(), &abs);
387 let exports = module_exports(&module);
388 let require_fn = make_require(&dir)?;
389 let (dirname, filename) = with_host(|h| {
390 (
391 h.new_str(dir.to_string_lossy().to_string()),
392 h.new_str(path.to_string_lossy().to_string()),
393 )
394 });
395
396 // Cache BEFORE running so a circular `require` back to this module observes
397 // the partial `exports`.
398 CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module.clone()));
399
400 host::invoke(
401 &wrapper,
402 vec![exports, require_fn, module.clone(), dirname, filename],
403 None,
404 )?;
405 mark_loaded(&module);
406
407 Ok(module_exports(&module))
408}
409
410/// A fresh `module` object: `{ id, path, exports, filename, loaded, children,
411/// paths }`, in that key order.
412///
413/// The order is observable (`Object.keys(module)`) and this is node's. Only
414/// `exports` used to be present, so a module reading `module.id` or
415/// `module.filename` — both of which a bundler-emitted or `__dirname`-avoiding
416/// package does — got `undefined`.
417///
418/// `paths` is the `node_modules` chain from `dir` up to the root, the same walk
419/// `require` performs to resolve a bare specifier. `loaded` starts `false`; it
420/// is set once the body returns.
421fn new_module(id: &str, dir: &str, filename: &str) -> Value {
422 let mut node_modules: Vec<String> = Vec::new();
423 let mut cur = Some(Path::new(dir));
424 while let Some(d) = cur.filter(|d| !d.as_os_str().is_empty()) {
425 node_modules.push(d.join("node_modules").to_string_lossy().into_owned());
426 cur = d.parent();
427 }
428 with_host(|h| {
429 let exports = h.new_object(indexmap::IndexMap::new());
430 let mut props = indexmap::IndexMap::new();
431 props.insert("id".to_string(), h.new_str(id.to_string()));
432 props.insert("path".to_string(), h.new_str(dir.to_string()));
433 props.insert("exports".to_string(), exports);
434 props.insert("filename".to_string(), h.new_str(filename.to_string()));
435 props.insert("loaded".to_string(), Value::Bool(false));
436 let children = h.new_array(Vec::new());
437 props.insert("children".to_string(), children);
438 let paths: Vec<Value> = node_modules.into_iter().map(|p| h.new_str(p)).collect();
439 let paths = h.new_array(paths);
440 props.insert("paths".to_string(), paths);
441 h.new_object(props)
442 })
443}
444
445/// Flip `module.loaded` once the body has run, as Node's loader does.
446fn mark_loaded(module: &Value) {
447 with_host(|h| {
448 if let Some(JsObj::Object(p)) = h.get_mut(module) {
449 p.insert("loaded".to_string(), Value::Bool(true));
450 }
451 });
452}
453
454/// Read `module.exports` (falls back to `undefined` for a malformed module).
455fn module_exports(module: &Value) -> Value {
456 with_host(|h| match h.get(module) {
457 Some(JsObj::Object(p)) => p.get("exports").cloned().unwrap_or(Value::Undef),
458 _ => Value::Undef,
459 })
460}
461
462/// Compile `<source>` wrapped in the Node module wrapper and return the wrapper
463/// FUNCTION value.
464fn compile_wrapper(source: &str) -> Result<Value, String> {
465 // A trailing newline before `})` guards a source ending in a `//` comment.
466 eval_binding(&format!(
467 "(function (exports, require, module, __dirname, __filename) {{\n{source}\n}})"
468 ))
469}
470
471/// Compile+run a single JS expression on the LIVE host — no reset, no
472/// event-loop drain — and return its value.
473///
474/// This delegates to `crate::eval_in_global_scope`, the frontend's one
475/// runtime-source evaluator, and two things changed with it. The wrapper used to
476/// be compiled as `var __cjs_wN = (function …);` and read back out of the scope
477/// with `read_name`, because a bare expression statement pops its value; a
478/// completion-value compile returns the expression directly, so the capture
479/// variable and its uniquifying counter are gone. And the run used to happen on
480/// the CALLER's frame, which let a module body see the locals of whatever
481/// function called `require`: measured against node v26.7.0,
482/// `function outer(){ let secret = 1; return require('./m.js'); }` with `m.js` =
483/// `module.exports = typeof secret` is `"undefined"` there and was `"number"`
484/// here.
485fn eval_binding(src: &str) -> Result<Value, String> {
486 crate::eval_in_global_scope(src)
487}
488
489/// Build a per-module `require` closure bound to `dir` (see module docs).
490fn make_require(dir: &Path) -> Result<Value, String> {
491 let factory = factory()?;
492 let dir_str = with_host(|h| h.new_str(dir.to_string_lossy().to_string()));
493 let req = host::invoke(&factory, vec![dir_str], None)?;
494 // Every `require` in the process reports the same `main` — the ENTRY
495 // module — which is what `require.main === module` tests against.
496 if let Some(main) = with_host(|h| h.builtin_static("require", "main")) {
497 // `req` is a FUNCTION value, so its properties live in the fn-prop side
498 // table, not in an object property map.
499 with_host(|h| h.set_fn_prop(&req, "main", main));
500 }
501 Ok(req)
502}
503
504/// The one-time compiled `require`-closure factory. `require.resolve` /
505/// `require.cache` are provided since some packages read them.
506fn factory() -> Result<Value, String> {
507 if let Some(f) = FACTORY.with(|f| f.borrow().clone()) {
508 return Ok(f);
509 }
510 let src = "(function (__cjs_dir) {\n\
511 var req = function (spec) { return __cjs_require(spec, __cjs_dir); };\n\
512 req.resolve = function (spec) { return __cjs_resolve(spec, __cjs_dir); };\n\
513 req.cache = __cjs_cache;\n\
514 req.main = undefined;\n\
515 req.extensions = {};\n\
516 return req;\n\
517 });";
518 let f = eval_binding(src)?;
519 FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
520 Ok(f)
521}
522
523/// An array of `depth` synthetic V8 CallSite objects for `Error.captureStackTrace`.
524/// Stack-introspection packages (e.g. `depd`) set `Error.prepareStackTrace` to a
525/// function that receives this array; the getters return neutral placeholders (no
526/// real frame info is available), which is enough for those packages to build
527/// their deprecation sites without throwing.
528pub fn callsite_stack(depth: usize) -> Result<Value, String> {
529 let factory = if let Some(f) = CALLSITE_FACTORY.with(|f| f.borrow().clone()) {
530 f
531 } else {
532 let src = "(function (n) {\n\
533 var a = [];\n\
534 for (var i = 0; i < n; i++) {\n\
535 a.push({\n\
536 getFileName: function () { return null; },\n\
537 getLineNumber: function () { return 0; },\n\
538 getColumnNumber: function () { return 0; },\n\
539 getFunctionName: function () { return null; },\n\
540 getMethodName: function () { return null; },\n\
541 getTypeName: function () { return null; },\n\
542 getThis: function () { return undefined; },\n\
543 isNative: function () { return false; },\n\
544 isEval: function () { return false; },\n\
545 toString: function () { return '<anonymous>'; }\n\
546 });\n\
547 }\n\
548 return a;\n\
549 });";
550 let f = eval_binding(src)?;
551 CALLSITE_FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
552 f
553 };
554 host::invoke(&factory, vec![Value::Float(depth as f64)], None)
555}