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(v) = crate::stdlib::data_module(spec) {
322 return Ok(v);
323 }
324 if let Some(ns) = crate::stdlib::resolve(spec) {
325 return Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string()))));
326 }
327 // Resolution is filesystem work — a `node_modules` walk, a set of extension
328 // probes, then `canonicalize` — and a program that requires the same
329 // specifier in a loop paid all of it on every call even though the module
330 // itself was already loaded and cached. The answer is memoized per
331 // `(specifier, from_dir)`.
332 let key = (spec.to_string(), from_dir.to_path_buf());
333 if let Some(hit) = PATH_CACHE.with(|c| c.borrow().get(&key).cloned()) {
334 return load_file(&hit);
335 }
336 let path = resolve(spec, from_dir).ok_or_else(|| {
337 crate::host::plain_coded_error(
338 "Error",
339 "MODULE_NOT_FOUND",
340 &format!("Cannot find module '{spec}'"),
341 )
342 })?;
343 // A canonical absolute key so the same file required via different relative
344 // specifiers shares one cache entry.
345 let path = std::fs::canonicalize(&path).unwrap_or(path);
346 PATH_CACHE.with(|c| c.borrow_mut().insert(key, path.clone()));
347 load_file(&path)
348}
349
350/// Load the resolved absolute file `path` (`.json` parses to its value; `.js`
351/// runs through the module wrapper) and return its exports, caching by path.
352fn load_file(path: &Path) -> Result<Value, String> {
353 if let Some(cached) = CACHE.with(|c| c.borrow().get(path).cloned()) {
354 // Re-read `.exports` — a cached module may have reassigned it.
355 return Ok(module_exports(&cached));
356 }
357 if path.extension().is_some_and(|e| e == "json") {
358 let text = std::fs::read_to_string(path)
359 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
360 let src = with_host(|h| h.new_str(text));
361 let val = crate::builtins::call_builtin_function("JSON.parse", vec![src])?;
362 // A JSON module's value IS the parsed data; cache a synthetic wrapper so
363 // repeated requires share it.
364 let abs = path.to_string_lossy().into_owned();
365 let dir = path.parent().unwrap_or(Path::new("")).to_string_lossy();
366 let module = new_module(&abs, &dir, &abs);
367 with_host(|h| {
368 if let Some(JsObj::Object(p)) = h.get_mut(&module) {
369 p.insert("exports".to_string(), val.clone());
370 p.insert("loaded".to_string(), Value::Bool(true));
371 }
372 });
373 CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module));
374 return Ok(val);
375 }
376
377 let source = std::fs::read_to_string(path)
378 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
379 let dir = path.parent().map(Path::to_path_buf).unwrap_or_default();
380
381 // Compile the Node module wrapper to obtain the wrapper FUNCTION value.
382 // A compile error is annotated with the offending file (Node does likewise).
383 let wrapper = compile_wrapper(&source)
384 .map_err(|e| format!("{e}\n while loading {}", path.display()))?;
385
386 // The `module` object, plus the aliases the wrapper receives. A required
387 // module's `id` IS its absolute filename (only the entry module's is `.`).
388 let abs = path.to_string_lossy().into_owned();
389 let module = new_module(&abs, &dir.to_string_lossy(), &abs);
390 let exports = module_exports(&module);
391 let require_fn = make_require(&dir)?;
392 let (dirname, filename) = with_host(|h| {
393 (
394 h.new_str(dir.to_string_lossy().to_string()),
395 h.new_str(path.to_string_lossy().to_string()),
396 )
397 });
398
399 // Cache BEFORE running so a circular `require` back to this module observes
400 // the partial `exports`.
401 CACHE.with(|c| c.borrow_mut().insert(path.to_path_buf(), module.clone()));
402
403 host::invoke(
404 &wrapper,
405 vec![exports, require_fn, module.clone(), dirname, filename],
406 None,
407 )?;
408 mark_loaded(&module);
409
410 Ok(module_exports(&module))
411}
412
413/// A fresh `module` object: `{ id, path, exports, filename, loaded, children,
414/// paths }`, in that key order.
415///
416/// The order is observable (`Object.keys(module)`) and this is node's. Only
417/// `exports` used to be present, so a module reading `module.id` or
418/// `module.filename` — both of which a bundler-emitted or `__dirname`-avoiding
419/// package does — got `undefined`.
420///
421/// `paths` is the `node_modules` chain from `dir` up to the root, the same walk
422/// `require` performs to resolve a bare specifier. `loaded` starts `false`; it
423/// is set once the body returns.
424fn new_module(id: &str, dir: &str, filename: &str) -> Value {
425 let mut node_modules: Vec<String> = Vec::new();
426 let mut cur = Some(Path::new(dir));
427 while let Some(d) = cur.filter(|d| !d.as_os_str().is_empty()) {
428 node_modules.push(d.join("node_modules").to_string_lossy().into_owned());
429 cur = d.parent();
430 }
431 with_host(|h| {
432 let exports = h.new_object(indexmap::IndexMap::new());
433 let mut props = indexmap::IndexMap::new();
434 props.insert("id".to_string(), h.new_str(id.to_string()));
435 props.insert("path".to_string(), h.new_str(dir.to_string()));
436 props.insert("exports".to_string(), exports);
437 props.insert("filename".to_string(), h.new_str(filename.to_string()));
438 props.insert("loaded".to_string(), Value::Bool(false));
439 // `module.parent` is long deprecated but PRESENT: node reports `null`
440 // for a file the loader reached directly, and code still tests
441 // `if (!module.parent)` to detect "run as the entry point". The key was
442 // missing entirely, so `'parent' in module` was false.
443 let null = h.null();
444 props.insert("parent".to_string(), null);
445 let children = h.new_array(Vec::new());
446 props.insert("children".to_string(), children);
447 let paths: Vec<Value> = node_modules.into_iter().map(|p| h.new_str(p)).collect();
448 let paths = h.new_array(paths);
449 props.insert("paths".to_string(), paths);
450 let obj = h.new_object(props);
451 // `parent` is present but NOT enumerable: `Object.keys(module)` does not
452 // list it, while `'parent' in module` is true. Adding it as an ordinary
453 // property changed the key order the es_parity module tests pin.
454 h.hide_prop(&obj, "parent");
455 obj
456 })
457}
458
459/// The directories `require.resolve` would search for `spec`, in order.
460///
461/// A RELATIVE specifier resolves against one directory — the requiring one — so
462/// node reports just that. A bare package name walks the `node_modules` chain
463/// up to the root.
464pub fn resolve_paths(spec: &str, from_dir: &std::path::Path) -> Vec<String> {
465 if spec.starts_with('.') || spec.starts_with('/') {
466 return vec![from_dir.to_string_lossy().into_owned()];
467 }
468 let mut out = Vec::new();
469 let mut cur = Some(from_dir);
470 while let Some(d) = cur {
471 out.push(d.join("node_modules").to_string_lossy().into_owned());
472 cur = d.parent();
473 }
474 out
475}
476
477/// Flip `module.loaded` once the body has run, as Node's loader does.
478fn mark_loaded(module: &Value) {
479 with_host(|h| {
480 if let Some(JsObj::Object(p)) = h.get_mut(module) {
481 p.insert("loaded".to_string(), Value::Bool(true));
482 }
483 });
484}
485
486/// Read `module.exports` (falls back to `undefined` for a malformed module).
487fn module_exports(module: &Value) -> Value {
488 with_host(|h| match h.get(module) {
489 Some(JsObj::Object(p)) => p.get("exports").cloned().unwrap_or(Value::Undef),
490 _ => Value::Undef,
491 })
492}
493
494/// Compile `<source>` wrapped in the Node module wrapper and return the wrapper
495/// FUNCTION value.
496fn compile_wrapper(source: &str) -> Result<Value, String> {
497 // A module may open with a hashbang line, which is only a comment at the
498 // start of the TEXT; inside the wrapper it would not be, so it becomes a
499 // `//` comment of the same length.
500 let source = match source.strip_prefix("#!") {
501 Some(rest) => format!("//{rest}"),
502 None => source.to_string(),
503 };
504 // A trailing newline before `})` guards a source ending in a `//` comment.
505 eval_binding(&format!(
506 "(function (exports, require, module, __dirname, __filename) {{\n{source}\n}})"
507 ))
508}
509
510/// Compile+run a single JS expression on the LIVE host — no reset, no
511/// event-loop drain — and return its value.
512///
513/// This delegates to `crate::eval_in_global_scope`, the frontend's one
514/// runtime-source evaluator, and two things changed with it. The wrapper used to
515/// be compiled as `var __cjs_wN = (function …);` and read back out of the scope
516/// with `read_name`, because a bare expression statement pops its value; a
517/// completion-value compile returns the expression directly, so the capture
518/// variable and its uniquifying counter are gone. And the run used to happen on
519/// the CALLER's frame, which let a module body see the locals of whatever
520/// function called `require`: measured against node v26.7.0,
521/// `function outer(){ let secret = 1; return require('./m.js'); }` with `m.js` =
522/// `module.exports = typeof secret` is `"undefined"` there and was `"number"`
523/// here.
524fn eval_binding(src: &str) -> Result<Value, String> {
525 crate::eval_in_global_scope(src)
526}
527
528/// Build a per-module `require` closure bound to `dir` (see module docs).
529fn make_require(dir: &Path) -> Result<Value, String> {
530 let factory = factory()?;
531 let dir_str = with_host(|h| h.new_str(dir.to_string_lossy().to_string()));
532 let req = host::invoke(&factory, vec![dir_str], None)?;
533 // Every `require` in the process reports the same `main` — the ENTRY
534 // module — which is what `require.main === module` tests against.
535 if let Some(main) = with_host(|h| h.builtin_static("require", "main")) {
536 // `req` is a FUNCTION value, so its properties live in the fn-prop side
537 // table, not in an object property map.
538 with_host(|h| h.set_fn_prop(&req, "main", main));
539 }
540 Ok(req)
541}
542
543/// The one-time compiled `require`-closure factory. `require.resolve` /
544/// `require.cache` are provided since some packages read them.
545fn factory() -> Result<Value, String> {
546 if let Some(f) = FACTORY.with(|f| f.borrow().clone()) {
547 return Ok(f);
548 }
549 let src = "(function (__cjs_dir) {\n\
550 var req = function (spec) { return __cjs_require(spec, __cjs_dir); };\n\
551 req.resolve = function (spec) { return __cjs_resolve(spec, __cjs_dir); };\n\
552 req.cache = __cjs_cache;\n\
553 req.main = undefined;\n\
554 req.extensions = {};\n\
555 return req;\n\
556 });";
557 let f = eval_binding(src)?;
558 FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
559 Ok(f)
560}
561
562/// An array of `depth` synthetic V8 CallSite objects for `Error.captureStackTrace`.
563/// Stack-introspection packages (e.g. `depd`) set `Error.prepareStackTrace` to a
564/// function that receives this array; the getters return neutral placeholders (no
565/// real frame info is available), which is enough for those packages to build
566/// their deprecation sites without throwing.
567pub fn callsite_stack(depth: usize) -> Result<Value, String> {
568 let factory = if let Some(f) = CALLSITE_FACTORY.with(|f| f.borrow().clone()) {
569 f
570 } else {
571 let src = "(function (n) {\n\
572 var a = [];\n\
573 for (var i = 0; i < n; i++) {\n\
574 a.push({\n\
575 getFileName: function () { return null; },\n\
576 getLineNumber: function () { return 0; },\n\
577 getColumnNumber: function () { return 0; },\n\
578 getFunctionName: function () { return null; },\n\
579 getMethodName: function () { return null; },\n\
580 getTypeName: function () { return null; },\n\
581 getThis: function () { return undefined; },\n\
582 isNative: function () { return false; },\n\
583 isEval: function () { return false; },\n\
584 toString: function () { return '<anonymous>'; }\n\
585 });\n\
586 }\n\
587 return a;\n\
588 });";
589 let f = eval_binding(src)?;
590 CALLSITE_FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
591 f
592 };
593 host::invoke(&factory, vec![Value::Float(depth as f64)], None)
594}