Skip to main content

ferrijs_std/
modules.rs

1//! The Node / web modules this crate serves, and the one table a host
2//! registers them from.
3//!
4//! Each entry carries its specifiers, the `ModuleDef` the ES loader
5//! declares, and the object `require()` hands back — so a host cannot
6//! wire up the import form and forget the CommonJS one, and cannot serve
7//! a module this crate does not know about.
8
9use rquickjs::module::{Declarations, Exports, ModuleDef};
10use rquickjs::{Ctx, Module, Object, Value};
11
12/// Declare `names` on a module.
13fn declare_all(decl: &Declarations<'_>, names: &[&str]) -> rquickjs::Result<()> {
14  for name in names {
15    decl.declare(*name)?;
16  }
17  Ok(())
18}
19
20/// Copy `names` from a namespace object into the module's ES exports.
21fn export_from<'js>(exports: &Exports<'js>, ns: &Object<'js>, names: &[&str]) -> rquickjs::Result<()> {
22  for name in names {
23    exports.export(*name, ns.get::<_, Value<'js>>(*name)?)?;
24  }
25  Ok(())
26}
27
28/// `import path from 'node:path'` — pure-computation POSIX-style subset
29/// (the sandbox is always a unix-style path space).
30pub struct PathModule;
31
32const PATH_MEMBERS: &[&str] = &[
33  "join",
34  "resolve",
35  "dirname",
36  "basename",
37  "extname",
38  "normalize",
39  "relative",
40  "isAbsolute",
41  "sep",
42  "delimiter",
43];
44const PATH_EXPORTS: &[&str] = &[
45  "default",
46  "join",
47  "resolve",
48  "dirname",
49  "basename",
50  "extname",
51  "normalize",
52  "relative",
53  "isAbsolute",
54  "sep",
55  "delimiter",
56];
57
58fn path_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
59  let obj = crate::node::path::path_object(ctx)?;
60  let ns = Object::new(ctx.clone())?;
61  ns.set("default", obj.clone())?;
62  for name in PATH_MEMBERS {
63    ns.set(*name, obj.get::<_, Value<'js>>(*name)?)?;
64  }
65  Ok(ns)
66}
67
68impl ModuleDef for PathModule {
69  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
70    declare_all(decl, PATH_EXPORTS)
71  }
72
73  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
74    export_from(exports, &path_namespace(ctx)?, PATH_EXPORTS)
75  }
76}
77
78/// `import { createHash } from 'node:crypto'` — the vendored llrt crypto
79/// module. `require('crypto')` reads the same members off the `crypto`
80/// global the runtime installs.
81pub use crate::crypto::CryptoModule;
82
83const CRYPTO_MEMBERS: &[&str] = &[
84  "createHash",
85  "createHmac",
86  "getRandomValues",
87  "randomBytes",
88  "randomFill",
89  "randomFillSync",
90  "randomInt",
91  "randomUUID",
92  "subtle",
93  "webcrypto",
94];
95
96fn crypto_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
97  let global: Object<'js> = ctx.globals().get("crypto")?;
98  let ns = Object::new(ctx.clone())?;
99  for name in CRYPTO_MEMBERS {
100    if let Ok(value) = global.get::<_, Value<'js>>(*name) {
101      if !value.is_undefined() {
102        ns.set(*name, value)?;
103      }
104    }
105  }
106  ns.set("webcrypto", global)?;
107  Ok(ns)
108}
109
110/// `import { Buffer } from 'node:buffer'` — the vendored llrt `Buffer`,
111/// which subclasses `Uint8Array`.
112pub use crate::buffer::BufferModule;
113
114/// `require('buffer')`: the same members the ES module exports, read off
115/// the globals the runtime installed.
116fn buffer_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
117  let ns = Object::new(ctx.clone())?;
118  for name in ["Buffer", "atob", "btoa"] {
119    if let Ok(value) = ctx.globals().get::<_, Value<'js>>(name) {
120      if !value.is_undefined() {
121        ns.set(name, value)?;
122      }
123    }
124  }
125  let constants = Object::new(ctx.clone())?;
126  constants.set("MAX_LENGTH", u32::MAX)?;
127  constants.set("MAX_STRING_LENGTH", (1_u32 << 30) - 1)?;
128  ns.set("constants", constants)?;
129  Ok(ns)
130}
131
132/// `import os from 'node:os'` — host introspection, served by the
133/// vendored `llrt_os`.
134pub struct OsModule;
135
136const OS_MEMBERS: &[&str] = &[
137  "arch",
138  "availableParallelism",
139  "cpus",
140  "devNull",
141  "endianness",
142  "EOL",
143  "freemem",
144  "getPriority",
145  "homedir",
146  "hostname",
147  "loadavg",
148  "machine",
149  "networkInterfaces",
150  "platform",
151  "release",
152  "setPriority",
153  "tmpdir",
154  "totalmem",
155  "type",
156  "uptime",
157  "userInfo",
158  "version",
159];
160
161fn os_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
162  crate::os::os_object(ctx)
163}
164
165impl ModuleDef for OsModule {
166  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
167    decl.declare("default")?;
168    declare_all(decl, OS_MEMBERS)
169  }
170
171  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
172    let object = os_namespace(ctx)?;
173    export_node_module(ctx, exports, &object, OS_MEMBERS)
174  }
175}
176
177/// Export a node module: `default` is the module object, and every member
178/// it actually carries is a named export. Members are read off the object
179/// rather than assumed, because some depend on globals a given host does
180/// not install.
181fn export_node_module<'js>(
182  ctx: &Ctx<'js>,
183  exports: &Exports<'js>,
184  object: &Object<'js>,
185  members: &[&str],
186) -> rquickjs::Result<()> {
187  exports.export("default", object.clone())?;
188  for name in members {
189    let value = object
190      .get::<_, Value<'js>>(*name)
191      .unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
192    exports.export(*name, value)?;
193  }
194  Ok(())
195}
196
197/// `import util from 'node:util'` — formatting, the promise/callback
198/// wrappers and `util.types`.
199pub struct UtilModule;
200
201impl ModuleDef for UtilModule {
202  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
203    decl.declare("default")?;
204    declare_all(decl, crate::node::util::UTIL_MEMBERS)
205  }
206
207  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
208    let object = crate::node::util::util_object(ctx)?;
209    export_node_module(ctx, exports, &object, crate::node::util::UTIL_MEMBERS)
210  }
211}
212
213/// `import assert from 'node:assert'`, and its always-strict twin.
214pub struct AssertModule;
215/// `import assert from 'node:assert/strict'`.
216pub struct AssertStrictModule;
217
218impl ModuleDef for AssertModule {
219  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
220    decl.declare("default")?;
221    declare_all(decl, crate::node::assert::ASSERT_MEMBERS)
222  }
223
224  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
225    let object = crate::node::assert::assert_object(ctx, false)?;
226    export_node_module(ctx, exports, &object, crate::node::assert::ASSERT_MEMBERS)
227  }
228}
229
230impl ModuleDef for AssertStrictModule {
231  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
232    decl.declare("default")?;
233    declare_all(decl, crate::node::assert::ASSERT_MEMBERS)
234  }
235
236  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
237    let object = crate::node::assert::assert_object(ctx, true)?;
238    export_node_module(ctx, exports, &object, crate::node::assert::ASSERT_MEMBERS)
239  }
240}
241
242/// `import { fileURLToPath } from 'node:url'` — the vendored `llrt_url`,
243/// whose `URL` and `URLSearchParams` are the runtime's globals.
244pub use crate::url::UrlModule;
245
246/// `require('url')`: the module's members over the same functions the ES
247/// module exports, with the classes read off the globals so both forms
248/// hand back one constructor.
249fn url_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
250  use rquickjs::function::Func;
251
252  let ns = Object::new(ctx.clone())?;
253  for name in ["URL", "URLSearchParams"] {
254    ns.set(name, ctx.globals().get::<_, Value<'js>>(name)?)?;
255  }
256  ns.set(
257    "domainToASCII",
258    Func::from(|domain: String| crate::url::domain_to_ascii(&domain)),
259  )?;
260  ns.set(
261    "domainToUnicode",
262    Func::from(|domain: String| crate::url::domain_to_unicode(&domain)),
263  )?;
264  ns.set("fileURLToPath", Func::from(crate::url::file_url_to_path))?;
265  ns.set("pathToFileURL", Func::from(crate::url::path_to_file_url))?;
266  ns.set("format", Func::from(crate::url::url_format))?;
267  ns.set(
268    "urlToHttpOptions",
269    Func::from(crate::url::url_class::url_to_http_options),
270  )?;
271  Ok(ns)
272}
273
274/// `import process from 'node:process'` — the module form of the global.
275pub struct ProcessModule;
276
277impl ModuleDef for ProcessModule {
278  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
279    decl.declare("default")?;
280    declare_all(decl, crate::node::process::PROCESS_MEMBERS)
281  }
282
283  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
284    let object = crate::node::process::process_object(ctx)?;
285    export_node_module(ctx, exports, &object, crate::node::process::PROCESS_MEMBERS)
286  }
287}
288
289/// `import { setTimeout } from 'node:timers'`, and the promise twin.
290pub struct TimersModule;
291/// `import { setTimeout } from 'node:timers/promises'`.
292pub struct TimersPromisesModule;
293
294impl ModuleDef for TimersModule {
295  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
296    decl.declare("default")?;
297    declare_all(decl, crate::node::timers::TIMERS_MEMBERS)
298  }
299
300  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
301    let object = crate::node::timers::timers_object(ctx)?;
302    export_node_module(ctx, exports, &object, crate::node::timers::TIMERS_MEMBERS)
303  }
304}
305
306impl ModuleDef for TimersPromisesModule {
307  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
308    decl.declare("default")?;
309    declare_all(decl, crate::node::timers::TIMERS_PROMISES_MEMBERS)
310  }
311
312  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
313    let object = crate::node::timers::timers_promises_object(ctx)?;
314    export_node_module(
315      ctx,
316      exports,
317      &object,
318      crate::node::timers::TIMERS_PROMISES_MEMBERS,
319    )
320  }
321}
322
323/// `import { EventEmitter } from 'node:events'` — the vendored llrt
324/// emitter, which the `EventTarget` globals already share.
325pub struct EventsModule;
326
327/// Key under which the per-context `EventEmitter` constructor is
328/// remembered. A symbol on `globalThis`, so it stays out of
329/// `Object.keys` and cannot collide with a suite's own globals.
330const EVENT_EMITTER_KEY: &str = "ferrijs.node.events.EventEmitter";
331
332fn events_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
333  use crate::events::{Emitter as _, EventEmitter};
334
335  let symbol: Object<'js> = ctx.globals().get("Symbol")?;
336  let symbol_for: rquickjs::Function<'js> = symbol.get("for")?;
337  let key: Value<'js> = symbol_for.call((EVENT_EMITTER_KEY,))?;
338
339  // One constructor per context, whichever path asks first: a second
340  // `create_constructor` hands back a different function object, and
341  // `require('events') === (await import('events')).EventEmitter` — plus
342  // every `instanceof` across the two — would be false.
343  if let Ok(existing) = ctx.globals().get::<_, Object<'js>>(key.clone()) {
344    return Ok(existing);
345  }
346
347  let ctor = rquickjs::Class::<EventEmitter<'js>>::create_constructor(ctx)?
348    .ok_or_else(|| rquickjs::Error::new_loading("events"))?;
349  EventEmitter::add_event_emitter_prototype(ctx)?;
350  let ctor = ctor
351    .as_object()
352    .cloned()
353    .ok_or_else(|| rquickjs::Error::new_loading("events"))?;
354
355  // Node's `module.exports` for this module IS the class, with the named
356  // export hanging off it — so `require('events')` can be extended.
357  ctor.set("EventEmitter", ctor.clone())?;
358  ctx.globals().set(key, ctor.clone())?;
359  Ok(ctor)
360}
361
362impl ModuleDef for EventsModule {
363  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
364    decl.declare("default")?;
365    decl.declare("EventEmitter")?;
366    Ok(())
367  }
368
369  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
370    let object = events_namespace(ctx)?;
371    export_node_module(ctx, exports, &object, &["EventEmitter"])
372  }
373}
374
375/// How a host declares one of these modules to the ES loader.
376pub type DeclareFn = for<'js> fn(Ctx<'js>, Vec<u8>) -> rquickjs::Result<Module<'js>>;
377
378/// How a host builds the object `require('<specifier>')` returns.
379pub type NamespaceFn = for<'js> fn(&Ctx<'js>) -> rquickjs::Result<Object<'js>>;
380
381/// One module, under every specifier it answers to.
382pub struct NodeModule {
383  pub specifiers: &'static [&'static str],
384  pub declare: DeclareFn,
385  pub namespace: NamespaceFn,
386}
387
388fn declare_fn<D: ModuleDef>() -> DeclareFn {
389  |ctx, name| Module::declare_def::<D, _>(ctx, name)
390}
391
392/// Every module this crate serves. A host registers all of them or none:
393/// the ES loader, the `require` table and the bundler's external list all
394/// read this one place.
395#[must_use]
396pub fn modules() -> Vec<NodeModule> {
397  vec![
398    NodeModule {
399      specifiers: &["path", "node:path"],
400      declare: declare_fn::<PathModule>(),
401      namespace: path_namespace,
402    },
403    NodeModule {
404      specifiers: &["buffer", "node:buffer"],
405      declare: declare_fn::<crate::buffer::BufferModule>(),
406      namespace: buffer_namespace,
407    },
408    NodeModule {
409      specifiers: &["fs", "node:fs"],
410      declare: declare_fn::<crate::fs::FsModule>(),
411      namespace: |ctx| crate::fs::fs_object(ctx),
412    },
413    NodeModule {
414      specifiers: &["fs/promises", "node:fs/promises"],
415      declare: declare_fn::<crate::fs::FsPromisesModule>(),
416      namespace: |ctx| crate::fs::fs_promises_object(ctx),
417    },
418    NodeModule {
419      specifiers: &["os", "node:os"],
420      declare: declare_fn::<OsModule>(),
421      namespace: os_namespace,
422    },
423    NodeModule {
424      specifiers: &["util", "node:util"],
425      declare: declare_fn::<UtilModule>(),
426      namespace: |ctx| crate::node::util::util_object(ctx),
427    },
428    NodeModule {
429      specifiers: &["events", "node:events"],
430      declare: declare_fn::<EventsModule>(),
431      namespace: events_namespace,
432    },
433    NodeModule {
434      specifiers: &["assert", "node:assert"],
435      declare: declare_fn::<AssertModule>(),
436      namespace: |ctx| crate::node::assert::assert_object(ctx, false),
437    },
438    NodeModule {
439      specifiers: &["assert/strict", "node:assert/strict"],
440      declare: declare_fn::<AssertStrictModule>(),
441      namespace: |ctx| crate::node::assert::assert_object(ctx, true),
442    },
443    NodeModule {
444      specifiers: &["url", "node:url"],
445      declare: declare_fn::<UrlModule>(),
446      namespace: url_namespace,
447    },
448    NodeModule {
449      specifiers: &["process", "node:process"],
450      declare: declare_fn::<ProcessModule>(),
451      namespace: |ctx| crate::node::process::process_object(ctx),
452    },
453    NodeModule {
454      specifiers: &["timers", "node:timers"],
455      declare: declare_fn::<TimersModule>(),
456      namespace: |ctx| crate::node::timers::timers_object(ctx),
457    },
458    NodeModule {
459      specifiers: &["timers/promises", "node:timers/promises"],
460      declare: declare_fn::<TimersPromisesModule>(),
461      namespace: |ctx| crate::node::timers::timers_promises_object(ctx),
462    },
463    NodeModule {
464      specifiers: &["crypto", "node:crypto"],
465      declare: declare_fn::<crate::crypto::CryptoModule>(),
466      namespace: crypto_namespace,
467    },
468    NodeModule {
469      specifiers: &["zlib", "node:zlib"],
470      declare: declare_fn::<crate::zlib::ZlibModule>(),
471      namespace: zlib_namespace,
472    },
473    NodeModule {
474      specifiers: &["string_decoder", "node:string_decoder"],
475      declare: declare_fn::<crate::string_decoder::StringDecoderModule>(),
476      namespace: string_decoder_namespace,
477    },
478    NodeModule {
479      specifiers: &["perf_hooks", "node:perf_hooks"],
480      declare: declare_fn::<crate::perf_hooks::PerfHooksModule>(),
481      namespace: perf_hooks_namespace,
482    },
483    NodeModule {
484      specifiers: &["tty", "node:tty"],
485      declare: declare_fn::<crate::tty::TtyModule>(),
486      namespace: tty_namespace,
487    },
488    // The implementation was already here as globals; without a
489    // specifier `import { ReadableStream } from 'node:stream/web'` --
490    // which is how Node names it and how a library reaches it without
491    // assuming a browser -- resolved to nothing.
492    NodeModule {
493      specifiers: &["stream/web", "node:stream/web"],
494      declare: declare_fn::<StreamWebModule>(),
495      namespace: stream_web_namespace,
496    },
497  ]
498}
499
500/// `zlib`'s members, read back off the module's own default export so
501/// the ES and `require()` forms cannot list different sets.
502const ZLIB_MEMBERS: &[&str] = &[
503  "deflate",
504  "deflateSync",
505  "deflateRaw",
506  "deflateRawSync",
507  "gzip",
508  "gzipSync",
509  "inflate",
510  "inflateSync",
511  "inflateRaw",
512  "inflateRawSync",
513  "gunzip",
514  "gunzipSync",
515  "brotliCompress",
516  "brotliCompressSync",
517  "brotliDecompress",
518  "brotliDecompressSync",
519  "zstdCompress",
520  "zstdCompressSync",
521  "zstdDecompress",
522  "zstdDecompressSync",
523];
524
525fn zlib_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
526  module_default_object::<crate::zlib::ZlibModule>(ctx, "zlib", ZLIB_MEMBERS)
527}
528
529fn string_decoder_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
530  module_default_object::<crate::string_decoder::StringDecoderModule>(ctx, "string_decoder", &["StringDecoder"])
531}
532
533fn perf_hooks_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
534  module_default_object::<crate::perf_hooks::PerfHooksModule>(ctx, "perf_hooks", &["performance"])
535}
536
537fn tty_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
538  module_default_object::<crate::tty::TtyModule>(ctx, "tty", &["isatty"])
539}
540
541/// The Streams surface under its Node module name. Every class is
542/// already a global (`jsstd::init`), so the module only has to name
543/// them.
544const STREAM_WEB_MEMBERS: &[&str] = &[
545  "ReadableStream",
546  "ReadableStreamDefaultReader",
547  "ReadableStreamBYOBReader",
548  "ReadableStreamDefaultController",
549  "ReadableByteStreamController",
550  "ReadableStreamBYOBRequest",
551  "WritableStream",
552  "WritableStreamDefaultWriter",
553  "WritableStreamDefaultController",
554  "TransformStream",
555  "TransformStreamDefaultController",
556  "ByteLengthQueuingStrategy",
557  "CountQueuingStrategy",
558];
559
560pub struct StreamWebModule;
561
562impl ModuleDef for StreamWebModule {
563  fn declare(decl: &Declarations<'_>) -> rquickjs::Result<()> {
564    declare_all(decl, STREAM_WEB_MEMBERS)?;
565    decl.declare("default")?;
566    Ok(())
567  }
568
569  fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
570    let ns = stream_web_namespace(ctx)?;
571    export_from(exports, &ns, STREAM_WEB_MEMBERS)?;
572    exports.export("default", ns)?;
573    Ok(())
574  }
575}
576
577fn stream_web_namespace<'js>(ctx: &Ctx<'js>) -> rquickjs::Result<Object<'js>> {
578  let globals = ctx.globals();
579  let ns = Object::new(ctx.clone())?;
580  for name in STREAM_WEB_MEMBERS {
581    if let Ok(value) = globals.get::<_, Value<'js>>(*name) {
582      if !value.is_undefined() {
583        ns.set(*name, value)?;
584      }
585    }
586  }
587  Ok(ns)
588}
589
590/// Evaluate a vendored module and hand back its `default` export as the
591/// `require()` namespace.
592///
593/// The vendored modules build their exports inside `evaluate` through
594/// upstream's `export_default`, so there is no Rust-side object to
595/// borrow the way `crypto` and `events` do. Reading the default export
596/// back is what keeps the two forms one implementation instead of two
597/// lists that drift.
598fn module_default_object<'js, D: ModuleDef>(
599  ctx: &Ctx<'js>,
600  name: &str,
601  members: &[&str],
602) -> rquickjs::Result<Object<'js>> {
603  // `evaluate_def` hands back the module and the promise its evaluation
604  // returns. Every module here is synchronous, so the promise is already
605  // settled and the namespace is readable straight away.
606  let (module, _promise) = Module::evaluate_def::<D, _>(ctx.clone(), name)?;
607  let namespace = module.namespace()?;
608  if let Ok(default) = namespace.get::<_, Object<'js>>("default") {
609    return Ok(default);
610  }
611  let ns = Object::new(ctx.clone())?;
612  for member in members {
613    if let Ok(value) = namespace.get::<_, Value<'js>>(*member) {
614      ns.set(*member, value)?;
615    }
616  }
617  Ok(ns)
618}