diff options
Diffstat (limited to 'cli/module_graph2.rs')
-rw-r--r-- | cli/module_graph2.rs | 1065 |
1 files changed, 1065 insertions, 0 deletions
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<EmitType, TextDocument>; + +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 `/// <reference ... />` comment reference. + static ref TRIPLE_SLASH_REFERENCE_RE: Regex = + Regex::new(r"(?i)^/\s*<reference\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<TypeScriptReference> { + 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<String> { + 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<Rc<RefCell<ImportMap>>>, + maybe_parsed_module: Option<ParsedModule>, + maybe_types: Option<(String, ModuleSpecifier)>, + maybe_version: Option<String>, + 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<Rc<RefCell<ImportMap>>>, + ) -> 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<Location>, + ) -> Result<ModuleSpecifier, AnyError> { + 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<D>(deserializer: D) -> result::Result<Self, D::Error> + 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<String>, +} + +/// 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<RefCell<dyn SpecifierHandler>>, + modules: HashMap<ModuleSpecifier, Module>, + roots: Vec<ModuleSpecifier>, +} + +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<RefCell<dyn SpecifierHandler>>) -> 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<Mutex<Lockfile>>, + ) -> 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<IgnoredCompilerOptions>), 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<ModuleSpecifier>, + graph: Graph2, + maybe_import_map: Option<Rc<RefCell<ImportMap>>>, + pending: FuturesUnordered<FetchFuture>, +} + +impl GraphBuilder2 { + pub fn new( + handler: Rc<RefCell<dyn SpecifierHandler>>, + maybe_import_map: Option<ImportMap>, + ) -> 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<Mutex<Lockfile>>, + ) -> Result<Graph2, AnyError> { + 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<ModuleSpecifier, TextDocument>, + pub build_info_calls: Vec<(ModuleSpecifier, EmitType, TextDocument)>, + pub cache_calls: Vec<( + ModuleSpecifier, + EmitType, + TextDocument, + Option<TextDocument>, + )>, + 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<CachedModule, AnyError> { + 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<Option<TextDocument>, AnyError> { + Ok(self.build_info.get(specifier).cloned()) + } + fn set_cache( + &mut self, + specifier: &ModuleSpecifier, + cache_type: &EmitType, + code: TextDocument, + maybe_map: Option<TextDocument>, + ) -> 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 `<div>`, probably a quirk in swc_ecma_codegen + assert!( + h.cache_calls[0] + .2 + .to_string() + .unwrap() + .contains("<div >Hello world!</div>"), + "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"); + } +} |