use std::collections::HashSet; use std::rc::Rc; use super::{ModuleFormat, ModuleMap, fail, fail_syntax, module_name}; use crate::Error; fn define_data<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, key: &str, value: v8::Local<'s, v8::Value>, ) -> Option<()> { let key_local = v8::String::new(scope, key)?; if object.create_data_property(scope, key_local.into(), value) != Some(true) { return fail(scope, format!("Failed to define {key}")); } Some(()) } fn require_arg_string( scope: &mut v8::PinScope, args: &v8::FunctionCallbackArguments, ) -> Option { let value = args.get(0); let string = match v8::Local::::try_from(value) { Ok(string) => string, Err(_) => { let message = v8::String::new(scope, "require() argument must be a string")?; scope.throw_exception(v8::Exception::type_error(scope, message)); return None; } }; Some(string.to_rust_string_lossy(scope)) } pub(super) fn strip_preamble(source: &str) -> &str { let source = source.strip_prefix('\u{FEFF}').unwrap_or(source); if source.starts_with("#!") { match source.find('\n') { Some(index) => &source[index..], None => "", } } else { source } } pub(super) fn strip_json(source: &str) -> &str { source.strip_prefix('\u{FEFF}').unwrap_or(source) } fn dirname(name: &str) -> String { std::path::Path::new(name) .parent() .and_then(|parent| parent.to_str()) .filter(|parent| !parent.is_empty()) .unwrap_or(".") .to_owned() } fn make_require<'s>( scope: &mut v8::PinScope<'s, '_>, referrer: &str, ) -> Option> { let referrer_value = v8::String::new(scope, referrer)?; let require = v8::Function::builder(require_call) .data(referrer_value.into()) .length(1) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope)?; let resolve = v8::Function::builder(require_resolve_call) .data(referrer_value.into()) .length(1) .constructor_behavior(v8::ConstructorBehavior::Throw) .build(scope)?; let require_value: v8::Local = require.into(); let require_object = v8::Local::::try_from(require_value).ok()?; let key = v8::String::new(scope, "resolve")?; if require_object.create_data_property(scope, key.into(), resolve.into()) != Some(true) { return None; } Some(require) } fn make_module<'s>( scope: &mut v8::PinScope<'s, '_>, name: &str, ) -> Option> { let require = make_require(scope, name)?; let module = v8::Object::new(scope); let id = v8::String::new(scope, name)?; define_data(scope, module, "id", id.into())?; let filename = v8::String::new(scope, name)?; define_data(scope, module, "filename", filename.into())?; define_data( scope, module, "loaded", v8::Boolean::new(scope, false).into(), )?; define_data(scope, module, "exports", v8::Object::new(scope).into())?; define_data(scope, module, "require", require.into())?; Some(module) } fn cloned_module(map: &Rc, name: &str) -> Option> { map.cjs_modules.borrow().get(name).cloned() } pub(super) fn load_synthetic<'s>( scope: &mut v8::PinScope<'s, '_>, map: &Rc, loader: &Rc, name: &str, ) -> Option> { let mut exports = vec!["default".to_owned()]; let named = match loader.cjs_named_exports(name) { Ok(named) => named, Err(Error::Syntax(message)) => return fail_syntax(scope, message), Err(error) => return fail(scope, error.to_string()), }; for export in named { if export != "default" && !exports.contains(&export) { exports.push(export); } } let module_name = v8::String::new(scope, name)?; let export_names = exports .iter() .map(|export| v8::String::new(scope, export)) .collect::>>()?; let module = v8::Module::create_synthetic_module(scope, module_name, &export_names, evaluate_synthetic); let hash = module.get_identity_hash().get(); let id = map.insert(name.into(), v8::Global::new(scope, module), None, hash); { let mut records = map.records.borrow_mut(); records[id.0 as usize].cjs_exports = Some(exports); } Some(v8::Local::new( scope, &map.records.borrow()[id.0 as usize].module, )) } fn evaluate_synthetic<'s>( context: v8::Local<'s, v8::Context>, module: v8::Local<'s, v8::Module>, ) -> Option> { // SAFETY: V8 invokes synthetic evaluation with this context entered. v8::callback_scope!(unsafe scope, context); let name = module_name(scope, module); let map = scope.get_slot::>().unwrap().clone(); let loader = map.loader.clone()?; if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } let stored = { let records = map.records.borrow(); let id = map.id_of(&name)?; records[id.0 as usize].cjs_exports.clone() }; let Some(stored) = stored else { return fail(scope, format!("Missing CommonJS exports for {name}")); }; let exports = ensure_exports(scope, &map, &loader, &name)?; if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } check_closure(scope, &map, &loader, &name)?; let exports_local = v8::Local::new(scope, &exports); let key = v8::String::new(scope, "default")?; module.set_synthetic_module_export(scope, key, exports_local)?; let is_object = exports_local.is_object() || exports_local.is_function(); for export in stored { if export == "default" { continue; } let key = v8::String::new(scope, &export)?; let value = if is_object { let object = v8::Local::::try_from(exports_local).ok(); object.and_then(|object| object.get(scope, key.into()))? } else { v8::undefined(scope).into() }; if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } check_closure(scope, &map, &loader, &name)?; module.set_synthetic_module_export(scope, key, value)?; } if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } check_closure(scope, &map, &loader, &name)?; let resolver = v8::PromiseResolver::new(scope)?; resolver.resolve(scope, v8::undefined(scope).into())?; Some(resolver.get_promise(scope).into()) } pub(super) fn check_closure( scope: &mut v8::PinScope, map: &Rc, loader: &Rc, root: &str, ) -> Option<()> { let mut visited = HashSet::new(); let mut pending = vec![root.to_owned()]; while let Some(name) = pending.pop() { if !visited.insert(name.clone()) { continue; } // Prior ESM semantics skip rechecking already-evaluated ESM deps; // only CJS/JSON closures (which share mutable exports) are rechecked // so cached ESM wrappers cannot bypass a later pledge via lazy deps. let is_cjs = { map.cjs_modules.borrow().contains_key(name.as_str()) || map.json_modules.borrow().contains_key(name.as_str()) || map.cjs_deps.borrow().contains_key(name.as_str()) }; if is_cjs && let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } pending.extend( map.cjs_deps .borrow() .get(name.as_str()) .cloned() .unwrap_or_default(), ); } Some(()) } fn record_edge(map: &Rc, parent: &str, child: &str) { if parent == child { return; } let mut deps = map.cjs_deps.borrow_mut(); let entry = deps.entry(parent.into()).or_default(); if !entry.iter().any(|existing| existing == child) { entry.push(child.to_owned()); } } fn require_call( scope: &mut v8::PinScope, args: v8::FunctionCallbackArguments, mut result: v8::ReturnValue, ) { let referrer = args.data().to_rust_string_lossy(scope); let Some(specifier) = require_arg_string(scope, &args) else { return; }; if let Some(value) = require_module(scope, &specifier, &referrer) { result.set(value); } } fn require_resolve_call( scope: &mut v8::PinScope, args: v8::FunctionCallbackArguments, mut result: v8::ReturnValue, ) { let referrer = args.data().to_rust_string_lossy(scope); let Some(specifier) = require_arg_string(scope, &args) else { return; }; let map = scope.get_slot::>().unwrap().clone(); let Some(loader) = map.loader.clone() else { fail::<()>(scope, "No module loader configured"); return; }; if specifier.starts_with("ext:") { fail::<()>(scope, "Extension modules are private"); return; } if let Err(error) = loader.check_access(&referrer) { match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail::<()>(scope, error.to_string()), }; return; } let name = match loader.require_resolve(&specifier, &referrer) { Ok(name) => name, Err(Error::Syntax(message)) => { fail_syntax::<()>(scope, message); return; } Err(error) => { fail::<()>(scope, error.to_string()); return; } }; if name.starts_with("ext:") { fail::<()>(scope, "Extension modules are private"); return; } if let Err(error) = loader.check_access(&name) { match error { Error::Syntax(message) => fail_syntax::<()>(scope, message), error => fail::<()>(scope, error.to_string()), }; return; } if let Some(value) = v8::String::new(scope, &name) { result.set(value.into()); } } fn require_module<'s>( scope: &mut v8::PinScope<'s, '_>, specifier: &str, referrer: &str, ) -> Option> { let map = scope.get_slot::>().unwrap().clone(); let loader = map.loader.clone()?; if specifier.starts_with("ext:") { return fail(scope, "Extension modules are private"); } if let Err(error) = loader.check_access(referrer) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } let name = match loader.require_resolve(specifier, referrer) { Ok(name) => name, Err(Error::Syntax(message)) => return fail_syntax(scope, message), Err(error) => return fail(scope, error.to_string()), }; if name.starts_with("ext:") { return fail(scope, "Extension modules are private"); } if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } let format = match loader.module_format(&name) { Ok(format) => format, Err(Error::Syntax(message)) => return fail_syntax(scope, message), Err(error) => return fail(scope, error.to_string()), }; match format { ModuleFormat::Esm => fail( scope, format!("require() of ESM {name} is not supported, use import() instead"), ), ModuleFormat::Json => { let value = require_json(scope, &map, &loader, &name)?; if let Err(error) = loader.check_access(referrer) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } check_closure(scope, &map, &loader, &name)?; record_edge(&map, referrer, &name); Some(value) } ModuleFormat::CommonJs => { let value = ensure_exports(scope, &map, &loader, &name)?; if let Err(error) = loader.check_access(referrer) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } if let Err(error) = loader.check_access(&name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } check_closure(scope, &map, &loader, &name)?; record_edge(&map, referrer, &name); Some(v8::Local::new(scope, &value)) } } } fn require_json<'s>( scope: &mut v8::PinScope<'s, '_>, map: &Rc, loader: &Rc, name: &str, ) -> Option> { let cached = { map.json_modules.borrow().get(name).cloned() }; if let Some(cached) = cached { return Some(v8::Local::new(scope, &cached)); } let source = match loader.load(name) { Ok(source) => source, Err(Error::Syntax(message)) => return fail_syntax(scope, message), Err(error) => return fail(scope, error.to_string()), }; let stripped = strip_json(&source); let text = v8::String::new(scope, stripped)?; let value = v8::json::parse(scope, text)?; { map.json_modules .borrow_mut() .insert(name.into(), v8::Global::new(scope, value)); } Some(value) } fn ensure_exports<'s>( scope: &mut v8::PinScope<'s, '_>, map: &Rc, loader: &Rc, name: &str, ) -> Option> { if let Some(module) = cloned_module(map, name) { check_closure(scope, map, loader, name)?; let module = v8::Local::new(scope, &module); let key = v8::String::new(scope, "exports")?; let current = module.get(scope, key.into())?; return Some(v8::Global::new(scope, current)); } execute(scope, map, loader, name)?; check_closure(scope, map, loader, name)?; let module = cloned_module(map, name)?; let module = v8::Local::new(scope, &module); let key = v8::String::new(scope, "exports")?; let current = module.get(scope, key.into())?; Some(v8::Global::new(scope, current)) } fn execute( scope: &mut v8::PinScope, map: &Rc, loader: &Rc, name: &str, ) -> Option<()> { let (module_global, exports_global, require_global) = { let module = make_module(scope, name)?; let exports_key = v8::String::new(scope, "exports")?; let exports = module.get(scope, exports_key.into())?; let require_key = v8::String::new(scope, "require")?; let require = module.get(scope, require_key.into())?; ( v8::Global::new(scope, module), v8::Global::new(scope, exports), v8::Global::new(scope, require), ) }; { map.cjs_modules .borrow_mut() .insert(name.into(), module_global.clone()); map.cjs_deps.borrow_mut().insert(name.into(), Vec::new()); } let remove = |map: &Rc| { map.cjs_modules.borrow_mut().remove(name); map.cjs_deps.borrow_mut().remove(name); }; let source = match loader.load(name) { Ok(source) => source, Err(Error::Syntax(message)) => { remove(map); return fail_syntax(scope, message); } Err(error) => { remove(map); return fail(scope, error.to_string()); } }; if let Err(error) = loader.check_access(name) { remove(map); return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } let stripped = strip_preamble(&source).to_owned(); let params = ["exports", "require", "module", "__filename", "__dirname"] .iter() .map(|name| v8::String::new(scope, name)) .collect::>>()?; let mut source_ref = crate::source(scope, name, &stripped, false, None); let function = match v8::script_compiler::compile_function( scope, &mut source_ref, ¶ms, &[], v8::script_compiler::CompileOptions::NoCompileOptions, v8::script_compiler::NoCacheReason::NoReason, ) { Some(function) => function, None => { remove(map); return None; } }; let (exports, require, module, filename, dirname) = { let exports = v8::Local::new(scope, &exports_global); let require = v8::Local::new(scope, &require_global); let module = v8::Local::new(scope, &module_global); let filename = v8::String::new(scope, name)?; let dirname = v8::String::new(scope, &dirname(name))?; (exports, require, module, filename, dirname) }; let args = [ exports, require, module.into(), filename.into(), dirname.into(), ]; let receiver: v8::Local = exports; if function.call(scope, receiver, &args).is_none() { remove(map); return None; } { let module = v8::Local::new(scope, &module_global); let key = v8::String::new(scope, "loaded")?; match module.create_data_property(scope, key.into(), v8::Boolean::new(scope, true).into()) { Some(true) => {} Some(false) => { remove(map); return fail(scope, format!("Failed to mark {name} loaded")); } None => { remove(map); return None; } } } if let Err(error) = loader.check_access(name) { return match error { Error::Syntax(message) => fail_syntax(scope, message), error => fail(scope, error.to_string()), }; } Some(()) }