node-js 0.1.12

JavaScript as a fusevm frontend: a lexer/parser and compiler to fusevm::Chunk on a JsHost object heap, with no bespoke VM or JIT
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
//! CommonJS module loader.
//!
//! Node's `require()` semantics, layered on the existing engine — no bespoke VM
//! primitive. A `.js` file is wrapped in the canonical Node module wrapper
//! `(function (exports, require, module, __dirname, __filename) { … })`, compiled
//! through the ordinary `compile` → `load_merged` path to obtain the wrapper
//! FUNCTION value, then `host::invoke`d with a fresh `module = { exports: {} }`.
//! Whatever the body assigns to `module.exports` (or hangs off `exports`) is the
//! module's value; it is cached by resolved absolute path so a second `require`
//! of the same file returns the identical object and circular requires observe
//! the partially-filled `exports`.
//!
//! Core modules (`fs`, `path`, `http`, …) short-circuit to their native
//! `JsObj::Builtin` namespace (see `stdlib::resolve`) and are never read from
//! disk. Everything else — relative paths, JSON files, and bare `node_modules`
//! packages with their `package.json` `"exports"`/`"main"` and `index.js`
//! fallbacks — resolves on the real filesystem and runs the genuine, unmodified
//! source.
//!
//! Per-module `require` is a real JS closure that bakes in the defining module's
//! directory, so a `require(...)` deferred inside a function called much later
//! still resolves against the module that defined it (a single global
//! "current dir" would resolve against the wrong module). The closure is minted
//! by a one-time compiled factory (`FACTORY`) invoked with the directory string;
//! it dispatches back into this loader through the `__cjs_require` /
//! `__cjs_resolve` global native builtins.

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! {
    /// Require cache: resolved absolute path → the `module` object (its `.exports`
    /// is re-read on every hit, matching Node — `module.exports = X` reassignment
    /// is observed by later requires).
    static CACHE: RefCell<HashMap<PathBuf, Value>> = RefCell::new(HashMap::new());
    /// Resolved `(specifier, from_dir)` to the CANONICAL absolute path it named,
    /// so a repeated `require` of an already-loaded module costs one hash lookup
    /// instead of walking `node_modules` and calling `canonicalize` again.
    ///
    /// Node keeps the same table (`Module._pathCache`) with the same
    /// consequence: a file that appears after a specifier has already resolved
    /// is not picked up by a later `require` of that specifier.
    static PATH_CACHE: RefCell<HashMap<(String, PathBuf), PathBuf>> =
        RefCell::new(HashMap::new());
    /// Base directory the ENTRY script's top-level `require` resolves against
    /// (the dir of `node app.js`, or cwd for `node -e`).
    static ENTRY_DIR: RefCell<PathBuf> = RefCell::new(std::env::current_dir().unwrap_or_default());
    /// The compiled per-module `require`-closure factory (see module docs),
    /// minted once per host and reused for every module.
    static FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
    /// The compiled synthetic-CallSite-array factory (for `Error.captureStackTrace`
    /// under a custom `Error.prepareStackTrace`), minted once per host.
    static CALLSITE_FACTORY: RefCell<Option<Value>> = const { RefCell::new(None) };
}

/// Clear all per-host loader state. Called from `host::reset_host` so a fresh
/// eval (which rebuilds the heap) never reuses a stale heap handle.
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());
}

/// The resolved filenames of every currently loaded module, in load order —
/// the keys `require.cache` exposes.
pub fn cache_keys() -> Vec<String> {
    CACHE.with(|c| {
        c.borrow()
            .keys()
            .map(|p| p.to_string_lossy().into_owned())
            .collect()
    })
}

/// The module object cached under the resolved filename `key`, if any.
pub fn cache_get(key: &str) -> Option<Value> {
    CACHE.with(|c| c.borrow().get(Path::new(key)).cloned())
}

/// Drop `key` from the module cache, so the next `require` of that file runs it
/// again. This is what `delete require.cache[id]` must do to mean anything.
pub fn cache_delete(key: &str) -> bool {
    CACHE.with(|c| c.borrow_mut().remove(Path::new(key)).is_some())
}

