deno/cli/module_graph.rs

2210 lines
71 KiB
Rust

// 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();
}
}