use crate::args::config_file::IgnoredCompilerOptions;use crate::args::ConfigFile;use crate::args::EmitConfigOptions;use crate::args::TsConfig;use crate::args::TypeCheckMode;use crate::cache::CacheType;use crate::cache::Cacher;use crate::colors;use crate::diagnostics::Diagnostics;use crate::graph_util::GraphData;use crate::graph_util::ModuleEntry;use crate::tsc;use crate::version;
use deno_ast::swc::bundler::Hook;use deno_ast::swc::bundler::ModuleRecord;use deno_ast::swc::common::Span;use deno_core::error::AnyError;use deno_core::parking_lot::RwLock;use deno_core::serde::Deserialize;use deno_core::serde::Deserializer;use deno_core::serde::Serialize;use deno_core::serde::Serializer;use deno_core::serde_json;use deno_core::serde_json::json;use deno_core::ModuleSpecifier;use deno_graph::MediaType;use deno_graph::ModuleGraph;use deno_graph::ModuleGraphError;use deno_graph::ModuleKind;use deno_graph::ResolutionError;use std::collections::HashMap;use std::collections::HashSet;use std::fmt;use std::result;use std::sync::Arc;use std::time::Instant;
#[derive(Debug, Clone, PartialEq)]pub struct SpecifierEmitCacheData { pub source_hash: String, pub text: String, pub map: Option<String>,}
pub trait EmitCache { fn get_emit_data( &self, specifier: &ModuleSpecifier, ) -> Option<SpecifierEmitCacheData>; fn get_source_hash(&self, specifier: &ModuleSpecifier) -> Option<String>; fn get_emit_text(&self, specifier: &ModuleSpecifier) -> Option<String>; fn set_emit_data( &self, specifier: ModuleSpecifier, data: SpecifierEmitCacheData, ) -> Result<(), AnyError>; fn get_tsbuildinfo(&self, specifier: &ModuleSpecifier) -> Option<String>; fn set_tsbuildinfo( &self, specifier: ModuleSpecifier, text: String, ) -> Result<(), AnyError>;}
impl<T: Cacher> EmitCache for T { fn get_emit_data( &self, specifier: &ModuleSpecifier, ) -> Option<SpecifierEmitCacheData> { Some(SpecifierEmitCacheData { source_hash: self.get_source_hash(specifier)?, text: self.get_emit_text(specifier)?, map: self.get(CacheType::SourceMap, specifier), }) }
fn get_source_hash(&self, specifier: &ModuleSpecifier) -> Option<String> { self.get(CacheType::Version, specifier) }
fn get_emit_text(&self, specifier: &ModuleSpecifier) -> Option<String> { self.get(CacheType::Emit, specifier) }
fn set_emit_data( &self, specifier: ModuleSpecifier, data: SpecifierEmitCacheData, ) -> Result<(), AnyError> { self.set(CacheType::Version, &specifier, data.source_hash)?; self.set(CacheType::Emit, &specifier, data.text)?; if let Some(map) = data.map { self.set(CacheType::SourceMap, &specifier, map)?; } Ok(()) }
fn get_tsbuildinfo(&self, specifier: &ModuleSpecifier) -> Option<String> { self.get(CacheType::TypeScriptBuildInfo, specifier) }
fn set_tsbuildinfo( &self, specifier: ModuleSpecifier, text: String, ) -> Result<(), AnyError> { self.set(CacheType::TypeScriptBuildInfo, &specifier, text) }}
#[derive(Clone, Debug, Default, Eq, PartialEq)]pub struct Stats(pub Vec<(String, u32)>);
impl<'de> Deserialize<'de> for Stats { fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error> where D: Deserializer<'de>, { let items: Vec<(String, u32)> = Deserialize::deserialize(deserializer)?; Ok(Stats(items)) }}
impl Serialize for Stats { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { Serialize::serialize(&self.0, serializer) }}
impl fmt::Display for Stats { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Compilation statistics:")?; for (key, value) in self.0.clone() { writeln!(f, " {}: {}", key, value)?; }
Ok(()) }}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]pub enum TsTypeLib { DenoWindow, DenoWorker, UnstableDenoWindow, UnstableDenoWorker,}
impl Default for TsTypeLib { fn default() -> Self { Self::DenoWindow }}
impl Serialize for TsTypeLib { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { let value = match self { Self::DenoWindow => vec!["deno.window".to_string()], Self::DenoWorker => vec!["deno.worker".to_string()], Self::UnstableDenoWindow => { vec!["deno.window".to_string(), "deno.unstable".to_string()] } Self::UnstableDenoWorker => { vec!["deno.worker".to_string(), "deno.unstable".to_string()] } }; Serialize::serialize(&value, serializer) }}
pub enum TsConfigType { Bundle, Check { lib: TsTypeLib, tsc_emit: bool }, Emit,}
pub struct TsConfigWithIgnoredOptions { pub ts_config: TsConfig, pub maybe_ignored_options: Option<IgnoredCompilerOptions>,}
pub fn get_ts_config_for_emit( config_type: TsConfigType, maybe_config_file: Option<&ConfigFile>,) -> Result<TsConfigWithIgnoredOptions, AnyError> { let mut ts_config = match config_type { TsConfigType::Bundle => TsConfig::new(json!({ "checkJs": false, "emitDecoratorMetadata": false, "importsNotUsedAsValues": "remove", "inlineSourceMap": false, "inlineSources": false, "sourceMap": false, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", })), TsConfigType::Check { tsc_emit, lib } => { let mut ts_config = TsConfig::new(json!({ "allowJs": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "incremental": true, "jsx": "react", "isolatedModules": true, "lib": lib, "module": "esnext", "resolveJsonModule": true, "strict": true, "target": "esnext", "tsBuildInfoFile": "deno:///.tsbuildinfo", "useDefineForClassFields": true, "useUnknownInCatchVariables": false, })); if tsc_emit { ts_config.merge(&json!({ "emitDecoratorMetadata": false, "importsNotUsedAsValues": "remove", "inlineSourceMap": true, "inlineSources": true, "outDir": "deno://", "removeComments": true, })); } else { ts_config.merge(&json!({ "noEmit": true, })); } ts_config } TsConfigType::Emit => TsConfig::new(json!({ "checkJs": false, "emitDecoratorMetadata": false, "importsNotUsedAsValues": "remove", "inlineSourceMap": true, "inlineSources": true, "sourceMap": false, "jsx": "react", "jsxFactory": "React.createElement", "jsxFragmentFactory": "React.Fragment", "resolveJsonModule": true, })), }; let maybe_ignored_options = ts_config.merge_tsconfig_from_config_file(maybe_config_file)?; ts_config.merge(&json!({ "moduleDetection": "force", })); Ok(TsConfigWithIgnoredOptions { ts_config, maybe_ignored_options, })}
fn get_tsc_roots( roots: &[(ModuleSpecifier, ModuleKind)], graph_data: &GraphData, check_js: bool,) -> Vec<(ModuleSpecifier, MediaType)> { if !check_js { graph_data .entries() .into_iter() .filter_map(|(specifier, module_entry)| match module_entry { ModuleEntry::Module { media_type, ts_check, .. } => match &media_type { MediaType::TypeScript | MediaType::Tsx | MediaType::Mts | MediaType::Cts | MediaType::Jsx => Some((specifier.clone(), *media_type)), MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs if check_js || *ts_check => { Some((specifier.clone(), *media_type)) } _ => None, }, _ => None, }) .collect() } else { roots .iter() .filter_map(|(specifier, _)| match graph_data.get(specifier) { Some(ModuleEntry::Module { media_type, .. }) => { Some((specifier.clone(), *media_type)) } _ => None, }) .collect() }}
fn get_version(source_bytes: &[u8], config_bytes: &[u8]) -> String { crate::checksum::gen(&[ source_bytes, version::deno().as_bytes(), config_bytes, ])}
pub fn is_emittable( kind: &ModuleKind, media_type: &MediaType, include_js: bool,) -> bool { if matches!(kind, ModuleKind::Synthetic) { return false; } match &media_type { MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Tsx | MediaType::Jsx => true, MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => include_js, _ => false, }}
pub struct CheckOptions { pub type_check_mode: TypeCheckMode, pub debug: bool, pub emit_with_diagnostics: bool, pub maybe_config_specifier: Option<ModuleSpecifier>, pub ts_config: TsConfig, pub log_checks: bool, pub reload: bool, pub reload_exclusions: HashSet<ModuleSpecifier>,}
#[derive(Debug, Default)]pub struct CheckEmitResult { pub diagnostics: Diagnostics, pub stats: Stats,}
pub fn check_and_maybe_emit( roots: &[(ModuleSpecifier, ModuleKind)], graph_data: Arc<RwLock<GraphData>>, cache: &dyn EmitCache, options: CheckOptions,) -> Result<CheckEmitResult, AnyError> { let check_js = options.ts_config.get_check_js(); let segment_graph_data = { let graph_data = graph_data.read(); graph_data.graph_segment(roots).unwrap() }; if valid_emit( &segment_graph_data, cache, &options.ts_config, options.reload, &options.reload_exclusions, ) { return Ok(Default::default()); } let root_names = get_tsc_roots(roots, &segment_graph_data, check_js); if options.log_checks { for (root, _) in roots { let root_str = root.to_string(); if !root_str.contains("$deno") { log::info!("{} {}", colors::green("Check"), root); } } } let maybe_tsbuildinfo = if options.reload { None } else { cache.get_tsbuildinfo(&roots[0].0) }; let hash_data = vec![ options.ts_config.as_bytes(), version::deno().as_bytes().to_owned(), ]; let config_bytes = options.ts_config.as_bytes();
let response = tsc::exec(tsc::Request { config: options.ts_config, debug: options.debug, graph_data: graph_data.clone(), hash_data, maybe_config_specifier: options.maybe_config_specifier, maybe_tsbuildinfo, root_names, })?;
let diagnostics = if options.type_check_mode == TypeCheckMode::Local { response.diagnostics.filter(|d| { if let Some(file_name) = &d.file_name { !file_name.starts_with("http") } else { true } }) } else { response.diagnostics };
if (diagnostics.is_empty() || options.emit_with_diagnostics) && !response.emitted_files.is_empty() { if let Some(info) = &response.maybe_tsbuildinfo { for (root, _) in roots { cache.set_tsbuildinfo(root.clone(), info.to_string())?; } }
struct SpecifierEmitData { pub version_hash: String, pub text: Option<String>, pub map: Option<String>, }
impl SpecifierEmitData { fn into_cache_data(self) -> Option<SpecifierEmitCacheData> { self.text.map(|text| SpecifierEmitCacheData { source_hash: self.version_hash, text, map: self.map, }) } }
let mut emit_data_items: HashMap<ModuleSpecifier, SpecifierEmitData> = HashMap::with_capacity(response.emitted_files.len()); for emit in response.emitted_files.into_iter() { if let Some(specifiers) = emit.maybe_specifiers { assert!(specifiers.len() == 1); let graph_data = graph_data.read(); let specifier = graph_data.follow_redirect(&specifiers[0]); let (source_bytes, media_type, ts_check) = match graph_data.get(&specifier) { Some(ModuleEntry::Module { code, media_type, ts_check, .. }) => (code.as_bytes(), *media_type, *ts_check), _ => { log::debug!("skipping emit for {}", specifier); continue; } }; if matches!(media_type, MediaType::Json) || (!check_js && !ts_check && matches!( media_type, MediaType::JavaScript | MediaType::Cjs | MediaType::Mjs )) { log::debug!("skipping emit for {}", specifier); continue; }
let mut emit_data_item = emit_data_items .entry(specifier.clone()) .or_insert_with(|| SpecifierEmitData { version_hash: get_version(source_bytes, &config_bytes), text: None, map: None, });
match emit.media_type { MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => { emit_data_item.text = Some(emit.data); } MediaType::SourceMap => { emit_data_item.map = Some(emit.data); } _ => unreachable!( "unexpected media_type {} {}", emit.media_type, specifier ), } } }
for (specifier, data) in emit_data_items.into_iter() { if let Some(cache_data) = data.into_cache_data() { cache.set_emit_data(specifier, cache_data)?; } } }
Ok(CheckEmitResult { diagnostics, stats: response.stats, })}
pub struct EmitOptions { pub ts_config: TsConfig, pub reload: bool, pub reload_exclusions: HashSet<ModuleSpecifier>,}
pub fn emit( graph: &ModuleGraph, cache: &dyn EmitCache, options: EmitOptions,) -> Result<CheckEmitResult, AnyError> { let start = Instant::now(); let config_bytes = options.ts_config.as_bytes(); let include_js = options.ts_config.get_check_js(); let emit_options = options.ts_config.into();
let mut emit_count = 0_u32; let mut file_count = 0_u32; for module in graph.modules() { file_count += 1; if !is_emittable(&module.kind, &module.media_type, include_js) { continue; } let needs_reload = options.reload && !options.reload_exclusions.contains(&module.specifier); let version = get_version( module.maybe_source.as_ref().map(|s| s.as_bytes()).unwrap(), &config_bytes, ); let is_valid = cache .get_source_hash(&module.specifier) .map_or(false, |v| v == version); if is_valid && !needs_reload { continue; } let transpiled_source = module .maybe_parsed_source .as_ref() .map(|ps| ps.transpile(&emit_options)) .unwrap()?; emit_count += 1; cache.set_emit_data( module.specifier.clone(), SpecifierEmitCacheData { source_hash: version, text: transpiled_source.text, map: transpiled_source.source_map, }, )?; }
let stats = Stats(vec![ ("Files".to_string(), file_count), ("Emitted".to_string(), emit_count), ("Total time".to_string(), start.elapsed().as_millis() as u32), ]);
Ok(CheckEmitResult { diagnostics: Diagnostics::default(), stats, })}
fn valid_emit( graph_data: &GraphData, cache: &dyn EmitCache, ts_config: &TsConfig, reload: bool, reload_exclusions: &HashSet<ModuleSpecifier>,) -> bool { let config_bytes = ts_config.as_bytes(); let check_js = ts_config.get_check_js(); for (specifier, module_entry) in graph_data.entries() { if let ModuleEntry::Module { code, media_type, ts_check, .. } = module_entry { match media_type { MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Tsx | MediaType::Jsx => {} MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => { if !check_js && !ts_check { continue; } } MediaType::Json | MediaType::TsBuildInfo | MediaType::SourceMap | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Wasm | MediaType::Unknown => continue, } if reload && !reload_exclusions.contains(specifier) { return false; } if let Some(source_hash) = cache.get_source_hash(specifier) { if source_hash != get_version(code.as_bytes(), &config_bytes) { return false; } } else { return false; } } } true}
#[derive(Debug)]pub struct GraphError(pub ModuleGraphError);
impl std::error::Error for GraphError {}
impl From<ModuleGraphError> for GraphError { fn from(err: ModuleGraphError) -> Self { Self(err) }}
impl fmt::Display for GraphError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { ModuleGraphError::ResolutionError(err) => { if matches!( err, ResolutionError::InvalidDowngrade { .. } | ResolutionError::InvalidLocalImport { .. } ) { write!(f, "{}", err.to_string_with_range()) } else { self.0.fmt(f) } } _ => self.0.fmt(f), } }}
pub struct BundleHook;
impl Hook for BundleHook { fn get_import_meta_props( &self, span: Span, module_record: &ModuleRecord, ) -> Result<Vec<deno_ast::swc::ast::KeyValueProp>, AnyError> { use deno_ast::swc::ast;
Ok(vec![ ast::KeyValueProp { key: ast::PropName::Ident(ast::Ident::new("url".into(), span)), value: Box::new(ast::Expr::Lit(ast::Lit::Str(ast::Str { span, value: module_record.file_name.to_string().into(), raw: None, }))), }, ast::KeyValueProp { key: ast::PropName::Ident(ast::Ident::new("main".into(), span)), value: Box::new(if module_record.is_entry { ast::Expr::Member(ast::MemberExpr { span, obj: Box::new(ast::Expr::MetaProp(ast::MetaPropExpr { span, kind: ast::MetaPropKind::ImportMeta, })), prop: ast::MemberProp::Ident(ast::Ident::new("main".into(), span)), }) } else { ast::Expr::Lit(ast::Lit::Bool(ast::Bool { span, value: false })) }), }, ]) }}
impl From<TsConfig> for deno_ast::EmitOptions { fn from(config: TsConfig) -> Self { let options: EmitConfigOptions = serde_json::from_value(config.0).unwrap(); let imports_not_used_as_values = match options.imports_not_used_as_values.as_str() { "preserve" => deno_ast::ImportsNotUsedAsValues::Preserve, "error" => deno_ast::ImportsNotUsedAsValues::Error, _ => deno_ast::ImportsNotUsedAsValues::Remove, }; let (transform_jsx, jsx_automatic, jsx_development) = match options.jsx.as_str() { "react" => (true, false, false), "react-jsx" => (true, true, false), "react-jsxdev" => (true, true, true), _ => (false, false, false), }; deno_ast::EmitOptions { emit_metadata: options.emit_decorator_metadata, imports_not_used_as_values, inline_source_map: options.inline_source_map, inline_sources: options.inline_sources, source_map: options.source_map, jsx_automatic, jsx_development, jsx_factory: options.jsx_factory, jsx_fragment_factory: options.jsx_fragment_factory, jsx_import_source: options.jsx_import_source, transform_jsx, var_decl_imports: false, } }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_is_emittable() { assert!(is_emittable( &ModuleKind::Esm, &MediaType::TypeScript, false )); assert!(!is_emittable( &ModuleKind::Synthetic, &MediaType::TypeScript, false )); assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Dts, false)); assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Dcts, false)); assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Dmts, false)); assert!(is_emittable(&ModuleKind::Esm, &MediaType::Tsx, false)); assert!(!is_emittable( &ModuleKind::Esm, &MediaType::JavaScript, false )); assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Cjs, false)); assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Mjs, false)); assert!(is_emittable(&ModuleKind::Esm, &MediaType::JavaScript, true)); assert!(is_emittable(&ModuleKind::Esm, &MediaType::Jsx, false)); assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Json, false)); }}