/// Set the base directory the ENTRY script's `require` resolves against.
pub fn set_entry_dir(dir: PathBuf) {
    ENTRY_DIR.with(|d| *d.borrow_mut() = dir);
}

/// The ENTRY script's base directory.
pub fn entry_dir() -> PathBuf {
    ENTRY_DIR.with(|d| d.borrow().clone())
}

/// Install the CJS wrapper variables the ENTRY script sees.
///
/// A `require`d module already receives `exports`/`require`/`module`/`__dirname`
/// /`__filename` as wrapper parameters (see `compile_wrapper`); the entry
/// script used to receive none of them, so `typeof module` was `"undefined"`
/// there and every UMD header took its browser branch. Node gives the entry
/// script the same five names, with values that DEPEND ON THE ENTRY POINT:
///
/// | | `node f.js` | `node -e` | `node -` / piped |
/// | --- | --- | --- | --- |
/// | `__filename` | resolved abs path | `[eval]` | `[stdin]` |
/// | `__dirname` | its directory | `.` | `.` |
/// | `module.id` | `.` | `[eval]` | `[stdin]` |
/// | `module.path` | its directory | `.` | `.` |
///
/// `module.filename` is `path.resolve(__filename)` in every case, so under `-e`
/// it is `<cwd>/[eval]` — a path that does not exist, which is Node's own
/// value. Measured on node v26.7.0.
///
/// `origin` is the `__filename` value: an absolute script path, or `[eval]` /
/// `[stdin]` for the two source-on-the-command-line entry points.
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());
        // `require.main === module` in the entry script is the canonical
        // "am I the program" test, and it read `undefined === <module>`
        // because nothing ever set `main`. The ENTRY module is the value for
        // every `require` in the process, not just this one, so it is recorded
        // for the per-module closures too (`make_require` installs it).
        // …but only when the program IS a module. `node -e` and a script on
        // stdin run as a Script, not a CommonJS module, and node reports
        // `require.main` as `undefined` for both.
        if from_file {
            h.set_builtin_static("require", "main", module.clone());
        }
        h.set_global("exports", exports.clone());
        // Top-level `this`: the module's `exports` from a file (CommonJS
        // module), `globalThis` from `-e` and from stdin (a Script). See
        // `JsHost::set_top_this` for the measurements.
        let top = if from_file {
            exports
        } else {
            h.global_object()
        };
        h.set_top_this(top);
    });
}

// ── resolution ───────────────────────────────────────────────────────────────

/// Append `.ext` to a path (Node appends the extension, it does not replace an
/// existing one — `foo.min` → `foo.min.js`, not `foo.js`).
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)
}

/// `require`-as-a-file: `p`, then `p.js`, then `p.json`. `.node` native addons
/// are skipped (unsupported), matching the resolution order minus that step.
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
}

/// `require`-as-a-directory: honor `package.json` `"exports"`/`"main"`, else
/// `index.js` / `index.json`.
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)
}

/// `index.js` / `index.json` inside directory `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
}

/// The relative entry path a `package.json` declares: the `"."` (or main-string)
/// `"exports"` target if present, else `"main"`. Only the common `"exports"`
/// shapes are handled — a bare string, or an object whose `"."` maps to a string
/// or to `{ "require"/"default"/"node": "…" }`. Anything more exotic falls back
/// to `"main"`, then to the directory's `index.js`.
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)
}

/// Resolve the `"exports"` field down to a single relative path for the `"."`
/// (package root) entry, across the shapes CommonJS packages commonly ship.
fn exports_main(exports: Option<&serde_json::Value>) -> Option<String> {
    let exports = exports?;
    // `"exports": "./index.js"` — a bare string is the `"."` target.
    if let Some(s) = exports.as_str() {
        return Some(s.to_string());
    }
    let obj = exports.as_object()?;
    // Either a subpath map keyed by `"."`, or a bare conditions map at the root.
    let target = obj.get(".").unwrap_or(exports);
    condition_target(target)
}

