diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs index 1cd4ea2343..ba21e5aa95 100644 --- a/cli/diagnostics.rs +++ b/cli/diagnostics.rs @@ -2,9 +2,11 @@ use crate::colors; +use deno_core::serde::Deserialize; +use deno_core::serde::Deserializer; +use deno_core::serde::Serialize; +use deno_core::serde::Serializer; use regex::Regex; -use serde::Deserialize; -use serde::Deserializer; use std::error::Error; use std::fmt; @@ -157,6 +159,21 @@ impl<'de> Deserialize<'de> for DiagnosticCategory { } } +impl Serialize for DiagnosticCategory { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let value = match self { + DiagnosticCategory::Warning => 0 as i32, + DiagnosticCategory::Error => 1 as i32, + DiagnosticCategory::Suggestion => 2 as i32, + DiagnosticCategory::Message => 3 as i32, + }; + Serialize::serialize(&value, serializer) + } +} + impl From for DiagnosticCategory { fn from(value: i64) -> Self { match value { @@ -169,7 +186,7 @@ impl From for DiagnosticCategory { } } -#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DiagnosticMessageChain { message_text: String, @@ -196,14 +213,14 @@ impl DiagnosticMessageChain { } } -#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Position { pub line: u64, pub character: u64, } -#[derive(Debug, Deserialize, Clone, Eq, PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Diagnostic { pub category: DiagnosticCategory, @@ -367,6 +384,15 @@ impl<'de> Deserialize<'de> for Diagnostics { } } +impl Serialize for Diagnostics { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Serialize::serialize(&self.0, serializer) + } +} + impl fmt::Display for Diagnostics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut i = 0; diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 78a7e79c2e..6eb39c21c4 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -303,9 +303,6 @@ declare namespace Deno { /** Provide full support for iterables in `for..of`, spread and * destructuring when targeting ES5 or ES3. Defaults to `false`. */ downlevelIteration?: boolean; - /** Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. - * Defaults to `false`. */ - emitBOM?: boolean; /** Only emit `.d.ts` declaration files. Defaults to `false`. */ emitDeclarationOnly?: boolean; /** Emit design-type metadata for decorated declarations in source. See issue @@ -316,8 +313,11 @@ declare namespace Deno { * ecosystem compatibility and enable `allowSyntheticDefaultImports` for type * system compatibility. Defaults to `true`. */ esModuleInterop?: boolean; - /** Enables experimental support for ES decorators. Defaults to `false`. */ + /** Enables experimental support for ES decorators. Defaults to `true`. */ experimentalDecorators?: boolean; + /** Import emit helpers (e.g. `__extends`, `__rest`, etc..) from + * [tslib](https://www.npmjs.com/package/tslib). */ + importHelpers?: boolean; /** Emit a single file with source maps instead of having a separate file. * Defaults to `false`. */ inlineSourceMap?: boolean; @@ -325,7 +325,7 @@ declare namespace Deno { * `inlineSourceMap` or `sourceMap` to be set. Defaults to `false`. */ inlineSources?: boolean; /** Perform additional checks to ensure that transpile only would be safe. - * Defaults to `false`. */ + * Defaults to `true`. */ isolatedModules?: boolean; /** Support JSX in `.tsx` files: `"react"`, `"preserve"`, `"react-native"`. * Defaults to `"react"`. */ @@ -333,12 +333,12 @@ declare namespace Deno { /** Specify the JSX factory function to use when targeting react JSX emit, * e.g. `React.createElement` or `h`. Defaults to `React.createElement`. */ jsxFactory?: string; + /** Specify the JSX fragment factory function to use when targeting react + * JSX emit, e.g. `Fragment`. Defaults to `React.Fragment`. */ + jsxFragmentFactory?: string; /** Resolve keyof to string valued property names only (no numbers or * symbols). Defaults to `false`. */ keyofStringsOnly?: string; - /** Emit class fields with ECMAScript-standard semantics. Defaults to `false`. - */ - useDefineForClassFields?: boolean; /** List of library files to be included in the compilation. If omitted, * then the Deno main runtime libs are used. */ lib?: string[]; @@ -389,10 +389,6 @@ declare namespace Deno { noUnusedLocals?: boolean; /** Report errors on unused parameters. Defaults to `false`. */ noUnusedParameters?: boolean; - /** Redirect output structure to the directory. This only impacts - * `Deno.compile` and only changes the emitted file names. Defaults to - * `undefined`. */ - outDir?: string; /** List of path mapping entries for module names to locations relative to the * `baseUrl`. Defaults to `undefined`. */ paths?: Record; @@ -402,8 +398,6 @@ declare namespace Deno { /** Remove all comments except copy-right header comments beginning with * `/*!`. Defaults to `true`. */ removeComments?: boolean; - /** Include modules imported with `.json` extension. Defaults to `true`. */ - resolveJsonModule?: boolean; /** Specifies the root directory of input files. Only use to control the * output directory structure with `outDir`. Defaults to `undefined`. */ rootDir?: string; @@ -418,6 +412,8 @@ declare namespace Deno { * specified will be embedded in the sourceMap to direct the debugger where * the source files will be located. Defaults to `undefined`. */ sourceRoot?: string; + /** Skip type checking of all declaration files (`*.d.ts`). */ + skipLibCheck?: boolean; /** Enable all strict type checking options. Enabling `strict` enables * `noImplicitAny`, `noImplicitThis`, `alwaysStrict`, `strictBindCallApply`, * `strictNullChecks`, `strictFunctionTypes` and @@ -472,6 +468,9 @@ declare namespace Deno { * ``` */ types?: string[]; + /** Emit class fields with ECMAScript-standard semantics. Defaults to + * `false`. */ + useDefineForClassFields?: boolean; } /** **UNSTABLE**: new API, yet to be vetted. diff --git a/cli/main.rs b/cli/main.rs index 0d67286c41..dc68546d58 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -35,7 +35,6 @@ mod lint; mod lockfile; mod media_type; mod metrics; -mod module_graph; mod module_graph2; mod module_loader; mod op_fetch_asset; @@ -50,7 +49,6 @@ mod specifier_handler; mod test_runner; mod text_encoding; mod tokio_util; -mod tsc; mod tsc2; mod tsc_config; mod upgrade; @@ -242,6 +240,11 @@ async fn cache_command( flags: Flags, files: Vec, ) -> Result<(), AnyError> { + let lib = if flags.unstable { + module_graph2::TypeLib::UnstableDenoWindow + } else { + module_graph2::TypeLib::DenoWindow + }; let program_state = ProgramState::new(flags)?; for file in files { @@ -249,7 +252,7 @@ async fn cache_command( program_state .prepare_module_load( specifier, - tsc::TargetLib::Main, + lib.clone(), Permissions::allow_all(), false, program_state.maybe_import_map.clone(), @@ -343,21 +346,20 @@ async fn bundle_command( module_graph2::TypeLib::DenoWindow }; let graph = graph.clone(); - let (stats, diagnostics, maybe_ignored_options) = - graph.check(module_graph2::CheckOptions { - debug, - emit: false, - lib, - maybe_config_path: flags.config_path.clone(), - reload: flags.reload, - })?; + let result_info = graph.check(module_graph2::CheckOptions { + debug, + emit: false, + lib, + maybe_config_path: flags.config_path.clone(), + reload: flags.reload, + })?; - debug!("{}", stats); - if let Some(ignored_options) = maybe_ignored_options { + debug!("{}", result_info.stats); + if let Some(ignored_options) = result_info.maybe_ignored_options { eprintln!("{}", ignored_options); } - if !diagnostics.is_empty() { - return Err(generic_error(diagnostics.to_string())); + if !result_info.diagnostics.is_empty() { + return Err(generic_error(result_info.diagnostics.to_string())); } } diff --git a/cli/media_type.rs b/cli/media_type.rs index 7d63439f6a..cb26bcff5b 100644 --- a/cli/media_type.rs +++ b/cli/media_type.rs @@ -1,5 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use deno_core::ModuleSpecifier; use serde::Serialize; use serde::Serializer; use std::fmt; @@ -60,6 +61,22 @@ impl<'a> From<&'a String> for MediaType { } } +impl<'a> From<&'a ModuleSpecifier> for MediaType { + fn from(specifier: &'a ModuleSpecifier) -> Self { + let url = specifier.as_url(); + let path = if url.scheme() == "file" { + if let Ok(path) = url.to_file_path() { + path + } else { + PathBuf::from(url.path()) + } + } else { + PathBuf::from(url.path()) + }; + MediaType::from_path(&path) + } +} + impl Default for MediaType { fn default() -> Self { MediaType::Unknown diff --git a/cli/module_graph.rs b/cli/module_graph.rs deleted file mode 100644 index f0645424eb..0000000000 --- a/cli/module_graph.rs +++ /dev/null @@ -1,968 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::ast::Location; -use crate::checksum; -use crate::file_fetcher::SourceFile; -use crate::file_fetcher::SourceFileFetcher; -use crate::import_map::ImportMap; -use crate::media_type::MediaType; -use crate::permissions::Permissions; -use crate::tsc::pre_process_file; -use crate::tsc::ImportDesc; -use crate::tsc::TsReferenceDesc; -use crate::tsc::TsReferenceKind; -use crate::tsc::AVAILABLE_LIBS; -use crate::version; -use deno_core::error::custom_error; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::futures::stream::FuturesUnordered; -use deno_core::futures::stream::StreamExt; -use deno_core::futures::Future; -use deno_core::futures::FutureExt; -use deno_core::ModuleSpecifier; -use serde::Serialize; -use serde::Serializer; -use std::collections::HashMap; -use std::collections::HashSet; -use std::pin::Pin; - -// TODO(bartlomieju): it'd be great if this function returned -// more structured data and possibly format the same as TS diagnostics. -/// Decorate error with location of import that caused the error. -fn err_with_location( - e: AnyError, - maybe_location: Option<&Location>, -) -> AnyError { - if let Some(location) = maybe_location { - let location_str = format!( - "\nImported from \"{}:{}\"", - location.filename, location.line - ); - let err_str = e.to_string(); - generic_error(format!("{}{}", err_str, location_str)) - } else { - e - } -} - -/// Disallow http:// imports from modules loaded over https:// -fn validate_no_downgrade( - module_specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - maybe_location: Option<&Location>, -) -> Result<(), AnyError> { - if let Some(referrer) = maybe_referrer.as_ref() { - if let "https" = referrer.as_url().scheme() { - if let "http" = module_specifier.as_url().scheme() { - let e = custom_error("PermissionDenied", - "Modules loaded over https:// are not allowed to import modules over http://" - ); - return Err(err_with_location(e, maybe_location)); - }; - }; - }; - - Ok(()) -} - -/// Verify that remote file doesn't try to statically import local file. -fn validate_no_file_from_remote( - module_specifier: &ModuleSpecifier, - maybe_referrer: Option<&ModuleSpecifier>, - maybe_location: Option<&Location>, -) -> Result<(), AnyError> { - if let Some(referrer) = maybe_referrer.as_ref() { - let referrer_url = referrer.as_url(); - match referrer_url.scheme() { - "http" | "https" => { - let specifier_url = module_specifier.as_url(); - match specifier_url.scheme() { - "http" | "https" => {} - _ => { - let e = custom_error( - "PermissionDenied", - "Remote modules are not allowed to statically import local \ - modules. Use dynamic import instead.", - ); - return Err(err_with_location(e, maybe_location)); - } - } - } - _ => {} - } - } - - Ok(()) -} - -// TODO(bartlomieju): handle imports/references in ambient contexts/TS modules -// https://github.com/denoland/deno/issues/6133 -fn resolve_imports_and_references( - referrer: ModuleSpecifier, - maybe_import_map: Option<&ImportMap>, - import_descs: Vec, - ref_descs: Vec, -) -> Result<(Vec, Vec), AnyError> { - let mut imports = vec![]; - let mut references = vec![]; - - for import_desc in import_descs { - let maybe_resolved = if let Some(import_map) = maybe_import_map.as_ref() { - import_map.resolve(&import_desc.specifier, &referrer.to_string())? - } else { - None - }; - - let resolved_specifier = if let Some(resolved) = maybe_resolved { - resolved - } else { - ModuleSpecifier::resolve_import( - &import_desc.specifier, - &referrer.to_string(), - )? - }; - - let resolved_type_directive = - if let Some(types_specifier) = import_desc.deno_types.as_ref() { - Some(ModuleSpecifier::resolve_import( - &types_specifier, - &referrer.to_string(), - )?) - } else { - None - }; - - let import_descriptor = ImportDescriptor { - specifier: import_desc.specifier.to_string(), - resolved_specifier, - type_directive: import_desc.deno_types.clone(), - resolved_type_directive, - location: import_desc.location, - }; - - imports.push(import_descriptor); - } - - for ref_desc in ref_descs { - if AVAILABLE_LIBS.contains(&ref_desc.specifier.as_str()) { - continue; - } - - let resolved_specifier = ModuleSpecifier::resolve_import( - &ref_desc.specifier, - &referrer.to_string(), - )?; - - let reference_descriptor = ReferenceDescriptor { - specifier: ref_desc.specifier.to_string(), - resolved_specifier, - kind: ref_desc.kind, - location: ref_desc.location, - }; - - references.push(reference_descriptor); - } - - Ok((imports, references)) -} - -fn serialize_module_specifier( - spec: &ModuleSpecifier, - s: S, -) -> Result -where - S: Serializer, -{ - s.serialize_str(&spec.to_string()) -} - -fn serialize_option_module_specifier( - maybe_spec: &Option, - s: S, -) -> Result -where - S: Serializer, -{ - if let Some(spec) = maybe_spec { - serialize_module_specifier(spec, s) - } else { - s.serialize_none() - } -} - -const SUPPORTED_MEDIA_TYPES: [MediaType; 4] = [ - MediaType::JavaScript, - MediaType::TypeScript, - MediaType::JSX, - MediaType::TSX, -]; - -pub type ModuleGraph = HashMap; - -#[derive(Clone, Debug, Serialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ImportDescriptor { - pub specifier: String, - #[serde(serialize_with = "serialize_module_specifier")] - pub resolved_specifier: ModuleSpecifier, - // These two fields are for support of @deno-types directive - // directly prepending import statement - pub type_directive: Option, - #[serde(serialize_with = "serialize_option_module_specifier")] - pub resolved_type_directive: Option, - #[serde(skip)] - pub location: Location, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ReferenceDescriptor { - pub specifier: String, - #[serde(serialize_with = "serialize_module_specifier")] - pub resolved_specifier: ModuleSpecifier, - #[serde(skip)] - pub kind: TsReferenceKind, - #[serde(skip)] - pub location: Location, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ModuleGraphFile { - pub specifier: String, - pub url: String, - pub redirect: Option, - pub filename: String, - pub version_hash: String, - pub imports: Vec, - pub referenced_files: Vec, - pub lib_directives: Vec, - pub types_directives: Vec, - pub type_headers: Vec, - pub media_type: MediaType, - pub source_code: String, -} - -type SourceFileFuture = Pin< - Box>>, ->; - -pub struct ModuleGraphLoader { - permissions: Permissions, - file_fetcher: SourceFileFetcher, - maybe_import_map: Option, - pending_downloads: FuturesUnordered, - has_downloaded: HashSet, - graph: ModuleGraph, - is_dyn_import: bool, - analyze_dynamic_imports: bool, -} - -impl ModuleGraphLoader { - pub fn new( - file_fetcher: SourceFileFetcher, - maybe_import_map: Option, - permissions: Permissions, - is_dyn_import: bool, - analyze_dynamic_imports: bool, - ) -> Self { - Self { - file_fetcher, - permissions, - maybe_import_map, - pending_downloads: FuturesUnordered::new(), - has_downloaded: HashSet::new(), - graph: ModuleGraph::new(), - is_dyn_import, - analyze_dynamic_imports, - } - } - - /// This method is used to add specified module and all of its - /// dependencies to the graph. - /// - /// It resolves when all dependent modules have been fetched and analyzed. - /// - /// This method can be called multiple times. - pub async fn add_to_graph( - &mut self, - specifier: &ModuleSpecifier, - maybe_referrer: Option, - ) -> Result<(), AnyError> { - self.download_module(specifier.clone(), maybe_referrer, None)?; - - loop { - let (specifier, source_file) = - self.pending_downloads.next().await.unwrap()?; - self.visit_module(&specifier, source_file)?; - if self.pending_downloads.is_empty() { - break; - } - } - - Ok(()) - } - - /// This method is used to create a graph from in-memory files stored in - /// a hash map. Useful for creating module graph for code received from - /// the runtime. - pub fn build_local_graph( - &mut self, - _root_name: &str, - source_map: &HashMap, - ) -> Result<(), AnyError> { - for (spec, source_code) in source_map.iter() { - self.visit_memory_module(spec.to_string(), source_code.to_string())?; - } - - Ok(()) - } - - /// Consumes the loader and returns created graph. - pub fn get_graph(self) -> ModuleGraph { - self.graph - } - - fn visit_memory_module( - &mut self, - specifier: String, - source_code: String, - ) -> Result<(), AnyError> { - let mut referenced_files = vec![]; - let mut lib_directives = vec![]; - let mut types_directives = vec![]; - - // FIXME(bartlomieju): - // The resolveModules op only handles fully qualified URLs for referrer. - // However we will have cases where referrer is "/foo.ts". We add this dummy - // prefix "memory://" in order to use resolution logic. - let module_specifier = - if let Ok(spec) = ModuleSpecifier::resolve_url(&specifier) { - spec - } else { - ModuleSpecifier::resolve_url(&format!("memory://{}", specifier))? - }; - - let (raw_imports, raw_references) = pre_process_file( - &module_specifier.to_string(), - MediaType::from(&specifier), - &source_code, - self.analyze_dynamic_imports, - )?; - let (imports, references) = resolve_imports_and_references( - module_specifier.clone(), - self.maybe_import_map.as_ref(), - raw_imports, - raw_references, - )?; - - for ref_descriptor in references { - match ref_descriptor.kind { - TsReferenceKind::Lib => { - lib_directives.push(ref_descriptor); - } - TsReferenceKind::Types => { - types_directives.push(ref_descriptor); - } - TsReferenceKind::Path => { - referenced_files.push(ref_descriptor); - } - } - } - - self.graph.insert( - module_specifier.to_string(), - ModuleGraphFile { - specifier: specifier.to_string(), - url: specifier.to_string(), - redirect: None, - version_hash: "".to_string(), - media_type: MediaType::from(&specifier), - filename: specifier, - source_code, - imports, - referenced_files, - lib_directives, - types_directives, - type_headers: vec![], - }, - ); - Ok(()) - } - - // TODO(bartlomieju): decorate errors with import location in the source code - // https://github.com/denoland/deno/issues/5080 - fn download_module( - &mut self, - module_specifier: ModuleSpecifier, - maybe_referrer: Option, - maybe_location: Option, - ) -> Result<(), AnyError> { - if self.has_downloaded.contains(&module_specifier) { - return Ok(()); - } - - validate_no_downgrade( - &module_specifier, - maybe_referrer.as_ref(), - maybe_location.as_ref(), - )?; - - if !self.is_dyn_import { - validate_no_file_from_remote( - &module_specifier, - maybe_referrer.as_ref(), - maybe_location.as_ref(), - )?; - } - - self.has_downloaded.insert(module_specifier.clone()); - let spec = module_specifier; - let file_fetcher = self.file_fetcher.clone(); - let perms = self.permissions.clone(); - - let load_future = async move { - let spec_ = spec.clone(); - let source_file = file_fetcher - .fetch_source_file(&spec_, maybe_referrer, perms) - .await - .map_err(|e| err_with_location(e, maybe_location.as_ref()))?; - - Ok((spec_.clone(), source_file)) - } - .boxed_local(); - - self.pending_downloads.push(load_future); - Ok(()) - } - - fn visit_module( - &mut self, - module_specifier: &ModuleSpecifier, - source_file: SourceFile, - ) -> Result<(), AnyError> { - let mut imports = vec![]; - let mut referenced_files = vec![]; - let mut lib_directives = vec![]; - let mut types_directives = vec![]; - let mut type_headers = vec![]; - - // IMPORTANT: source_file.url might be different than requested - // module_specifier because of HTTP redirects. In such - // situation we add an "empty" ModuleGraphFile with 'redirect' - // field set that will be later used in TS worker when building - // map of available source file. It will perform substitution - // for proper URL point to redirect target. - if module_specifier.as_url() != &source_file.url { - // TODO(bartlomieju): refactor, this is a band-aid - self.graph.insert( - module_specifier.to_string(), - ModuleGraphFile { - specifier: module_specifier.to_string(), - url: module_specifier.to_string(), - redirect: Some(source_file.url.to_string()), - filename: source_file.filename.to_str().unwrap().to_string(), - version_hash: checksum::gen(&[ - &source_file.source_code.as_bytes(), - &version::DENO.as_bytes(), - ]), - media_type: source_file.media_type, - source_code: "".to_string(), - imports: vec![], - referenced_files: vec![], - lib_directives: vec![], - types_directives: vec![], - type_headers: vec![], - }, - ); - } - - let module_specifier = ModuleSpecifier::from(source_file.url.clone()); - let version_hash = checksum::gen(&[ - &source_file.source_code.as_bytes(), - &version::DENO.as_bytes(), - ]); - let source_code = source_file.source_code.clone(); - - if SUPPORTED_MEDIA_TYPES.contains(&source_file.media_type) { - if let Some(types_specifier) = source_file.types_header { - let type_header = ReferenceDescriptor { - specifier: types_specifier.to_string(), - resolved_specifier: ModuleSpecifier::resolve_import( - &types_specifier, - &module_specifier.to_string(), - )?, - kind: TsReferenceKind::Types, - // TODO(bartlomieju): location is not needed in here and constructing - // location by hand is bad - location: Location { - filename: module_specifier.to_string(), - line: 0, - col: 0, - }, - }; - self.download_module( - type_header.resolved_specifier.clone(), - Some(module_specifier.clone()), - None, - )?; - type_headers.push(type_header); - } - - let (raw_imports, raw_refs) = pre_process_file( - &module_specifier.to_string(), - source_file.media_type, - &source_code, - self.analyze_dynamic_imports, - )?; - let (imports_, references) = resolve_imports_and_references( - module_specifier.clone(), - self.maybe_import_map.as_ref(), - raw_imports, - raw_refs, - )?; - - for import_descriptor in imports_ { - self.download_module( - import_descriptor.resolved_specifier.clone(), - Some(module_specifier.clone()), - Some(import_descriptor.location.clone()), - )?; - - if let Some(type_dir_url) = - import_descriptor.resolved_type_directive.as_ref() - { - self.download_module( - type_dir_url.clone(), - Some(module_specifier.clone()), - Some(import_descriptor.location.clone()), - )?; - } - - imports.push(import_descriptor); - } - - for ref_descriptor in references { - self.download_module( - ref_descriptor.resolved_specifier.clone(), - Some(module_specifier.clone()), - Some(ref_descriptor.location.clone()), - )?; - - match ref_descriptor.kind { - TsReferenceKind::Lib => { - lib_directives.push(ref_descriptor); - } - TsReferenceKind::Types => { - types_directives.push(ref_descriptor); - } - TsReferenceKind::Path => { - referenced_files.push(ref_descriptor); - } - } - } - } - - self.graph.insert( - module_specifier.to_string(), - ModuleGraphFile { - specifier: module_specifier.to_string(), - url: module_specifier.to_string(), - redirect: None, - version_hash, - filename: source_file.filename.to_str().unwrap().to_string(), - media_type: source_file.media_type, - source_code, - imports, - referenced_files, - lib_directives, - types_directives, - type_headers, - }, - ); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::program_state::ProgramState; - use deno_core::serde_json; - use deno_core::serde_json::json; - - async fn build_graph( - module_specifier: &ModuleSpecifier, - ) -> Result { - let program_state = ProgramState::new(Default::default()).unwrap(); - let mut graph_loader = ModuleGraphLoader::new( - program_state.file_fetcher.clone(), - None, - Permissions::allow_all(), - false, - false, - ); - graph_loader.add_to_graph(&module_specifier, None).await?; - Ok(graph_loader.get_graph()) - } - - // TODO(bartlomieju): this test is flaky, because it's using 019_media_types - // file, reenable once Python server is replaced with Rust one. - #[ignore] - #[tokio::test] - async fn source_graph_fetch() { - let _http_server_guard = test_util::http_server(); - - let module_specifier = ModuleSpecifier::resolve_url_or_path( - "http://localhost:4545/cli/tests/019_media_types.ts", - ) - .unwrap(); - let graph = build_graph(&module_specifier) - .await - .expect("Failed to build graph"); - - let a = graph - .get("http://localhost:4545/cli/tests/019_media_types.ts") - .unwrap(); - - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js" - )); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/mt_video_vdn.t2.ts" - )); - assert!(graph.contains_key("http://localhost:4545/cli/tests/subdir/mt_application_x_typescript.t4.ts")); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/mt_video_mp2t.t3.ts" - )); - assert!(graph.contains_key("http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js")); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/mt_application_ecmascript.j2.js" - )); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/mt_text_javascript.j1.js" - )); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/mt_text_typescript.t1.ts" - )); - - assert_eq!( - serde_json::to_value(&a.imports).unwrap(), - json!([ - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_text_typescript.t1.ts", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_text_typescript.t1.ts", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_video_vdn.t2.ts", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_video_vdn.t2.ts", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_video_mp2t.t3.ts", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_video_mp2t.t3.ts", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_typescript.t4.ts", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_typescript.t4.ts", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_text_javascript.j1.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_text_javascript.j1.js", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_application_ecmascript.j2.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_application_ecmascript.j2.js", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_text_ecmascript.j3.js", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - { - "specifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/mt_application_x_javascript.j4.js", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - ]) - ); - } - - #[tokio::test] - async fn source_graph_type_references() { - let _http_server_guard = test_util::http_server(); - - let module_specifier = ModuleSpecifier::resolve_url_or_path( - "http://localhost:4545/cli/tests/type_definitions.ts", - ) - .unwrap(); - - let graph = build_graph(&module_specifier) - .await - .expect("Failed to build graph"); - - eprintln!("json {:#?}", serde_json::to_value(&graph).unwrap()); - - let a = graph - .get("http://localhost:4545/cli/tests/type_definitions.ts") - .unwrap(); - assert_eq!( - serde_json::to_value(&a.imports).unwrap(), - json!([ - { - "specifier": "./type_definitions/foo.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/type_definitions/foo.js", - "typeDirective": "./type_definitions/foo.d.ts", - "resolvedTypeDirective": "http://localhost:4545/cli/tests/type_definitions/foo.d.ts" - }, - { - "specifier": "./type_definitions/fizz.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/type_definitions/fizz.js", - "typeDirective": "./type_definitions/fizz.d.ts", - "resolvedTypeDirective": "http://localhost:4545/cli/tests/type_definitions/fizz.d.ts" - }, - { - "specifier": "./type_definitions/qat.ts", - "resolvedSpecifier": "http://localhost:4545/cli/tests/type_definitions/qat.ts", - "typeDirective": null, - "resolvedTypeDirective": null, - }, - ]) - ); - assert!(graph - .contains_key("http://localhost:4545/cli/tests/type_definitions/foo.js")); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/type_definitions/foo.d.ts" - )); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/type_definitions/fizz.js" - )); - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/type_definitions/fizz.d.ts" - )); - assert!(graph - .contains_key("http://localhost:4545/cli/tests/type_definitions/qat.ts")); - } - - #[tokio::test] - async fn source_graph_type_references2() { - let _http_server_guard = test_util::http_server(); - - let module_specifier = ModuleSpecifier::resolve_url_or_path( - "http://localhost:4545/cli/tests/type_directives_02.ts", - ) - .unwrap(); - - let graph = build_graph(&module_specifier) - .await - .expect("Failed to build graph"); - - eprintln!("{:#?}", serde_json::to_value(&graph).unwrap()); - - let a = graph - .get("http://localhost:4545/cli/tests/type_directives_02.ts") - .unwrap(); - assert_eq!( - serde_json::to_value(&a.imports).unwrap(), - json!([ - { - "specifier": "./subdir/type_reference.js", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/type_reference.js", - "typeDirective": null, - "resolvedTypeDirective": null, - } - ]) - ); - - assert!(graph.contains_key( - "http://localhost:4545/cli/tests/subdir/type_reference.d.ts" - )); - - let b = graph - .get("http://localhost:4545/cli/tests/subdir/type_reference.js") - .unwrap(); - assert_eq!( - serde_json::to_value(&b.types_directives).unwrap(), - json!([ - { - "specifier": "./type_reference.d.ts", - "resolvedSpecifier": "http://localhost:4545/cli/tests/subdir/type_reference.d.ts", - } - ]) - ); - } - - #[tokio::test] - async fn source_graph_type_references3() { - let _http_server_guard = test_util::http_server(); - - let module_specifier = ModuleSpecifier::resolve_url_or_path( - "http://localhost:4545/cli/tests/type_directives_01.ts", - ) - .unwrap(); - - let graph = build_graph(&module_specifier) - .await - .expect("Failed to build graph"); - - let ts = graph - .get("http://localhost:4545/cli/tests/type_directives_01.ts") - .unwrap(); - assert_eq!( - serde_json::to_value(&ts.imports).unwrap(), - json!([ - { - "specifier": "http://127.0.0.1:4545/xTypeScriptTypes.js", - "resolvedSpecifier": "http://127.0.0.1:4545/xTypeScriptTypes.js", - "typeDirective": null, - "resolvedTypeDirective": null, - } - ]) - ); - - let headers = graph - .get("http://127.0.0.1:4545/xTypeScriptTypes.js") - .unwrap(); - assert_eq!( - serde_json::to_value(&headers.type_headers).unwrap(), - json!([ - { - "specifier": "./xTypeScriptTypes.d.ts", - "resolvedSpecifier": "http://127.0.0.1:4545/xTypeScriptTypes.d.ts" - } - ]) - ); - } - - #[tokio::test] - async fn source_graph_different_langs() { - let _http_server_guard = test_util::http_server(); - - // ModuleGraphLoader was mistakenly parsing this file as TSX - // https://github.com/denoland/deno/issues/5867 - - let module_specifier = ModuleSpecifier::resolve_url_or_path( - "http://localhost:4545/cli/tests/ts_with_generic.ts", - ) - .unwrap(); - - build_graph(&module_specifier) - .await - .expect("Failed to build graph"); - } -} - -// TODO(bartlomieju): use baseline tests from TSC to ensure -// compatibility -#[test] -fn test_pre_process_file() { - let source = r#" -// This comment is placed to make sure that directives are parsed -// even when they start on non-first line - -/// -/// -/// -// @deno-types="./type_definitions/foo.d.ts" -import { foo } from "./type_definitions/foo.js"; -// @deno-types="./type_definitions/fizz.d.ts" -import "./type_definitions/fizz.js"; - -/// - -import * as qat from "./type_definitions/qat.ts"; - -console.log(foo); -console.log(fizz); -console.log(qat.qat); -"#; - - let (imports, references) = - pre_process_file("some/file.ts", MediaType::TypeScript, source, true) - .expect("Failed to parse"); - - assert_eq!( - imports, - vec![ - ImportDesc { - specifier: "./type_definitions/foo.js".to_string(), - deno_types: Some("./type_definitions/foo.d.ts".to_string()), - location: Location { - filename: "some/file.ts".to_string(), - line: 9, - col: 0, - }, - }, - ImportDesc { - specifier: "./type_definitions/fizz.js".to_string(), - deno_types: Some("./type_definitions/fizz.d.ts".to_string()), - location: Location { - filename: "some/file.ts".to_string(), - line: 11, - col: 0, - }, - }, - ImportDesc { - specifier: "./type_definitions/qat.ts".to_string(), - deno_types: None, - location: Location { - filename: "some/file.ts".to_string(), - line: 15, - col: 0, - }, - }, - ] - ); - - // According to TS docs (https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) - // directives that are not at the top of the file are ignored, so only - // 3 references should be captured instead of 4. - let file_specifier = - ModuleSpecifier::resolve_url_or_path("some/file.ts").unwrap(); - assert_eq!( - references, - vec![ - TsReferenceDesc { - specifier: "dom".to_string(), - kind: TsReferenceKind::Lib, - location: Location { - filename: file_specifier.to_string(), - line: 5, - col: 0, - }, - }, - TsReferenceDesc { - specifier: "./type_reference.d.ts".to_string(), - kind: TsReferenceKind::Types, - location: Location { - filename: file_specifier.to_string(), - line: 6, - col: 0, - }, - }, - TsReferenceDesc { - specifier: "./type_reference/dep.ts".to_string(), - kind: TsReferenceKind::Path, - location: Location { - filename: file_specifier.to_string(), - line: 7, - col: 0, - }, - }, - ] - ); -} diff --git a/cli/module_graph2.rs b/cli/module_graph2.rs index 3fc900373b..6ac27906d0 100644 --- a/cli/module_graph2.rs +++ b/cli/module_graph2.rs @@ -1,9 +1,9 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +use crate::ast; use crate::ast::parse; use crate::ast::transpile_module; use crate::ast::BundleHook; -use crate::ast::EmitOptions; use crate::ast::Location; use crate::ast::ParsedModule; use crate::colors; @@ -22,8 +22,7 @@ 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::tsc2; use crate::tsc_config::IgnoredCompilerOptions; use crate::tsc_config::TsConfig; use crate::version; @@ -35,6 +34,7 @@ use deno_core::futures::stream::StreamExt; use deno_core::serde::Serialize; use deno_core::serde::Serializer; use deno_core::serde_json::json; +use deno_core::serde_json::Value; use deno_core::ModuleResolutionError; use deno_core::ModuleSpecifier; use regex::Regex; @@ -121,13 +121,13 @@ impl Error for GraphError {} struct BundleLoader<'a> { cm: Rc, graph: &'a Graph2, - emit_options: &'a EmitOptions, + emit_options: &'a ast::EmitOptions, } impl<'a> BundleLoader<'a> { pub fn new( graph: &'a Graph2, - emit_options: &'a EmitOptions, + emit_options: &'a ast::EmitOptions, cm: Rc, ) -> Self { BundleLoader { @@ -480,7 +480,7 @@ impl Module { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Stats(pub Vec<(String, u128)>); impl<'de> Deserialize<'de> for Stats { @@ -504,6 +504,23 @@ impl fmt::Display for Stats { } } +/// A structure that provides information about a module graph result. +#[derive(Debug, Default)] +pub struct ResultInfo { + /// A structure which provides diagnostic information (usually from `tsc`) + /// about the code in the module graph. + pub diagnostics: Diagnostics, + /// Optionally ignored compiler options that represent any options that were + /// ignored if there was a user provided configuration. + pub maybe_ignored_options: Option, + /// A structure providing key metrics around the operation performed, in + /// milliseconds. + pub stats: Stats, +} + +/// Represents the "default" type library that should be used when type +/// checking the code in the module graph. Note that a user provided config +/// of `"lib"` would override this value. #[derive(Debug, Clone, Eq, PartialEq)] pub enum TypeLib { DenoWindow, @@ -539,7 +556,11 @@ impl Serialize for TypeLib { #[derive(Debug, Default)] pub struct BundleOptions { + /// If `true` then debug logging will be output from the isolate. pub debug: bool, + /// An optional string that points to a user supplied TypeScript configuration + /// file that augments the the default configuration passed to the TypeScript + /// compiler. pub maybe_config_path: Option, } @@ -560,6 +581,35 @@ pub struct CheckOptions { pub reload: bool, } +#[derive(Debug, Eq, PartialEq)] +pub enum BundleType { + /// Return the emitted contents of the program as a single "flattened" ES + /// module. + Esm, + // TODO(@kitsonk) once available in swc + // Iife, + /// Do not bundle the emit, instead returning each of the modules that are + /// part of the program as individual files. + None, +} + +impl Default for BundleType { + fn default() -> Self { + BundleType::None + } +} + +#[derive(Debug, Default)] +pub struct EmitOptions { + /// Indicate the form the result of the emit should take. + pub bundle_type: BundleType, + /// If `true` then debug logging will be output from the isolate. + pub debug: bool, + /// An optional map that contains user supplied TypeScript compiler + /// configuration options that are passed to the TypeScript compiler. + pub maybe_user_config: Option>, +} + /// A structure which provides options when transpiling modules. #[derive(Debug, Default)] pub struct TranspileOptions { @@ -647,47 +697,8 @@ impl Graph2 { })); let maybe_ignored_options = ts_config.merge_tsconfig(options.maybe_config_path)?; - let emit_options: EmitOptions = ts_config.into(); - let cm = Rc::new(swc_common::SourceMap::new( - swc_common::FilePathMapping::empty(), - )); - let loader = BundleLoader::new(self, &emit_options, cm.clone()); - let hook = Box::new(BundleHook); - let globals = swc_common::Globals::new(); - let bundler = swc_bundler::Bundler::new( - &globals, - cm.clone(), - loader, - self, - swc_bundler::Config::default(), - hook, - ); - let mut entries = HashMap::new(); - entries.insert( - "bundle".to_string(), - swc_common::FileName::Custom(root_specifier.to_string()), - ); - let output = bundler - .bundle(entries) - .context("Unable to output bundle during Graph2::bundle().")?; - let mut buf = Vec::new(); - { - let mut emitter = swc_ecmascript::codegen::Emitter { - cfg: swc_ecmascript::codegen::Config { minify: false }, - cm: cm.clone(), - comments: None, - wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new( - cm, "\n", &mut buf, None, - )), - }; - emitter - .emit_module(&output[0].module) - .context("Unable to emit bundle during Graph2::bundle().")?; - } - - let s = String::from_utf8(buf) - .context("Emitted bundle is an invalid utf-8 string.")?; + let s = self.emit_bundle(&root_specifier, &ts_config.into())?; let stats = Stats(vec![ ("Files".to_string(), self.modules.len() as u128), ("Total time".to_string(), start.elapsed().as_millis()), @@ -697,11 +708,7 @@ impl Graph2 { } /// Type check the module graph, corresponding to the options provided. - pub fn check( - self, - options: CheckOptions, - ) -> Result<(Stats, Diagnostics, Option), AnyError> - { + pub fn check(self, options: CheckOptions) -> Result { let mut config = TsConfig::new(json!({ "allowJs": true, // TODO(@kitsonk) is this really needed? @@ -745,11 +752,10 @@ impl Graph2 { && (!options.reload || self.roots_dynamic)) { debug!("graph does not need to be checked or emitted."); - return Ok(( - Stats(Vec::new()), - Diagnostics::default(), + return Ok(ResultInfo { maybe_ignored_options, - )); + ..Default::default() + }); } // TODO(@kitsonk) not totally happy with this here, but this is the first @@ -760,26 +766,15 @@ impl Graph2 { info!("{} {}", colors::green("Check"), specifier); } - let root_names: Vec<(ModuleSpecifier, MediaType)> = self - .roots - .iter() - .map(|ms| { - ( - // root modules can be redirects, so before we pass it to tsc we need - // to resolve the redirect - self.resolve_specifier(ms).clone(), - self.get_media_type(ms).unwrap(), - ) - }) - .collect(); + let root_names = self.get_root_names(); let maybe_tsbuildinfo = self.maybe_tsbuildinfo.clone(); let hash_data = vec![config.as_bytes(), version::DENO.as_bytes().to_owned()]; let graph = Rc::new(RefCell::new(self)); - let response = exec( + let response = tsc2::exec( js::compiler_isolate_init(), - Request { + tsc2::Request { config: config.clone(), debug: options.debug, graph: graph.clone(), @@ -837,7 +832,11 @@ impl Graph2 { } graph.flush()?; - Ok((response.stats, response.diagnostics, maybe_ignored_options)) + Ok(ResultInfo { + diagnostics: response.diagnostics, + maybe_ignored_options, + stats: response.stats, + }) } fn contains_module(&self, specifier: &ModuleSpecifier) -> bool { @@ -845,6 +844,165 @@ impl Graph2 { self.modules.contains_key(s) } + /// Emit the module graph in a specific format. This is specifically designed + /// to be an "all-in-one" API for access by the runtime, allowing both + /// emitting single modules as well as bundles, using Deno module resolution + /// or supplied sources. + pub fn emit( + self, + options: EmitOptions, + ) -> Result<(HashMap, ResultInfo), AnyError> { + let mut config = TsConfig::new(json!({ + "allowJs": true, + // TODO(@kitsonk) consider enabling this by default + // see: https://github.com/denoland/deno/issues/7732 + "emitDecoratorMetadata": false, + "esModuleInterop": true, + "experimentalDecorators": true, + "isolatedModules": true, + "jsx": "react", + "lib": TypeLib::DenoWindow, + "module": "esnext", + "strict": true, + "target": "esnext", + })); + let opts = match options.bundle_type { + BundleType::Esm => json!({ + "checkJs": false, + "inlineSourceMap": false, + "noEmit": true, + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + }), + BundleType::None => json!({ + "outDir": "deno://", + "removeComments": true, + "sourceMap": true, + }), + }; + config.merge(&opts); + let maybe_ignored_options = + if let Some(user_options) = &options.maybe_user_config { + config.merge_user_config(user_options)? + } else { + None + }; + + let root_names = self.get_root_names(); + let hash_data = + vec![config.as_bytes(), version::DENO.as_bytes().to_owned()]; + let graph = Rc::new(RefCell::new(self)); + + let response = tsc2::exec( + js::compiler_isolate_init(), + tsc2::Request { + config: config.clone(), + debug: options.debug, + graph: graph.clone(), + hash_data, + maybe_tsbuildinfo: None, + root_names, + }, + )?; + + let mut emitted_files = HashMap::new(); + match options.bundle_type { + BundleType::Esm => { + assert!( + response.emitted_files.is_empty(), + "No files should have been emitted from tsc." + ); + let graph = graph.borrow(); + assert_eq!( + graph.roots.len(), + 1, + "Only a single root module supported." + ); + let specifier = &graph.roots[0]; + let s = graph.emit_bundle(specifier, &config.into())?; + emitted_files.insert("deno:///bundle.js".to_string(), s); + } + BundleType::None => { + for emitted_file in &response.emitted_files { + assert!( + emitted_file.maybe_specifiers.is_some(), + "Orphaned file emitted." + ); + let specifiers = emitted_file.maybe_specifiers.clone().unwrap(); + assert_eq!( + specifiers.len(), + 1, + "An unexpected number of specifiers associated with emitted file." + ); + let specifier = specifiers[0].clone(); + let extension = match emitted_file.media_type { + MediaType::JavaScript => ".js", + MediaType::SourceMap => ".js.map", + _ => unreachable!(), + }; + let key = format!("{}{}", specifier, extension); + emitted_files.insert(key, emitted_file.data.clone()); + } + } + }; + + Ok(( + emitted_files, + ResultInfo { + diagnostics: response.diagnostics, + maybe_ignored_options, + stats: response.stats, + }, + )) + } + + /// Shared between `bundle()` and `emit()`. + fn emit_bundle( + &self, + specifier: &ModuleSpecifier, + emit_options: &ast::EmitOptions, + ) -> Result { + let cm = Rc::new(swc_common::SourceMap::new( + swc_common::FilePathMapping::empty(), + )); + let loader = BundleLoader::new(self, emit_options, cm.clone()); + let hook = Box::new(BundleHook); + let globals = swc_common::Globals::new(); + let bundler = swc_bundler::Bundler::new( + &globals, + cm.clone(), + loader, + self, + swc_bundler::Config::default(), + hook, + ); + let mut entries = HashMap::new(); + entries.insert( + "bundle".to_string(), + swc_common::FileName::Custom(specifier.to_string()), + ); + let output = bundler + .bundle(entries) + .context("Unable to output bundle during Graph2::bundle().")?; + let mut buf = Vec::new(); + { + let mut emitter = swc_ecmascript::codegen::Emitter { + cfg: swc_ecmascript::codegen::Config { minify: false }, + cm: cm.clone(), + comments: None, + wr: Box::new(swc_ecmascript::codegen::text_writer::JsWriter::new( + cm, "\n", &mut buf, None, + )), + }; + + emitter + .emit_module(&output[0].module) + .context("Unable to emit bundle during Graph2::bundle().")?; + } + + String::from_utf8(buf).context("Emitted bundle is an invalid utf-8 string.") + } + /// Update the handler with any modules that are marked as _dirty_ and update /// any build info if present. fn flush(&mut self) -> Result<(), AnyError> { @@ -963,22 +1121,6 @@ impl Graph2 { self.modules.get(s) } - /// Consume graph and return list of all module specifiers - /// contained in the graph. - pub fn get_modules(&self) -> Vec { - self.modules.keys().map(|s| s.to_owned()).collect() - } - - /// Get the source for a given module specifier. If the module is not part - /// of the graph, the result will be `None`. - pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option { - if let Some(module) = self.get_module(specifier) { - Some(module.source.clone()) - } else { - None - } - } - fn get_module_mut( &mut self, specifier: &ModuleSpecifier, @@ -993,6 +1135,41 @@ impl Graph2 { self.modules.get_mut(s) } + /// Consume graph and return list of all module specifiers contained in the + /// graph. + pub fn get_modules(&self) -> Vec { + self.modules.keys().map(|s| s.to_owned()).collect() + } + + /// Transform `self.roots` into something that works for `tsc`, because `tsc` + /// doesn't like root names without extensions that match its expectations, + /// nor does it have any concept of redirection, so we have to resolve all + /// that upfront before feeding it to `tsc`. + fn get_root_names(&self) -> Vec<(ModuleSpecifier, MediaType)> { + self + .roots + .iter() + .map(|ms| { + ( + // root modules can be redirects, so before we pass it to tsc we need + // to resolve the redirect + self.resolve_specifier(ms).clone(), + self.get_media_type(ms).unwrap(), + ) + }) + .collect() + } + + /// Get the source for a given module specifier. If the module is not part + /// of the graph, the result will be `None`. + pub fn get_source(&self, specifier: &ModuleSpecifier) -> Option { + if let Some(module) = self.get_module(specifier) { + Some(module.source.clone()) + } else { + None + } + } + /// Return a structure which provides information about the module graph and /// the relationship of the modules in the graph. This structure is used to /// provide information for the `info` subcommand. @@ -1209,7 +1386,7 @@ impl Graph2 { let maybe_ignored_options = ts_config.merge_tsconfig(options.maybe_config_path)?; - let emit_options: EmitOptions = ts_config.clone().into(); + let emit_options: ast::EmitOptions = ts_config.clone().into(); let mut emit_count: u128 = 0; let config = ts_config.as_bytes(); @@ -1434,12 +1611,25 @@ impl GraphBuilder2 { pub mod tests { use super::*; + use crate::specifier_handler::MemoryHandler; use deno_core::futures::future; use std::env; use std::fs; use std::path::PathBuf; use std::sync::Mutex; + macro_rules! map ( + { $($key:expr => $value:expr),+ } => { + { + let mut m = ::std::collections::HashMap::new(); + $( + m.insert($key, $value); + )+ + m + } + }; + ); + /// This is a testing mock for `SpecifierHandler` that uses a special file /// system renaming to mock local and remote modules as well as provides /// "spies" for the critical methods for testing purposes. @@ -1465,20 +1655,7 @@ pub mod tests { .replace("://", "_") .replace("/", "-"); let source_path = self.fixtures.join(specifier_text); - let media_type = match source_path.extension().unwrap().to_str().unwrap() - { - "ts" => { - if source_path.to_string_lossy().ends_with(".d.ts") { - MediaType::Dts - } else { - MediaType::TypeScript - } - } - "tsx" => MediaType::TSX, - "js" => MediaType::JavaScript, - "jsx" => MediaType::JSX, - _ => MediaType::Unknown, - }; + let media_type = MediaType::from(&source_path); let source = fs::read_to_string(&source_path)?; let is_remote = specifier.as_url().scheme() != "file"; @@ -1572,6 +1749,24 @@ pub mod tests { (builder.get_graph(), handler) } + async fn setup_memory( + specifier: ModuleSpecifier, + sources: HashMap<&str, &str>, + ) -> Graph2 { + let sources: HashMap = sources + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let handler = Rc::new(RefCell::new(MemoryHandler::new(sources))); + let mut builder = GraphBuilder2::new(handler.clone(), None, None); + builder + .add(&specifier, false) + .await + .expect("module not inserted"); + + builder.get_graph() + } + #[test] fn test_get_version() { let doc_a = "console.log(42);"; @@ -1694,7 +1889,7 @@ pub mod tests { 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 + let result_info = graph .check(CheckOptions { debug: false, emit: true, @@ -1703,9 +1898,9 @@ pub mod tests { reload: false, }) .expect("should have checked"); - assert!(maybe_ignored_options.is_none()); - assert_eq!(stats.0.len(), 12); - assert!(diagnostics.is_empty()); + assert!(result_info.maybe_ignored_options.is_none()); + assert_eq!(result_info.stats.0.len(), 12); + assert!(result_info.diagnostics.is_empty()); let h = handler.borrow(); assert_eq!(h.cache_calls.len(), 2); assert_eq!(h.tsbuildinfo_calls.len(), 1); @@ -1717,7 +1912,7 @@ pub mod tests { 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 + let result_info = graph .check(CheckOptions { debug: false, emit: false, @@ -1726,9 +1921,9 @@ pub mod tests { reload: false, }) .expect("should have checked"); - assert!(maybe_ignored_options.is_none()); - assert_eq!(stats.0.len(), 12); - assert!(diagnostics.is_empty()); + assert!(result_info.maybe_ignored_options.is_none()); + assert_eq!(result_info.stats.0.len(), 12); + assert!(result_info.diagnostics.is_empty()); let h = handler.borrow(); assert_eq!(h.cache_calls.len(), 0); assert_eq!(h.tsbuildinfo_calls.len(), 1); @@ -1740,7 +1935,7 @@ pub mod tests { ModuleSpecifier::resolve_url_or_path("file:///tests/checkwithconfig.ts") .expect("could not resolve module"); let (graph, handler) = setup(specifier.clone()).await; - let (_, diagnostics, maybe_ignored_options) = graph + let result_info = graph .check(CheckOptions { debug: false, emit: true, @@ -1751,8 +1946,8 @@ pub mod tests { reload: true, }) .expect("should have checked"); - assert!(maybe_ignored_options.is_none()); - assert!(diagnostics.is_empty()); + assert!(result_info.maybe_ignored_options.is_none()); + assert!(result_info.diagnostics.is_empty()); let h = handler.borrow(); assert_eq!(h.version_calls.len(), 2); let ver0 = h.version_calls[0].1.clone(); @@ -1760,7 +1955,7 @@ pub mod tests { // let's do it all over again to ensure that the versions are determinstic let (graph, handler) = setup(specifier).await; - let (_, diagnostics, maybe_ignored_options) = graph + let result_info = graph .check(CheckOptions { debug: false, emit: true, @@ -1771,14 +1966,89 @@ pub mod tests { reload: true, }) .expect("should have checked"); - assert!(maybe_ignored_options.is_none()); - assert!(diagnostics.is_empty()); + assert!(result_info.maybe_ignored_options.is_none()); + assert!(result_info.diagnostics.is_empty()); let h = handler.borrow(); assert_eq!(h.version_calls.len(), 2); assert!(h.version_calls[0].1 == ver0 || h.version_calls[0].1 == ver1); assert!(h.version_calls[1].1 == ver0 || h.version_calls[1].1 == ver1); } + #[tokio::test] + async fn test_graph_emit() { + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///a.ts").unwrap(); + let graph = setup_memory( + specifier, + map!( + "/a.ts" => r#" + import * as b from "./b.ts"; + + console.log(b); + "#, + "/b.ts" => r#" + export const b = "b"; + "# + ), + ) + .await; + let (emitted_files, result_info) = graph + .emit(EmitOptions { + bundle_type: BundleType::None, + debug: false, + maybe_user_config: None, + }) + .expect("should have emitted"); + assert!(result_info.diagnostics.is_empty()); + assert!(result_info.maybe_ignored_options.is_none()); + assert_eq!(emitted_files.len(), 4); + let out_a = emitted_files.get("file:///a.ts.js"); + assert!(out_a.is_some()); + let out_a = out_a.unwrap(); + assert!(out_a.starts_with("import * as b from")); + assert!(emitted_files.contains_key("file:///a.ts.js.map")); + let out_b = emitted_files.get("file:///b.ts.js"); + assert!(out_b.is_some()); + let out_b = out_b.unwrap(); + assert!(out_b.starts_with("export const b = \"b\";")); + assert!(emitted_files.contains_key("file:///b.ts.js.map")); + } + + #[tokio::test] + async fn test_graph_emit_bundle() { + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///a.ts").unwrap(); + let graph = setup_memory( + specifier, + map!( + "/a.ts" => r#" + import * as b from "./b.ts"; + + console.log(b); + "#, + "/b.ts" => r#" + export const b = "b"; + "# + ), + ) + .await; + let (emitted_files, result_info) = graph + .emit(EmitOptions { + bundle_type: BundleType::Esm, + debug: false, + maybe_user_config: None, + }) + .expect("should have emitted"); + assert!(result_info.diagnostics.is_empty()); + assert!(result_info.maybe_ignored_options.is_none()); + assert_eq!(emitted_files.len(), 1); + let actual = emitted_files.get("deno:///bundle.js"); + assert!(actual.is_some()); + let actual = actual.unwrap(); + assert!(actual.contains("const b = \"b\";")); + assert!(actual.contains("console.log(b);")); + } + #[tokio::test] async fn test_graph_info() { let specifier = diff --git a/cli/module_loader.rs b/cli/module_loader.rs index 39690465cf..b19476fe2d 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -1,9 +1,9 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::import_map::ImportMap; +use crate::module_graph2::TypeLib; use crate::permissions::Permissions; use crate::program_state::ProgramState; -use crate::tsc::TargetLib; use deno_core::error::AnyError; use deno_core::futures::future::FutureExt; use deno_core::futures::Future; @@ -21,7 +21,7 @@ pub struct CliModuleLoader { /// When flags contains a `.import_map_path` option, the content of the /// import map file will be resolved and set. pub import_map: Option, - pub target_lib: TargetLib, + pub lib: TypeLib, pub is_main: bool, } @@ -29,7 +29,7 @@ impl CliModuleLoader { pub fn new(maybe_import_map: Option) -> Rc { Rc::new(CliModuleLoader { import_map: maybe_import_map, - target_lib: TargetLib::Main, + lib: TypeLib::DenoWindow, is_main: true, }) } @@ -37,7 +37,7 @@ impl CliModuleLoader { pub fn new_for_worker() -> Rc { Rc::new(CliModuleLoader { import_map: None, - target_lib: TargetLib::Worker, + lib: TypeLib::DenoWorker, is_main: false, }) } @@ -117,13 +117,21 @@ impl ModuleLoader for CliModuleLoader { is_dynamic: bool, ) -> Pin>>> { let specifier = specifier.clone(); - let target_lib = self.target_lib.clone(); let maybe_import_map = self.import_map.clone(); let state = op_state.borrow(); // The permissions that should be applied to any dynamically imported module let dynamic_permissions = state.borrow::().clone(); let program_state = state.borrow::>().clone(); + let lib = if program_state.flags.unstable { + if self.lib == TypeLib::DenoWindow { + TypeLib::UnstableDenoWindow + } else { + TypeLib::UnstableDenoWorker + } + } else { + self.lib.clone() + }; drop(state); // TODO(bartlomieju): `prepare_module_load` should take `load_id` param @@ -131,7 +139,7 @@ impl ModuleLoader for CliModuleLoader { program_state .prepare_module_load( specifier, - target_lib, + lib, dynamic_permissions, is_dynamic, maybe_import_map, diff --git a/cli/op_fetch_asset.rs b/cli/op_fetch_asset.rs index 3ff8b782ff..dcb54cde5b 100644 --- a/cli/op_fetch_asset.rs +++ b/cli/op_fetch_asset.rs @@ -17,8 +17,6 @@ pub fn get_asset(name: &str) -> Option<&'static str> { }; } match name { - "system_loader.js" => Some(include_str!("system_loader.js")), - "system_loader_es5.js" => Some(include_str!("system_loader_es5.js")), "bootstrap.ts" => Some("console.log(\"hello deno\");"), "typescript.d.ts" => inc!("typescript.d.ts"), "lib.dom.d.ts" => inc!("lib.dom.d.ts"), @@ -85,6 +83,11 @@ pub fn get_asset(name: &str) -> Option<&'static str> { /// Warning: Returns a non-JSON op dispatcher. Must be manually attached to /// JsRuntime. +/// +/// TODO(@kitsonk) this is only used when building the snapshot, and needs to +/// be refactored somewhere else. It is no longer used by `main.rs` and +/// therefore requires the allow unused. +#[allow(unused)] pub fn op_fetch_asset( custom_assets: HashMap, ) -> impl Fn(Rc>, BufVec) -> Op { diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index 5ceb903167..02d0933754 100644 --- a/cli/ops/runtime_compiler.rs +++ b/cli/ops/runtime_compiler.rs @@ -3,17 +3,23 @@ use crate::ast; use crate::colors; use crate::media_type::MediaType; +use crate::module_graph2::BundleType; +use crate::module_graph2::EmitOptions; +use crate::module_graph2::GraphBuilder2; use crate::permissions::Permissions; -use crate::tsc::runtime_bundle; -use crate::tsc::runtime_compile; +use crate::specifier_handler::FetchHandler; +use crate::specifier_handler::MemoryHandler; +use crate::specifier_handler::SpecifierHandler; use crate::tsc_config; + use deno_core::error::AnyError; -use deno_core::futures::FutureExt; +use deno_core::error::Context; use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::BufVec; +use deno_core::ModuleSpecifier; use deno_core::OpState; use serde::Deserialize; use std::cell::RefCell; @@ -39,35 +45,53 @@ async fn op_compile( args: Value, _data: BufVec, ) -> Result { - super::check_unstable2(&state, "Deno.compile"); let args: CompileArgs = serde_json::from_value(args)?; - let cli_state = super::global_state2(&state); - let program_state = cli_state.clone(); - let permissions = { + if args.bundle { + super::check_unstable2(&state, "Deno.bundle"); + } else { + super::check_unstable2(&state, "Deno.compile"); + } + let program_state = super::global_state2(&state); + let runtime_permissions = { let state = state.borrow(); state.borrow::().clone() }; - let fut = if args.bundle { - runtime_bundle( - &program_state, - permissions, - &args.root_name, - &args.sources, - &args.options, - ) - .boxed_local() + let handler: Rc> = + if let Some(sources) = args.sources { + Rc::new(RefCell::new(MemoryHandler::new(sources))) + } else { + Rc::new(RefCell::new(FetchHandler::new( + &program_state, + runtime_permissions, + )?)) + }; + let mut builder = GraphBuilder2::new(handler, None, None); + let specifier = ModuleSpecifier::resolve_url_or_path(&args.root_name) + .context("The root specifier is invalid.")?; + builder.add(&specifier, false).await?; + let graph = builder.get_graph(); + let bundle_type = if args.bundle { + BundleType::Esm } else { - runtime_compile( - &program_state, - permissions, - &args.root_name, - &args.sources, - &args.options, - ) - .boxed_local() + BundleType::None }; - let result = fut.await?; - Ok(result) + let debug = program_state.flags.log_level == Some(log::Level::Debug); + let maybe_user_config: Option> = + if let Some(options) = args.options { + Some(serde_json::from_str(&options)?) + } else { + None + }; + let (emitted_files, result_info) = graph.emit(EmitOptions { + bundle_type, + debug, + maybe_user_config, + })?; + + Ok(json!({ + "emittedFiles": emitted_files, + "diagnostics": result_info.diagnostics, + })) } #[derive(Deserialize, Debug)] diff --git a/cli/program_state.rs b/cli/program_state.rs index 9eeb72f235..027bbc7926 100644 --- a/cli/program_state.rs +++ b/cli/program_state.rs @@ -15,9 +15,6 @@ 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; @@ -37,6 +34,12 @@ pub fn exit_unstable(api_name: &str) { std::process::exit(70); } +// TODO(@kitsonk) probably can refactor this better with the graph. +pub struct CompiledModule { + pub code: String, + pub name: String, +} + /// This structure represents state of single "deno" program. /// /// It is shared by all created workers (thus V8 isolates). @@ -47,7 +50,6 @@ pub struct ProgramState { pub permissions: Permissions, pub dir: deno_dir::DenoDir, pub file_fetcher: SourceFileFetcher, - pub ts_compiler: TsCompiler, pub lockfile: Option>>, pub maybe_import_map: Option, pub maybe_inspector_server: Option>, @@ -70,12 +72,6 @@ impl ProgramState { ca_file.as_deref(), )?; - let ts_compiler = TsCompiler::new( - file_fetcher.clone(), - flags.clone(), - dir.gen_cache.clone(), - )?; - let lockfile = if let Some(filename) = &flags.lock { let lockfile = Lockfile::new(filename.clone(), flags.lock_write)?; Some(Arc::new(Mutex::new(lockfile))) @@ -105,7 +101,6 @@ impl ProgramState { permissions: Permissions::from_flags(&flags), flags, file_fetcher, - ts_compiler, lockfile, maybe_import_map, maybe_inspector_server, @@ -120,7 +115,7 @@ impl ProgramState { pub async fn prepare_module_load( self: &Arc, specifier: ModuleSpecifier, - target_lib: TargetLib, + lib: TypeLib, runtime_permissions: Permissions, is_dynamic: bool, maybe_import_map: Option, @@ -129,7 +124,7 @@ impl ProgramState { // Workers are subject to the current runtime permissions. We do the // permission check here early to avoid "wasting" time building a module // graph for a module that cannot be loaded. - if target_lib == TargetLib::Worker { + if lib == TypeLib::DenoWorker || lib == TypeLib::UnstableDenoWorker { runtime_permissions.check_specifier(&specifier)?; } let handler = @@ -153,37 +148,20 @@ impl ProgramState { eprintln!("{}", ignored_options); } } else { - 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, - })?; + let result_info = graph.check(CheckOptions { + debug, + emit: true, + lib, + maybe_config_path, + reload: self.flags.reload, + })?; - debug!("{}", stats); - if let Some(ignored_options) = maybe_ignored_options { + debug!("{}", result_info.stats); + if let Some(ignored_options) = result_info.maybe_ignored_options { eprintln!("{}", ignored_options); } - if !diagnostics.is_empty() { - return Err(generic_error(diagnostics.to_string())); + if !result_info.diagnostics.is_empty() { + return Err(generic_error(result_info.diagnostics.to_string())); } }; diff --git a/cli/rt/40_compiler_api.js b/cli/rt/40_compiler_api.js index ffe58559e2..db12c857db 100644 --- a/cli/rt/40_compiler_api.js +++ b/cli/rt/40_compiler_api.js @@ -52,19 +52,14 @@ sources: !!sources, options, }); + /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ const result = await opCompile(payload); - util.assert(result.emitMap); + util.assert(result.emittedFiles); const maybeDiagnostics = result.diagnostics.length === 0 ? undefined : result.diagnostics; - const emitMap = {}; - - for (const [key, emittedSource] of Object.entries(result.emitMap)) { - emitMap[key] = emittedSource.contents; - } - - return [maybeDiagnostics, emitMap]; + return [maybeDiagnostics, result.emittedFiles]; } // TODO(bartlomieju): change return type to interface? @@ -84,12 +79,14 @@ sources: !!sources, options, }); + /** @type {{ emittedFiles: Record, diagnostics: any[] }} */ const result = await opCompile(payload); - util.assert(result.output); + let output = result.emittedFiles["deno:///bundle.js"]; + util.assert(output); const maybeDiagnostics = result.diagnostics.length === 0 ? undefined : result.diagnostics; - return [maybeDiagnostics, result.output]; + return [maybeDiagnostics, output]; } window.__bootstrap.compilerApi = { diff --git a/cli/specifier_handler.rs b/cli/specifier_handler.rs index 988cad72be..aeb268544d 100644 --- a/cli/specifier_handler.rs +++ b/cli/specifier_handler.rs @@ -8,7 +8,9 @@ use crate::media_type::MediaType; use crate::permissions::Permissions; use crate::program_state::ProgramState; +use deno_core::error::custom_error; use deno_core::error::AnyError; +use deno_core::futures::future; use deno_core::futures::Future; use deno_core::futures::FutureExt; use deno_core::serde_json; @@ -61,7 +63,6 @@ pub struct CachedModule { pub specifier: ModuleSpecifier, } -#[cfg(test)] impl Default for CachedModule { fn default() -> Self { let specifier = ModuleSpecifier::resolve_url("file:///example.js").unwrap(); @@ -422,12 +423,119 @@ impl SpecifierHandler for FetchHandler { } } +pub struct MemoryHandler { + sources: HashMap, +} + +impl MemoryHandler { + pub fn new(sources: HashMap) -> Self { + Self { sources } + } +} + +impl SpecifierHandler for MemoryHandler { + fn fetch( + &mut self, + specifier: ModuleSpecifier, + _maybe_referrer: Option, + _is_dynamic: bool, + ) -> FetchFuture { + let mut specifier_text = specifier.to_string(); + if !self.sources.contains_key(&specifier_text) { + specifier_text = specifier_text.replace("file:///", "/"); + if !self.sources.contains_key(&specifier_text) { + // Convert `C:/a/path/file.ts` to `/a/path/file.ts` + specifier_text = specifier_text[3..].to_string() + } + } + let result = if let Some(source) = self.sources.get(&specifier_text) { + let media_type = MediaType::from(&specifier); + let is_remote = specifier.as_url().scheme() != "file"; + + Ok(CachedModule { + source: source.to_string(), + requested_specifier: specifier.clone(), + specifier, + media_type, + is_remote, + ..Default::default() + }) + } else { + Err(custom_error( + "NotFound", + format!("Unable to find specifier in sources: {}", specifier), + )) + }; + + Box::pin(future::ready(result)) + } + + fn get_tsbuildinfo( + &self, + _specifier: &ModuleSpecifier, + ) -> Result, AnyError> { + Ok(None) + } + + fn set_cache( + &mut self, + _specifier: &ModuleSpecifier, + _emit: &Emit, + ) -> Result<(), AnyError> { + Ok(()) + } + + fn set_types( + &mut self, + _specifier: &ModuleSpecifier, + _types: String, + ) -> Result<(), AnyError> { + Ok(()) + } + + fn set_tsbuildinfo( + &mut self, + _specifier: &ModuleSpecifier, + _tsbuildinfo: String, + ) -> Result<(), AnyError> { + Ok(()) + } + + fn set_deps( + &mut self, + _specifier: &ModuleSpecifier, + _dependencies: DependencyMap, + ) -> Result<(), AnyError> { + Ok(()) + } + + fn set_version( + &mut self, + _specifier: &ModuleSpecifier, + _version: String, + ) -> Result<(), AnyError> { + Ok(()) + } +} + #[cfg(test)] pub mod tests { use super::*; use crate::http_cache::HttpCache; use tempfile::TempDir; + macro_rules! map ( + { $($key:expr => $value:expr),+ } => { + { + let mut m = ::std::collections::HashMap::new(); + $( + m.insert($key, $value); + )+ + m + } + }; + ); + fn setup() -> (TempDir, FetchHandler) { let temp_dir = TempDir::new().expect("could not setup"); let deno_dir = DenoDir::new(Some(temp_dir.path().to_path_buf())) @@ -522,4 +630,111 @@ pub mod tests { file_fetcher.fetch(specifier, None, false).await.unwrap(); assert_eq!(cached_module.is_remote, false); } + + #[tokio::test] + async fn test_memory_handler_fetch() { + let a_src = r#" + import * as b from "./b.ts"; + console.log(b); + "#; + let b_src = r#" + export const b = "b"; + "#; + let c_src = r#" + export const c = "c"; + "#; + let d_src = r#" + export const d: string; + "#; + let sources = map!( + "/a.ts" => a_src, + "/b.ts" => b_src, + "https://deno.land/x/c.js" => c_src, + "https://deno.land/x/d.d.ts" => d_src + ); + let sources: HashMap = sources + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + let mut handler = MemoryHandler::new(sources); + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///a.ts").unwrap(); + let actual: CachedModule = handler + .fetch(specifier.clone(), None, false) + .await + .expect("could not fetch module"); + assert_eq!(actual.source, a_src.to_string()); + assert_eq!(actual.requested_specifier, specifier); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::TypeScript); + assert_eq!(actual.is_remote, false); + + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///b.ts").unwrap(); + let actual: CachedModule = handler + .fetch(specifier.clone(), None, false) + .await + .expect("could not fetch module"); + assert_eq!(actual.source, b_src.to_string()); + assert_eq!(actual.requested_specifier, specifier); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::TypeScript); + assert_eq!(actual.is_remote, false); + + let specifier = + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/c.js").unwrap(); + let actual: CachedModule = handler + .fetch(specifier.clone(), None, false) + .await + .expect("could not fetch module"); + assert_eq!(actual.source, c_src.to_string()); + assert_eq!(actual.requested_specifier, specifier); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::JavaScript); + assert_eq!(actual.is_remote, true); + + let specifier = + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/d.d.ts") + .unwrap(); + let actual: CachedModule = handler + .fetch(specifier.clone(), None, false) + .await + .expect("could not fetch module"); + assert_eq!(actual.source, d_src.to_string()); + assert_eq!(actual.requested_specifier, specifier); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::Dts); + assert_eq!(actual.is_remote, true); + + let specifier = + ModuleSpecifier::resolve_url_or_path("https://deno.land/x/missing.ts") + .unwrap(); + handler + .fetch(specifier.clone(), None, false) + .await + .expect_err("should have errored"); + + let specifier = ModuleSpecifier::resolve_url_or_path("/a.ts").unwrap(); + let actual: CachedModule = handler + .fetch(specifier.clone(), None, false) + .await + .expect("could not fetch module"); + assert_eq!(actual.source, a_src.to_string()); + assert_eq!(actual.requested_specifier, specifier); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::TypeScript); + assert_eq!(actual.is_remote, false); + + let specifier = + ModuleSpecifier::resolve_url_or_path("file:///C:/a.ts").unwrap(); + let actual: CachedModule = handler + .fetch(specifier.clone(), None, false) + .await + .expect("could not fetch module"); + assert_eq!(actual.source, a_src.to_string()); + assert_eq!(actual.requested_specifier, specifier); + assert_eq!(actual.specifier, specifier); + assert_eq!(actual.media_type, MediaType::TypeScript); + assert_eq!(actual.is_remote, false); + } } diff --git a/cli/tests/compiler_api_test.ts b/cli/tests/compiler_api_test.ts index a845e58c68..a236a3f3fa 100644 --- a/cli/tests/compiler_api_test.ts +++ b/cli/tests/compiler_api_test.ts @@ -14,12 +14,11 @@ Deno.test({ }); assert(diagnostics == null); assert(actual); - assertEquals(Object.keys(actual), [ - "/bar.js.map", - "/bar.js", - "/foo.js.map", - "/foo.js", - ]); + const keys = Object.keys(actual).sort(); + assert(keys[0].endsWith("/bar.ts.js")); + assert(keys[1].endsWith("/bar.ts.js.map")); + assert(keys[2].endsWith("/foo.ts.js")); + assert(keys[3].endsWith("/foo.ts.js.map")); }, }); @@ -29,10 +28,10 @@ Deno.test({ const [diagnostics, actual] = await Deno.compile("./subdir/mod1.ts"); assert(diagnostics == null); assert(actual); - const keys = Object.keys(actual); + const keys = Object.keys(actual).sort(); assertEquals(keys.length, 6); - assert(keys[0].endsWith("print_hello.js.map")); - assert(keys[1].endsWith("print_hello.js")); + assert(keys[0].endsWith("cli/tests/subdir/mod1.ts.js")); + assert(keys[1].endsWith("cli/tests/subdir/mod1.ts.js.map")); }, }); @@ -51,8 +50,11 @@ Deno.test({ ); assert(diagnostics == null); assert(actual); - assertEquals(Object.keys(actual), ["/foo.js"]); - assert(actual["/foo.js"].startsWith("define(")); + const keys = Object.keys(actual); + assertEquals(keys.length, 1); + const key = keys[0]; + assert(key.endsWith("/foo.ts.js")); + assert(actual[key].startsWith("define(")); }, }); @@ -60,9 +62,9 @@ Deno.test({ name: "Deno.compile() - pass lib in compiler options", async fn() { const [diagnostics, actual] = await Deno.compile( - "/foo.ts", + "file:///foo.ts", { - "/foo.ts": `console.log(document.getElementById("foo")); + "file:///foo.ts": `console.log(document.getElementById("foo")); console.log(Deno.args);`, }, { @@ -71,45 +73,37 @@ Deno.test({ ); assert(diagnostics == null); assert(actual); - assertEquals(Object.keys(actual), ["/foo.js.map", "/foo.js"]); + assertEquals( + Object.keys(actual).sort(), + ["file:///foo.ts.js", "file:///foo.ts.js.map"], + ); }, }); -Deno.test({ - name: "Deno.compile() - pass outDir in compiler options", - async fn() { - const [diagnostics, actual] = await Deno.compile( - "src/foo.ts", - { - "src/foo.ts": "console.log('Hello world')", - }, - { - outDir: "./lib", - }, - ); - assert(diagnostics == null); - assert(actual); - assertEquals(Object.keys(actual), ["lib/foo.js.map", "lib/foo.js"]); - }, -}); - -Deno.test({ - name: "Deno.compile() - properly handles .d.ts files", - async fn() { - const [diagnostics, actual] = await Deno.compile( - "/foo.ts", - { - "/foo.ts": `console.log(Foo.bar);`, - }, - { - types: ["./subdir/foo_types.d.ts"], - }, - ); - assert(diagnostics == null); - assert(actual); - assertEquals(Object.keys(actual), ["/foo.js.map", "/foo.js"]); - }, -}); +// TODO(@kitsonk) figure the "right way" to restore support for types +// Deno.test({ +// name: "Deno.compile() - properly handles .d.ts files", +// async fn() { +// const [diagnostics, actual] = await Deno.compile( +// "/foo.ts", +// { +// "/foo.ts": `console.log(Foo.bar);`, +// "/foo_types.d.ts": `declare namespace Foo { +// const bar: string; +// }`, +// }, +// { +// types: ["/foo_types.d.ts"], +// }, +// ); +// assert(diagnostics == null); +// assert(actual); +// assertEquals( +// Object.keys(actual).sort(), +// ["file:///foo.ts.js", "file:///file.ts.js.map"], +// ); +// }, +// }); Deno.test({ name: "Deno.transpileOnly()", @@ -150,8 +144,7 @@ Deno.test({ "/bar.ts": `export const bar = "bar";\n`, }); assert(diagnostics == null); - assert(actual.includes(`__instantiate("foo", false)`)); - assert(actual.includes(`__exp["bar"]`)); + assert(actual.includes(`const bar = "bar"`)); }, }); @@ -160,26 +153,7 @@ Deno.test({ async fn() { const [diagnostics, actual] = await Deno.bundle("./subdir/mod1.ts"); assert(diagnostics == null); - assert(actual.includes(`__instantiate("mod1", false)`)); - assert(actual.includes(`__exp["printHello3"]`)); - }, -}); - -Deno.test({ - name: "Deno.bundle() - compiler config effects emit", - async fn() { - const [diagnostics, actual] = await Deno.bundle( - "/foo.ts", - { - "/foo.ts": `// random comment\nexport * from "./bar.ts";\n`, - "/bar.ts": `export const bar = "bar";\n`, - }, - { - removeComments: true, - }, - ); - assert(diagnostics == null); - assert(!actual.includes(`random`)); + assert(actual.length); }, }); @@ -191,22 +165,7 @@ Deno.test({ "/bar.js": `export const bar = "bar";\n`, }); assert(diagnostics == null); - assert(actual.includes(`System.register("bar",`)); - }, -}); - -Deno.test({ - name: "Deno.bundle - pre ES2017 uses ES5 loader", - async fn() { - const [diagnostics, actual] = await Deno.bundle( - "/foo.ts", - { - "/foo.ts": `console.log("hello world!")\n`, - }, - { target: "es2015" }, - ); - assert(diagnostics == null); - assert(actual.includes(`var __awaiter = `)); + assert(actual.includes(`const bar = "bar"`)); }, }); @@ -226,8 +185,8 @@ Deno.test({ name: "Deno.compile() - SWC diagnostics", async fn() { await assertThrowsAsync(async () => { - await Deno.compile("main.js", { - "main.js": ` + await Deno.compile("/main.js", { + "/main.js": ` export class Foo { constructor() { console.log("foo"); diff --git a/cli/tests/compiler_js_error.ts b/cli/tests/compiler_js_error.ts deleted file mode 100644 index 0b981ae3af..0000000000 --- a/cli/tests/compiler_js_error.ts +++ /dev/null @@ -1 +0,0 @@ -Deno.compile("main.js", { "main.js": "console.log(foo);" }); diff --git a/cli/tests/compiler_js_error.ts.out b/cli/tests/compiler_js_error.ts.out deleted file mode 100644 index ef6ed7e80a..0000000000 --- a/cli/tests/compiler_js_error.ts.out +++ /dev/null @@ -1,4 +0,0 @@ -Check [WILDCARD]compiler_js_error.ts -error: Uncaught (in promise) Error: Error in TS compiler: -AssertionError: Unexpected skip of the emit. -[WILDCARD] diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 979a2ffad2..64ef69ba13 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -3001,12 +3001,6 @@ itest!(deno_doc_import_map { output: "doc/use_import_map.out", }); -itest!(compiler_js_error { - args: "run --unstable compiler_js_error.ts", - output: "compiler_js_error.ts.out", - exit_code: 1, -}); - itest!(import_file_with_colon { args: "run --quiet --reload import_file_with_colon.ts", output: "import_file_with_colon.ts.out", diff --git a/cli/tests/lib_ref.ts b/cli/tests/lib_ref.ts index 0a4ce3fc72..7b7bc4ecab 100644 --- a/cli/tests/lib_ref.ts +++ b/cli/tests/lib_ref.ts @@ -1,7 +1,7 @@ const [errors, program] = await Deno.compile( - "main.ts", + "/main.ts", { - "main.ts": + "/main.ts": `/// \n\ndocument.getElementById("foo");\nDeno.args;`, }, { @@ -11,4 +11,4 @@ const [errors, program] = await Deno.compile( ); console.log(errors); -console.log(Object.keys(program)); +console.log(Object.keys(program).sort()); diff --git a/cli/tests/lib_ref.ts.out b/cli/tests/lib_ref.ts.out index 9f8c62d8af..7aee0cc58c 100644 --- a/cli/tests/lib_ref.ts.out +++ b/cli/tests/lib_ref.ts.out @@ -1,2 +1,2 @@ undefined -[ "main.js.map", "main.js" ] +[ "file:///[WILDCARD]main.ts.js", "file:///[WILDCARD]main.ts.js.map" ] diff --git a/cli/tests/lib_runtime_api.ts b/cli/tests/lib_runtime_api.ts index 788288f72d..fc00825e9c 100644 --- a/cli/tests/lib_runtime_api.ts +++ b/cli/tests/lib_runtime_api.ts @@ -1,7 +1,7 @@ const [errors, program] = await Deno.compile( - "main.ts", + "/main.ts", { - "main.ts": `document.getElementById("foo");`, + "/main.ts": `document.getElementById("foo");`, }, { lib: ["dom", "esnext"], @@ -9,4 +9,4 @@ const [errors, program] = await Deno.compile( ); console.log(errors); -console.log(Object.keys(program)); +console.log(Object.keys(program).sort()); diff --git a/cli/tests/lib_runtime_api.ts.out b/cli/tests/lib_runtime_api.ts.out index 9f8c62d8af..7aee0cc58c 100644 --- a/cli/tests/lib_runtime_api.ts.out +++ b/cli/tests/lib_runtime_api.ts.out @@ -1,2 +1,2 @@ undefined -[ "main.js.map", "main.js" ] +[ "file:///[WILDCARD]main.ts.js", "file:///[WILDCARD]main.ts.js.map" ] diff --git a/cli/tsc.rs b/cli/tsc.rs deleted file mode 100644 index 303868d442..0000000000 --- a/cli/tsc.rs +++ /dev/null @@ -1,1005 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::ast::parse; -use crate::ast::Location; -use crate::diagnostics::Diagnostics; -use crate::disk_cache::DiskCache; -use crate::file_fetcher::SourceFile; -use crate::file_fetcher::SourceFileFetcher; -use crate::flags::Flags; -use crate::fs::canonicalize_path; -use crate::js; -use crate::media_type::MediaType; -use crate::module_graph::ModuleGraph; -use crate::module_graph::ModuleGraphLoader; -use crate::permissions::Permissions; -use crate::program_state::ProgramState; -use crate::tsc_config; -use crate::version; -use deno_core::error::generic_error; -use deno_core::error::AnyError; -use deno_core::error::JsError; -use deno_core::json_op_sync; -use deno_core::serde_json; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use deno_core::url::Url; -use deno_core::JsRuntime; -use deno_core::ModuleSpecifier; -use deno_core::RuntimeOptions; -use log::debug; -use regex::Regex; -use serde::Deserialize; -use serde::Serialize; -use serde::Serializer; -use sourcemap::SourceMap; -use std::collections::HashMap; -use std::collections::HashSet; -use std::fs; -use std::io; -use std::ops::Deref; -use std::path::PathBuf; -use std::str; -use std::sync::Arc; -use std::sync::Mutex; -use swc_common::comments::Comment; -use swc_common::comments::CommentKind; -use swc_ecmascript::dep_graph; - -pub const AVAILABLE_LIBS: &[&str] = &[ - "deno.ns", - "deno.window", - "deno.worker", - "deno.shared_globals", - "deno.unstable", - "dom", - "dom.iterable", - "es5", - "es6", - "esnext", - "es2020", - "es2020.full", - "es2019", - "es2019.full", - "es2018", - "es2018.full", - "es2017", - "es2017.full", - "es2016", - "es2016.full", - "es2015", - "es2015.collection", - "es2015.core", - "es2015.generator", - "es2015.iterable", - "es2015.promise", - "es2015.proxy", - "es2015.reflect", - "es2015.symbol", - "es2015.symbol.wellknown", - "es2016.array.include", - "es2017.intl", - "es2017.object", - "es2017.sharedmemory", - "es2017.string", - "es2017.typedarrays", - "es2018.asyncgenerator", - "es2018.asynciterable", - "es2018.intl", - "es2018.promise", - "es2018.regexp", - "es2019.array", - "es2019.object", - "es2019.string", - "es2019.symbol", - "es2020.bigint", - "es2020.promise", - "es2020.string", - "es2020.symbol.wellknown", - "esnext.array", - "esnext.asynciterable", - "esnext.bigint", - "esnext.intl", - "esnext.promise", - "esnext.string", - "esnext.symbol", - "esnext.weakref", - "scripthost", - "webworker", - "webworker.importscripts", -]; - -#[derive(Debug, Clone)] -pub struct CompiledModule { - pub code: String, - pub name: String, -} - -lazy_static! { - /// Matches the `@deno-types` pragma. - static ref DENO_TYPES_RE: Regex = - Regex::new(r#"(?i)^\s*@deno-types\s*=\s*(?:["']([^"']+)["']|(\S+))"#) - .unwrap(); - /// Matches a `/// ` comment reference. - static ref TRIPLE_SLASH_REFERENCE_RE: Regex = - Regex::new(r"(?i)^/\s*").unwrap(); - /// Matches a path reference, which adds a dependency to a module - static ref PATH_REFERENCE_RE: Regex = - Regex::new(r#"(?i)\spath\s*=\s*["']([^"']*)["']"#).unwrap(); - /// Matches a types reference, which for JavaScript files indicates the - /// location of types to use when type checking a program that includes it as - /// a dependency. - static ref TYPES_REFERENCE_RE: Regex = - Regex::new(r#"(?i)\stypes\s*=\s*["']([^"']*)["']"#).unwrap(); - /// Matches a lib reference. - static ref LIB_REFERENCE_RE: Regex = - Regex::new(r#"(?i)\slib\s*=\s*["']([^"']*)["']"#).unwrap(); -} - -#[derive(Clone, Eq, PartialEq)] -pub enum TargetLib { - Main, - Worker, -} - -/// Struct which represents the state of the compiler -/// configuration where the first is canonical name for the configuration file, -/// second is a vector of the bytes of the contents of the configuration file, -/// third is bytes of the hash of contents. -#[derive(Clone)] -pub struct CompilerConfig { - pub path: Option, - pub options: Value, - pub maybe_ignored_options: Option, - pub hash: String, - pub compile_js: bool, -} - -impl CompilerConfig { - /// Take the passed flag and resolve the file name relative to the cwd. - pub fn load(maybe_config_path: Option) -> Result { - if maybe_config_path.is_none() { - return Ok(Self { - path: Some(PathBuf::new()), - options: json!({}), - maybe_ignored_options: None, - hash: "".to_string(), - compile_js: false, - }); - } - - let raw_config_path = maybe_config_path.unwrap(); - debug!("Compiler config file: {}", raw_config_path); - let cwd = std::env::current_dir().unwrap(); - let config_file = cwd.join(raw_config_path); - - // Convert the PathBuf to a canonicalized string. This is needed by the - // compiler to properly deal with the configuration. - let config_path = canonicalize_path(&config_file).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "Could not find the config file: {}", - config_file.to_string_lossy() - ), - ) - })?; - - // Load the contents of the configuration file - debug!("Attempt to load config: {}", config_path.to_str().unwrap()); - let config_bytes = fs::read(&config_file)?; - let config_hash = crate::checksum::gen(&[&config_bytes]); - let config_str = String::from_utf8(config_bytes)?; - - let (options, maybe_ignored_options) = if config_str.is_empty() { - (json!({}), None) - } else { - tsc_config::parse_config(&config_str, &config_path)? - }; - - // If `checkJs` is set to true in `compilerOptions` then we're gonna be compiling - // JavaScript files as well - let compile_js = options["checkJs"].as_bool().unwrap_or(false); - - Ok(Self { - path: Some(config_path), - options, - maybe_ignored_options, - hash: config_hash, - compile_js, - }) - } -} - -/// Information associated with compiled file in cache. -/// version_hash is used to validate versions of the file -/// and could be used to remove stale file in cache. -#[derive(Deserialize, Serialize)] -pub struct CompiledFileMetadata { - pub version_hash: String, -} - -impl CompiledFileMetadata { - pub fn to_json_string(&self) -> Result { - serde_json::to_string(self) - } -} - -/// Emit a SHA256 hash based on source code, deno version and TS config. -/// Used to check if a recompilation for source code is needed. -fn source_code_version_hash( - source_code: &[u8], - version: &str, - config_hash: &[u8], -) -> String { - crate::checksum::gen(&[source_code, version.as_bytes(), config_hash]) -} - -pub struct TsCompilerInner { - pub file_fetcher: SourceFileFetcher, - pub flags: Flags, - pub config: CompilerConfig, - pub disk_cache: DiskCache, - /// Set of all URLs that have been compiled. This prevents double - /// compilation of module. - pub compiled: Mutex>, - /// This setting is controlled by `--reload` flag. Unless the flag - /// is provided disk cache is used. - pub use_disk_cache: bool, - /// This setting is controlled by `compilerOptions.checkJs` - pub compile_js: bool, -} - -#[derive(Clone)] -pub struct TsCompiler(Arc); - -impl Deref for TsCompiler { - type Target = TsCompilerInner; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Stat { - key: String, - value: f64, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct EmittedSource { - filename: String, - contents: String, -} - -// TODO(bartlomieju): possible deduplicate once TS refactor is stabilized -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -#[allow(unused)] -struct RuntimeBundleResponse { - diagnostics: Diagnostics, - output: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct RuntimeCompileResponse { - diagnostics: Diagnostics, - emit_map: HashMap, -} - -impl TsCompiler { - pub fn new( - file_fetcher: SourceFileFetcher, - flags: Flags, - disk_cache: DiskCache, - ) -> Result { - let config = CompilerConfig::load(flags.config_path.clone())?; - let use_disk_cache = !flags.reload; - - Ok(TsCompiler(Arc::new(TsCompilerInner { - file_fetcher, - flags, - disk_cache, - compile_js: config.compile_js, - config, - compiled: Mutex::new(HashSet::new()), - use_disk_cache, - }))) - } - - /// Mark given module URL as compiled to avoid multiple compilations of same - /// module in single run. - fn mark_compiled(&self, url: &Url) { - let mut c = self.compiled.lock().unwrap(); - c.insert(url.clone()); - } - - fn cache_emitted_files( - &self, - emit_map: HashMap, - ) -> std::io::Result<()> { - for (emitted_name, source) in emit_map.iter() { - let specifier = ModuleSpecifier::resolve_url(&source.filename) - .expect("Should be a valid module specifier"); - - let source_file = self - .file_fetcher - .fetch_cached_source_file(&specifier, Permissions::allow_all()) - .expect("Source file not found"); - - // NOTE: JavaScript files are only cached to disk if `checkJs` - // option in on - if source_file.media_type == MediaType::JavaScript && !self.compile_js { - continue; - } - - if emitted_name.ends_with(".map") { - self.cache_source_map(&specifier, &source.contents)?; - } else if emitted_name.ends_with(".js") { - self.cache_compiled_file(&specifier, source_file, &source.contents)?; - } else { - panic!("Trying to cache unknown file type {}", emitted_name); - } - } - - Ok(()) - } - - /// Save compiled JS file for given TS module to on-disk cache. - /// - /// Along compiled file a special metadata file is saved as well containing - /// hash that can be validated to avoid unnecessary recompilation. - fn cache_compiled_file( - &self, - module_specifier: &ModuleSpecifier, - source_file: SourceFile, - contents: &str, - ) -> std::io::Result<()> { - let js_key = self - .disk_cache - .get_cache_filename_with_extension(module_specifier.as_url(), "js"); - self.disk_cache.set(&js_key, contents.as_bytes())?; - self.mark_compiled(module_specifier.as_url()); - - let version_hash = source_code_version_hash( - &source_file.source_code.as_bytes(), - version::DENO, - &self.config.hash.as_bytes(), - ); - - let compiled_file_metadata = CompiledFileMetadata { version_hash }; - let meta_key = self - .disk_cache - .get_cache_filename_with_extension(module_specifier.as_url(), "meta"); - self.disk_cache.set( - &meta_key, - compiled_file_metadata.to_json_string()?.as_bytes(), - ) - } - - /// Save source map file for given TS module to on-disk cache. - fn cache_source_map( - &self, - module_specifier: &ModuleSpecifier, - contents: &str, - ) -> std::io::Result<()> { - let js_key = self - .disk_cache - .get_cache_filename_with_extension(module_specifier.as_url(), "js"); - let js_path = self.disk_cache.location.join(js_key); - let js_file_url = - Url::from_file_path(js_path).expect("Bad file URL for file"); - - let source_map_key = self - .disk_cache - .get_cache_filename_with_extension(module_specifier.as_url(), "js.map"); - - let mut sm = SourceMap::from_slice(contents.as_bytes()) - .expect("Invalid source map content"); - sm.set_file(Some(&js_file_url.to_string())); - sm.set_source(0, &module_specifier.to_string()); - - let mut output: Vec = vec![]; - sm.to_writer(&mut output) - .expect("Failed to write source map"); - - self.disk_cache.set(&source_map_key, &output) - } -} - -#[derive(Debug, Deserialize)] -struct CreateHashArgs { - data: String, -} - -fn execute_in_tsc( - program_state: Arc, - req: String, -) -> Result { - let mut js_runtime = JsRuntime::new(RuntimeOptions { - startup_snapshot: Some(js::compiler_isolate_init()), - ..Default::default() - }); - - let debug_flag = program_state - .flags - .log_level - .map_or(false, |l| l == log::Level::Debug); - let response = Arc::new(Mutex::new(None)); - - { - js_runtime.register_op( - "op_fetch_asset", - crate::op_fetch_asset::op_fetch_asset(HashMap::default()), - ); - let res = response.clone(); - js_runtime.register_op( - "op_compiler_respond", - json_op_sync(move |_state, args, _bufs| { - let mut response_slot = res.lock().unwrap(); - let replaced_value = response_slot.replace(args.to_string()); - assert!( - replaced_value.is_none(), - "op_compiler_respond found unexpected existing compiler output", - ); - Ok(json!({})) - }), - ); - js_runtime.register_op( - "op_create_hash", - json_op_sync(move |_s, args, _bufs| { - let v: CreateHashArgs = serde_json::from_value(args)?; - let hash = crate::checksum::gen(&[v.data.as_bytes()]); - Ok(json!({ "hash": hash })) - }), - ); - } - - let bootstrap_script = format!( - "globalThis.startup({{ debugFlag: {}, legacy: true }})", - debug_flag - ); - js_runtime.execute("", &bootstrap_script)?; - - let script = format!("globalThis.tsCompilerOnMessage({{ data: {} }});", req); - js_runtime.execute("", &script)?; - - let maybe_response = response.lock().unwrap().take(); - assert!( - maybe_response.is_some(), - "Unexpected missing response from TS compiler" - ); - - Ok(maybe_response.unwrap()) -} - -async fn create_runtime_module_graph( - program_state: &Arc, - permissions: Permissions, - root_name: &str, - sources: &Option>, - type_files: Vec, -) -> Result<(Vec, ModuleGraph), AnyError> { - let mut root_names = vec![]; - let mut module_graph_loader = ModuleGraphLoader::new( - program_state.file_fetcher.clone(), - None, - permissions, - false, - false, - ); - - if let Some(s_map) = sources { - root_names.push(root_name.to_string()); - module_graph_loader.build_local_graph(root_name, s_map)?; - } else { - let module_specifier = - ModuleSpecifier::resolve_import(root_name, "")?; - root_names.push(module_specifier.to_string()); - module_graph_loader - .add_to_graph(&module_specifier, None) - .await?; - } - - // download all additional files from TSconfig and add them to root_names - for type_file in type_files { - let type_specifier = ModuleSpecifier::resolve_url_or_path(&type_file)?; - module_graph_loader - .add_to_graph(&type_specifier, None) - .await?; - root_names.push(type_specifier.to_string()) - } - - Ok((root_names, module_graph_loader.get_graph())) -} - -fn extract_js_error(error: AnyError) -> AnyError { - match error.downcast::() { - Ok(js_error) => { - let msg = format!("Error in TS compiler:\n{}", js_error); - generic_error(msg) - } - Err(error) => error, - } -} - -/// This function is used by `Deno.compile()` API. -pub async fn runtime_compile( - program_state: &Arc, - permissions: Permissions, - root_name: &str, - sources: &Option>, - maybe_options: &Option, -) -> Result { - let mut user_options = if let Some(options) = maybe_options { - tsc_config::parse_raw_config(options)? - } else { - json!({}) - }; - - // Intentionally calling "take()" to replace value with `null` - otherwise TSC will try to load that file - // using `fileExists` API - let type_files = if let Some(types) = user_options["types"].take().as_array() - { - types - .iter() - .map(|type_value| type_value.as_str().unwrap_or("").to_string()) - .filter(|type_str| !type_str.is_empty()) - .collect() - } else { - vec![] - }; - - let unstable = program_state.flags.unstable; - - let mut lib = vec![]; - if let Some(user_libs) = user_options["lib"].take().as_array() { - let libs = user_libs - .iter() - .map(|type_value| type_value.as_str().unwrap_or("").to_string()) - .filter(|type_str| !type_str.is_empty()) - .collect::>(); - lib.extend(libs); - } else { - lib.push("deno.window".to_string()); - } - - if unstable { - lib.push("deno.unstable".to_string()); - } - - let mut compiler_options = json!({ - "allowJs": false, - "allowNonTsExtensions": true, - "checkJs": false, - "esModuleInterop": true, - "isolatedModules": true, - "jsx": "react", - "module": "esnext", - "sourceMap": true, - "strict": true, - "removeComments": true, - "target": "esnext", - }); - - tsc_config::json_merge(&mut compiler_options, &user_options); - tsc_config::json_merge(&mut compiler_options, &json!({ "lib": lib })); - - let (root_names, module_graph) = create_runtime_module_graph( - &program_state, - permissions.clone(), - root_name, - sources, - type_files, - ) - .await?; - let module_graph_json = - serde_json::to_value(module_graph).expect("Failed to serialize data"); - - let req_msg = json!({ - "type": CompilerRequestType::RuntimeCompile, - "target": "runtime", - "rootNames": root_names, - "sourceFileMap": module_graph_json, - "compilerOptions": compiler_options, - }) - .to_string(); - - let compiler = program_state.ts_compiler.clone(); - - let json_str = - execute_in_tsc(program_state.clone(), req_msg).map_err(extract_js_error)?; - let response: RuntimeCompileResponse = serde_json::from_str(&json_str)?; - - if response.diagnostics.is_empty() && sources.is_none() { - compiler.cache_emitted_files(response.emit_map)?; - } - - // We're returning `Ok()` instead of `Err()` because it's not runtime - // error if there were diagnostics produced; we want to let user handle - // diagnostics in the runtime. - Ok(serde_json::from_str::(&json_str).unwrap()) -} - -/// This function is used by `Deno.bundle()` API. -pub async fn runtime_bundle( - program_state: &Arc, - permissions: Permissions, - root_name: &str, - sources: &Option>, - maybe_options: &Option, -) -> Result { - let mut user_options = if let Some(options) = maybe_options { - tsc_config::parse_raw_config(options)? - } else { - json!({}) - }; - - // Intentionally calling "take()" to replace value with `null` - otherwise TSC will try to load that file - // using `fileExists` API - let type_files = if let Some(types) = user_options["types"].take().as_array() - { - types - .iter() - .map(|type_value| type_value.as_str().unwrap_or("").to_string()) - .filter(|type_str| !type_str.is_empty()) - .collect() - } else { - vec![] - }; - - let (root_names, module_graph) = create_runtime_module_graph( - &program_state, - permissions.clone(), - root_name, - sources, - type_files, - ) - .await?; - let module_graph_json = - serde_json::to_value(module_graph).expect("Failed to serialize data"); - - let unstable = program_state.flags.unstable; - - let mut lib = vec![]; - if let Some(user_libs) = user_options["lib"].take().as_array() { - let libs = user_libs - .iter() - .map(|type_value| type_value.as_str().unwrap_or("").to_string()) - .filter(|type_str| !type_str.is_empty()) - .collect::>(); - lib.extend(libs); - } else { - lib.push("deno.window".to_string()); - } - - if unstable { - lib.push("deno.unstable".to_string()); - } - - let mut compiler_options = json!({ - "allowJs": false, - "allowNonTsExtensions": true, - "checkJs": false, - "esModuleInterop": true, - "jsx": "react", - "module": "esnext", - "outDir": null, - "sourceMap": true, - "strict": true, - "removeComments": true, - "target": "esnext", - }); - - let bundler_options = json!({ - "allowJs": true, - "inlineSourceMap": false, - "module": "system", - "outDir": null, - "outFile": "deno:///bundle.js", - // disabled until we have effective way to modify source maps - "sourceMap": false, - }); - - tsc_config::json_merge(&mut compiler_options, &user_options); - tsc_config::json_merge(&mut compiler_options, &json!({ "lib": lib })); - tsc_config::json_merge(&mut compiler_options, &bundler_options); - - let req_msg = json!({ - "type": CompilerRequestType::RuntimeBundle, - "target": "runtime", - "rootNames": root_names, - "sourceFileMap": module_graph_json, - "compilerOptions": compiler_options, - }) - .to_string(); - - let json_str = - execute_in_tsc(program_state.clone(), req_msg).map_err(extract_js_error)?; - let _response: RuntimeBundleResponse = serde_json::from_str(&json_str)?; - // We're returning `Ok()` instead of `Err()` because it's not runtime - // error if there were diagnostics produced; we want to let user handle - // diagnostics in the runtime. - Ok(serde_json::from_str::(&json_str).unwrap()) -} - -#[derive(Clone, Debug, PartialEq)] -pub struct ImportDesc { - pub specifier: String, - pub deno_types: Option, - pub location: Location, -} - -#[derive(Clone, Debug, PartialEq)] -pub enum TsReferenceKind { - Lib, - Types, - Path, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct TsReferenceDesc { - pub kind: TsReferenceKind, - pub specifier: String, - pub location: Location, -} - -// TODO(bartlomieju): handle imports in ambient contexts/TS modules -/// This function is a port of `ts.preProcessFile()` -/// -/// Additionally it captures `@deno-types` references directly -/// preceeding `import .. from` and `export .. from` statements. -pub fn pre_process_file( - file_name: &str, - media_type: MediaType, - source_code: &str, - analyze_dynamic_imports: bool, -) -> Result<(Vec, Vec), AnyError> { - let specifier = ModuleSpecifier::resolve_url_or_path(file_name)?; - let module = parse(specifier.as_str(), source_code, &media_type)?; - - let dependency_descriptors = module.analyze_dependencies(); - - // for each import check if there's relevant @deno-types directive - let imports = dependency_descriptors - .iter() - .filter(|desc| desc.kind != dep_graph::DependencyKind::Require) - .filter(|desc| { - if analyze_dynamic_imports { - return true; - } - !desc.is_dynamic - }) - .map(|desc| { - let deno_types = get_deno_types(&desc.leading_comments); - ImportDesc { - specifier: desc.specifier.to_string(), - deno_types, - location: Location { - filename: file_name.to_string(), - col: desc.col, - line: desc.line, - }, - } - }) - .collect(); - - // analyze comment from beginning of the file and find TS directives - let comments = module.get_leading_comments(); - - let mut references = vec![]; - for comment in comments { - if comment.kind != CommentKind::Line { - continue; - } - - let text = comment.text.to_string(); - if let Some((kind, specifier)) = parse_ts_reference(text.trim()) { - let location = module.get_location(&comment.span); - references.push(TsReferenceDesc { - kind, - specifier, - location, - }); - } - } - Ok((imports, references)) -} - -fn get_deno_types(comments: &[Comment]) -> Option { - if comments.is_empty() { - return None; - } - - // @deno-types must directly prepend import statement - hence - // checking last comment for span - let last = comments.last().unwrap(); - let comment = last.text.trim_start(); - parse_deno_types(&comment) -} - -fn parse_ts_reference(comment: &str) -> Option<(TsReferenceKind, String)> { - if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) { - return None; - } - - let (kind, specifier) = - if let Some(capture_groups) = PATH_REFERENCE_RE.captures(comment) { - (TsReferenceKind::Path, capture_groups.get(1).unwrap()) - } else if let Some(capture_groups) = TYPES_REFERENCE_RE.captures(comment) { - (TsReferenceKind::Types, capture_groups.get(1).unwrap()) - } else if let Some(capture_groups) = LIB_REFERENCE_RE.captures(comment) { - (TsReferenceKind::Lib, capture_groups.get(1).unwrap()) - } else { - return None; - }; - - Some((kind, specifier.as_str().to_string())) -} - -fn parse_deno_types(comment: &str) -> Option { - if let Some(capture_groups) = DENO_TYPES_RE.captures(comment) { - if let Some(specifier) = capture_groups.get(1) { - return Some(specifier.as_str().to_string()); - } - if let Some(specifier) = capture_groups.get(2) { - return Some(specifier.as_str().to_string()); - } - } - - None -} - -// Warning! The values in this enum are duplicated in js/compiler.ts -// Update carefully! -#[repr(i32)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum CompilerRequestType { - RuntimeCompile = 2, - RuntimeBundle = 3, -} - -impl Serialize for CompilerRequestType { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let value: i32 = match self { - CompilerRequestType::RuntimeCompile => 2 as i32, - CompilerRequestType::RuntimeBundle => 3 as i32, - }; - Serialize::serialize(&value, serializer) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::fs as deno_fs; - use tempfile::TempDir; - - #[test] - fn test_parse_deno_types() { - assert_eq!( - parse_deno_types("@deno-types=./a/b/c.d.ts"), - Some("./a/b/c.d.ts".to_string()) - ); - assert_eq!( - parse_deno_types("@deno-types=\"./a/b/c.d.ts\""), - Some("./a/b/c.d.ts".to_string()) - ); - assert_eq!( - parse_deno_types("@deno-types = https://dneo.land/x/some/package/a.d.ts"), - Some("https://dneo.land/x/some/package/a.d.ts".to_string()) - ); - assert_eq!( - parse_deno_types("@deno-types = ./a/b/c.d.ts"), - Some("./a/b/c.d.ts".to_string()) - ); - assert!(parse_deno_types("asdf").is_none()); - assert!(parse_deno_types("// deno-types = fooo").is_none()); - assert_eq!( - parse_deno_types("@deno-types=./a/b/c.d.ts some comment"), - Some("./a/b/c.d.ts".to_string()) - ); - assert_eq!( - parse_deno_types( - "@deno-types=./a/b/c.d.ts // some comment after slashes" - ), - Some("./a/b/c.d.ts".to_string()) - ); - assert_eq!( - parse_deno_types(r#"@deno-types="https://deno.land/x/foo/index.d.ts";"#), - Some("https://deno.land/x/foo/index.d.ts".to_string()) - ); - } - - #[test] - fn test_parse_ts_reference() { - assert_eq!( - parse_ts_reference(r#"/ "#), - Some((TsReferenceKind::Lib, "deno.shared_globals".to_string())) - ); - assert_eq!( - parse_ts_reference(r#"/ "#), - Some((TsReferenceKind::Path, "./type/reference/dep.ts".to_string())) - ); - assert_eq!( - parse_ts_reference(r#"/ "#), - Some((TsReferenceKind::Types, "./type/reference.d.ts".to_string())) - ); - assert!(parse_ts_reference("asdf").is_none()); - assert!( - parse_ts_reference(r#"/ "#).is_none() - ); - assert!(parse_ts_reference(r#"/ "#).is_none()); - } - - #[test] - fn test_source_code_version_hash() { - assert_eq!( - "0185b42de0686b4c93c314daaa8dee159f768a9e9a336c2a5e3d5b8ca6c4208c", - source_code_version_hash(b"1+2", "0.4.0", b"{}") - ); - // Different source_code should result in different hash. - assert_eq!( - "e58631f1b6b6ce2b300b133ec2ad16a8a5ba6b7ecf812a8c06e59056638571ac", - source_code_version_hash(b"1", "0.4.0", b"{}") - ); - // Different version should result in different hash. - assert_eq!( - "307e6200347a88dbbada453102deb91c12939c65494e987d2d8978f6609b5633", - source_code_version_hash(b"1", "0.1.0", b"{}") - ); - // Different config should result in different hash. - assert_eq!( - "195eaf104a591d1d7f69fc169c60a41959c2b7a21373cd23a8f675f877ec385f", - source_code_version_hash(b"1", "0.4.0", b"{\"compilerOptions\": {}}") - ); - } - - #[test] - fn test_compile_js() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let temp_dir_path = temp_dir.path(); - - let test_cases = vec![ - // valid JSON - (r#"{ "compilerOptions": { "checkJs": true } } "#, true), - // JSON with comment - ( - r#"{ - "compilerOptions": { - // force .js file compilation by Deno - "checkJs": true - } - }"#, - true, - ), - // without content - ("", false), - ]; - - let path = temp_dir_path.join("tsconfig.json"); - let path_str = path.to_str().unwrap().to_string(); - - for (json_str, expected) in test_cases { - deno_fs::write_file(&path, json_str.as_bytes(), 0o666).unwrap(); - let config = CompilerConfig::load(Some(path_str.clone())).unwrap(); - assert_eq!(config.compile_js, expected); - } - } - - #[test] - fn test_compiler_config_load() { - let temp_dir = TempDir::new().expect("tempdir fail"); - let temp_dir_path = temp_dir.path(); - let path = temp_dir_path.join("doesnotexist.json"); - let path_str = path.to_str().unwrap().to_string(); - let res = CompilerConfig::load(Some(path_str)); - assert!(res.is_err()); - } -} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index b286f596d0..e2a481d0f3 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -118,9 +118,6 @@ delete Object.prototype.__proto__; return core.decode(sourceCodeBytes); } - // Constants used by `normalizeString` and `resolvePath` - const CHAR_DOT = 46; /* . */ - const CHAR_FORWARD_SLASH = 47; /* / */ // Using incremental compile APIs requires that all // paths must be either relative or absolute. Since // analysis in Rust operates on fully resolved URLs, @@ -218,18 +215,6 @@ delete Object.prototype.__proto__; */ const RESOLVED_SPECIFIER_CACHE = new Map(); - function parseCompilerOptions(compilerOptions) { - const { options, errors } = ts.convertCompilerOptionsFromJson( - compilerOptions, - "", - "tsconfig.json", - ); - return { - options, - diagnostics: errors.length ? errors : undefined, - }; - } - class SourceFile { constructor(json) { this.processed = false; @@ -541,95 +526,6 @@ delete Object.prototype.__proto__; host, }); - // This function is called only during snapshotting process - const SYSTEM_LOADER = getAsset("system_loader.js"); - const SYSTEM_LOADER_ES5 = getAsset("system_loader_es5.js"); - - function buildLocalSourceFileCache(sourceFileMap) { - for (const entry of Object.values(sourceFileMap)) { - assert(entry.sourceCode.length > 0); - SourceFile.addToCache({ - url: entry.url, - filename: entry.url, - mediaType: entry.mediaType, - sourceCode: entry.sourceCode, - versionHash: entry.versionHash, - }); - - for (const importDesc of entry.imports) { - let mappedUrl = importDesc.resolvedSpecifier; - const importedFile = sourceFileMap[importDesc.resolvedSpecifier]; - assert(importedFile); - const isJsOrJsx = importedFile.mediaType === MediaType.JavaScript || - importedFile.mediaType === MediaType.JSX; - // If JS or JSX perform substitution for types if available - if (isJsOrJsx) { - // @deno-types has highest precedence, followed by - // X-TypeScript-Types header - if (importDesc.resolvedTypeDirective) { - mappedUrl = importDesc.resolvedTypeDirective; - } else if (importedFile.typeHeaders.length > 0) { - const typeHeaders = importedFile.typeHeaders[0]; - mappedUrl = typeHeaders.resolvedSpecifier; - } else if (importedFile.typesDirectives.length > 0) { - const typeDirective = importedFile.typesDirectives[0]; - mappedUrl = typeDirective.resolvedSpecifier; - } - } - - mappedUrl = mappedUrl.replace("memory://", ""); - SourceFile.cacheResolvedUrl(mappedUrl, importDesc.specifier, entry.url); - } - for (const fileRef of entry.referencedFiles) { - SourceFile.cacheResolvedUrl( - fileRef.resolvedSpecifier.replace("memory://", ""), - fileRef.specifier, - entry.url, - ); - } - for (const fileRef of entry.libDirectives) { - SourceFile.cacheResolvedUrl( - fileRef.resolvedSpecifier.replace("memory://", ""), - fileRef.specifier, - entry.url, - ); - } - } - } - - // Warning! The values in this enum are duplicated in `cli/msg.rs` - // Update carefully! - const CompilerRequestType = { - RuntimeCompile: 2, - RuntimeBundle: 3, - }; - - function createBundleWriteFile(state) { - return function writeFile(_fileName, data, sourceFiles) { - assert(sourceFiles != null); - assert(state.options); - // we only support single root names for bundles - assert(state.rootNames.length === 1); - state.bundleOutput = buildBundle( - state.rootNames[0], - data, - sourceFiles, - state.options.target ?? ts.ScriptTarget.ESNext, - ); - }; - } - - function createRuntimeCompileWriteFile(state) { - return function writeFile(fileName, data, sourceFiles) { - assert(sourceFiles); - assert(sourceFiles.length === 1); - state.emitMap[fileName] = { - filename: sourceFiles[0].fileName, - contents: data, - }; - }; - } - const IGNORED_DIAGNOSTICS = [ // TS2306: File 'file:///Users/rld/src/deno/cli/tests/subdir/amd_like.js' is // not a module. @@ -674,7 +570,6 @@ delete Object.prototype.__proto__; function performanceStart() { stats.length = 0; - // TODO(kitsonk) replace with performance.mark() when landed statsStart = new Date(); ts.performance.enable(); } @@ -716,317 +611,6 @@ delete Object.prototype.__proto__; return stats; } - function normalizeString(path) { - let res = ""; - let lastSegmentLength = 0; - let lastSlash = -1; - let dots = 0; - let code; - for (let i = 0, len = path.length; i <= len; ++i) { - if (i < len) code = path.charCodeAt(i); - else if (code === CHAR_FORWARD_SLASH) break; - else code = CHAR_FORWARD_SLASH; - - if (code === CHAR_FORWARD_SLASH) { - if (lastSlash === i - 1 || dots === 1) { - // NOOP - } else if (lastSlash !== i - 1 && dots === 2) { - if ( - res.length < 2 || - lastSegmentLength !== 2 || - res.charCodeAt(res.length - 1) !== CHAR_DOT || - res.charCodeAt(res.length - 2) !== CHAR_DOT - ) { - if (res.length > 2) { - const lastSlashIndex = res.lastIndexOf("/"); - if (lastSlashIndex === -1) { - res = ""; - lastSegmentLength = 0; - } else { - res = res.slice(0, lastSlashIndex); - lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); - } - lastSlash = i; - dots = 0; - continue; - } else if (res.length === 2 || res.length === 1) { - res = ""; - lastSegmentLength = 0; - lastSlash = i; - dots = 0; - continue; - } - } - } else { - if (res.length > 0) res += "/" + path.slice(lastSlash + 1, i); - else res = path.slice(lastSlash + 1, i); - lastSegmentLength = i - lastSlash - 1; - } - lastSlash = i; - dots = 0; - } else if (code === CHAR_DOT && dots !== -1) { - ++dots; - } else { - dots = -1; - } - } - return res; - } - - function commonPath(paths, sep = "/") { - const [first = "", ...remaining] = paths; - if (first === "" || remaining.length === 0) { - return first.substring(0, first.lastIndexOf(sep) + 1); - } - const parts = first.split(sep); - - let endOfPrefix = parts.length; - for (const path of remaining) { - const compare = path.split(sep); - for (let i = 0; i < endOfPrefix; i++) { - if (compare[i] !== parts[i]) { - endOfPrefix = i; - } - } - - if (endOfPrefix === 0) { - return ""; - } - } - const prefix = parts.slice(0, endOfPrefix).join(sep); - return prefix.endsWith(sep) ? prefix : `${prefix}${sep}`; - } - - let rootExports; - - function normalizeUrl(rootName) { - const match = /^(\S+:\/{2,3})(.+)$/.exec(rootName); - if (match) { - const [, protocol, path] = match; - return `${protocol}${normalizeString(path)}`; - } else { - return rootName; - } - } - - function buildBundle(rootName, data, sourceFiles, target) { - // when outputting to AMD and a single outfile, TypeScript makes up the module - // specifiers which are used to define the modules, and doesn't expose them - // publicly, so we have to try to replicate - const sources = sourceFiles.map((sf) => sf.fileName); - const sharedPath = commonPath(sources); - rootName = normalizeUrl(rootName) - .replace(sharedPath, "") - .replace(/\.\w+$/i, ""); - // If one of the modules requires support for top-level-await, TypeScript will - // emit the execute function as an async function. When this is the case we - // need to bubble up the TLA to the instantiation, otherwise we instantiate - // synchronously. - const hasTla = data.match(/execute:\sasync\sfunction\s/); - let instantiate; - if (rootExports && rootExports.length) { - instantiate = hasTla - ? `const __exp = await __instantiate("${rootName}", true);\n` - : `const __exp = __instantiate("${rootName}", false);\n`; - for (const rootExport of rootExports) { - if (rootExport === "default") { - instantiate += `export default __exp["${rootExport}"];\n`; - } else { - instantiate += - `export const ${rootExport} = __exp["${rootExport}"];\n`; - } - } - } else { - instantiate = hasTla - ? `await __instantiate("${rootName}", true);\n` - : `__instantiate("${rootName}", false);\n`; - } - const es5Bundle = target === ts.ScriptTarget.ES3 || - target === ts.ScriptTarget.ES5 || - target === ts.ScriptTarget.ES2015 || - target === ts.ScriptTarget.ES2016; - return `${ - es5Bundle ? SYSTEM_LOADER_ES5 : SYSTEM_LOADER - }\n${data}\n${instantiate}`; - } - - function setRootExports(program, rootModule) { - // get a reference to the type checker, this will let us find symbols from - // the AST. - const checker = program.getTypeChecker(); - // get a reference to the main source file for the bundle - const mainSourceFile = program.getSourceFile(rootModule); - assert(mainSourceFile); - // retrieve the internal TypeScript symbol for this AST node - const mainSymbol = checker.getSymbolAtLocation(mainSourceFile); - if (!mainSymbol) { - return; - } - rootExports = checker - .getExportsOfModule(mainSymbol) - // .getExportsOfModule includes type only symbols which are exported from - // the module, so we need to try to filter those out. While not critical - // someone looking at the bundle would think there is runtime code behind - // that when there isn't. There appears to be no clean way of figuring that - // out, so inspecting SymbolFlags that might be present that are type only - .filter( - (sym) => - sym.flags & ts.SymbolFlags.Class || - !( - sym.flags & ts.SymbolFlags.Interface || - sym.flags & ts.SymbolFlags.TypeLiteral || - sym.flags & ts.SymbolFlags.Signature || - sym.flags & ts.SymbolFlags.TypeParameter || - sym.flags & ts.SymbolFlags.TypeAlias || - sym.flags & ts.SymbolFlags.Type || - sym.flags & ts.SymbolFlags.Namespace || - sym.flags & ts.SymbolFlags.InterfaceExcludes || - sym.flags & ts.SymbolFlags.TypeParameterExcludes || - sym.flags & ts.SymbolFlags.TypeAliasExcludes - ), - ) - .map((sym) => sym.getName()); - } - - function runtimeCompile(request) { - const { compilerOptions, rootNames, target, sourceFileMap } = request; - - debug(">>> runtime compile start", { - rootNames, - }); - - // if there are options, convert them into TypeScript compiler options, - // and resolve any external file references - const result = parseCompilerOptions( - compilerOptions, - ); - const options = result.options; - // 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; - - buildLocalSourceFileCache(sourceFileMap); - - const state = { - rootNames, - emitMap: {}, - }; - legacyHostState.target = target; - legacyHostState.writeFile = createRuntimeCompileWriteFile(state); - const program = ts.createProgram({ - rootNames, - options, - host, - }); - - const diagnostics = ts - .getPreEmitDiagnostics(program) - .filter(({ code }) => - !IGNORED_DIAGNOSTICS.includes(code) && - !IGNORED_COMPILE_DIAGNOSTICS.includes(code) - ); - - const emitResult = program.emit(); - assert(emitResult.emitSkipped === false, "Unexpected skip of the emit."); - - debug("<<< runtime compile finish", { - rootNames, - emitMap: Object.keys(state.emitMap), - }); - - const maybeDiagnostics = diagnostics.length - ? fromTypeScriptDiagnostic(diagnostics) - : []; - - return { - diagnostics: maybeDiagnostics, - emitMap: state.emitMap, - }; - } - - function runtimeBundle(request) { - const { compilerOptions, rootNames, target, sourceFileMap } = request; - - debug(">>> runtime bundle start", { - rootNames, - }); - - // if there are options, convert them into TypeScript compiler options, - // and resolve any external file references - const result = parseCompilerOptions( - compilerOptions, - ); - const options = result.options; - // 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; - - buildLocalSourceFileCache(sourceFileMap); - - const state = { - rootNames, - bundleOutput: undefined, - }; - - legacyHostState.target = target; - legacyHostState.writeFile = createBundleWriteFile(state); - state.options = options; - - const program = ts.createProgram({ - rootNames, - options, - host, - }); - - setRootExports(program, rootNames[0]); - const diagnostics = ts - .getPreEmitDiagnostics(program) - .filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)); - - const emitResult = program.emit(); - - assert(emitResult.emitSkipped === false, "Unexpected skip of the emit."); - - debug("<<< runtime bundle finish", { - rootNames, - }); - - const maybeDiagnostics = diagnostics.length - ? fromTypeScriptDiagnostic(diagnostics) - : []; - - return { - diagnostics: maybeDiagnostics, - output: state.bundleOutput, - }; - } - - function opCompilerRespond(msg) { - core.jsonOpSync("op_compiler_respond", msg); - } - - function tsCompilerOnMessage(msg) { - const request = msg.data; - switch (request.type) { - case CompilerRequestType.RuntimeCompile: { - const result = runtimeCompile(request); - opCompilerRespond(result); - break; - } - case CompilerRequestType.RuntimeBundle: { - const result = runtimeBundle(request); - opCompilerRespond(result); - break; - } - default: - throw new Error( - `!!! unhandled CompilerRequestType: ${request.type} (${ - CompilerRequestType[request.type] - })`, - ); - } - } - /** * @typedef {object} Request * @property {Record} config @@ -1094,6 +678,4 @@ delete Object.prototype.__proto__; globalThis.startup = startup; globalThis.exec = exec; - // TODO(@kitsonk) remove when converted from legacy tsc - globalThis.tsCompilerOnMessage = tsCompilerOnMessage; })(this); diff --git a/cli/tsc_config.rs b/cli/tsc_config.rs index 52daf2e462..92332cca67 100644 --- a/cli/tsc_config.rs +++ b/cli/tsc_config.rs @@ -52,37 +52,49 @@ impl fmt::Display for IgnoredCompilerOptions { /// A static slice of all the compiler options that should be ignored that /// either have no effect on the compilation or would cause the emit to not work /// in Deno. -const IGNORED_COMPILER_OPTIONS: [&str; 10] = [ +const IGNORED_COMPILER_OPTIONS: &[&str] = &[ "allowSyntheticDefaultImports", + "allowUmdGlobalAccess", + "baseUrl", + "declaration", + "declarationMap", + "downlevelIteration", "esModuleInterop", + "emitDeclarationOnly", + "importHelpers", "inlineSourceMap", "inlineSources", // TODO(nayeemrmn): Add "isolatedModules" here for 1.6.0. "module", + "noEmitHelpers", "noLib", + "noResolve", + "outDir", + "paths", "preserveConstEnums", "reactNamespace", + "rootDir", + "rootDirs", + "skipLibCheck", "sourceMap", + "sourceRoot", "target", + "types", + "useDefineForClassFields", ]; -const IGNORED_RUNTIME_COMPILER_OPTIONS: [&str; 50] = [ - "allowUmdGlobalAccess", +const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[ "assumeChangesOnlyAffectDirectDependencies", - "baseUrl", "build", + "charset", "composite", - "declaration", - "declarationMap", "diagnostics", - "downlevelIteration", + "disableSizeLimit", "emitBOM", - "emitDeclarationOnly", "extendedDiagnostics", "forceConsistentCasingInFileNames", "generateCpuProfile", "help", - "importHelpers", "incremental", "init", "listEmittedFiles", @@ -92,29 +104,21 @@ const IGNORED_RUNTIME_COMPILER_OPTIONS: [&str; 50] = [ "moduleResolution", "newLine", "noEmit", - "noEmitHelpers", "noEmitOnError", - "noResolve", "out", "outDir", "outFile", - "paths", "preserveSymlinks", "preserveWatchOutput", "pretty", + "project", "resolveJsonModule", - "rootDir", - "rootDirs", "showConfig", "skipDefaultLibCheck", - "skipLibCheck", - "sourceRoot", "stripInternal", "traceResolution", "tsBuildInfoFile", - "types", "typeRoots", - "useDefineForClassFields", "version", "watch", ]; @@ -170,12 +174,6 @@ struct TSConfigJson { type_acquisition: Option, } -pub fn parse_raw_config(config_text: &str) -> Result { - assert!(!config_text.is_empty()); - let jsonc = jsonc_parser::parse_to_value(config_text)?.unwrap(); - Ok(jsonc_to_serde(jsonc)) -} - fn parse_compiler_options( compiler_options: &HashMap, maybe_path: Option, @@ -394,18 +392,6 @@ mod tests { ); } - #[test] - fn test_parse_raw_config() { - let invalid_config_text = r#"{ - "compilerOptions": { - // comments are allowed - }"#; - let errbox = parse_raw_config(invalid_config_text).unwrap_err(); - assert!(errbox - .to_string() - .starts_with("Unterminated object on line 1")); - } - #[test] fn test_tsconfig_as_bytes() { let mut tsconfig1 = TsConfig::new(json!({