diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2021-12-15 19:22:36 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-15 19:22:36 +0100 |
commit | a1f0796fccfafee19b2fe06155efe746da2e9654 (patch) | |
tree | 01942b1bc202352418c88dbf8a1c447e72f6f976 /core/modules.rs | |
parent | ec7d90666f68ae0bf6036a6915296622adc9b65c (diff) |
feat: Add support for import assertions and JSON modules (#12866)
This commit adds proper support for import assertions and JSON modules.
Implementation of "core/modules.rs" was changed to account for multiple possible
module types, instead of always assuming that the code is an "ES module". In
effect "ModuleMap" now has knowledge about each modules' type (stored via
"ModuleType" enum). Module loading pipeline now stores information about
expected module type for each request and validates that expected type matches
discovered module type based on file's "MediaType".
Relevant tests were added to "core/modules.rs" and integration tests,
additionally multiple WPT tests were enabled.
There are still some rough edges in the implementation and not all WPT were
enabled, due to:
a) unclear BOM handling in source code by "FileFetcher"
b) design limitation of Deno's "FileFetcher" that doesn't download the same
module multiple times in a single run
Co-authored-by: Kitson Kelly <me@kitsonkelly.com>
Diffstat (limited to 'core/modules.rs')
-rw-r--r-- | core/modules.rs | 868 |
1 files changed, 706 insertions, 162 deletions
diff --git a/core/modules.rs b/core/modules.rs index 067ee48e3..8c713587e 100644 --- a/core/modules.rs +++ b/core/modules.rs @@ -1,8 +1,11 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::bindings; +use crate::error::attach_handle_to_error; use crate::error::generic_error; use crate::module_specifier::ModuleSpecifier; +use crate::resolve_import; +use crate::resolve_url; use crate::runtime::exception_to_err_result; use crate::OpState; use anyhow::Error; @@ -19,18 +22,159 @@ use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; use std::rc::Rc; -use std::sync::atomic::AtomicI32; -use std::sync::atomic::Ordering; use std::task::Context; use std::task::Poll; -lazy_static::lazy_static! { - pub static ref NEXT_LOAD_ID: AtomicI32 = AtomicI32::new(0); -} - pub type ModuleId = i32; pub type ModuleLoadId = i32; +pub const BOM_CHAR: char = '\u{FEFF}'; + +/// Strips the byte order mark from the provided text if it exists. +pub fn strip_bom(text: &str) -> &str { + if text.starts_with(BOM_CHAR) { + &text[BOM_CHAR.len_utf8()..] + } else { + text + } +} + +const SUPPORTED_TYPE_ASSERTIONS: &[&str] = &["json"]; + +/// Throws V8 exception if assertions are invalid +pub(crate) fn validate_import_assertions( + scope: &mut v8::HandleScope, + assertions: &HashMap<String, String>, +) { + for (key, value) in assertions { + if key == "type" && !SUPPORTED_TYPE_ASSERTIONS.contains(&value.as_str()) { + let message = v8::String::new( + scope, + &format!("\"{}\" is not a valid module type.", value), + ) + .unwrap(); + let exception = v8::Exception::type_error(scope, message); + scope.throw_exception(exception); + return; + } + } +} + +#[derive(Debug)] +pub(crate) enum ImportAssertionsKind { + StaticImport, + DynamicImport, +} + +pub(crate) fn parse_import_assertions( + scope: &mut v8::HandleScope, + import_assertions: v8::Local<v8::FixedArray>, + kind: ImportAssertionsKind, +) -> HashMap<String, String> { + let mut assertions: HashMap<String, String> = HashMap::default(); + + let assertions_per_line = match kind { + // For static imports, assertions are triples of (keyword, value and source offset) + // Also used in `module_resolve_callback`. + ImportAssertionsKind::StaticImport => 3, + // For dynamic imports, assertions are tuples of (keyword, value) + ImportAssertionsKind::DynamicImport => 2, + }; + assert_eq!(import_assertions.length() % assertions_per_line, 0); + let no_of_assertions = import_assertions.length() / assertions_per_line; + + for i in 0..no_of_assertions { + let assert_key = import_assertions + .get(scope, assertions_per_line * i) + .unwrap(); + let assert_key_val = v8::Local::<v8::Value>::try_from(assert_key).unwrap(); + let assert_value = import_assertions + .get(scope, (assertions_per_line * i) + 1) + .unwrap(); + let assert_value_val = + v8::Local::<v8::Value>::try_from(assert_value).unwrap(); + assertions.insert( + assert_key_val.to_rust_string_lossy(scope), + assert_value_val.to_rust_string_lossy(scope), + ); + } + + assertions +} + +pub(crate) fn get_module_type_from_assertions( + assertions: &HashMap<String, String>, +) -> ModuleType { + assertions + .get("type") + .map(|ty| { + if ty == "json" { + ModuleType::Json + } else { + ModuleType::JavaScript + } + }) + .unwrap_or(ModuleType::JavaScript) +} + +// Clippy thinks the return value doesn't need to be an Option, it's unaware +// of the mapping that MapFnFrom<F> does for ResolveModuleCallback. +#[allow(clippy::unnecessary_wraps)] +fn json_module_evaluation_steps<'a>( + context: v8::Local<'a, v8::Context>, + module: v8::Local<v8::Module>, +) -> Option<v8::Local<'a, v8::Value>> { + let scope = &mut unsafe { v8::CallbackScope::new(context) }; + let tc_scope = &mut v8::TryCatch::new(scope); + let module_map = tc_scope + .get_slot::<Rc<RefCell<ModuleMap>>>() + .unwrap() + .clone(); + + let handle = v8::Global::<v8::Module>::new(tc_scope, module); + let value_handle = module_map + .borrow_mut() + .json_value_store + .remove(&handle) + .unwrap(); + let value_local = v8::Local::new(tc_scope, value_handle); + + let name = v8::String::new(tc_scope, "default").unwrap(); + // This should never fail + assert!( + module.set_synthetic_module_export(tc_scope, name, value_local) + == Some(true) + ); + assert!(!tc_scope.has_caught()); + + // Since TLA is active we need to return a promise. + let resolver = v8::PromiseResolver::new(tc_scope).unwrap(); + let undefined = v8::undefined(tc_scope); + resolver.resolve(tc_scope, undefined.into()); + Some(resolver.get_promise(tc_scope).into()) +} + +/// A type of module to be executed. +/// +/// For non-`JavaScript` modules, this value doesn't tell +/// how to interpret the module; it is only used to validate +/// the module against an import assertion (if one is present +/// in the import statement). +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ModuleType { + JavaScript, + Json, +} + +impl std::fmt::Display for ModuleType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::JavaScript => write!(f, "JavaScript"), + Self::Json => write!(f, "JSON"), + } + } +} + /// EsModule source code that will be loaded into V8. /// /// Users can implement `Into<ModuleInfo>` for different file types that @@ -49,6 +193,7 @@ pub type ModuleLoadId = i32; #[derive(Debug, Clone, Eq, PartialEq)] pub struct ModuleSource { pub code: String, + pub module_type: ModuleType, pub module_url_specified: String, pub module_url_found: String, } @@ -57,6 +202,9 @@ pub type PrepareLoadFuture = dyn Future<Output = (ModuleLoadId, Result<RecursiveModuleLoad, Error>)>; pub type ModuleSourceFuture = dyn Future<Output = Result<ModuleSource, Error>>; +type ModuleLoadFuture = + dyn Future<Output = Result<(ModuleRequest, ModuleSource), Error>>; + pub trait ModuleLoader { /// Returns an absolute URL. /// When implementing an spec-complaint VM, this should be exactly the @@ -142,7 +290,7 @@ impl ModuleLoader for FsModuleLoader { referrer: &str, _is_main: bool, ) -> Result<ModuleSpecifier, Error> { - Ok(crate::resolve_import(specifier, referrer)?) + Ok(resolve_import(specifier, referrer)?) } fn load( @@ -159,9 +307,21 @@ impl ModuleLoader for FsModuleLoader { module_specifier )) })?; + let module_type = if let Some(extension) = path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if ext == "json" { + ModuleType::Json + } else { + ModuleType::JavaScript + } + } else { + ModuleType::JavaScript + }; + let code = std::fs::read_to_string(path)?; let module = ModuleSource { code, + module_type, module_url_specified: module_specifier.to_string(), module_url_found: module_specifier.to_string(), }; @@ -178,8 +338,9 @@ enum LoadInit { Main(String), /// Module specifier for side module. Side(String), - /// Dynamic import specifier with referrer. - DynamicImport(String, String), + /// Dynamic import specifier with referrer and expected + /// module type (which is determined by import assertion). + DynamicImport(String, String, ModuleType), } #[derive(Debug, Eq, PartialEq)] @@ -197,14 +358,15 @@ pub struct RecursiveModuleLoad { // be randomized pub id: ModuleLoadId, pub root_module_id: Option<ModuleId>, + pub root_module_type: Option<ModuleType>, pub state: LoadState, pub module_map_rc: Rc<RefCell<ModuleMap>>, // These two fields are copied from `module_map_rc`, but they are cloned ahead // of time to avoid already-borrowed errors. pub op_state: Rc<RefCell<OpState>>, pub loader: Rc<dyn ModuleLoader>, - pub pending: FuturesUnordered<Pin<Box<ModuleSourceFuture>>>, - pub visited: HashSet<ModuleSpecifier>, + pub pending: FuturesUnordered<Pin<Box<ModuleLoadFuture>>>, + pub visited: HashSet<ModuleRequest>, } impl RecursiveModuleLoad { @@ -220,10 +382,14 @@ impl RecursiveModuleLoad { pub fn dynamic_import( specifier: &str, referrer: &str, + module_type: ModuleType, module_map_rc: Rc<RefCell<ModuleMap>>, ) -> Self { - let init = - LoadInit::DynamicImport(specifier.to_string(), referrer.to_string()); + let init = LoadInit::DynamicImport( + specifier.to_string(), + referrer.to_string(), + module_type, + ); Self::new(init, module_map_rc) } @@ -232,11 +398,22 @@ impl RecursiveModuleLoad { } fn new(init: LoadInit, module_map_rc: Rc<RefCell<ModuleMap>>) -> Self { + let id = { + let mut module_map = module_map_rc.borrow_mut(); + let id = module_map.next_load_id; + module_map.next_load_id += 1; + id + }; let op_state = module_map_rc.borrow().op_state.clone(); let loader = module_map_rc.borrow().loader.clone(); + let expected_module_type = match init { + LoadInit::DynamicImport(_, _, module_type) => module_type, + _ => ModuleType::JavaScript, + }; let mut load = Self { - id: NEXT_LOAD_ID.fetch_add(1, Ordering::SeqCst), + id, root_module_id: None, + root_module_type: None, init, state: LoadState::Init, module_map_rc: module_map_rc.clone(), @@ -247,10 +424,12 @@ impl RecursiveModuleLoad { }; // Ignore the error here, let it be hit in `Stream::poll_next()`. if let Ok(root_specifier) = load.resolve_root() { - if let Some(module_id) = - module_map_rc.borrow().get_id(root_specifier.as_str()) + if let Some(module_id) = module_map_rc + .borrow() + .get_id(root_specifier.as_str(), expected_module_type) { load.root_module_id = Some(module_id); + load.root_module_type = Some(expected_module_type); } } load @@ -264,7 +443,7 @@ impl RecursiveModuleLoad { LoadInit::Side(ref specifier) => { self.loader.resolve(specifier, ".", false) } - LoadInit::DynamicImport(ref specifier, ref referrer) => { + LoadInit::DynamicImport(ref specifier, ref referrer, _) => { self.loader.resolve(specifier, referrer, false) } } @@ -281,7 +460,7 @@ impl RecursiveModuleLoad { let spec = self.loader.resolve(specifier, ".", false)?; (spec, None) } - LoadInit::DynamicImport(ref specifier, ref referrer) => { + LoadInit::DynamicImport(ref specifier, ref referrer, _) => { let spec = self.loader.resolve(specifier, referrer, false)?; (spec, Some(referrer.to_string())) } @@ -308,20 +487,29 @@ impl RecursiveModuleLoad { pub fn register_and_recurse( &mut self, scope: &mut v8::HandleScope, + module_request: &ModuleRequest, module_source: &ModuleSource, ) -> Result<(), Error> { + if module_request.expected_module_type != module_source.module_type { + return Err(generic_error(format!( + "Expected a \"{}\" module but loaded a \"{}\" module.", + module_request.expected_module_type, module_source.module_type, + ))); + } + // Register the module in the module map unless it's already there. If the // specified URL and the "true" URL are different, register the alias. if module_source.module_url_specified != module_source.module_url_found { self.module_map_rc.borrow_mut().alias( &module_source.module_url_specified, + module_source.module_type, &module_source.module_url_found, ); } let maybe_module_id = self .module_map_rc .borrow() - .get_id(&module_source.module_url_found); + .get_id(&module_source.module_url_found, module_source.module_type); let module_id = match maybe_module_id { Some(id) => { debug!( @@ -330,12 +518,21 @@ impl RecursiveModuleLoad { ); id } - None => self.module_map_rc.borrow_mut().new_module( - scope, - self.is_currently_loading_main_module(), - &module_source.module_url_found, - &module_source.code, - )?, + None => match module_source.module_type { + ModuleType::JavaScript => { + self.module_map_rc.borrow_mut().new_es_module( + scope, + self.is_currently_loading_main_module(), + &module_source.module_url_found, + &module_source.code, + )? + } + ModuleType::Json => self.module_map_rc.borrow_mut().new_json_module( + scope, + &module_source.module_url_found, + &module_source.code, + )?, + }, }; // Recurse the module's imports. There are two cases for each import: @@ -346,33 +543,43 @@ impl RecursiveModuleLoad { // recursed synchronously here. // This robustly ensures that the whole graph is in the module map before // `LoadState::Done` is set. - let specifier = - crate::resolve_url(&module_source.module_url_found).unwrap(); let mut already_registered = VecDeque::new(); - already_registered.push_back((module_id, specifier.clone())); - self.visited.insert(specifier); - while let Some((module_id, referrer)) = already_registered.pop_front() { + already_registered.push_back((module_id, module_request.clone())); + self.visited.insert(module_request.clone()); + while let Some((module_id, module_request)) = already_registered.pop_front() + { + let referrer = module_request.specifier.clone(); let imports = self .module_map_rc .borrow() - .get_children(module_id) + .get_requested_modules(module_id) .unwrap() .clone(); - for specifier in imports { - if !self.visited.contains(&specifier) { - if let Some(module_id) = - self.module_map_rc.borrow().get_id(specifier.as_str()) - { - already_registered.push_back((module_id, specifier.clone())); + for module_request in imports { + if !self.visited.contains(&module_request) { + if let Some(module_id) = self.module_map_rc.borrow().get_id( + module_request.specifier.as_str(), + module_request.expected_module_type, + ) { + already_registered.push_back((module_id, module_request.clone())); } else { - let fut = self.loader.load( - &specifier, - Some(referrer.clone()), - self.is_dynamic_import(), - ); + let referrer = referrer.clone(); + let request = module_request.clone(); + let loader = self.loader.clone(); + let is_dynamic_import = self.is_dynamic_import(); + let fut = async move { + let load_result = loader + .load( + &request.specifier, + Some(referrer.clone()), + is_dynamic_import, + ) + .await; + load_result.map(|s| (request, s)) + }; self.pending.push(fut.boxed_local()); } - self.visited.insert(specifier); + self.visited.insert(module_request); } } } @@ -380,6 +587,7 @@ impl RecursiveModuleLoad { // Update `self.state` however applicable. if self.state == LoadState::LoadingRoot { self.root_module_id = Some(module_id); + self.root_module_type = Some(module_source.module_type); self.state = LoadState::LoadingImports; } if self.pending.is_empty() { @@ -391,7 +599,7 @@ impl RecursiveModuleLoad { } impl Stream for RecursiveModuleLoad { - type Item = Result<ModuleSource, Error>; + type Item = Result<(ModuleRequest, ModuleSource), Error>; fn poll_next( self: Pin<&mut Self>, @@ -407,31 +615,51 @@ impl Stream for RecursiveModuleLoad { Err(error) => return Poll::Ready(Some(Err(error))), }; let load_fut = if let Some(_module_id) = inner.root_module_id { + // FIXME(bartlomieju): this is very bad // The root module is already in the module map. // TODO(nayeemrmn): In this case we would ideally skip to // `LoadState::LoadingImports` and synchronously recurse the imports // like the bottom of `RecursiveModuleLoad::register_and_recurse()`. // But the module map cannot be borrowed here. Instead fake a load // event so it gets passed to that function and recursed eventually. - futures::future::ok(ModuleSource { + let module_type = inner.root_module_type.unwrap(); + let module_request = ModuleRequest { + specifier: module_specifier.clone(), + expected_module_type: module_type, + }; + let module_source = ModuleSource { module_url_specified: module_specifier.to_string(), module_url_found: module_specifier.to_string(), // The code will be discarded, since this module is already in the // module map. code: Default::default(), - }) - .boxed() + module_type, + }; + futures::future::ok((module_request, module_source)).boxed() } else { let maybe_referrer = match inner.init { - LoadInit::DynamicImport(_, ref referrer) => { - crate::resolve_url(referrer).ok() + LoadInit::DynamicImport(_, ref referrer, _) => { + resolve_url(referrer).ok() } _ => None, }; - inner - .loader - .load(&module_specifier, maybe_referrer, inner.is_dynamic_import()) - .boxed_local() + let expected_module_type = match inner.init { + LoadInit::DynamicImport(_, _, module_type) => module_type, + _ => ModuleType::JavaScript, + }; + let module_request = ModuleRequest { + specifier: module_specifier.clone(), + expected_module_type, + }; + let loader = inner.loader.clone(); + let is_dynamic_import = inner.is_dynamic_import(); + async move { + let result = loader + .load(&module_specifier, maybe_referrer, is_dynamic_import) + .await; + result.map(|s| (module_request, s)) + } + .boxed_local() }; inner.pending.push(load_fut); inner.state = LoadState::LoadingRoot; @@ -449,12 +677,22 @@ impl Stream for RecursiveModuleLoad { } } +/// Describes what is the expected type of module, usually +/// it's `ModuleType::JavaScript`, but if there were import assertions +/// it might be `ModuleType::Json`. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct ModuleRequest { + pub specifier: ModuleSpecifier, + pub expected_module_type: ModuleType, +} + pub struct ModuleInfo { pub id: ModuleId, // Used in "bindings.rs" for "import.meta.main" property value. pub main: bool, pub name: String, - pub import_specifiers: Vec<ModuleSpecifier>, + pub requests: Vec<ModuleRequest>, + pub module_type: ModuleType, } /// A symbolic module entity. @@ -473,8 +711,9 @@ pub struct ModuleMap { ids_by_handle: HashMap<v8::Global<v8::Module>, ModuleId>, handles_by_id: HashMap<ModuleId, v8::Global<v8::Module>>, info: HashMap<ModuleId, ModuleInfo>, - by_name: HashMap<String, SymbolicModule>, + by_name: HashMap<(String, ModuleType), SymbolicModule>, next_module_id: ModuleId, + next_load_id: ModuleLoadId, // Handling of futures for loading module sources pub loader: Rc<dyn ModuleLoader>, @@ -485,6 +724,10 @@ pub struct ModuleMap { FuturesUnordered<Pin<Box<PrepareLoadFuture>>>, pub(crate) pending_dynamic_imports: FuturesUnordered<StreamFuture<RecursiveModuleLoad>>, + + // This store is used temporarly, to forward parsed JSON + // value from `new_json_module` to `json_module_evaluation_steps` + json_value_store: HashMap<v8::Global<v8::Module>, v8::Global<v8::Value>>, } impl ModuleMap { @@ -498,20 +741,27 @@ impl ModuleMap { info: HashMap::new(), by_name: HashMap::new(), next_module_id: 1, + next_load_id: 1, loader, op_state, dynamic_import_map: HashMap::new(), preparing_dynamic_imports: FuturesUnordered::new(), pending_dynamic_imports: FuturesUnordered::new(), + json_value_store: HashMap::new(), } } /// Get module id, following all aliases in case of module specifier /// that had been redirected. - pub fn get_id(&self, name: &str) -> Option<ModuleId> { + pub fn get_id( + &self, + name: &str, + module_type: ModuleType, + ) -> Option<ModuleId> { let mut mod_name = name; loop { - let symbolic_module = self.by_name.get(mod_name)?; + let symbolic_module = + self.by_name.get(&(mod_name.to_string(), module_type))?; match symbolic_module { SymbolicModule::Alias(target) => { mod_name = target; @@ -521,8 +771,64 @@ impl ModuleMap { } } + fn new_json_module( + &mut self, + scope: &mut v8::HandleScope, + name: &str, + source: &str, + ) -> Result<ModuleId, Error> { + let name_str = v8::String::new(scope, name).unwrap(); + let source_str = v8::String::new(scope, strip_bom(source)).unwrap(); + + let tc_scope = &mut v8::TryCatch::new(scope); + + let parsed_json = match v8::json::parse(tc_scope, source_str) { + Some(parsed_json) => parsed_json, + None => { + assert!(tc_scope.has_caught()); + let exception = tc_scope.exception().unwrap(); + let err = exception_to_err_result(tc_scope, exception, false) + .map_err(|err| attach_handle_to_error(tc_scope, err, exception)); + return err; + } + }; + + let export_names = [v8::String::new(tc_scope, "default").unwrap()]; + let module = v8::Module::create_synthetic_module( + tc_scope, + name_str, + &export_names, + json_module_evaluation_steps, + ); + + let handle = v8::Global::<v8::Module>::new(tc_scope, module); + let value_handle = v8::Global::<v8::Value>::new(tc_scope, parsed_json); + self.json_value_store.insert(handle.clone(), value_handle); + + let id = self.next_module_id; + self.next_module_id += 1; + self.by_name.insert( + (name.to_string(), ModuleType::Json), + SymbolicModule::Mod(id), + ); + self.handles_by_id.insert(id, handle.clone()); + self.ids_by_handle.insert(handle, id); + self.info.insert( + id, + ModuleInfo { + id, + main: false, + name: name.to_string(), + requests: vec![], + module_type: ModuleType::Json, + }, + ); + + Ok(id) + } + // Create and compile an ES module. - pub(crate) fn new_module( + pub(crate) fn new_es_module( &mut self, scope: &mut v8::HandleScope, main: bool, @@ -547,7 +853,7 @@ impl ModuleMap { let module = maybe_module.unwrap(); - let mut import_specifiers: Vec<ModuleSpecifier> = vec![]; + let mut requests: Vec<ModuleRequest> = vec![]; let module_requests = module.get_module_requests(); for i in 0..module_requests.length() { let module_request = v8::Local::<v8::ModuleRequest>::try_from( @@ -557,9 +863,31 @@ impl ModuleMap { let import_specifier = module_request .get_specifier() .to_rust_string_lossy(tc_scope); + + let import_assertions = module_request.get_import_assertions(); + + let assertions = parse_import_assertions( + tc_scope, + import_assertions, + ImportAssertionsKind::StaticImport, + ); + + // FIXME(bartomieju): there are no stack frames if exception + // is thrown here + validate_import_assertions(tc_scope, &assertions); + if tc_scope.has_caught() { + let e = tc_scope.exception().unwrap(); + return exception_to_err_result(tc_scope, e, false); + } + let module_specifier = self.loader.resolve(&import_specifier, name, false)?; - import_specifiers.push(module_specifier); + let expected_module_type = get_module_type_from_assertions(&assertions); + let request = ModuleRequest { + specifier: module_specifier, + expected_module_type, + }; + requests.push(request); } if main { @@ -576,9 +904,10 @@ impl ModuleMap { let handle = v8::Global::<v8::Module>::new(tc_scope, module); let id = self.next_module_id; self.next_module_id += 1; - self - .by_name - .insert(name.to_string(), SymbolicModule::Mod(id)); + self.by_name.insert( + (name.to_string(), ModuleType::JavaScript), + SymbolicModule::Mod(id), + ); self.handles_by_id.insert(id, handle.clone()); self.ids_by_handle.insert(handle, id); self.info.insert( @@ -587,30 +916,44 @@ impl ModuleMap { id, main, name: name.to_string(), - import_specifiers, + requests, + module_type: ModuleType::JavaScript, }, ); Ok(id) } - pub fn get_children(&self, id: ModuleId) -> Option<&Vec<ModuleSpecifier>> { - self.info.get(&id).map(|i| &i.import_specifiers) + pub fn get_requested_modules( + &self, + id: ModuleId, + ) -> Option<&Vec<ModuleRequest>> { + self.info.get(&id).map(|i| &i.requests) } - pub fn is_registered(&self, specifier: &ModuleSpecifier) -> bool { - self.get_id(specifier.as_str()).is_some() + pub fn is_registered( + &self, + specifier: &ModuleSpecifier, + module_type: ModuleType, + ) -> bool { + if let Some(id) = self.get_id(specifier.as_str(), module_type) { + let info = self.get_info_by_id(&id).unwrap(); + return info.module_type == module_type; + } + + false } - pub fn alias(&mut self, name: &str, target: &str) { - self - .by_name - .insert(name.to_string(), SymbolicModule::Alias(target.to_string())); + pub fn alias(&mut self, name: &str, module_type: ModuleType, target: &str) { + self.by_name.insert( + (name.to_string(), module_type), + SymbolicModule::Alias(target.to_string()), + ); } #[cfg(test)] - pub fn is_alias(&self, name: &str) -> bool { - let cond = self.by_name.get(name); + pub fn is_alias(&self, name: &str, module_type: ModuleType) -> bool { + let cond = self.by_name.get(&(name.to_string(), module_type)); matches!(cond, Some(SymbolicModule::Alias(_))) } @@ -656,11 +999,13 @@ impl ModuleMap { module_map_rc: Rc<RefCell<ModuleMap>>, specifier: &str, referrer: &str, + module_type: ModuleType, resolver_handle: v8::Global<v8::PromiseResolver>, ) { let load = RecursiveModuleLoad::dynamic_import( specifier, referrer, + module_type, module_map_rc.clone(), ); module_map_rc @@ -673,7 +1018,10 @@ impl ModuleMap { .resolve(specifier, referrer, false); let fut = match resolve_result { Ok(module_specifier) => { - if module_map_rc.borrow().is_registered(&module_specifier) { + if module_map_rc + .borrow() + .is_registered(&module_specifier, module_type) + { async move { (load.id, Ok(load)) }.boxed_local() } else { async move { (load.id, load.prepare().await.map(|()| load)) } @@ -699,13 +1047,16 @@ impl ModuleMap { scope: &mut v8::HandleScope<'s>, specifier: &str, referrer: &str, + import_assertions: HashMap<String, String>, ) -> Option<v8::Local<'s, v8::Module>> { let resolved_specifier = self .loader .resolve(specifier, referrer, false) .expect("Module should have been already resolved"); - if let Some(id) = self.get_id(resolved_specifier.as_str()) { + let module_type = get_module_type_from_assertions(&import_assertions); + + if let Some(id) = self.get_id(resolved_specifier.as_str(), module_type) { if let Some(handle) = self.get_handle(id) { return Some(v8::Local::new(scope, handle)); } @@ -818,6 +1169,7 @@ mod tests { match mock_source_code(&inner.url) { Some(src) => Poll::Ready(Ok(ModuleSource { code: src.0.to_owned(), + module_type: ModuleType::JavaScript, module_url_specified: inner.url.clone(), module_url_found: src.1.to_owned(), })), @@ -841,7 +1193,7 @@ mod tests { eprintln!(">> RESOLVING, S: {}, R: {}", specifier, referrer); - let output_specifier = match crate::resolve_import(specifier, referrer) { + let output_specifier = match resolve_import(specifier, referrer) { Ok(specifier) => specifier, Err(..) => return Err(MockError::ResolveErr.into()), }; @@ -905,7 +1257,7 @@ mod tests { module_loader: Some(loader), ..Default::default() }); - let spec = crate::resolve_url("file:///a.js").unwrap(); + let spec = resolve_url("file:///a.js").unwrap(); let a_id_fut = runtime.load_main_module(&spec, None); let a_id = futures::executor::block_on(a_id_fut).expect("Failed to load"); @@ -925,26 +1277,47 @@ mod tests { let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); - assert_eq!(modules.get_id("file:///a.js"), Some(a_id)); - let b_id = modules.get_id("file:///b.js").unwrap(); - let c_id = modules.get_id("file:///c.js").unwrap(); - let d_id = modules.get_id("file:///d.js").unwrap(); assert_eq!( - modules.get_children(a_id), + modules.get_id("file:///a.js", ModuleType::JavaScript), + Some(a_id) + ); + let b_id = modules + .get_id("file:///b.js", ModuleType::JavaScript) + .unwrap(); + let c_id = modules + .get_id("file:///c.js", ModuleType::JavaScript) + .unwrap(); + let d_id = modules + .get_id("file:///d.js", ModuleType::JavaScript) + .unwrap(); + assert_eq!( + modules.get_requested_modules(a_id), Some(&vec![ - crate::resolve_url("file:///b.js").unwrap(), - crate::resolve_url("file:///c.js").unwrap() + ModuleRequest { + specifier: resolve_url("file:///b.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }, + ModuleRequest { + specifier: resolve_url("file:///c.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }, ]) ); assert_eq!( - modules.get_children(b_id), - Some(&vec![crate::resolve_url("file:///c.js").unwrap()]) + modules.get_requested_modules(b_id), + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///c.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + },]) ); assert_eq!( - modules.get_children(c_id), - Some(&vec![crate::resolve_url("file:///d.js").unwrap()]) + modules.get_requested_modules(c_id), + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///d.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + },]) ); - assert_eq!(modules.get_children(d_id), Some(&vec![])); + assert_eq!(modules.get_requested_modules(d_id), Some(&vec![])); } const CIRCULAR1_SRC: &str = r#" @@ -980,7 +1353,7 @@ mod tests { self.count.fetch_add(1, Ordering::Relaxed); assert_eq!(specifier, "./b.js"); assert_eq!(referrer, "file:///a.js"); - let s = crate::resolve_import(specifier, referrer).unwrap(); + let s = resolve_import(specifier, referrer).unwrap(); Ok(s) } @@ -1037,7 +1410,7 @@ mod tests { let mut module_map = module_map_rc.borrow_mut(); let specifier_a = "file:///a.js".to_string(); let mod_a = module_map - .new_module( + .new_es_module( scope, true, &specifier_a, @@ -1051,21 +1424,24 @@ mod tests { .unwrap(); assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); - let imports = module_map.get_children(mod_a); + let imports = module_map.get_requested_modules(mod_a); assert_eq!( imports, - Some(&vec![crate::resolve_url("file:///b.js").unwrap()]) + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///b.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + },]) ); let mod_b = module_map - .new_module( + .new_es_module( scope, false, "file:///b.js", "export function b() { return 'b' }", ) .unwrap(); - let imports = module_map.get_children(mod_b).unwrap(); + let imports = module_map.get_requested_modules(mod_b).unwrap(); assert_eq!(imports.len(), 0); (mod_a, mod_b) }; @@ -1082,6 +1458,109 @@ mod tests { } #[test] + fn test_json_module() { + #[derive(Default)] + struct ModsLoader { + pub count: Arc<AtomicUsize>, + } + + impl ModuleLoader for ModsLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _is_main: bool, + ) -> Result<ModuleSpecifier, Error> { + self.count.fetch_add(1, Ordering::Relaxed); + assert_eq!(specifier, "./b.json"); + assert_eq!(referrer, "file:///a.js"); + let s = resolve_import(specifier, referrer).unwrap(); + Ok(s) + } + + fn load( + &self, + _module_specifier: &ModuleSpecifier, + _maybe_referrer: Option<ModuleSpecifier>, + _is_dyn_import: bool, + ) -> Pin<Box<ModuleSourceFuture>> { + unreachable!() + } + } + + let loader = Rc::new(ModsLoader::default()); + + let resolve_count = loader.count.clone(); + + let mut runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(loader), + ..Default::default() + }); + + runtime + .execute_script( + "setup.js", + r#" + function assert(cond) { + if (!cond) { + throw Error("assert"); + } + } + "#, + ) + .unwrap(); + + let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + + let (mod_a, mod_b) = { + let scope = &mut runtime.handle_scope(); + let mut module_map = module_map_rc.borrow_mut(); + let specifier_a = "file:///a.js".to_string(); + let mod_a = module_map + .new_es_module( + scope, + true, + &specifier_a, + r#" + import jsonData from './b.json' assert {type: "json"}; + assert(jsonData.a == "b"); + assert(jsonData.c.d == 10); + "#, + ) + .unwrap(); + + let imports = module_map.get_requested_modules(mod_a); + assert_eq!( + imports, + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///b.json").unwrap(), + expected_module_type: ModuleType::Json, + },]) + ); + + let mod_b = module_map + .new_json_module( + scope, + "file:///b.json", + "{\"a\": \"b\", \"c\": {\"d\": 10}}", + ) + .unwrap(); + let imports = module_map.get_requested_modules(mod_b).unwrap(); + assert_eq!(imports.len(), 0); + (mod_a, mod_b) + }; + + runtime.instantiate_module(mod_b).unwrap(); + assert_eq!(resolve_count.load(Ordering::SeqCst), 1); + + runtime.instantiate_module(mod_a).unwrap(); + + let receiver = runtime.mod_evaluate(mod_a); + futures::executor::block_on(runtime.run_event_loop(false)).unwrap(); + futures::executor::block_on(receiver).unwrap().unwrap(); + } + + #[test] fn dyn_import_err() { #[derive(Clone, Default)] struct DynImportErrLoader { @@ -1098,7 +1577,7 @@ mod tests { self.count.fetch_add(1, Ordering::Relaxed); assert_eq!(specifier, "/foo.js"); assert_eq!(referrer, "file:///dyn_import2.js"); - let s = crate::resolve_import(specifier, referrer).unwrap(); + let s = resolve_import(specifier, referrer).unwrap(); Ok(s) } @@ -1159,7 +1638,7 @@ mod tests { assert!(c < 7); assert_eq!(specifier, "./b.js"); assert_eq!(referrer, "file:///dyn_import3.js"); - let s = crate::resolve_import(specifier, referrer).unwrap(); + let s = resolve_import(specifier, referrer).unwrap(); Ok(s) } @@ -1174,6 +1653,7 @@ mod tests { module_url_specified: specifier.to_string(), module_url_found: specifier.to_string(), code: "export function b() { return 'b' }".to_owned(), + module_type: ModuleType::JavaScript, }; async move { Ok(info) }.boxed() } @@ -1292,7 +1772,7 @@ mod tests { _is_main: bool, ) -> Result<ModuleSpecifier, Error> { self.resolve_count.fetch_add(1, Ordering::Relaxed); - let s = crate::resolve_import(specifier, referrer).unwrap(); + let s = resolve_import(specifier, referrer).unwrap(); Ok(s) } @@ -1320,6 +1800,7 @@ mod tests { module_url_specified: specifier.to_string(), module_url_found: specifier.to_string(), code: code.to_owned(), + module_type: ModuleType::JavaScript, }; async move { Ok(info) }.boxed() } @@ -1357,7 +1838,7 @@ mod tests { }); let fut = async move { - let spec = crate::resolve_url("file:///circular1.js").unwrap(); + let spec = resolve_url("file:///circular1.js").unwrap(); let result = runtime.load_main_module(&spec, None).await; assert!(result.is_ok()); let circular1_id = result.unwrap(); @@ -1377,26 +1858,47 @@ mod tests { let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); - assert_eq!(modules.get_id("file:///circular1.js"), Some(circular1_id)); - let circular2_id = modules.get_id("file:///circular2.js").unwrap(); + assert_eq!( + modules.get_id("file:///circular1.js", ModuleType::JavaScript), + Some(circular1_id) + ); + let circular2_id = modules + .get_id("file:///circular2.js", ModuleType::JavaScript) + .unwrap(); assert_eq!( - modules.get_children(circular1_id), - Some(&vec![crate::resolve_url("file:///circular2.js").unwrap()]) + modules.get_requested_modules(circular1_id), + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///circular2.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }]) ); assert_eq!( - modules.get_children(circular2_id), - Some(&vec![crate::resolve_url("file:///circular3.js").unwrap()]) + modules.get_requested_modules(circular2_id), + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///circular3.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }]) ); - assert!(modules.get_id("file:///circular3.js").is_some()); - let circular3_id = modules.get_id("file:///circular3.js").unwrap(); + assert!(modules + .get_id("file:///circular3.js", ModuleType::JavaScript) + .is_some()); + let circular3_id = modules + .get_id("file:///circular3.js", ModuleType::JavaScript) + .unwrap(); assert_eq!( - modules.get_children(circular3_id), + modules.get_requested_modules(circular3_id), Some(&vec![ - crate::resolve_url("file:///circular1.js").unwrap(), - crate::resolve_url("file:///circular2.js").unwrap() + ModuleRequest { + specifier: resolve_url("file:///circular1.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }, + ModuleRequest { + specifier: resolve_url("file:///circular2.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + } ]) ); } @@ -1428,43 +1930,62 @@ mod tests { ..Default::default() }); - let fut = async move { - let spec = crate::resolve_url("file:///redirect1.js").unwrap(); - let result = runtime.load_main_module(&spec, None).await; - println!(">> result {:?}", result); - assert!(result.is_ok()); - let redirect1_id = result.unwrap(); - let _ = runtime.mod_evaluate(redirect1_id); - runtime.run_event_loop(false).await.unwrap(); - let l = loads.lock(); - assert_eq!( - l.to_vec(), - vec![ - "file:///redirect1.js", - "file:///redirect2.js", - "file:///dir/redirect3.js" - ] - ); + let fut = + async move { + let spec = resolve_url("file:///redirect1.js").unwrap(); + let result = runtime.load_main_module(&spec, None).await; + println!(">> result {:?}", result); + assert!(result.is_ok()); + let redirect1_id = result.unwrap(); + let _ = runtime.mod_evaluate(redirect1_id); + runtime.run_event_loop(false).await.unwrap(); + let l = loads.lock(); + assert_eq!( + l.to_vec(), + vec![ + "file:///redirect1.js", + "file:///redirect2.js", + "file:///dir/redirect3.js" + ] + ); - let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); - let modules = module_map_rc.borrow(); + let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); + let modules = module_map_rc.borrow(); - assert_eq!(modules.get_id("file:///redirect1.js"), Some(redirect1_id)); + assert_eq!( + modules.get_id("file:///redirect1.js", ModuleType::JavaScript), + Some(redirect1_id) + ); - let redirect2_id = modules.get_id("file:///dir/redirect2.js").unwrap(); - assert!(modules.is_alias("file:///redirect2.js")); - assert!(!modules.is_alias("file:///dir/redirect2.js")); - assert_eq!(modules.get_id("file:///redirect2.js"), Some(redirect2_id)); + let redirect2_id = modules + .get_id("file:///dir/redirect2.js", ModuleType::JavaScript) + .unwrap(); + assert!( + modules.is_alias("file:///redirect2.js", ModuleType::JavaScript) + ); + assert!( + !modules.is_alias("file:///dir/redirect2.js", ModuleType::JavaScript) + ); + assert_eq!( + modules.get_id("file:///redirect2.js", ModuleType::JavaScript), + Some(redirect2_id) + ); - let redirect3_id = modules.get_id("file:///redirect3.js").unwrap(); - assert!(modules.is_alias("file:///dir/redirect3.js")); - assert!(!modules.is_alias("file:///redirect3.js")); - assert_eq!( - modules.get_id("file:///dir/redirect3.js"), - Some(redirect3_id) - ); - } - .boxed_local(); + let redirect3_id = modules + .get_id("file:///redirect3.js", ModuleType::JavaScript) + .unwrap(); + assert!( + modules.is_alias("file:///dir/redirect3.js", ModuleType::JavaScript) + ); + assert!( + !modules.is_alias("file:///redirect3.js", ModuleType::JavaScript) + ); + assert_eq!( + modules.get_id("file:///dir/redirect3.js", ModuleType::JavaScript), + Some(redirect3_id) + ); + } + .boxed_local(); futures::executor::block_on(fut); } @@ -1494,7 +2015,7 @@ mod tests { module_loader: Some(loader), ..Default::default() }); - let spec = crate::resolve_url("file:///main.js").unwrap(); + let spec = resolve_url("file:///main.js").unwrap(); let mut recursive_load = runtime.load_main_module(&spec, None).boxed_local(); @@ -1543,7 +2064,7 @@ mod tests { module_loader: Some(loader), ..Default::default() }); - let spec = crate::resolve_url("file:///bad_import.js").unwrap(); + let spec = resolve_url("file:///bad_import.js").unwrap(); let mut load_fut = runtime.load_main_module(&spec, None).boxed_local(); let result = load_fut.poll_unpin(cx); if let Poll::Ready(Err(err)) = result { @@ -1577,7 +2098,7 @@ mod tests { // In default resolution code should be empty. // Instead we explicitly pass in our own code. // The behavior should be very similar to /a.js. - let spec = crate::resolve_url("file:///main_with_code.js").unwrap(); + let spec = resolve_url("file:///main_with_code.js").unwrap(); let main_id_fut = runtime .load_main_module(&spec, Some(MAIN_WITH_CODE_SRC.to_owned())) .boxed_local(); @@ -1596,35 +2117,56 @@ mod tests { let module_map_rc = JsRuntime::module_map(runtime.v8_isolate()); let modules = module_map_rc.borrow(); - assert_eq!(modules.get_id("file:///main_with_code.js"), Some(main_id)); - let b_id = modules.get_id("file:///b.js").unwrap(); - let c_id = modules.get_id("file:///c.js").unwrap(); - let d_id = modules.get_id("file:///d.js").unwrap(); + assert_eq!( + modules.get_id("file:///main_with_code.js", ModuleType::JavaScript), + Some(main_id) + ); + let b_id = modules + .get_id("file:///b.js", ModuleType::JavaScript) + .unwrap(); + let c_id = modules + .get_id("file:///c.js", ModuleType::JavaScript) + .unwrap(); + let d_id = modules + .get_id("file:///d.js", ModuleType::JavaScript) + .unwrap(); assert_eq!( - modules.get_children(main_id), + modules.get_requested_modules(main_id), Some(&vec![ - crate::resolve_url("file:///b.js").unwrap(), - crate::resolve_url("file:///c.js").unwrap() + ModuleRequest { + specifier: resolve_url("file:///b.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }, + ModuleRequest { + specifier: resolve_url("file:///c.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + } ]) ); assert_eq!( - modules.get_children(b_id), - Some(&vec![crate::resolve_url("file:///c.js").unwrap()]) + modules.get_requested_modules(b_id), + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///c.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }]) ); assert_eq!( - modules.get_children(c_id), - Some(&vec![crate::resolve_url("file:///d.js").unwrap()]) + modules.get_requested_modules(c_id), + Some(&vec![ModuleRequest { + specifier: resolve_url("file:///d.js").unwrap(), + expected_module_type: ModuleType::JavaScript, + }]) ); - assert_eq!(modules.get_children(d_id), Some(&vec![])); + assert_eq!(modules.get_requested_modules(d_id), Some(&vec![])); } #[test] fn main_and_side_module() { struct ModsLoader {} - let main_specifier = crate::resolve_url("file:///main_module.js").unwrap(); - let side_specifier = crate::resolve_url("file:///side_module.js").unwrap(); + let main_specifier = resolve_url("file:///main_module.js").unwrap(); + let side_specifier = resolve_url("file:///side_module.js").unwrap(); impl ModuleLoader for ModsLoader { fn resolve( @@ -1633,7 +2175,7 @@ mod tests { referrer: &str, _is_main: bool, ) -> Result<ModuleSpecifier, Error> { - let s = crate::resolve_import(specifier, referrer).unwrap(); + let s = resolve_import(specifier, referrer).unwrap(); Ok(s) } @@ -1648,11 +2190,13 @@ mod tests { module_url_specified: "file:///main_module.js".to_string(), module_url_found: "file:///main_module.js".to_string(), code: "if (!import.meta.main) throw Error();".to_owned(), + module_type: ModuleType::JavaScript, }), "file:///side_module.js" => Ok(ModuleSource { module_url_specified: "file:///side_module.js".to_string(), module_url_found: "file:///side_module.js".to_string(), code: "if (import.meta.main) throw Error();".to_owned(), + module_type: ModuleType::JavaScript, }), _ => unreachable!(), }; |