use std::sync::Arc;
use rquickjs::loader::{BuiltinResolver, ImportAttributes, Loader, Resolver};
use rquickjs::module::ModuleDef;
use rquickjs::{Ctx, Module, Object};
pub type DeclareFn = Arc<dyn for<'js> Fn(Ctx<'js>, Vec<u8>) -> rquickjs::Result<Module<'js>> + Send + Sync>;
pub type NamespaceFn = Arc<dyn for<'js> Fn(&Ctx<'js>) -> rquickjs::Result<Object<'js>> + Send + Sync>;
#[derive(Clone)]
pub struct NativeModule {
pub specifiers: Vec<String>,
pub declare: DeclareFn,
pub namespace: NamespaceFn,
}
impl std::fmt::Debug for NativeModule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeModule")
.field("specifiers", &self.specifiers)
.finish_non_exhaustive()
}
}
impl NativeModule {
pub fn new<D, N>(specifiers: impl IntoIterator<Item = impl Into<String>>, namespace: N) -> Self
where
D: ModuleDef,
N: for<'js> Fn(&Ctx<'js>) -> rquickjs::Result<Object<'js>> + Send + Sync + 'static,
{
Self {
specifiers: specifiers.into_iter().map(Into::into).collect(),
declare: Arc::new(|ctx, name| Module::declare_def::<D, _>(ctx, name)),
namespace: Arc::new(namespace),
}
}
pub fn from_def<D: ModuleDef>(specifiers: impl IntoIterator<Item = impl Into<String>>) -> Self {
let specifiers: Vec<String> = specifiers.into_iter().map(Into::into).collect();
let canonical = specifiers.first().cloned().unwrap_or_default();
Self {
specifiers,
declare: Arc::new(|ctx, name| Module::declare_def::<D, _>(ctx, name)),
namespace: Arc::new(move |ctx| module_default_object::<D>(ctx, &canonical)),
}
}
#[must_use]
pub fn canonical(&self) -> &str {
self.specifiers.first().map_or("", String::as_str)
}
#[must_use]
pub fn answers_to(&self, specifier: &str) -> bool {
self.specifiers.iter().any(|s| s == specifier)
}
}
impl From<ferrijs_std::modules::NodeModule> for NativeModule {
fn from(m: ferrijs_std::modules::NodeModule) -> Self {
Self {
specifiers: m.specifiers.iter().map(|s| (*s).to_string()).collect(),
declare: Arc::new(m.declare),
namespace: Arc::new(m.namespace),
}
}
}
fn module_default_object<'js, D: ModuleDef>(ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Object<'js>> {
let (module, _promise) = Module::evaluate_def::<D, _>(ctx.clone(), name)?;
let namespace = module.namespace()?;
if let Ok(default) = namespace.get::<_, Object<'js>>("default") {
return Ok(default);
}
Ok(namespace)
}
#[derive(Clone, Default)]
pub struct ModuleRegistry {
modules: Vec<NativeModule>,
aliases: Vec<(String, String)>,
reserved_prefixes: Vec<String>,
reserved_names: Vec<String>,
}
impl std::fmt::Debug for ModuleRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleRegistry")
.field("modules", &self.names())
.field("aliases", &self.aliases)
.finish_non_exhaustive()
}
}
impl ModuleRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_std() -> Self {
let mut registry = Self::new();
for module in ferrijs_std::modules::modules() {
registry.modules.push(module.into());
}
registry.reserved_prefixes.push("node:".to_string());
registry
}
pub fn register(&mut self, module: NativeModule) -> Result<(), String> {
for specifier in &module.specifiers {
if self.serves(specifier) {
return Err(format!("module `{specifier}` is already served by this runtime"));
}
}
self.modules.push(module);
Ok(())
}
#[must_use]
pub fn with(mut self, module: NativeModule) -> Self {
if let Err(e) = self.register(module) {
panic!("{e}");
}
self
}
pub fn alias(&mut self, from: impl Into<String>, to: impl Into<String>) -> Result<(), String> {
let (from, to) = (from.into(), to.into());
if self.modules.iter().any(|m| m.answers_to(&from)) {
return Err(format!(
"module alias `{from}`: cannot alias a specifier the runtime already serves natively"
));
}
if !self.modules.iter().any(|m| m.answers_to(&to)) {
return Err(format!(
"module alias `{from}` -> `{to}`: `{to}` is not a native module (expected one of {})",
self.names().join(", ")
));
}
match self.aliases.iter_mut().find(|(f, _)| *f == from) {
Some(entry) => entry.1 = to,
None => self.aliases.push((from, to)),
}
Ok(())
}
pub fn retain(&mut self, keep: impl Fn(&str) -> bool) {
self.modules.retain(|m| keep(m.canonical()));
let served: Vec<String> = self.modules.iter().flat_map(|m| m.specifiers.clone()).collect();
self.aliases.retain(|(_, to)| served.contains(to));
}
pub fn reserve_prefix(&mut self, prefix: impl Into<String>) {
self.reserved_prefixes.push(prefix.into());
}
pub fn reserve_name(&mut self, name: impl Into<String>) {
self.reserved_names.push(name.into());
}
#[must_use]
pub fn names(&self) -> Vec<String> {
let mut names: Vec<String> = self.modules.iter().flat_map(|m| m.specifiers.clone()).collect();
names.extend(self.aliases.iter().map(|(from, _)| from.clone()));
names
}
#[must_use]
pub fn modules(&self) -> &[NativeModule] {
&self.modules
}
#[must_use]
pub fn aliases(&self) -> &[(String, String)] {
&self.aliases
}
#[must_use]
pub fn serves(&self, specifier: &str) -> bool {
self.canonical(specifier).is_some()
}
#[must_use]
pub fn canonical(&self, specifier: &str) -> Option<String> {
let target = self
.aliases
.iter()
.find(|(from, _)| from == specifier)
.map_or(specifier, |(_, to)| to.as_str());
self
.modules
.iter()
.find(|m| m.answers_to(target))
.map(|m| m.canonical().to_string())
}
#[must_use]
pub fn is_reserved(&self, specifier: &str) -> bool {
if self.serves(specifier) || self.reserved_names.iter().any(|n| n == specifier) {
return true;
}
if self.reserved_prefixes.iter().any(|p| specifier.starts_with(p)) {
return true;
}
self
.names()
.iter()
.any(|name| name.strip_prefix("node:") == Some(specifier))
}
#[must_use]
pub fn fingerprint(&self) -> u64 {
use std::hash::{Hash, Hasher};
let mut names = self.names();
names.sort();
let mut aliases = self.aliases.clone();
aliases.sort();
let mut h = std::collections::hash_map::DefaultHasher::new();
names.hash(&mut h);
aliases.hash(&mut h);
h.finish()
}
fn module_for(&self, specifier: &str) -> Option<&NativeModule> {
let canonical = self.canonical(specifier)?;
self.modules.iter().find(|m| m.canonical() == canonical)
}
pub fn namespace<'js>(&self, ctx: &Ctx<'js>, specifier: &str) -> rquickjs::Result<Option<Object<'js>>> {
match self.module_for(specifier) {
Some(module) => (module.namespace)(ctx).map(Some),
None => Ok(None),
}
}
#[must_use]
pub fn loader(self: &Arc<Self>) -> (NativeResolver, NativeLoader) {
let mut builtin = BuiltinResolver::default();
for name in self.names() {
builtin.add_module(name);
}
(
NativeResolver {
builtin,
registry: Arc::clone(self),
},
NativeLoader {
registry: Arc::clone(self),
},
)
}
}
pub struct NativeResolver {
builtin: BuiltinResolver,
registry: Arc<ModuleRegistry>,
}
impl Resolver for NativeResolver {
fn resolve<'js>(
&mut self,
ctx: &Ctx<'js>,
base: &str,
name: &str,
attributes: Option<ImportAttributes<'js>>,
) -> rquickjs::Result<String> {
let _ = &self.registry;
self.builtin.resolve(ctx, base, name, attributes)
}
}
pub struct NativeLoader {
registry: Arc<ModuleRegistry>,
}
impl Loader for NativeLoader {
fn load<'js>(
&mut self,
ctx: &Ctx<'js>,
path: &str,
_attributes: Option<ImportAttributes<'js>>,
) -> rquickjs::Result<Module<'js>> {
let module = self
.registry
.module_for(path)
.ok_or_else(|| rquickjs::Error::new_loading(path))?;
(module.declare)(ctx.clone(), Vec::from(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Dummy;
impl ModuleDef for Dummy {
fn declare(decl: &rquickjs::module::Declarations<'_>) -> rquickjs::Result<()> {
decl.declare("x")?;
Ok(())
}
fn evaluate<'js>(_ctx: &Ctx<'js>, exports: &rquickjs::module::Exports<'js>) -> rquickjs::Result<()> {
exports.export("x", 1)?;
Ok(())
}
}
#[test]
fn std_table_serves_node_modules_under_both_spellings() {
let r = ModuleRegistry::with_std();
assert!(r.serves("fs"));
assert!(r.serves("node:fs"));
assert_eq!(r.canonical("node:fs").as_deref(), Some("fs"));
assert!(r.is_reserved("node:anything"));
assert!(!r.serves("lodash"));
}
#[test]
fn register_refuses_a_clash_and_alias_refuses_a_redirect() {
let mut r = ModuleRegistry::with_std();
assert!(r.register(NativeModule::from_def::<Dummy>(["fs"])).is_err());
r.register(NativeModule::from_def::<Dummy>(["acme"])).unwrap();
assert!(r.alias("fs", "acme").is_err());
assert!(r.alias("acme2", "nope").is_err());
r.alias("acme2", "acme").unwrap();
assert_eq!(r.canonical("acme2").as_deref(), Some("acme"));
assert!(r.is_reserved("acme2"));
}
#[test]
fn fingerprint_ignores_order() {
let mut a = ModuleRegistry::new();
a.register(NativeModule::from_def::<Dummy>(["one"])).unwrap();
a.register(NativeModule::from_def::<Dummy>(["two"])).unwrap();
let mut b = ModuleRegistry::new();
b.register(NativeModule::from_def::<Dummy>(["two"])).unwrap();
b.register(NativeModule::from_def::<Dummy>(["one"])).unwrap();
assert_eq!(a.fingerprint(), b.fingerprint());
b.alias("three", "one").unwrap();
assert_ne!(a.fingerprint(), b.fingerprint());
}
}