diff options
-rw-r--r-- | cli/state.rs | 8 | ||||
-rw-r--r-- | cli/worker.rs | 24 | ||||
-rw-r--r-- | core/isolate.rs | 279 | ||||
-rw-r--r-- | core/module_specifier.rs | 6 | ||||
-rw-r--r-- | core/modules.rs | 756 | ||||
-rw-r--r-- | tests/013_dynamic_import.test (renamed from tests/013_dynamic_import.disabled) | 0 | ||||
-rw-r--r-- | tests/014_duplicate_import.test (renamed from tests/014_duplicate_import.disabled) | 0 | ||||
-rw-r--r-- | tests/015_duplicate_parallel_import.js | 20 | ||||
-rw-r--r-- | tests/015_duplicate_parallel_import.js.out | 1 | ||||
-rw-r--r-- | tests/015_duplicate_parallel_import.test | 2 | ||||
-rw-r--r-- | tests/error_014_catch_dynamic_import_error.js | 31 | ||||
-rw-r--r-- | tests/error_014_catch_dynamic_import_error.js.out | 12 | ||||
-rw-r--r-- | tests/error_014_catch_dynamic_import_error.test | 2 | ||||
-rw-r--r-- | tests/subdir/indirect_import_error.js | 1 | ||||
-rw-r--r-- | tests/subdir/indirect_throws.js | 1 | ||||
-rw-r--r-- | tests/subdir/throws.js | 5 |
16 files changed, 723 insertions, 425 deletions
diff --git a/cli/state.rs b/cli/state.rs index e1480c027..eb912161d 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -118,9 +118,9 @@ impl Loader for ThreadSafeState { &self, specifier: &str, referrer: &str, - is_root: bool, + is_main: bool, ) -> Result<ModuleSpecifier, ErrBox> { - if !is_root { + if !is_main { if let Some(import_map) = &self.import_map { let result = import_map.resolve(specifier, referrer)?; if result.is_some() { @@ -138,12 +138,14 @@ impl Loader for ThreadSafeState { module_specifier: &ModuleSpecifier, ) -> Box<deno::SourceCodeInfoFuture> { self.metrics.resolve_count.fetch_add(1, Ordering::SeqCst); + let module_url_specified = module_specifier.to_string(); Box::new(self.fetch_compiled_module(module_specifier).map( |compiled_module| deno::SourceCodeInfo { // Real module name, might be different from initial specifier // due to redirections. code: compiled_module.code, - module_name: compiled_module.name, + module_url_specified, + module_url_found: compiled_module.name, }, )) } diff --git a/cli/worker.rs b/cli/worker.rs index f707f4a58..db948cc3c 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -5,6 +5,7 @@ use crate::tokio_util; use deno; use deno::ErrBox; use deno::ModuleSpecifier; +use deno::RecursiveLoad; use deno::StartupData; use futures::Async; use futures::Future; @@ -28,10 +29,24 @@ impl Worker { let isolate = Arc::new(Mutex::new(deno::Isolate::new(startup_data, false))); { let mut i = isolate.lock().unwrap(); + let state_ = state.clone(); i.set_dispatch(move |op_id, control_buf, zero_copy_buf| { state_.dispatch(op_id, control_buf, zero_copy_buf) }); + + let state_ = state.clone(); + i.set_dyn_import(move |id, specifier, referrer| { + let load_stream = RecursiveLoad::dynamic_import( + id, + specifier, + referrer, + state_.clone(), + state_.modules.clone(), + ); + Box::new(load_stream) + }); + let state_ = state.clone(); i.set_js_error_create(move |v8_exception| { JSError::from_v8_exception(v8_exception, &state_.ts_compiler) @@ -66,12 +81,9 @@ impl Worker { let loader = self.state.clone(); let isolate = self.isolate.clone(); let modules = self.state.modules.clone(); - let recursive_load = deno::RecursiveLoad::new( - &module_specifier.to_string(), - loader, - isolate, - modules, - ); + let recursive_load = + RecursiveLoad::main(&module_specifier.to_string(), loader, modules) + .get_future(isolate); recursive_load.and_then(move |id| -> Result<(), ErrBox> { worker.state.progress.done(); if is_prefetch { diff --git a/core/isolate.rs b/core/isolate.rs index d3ac4457e..5d6088ca5 100644 --- a/core/isolate.rs +++ b/core/isolate.rs @@ -1,8 +1,9 @@ // Copyright 2018 the Deno authors. All rights reserved. MIT license. -// Do not add dependenies to modules.rs. it should remain decoupled from the -// isolate to keep the Isolate struct from becoming too bloating for users who -// do not need asynchronous module loading. +// Do not add any dependency to modules.rs! +// modules.rs is complex and should remain decoupled from isolate.rs to keep the +// Isolate struct from becoming too bloating for users who do not need +// asynchronous module loading. use crate::any_error::ErrBox; use crate::js_errors::CoreJSError; @@ -18,7 +19,9 @@ use crate::libdeno::Snapshot1; use crate::libdeno::Snapshot2; use crate::shared_queue::SharedQueue; use crate::shared_queue::RECOMMENDED_SIZE; -use futures::stream::{FuturesUnordered, Stream}; +use futures::stream::FuturesUnordered; +use futures::stream::Stream; +use futures::stream::StreamFuture; use futures::task; use futures::Async::*; use futures::Future; @@ -27,6 +30,7 @@ use libc::c_char; use libc::c_void; use std::ffi::CStr; use std::ffi::CString; +use std::fmt; use std::ptr::null; use std::sync::{Arc, Mutex, Once}; @@ -57,6 +61,76 @@ pub struct Script<'a> { pub filename: &'a str, } +/// Represent result of fetching the source code of a module. Found module URL +/// might be different from specified URL used for loading due to redirections +/// (like HTTP 303). E.G. Both https://example.com/a.ts and +/// https://example.com/b.ts may point to https://example.com/c.ts +/// By keeping track of specified and found URL we can alias modules and avoid +/// recompiling the same code 3 times. +#[derive(Debug, Eq, PartialEq)] +pub struct SourceCodeInfo { + pub code: String, + pub module_url_specified: String, + pub module_url_found: String, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum RecursiveLoadEvent { + Fetch(SourceCodeInfo), + Instantiate(deno_mod), +} + +pub trait ImportStream: Stream { + fn register( + &mut self, + source_code_info: SourceCodeInfo, + isolate: &mut Isolate, + ) -> Result<(), ErrBox>; +} + +type DynImportStream = + Box<dyn ImportStream<Item = RecursiveLoadEvent, Error = ErrBox> + Send>; + +type DynImportFn = Fn(deno_dyn_import_id, &str, &str) -> DynImportStream; + +/// Wraps DynImportStream to include the deno_dyn_import_id, so that it doesn't +/// need to be exposed. +#[derive(Debug)] +struct DynImport { + pub id: deno_dyn_import_id, + pub inner: DynImportStream, +} + +impl fmt::Debug for DynImportStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "DynImportStream(..)") + } +} + +impl Stream for DynImport { + type Item = (deno_dyn_import_id, RecursiveLoadEvent); + type Error = (deno_dyn_import_id, ErrBox); + + fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { + match self.inner.poll() { + Ok(Ready(Some(event))) => Ok(Ready(Some((self.id, event)))), + Ok(Ready(None)) => unreachable!(), + Err(e) => Err((self.id, e)), + Ok(NotReady) => Ok(NotReady), + } + } +} + +impl ImportStream for DynImport { + fn register( + &mut self, + source_code_info: SourceCodeInfo, + isolate: &mut Isolate, + ) -> Result<(), ErrBox> { + self.inner.register(source_code_info, isolate) + } +} + // TODO(ry) It's ugly that we have both Script and OwnedScript. Ideally we // wouldn't expose such twiddly complexity. struct OwnedScript { @@ -83,31 +157,8 @@ pub enum StartupData<'a> { None, } -pub type DynImportFuture = - Box<dyn Future<Item = deno_mod, Error = ErrBox> + Send>; -type DynImportFn = dyn Fn(&str, &str) -> DynImportFuture; - type JSErrorCreateFn = dyn Fn(V8Exception) -> ErrBox; -/// Wraps DynImportFuture to include the deno_dyn_import_id, so that it doesn't -/// need to be exposed. -struct DynImport { - id: deno_dyn_import_id, - inner: DynImportFuture, -} - -impl Future for DynImport { - type Item = (deno_dyn_import_id, deno_mod); - type Error = (deno_mod, ErrBox); - fn poll(&mut self) -> Poll<Self::Item, Self::Error> { - match self.inner.poll() { - Ok(Ready(mod_id)) => Ok(Ready((self.id, mod_id))), - Ok(NotReady) => Ok(NotReady), - Err(e) => Err((self.id, e)), - } - } -} - /// A single execution context of JavaScript. Corresponds roughly to the "Web /// Worker" concept in the DOM. An Isolate is a Future that can be used with /// Tokio. The Isolate future complete when there is an error or when all @@ -125,7 +176,7 @@ pub struct Isolate { needs_init: bool, shared: SharedQueue, pending_ops: FuturesUnordered<PendingOpFuture>, - pending_dyn_imports: FuturesUnordered<DynImport>, + pending_dyn_imports: FuturesUnordered<StreamFuture<DynImport>>, have_unpolled_ops: bool, startup_script: Option<OwnedScript>, } @@ -208,7 +259,10 @@ impl Isolate { pub fn set_dyn_import<F>(&mut self, f: F) where - F: Fn(&str, &str) -> DynImportFuture + Send + Sync + 'static, + F: Fn(deno_dyn_import_id, &str, &str) -> DynImportStream + + Send + + Sync + + 'static, { self.dyn_import = Some(Arc::new(f)); } @@ -257,10 +311,10 @@ impl Isolate { debug!("dyn_import specifier {} referrer {} ", specifier, referrer); if let Some(ref f) = isolate.dyn_import { - let inner = f(specifier, referrer); - let fut = DynImport { inner, id }; + let inner = f(id, specifier, referrer); + let stream = DynImport { inner, id }; task::current().notify(); - isolate.pending_dyn_imports.push(fut); + isolate.pending_dyn_imports.push(stream.into_future()); } else { panic!("dyn_import callback not set") } @@ -434,18 +488,19 @@ impl Isolate { let (mod_id, maybe_err_str) = match result { Ok(mod_id) => (mod_id, None), Err(None) => (0, None), - Err(Some(err_str)) => (0, Some(err_str)), + Err(Some(err_str)) => (0, Some(CString::new(err_str).unwrap())), + }; + let err_str_ptr = match maybe_err_str { + Some(ref err_str) => err_str.as_ptr(), + None => std::ptr::null(), }; - let err_ptr = maybe_err_str - .map(|e| e.as_ptr() as *const c_char) - .unwrap_or(std::ptr::null()); unsafe { libdeno::deno_dyn_import_done( self.libdeno_isolate, self.as_raw_ptr(), id, mod_id, - err_ptr, + err_str_ptr, ) }; self.check_last_exception().map_err(|err| { @@ -453,6 +508,46 @@ impl Isolate { err }) } + + fn poll_dyn_imports(&mut self) -> Poll<(), ErrBox> { + use RecursiveLoadEvent::*; + loop { + match self.pending_dyn_imports.poll() { + Ok(NotReady) | Ok(Ready(None)) => { + // There are no active dynamic import loaders, or none are ready. + return Ok(futures::Async::Ready(())); + } + Ok(Ready(Some(( + Some((dyn_import_id, Fetch(source_code_info))), + mut stream, + )))) => { + // A module (not necessarily the one dynamically imported) has been + // fetched. Create and register it, and if successful, poll for the + // next recursive-load event related to this dynamic import. + match stream.register(source_code_info, self) { + Ok(()) => self.pending_dyn_imports.push(stream.into_future()), + Err(err) => { + self.dyn_import_done(dyn_import_id, Err(Some(err.to_string())))? + } + } + } + Ok(Ready(Some((Some((dyn_import_id, Instantiate(module_id))), _)))) => { + // The top-level module from a dynamic import has been instantiated. + match self.mod_evaluate(module_id) { + Ok(()) => self.dyn_import_done(dyn_import_id, Ok(module_id))?, + Err(..) => self.dyn_import_done(dyn_import_id, Err(None))?, + } + } + Err(((dyn_import_id, err), _)) => { + // A non-javascript error occurred; this could be due to a an invalid + // module specifier, or a problem with the source map, or a failure + // to fetch the module source code. + self.dyn_import_done(dyn_import_id, Err(Some(err.to_string())))? + } + Ok(Ready(Some((None, _)))) => unreachable!(), + } + } + } } /// Called during mod_instantiate() to resolve imports. @@ -556,20 +651,7 @@ impl Future for Isolate { loop { // If there are any pending dyn_import futures, do those first. - loop { - match self.pending_dyn_imports.poll() { - Ok(NotReady) | Ok(Ready(None)) => break, - Ok(Ready(Some((dyn_import_id, mod_id)))) => { - match self.mod_evaluate(mod_id) { - Ok(()) => self.dyn_import_done(dyn_import_id, Ok(mod_id))?, - Err(..) => self.dyn_import_done(dyn_import_id, Err(None))?, - } - } - Err((dyn_import_id, err)) => { - self.dyn_import_done(dyn_import_id, Err(Some(err.to_string())))? - } - } - } + self.poll_dyn_imports()?; // Now handle actual ops. self.have_unpolled_ops = false; @@ -612,7 +694,7 @@ impl Future for Isolate { self.check_last_exception()?; // We're idle if pending_ops is empty. - if self.pending_ops.is_empty() { + if self.pending_ops.is_empty() && self.pending_dyn_imports.is_empty() { Ok(futures::Async::Ready(())) } else { if self.have_unpolled_ops { @@ -658,10 +740,11 @@ pub mod tests { use futures::future::lazy; use futures::future::ok; use futures::Async; + use std::io; use std::ops::FnOnce; use std::sync::atomic::{AtomicUsize, Ordering}; - fn run_in_task<F, R>(f: F) -> R + pub fn run_in_task<F, R>(f: F) -> R where F: FnOnce() -> R, { @@ -864,6 +947,40 @@ pub mod tests { }); } + struct MockImportStream(Vec<Result<RecursiveLoadEvent, ErrBox>>); + + impl Stream for MockImportStream { + type Item = RecursiveLoadEvent; + type Error = ErrBox; + fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { + let event = if self.0.is_empty() { + None + } else { + Some(self.0.remove(0)?) + }; + Ok(Ready(event)) + } + } + + impl ImportStream for MockImportStream { + fn register( + &mut self, + module_data: SourceCodeInfo, + isolate: &mut Isolate, + ) -> Result<(), ErrBox> { + let id = isolate.mod_new( + false, + &module_data.module_url_found, + &module_data.code, + )?; + println!( + "MockImportStream register {} {}", + id, module_data.module_url_found + ); + Ok(()) + } + } + #[test] fn dyn_import_err() { // Test an erroneous dynamic import where the specified module isn't found. @@ -871,13 +988,13 @@ pub mod tests { let count = Arc::new(AtomicUsize::new(0)); let count_ = count.clone(); let mut isolate = Isolate::new(StartupData::None, false); - isolate.set_dyn_import(move |specifier, referrer| { + isolate.set_dyn_import(move |_, specifier, referrer| { count_.fetch_add(1, Ordering::Relaxed); assert_eq!(specifier, "foo.js"); assert_eq!(referrer, "dyn_import2.js"); - Box::new(futures::future::err( - std::io::Error::new(std::io::ErrorKind::Other, "oh no!").into(), - )) + let err = io::Error::from(io::ErrorKind::NotFound); + let stream = MockImportStream(vec![Err(err.into())]); + Box::new(stream) }); js_check(isolate.execute( "dyn_import2.js", @@ -902,17 +1019,29 @@ pub mod tests { let count_ = count.clone(); // Sometimes Rust is really annoying. - use std::sync::Mutex; let mod_b = Arc::new(Mutex::new(0)); let mod_b2 = mod_b.clone(); let mut isolate = Isolate::new(StartupData::None, false); - isolate.set_dyn_import(move |_specifier, referrer| { - count_.fetch_add(1, Ordering::Relaxed); - // assert_eq!(specifier, "foo.js"); + isolate.set_dyn_import(move |_id, specifier, referrer| { + let c = count_.fetch_add(1, Ordering::Relaxed); + match c { + 0 => assert_eq!(specifier, "foo1.js"), + 1 => assert_eq!(specifier, "foo2.js"), + _ => unreachable!(), + } assert_eq!(referrer, "dyn_import3.js"); - let mod_id = mod_b2.lock().unwrap(); - Box::new(futures::future::ok(*mod_id)) + let mod_id = *mod_b2.lock().unwrap(); + let source_code_info = SourceCodeInfo { + module_url_specified: "foo.js".to_owned(), + module_url_found: "foo.js".to_owned(), + code: "".to_owned(), + }; + let stream = MockImportStream(vec![ + Ok(RecursiveLoadEvent::Fetch(source_code_info)), + Ok(RecursiveLoadEvent::Instantiate(mod_id)), + ]); + Box::new(stream) }); // Instantiate mod_b @@ -930,18 +1059,18 @@ pub mod tests { js_check(isolate.execute( "dyn_import3.js", r#" - (async () => { - let mod = await import("foo1.js"); - if (mod.b() !== 'b') { - throw Error("bad1"); - } - // And again! - mod = await import("foo2.js"); - if (mod.b() !== 'b') { - throw Error("bad2"); - } - })(); - "#, + (async () => { + let mod = await import("foo1.js"); + if (mod.b() !== 'b') { + throw Error("bad1"); + } + // And again! + mod = await import("foo2.js"); + if (mod.b() !== 'b') { + throw Error("bad2"); + } + })(); + "#, )); assert_eq!(count.load(Ordering::Relaxed), 1); diff --git a/core/module_specifier.rs b/core/module_specifier.rs index 631b0c924..3cfabd03f 100644 --- a/core/module_specifier.rs +++ b/core/module_specifier.rs @@ -41,7 +41,7 @@ impl fmt::Display for ModuleResolutionError { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Eq, Hash, PartialEq)] /// Resolved module specifier pub struct ModuleSpecifier(Url); @@ -50,6 +50,10 @@ impl ModuleSpecifier { &self.0 } + pub fn as_str(&self) -> &str { + self.0.as_str() + } + /// Resolves module using this algorithm: /// https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier pub fn resolve_import( diff --git a/core/modules.rs b/core/modules.rs index ea47b316e..072de4bc9 100644 --- a/core/modules.rs +++ b/core/modules.rs @@ -7,32 +7,26 @@ // synchronously. The isolate.rs module should never depend on this module. use crate::any_error::ErrBox; +use crate::isolate::ImportStream; use crate::isolate::Isolate; +use crate::isolate::RecursiveLoadEvent as Event; +use crate::isolate::SourceCodeInfo; +use crate::libdeno::deno_dyn_import_id; use crate::libdeno::deno_mod; use crate::module_specifier::ModuleSpecifier; -use futures::Async; +use futures::future::loop_fn; +use futures::future::Loop; +use futures::stream::FuturesUnordered; +use futures::stream::Stream; +use futures::Async::*; use futures::Future; use futures::Poll; use std::collections::HashMap; use std::collections::HashSet; use std::fmt; -use std::marker::PhantomData; use std::sync::Arc; use std::sync::Mutex; -/// Represent result of fetching the source code of a module. -/// Contains both module name and code. -/// Module name might be different from initial URL used for loading -/// due to redirections. -/// e.g. Both https://example.com/a.ts and https://example.com/b.ts -/// may point to https://example.com/c.ts. By specifying module_name -/// all be https://example.com/c.ts in module_name (for aliasing), -/// we avoid recompiling the same code for 3 different times. -pub struct SourceCodeInfo { - pub module_name: String, - pub code: String, -} - pub type SourceCodeInfoFuture = dyn Future<Item = SourceCodeInfo, Error = ErrBox> + Send; @@ -45,7 +39,7 @@ pub trait Loader: Send + Sync { &self, specifier: &str, referrer: &str, - is_root: bool, + is_main: bool, ) -> Result<ModuleSpecifier, ErrBox>; /// Given ModuleSpecifier, load its source code. @@ -55,209 +49,269 @@ pub trait Loader: Send + Sync { ) -> Box<SourceCodeInfoFuture>; } -struct PendingLoad { - url: String, - is_root: bool, - source_code_info_future: Box<SourceCodeInfoFuture>, +#[derive(Debug, Eq, PartialEq)] +enum Kind { + Main, + DynamicImport(deno_dyn_import_id), +} + +#[derive(Debug, Eq, PartialEq)] +enum State { + ResolveMain(String), // specifier + ResolveImport(String, String), // specifier, referrer + LoadingRoot, + LoadingImports(deno_mod), + Instantiated(deno_mod), } /// This future is used to implement parallel async module loading without -/// complicating the Isolate API. Note that RecursiveLoad will take ownership of -/// an Isolate during load. +/// complicating the Isolate API. +/// TODO: RecursiveLoad desperately needs to be merged with Modules. pub struct RecursiveLoad<L: Loader> { + kind: Kind, + state: State, loader: L, - isolate: Arc<Mutex<Isolate>>, modules: Arc<Mutex<Modules>>, - pending: Vec<PendingLoad>, - is_pending: HashSet<String>, - phantom: PhantomData<L>, - // TODO(ry) The following can all be combined into a single enum State type. - root: Option<String>, // Empty before polled. - root_specifier: Option<String>, // Empty after first poll - root_id: Option<deno_mod>, + pending: FuturesUnordered<Box<SourceCodeInfoFuture>>, + is_pending: HashSet<ModuleSpecifier>, } impl<L: Loader> RecursiveLoad<L> { - /// Starts a new parallel load of the given URL. - pub fn new( - url: &str, + /// Starts a new parallel load of the given URL of the main module. + pub fn main( + specifier: &str, + loader: L, + modules: Arc<Mutex<Modules>>, + ) -> Self { + let kind = Kind::Main; + let state = State::ResolveMain(specifier.to_owned()); + Self::new(kind, state, loader, modules) + } + + pub fn dynamic_import( + id: deno_dyn_import_id, + specifier: &str, + referrer: &str, + loader: L, + modules: Arc<Mutex<Modules>>, + ) -> Self { + let kind = Kind::DynamicImport(id); + let state = State::ResolveImport(specifier.to_owned(), referrer.to_owned()); + Self::new(kind, state, loader, modules) + } + + pub fn dyn_import_id(&self) -> Option<deno_dyn_import_id> { + match self.kind { + Kind::Main => None, + Kind::DynamicImport(id) => Some(id), + } + } + + fn new( + kind: Kind, + state: State, loader: L, - isolate: Arc<Mutex<Isolate>>, modules: Arc<Mutex<Modules>>, ) -> Self { Self { + kind, + state, loader, - isolate, modules, - root: None, - root_specifier: Some(url.to_string()), - root_id: None, - pending: Vec::new(), + pending: FuturesUnordered::new(), is_pending: HashSet::new(), - phantom: PhantomData, } } - fn add( + fn add_root(&mut self) -> Result<(), ErrBox> { + let module_specifier = match self.state { + State::ResolveMain(ref specifier) => { + self.loader.resolve(specifier, ".", true)? + } + State::ResolveImport(ref specifier, ref referrer) => { + self.loader.resolve(specifier, referrer, false)? + } + _ => unreachable!(), + }; + + // We deliberately do not check if this module is already present in the + // module map. That's because the module map doesn't track whether a + // a module's dependencies have been loaded and whether it's been + // instantiated, so if we did find this module in the module map and used + // its id, this could lead to a crash. + // + // For the time being code and metadata for a module specifier is fetched + // multiple times, register() uses only the first result, and assigns the + // same module id to all instances. + // + // TODO: this is very ugly. The module map and recursive loader should be + // integrated into one thing. + self + .pending + .push(Box::new(self.loader.load(&module_specifier))); + self.state = State::LoadingRoot; + + Ok(()) + } + + fn add_import( &mut self, specifier: &str, referrer: &str, - parent_id: Option<deno_mod>, - ) -> Result<String, ErrBox> { - let is_root = parent_id.is_none(); - let module_specifier = self.loader.resolve(specifier, referrer, is_root)?; - let module_name = module_specifier.to_string(); - - if !is_root { - { - let mut m = self.modules.lock().unwrap(); - m.add_child(parent_id.unwrap(), &module_name); - } - } + parent_id: deno_mod, + ) -> Result<(), ErrBox> { + let module_specifier = self.loader.resolve(specifier, referrer, false)?; + let module_name = module_specifier.as_str(); + + let mut modules = self.modules.lock().unwrap(); + + modules.add_child(parent_id, module_name); + if !modules.is_registered(module_name) + && !self.is_pending.contains(&module_specifier) { - // #B We only add modules that have not yet been resolved for RecursiveLoad. - // Only short circuit after add_child(). - // This impacts possible conditions in #A. - let modules = self.modules.lock().unwrap(); - if modules.is_registered(&module_name) { - return Ok(module_name); - } + self + .pending + .push(Box::new(self.loader.load(&module_specifier))); + self.is_pending.insert(module_specifier); } - if !self.is_pending.contains(&module_name) { - self.is_pending.insert(module_name.to_string()); - let source_code_info_future = { self.loader.load(&module_specifier) }; - self.pending.push(PendingLoad { - url: module_name.to_string(), - source_code_info_future, - is_root, - }); - } + Ok(()) + } - Ok(module_name) + /// Returns a future that resolves to the final module id of the root module. + /// This future needs to take ownership of the isolate. + pub fn get_future( + self, + isolate: Arc<Mutex<Isolate>>, + ) -> impl Future<Item = deno_mod, Error = ErrBox> { + loop_fn(self, move |load| { + let isolate = isolate.clone(); + load.into_future().map_err(|(e, _)| e).and_then( + move |(event, mut load)| { + Ok(match event.unwrap() { + Event::Fetch(info) => { + let mut isolate = isolate.lock().unwrap(); + load.register(info, &mut isolate)?; + Loop::Continue(load) + } + Event::Instantiate(id) => Loop::Break(id), + }) + }, + ) + }) } } -impl<L: Loader> Future for RecursiveLoad<L> { - type Item = deno_mod; - type Error = ErrBox; +impl<L: Loader> ImportStream for RecursiveLoad<L> { + // TODO: this should not be part of RecursiveLoad. + fn register( + &mut self, + source_code_info: SourceCodeInfo, + isolate: &mut Isolate, + ) -> Result<(), ErrBox> { + // #A There are 3 cases to handle at this moment: + // 1. Source code resolved result have the same module name as requested + // and is not yet registered + // -> register + // 2. Source code resolved result have a different name as requested: + // 2a. The module with resolved module name has been registered + // -> alias + // 2b. The module with resolved module name has not yet been registerd + // -> register & alias + let SourceCodeInfo { + code, + module_url_specified, + module_url_found, + } = source_code_info; + + let is_main = self.kind == Kind::Main && self.state == State::LoadingRoot; + + let module_id = { + let mut modules = self.modules.lock().unwrap(); + + // If necessary, register an alias. + if module_url_specified != module_url_found { + modules.alias(&module_url_specified, &module_url_found); + } - fn poll(&mut self) -> Poll<Self::Item, Self::Error> { - if self.root.is_none() && self.root_specifier.is_some() { - let s = self.root_specifier.take().unwrap(); - match self.add(&s, ".", None) { - Err(err) => { - return Err(err); + match modules.get_id(&module_url_found) { + // Module has already been registered. + Some(id) => { + debug!( + "Already-registered module fetched again: {}", + module_url_found + ); + id } - Ok(root) => { - self.root = Some(root); + // Module not registered yet, do it now. + None => { + let id = isolate.mod_new(is_main, &module_url_found, &code)?; + modules.register(id, &module_url_found); + id } } + }; + + // Now we must iterate over all imports of the module and load them. + let imports = isolate.mod_get_imports(module_id); + for import in imports { + self.add_import(&import, &module_url_found, module_id)?; } - assert!(self.root_specifier.is_none()); - assert!(self.root.is_some()); - - let mut i = 0; - while i < self.pending.len() { - let pending = &mut self.pending[i]; - match pending.source_code_info_future.poll() { - Err(err) => { - return Err(err); - } - Ok(Async::NotReady) => { - i += 1; - } - Ok(Async::Ready(source_code_info)) => { - // We have completed loaded one of the modules. - let completed = self.pending.remove(i); - - // #A There are 3 cases to handle at this moment: - // 1. Source code resolved result have the same module name as requested - // and is not yet registered - // -> register - // 2. Source code resolved result have a different name as requested: - // 2a. The module with resolved module name has been registered - // -> alias - // 2b. The module with resolved module name has not yet been registerd - // -> register & alias - let is_module_registered = { - let modules = self.modules.lock().unwrap(); - modules.is_registered(&source_code_info.module_name) - }; - - let need_alias = source_code_info.module_name != completed.url; - - if !is_module_registered { - let module_name = &source_code_info.module_name; - - let mod_id = { - let isolate = self.isolate.lock().unwrap(); - isolate.mod_new( - completed.is_root, - module_name, - &source_code_info.code, - ) - }?; - - if completed.is_root { - assert!(self.root_id.is_none()); - self.root_id = Some(mod_id); - } - // Register new module. - { - let mut modules = self.modules.lock().unwrap(); - modules.register(mod_id, module_name); - // If necessary, register the alias. - if need_alias { - let module_alias = &completed.url; - modules.alias(module_alias, module_name); - } - } + // If we just finished loading the root module, store the root module id. + match self.state { + State::LoadingRoot => self.state = State::LoadingImports(module_id), + State::LoadingImports(..) => {} + _ => unreachable!(), + }; - // Now we must iterate over all imports of the module and load them. - let imports = { - let isolate = self.isolate.lock().unwrap(); - isolate.mod_get_imports(mod_id) - }; - let referrer = module_name; - for specifier in imports { - self.add(&specifier, referrer, Some(mod_id))?; - } - } else if need_alias { - let mut modules = self.modules.lock().unwrap(); - modules.alias(&completed.url, &source_code_info.module_name); + // If all imports have been loaded, instantiate the root module. + if self.pending.is_empty() { + let root_id = match self.state { + State::LoadingImports(mod_id) => mod_id, + _ => unreachable!(), + }; + + let mut resolve_cb = + |specifier: &str, referrer_id: deno_mod| -> deno_mod { + let modules = self.modules.lock().unwrap(); + let referrer = modules.get_name(referrer_id).unwrap(); + match self.loader.resolve(specifier, &referrer, is_main) { + Ok(specifier) => modules.get_id(specifier.as_str()).unwrap_or(0), + // We should have already resolved and Ready this module, so + // resolve() will not fail this time. + Err(..) => unreachable!(), } - } - } - } + }; + isolate.mod_instantiate(root_id, &mut resolve_cb)?; - if !self.pending.is_empty() { - return Ok(Async::NotReady); + self.state = State::Instantiated(root_id); } - let root_id = self.root_id.unwrap(); + Ok(()) + } +} - let mut resolve_cb = |specifier: &str, referrer_id: deno_mod| -> deno_mod { - let modules = self.modules.lock().unwrap(); - let referrer = modules.get_name(referrer_id).unwrap(); - // this callback is only called for non-root modules - match self.loader.resolve(specifier, &referrer, false) { - Ok(specifier) => match modules.get_id(&specifier.to_string()) { - Some(id) => id, - None => 0, - }, - // We should have already resolved and loaded this module, so - // resolve() will not fail this time. - Err(_err) => unreachable!(), - } - }; +impl<L: Loader> Stream for RecursiveLoad<L> { + type Item = Event; + type Error = ErrBox; - let mut isolate = self.isolate.lock().unwrap(); - isolate - .mod_instantiate(root_id, &mut resolve_cb) - .map(|_| Async::Ready(root_id)) + fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> { + Ok(match self.state { + State::ResolveMain(..) | State::ResolveImport(..) => { + self.add_root()?; + self.poll()? + } + State::LoadingRoot | State::LoadingImports(..) => { + match self.pending.poll()? { + Ready(None) => unreachable!(), + Ready(Some(info)) => Ready(Some(Event::Fetch(info))), + NotReady => NotReady, + } + } + State::Instantiated(id) => Ready(Some(Event::Instantiate(id))), + }) } } @@ -519,6 +573,7 @@ mod tests { use super::*; use crate::isolate::js_check; use crate::isolate::tests::*; + use futures::Async; use std::error::Error; use std::fmt; @@ -557,7 +612,7 @@ mod tests { "/dir/redirect3.js" => Some((REDIRECT3_SRC, "file:///redirect3.js")), "/slow.js" => Some((SLOW_SRC, "file:///slow.js")), "/never_ready.js" => { - Some(("should never be loaded", "file:///never_ready.js")) + Some(("should never be Ready", "file:///never_ready.js")) } "/main.js" => Some((MAIN_SRC, "file:///main.js")), "/bad_import.js" => Some((BAD_IMPORT_SRC, "file:///bad_import.js")), @@ -594,15 +649,20 @@ mod tests { fn poll(&mut self) -> Poll<Self::Item, ErrBox> { self.counter += 1; - if self.url == "file:///never_ready.js" - || (self.url == "file:///slow.js" && self.counter < 2) - { + if self.url == "file:///never_ready.js" { + return Ok(Async::NotReady); + } + if self.url == "file:///slow.js" && self.counter < 2 { + // TODO(ry) Hopefully in the future we can remove current task + // notification. See comment above run_in_task. + futures::task::current().notify(); return Ok(Async::NotReady); } match mock_source_code(&self.url) { Some(src) => Ok(Async::Ready(SourceCodeInfo { code: src.0.to_owned(), - module_name: src.1.to_owned(), + module_url_specified: self.url.clone(), + module_url_found: src.1.to_owned(), })), None => Err(MockError::LoadErr.into()), } @@ -679,20 +739,34 @@ mod tests { if (import.meta.url != 'file:///d.js') throw Error(); "#; + // TODO(ry) Sadly FuturesUnordered requires the current task to be set. So + // even though we are only using poll() in these tests and not Tokio, we must + // nevertheless run it in the tokio executor. Ideally run_in_task can be + // removed in the future. + use crate::isolate::tests::run_in_task; + #[test] fn test_recursive_load() { - let loader = MockLoader::new(); - let modules = loader.modules.clone(); - let modules_ = modules.clone(); - let isolate = loader.isolate.clone(); - let isolate_ = isolate.clone(); - let loads = loader.loads.clone(); - let mut recursive_load = - RecursiveLoad::new("/a.js", loader, isolate, modules); - - let result = recursive_load.poll(); - assert!(result.is_ok()); - if let Async::Ready(a_id) = result.ok().unwrap() { + run_in_task(|| { + let loader = MockLoader::new(); + let modules = loader.modules.clone(); + let modules_ = modules.clone(); + let isolate = loader.isolate.clone(); + let isolate_ = isolate.clone(); + let loads = loader.loads.clone(); + let mut recursive_load = RecursiveLoad::main("/a.js", loader, modules); + + let a_id = loop { + match recursive_load.poll() { + Ok(Ready(Some(Event::Fetch(info)))) => { + let mut isolate = isolate.lock().unwrap(); + recursive_load.register(info, &mut isolate).unwrap(); + } + Ok(Ready(Some(Event::Instantiate(id)))) => break id, + _ => panic!("unexpected result"), + }; + }; + let mut isolate = isolate_.lock().unwrap(); js_check(isolate.mod_evaluate(a_id)); @@ -730,9 +804,7 @@ mod tests { Some(&vec!["file:///d.js".to_string()]) ); assert_eq!(modules.get_children(d_id), Some(&vec![])); - } else { - unreachable!(); - } + }) } const CIRCULAR1_SRC: &str = r#" @@ -753,58 +825,59 @@ mod tests { #[test] fn test_circular_load() { - let loader = MockLoader::new(); - let isolate = loader.isolate.clone(); - let isolate_ = isolate.clone(); - let modules = loader.modules.clone(); - let modules_ = modules.clone(); - let loads = loader.loads.clone(); - let mut recursive_load = - RecursiveLoad::new("/circular1.js", loader, isolate, modules); - - let result = recursive_load.poll(); - assert!(result.is_ok()); - if let Async::Ready(circular1_id) = result.ok().unwrap() { - let mut isolate = isolate_.lock().unwrap(); - js_check(isolate.mod_evaluate(circular1_id)); - - let l = loads.lock().unwrap(); - assert_eq!( - l.to_vec(), - vec![ - "file:///circular1.js", - "file:///circular2.js", - "file:///circular3.js" - ] - ); - - let modules = modules_.lock().unwrap(); - - 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_children(circular1_id), - Some(&vec!["file:///circular2.js".to_string()]) - ); - - assert_eq!( - modules.get_children(circular2_id), - Some(&vec!["file:///circular3.js".to_string()]) - ); - - assert!(modules.get_id("file:///circular3.js").is_some()); - let circular3_id = modules.get_id("file:///circular3.js").unwrap(); - assert_eq!( - modules.get_children(circular3_id), - Some(&vec![ - "file:///circular1.js".to_string(), - "file:///circular2.js".to_string() - ]) - ); - } else { - unreachable!(); - } + run_in_task(|| { + let loader = MockLoader::new(); + let isolate = loader.isolate.clone(); + let isolate_ = isolate.clone(); + let modules = loader.modules.clone(); + let modules_ = modules.clone(); + let loads = loader.loads.clone(); + let recursive_load = + RecursiveLoad::main("/circular1.js", loader, modules); + let result = recursive_load.get_future(isolate.clone()).poll(); + assert!(result.is_ok()); + if let Async::Ready(circular1_id) = result.ok().unwrap() { + let mut isolate = isolate_.lock().unwrap(); + js_check(isolate.mod_evaluate(circular1_id)); + + let l = loads.lock().unwrap(); + assert_eq!( + l.to_vec(), + vec![ + "file:///circular1.js", + "file:///circular2.js", + "file:///circular3.js" + ] + ); + + let modules = modules_.lock().unwrap(); + + 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_children(circular1_id), + Some(&vec!["file:///circular2.js".to_string()]) + ); + + assert_eq!( + modules.get_children(circular2_id), + Some(&vec!["file:///circular3.js".to_string()]) + ); + + assert!(modules.get_id("file:///circular3.js").is_some()); + let circular3_id = modules.get_id("file:///circular3.js").unwrap(); + assert_eq!( + modules.get_children(circular3_id), + Some(&vec![ + "file:///circular1.js".to_string(), + "file:///circular2.js".to_string() + ]) + ); + } else { + unreachable!(); + } + }) } const REDIRECT1_SRC: &str = r#" @@ -823,49 +896,51 @@ mod tests { #[test] fn test_redirect_load() { - let loader = MockLoader::new(); - let isolate = loader.isolate.clone(); - let isolate_ = isolate.clone(); - let modules = loader.modules.clone(); - let modules_ = modules.clone(); - let loads = loader.loads.clone(); - let mut recursive_load = - RecursiveLoad::new("/redirect1.js", loader, isolate, modules); - - let result = recursive_load.poll(); - assert!(result.is_ok()); - if let Async::Ready(redirect1_id) = result.ok().unwrap() { - let mut isolate = isolate_.lock().unwrap(); - js_check(isolate.mod_evaluate(redirect1_id)); - let l = loads.lock().unwrap(); - assert_eq!( - l.to_vec(), - vec![ - "file:///redirect1.js", - "file:///redirect2.js", - "file:///dir/redirect3.js" - ] - ); - - let modules = modules_.lock().unwrap(); - - assert_eq!(modules.get_id("file:///redirect1.js"), 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 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) - ); - } else { - unreachable!(); - } + run_in_task(|| { + let loader = MockLoader::new(); + let isolate = loader.isolate.clone(); + let isolate_ = isolate.clone(); + let modules = loader.modules.clone(); + let modules_ = modules.clone(); + let loads = loader.loads.clone(); + let recursive_load = + RecursiveLoad::main("/redirect1.js", loader, modules); + let result = recursive_load.get_future(isolate.clone()).poll(); + println!(">> result {:?}", result); + assert!(result.is_ok()); + if let Async::Ready(redirect1_id) = result.ok().unwrap() { + let mut isolate = isolate_.lock().unwrap(); + js_check(isolate.mod_evaluate(redirect1_id)); + let l = loads.lock().unwrap(); + assert_eq!( + l.to_vec(), + vec![ + "file:///redirect1.js", + "file:///redirect2.js", + "file:///dir/redirect3.js" + ] + ); + + let modules = modules_.lock().unwrap(); + + assert_eq!(modules.get_id("file:///redirect1.js"), 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 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) + ); + } else { + unreachable!(); + } + }) } // main.js @@ -886,47 +961,46 @@ mod tests { #[test] fn slow_never_ready_modules() { - let loader = MockLoader::new(); - let isolate = loader.isolate.clone(); - let modules = loader.modules.clone(); - let loads = loader.loads.clone(); - let mut recursive_load = - RecursiveLoad::new("/main.js", loader, isolate, modules); - - let result = recursive_load.poll(); - assert!(result.is_ok()); - assert!(result.ok().unwrap().is_not_ready()); + run_in_task(|| { + let loader = MockLoader::new(); + let isolate = loader.isolate.clone(); + let modules = loader.modules.clone(); + let loads = loader.loads.clone(); + let mut recursive_load = + RecursiveLoad::main("/main.js", loader, modules).get_future(isolate); - { - let l = loads.lock().unwrap(); - assert_eq!( - l.to_vec(), - vec![ - "file:///main.js", - "file:///never_ready.js", - "file:///slow.js" - ] - ); - } - - for _ in 0..10 { let result = recursive_load.poll(); assert!(result.is_ok()); assert!(result.ok().unwrap().is_not_ready()); - let l = loads.lock().unwrap();; - assert_eq!( - l.to_vec(), - vec![ - "file:///main.js", - "file:///never_ready.js", - "file:///slow.js", - "file:///a.js", - "file:///b.js", - "file:///c.js", - "file:///d.js" - ] - ); - } + + // TODO(ry) Arguably the first time we poll only the following modules + // should be loaded: + // "file:///main.js", + // "file:///never_ready.js", + // "file:///slow.js" + // But due to current task notification in DelayedSourceCodeFuture they + // all get loaded in a single poll. Also see the comment above + // run_in_task. + + for _ in 0..10 { + let result = recursive_load.poll(); + assert!(result.is_ok()); + assert!(result.ok().unwrap().is_not_ready()); + let l = loads.lock().unwrap();; + assert_eq!( + l.to_vec(), + vec![ + "file:///main.js", + "file:///never_ready.js", + "file:///slow.js", + "file:///a.js", + "file:///b.js", + "file:///c.js", + "file:///d.js" + ] + ); + } + }) } // bad_import.js @@ -936,18 +1010,20 @@ mod tests { #[test] fn loader_disappears_after_error() { - let loader = MockLoader::new(); - let isolate = loader.isolate.clone(); - let modules = loader.modules.clone(); - let mut recursive_load = - RecursiveLoad::new("/bad_import.js", loader, isolate, modules); - let result = recursive_load.poll(); - assert!(result.is_err()); - let err = result.err().unwrap(); - assert_eq!( - err.downcast_ref::<MockError>().unwrap(), - &MockError::ResolveErr - ); + run_in_task(|| { + let loader = MockLoader::new(); + let isolate = loader.isolate.clone(); + let modules = loader.modules.clone(); + let recursive_load = + RecursiveLoad::main("/bad_import.js", loader, modules); + let result = recursive_load.get_future(isolate).poll(); + assert!(result.is_err()); + let err = result.err().unwrap(); + assert_eq!( + err.downcast_ref::<MockError>().unwrap(), + &MockError::ResolveErr + ); + }) } #[test] diff --git a/tests/013_dynamic_import.disabled b/tests/013_dynamic_import.test index 8fe463b20..8fe463b20 100644 --- a/tests/013_dynamic_import.disabled +++ b/tests/013_dynamic_import.test diff --git a/tests/014_duplicate_import.disabled b/tests/014_duplicate_import.test index 57d5b6e8b..57d5b6e8b 100644 --- a/tests/014_duplicate_import.disabled +++ b/tests/014_duplicate_import.test diff --git a/tests/015_duplicate_parallel_import.js b/tests/015_duplicate_parallel_import.js new file mode 100644 index 000000000..37033cfa2 --- /dev/null +++ b/tests/015_duplicate_parallel_import.js @@ -0,0 +1,20 @@ +// Importing the same module in parallel, the module should only be +// instantiated once. + +const promises = new Array(100) + .fill(null) + .map(() => import("./subdir/mod1.ts")); + +Promise.all(promises).then(imports => { + const mod = imports.reduce((first, cur) => { + if (typeof first !== "object") { + throw new Error("Expected an object."); + } + if (first !== cur) { + throw new Error("More than one instance of the same module."); + } + return first; + }); + + mod.printHello3(); +}); diff --git a/tests/015_duplicate_parallel_import.js.out b/tests/015_duplicate_parallel_import.js.out new file mode 100644 index 000000000..e965047ad --- /dev/null +++ b/tests/015_duplicate_parallel_import.js.out @@ -0,0 +1 @@ +Hello diff --git a/tests/015_duplicate_parallel_import.test b/tests/015_duplicate_parallel_import.test new file mode 100644 index 000000000..ec8c79b21 --- /dev/null +++ b/tests/015_duplicate_parallel_import.test @@ -0,0 +1,2 @@ +args: tests/015_duplicate_parallel_import.js --reload +output: tests/015_duplicate_parallel_import.js.out diff --git a/tests/error_014_catch_dynamic_import_error.js b/tests/error_014_catch_dynamic_import_error.js new file mode 100644 index 000000000..ad3735fc3 --- /dev/null +++ b/tests/error_014_catch_dynamic_import_error.js @@ -0,0 +1,31 @@ +(async () => { + try { + await import("does not exist"); + } catch (err) { + console.log("Caught direct dynamic import error."); + console.log(err); + } + + try { + await import("./subdir/indirect_import_error.js"); + } catch (err) { + console.log("Caught indirect direct dynamic import error."); + console.log(err); + } + + try { + await import("./subdir/throws.js"); + } catch (err) { + console.log("Caught error thrown by dynamically imported module."); + console.log(err); + } + + try { + await import("./subdir/indirect_throws.js"); + } catch (err) { + console.log( + "Caught error thrown indirectly by dynamically imported module." + ); + console.log(err); + } +})(); diff --git a/tests/error_014_catch_dynamic_import_error.js.out b/tests/error_014_catch_dynamic_import_error.js.out new file mode 100644 index 000000000..c18b680a1 --- /dev/null +++ b/tests/error_014_catch_dynamic_import_error.js.out @@ -0,0 +1,12 @@ +Caught direct dynamic import error. +TypeError: relative import path "does not exist" not prefixed with / or ./ or ../ + +Caught indirect direct dynamic import error. +TypeError: relative import path "does not exist either" not prefixed with / or ./ or ../ + +Caught error thrown by dynamically imported module. +Error: An error + at file:///[WILDCARD]tests/subdir/throws.js:5:7 +Caught error thrown indirectly by dynamically imported module. +Error: An error + at file:///[WILDCARD]tests/subdir/throws.js:5:7 diff --git a/tests/error_014_catch_dynamic_import_error.test b/tests/error_014_catch_dynamic_import_error.test new file mode 100644 index 000000000..8345ad1c8 --- /dev/null +++ b/tests/error_014_catch_dynamic_import_error.test @@ -0,0 +1,2 @@ +args: tests/error_014_catch_dynamic_import_error.js --reload +output: tests/error_014_catch_dynamic_import_error.js.out diff --git a/tests/subdir/indirect_import_error.js b/tests/subdir/indirect_import_error.js new file mode 100644 index 000000000..84011d291 --- /dev/null +++ b/tests/subdir/indirect_import_error.js @@ -0,0 +1 @@ +export * from "does not exist either"; diff --git a/tests/subdir/indirect_throws.js b/tests/subdir/indirect_throws.js new file mode 100644 index 000000000..e1810a66c --- /dev/null +++ b/tests/subdir/indirect_throws.js @@ -0,0 +1 @@ +export * from "./throws.js"; diff --git a/tests/subdir/throws.js b/tests/subdir/throws.js new file mode 100644 index 000000000..b77e7104f --- /dev/null +++ b/tests/subdir/throws.js @@ -0,0 +1,5 @@ +export function boo() { + console.log("Boo!"); +} + +throw new Error("An error"); |