summaryrefslogtreecommitdiff
path: root/cli/module_graph.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/module_graph.rs')
-rw-r--r--cli/module_graph.rs2209
1 files changed, 2209 insertions, 0 deletions
diff --git a/cli/module_graph.rs b/cli/module_graph.rs
new file mode 100644
index 000000000..d757aa0a1
--- /dev/null
+++ b/cli/module_graph.rs
@@ -0,0 +1,2209 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use crate::ast;
+use crate::ast::parse;
+use crate::ast::transpile_module;
+use crate::ast::BundleHook;
+use crate::ast::Location;
+use crate::ast::ParsedModule;
+use crate::colors;
+use crate::diagnostics::Diagnostics;
+use crate::import_map::ImportMap;
+use crate::info::ModuleGraphInfo;
+use crate::info::ModuleInfo;
+use crate::info::ModuleInfoMap;
+use crate::info::ModuleInfoMapItem;
+use crate::js;
+use crate::lockfile::Lockfile;
+use crate::media_type::MediaType;
+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::tsc_config::IgnoredCompilerOptions;
+use crate::tsc_config::TsConfig;
+use crate::version;
+use crate::AnyError;
+
+use deno_core::error::Context;
+use deno_core::futures::stream::FuturesUnordered;
+use deno_core::futures::stream::StreamExt;
+use deno_core::serde::Serialize;
+use deno_core::serde::Serializer;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use deno_core::ModuleResolutionError;
+use deno_core::ModuleSpecifier;
+use regex::Regex;
+use serde::Deserialize;
+use serde::Deserializer;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::error::Error;
+use std::fmt;
+use std::path::PathBuf;
+use std::rc::Rc;
+use std::result;
+use std::sync::Arc;
+use std::sync::Mutex;
+use std::time::Instant;
+
+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<swc_common::SourceMap>,
+ emit_options: &'a ast::EmitOptions,
+ globals: &'a swc_common::Globals,
+ graph: &'a Graph,
+}
+
+impl<'a> BundleLoader<'a> {
+ pub fn new(
+ graph: &'a Graph,
+ emit_options: &'a ast::EmitOptions,
+ globals: &'a swc_common::Globals,
+ cm: Rc<swc_common::SourceMap>,
+ ) -> Self {
+ BundleLoader {
+ cm,
+ emit_options,
+ globals,
+ graph,
+ }
+ }
+}
+
+impl swc_bundler::Load for BundleLoader<'_> {
+ fn load(
+ &self,
+ file: &swc_common::FileName,
+ ) -> Result<(Rc<swc_common::SourceFile>, swc_ecmascript::ast::Module), AnyError>
+ {
+ match file {
+ swc_common::FileName::Custom(filename) => {
+ let specifier = ModuleSpecifier::resolve_url_or_path(filename)
+ .context("Failed to convert swc FileName to ModuleSpecifier.")?;
+ 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.")?;
+ transpile_module(
+ filename,
+ &src,
+ &media_type,
+ self.emit_options,
+ self.globals,
+ self.cm.clone(),
+ )
+ } else {
+ Err(
+ GraphError::MissingDependency(specifier, "<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)]
+enum TypeScriptReference {
+ Path(String),
+ Types(String),
+}
+
+/// Determine if a comment contains a triple slash reference and optionally
+/// return its kind and value.
+fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
+ if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) {
+ None
+ } else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) {
+ Some(TypeScriptReference::Path(
+ captures.get(1).unwrap().as_str().to_string(),
+ ))
+ } else if let Some(captures) = TYPES_REFERENCE_RE.captures(comment) {
+ Some(TypeScriptReference::Types(
+ captures.get(1).unwrap().as_str().to_string(),
+ ))
+ } else {
+ None
+ }
+}
+
+/// Determine if a comment contains a `@deno-types` pragma and optionally return
+/// its value.
+fn parse_deno_types(comment: &str) -> Option<String> {
+ if let Some(captures) = DENO_TYPES_RE.captures(comment) {
+ if let Some(m) = captures.get(1) {
+ Some(m.as_str().to_string())
+ } else if let Some(m) = captures.get(2) {
+ Some(m.as_str().to_string())
+ } else {
+ panic!("unreachable");
+ }
+ } else {
+ None
+ }
+}
+
+/// A hashing function that takes the source code, version and optionally a
+/// user provided config and generates a string hash which can be stored to
+/// determine if the cached emit is valid or not.
+fn get_version(source: &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)]
+struct Module {
+ dependencies: DependencyMap,
+ is_dirty: bool,
+ is_parsed: bool,
+ maybe_emit: Option<Emit>,
+ maybe_emit_path: Option<(PathBuf, Option<PathBuf>)>,
+ maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
+ maybe_parsed_module: Option<ParsedModule>,
+ maybe_types: Option<(String, ModuleSpecifier)>,
+ maybe_version: Option<String>,
+ media_type: MediaType,
+ specifier: ModuleSpecifier,
+ source: String,
+ 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_parsed_module: None,
+ maybe_types: None,
+ maybe_version: None,
+ media_type: MediaType::Unknown,
+ specifier: ModuleSpecifier::resolve_url("file:///example.js").unwrap(),
+ source: "".to_string(),
+ source_path: PathBuf::new(),
+ }
+ }
+}
+
+impl Module {
+ pub fn new(
+ cached_module: CachedModule,
+ is_root: bool,
+ maybe_import_map: Option<Rc<RefCell<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,
+ source: 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 = if let Some(ref specifier) = cached_module.maybe_types
+ {
+ Some((
+ specifier.clone(),
+ module
+ .resolve_import(&specifier, None)
+ .expect("could not resolve module"),
+ ))
+ } else {
+ None
+ };
+ 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.source, 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<(), AnyError> {
+ let parsed_module =
+ parse(self.specifier.as_str(), &self.source, &self.media_type)?;
+
+ // parse out any triple slash references
+ for comment in parsed_module.get_leading_comments().iter() {
+ if let Some(ts_reference) = parse_ts_reference(&comment.text) {
+ let location = parsed_module.get_location(&comment.span);
+ 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 = parsed_module.analyze_dependencies();
+ for desc in dependencies.iter().filter(|desc| {
+ desc.kind != swc_ecmascript::dep_graph::DependencyKind::Require
+ }) {
+ let location = Location {
+ filename: self.specifier.to_string(),
+ col: desc.col,
+ line: desc.line,
+ };
+
+ // 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(_, _)) => None,
+ _ => {
+ 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.text).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;
+ if let Some(specifier) = maybe_specifier {
+ if desc.kind == swc_ecmascript::dep_graph::DependencyKind::ExportType
+ || desc.kind == swc_ecmascript::dep_graph::DependencyKind::ImportType
+ {
+ dep.maybe_type = Some(specifier);
+ } else {
+ dep.maybe_code = Some(specifier);
+ }
+ }
+ // If the dependency wasn't a type only dependency already, and there is
+ // a `@deno-types` comment, then we will set the `maybe_type` dependency.
+ if maybe_type.is_some() && dep.maybe_type.is_none() {
+ dep.maybe_type = maybe_type;
+ }
+ }
+
+ self.maybe_parsed_module = Some(parsed_module);
+ Ok(())
+ }
+
+ fn resolve_import(
+ &self,
+ specifier: &str,
+ maybe_location: Option<Location>,
+ ) -> Result<ModuleSpecifier, AnyError> {
+ let maybe_resolve = if let Some(import_map) = self.maybe_import_map.clone()
+ {
+ import_map
+ .borrow()
+ .resolve(specifier, self.specifier.as_str())?
+ } else {
+ None
+ };
+ let specifier = if let Some(module_specifier) = maybe_resolve {
+ module_specifier
+ } else {
+ ModuleSpecifier::resolve_import(specifier, self.specifier.as_str())?
+ };
+
+ let referrer_scheme = self.specifier.as_url().scheme();
+ let specifier_scheme = specifier.as_url().scheme();
+ let location = maybe_location.unwrap_or(Location {
+ filename: self.specifier.to_string(),
+ line: 0,
+ col: 0,
+ });
+
+ // Disallow downgrades from HTTPS to HTTP
+ if referrer_scheme == "https" && specifier_scheme == "http" {
+ return Err(
+ GraphError::InvalidDowngrade(specifier.clone(), location).into(),
+ );
+ }
+
+ // Disallow a remote URL from trying to import a local URL
+ if (referrer_scheme == "https" || referrer_scheme == "http")
+ && !(specifier_scheme == "https" || specifier_scheme == "http")
+ {
+ return Err(
+ GraphError::InvalidLocalImport(specifier.clone(), location).into(),
+ );
+ }
+
+ Ok(specifier)
+ }
+
+ /// Calculate the hashed version of the module and update the `maybe_version`.
+ pub fn set_version(&mut self, config: &[u8]) {
+ self.maybe_version = Some(get_version(&self.source, version::DENO, config))
+ }
+
+ pub fn size(&self) -> usize {
+ self.source.as_bytes().len()
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct Stats(pub Vec<(String, u128)>);
+
+impl<'de> Deserialize<'de> for Stats {
+ fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let items: Vec<(String, u128)> = Deserialize::deserialize(deserializer)?;
+ Ok(Stats(items))
+ }
+}
+
+impl fmt::Display for Stats {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ 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,
+ /// 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.worker".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 string that points to a user supplied TypeScript configuration
+ /// file that augments the the default configuration passed to the TypeScript
+ /// compiler.
+ pub maybe_config_path: Option<String>,
+}
+
+#[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 string that points to a user supplied TypeScript configuration
+ /// file that augments the the default configuration passed to the TypeScript
+ /// compiler.
+ pub maybe_config_path: Option<String>,
+ /// Ignore any previously emits and ensure that all files are emitted from
+ /// source.
+ pub reload: bool,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum BundleType {
+ /// Return the emitted contents of the program as a single "flattened" ES
+ /// module.
+ Esm,
+ // TODO(@kitsonk) once available in swc
+ // Iife,
+ /// 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 {
+ /// 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 string that points to a user supplied TypeScript configuration
+ /// file that augments the the default configuration passed to the TypeScript
+ /// compiler.
+ pub maybe_config_path: Option<String>,
+ /// Ignore any previously emits and ensure that all files are emitted from
+ /// source.
+ pub reload: bool,
+}
+
+/// 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: Rc<RefCell<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, Module>,
+ /// 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>>>,
+}
+
+impl Graph {
+ /// Create a new instance of a graph, ready to have modules loaded it.
+ ///
+ /// The argument `handler` is an instance of a structure that implements the
+ /// `SpecifierHandler` trait.
+ ///
+ pub fn new(
+ handler: Rc<RefCell<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,
+ "inlineSourceMap": true,
+ "jsx": "react",
+ "jsxFactory": "React.createElement",
+ "jsxFragmentFactory": "React.Fragment",
+ }));
+ let maybe_ignored_options =
+ ts_config.merge_tsconfig(options.maybe_config_path)?;
+
+ let s = self.emit_bundle(&root_specifier, &ts_config.into())?;
+ let stats = Stats(vec![
+ ("Files".to_string(), self.modules.len() as u128),
+ ("Total time".to_string(), start.elapsed().as_millis()),
+ ]);
+
+ Ok((s, stats, maybe_ignored_options))
+ }
+
+ /// Type check the module graph, corresponding to the options provided.
+ pub fn check(self, options: CheckOptions) -> Result<ResultInfo, AnyError> {
+ 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,
+ "isolatedModules": true,
+ "lib": options.lib,
+ "module": "esnext",
+ "strict": true,
+ "target": "esnext",
+ "tsBuildInfoFile": "deno:///.tsbuildinfo",
+ }));
+ if options.emit {
+ config.merge(&json!({
+ // TODO(@kitsonk) consider enabling this by default
+ // see: https://github.com/denoland/deno/issues/7732
+ "emitDecoratorMetadata": false,
+ "jsx": "react",
+ "inlineSourceMap": true,
+ "outDir": "deno://",
+ "removeComments": true,
+ }));
+ } else {
+ config.merge(&json!({
+ "noEmit": true,
+ }));
+ }
+ let maybe_ignored_options =
+ config.merge_tsconfig(options.maybe_config_path)?;
+
+ // Short circuit if none of the modules require an emit, or all of the
+ // modules that require an emit have a valid emit. There is also an edge
+ // case where there are multiple imports of a dynamic module during a
+ // single invocation, if that is the case, even if there is a reload, we
+ // will simply look at if the emit is invalid, to avoid two checks for the
+ // same programme.
+ if !self.needs_emit(&config)
+ || (self.is_emit_valid(&config)
+ && (!options.reload || self.roots_dynamic))
+ {
+ debug!("graph does not need to be checked or emitted.");
+ return Ok(ResultInfo {
+ maybe_ignored_options,
+ ..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 {
+ info!("{} {}", colors::green("Check"), specifier);
+ }
+
+ let root_names = self.get_root_names();
+ let maybe_tsbuildinfo = self.maybe_tsbuildinfo.clone();
+ let hash_data =
+ vec![config.as_bytes(), version::DENO.as_bytes().to_owned()];
+ let graph = Rc::new(RefCell::new(self));
+
+ let response = tsc::exec(
+ js::compiler_isolate_init(),
+ tsc::Request {
+ config: config.clone(),
+ debug: options.debug,
+ graph: graph.clone(),
+ hash_data,
+ maybe_tsbuildinfo,
+ root_names,
+ },
+ )?;
+
+ let mut graph = graph.borrow_mut();
+ 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() && !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 Some(module) = graph.get_module_mut(specifier) {
+ module.maybe_emit =
+ Some(Emit::Cli((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,
+ maybe_ignored_options,
+ stats: response.stats,
+ })
+ }
+
+ fn contains_module(&self, specifier: &ModuleSpecifier) -> bool {
+ let s = self.resolve_specifier(specifier);
+ self.modules.contains_key(s)
+ }
+
+ /// 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(
+ self,
+ options: EmitOptions,
+ ) -> Result<(HashMap<String, String>, ResultInfo), AnyError> {
+ let mut config = TsConfig::new(json!({
+ "allowJs": true,
+ // TODO(@kitsonk) consider enabling this by default
+ // see: https://github.com/denoland/deno/issues/7732
+ "emitDecoratorMetadata": false,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "isolatedModules": true,
+ "jsx": "react",
+ "lib": TypeLib::DenoWindow,
+ "module": "esnext",
+ "strict": true,
+ "target": "esnext",
+ }));
+ let opts = match options.bundle_type {
+ BundleType::Esm => json!({
+ "checkJs": false,
+ "inlineSourceMap": false,
+ "noEmit": true,
+ "jsxFactory": "React.createElement",
+ "jsxFragmentFactory": "React.Fragment",
+ }),
+ 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
+ };
+
+ let root_names = self.get_root_names();
+ let hash_data =
+ vec![config.as_bytes(), version::DENO.as_bytes().to_owned()];
+ let graph = Rc::new(RefCell::new(self));
+
+ let response = tsc::exec(
+ js::compiler_isolate_init(),
+ tsc::Request {
+ config: config.clone(),
+ debug: options.debug,
+ graph: graph.clone(),
+ hash_data,
+ maybe_tsbuildinfo: None,
+ root_names,
+ },
+ )?;
+
+ let mut emitted_files = HashMap::new();
+ match options.bundle_type {
+ BundleType::Esm => {
+ assert!(
+ response.emitted_files.is_empty(),
+ "No files should have been emitted from tsc."
+ );
+ let graph = graph.borrow();
+ assert_eq!(
+ graph.roots.len(),
+ 1,
+ "Only a single root module supported."
+ );
+ let specifier = &graph.roots[0];
+ let s = graph.emit_bundle(specifier, &config.into())?;
+ emitted_files.insert("deno:///bundle.js".to_string(), s);
+ }
+ 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",
+ _ => unreachable!(),
+ };
+ let key = format!("{}{}", specifier, extension);
+ emitted_files.insert(key, emitted_file.data.clone());
+ }
+ }
+ };
+
+ Ok((
+ emitted_files,
+ ResultInfo {
+ diagnostics: response.diagnostics,
+ maybe_ignored_options,
+ stats: response.stats,
+ },
+ ))
+ }
+
+ /// Shared between `bundle()` and `emit()`.
+ fn emit_bundle(
+ &self,
+ specifier: &ModuleSpecifier,
+ emit_options: &ast::EmitOptions,
+ ) -> Result<String, AnyError> {
+ let cm = Rc::new(swc_common::SourceMap::new(
+ swc_common::FilePathMapping::empty(),
+ ));
+ let globals = swc_common::Globals::new();
+ let loader = BundleLoader::new(self, emit_options, &globals, cm.clone());
+ let hook = Box::new(BundleHook);
+ let bundler = swc_bundler::Bundler::new(
+ &globals,
+ cm.clone(),
+ loader,
+ self,
+ swc_bundler::Config::default(),
+ hook,
+ );
+ let mut entries = HashMap::new();
+ entries.insert(
+ "bundle".to_string(),
+ swc_common::FileName::Custom(specifier.to_string()),
+ );
+ let output = bundler
+ .bundle(entries)
+ .context("Unable to output bundle during Graph::bundle().")?;
+ let mut buf = Vec::new();
+ {
+ let mut emitter = swc_ecmascript::codegen::Emitter {
+ cfg: swc_ecmascript::codegen::Config { minify: false },
+ cm: cm.clone(),
+ comments: None,
+ wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new(
+ cm, "\n", &mut buf, None,
+ )),
+ };
+
+ emitter
+ .emit_module(&output[0].module)
+ .context("Unable to emit bundle during Graph::bundle().")?;
+ }
+
+ String::from_utf8(buf).context("Emitted bundle is an invalid utf-8 string.")
+ }
+
+ /// 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.borrow_mut();
+ for (_, module) in self.modules.iter_mut() {
+ 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(())
+ }
+
+ fn get_info(
+ &self,
+ specifier: &ModuleSpecifier,
+ seen: &mut HashSet<ModuleSpecifier>,
+ totals: &mut HashMap<ModuleSpecifier, usize>,
+ ) -> ModuleInfo {
+ let not_seen = seen.insert(specifier.clone());
+ let module = self.get_module(specifier).unwrap();
+ let mut deps = Vec::new();
+ let mut total_size = None;
+
+ if not_seen {
+ let mut seen_deps = HashSet::new();
+ // TODO(@kitsonk) https://github.com/denoland/deno/issues/7927
+ for (_, dep) in module.dependencies.iter() {
+ // Check the runtime code dependency
+ if let Some(code_dep) = &dep.maybe_code {
+ if seen_deps.insert(code_dep.clone()) {
+ deps.push(self.get_info(code_dep, seen, totals));
+ }
+ }
+ }
+ deps.sort();
+ total_size = if let Some(total) = totals.get(specifier) {
+ Some(total.to_owned())
+ } else {
+ let mut total = deps
+ .iter()
+ .map(|d| {
+ if let Some(total_size) = d.total_size {
+ total_size
+ } else {
+ 0
+ }
+ })
+ .sum();
+ total += module.size();
+ totals.insert(specifier.clone(), total);
+ Some(total)
+ };
+ }
+
+ ModuleInfo {
+ deps,
+ name: specifier.clone(),
+ size: module.size(),
+ total_size,
+ }
+ }
+
+ fn get_info_map(&self) -> ModuleInfoMap {
+ let map = self
+ .modules
+ .iter()
+ .map(|(specifier, module)| {
+ let mut deps = HashSet::new();
+ for (_, dep) in module.dependencies.iter() {
+ if let Some(code_dep) = &dep.maybe_code {
+ deps.insert(code_dep.clone());
+ }
+ if let Some(type_dep) = &dep.maybe_type {
+ deps.insert(type_dep.clone());
+ }
+ }
+ if let Some((_, types_dep)) = &module.maybe_types {
+ deps.insert(types_dep.clone());
+ }
+ let item = ModuleInfoMapItem {
+ deps: deps.into_iter().collect(),
+ size: module.size(),
+ };
+ (specifier.clone(), item)
+ })
+ .collect();
+
+ ModuleInfoMap::new(map)
+ }
+
+ pub fn get_media_type(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Option<MediaType> {
+ if let Some(module) = self.get_module(specifier) {
+ Some(module.media_type)
+ } else {
+ None
+ }
+ }
+
+ fn get_module(&self, specifier: &ModuleSpecifier) -> Option<&Module> {
+ let s = self.resolve_specifier(specifier);
+ self.modules.get(s)
+ }
+
+ fn get_module_mut(
+ &mut self,
+ specifier: &ModuleSpecifier,
+ ) -> Option<&mut Module> {
+ // 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)
+ }
+
+ /// 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`.
+ fn get_root_names(&self) -> Vec<(ModuleSpecifier, MediaType)> {
+ self
+ .roots
+ .iter()
+ .map(|ms| {
+ (
+ // root modules can be redirects, so before we pass it to tsc we need
+ // to resolve the redirect
+ self.resolve_specifier(ms).clone(),
+ self.get_media_type(ms).unwrap(),
+ )
+ })
+ .collect()
+ }
+
+ /// 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<String> {
+ if let Some(module) = self.get_module(specifier) {
+ Some(module.source.clone())
+ } 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<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 module = self.roots[0].clone();
+ let m = self.get_module(&module).unwrap();
+
+ let mut seen = HashSet::new();
+ let mut totals = HashMap::new();
+ let info = self.get_info(&module, &mut seen, &mut totals);
+
+ let files = self.get_info_map();
+ let total_size = totals.get(&module).unwrap_or(&m.size()).to_owned();
+ let (compiled, map) =
+ if let Some((emit_path, maybe_map_path)) = &m.maybe_emit_path {
+ (Some(emit_path.clone()), maybe_map_path.clone())
+ } else {
+ (None, None)
+ };
+
+ Ok(ModuleGraphInfo {
+ compiled,
+ dep_count: self.modules.len() - 1,
+ file_type: m.media_type,
+ files,
+ info,
+ local: m.source_path.clone(),
+ map,
+ module,
+ total_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)| {
+ 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
+ }
+ })
+ }
+
+ /// 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().unwrap();
+ for (ms, module) in self.modules.iter() {
+ let specifier = module.specifier.to_string();
+ let valid = lockfile.check_or_insert(&specifier, &module.source);
+ 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.media_type {
+ MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true,
+ MediaType::JavaScript => check_js,
+ _ => 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> {
+ if !self.contains_module(referrer) {
+ return Err(GraphError::MissingSpecifier(referrer.to_owned()).into());
+ }
+ let module = self.get_module(referrer).unwrap();
+ 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(),
+ );
+ };
+ if !self.contains_module(&resolved_specifier) {
+ return Err(
+ GraphError::MissingDependency(
+ referrer.to_owned(),
+ resolved_specifier.to_string(),
+ )
+ .into(),
+ );
+ }
+ let dep_module = self.get_module(&resolved_specifier).unwrap();
+ // In the case that there is a X-TypeScript-Types or a triple-slash types,
+ // then the `maybe_types` specifier will be populated and we should use that
+ // instead.
+ let result = if 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<(Stats, Option<IgnoredCompilerOptions>), AnyError> {
+ let start = Instant::now();
+
+ let mut ts_config = TsConfig::new(json!({
+ "checkJs": false,
+ "emitDecoratorMetadata": false,
+ "inlineSourceMap": true,
+ "jsx": "react",
+ "jsxFactory": "React.createElement",
+ "jsxFragmentFactory": "React.Fragment",
+ }));
+
+ let maybe_ignored_options =
+ ts_config.merge_tsconfig(options.maybe_config_path)?;
+
+ let emit_options: ast::EmitOptions = ts_config.clone().into();
+
+ let mut emit_count: u128 = 0;
+ let config = ts_config.as_bytes();
+ for (_, module) in self.modules.iter_mut() {
+ // TODO(kitsonk) a lot of this logic should be refactored into `Module` as
+ // we start to support other methods on the graph. Especially managing
+ // the dirty state is something the module itself should "own".
+
+ // if the module is a Dts file we should skip it
+ if module.media_type == MediaType::Dts {
+ continue;
+ }
+ // if we don't have check_js enabled, we won't touch non TypeScript
+ // modules
+ if !(emit_options.check_js
+ || module.media_type == MediaType::TSX
+ || module.media_type == MediaType::TypeScript)
+ {
+ continue;
+ }
+ // skip modules that already have a valid emit
+ if !options.reload && module.is_emit_valid(&config) {
+ continue;
+ }
+ if module.maybe_parsed_module.is_none() {
+ module.parse()?;
+ }
+ let parsed_module = module.maybe_parsed_module.clone().unwrap();
+ let emit = parsed_module.transpile(&emit_options)?;
+ emit_count += 1;
+ module.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 u128),
+ ("Emitted".to_string(), emit_count),
+ ("Total time".to_string(), start.elapsed().as_millis()),
+ ]);
+
+ Ok((stats, maybe_ignored_options))
+ }
+}
+
+impl swc_bundler::Resolve for Graph {
+ fn resolve(
+ &self,
+ referrer: &swc_common::FileName,
+ specifier: &str,
+ ) -> Result<swc_common::FileName, AnyError> {
+ let referrer = if let swc_common::FileName::Custom(referrer) = referrer {
+ ModuleSpecifier::resolve_url_or_path(referrer)
+ .context("Cannot resolve swc FileName to a module specifier")?
+ } else {
+ unreachable!(
+ "An unexpected referrer was passed when bundling: {:?}",
+ referrer
+ )
+ };
+ let specifier = self.resolve(specifier, &referrer, false)?;
+
+ Ok(swc_common::FileName::Custom(specifier.to_string()))
+ }
+}
+
+/// A structure for building a dependency graph of modules.
+pub struct GraphBuilder {
+ fetched: HashSet<ModuleSpecifier>,
+ graph: Graph,
+ maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
+ pending: FuturesUnordered<FetchFuture>,
+}
+
+impl GraphBuilder {
+ pub fn new(
+ handler: Rc<RefCell<dyn SpecifierHandler>>,
+ maybe_import_map: Option<ImportMap>,
+ maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
+ ) -> Self {
+ let internal_import_map = if let Some(import_map) = maybe_import_map {
+ Some(Rc::new(RefCell::new(import_map)))
+ } else {
+ None
+ };
+ GraphBuilder {
+ graph: Graph::new(handler, maybe_lockfile),
+ fetched: HashSet::new(),
+ 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.fetch(specifier, &None, is_dynamic)?;
+
+ loop {
+ let cached_module = self.pending.next().await.unwrap()?;
+ let is_root = &cached_module.specifier == specifier;
+ self.visit(cached_module, is_root)?;
+ if self.pending.is_empty() {
+ break;
+ }
+ }
+
+ 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.borrow();
+ self.graph.maybe_tsbuildinfo = handler.get_tsbuildinfo(specifier)?;
+ }
+ }
+
+ 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,
+ ) -> Result<(), AnyError> {
+ if self.fetched.contains(&specifier) {
+ return Ok(());
+ }
+
+ self.fetched.insert(specifier.clone());
+ let future = self.graph.handler.borrow_mut().fetch(
+ specifier.clone(),
+ maybe_referrer.clone(),
+ is_dynamic,
+ );
+ self.pending.push(future);
+
+ Ok(())
+ }
+
+ /// Visit a module that has been fetched, hydrating the module, analyzing its
+ /// dependencies if required, fetching those dependencies, and inserting the
+ /// module into the graph.
+ fn visit(
+ &mut self,
+ cached_module: CachedModule,
+ is_root: 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.borrow_mut();
+ handler.set_deps(&specifier, module.dependencies.clone())?;
+ if !has_types {
+ if let Some((types, _)) = module.maybe_types.clone() {
+ handler.set_types(&specifier, types)?;
+ }
+ }
+ }
+ }
+ for (_, dep) in module.dependencies.iter() {
+ let maybe_referrer = Some(dep.location.clone());
+ if let Some(specifier) = dep.maybe_code.as_ref() {
+ self.fetch(specifier, &maybe_referrer, dep.is_dynamic)?;
+ }
+ if let Some(specifier) = dep.maybe_type.as_ref() {
+ self.fetch(specifier, &maybe_referrer, dep.is_dynamic)?;
+ }
+ }
+ if let Some((_, specifier)) = module.maybe_types.as_ref() {
+ self.fetch(specifier, &None, false)?;
+ }
+ if specifier != requested_specifier {
+ self
+ .graph
+ .redirects
+ .insert(requested_specifier, specifier.clone());
+ }
+ self.graph.modules.insert(specifier, 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 std::env;
+ use std::fs;
+ use std::path::PathBuf;
+ use std::sync::Mutex;
+
+ 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, 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 = fs::read_to_string(&source_path)?;
+ let is_remote = specifier.as_url().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, Rc<RefCell<MockSpecifierHandler>>) {
+ let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
+ let fixtures = c.join("tests/module_graph");
+ let handler = Rc::new(RefCell::new(MockSpecifierHandler {
+ fixtures,
+ ..MockSpecifierHandler::default()
+ }));
+ let mut builder = GraphBuilder::new(handler.clone(), None, 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, String> = sources
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+ let handler = Rc::new(RefCell::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);".to_string();
+ let maybe_version = Some(get_version(&source, version::DENO, b""));
+ let module = Module {
+ source,
+ maybe_version,
+ ..Module::default()
+ };
+ assert!(module.is_emit_valid(b""));
+
+ let source = "console.log(42);".to_string();
+ let old_source = "console.log(43);";
+ let maybe_version = Some(get_version(old_source, version::DENO, b""));
+ let module = Module {
+ source,
+ maybe_version,
+ ..Module::default()
+ };
+ assert!(!module.is_emit_valid(b""));
+
+ let source = "console.log(42);".to_string();
+ let maybe_version = Some(get_version(&source, "0.0.0", b""));
+ let module = Module {
+ source,
+ maybe_version,
+ ..Module::default()
+ };
+ assert!(!module.is_emit_valid(b""));
+
+ let source = "console.log(42);".to_string();
+ let module = Module {
+ source,
+ ..Module::default()
+ };
+ assert!(!module.is_emit_valid(b""));
+ }
+
+ #[test]
+ fn test_module_set_version() {
+ let source = "console.log(42);".to_string();
+ let expected = Some(get_version(&source, version::DENO, b""));
+ let mut module = Module {
+ source,
+ ..Module::default()
+ };
+ assert!(module.maybe_version.is_none());
+ module.set_version(b"");
+ assert_eq!(module.maybe_version, expected);
+ }
+
+ #[tokio::test]
+ async fn test_graph_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"),
+ ];
+ let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
+ let fixtures = c.join("tests/bundle");
+
+ for (specifier, expected_str) in tests {
+ let specifier = ModuleSpecifier::resolve_url_or_path(specifier).unwrap();
+ let handler = Rc::new(RefCell::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 =
+ ModuleSpecifier::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_path: None,
+ reload: false,
+ })
+ .expect("should have checked");
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert_eq!(result_info.stats.0.len(), 12);
+ println!("{}", result_info.diagnostics);
+ assert!(result_info.diagnostics.is_empty());
+ let h = handler.borrow();
+ assert_eq!(h.cache_calls.len(), 2);
+ assert_eq!(h.tsbuildinfo_calls.len(), 1);
+ }
+
+ #[tokio::test]
+ async fn test_graph_check_no_emit() {
+ let specifier =
+ ModuleSpecifier::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_path: None,
+ reload: false,
+ })
+ .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.borrow();
+ assert_eq!(h.cache_calls.len(), 0);
+ assert_eq!(h.tsbuildinfo_calls.len(), 1);
+ }
+
+ #[tokio::test]
+ async fn test_graph_check_user_config() {
+ let specifier =
+ ModuleSpecifier::resolve_url_or_path("file:///tests/checkwithconfig.ts")
+ .expect("could not resolve module");
+ let (graph, handler) = setup(specifier.clone()).await;
+ let result_info = graph
+ .check(CheckOptions {
+ debug: false,
+ emit: true,
+ lib: TypeLib::DenoWindow,
+ maybe_config_path: Some(
+ "tests/module_graph/tsconfig_01.json".to_string(),
+ ),
+ reload: true,
+ })
+ .expect("should have checked");
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert!(result_info.diagnostics.is_empty());
+ let h = handler.borrow();
+ assert_eq!(h.version_calls.len(), 2);
+ let ver0 = h.version_calls[0].1.clone();
+ let ver1 = 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 result_info = graph
+ .check(CheckOptions {
+ debug: false,
+ emit: true,
+ lib: TypeLib::DenoWindow,
+ maybe_config_path: Some(
+ "tests/module_graph/tsconfig_01.json".to_string(),
+ ),
+ reload: true,
+ })
+ .expect("should have checked");
+ assert!(result_info.maybe_ignored_options.is_none());
+ assert!(result_info.diagnostics.is_empty());
+ let h = handler.borrow();
+ 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 =
+ ModuleSpecifier::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 {
+ 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 =
+ ModuleSpecifier::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 {
+ bundle_type: BundleType::Esm,
+ 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(), 1);
+ let actual = emitted_files.get("deno:///bundle.js");
+ assert!(actual.is_some());
+ let actual = actual.unwrap();
+ assert!(actual.contains("const b = \"b\";"));
+ assert!(actual.contains("console.log(b);"));
+ }
+
+ #[tokio::test]
+ async fn test_graph_info() {
+ let specifier =
+ ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
+ .expect("could not resolve module");
+ let (graph, _) = setup(specifier).await;
+ let info = graph.info().expect("could not get info");
+ assert!(info.compiled.is_none());
+ assert_eq!(info.dep_count, 6);
+ assert_eq!(info.file_type, MediaType::TypeScript);
+ assert_eq!(info.files.0.len(), 7);
+ assert!(info.local.to_string_lossy().ends_with("file_tests-main.ts"));
+ assert!(info.map.is_none());
+ assert_eq!(
+ info.module,
+ ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts").unwrap()
+ );
+ assert_eq!(info.total_size, 344);
+ }
+
+ #[tokio::test]
+ async fn test_graph_import_json() {
+ let specifier =
+ ModuleSpecifier::resolve_url_or_path("file:///tests/importjson.ts")
+ .expect("could not resolve module");
+ let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
+ let fixtures = c.join("tests/module_graph");
+ let handler = Rc::new(RefCell::new(MockSpecifierHandler {
+ fixtures,
+ ..MockSpecifierHandler::default()
+ }));
+ let mut builder = GraphBuilder::new(handler.clone(), None, 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 =
+ ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts")
+ .expect("could not resolve module");
+ let (mut graph, handler) = setup(specifier).await;
+ let (stats, maybe_ignored_options) =
+ graph.transpile(TranspileOptions::default()).unwrap();
+ assert_eq!(stats.0.len(), 3);
+ assert_eq!(maybe_ignored_options, None);
+ let h = handler.borrow();
+ assert_eq!(h.cache_calls.len(), 2);
+ 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,
+ ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts").unwrap()
+ );
+ assert_eq!(h.deps_calls[0].1.len(), 1);
+ assert_eq!(
+ h.deps_calls[1].0,
+ ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.js")
+ .unwrap()
+ );
+ assert_eq!(h.deps_calls[1].1.len(), 3);
+ assert_eq!(
+ h.deps_calls[2].0,
+ ModuleSpecifier::resolve_url_or_path("https://deno.land/x/lib/mod.d.ts")
+ .unwrap()
+ );
+ assert_eq!(h.deps_calls[2].1.len(), 3, "should have 3 dependencies");
+ // sometimes the calls are not deterministic, and so checking the contents
+ // can cause some failures
+ assert_eq!(h.deps_calls[3].1.len(), 0, "should have no dependencies");
+ assert_eq!(h.deps_calls[4].1.len(), 0, "should have no dependencies");
+ assert_eq!(h.deps_calls[5].1.len(), 0, "should have no dependencies");
+ assert_eq!(h.deps_calls[6].1.len(), 0, "should have no dependencies");
+ }
+
+ #[tokio::test]
+ async fn test_graph_transpile_user_config() {
+ let specifier =
+ ModuleSpecifier::resolve_url_or_path("https://deno.land/x/transpile.tsx")
+ .expect("could not resolve module");
+ let (mut graph, handler) = setup(specifier).await;
+ let (_, maybe_ignored_options) = graph
+ .transpile(TranspileOptions {
+ debug: false,
+ maybe_config_path: Some("tests/module_graph/tsconfig.json".to_string()),
+ reload: false,
+ })
+ .unwrap();
+ assert_eq!(
+ maybe_ignored_options.unwrap().items,
+ vec!["target".to_string()],
+ "the 'target' options should have been ignored"
+ );
+ let h = handler.borrow();
+ assert_eq!(h.cache_calls.len(), 1, "only one file should be emitted");
+ // FIXME(bartlomieju): had to add space in `<div>`, probably a quirk in swc_ecma_codegen
+ 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_with_lockfile() {
+ let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
+ let fixtures = c.join("tests/module_graph");
+ let lockfile_path = fixtures.join("lockfile.json");
+ let lockfile =
+ Lockfile::new(lockfile_path, false).expect("could not load lockfile");
+ let maybe_lockfile = Some(Arc::new(Mutex::new(lockfile)));
+ let handler = Rc::new(RefCell::new(MockSpecifierHandler {
+ fixtures,
+ ..MockSpecifierHandler::default()
+ }));
+ let mut builder = GraphBuilder::new(handler.clone(), None, maybe_lockfile);
+ let specifier =
+ ModuleSpecifier::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();
+ }
+}