diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2021-10-11 08:26:22 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-11 08:26:22 +1100 |
commit | a7baf5f2bbb50dc0cb571de141b800b9155faca7 (patch) | |
tree | 4bebaabd1d3ed4595e8a388e0fae559bb5558974 /cli/module_graph.rs | |
parent | 5a8a989b7815023f33a1e3183a55cc8999af5d98 (diff) |
refactor: integrate deno_graph into CLI (#12369)
Diffstat (limited to 'cli/module_graph.rs')
-rw-r--r-- | cli/module_graph.rs | 2834 |
1 files changed, 0 insertions, 2834 deletions
diff --git a/cli/module_graph.rs b/cli/module_graph.rs deleted file mode 100644 index 7bea57e62..000000000 --- a/cli/module_graph.rs +++ /dev/null @@ -1,2834 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -use crate::ast; -use crate::ast::transpile; -use crate::ast::transpile_module; -use crate::ast::BundleHook; -use crate::ast::Location; -use crate::checksum; -use crate::colors; -use crate::config_file::CompilerOptions; -use crate::config_file::ConfigFile; -use crate::config_file::IgnoredCompilerOptions; -use crate::config_file::TsConfig; -use crate::diagnostics::Diagnostics; -use crate::info; -use crate::lockfile::Lockfile; -use crate::specifier_handler::CachedModule; -use crate::specifier_handler::Dependency; -use crate::specifier_handler::DependencyMap; -use crate::specifier_handler::Emit; -use crate::specifier_handler::FetchFuture; -use crate::specifier_handler::SpecifierHandler; -use crate::tsc; -use crate::version; -use deno_ast::swc::common::comments::Comment; -use deno_ast::swc::common::BytePos; -use deno_ast::swc::common::Span; -use deno_ast::MediaType; -use deno_ast::ParsedSource; -use deno_ast::SourceTextInfo; -use deno_core::error::anyhow; -use deno_core::error::custom_error; -use deno_core::error::get_custom_error_class; -use deno_core::error::AnyError; -use deno_core::error::Context; -use deno_core::futures::stream::FuturesUnordered; -use deno_core::futures::stream::StreamExt; -use deno_core::parking_lot::Mutex; -use deno_core::resolve_import; -use deno_core::resolve_url_or_path; -use deno_core::serde::Deserialize; -use deno_core::serde::Deserializer; -use deno_core::serde::Serialize; -use deno_core::serde::Serializer; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::url::Url; -use deno_core::ModuleResolutionError; -use deno_core::ModuleSource; -use deno_core::ModuleSpecifier; -use deno_graph::analyze_dependencies; -use import_map::ImportMap; -use import_map::ImportMapError; -use log::debug; -use regex::Regex; -use std::collections::HashMap; -use std::collections::HashSet; -use std::error::Error; -use std::fmt; -use std::path::PathBuf; -use std::rc::Rc; -use std::result; -use std::sync::Arc; -use std::time::Instant; - -lazy_static::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. -#[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), - /// The source code is invalid, as it does not match the expected hash in the - /// lockfile. - InvalidSource(ModuleSpecifier, PathBuf), - /// An unexpected dependency was requested for a module. - MissingDependency(ModuleSpecifier, String), - /// An unexpected specifier was requested. - MissingSpecifier(ModuleSpecifier), - /// The current feature is not supported. - NotSupported(String), - /// A unsupported media type was attempted to be imported as a module. - UnsupportedImportType(ModuleSpecifier, MediaType), -} - -impl fmt::Display for GraphError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - GraphError::InvalidDowngrade(ref specifier, ref location) => write!(f, "Modules imported via https are not allowed to import http modules.\n Importing: {}\n at {}", specifier, location), - GraphError::InvalidLocalImport(ref specifier, ref location) => write!(f, "Remote modules are not allowed to import local modules. Consider using a dynamic import instead.\n Importing: {}\n at {}", specifier, location), - GraphError::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.to_str().unwrap()), - GraphError::MissingDependency(ref referrer, specifier) => write!( - f, - "The graph is missing a dependency.\n Specifier: {} from {}", - specifier, referrer - ), - GraphError::MissingSpecifier(ref specifier) => write!( - f, - "The graph is missing a specifier.\n Specifier: {}", - specifier - ), - GraphError::NotSupported(ref msg) => write!(f, "{}", msg), - GraphError::UnsupportedImportType(ref specifier, ref media_type) => write!(f, "An unsupported media type was attempted to be imported as a module.\n Specifier: {}\n MediaType: {}", specifier, media_type), - } - } -} - -impl Error for GraphError {} - -/// A structure for handling bundle loading, which is implemented here, to -/// avoid a circular dependency with `ast`. -struct BundleLoader<'a> { - cm: Rc<deno_ast::swc::common::SourceMap>, - emit_options: &'a ast::EmitOptions, - globals: &'a deno_ast::swc::common::Globals, - graph: &'a Graph, -} - -impl<'a> BundleLoader<'a> { - pub fn new( - graph: &'a Graph, - emit_options: &'a ast::EmitOptions, - globals: &'a deno_ast::swc::common::Globals, - cm: Rc<deno_ast::swc::common::SourceMap>, - ) -> Self { - BundleLoader { - cm, - emit_options, - globals, - graph, - } - } -} - -impl deno_ast::swc::bundler::Load for BundleLoader<'_> { - fn load( - &self, - file: &deno_ast::swc::common::FileName, - ) -> Result<deno_ast::swc::bundler::ModuleData, AnyError> { - match file { - deno_ast::swc::common::FileName::Url(specifier) => { - if let Some(src) = self.graph.get_source(specifier) { - let media_type = self - .graph - .get_media_type(specifier) - .context("Looking up media type during bundling.")?; - let (source_file, module) = transpile_module( - specifier, - &src, - media_type, - self.emit_options, - self.globals, - self.cm.clone(), - )?; - Ok(deno_ast::swc::bundler::ModuleData { - fm: source_file, - module, - helpers: Default::default(), - }) - } else { - Err( - GraphError::MissingDependency( - specifier.clone(), - "<bundle>".to_string(), - ) - .into(), - ) - } - } - _ => unreachable!("Received request for unsupported filename {:?}", file), - } - } -} - -/// An enum which represents the parsed out values of references in source code. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum TypeScriptReference { - Path(String), - Types(String), -} - -fn match_to_span(comment: &Comment, m: ®ex::Match) -> Span { - Span { - lo: comment.span.lo + BytePos((m.start() + 1) as u32), - hi: comment.span.lo + BytePos((m.end() + 1) as u32), - ctxt: comment.span.ctxt, - } -} - -/// Determine if a comment contains a triple slash reference and optionally -/// return its kind and value. -pub fn parse_ts_reference( - comment: &Comment, -) -> Option<(TypeScriptReference, Span)> { - if !TRIPLE_SLASH_REFERENCE_RE.is_match(&comment.text) { - None - } else if let Some(captures) = PATH_REFERENCE_RE.captures(&comment.text) { - let m = captures.get(1).unwrap(); - Some(( - TypeScriptReference::Path(m.as_str().to_string()), - match_to_span(comment, &m), - )) - } else { - TYPES_REFERENCE_RE.captures(&comment.text).map(|captures| { - let m = captures.get(1).unwrap(); - ( - TypeScriptReference::Types(m.as_str().to_string()), - match_to_span(comment, &m), - ) - }) - } -} - -/// Determine if a comment contains a `@deno-types` pragma and optionally return -/// its value. -pub fn parse_deno_types(comment: &Comment) -> Option<(String, Span)> { - let captures = DENO_TYPES_RE.captures(&comment.text)?; - if let Some(m) = captures.get(1) { - Some((m.as_str().to_string(), match_to_span(comment, &m))) - } else if let Some(m) = captures.get(2) { - Some((m.as_str().to_string(), match_to_span(comment, &m))) - } else { - unreachable!(); - } -} - -/// 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: &str, version: &str, config: &[u8]) -> String { - crate::checksum::gen(&[source.as_bytes(), version.as_bytes(), config]) -} - -/// A logical representation of a module within a graph. -#[derive(Debug, Clone)] -pub struct Module { - pub dependencies: DependencyMap, - is_dirty: bool, - is_parsed: bool, - maybe_emit: Option<Emit>, - maybe_emit_path: Option<(PathBuf, Option<PathBuf>)>, - maybe_import_map: Option<Arc<Mutex<ImportMap>>>, - maybe_types: Option<(String, ModuleSpecifier)>, - maybe_version: Option<String>, - media_type: MediaType, - specifier: ModuleSpecifier, - text_info: SourceTextInfo, - source_path: PathBuf, -} - -impl Default for Module { - fn default() -> Self { - Module { - dependencies: HashMap::new(), - is_dirty: false, - is_parsed: false, - maybe_emit: None, - maybe_emit_path: None, - maybe_import_map: None, - maybe_types: None, - maybe_version: None, - media_type: MediaType::Unknown, - specifier: deno_core::resolve_url("file:///example.js").unwrap(), - text_info: SourceTextInfo::from_string("".to_string()), - source_path: PathBuf::new(), - } - } -} - -impl Module { - pub fn new( - cached_module: CachedModule, - is_root: bool, - maybe_import_map: Option<Arc<Mutex<ImportMap>>>, - ) -> Self { - // If this is a local root file, and its media type is unknown, set the - // media type to JavaScript. This allows easier ability to create "shell" - // scripts with Deno. - let media_type = if is_root - && !cached_module.is_remote - && cached_module.media_type == MediaType::Unknown - { - MediaType::JavaScript - } else { - cached_module.media_type - }; - let mut module = Module { - specifier: cached_module.specifier, - maybe_import_map, - media_type, - text_info: SourceTextInfo::new(cached_module.source), - source_path: cached_module.source_path, - maybe_emit: cached_module.maybe_emit, - maybe_emit_path: cached_module.maybe_emit_path, - maybe_version: cached_module.maybe_version, - is_dirty: false, - ..Self::default() - }; - if module.maybe_import_map.is_none() { - if let Some(dependencies) = cached_module.maybe_dependencies { - module.dependencies = dependencies; - module.is_parsed = true; - } - } - module.maybe_types = cached_module.maybe_types.map(|specifier| { - ( - specifier.clone(), - module - .resolve_import(&specifier, None) - .expect("could not resolve module"), - ) - }); - module - } - - /// Return `true` if the current hash of the module matches the stored - /// version. - pub fn is_emit_valid(&self, config: &[u8]) -> bool { - if let Some(version) = self.maybe_version.clone() { - version - == get_version(self.text_info.text_str(), &version::deno(), config) - } else { - false - } - } - - /// Parse a module, populating the structure with data retrieved from the - /// source of the module. - pub fn parse(&mut self) -> Result<ParsedSource, AnyError> { - let parsed_module = deno_ast::parse_module(deno_ast::ParseParams { - specifier: self.specifier.as_str().to_string(), - source: self.text_info.clone(), - media_type: self.media_type, - capture_tokens: false, - maybe_syntax: None, - })?; - - // parse out any triple slash references - for comment in parsed_module.get_leading_comments().iter() { - if let Some((ts_reference, _)) = parse_ts_reference(comment) { - let location = Location::from_pos(&parsed_module, comment.span.lo); - match ts_reference { - TypeScriptReference::Path(import) => { - let specifier = - self.resolve_import(&import, Some(location.clone()))?; - let dep = self - .dependencies - .entry(import) - .or_insert_with(|| Dependency::new(location)); - dep.maybe_code = Some(specifier); - } - TypeScriptReference::Types(import) => { - let specifier = - self.resolve_import(&import, Some(location.clone()))?; - 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_insert_with(|| Dependency::new(location)); - dep.maybe_type = Some(specifier); - } - } - } - } - } - - // Parse out all the syntactical dependencies for a module - let dependencies = analyze_dependencies(&parsed_module); - for desc in dependencies.iter().filter(|desc| { - desc.kind != deno_ast::swc::dep_graph::DependencyKind::Require - }) { - let location = Location::from_pos(&parsed_module, desc.span.lo); - - // In situations where there is a potential issue with resolving the - // import specifier, that ends up being a module resolution error for a - // code dependency, we should not throw in the `ModuleGraph` but instead - // wait until runtime and throw there, as with dynamic imports they need - // to be catchable, which means they need to be resolved at runtime. - let maybe_specifier = - match self.resolve_import(&desc.specifier, Some(location.clone())) { - Ok(specifier) => Some(specifier), - Err(any_error) => { - match any_error.downcast_ref::<ModuleResolutionError>() { - Some(ModuleResolutionError::ImportPrefixMissing(..)) => { - Some(Url::parse(&format!("bare:{}", &desc.specifier)).unwrap()) - } - _ => match any_error.downcast_ref::<ImportMapError>() { - Some(ImportMapError::UnmappedBareSpecifier(..)) => Some( - Url::parse(&format!("bare:{}", &desc.specifier)).unwrap(), - ), - _ => { - return Err(any_error); - } - }, - } - } - }; - - // Parse out any `@deno-types` pragmas and modify dependency - let maybe_type = if !desc.leading_comments.is_empty() { - let comment = desc.leading_comments.last().unwrap(); - if let Some((deno_types, _)) = parse_deno_types(comment).as_ref() { - Some(self.resolve_import(deno_types, Some(location.clone()))?) - } else { - None - } - } else { - None - }; - - let dep = self - .dependencies - .entry(desc.specifier.to_string()) - .or_insert_with(|| Dependency::new(location)); - dep.is_dynamic = desc.is_dynamic; - dep.maybe_code = maybe_specifier; - // If there is a `@deno-types` pragma, we will add it to the dependency - // if one doesn't already exist. - if maybe_type.is_some() && dep.maybe_type.is_none() { - dep.maybe_type = maybe_type; - } - } - Ok(parsed_module) - } - - 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() - { - let import_map = import_map.lock(); - Some(import_map.resolve(specifier, self.specifier.as_str())?) - } else { - None - }; - let mut remapped_import = false; - let specifier = if let Some(module_specifier) = maybe_resolve { - remapped_import = true; - module_specifier - } else { - deno_core::resolve_import(specifier, self.specifier.as_str())? - }; - - let referrer_scheme = self.specifier.scheme(); - let specifier_scheme = specifier.scheme(); - let location = maybe_location.unwrap_or(Location { - specifier: self.specifier.to_string(), - line: 0, - col: 0, - }); - - // Disallow downgrades from HTTPS to HTTP - if referrer_scheme == "https" && specifier_scheme == "http" { - return Err( - GraphError::InvalidDowngrade(specifier.clone(), location).into(), - ); - } - - // Disallow a remote URL from trying to import a local URL, unless it is a - // remapped import via the import map - if (referrer_scheme == "https" || referrer_scheme == "http") - && !(specifier_scheme == "https" || specifier_scheme == "http") - && !remapped_import - { - return Err( - GraphError::InvalidLocalImport(specifier.clone(), location).into(), - ); - } - - Ok(specifier) - } - - pub fn set_emit(&mut self, code: String, maybe_map: Option<String>) { - self.maybe_emit = Some(Emit::Cli((code, maybe_map))); - } - - /// 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.text_info.text_str(), - &version::deno(), - config, - )) - } - - pub fn size(&self) -> usize { - self.text_info.text_str().len() - } -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Stats(pub Vec<(String, u32)>); - -impl<'de> Deserialize<'de> for Stats { - fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let items: Vec<(String, u32)> = Deserialize::deserialize(deserializer)?; - Ok(Stats(items)) - } -} - -impl Serialize for Stats { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - Serialize::serialize(&self.0, serializer) - } -} - -impl fmt::Display for Stats { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Compilation statistics:")?; - for (key, value) in self.0.clone() { - writeln!(f, " {}: {}", key, value)?; - } - - Ok(()) - } -} - -/// A structure that provides information about a module graph result. -#[derive(Debug, Default)] -pub struct ResultInfo { - /// A structure which provides diagnostic information (usually from `tsc`) - /// about the code in the module graph. - pub diagnostics: Diagnostics, - /// A map of specifiers to the result of their resolution in the module graph. - pub loadable_modules: - HashMap<ModuleSpecifier, Result<ModuleSource, AnyError>>, - /// Optionally ignored compiler options that represent any options that were - /// ignored if there was a user provided configuration. - pub maybe_ignored_options: Option<IgnoredCompilerOptions>, - /// A structure providing key metrics around the operation performed, in - /// milliseconds. - pub stats: Stats, -} - -/// Represents the "default" type library that should be used when type -/// checking the code in the module graph. Note that a user provided config -/// of `"lib"` would override this value. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum TypeLib { - DenoWindow, - DenoWorker, - UnstableDenoWindow, - UnstableDenoWorker, -} - -impl Default for TypeLib { - fn default() -> Self { - TypeLib::DenoWindow - } -} - -impl Serialize for TypeLib { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - let value = match self { - TypeLib::DenoWindow => vec!["deno.window".to_string()], - TypeLib::DenoWorker => vec!["deno.worker".to_string()], - TypeLib::UnstableDenoWindow => { - vec!["deno.window".to_string(), "deno.unstable".to_string()] - } - TypeLib::UnstableDenoWorker => { - vec!["deno.worker".to_string(), "deno.unstable".to_string()] - } - }; - Serialize::serialize(&value, serializer) - } -} - -#[derive(Debug, Default)] -pub struct BundleOptions { - /// If `true` then debug logging will be output from the isolate. - pub debug: bool, - /// An optional config file with user supplied TypeScript configuration - /// that augments the the default configuration passed to the TypeScript - /// compiler. - pub maybe_config_file: Option<ConfigFile>, -} - -#[derive(Debug, Default)] -pub struct CheckOptions { - /// If `true` then debug logging will be output from the isolate. - pub debug: bool, - /// Utilise the emit from `tsc` to update the emitted code for modules. - pub emit: bool, - /// The base type libraries that should be used when type checking. - pub lib: TypeLib, - /// An optional config file with user supplied TypeScript configuration - /// that augments the the default configuration passed to the TypeScript - /// compiler. - pub maybe_config_file: Option<ConfigFile>, - /// Ignore any previously emits and ensure that all files are emitted from - /// source. - pub reload: bool, - /// A set of module specifiers to be excluded from the effect of - /// `CheckOptions::reload` if it is `true`. Perhaps because they have already - /// reloaded once in this process. - pub reload_exclusions: HashSet<ModuleSpecifier>, -} - -#[derive(Debug, Eq, PartialEq)] -pub enum BundleType { - /// Return the emitted contents of the program as a single "flattened" ES - /// module. - Module, - /// Return the emitted contents of the program as a single script that - /// executes the program using an immediately invoked function execution - /// (IIFE). - Classic, - /// Do not bundle the emit, instead returning each of the modules that are - /// part of the program as individual files. - None, -} - -impl Default for BundleType { - fn default() -> Self { - BundleType::None - } -} - -#[derive(Debug, Default)] -pub struct EmitOptions { - /// If true, then code will be type checked, otherwise type checking will be - /// skipped. If false, then swc will be used for the emit, otherwise tsc will - /// be used. - pub check: bool, - /// Indicate the form the result of the emit should take. - pub bundle_type: BundleType, - /// If `true` then debug logging will be output from the isolate. - pub debug: bool, - /// An optional map that contains user supplied TypeScript compiler - /// configuration options that are passed to the TypeScript compiler. - pub maybe_user_config: Option<HashMap<String, Value>>, -} - -/// 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 config file with user supplied TypeScript configuration - /// that augments the the default configuration passed to the TypeScript - /// compiler. - pub maybe_config_file: Option<ConfigFile>, - /// Ignore any previously emits and ensure that all files are emitted from - /// source. - pub reload: bool, - /// A set of module specifiers to be excluded from the effect of - /// `CheckOptions::reload` if it is `true`. Perhaps because they have already - /// reloaded once in this process. - pub reload_exclusions: HashSet<ModuleSpecifier>, -} - -#[derive(Debug, Clone)] -enum ModuleSlot { - /// The module fetch resulted in a non-recoverable error. - Err(Arc<AnyError>), - /// The the fetch resulted in a module. - Module(Box<Module>), - /// Used to denote a module that isn't part of the graph. - None, - /// The fetch of the module is pending. - Pending, -} - -/// 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, Clone)] -pub struct Graph { - /// A reference to the specifier handler that will retrieve and cache modules - /// for the graph. - handler: Arc<Mutex<dyn SpecifierHandler>>, - /// Optional TypeScript build info that will be passed to `tsc` if `tsc` is - /// invoked. - maybe_tsbuildinfo: Option<String>, - /// The modules that are part of the graph. - modules: HashMap<ModuleSpecifier, ModuleSlot>, - /// A map of redirects, where a module specifier is redirected to another - /// module specifier by the handler. All modules references should be - /// resolved internally via this, before attempting to access the module via - /// the handler, to make sure the correct modules is being dealt with. - redirects: HashMap<ModuleSpecifier, ModuleSpecifier>, - /// The module specifiers that have been uniquely added to the graph, which - /// does not include any transient dependencies. - roots: Vec<ModuleSpecifier>, - /// If all of the root modules are dynamically imported, then this is true. - /// This is used to ensure correct `--reload` behavior, where subsequent - /// calls to a module graph where the emit is already valid do not cause the - /// graph to re-emit. - roots_dynamic: bool, - // A reference to lock file that will be used to check module integrity. - maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, -} - -/// Convert a specifier and a module slot in a result to the module source which -/// is needed by Deno core for loading the module. -fn to_module_result( - (specifier, module_slot): (&ModuleSpecifier, &ModuleSlot), -) -> (ModuleSpecifier, Result<ModuleSource, AnyError>) { - match module_slot { - ModuleSlot::Err(err) => (specifier.clone(), Err(anyhow!(err.to_string()))), - ModuleSlot::Module(module) => ( - specifier.clone(), - if let Some(emit) = &module.maybe_emit { - match emit { - Emit::Cli((code, _)) => Ok(ModuleSource { - code: code.clone(), - module_url_found: module.specifier.to_string(), - module_url_specified: specifier.to_string(), - }), - } - } else { - match module.media_type { - MediaType::JavaScript | MediaType::Unknown => Ok(ModuleSource { - code: module.text_info.text_str().to_string(), - module_url_found: module.specifier.to_string(), - module_url_specified: specifier.to_string(), - }), - _ => Err(custom_error( - "NotFound", - format!("Compiled module not found \"{}\"", specifier), - )), - } - }, - ), - _ => ( - specifier.clone(), - Err(anyhow!("Module \"{}\" unavailable.", specifier)), - ), - } -} - -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: Arc<Mutex<dyn SpecifierHandler>>, - maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, - ) -> Self { - Graph { - handler, - maybe_tsbuildinfo: None, - modules: HashMap::new(), - redirects: HashMap::new(), - roots: Vec::new(), - roots_dynamic: true, - maybe_lockfile, - } - } - - /// Transform the module graph into a single JavaScript module which is - /// returned as a `String` in the result. - pub fn bundle( - &self, - options: BundleOptions, - ) -> Result<(String, Stats, Option<IgnoredCompilerOptions>), AnyError> { - if self.roots.is_empty() || self.roots.len() > 1 { - return Err(GraphError::NotSupported(format!("Bundling is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); - } - - let start = Instant::now(); - let root_specifier = self.roots[0].clone(); - let mut ts_config = TsConfig::new(json!({ - "checkJs": false, - "emitDecoratorMetadata": false, - "importsNotUsedAsValues": "remove", - "inlineSourceMap": false, - "inlineSources": false, - "sourceMap": false, - "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - })); - let maybe_ignored_options = ts_config - .merge_tsconfig_from_config_file(options.maybe_config_file.as_ref())?; - - let (src, _) = self.emit_bundle( - &root_specifier, - &ts_config.into(), - &BundleType::Module, - )?; - let stats = Stats(vec![ - ("Files".to_string(), self.modules.len() as u32), - ("Total time".to_string(), start.elapsed().as_millis() as u32), - ]); - - Ok((src, stats, maybe_ignored_options)) - } - - /// Type check the module graph, corresponding to the options provided. - pub fn check(self, options: CheckOptions) -> Result<ResultInfo, AnyError> { - self.validate()?; - let mut config = TsConfig::new(json!({ - "allowJs": true, - // TODO(@kitsonk) is this really needed? - "esModuleInterop": true, - // Enabled by default to align to transpile/swc defaults - "experimentalDecorators": true, - "incremental": true, - "jsx": "react", - "isolatedModules": true, - "lib": options.lib, - "module": "esnext", - "strict": true, - "target": "esnext", - "tsBuildInfoFile": "deno:///.tsbuildinfo", - "useDefineForClassFields": true, - // TODO(@kitsonk) remove for Deno 1.15 - "useUnknownInCatchVariables": false, - })); - if options.emit { - config.merge(&json!({ - // TODO(@kitsonk) consider enabling this by default - // see: https://github.com/denoland/deno/issues/7732 - "emitDecoratorMetadata": false, - "importsNotUsedAsValues": "remove", - "inlineSourceMap": true, - "inlineSources": true, - "outDir": "deno://", - "removeComments": true, - })); - } else { - config.merge(&json!({ - "noEmit": true, - })); - } - let maybe_ignored_options = config - .merge_tsconfig_from_config_file(options.maybe_config_file.as_ref())?; - - let needs_reload = options.reload - && !self - .roots - .iter() - .all(|u| options.reload_exclusions.contains(u)); - // Short circuit if none of the modules require an emit, or all of the - // modules that require an emit have a valid emit. - if !self.needs_emit(&config) || self.is_emit_valid(&config) && !needs_reload - { - debug!("graph does not need to be checked or emitted."); - return Ok(ResultInfo { - maybe_ignored_options, - loadable_modules: self.get_loadable_modules(), - ..Default::default() - }); - } - - // TODO(@kitsonk) not totally happy with this here, but this is the first - // point where we know we are actually going to check the program. If we - // moved it out of here, we wouldn't know until after the check has already - // happened, which isn't informative to the users. - for specifier in &self.roots { - log::info!("{} {}", colors::green("Check"), specifier); - } - - let root_names = self.get_root_names(!config.get_check_js())?; - let maybe_tsbuildinfo = self.maybe_tsbuildinfo.clone(); - let hash_data = - vec![config.as_bytes(), version::deno().as_bytes().to_owned()]; - let graph = Arc::new(Mutex::new(self)); - - let maybe_config_specifier = - if let Some(config_file) = &options.maybe_config_file { - ModuleSpecifier::from_file_path(&config_file.path).ok() - } else { - None - }; - debug!("maybe_config_specifier: {:?}", maybe_config_specifier); - - let response = tsc::exec(tsc::Request { - config: config.clone(), - debug: options.debug, - graph: graph.clone(), - hash_data, - maybe_config_specifier, - maybe_tsbuildinfo, - root_names, - })?; - - let mut graph = graph.lock(); - graph.maybe_tsbuildinfo = response.maybe_tsbuildinfo; - // Only process changes to the graph if there are no diagnostics and there - // were files emitted. - if response.diagnostics.is_empty() { - if !response.emitted_files.is_empty() { - let mut codes = HashMap::new(); - let mut maps = HashMap::new(); - let check_js = config.get_check_js(); - for emit in &response.emitted_files { - if let Some(specifiers) = &emit.maybe_specifiers { - assert!(specifiers.len() == 1, "Unexpected specifier length"); - // The specifier emitted might not be the redirected specifier, and - // therefore we need to ensure it is the correct one. - let specifier = graph.resolve_specifier(&specifiers[0]); - // Sometimes if tsc sees a CommonJS file it will _helpfully_ output it - // to ESM, which we don't really want unless someone has enabled the - // check_js option. - if !check_js - && graph.get_media_type(specifier) == Some(MediaType::JavaScript) - { - debug!("skipping emit for {}", specifier); - continue; - } - match emit.media_type { - MediaType::JavaScript => { - codes.insert(specifier.clone(), emit.data.clone()); - } - MediaType::SourceMap => { - maps.insert(specifier.clone(), emit.data.clone()); - } - _ => unreachable!(), - } - } - } - let config = config.as_bytes(); - for (specifier, code) in codes.iter() { - if let ModuleSlot::Module(module) = - graph.get_module_mut(specifier).unwrap() - { - module.set_emit(code.clone(), maps.get(specifier).cloned()); - module.set_version(&config); - module.is_dirty = true; - } else { - return Err(GraphError::MissingSpecifier(specifier.clone()).into()); - } - } - } - graph.flush()?; - } - - Ok(ResultInfo { - diagnostics: response.diagnostics, - loadable_modules: graph.get_loadable_modules(), - maybe_ignored_options, - stats: response.stats, - }) - } - - /// Indicates if the module graph contains the supplied specifier or not. - pub fn contains(&self, specifier: &ModuleSpecifier) -> bool { - matches!(self.get_module(specifier), ModuleSlot::Module(_)) - } - - /// Emit the module graph in a specific format. This is specifically designed - /// to be an "all-in-one" API for access by the runtime, allowing both - /// emitting single modules as well as bundles, using Deno module resolution - /// or supplied sources. - pub fn emit( - mut self, - options: EmitOptions, - ) -> Result<(HashMap<String, String>, ResultInfo), AnyError> { - let mut config = TsConfig::new(json!({ - "allowJs": true, - "checkJs": false, - // TODO(@kitsonk) consider enabling this by default - // see: https://github.com/denoland/deno/issues/7732 - "emitDecoratorMetadata": false, - "esModuleInterop": true, - "experimentalDecorators": true, - "importsNotUsedAsValues": "remove", - "inlineSourceMap": false, - "inlineSources": false, - "sourceMap": false, - "isolatedModules": true, - "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - "lib": TypeLib::DenoWindow, - "module": "esnext", - "strict": true, - "target": "esnext", - "useDefineForClassFields": true, - // TODO(@kitsonk) remove for Deno 1.15 - "useUnknownInCatchVariables": false, - })); - let opts = match options.bundle_type { - BundleType::Module | BundleType::Classic => json!({ - "noEmit": true, - "removeComments": true, - "sourceMap": true, - }), - BundleType::None => json!({ - "outDir": "deno://", - "removeComments": true, - "sourceMap": true, - }), - }; - config.merge(&opts); - let maybe_ignored_options = - if let Some(user_options) = &options.maybe_user_config { - config.merge_user_config(user_options)? - } else { - None - }; - - if !options.check && config.get_declaration() { - return Err(anyhow!("The option of `check` is false, but the compiler option of `declaration` is true which is not currently supported.")); - } - if options.bundle_type != BundleType::None && config.get_declaration() { - return Err(anyhow!("The bundle option is set, but the compiler option of `declaration` is true which is not currently supported.")); - } - - let mut emitted_files = HashMap::new(); - if options.check { - let root_names = self.get_root_names(!config.get_check_js())?; - let hash_data = - vec![config.as_bytes(), version::deno().as_bytes().to_owned()]; - let graph = Arc::new(Mutex::new(self)); - let response = tsc::exec(tsc::Request { - config: config.clone(), - debug: options.debug, - graph: graph.clone(), - hash_data, - maybe_config_specifier: None, - maybe_tsbuildinfo: None, - root_names, - })?; - - let graph = graph.lock(); - match options.bundle_type { - BundleType::Module | BundleType::Classic => { - assert!( - response.emitted_files.is_empty(), - "No files should have been emitted from tsc." - ); - assert_eq!( - graph.roots.len(), - 1, - "Only a single root module supported." - ); - let specifier = &graph.roots[0]; - let (src, maybe_src_map) = graph.emit_bundle( - specifier, - &config.into(), - &options.bundle_type, - )?; - emitted_files.insert("deno:///bundle.js".to_string(), src); - if let Some(src_map) = maybe_src_map { - emitted_files.insert("deno:///bundle.js.map".to_string(), src_map); - } - } - BundleType::None => { - for emitted_file in &response.emitted_files { - assert!( - emitted_file.maybe_specifiers.is_some(), - "Orphaned file emitted." - ); - let specifiers = emitted_file.maybe_specifiers.clone().unwrap(); - assert_eq!( - specifiers.len(), - 1, - "An unexpected number of specifiers associated with emitted file." - ); - let specifier = specifiers[0].clone(); - let extension = match emitted_file.media_type { - MediaType::JavaScript => ".js", - MediaType::SourceMap => ".js.map", - MediaType::Dts => ".d.ts", - _ => unreachable!(), - }; - let key = format!("{}{}", specifier, extension); - emitted_files.insert(key, emitted_file.data.clone()); - } - } - }; - - Ok(( - emitted_files, - ResultInfo { - diagnostics: response.diagnostics, - loadable_modules: graph.get_loadable_modules(), - maybe_ignored_options, - stats: response.stats, - }, - )) - } else { - let start = Instant::now(); - let mut emit_count = 0_u32; - match options.bundle_type { - BundleType::Module | BundleType::Classic => { - assert_eq!( - self.roots.len(), - 1, - "Only a single root module supported." - ); - let specifier = &self.roots[0]; - let (src, maybe_src_map) = self.emit_bundle( - specifier, - &config.into(), - &options.bundle_type, - )?; - emit_count += 1; - emitted_files.insert("deno:///bundle.js".to_string(), src); - if let Some(src_map) = maybe_src_map { - emitted_files.insert("deno:///bundle.js.map".to_string(), src_map); - } - } - BundleType::None => { - let check_js = config.get_check_js(); - let emit_options: ast::EmitOptions = config.into(); - for (_, module_slot) in self.modules.iter_mut() { - if let ModuleSlot::Module(module) = module_slot { - if !(check_js - || module.media_type == MediaType::Jsx - || module.media_type == MediaType::Tsx - || module.media_type == MediaType::TypeScript) - { - emitted_files.insert( - module.specifier.to_string(), - module.text_info.text_str().to_string(), - ); - } - let parsed_module = module.parse()?; - let (code, maybe_map) = transpile(&parsed_module, &emit_options)?; - emit_count += 1; - emitted_files.insert(format!("{}.js", module.specifier), code); - if let Some(map) = maybe_map { - emitted_files - .insert(format!("{}.js.map", module.specifier), map); - } - } - } - self.flush()?; - } - } - - let stats = Stats(vec![ - ("Files".to_string(), self.modules.len() as u32), - ("Emitted".to_string(), emit_count), - ("Total time".to_string(), start.elapsed().as_millis() as u32), - ]); - - Ok(( - emitted_files, - ResultInfo { - diagnostics: Default::default(), - loadable_modules: self.get_loadable_modules(), - maybe_ignored_options, - stats, - }, - )) - } - } - - /// Shared between `bundle()` and `emit()`. - fn emit_bundle( - &self, - specifier: &ModuleSpecifier, - emit_options: &ast::EmitOptions, - bundle_type: &BundleType, - ) -> Result<(String, Option<String>), AnyError> { - let cm = Rc::new(deno_ast::swc::common::SourceMap::new( - deno_ast::swc::common::FilePathMapping::empty(), - )); - let globals = deno_ast::swc::common::Globals::new(); - let loader = BundleLoader::new(self, emit_options, &globals, cm.clone()); - let hook = Box::new(BundleHook); - let module = match bundle_type { - BundleType::Module => deno_ast::swc::bundler::ModuleType::Es, - BundleType::Classic => deno_ast::swc::bundler::ModuleType::Iife, - _ => unreachable!("invalid bundle type"), - }; - let bundler = deno_ast::swc::bundler::Bundler::new( - &globals, - cm.clone(), - loader, - self, - deno_ast::swc::bundler::Config { - module, - ..Default::default() - }, - hook, - ); - let mut entries = HashMap::new(); - entries.insert( - "bundle".to_string(), - deno_ast::swc::common::FileName::Url(specifier.clone()), - ); - let output = bundler - .bundle(entries) - .context("Unable to output bundle during Graph::bundle().")?; - let mut buf = Vec::new(); - let mut src_map_buf = Vec::new(); - { - let mut emitter = deno_ast::swc::codegen::Emitter { - cfg: deno_ast::swc::codegen::Config { minify: false }, - cm: cm.clone(), - comments: None, - wr: Box::new(deno_ast::swc::codegen::text_writer::JsWriter::new( - cm.clone(), - "\n", - &mut buf, - Some(&mut src_map_buf), - )), - }; - - emitter - .emit_module(&output[0].module) - .context("Unable to emit bundle during Graph::bundle().")?; - } - let mut src = String::from_utf8(buf) - .context("Emitted bundle is an invalid utf-8 string.")?; - let mut map: Option<String> = None; - { - let mut buf = Vec::new(); - cm.build_source_map_from(&mut src_map_buf, None) - .to_writer(&mut buf)?; - - if emit_options.inline_source_map { - src.push_str("//# sourceMappingURL=data:application/json;base64,"); - let encoded_map = base64::encode(buf); - src.push_str(&encoded_map); - } else if emit_options.source_map { - map = Some(String::from_utf8(buf)?); - } - } - - Ok((src, map)) - } - - /// Update the handler with any modules that are marked as _dirty_ and update - /// any build info if present. - fn flush(&mut self) -> Result<(), AnyError> { - let mut handler = self.handler.lock(); - for (_, module_slot) in self.modules.iter_mut() { - if let ModuleSlot::Module(module) = module_slot { - if module.is_dirty { - if let Some(emit) = &module.maybe_emit { - handler.set_cache(&module.specifier, emit)?; - } - if let Some(version) = &module.maybe_version { - handler.set_version(&module.specifier, version.clone())?; - } - module.is_dirty = false; - } - } - } - for root_specifier in self.roots.iter() { - if let Some(tsbuildinfo) = &self.maybe_tsbuildinfo { - handler.set_tsbuildinfo(root_specifier, tsbuildinfo.to_owned())?; - } - } - - Ok(()) - } - - /// Retrieve the first module loading error from the graph and return it. - pub fn get_errors(&self) -> HashMap<ModuleSpecifier, String> { - self - .modules - .iter() - .filter_map(|(s, sl)| match sl { - ModuleSlot::Err(err) => Some((s.clone(), err.to_string())), - _ => None, - }) - .collect() - } - - /// Retrieve a map that contains a representation of each module in the graph - /// which can be used to provide code to a module loader without holding all - /// the state to be able to operate on the graph. - pub fn get_loadable_modules( - &self, - ) -> HashMap<ModuleSpecifier, Result<ModuleSource, AnyError>> { - let mut loadable_modules: HashMap< - ModuleSpecifier, - Result<ModuleSource, AnyError>, - > = self.modules.iter().map(to_module_result).collect(); - for (specifier, _) in self.redirects.iter() { - if let Some(module_slot) = - self.modules.get(self.resolve_specifier(specifier)) - { - let (_, result) = to_module_result((specifier, module_slot)); - loadable_modules.insert(specifier.clone(), result); - } - } - loadable_modules - } - - pub fn get_media_type( - &self, - specifier: &ModuleSpecifier, - ) -> Option<MediaType> { - if let ModuleSlot::Module(module) = self.get_module(specifier) { - Some(module.media_type) - } else { - None - } - } - - fn get_module(&self, specifier: &ModuleSpecifier) -> &ModuleSlot { - let s = self.resolve_specifier(specifier); - if let Some(module_slot) = self.modules.get(s) { - module_slot - } else { - &ModuleSlot::None - } - } - - fn get_module_mut( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option<&mut ModuleSlot> { - // this is duplicated code because `.resolve_specifier` requires an - // immutable borrow, but if `.resolve_specifier` is mut, then everything - // that calls it is is mut - let mut s = specifier; - while let Some(redirect) = self.redirects.get(s) { - s = redirect; - } - self.modules.get_mut(s) - } - - pub fn get_specifier( - &self, - specifier: &ModuleSpecifier, - ) -> Result<&Module, AnyError> { - let s = self.resolve_specifier(specifier); - match self.get_module(s) { - ModuleSlot::Module(m) => Ok(m.as_ref()), - ModuleSlot::Err(e) => Err(anyhow!(e.to_string())), - _ => Err(GraphError::MissingSpecifier(specifier.clone()).into()), - } - } - - /// Consume graph and return list of all module specifiers contained in the - /// graph. - pub fn get_modules(&self) -> Vec<ModuleSpecifier> { - self.modules.keys().map(|s| s.to_owned()).collect() - } - - /// Transform `self.roots` into something that works for `tsc`, because `tsc` - /// doesn't like root names without extensions that match its expectations, - /// nor does it have any concept of redirection, so we have to resolve all - /// that upfront before feeding it to `tsc`. In addition, if checkJs is not - /// true, we should pass all emittable files in as the roots, so that `tsc` - /// type checks them and potentially emits them. - fn get_root_names( - &self, - include_emittable: bool, - ) -> Result<Vec<(ModuleSpecifier, MediaType)>, AnyError> { - let root_names: Vec<ModuleSpecifier> = if include_emittable { - // in situations where there is `allowJs` with tsc, but not `checkJs`, - // then tsc will not parse the whole module graph, meaning that any - // JavaScript importing TypeScript will get ignored, meaning that those - // files will not get emitted. To counter act that behavior, we will - // include all modules that are emittable. - let mut specifiers = HashSet::<&ModuleSpecifier>::new(); - for (_, module_slot) in self.modules.iter() { - if let ModuleSlot::Module(module) = module_slot { - if module.media_type == MediaType::Jsx - || module.media_type == MediaType::TypeScript - || module.media_type == MediaType::Tsx - { - specifiers.insert(&module.specifier); - } - } - } - // We should include all the original roots as well. - for specifier in self.roots.iter() { - specifiers.insert(specifier); - } - specifiers.into_iter().cloned().collect() - } else { - self.roots.clone() - }; - let mut root_types = vec![]; - for ms in root_names { - // if the root module has a types specifier, we should be sending that - // to tsc instead of the original specifier - let specifier = self.resolve_specifier(&ms); - let module = match self.get_module(specifier) { - ModuleSlot::Module(module) => module, - ModuleSlot::Err(error) => { - // It would be great if we could just clone the error here... - if let Some(class) = get_custom_error_class(error) { - return Err(custom_error(class, error.to_string())); - } else { - panic!("unsupported ModuleSlot error"); - } - } - _ => { - panic!("missing module"); - } - }; - let specifier = if let Some((_, types_specifier)) = &module.maybe_types { - self.resolve_specifier(types_specifier) - } else { - specifier - }; - root_types.push(( - // root modules can be redirects, so before we pass it to tsc we need - // to resolve the redirect - specifier.clone(), - self.get_media_type(specifier).unwrap(), - )); - } - Ok(root_types) - } - - /// Get the source for a given module specifier. If the module is not part - /// of the graph, the result will be `None`. - pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option<Arc<String>> { - if let ModuleSlot::Module(module) = self.get_module(specifier) { - Some(module.text_info.text()) - } else { - None - } - } - - /// Return a structure which provides information about the module graph and - /// the relationship of the modules in the graph. This structure is used to - /// provide information for the `info` subcommand. - pub fn info(&self) -> Result<info::ModuleGraphInfo, AnyError> { - if self.roots.is_empty() || self.roots.len() > 1 { - return Err(GraphError::NotSupported(format!("Info is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); - } - - let root = self.resolve_specifier(&self.roots[0]).clone(); - let mut modules: Vec<info::ModuleGraphInfoMod> = self - .modules - .iter() - .filter_map(|(sp, sl)| match sl { - ModuleSlot::Module(module) => { - let mut dependencies: Vec<info::ModuleGraphInfoDep> = module - .dependencies - .iter() - .map(|(k, v)| info::ModuleGraphInfoDep { - specifier: k.clone(), - is_dynamic: v.is_dynamic, - maybe_code: v - .maybe_code - .clone() - .map(|s| self.resolve_specifier(&s).clone()), - maybe_type: v - .maybe_type - .clone() - .map(|s| self.resolve_specifier(&s).clone()), - }) - .collect(); - dependencies.sort(); - let (emit, map) = - if let Some((emit, maybe_map)) = &module.maybe_emit_path { - (Some(emit.clone()), maybe_map.clone()) - } else { - (None, None) - }; - let maybe_type_dependency = - module.maybe_types.clone().map(|(specifier, _type)| { - info::ModuleGraphInfoDep { - specifier, - is_dynamic: false, - maybe_code: None, - maybe_type: Some(_type), - } - }); - Some(info::ModuleGraphInfoMod { - specifier: sp.clone(), - dependencies, - maybe_type_dependency, - size: Some(module.size()), - media_type: Some(module.media_type), - local: Some(module.source_path.clone()), - checksum: Some(checksum::gen(&[module - .text_info - .text_str() - .as_bytes()])), - emit, - map, - ..Default::default() - }) - } - ModuleSlot::Err(err) => Some(info::ModuleGraphInfoMod { - specifier: sp.clone(), - error: Some(err.to_string()), - ..Default::default() - }), - _ => None, - }) - .collect(); - - modules.sort(); - - let size = modules.iter().fold(0_usize, |acc, m| { - if let Some(size) = &m.size { - acc + size - } else { - acc - } - }); - - Ok(info::ModuleGraphInfo { - root, - modules, - size, - }) - } - - /// Determines if all of the modules in the graph that require an emit have - /// a valid emit. Returns `true` if all the modules have a valid emit, - /// otherwise false. - fn is_emit_valid(&self, config: &TsConfig) -> bool { - let check_js = config.get_check_js(); - let config = config.as_bytes(); - self.modules.iter().all(|(_, m)| { - if let ModuleSlot::Module(m) = m { - let needs_emit = match m.media_type { - MediaType::TypeScript | MediaType::Tsx | MediaType::Jsx => true, - MediaType::JavaScript => check_js, - _ => false, - }; - if needs_emit { - m.is_emit_valid(&config) - } else { - true - } - } else { - true - } - }) - } - - /// 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) { - if let Some(lf) = self.maybe_lockfile.as_ref() { - let mut lockfile = lf.lock(); - for (ms, module_slot) in self.modules.iter() { - if let ModuleSlot::Module(module) = module_slot { - let specifier = module.specifier.to_string(); - let valid = - lockfile.check_or_insert(&specifier, module.text_info.text_str()); - if !valid { - eprintln!( - "{}", - GraphError::InvalidSource(ms.clone(), lockfile.filename.clone()) - ); - std::process::exit(10); - } - } - } - } - } - - /// Determines if any of the modules in the graph are required to be emitted. - /// This is similar to `emit_valid()` except that the actual emit isn't - /// checked to determine if it is valid. - fn needs_emit(&self, config: &TsConfig) -> bool { - let check_js = config.get_check_js(); - self.modules.iter().any(|(_, m)| match m { - ModuleSlot::Module(m) => match m.media_type { - MediaType::TypeScript | MediaType::Tsx | MediaType::Jsx => true, - MediaType::JavaScript => check_js, - _ => false, - }, - _ => false, - }) - } - - /// 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. - /// - /// # Arguments - /// - /// * `specifier` - The string form of the module specifier that needs to be - /// resolved. - /// * `referrer` - The referring `ModuleSpecifier`. - /// * `prefer_types` - When resolving to a module specifier, determine if a - /// type dependency is preferred over a code dependency. This is set to - /// `true` when resolving module names for `tsc` as it needs the type - /// dependency over the code, while other consumers do not handle type only - /// dependencies. - pub fn resolve( - &self, - specifier: &str, - referrer: &ModuleSpecifier, - prefer_types: bool, - ) -> Result<ModuleSpecifier, AnyError> { - let module = if let ModuleSlot::Module(module) = self.get_module(referrer) { - module - } else { - return Err(GraphError::MissingSpecifier(referrer.clone()).into()); - }; - if !module.dependencies.contains_key(specifier) { - return Err( - GraphError::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 prefer_types && dependency.maybe_type.is_some() - { - dependency.maybe_type.clone().unwrap() - } else if let Some(code_specifier) = dependency.maybe_code.clone() { - code_specifier - } else { - return Err( - GraphError::MissingDependency( - referrer.to_owned(), - specifier.to_owned(), - ) - .into(), - ); - }; - let dep_module = if let ModuleSlot::Module(dep_module) = - self.get_module(&resolved_specifier) - { - dep_module - } else { - return Err( - GraphError::MissingDependency( - referrer.to_owned(), - resolved_specifier.to_string(), - ) - .into(), - ); - }; - // 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 prefer_types && dep_module.maybe_types.is_some() { - let (_, types) = dep_module.maybe_types.clone().unwrap(); - // It is possible that `types` points to a redirected specifier, so we - // need to ensure it resolves to the final specifier in the graph. - self.resolve_specifier(&types).clone() - } else { - dep_module.specifier.clone() - }; - - Ok(result) - } - - /// Takes a module specifier and returns the "final" specifier, accounting for - /// any redirects that may have occurred. - fn resolve_specifier<'a>( - &'a self, - specifier: &'a ModuleSpecifier, - ) -> &'a ModuleSpecifier { - let mut s = specifier; - let mut seen = HashSet::new(); - seen.insert(s.clone()); - while let Some(redirect) = self.redirects.get(s) { - if !seen.insert(redirect.clone()) { - eprintln!("An infinite loop of module redirections detected.\n Original specifier: {}", specifier); - break; - } - s = redirect; - if seen.len() > 5 { - eprintln!("An excessive number of module redirections detected.\n Original specifier: {}", specifier); - break; - } - } - s - } - - /// 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<ResultInfo, AnyError> { - let start = Instant::now(); - - let mut ts_config = TsConfig::new(json!({ - "checkJs": false, - "emitDecoratorMetadata": false, - "importsNotUsedAsValues": "remove", - "inlineSourceMap": true, - // TODO(@kitsonk) make this actually work when https://github.com/swc-project/swc/issues/2218 addressed. - "inlineSources": true, - "sourceMap": false, - "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - })); - - let maybe_ignored_options = ts_config - .merge_tsconfig_from_config_file(options.maybe_config_file.as_ref())?; - - let config = ts_config.as_bytes(); - let check_js = ts_config.get_check_js(); - let emit_options: ast::EmitOptions = ts_config.into(); - let mut emit_count = 0_u32; - for (specifier, module_slot) in self.modules.iter_mut() { - if let ModuleSlot::Module(module) = module_slot { - // 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 or JSX - // modules - if !(check_js - || module.media_type == MediaType::Jsx - || module.media_type == MediaType::Tsx - || module.media_type == MediaType::TypeScript) - { - continue; - } - - let needs_reload = - options.reload && !options.reload_exclusions.contains(specifier); - // skip modules that already have a valid emit - if module.is_emit_valid(&config) && !needs_reload { - continue; - } - let parsed_module = module.parse()?; - let emit = transpile(&parsed_module, &emit_options)?; - emit_count += 1; - module.maybe_emit = Some(Emit::Cli(emit)); - module.set_version(&config); - module.is_dirty = true; - } - } - self.flush()?; - - let stats = Stats(vec![ - ("Files".to_string(), self.modules.len() as u32), - ("Emitted".to_string(), emit_count), - ("Total time".to_string(), start.elapsed().as_millis() as u32), - ]); - - Ok(ResultInfo { - diagnostics: Default::default(), - loadable_modules: self.get_loadable_modules(), - maybe_ignored_options, - stats, - }) - } - - /// Validate that the module graph is "valid" in that there are not module - /// slots that have errorred that should be available to be able to statically - /// analyze. In certain situations, we can spin up tsc with an "invalid" - /// graph. - fn validate(&self) -> Result<(), AnyError> { - fn validate_module<F>( - specifier: &ModuleSpecifier, - seen: &mut HashSet<ModuleSpecifier>, - get_module: &F, - ) -> Result<(), AnyError> - where - F: Fn(&ModuleSpecifier) -> ModuleSlot, - { - if seen.contains(specifier) { - return Ok(()); - } - seen.insert(specifier.clone()); - match get_module(specifier) { - ModuleSlot::Err(err) => Err(anyhow!(err.to_string())), - ModuleSlot::Module(module) => { - for (_, dep) in module.dependencies.iter() { - // a dynamic import should be skipped, because while it might not - // be available to statically analyze, it might be available at - // runtime. - if !dep.is_dynamic { - if let Some(code_specifier) = &dep.maybe_code { - validate_module(code_specifier, seen, get_module)?; - } - if let Some(type_specifier) = &dep.maybe_type { - validate_module(type_specifier, seen, get_module)?; - } - } - } - Ok(()) - }, - ModuleSlot::None => Err(custom_error("NotFound", format!("The specifier \"{}\" is unexpectedly not in the module graph.", specifier))), - ModuleSlot::Pending => Err(custom_error("InvalidState", format!("The specifier \"{}\" is in an unexpected state in the module graph.", specifier))), - } - } - - let mut seen = HashSet::new(); - for specifier in &self.roots { - validate_module(specifier, &mut seen, &|s| self.get_module(s).clone())?; - } - Ok(()) - } -} - -impl deno_ast::swc::bundler::Resolve for Graph { - fn resolve( - &self, - referrer: &deno_ast::swc::common::FileName, - specifier: &str, - ) -> Result<deno_ast::swc::common::FileName, AnyError> { - let referrer = - if let deno_ast::swc::common::FileName::Url(referrer) = referrer { - referrer - } else { - unreachable!( - "An unexpected referrer was passed when bundling: {:?}", - referrer - ) - }; - let specifier = self.resolve(specifier, referrer, false)?; - - Ok(deno_ast::swc::common::FileName::Url(specifier)) - } -} - -/// A structure for building a dependency graph of modules. -pub struct GraphBuilder { - graph: Graph, - maybe_import_map: Option<Arc<Mutex<ImportMap>>>, - pending: FuturesUnordered<FetchFuture>, -} - -impl GraphBuilder { - pub fn new( - handler: Arc<Mutex<dyn SpecifierHandler>>, - maybe_import_map: Option<ImportMap>, - maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, - ) -> Self { - let internal_import_map = - maybe_import_map.map(|import_map| Arc::new(Mutex::new(import_map))); - GraphBuilder { - graph: Graph::new(handler, maybe_lockfile), - maybe_import_map: internal_import_map, - pending: FuturesUnordered::new(), - } - } - - /// Add 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 add( - &mut self, - specifier: &ModuleSpecifier, - is_dynamic: bool, - ) -> Result<(), AnyError> { - self.insert(specifier, is_dynamic).await?; - - if !self.graph.roots.contains(specifier) { - self.graph.roots.push(specifier.clone()); - self.graph.roots_dynamic = self.graph.roots_dynamic && is_dynamic; - if self.graph.maybe_tsbuildinfo.is_none() { - let handler = self.graph.handler.lock(); - self.graph.maybe_tsbuildinfo = handler.get_tsbuildinfo(specifier)?; - } - } - - Ok(()) - } - - /// Analyze compiler options, identifying any specifiers that need to be - /// resolved and added to the graph. - pub async fn analyze_compiler_options( - &mut self, - maybe_compiler_options: &Option<HashMap<String, Value>>, - ) -> Result<(), AnyError> { - if let Some(user_config) = maybe_compiler_options { - if let Some(value) = user_config.get("types") { - let types: Vec<String> = serde_json::from_value(value.clone())?; - for specifier in types { - if let Ok(specifier) = resolve_url_or_path(&specifier) { - self.insert(&specifier, false).await?; - } - } - } - } - Ok(()) - } - - /// Analyze a config file, identifying any specifiers that need to be resolved - /// and added to the graph. - pub async fn analyze_config_file( - &mut self, - maybe_config_file: &Option<ConfigFile>, - ) -> Result<(), AnyError> { - if let Some(config_file) = maybe_config_file { - let referrer = ModuleSpecifier::from_file_path(&config_file.path) - .map_err(|_| { - anyhow!("Could not convert file path: \"{:?}\"", config_file.path) - })?; - if let Some(compiler_options) = &config_file.json.compiler_options { - let compiler_options: CompilerOptions = - serde_json::from_value(compiler_options.clone())?; - if let Some(types) = compiler_options.types { - for specifier in types { - if let Ok(specifier) = - resolve_import(&specifier, &referrer.to_string()) - { - self.insert(&specifier, false).await?; - } - } - } - } - } - Ok(()) - } - - /// 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, - maybe_referrer: &Option<Location>, - is_dynamic: bool, - ) { - if !self.graph.modules.contains_key(specifier) { - self - .graph - .modules - .insert(specifier.clone(), ModuleSlot::Pending); - let mut handler = self.graph.handler.lock(); - let future = - handler.fetch(specifier.clone(), maybe_referrer.clone(), is_dynamic); - self.pending.push(future); - } - } - - /// An internal method that fetches the specifier and recursively fetches any - /// of the dependencies, adding them to the graph. - async fn insert( - &mut self, - specifier: &ModuleSpecifier, - is_dynamic: bool, - ) -> Result<(), AnyError> { - self.fetch(specifier, &None, is_dynamic); - - loop { - match self.pending.next().await { - Some(Err((specifier, err))) => { - self - .graph - .modules - .insert(specifier, ModuleSlot::Err(Arc::new(err))); - } - Some(Ok(cached_module)) => { - let is_root = &cached_module.specifier == specifier; - self.visit(cached_module, is_root, is_dynamic)?; - } - _ => {} - } - if self.pending.is_empty() { - break; - } - } - - 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, - is_root: bool, - is_root_dynamic: bool, - ) -> Result<(), AnyError> { - let specifier = cached_module.specifier.clone(); - let requested_specifier = cached_module.requested_specifier.clone(); - let mut module = - Module::new(cached_module, is_root, self.maybe_import_map.clone()); - match module.media_type { - MediaType::Json - | MediaType::SourceMap - | MediaType::TsBuildInfo - | MediaType::Unknown => { - return Err( - GraphError::UnsupportedImportType( - module.specifier, - module.media_type, - ) - .into(), - ); - } - _ => (), - } - 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.lock(); - 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() { - let maybe_referrer = Some(dep.location.clone()); - for maybe_specifier in &[dep.maybe_code.as_ref(), dep.maybe_type.as_ref()] - { - if let Some(&dep_specifier) = maybe_specifier.as_ref() { - if dep_specifier.scheme() == "bare" { - self.graph.modules.insert( - dep_specifier.clone(), - ModuleSlot::Err(Arc::new( - ModuleResolutionError::ImportPrefixMissing( - dep_specifier.path().to_string(), - Some(specifier.to_string()), - ) - .into(), - )), - ); - } else { - self.fetch( - dep_specifier, - &maybe_referrer, - is_root_dynamic || dep.is_dynamic, - ); - } - } - } - } - if let Some((_, specifier)) = module.maybe_types.as_ref() { - self.fetch(specifier, &None, is_root_dynamic); - } - if specifier != requested_specifier { - self - .graph - .redirects - .insert(requested_specifier, specifier.clone()); - } - self - .graph - .modules - .insert(specifier, ModuleSlot::Module(Box::new(module))); - - 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, an error will be logged and the process will exit. - pub fn get_graph(self) -> Graph { - self.graph.lock(); - self.graph - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - - use crate::specifier_handler::MemoryHandler; - use deno_core::futures::future; - use deno_core::parking_lot::Mutex; - use std::fs; - use std::path::PathBuf; - - macro_rules! map ( - { $($key:expr => $value:expr),+ } => { - { - let mut m = ::std::collections::HashMap::new(); - $( - m.insert($key, $value); - )+ - m - } - }; - ); - - /// 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 maybe_tsbuildinfo: Option<String>, - pub tsbuildinfo_calls: Vec<(ModuleSpecifier, String)>, - pub cache_calls: Vec<(ModuleSpecifier, Emit)>, - 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, (ModuleSpecifier, AnyError)> { - let specifier_text = specifier - .to_string() - .replace(":///", "_") - .replace("://", "_") - .replace("/", "-"); - let source_path = self.fixtures.join(specifier_text); - let media_type = MediaType::from(&source_path); - let source = Arc::new( - fs::read_to_string(&source_path) - .map_err(|err| (specifier.clone(), err.into()))?, - ); - let is_remote = specifier.scheme() != "file"; - - Ok(CachedModule { - source, - requested_specifier: specifier.clone(), - source_path, - specifier, - media_type, - is_remote, - ..CachedModule::default() - }) - } - } - - impl SpecifierHandler for MockSpecifierHandler { - fn fetch( - &mut self, - specifier: ModuleSpecifier, - _maybe_referrer: Option<Location>, - _is_dynamic: bool, - ) -> FetchFuture { - Box::pin(future::ready(self.get_cache(specifier))) - } - fn get_tsbuildinfo( - &self, - _specifier: &ModuleSpecifier, - ) -> Result<Option<String>, AnyError> { - Ok(self.maybe_tsbuildinfo.clone()) - } - fn set_cache( - &mut self, - specifier: &ModuleSpecifier, - emit: &Emit, - ) -> Result<(), AnyError> { - self.cache_calls.push((specifier.clone(), emit.clone())); - Ok(()) - } - fn set_types( - &mut self, - specifier: &ModuleSpecifier, - types: String, - ) -> Result<(), AnyError> { - self.types_calls.push((specifier.clone(), types)); - Ok(()) - } - fn set_tsbuildinfo( - &mut self, - specifier: &ModuleSpecifier, - tsbuildinfo: String, - ) -> Result<(), AnyError> { - self.maybe_tsbuildinfo = Some(tsbuildinfo.clone()); - self - .tsbuildinfo_calls - .push((specifier.clone(), tsbuildinfo)); - 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(()) - } - } - - async fn setup( - specifier: ModuleSpecifier, - ) -> (Graph, Arc<Mutex<MockSpecifierHandler>>) { - let fixtures = test_util::testdata_path().join("module_graph"); - let handler = Arc::new(Mutex::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None, None); - builder - .add(&specifier, false) - .await - .expect("module not inserted"); - - (builder.get_graph(), handler) - } - - async fn setup_memory( - specifier: ModuleSpecifier, - sources: HashMap<&str, &str>, - ) -> Graph { - let sources: HashMap<String, Arc<String>> = sources - .iter() - .map(|(k, v)| (k.to_string(), Arc::new(v.to_string()))) - .collect(); - let handler = Arc::new(Mutex::new(MemoryHandler::new(sources))); - let mut builder = GraphBuilder::new(handler.clone(), None, None); - builder - .add(&specifier, false) - .await - .expect("module not inserted"); - - builder.get_graph() - } - - #[test] - fn test_get_version() { - let doc_a = "console.log(42);"; - let version_a = get_version(doc_a, "1.2.3", b""); - let doc_b = "console.log(42);"; - 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 = "console.log(42);"; - let maybe_version = Some(get_version(source, &version::deno(), b"")); - let module = Module { - maybe_version, - text_info: SourceTextInfo::from_string(source.to_string()), - ..Module::default() - }; - assert!(module.is_emit_valid(b"")); - - let source = "console.log(42);"; - let old_source = "console.log(43);"; - let maybe_version = Some(get_version(old_source, &version::deno(), b"")); - let module = Module { - maybe_version, - text_info: SourceTextInfo::from_string(source.to_string()), - ..Module::default() - }; - assert!(!module.is_emit_valid(b"")); - - let source = "console.log(42);"; - let maybe_version = Some(get_version(source, "0.0.0", b"")); - let module = Module { - maybe_version, - text_info: SourceTextInfo::from_string(source.to_string()), - ..Module::default() - }; - assert!(!module.is_emit_valid(b"")); - - let source = "console.log(42);"; - let module = Module { - text_info: SourceTextInfo::from_string(source.to_string()), - ..Module::default() - }; - assert!(!module.is_emit_valid(b"")); - } - - #[test] - fn test_module_set_version() { - let source = "console.log(42);"; - let expected = Some(get_version(source, &version::deno(), b"")); - let mut module = Module { - text_info: SourceTextInfo::from_string(source.to_string()), - ..Module::default() - }; - assert!(module.maybe_version.is_none()); - module.set_version(b""); - assert_eq!(module.maybe_version, expected); - } - - #[tokio::test] - async fn test_graph_bundle() { - let tests = vec![ - ("file:///tests/fixture01.ts", "fixture01.out"), - ("file:///tests/fixture02.ts", "fixture02.out"), - ("file:///tests/fixture03.ts", "fixture03.out"), - ("file:///tests/fixture04.ts", "fixture04.out"), - ("file:///tests/fixture05.ts", "fixture05.out"), - ("file:///tests/fixture06.ts", "fixture06.out"), - ("file:///tests/fixture07.ts", "fixture07.out"), - ("file:///tests/fixture08.ts", "fixture08.out"), - ("file:///tests/fixture09.ts", "fixture09.out"), - ("file:///tests/fixture10.ts", "fixture10.out"), - ("file:///tests/fixture11.ts", "fixture11.out"), - ("file:///tests/fixture12.ts", "fixture12.out"), - ("file:///tests/fixture13.ts", "fixture13.out"), - ("file:///tests/fixture14.ts", "fixture14.out"), - ("file:///tests/fixture15.ts", "fixture15.out"), - ]; - let fixtures = test_util::testdata_path().join("bundle"); - - for (specifier, expected_str) in tests { - let specifier = resolve_url_or_path(specifier).unwrap(); - let handler = Arc::new(Mutex::new(MockSpecifierHandler { - fixtures: fixtures.clone(), - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None, None); - builder - .add(&specifier, false) - .await - .expect("module not inserted"); - let graph = builder.get_graph(); - let (actual, stats, maybe_ignored_options) = graph - .bundle(BundleOptions::default()) - .expect("could not bundle"); - assert_eq!(stats.0.len(), 2); - assert_eq!(maybe_ignored_options, None); - let expected_path = fixtures.join(expected_str); - let expected = fs::read_to_string(expected_path).unwrap(); - assert_eq!(actual, expected, "fixture: {}", specifier); - } - } - - #[tokio::test] - async fn test_graph_check_emit() { - let specifier = resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - let (graph, handler) = setup(specifier).await; - let result_info = graph - .check(CheckOptions { - debug: false, - emit: true, - lib: TypeLib::DenoWindow, - maybe_config_file: None, - reload: false, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(result_info.stats.0.len(), 12); - assert!(result_info.diagnostics.is_empty()); - let h = handler.lock(); - assert_eq!(h.cache_calls.len(), 2); - assert_eq!(h.tsbuildinfo_calls.len(), 1); - } - - #[tokio::test] - async fn test_graph_check_ignores_dynamic_import_errors() { - let specifier = resolve_url_or_path("file:///tests/dynamicimport.ts") - .expect("could not resolve module"); - let (graph, _) = setup(specifier).await; - let result_info = graph - .check(CheckOptions { - debug: false, - emit: false, - lib: TypeLib::DenoWindow, - maybe_config_file: None, - reload: false, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.diagnostics.is_empty()); - } - - #[tokio::test] - async fn fix_graph_check_emit_diagnostics() { - let specifier = resolve_url_or_path("file:///tests/diag.ts") - .expect("could not resolve module"); - let (graph, handler) = setup(specifier).await; - let result_info = graph - .check(CheckOptions { - debug: false, - emit: true, - lib: TypeLib::DenoWindow, - maybe_config_file: None, - reload: false, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(result_info.stats.0.len(), 12); - assert!(!result_info.diagnostics.is_empty()); - let h = handler.lock(); - // we shouldn't cache any files or write out tsbuildinfo if there are - // diagnostic errors - assert_eq!(h.cache_calls.len(), 0); - assert_eq!(h.tsbuildinfo_calls.len(), 0); - } - - #[tokio::test] - async fn test_graph_check_no_emit() { - let specifier = resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - let (graph, handler) = setup(specifier).await; - let result_info = graph - .check(CheckOptions { - debug: false, - emit: false, - lib: TypeLib::DenoWindow, - maybe_config_file: None, - reload: false, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(result_info.stats.0.len(), 12); - assert!(result_info.diagnostics.is_empty()); - let h = handler.lock(); - assert_eq!(h.cache_calls.len(), 0); - assert_eq!(h.tsbuildinfo_calls.len(), 1); - } - - #[tokio::test] - async fn fix_graph_check_mjs_root() { - let specifier = resolve_url_or_path("file:///tests/a.mjs") - .expect("could not resolve module"); - let (graph, handler) = setup(specifier).await; - let result_info = graph - .check(CheckOptions { - debug: false, - emit: true, - lib: TypeLib::DenoWindow, - maybe_config_file: None, - reload: false, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.maybe_ignored_options.is_none()); - assert!(result_info.diagnostics.is_empty()); - let h = handler.lock(); - assert_eq!(h.cache_calls.len(), 1); - assert_eq!(h.tsbuildinfo_calls.len(), 1); - } - - #[tokio::test] - async fn fix_graph_check_types_root() { - let specifier = resolve_url_or_path("file:///typesref.js") - .expect("could not resolve module"); - let (graph, _) = setup(specifier).await; - let result_info = graph - .check(CheckOptions { - debug: false, - emit: false, - lib: TypeLib::DenoWindow, - maybe_config_file: None, - reload: false, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.diagnostics.is_empty()); - } - - #[tokio::test] - async fn test_graph_check_user_config() { - let specifier = resolve_url_or_path("file:///tests/checkwithconfig.ts") - .expect("could not resolve module"); - let (graph, handler) = setup(specifier.clone()).await; - let config_file = ConfigFile::read( - test_util::testdata_path().join("module_graph/tsconfig_01.json"), - ) - .unwrap(); - let result_info = graph - .check(CheckOptions { - debug: false, - emit: true, - lib: TypeLib::DenoWindow, - maybe_config_file: Some(config_file), - reload: true, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.maybe_ignored_options.is_none()); - assert!(result_info.diagnostics.is_empty()); - let (ver0, ver1) = { - let h = handler.lock(); - assert_eq!(h.version_calls.len(), 2); - (h.version_calls[0].1.clone(), h.version_calls[1].1.clone()) - }; - - // let's do it all over again to ensure that the versions are determinstic - let (graph, handler) = setup(specifier).await; - let config_file = ConfigFile::read( - test_util::testdata_path().join("module_graph/tsconfig_01.json"), - ) - .unwrap(); - let result_info = graph - .check(CheckOptions { - debug: false, - emit: true, - lib: TypeLib::DenoWindow, - maybe_config_file: Some(config_file), - reload: true, - ..Default::default() - }) - .expect("should have checked"); - assert!(result_info.maybe_ignored_options.is_none()); - assert!(result_info.diagnostics.is_empty()); - let h = handler.lock(); - assert_eq!(h.version_calls.len(), 2); - assert!(h.version_calls[0].1 == ver0 || h.version_calls[0].1 == ver1); - assert!(h.version_calls[1].1 == ver0 || h.version_calls[1].1 == ver1); - } - - #[tokio::test] - async fn test_graph_emit() { - let specifier = resolve_url_or_path("file:///a.ts").unwrap(); - let graph = setup_memory( - specifier, - map!( - "/a.ts" => r#" - import * as b from "./b.ts"; - - console.log(b); - "#, - "/b.ts" => r#" - export const b = "b"; - "# - ), - ) - .await; - let (emitted_files, result_info) = graph - .emit(EmitOptions { - check: true, - bundle_type: BundleType::None, - debug: false, - maybe_user_config: None, - }) - .expect("should have emitted"); - assert!(result_info.diagnostics.is_empty()); - assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(emitted_files.len(), 4); - let out_a = emitted_files.get("file:///a.ts.js"); - assert!(out_a.is_some()); - let out_a = out_a.unwrap(); - assert!(out_a.starts_with("import * as b from")); - assert!(emitted_files.contains_key("file:///a.ts.js.map")); - let out_b = emitted_files.get("file:///b.ts.js"); - assert!(out_b.is_some()); - let out_b = out_b.unwrap(); - assert!(out_b.starts_with("export const b = \"b\";")); - assert!(emitted_files.contains_key("file:///b.ts.js.map")); - } - - #[tokio::test] - async fn test_graph_emit_bundle() { - let specifier = resolve_url_or_path("file:///a.ts").unwrap(); - let graph = setup_memory( - specifier, - map!( - "/a.ts" => r#" - import * as b from "./b.ts"; - - console.log(b); - "#, - "/b.ts" => r#" - export const b = "b"; - "# - ), - ) - .await; - let (emitted_files, result_info) = graph - .emit(EmitOptions { - check: true, - bundle_type: BundleType::Module, - debug: false, - maybe_user_config: None, - }) - .expect("should have emitted"); - assert!(result_info.diagnostics.is_empty()); - assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(emitted_files.len(), 2); - let actual = emitted_files.get("deno:///bundle.js"); - assert!(actual.is_some()); - assert!(emitted_files.contains_key("deno:///bundle.js.map")); - let actual = actual.unwrap(); - assert!(actual.contains("const b = \"b\";")); - assert!(actual.contains("console.log(mod);")); - } - - #[tokio::test] - async fn fix_graph_emit_declaration() { - let specifier = resolve_url_or_path("file:///a.ts").unwrap(); - let graph = setup_memory( - specifier, - map!( - "/a.ts" => r#" - import * as b from "./b.ts"; - - console.log(b); - "#, - "/b.ts" => r#" - export const b = "b"; - "# - ), - ) - .await; - let mut user_config = HashMap::<String, Value>::new(); - user_config.insert("declaration".to_string(), json!(true)); - let (emitted_files, result_info) = graph - .emit(EmitOptions { - check: true, - bundle_type: BundleType::None, - debug: false, - maybe_user_config: Some(user_config), - }) - .expect("should have emitted"); - assert!(result_info.diagnostics.is_empty()); - assert!(result_info.maybe_ignored_options.is_none()); - assert_eq!(emitted_files.len(), 6); - let out_a = emitted_files.get("file:///a.ts.js"); - assert!(out_a.is_some()); - let out_a = out_a.unwrap(); - assert!(out_a.starts_with("import * as b from")); - assert!(emitted_files.contains_key("file:///a.ts.js.map")); - assert!(emitted_files.contains_key("file:///a.ts.d.ts")); - let out_b = emitted_files.get("file:///b.ts.js"); - assert!(out_b.is_some()); - let out_b = out_b.unwrap(); - assert!(out_b.starts_with("export const b = \"b\";")); - assert!(emitted_files.contains_key("file:///b.ts.js.map")); - assert!(emitted_files.contains_key("file:///b.ts.d.ts")); - } - - #[tokio::test] - async fn test_graph_info() { - let specifier = resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - let (graph, _) = setup(specifier.clone()).await; - let info = graph.info().expect("could not get info"); - assert_eq!(info.root, specifier); - assert_eq!(info.modules.len(), 7); - assert_eq!(info.size, 518); - } - - #[tokio::test] - async fn test_graph_import_json() { - let specifier = resolve_url_or_path("file:///tests/importjson.ts") - .expect("could not resolve module"); - let fixtures = test_util::testdata_path().join("module_graph"); - let handler = Arc::new(Mutex::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None, None); - builder - .add(&specifier, false) - .await - .expect_err("should have errored"); - } - - #[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 specifier = resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - let (mut graph, handler) = setup(specifier).await; - let result_info = graph.transpile(TranspileOptions::default()).unwrap(); - assert_eq!(result_info.stats.0.len(), 3); - assert_eq!(result_info.maybe_ignored_options, None); - let h = handler.lock(); - assert_eq!(h.cache_calls.len(), 2); - match &h.cache_calls[0].1 { - Emit::Cli((code, maybe_map)) => { - assert!( - code.contains("# sourceMappingURL=data:application/json;base64,") - ); - assert!(maybe_map.is_none()); - } - }; - match &h.cache_calls[1].1 { - Emit::Cli((code, maybe_map)) => { - assert!( - code.contains("# sourceMappingURL=data:application/json;base64,") - ); - assert!(maybe_map.is_none()); - } - }; - assert_eq!(h.deps_calls.len(), 7); - assert_eq!( - h.deps_calls[0].0, - 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, - 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, - 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 specifier = resolve_url_or_path("https://deno.land/x/transpile.tsx") - .expect("could not resolve module"); - let (mut graph, handler) = setup(specifier).await; - let config_file = ConfigFile::read( - test_util::testdata_path().join("module_graph/tsconfig.json"), - ) - .unwrap(); - let result_info = graph - .transpile(TranspileOptions { - debug: false, - maybe_config_file: Some(config_file), - reload: false, - ..Default::default() - }) - .unwrap(); - assert_eq!( - result_info.maybe_ignored_options.unwrap().items, - vec!["target".to_string()], - "the 'target' options should have been ignored" - ); - let h = handler.lock(); - 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 - match &h.cache_calls[0].1 { - Emit::Cli((code, _)) => { - assert!( - code.contains("<div >Hello world!</div>"), - "jsx should have been preserved" - ); - } - } - } - - #[tokio::test] - async fn test_graph_import_map_remote_to_local() { - let fixtures = test_util::testdata_path().join("module_graph"); - let maybe_import_map = Some( - ImportMap::from_json( - "file:///tests/importmap.json", - r#"{ - "imports": { - "https://deno.land/x/b/mod.js": "./b/mod.js" - } - } - "#, - ) - .expect("could not parse import map"), - ); - let handler = Arc::new(Mutex::new(MockSpecifierHandler { - fixtures, - ..Default::default() - })); - let mut builder = GraphBuilder::new(handler, maybe_import_map, None); - let specifier = resolve_url_or_path("file:///tests/importremap.ts") - .expect("could not resolve module"); - builder.add(&specifier, false).await.expect("could not add"); - builder.get_graph(); - } - - #[tokio::test] - async fn test_graph_with_lockfile() { - let fixtures = test_util::testdata_path().join("module_graph"); - let lockfile_path = fixtures.join("lockfile.json"); - let lockfile = - Lockfile::new(lockfile_path, false).expect("could not load lockfile"); - let maybe_lockfile = Some(Arc::new(Mutex::new(lockfile))); - let handler = Arc::new(Mutex::new(MockSpecifierHandler { - fixtures, - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder::new(handler.clone(), None, maybe_lockfile); - let specifier = resolve_url_or_path("file:///tests/main.ts") - .expect("could not resolve module"); - builder - .add(&specifier, false) - .await - .expect("module not inserted"); - builder.get_graph(); - } -} |