diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2021-10-11 08:26:22 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-11 08:26:22 +1100 |
commit | a7baf5f2bbb50dc0cb571de141b800b9155faca7 (patch) | |
tree | 4bebaabd1d3ed4595e8a388e0fae559bb5558974 /cli/emit.rs | |
parent | 5a8a989b7815023f33a1e3183a55cc8999af5d98 (diff) |
refactor: integrate deno_graph into CLI (#12369)
Diffstat (limited to 'cli/emit.rs')
-rw-r--r-- | cli/emit.rs | 927 |
1 files changed, 927 insertions, 0 deletions
diff --git a/cli/emit.rs b/cli/emit.rs new file mode 100644 index 000000000..5a1cf61d1 --- /dev/null +++ b/cli/emit.rs @@ -0,0 +1,927 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +//! The collection of APIs to be able to take `deno_graph` module graphs and +//! populate a cache, emit files, and transform a graph into the structures for +//! loading into an isolate. + +use crate::ast; +use crate::cache::CacheType; +use crate::cache::Cacher; +use crate::colors; +use crate::config_file::ConfigFile; +use crate::config_file::IgnoredCompilerOptions; +use crate::config_file::TsConfig; +use crate::diagnostics::Diagnostics; +use crate::tsc; +use crate::version; + +use deno_ast::swc; +use deno_core::error::anyhow; +use deno_core::error::custom_error; +use deno_core::error::AnyError; +use deno_core::error::Context; +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::json; +use deno_core::serde_json::Value; +use deno_core::ModuleSource; +use deno_core::ModuleSpecifier; +use deno_graph::MediaType; +use deno_graph::ModuleGraph; +use deno_graph::ModuleGraphError; +use deno_graph::ResolutionError; +use std::collections::HashMap; +use std::collections::HashSet; +use std::fmt; +use std::rc::Rc; +use std::result; +use std::sync::Arc; +use std::time::Instant; + +/// 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(crate) enum TypeLib { + DenoWindow, + DenoWorker, + UnstableDenoWindow, + UnstableDenoWorker, +} + +impl Default for TypeLib { + fn default() -> Self { + Self::DenoWindow + } +} + +impl Serialize for TypeLib { + 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) + } +} + +type Modules = HashMap<ModuleSpecifier, Result<ModuleSource, AnyError>>; + +/// A structure representing stats from an emit operation for a graph. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub(crate) 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(()) + } +} + +/// An enum that represents the base tsc configuration to return. +pub(crate) enum ConfigType { + /// Return a configuration for bundling, using swc to emit the bundle. This is + /// independent of type checking. + Bundle, + /// Return a configuration to use tsc to type check and optionally emit. This + /// is independent of either bundling or just emitting via swc + Check { lib: TypeLib, tsc_emit: bool }, + /// Return a configuration to use swc to emit single module files. + Emit, + /// Return a configuration as a base for the runtime `Deno.emit()` API. + RuntimeEmit { tsc_emit: bool }, +} + +/// For a given configuration type and optionally a configuration file, return a +/// tuple of the resulting `TsConfig` struct and optionally any user +/// configuration options that were ignored. +pub(crate) fn get_ts_config( + config_type: ConfigType, + maybe_config_file: Option<&ConfigFile>, + maybe_user_config: Option<&HashMap<String, Value>>, +) -> Result<(TsConfig, Option<IgnoredCompilerOptions>), AnyError> { + let mut ts_config = match config_type { + ConfigType::Bundle => TsConfig::new(json!({ + "checkJs": false, + "emitDecoratorMetadata": false, + "importsNotUsedAsValues": "remove", + "inlineSourceMap": false, + "inlineSources": false, + "sourceMap": false, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + })), + ConfigType::Check { tsc_emit, lib } => { + let mut ts_config = TsConfig::new(json!({ + "allowJs": true, + "experimentalDecorators": true, + "incremental": true, + "jsx": "react", + "isolatedModules": true, + "lib": lib, + "module": "esnext", + "strict": true, + "target": "esnext", + "tsBuildInfoFile": "deno:///.tsbuildinfo", + "useDefineForClassFields": true, + // TODO(@kitsonk) remove for Deno 2.0 + "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 + } + ConfigType::Emit => TsConfig::new(json!({ + "checkJs": false, + "emitDecoratorMetadata": false, + "importsNotUsedAsValues": "remove", + "inlineSourceMap": true, + // TODO(@kitsonk) make this actually work when https://github.com/swc-project/swc/issues/2218 addressed. + "inlineSources": true, + "sourceMap": false, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + })), + ConfigType::RuntimeEmit { tsc_emit } => { + let mut ts_config = TsConfig::new(json!({ + "allowJs": true, + "checkJs": false, + "emitDecoratorMetadata": false, + "experimentalDecorators": true, + "importsNotUsedAsValues": "remove", + "incremental": true, + "isolatedModules": true, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + "lib": TypeLib::DenoWindow, + "module": "esnext", + "removeComments": true, + "inlineSourceMap": false, + "inlineSources": false, + "sourceMap": true, + "strict": true, + "target": "esnext", + "tsBuildInfoFile": "deno:///.tsbuildinfo", + "useDefineForClassFields": true, + // TODO(@kitsonk) remove for Deno 2.0 + "useUnknownInCatchVariables": false, + })); + if tsc_emit { + ts_config.merge(&json!({ + "importsNotUsedAsValues": "remove", + "outDir": "deno://", + })); + } else { + ts_config.merge(&json!({ + "noEmit": true, + })); + } + ts_config + } + }; + let maybe_ignored_options = if let Some(user_options) = maybe_user_config { + ts_config.merge_user_config(user_options)? + } else { + ts_config.merge_tsconfig_from_config_file(maybe_config_file)? + }; + Ok((ts_config, maybe_ignored_options)) +} + +/// Transform the graph into root specifiers that we can feed `tsc`. We have to +/// provide the media type for root modules because `tsc` does not "resolve" the +/// media type like other modules, as well as a root specifier needs any +/// redirects resolved. If we aren't checking JavaScript, we need to include all +/// the emittable files in the roots, so they get type checked and optionally +/// emitted, otherwise they would be ignored if only imported into JavaScript. +fn get_root_names( + graph: &ModuleGraph, + check_js: bool, +) -> Vec<(ModuleSpecifier, MediaType)> { + if !check_js { + graph + .specifiers() + .into_iter() + .filter_map(|(_, r)| match r { + Ok((s, mt)) => match &mt { + MediaType::TypeScript | MediaType::Tsx | MediaType::Jsx => { + Some((s, mt)) + } + _ => None, + }, + _ => None, + }) + .collect() + } else { + graph + .roots + .iter() + .filter_map(|s| graph.get(s).map(|m| (m.specifier.clone(), m.media_type))) + .collect() + } +} + +/// A hashing function that takes the source code, version and optionally a +/// user provided config and generates a string hash which can be stored to +/// determine if the cached emit is valid or not. +fn get_version(source_bytes: &[u8], config_bytes: &[u8]) -> String { + crate::checksum::gen(&[ + source_bytes, + version::deno().as_bytes(), + config_bytes, + ]) +} + +/// Determine if a given media type is emittable or not. +fn is_emittable(media_type: &MediaType, include_js: bool) -> bool { + match &media_type { + MediaType::TypeScript | MediaType::Tsx | MediaType::Jsx => true, + MediaType::JavaScript => include_js, + _ => false, + } +} + +/// Options for performing a check of a module graph. Note that the decision to +/// emit or not is determined by the `ts_config` settings. +pub(crate) struct CheckOptions { + /// Set the debug flag on the TypeScript type checker. + pub debug: bool, + /// If true, any files emitted will be cached, even if there are diagnostics + /// produced. If false, if there are diagnostics, caching emitted files will + /// be skipped. + pub emit_with_diagnostics: bool, + /// The module specifier to the configuration file, passed to tsc so that + /// configuration related diagnostics are properly formed. + pub maybe_config_specifier: Option<ModuleSpecifier>, + /// The derived tsconfig that should be used when checking. + pub ts_config: TsConfig, +} + +/// The result of a check or emit of a module graph. Note that the actual +/// emitted sources are stored in the cache and are not returned in the result. +#[derive(Debug, Default)] +pub(crate) struct CheckEmitResult { + pub diagnostics: Diagnostics, + pub stats: Stats, +} + +/// Given a module graph, type check the module graph and optionally emit +/// modules, updating the cache as appropriate. Emitting is determined by the +/// `ts_config` supplied in the options, and if emitting, the files are stored +/// in the cache. +/// +/// It is expected that it is determined if a check and/or emit is validated +/// before the function is called. +pub(crate) fn check_and_maybe_emit( + graph: Arc<ModuleGraph>, + cache: &mut dyn Cacher, + options: CheckOptions, +) -> Result<CheckEmitResult, AnyError> { + let check_js = options.ts_config.get_check_js(); + let root_names = get_root_names(&graph, check_js); + // while there might be multiple roots, we can't "merge" the build info, so we + // try to retrieve the build info for first root, which is the most common use + // case. + let maybe_tsbuildinfo = + cache.get(CacheType::TypeScriptBuildInfo, &graph.roots[0]); + // to make tsc build info work, we need to consistently hash modules, so that + // tsc can better determine if an emit is still valid or not, so we provide + // that data here. + 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: graph.clone(), + hash_data, + maybe_config_specifier: options.maybe_config_specifier, + maybe_tsbuildinfo, + root_names, + })?; + + if let Some(info) = &response.maybe_tsbuildinfo { + // while we retrieve the build info for just the first module, it can be + // used for all the roots in the graph, so we will cache it for all roots + for root in &graph.roots { + cache.set(CacheType::TypeScriptBuildInfo, root, info.clone())?; + } + } + // sometimes we want to emit when there are diagnostics, and sometimes we + // don't. tsc will always return an emit if there are diagnostics + if (response.diagnostics.is_empty() || options.emit_with_diagnostics) + && !response.emitted_files.is_empty() + { + for emit in response.emitted_files.into_iter() { + if let Some(specifiers) = emit.maybe_specifiers { + assert!(specifiers.len() == 1); + // The emitted specifier might not be the file specifier we want, so we + // resolve it via the graph. + let specifier = graph.resolve(&specifiers[0]); + let (media_type, source) = if let Some(module) = graph.get(&specifier) { + (&module.media_type, module.source.clone()) + } else { + log::debug!("module missing, skipping emit for {}", specifier); + continue; + }; + // Sometimes if `tsc` sees a CommonJS file it will _helpfully_ output it + // to ESM, which we don't really want to do unless someone has enabled + // check_js. + if !check_js && *media_type == MediaType::JavaScript { + log::debug!("skipping emit for {}", specifier); + continue; + } + match emit.media_type { + MediaType::JavaScript => { + let version = get_version(source.as_bytes(), &config_bytes); + cache.set(CacheType::Version, &specifier, version)?; + cache.set(CacheType::Emit, &specifier, emit.data)?; + } + MediaType::SourceMap => { + cache.set(CacheType::SourceMap, &specifier, emit.data)?; + } + // this only occurs with the runtime emit, but we are using the same + // code paths, so we handle it here. + MediaType::Dts => { + cache.set(CacheType::Declaration, &specifier, emit.data)?; + } + _ => unreachable!(), + } + } + } + } + + Ok(CheckEmitResult { + diagnostics: response.diagnostics, + stats: response.stats, + }) +} + +pub(crate) enum BundleType { + /// Return the emitted contents of the program as a single "flattened" ES + /// module. + Module, + /// Return the emitted contents of the program as a single script that + /// executes the program using an immediately invoked function execution + /// (IIFE). + Classic, +} + +impl From<BundleType> for swc::bundler::ModuleType { + fn from(bundle_type: BundleType) -> Self { + match bundle_type { + BundleType::Classic => Self::Iife, + BundleType::Module => Self::Es, + } + } +} + +pub(crate) struct BundleOptions { + pub bundle_type: BundleType, + pub ts_config: TsConfig, +} + +/// A module loader for swc which does the appropriate retrieval and transpiling +/// of modules from the graph. +struct BundleLoader<'a> { + cm: Rc<swc::common::SourceMap>, + emit_options: &'a ast::EmitOptions, + globals: &'a deno_ast::swc::common::Globals, + graph: &'a ModuleGraph, +} + +impl swc::bundler::Load for BundleLoader<'_> { + fn load( + &self, + file_name: &swc::common::FileName, + ) -> Result<swc::bundler::ModuleData, AnyError> { + match file_name { + swc::common::FileName::Url(specifier) => { + if let Some(m) = self.graph.get(specifier) { + let (fm, module) = ast::transpile_module( + specifier, + &m.source, + m.media_type, + self.emit_options, + self.globals, + self.cm.clone(), + )?; + Ok(swc::bundler::ModuleData { + fm, + module, + helpers: Default::default(), + }) + } else { + Err(anyhow!( + "Module \"{}\" unexpectedly missing when bundling.", + specifier + )) + } + } + _ => unreachable!( + "Received a request for unsupported filename {:?}", + file_name + ), + } + } +} + +/// A resolver implementation for swc that resolves specifiers from the graph. +struct BundleResolver<'a>(&'a ModuleGraph); + +impl swc::bundler::Resolve for BundleResolver<'_> { + fn resolve( + &self, + referrer: &swc::common::FileName, + specifier: &str, + ) -> Result<swc::common::FileName, AnyError> { + let referrer = if let swc::common::FileName::Url(referrer) = referrer { + referrer + } else { + unreachable!( + "An unexpected referrer was passed when bundling: {:?}", + referrer + ); + }; + if let Some(specifier) = + self.0.resolve_dependency(specifier, referrer, false) + { + Ok(deno_ast::swc::common::FileName::Url(specifier.clone())) + } else { + Err(anyhow!( + "Cannot resolve \"{}\" from \"{}\".", + specifier, + referrer + )) + } + } +} + +/// Given a module graph, generate and return a bundle of the graph and +/// optionally its source map. Unlike emitting with `check_and_maybe_emit` and +/// `emit`, which store the emitted modules in the cache, this function simply +/// returns the output. +pub(crate) fn bundle( + graph: &ModuleGraph, + options: BundleOptions, +) -> Result<(String, Option<String>), AnyError> { + let emit_options: ast::EmitOptions = options.ts_config.into(); + + let cm = Rc::new(swc::common::SourceMap::new( + swc::common::FilePathMapping::empty(), + )); + let globals = swc::common::Globals::new(); + let loader = BundleLoader { + graph, + emit_options: &emit_options, + globals: &globals, + cm: cm.clone(), + }; + let resolver = BundleResolver(graph); + let config = swc::bundler::Config { + module: options.bundle_type.into(), + ..Default::default() + }; + // This hook will rewrite the `import.meta` when bundling to give a consistent + // behavior between bundled and unbundled code. + let hook = Box::new(ast::BundleHook); + let bundler = swc::bundler::Bundler::new( + &globals, + cm.clone(), + loader, + resolver, + config, + hook, + ); + let mut entries = HashMap::new(); + entries.insert( + "bundle".to_string(), + swc::common::FileName::Url(graph.roots[0].clone()), + ); + let output = bundler + .bundle(entries) + .context("Unable to output during bundling.")?; + let mut buf = Vec::new(); + let mut srcmap = Vec::new(); + { + let cfg = swc::codegen::Config { minify: false }; + let wr = Box::new(swc::codegen::text_writer::JsWriter::new( + cm.clone(), + "\n", + &mut buf, + Some(&mut srcmap), + )); + let mut emitter = swc::codegen::Emitter { + cfg, + cm: cm.clone(), + comments: None, + wr, + }; + emitter + .emit_module(&output[0].module) + .context("Unable to emit during bundling.")?; + } + let mut code = + String::from_utf8(buf).context("Emitted code is an invalid string.")?; + let mut maybe_map: Option<String> = None; + { + let mut buf = Vec::new(); + cm.build_source_map_from(&mut srcmap, None) + .to_writer(&mut buf)?; + if emit_options.inline_source_map { + let encoded_map = format!( + "//# sourceMappingURL=data:application/json;base64,{}\n", + base64::encode(buf) + ); + code.push_str(&encoded_map); + } else if emit_options.source_map { + maybe_map = Some(String::from_utf8(buf)?); + } + } + + Ok((code, maybe_map)) +} + +pub(crate) struct EmitOptions { + pub ts_config: TsConfig, + pub reload_exclusions: HashSet<ModuleSpecifier>, + pub reload: bool, +} + +/// Given a module graph, emit any appropriate modules and cache them. +pub(crate) fn emit( + graph: &ModuleGraph, + cache: &mut dyn Cacher, + 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.media_type, include_js) { + continue; + } + let needs_reload = + options.reload && !options.reload_exclusions.contains(&module.specifier); + let version = get_version(module.source.as_bytes(), &config_bytes); + let is_valid = cache + .get(CacheType::Version, &module.specifier) + .map_or(false, |v| { + v == get_version(module.source.as_bytes(), &config_bytes) + }); + if is_valid && !needs_reload { + continue; + } + let (emit, maybe_map) = + ast::transpile(&module.parsed_source, &emit_options)?; + emit_count += 1; + cache.set(CacheType::Emit, &module.specifier, emit)?; + if let Some(map) = maybe_map { + cache.set(CacheType::SourceMap, &module.specifier, map)?; + } + if !is_valid { + cache.set(CacheType::Version, &module.specifier, version)?; + } + } + + 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, + }) +} + +/// Check the sub-resource integrity of a module graph, exiting if the graph is +/// not valid. +pub(crate) fn lock(graph: &ModuleGraph) { + if let Err(err) = graph.lock() { + log::error!("{} {}", colors::red("error:"), err); + std::process::exit(10); + } +} + +/// Check a module graph to determine if the graph contains anything that +/// is required to be emitted to be valid. It determines what modules in the +/// graph are emittable and for those that are emittable, if there is currently +/// a valid emit in the cache. +pub(crate) fn valid_emit( + graph: &ModuleGraph, + cache: &dyn Cacher, + ts_config: &TsConfig, + reload: bool, + reload_exclusions: &HashSet<ModuleSpecifier>, +) -> bool { + let config_bytes = ts_config.as_bytes(); + let emit_js = ts_config.get_check_js(); + graph + .specifiers() + .iter() + .filter(|(_, r)| match r { + Ok((_, MediaType::TypeScript)) + | Ok((_, MediaType::Tsx)) + | Ok((_, MediaType::Jsx)) => true, + Ok((_, MediaType::JavaScript)) => emit_js, + _ => false, + }) + .all(|(_, r)| { + if let Ok((s, _)) = r { + if reload && !reload_exclusions.contains(s) { + // we are reloading and the specifier isn't excluded from being + // reloaded + false + } else if let Some(version) = cache.get(CacheType::Version, s) { + if let Some(module) = graph.get(s) { + version == get_version(module.source.as_bytes(), &config_bytes) + } else { + // We have a source module in the graph we can't find, so the emit is + // clearly wrong + false + } + } else { + // A module that requires emitting doesn't have a version, so it doesn't + // have a valid emit + false + } + } else { + // Something in the module graph is missing, but that doesn't mean the + // emit is invalid + true + } + }) +} + +/// An adapter struct to make a deno_graph::ModuleGraphError display as expected +/// in the Deno CLI. +#[derive(Debug)] +pub(crate) 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_span()) + } else { + self.0.fmt(f) + } + } + _ => self.0.fmt(f), + } + } +} + +/// Convert a module graph to a map of "files", which are used by the runtime +/// emit to be passed back to the caller. +pub(crate) fn to_file_map( + graph: &ModuleGraph, + cache: &dyn Cacher, +) -> HashMap<String, String> { + let mut files = HashMap::new(); + for (_, result) in graph.specifiers().into_iter() { + if let Ok((specifier, media_type)) = result { + if let Some(emit) = cache.get(CacheType::Emit, &specifier) { + files.insert(format!("{}.js", specifier), emit); + if let Some(map) = cache.get(CacheType::SourceMap, &specifier) { + files.insert(format!("{}.js.map", specifier), map); + } + } else if media_type == MediaType::JavaScript + || media_type == MediaType::Unknown + { + if let Some(module) = graph.get(&specifier) { + files.insert(specifier.to_string(), module.source.to_string()); + } + } + if let Some(declaration) = cache.get(CacheType::Declaration, &specifier) { + files.insert(format!("{}.d.ts", specifier), declaration); + } + } + } + files +} + +/// Convert a module graph to a map of module sources, which are used by +/// `deno_core` to load modules into V8. +pub(crate) fn to_module_sources( + graph: &ModuleGraph, + cache: &dyn Cacher, +) -> Modules { + graph + .specifiers() + .into_iter() + .map(|(requested_specifier, r)| match r { + Err(err) => (requested_specifier, Err(err.into())), + Ok((found_specifier, media_type)) => { + // First we check to see if there is an emitted file in the cache. + if let Some(code) = cache.get(CacheType::Emit, &found_specifier) { + ( + requested_specifier.clone(), + Ok(ModuleSource { + code, + module_url_found: found_specifier.to_string(), + module_url_specified: requested_specifier.to_string(), + }), + ) + // Then if the file is JavaScript (or unknown) and wasn't emitted, we + // will load the original source code in the module. + } else if media_type == MediaType::JavaScript + || media_type == MediaType::Unknown + { + if let Some(module) = graph.get(&found_specifier) { + ( + requested_specifier.clone(), + Ok(ModuleSource { + code: module.source.as_str().to_string(), + module_url_found: module.specifier.to_string(), + module_url_specified: requested_specifier.to_string(), + }), + ) + } else { + unreachable!( + "unexpected module missing from graph: {}", + found_specifier + ) + } + // Otherwise we will add a not found error. + } else { + ( + requested_specifier.clone(), + Err(custom_error( + "NotFound", + format!( + "Emitted code for \"{}\" not found.", + requested_specifier + ), + )), + ) + } + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cache::MemoryCacher; + + #[test] + fn test_is_emittable() { + assert!(is_emittable(&MediaType::TypeScript, false)); + assert!(!is_emittable(&MediaType::Dts, false)); + assert!(is_emittable(&MediaType::Tsx, false)); + assert!(!is_emittable(&MediaType::JavaScript, false)); + assert!(is_emittable(&MediaType::JavaScript, true)); + assert!(is_emittable(&MediaType::Jsx, false)); + assert!(!is_emittable(&MediaType::Json, false)); + } + + async fn setup<S: AsRef<str>>( + root: S, + sources: Vec<(S, S)>, + ) -> (ModuleGraph, MemoryCacher) { + let roots = vec![ModuleSpecifier::parse(root.as_ref()).unwrap()]; + let sources = sources + .into_iter() + .map(|(s, c)| (s.as_ref().to_string(), Arc::new(c.as_ref().to_string()))) + .collect(); + let mut cache = MemoryCacher::new(sources); + let graph = deno_graph::create_graph( + roots, false, None, &mut cache, None, None, None, + ) + .await; + (graph, cache) + } + + #[tokio::test] + async fn test_to_module_sources_emitted() { + let (graph, mut cache) = setup( + "https://example.com/a.ts", + vec![("https://example.com/a.ts", r#"console.log("hello deno");"#)], + ) + .await; + let (ts_config, _) = get_ts_config(ConfigType::Emit, None, None).unwrap(); + emit( + &graph, + &mut cache, + EmitOptions { + ts_config, + reload_exclusions: HashSet::default(), + reload: false, + }, + ) + .unwrap(); + let modules = to_module_sources(&graph, &cache); + assert_eq!(modules.len(), 1); + let root = ModuleSpecifier::parse("https://example.com/a.ts").unwrap(); + let maybe_result = modules.get(&root); + assert!(maybe_result.is_some()); + let result = maybe_result.unwrap(); + assert!(result.is_ok()); + let module_source = result.as_ref().unwrap(); + assert!(module_source + .code + .starts_with(r#"console.log("hello deno");"#)); + } + + #[tokio::test] + async fn test_to_module_sources_not_emitted() { + let (graph, mut cache) = setup( + "https://example.com/a.js", + vec![("https://example.com/a.js", r#"console.log("hello deno");"#)], + ) + .await; + let (ts_config, _) = get_ts_config(ConfigType::Emit, None, None).unwrap(); + emit( + &graph, + &mut cache, + EmitOptions { + ts_config, + reload_exclusions: HashSet::default(), + reload: false, + }, + ) + .unwrap(); + let modules = to_module_sources(&graph, &cache); + assert_eq!(modules.len(), 1); + let root = ModuleSpecifier::parse("https://example.com/a.js").unwrap(); + let maybe_result = modules.get(&root); + assert!(maybe_result.is_some()); + let result = maybe_result.unwrap(); + assert!(result.is_ok()); + let module_source = result.as_ref().unwrap(); + assert_eq!(module_source.code, r#"console.log("hello deno");"#); + } +} |