/// Reduce an `"exports"` target — a string, or a conditions object — to a path,
/// preferring the CommonJS-relevant conditions (`require`/`node`/`default`).
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
}

/// Drop a leading `./` from a package-relative path.
fn strip_dot_slash(s: &str) -> String {
    s.strip_prefix("./").unwrap_or(s).to_string()
}

/// Resolve `spec` (already known to be a bare specifier) by walking parent
/// directories from `from_dir`, checking `<dir>/node_modules/<spec>` at each
/// level with the file-then-directory rules.
fn resolve_bare(spec: &str, from_dir: &Path) -> Option<PathBuf> {
    let mut dir = Some(from_dir);
    while let Some(d) = dir {
        // Skip a `node_modules/node_modules` descent.
        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
}

/// Resolve `spec` relative to `from_dir` to an absolute file path, or `None` if
/// no file matches (core modules are handled earlier, by the caller).
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 {
        // NORMALIZED, not merely joined: `Path::join` keeps the `.` in
        // `<dir>/./d.js`, and that string is what `require.resolve` returns and
        // what keys the module cache — so `./d.js` and `d.js` from the same
        // directory would be two cache entries of one file.
        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)
}

// ── loading / execution ──────────────────────────────────────────────────────

/// `require(spec)` from `from_dir`: the single entry point shared by the
/// top-level `require` builtin and the per-module `__cjs_require`. Returns the
/// module's exports value.
pub fn require(spec: &str, from_dir: &Path) -> Result<Value, String> {
    // Core module: the native namespace value, never a file (mirrors the legacy
    // `require` path — `require('events')` yields the EventEmitter ctor, etc.).
    if let Some(v) = crate::stdlib::data_module(spec) {
        return Ok(v);
    }
    if let Some(ns) = crate::stdlib::resolve(spec) {
        return Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string()))));
    }
    // Resolution is filesystem work — a `node_modules` walk, a set of extension
    // probes, then `canonicalize` — and a program that requires the same
    // specifier in a loop paid all of it on every call even though the module
    // itself was already loaded and cached. The answer is memoized per
    // `(specifier, from_dir)`.
    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}'"),
        )
    })?;
    // A canonical absolute key so the same file required via different relative
    // specifiers shares one cache entry.
    let path = std::fs::canonicalize(&path).unwrap_or(path);
    PATH_CACHE.with(|c| c.borrow_mut().insert(key, path.clone()));
    load_file(&path)
}

/// Load the resolved absolute file `path` (`.json` parses to its value; `.js`
/// runs through the module wrapper) and return its exports, caching by path.
fn load_file(path: &Path) -> Result<Value, String> {
    if let Some(cached) = CACHE.with(|c| c.borrow().get(path).cloned()) {
        // Re-read `.exports` — a cached module may have reassigned it.
        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])?;
        // A JSON module's value IS the parsed data; cache a synthetic wrapper so
        // repeated requires share it.
        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();

    // Compile the Node module wrapper to obtain the wrapper FUNCTION value.
    // A compile error is annotated with the offending file (Node does likewise).
    let wrapper = compile_wrapper(&source)
        .map_err(|e| format!("{e}\n    while loading {}", path.display()))?;

    // The `module` object, plus the aliases the wrapper receives. A required
    // module's `id` IS its absolute filename (only the entry module's is `.`).
    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 BEFORE running so a circular `require` back to this module observes
    // the partial `exports`.
    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))
}

