Skip to main content

ferrijs/modules/
registry.rs

1//! The native modules a realm serves, and the one table they come from.
2//!
3//! A native module is a Rust [`ModuleDef`] the ES loader declares by
4//! name -- no generated JS glue, no bundled source. A bundler marks
5//! these specifiers external, so the emitted chunk keeps the bare
6//! `import ... from 'node:fs'` and the written bytecode re-links by NAME
7//! against whatever realm loads it. `QuickJS` resolves the module graph
8//! EAGERLY at declare time, so a throwaway compile realm must register
9//! the same names as the realm that will run the result; both read this
10//! table.
11//!
12//! Every module is served twice from one definition: as an ES module,
13//! and as the object `require('<specifier>')` hands back, so a host
14//! cannot wire up the import form and forget the CommonJS one.
15
16use std::sync::Arc;
17
18use rquickjs::loader::{BuiltinResolver, ImportAttributes, Loader, Resolver};
19use rquickjs::module::ModuleDef;
20use rquickjs::{Ctx, Module, Object};
21
22/// How a module is declared to the ES loader.
23pub type DeclareFn = Arc<dyn for<'js> Fn(Ctx<'js>, Vec<u8>) -> rquickjs::Result<Module<'js>> + Send + Sync>;
24
25/// How the object `require('<specifier>')` returns is built.
26pub type NamespaceFn = Arc<dyn for<'js> Fn(&Ctx<'js>) -> rquickjs::Result<Object<'js>> + Send + Sync>;
27
28/// One module, under every specifier it answers to.
29#[derive(Clone)]
30pub struct NativeModule {
31  /// Every name this module is imported by. The first is canonical;
32  /// the rest are the same module under other spellings (`fs` and
33  /// `node:fs`), so an import of any of them links to one instance.
34  pub specifiers: Vec<String>,
35  pub declare: DeclareFn,
36  pub namespace: NamespaceFn,
37}
38
39impl std::fmt::Debug for NativeModule {
40  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41    f.debug_struct("NativeModule")
42      .field("specifiers", &self.specifiers)
43      .finish_non_exhaustive()
44  }
45}
46
47impl NativeModule {
48  /// A module from a [`ModuleDef`] and a `require` namespace builder.
49  pub fn new<D, N>(specifiers: impl IntoIterator<Item = impl Into<String>>, namespace: N) -> Self
50  where
51    D: ModuleDef,
52    N: for<'js> Fn(&Ctx<'js>) -> rquickjs::Result<Object<'js>> + Send + Sync + 'static,
53  {
54    Self {
55      specifiers: specifiers.into_iter().map(Into::into).collect(),
56      declare: Arc::new(|ctx, name| Module::declare_def::<D, _>(ctx, name)),
57      namespace: Arc::new(namespace),
58    }
59  }
60
61  /// A module whose `require` form is the ES module's own namespace,
62  /// evaluated on demand. For a module built entirely inside its
63  /// `evaluate`, with no Rust-side object to borrow.
64  pub fn from_def<D: ModuleDef>(specifiers: impl IntoIterator<Item = impl Into<String>>) -> Self {
65    let specifiers: Vec<String> = specifiers.into_iter().map(Into::into).collect();
66    let canonical = specifiers.first().cloned().unwrap_or_default();
67    Self {
68      specifiers,
69      declare: Arc::new(|ctx, name| Module::declare_def::<D, _>(ctx, name)),
70      namespace: Arc::new(move |ctx| module_default_object::<D>(ctx, &canonical)),
71    }
72  }
73
74  #[must_use]
75  pub fn canonical(&self) -> &str {
76    self.specifiers.first().map_or("", String::as_str)
77  }
78
79  #[must_use]
80  pub fn answers_to(&self, specifier: &str) -> bool {
81    self.specifiers.iter().any(|s| s == specifier)
82  }
83}
84
85impl From<ferrijs_std::modules::NodeModule> for NativeModule {
86  fn from(m: ferrijs_std::modules::NodeModule) -> Self {
87    Self {
88      specifiers: m.specifiers.iter().map(|s| (*s).to_string()).collect(),
89      declare: Arc::new(m.declare),
90      namespace: Arc::new(m.namespace),
91    }
92  }
93}
94
95/// Evaluate a module and hand back its `default` export (or, failing
96/// that, its whole namespace) as the `require()` object.
97fn module_default_object<'js, D: ModuleDef>(ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Object<'js>> {
98  let (module, _promise) = Module::evaluate_def::<D, _>(ctx.clone(), name)?;
99  let namespace = module.namespace()?;
100  if let Ok(default) = namespace.get::<_, Object<'js>>("default") {
101    return Ok(default);
102  }
103  Ok(namespace)
104}
105
106/// Every native module a realm serves, plus the aliases and the names
107/// nothing may claim.
108///
109/// A value, not a process global: two runtimes in one process may serve
110/// different tables, and a bundler asked to mark externals reads the
111/// table of the runtime that will run its output.
112#[derive(Clone, Default)]
113pub struct ModuleRegistry {
114  modules: Vec<NativeModule>,
115  /// `from -> to`: an extra specifier answered by the module `to` names.
116  aliases: Vec<(String, String)>,
117  /// Specifier prefixes and names reserved for the runtime, beyond the
118  /// modules it serves.
119  reserved_prefixes: Vec<String>,
120  reserved_names: Vec<String>,
121}
122
123impl std::fmt::Debug for ModuleRegistry {
124  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125    f.debug_struct("ModuleRegistry")
126      .field("modules", &self.names())
127      .field("aliases", &self.aliases)
128      .finish_non_exhaustive()
129  }
130}
131
132impl ModuleRegistry {
133  /// An empty table.
134  #[must_use]
135  pub fn new() -> Self {
136    Self::default()
137  }
138
139  /// The table with every Node / web module the standard library
140  /// serves (`node:fs`, `node:path`, `node:buffer`, ...).
141  #[must_use]
142  pub fn with_std() -> Self {
143    let mut registry = Self::new();
144    for module in ferrijs_std::modules::modules() {
145      registry.modules.push(module.into());
146    }
147    registry.reserved_prefixes.push("node:".to_string());
148    registry
149  }
150
151  /// Add a module. A specifier already served is an error: the second
152  /// registration would silently shadow the first, and which one won
153  /// would depend on registration order.
154  ///
155  /// # Errors
156  ///
157  /// When one of the module's specifiers is already served or aliased.
158  pub fn register(&mut self, module: NativeModule) -> Result<(), String> {
159    for specifier in &module.specifiers {
160      if self.serves(specifier) {
161        return Err(format!("module `{specifier}` is already served by this runtime"));
162      }
163    }
164    self.modules.push(module);
165    Ok(())
166  }
167
168  /// [`Self::register`], panicking on a clash. For static tables built
169  /// at startup, where a clash is a programming error.
170  ///
171  /// # Panics
172  ///
173  /// When a specifier is already served.
174  #[must_use]
175  pub fn with(mut self, module: NativeModule) -> Self {
176    if let Err(e) = self.register(module) {
177      panic!("{e}");
178    }
179    self
180  }
181
182  /// Answer `from` with the module `to` names.
183  ///
184  /// # Errors
185  ///
186  /// When `from` is already served (an alias may not redirect a native
187  /// specifier) or `to` is not.
188  pub fn alias(&mut self, from: impl Into<String>, to: impl Into<String>) -> Result<(), String> {
189    let (from, to) = (from.into(), to.into());
190    if self.modules.iter().any(|m| m.answers_to(&from)) {
191      return Err(format!(
192        "module alias `{from}`: cannot alias a specifier the runtime already serves natively"
193      ));
194    }
195    if !self.modules.iter().any(|m| m.answers_to(&to)) {
196      return Err(format!(
197        "module alias `{from}` -> `{to}`: `{to}` is not a native module (expected one of {})",
198        self.names().join(", ")
199      ));
200    }
201    match self.aliases.iter_mut().find(|(f, _)| *f == from) {
202      Some(entry) => entry.1 = to,
203      None => self.aliases.push((from, to)),
204    }
205    Ok(())
206  }
207
208  /// Keep only the modules whose canonical name `keep` accepts. What a
209  /// policy that serves a subset of the standard library applies.
210  pub fn retain(&mut self, keep: impl Fn(&str) -> bool) {
211    self.modules.retain(|m| keep(m.canonical()));
212    let served: Vec<String> = self.modules.iter().flat_map(|m| m.specifiers.clone()).collect();
213    self.aliases.retain(|(_, to)| served.contains(to));
214  }
215
216  /// Reserve a specifier prefix (`@acme/`) so nothing else may claim a
217  /// name under it.
218  pub fn reserve_prefix(&mut self, prefix: impl Into<String>) {
219    self.reserved_prefixes.push(prefix.into());
220  }
221
222  /// Reserve one specifier.
223  pub fn reserve_name(&mut self, name: impl Into<String>) {
224    self.reserved_names.push(name.into());
225  }
226
227  /// Every specifier served natively, aliases included.
228  #[must_use]
229  pub fn names(&self) -> Vec<String> {
230    let mut names: Vec<String> = self.modules.iter().flat_map(|m| m.specifiers.clone()).collect();
231    names.extend(self.aliases.iter().map(|(from, _)| from.clone()));
232    names
233  }
234
235  #[must_use]
236  pub fn modules(&self) -> &[NativeModule] {
237    &self.modules
238  }
239
240  #[must_use]
241  pub fn aliases(&self) -> &[(String, String)] {
242    &self.aliases
243  }
244
245  /// Whether `specifier` is served, directly or through an alias.
246  #[must_use]
247  pub fn serves(&self, specifier: &str) -> bool {
248    self.canonical(specifier).is_some()
249  }
250
251  /// The canonical name of the module `specifier` resolves to: the
252  /// module's first specifier, so two spellings of one module compare
253  /// equal.
254  #[must_use]
255  pub fn canonical(&self, specifier: &str) -> Option<String> {
256    let target = self
257      .aliases
258      .iter()
259      .find(|(from, _)| from == specifier)
260      .map_or(specifier, |(_, to)| to.as_str());
261    self
262      .modules
263      .iter()
264      .find(|m| m.answers_to(target))
265      .map(|m| m.canonical().to_string())
266  }
267
268  /// Whether `specifier` is off-limits to anything outside the runtime:
269  /// served, reserved by name, under a reserved prefix, or the bare twin
270  /// of a served `node:` name (a claim on `fs` while the runtime serves
271  /// `node:fs` is the same hijack spelled differently).
272  #[must_use]
273  pub fn is_reserved(&self, specifier: &str) -> bool {
274    if self.serves(specifier) || self.reserved_names.iter().any(|n| n == specifier) {
275      return true;
276    }
277    if self.reserved_prefixes.iter().any(|p| specifier.starts_with(p)) {
278      return true;
279    }
280    self
281      .names()
282      .iter()
283      .any(|name| name.strip_prefix("node:") == Some(specifier))
284  }
285
286  /// A stable fingerprint of the served names and aliases, for a cache
287  /// key: adding or removing a native specifier flips it between
288  /// "external bare import" and "resolved into the chunk", which
289  /// changes a bundle's output for byte-identical inputs.
290  #[must_use]
291  pub fn fingerprint(&self) -> u64 {
292    use std::hash::{Hash, Hasher};
293    let mut names = self.names();
294    names.sort();
295    let mut aliases = self.aliases.clone();
296    aliases.sort();
297    let mut h = std::collections::hash_map::DefaultHasher::new();
298    names.hash(&mut h);
299    aliases.hash(&mut h);
300    h.finish()
301  }
302
303  fn module_for(&self, specifier: &str) -> Option<&NativeModule> {
304    let canonical = self.canonical(specifier)?;
305    self.modules.iter().find(|m| m.canonical() == canonical)
306  }
307
308  /// The object `require(specifier)` returns, or `None` for a specifier
309  /// this table does not serve.
310  ///
311  /// # Errors
312  ///
313  /// Propagates the module's own namespace construction.
314  pub fn namespace<'js>(&self, ctx: &Ctx<'js>, specifier: &str) -> rquickjs::Result<Option<Object<'js>>> {
315    match self.module_for(specifier) {
316      Some(module) => (module.namespace)(ctx).map(Some),
317      None => Ok(None),
318    }
319  }
320
321  /// The resolver / loader pair for this table, to chain ahead of a file
322  /// loader in `AsyncRuntime::set_loader`.
323  #[must_use]
324  pub fn loader(self: &Arc<Self>) -> (NativeResolver, NativeLoader) {
325    let mut builtin = BuiltinResolver::default();
326    for name in self.names() {
327      builtin.add_module(name);
328    }
329    (
330      NativeResolver {
331        builtin,
332        registry: Arc::clone(self),
333      },
334      NativeLoader {
335        registry: Arc::clone(self),
336      },
337    )
338  }
339}
340
341/// Accepts exactly the served specifiers and aliases.
342pub struct NativeResolver {
343  builtin: BuiltinResolver,
344  registry: Arc<ModuleRegistry>,
345}
346
347impl Resolver for NativeResolver {
348  fn resolve<'js>(
349    &mut self,
350    ctx: &Ctx<'js>,
351    base: &str,
352    name: &str,
353    attributes: Option<ImportAttributes<'js>>,
354  ) -> rquickjs::Result<String> {
355    // The resolver answers with the specifier as written (not the
356    // canonical name): `QuickJS` keys module instances by the resolved
357    // name, and the loader declares the same `ModuleDef` under each
358    // spelling, so `import 'fs'` and `import 'node:fs'` each link to a
359    // module whose exports are the same objects.
360    let _ = &self.registry;
361    self.builtin.resolve(ctx, base, name, attributes)
362  }
363}
364
365/// Non-consuming native module loader. `rquickjs::loader::ModuleLoader`
366/// REMOVES an entry on first load, which breaks the second context on a
367/// shared runtime (and any re-link); `QuickJS` only calls the loader once
368/// per name per context, but the loader itself should not be single-shot.
369pub struct NativeLoader {
370  registry: Arc<ModuleRegistry>,
371}
372
373impl Loader for NativeLoader {
374  fn load<'js>(
375    &mut self,
376    ctx: &Ctx<'js>,
377    path: &str,
378    _attributes: Option<ImportAttributes<'js>>,
379  ) -> rquickjs::Result<Module<'js>> {
380    let module = self
381      .registry
382      .module_for(path)
383      .ok_or_else(|| rquickjs::Error::new_loading(path))?;
384    (module.declare)(ctx.clone(), Vec::from(path))
385  }
386}
387
388#[cfg(test)]
389mod tests {
390  use super::*;
391
392  struct Dummy;
393  impl ModuleDef for Dummy {
394    fn declare(decl: &rquickjs::module::Declarations<'_>) -> rquickjs::Result<()> {
395      decl.declare("x")?;
396      Ok(())
397    }
398    fn evaluate<'js>(_ctx: &Ctx<'js>, exports: &rquickjs::module::Exports<'js>) -> rquickjs::Result<()> {
399      exports.export("x", 1)?;
400      Ok(())
401    }
402  }
403
404  #[test]
405  fn std_table_serves_node_modules_under_both_spellings() {
406    let r = ModuleRegistry::with_std();
407    assert!(r.serves("fs"));
408    assert!(r.serves("node:fs"));
409    assert_eq!(r.canonical("node:fs").as_deref(), Some("fs"));
410    assert!(r.is_reserved("node:anything"));
411    assert!(!r.serves("lodash"));
412  }
413
414  #[test]
415  fn register_refuses_a_clash_and_alias_refuses_a_redirect() {
416    let mut r = ModuleRegistry::with_std();
417    assert!(r.register(NativeModule::from_def::<Dummy>(["fs"])).is_err());
418    r.register(NativeModule::from_def::<Dummy>(["acme"])).unwrap();
419    assert!(r.alias("fs", "acme").is_err());
420    assert!(r.alias("acme2", "nope").is_err());
421    r.alias("acme2", "acme").unwrap();
422    assert_eq!(r.canonical("acme2").as_deref(), Some("acme"));
423    assert!(r.is_reserved("acme2"));
424  }
425
426  #[test]
427  fn fingerprint_ignores_order() {
428    let mut a = ModuleRegistry::new();
429    a.register(NativeModule::from_def::<Dummy>(["one"])).unwrap();
430    a.register(NativeModule::from_def::<Dummy>(["two"])).unwrap();
431    let mut b = ModuleRegistry::new();
432    b.register(NativeModule::from_def::<Dummy>(["two"])).unwrap();
433    b.register(NativeModule::from_def::<Dummy>(["one"])).unwrap();
434    assert_eq!(a.fingerprint(), b.fingerprint());
435    b.alias("three", "one").unwrap();
436    assert_ne!(a.fingerprint(), b.fingerprint());
437  }
438}