From 7ab645f512057d74fda2d0edf2d336cea3ed524e Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Wed, 7 Oct 2020 16:24:15 +1100 Subject: refactor(cli): cleanups to new module graph (#7846) --- cli/global_state.rs | 6 +- cli/graph.rs | 1095 ---------------------------------------------- cli/main.rs | 2 +- cli/module_graph2.rs | 1065 ++++++++++++++++++++++++++++++++++++++++++++ cli/specifier_handler.rs | 131 ------ 5 files changed, 1069 insertions(+), 1230 deletions(-) delete mode 100644 cli/graph.rs create mode 100644 cli/module_graph2.rs diff --git a/cli/global_state.rs b/cli/global_state.rs index 336dcae00..9f5158332 100644 --- a/cli/global_state.rs +++ b/cli/global_state.rs @@ -3,8 +3,6 @@ use crate::deno_dir; use crate::file_fetcher::SourceFileFetcher; use crate::flags; -use crate::graph::GraphBuilder; -use crate::graph::TranspileOptions; use crate::http_cache; use crate::import_map::ImportMap; use crate::inspector::InspectorServer; @@ -12,6 +10,8 @@ use crate::lockfile::Lockfile; use crate::media_type::MediaType; use crate::module_graph::ModuleGraphFile; use crate::module_graph::ModuleGraphLoader; +use crate::module_graph2::GraphBuilder2; +use crate::module_graph2::TranspileOptions; use crate::permissions::Permissions; use crate::specifier_handler::FetchHandler; use crate::tsc::CompiledModule; @@ -130,7 +130,7 @@ impl GlobalState { // something that should be handled better in the future. let handler = Rc::new(RefCell::new(FetchHandler::new(self, permissions.clone())?)); - let mut builder = GraphBuilder::new(handler, maybe_import_map); + let mut builder = GraphBuilder2::new(handler, maybe_import_map); builder.insert(&module_specifier).await?; let mut graph = builder.get_graph(&self.lockfile)?; diff --git a/cli/graph.rs b/cli/graph.rs deleted file mode 100644 index ed8f5cad7..000000000 --- a/cli/graph.rs +++ /dev/null @@ -1,1095 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::ast; -use crate::ast::parse; -use crate::ast::Location; -use crate::ast::ParsedModule; -use crate::file_fetcher::TextDocument; -use crate::import_map::ImportMap; -use crate::lockfile::Lockfile; -use crate::media_type::MediaType; -use crate::specifier_handler::CachedModule; -use crate::specifier_handler::DependencyMap; -use crate::specifier_handler::EmitMap; -use crate::specifier_handler::EmitType; -use crate::specifier_handler::FetchFuture; -use crate::specifier_handler::SpecifierHandler; -use crate::tsc_config::IgnoredCompilerOptions; -use crate::tsc_config::TsConfig; -use crate::version; -use crate::AnyError; - -use deno_core::futures::stream::FuturesUnordered; -use deno_core::futures::stream::StreamExt; -use deno_core::serde_json::json; -use deno_core::ModuleSpecifier; -use regex::Regex; -use serde::Deserialize; -use serde::Deserializer; -use std::cell::RefCell; -use std::collections::HashMap; -use std::collections::HashSet; -use std::error::Error; -use std::fmt; -use std::rc::Rc; -use std::result; -use std::sync::Mutex; -use std::time::Instant; -use swc_ecmascript::dep_graph::DependencyKind; - -pub type BuildInfoMap = HashMap; - -lazy_static! { - /// Matched the `@deno-types` pragma. - static ref DENO_TYPES_RE: Regex = - Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#) - .unwrap(); - /// Matches a `/// ` comment reference. - static ref TRIPLE_SLASH_REFERENCE_RE: Regex = - Regex::new(r"(?i)^/\s*").unwrap(); - /// Matches a path reference, which adds a dependency to a module - static ref PATH_REFERENCE_RE: Regex = - Regex::new(r#"(?i)\spath\s*=\s*["']([^"']*)["']"#).unwrap(); - /// Matches a types reference, which for JavaScript files indicates the - /// location of types to use when type checking a program that includes it as - /// a dependency. - static ref TYPES_REFERENCE_RE: Regex = - Regex::new(r#"(?i)\stypes\s*=\s*["']([^"']*)["']"#).unwrap(); -} - -/// A group of errors that represent errors that can occur when interacting with -/// a module graph. -#[allow(unused)] -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum GraphError { - /// A module using the HTTPS protocol is trying to import a module with an - /// HTTP schema. - InvalidDowngrade(ModuleSpecifier, Location), - /// A remote module is trying to import a local module. - InvalidLocalImport(ModuleSpecifier, Location), - /// A remote module is trying to import a local module. - InvalidSource(ModuleSpecifier, String), - /// A module specifier could not be resolved for a given import. - InvalidSpecifier(String, Location), - /// An unexpected dependency was requested for a module. - MissingDependency(ModuleSpecifier, String), - /// An unexpected specifier was requested. - MissingSpecifier(ModuleSpecifier), - /// Snapshot data was not present in a situation where it was required. - MissingSnapshotData, - /// The current feature is not supported. - NotSupported(String), -} -use GraphError::*; - -impl fmt::Display for GraphError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - InvalidDowngrade(ref specifier, ref location) => write!(f, "Modules imported via https are not allowed to import http modules.\n Importing: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), - InvalidLocalImport(ref specifier, ref location) => write!(f, "Remote modules are not allowed to import local modules.\n Importing: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), - InvalidSource(ref specifier, ref lockfile) => write!(f, "The source code is invalid, as it does not match the expected hash in the lock file.\n Specifier: {}\n Lock file: {}", specifier, lockfile), - InvalidSpecifier(ref specifier, ref location) => write!(f, "Unable to resolve dependency specifier.\n Specifier: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), - MissingDependency(ref referrer, specifier) => write!( - f, - "The graph is missing a dependency.\n Specifier: {} from {}", - specifier, referrer - ), - MissingSpecifier(ref specifier) => write!( - f, - "The graph is missing a specifier.\n Specifier: {}", - specifier - ), - MissingSnapshotData => write!(f, "Snapshot data was not supplied, but required."), - NotSupported(ref msg) => write!(f, "{}", msg), - } - } -} - -impl Error for GraphError {} - -/// A trait, implemented by `Graph` that provides the interfaces that the -/// compiler ops require to be able to retrieve information about the graph. -pub trait ModuleProvider { - /// Get the source for a given module specifier. If the module is not part - /// of the graph, the result will be `None`. - fn get_source(&self, specifier: &ModuleSpecifier) -> Option; - /// Given a string specifier and a referring module specifier, provide the - /// resulting module specifier and media type for the module that is part of - /// the graph. - fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> Result<(ModuleSpecifier, MediaType), AnyError>; -} - -/// An enum which represents the parsed out values of references in source code. -#[derive(Debug, Clone, Eq, PartialEq)] -enum TypeScriptReference { - Path(String), - Types(String), -} - -/// Determine if a comment contains a triple slash reference and optionally -/// return its kind and value. -fn parse_ts_reference(comment: &str) -> Option { - if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) { - None - } else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) { - Some(TypeScriptReference::Path( - captures.get(1).unwrap().as_str().to_string(), - )) - } else if let Some(captures) = TYPES_REFERENCE_RE.captures(comment) { - Some(TypeScriptReference::Types( - captures.get(1).unwrap().as_str().to_string(), - )) - } else { - None - } -} - -/// Determine if a comment contains a `@deno-types` pragma and optionally return -/// its value. -fn parse_deno_types(comment: &str) -> Option { - if let Some(captures) = DENO_TYPES_RE.captures(comment) { - if let Some(m) = captures.get(1) { - Some(m.as_str().to_string()) - } else if let Some(m) = captures.get(2) { - Some(m.as_str().to_string()) - } else { - panic!("unreachable"); - } - } else { - None - } -} - -/// A hashing function that takes the source code, version and optionally a -/// user provided config and generates a string hash which can be stored to -/// determine if the cached emit is valid or not. -fn get_version(source: &TextDocument, version: &str, config: &[u8]) -> String { - crate::checksum::gen(&[ - source.to_str().unwrap().as_bytes(), - version.as_bytes(), - config, - ]) -} - -/// A logical representation of a module within a graph. -#[derive(Debug, Clone)] -struct Module { - dependencies: DependencyMap, - emits: EmitMap, - is_dirty: bool, - is_hydrated: bool, - is_parsed: bool, - maybe_import_map: Option>>, - maybe_parsed_module: Option, - maybe_types: Option<(String, ModuleSpecifier)>, - maybe_version: Option, - media_type: MediaType, - specifier: ModuleSpecifier, - source: TextDocument, -} - -impl Default for Module { - fn default() -> Self { - Module { - dependencies: HashMap::new(), - emits: HashMap::new(), - is_dirty: false, - is_hydrated: false, - is_parsed: false, - maybe_import_map: None, - maybe_parsed_module: None, - maybe_types: None, - maybe_version: None, - media_type: MediaType::Unknown, - specifier: ModuleSpecifier::resolve_url("https://deno.land/x/").unwrap(), - source: TextDocument::new(Vec::new(), Option::<&str>::None), - } - } -} - -impl Module { - pub fn new( - specifier: ModuleSpecifier, - maybe_import_map: Option>>, - ) -> Self { - Module { - specifier, - maybe_import_map, - ..Module::default() - } - } - - /// Return `true` if the current hash of the module matches the stored - /// version. - pub fn emit_valid(&self, config: &[u8]) -> bool { - if let Some(version) = self.maybe_version.clone() { - version == get_version(&self.source, version::DENO, config) - } else { - false - } - } - - pub fn hydrate(&mut self, cached_module: CachedModule) { - self.media_type = cached_module.media_type; - self.source = cached_module.source; - if self.maybe_import_map.is_none() { - if let Some(dependencies) = cached_module.maybe_dependencies { - self.dependencies = dependencies; - self.is_parsed = true; - } - } - self.maybe_types = if let Some(ref specifier) = cached_module.maybe_types { - Some(( - specifier.clone(), - self - .resolve_import(&specifier, None) - .expect("could not resolve module"), - )) - } else { - None - }; - self.is_dirty = false; - self.emits = cached_module.emits; - self.maybe_version = cached_module.maybe_version; - self.is_hydrated = true; - } - - pub fn parse(&mut self) -> Result<(), AnyError> { - let parsed_module = - parse(&self.specifier, &self.source.to_str()?, &self.media_type)?; - - // parse out any triple slash references - for comment in parsed_module.get_leading_comments().iter() { - if let Some(ts_reference) = parse_ts_reference(&comment.text) { - let location: Location = parsed_module.get_location(&comment.span); - match ts_reference { - TypeScriptReference::Path(import) => { - let specifier = self.resolve_import(&import, Some(location))?; - let dep = self.dependencies.entry(import).or_default(); - dep.maybe_code = Some(specifier); - } - TypeScriptReference::Types(import) => { - let specifier = self.resolve_import(&import, Some(location))?; - if self.media_type == MediaType::JavaScript - || self.media_type == MediaType::JSX - { - // TODO(kitsonk) we need to specifically update the cache when - // this value changes - self.maybe_types = Some((import.clone(), specifier)); - } else { - let dep = self.dependencies.entry(import).or_default(); - dep.maybe_type = Some(specifier); - } - } - } - } - } - - // Parse out all the syntactical dependencies for a module - let dependencies = parsed_module.analyze_dependencies(); - for desc in dependencies - .iter() - .filter(|desc| desc.kind != DependencyKind::Require) - { - let location = Location { - filename: self.specifier.to_string(), - col: desc.col, - line: desc.line, - }; - let specifier = - self.resolve_import(&desc.specifier, Some(location.clone()))?; - - // Parse out any `@deno-types` pragmas and modify dependency - let maybe_types_specifier = if !desc.leading_comments.is_empty() { - let comment = desc.leading_comments.last().unwrap(); - if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() { - Some(self.resolve_import(deno_types, Some(location))?) - } else { - None - } - } else { - None - }; - - let dep = self - .dependencies - .entry(desc.specifier.to_string()) - .or_default(); - if desc.kind == DependencyKind::ExportType - || desc.kind == DependencyKind::ImportType - { - dep.maybe_type = Some(specifier); - } else { - dep.maybe_code = Some(specifier); - } - if let Some(types_specifier) = maybe_types_specifier { - dep.maybe_type = Some(types_specifier); - } - } - - self.maybe_parsed_module = Some(parsed_module); - Ok(()) - } - - fn resolve_import( - &self, - specifier: &str, - maybe_location: Option, - ) -> Result { - let maybe_resolve = if let Some(import_map) = self.maybe_import_map.clone() - { - import_map - .borrow() - .resolve(specifier, self.specifier.as_str())? - } else { - None - }; - let specifier = if let Some(module_specifier) = maybe_resolve { - module_specifier - } else { - ModuleSpecifier::resolve_import(specifier, self.specifier.as_str())? - }; - - let referrer_scheme = self.specifier.as_url().scheme(); - let specifier_scheme = specifier.as_url().scheme(); - let location = maybe_location.unwrap_or(Location { - filename: self.specifier.to_string(), - line: 0, - col: 0, - }); - - // Disallow downgrades from HTTPS to HTTP - if referrer_scheme == "https" && specifier_scheme == "http" { - return Err(InvalidDowngrade(specifier.clone(), location).into()); - } - - // Disallow a remote URL from trying to import a local URL - if (referrer_scheme == "https" || referrer_scheme == "http") - && !(specifier_scheme == "https" || specifier_scheme == "http") - { - return Err(InvalidLocalImport(specifier.clone(), location).into()); - } - - Ok(specifier) - } - - /// Calculate the hashed version of the module and update the `maybe_version`. - pub fn set_version(&mut self, config: &[u8]) { - self.maybe_version = Some(get_version(&self.source, version::DENO, config)) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Stats(Vec<(String, u128)>); - -impl<'de> Deserialize<'de> for Stats { - fn deserialize(deserializer: D) -> result::Result - where - D: Deserializer<'de>, - { - let items: Vec<(String, u128)> = Deserialize::deserialize(deserializer)?; - Ok(Stats(items)) - } -} - -impl fmt::Display for Stats { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for (key, value) in self.0.clone() { - write!(f, "{}: {}", key, value)?; - } - - Ok(()) - } -} - -/// A structure which provides options when transpiling modules. -#[derive(Debug, Default)] -pub struct TranspileOptions { - /// If `true` then debug logging will be output from the isolate. - pub debug: bool, - /// An optional string that points to a user supplied TypeScript configuration - /// file that augments the the default configuration passed to the TypeScript - /// compiler. - pub maybe_config_path: Option, -} - -/// A dependency graph of modules, were the modules that have been inserted via -/// the builder will be loaded into the graph. Also provides an interface to -/// be able to manipulate and handle the graph. - -#[derive(Debug)] -pub struct Graph { - build_info: BuildInfoMap, - handler: Rc>, - modules: HashMap, - roots: Vec, -} - -impl Graph { - /// Create a new instance of a graph, ready to have modules loaded it. - /// - /// The argument `handler` is an instance of a structure that implements the - /// `SpecifierHandler` trait. - /// - pub fn new(handler: Rc>) -> Self { - Graph { - build_info: HashMap::new(), - handler, - modules: HashMap::new(), - roots: Vec::new(), - } - } - - /// Update the handler with any modules that are marked as _dirty_ and update - /// any build info if present. - fn flush(&mut self, emit_type: &EmitType) -> Result<(), AnyError> { - let mut handler = self.handler.borrow_mut(); - for (_, module) in self.modules.iter_mut() { - if module.is_dirty { - let (code, maybe_map) = module.emits.get(emit_type).unwrap(); - handler.set_cache( - &module.specifier, - &emit_type, - code.clone(), - maybe_map.clone(), - )?; - module.is_dirty = false; - if let Some(version) = &module.maybe_version { - handler.set_version(&module.specifier, version.clone())?; - } - } - } - for root_specifier in self.roots.iter() { - if let Some(build_info) = self.build_info.get(&emit_type) { - handler.set_build_info( - root_specifier, - &emit_type, - build_info.to_owned(), - )?; - } - } - - Ok(()) - } - - /// Verify the subresource integrity of the graph based upon the optional - /// lockfile, updating the lockfile with any missing resources. This will - /// error if any of the resources do not match their lock status. - pub fn lock( - &self, - maybe_lockfile: &Option>, - ) -> Result<(), AnyError> { - if let Some(lf) = maybe_lockfile { - let mut lockfile = lf.lock().unwrap(); - for (ms, module) in self.modules.iter() { - let specifier = module.specifier.to_string(); - let code = module.source.to_string()?; - let valid = lockfile.check_or_insert(&specifier, &code); - if !valid { - return Err( - InvalidSource(ms.clone(), lockfile.filename.clone()).into(), - ); - } - } - } - - Ok(()) - } - - /// Transpile (only transform) the graph, updating any emitted modules - /// with the specifier handler. The result contains any performance stats - /// from the compiler and optionally any user provided configuration compiler - /// options that were ignored. - /// - /// # Arguments - /// - /// - `options` - A structure of options which impact how the code is - /// transpiled. - /// - pub fn transpile( - &mut self, - options: TranspileOptions, - ) -> Result<(Stats, Option), AnyError> { - let start = Instant::now(); - let emit_type = EmitType::Cli; - - let mut ts_config = TsConfig::new(json!({ - "checkJs": false, - "emitDecoratorMetadata": false, - "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - })); - - let maybe_ignored_options = - ts_config.merge_user_config(options.maybe_config_path)?; - - let compiler_options = ts_config.as_transpile_config()?; - let check_js = compiler_options.check_js; - let transform_jsx = compiler_options.jsx == "react"; - let emit_options = ast::TranspileOptions { - emit_metadata: compiler_options.emit_decorator_metadata, - inline_source_map: true, - jsx_factory: compiler_options.jsx_factory, - jsx_fragment_factory: compiler_options.jsx_fragment_factory, - transform_jsx, - }; - - let mut emit_count: u128 = 0; - for (_, module) in self.modules.iter_mut() { - // TODO(kitsonk) a lot of this logic should be refactored into `Module` as - // we start to support other methods on the graph. Especially managing - // the dirty state is something the module itself should "own". - - // if the module is a Dts file we should skip it - if module.media_type == MediaType::Dts { - continue; - } - // if we don't have check_js enabled, we won't touch non TypeScript - // modules - if !(check_js - || module.media_type == MediaType::TSX - || module.media_type == MediaType::TypeScript) - { - continue; - } - let config = ts_config.as_bytes(); - // skip modules that already have a valid emit - if module.emits.contains_key(&emit_type) && module.emit_valid(&config) { - continue; - } - if module.maybe_parsed_module.is_none() { - module.parse()?; - } - let parsed_module = module.maybe_parsed_module.clone().unwrap(); - let emit = parsed_module.transpile(&emit_options)?; - emit_count += 1; - module.emits.insert(emit_type.clone(), emit); - module.set_version(&config); - module.is_dirty = true; - } - self.flush(&emit_type)?; - - let stats = Stats(vec![ - ("Files".to_string(), self.modules.len() as u128), - ("Emitted".to_string(), emit_count), - ("Total time".to_string(), start.elapsed().as_millis()), - ]); - - Ok((stats, maybe_ignored_options)) - } -} - -impl<'a> ModuleProvider for Graph { - fn get_source(&self, specifier: &ModuleSpecifier) -> Option { - if let Some(module) = self.modules.get(specifier) { - if let Ok(source) = module.source.to_string() { - Some(source) - } else { - None - } - } else { - None - } - } - - fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - ) -> Result<(ModuleSpecifier, MediaType), AnyError> { - if !self.modules.contains_key(referrer) { - return Err(MissingSpecifier(referrer.to_owned()).into()); - } - let module = self.modules.get(referrer).unwrap(); - if !module.dependencies.contains_key(specifier) { - return Err( - MissingDependency(referrer.to_owned(), specifier.to_owned()).into(), - ); - } - let dependency = module.dependencies.get(specifier).unwrap(); - // If there is a @deno-types pragma that impacts the dependency, then the - // maybe_type property will be set with that specifier, otherwise we use the - // specifier that point to the runtime code. - let resolved_specifier = - if let Some(type_specifier) = dependency.maybe_type.clone() { - type_specifier - } else if let Some(code_specifier) = dependency.maybe_code.clone() { - code_specifier - } else { - return Err( - MissingDependency(referrer.to_owned(), specifier.to_owned()).into(), - ); - }; - if !self.modules.contains_key(&resolved_specifier) { - return Err( - MissingDependency(referrer.to_owned(), resolved_specifier.to_string()) - .into(), - ); - } - let dep_module = self.modules.get(&resolved_specifier).unwrap(); - // In the case that there is a X-TypeScript-Types or a triple-slash types, - // then the `maybe_types` specifier will be populated and we should use that - // instead. - let result = if let Some((_, types)) = dep_module.maybe_types.clone() { - if let Some(types_module) = self.modules.get(&types) { - (types, types_module.media_type) - } else { - return Err( - MissingDependency(referrer.to_owned(), types.to_string()).into(), - ); - } - } else { - (resolved_specifier, dep_module.media_type) - }; - - Ok(result) - } -} - -/// A structure for building a dependency graph of modules. -pub struct GraphBuilder { - fetched: HashSet, - graph: Graph, - maybe_import_map: Option>>, - pending: FuturesUnordered, -} - -impl GraphBuilder { - pub fn new( - handler: Rc>, - maybe_import_map: Option, - ) -> Self { - let internal_import_map = if let Some(import_map) = maybe_import_map { - Some(Rc::new(RefCell::new(import_map))) - } else { - None - }; - GraphBuilder { - graph: Graph::new(handler), - fetched: HashSet::new(), - maybe_import_map: internal_import_map, - pending: FuturesUnordered::new(), - } - } - - /// Request a module to be fetched from the handler and queue up its future - /// to be awaited to be resolved. - fn fetch(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { - if self.fetched.contains(&specifier) { - return Ok(()); - } - - self.fetched.insert(specifier.clone()); - let future = self.graph.handler.borrow_mut().fetch(specifier.clone()); - self.pending.push(future); - - Ok(()) - } - - /// Visit a module that has been fetched, hydrating the module, analyzing its - /// dependencies if required, fetching those dependencies, and inserting the - /// module into the graph. - fn visit(&mut self, cached_module: CachedModule) -> Result<(), AnyError> { - let specifier = cached_module.specifier.clone(); - let mut module = - Module::new(specifier.clone(), self.maybe_import_map.clone()); - module.hydrate(cached_module); - if !module.is_parsed { - let has_types = module.maybe_types.is_some(); - module.parse()?; - if self.maybe_import_map.is_none() { - let mut handler = self.graph.handler.borrow_mut(); - handler.set_deps(&specifier, module.dependencies.clone())?; - if !has_types { - if let Some((types, _)) = module.maybe_types.clone() { - handler.set_types(&specifier, types)?; - } - } - } - } - for (_, dep) in module.dependencies.iter() { - if let Some(specifier) = dep.maybe_code.as_ref() { - self.fetch(specifier)?; - } - if let Some(specifier) = dep.maybe_type.as_ref() { - self.fetch(specifier)?; - } - } - if let Some((_, specifier)) = module.maybe_types.as_ref() { - self.fetch(specifier)?; - } - self.graph.modules.insert(specifier, module); - - Ok(()) - } - - /// Insert a module into the graph based on a module specifier. The module - /// and any dependencies will be fetched from the handler. The module will - /// also be treated as a _root_ module in the graph. - pub async fn insert( - &mut self, - specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - self.fetch(specifier)?; - - loop { - let cached_module = self.pending.next().await.unwrap()?; - self.visit(cached_module)?; - if self.pending.is_empty() { - break; - } - } - - if !self.graph.roots.contains(specifier) { - self.graph.roots.push(specifier.clone()); - } - - Ok(()) - } - - /// Move out the graph from the builder to be utilized further. An optional - /// lockfile can be provided, where if the sources in the graph do not match - /// the expected lockfile, the method with error instead of returning the - /// graph. - pub fn get_graph( - self, - maybe_lockfile: &Option>, - ) -> Result { - self.graph.lock(maybe_lockfile)?; - Ok(self.graph) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::specifier_handler::tests::MockSpecifierHandler; - - use std::env; - use std::path::PathBuf; - use std::sync::Mutex; - - #[test] - fn test_get_version() { - let doc_a = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let version_a = get_version(&doc_a, "1.2.3", b""); - let doc_b = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let version_b = get_version(&doc_b, "1.2.3", b""); - assert_eq!(version_a, version_b); - - let version_c = get_version(&doc_a, "1.2.3", b"options"); - assert_ne!(version_a, version_c); - - let version_d = get_version(&doc_b, "1.2.3", b"options"); - assert_eq!(version_c, version_d); - - let version_e = get_version(&doc_a, "1.2.4", b""); - assert_ne!(version_a, version_e); - - let version_f = get_version(&doc_b, "1.2.4", b""); - assert_eq!(version_e, version_f); - } - - #[test] - fn test_module_emit_valid() { - let source = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let maybe_version = Some(get_version(&source, version::DENO, b"")); - let module = Module { - source, - maybe_version, - ..Module::default() - }; - assert!(module.emit_valid(b"")); - - let source = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let old_source = - TextDocument::new(b"console.log(43);".to_vec(), Option::<&str>::None); - let maybe_version = Some(get_version(&old_source, version::DENO, b"")); - let module = Module { - source, - maybe_version, - ..Module::default() - }; - assert!(!module.emit_valid(b"")); - - let source = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let maybe_version = Some(get_version(&source, "0.0.0", b"")); - let module = Module { - source, - maybe_version, - ..Module::default() - }; - assert!(!module.emit_valid(b"")); - - let source = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let module = Module { - source, - ..Module::default() - }; - assert!(!module.emit_valid(b"")); - } - - #[test] - fn test_module_set_version() { - let source = - TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); - let expected = Some(get_version(&source, version::DENO, b"")); - let mut module = Module { - source, - ..Module::default() - }; - assert!(module.maybe_version.is_none()); - module.set_version(b""); - assert_eq!(module.maybe_version, expected); - } - - #[tokio::test] - async fn test_graph_builder() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler, None); - let specifier = - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let graph = builder.get_graph(&None).expect("error getting graph"); - let actual = graph - .resolve("./a.ts", &specifier) - .expect("module to resolve"); - let expected = ( - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts") - .expect("unable to resolve"), - MediaType::TypeScript, - ); - assert_eq!(actual, expected); - } - - #[tokio::test] - async fn test_graph_builder_import_map() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let import_map = ImportMap::from_json( - "https://deno.land/x/import_map.ts", - r#"{ - "imports": { - "jquery": "./jquery.js", - "lodash": "https://unpkg.com/lodash/index.js" - } - }"#, - ) - .expect("could not load import map"); - let mut builder = GraphBuilder::new(handler, Some(import_map)); - let specifier = - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/import_map.ts") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let graph = builder.get_graph(&None).expect("could not get graph"); - let actual_jquery = graph - .resolve("jquery", &specifier) - .expect("module to resolve"); - let expected_jquery = ( - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/jquery.js") - .expect("unable to resolve"), - MediaType::JavaScript, - ); - assert_eq!(actual_jquery, expected_jquery); - let actual_lodash = graph - .resolve("lodash", &specifier) - .expect("module to resolve"); - let expected_lodash = ( - ModuleSpecifier::resolve_url_or_path("https://unpkg.com/lodash/index.js") - .expect("unable to resolve"), - MediaType::JavaScript, - ); - assert_eq!(actual_lodash, expected_lodash); - } - - #[tokio::test] - async fn test_graph_transpile() { - // This is a complex scenario of transpiling, where we have TypeScript - // importing a JavaScript file (with type definitions) which imports - // TypeScript, JavaScript, and JavaScript with type definitions. - // For scenarios where we transpile, we only want the TypeScript files - // to be actually emitted. - // - // This also exercises "@deno-types" and type references. - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None); - let specifier = - ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let mut graph = builder.get_graph(&None).expect("could not get graph"); - let (stats, maybe_ignored_options) = - graph.transpile(TranspileOptions::default()).unwrap(); - assert_eq!(stats.0.len(), 3); - assert_eq!(maybe_ignored_options, None); - let h = handler.borrow(); - assert_eq!(h.cache_calls.len(), 2); - assert_eq!(h.cache_calls[0].1, EmitType::Cli); - assert!(h.cache_calls[0] - .2 - .to_string() - .unwrap() - .contains("# sourceMappingURL=data:application/json;base64,")); - assert_eq!(h.cache_calls[0].3, None); - assert_eq!(h.cache_calls[1].1, EmitType::Cli); - assert!(h.cache_calls[1] - .2 - .to_string() - .unwrap() - .contains("# sourceMappingURL=data:application/json;base64,")); - assert_eq!(h.cache_calls[0].3, None); - assert_eq!(h.deps_calls.len(), 7); - assert_eq!( - h.deps_calls[0].0, - ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts").unwrap() - ); - assert_eq!(h.deps_calls[0].1.len(), 1); - assert_eq!( - h.deps_calls[1].0, - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.js") - .unwrap() - ); - assert_eq!(h.deps_calls[1].1.len(), 3); - assert_eq!( - h.deps_calls[2].0, - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.d.ts") - .unwrap() - ); - assert_eq!(h.deps_calls[2].1.len(), 3, "should have 3 dependencies"); - // sometimes the calls are not deterministic, and so checking the contents - // can cause some failures - assert_eq!(h.deps_calls[3].1.len(), 0, "should have no dependencies"); - assert_eq!(h.deps_calls[4].1.len(), 0, "should have no dependencies"); - assert_eq!(h.deps_calls[5].1.len(), 0, "should have no dependencies"); - assert_eq!(h.deps_calls[6].1.len(), 0, "should have no dependencies"); - } - - #[tokio::test] - async fn test_graph_transpile_user_config() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures: fixtures.clone(), - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None); - let specifier = - ModuleSpecifier::resolve_url_or_path("https://deno.land/x/transpile.tsx") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - let mut graph = builder.get_graph(&None).expect("could not get graph"); - let (_, maybe_ignored_options) = graph - .transpile(TranspileOptions { - debug: false, - maybe_config_path: Some("tests/module_graph/tsconfig.json".to_string()), - }) - .unwrap(); - assert_eq!( - maybe_ignored_options.unwrap().items, - vec!["target".to_string()], - "the 'target' options should have been ignored" - ); - let h = handler.borrow(); - assert_eq!(h.cache_calls.len(), 1, "only one file should be emitted"); - // FIXME(bartlomieju): had to add space in `
`, probably a quirk in swc_ecma_codegen - assert!( - h.cache_calls[0] - .2 - .to_string() - .unwrap() - .contains("
Hello world!
"), - "jsx should have been preserved" - ); - } - - #[tokio::test] - async fn test_graph_with_lockfile() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let lockfile_path = fixtures.join("lockfile.json"); - let lockfile = - Lockfile::new(lockfile_path.to_string_lossy().to_string(), false) - .expect("could not load lockfile"); - let maybe_lockfile = Some(Mutex::new(lockfile)); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None); - let specifier = - ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - builder - .get_graph(&maybe_lockfile) - .expect("could not get graph"); - } - - #[tokio::test] - async fn test_graph_with_lockfile_fail() { - let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let fixtures = c.join("tests/module_graph"); - let lockfile_path = fixtures.join("lockfile_fail.json"); - let lockfile = - Lockfile::new(lockfile_path.to_string_lossy().to_string(), false) - .expect("could not load lockfile"); - let maybe_lockfile = Some(Mutex::new(lockfile)); - let handler = Rc::new(RefCell::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None); - let specifier = - ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - builder - .insert(&specifier) - .await - .expect("module not inserted"); - builder - .get_graph(&maybe_lockfile) - .expect_err("expected an error"); - } -} diff --git a/cli/main.rs b/cli/main.rs index 8672a5681..bd39d80bc 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -25,7 +25,6 @@ pub mod fmt_errors; mod fs; pub mod global_state; mod global_timer; -mod graph; pub mod http_cache; mod http_util; mod import_map; @@ -38,6 +37,7 @@ mod lockfile; mod media_type; mod metrics; mod module_graph; +mod module_graph2; mod op_fetch_asset; pub mod ops; pub mod permissions; diff --git a/cli/module_graph2.rs b/cli/module_graph2.rs new file mode 100644 index 000000000..b04f21200 --- /dev/null +++ b/cli/module_graph2.rs @@ -0,0 +1,1065 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use crate::ast; +use crate::ast::parse; +use crate::ast::Location; +use crate::ast::ParsedModule; +use crate::file_fetcher::TextDocument; +use crate::import_map::ImportMap; +use crate::lockfile::Lockfile; +use crate::media_type::MediaType; +use crate::specifier_handler::CachedModule; +use crate::specifier_handler::DependencyMap; +use crate::specifier_handler::EmitMap; +use crate::specifier_handler::EmitType; +use crate::specifier_handler::FetchFuture; +use crate::specifier_handler::SpecifierHandler; +use crate::tsc_config::IgnoredCompilerOptions; +use crate::tsc_config::TsConfig; +use crate::version; +use crate::AnyError; + +use deno_core::futures::stream::FuturesUnordered; +use deno_core::futures::stream::StreamExt; +use deno_core::serde_json::json; +use deno_core::ModuleSpecifier; +use regex::Regex; +use serde::Deserialize; +use serde::Deserializer; +use std::cell::RefCell; +use std::collections::HashMap; +use std::collections::HashSet; +use std::error::Error; +use std::fmt; +use std::rc::Rc; +use std::result; +use std::sync::Mutex; +use std::time::Instant; +use swc_ecmascript::dep_graph::DependencyKind; + +pub type BuildInfoMap = HashMap; + +lazy_static! { + /// Matched the `@deno-types` pragma. + static ref DENO_TYPES_RE: Regex = + Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#) + .unwrap(); + /// Matches a `/// ` comment reference. + static ref TRIPLE_SLASH_REFERENCE_RE: Regex = + Regex::new(r"(?i)^/\s*").unwrap(); + /// Matches a path reference, which adds a dependency to a module + static ref PATH_REFERENCE_RE: Regex = + Regex::new(r#"(?i)\spath\s*=\s*["']([^"']*)["']"#).unwrap(); + /// Matches a types reference, which for JavaScript files indicates the + /// location of types to use when type checking a program that includes it as + /// a dependency. + static ref TYPES_REFERENCE_RE: Regex = + Regex::new(r#"(?i)\stypes\s*=\s*["']([^"']*)["']"#).unwrap(); +} + +/// A group of errors that represent errors that can occur when interacting with +/// a module graph. +#[allow(unused)] +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum GraphError { + /// A module using the HTTPS protocol is trying to import a module with an + /// HTTP schema. + InvalidDowngrade(ModuleSpecifier, Location), + /// A remote module is trying to import a local module. + InvalidLocalImport(ModuleSpecifier, Location), + /// A remote module is trying to import a local module. + InvalidSource(ModuleSpecifier, String), + /// A module specifier could not be resolved for a given import. + InvalidSpecifier(String, Location), + /// An unexpected dependency was requested for a module. + MissingDependency(ModuleSpecifier, String), + /// An unexpected specifier was requested. + MissingSpecifier(ModuleSpecifier), + /// Snapshot data was not present in a situation where it was required. + MissingSnapshotData, + /// The current feature is not supported. + NotSupported(String), +} +use GraphError::*; + +impl fmt::Display for GraphError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + InvalidDowngrade(ref specifier, ref location) => write!(f, "Modules imported via https are not allowed to import http modules.\n Importing: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), + InvalidLocalImport(ref specifier, ref location) => write!(f, "Remote modules are not allowed to import local modules.\n Importing: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), + InvalidSource(ref specifier, ref lockfile) => write!(f, "The source code is invalid, as it does not match the expected hash in the lock file.\n Specifier: {}\n Lock file: {}", specifier, lockfile), + InvalidSpecifier(ref specifier, ref location) => write!(f, "Unable to resolve dependency specifier.\n Specifier: {}\n at {}:{}:{}", specifier, location.filename, location.line, location.col), + MissingDependency(ref referrer, specifier) => write!( + f, + "The graph is missing a dependency.\n Specifier: {} from {}", + specifier, referrer + ), + MissingSpecifier(ref specifier) => write!( + f, + "The graph is missing a specifier.\n Specifier: {}", + specifier + ), + MissingSnapshotData => write!(f, "Snapshot data was not supplied, but required."), + NotSupported(ref msg) => write!(f, "{}", msg), + } + } +} + +impl Error for GraphError {} + +/// An enum which represents the parsed out values of references in source code. +#[derive(Debug, Clone, Eq, PartialEq)] +enum TypeScriptReference { + Path(String), + Types(String), +} + +/// Determine if a comment contains a triple slash reference and optionally +/// return its kind and value. +fn parse_ts_reference(comment: &str) -> Option { + if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) { + None + } else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) { + Some(TypeScriptReference::Path( + captures.get(1).unwrap().as_str().to_string(), + )) + } else if let Some(captures) = TYPES_REFERENCE_RE.captures(comment) { + Some(TypeScriptReference::Types( + captures.get(1).unwrap().as_str().to_string(), + )) + } else { + None + } +} + +/// Determine if a comment contains a `@deno-types` pragma and optionally return +/// its value. +fn parse_deno_types(comment: &str) -> Option { + if let Some(captures) = DENO_TYPES_RE.captures(comment) { + if let Some(m) = captures.get(1) { + Some(m.as_str().to_string()) + } else if let Some(m) = captures.get(2) { + Some(m.as_str().to_string()) + } else { + panic!("unreachable"); + } + } else { + None + } +} + +/// A hashing function that takes the source code, version and optionally a +/// user provided config and generates a string hash which can be stored to +/// determine if the cached emit is valid or not. +fn get_version(source: &TextDocument, version: &str, config: &[u8]) -> String { + crate::checksum::gen(&[ + source.to_str().unwrap().as_bytes(), + version.as_bytes(), + config, + ]) +} + +/// A logical representation of a module within a graph. +#[derive(Debug, Clone)] +struct Module { + dependencies: DependencyMap, + emits: EmitMap, + is_dirty: bool, + is_hydrated: bool, + is_parsed: bool, + maybe_import_map: Option>>, + maybe_parsed_module: Option, + maybe_types: Option<(String, ModuleSpecifier)>, + maybe_version: Option, + media_type: MediaType, + specifier: ModuleSpecifier, + source: TextDocument, +} + +impl Default for Module { + fn default() -> Self { + Module { + dependencies: HashMap::new(), + emits: HashMap::new(), + is_dirty: false, + is_hydrated: false, + is_parsed: false, + maybe_import_map: None, + maybe_parsed_module: None, + maybe_types: None, + maybe_version: None, + media_type: MediaType::Unknown, + specifier: ModuleSpecifier::resolve_url("https://deno.land/x/").unwrap(), + source: TextDocument::new(Vec::new(), Option::<&str>::None), + } + } +} + +impl Module { + pub fn new( + specifier: ModuleSpecifier, + maybe_import_map: Option>>, + ) -> Self { + Module { + specifier, + maybe_import_map, + ..Module::default() + } + } + + /// Return `true` if the current hash of the module matches the stored + /// version. + pub fn emit_valid(&self, config: &[u8]) -> bool { + if let Some(version) = self.maybe_version.clone() { + version == get_version(&self.source, version::DENO, config) + } else { + false + } + } + + pub fn hydrate(&mut self, cached_module: CachedModule) { + self.media_type = cached_module.media_type; + self.source = cached_module.source; + if self.maybe_import_map.is_none() { + if let Some(dependencies) = cached_module.maybe_dependencies { + self.dependencies = dependencies; + self.is_parsed = true; + } + } + self.maybe_types = if let Some(ref specifier) = cached_module.maybe_types { + Some(( + specifier.clone(), + self + .resolve_import(&specifier, None) + .expect("could not resolve module"), + )) + } else { + None + }; + self.is_dirty = false; + self.emits = cached_module.emits; + self.maybe_version = cached_module.maybe_version; + self.is_hydrated = true; + } + + pub fn parse(&mut self) -> Result<(), AnyError> { + let parsed_module = + parse(&self.specifier, &self.source.to_str()?, &self.media_type)?; + + // parse out any triple slash references + for comment in parsed_module.get_leading_comments().iter() { + if let Some(ts_reference) = parse_ts_reference(&comment.text) { + let location: Location = parsed_module.get_location(&comment.span); + match ts_reference { + TypeScriptReference::Path(import) => { + let specifier = self.resolve_import(&import, Some(location))?; + let dep = self.dependencies.entry(import).or_default(); + dep.maybe_code = Some(specifier); + } + TypeScriptReference::Types(import) => { + let specifier = self.resolve_import(&import, Some(location))?; + if self.media_type == MediaType::JavaScript + || self.media_type == MediaType::JSX + { + // TODO(kitsonk) we need to specifically update the cache when + // this value changes + self.maybe_types = Some((import.clone(), specifier)); + } else { + let dep = self.dependencies.entry(import).or_default(); + dep.maybe_type = Some(specifier); + } + } + } + } + } + + // Parse out all the syntactical dependencies for a module + let dependencies = parsed_module.analyze_dependencies(); + for desc in dependencies + .iter() + .filter(|desc| desc.kind != DependencyKind::Require) + { + let location = Location { + filename: self.specifier.to_string(), + col: desc.col, + line: desc.line, + }; + let specifier = + self.resolve_import(&desc.specifier, Some(location.clone()))?; + + // Parse out any `@deno-types` pragmas and modify dependency + let maybe_types_specifier = if !desc.leading_comments.is_empty() { + let comment = desc.leading_comments.last().unwrap(); + if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() { + Some(self.resolve_import(deno_types, Some(location))?) + } else { + None + } + } else { + None + }; + + let dep = self + .dependencies + .entry(desc.specifier.to_string()) + .or_default(); + if desc.kind == DependencyKind::ExportType + || desc.kind == DependencyKind::ImportType + { + dep.maybe_type = Some(specifier); + } else { + dep.maybe_code = Some(specifier); + } + if let Some(types_specifier) = maybe_types_specifier { + dep.maybe_type = Some(types_specifier); + } + } + + self.maybe_parsed_module = Some(parsed_module); + Ok(()) + } + + fn resolve_import( + &self, + specifier: &str, + maybe_location: Option, + ) -> Result { + let maybe_resolve = if let Some(import_map) = self.maybe_import_map.clone() + { + import_map + .borrow() + .resolve(specifier, self.specifier.as_str())? + } else { + None + }; + let specifier = if let Some(module_specifier) = maybe_resolve { + module_specifier + } else { + ModuleSpecifier::resolve_import(specifier, self.specifier.as_str())? + }; + + let referrer_scheme = self.specifier.as_url().scheme(); + let specifier_scheme = specifier.as_url().scheme(); + let location = maybe_location.unwrap_or(Location { + filename: self.specifier.to_string(), + line: 0, + col: 0, + }); + + // Disallow downgrades from HTTPS to HTTP + if referrer_scheme == "https" && specifier_scheme == "http" { + return Err(InvalidDowngrade(specifier.clone(), location).into()); + } + + // Disallow a remote URL from trying to import a local URL + if (referrer_scheme == "https" || referrer_scheme == "http") + && !(specifier_scheme == "https" || specifier_scheme == "http") + { + return Err(InvalidLocalImport(specifier.clone(), location).into()); + } + + Ok(specifier) + } + + /// Calculate the hashed version of the module and update the `maybe_version`. + pub fn set_version(&mut self, config: &[u8]) { + self.maybe_version = Some(get_version(&self.source, version::DENO, config)) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Stats(Vec<(String, u128)>); + +impl<'de> Deserialize<'de> for Stats { + fn deserialize(deserializer: D) -> result::Result + where + D: Deserializer<'de>, + { + let items: Vec<(String, u128)> = Deserialize::deserialize(deserializer)?; + Ok(Stats(items)) + } +} + +impl fmt::Display for Stats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for (key, value) in self.0.clone() { + write!(f, "{}: {}", key, value)?; + } + + Ok(()) + } +} + +/// A structure which provides options when transpiling modules. +#[derive(Debug, Default)] +pub struct TranspileOptions { + /// If `true` then debug logging will be output from the isolate. + pub debug: bool, + /// An optional string that points to a user supplied TypeScript configuration + /// file that augments the the default configuration passed to the TypeScript + /// compiler. + pub maybe_config_path: Option, +} + +/// A dependency graph of modules, were the modules that have been inserted via +/// the builder will be loaded into the graph. Also provides an interface to +/// be able to manipulate and handle the graph. +#[derive(Debug)] +pub struct Graph2 { + build_info: BuildInfoMap, + handler: Rc>, + modules: HashMap, + roots: Vec, +} + +impl Graph2 { + /// Create a new instance of a graph, ready to have modules loaded it. + /// + /// The argument `handler` is an instance of a structure that implements the + /// `SpecifierHandler` trait. + /// + pub fn new(handler: Rc>) -> Self { + Graph2 { + build_info: HashMap::new(), + handler, + modules: HashMap::new(), + roots: Vec::new(), + } + } + + /// Update the handler with any modules that are marked as _dirty_ and update + /// any build info if present. + fn flush(&mut self, emit_type: &EmitType) -> Result<(), AnyError> { + let mut handler = self.handler.borrow_mut(); + for (_, module) in self.modules.iter_mut() { + if module.is_dirty { + let (code, maybe_map) = module.emits.get(emit_type).unwrap(); + handler.set_cache( + &module.specifier, + &emit_type, + code.clone(), + maybe_map.clone(), + )?; + module.is_dirty = false; + if let Some(version) = &module.maybe_version { + handler.set_version(&module.specifier, version.clone())?; + } + } + } + for root_specifier in self.roots.iter() { + if let Some(build_info) = self.build_info.get(&emit_type) { + handler.set_build_info( + root_specifier, + &emit_type, + build_info.to_owned(), + )?; + } + } + + Ok(()) + } + + /// Verify the subresource integrity of the graph based upon the optional + /// lockfile, updating the lockfile with any missing resources. This will + /// error if any of the resources do not match their lock status. + pub fn lock( + &self, + maybe_lockfile: &Option>, + ) -> Result<(), AnyError> { + if let Some(lf) = maybe_lockfile { + let mut lockfile = lf.lock().unwrap(); + for (ms, module) in self.modules.iter() { + let specifier = module.specifier.to_string(); + let code = module.source.to_string()?; + let valid = lockfile.check_or_insert(&specifier, &code); + if !valid { + return Err( + InvalidSource(ms.clone(), lockfile.filename.clone()).into(), + ); + } + } + } + + Ok(()) + } + + /// Transpile (only transform) the graph, updating any emitted modules + /// with the specifier handler. The result contains any performance stats + /// from the compiler and optionally any user provided configuration compiler + /// options that were ignored. + /// + /// # Arguments + /// + /// - `options` - A structure of options which impact how the code is + /// transpiled. + /// + pub fn transpile( + &mut self, + options: TranspileOptions, + ) -> Result<(Stats, Option), AnyError> { + let start = Instant::now(); + let emit_type = EmitType::Cli; + + let mut ts_config = TsConfig::new(json!({ + "checkJs": false, + "emitDecoratorMetadata": false, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + })); + + let maybe_ignored_options = + ts_config.merge_user_config(options.maybe_config_path)?; + + let compiler_options = ts_config.as_transpile_config()?; + let check_js = compiler_options.check_js; + let transform_jsx = compiler_options.jsx == "react"; + let emit_options = ast::TranspileOptions { + emit_metadata: compiler_options.emit_decorator_metadata, + inline_source_map: true, + jsx_factory: compiler_options.jsx_factory, + jsx_fragment_factory: compiler_options.jsx_fragment_factory, + transform_jsx, + }; + + let mut emit_count: u128 = 0; + for (_, module) in self.modules.iter_mut() { + // TODO(kitsonk) a lot of this logic should be refactored into `Module` as + // we start to support other methods on the graph. Especially managing + // the dirty state is something the module itself should "own". + + // if the module is a Dts file we should skip it + if module.media_type == MediaType::Dts { + continue; + } + // if we don't have check_js enabled, we won't touch non TypeScript + // modules + if !(check_js + || module.media_type == MediaType::TSX + || module.media_type == MediaType::TypeScript) + { + continue; + } + let config = ts_config.as_bytes(); + // skip modules that already have a valid emit + if module.emits.contains_key(&emit_type) && module.emit_valid(&config) { + continue; + } + if module.maybe_parsed_module.is_none() { + module.parse()?; + } + let parsed_module = module.maybe_parsed_module.clone().unwrap(); + let emit = parsed_module.transpile(&emit_options)?; + emit_count += 1; + module.emits.insert(emit_type.clone(), emit); + module.set_version(&config); + module.is_dirty = true; + } + self.flush(&emit_type)?; + + let stats = Stats(vec![ + ("Files".to_string(), self.modules.len() as u128), + ("Emitted".to_string(), emit_count), + ("Total time".to_string(), start.elapsed().as_millis()), + ]); + + Ok((stats, maybe_ignored_options)) + } +} + +/// A structure for building a dependency graph of modules. +pub struct GraphBuilder2 { + fetched: HashSet, + graph: Graph2, + maybe_import_map: Option>>, + pending: FuturesUnordered, +} + +impl GraphBuilder2 { + pub fn new( + handler: Rc>, + maybe_import_map: Option, + ) -> Self { + let internal_import_map = if let Some(import_map) = maybe_import_map { + Some(Rc::new(RefCell::new(import_map))) + } else { + None + }; + GraphBuilder2 { + graph: Graph2::new(handler), + fetched: HashSet::new(), + maybe_import_map: internal_import_map, + pending: FuturesUnordered::new(), + } + } + + /// Request a module to be fetched from the handler and queue up its future + /// to be awaited to be resolved. + fn fetch(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { + if self.fetched.contains(&specifier) { + return Ok(()); + } + + self.fetched.insert(specifier.clone()); + let future = self.graph.handler.borrow_mut().fetch(specifier.clone()); + self.pending.push(future); + + Ok(()) + } + + /// Visit a module that has been fetched, hydrating the module, analyzing its + /// dependencies if required, fetching those dependencies, and inserting the + /// module into the graph. + fn visit(&mut self, cached_module: CachedModule) -> Result<(), AnyError> { + let specifier = cached_module.specifier.clone(); + let mut module = + Module::new(specifier.clone(), self.maybe_import_map.clone()); + module.hydrate(cached_module); + if !module.is_parsed { + let has_types = module.maybe_types.is_some(); + module.parse()?; + if self.maybe_import_map.is_none() { + let mut handler = self.graph.handler.borrow_mut(); + handler.set_deps(&specifier, module.dependencies.clone())?; + if !has_types { + if let Some((types, _)) = module.maybe_types.clone() { + handler.set_types(&specifier, types)?; + } + } + } + } + for (_, dep) in module.dependencies.iter() { + if let Some(specifier) = dep.maybe_code.as_ref() { + self.fetch(specifier)?; + } + if let Some(specifier) = dep.maybe_type.as_ref() { + self.fetch(specifier)?; + } + } + if let Some((_, specifier)) = module.maybe_types.as_ref() { + self.fetch(specifier)?; + } + self.graph.modules.insert(specifier, module); + + Ok(()) + } + + /// Insert a module into the graph based on a module specifier. The module + /// and any dependencies will be fetched from the handler. The module will + /// also be treated as a _root_ module in the graph. + pub async fn insert( + &mut self, + specifier: &ModuleSpecifier, + ) -> Result<(), AnyError> { + self.fetch(specifier)?; + + loop { + let cached_module = self.pending.next().await.unwrap()?; + self.visit(cached_module)?; + if self.pending.is_empty() { + break; + } + } + + if !self.graph.roots.contains(specifier) { + self.graph.roots.push(specifier.clone()); + } + + Ok(()) + } + + /// Move out the graph from the builder to be utilized further. An optional + /// lockfile can be provided, where if the sources in the graph do not match + /// the expected lockfile, the method with error instead of returning the + /// graph. + /// + /// TODO(@kitsonk) this should really be owned by the graph, but currently + /// the lockfile is behind a mutex in global_state, which makes it really + /// hard to not pass around as a reference, which if the Graph owned it, it + /// would need lifetime parameters and lifetime parameters are 😭 + pub fn get_graph( + self, + maybe_lockfile: &Option>, + ) -> Result { + self.graph.lock(maybe_lockfile)?; + Ok(self.graph) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use deno_core::futures::future; + use std::env; + use std::fs; + use std::path::PathBuf; + use std::sync::Mutex; + + /// This is a testing mock for `SpecifierHandler` that uses a special file + /// system renaming to mock local and remote modules as well as provides + /// "spies" for the critical methods for testing purposes. + #[derive(Debug, Default)] + pub struct MockSpecifierHandler { + pub fixtures: PathBuf, + pub build_info: HashMap, + pub build_info_calls: Vec<(ModuleSpecifier, EmitType, TextDocument)>, + pub cache_calls: Vec<( + ModuleSpecifier, + EmitType, + TextDocument, + Option, + )>, + pub deps_calls: Vec<(ModuleSpecifier, DependencyMap)>, + pub types_calls: Vec<(ModuleSpecifier, String)>, + pub version_calls: Vec<(ModuleSpecifier, String)>, + } + + impl MockSpecifierHandler { + fn get_cache( + &self, + specifier: ModuleSpecifier, + ) -> Result { + let specifier_text = specifier + .to_string() + .replace(":///", "_") + .replace("://", "_") + .replace("/", "-"); + let specifier_path = self.fixtures.join(specifier_text); + let media_type = + match specifier_path.extension().unwrap().to_str().unwrap() { + "ts" => { + if specifier_path.to_string_lossy().ends_with(".d.ts") { + MediaType::Dts + } else { + MediaType::TypeScript + } + } + "tsx" => MediaType::TSX, + "js" => MediaType::JavaScript, + "jsx" => MediaType::JSX, + _ => MediaType::Unknown, + }; + let source = + TextDocument::new(fs::read(specifier_path)?, Option::<&str>::None); + + Ok(CachedModule { + source, + specifier, + media_type, + ..CachedModule::default() + }) + } + } + + impl SpecifierHandler for MockSpecifierHandler { + fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture { + Box::pin(future::ready(self.get_cache(specifier))) + } + fn get_build_info( + &self, + specifier: &ModuleSpecifier, + _cache_type: &EmitType, + ) -> Result, AnyError> { + Ok(self.build_info.get(specifier).cloned()) + } + fn set_cache( + &mut self, + specifier: &ModuleSpecifier, + cache_type: &EmitType, + code: TextDocument, + maybe_map: Option, + ) -> Result<(), AnyError> { + self.cache_calls.push(( + specifier.clone(), + cache_type.clone(), + code, + maybe_map, + )); + Ok(()) + } + fn set_types( + &mut self, + specifier: &ModuleSpecifier, + types: String, + ) -> Result<(), AnyError> { + self.types_calls.push((specifier.clone(), types)); + Ok(()) + } + fn set_build_info( + &mut self, + specifier: &ModuleSpecifier, + cache_type: &EmitType, + build_info: TextDocument, + ) -> Result<(), AnyError> { + self + .build_info + .insert(specifier.clone(), build_info.clone()); + self.build_info_calls.push(( + specifier.clone(), + cache_type.clone(), + build_info, + )); + Ok(()) + } + fn set_deps( + &mut self, + specifier: &ModuleSpecifier, + dependencies: DependencyMap, + ) -> Result<(), AnyError> { + self.deps_calls.push((specifier.clone(), dependencies)); + Ok(()) + } + fn set_version( + &mut self, + specifier: &ModuleSpecifier, + version: String, + ) -> Result<(), AnyError> { + self.version_calls.push((specifier.clone(), version)); + Ok(()) + } + } + + #[test] + fn test_get_version() { + let doc_a = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let version_a = get_version(&doc_a, "1.2.3", b""); + let doc_b = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let version_b = get_version(&doc_b, "1.2.3", b""); + assert_eq!(version_a, version_b); + + let version_c = get_version(&doc_a, "1.2.3", b"options"); + assert_ne!(version_a, version_c); + + let version_d = get_version(&doc_b, "1.2.3", b"options"); + assert_eq!(version_c, version_d); + + let version_e = get_version(&doc_a, "1.2.4", b""); + assert_ne!(version_a, version_e); + + let version_f = get_version(&doc_b, "1.2.4", b""); + assert_eq!(version_e, version_f); + } + + #[test] + fn test_module_emit_valid() { + let source = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let maybe_version = Some(get_version(&source, version::DENO, b"")); + let module = Module { + source, + maybe_version, + ..Module::default() + }; + assert!(module.emit_valid(b"")); + + let source = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let old_source = + TextDocument::new(b"console.log(43);".to_vec(), Option::<&str>::None); + let maybe_version = Some(get_version(&old_source, version::DENO, b"")); + let module = Module { + source, + maybe_version, + ..Module::default() + }; + assert!(!module.emit_valid(b"")); + + let source = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let maybe_version = Some(get_version(&source, "0.0.0", b"")); + let module = Module { + source, + maybe_version, + ..Module::default() + }; + assert!(!module.emit_valid(b"")); + + let source = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let module = Module { + source, + ..Module::default() + }; + assert!(!module.emit_valid(b"")); + } + + #[test] + fn test_module_set_version() { + let source = + TextDocument::new(b"console.log(42);".to_vec(), Option::<&str>::None); + let expected = Some(get_version(&source, version::DENO, b"")); + let mut module = Module { + source, + ..Module::default() + }; + assert!(module.maybe_version.is_none()); + module.set_version(b""); + assert_eq!(module.maybe_version, expected); + } + + #[tokio::test] + async fn test_graph_transpile() { + // This is a complex scenario of transpiling, where we have TypeScript + // importing a JavaScript file (with type definitions) which imports + // TypeScript, JavaScript, and JavaScript with type definitions. + // For scenarios where we transpile, we only want the TypeScript files + // to be actually emitted. + // + // This also exercises "@deno-types" and type references. + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/module_graph"); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") + .expect("could not resolve module"); + builder + .insert(&specifier) + .await + .expect("module not inserted"); + let mut graph = builder.get_graph(&None).expect("could not get graph"); + let (stats, maybe_ignored_options) = + graph.transpile(TranspileOptions::default()).unwrap(); + assert_eq!(stats.0.len(), 3); + assert_eq!(maybe_ignored_options, None); + let h = handler.borrow(); + assert_eq!(h.cache_calls.len(), 2); + assert_eq!(h.cache_calls[0].1, EmitType::Cli); + assert!(h.cache_calls[0] + .2 + .to_string() + .unwrap() + .contains("# sourceMappingURL=data:application/json;base64,")); + assert_eq!(h.cache_calls[0].3, None); + assert_eq!(h.cache_calls[1].1, EmitType::Cli); + assert!(h.cache_calls[1] + .2 + .to_string() + .unwrap() + .contains("# sourceMappingURL=data:application/json;base64,")); + assert_eq!(h.cache_calls[0].3, None); + assert_eq!(h.deps_calls.len(), 7); + assert_eq!( + h.deps_calls[0].0, + ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts").unwrap() + ); + assert_eq!(h.deps_calls[0].1.len(), 1); + assert_eq!( + h.deps_calls[1].0, + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.js") + .unwrap() + ); + assert_eq!(h.deps_calls[1].1.len(), 3); + assert_eq!( + h.deps_calls[2].0, + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.d.ts") + .unwrap() + ); + assert_eq!(h.deps_calls[2].1.len(), 3, "should have 3 dependencies"); + // sometimes the calls are not deterministic, and so checking the contents + // can cause some failures + assert_eq!(h.deps_calls[3].1.len(), 0, "should have no dependencies"); + assert_eq!(h.deps_calls[4].1.len(), 0, "should have no dependencies"); + assert_eq!(h.deps_calls[5].1.len(), 0, "should have no dependencies"); + assert_eq!(h.deps_calls[6].1.len(), 0, "should have no dependencies"); + } + + #[tokio::test] + async fn test_graph_transpile_user_config() { + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/module_graph"); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures: fixtures.clone(), + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + let specifier = + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/transpile.tsx") + .expect("could not resolve module"); + builder + .insert(&specifier) + .await + .expect("module not inserted"); + let mut graph = builder.get_graph(&None).expect("could not get graph"); + let (_, maybe_ignored_options) = graph + .transpile(TranspileOptions { + debug: false, + maybe_config_path: Some("tests/module_graph/tsconfig.json".to_string()), + }) + .unwrap(); + assert_eq!( + maybe_ignored_options.unwrap().items, + vec!["target".to_string()], + "the 'target' options should have been ignored" + ); + let h = handler.borrow(); + assert_eq!(h.cache_calls.len(), 1, "only one file should be emitted"); + // FIXME(bartlomieju): had to add space in `
`, probably a quirk in swc_ecma_codegen + assert!( + h.cache_calls[0] + .2 + .to_string() + .unwrap() + .contains("
Hello world!
"), + "jsx should have been preserved" + ); + } + + #[tokio::test] + async fn test_graph_with_lockfile() { + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/module_graph"); + let lockfile_path = fixtures.join("lockfile.json"); + let lockfile = + Lockfile::new(lockfile_path.to_string_lossy().to_string(), false) + .expect("could not load lockfile"); + let maybe_lockfile = Some(Mutex::new(lockfile)); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") + .expect("could not resolve module"); + builder + .insert(&specifier) + .await + .expect("module not inserted"); + builder + .get_graph(&maybe_lockfile) + .expect("could not get graph"); + } + + #[tokio::test] + async fn test_graph_with_lockfile_fail() { + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let fixtures = c.join("tests/module_graph"); + let lockfile_path = fixtures.join("lockfile_fail.json"); + let lockfile = + Lockfile::new(lockfile_path.to_string_lossy().to_string(), false) + .expect("could not load lockfile"); + let maybe_lockfile = Some(Mutex::new(lockfile)); + let handler = Rc::new(RefCell::new(MockSpecifierHandler { + fixtures, + ..MockSpecifierHandler::default() + })); + let mut builder = GraphBuilder2::new(handler.clone(), None); + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") + .expect("could not resolve module"); + builder + .insert(&specifier) + .await + .expect("module not inserted"); + builder + .get_graph(&maybe_lockfile) + .expect_err("expected an error"); + } +} diff --git a/cli/specifier_handler.rs b/cli/specifier_handler.rs index 80fb28b19..c2f852ab6 100644 --- a/cli/specifier_handler.rs +++ b/cli/specifier_handler.rs @@ -372,140 +372,9 @@ impl SpecifierHandler for FetchHandler { #[cfg(test)] pub mod tests { use super::*; - use crate::http_cache::HttpCache; - - use deno_core::futures::future; - use std::fs; - use std::path::PathBuf; use tempfile::TempDir; - /// This is a testing mock for `SpecifierHandler` that uses a special file - /// system renaming to mock local and remote modules as well as provides - /// "spies" for the critical methods for testing purposes. - #[derive(Debug, Default)] - pub struct MockSpecifierHandler { - pub fixtures: PathBuf, - pub build_info: HashMap, - pub build_info_calls: Vec<(ModuleSpecifier, EmitType, TextDocument)>, - pub cache_calls: Vec<( - ModuleSpecifier, - EmitType, - TextDocument, - Option, - )>, - pub deps_calls: Vec<(ModuleSpecifier, DependencyMap)>, - pub types_calls: Vec<(ModuleSpecifier, String)>, - pub version_calls: Vec<(ModuleSpecifier, String)>, - } - - impl MockSpecifierHandler {} - - impl MockSpecifierHandler { - fn get_cache( - &self, - specifier: ModuleSpecifier, - ) -> Result { - let specifier_text = specifier - .to_string() - .replace(":///", "_") - .replace("://", "_") - .replace("/", "-"); - let specifier_path = self.fixtures.join(specifier_text); - let media_type = - match specifier_path.extension().unwrap().to_str().unwrap() { - "ts" => { - if specifier_path.to_string_lossy().ends_with(".d.ts") { - MediaType::Dts - } else { - MediaType::TypeScript - } - } - "tsx" => MediaType::TSX, - "js" => MediaType::JavaScript, - "jsx" => MediaType::JSX, - _ => MediaType::Unknown, - }; - let source = - TextDocument::new(fs::read(specifier_path)?, Option::<&str>::None); - - Ok(CachedModule { - source, - specifier, - media_type, - ..CachedModule::default() - }) - } - } - - impl SpecifierHandler for MockSpecifierHandler { - fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture { - Box::pin(future::ready(self.get_cache(specifier))) - } - fn get_build_info( - &self, - specifier: &ModuleSpecifier, - _cache_type: &EmitType, - ) -> Result, AnyError> { - Ok(self.build_info.get(specifier).cloned()) - } - fn set_cache( - &mut self, - specifier: &ModuleSpecifier, - cache_type: &EmitType, - code: TextDocument, - maybe_map: Option, - ) -> Result<(), AnyError> { - self.cache_calls.push(( - specifier.clone(), - cache_type.clone(), - code, - maybe_map, - )); - Ok(()) - } - fn set_types( - &mut self, - specifier: &ModuleSpecifier, - types: String, - ) -> Result<(), AnyError> { - self.types_calls.push((specifier.clone(), types)); - Ok(()) - } - fn set_build_info( - &mut self, - specifier: &ModuleSpecifier, - cache_type: &EmitType, - build_info: TextDocument, - ) -> Result<(), AnyError> { - self - .build_info - .insert(specifier.clone(), build_info.clone()); - self.build_info_calls.push(( - specifier.clone(), - cache_type.clone(), - build_info, - )); - Ok(()) - } - fn set_deps( - &mut self, - specifier: &ModuleSpecifier, - dependencies: DependencyMap, - ) -> Result<(), AnyError> { - self.deps_calls.push((specifier.clone(), dependencies)); - Ok(()) - } - fn set_version( - &mut self, - specifier: &ModuleSpecifier, - version: String, - ) -> Result<(), AnyError> { - self.version_calls.push((specifier.clone(), version)); - Ok(()) - } - } - fn setup() -> (TempDir, FetchHandler) { let temp_dir = TempDir::new().expect("could not setup"); let deno_dir = DenoDir::new(Some(temp_dir.path().to_path_buf())) -- cgit v1.2.3