summaryrefslogtreecommitdiff
path: root/cli/module_graph.rs
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2021-10-11 08:26:22 +1100
committerGitHub <noreply@github.com>2021-10-11 08:26:22 +1100
commita7baf5f2bbb50dc0cb571de141b800b9155faca7 (patch)
tree4bebaabd1d3ed4595e8a388e0fae559bb5558974 /cli/module_graph.rs
parent5a8a989b7815023f33a1e3183a55cc8999af5d98 (diff)
refactor: integrate deno_graph into CLI (#12369)
Diffstat (limited to 'cli/module_graph.rs')
-rw-r--r--cli/module_graph.rs2834
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: &regex::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();
- }
-}