/// A fresh `module` object: `{ id, path, exports, filename, loaded, children,
/// paths }`, in that key order.
///
/// The order is observable (`Object.keys(module)`) and this is node's. Only
/// `exports` used to be present, so a module reading `module.id` or
/// `module.filename` — both of which a bundler-emitted or `__dirname`-avoiding
/// package does — got `undefined`.
///
/// `paths` is the `node_modules` chain from `dir` up to the root, the same walk
/// `require` performs to resolve a bare specifier. `loaded` starts `false`; it
/// is set once the body returns.
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));
        // `module.parent` is long deprecated but PRESENT: node reports `null`
        // for a file the loader reached directly, and code still tests
        // `if (!module.parent)` to detect "run as the entry point". The key was
        // missing entirely, so `'parent' in module` was false.
        let null = h.null();
        props.insert("parent".to_string(), null);
        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);
        let obj = h.new_object(props);
        // `parent` is present but NOT enumerable: `Object.keys(module)` does not
        // list it, while `'parent' in module` is true. Adding it as an ordinary
        // property changed the key order the es_parity module tests pin.
        h.hide_prop(&obj, "parent");
        obj
    })
}

/// The directories `require.resolve` would search for `spec`, in order.
///
/// A RELATIVE specifier resolves against one directory — the requiring one — so
/// node reports just that. A bare package name walks the `node_modules` chain
/// up to the root.
pub fn resolve_paths(spec: &str, from_dir: &std::path::Path) -> Vec<String> {
    if spec.starts_with('.') || spec.starts_with('/') {
        return vec![from_dir.to_string_lossy().into_owned()];
    }
    let mut out = Vec::new();
    let mut cur = Some(from_dir);
    while let Some(d) = cur {
        out.push(d.join("node_modules").to_string_lossy().into_owned());
        cur = d.parent();
    }
    out
}

/// Flip `module.loaded` once the body has run, as Node's loader does.
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));
        }
    });
}

/// Read `module.exports` (falls back to `undefined` for a malformed module).
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,
    })
}

/// Compile `<source>` wrapped in the Node module wrapper and return the wrapper
/// FUNCTION value.
fn compile_wrapper(source: &str) -> Result<Value, String> {
    // A module may open with a hashbang line, which is only a comment at the
    // start of the TEXT; inside the wrapper it would not be, so it becomes a
    // `//` comment of the same length.
    let source = match source.strip_prefix("#!") {
        Some(rest) => format!("//{rest}"),
        None => source.to_string(),
    };
    // A trailing newline before `})` guards a source ending in a `//` comment.
    eval_binding(&format!(
        "(function (exports, require, module, __dirname, __filename) {{\n{source}\n}})"
    ))
}

/// Compile+run a single JS expression on the LIVE host — no reset, no
/// event-loop drain — and return its value.
///
/// This delegates to `crate::eval_in_global_scope`, the frontend's one
/// runtime-source evaluator, and two things changed with it. The wrapper used to
/// be compiled as `var __cjs_wN = (function …);` and read back out of the scope
/// with `read_name`, because a bare expression statement pops its value; a
/// completion-value compile returns the expression directly, so the capture
/// variable and its uniquifying counter are gone. And the run used to happen on
/// the CALLER's frame, which let a module body see the locals of whatever
/// function called `require`: measured against node v26.7.0,
/// `function outer(){ let secret = 1; return require('./m.js'); }` with `m.js` =
/// `module.exports = typeof secret` is `"undefined"` there and was `"number"`
/// here.
fn eval_binding(src: &str) -> Result<Value, String> {
    crate::eval_in_global_scope(src)
}

/// Build a per-module `require` closure bound to `dir` (see module docs).
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)?;
    // Every `require` in the process reports the same `main` — the ENTRY
    // module — which is what `require.main === module` tests against.
    if let Some(main) = with_host(|h| h.builtin_static("require", "main")) {
        // `req` is a FUNCTION value, so its properties live in the fn-prop side
        // table, not in an object property map.
        with_host(|h| h.set_fn_prop(&req, "main", main));
    }
    Ok(req)
}

/// The one-time compiled `require`-closure factory. `require.resolve` /
/// `require.cache` are provided since some packages read them.
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)
}

/// An array of `depth` synthetic V8 CallSite objects for `Error.captureStackTrace`.
/// Stack-introspection packages (e.g. `depd`) set `Error.prepareStackTrace` to a
/// function that receives this array; the getters return neutral placeholders (no
/// real frame info is available), which is enough for those packages to build
/// their deprecation sites without throwing.
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)
}