2020-09-24 18:31:17 -04:00
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate ::ast ;
use crate ::ast ::parse ;
use crate ::ast ::Location ;
use crate ::ast ::ParsedModule ;
use crate ::file_fetcher ::TextDocument ;
use crate ::import_map ::ImportMap ;
use crate ::lockfile ::Lockfile ;
use crate ::media_type ::MediaType ;
use crate ::specifier_handler ::CachedModule ;
use crate ::specifier_handler ::DependencyMap ;
use crate ::specifier_handler ::EmitMap ;
use crate ::specifier_handler ::EmitType ;
use crate ::specifier_handler ::FetchFuture ;
use crate ::specifier_handler ::SpecifierHandler ;
use crate ::tsc_config ::IgnoredCompilerOptions ;
2020-09-29 03:16:12 -04:00
use crate ::tsc_config ::TsConfig ;
2020-09-30 07:46:42 -04:00
use crate ::version ;
2020-09-24 18:31:17 -04:00
use crate ::AnyError ;
use deno_core ::futures ::stream ::FuturesUnordered ;
use deno_core ::futures ::stream ::StreamExt ;
use deno_core ::serde_json ::json ;
use deno_core ::ModuleSpecifier ;
use regex ::Regex ;
use serde ::Deserialize ;
use serde ::Deserializer ;
use std ::cell ::RefCell ;
use std ::collections ::HashMap ;
use std ::collections ::HashSet ;
use std ::error ::Error ;
use std ::fmt ;
use std ::rc ::Rc ;
use std ::result ;
use std ::sync ::Mutex ;
use std ::time ::Instant ;
use swc_ecmascript ::dep_graph ::DependencyKind ;
pub type BuildInfoMap = HashMap < EmitType , TextDocument > ;
lazy_static! {
/// Matched the `@deno-types` pragma.
static ref DENO_TYPES_RE : Regex =
Regex ::new ( r # "(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"# )
. unwrap ( ) ;
/// Matches a `/// <reference ... />` comment reference.
static ref TRIPLE_SLASH_REFERENCE_RE : Regex =
Regex ::new ( r "(?i)^/\s*<reference\s.*?/>" ) . unwrap ( ) ;
/// Matches a path reference, which adds a dependency to a module
static ref PATH_REFERENCE_RE : Regex =
Regex ::new ( r # "(?i)\spath\s*=\s*["']([^"']*)["']"# ) . unwrap ( ) ;
/// Matches a types reference, which for JavaScript files indicates the
/// location of types to use when type checking a program that includes it as
/// a dependency.
static ref TYPES_REFERENCE_RE : Regex =
Regex ::new ( r # "(?i)\stypes\s*=\s*["']([^"']*)["']"# ) . unwrap ( ) ;
}
/// A group of errors that represent errors that can occur when interacting with
/// a module graph.
#[ allow(unused) ]
#[ derive(Debug, Clone, Eq, PartialEq) ]
pub enum GraphError {
/// A module using the HTTPS protocol is trying to import a module with an
/// HTTP schema.
InvalidDowngrade ( ModuleSpecifier , Location ) ,
/// A remote module is trying to import a local module.
InvalidLocalImport ( ModuleSpecifier , Location ) ,
/// A remote module is trying to import a local module.
InvalidSource ( ModuleSpecifier , String ) ,
/// A module specifier could not be resolved for a given import.
InvalidSpecifier ( String , Location ) ,
/// An unexpected dependency was requested for a module.
MissingDependency ( ModuleSpecifier , String ) ,
/// An unexpected specifier was requested.
MissingSpecifier ( ModuleSpecifier ) ,
/// Snapshot data was not present in a situation where it was required.
MissingSnapshotData ,
/// The current feature is not supported.
NotSupported ( String ) ,
}
use GraphError ::* ;
impl fmt ::Display for GraphError {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
match self {
InvalidDowngrade ( ref specifier , ref location ) = > write! ( f , " Modules imported via https are not allowed to import http modules. \n Importing: {} \n at {}:{}:{} " , specifier , location . filename , location . line , location . col ) ,
InvalidLocalImport ( ref specifier , ref location ) = > write! ( f , " Remote modules are not allowed to import local modules. \n Importing: {} \n at {}:{}:{} " , specifier , location . filename , location . line , location . col ) ,
InvalidSource ( ref specifier , ref lockfile ) = > write! ( f , " The source code is invalid, as it does not match the expected hash in the lock file. \n Specifier: {} \n Lock file: {} " , specifier , lockfile ) ,
InvalidSpecifier ( ref specifier , ref location ) = > write! ( f , " Unable to resolve dependency specifier. \n Specifier: {} \n at {}:{}:{} " , specifier , location . filename , location . line , location . col ) ,
MissingDependency ( ref referrer , specifier ) = > write! (
f ,
" The graph is missing a dependency. \n Specifier: {} from {} " ,
specifier , referrer
) ,
MissingSpecifier ( ref specifier ) = > write! (
f ,
" The graph is missing a specifier. \n Specifier: {} " ,
specifier
) ,
MissingSnapshotData = > write! ( f , " Snapshot data was not supplied, but required. " ) ,
NotSupported ( ref msg ) = > write! ( f , " {} " , msg ) ,
}
}
}
impl Error for GraphError { }
/// A trait, implemented by `Graph` that provides the interfaces that the
/// compiler ops require to be able to retrieve information about the graph.
pub trait ModuleProvider {
/// Get the source for a given module specifier. If the module is not part
/// of the graph, the result will be `None`.
fn get_source ( & self , specifier : & ModuleSpecifier ) -> Option < String > ;
/// 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.
fn resolve (
& self ,
specifier : & str ,
referrer : & ModuleSpecifier ,
2020-09-29 03:16:12 -04:00
) -> Result < ( ModuleSpecifier , MediaType ) , AnyError > ;
2020-09-24 18:31:17 -04:00
}
/// 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
}
}
2020-09-30 07:46:42 -04:00
/// A hashing function that takes the source code, version and optionally a
/// user provided config and generates a string hash which can be stored to
/// determine if the cached emit is valid or not.
fn get_version ( source : & TextDocument , version : & str , config : & [ u8 ] ) -> String {
crate ::checksum ::gen ( & [
source . to_str ( ) . unwrap ( ) . as_bytes ( ) ,
version . as_bytes ( ) ,
config ,
] )
}
2020-09-24 18:31:17 -04:00
/// A logical representation of a module within a graph.
#[ derive(Debug, Clone) ]
struct Module {
dependencies : DependencyMap ,
emits : EmitMap ,
is_dirty : bool ,
is_hydrated : bool ,
is_parsed : bool ,
maybe_import_map : Option < Rc < RefCell < ImportMap > > > ,
maybe_parsed_module : Option < ParsedModule > ,
maybe_types : Option < ( String , ModuleSpecifier ) > ,
2020-09-30 07:46:42 -04:00
maybe_version : Option < String > ,
2020-09-24 18:31:17 -04:00
media_type : MediaType ,
specifier : ModuleSpecifier ,
source : TextDocument ,
}
impl Default for Module {
fn default ( ) -> Self {
Module {
dependencies : HashMap ::new ( ) ,
emits : HashMap ::new ( ) ,
is_dirty : false ,
is_hydrated : false ,
is_parsed : false ,
maybe_import_map : None ,
maybe_parsed_module : None ,
maybe_types : None ,
2020-09-30 07:46:42 -04:00
maybe_version : None ,
2020-09-24 18:31:17 -04:00
media_type : MediaType ::Unknown ,
specifier : ModuleSpecifier ::resolve_url ( " https://deno.land/x/ " ) . unwrap ( ) ,
source : TextDocument ::new ( Vec ::new ( ) , Option ::< & str > ::None ) ,
}
}
}
impl Module {
pub fn new (
specifier : ModuleSpecifier ,
maybe_import_map : Option < Rc < RefCell < ImportMap > > > ,
) -> Self {
Module {
specifier ,
maybe_import_map ,
.. Module ::default ( )
}
}
2020-09-30 07:46:42 -04:00
/// Return `true` if the current hash of the module matches the stored
/// version.
pub fn emit_valid ( & self , config : & [ u8 ] ) -> bool {
if let Some ( version ) = self . maybe_version . clone ( ) {
version = = get_version ( & self . source , version ::DENO , config )
} else {
false
}
}
2020-09-24 18:31:17 -04:00
pub fn hydrate ( & mut self , cached_module : CachedModule ) {
self . media_type = cached_module . media_type ;
self . source = cached_module . source ;
if self . maybe_import_map . is_none ( ) {
if let Some ( dependencies ) = cached_module . maybe_dependencies {
self . dependencies = dependencies ;
self . is_parsed = true ;
}
}
self . maybe_types = if let Some ( ref specifier ) = cached_module . maybe_types {
Some ( (
specifier . clone ( ) ,
self
. resolve_import ( & specifier , None )
. expect ( " could not resolve module " ) ,
) )
} else {
None
} ;
self . is_dirty = false ;
self . emits = cached_module . emits ;
2020-09-30 07:46:42 -04:00
self . maybe_version = cached_module . maybe_version ;
2020-09-24 18:31:17 -04:00
self . is_hydrated = true ;
}
2020-09-29 03:16:12 -04:00
pub fn parse ( & mut self ) -> Result < ( ) , AnyError > {
2020-09-24 18:31:17 -04:00
let parsed_module =
parse ( & self . specifier , & self . source . to_str ( ) ? , & self . media_type ) ? ;
// parse out any triple slash references
for comment in parsed_module . get_leading_comments ( ) . iter ( ) {
if let Some ( ts_reference ) = parse_ts_reference ( & comment . text ) {
let location : Location = parsed_module . get_location ( & comment . span ) ;
match ts_reference {
TypeScriptReference ::Path ( import ) = > {
let specifier = self . resolve_import ( & import , Some ( location ) ) ? ;
let dep = self . dependencies . entry ( import ) . or_default ( ) ;
dep . maybe_code = Some ( specifier ) ;
}
TypeScriptReference ::Types ( import ) = > {
let specifier = self . resolve_import ( & import , Some ( location ) ) ? ;
if self . media_type = = MediaType ::JavaScript
| | self . media_type = = MediaType ::JSX
{
// TODO(kitsonk) we need to specifically update the cache when
// this value changes
self . maybe_types = Some ( ( import . clone ( ) , specifier ) ) ;
} else {
let dep = self . dependencies . entry ( import ) . or_default ( ) ;
dep . maybe_type = Some ( specifier ) ;
}
}
}
}
}
// Parse out all the syntactical dependencies for a module
let dependencies = parsed_module . analyze_dependencies ( ) ;
2020-09-27 14:16:18 -04:00
for desc in dependencies
. iter ( )
. filter ( | desc | desc . kind ! = DependencyKind ::Require )
{
2020-09-24 18:31:17 -04:00
let location = Location {
filename : self . specifier . to_string ( ) ,
col : desc . col ,
line : desc . line ,
} ;
let specifier =
self . resolve_import ( & desc . specifier , Some ( location . clone ( ) ) ) ? ;
// Parse out any `@deno-types` pragmas and modify dependency
let maybe_types_specifier = if ! desc . leading_comments . is_empty ( ) {
let comment = desc . leading_comments . last ( ) . unwrap ( ) ;
if let Some ( deno_types ) = parse_deno_types ( & comment . text ) . as_ref ( ) {
Some ( self . resolve_import ( deno_types , Some ( location ) ) ? )
} else {
None
}
} else {
None
} ;
let dep = self
. dependencies
. entry ( desc . specifier . to_string ( ) )
. or_default ( ) ;
if desc . kind = = DependencyKind ::ExportType
| | desc . kind = = DependencyKind ::ImportType
{
dep . maybe_type = Some ( specifier ) ;
} else {
dep . maybe_code = Some ( specifier ) ;
}
if let Some ( types_specifier ) = maybe_types_specifier {
dep . maybe_type = Some ( types_specifier ) ;
}
}
self . maybe_parsed_module = Some ( parsed_module ) ;
Ok ( ( ) )
}
fn resolve_import (
& self ,
specifier : & str ,
maybe_location : Option < Location > ,
2020-09-29 03:16:12 -04:00
) -> Result < ModuleSpecifier , AnyError > {
2020-09-24 18:31:17 -04:00
let maybe_resolve = if let Some ( import_map ) = self . maybe_import_map . clone ( )
{
import_map
. borrow ( )
. resolve ( specifier , self . specifier . as_str ( ) ) ?
} else {
None
} ;
let specifier = if let Some ( module_specifier ) = maybe_resolve {
module_specifier
} else {
ModuleSpecifier ::resolve_import ( specifier , self . specifier . as_str ( ) ) ?
} ;
let referrer_scheme = self . specifier . as_url ( ) . scheme ( ) ;
let specifier_scheme = specifier . as_url ( ) . scheme ( ) ;
let location = maybe_location . unwrap_or ( Location {
filename : self . specifier . to_string ( ) ,
line : 0 ,
col : 0 ,
} ) ;
// Disallow downgrades from HTTPS to HTTP
if referrer_scheme = = " https " & & specifier_scheme = = " http " {
return Err ( InvalidDowngrade ( specifier . clone ( ) , location ) . into ( ) ) ;
}
// Disallow a remote URL from trying to import a local URL
if ( referrer_scheme = = " https " | | referrer_scheme = = " http " )
& & ! ( specifier_scheme = = " https " | | specifier_scheme = = " http " )
{
return Err ( InvalidLocalImport ( specifier . clone ( ) , location ) . into ( ) ) ;
}
Ok ( specifier )
}
2020-09-30 07:46:42 -04:00
/// 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 ) )
}
2020-09-24 18:31:17 -04:00
}
#[ derive(Clone, Debug, PartialEq) ]
pub struct Stats ( Vec < ( String , u128 ) > ) ;
impl < ' de > Deserialize < ' de > for Stats {
fn deserialize < D > ( deserializer : D ) -> result ::Result < Self , D ::Error >
where
D : Deserializer < ' de > ,
{
let items : Vec < ( String , u128 ) > = Deserialize ::deserialize ( deserializer ) ? ;
Ok ( Stats ( items ) )
}
}
impl fmt ::Display for Stats {
fn fmt ( & self , f : & mut fmt ::Formatter ) -> fmt ::Result {
for ( key , value ) in self . 0. clone ( ) {
write! ( f , " {}: {} " , key , value ) ? ;
}
Ok ( ( ) )
}
}
/// A structure which provides options when transpiling modules.
#[ derive(Debug, Default) ]
pub struct TranspileOptions {
/// If `true` then debug logging will be output from the isolate.
pub debug : bool ,
2020-09-29 03:16:12 -04:00
/// 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 > ,
2020-09-24 18:31:17 -04:00
}
/// A dependency graph of modules, were the modules that have been inserted via
/// the builder will be loaded into the graph. Also provides an interface to
/// be able to manipulate and handle the graph.
#[ derive(Debug) ]
pub struct Graph {
build_info : BuildInfoMap ,
handler : Rc < RefCell < dyn SpecifierHandler > > ,
modules : HashMap < ModuleSpecifier , Module > ,
roots : Vec < ModuleSpecifier > ,
}
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 > > ) -> Self {
Graph {
build_info : HashMap ::new ( ) ,
handler ,
modules : HashMap ::new ( ) ,
roots : Vec ::new ( ) ,
}
}
/// Update the handler with any modules that are marked as _dirty_ and update
/// any build info if present.
2020-09-29 03:16:12 -04:00
fn flush ( & mut self , emit_type : & EmitType ) -> Result < ( ) , AnyError > {
2020-09-24 18:31:17 -04:00
let mut handler = self . handler . borrow_mut ( ) ;
for ( _ , module ) in self . modules . iter_mut ( ) {
if module . is_dirty {
let ( code , maybe_map ) = module . emits . get ( emit_type ) . unwrap ( ) ;
handler . set_cache (
& module . specifier ,
& emit_type ,
code . clone ( ) ,
maybe_map . clone ( ) ,
) ? ;
module . is_dirty = false ;
2020-09-30 07:46:42 -04:00
if let Some ( version ) = & module . maybe_version {
handler . set_version ( & module . specifier , version . clone ( ) ) ? ;
}
2020-09-24 18:31:17 -04:00
}
}
for root_specifier in self . roots . iter ( ) {
if let Some ( build_info ) = self . build_info . get ( & emit_type ) {
handler . set_build_info (
root_specifier ,
& emit_type ,
build_info . to_owned ( ) ,
) ? ;
}
}
Ok ( ( ) )
}
/// Verify the subresource integrity of the graph based upon the optional
/// lockfile, updating the lockfile with any missing resources. This will
/// error if any of the resources do not match their lock status.
2020-09-29 03:16:12 -04:00
pub fn lock (
& self ,
maybe_lockfile : & Option < Mutex < Lockfile > > ,
) -> Result < ( ) , AnyError > {
2020-09-24 18:31:17 -04:00
if let Some ( lf ) = maybe_lockfile {
let mut lockfile = lf . lock ( ) . unwrap ( ) ;
for ( ms , module ) in self . modules . iter ( ) {
let specifier = module . specifier . to_string ( ) ;
let code = module . source . to_string ( ) ? ;
let valid = lockfile . check_or_insert ( & specifier , & code ) ;
if ! valid {
return Err (
InvalidSource ( ms . clone ( ) , lockfile . filename . clone ( ) ) . into ( ) ,
) ;
}
}
}
Ok ( ( ) )
}
/// Transpile (only transform) the graph, updating any emitted modules
/// with the specifier handler. The result contains any performance stats
/// from the compiler and optionally any user provided configuration compiler
/// options that were ignored.
///
/// # Arguments
///
/// - `options` - A structure of options which impact how the code is
/// transpiled.
///
pub fn transpile (
& mut self ,
options : TranspileOptions ,
2020-09-29 03:16:12 -04:00
) -> Result < ( Stats , Option < IgnoredCompilerOptions > ) , AnyError > {
2020-09-24 18:31:17 -04:00
let start = Instant ::now ( ) ;
let emit_type = EmitType ::Cli ;
2020-09-29 03:16:12 -04:00
let mut ts_config = TsConfig ::new ( json! ( {
2020-09-24 18:31:17 -04:00
" checkJs " : false ,
" emitDecoratorMetadata " : false ,
" jsx " : " react " ,
" jsxFactory " : " React.createElement " ,
" jsxFragmentFactory " : " React.Fragment " ,
2020-09-29 03:16:12 -04:00
} ) ) ;
2020-09-24 18:31:17 -04:00
2020-09-29 03:16:12 -04:00
let maybe_ignored_options =
ts_config . merge_user_config ( options . maybe_config_path ) ? ;
2020-09-24 18:31:17 -04:00
2020-09-29 03:16:12 -04:00
let compiler_options = ts_config . as_transpile_config ( ) ? ;
2020-09-24 18:31:17 -04:00
let check_js = compiler_options . check_js ;
let transform_jsx = compiler_options . jsx = = " react " ;
let emit_options = ast ::TranspileOptions {
emit_metadata : compiler_options . emit_decorator_metadata ,
inline_source_map : true ,
jsx_factory : compiler_options . jsx_factory ,
jsx_fragment_factory : compiler_options . jsx_fragment_factory ,
transform_jsx ,
} ;
let mut emit_count : u128 = 0 ;
for ( _ , module ) in self . modules . iter_mut ( ) {
2020-09-30 07:46:42 -04:00
// 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".
2020-09-24 18:31:17 -04:00
// if the module is a Dts file we should skip it
if module . media_type = = MediaType ::Dts {
continue ;
}
// if we don't have check_js enabled, we won't touch non TypeScript
// modules
if ! ( check_js
| | module . media_type = = MediaType ::TSX
| | module . media_type = = MediaType ::TypeScript )
{
continue ;
}
2020-09-30 07:46:42 -04:00
let config = ts_config . as_bytes ( ) ;
// skip modules that already have a valid emit
if module . emits . contains_key ( & emit_type ) & & module . emit_valid ( & config ) {
continue ;
}
2020-09-24 18:31:17 -04:00
if module . maybe_parsed_module . is_none ( ) {
module . parse ( ) ? ;
}
let parsed_module = module . maybe_parsed_module . clone ( ) . unwrap ( ) ;
let emit = parsed_module . transpile ( & emit_options ) ? ;
emit_count + = 1 ;
module . emits . insert ( emit_type . clone ( ) , emit ) ;
2020-09-30 07:46:42 -04:00
module . set_version ( & config ) ;
2020-09-24 18:31:17 -04:00
module . is_dirty = true ;
}
self . flush ( & emit_type ) ? ;
let stats = Stats ( vec! [
( " Files " . to_string ( ) , self . modules . len ( ) as u128 ) ,
( " Emitted " . to_string ( ) , emit_count ) ,
( " Total time " . to_string ( ) , start . elapsed ( ) . as_millis ( ) ) ,
] ) ;
Ok ( ( stats , maybe_ignored_options ) )
}
}
impl < ' a > ModuleProvider for Graph {
fn get_source ( & self , specifier : & ModuleSpecifier ) -> Option < String > {
if let Some ( module ) = self . modules . get ( specifier ) {
if let Ok ( source ) = module . source . to_string ( ) {
Some ( source )
} else {
None
}
} else {
None
}
}
fn resolve (
& self ,
specifier : & str ,
referrer : & ModuleSpecifier ,
2020-09-29 03:16:12 -04:00
) -> Result < ( ModuleSpecifier , MediaType ) , AnyError > {
2020-09-24 18:31:17 -04:00
if ! self . modules . contains_key ( referrer ) {
return Err ( MissingSpecifier ( referrer . to_owned ( ) ) . into ( ) ) ;
}
let module = self . modules . get ( referrer ) . unwrap ( ) ;
if ! module . dependencies . contains_key ( specifier ) {
return Err (
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 let Some ( type_specifier ) = dependency . maybe_type . clone ( ) {
type_specifier
} else if let Some ( code_specifier ) = dependency . maybe_code . clone ( ) {
code_specifier
} else {
return Err (
MissingDependency ( referrer . to_owned ( ) , specifier . to_owned ( ) ) . into ( ) ,
) ;
} ;
if ! self . modules . contains_key ( & resolved_specifier ) {
return Err (
MissingDependency ( referrer . to_owned ( ) , resolved_specifier . to_string ( ) )
. into ( ) ,
) ;
}
let dep_module = self . modules . get ( & 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 let Some ( ( _ , types ) ) = dep_module . maybe_types . clone ( ) {
if let Some ( types_module ) = self . modules . get ( & types ) {
( types , types_module . media_type )
} else {
return Err (
MissingDependency ( referrer . to_owned ( ) , types . to_string ( ) ) . into ( ) ,
) ;
}
} else {
( resolved_specifier , dep_module . media_type )
} ;
Ok ( result )
}
}
/// 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 > ,
) -> 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 ) ,
fetched : HashSet ::new ( ) ,
maybe_import_map : internal_import_map ,
pending : FuturesUnordered ::new ( ) ,
}
}
/// Request a module to be fetched from the handler and queue up its future
/// to be awaited to be resolved.
2020-09-29 03:16:12 -04:00
fn fetch ( & mut self , specifier : & ModuleSpecifier ) -> Result < ( ) , AnyError > {
2020-09-24 18:31:17 -04:00
if self . fetched . contains ( & specifier ) {
return Ok ( ( ) ) ;
}
self . fetched . insert ( specifier . clone ( ) ) ;
let future = self . graph . handler . borrow_mut ( ) . fetch ( specifier . clone ( ) ) ;
self . pending . push ( future ) ;
Ok ( ( ) )
}
/// Visit a module that has been fetched, hydrating the module, analyzing its
/// dependencies if required, fetching those dependencies, and inserting the
/// module into the graph.
2020-09-29 03:16:12 -04:00
fn visit ( & mut self , cached_module : CachedModule ) -> Result < ( ) , AnyError > {
2020-09-24 18:31:17 -04:00
let specifier = cached_module . specifier . clone ( ) ;
let mut module =
Module ::new ( specifier . clone ( ) , self . maybe_import_map . clone ( ) ) ;
module . hydrate ( cached_module ) ;
if ! module . is_parsed {
let has_types = module . maybe_types . is_some ( ) ;
module . parse ( ) ? ;
if self . maybe_import_map . is_none ( ) {
let mut handler = self . graph . handler . borrow_mut ( ) ;
handler . set_deps ( & specifier , module . dependencies . clone ( ) ) ? ;
if ! has_types {
if let Some ( ( types , _ ) ) = module . maybe_types . clone ( ) {
handler . set_types ( & specifier , types ) ? ;
}
}
}
}
for ( _ , dep ) in module . dependencies . iter ( ) {
if let Some ( specifier ) = dep . maybe_code . as_ref ( ) {
self . fetch ( specifier ) ? ;
}
if let Some ( specifier ) = dep . maybe_type . as_ref ( ) {
self . fetch ( specifier ) ? ;
}
}
if let Some ( ( _ , specifier ) ) = module . maybe_types . as_ref ( ) {
self . fetch ( specifier ) ? ;
}
self . graph . modules . insert ( specifier , module ) ;
Ok ( ( ) )
}
/// Insert a module into the graph based on a module specifier. The module
/// and any dependencies will be fetched from the handler. The module will
/// also be treated as a _root_ module in the graph.
2020-09-29 03:16:12 -04:00
pub async fn insert (
& mut self ,
specifier : & ModuleSpecifier ,
) -> Result < ( ) , AnyError > {
2020-09-24 18:31:17 -04:00
self . fetch ( specifier ) ? ;
loop {
let cached_module = self . pending . next ( ) . await . unwrap ( ) ? ;
self . visit ( cached_module ) ? ;
if self . pending . is_empty ( ) {
break ;
}
}
if ! self . graph . roots . contains ( specifier ) {
self . graph . roots . push ( specifier . clone ( ) ) ;
}
Ok ( ( ) )
}
/// Move out the graph from the builder to be utilized further. An optional
/// lockfile can be provided, where if the sources in the graph do not match
/// the expected lockfile, the method with error instead of returning the
/// graph.
pub fn get_graph (
self ,
maybe_lockfile : & Option < Mutex < Lockfile > > ,
2020-09-29 03:16:12 -04:00
) -> Result < Graph , AnyError > {
2020-09-24 18:31:17 -04:00
self . graph . lock ( maybe_lockfile ) ? ;
Ok ( self . graph )
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use crate ::specifier_handler ::tests ::MockSpecifierHandler ;
use std ::env ;
use std ::path ::PathBuf ;
use std ::sync ::Mutex ;
2020-09-30 07:46:42 -04:00
#[ test ]
fn test_get_version ( ) {
let doc_a =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let version_a = get_version ( & doc_a , " 1.2.3 " , b " " ) ;
let doc_b =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let version_b = get_version ( & doc_b , " 1.2.3 " , b " " ) ;
assert_eq! ( version_a , version_b ) ;
let version_c = get_version ( & doc_a , " 1.2.3 " , b " options " ) ;
assert_ne! ( version_a , version_c ) ;
let version_d = get_version ( & doc_b , " 1.2.3 " , b " options " ) ;
assert_eq! ( version_c , version_d ) ;
let version_e = get_version ( & doc_a , " 1.2.4 " , b " " ) ;
assert_ne! ( version_a , version_e ) ;
let version_f = get_version ( & doc_b , " 1.2.4 " , b " " ) ;
assert_eq! ( version_e , version_f ) ;
}
#[ test ]
fn test_module_emit_valid ( ) {
let source =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let maybe_version = Some ( get_version ( & source , version ::DENO , b " " ) ) ;
let module = Module {
source ,
maybe_version ,
.. Module ::default ( )
} ;
assert! ( module . emit_valid ( b " " ) ) ;
let source =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let old_source =
TextDocument ::new ( b " console.log(43); " . to_vec ( ) , Option ::< & str > ::None ) ;
let maybe_version = Some ( get_version ( & old_source , version ::DENO , b " " ) ) ;
let module = Module {
source ,
maybe_version ,
.. Module ::default ( )
} ;
assert! ( ! module . emit_valid ( b " " ) ) ;
let source =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let maybe_version = Some ( get_version ( & source , " 0.0.0 " , b " " ) ) ;
let module = Module {
source ,
maybe_version ,
.. Module ::default ( )
} ;
assert! ( ! module . emit_valid ( b " " ) ) ;
let source =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let module = Module {
source ,
.. Module ::default ( )
} ;
assert! ( ! module . emit_valid ( b " " ) ) ;
}
#[ test ]
fn test_module_set_version ( ) {
let source =
TextDocument ::new ( b " console.log(42); " . to_vec ( ) , Option ::< & str > ::None ) ;
let expected = Some ( get_version ( & source , version ::DENO , b " " ) ) ;
let mut module = Module {
source ,
.. Module ::default ( )
} ;
assert! ( module . maybe_version . is_none ( ) ) ;
module . set_version ( b " " ) ;
assert_eq! ( module . maybe_version , expected ) ;
}
2020-09-24 18:31:17 -04:00
#[ tokio::test ]
async fn test_graph_builder ( ) {
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 , None ) ;
let specifier =
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/mod.ts " )
. expect ( " could not resolve module " ) ;
builder
. insert ( & specifier )
. await
. expect ( " module not inserted " ) ;
let graph = builder . get_graph ( & None ) . expect ( " error getting graph " ) ;
let actual = graph
. resolve ( " ./a.ts " , & specifier )
. expect ( " module to resolve " ) ;
let expected = (
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/a.ts " )
. expect ( " unable to resolve " ) ,
MediaType ::TypeScript ,
) ;
assert_eq! ( actual , expected ) ;
}
#[ tokio::test ]
async fn test_graph_builder_import_map ( ) {
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 import_map = ImportMap ::from_json (
" https://deno.land/x/import_map.ts " ,
r #" {
" imports " : {
" jquery " : " ./jquery.js " ,
" lodash " : " https://unpkg.com/lodash/index.js "
}
} " #,
)
. expect ( " could not load import map " ) ;
let mut builder = GraphBuilder ::new ( handler , Some ( import_map ) ) ;
let specifier =
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/import_map.ts " )
. expect ( " could not resolve module " ) ;
builder
. insert ( & specifier )
. await
. expect ( " module not inserted " ) ;
let graph = builder . get_graph ( & None ) . expect ( " could not get graph " ) ;
let actual_jquery = graph
. resolve ( " jquery " , & specifier )
. expect ( " module to resolve " ) ;
let expected_jquery = (
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/jquery.js " )
. expect ( " unable to resolve " ) ,
MediaType ::JavaScript ,
) ;
assert_eq! ( actual_jquery , expected_jquery ) ;
let actual_lodash = graph
. resolve ( " lodash " , & specifier )
. expect ( " module to resolve " ) ;
let expected_lodash = (
ModuleSpecifier ::resolve_url_or_path ( " https://unpkg.com/lodash/index.js " )
. expect ( " unable to resolve " ) ,
MediaType ::JavaScript ,
) ;
assert_eq! ( actual_lodash , expected_lodash ) ;
}
#[ tokio::test ]
async fn test_graph_transpile ( ) {
// This is a complex scenario of transpiling, where we have TypeScript
// importing a JavaScript file (with type definitions) which imports
// TypeScript, JavaScript, and JavaScript with type definitions.
// For scenarios where we transpile, we only want the TypeScript files
// to be actually emitted.
//
// This also exercises "@deno-types" and type references.
let c = PathBuf ::from ( env ::var_os ( " CARGO_MANIFEST_DIR " ) . unwrap ( ) ) ;
let fixtures = c . join ( " tests/module_graph " ) ;
let handler = Rc ::new ( RefCell ::new ( MockSpecifierHandler {
fixtures ,
.. MockSpecifierHandler ::default ( )
} ) ) ;
let mut builder = GraphBuilder ::new ( handler . clone ( ) , None ) ;
let specifier =
ModuleSpecifier ::resolve_url_or_path ( " file:///tests/main.ts " )
. expect ( " could not resolve module " ) ;
builder
. insert ( & specifier )
. await
. expect ( " module not inserted " ) ;
let mut graph = builder . get_graph ( & None ) . expect ( " could not get graph " ) ;
let ( stats , maybe_ignored_options ) =
graph . transpile ( TranspileOptions ::default ( ) ) . unwrap ( ) ;
assert_eq! ( stats . 0. len ( ) , 3 ) ;
assert_eq! ( maybe_ignored_options , None ) ;
let h = handler . borrow ( ) ;
assert_eq! ( h . cache_calls . len ( ) , 2 ) ;
assert_eq! ( h . cache_calls [ 0 ] . 1 , EmitType ::Cli ) ;
assert! ( h . cache_calls [ 0 ]
. 2
. to_string ( )
. unwrap ( )
. contains ( " # sourceMappingURL=data:application/json;base64, " ) ) ;
assert_eq! ( h . cache_calls [ 0 ] . 3 , None ) ;
assert_eq! ( h . cache_calls [ 1 ] . 1 , EmitType ::Cli ) ;
assert! ( h . cache_calls [ 1 ]
. 2
. to_string ( )
. unwrap ( )
. contains ( " # sourceMappingURL=data:application/json;base64, " ) ) ;
assert_eq! ( h . cache_calls [ 0 ] . 3 , None ) ;
assert_eq! ( h . deps_calls . len ( ) , 7 ) ;
assert_eq! (
h . deps_calls [ 0 ] . 0 ,
ModuleSpecifier ::resolve_url_or_path ( " file:///tests/main.ts " ) . unwrap ( )
) ;
assert_eq! ( h . deps_calls [ 0 ] . 1. len ( ) , 1 ) ;
assert_eq! (
h . deps_calls [ 1 ] . 0 ,
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/lib/mod.js " )
. unwrap ( )
) ;
assert_eq! ( h . deps_calls [ 1 ] . 1. len ( ) , 3 ) ;
assert_eq! (
h . deps_calls [ 2 ] . 0 ,
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/lib/mod.d.ts " )
. unwrap ( )
) ;
assert_eq! ( h . deps_calls [ 2 ] . 1. len ( ) , 3 , " should have 3 dependencies " ) ;
// sometimes the calls are not deterministic, and so checking the contents
// can cause some failures
assert_eq! ( h . deps_calls [ 3 ] . 1. len ( ) , 0 , " should have no dependencies " ) ;
assert_eq! ( h . deps_calls [ 4 ] . 1. len ( ) , 0 , " should have no dependencies " ) ;
assert_eq! ( h . deps_calls [ 5 ] . 1. len ( ) , 0 , " should have no dependencies " ) ;
assert_eq! ( h . deps_calls [ 6 ] . 1. len ( ) , 0 , " should have no dependencies " ) ;
}
#[ tokio::test ]
async fn test_graph_transpile_user_config ( ) {
let c = PathBuf ::from ( env ::var_os ( " CARGO_MANIFEST_DIR " ) . unwrap ( ) ) ;
let fixtures = c . join ( " tests/module_graph " ) ;
let handler = Rc ::new ( RefCell ::new ( MockSpecifierHandler {
2020-09-29 03:16:12 -04:00
fixtures : fixtures . clone ( ) ,
2020-09-24 18:31:17 -04:00
.. MockSpecifierHandler ::default ( )
} ) ) ;
let mut builder = GraphBuilder ::new ( handler . clone ( ) , None ) ;
let specifier =
ModuleSpecifier ::resolve_url_or_path ( " https://deno.land/x/transpile.tsx " )
. expect ( " could not resolve module " ) ;
builder
. insert ( & specifier )
. await
. expect ( " module not inserted " ) ;
let mut graph = builder . get_graph ( & None ) . expect ( " could not get graph " ) ;
let ( _ , maybe_ignored_options ) = graph
. transpile ( TranspileOptions {
debug : false ,
2020-09-29 03:16:12 -04:00
maybe_config_path : Some ( " tests/module_graph/tsconfig.json " . to_string ( ) ) ,
2020-09-24 18:31:17 -04:00
} )
. unwrap ( ) ;
assert_eq! (
2020-09-29 03:16:12 -04:00
maybe_ignored_options . unwrap ( ) . items ,
vec! [ " target " . to_string ( ) ] ,
2020-09-24 18:31:17 -04:00
" the 'target' options should have been ignored "
) ;
let h = handler . borrow ( ) ;
assert_eq! ( h . cache_calls . len ( ) , 1 , " only one file should be emitted " ) ;
2020-10-02 07:51:37 -04:00
// FIXME(bartlomieju): had to add space in `<div>`, probably a quirk in swc_ecma_codegen
2020-09-24 18:31:17 -04:00
assert! (
h . cache_calls [ 0 ]
. 2
. to_string ( )
. unwrap ( )
2020-10-02 07:51:37 -04:00
. contains ( " <div >Hello world!</div> " ) ,
2020-09-24 18:31:17 -04:00
" jsx should have been preserved "
) ;
}
#[ tokio::test ]
async fn test_graph_with_lockfile ( ) {
let c = PathBuf ::from ( env ::var_os ( " CARGO_MANIFEST_DIR " ) . unwrap ( ) ) ;
let fixtures = c . join ( " tests/module_graph " ) ;
let lockfile_path = fixtures . join ( " lockfile.json " ) ;
let lockfile =
Lockfile ::new ( lockfile_path . to_string_lossy ( ) . to_string ( ) , false )
. expect ( " could not load lockfile " ) ;
let maybe_lockfile = Some ( Mutex ::new ( lockfile ) ) ;
let handler = Rc ::new ( RefCell ::new ( MockSpecifierHandler {
fixtures ,
.. MockSpecifierHandler ::default ( )
} ) ) ;
let mut builder = GraphBuilder ::new ( handler . clone ( ) , None ) ;
let specifier =
ModuleSpecifier ::resolve_url_or_path ( " file:///tests/main.ts " )
. expect ( " could not resolve module " ) ;
builder
. insert ( & specifier )
. await
. expect ( " module not inserted " ) ;
builder
. get_graph ( & maybe_lockfile )
. expect ( " could not get graph " ) ;
}
#[ tokio::test ]
async fn test_graph_with_lockfile_fail ( ) {
let c = PathBuf ::from ( env ::var_os ( " CARGO_MANIFEST_DIR " ) . unwrap ( ) ) ;
let fixtures = c . join ( " tests/module_graph " ) ;
let lockfile_path = fixtures . join ( " lockfile_fail.json " ) ;
let lockfile =
Lockfile ::new ( lockfile_path . to_string_lossy ( ) . to_string ( ) , false )
. expect ( " could not load lockfile " ) ;
let maybe_lockfile = Some ( Mutex ::new ( lockfile ) ) ;
let handler = Rc ::new ( RefCell ::new ( MockSpecifierHandler {
fixtures ,
.. MockSpecifierHandler ::default ( )
} ) ) ;
let mut builder = GraphBuilder ::new ( handler . clone ( ) , None ) ;
let specifier =
ModuleSpecifier ::resolve_url_or_path ( " file:///tests/main.ts " )
. expect ( " could not resolve module " ) ;
builder
. insert ( & specifier )
. await
. expect ( " module not inserted " ) ;
builder
. get_graph ( & maybe_lockfile )
. expect_err ( " expected an error " ) ;
}
}