diff --git a/cli/ast.rs b/cli/ast.rs index 95f2437172..78cafca1be 100644 --- a/cli/ast.rs +++ b/cli/ast.rs @@ -72,6 +72,18 @@ impl Into for swc_common::Loc { } } +impl Into for Location { + fn into(self) -> ModuleSpecifier { + ModuleSpecifier::resolve_url_or_path(&self.filename).unwrap() + } +} + +impl std::fmt::Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}:{}:{}", self.filename, self.line, self.col) + } +} + /// A buffer for collecting diagnostic messages from the AST parser. #[derive(Debug)] pub struct DiagnosticBuffer(Vec); diff --git a/cli/disk_cache.rs b/cli/disk_cache.rs index 8978065de5..1fc9b32898 100644 --- a/cli/disk_cache.rs +++ b/cli/disk_cache.rs @@ -107,8 +107,9 @@ impl DiskCache { } scheme => { unimplemented!( - "Don't know how to create cache name for scheme: {}", - scheme + "Don't know how to create cache name for scheme: {}\n Url: {}", + scheme, + url ); } }; diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index 25e9e88355..c0a9c7227d 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -579,6 +579,29 @@ fn map_js_like_extension(path: &Path, default: MediaType) -> MediaType { None => default, Some("jsx") => MediaType::JSX, Some("tsx") => MediaType::TSX, + // Because DTS files do not have a separate media type, or a unique + // extension, we have to "guess" at those things that we consider that + // look like TypeScript, and end with `.d.ts` are DTS files. + Some("ts") => { + if default == MediaType::TypeScript { + match path.file_stem() { + None => default, + Some(os_str) => { + if let Some(file_stem) = os_str.to_str() { + if file_stem.ends_with(".d") { + MediaType::Dts + } else { + default + } + } else { + default + } + } + } + } else { + default + } + } Some(_) => default, }, } @@ -1564,7 +1587,7 @@ mod tests { ); assert_eq!( map_content_type(Path::new("foo/bar.d.ts"), None).0, - MediaType::TypeScript + MediaType::Dts ); assert_eq!( map_content_type(Path::new("foo/bar.js"), None).0, @@ -1741,6 +1764,26 @@ mod tests { .0, MediaType::JSX ); + assert_eq!( + map_content_type( + Path::new("foo/bar.d.ts"), + Some("application/x-javascript") + ) + .0, + MediaType::JavaScript + ); + assert_eq!( + map_content_type(Path::new("foo/bar.d.ts"), Some("text/plain")).0, + MediaType::Dts + ); + assert_eq!( + map_content_type( + Path::new("foo/bar.d.ts"), + Some("video/vnd.dlna.mpeg-tts"), + ) + .0, + MediaType::Dts + ); } #[test] diff --git a/cli/fmt_errors.rs b/cli/fmt_errors.rs index 333c477567..890f9b83f2 100644 --- a/cli/fmt_errors.rs +++ b/cli/fmt_errors.rs @@ -7,6 +7,7 @@ use deno_core::error::{AnyError, JsError as CoreJsError, JsStackFrame}; use std::error::Error; use std::fmt; use std::ops::Deref; +use std::sync::Arc; const SOURCE_ABBREV_THRESHOLD: usize = 150; @@ -237,7 +238,7 @@ pub struct JsError(CoreJsError); impl JsError { pub fn create( core_js_error: CoreJsError, - source_map_getter: &impl SourceMapGetter, + source_map_getter: Arc, ) -> AnyError { let core_js_error = apply_source_map(&core_js_error, source_map_getter); let js_error = Self(core_js_error); diff --git a/cli/main.rs b/cli/main.rs index 56e3c19f83..51355555ee 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -51,7 +51,7 @@ mod test_runner; mod text_encoding; mod tokio_util; mod tsc; -pub mod tsc2; +mod tsc2; mod tsc_config; mod upgrade; mod version; @@ -174,14 +174,16 @@ async fn info_command( let specifier = ModuleSpecifier::resolve_url_or_path(&specifier)?; let handler = Rc::new(RefCell::new(specifier_handler::FetchHandler::new( &program_state, + // info accesses dynamically imported modules just for their information + // so we allow access to all of them. Permissions::allow_all(), )?)); let mut builder = module_graph2::GraphBuilder2::new( handler, program_state.maybe_import_map.clone(), ); - builder.insert(&specifier).await?; - let graph = builder.get_graph(&program_state.lockfile)?; + builder.add(&specifier, false).await?; + let graph = builder.get_graph(&program_state.lockfile); let info = graph.info()?; if json { @@ -312,14 +314,16 @@ async fn bundle_command( let output = if flags.no_check { let handler = Rc::new(RefCell::new(FetchHandler::new( &program_state, + // when bundling, dynamic imports are only access for their type safety, + // therefore we will allow the graph to access any module. Permissions::allow_all(), )?)); let mut builder = module_graph2::GraphBuilder2::new( handler, program_state.maybe_import_map.clone(), ); - builder.insert(&module_specifier).await?; - let graph = builder.get_graph(&program_state.lockfile)?; + builder.add(&module_specifier, false).await?; + let graph = builder.get_graph(&program_state.lockfile); let (s, stats, maybe_ignored_options) = graph.bundle(module_graph2::BundleOptions { diff --git a/cli/media_type.rs b/cli/media_type.rs index c3c2f8e238..7d63439f6a 100644 --- a/cli/media_type.rs +++ b/cli/media_type.rs @@ -77,7 +77,19 @@ impl MediaType { }, }, Some(os_str) => match os_str.to_str() { - Some("ts") => MediaType::TypeScript, + Some("ts") => match path.file_stem() { + Some(os_str) => match os_str.to_str() { + Some(file_name) => { + if file_name.ends_with(".d") { + MediaType::Dts + } else { + MediaType::TypeScript + } + } + None => MediaType::TypeScript, + }, + None => MediaType::TypeScript, + }, Some("tsx") => MediaType::TSX, Some("js") => MediaType::JavaScript, Some("jsx") => MediaType::JSX, @@ -121,6 +133,19 @@ impl MediaType { ext.into() } + + /// Map the media type to a `ts.ScriptKind` + pub fn as_ts_script_kind(&self) -> i32 { + match self { + MediaType::JavaScript => 1, + MediaType::JSX => 2, + MediaType::TypeScript => 3, + MediaType::Dts => 3, + MediaType::TSX => 4, + MediaType::Json => 5, + _ => 0, + } + } } impl Serialize for MediaType { @@ -167,10 +192,7 @@ mod tests { MediaType::TypeScript ); assert_eq!(MediaType::from(Path::new("foo/bar.tsx")), MediaType::TSX); - assert_eq!( - MediaType::from(Path::new("foo/bar.d.ts")), - MediaType::TypeScript - ); + assert_eq!(MediaType::from(Path::new("foo/bar.d.ts")), MediaType::Dts); assert_eq!( MediaType::from(Path::new("foo/bar.js")), MediaType::JavaScript diff --git a/cli/module_graph2.rs b/cli/module_graph2.rs index e2dcdfefc1..678fe8da50 100644 --- a/cli/module_graph2.rs +++ b/cli/module_graph2.rs @@ -6,18 +6,24 @@ use crate::ast::BundleHook; use crate::ast::EmitOptions; 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::tsc2::exec; +use crate::tsc2::Request; use crate::tsc_config::IgnoredCompilerOptions; use crate::tsc_config::TsConfig; use crate::version; @@ -26,7 +32,10 @@ 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::ModuleResolutionError; use deno_core::ModuleSpecifier; use regex::Regex; use serde::Deserialize; @@ -62,7 +71,6 @@ lazy_static! { /// 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 @@ -70,40 +78,37 @@ pub enum GraphError { 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), + /// 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), - /// Snapshot data was not present in a situation where it was required. - MissingSnapshotData, /// The current feature is not supported. NotSupported(String), + /// A unsupported media type was attempted to be imported as a module. + UnsupportedImportType(ModuleSpecifier, MediaType), } -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!( + 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 ), - MissingSpecifier(ref specifier) => write!( + GraphError::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), + 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), } } } @@ -155,7 +160,10 @@ impl swc_bundler::Load for BundleLoader<'_> { self.cm.clone(), ) } else { - Err(MissingDependency(specifier, "".to_string()).into()) + Err( + GraphError::MissingDependency(specifier, "".to_string()) + .into(), + ) } } _ => unreachable!("Received request for unsupported filename {:?}", file), @@ -252,12 +260,24 @@ impl Default for Module { impl Module { pub fn new( cached_module: CachedModule, + is_root: bool, maybe_import_map: Option>>, ) -> 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: cached_module.media_type, + media_type, source: cached_module.source, source_path: cached_module.source_path, maybe_emit: cached_module.maybe_emit, @@ -296,21 +316,28 @@ impl Module { } } + /// 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, &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: Location = parsed_module.get_location(&comment.span); + let 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(); + 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))?; + let specifier = + self.resolve_import(&import, Some(location.clone()))?; if self.media_type == MediaType::JavaScript || self.media_type == MediaType::JSX { @@ -318,7 +345,10 @@ impl Module { // this value changes self.maybe_types = Some((import.clone(), specifier)); } else { - let dep = self.dependencies.entry(import).or_default(); + let dep = self + .dependencies + .entry(import) + .or_insert_with(|| Dependency::new(location)); dep.maybe_type = Some(specifier); } } @@ -336,14 +366,30 @@ impl Module { col: desc.col, line: desc.line, }; - let specifier = - self.resolve_import(&desc.specifier, Some(location.clone()))?; + + // 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::() { + Some(ModuleResolutionError::ImportPrefixMissing(_, _)) => None, + _ => { + return Err(any_error); + } + } + } + }; // Parse out any `@deno-types` pragmas and modify dependency - let maybe_types_specifier = if !desc.leading_comments.is_empty() { + 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))?) + Some(self.resolve_import(deno_types, Some(location.clone()))?) } else { None } @@ -354,16 +400,21 @@ impl Module { let dep = self .dependencies .entry(desc.specifier.to_string()) - .or_default(); - 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); + .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 let Some(types_specifier) = maybe_types_specifier { - dep.maybe_type = Some(types_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; } } @@ -400,14 +451,18 @@ impl Module { // Disallow downgrades from HTTPS to HTTP if referrer_scheme == "https" && specifier_scheme == "http" { - return Err(InvalidDowngrade(specifier.clone(), location).into()); + 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(InvalidLocalImport(specifier.clone(), location).into()); + return Err( + GraphError::InvalidLocalImport(specifier.clone(), location).into(), + ); } Ok(specifier) @@ -438,20 +493,71 @@ impl<'de> Deserialize<'de> for Stats { 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() { - write!(f, "{}: {}", key, value)?; + writeln!(f, " {}: {}", key, value)?; } Ok(()) } } +#[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(&self, serializer: S) -> Result + 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 { pub debug: bool, pub maybe_config_path: Option, } +#[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, + /// Ignore any previously emits and ensure that all files are emitted from + /// source. + pub reload: bool, +} + /// A structure which provides options when transpiling modules. #[derive(Debug, Default)] pub struct TranspileOptions { @@ -461,6 +567,9 @@ pub struct TranspileOptions { /// file that augments the the default configuration passed to the TypeScript /// compiler. pub maybe_config_path: Option, + /// 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 @@ -468,11 +577,27 @@ pub struct TranspileOptions { /// be able to manipulate and handle the graph. #[derive(Debug)] pub struct Graph2 { + /// A reference to the specifier handler that will retrieve and cache modules + /// for the graph. handler: Rc>, - maybe_ts_build_info: Option, + /// Optional TypeScript build info that will be passed to `tsc` if `tsc` is + /// invoked. + maybe_tsbuildinfo: Option, + /// The modules that are part of the graph. modules: HashMap, + /// 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, + /// The module specifiers that have been uniquely added to the graph, which + /// does not include any transient dependencies. roots: Vec, + /// 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, } impl Graph2 { @@ -484,10 +609,11 @@ impl Graph2 { pub fn new(handler: Rc>) -> Self { Graph2 { handler, - maybe_ts_build_info: None, + maybe_tsbuildinfo: None, modules: HashMap::new(), redirects: HashMap::new(), roots: Vec::new(), + roots_dynamic: true, } } @@ -498,7 +624,7 @@ impl Graph2 { options: BundleOptions, ) -> Result<(String, Stats, Option), AnyError> { if self.roots.is_empty() || self.roots.len() > 1 { - return Err(NotSupported(format!("Bundling is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); + 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(); @@ -566,6 +692,141 @@ impl Graph2 { self.modules.contains_key(s) } + /// Type check the module graph, corresponding to the options provided. + pub fn check( + self, + options: CheckOptions, + ) -> Result<(Stats, Diagnostics, Option), AnyError> + { + // TODO(@kitsonk) set to `true` in followup PR + let unstable = options.lib == TypeLib::UnstableDenoWindow + || options.lib == TypeLib::UnstableDenoWorker; + 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": unstable, + "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_user_config(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(( + Stats(Vec::new()), + Diagnostics(Vec::new()), + maybe_ignored_options, + )); + } + + // 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: Vec = + self.roots.iter().map(|ms| ms.to_string()).collect(); + 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 = exec( + js::compiler_isolate_init(), + 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.0.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"); + let specifier = specifiers[0].clone(); + // 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, emit.data.clone()); + } + MediaType::SourceMap => { + maps.insert(specifier, 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((response.stats, response.diagnostics, maybe_ignored_options)) + } + /// Update the handler with any modules that are marked as _dirty_ and update /// any build info if present. fn flush(&mut self) -> Result<(), AnyError> { @@ -582,8 +843,8 @@ impl Graph2 { } } for root_specifier in self.roots.iter() { - if let Some(ts_build_info) = &self.maybe_ts_build_info { - handler.set_ts_build_info(root_specifier, ts_build_info.to_owned())?; + if let Some(tsbuildinfo) = &self.maybe_tsbuildinfo { + handler.set_tsbuildinfo(root_specifier, tsbuildinfo.to_owned())?; } } @@ -694,12 +955,26 @@ impl Graph2 { } } + 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) + } + /// 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 { if self.roots.is_empty() || self.roots.len() > 1 { - return Err(NotSupported(format!("Info is only supported when there is a single root module in the graph. Found: {}", self.roots.len())).into()); + 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(); @@ -731,72 +1006,124 @@ impl Graph2 { }) } + /// 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, - maybe_lockfile: &Option>, - ) -> Result<(), AnyError> { + pub fn lock(&self, maybe_lockfile: &Option>) { 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 valid = lockfile.check_or_insert(&specifier, &module.source); if !valid { - return Err( - InvalidSource(ms.clone(), lockfile.filename.display().to_string()) - .into(), + eprintln!( + "{}", + GraphError::InvalidSource(ms.clone(), lockfile.filename.clone()) ); + std::process::exit(10); } } } + } - Ok(()) + /// 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 { if !self.contains_module(referrer) { - return Err(MissingSpecifier(referrer.to_owned()).into()); + return Err(GraphError::MissingSpecifier(referrer.to_owned()).into()); } let module = self.get_module(referrer).unwrap(); if !module.dependencies.contains_key(specifier) { return Err( - MissingDependency(referrer.to_owned(), specifier.to_owned()).into(), + 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 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(), - ); - }; + 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( - MissingDependency(referrer.to_owned(), resolved_specifier.to_string()) - .into(), + 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 let Some((_, types)) = dep_module.maybe_types.clone() { + let result = if prefer_types && dep_module.maybe_types.is_some() { + let (_, types) = dep_module.maybe_types.clone().unwrap(); types } else { resolved_specifier @@ -835,7 +1162,7 @@ impl Graph2 { /// /// # Arguments /// - /// - `options` - A structure of options which impact how the code is + /// * `options` - A structure of options which impact how the code is /// transpiled. /// pub fn transpile( @@ -858,6 +1185,7 @@ impl Graph2 { let emit_options: 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 @@ -875,9 +1203,8 @@ impl Graph2 { { continue; } - let config = ts_config.as_bytes(); // skip modules that already have a valid emit - if module.maybe_emit.is_some() && module.is_emit_valid(&config) { + if !options.reload && module.is_emit_valid(&config) { continue; } if module.maybe_parsed_module.is_none() { @@ -917,7 +1244,7 @@ impl swc_bundler::Resolve for Graph2 { referrer ) }; - let specifier = self.resolve(specifier, &referrer)?; + let specifier = self.resolve(specifier, &referrer, false)?; Ok(swc_common::FileName::Custom(specifier.to_string())) } @@ -949,15 +1276,55 @@ impl GraphBuilder2 { } } + /// 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) -> Result<(), AnyError> { + fn fetch( + &mut self, + specifier: &ModuleSpecifier, + maybe_referrer: &Option, + 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()); + let future = self.graph.handler.borrow_mut().fetch( + specifier.clone(), + maybe_referrer.clone(), + is_dynamic, + ); self.pending.push(future); Ok(()) @@ -966,10 +1333,30 @@ impl GraphBuilder2 { /// 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) -> Result<(), AnyError> { + 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, self.maybe_import_map.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()?; @@ -984,15 +1371,16 @@ impl GraphBuilder2 { } } 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)?; + self.fetch(specifier, &maybe_referrer, dep.is_dynamic)?; } if let Some(specifier) = dep.maybe_type.as_ref() { - self.fetch(specifier)?; + self.fetch(specifier, &maybe_referrer, dep.is_dynamic)?; } } if let Some((_, specifier)) = module.maybe_types.as_ref() { - self.fetch(specifier)?; + self.fetch(specifier, &None, false)?; } if specifier != requested_specifier { self @@ -1005,45 +1393,17 @@ impl GraphBuilder2 { 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. - pub async fn insert( - &mut self, - specifier: &ModuleSpecifier, - ) -> Result<(), AnyError> { - 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. + /// the expected lockfile, an error will be logged and the process will exit. /// /// TODO(@kitsonk) this should really be owned by the graph, but currently /// the lockfile is behind a mutex in program_state, which makes it really /// hard to not pass around as a reference, which if the Graph owned it, it /// would need lifetime parameters and lifetime parameters are 😭 - pub fn get_graph( - self, - maybe_lockfile: &Option>, - ) -> Result { - self.graph.lock(maybe_lockfile)?; - Ok(self.graph) + pub fn get_graph(self, maybe_lockfile: &Option>) -> Graph2 { + self.graph.lock(maybe_lockfile); + self.graph } } @@ -1063,8 +1423,8 @@ pub mod tests { #[derive(Debug, Default)] pub struct MockSpecifierHandler { pub fixtures: PathBuf, - pub maybe_ts_build_info: Option, - pub ts_build_info_calls: Vec<(ModuleSpecifier, String)>, + pub maybe_tsbuildinfo: Option, + pub tsbuildinfo_calls: Vec<(ModuleSpecifier, String)>, pub cache_calls: Vec<(ModuleSpecifier, Emit)>, pub deps_calls: Vec<(ModuleSpecifier, DependencyMap)>, pub types_calls: Vec<(ModuleSpecifier, String)>, @@ -1097,6 +1457,7 @@ pub mod tests { _ => MediaType::Unknown, }; let source = fs::read_to_string(&source_path)?; + let is_remote = specifier.as_url().scheme() != "file"; Ok(CachedModule { source, @@ -1104,20 +1465,26 @@ pub mod tests { source_path, specifier, media_type, + is_remote, ..CachedModule::default() }) } } impl SpecifierHandler for MockSpecifierHandler { - fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture { + fn fetch( + &mut self, + specifier: ModuleSpecifier, + _maybe_referrer: Option, + _is_dynamic: bool, + ) -> FetchFuture { Box::pin(future::ready(self.get_cache(specifier))) } - fn get_ts_build_info( + fn get_tsbuildinfo( &self, _specifier: &ModuleSpecifier, ) -> Result, AnyError> { - Ok(self.maybe_ts_build_info.clone()) + Ok(self.maybe_tsbuildinfo.clone()) } fn set_cache( &mut self, @@ -1135,15 +1502,15 @@ pub mod tests { self.types_calls.push((specifier.clone(), types)); Ok(()) } - fn set_ts_build_info( + fn set_tsbuildinfo( &mut self, specifier: &ModuleSpecifier, - ts_build_info: String, + tsbuildinfo: String, ) -> Result<(), AnyError> { - self.maybe_ts_build_info = Some(ts_build_info.clone()); + self.maybe_tsbuildinfo = Some(tsbuildinfo.clone()); self - .ts_build_info_calls - .push((specifier.clone(), ts_build_info)); + .tsbuildinfo_calls + .push((specifier.clone(), tsbuildinfo)); Ok(()) } fn set_deps( @@ -1164,6 +1531,24 @@ pub mod tests { } } + async fn setup( + specifier: ModuleSpecifier, + ) -> (Graph2, Rc>) { + 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 = GraphBuilder2::new(handler.clone(), None); + builder + .add(&specifier, false) + .await + .expect("module not inserted"); + + (builder.get_graph(&None), handler) + } + #[test] fn test_get_version() { let doc_a = "console.log(42);"; @@ -1265,10 +1650,10 @@ pub mod tests { })); let mut builder = GraphBuilder2::new(handler.clone(), None); builder - .insert(&specifier) + .add(&specifier, false) .await .expect("module not inserted"); - let graph = builder.get_graph(&None).expect("could not get graph"); + let graph = builder.get_graph(&None); let (actual, stats, maybe_ignored_options) = graph .bundle(BundleOptions::default()) .expect("could not bundle"); @@ -1281,22 +1666,57 @@ pub mod tests { } #[tokio::test] - async fn test_graph_info() { - 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 = GraphBuilder2::new(handler.clone(), None); + async fn test_graph_check_emit() { 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 graph = builder.get_graph(&None).expect("could not get graph"); + let (graph, handler) = setup(specifier).await; + let (stats, diagnostics, maybe_ignored_options) = graph + .check(CheckOptions { + debug: false, + emit: true, + lib: TypeLib::DenoWindow, + maybe_config_path: None, + reload: false, + }) + .expect("should have checked"); + assert!(maybe_ignored_options.is_none()); + assert_eq!(stats.0.len(), 12); + assert!(diagnostics.0.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 (stats, diagnostics, maybe_ignored_options) = graph + .check(CheckOptions { + debug: false, + emit: false, + lib: TypeLib::DenoWindow, + maybe_config_path: None, + reload: false, + }) + .expect("should have checked"); + assert!(maybe_ignored_options.is_none()); + assert_eq!(stats.0.len(), 12); + assert!(diagnostics.0.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_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); @@ -1311,6 +1731,24 @@ pub mod tests { 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 = GraphBuilder2::new(handler.clone(), 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 @@ -1320,21 +1758,10 @@ pub mod tests { // 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 = GraphBuilder2::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 (mut graph, handler) = setup(specifier).await; let (stats, maybe_ignored_options) = graph.transpile(TranspileOptions::default()).unwrap(); assert_eq!(stats.0.len(), 3); @@ -1385,25 +1812,15 @@ pub mod tests { #[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 { - fixtures: fixtures.clone(), - ..MockSpecifierHandler::default() - })); - let mut builder = GraphBuilder2::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 (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!( @@ -1441,36 +1858,9 @@ pub mod tests { ModuleSpecifier::resolve_url_or_path("file:///tests/main.ts") .expect("could not resolve module"); builder - .insert(&specifier) + .add(&specifier, false) .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, 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 = GraphBuilder2::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"); + builder.get_graph(&maybe_lockfile); } } diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 3cbcade2ae..39690465cf 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -83,7 +83,7 @@ impl ModuleLoader for CliModuleLoader { op_state: Rc>, module_specifier: &ModuleSpecifier, maybe_referrer: Option, - _is_dyn_import: bool, + _is_dynamic: bool, ) -> Pin> { let module_specifier = module_specifier.to_owned(); let module_url_specified = module_specifier.to_string(); @@ -92,11 +92,10 @@ impl ModuleLoader for CliModuleLoader { state.borrow::>().clone() }; - // TODO(bartlomieju): `fetch_compiled_module` should take `load_id` param + // TODO(@kitsonk) this shouldn't be async let fut = async move { let compiled_module = program_state - .fetch_compiled_module(module_specifier, maybe_referrer) - .await?; + .fetch_compiled_module(module_specifier, maybe_referrer)?; Ok(deno_core::ModuleSource { // Real module name, might be different from initial specifier // due to redirections. @@ -113,44 +112,28 @@ impl ModuleLoader for CliModuleLoader { &self, op_state: Rc>, _load_id: ModuleLoadId, - module_specifier: &ModuleSpecifier, - maybe_referrer: Option, - is_dyn_import: bool, + specifier: &ModuleSpecifier, + _maybe_referrer: Option, + is_dynamic: bool, ) -> Pin>>> { - let module_specifier = module_specifier.clone(); + let specifier = specifier.clone(); let target_lib = self.target_lib.clone(); let maybe_import_map = self.import_map.clone(); let state = op_state.borrow(); - // Only "main" module is loaded without permission check, - // ie. module that is associated with "is_main" state - // and is not a dynamic import. - let permissions = if self.is_main && !is_dyn_import { - Permissions::allow_all() - } else { - state.borrow::().clone() - }; + // The permissions that should be applied to any dynamically imported module + let dynamic_permissions = state.borrow::().clone(); let program_state = state.borrow::>().clone(); drop(state); - // TODO(bartlomieju): I'm not sure if it's correct to ignore - // bad referrer - this is the case for `Deno.core.evalContext()` where - // `ref_str` is ``. - let maybe_referrer = if let Some(ref_str) = maybe_referrer { - ModuleSpecifier::resolve_url(&ref_str).ok() - } else { - None - }; - // TODO(bartlomieju): `prepare_module_load` should take `load_id` param async move { program_state .prepare_module_load( - module_specifier, - maybe_referrer, + specifier, target_lib, - permissions, - is_dyn_import, + dynamic_permissions, + is_dynamic, maybe_import_map, ) .await diff --git a/cli/ops/errors.rs b/cli/ops/errors.rs index 04281e3835..dbb72139dd 100644 --- a/cli/ops/errors.rs +++ b/cli/ops/errors.rs @@ -39,7 +39,7 @@ fn op_apply_source_map( args.line_number.into(), args.column_number.into(), &mut mappings_map, - &super::program_state(state).ts_compiler, + super::program_state(state), ); Ok(json!({ diff --git a/cli/program_state.rs b/cli/program_state.rs index b921b68075..6e3a516636 100644 --- a/cli/program_state.rs +++ b/cli/program_state.rs @@ -8,16 +8,20 @@ use crate::import_map::ImportMap; use crate::inspector::InspectorServer; use crate::lockfile::Lockfile; use crate::media_type::MediaType; -use crate::module_graph::ModuleGraphFile; -use crate::module_graph::ModuleGraphLoader; +use crate::module_graph2::CheckOptions; use crate::module_graph2::GraphBuilder2; use crate::module_graph2::TranspileOptions; +use crate::module_graph2::TypeLib; use crate::permissions::Permissions; +use crate::source_maps::SourceMapGetter; use crate::specifier_handler::FetchHandler; use crate::tsc::CompiledModule; use crate::tsc::TargetLib; use crate::tsc::TsCompiler; + +use deno_core::error::generic_error; use deno_core::error::AnyError; +use deno_core::url::Url; use deno_core::ModuleSpecifier; use std::cell::RefCell; use std::env; @@ -115,89 +119,66 @@ impl ProgramState { /// and traspilation. pub async fn prepare_module_load( self: &Arc, - module_specifier: ModuleSpecifier, - maybe_referrer: Option, + specifier: ModuleSpecifier, target_lib: TargetLib, - permissions: Permissions, - is_dyn_import: bool, + dynamic_permissions: Permissions, + is_dynamic: bool, maybe_import_map: Option, ) -> Result<(), AnyError> { - let module_specifier = module_specifier.clone(); + let specifier = specifier.clone(); + let handler = + Rc::new(RefCell::new(FetchHandler::new(self, dynamic_permissions)?)); + let mut builder = GraphBuilder2::new(handler, maybe_import_map); + builder.add(&specifier, is_dynamic).await?; + let mut graph = builder.get_graph(&self.lockfile); + let debug = self.flags.log_level == Some(log::Level::Debug); + let maybe_config_path = self.flags.config_path.clone(); if self.flags.no_check { - debug!("Transpiling root: {}", module_specifier); - // TODO(kitsonk) note that self.permissions != permissions, which is - // something that should be handled better in the future. - let handler = - Rc::new(RefCell::new(FetchHandler::new(self, permissions.clone())?)); - let mut builder = GraphBuilder2::new(handler, maybe_import_map); - builder.insert(&module_specifier).await?; - let mut graph = builder.get_graph(&self.lockfile)?; - let (stats, maybe_ignored_options) = graph.transpile(TranspileOptions { - debug: self.flags.log_level == Some(log::Level::Debug), - maybe_config_path: self.flags.config_path.clone(), + debug, + maybe_config_path, + reload: self.flags.reload, })?; - + debug!("{}", stats); if let Some(ignored_options) = maybe_ignored_options { eprintln!("{}", ignored_options); } - - debug!("{}", stats); } else { - let mut module_graph_loader = ModuleGraphLoader::new( - self.file_fetcher.clone(), - maybe_import_map, - permissions.clone(), - is_dyn_import, - false, - ); - module_graph_loader - .add_to_graph(&module_specifier, maybe_referrer) - .await?; - let module_graph = module_graph_loader.get_graph(); - - let out = self - .file_fetcher - .fetch_cached_source_file(&module_specifier, permissions.clone()) - .expect("Source file not found"); - - let module_graph_files = module_graph.values().collect::>(); - // Check integrity of every file in module graph - if let Some(ref lockfile) = self.lockfile { - let mut g = lockfile.lock().unwrap(); - - for graph_file in &module_graph_files { - let check_passed = - g.check_or_insert(&graph_file.url, &graph_file.source_code); - - if !check_passed { - eprintln!( - "Subresource integrity check failed --lock={}\n{}", - g.filename.display(), - graph_file.url - ); - std::process::exit(10); + let lib = match target_lib { + TargetLib::Main => { + if self.flags.unstable { + TypeLib::UnstableDenoWindow + } else { + TypeLib::DenoWindow } } - } + TargetLib::Worker => { + if self.flags.unstable { + TypeLib::UnstableDenoWorker + } else { + TypeLib::DenoWorker + } + } + }; + let (stats, diagnostics, maybe_ignored_options) = + graph.check(CheckOptions { + debug, + emit: true, + lib, + maybe_config_path, + reload: self.flags.reload, + })?; - // Check if we need to compile files. - let should_compile = needs_compilation( - self.ts_compiler.compile_js, - out.media_type, - &module_graph_files, - ); - let allow_js = should_allow_js(&module_graph_files); - - if should_compile { - self - .ts_compiler - .compile(self, &out, target_lib, &module_graph, allow_js) - .await?; + debug!("{}", stats); + if let Some(ignored_options) = maybe_ignored_options { + eprintln!("{}", ignored_options); } - } + if !diagnostics.0.is_empty() { + return Err(generic_error(diagnostics.to_string())); + } + }; if let Some(ref lockfile) = self.lockfile { let g = lockfile.lock().unwrap(); @@ -207,44 +188,39 @@ impl ProgramState { Ok(()) } - // TODO(bartlomieju): this method doesn't need to be async anymore - /// This method is used after `prepare_module_load` finishes and JsRuntime - /// starts loading source and executing source code. This method shouldn't - /// perform any IO (besides $DENO_DIR) and only operate on sources collected - /// during `prepare_module_load`. - pub async fn fetch_compiled_module( + pub fn fetch_compiled_module( &self, module_specifier: ModuleSpecifier, - _maybe_referrer: Option, + maybe_referrer: Option, ) -> Result { let out = self .file_fetcher .fetch_cached_source_file(&module_specifier, Permissions::allow_all()) .expect("Cached source file doesn't exist"); - // Check if we need to compile files - let was_compiled = match out.media_type { - MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true, - MediaType::JavaScript => self.ts_compiler.compile_js, - _ => false, - }; - - let compiled_module = if was_compiled { - match self.ts_compiler.get_compiled_module(&out.url) { - Ok(module) => module, - Err(e) => { - let msg = format!( - "Failed to get compiled source code of \"{}\".\nReason: {}\n\ - If the source file provides only type exports, prefer to use \"import type\" or \"export type\" syntax instead.", - out.url, e.to_string() - ); - info!("{} {}", crate::colors::yellow("Warning"), msg); - - CompiledModule { - code: "".to_string(), - name: out.url.to_string(), - } - } + let url = out.url.clone(); + let compiled_module = if let Some((code, _)) = self.get_emit(&url) { + CompiledModule { + code: String::from_utf8(code).unwrap(), + name: out.url.to_string(), + } + // We expect a compiled source for any non-JavaScript files, except for + // local files that have an unknown media type and no referrer (root modules + // that do not have an extension.) + } else if out.media_type != MediaType::JavaScript + && !(out.media_type == MediaType::Unknown + && maybe_referrer.is_none() + && url.scheme() == "file") + { + let message = if let Some(referrer) = maybe_referrer { + format!("Compiled module not found \"{}\"\n From: {}\n If the source module contains only types, use `import type` and `export type` to import it instead.", module_specifier, referrer) + } else { + format!("Compiled module not found \"{}\"\n If the source module contains only types, use `import type` and `export type` to import it instead.", module_specifier) + }; + info!("{}: {}", crate::colors::yellow("warning"), message); + CompiledModule { + code: "".to_string(), + name: out.url.to_string(), } } else { CompiledModule { @@ -256,6 +232,37 @@ impl ProgramState { Ok(compiled_module) } + // TODO(@kitsonk) this should be a straight forward API on file_fetcher or + // whatever future refactors do... + fn get_emit(&self, url: &Url) -> Option<(Vec, Option>)> { + match url.scheme() { + // we should only be looking for emits for schemes that denote external + // modules, which the disk_cache supports + "wasm" | "file" | "http" | "https" => (), + _ => { + return None; + } + } + let emit_path = self + .dir + .gen_cache + .get_cache_filename_with_extension(&url, "js"); + let emit_map_path = self + .dir + .gen_cache + .get_cache_filename_with_extension(&url, "js.map"); + if let Ok(code) = self.dir.gen_cache.get(&emit_path) { + let maybe_map = if let Ok(map) = self.dir.gen_cache.get(&emit_map_path) { + Some(map) + } else { + None + }; + Some((code, maybe_map)) + } else { + None + } + } + /// Quits the process if the --unstable flag was not provided. /// /// This is intentionally a non-recoverable check so that people cannot probe @@ -279,57 +286,62 @@ impl ProgramState { } } -/// Determine if TS compiler should be run with `allowJs` setting on. This -/// is the case when there's either: -/// - a JavaScript file with non-JavaScript import -/// - JSX import -fn should_allow_js(module_graph_files: &[&ModuleGraphFile]) -> bool { - module_graph_files.iter().any(|module_file| { - if module_file.media_type == MediaType::JSX { - true - } else if module_file.media_type == MediaType::JavaScript { - module_file.imports.iter().any(|import_desc| { - let import_file = module_graph_files - .iter() - .find(|f| { - f.specifier == import_desc.resolved_specifier.to_string().as_str() - }) - .expect("Failed to find imported file"); - let media_type = import_file.media_type; - media_type == MediaType::TypeScript - || media_type == MediaType::TSX - || media_type == MediaType::JSX - }) +// TODO(@kitsonk) this is only temporary, but should be refactored to somewhere +// else, like a refactored file_fetcher. +impl SourceMapGetter for ProgramState { + fn get_source_map(&self, file_name: &str) -> Option> { + if let Ok(specifier) = ModuleSpecifier::resolve_url(file_name) { + if let Some((code, maybe_map)) = self.get_emit(&specifier.as_url()) { + if maybe_map.is_some() { + maybe_map + } else { + let code = String::from_utf8(code).unwrap(); + let lines: Vec<&str> = code.split('\n').collect(); + if let Some(last_line) = lines.last() { + if last_line + .starts_with("//# sourceMappingURL=data:application/json;base64,") + { + let input = last_line.trim_start_matches( + "//# sourceMappingURL=data:application/json;base64,", + ); + let decoded_map = base64::decode(input) + .expect("Unable to decode source map from emitted file."); + Some(decoded_map) + } else { + None + } + } else { + None + } + } + } else { + None + } } else { - false + None } - }) -} + } -// Compilation happens if either: -// - `checkJs` is set to true in TS config -// - entry point is a TS file -// - any dependency in module graph is a TS file -fn needs_compilation( - compile_js: bool, - media_type: MediaType, - module_graph_files: &[&ModuleGraphFile], -) -> bool { - let mut needs_compilation = match media_type { - MediaType::TypeScript | MediaType::TSX | MediaType::JSX => true, - MediaType::JavaScript => compile_js, - _ => false, - }; - - needs_compilation |= module_graph_files.iter().any(|module_file| { - let media_type = module_file.media_type; - - media_type == (MediaType::TypeScript) - || media_type == (MediaType::TSX) - || media_type == (MediaType::JSX) - }); - - needs_compilation + fn get_source_line( + &self, + file_name: &str, + line_number: usize, + ) -> Option { + if let Ok(specifier) = ModuleSpecifier::resolve_url(file_name) { + self + .file_fetcher + .fetch_cached_source_file(&specifier, Permissions::allow_all()) + .map(|out| { + // Do NOT use .lines(): it skips the terminating empty line. + // (due to internally using .split_terminator() instead of .split()) + let lines: Vec<&str> = out.source_code.split('\n').collect(); + assert!(lines.len() > line_number); + lines[line_number].to_string() + }) + } else { + None + } + } } #[test] @@ -337,203 +349,3 @@ fn thread_safe() { fn f(_: S) {} f(ProgramState::mock(vec![], None)); } - -#[test] -fn test_should_allow_js() { - use crate::ast::Location; - use crate::module_graph::ImportDescriptor; - - assert!(should_allow_js(&[ - &ModuleGraphFile { - specifier: "file:///some/file.ts".to_string(), - url: "file:///some/file.ts".to_string(), - redirect: None, - filename: "some/file.ts".to_string(), - imports: vec![], - version_hash: "1".to_string(), - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - media_type: MediaType::TypeScript, - source_code: "function foo() {}".to_string(), - }, - &ModuleGraphFile { - specifier: "file:///some/file1.js".to_string(), - url: "file:///some/file1.js".to_string(), - redirect: None, - filename: "some/file1.js".to_string(), - version_hash: "1".to_string(), - imports: vec![ImportDescriptor { - specifier: "./file.ts".to_string(), - resolved_specifier: ModuleSpecifier::resolve_url( - "file:///some/file.ts", - ) - .unwrap(), - type_directive: None, - resolved_type_directive: None, - location: Location { - filename: "file:///some/file1.js".to_string(), - line: 0, - col: 0, - }, - }], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - media_type: MediaType::JavaScript, - source_code: "function foo() {}".to_string(), - }, - ],)); - - assert!(should_allow_js(&[ - &ModuleGraphFile { - specifier: "file:///some/file.jsx".to_string(), - url: "file:///some/file.jsx".to_string(), - redirect: None, - filename: "some/file.jsx".to_string(), - imports: vec![], - version_hash: "1".to_string(), - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - media_type: MediaType::JSX, - source_code: "function foo() {}".to_string(), - }, - &ModuleGraphFile { - specifier: "file:///some/file.ts".to_string(), - url: "file:///some/file.ts".to_string(), - redirect: None, - filename: "some/file.ts".to_string(), - version_hash: "1".to_string(), - imports: vec![ImportDescriptor { - specifier: "./file.jsx".to_string(), - resolved_specifier: ModuleSpecifier::resolve_url( - "file:///some/file.jsx", - ) - .unwrap(), - type_directive: None, - resolved_type_directive: None, - location: Location { - filename: "file:///some/file1.ts".to_string(), - line: 0, - col: 0, - }, - }], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - media_type: MediaType::TypeScript, - source_code: "function foo() {}".to_string(), - }, - ])); - - assert!(!should_allow_js(&[ - &ModuleGraphFile { - specifier: "file:///some/file.js".to_string(), - url: "file:///some/file.js".to_string(), - redirect: None, - filename: "some/file.js".to_string(), - imports: vec![], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - version_hash: "1".to_string(), - type_headers: vec![], - media_type: MediaType::JavaScript, - source_code: "function foo() {}".to_string(), - }, - &ModuleGraphFile { - specifier: "file:///some/file1.js".to_string(), - url: "file:///some/file1.js".to_string(), - redirect: None, - filename: "some/file1.js".to_string(), - imports: vec![ImportDescriptor { - specifier: "./file.js".to_string(), - resolved_specifier: ModuleSpecifier::resolve_url( - "file:///some/file.js", - ) - .unwrap(), - type_directive: None, - resolved_type_directive: None, - location: Location { - filename: "file:///some/file.js".to_string(), - line: 0, - col: 0, - }, - }], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - version_hash: "1".to_string(), - type_headers: vec![], - media_type: MediaType::JavaScript, - source_code: "function foo() {}".to_string(), - }, - ],)); -} - -#[test] -fn test_needs_compilation() { - assert!(!needs_compilation( - false, - MediaType::JavaScript, - &[&ModuleGraphFile { - specifier: "some/file.js".to_string(), - url: "file:///some/file.js".to_string(), - redirect: None, - filename: "some/file.js".to_string(), - imports: vec![], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - version_hash: "1".to_string(), - media_type: MediaType::JavaScript, - source_code: "function foo() {}".to_string(), - }], - )); - - assert!(!needs_compilation(false, MediaType::JavaScript, &[])); - assert!(needs_compilation(true, MediaType::JavaScript, &[])); - assert!(needs_compilation(false, MediaType::TypeScript, &[])); - assert!(needs_compilation(false, MediaType::JSX, &[])); - assert!(needs_compilation(false, MediaType::TSX, &[])); - assert!(needs_compilation( - false, - MediaType::JavaScript, - &[ - &ModuleGraphFile { - specifier: "file:///some/file.ts".to_string(), - url: "file:///some/file.ts".to_string(), - redirect: None, - filename: "some/file.ts".to_string(), - imports: vec![], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - media_type: MediaType::TypeScript, - version_hash: "1".to_string(), - source_code: "function foo() {}".to_string(), - }, - &ModuleGraphFile { - specifier: "file:///some/file1.js".to_string(), - url: "file:///some/file1.js".to_string(), - redirect: None, - filename: "some/file1.js".to_string(), - imports: vec![], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - version_hash: "1".to_string(), - media_type: MediaType::JavaScript, - source_code: "function foo() {}".to_string(), - }, - ], - )); -} diff --git a/cli/source_maps.rs b/cli/source_maps.rs index 4744482a77..f31228bdc8 100644 --- a/cli/source_maps.rs +++ b/cli/source_maps.rs @@ -6,8 +6,9 @@ use deno_core::error::JsError as CoreJsError; use sourcemap::SourceMap; use std::collections::HashMap; use std::str; +use std::sync::Arc; -pub trait SourceMapGetter { +pub trait SourceMapGetter: Sync + Send { /// Returns the raw source map file. fn get_source_map(&self, file_name: &str) -> Option>; fn get_source_line( @@ -26,7 +27,7 @@ pub type CachedMaps = HashMap>; /// source, rather than the transpiled source code. pub fn apply_source_map( js_error: &CoreJsError, - getter: &G, + getter: Arc, ) -> CoreJsError { // Note that js_error.frames has already been source mapped in // prepareStackTrace(). @@ -39,7 +40,7 @@ pub fn apply_source_map( // start_column is 0-based, we need 1-based here. js_error.start_column.map(|n| n + 1), &mut mappings_map, - getter, + getter.clone(), ); let start_column = start_column.map(|n| n - 1); // It is better to just move end_column to be the same distance away from @@ -87,7 +88,7 @@ fn get_maybe_orig_position( line_number: Option, column_number: Option, mappings_map: &mut CachedMaps, - getter: &G, + getter: Arc, ) -> (Option, Option, Option) { match (file_name, line_number, column_number) { (Some(file_name_v), Some(line_v), Some(column_v)) => { @@ -104,7 +105,7 @@ pub fn get_orig_position( line_number: i64, column_number: i64, mappings_map: &mut CachedMaps, - getter: &G, + getter: Arc, ) -> (String, i64, i64) { let maybe_source_map = get_mappings(&file_name, mappings_map, getter); let default_pos = (file_name, line_number, column_number); @@ -134,7 +135,7 @@ pub fn get_orig_position( fn get_mappings<'a, G: SourceMapGetter>( file_name: &str, mappings_map: &'a mut CachedMaps, - getter: &G, + getter: Arc, ) -> &'a Option { mappings_map .entry(file_name.to_string()) @@ -145,7 +146,7 @@ fn get_mappings<'a, G: SourceMapGetter>( // the module meta data. fn parse_map_string( file_name: &str, - getter: &G, + getter: Arc, ) -> Option { getter .get_source_map(file_name) @@ -207,8 +208,8 @@ mod tests { frames: vec![], stack: None, }; - let getter = MockSourceMapGetter {}; - let actual = apply_source_map(&e, &getter); + let getter = Arc::new(MockSourceMapGetter {}); + let actual = apply_source_map(&e, getter); assert_eq!(actual.source_line, Some("console.log('foo');".to_string())); } } diff --git a/cli/specifier_handler.rs b/cli/specifier_handler.rs index 5d9c19a5e9..016ad04689 100644 --- a/cli/specifier_handler.rs +++ b/cli/specifier_handler.rs @@ -1,5 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::ast::Location; use crate::deno_dir::DenoDir; use crate::disk_cache::DiskCache; use crate::file_fetcher::SourceFileFetcher; @@ -25,8 +26,29 @@ pub type DependencyMap = HashMap; pub type FetchFuture = Pin> + 'static)>>; +/// A group of errors that represent errors that can occur with an +/// an implementation of `SpecifierHandler`. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum HandlerError { + /// A fetch error, where we have a location associated with it. + FetchErrorWithLocation(String, Location), +} + +impl fmt::Display for HandlerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HandlerError::FetchErrorWithLocation(ref err, ref location) => { + write!(f, "{}\n at {}", err, location) + } + } + } +} + +impl std::error::Error for HandlerError {} + #[derive(Debug, Clone)] pub struct CachedModule { + pub is_remote: bool, pub maybe_dependencies: Option, pub maybe_emit: Option, pub maybe_emit_path: Option<(PathBuf, Option)>, @@ -44,6 +66,7 @@ impl Default for CachedModule { fn default() -> Self { let specifier = ModuleSpecifier::resolve_url("file:///example.js").unwrap(); CachedModule { + is_remote: false, maybe_dependencies: None, maybe_emit: None, maybe_emit_path: None, @@ -76,8 +99,12 @@ impl Default for Emit { } } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Dependency { + /// Flags if the dependency is a dynamic import or not. + pub is_dynamic: bool, + /// The location in the source code where the dependency statement occurred. + pub location: Location, /// The module specifier that resolves to the runtime code dependency for the /// module. pub maybe_code: Option, @@ -86,17 +113,33 @@ pub struct Dependency { pub maybe_type: Option, } +impl Dependency { + pub fn new(location: Location) -> Self { + Dependency { + is_dynamic: false, + location, + maybe_code: None, + maybe_type: None, + } + } +} + pub trait SpecifierHandler { /// Instructs the handler to fetch a specifier or retrieve its value from the /// cache. - fn fetch(&mut self, specifier: ModuleSpecifier) -> FetchFuture; + fn fetch( + &mut self, + specifier: ModuleSpecifier, + maybe_location: Option, + is_dynamic: bool, + ) -> FetchFuture; /// Get the optional build info from the cache for a given module specifier. /// Because build infos are only associated with the "root" modules, they are /// not expected to be cached for each module, but are "lazily" checked when /// a root module is identified. The `emit_type` also indicates what form /// of the module the build info is valid for. - fn get_ts_build_info( + fn get_tsbuildinfo( &self, specifier: &ModuleSpecifier, ) -> Result, AnyError>; @@ -117,10 +160,10 @@ pub trait SpecifierHandler { ) -> Result<(), AnyError>; /// Set the build info for a module specifier, also providing the cache type. - fn set_ts_build_info( + fn set_tsbuildinfo( &mut self, specifier: &ModuleSpecifier, - ts_build_info: String, + tsbuildinfo: String, ) -> Result<(), AnyError>; /// Set the graph dependencies for a given module specifier. @@ -170,15 +213,18 @@ impl CompiledFileMetadata { /// existing `file_fetcher` interface, which will eventually be refactored to /// align it more to the `SpecifierHandler` trait. pub struct FetchHandler { + /// An instance of disk where generated (emitted) files are stored. disk_cache: DiskCache, + /// A set of permissions to apply to dynamic imports. + dynamic_permissions: Permissions, + /// A clone of the `program_state` file fetcher. file_fetcher: SourceFileFetcher, - permissions: Permissions, } impl FetchHandler { pub fn new( program_state: &Arc, - permissions: Permissions, + dynamic_permissions: Permissions, ) -> Result { let custom_root = env::var("DENO_DIR").map(String::into).ok(); let deno_dir = DenoDir::new(custom_root)?; @@ -187,23 +233,54 @@ impl FetchHandler { Ok(FetchHandler { disk_cache, + dynamic_permissions, file_fetcher, - permissions, }) } } impl SpecifierHandler for FetchHandler { - fn fetch(&mut self, requested_specifier: ModuleSpecifier) -> FetchFuture { - let permissions = self.permissions.clone(); + fn fetch( + &mut self, + requested_specifier: ModuleSpecifier, + maybe_location: Option, + is_dynamic: bool, + ) -> FetchFuture { + // When the module graph fetches dynamic modules, the set of dynamic + // permissions need to be applied. Other static imports have all + // permissions. + let permissions = if is_dynamic { + self.dynamic_permissions.clone() + } else { + Permissions::allow_all() + }; let file_fetcher = self.file_fetcher.clone(); let disk_cache = self.disk_cache.clone(); + let maybe_referrer: Option = + if let Some(location) = &maybe_location { + Some(location.clone().into()) + } else { + None + }; async move { let source_file = file_fetcher - .fetch_source_file(&requested_specifier, None, permissions) - .await?; + .fetch_source_file(&requested_specifier, maybe_referrer, permissions) + .await + .map_err(|err| { + if let Some(location) = maybe_location { + if !is_dynamic { + HandlerError::FetchErrorWithLocation(err.to_string(), location) + .into() + } else { + err + } + } else { + err + } + })?; let url = source_file.url.clone(); + let is_remote = url.scheme() != "file"; let filename = disk_cache.get_cache_filename_with_extension(&url, "meta"); let maybe_version = if let Ok(bytes) = disk_cache.get(&filename) { if let Ok(compiled_file_metadata) = @@ -237,6 +314,7 @@ impl SpecifierHandler for FetchHandler { let specifier = ModuleSpecifier::from(url); Ok(CachedModule { + is_remote, maybe_dependencies: None, maybe_emit, maybe_emit_path, @@ -252,31 +330,32 @@ impl SpecifierHandler for FetchHandler { .boxed_local() } - fn get_ts_build_info( + fn get_tsbuildinfo( &self, specifier: &ModuleSpecifier, ) -> Result, AnyError> { let filename = self .disk_cache .get_cache_filename_with_extension(specifier.as_url(), "buildinfo"); - if let Ok(ts_build_info) = self.disk_cache.get(&filename) { - return Ok(Some(String::from_utf8(ts_build_info)?)); + if let Ok(tsbuildinfo) = self.disk_cache.get(&filename) { + Ok(Some(String::from_utf8(tsbuildinfo)?)) + } else { + Ok(None) } - - Ok(None) } - fn set_ts_build_info( + fn set_tsbuildinfo( &mut self, specifier: &ModuleSpecifier, - ts_build_info: String, + tsbuildinfo: String, ) -> Result<(), AnyError> { let filename = self .disk_cache .get_cache_filename_with_extension(specifier.as_url(), "buildinfo"); + debug!("set_tsbuildinfo - filename {:?}", filename); self .disk_cache - .set(&filename, ts_build_info.as_bytes()) + .set(&filename, tsbuildinfo.as_bytes()) .map_err(|e| e.into()) } @@ -366,8 +445,8 @@ pub mod tests { let fetch_handler = FetchHandler { disk_cache, + dynamic_permissions: Permissions::default(), file_fetcher, - permissions: Permissions::allow_all(), }; (temp_dir, fetch_handler) @@ -381,8 +460,10 @@ pub mod tests { "http://localhost:4545/cli/tests/subdir/mod2.ts", ) .unwrap(); - let cached_module: CachedModule = - file_fetcher.fetch(specifier.clone()).await.unwrap(); + let cached_module: CachedModule = file_fetcher + .fetch(specifier.clone(), None, false) + .await + .unwrap(); assert!(cached_module.maybe_emit.is_none()); assert!(cached_module.maybe_dependencies.is_none()); assert_eq!(cached_module.media_type, MediaType::TypeScript); @@ -401,18 +482,43 @@ pub mod tests { "http://localhost:4545/cli/tests/subdir/mod2.ts", ) .unwrap(); - let cached_module: CachedModule = - file_fetcher.fetch(specifier.clone()).await.unwrap(); + let cached_module: CachedModule = file_fetcher + .fetch(specifier.clone(), None, false) + .await + .unwrap(); assert!(cached_module.maybe_emit.is_none()); let code = String::from("some code"); file_fetcher .set_cache(&specifier, &Emit::Cli((code, None))) .expect("could not set cache"); - let cached_module: CachedModule = - file_fetcher.fetch(specifier.clone()).await.unwrap(); + let cached_module: CachedModule = file_fetcher + .fetch(specifier.clone(), None, false) + .await + .unwrap(); assert_eq!( cached_module.maybe_emit, Some(Emit::Cli(("some code".to_string(), None))) ); } + + #[tokio::test] + async fn test_fetch_handler_is_remote() { + let _http_server_guard = test_util::http_server(); + let (_, mut file_fetcher) = setup(); + let specifier = ModuleSpecifier::resolve_url_or_path( + "http://localhost:4545/cli/tests/subdir/mod2.ts", + ) + .unwrap(); + let cached_module: CachedModule = + file_fetcher.fetch(specifier, None, false).await.unwrap(); + assert_eq!(cached_module.is_remote, true); + let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let specifier = ModuleSpecifier::resolve_url_or_path( + c.join("tests/subdir/mod1.ts").as_os_str().to_str().unwrap(), + ) + .unwrap(); + let cached_module: CachedModule = + file_fetcher.fetch(specifier, None, false).await.unwrap(); + assert_eq!(cached_module.is_remote, false); + } } diff --git a/cli/tests/020_json_modules.ts.out b/cli/tests/020_json_modules.ts.out index 8c07af2031..a89bd2ad66 100644 --- a/cli/tests/020_json_modules.ts.out +++ b/cli/tests/020_json_modules.ts.out @@ -1,3 +1,5 @@ [WILDCARD] -error: TypeError: Cannot resolve extension for "[WILDCARD]config.json" with mediaType "Json". +error: An unsupported media type was attempted to be imported as a module. + Specifier: [WILDCARD]cli/tests/subdir/config.json + MediaType: Json [WILDCARD] \ No newline at end of file diff --git a/cli/tests/023_no_ext b/cli/tests/023_no_ext new file mode 100644 index 0000000000..0dcfb62092 --- /dev/null +++ b/cli/tests/023_no_ext @@ -0,0 +1,2 @@ +import * as mod4 from "./subdir/mod4.js"; +console.log(mod4.isMod4); diff --git a/cli/tests/023_no_ext.out b/cli/tests/023_no_ext.out new file mode 100644 index 0000000000..27ba77ddaf --- /dev/null +++ b/cli/tests/023_no_ext.out @@ -0,0 +1 @@ +true diff --git a/cli/tests/023_no_ext_with_headers b/cli/tests/023_no_ext_with_headers deleted file mode 100644 index 87951d835e..0000000000 --- a/cli/tests/023_no_ext_with_headers +++ /dev/null @@ -1 +0,0 @@ -console.log("HELLO"); diff --git a/cli/tests/023_no_ext_with_headers.out b/cli/tests/023_no_ext_with_headers.out deleted file mode 100644 index e427984d4a..0000000000 --- a/cli/tests/023_no_ext_with_headers.out +++ /dev/null @@ -1 +0,0 @@ -HELLO diff --git a/cli/tests/bundle/fixture14.out b/cli/tests/bundle/fixture14.out index 06e93a7cc5..c1a14cebe1 100644 --- a/cli/tests/bundle/fixture14.out +++ b/cli/tests/bundle/fixture14.out @@ -12,12 +12,12 @@ const lib = function() { }; }(); const c = function() { - const c1; + const c1 = []; return { c: c1 }; }(); - const mod; + const mod = []; return { mod }; diff --git a/cli/tests/config.ts b/cli/tests/config.ts index e08061e774..6b1cb73224 100644 --- a/cli/tests/config.ts +++ b/cli/tests/config.ts @@ -1,5 +1,17 @@ -const map = new Map(); - -if (map.get("bar").foo) { - console.log("here"); +/* eslint-disable */ +function b() { + return function ( + _target: any, + _propertyKey: string, + _descriptor: PropertyDescriptor, + ) { + console.log("b"); + }; +} + +class A { + @b() + a() { + console.log("a"); + } } diff --git a/cli/tests/config.ts.out b/cli/tests/config.ts.out index 9840dba2ec..99b4a7ea4f 100644 --- a/cli/tests/config.ts.out +++ b/cli/tests/config.ts.out @@ -1,7 +1,7 @@ [WILDCARD]Unsupported compiler options in "[WILDCARD]config.tsconfig.json". The following options were ignored: module, target -error: TS2532 [ERROR]: Object is possibly 'undefined'. -if (map.get("bar").foo) { - ~~~~~~~~~~~~~~ - at [WILDCARD]tests/config.ts:3:5 +error: TS1219 [ERROR]: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. + a() { + ^ + at file:///[WILDCARD]cli/tests/config.ts:[WILDCARD] diff --git a/cli/tests/config.tsconfig.json b/cli/tests/config.tsconfig.json index 074d7ac0bc..dcabb50a4a 100644 --- a/cli/tests/config.tsconfig.json +++ b/cli/tests/config.tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { + "experimentalDecorators": false, "module": "amd", - "strict": true, "target": "es5" } } diff --git a/cli/tests/disallow_http_from_https_js.out b/cli/tests/disallow_http_from_https_js.out index e4e4211590..405859e4d4 100644 --- a/cli/tests/disallow_http_from_https_js.out +++ b/cli/tests/disallow_http_from_https_js.out @@ -1,2 +1,3 @@ -error: Modules loaded over https:// are not allowed to import modules over http:// -Imported from "https://localhost:5545/cli/tests/disallow_http_from_https.js:2" +error: Modules imported via https are not allowed to import http modules. + Importing: http://localhost:4545/cli/tests/001_hello.js + at https://localhost:5545/cli/tests/disallow_http_from_https.js:2:0 diff --git a/cli/tests/disallow_http_from_https_ts.out b/cli/tests/disallow_http_from_https_ts.out index 55e10b7333..b63ba0c678 100644 --- a/cli/tests/disallow_http_from_https_ts.out +++ b/cli/tests/disallow_http_from_https_ts.out @@ -1,2 +1,3 @@ -error: Modules loaded over https:// are not allowed to import modules over http:// -Imported from "https://localhost:5545/cli/tests/disallow_http_from_https.ts:2" +error: Modules imported via https are not allowed to import http modules. + Importing: http://localhost:4545/cli/tests/001_hello.js + at https://localhost:5545/cli/tests/disallow_http_from_https.ts:2:0 diff --git a/cli/tests/error_004_missing_module.ts.out b/cli/tests/error_004_missing_module.ts.out index 121555868b..68032afb4f 100644 --- a/cli/tests/error_004_missing_module.ts.out +++ b/cli/tests/error_004_missing_module.ts.out @@ -1,2 +1,2 @@ -[WILDCARD]error: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/error_004_missing_module.ts" -Imported from "[WILDCARD]/error_004_missing_module.ts:2" +[WILDCARD]error: Cannot resolve module "file:///[WILDCARD]cli/tests/bad-module.ts" from "file:///[WILDCARD]cli/tests/error_004_missing_module.ts" + at file:///[WILDCARD]cli/tests/error_004_missing_module.ts:2:0 diff --git a/cli/tests/error_005_missing_dynamic_import.ts.out b/cli/tests/error_005_missing_dynamic_import.ts.out index 8a64175ec4..346e8cd6f9 100644 --- a/cli/tests/error_005_missing_dynamic_import.ts.out +++ b/cli/tests/error_005_missing_dynamic_import.ts.out @@ -1 +1 @@ -error: Uncaught TypeError: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/error_005_missing_dynamic_import.ts" +error: Cannot resolve module "[WILDCARD]/bad-module.ts" from "[WILDCARD]/error_005_missing_dynamic_import.ts" diff --git a/cli/tests/error_006_import_ext_failure.ts.out b/cli/tests/error_006_import_ext_failure.ts.out index 9e1c999708..01f8af07e4 100644 --- a/cli/tests/error_006_import_ext_failure.ts.out +++ b/cli/tests/error_006_import_ext_failure.ts.out @@ -1,2 +1,2 @@ [WILDCARD]error: Cannot resolve module "[WILDCARD]/non-existent" from "[WILDCARD]/error_006_import_ext_failure.ts" -Imported from "[WILDCARD]/error_006_import_ext_failure.ts:1" + at file:///[WILDCARD]cli/tests/error_006_import_ext_failure.ts:1:0 diff --git a/cli/tests/error_011_bad_module_specifier.ts b/cli/tests/error_011_bad_module_specifier.ts index e74d6b821f..a9ccc45231 100644 --- a/cli/tests/error_011_bad_module_specifier.ts +++ b/cli/tests/error_011_bad_module_specifier.ts @@ -1,2 +1,4 @@ // eslint-disable-next-line import * as badModule from "bad-module.ts"; + +console.log(badModule); diff --git a/cli/tests/error_015_dynamic_import_permissions.out b/cli/tests/error_015_dynamic_import_permissions.out index 7078ac61c0..9b47ebe9dd 100644 --- a/cli/tests/error_015_dynamic_import_permissions.out +++ b/cli/tests/error_015_dynamic_import_permissions.out @@ -1 +1 @@ -error: Uncaught TypeError: network access to "http://localhost:4545/cli/tests/subdir/mod4.js", run again with the --allow-net flag +error: network access to "http://localhost:4545/cli/tests/subdir/mod4.js", run again with the --allow-net flag diff --git a/cli/tests/error_016_dynamic_import_permissions2.out b/cli/tests/error_016_dynamic_import_permissions2.out index 2babfbf9f4..f54b4a7fe0 100644 --- a/cli/tests/error_016_dynamic_import_permissions2.out +++ b/cli/tests/error_016_dynamic_import_permissions2.out @@ -1,3 +1,4 @@ [WILDCARD] -error: Uncaught TypeError: read access to "[WILDCARD]passwd", run again with the --allow-read flag -Imported from "[WILDCARD]evil_remote_import.js:3" +error: Remote modules are not allowed to import local modules. Consider using a dynamic import instead. + Importing: file:///c:/etc/passwd + at http://localhost:4545/cli/tests/subdir/evil_remote_import.js:3:0 diff --git a/cli/tests/error_local_static_import_from_remote.js.out b/cli/tests/error_local_static_import_from_remote.js.out index 1a2dcb2e3e..071ca36d4f 100644 --- a/cli/tests/error_local_static_import_from_remote.js.out +++ b/cli/tests/error_local_static_import_from_remote.js.out @@ -1,3 +1,4 @@ [WILDCARD] -error: Remote modules are not allowed to statically import local modules. Use dynamic import instead. -Imported from "[WILDCARD]error_local_static_import_from_remote.js:1" +error: Remote modules are not allowed to import local modules. Consider using a dynamic import instead. + Importing: file:///some/dir/file.js + at http://localhost:4545/cli/tests/error_local_static_import_from_remote.js:1:0 diff --git a/cli/tests/error_local_static_import_from_remote.ts.out b/cli/tests/error_local_static_import_from_remote.ts.out index a2f2e1bbf1..38f4c02bcc 100644 --- a/cli/tests/error_local_static_import_from_remote.ts.out +++ b/cli/tests/error_local_static_import_from_remote.ts.out @@ -1,3 +1,4 @@ [WILDCARD] -error: Remote modules are not allowed to statically import local modules. Use dynamic import instead. -Imported from "[WILDCARD]error_local_static_import_from_remote.ts:1" +error: Remote modules are not allowed to import local modules. Consider using a dynamic import instead. + Importing: file:///some/dir/file.ts + at http://localhost:4545/cli/tests/error_local_static_import_from_remote.ts:1:0 diff --git a/cli/tests/fix_exotic_specifiers.ts b/cli/tests/fix_exotic_specifiers.ts new file mode 100644 index 0000000000..101667b2a9 --- /dev/null +++ b/cli/tests/fix_exotic_specifiers.ts @@ -0,0 +1,3 @@ +import clone from "https://jspm.dev/lodash@4/clone"; + +console.log(clone); diff --git a/cli/tests/fix_exotic_specifiers.ts.out b/cli/tests/fix_exotic_specifiers.ts.out new file mode 100644 index 0000000000..7afdb808db --- /dev/null +++ b/cli/tests/fix_exotic_specifiers.ts.out @@ -0,0 +1 @@ +[Function: clone] diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 047440a70c..b9264aa0f5 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1807,9 +1807,9 @@ itest!(_022_info_flag_script { http_server: true, }); -itest!(_023_no_ext_with_headers { - args: "run --reload 023_no_ext_with_headers", - output: "023_no_ext_with_headers.out", +itest!(_023_no_ext { + args: "run --reload 023_no_ext", + output: "023_no_ext.out", }); // TODO(lucacasonato): remove --unstable when permissions goes stable @@ -2018,7 +2018,7 @@ itest!(_044_bad_resource { }); itest!(_045_proxy { - args: "run --allow-net --allow-env --allow-run --allow-read --reload --quiet 045_proxy_test.ts", + args: "run -L debug --allow-net --allow-env --allow-run --allow-read --reload --quiet 045_proxy_test.ts", output: "045_proxy_test.ts.out", http_server: true, }); @@ -2764,6 +2764,11 @@ itest!(tsx_imports { output: "tsx_imports.ts.out", }); +itest!(fix_exotic_specifiers { + args: "run --quiet --reload fix_exotic_specifiers.ts", + output: "fix_exotic_specifiers.ts.out", +}); + itest!(fix_js_import_js { args: "run --quiet --reload fix_js_import_js.ts", output: "fix_js_import_js.ts.out", diff --git a/cli/tests/lock_check_err.out b/cli/tests/lock_check_err.out index 87f0242f7a..c12f4af349 100644 --- a/cli/tests/lock_check_err.out +++ b/cli/tests/lock_check_err.out @@ -1,2 +1,3 @@ -[WILDCARD]Subresource integrity check failed --lock=lock_check_err.json -http://127.0.0.1:4545/cli/tests/003_relative_import.ts +[WILDCARD]The source code is invalid, as it does not match the expected hash in the lock file. + Specifier: http://127.0.0.1:4545/cli/tests/003_relative_import.ts + Lock file: lock_check_err.json diff --git a/cli/tests/lock_check_err2.out b/cli/tests/lock_check_err2.out index 6b81c9713a..d3ccfc46ef 100644 --- a/cli/tests/lock_check_err2.out +++ b/cli/tests/lock_check_err2.out @@ -1,2 +1,3 @@ -[WILDCARD]Subresource integrity check failed --lock=lock_check_err2.json -http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js +[WILDCARD]The source code is invalid, as it does not match the expected hash in the lock file. + Specifier: http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js + Lock file: lock_check_err2.json diff --git a/cli/tests/lock_dynamic_imports.out b/cli/tests/lock_dynamic_imports.out index 57bc053b98..3bf6342c8b 100644 --- a/cli/tests/lock_dynamic_imports.out +++ b/cli/tests/lock_dynamic_imports.out @@ -1,3 +1,4 @@ [WILDCARD] -Subresource integrity check failed --lock=lock_dynamic_imports.json -http://127.0.0.1:4545/cli/tests/subdir/subdir2/mod2.ts +The source code is invalid, as it does not match the expected hash in the lock file. + Specifier: http://127.0.0.1:4545/cli/tests/subdir/subdir2/mod2.ts + Lock file: lock_dynamic_imports.json diff --git a/cli/tests/module_graph/file_tests-importjson.ts b/cli/tests/module_graph/file_tests-importjson.ts new file mode 100644 index 0000000000..c2bc2bca78 --- /dev/null +++ b/cli/tests/module_graph/file_tests-importjson.ts @@ -0,0 +1,3 @@ +import * as config from "./some.json"; + +console.log(config); diff --git a/cli/tests/module_graph/file_tests-some.json b/cli/tests/module_graph/file_tests-some.json new file mode 100644 index 0000000000..567c4ba21a --- /dev/null +++ b/cli/tests/module_graph/file_tests-some.json @@ -0,0 +1,5 @@ +{ + "config": { + "debug": true + } +} diff --git a/cli/tests/performance_stats.out b/cli/tests/performance_stats.out index 0fe7ba7f0d..141829ee6c 100644 --- a/cli/tests/performance_stats.out +++ b/cli/tests/performance_stats.out @@ -1,14 +1,16 @@ [WILDCARD] -DEBUG RS - [WILDCARD] - Files: [WILDCARD] -DEBUG RS - [WILDCARD] - Nodes: [WILDCARD] -DEBUG RS - [WILDCARD] - Identifiers: [WILDCARD] -DEBUG RS - [WILDCARD] - Symbols: [WILDCARD] -DEBUG RS - [WILDCARD] - Types: [WILDCARD] -DEBUG RS - [WILDCARD] - Instantiations: [WILDCARD] -DEBUG RS - [WILDCARD] - Parse time: [WILDCARD] -DEBUG RS - [WILDCARD] - Bind time: [WILDCARD] -DEBUG RS - [WILDCARD] - Check time: [WILDCARD] -DEBUG RS - [WILDCARD] - Emit time: [WILDCARD] -DEBUG RS - [WILDCARD] - Total TS time: [WILDCARD] -DEBUG RS - [WILDCARD] - Compile time: [WILDCARD] +DEBUG RS - [WILDCARD] - Compilation statistics: + Files: [WILDCARD] + Nodes: [WILDCARD] + Identifiers: [WILDCARD] + Symbols: [WILDCARD] + Types: [WILDCARD] + Instantiations: [WILDCARD] + Parse time: [WILDCARD] + Bind time: [WILDCARD] + Check time: [WILDCARD] + Emit time: [WILDCARD] + Total TS time: [WILDCARD] + Compile time: [WILDCARD] + [WILDCARD] diff --git a/cli/tests/single_compile_with_reload.ts.out b/cli/tests/single_compile_with_reload.ts.out index 88c3f97ab6..4ffaa6e772 100644 --- a/cli/tests/single_compile_with_reload.ts.out +++ b/cli/tests/single_compile_with_reload.ts.out @@ -1,5 +1,4 @@ Check [WILDCARD]single_compile_with_reload.ts -Check [WILDCARD]single_compile_with_reload_dyn.ts Hello 1 2 diff --git a/cli/tests/ts_type_only_import.ts.out b/cli/tests/ts_type_only_import.ts.out index d7120966f0..f808ed21ad 100644 --- a/cli/tests/ts_type_only_import.ts.out +++ b/cli/tests/ts_type_only_import.ts.out @@ -1,4 +1,4 @@ Check [WILDCARD]ts_type_only_import.ts -Warning Failed to get compiled source code of "[WILDCARD]ts_type_only_import.d.ts". -Reason: [WILDCARD] (os error 2) -If the source file provides only type exports, prefer to use "import type" or "export type" syntax instead. +warning: Compiled module not found "[WILDCARD]ts_type_only_import.d.ts" + From: [WILDCARD]ts_type_only_import.ts + If the source module contains only types, use `import type` and `export type` to import it instead. diff --git a/cli/tests/unsupported_dynamic_import_scheme.out b/cli/tests/unsupported_dynamic_import_scheme.out index 2a1a4e01f0..0161b7a99f 100644 --- a/cli/tests/unsupported_dynamic_import_scheme.out +++ b/cli/tests/unsupported_dynamic_import_scheme.out @@ -1,4 +1,4 @@ -error: Uncaught TypeError: Unsupported scheme "xxx" for module "xxx:". Supported schemes: [ +error: Unsupported scheme "xxx" for module "xxx:". Supported schemes: [ "http", "https", "file", diff --git a/cli/tsc.rs b/cli/tsc.rs index ac73e8886f..4cf253b7c4 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -2,7 +2,6 @@ use crate::ast::parse; use crate::ast::Location; -use crate::colors; use crate::diagnostics::Diagnostics; use crate::disk_cache::DiskCache; use crate::file_fetcher::SourceFile; @@ -14,7 +13,6 @@ use crate::module_graph::ModuleGraph; use crate::module_graph::ModuleGraphLoader; use crate::permissions::Permissions; use crate::program_state::ProgramState; -use crate::source_maps::SourceMapGetter; use crate::tsc_config; use crate::version; use deno_core::error::generic_error; @@ -29,7 +27,6 @@ use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_core::RuntimeOptions; use log::debug; -use log::info; use log::Level; use regex::Regex; use serde::Deserialize; @@ -231,12 +228,6 @@ pub struct CompiledFileMetadata { } impl CompiledFileMetadata { - pub fn from_json_string( - metadata_string: String, - ) -> Result { - serde_json::from_str::(&metadata_string) - } - pub fn to_json_string(&self) -> Result { serde_json::to_string(self) } @@ -308,15 +299,6 @@ struct BundleResponse { stats: Option>, } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct CompileResponse { - diagnostics: Diagnostics, - emit_map: HashMap, - build_info: Option, - stats: Option>, -} - // TODO(bartlomieju): possible deduplicate once TS refactor is stabilized #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -360,197 +342,6 @@ impl TsCompiler { c.insert(url.clone()); } - fn has_compiled(&self, url: &Url) -> bool { - let c = self.compiled.lock().unwrap(); - c.contains(url) - } - - /// Check if there is compiled source in cache that is valid and can be used - /// again. - fn has_compiled_source(&self, url: &Url) -> bool { - let specifier = ModuleSpecifier::from(url.clone()); - if let Some(source_file) = self - .file_fetcher - .fetch_cached_source_file(&specifier, Permissions::allow_all()) - { - if let Some(metadata) = self.get_metadata(&url) { - // Compare version hashes - let version_hash_to_validate = source_code_version_hash( - &source_file.source_code.as_bytes(), - version::DENO, - &self.config.hash.as_bytes(), - ); - - if metadata.version_hash == version_hash_to_validate { - return true; - } - } - } - - false - } - - fn has_valid_cache( - &self, - url: &Url, - build_info: &Option, - ) -> Result { - if let Some(build_info_str) = build_info.as_ref() { - let build_inf_json: Value = serde_json::from_str(build_info_str)?; - let program_val = build_inf_json["program"].as_object().unwrap(); - let file_infos = program_val["fileInfos"].as_object().unwrap(); - - if !self.has_compiled_source(url) { - return Ok(false); - } - - for (filename, file_info) in file_infos.iter() { - if filename.starts_with("asset://") { - continue; - } - - let url = Url::parse(&filename).expect("Filename is not a valid url"); - let specifier = ModuleSpecifier::from(url); - - if let Some(source_file) = self - .file_fetcher - .fetch_cached_source_file(&specifier, Permissions::allow_all()) - { - let existing_hash = crate::checksum::gen(&[ - &source_file.source_code.as_bytes(), - &version::DENO.as_bytes(), - ]); - let expected_hash = - file_info["version"].as_str().unwrap().to_string(); - if existing_hash != expected_hash { - // hashes don't match, somethings changed - return Ok(false); - } - } else { - // no cached source file - return Ok(false); - } - } - } else { - // no build info - return Ok(false); - } - - Ok(true) - } - - /// Asynchronously compile module and all it's dependencies. - /// - /// This method compiled every module at most once. - /// - /// If `--reload` flag was provided then compiler will not on-disk cache and - /// force recompilation. - /// - /// If compilation is required then new V8 worker is spawned with fresh TS - /// compiler. - pub async fn compile( - &self, - program_state: &Arc, - source_file: &SourceFile, - target: TargetLib, - module_graph: &ModuleGraph, - allow_js: bool, - ) -> Result<(), AnyError> { - let module_url = source_file.url.clone(); - let build_info_key = self - .disk_cache - .get_cache_filename_with_extension(&module_url, "buildinfo"); - let build_info = match self.disk_cache.get(&build_info_key) { - Ok(bytes) => Some(String::from_utf8(bytes)?), - Err(_) => None, - }; - - // Only use disk cache if `--reload` flag was not used or this file has - // already been compiled during current process lifetime. - if (self.use_disk_cache || self.has_compiled(&source_file.url)) - && self.has_valid_cache(&source_file.url, &build_info)? - { - return Ok(()); - } - - let module_graph_json = - serde_json::to_value(module_graph).expect("Failed to serialize data"); - let target = match target { - TargetLib::Main => "main", - TargetLib::Worker => "worker", - }; - let root_names = vec![module_url.to_string()]; - let unstable = self.flags.unstable; - let performance = matches!(self.flags.log_level, Some(Level::Debug)); - let compiler_config = self.config.clone(); - - // TODO(bartlomieju): lift this call up - TSC shouldn't print anything - info!("{} {}", colors::green("Check"), module_url.to_string()); - - let mut lib = if target == "main" { - vec!["deno.window"] - } else { - vec!["deno.worker"] - }; - - if unstable { - lib.push("deno.unstable"); - } - - let mut compiler_options = json!({ - "allowJs": allow_js, - "allowNonTsExtensions": true, - "checkJs": false, - "esModuleInterop": true, - "incremental": true, - "inlineSourceMap": true, - // TODO(lucacasonato): enable this by default in 1.5.0 - "isolatedModules": unstable, - "jsx": "react", - "lib": lib, - "module": "esnext", - "outDir": "deno://", - "resolveJsonModule": true, - "sourceMap": false, - "strict": true, - "removeComments": true, - "target": "esnext", - "tsBuildInfoFile": "cache:///tsbuildinfo.json", - }); - - tsc_config::json_merge(&mut compiler_options, &compiler_config.options); - - warn_ignored_options(compiler_config.maybe_ignored_options); - - let j = json!({ - "type": CompilerRequestType::Compile, - "target": target, - "rootNames": root_names, - "performance": performance, - "compilerOptions": compiler_options, - "sourceFileMap": module_graph_json, - "buildInfo": if self.use_disk_cache { build_info } else { None }, - }); - - let req_msg = j.to_string(); - - let json_str = execute_in_tsc(program_state.clone(), req_msg)?; - - let compile_response: CompileResponse = serde_json::from_str(&json_str)?; - - if !compile_response.diagnostics.0.is_empty() { - return Err(generic_error(compile_response.diagnostics.to_string())); - } - - maybe_log_stats(compile_response.stats); - - if let Some(build_info) = compile_response.build_info { - self.cache_build_info(&module_url, build_info)?; - } - self.cache_emitted_files(compile_response.emit_map)?; - Ok(()) - } - /// For a given module, generate a single file JavaScript output that includes /// all the dependencies for that module. pub async fn bundle( @@ -666,39 +457,6 @@ impl TsCompiler { Ok(output) } - /// Get associated `CompiledFileMetadata` for given module if it exists. - fn get_metadata(&self, url: &Url) -> Option { - // Try to load cached version: - // 1. check if there's 'meta' file - let cache_key = self - .disk_cache - .get_cache_filename_with_extension(url, "meta"); - if let Ok(metadata_bytes) = self.disk_cache.get(&cache_key) { - if let Ok(metadata) = std::str::from_utf8(&metadata_bytes) { - if let Ok(read_metadata) = - CompiledFileMetadata::from_json_string(metadata.to_string()) - { - return Some(read_metadata); - } - } - } - - None - } - - fn cache_build_info( - &self, - url: &Url, - build_info: String, - ) -> std::io::Result<()> { - let js_key = self - .disk_cache - .get_cache_filename_with_extension(url, "buildinfo"); - self.disk_cache.set(&js_key, build_info.as_bytes())?; - - Ok(()) - } - fn cache_emitted_files( &self, emit_map: HashMap, @@ -730,45 +488,6 @@ impl TsCompiler { Ok(()) } - pub fn get_compiled_module( - &self, - module_url: &Url, - ) -> Result { - let compiled_source_file = self.get_compiled_source_file(module_url)?; - - let compiled_module = CompiledModule { - code: compiled_source_file.source_code, - name: module_url.to_string(), - }; - - Ok(compiled_module) - } - - /// Return compiled JS file for given TS module. - // TODO: ideally we shouldn't construct SourceFile by hand, but it should be - // delegated to SourceFileFetcher. - pub fn get_compiled_source_file( - &self, - module_url: &Url, - ) -> Result { - let cache_key = self - .disk_cache - .get_cache_filename_with_extension(&module_url, "js"); - let compiled_code = self.disk_cache.get(&cache_key)?; - let compiled_code_filename = self.disk_cache.location.join(cache_key); - debug!("compiled filename: {:?}", compiled_code_filename); - - let compiled_module = SourceFile { - url: module_url.clone(), - filename: compiled_code_filename, - media_type: MediaType::JavaScript, - source_code: String::from_utf8(compiled_code)?, - types_header: None, - }; - - Ok(compiled_module) - } - /// Save compiled JS file for given TS module to on-disk cache. /// /// Along compiled file a special metadata file is saved as well containing @@ -801,31 +520,6 @@ impl TsCompiler { ) } - /// Return associated source map file for given TS module. - // TODO: ideally we shouldn't construct SourceFile by hand, but it should be delegated to - // SourceFileFetcher - pub fn get_source_map_file( - &self, - module_specifier: &ModuleSpecifier, - ) -> Result { - let cache_key = self - .disk_cache - .get_cache_filename_with_extension(module_specifier.as_url(), "js.map"); - let source_code = self.disk_cache.get(&cache_key)?; - let source_map_filename = self.disk_cache.location.join(cache_key); - debug!("source map filename: {:?}", source_map_filename); - - let source_map_file = SourceFile { - url: module_specifier.as_url().to_owned(), - filename: source_map_filename, - media_type: MediaType::JavaScript, - source_code: String::from_utf8(source_code)?, - types_header: None, - }; - - Ok(source_map_file) - } - /// Save source map file for given TS module to on-disk cache. fn cache_source_map( &self, @@ -856,91 +550,6 @@ impl TsCompiler { } } -impl SourceMapGetter for TsCompiler { - fn get_source_map(&self, script_name: &str) -> Option> { - self.try_to_resolve_and_get_source_map(script_name) - } - - fn get_source_line(&self, script_name: &str, line: usize) -> Option { - self - .try_resolve_and_get_source_file(script_name) - .map(|out| { - // Do NOT use .lines(): it skips the terminating empty line. - // (due to internally using .split_terminator() instead of .split()) - let lines: Vec<&str> = out.source_code.split('\n').collect(); - assert!(lines.len() > line); - lines[line].to_string() - }) - } -} - -// `SourceMapGetter` related methods -impl TsCompiler { - fn try_to_resolve(&self, script_name: &str) -> Option { - // if `script_name` can't be resolved to ModuleSpecifier it's probably internal - // script (like `gen/cli/bundle/compiler.js`) so we won't be - // able to get source for it anyway - ModuleSpecifier::resolve_url(script_name).ok() - } - - fn try_resolve_and_get_source_file( - &self, - script_name: &str, - ) -> Option { - if let Some(module_specifier) = self.try_to_resolve(script_name) { - return self - .file_fetcher - .fetch_cached_source_file(&module_specifier, Permissions::allow_all()); - } - - None - } - - fn try_to_resolve_and_get_source_map( - &self, - script_name: &str, - ) -> Option> { - if let Some(module_specifier) = self.try_to_resolve(script_name) { - if module_specifier.as_url().scheme() == "deno" { - return None; - } - return match self.get_source_map_file(&module_specifier) { - Ok(out) => Some(out.source_code.into_bytes()), - Err(_) => { - // Check if map is inlined - if let Ok(compiled_source) = - self.get_compiled_module(module_specifier.as_url()) - { - let mut content_lines = compiled_source - .code - .split('\n') - .map(|s| s.to_string()) - .collect::>(); - - if !content_lines.is_empty() { - let last_line = content_lines.pop().unwrap(); - if last_line.starts_with( - "//# sourceMappingURL=data:application/json;base64,", - ) { - let encoded = last_line.trim_start_matches( - "//# sourceMappingURL=data:application/json;base64,", - ); - let decoded_map = - base64::decode(encoded).expect("failed to parse source map"); - return Some(decoded_map); - } - } - } - - None - } - }; - } - - None - } -} - #[derive(Debug, Deserialize)] struct CreateHashArgs { data: String, @@ -1425,7 +1034,6 @@ fn parse_deno_types(comment: &str) -> Option { #[repr(i32)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum CompilerRequestType { - Compile = 0, Bundle = 1, RuntimeCompile = 2, RuntimeBundle = 3, @@ -1438,7 +1046,6 @@ impl Serialize for CompilerRequestType { S: Serializer, { let value: i32 = match self { - CompilerRequestType::Compile => 0 as i32, CompilerRequestType::Bundle => 1 as i32, CompilerRequestType::RuntimeCompile => 2 as i32, CompilerRequestType::RuntimeBundle => 3 as i32, @@ -1451,12 +1058,8 @@ impl Serialize for CompilerRequestType { #[cfg(test)] mod tests { use super::*; - use crate::deno_dir; use crate::fs as deno_fs; - use crate::http_cache; use crate::program_state::ProgramState; - use deno_core::ModuleSpecifier; - use std::path::PathBuf; use tempfile::TempDir; #[test] @@ -1516,75 +1119,6 @@ mod tests { assert!(parse_ts_reference(r#"/ "#).is_none()); } - #[tokio::test] - async fn test_compile() { - let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .join("cli/tests/002_hello.ts"); - let specifier = - ModuleSpecifier::resolve_url_or_path(p.to_str().unwrap()).unwrap(); - let out = SourceFile { - url: specifier.as_url().clone(), - filename: PathBuf::from(p.to_str().unwrap().to_string()), - media_type: MediaType::TypeScript, - source_code: include_str!("./tests/002_hello.ts").to_string(), - types_header: None, - }; - let dir = - deno_dir::DenoDir::new(Some(test_util::new_deno_dir().path().to_owned())) - .unwrap(); - let http_cache = http_cache::HttpCache::new(&dir.root.join("deps")); - let mock_state = ProgramState::mock( - vec![String::from("deno"), String::from("hello.ts")], - None, - ); - let file_fetcher = SourceFileFetcher::new( - http_cache, - true, - mock_state.flags.cache_blocklist.clone(), - false, - false, - None, - ) - .unwrap(); - - let mut module_graph_loader = ModuleGraphLoader::new( - file_fetcher.clone(), - None, - Permissions::allow_all(), - false, - false, - ); - module_graph_loader - .add_to_graph(&specifier, None) - .await - .expect("Failed to create graph"); - let module_graph = module_graph_loader.get_graph(); - - let ts_compiler = TsCompiler::new( - file_fetcher, - mock_state.flags.clone(), - dir.gen_cache.clone(), - ) - .unwrap(); - - let result = ts_compiler - .compile(&mock_state, &out, TargetLib::Main, &module_graph, false) - .await; - assert!(result.is_ok()); - let compiled_file = ts_compiler.get_compiled_module(&out.url).unwrap(); - let source_code = compiled_file.code; - assert!(source_code - .as_bytes() - .starts_with(b"\"use strict\";\nconsole.log(\"Hello World\");")); - let mut lines: Vec = - source_code.split('\n').map(|s| s.to_string()).collect(); - let last_line = lines.pop().unwrap(); - assert!(last_line - .starts_with("//# sourceMappingURL=data:application/json;base64")); - } - #[tokio::test] async fn test_bundle() { let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 91bce61e3d..86a68a6bd4 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -163,8 +163,9 @@ delete Object.prototype.__proto__; 4: "TSX", 5: "Json", 6: "Wasm", - 7: "BuildInfo", - 8: "Unknown", + 7: "TsBuildInfo", + 8: "SourceMap", + 9: "Unknown", JavaScript: 0, JSX: 1, TypeScript: 2, @@ -172,8 +173,9 @@ delete Object.prototype.__proto__; TSX: 4, Json: 5, Wasm: 6, - BuildInfo: 7, - Unknown: 6, + TsBuildInfo: 7, + SourceMap: 8, + Unknown: 9, }; function getExtension(fileName, mediaType) { @@ -183,7 +185,9 @@ delete Object.prototype.__proto__; case MediaType.JSX: return ts.Extension.Jsx; case MediaType.TypeScript: - return fileName.endsWith(".d.ts") ? ts.Extension.Dts : ts.Extension.Ts; + return ts.Extension.Ts; + case MediaType.Dts: + return ts.Extension.Dts; case MediaType.TSX: return ts.Extension.Tsx; case MediaType.Wasm: @@ -366,7 +370,7 @@ delete Object.prototype.__proto__; } /** @type {{ data: string; hash: string; }} */ - const { data, hash } = core.jsonOpSync( + const { data, hash, scriptKind } = core.jsonOpSync( "op_load", { specifier }, ); @@ -375,6 +379,8 @@ delete Object.prototype.__proto__; specifier, data, languageVersion, + false, + scriptKind, ); sourceFile.moduleName = specifier; sourceFile.version = hash; @@ -406,7 +412,6 @@ delete Object.prototype.__proto__; let maybeSpecifiers; if (sourceFiles) { maybeSpecifiers = sourceFiles.map((sf) => sf.moduleName); - debug(` specifiers: ${maybeSpecifiers.join(", ")}`); } return core.jsonOpSync( "op_emit", @@ -465,11 +470,12 @@ delete Object.prototype.__proto__; specifiers, base, }); - return resolved.map(([resolvedFileName, extension]) => ({ + let r = resolved.map(([resolvedFileName, extension]) => ({ resolvedFileName, extension, isExternalLibraryImport: false, })); + return r; } }, createHash(data) { @@ -649,7 +655,6 @@ delete Object.prototype.__proto__; // Warning! The values in this enum are duplicated in `cli/msg.rs` // Update carefully! const CompilerRequestType = { - Compile: 0, Bundle: 1, RuntimeCompile: 2, RuntimeBundle: 3, @@ -671,25 +676,6 @@ delete Object.prototype.__proto__; }; } - function createCompileWriteFile(state) { - return function writeFile(fileName, data, sourceFiles) { - const isBuildInfo = fileName === TS_BUILD_INFO; - - if (isBuildInfo) { - assert(isBuildInfo); - state.buildInfo = data; - return; - } - - assert(sourceFiles); - assert(sourceFiles.length === 1); - state.emitMap[fileName] = { - filename: sourceFiles[0].fileName, - contents: data, - }; - }; - } - function createRuntimeCompileWriteFile(state) { return function writeFile(fileName, data, sourceFiles) { assert(sourceFiles); @@ -959,101 +945,6 @@ delete Object.prototype.__proto__; .map((sym) => sym.getName()); } - function compile({ - buildInfo, - compilerOptions, - rootNames, - target, - sourceFileMap, - type, - performance, - }) { - if (performance) { - performanceStart(); - } - debug(">>> compile start", { rootNames, type: CompilerRequestType[type] }); - - // When a programme is emitted, TypeScript will call `writeFile` with - // each file that needs to be emitted. The Deno compiler host delegates - // this, to make it easier to perform the right actions, which vary - // based a lot on the request. - const state = { - rootNames, - emitMap: {}, - }; - - let diagnostics = []; - - const { options, diagnostics: diags } = parseCompilerOptions( - compilerOptions, - ); - - diagnostics = diags.filter( - ({ code }) => code != 5023 && !IGNORED_DIAGNOSTICS.includes(code), - ); - - // TODO(bartlomieju): this options is excluded by `ts.convertCompilerOptionsFromJson` - // however stuff breaks if it's not passed (type_directives_js_main.js, compiler_js_error.ts) - options.allowNonTsExtensions = true; - - legacyHostState.target = target; - legacyHostState.writeFile = createCompileWriteFile(state); - legacyHostState.buildInfo = buildInfo; - - buildSourceFileCache(sourceFileMap); - // if there was a configuration and no diagnostics with it, we will continue - // to generate the program and possibly emit it. - if (diagnostics.length === 0) { - const program = ts.createIncrementalProgram({ - rootNames, - options, - host, - }); - - // TODO(bartlomieju): check if this is ok - diagnostics = [ - ...program.getConfigFileParsingDiagnostics(), - ...program.getSyntacticDiagnostics(), - ...program.getOptionsDiagnostics(), - ...program.getGlobalDiagnostics(), - ...program.getSemanticDiagnostics(), - ]; - diagnostics = diagnostics.filter( - ({ code }) => - !IGNORED_DIAGNOSTICS.includes(code) && - !IGNORED_COMPILE_DIAGNOSTICS.includes(code), - ); - - // We will only proceed with the emit if there are no diagnostics. - if (diagnostics.length === 0) { - const emitResult = program.emit(); - // If `checkJs` is off we still might be compiling entry point JavaScript file - // (if it has `.ts` imports), but it won't be emitted. In that case we skip - // assertion. - if (options.checkJs) { - assert( - emitResult.emitSkipped === false, - "Unexpected skip of the emit.", - ); - } - // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned - // without casting. - diagnostics = emitResult.diagnostics; - } - performanceProgram({ program }); - } - - debug("<<< compile end", { rootNames, type: CompilerRequestType[type] }); - const stats = performance ? performanceEnd() : undefined; - - return { - emitMap: state.emitMap, - buildInfo: state.buildInfo, - diagnostics: fromTypeScriptDiagnostic(diagnostics), - stats, - }; - } - function bundle({ compilerOptions, rootNames, @@ -1296,11 +1187,6 @@ delete Object.prototype.__proto__; function tsCompilerOnMessage(msg) { const request = msg.data; switch (request.type) { - case CompilerRequestType.Compile: { - const result = compile(request); - opCompilerRespond(result); - break; - } case CompilerRequestType.Bundle: { const result = bundle(request); opCompilerRespond(result); diff --git a/cli/tsc2.rs b/cli/tsc2.rs index 64563ce01e..b3c14d6310 100644 --- a/cli/tsc2.rs +++ b/cli/tsc2.rs @@ -21,6 +21,7 @@ use deno_core::RuntimeOptions; use deno_core::Snapshot; use serde::Deserialize; use serde::Serialize; +use std::cell::RefCell; use std::rc::Rc; #[derive(Debug, Clone, Default, Eq, PartialEq)] @@ -40,7 +41,7 @@ pub struct Request { /// Indicates to the tsc runtime if debug logging should occur. pub debug: bool, #[serde(skip_serializing)] - pub graph: Rc, + pub graph: Rc>, #[serde(skip_serializing)] pub hash_data: Vec>, #[serde(skip_serializing)] @@ -65,14 +66,14 @@ pub struct Response { struct State { hash_data: Vec>, emitted_files: Vec, - graph: Rc, + graph: Rc>, maybe_tsbuildinfo: Option, maybe_response: Option, } impl State { pub fn new( - graph: Rc, + graph: Rc>, hash_data: Vec>, maybe_tsbuildinfo: Option, ) -> Self { @@ -162,10 +163,23 @@ fn load(state: &mut State, args: Value) -> Result { let specifier = ModuleSpecifier::resolve_url_or_path(&v.specifier) .context("Error converting a string module specifier for \"op_load\".")?; let mut hash: Option = None; + let mut media_type = MediaType::Unknown; let data = if &v.specifier == "deno:///.tsbuildinfo" { state.maybe_tsbuildinfo.clone() + // in certain situations we return a "blank" module to tsc and we need to + // handle the request for that module here. + } else if &v.specifier == "deno:///none.d.ts" { + hash = Some("1".to_string()); + media_type = MediaType::TypeScript; + Some("declare var a: any;\nexport = a;\n".to_string()) } else { - let maybe_source = state.graph.get_source(&specifier); + let graph = state.graph.borrow(); + let maybe_source = graph.get_source(&specifier); + media_type = if let Some(media_type) = graph.get_media_type(&specifier) { + media_type + } else { + MediaType::Unknown + }; if let Some(source) = &maybe_source { let mut data = vec![source.as_bytes().to_owned()]; data.extend_from_slice(&state.hash_data); @@ -174,7 +188,9 @@ fn load(state: &mut State, args: Value) -> Result { maybe_source }; - Ok(json!({ "data": data, "hash": hash })) + Ok( + json!({ "data": data, "hash": hash, "scriptKind": media_type.as_ts_script_kind() }), + ) } #[derive(Debug, Deserialize)] @@ -201,19 +217,31 @@ fn resolve(state: &mut State, args: Value) -> Result { MediaType::from(specifier).as_ts_extension().to_string(), )); } else { - let resolved_specifier = state.graph.resolve(specifier, &referrer)?; - let media_type = if let Some(media_type) = - state.graph.get_media_type(&resolved_specifier) - { - media_type - } else { - bail!( - "Unable to resolve media type for specifier: \"{}\"", - resolved_specifier - ) - }; - resolved - .push((resolved_specifier.to_string(), media_type.as_ts_extension())); + let graph = state.graph.borrow(); + match graph.resolve(specifier, &referrer, true) { + Ok(resolved_specifier) => { + let media_type = if let Some(media_type) = + graph.get_media_type(&resolved_specifier) + { + media_type + } else { + bail!( + "Unable to resolve media type for specifier: \"{}\"", + resolved_specifier + ) + }; + resolved.push(( + resolved_specifier.to_string(), + media_type.as_ts_extension(), + )); + } + // in certain situations, like certain dynamic imports, we won't have + // the source file in the graph, so we will return a fake module to + // make tsc happy. + Err(_) => { + resolved.push(("deno:///none.d.ts".to_string(), ".d.ts".to_string())); + } + } } } @@ -221,7 +249,7 @@ fn resolve(state: &mut State, args: Value) -> Result { } #[derive(Debug, Deserialize, Eq, PartialEq)] -pub struct RespondArgs { +struct RespondArgs { pub diagnostics: Diagnostics, pub stats: Stats, } @@ -269,9 +297,7 @@ pub fn exec( runtime .execute("[native code]", startup_source) .context("Could not properly start the compiler runtime.")?; - runtime - .execute("[native_code]", &exec_source) - .context("Execute request failed.")?; + runtime.execute("[native_code]", &exec_source)?; let op_state = runtime.op_state(); let mut op_state = op_state.borrow_mut(); @@ -324,10 +350,10 @@ mod tests { })); let mut builder = GraphBuilder2::new(handler.clone(), None); builder - .insert(&specifier) + .add(&specifier, false) .await .expect("module not inserted"); - let graph = Rc::new(builder.get_graph(&None).expect("could not get graph")); + let graph = Rc::new(RefCell::new(builder.get_graph(&None))); State::new(graph, hash_data, maybe_tsbuildinfo) } @@ -410,7 +436,8 @@ mod tests { actual, json!({ "data": "console.log(\"hello deno\");\n", - "hash": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729" + "hash": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729", + "scriptKind": 3, }) ); } @@ -433,7 +460,8 @@ mod tests { actual, json!({ "data": "some content", - "hash": null + "hash": null, + "scriptKind": 0, }) ); } @@ -451,6 +479,7 @@ mod tests { json!({ "data": null, "hash": null, + "scriptKind": 0, }) ) } @@ -475,7 +504,7 @@ mod tests { } #[tokio::test] - async fn test_resolve_error() { + async fn test_resolve_empty() { let mut state = setup( Some( ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts") @@ -485,10 +514,11 @@ mod tests { None, ) .await; - resolve( + let actual = resolve( &mut state, json!({ "base": "https://deno.land/x/a.ts", "specifiers": [ "./bad.ts" ]}), - ).expect_err("should have errored"); + ).expect("should have not errored"); + assert_eq!(actual, json!([["deno:///none.d.ts", ".d.ts"]])); } #[tokio::test] @@ -544,17 +574,16 @@ mod tests { })); let mut builder = GraphBuilder2::new(handler.clone(), None); builder - .insert(&specifier) + .add(&specifier, false) .await .expect("module not inserted"); - let graph = Rc::new(builder.get_graph(&None).expect("could not get graph")); + let graph = Rc::new(RefCell::new(builder.get_graph(&None))); let config = TsConfig::new(json!({ "allowJs": true, "checkJs": false, "esModuleInterop": true, "emitDecoratorMetadata": false, "incremental": true, - "isolatedModules": true, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", diff --git a/cli/tsc_config.rs b/cli/tsc_config.rs index 15a172a729..9d05c33f79 100644 --- a/cli/tsc_config.rs +++ b/cli/tsc_config.rs @@ -214,6 +214,21 @@ impl TsConfig { self.0.to_string().as_bytes().to_owned() } + /// Return the value of the `checkJs` compiler option, defaulting to `false` + /// if not present. + pub fn get_check_js(&self) -> bool { + if let Some(check_js) = self.0.get("checkJs") { + check_js.as_bool().unwrap_or(false) + } else { + false + } + } + + /// Merge a serde_json value into the configuration. + pub fn merge(&mut self, value: &Value) { + json_merge(&mut self.0, value); + } + /// Take an optional string representing a user provided TypeScript config file /// which was passed in via the `--config` compiler option and merge it with /// the configuration. Returning the result which optionally contains any diff --git a/cli/worker.rs b/cli/worker.rs index 877af32089..a8722e7a4e 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -121,7 +121,7 @@ impl Worker { module_loader: Some(module_loader), startup_snapshot: Some(startup_snapshot), js_error_create_fn: Some(Box::new(move |core_js_error| { - JsError::create(core_js_error, &global_state_.ts_compiler) + JsError::create(core_js_error, global_state_.clone()) })), ..Default::default() });