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