diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2022-11-25 18:29:48 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-25 18:29:48 -0500 |
commit | dcb4ffb93a380710c32cc212b937ea38db5ceacc (patch) | |
tree | 18bf860912a14b84287bb8dbafdc41c5e3cdc6ab /cli/tsc/mod.rs | |
parent | 0cc90d9246ff2c392457632d5030eaca2ca1ca6f (diff) |
refactor: move dts files, diagnostics.rs, and tsc.rs to tsc folder (#16820)
Diffstat (limited to 'cli/tsc/mod.rs')
-rw-r--r-- | cli/tsc/mod.rs | 1226 |
1 files changed, 1226 insertions, 0 deletions
diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs new file mode 100644 index 000000000..a8cb7bcab --- /dev/null +++ b/cli/tsc/mod.rs @@ -0,0 +1,1226 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::args::TsConfig; +use crate::graph_util::GraphData; +use crate::graph_util::ModuleEntry; +use crate::node; +use crate::node::node_resolve_npm_reference; +use crate::node::NodeResolution; +use crate::node::NodeResolutionMode; +use crate::npm::NpmPackageReference; +use crate::npm::NpmPackageResolver; + +use deno_ast::MediaType; +use deno_core::anyhow::anyhow; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::located_script_name; +use deno_core::op; +use deno_core::parking_lot::RwLock; +use deno_core::resolve_url_or_path; +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::serde_json::Value; +use deno_core::Extension; +use deno_core::JsRuntime; +use deno_core::ModuleSpecifier; +use deno_core::OpState; +use deno_core::RuntimeOptions; +use deno_core::Snapshot; +use deno_graph::Resolved; +use once_cell::sync::Lazy; +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt; +use std::path::PathBuf; +use std::sync::Arc; + +mod diagnostics; + +pub use self::diagnostics::Diagnostic; +pub use self::diagnostics::DiagnosticCategory; +pub use self::diagnostics::DiagnosticMessageChain; +pub use self::diagnostics::Diagnostics; +pub use self::diagnostics::Position; + +// Declaration files + +pub static DENO_NS_LIB: &str = include_str!("dts/lib.deno.ns.d.ts"); +pub static DENO_CONSOLE_LIB: &str = include_str!(env!("DENO_CONSOLE_LIB_PATH")); +pub static DENO_URL_LIB: &str = include_str!(env!("DENO_URL_LIB_PATH")); +pub static DENO_WEB_LIB: &str = include_str!(env!("DENO_WEB_LIB_PATH")); +pub static DENO_FETCH_LIB: &str = include_str!(env!("DENO_FETCH_LIB_PATH")); +pub static DENO_WEBGPU_LIB: &str = include_str!(env!("DENO_WEBGPU_LIB_PATH")); +pub static DENO_WEBSOCKET_LIB: &str = + include_str!(env!("DENO_WEBSOCKET_LIB_PATH")); +pub static DENO_WEBSTORAGE_LIB: &str = + include_str!(env!("DENO_WEBSTORAGE_LIB_PATH")); +pub static DENO_CACHE_LIB: &str = include_str!(env!("DENO_CACHE_LIB_PATH")); +pub static DENO_CRYPTO_LIB: &str = include_str!(env!("DENO_CRYPTO_LIB_PATH")); +pub static DENO_BROADCAST_CHANNEL_LIB: &str = + include_str!(env!("DENO_BROADCAST_CHANNEL_LIB_PATH")); +pub static DENO_NET_LIB: &str = include_str!(env!("DENO_NET_LIB_PATH")); +pub static SHARED_GLOBALS_LIB: &str = + include_str!("dts/lib.deno.shared_globals.d.ts"); +pub static WINDOW_LIB: &str = include_str!("dts/lib.deno.window.d.ts"); +pub static UNSTABLE_NS_LIB: &str = include_str!("dts/lib.deno.unstable.d.ts"); + +pub static COMPILER_SNAPSHOT: Lazy<Box<[u8]>> = Lazy::new( + #[cold] + #[inline(never)] + || { + static COMPRESSED_COMPILER_SNAPSHOT: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/COMPILER_SNAPSHOT.bin")); + + zstd::bulk::decompress( + &COMPRESSED_COMPILER_SNAPSHOT[4..], + u32::from_le_bytes(COMPRESSED_COMPILER_SNAPSHOT[0..4].try_into().unwrap()) + as usize, + ) + .unwrap() + .into_boxed_slice() + }, +); + +pub fn compiler_snapshot() -> Snapshot { + Snapshot::Static(&COMPILER_SNAPSHOT) +} + +macro_rules! inc { + ($e:expr) => { + include_str!(concat!("./dts/", $e)) + }; +} + +/// Contains static assets that are not preloaded in the compiler snapshot. +pub static STATIC_ASSETS: Lazy<HashMap<&'static str, &'static str>> = + Lazy::new(|| { + ([ + ( + "lib.dom.asynciterable.d.ts", + inc!("lib.dom.asynciterable.d.ts"), + ), + ("lib.dom.d.ts", inc!("lib.dom.d.ts")), + ("lib.dom.extras.d.ts", inc!("lib.dom.extras.d.ts")), + ("lib.dom.iterable.d.ts", inc!("lib.dom.iterable.d.ts")), + ("lib.es6.d.ts", inc!("lib.es6.d.ts")), + ("lib.es2016.full.d.ts", inc!("lib.es2016.full.d.ts")), + ("lib.es2017.full.d.ts", inc!("lib.es2017.full.d.ts")), + ("lib.es2018.full.d.ts", inc!("lib.es2018.full.d.ts")), + ("lib.es2019.full.d.ts", inc!("lib.es2019.full.d.ts")), + ("lib.es2020.full.d.ts", inc!("lib.es2020.full.d.ts")), + ("lib.es2021.full.d.ts", inc!("lib.es2021.full.d.ts")), + ("lib.es2022.full.d.ts", inc!("lib.es2022.full.d.ts")), + ("lib.esnext.full.d.ts", inc!("lib.esnext.full.d.ts")), + ("lib.scripthost.d.ts", inc!("lib.scripthost.d.ts")), + ("lib.webworker.d.ts", inc!("lib.webworker.d.ts")), + ( + "lib.webworker.importscripts.d.ts", + inc!("lib.webworker.importscripts.d.ts"), + ), + ( + "lib.webworker.iterable.d.ts", + inc!("lib.webworker.iterable.d.ts"), + ), + ]) + .iter() + .cloned() + .collect() + }); + +/// A structure representing stats from a type check operation for a graph. +#[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<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(()) + } +} + +/// Retrieve a static asset that are included in the binary. +pub fn get_asset(asset: &str) -> Option<&'static str> { + STATIC_ASSETS.get(asset).map(|s| s.to_owned()) +} + +fn get_maybe_hash( + maybe_source: Option<&str>, + hash_data: &[Vec<u8>], +) -> Option<String> { + if let Some(source) = maybe_source { + let mut data = vec![source.as_bytes().to_owned()]; + data.extend_from_slice(hash_data); + Some(crate::checksum::gen(&data)) + } else { + None + } +} + +/// Hash the URL so it can be sent to `tsc` in a supportable way +fn hash_url(specifier: &ModuleSpecifier, media_type: MediaType) -> String { + let hash = crate::checksum::gen(&[specifier.path().as_bytes()]); + format!( + "{}:///{}{}", + specifier.scheme(), + hash, + media_type.as_ts_extension() + ) +} + +/// If the provided URLs derivable tsc media type doesn't match the media type, +/// we will add an extension to the output. This is to avoid issues with +/// specifiers that don't have extensions, that tsc refuses to emit because they +/// think a `.js` version exists, when it doesn't. +fn maybe_remap_specifier( + specifier: &ModuleSpecifier, + media_type: MediaType, +) -> Option<String> { + let path = if specifier.scheme() == "file" { + if let Ok(path) = specifier.to_file_path() { + path + } else { + PathBuf::from(specifier.path()) + } + } else { + PathBuf::from(specifier.path()) + }; + if path.extension().is_none() { + Some(format!("{}{}", specifier, media_type.as_ts_extension())) + } else { + None + } +} + +/// tsc only supports `.ts`, `.tsx`, `.d.ts`, `.js`, or `.jsx` as root modules +/// and so we have to detect the apparent media type based on extensions it +/// supports. +fn get_tsc_media_type(specifier: &ModuleSpecifier) -> MediaType { + let path = if specifier.scheme() == "file" { + if let Ok(path) = specifier.to_file_path() { + path + } else { + PathBuf::from(specifier.path()) + } + } else { + PathBuf::from(specifier.path()) + }; + match path.extension() { + None => MediaType::Unknown, + Some(os_str) => match os_str.to_str() { + Some("ts") => { + if let Some(os_str) = path.file_stem() { + if let Some(file_name) = os_str.to_str() { + if file_name.ends_with(".d") { + return MediaType::Dts; + } + } + } + MediaType::TypeScript + } + Some("mts") => { + if let Some(os_str) = path.file_stem() { + if let Some(file_name) = os_str.to_str() { + if file_name.ends_with(".d") { + return MediaType::Dmts; + } + } + } + MediaType::Mts + } + Some("cts") => { + if let Some(os_str) = path.file_stem() { + if let Some(file_name) = os_str.to_str() { + if file_name.ends_with(".d") { + return MediaType::Dcts; + } + } + } + MediaType::Cts + } + Some("tsx") => MediaType::Tsx, + Some("js") => MediaType::JavaScript, + Some("mjs") => MediaType::Mjs, + Some("cjs") => MediaType::Cjs, + Some("jsx") => MediaType::Jsx, + _ => MediaType::Unknown, + }, + } +} + +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct EmittedFile { + pub data: String, + pub maybe_specifiers: Option<Vec<ModuleSpecifier>>, + pub media_type: MediaType, +} + +/// A structure representing a request to be sent to the tsc runtime. +#[derive(Debug)] +pub struct Request { + /// The TypeScript compiler options which will be serialized and sent to + /// tsc. + pub config: TsConfig, + /// Indicates to the tsc runtime if debug logging should occur. + pub debug: bool, + pub graph_data: Arc<RwLock<GraphData>>, + pub hash_data: Vec<Vec<u8>>, + pub maybe_config_specifier: Option<ModuleSpecifier>, + pub maybe_npm_resolver: Option<NpmPackageResolver>, + pub maybe_tsbuildinfo: Option<String>, + /// A vector of strings that represent the root/entry point modules for the + /// program. + pub root_names: Vec<(ModuleSpecifier, MediaType)>, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Response { + /// Any diagnostics that have been returned from the checker. + pub diagnostics: Diagnostics, + /// If there was any build info associated with the exec request. + pub maybe_tsbuildinfo: Option<String>, + /// Statistics from the check. + pub stats: Stats, +} + +#[derive(Debug, Default)] +struct State { + hash_data: Vec<Vec<u8>>, + graph_data: Arc<RwLock<GraphData>>, + maybe_config_specifier: Option<ModuleSpecifier>, + maybe_tsbuildinfo: Option<String>, + maybe_response: Option<RespondArgs>, + maybe_npm_resolver: Option<NpmPackageResolver>, + remapped_specifiers: HashMap<String, ModuleSpecifier>, + root_map: HashMap<String, ModuleSpecifier>, +} + +impl State { + pub fn new( + graph_data: Arc<RwLock<GraphData>>, + hash_data: Vec<Vec<u8>>, + maybe_config_specifier: Option<ModuleSpecifier>, + maybe_npm_resolver: Option<NpmPackageResolver>, + maybe_tsbuildinfo: Option<String>, + root_map: HashMap<String, ModuleSpecifier>, + remapped_specifiers: HashMap<String, ModuleSpecifier>, + ) -> Self { + State { + hash_data, + graph_data, + maybe_config_specifier, + maybe_npm_resolver, + maybe_tsbuildinfo, + maybe_response: None, + remapped_specifiers, + root_map, + } + } +} + +fn normalize_specifier(specifier: &str) -> Result<ModuleSpecifier, AnyError> { + resolve_url_or_path(&specifier.replace(".d.ts.d.ts", ".d.ts")) + .map_err(|err| err.into()) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateHashArgs { + /// The string data to be used to generate the hash. This will be mixed with + /// other state data in Deno to derive the final hash. + data: String, +} + +#[op] +fn op_create_hash(s: &mut OpState, args: Value) -> Result<Value, AnyError> { + let state = s.borrow_mut::<State>(); + let v: CreateHashArgs = serde_json::from_value(args) + .context("Invalid request from JavaScript for \"op_create_hash\".")?; + let mut data = vec![v.data.as_bytes().to_owned()]; + data.extend_from_slice(&state.hash_data); + let hash = crate::checksum::gen(&data); + Ok(json!({ "hash": hash })) +} + +#[op] +fn op_cwd(s: &mut OpState) -> Result<String, AnyError> { + let state = s.borrow_mut::<State>(); + if let Some(config_specifier) = &state.maybe_config_specifier { + let cwd = config_specifier.join("./")?; + Ok(cwd.to_string()) + } else { + Ok("cache:///".to_string()) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct EmitArgs { + /// The text data/contents of the file. + data: String, + /// The _internal_ filename for the file. This will be used to determine how + /// the file is cached and stored. + file_name: String, +} + +#[op] +fn op_emit(state: &mut OpState, args: EmitArgs) -> bool { + let state = state.borrow_mut::<State>(); + match args.file_name.as_ref() { + "deno:///.tsbuildinfo" => state.maybe_tsbuildinfo = Some(args.data), + _ => { + if cfg!(debug_assertions) { + panic!("Unhandled emit write: {}", args.file_name); + } + } + } + + true +} + +#[derive(Debug, Deserialize)] +struct ExistsArgs { + /// The fully qualified specifier that should be loaded. + specifier: String, +} + +#[op] +fn op_exists(state: &mut OpState, args: ExistsArgs) -> bool { + let state = state.borrow_mut::<State>(); + let graph_data = state.graph_data.read(); + if let Ok(specifier) = normalize_specifier(&args.specifier) { + if specifier.scheme() == "asset" || specifier.scheme() == "data" { + true + } else { + matches!( + graph_data.get(&graph_data.follow_redirect(&specifier)), + Some(ModuleEntry::Module { .. }) + ) + } + } else { + false + } +} + +#[derive(Debug, Deserialize)] +struct LoadArgs { + /// The fully qualified specifier that should be loaded. + specifier: String, +} + +pub fn as_ts_script_kind(media_type: MediaType) -> i32 { + match media_type { + MediaType::JavaScript => 1, + MediaType::Jsx => 2, + MediaType::Mjs => 1, + MediaType::Cjs => 1, + MediaType::TypeScript => 3, + MediaType::Mts => 3, + MediaType::Cts => 3, + MediaType::Dts => 3, + MediaType::Dmts => 3, + MediaType::Dcts => 3, + MediaType::Tsx => 4, + MediaType::Json => 6, + MediaType::SourceMap + | MediaType::TsBuildInfo + | MediaType::Wasm + | MediaType::Unknown => 0, + } +} + +#[op] +fn op_load(state: &mut OpState, args: Value) -> Result<Value, AnyError> { + let state = state.borrow_mut::<State>(); + let v: LoadArgs = serde_json::from_value(args) + .context("Invalid request from JavaScript for \"op_load\".")?; + let specifier = normalize_specifier(&v.specifier) + .context("Error converting a string module specifier for \"op_load\".")?; + let mut hash: Option<String> = None; + let mut media_type = MediaType::Unknown; + let graph_data = state.graph_data.read(); + let data = if &v.specifier == "deno:///.tsbuildinfo" { + state.maybe_tsbuildinfo.as_deref().map(Cow::Borrowed) + // in certain situations we return a "blank" module to tsc and we need to + // handle the request for that module here. + } else if &v.specifier == "deno:///missing_dependency.d.ts" { + hash = Some("1".to_string()); + media_type = MediaType::Dts; + Some(Cow::Borrowed("declare const __: any;\nexport = __;\n")) + } else if v.specifier.starts_with("asset:///") { + let name = v.specifier.replace("asset:///", ""); + let maybe_source = get_asset(&name); + hash = get_maybe_hash(maybe_source, &state.hash_data); + media_type = MediaType::from(&v.specifier); + maybe_source.map(Cow::Borrowed) + } else { + let specifier = if let Some(remapped_specifier) = + state.remapped_specifiers.get(&v.specifier) + { + remapped_specifier.clone() + } else if let Some(remapped_specifier) = state.root_map.get(&v.specifier) { + remapped_specifier.clone() + } else { + specifier + }; + let maybe_source = if let Some(ModuleEntry::Module { + code, + media_type: mt, + .. + }) = + graph_data.get(&graph_data.follow_redirect(&specifier)) + { + media_type = *mt; + Some(Cow::Borrowed(code as &str)) + } else if state + .maybe_npm_resolver + .as_ref() + .map(|resolver| resolver.in_npm_package(&specifier)) + .unwrap_or(false) + { + media_type = MediaType::from(&specifier); + let file_path = specifier.to_file_path().unwrap(); + let code = std::fs::read_to_string(&file_path) + .with_context(|| format!("Unable to load {}", file_path.display()))?; + Some(Cow::Owned(code)) + } else { + media_type = MediaType::Unknown; + None + }; + hash = get_maybe_hash(maybe_source.as_deref(), &state.hash_data); + maybe_source + }; + + Ok(json!({ + "data": data, + "version": hash, + "scriptKind": as_ts_script_kind(media_type), + })) +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResolveArgs { + /// The base specifier that the supplied specifier strings should be resolved + /// relative to. + pub base: String, + /// A list of specifiers that should be resolved. + pub specifiers: Vec<String>, +} + +#[op] +fn op_resolve( + state: &mut OpState, + args: ResolveArgs, +) -> Result<Vec<(String, String)>, AnyError> { + let state = state.borrow_mut::<State>(); + let mut resolved: Vec<(String, String)> = Vec::new(); + let referrer = if let Some(remapped_specifier) = + state.remapped_specifiers.get(&args.base) + { + remapped_specifier.clone() + } else if let Some(remapped_base) = state.root_map.get(&args.base) { + remapped_base.clone() + } else { + normalize_specifier(&args.base).context( + "Error converting a string module specifier for \"op_resolve\".", + )? + }; + for specifier in &args.specifiers { + if specifier.starts_with("asset:///") { + resolved.push(( + specifier.clone(), + MediaType::from(specifier).as_ts_extension().to_string(), + )); + } else { + let graph_data = state.graph_data.read(); + let resolved_dep = match graph_data.get_dependencies(&referrer) { + Some(dependencies) => dependencies.get(specifier).map(|d| { + if matches!(d.maybe_type, Resolved::Ok { .. }) { + &d.maybe_type + } else { + &d.maybe_code + } + }), + None => None, + }; + let maybe_result = match resolved_dep { + Some(Resolved::Ok { specifier, .. }) => { + let specifier = graph_data.follow_redirect(specifier); + match graph_data.get(&specifier) { + Some(ModuleEntry::Module { + media_type, + maybe_types, + .. + }) => match maybe_types { + Some(Resolved::Ok { specifier, .. }) => { + let types = graph_data.follow_redirect(specifier); + match graph_data.get(&types) { + Some(ModuleEntry::Module { media_type, .. }) => { + Some((types, *media_type)) + } + _ => None, + } + } + _ => Some((specifier, *media_type)), + }, + _ => { + // handle npm:<package> urls + if let Ok(npm_ref) = + NpmPackageReference::from_specifier(&specifier) + { + if let Some(npm_resolver) = &state.maybe_npm_resolver { + Some(resolve_npm_package_reference_types( + &npm_ref, + npm_resolver, + )?) + } else { + None + } + } else { + None + } + } + } + } + _ => { + state.maybe_npm_resolver.as_ref().and_then(|npm_resolver| { + if npm_resolver.in_npm_package(&referrer) { + // we're in an npm package, so use node resolution + Some(NodeResolution::into_specifier_and_media_type( + node::node_resolve( + specifier, + &referrer, + node::NodeResolutionMode::Types, + npm_resolver, + ) + .ok() + .flatten(), + )) + } else { + None + } + }) + } + }; + let result = match maybe_result { + Some((specifier, media_type)) => { + let specifier_str = match specifier.scheme() { + "data" | "blob" => { + let specifier_str = hash_url(&specifier, media_type); + state + .remapped_specifiers + .insert(specifier_str.clone(), specifier); + specifier_str + } + _ => { + if let Some(specifier_str) = + maybe_remap_specifier(&specifier, media_type) + { + state + .remapped_specifiers + .insert(specifier_str.clone(), specifier); + specifier_str + } else { + specifier.to_string() + } + } + }; + (specifier_str, media_type.as_ts_extension().into()) + } + None => ( + "deno:///missing_dependency.d.ts".to_string(), + ".d.ts".to_string(), + ), + }; + resolved.push(result); + } + } + + Ok(resolved) +} + +pub fn resolve_npm_package_reference_types( + npm_ref: &NpmPackageReference, + npm_resolver: &NpmPackageResolver, +) -> Result<(ModuleSpecifier, MediaType), AnyError> { + let maybe_resolution = node_resolve_npm_reference( + npm_ref, + NodeResolutionMode::Types, + npm_resolver, + )?; + Ok(NodeResolution::into_specifier_and_media_type( + maybe_resolution, + )) +} + +#[op] +fn op_is_node_file(state: &mut OpState, path: String) -> bool { + let state = state.borrow::<State>(); + match ModuleSpecifier::parse(&path) { + Ok(specifier) => state + .maybe_npm_resolver + .as_ref() + .map(|r| r.in_npm_package(&specifier)) + .unwrap_or(false), + Err(_) => false, + } +} + +#[derive(Debug, Deserialize, Eq, PartialEq)] +struct RespondArgs { + pub diagnostics: Diagnostics, + pub stats: Stats, +} + +#[op] +fn op_respond(state: &mut OpState, args: Value) -> Result<Value, AnyError> { + let state = state.borrow_mut::<State>(); + let v: RespondArgs = serde_json::from_value(args) + .context("Error converting the result for \"op_respond\".")?; + state.maybe_response = Some(v); + Ok(json!(true)) +} + +/// Execute a request on the supplied snapshot, returning a response which +/// contains information, like any emitted files, diagnostics, statistics and +/// optionally an updated TypeScript build info. +pub fn exec(request: Request) -> Result<Response, AnyError> { + // tsc cannot handle root specifiers that don't have one of the "acceptable" + // extensions. Therefore, we have to check the root modules against their + // extensions and remap any that are unacceptable to tsc and add them to the + // op state so when requested, we can remap to the original specifier. + let mut root_map = HashMap::new(); + let mut remapped_specifiers = HashMap::new(); + let root_names: Vec<String> = request + .root_names + .iter() + .map(|(s, mt)| match s.scheme() { + "data" | "blob" => { + let specifier_str = hash_url(s, *mt); + remapped_specifiers.insert(specifier_str.clone(), s.clone()); + specifier_str + } + _ => { + let ext_media_type = get_tsc_media_type(s); + if *mt != ext_media_type { + let new_specifier = format!("{}{}", s, mt.as_ts_extension()); + root_map.insert(new_specifier.clone(), s.clone()); + new_specifier + } else { + s.as_str().to_owned() + } + } + }) + .collect(); + let mut runtime = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(compiler_snapshot()), + extensions: vec![Extension::builder() + .ops(vec![ + op_cwd::decl(), + op_create_hash::decl(), + op_emit::decl(), + op_exists::decl(), + op_is_node_file::decl(), + op_load::decl(), + op_resolve::decl(), + op_respond::decl(), + ]) + .state(move |state| { + state.put(State::new( + request.graph_data.clone(), + request.hash_data.clone(), + request.maybe_config_specifier.clone(), + request.maybe_npm_resolver.clone(), + request.maybe_tsbuildinfo.clone(), + root_map.clone(), + remapped_specifiers.clone(), + )); + Ok(()) + }) + .build()], + ..Default::default() + }); + + let startup_source = "globalThis.startup({ legacyFlag: false })"; + let request_value = json!({ + "config": request.config, + "debug": request.debug, + "rootNames": root_names, + }); + let request_str = request_value.to_string(); + let exec_source = format!("globalThis.exec({})", request_str); + + runtime + .execute_script(&located_script_name!(), startup_source) + .context("Could not properly start the compiler runtime.")?; + runtime.execute_script(&located_script_name!(), &exec_source)?; + + let op_state = runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + let state = op_state.take::<State>(); + + if let Some(response) = state.maybe_response { + let diagnostics = response.diagnostics; + let maybe_tsbuildinfo = state.maybe_tsbuildinfo; + let stats = response.stats; + + Ok(Response { + diagnostics, + maybe_tsbuildinfo, + stats, + }) + } else { + Err(anyhow!("The response for the exec request was not set.")) + } +} + +#[cfg(test)] +mod tests { + use super::Diagnostic; + use super::DiagnosticCategory; + use super::*; + use crate::args::TsConfig; + use deno_core::futures::future; + use deno_core::OpState; + use deno_graph::ModuleKind; + use std::fs; + + #[derive(Debug, Default)] + pub struct MockLoader { + pub fixtures: PathBuf, + } + + impl deno_graph::source::Loader for MockLoader { + fn load( + &mut self, + specifier: &ModuleSpecifier, + _is_dynamic: bool, + ) -> deno_graph::source::LoadFuture { + let specifier_text = specifier + .to_string() + .replace(":///", "_") + .replace("://", "_") + .replace('/', "-"); + let source_path = self.fixtures.join(specifier_text); + let response = fs::read_to_string(&source_path) + .map(|c| { + Some(deno_graph::source::LoadResponse::Module { + specifier: specifier.clone(), + maybe_headers: None, + content: c.into(), + }) + }) + .map_err(|err| err.into()); + Box::pin(future::ready(response)) + } + } + + async fn setup( + maybe_specifier: Option<ModuleSpecifier>, + maybe_hash_data: Option<Vec<Vec<u8>>>, + maybe_tsbuildinfo: Option<String>, + ) -> OpState { + let specifier = maybe_specifier + .unwrap_or_else(|| resolve_url_or_path("file:///main.ts").unwrap()); + let hash_data = maybe_hash_data.unwrap_or_else(|| vec![b"".to_vec()]); + let fixtures = test_util::testdata_path().join("tsc2"); + let mut loader = MockLoader { fixtures }; + let graph = deno_graph::create_graph( + vec![(specifier, ModuleKind::Esm)], + &mut loader, + deno_graph::GraphOptions { + is_dynamic: false, + imports: None, + resolver: None, + locker: None, + module_analyzer: None, + reporter: None, + }, + ) + .await; + let state = State::new( + Arc::new(RwLock::new((&graph).into())), + hash_data, + None, + None, + maybe_tsbuildinfo, + HashMap::new(), + HashMap::new(), + ); + let mut op_state = OpState::new(1); + op_state.put(state); + op_state + } + + async fn test_exec( + specifier: &ModuleSpecifier, + ) -> Result<Response, AnyError> { + let hash_data = vec![b"something".to_vec()]; + let fixtures = test_util::testdata_path().join("tsc2"); + let mut loader = MockLoader { fixtures }; + let graph = deno_graph::create_graph( + vec![(specifier.clone(), ModuleKind::Esm)], + &mut loader, + deno_graph::GraphOptions { + is_dynamic: false, + imports: None, + resolver: None, + locker: None, + module_analyzer: None, + reporter: None, + }, + ) + .await; + let config = TsConfig::new(json!({ + "allowJs": true, + "checkJs": false, + "esModuleInterop": true, + "emitDecoratorMetadata": false, + "incremental": true, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + "lib": ["deno.window"], + "module": "esnext", + "noEmit": true, + "outDir": "deno:///", + "strict": true, + "target": "esnext", + "tsBuildInfoFile": "deno:///.tsbuildinfo", + })); + let request = Request { + config, + debug: false, + graph_data: Arc::new(RwLock::new((&graph).into())), + hash_data, + maybe_config_specifier: None, + maybe_npm_resolver: None, + maybe_tsbuildinfo: None, + root_names: vec![(specifier.clone(), MediaType::TypeScript)], + }; + exec(request) + } + + #[test] + fn test_compiler_snapshot() { + let mut js_runtime = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(compiler_snapshot()), + ..Default::default() + }); + js_runtime + .execute_script( + "<anon>", + r#" + if (!(startup)) { + throw Error("bad"); + } + console.log(`ts version: ${ts.version}`); + "#, + ) + .unwrap(); + } + + #[tokio::test] + async fn test_create_hash() { + let mut state = setup(None, Some(vec![b"something".to_vec()]), None).await; + let actual = op_create_hash::call( + &mut state, + json!({ "data": "some sort of content" }), + ) + .expect("could not invoke op"); + assert_eq!( + actual, + json!({"hash": "ae92df8f104748768838916857a1623b6a3c593110131b0a00f81ad9dac16511"}) + ); + } + + #[test] + fn test_hash_url() { + let specifier = deno_core::resolve_url( + "data:application/javascript,console.log(\"Hello%20Deno\");", + ) + .unwrap(); + assert_eq!(hash_url(&specifier, MediaType::JavaScript), "data:///d300ea0796bd72b08df10348e0b70514c021f2e45bfe59cec24e12e97cd79c58.js"); + } + + #[test] + fn test_get_tsc_media_type() { + let fixtures = vec![ + ("file:///a.ts", MediaType::TypeScript), + ("file:///a.cts", MediaType::Cts), + ("file:///a.mts", MediaType::Mts), + ("file:///a.tsx", MediaType::Tsx), + ("file:///a.d.ts", MediaType::Dts), + ("file:///a.d.cts", MediaType::Dcts), + ("file:///a.d.mts", MediaType::Dmts), + ("file:///a.js", MediaType::JavaScript), + ("file:///a.jsx", MediaType::Jsx), + ("file:///a.cjs", MediaType::Cjs), + ("file:///a.mjs", MediaType::Mjs), + ("file:///a.json", MediaType::Unknown), + ("file:///a.wasm", MediaType::Unknown), + ("file:///a.js.map", MediaType::Unknown), + ("file:///.tsbuildinfo", MediaType::Unknown), + ]; + for (specifier, media_type) in fixtures { + let specifier = resolve_url_or_path(specifier).unwrap(); + assert_eq!(get_tsc_media_type(&specifier), media_type); + } + } + + #[tokio::test] + async fn test_emit_tsbuildinfo() { + let mut state = setup(None, None, None).await; + let actual = op_emit::call( + &mut state, + EmitArgs { + data: "some file content".to_string(), + file_name: "deno:///.tsbuildinfo".to_string(), + }, + ); + assert!(actual); + let state = state.borrow::<State>(); + assert_eq!( + state.maybe_tsbuildinfo, + Some("some file content".to_string()) + ); + } + + #[tokio::test] + async fn test_load() { + let mut state = setup( + Some(resolve_url_or_path("https://deno.land/x/mod.ts").unwrap()), + None, + Some("some content".to_string()), + ) + .await; + let actual = op_load::call( + &mut state, + json!({ "specifier": "https://deno.land/x/mod.ts"}), + ) + .expect("should have invoked op"); + assert_eq!( + actual, + json!({ + "data": "console.log(\"hello deno\");\n", + "version": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729", + "scriptKind": 3, + }) + ); + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct LoadResponse { + data: String, + version: Option<String>, + script_kind: i64, + } + + #[tokio::test] + async fn test_load_asset() { + let mut state = setup( + Some(resolve_url_or_path("https://deno.land/x/mod.ts").unwrap()), + None, + Some("some content".to_string()), + ) + .await; + let value = op_load::call( + &mut state, + json!({ "specifier": "asset:///lib.dom.d.ts" }), + ) + .expect("should have invoked op"); + let actual: LoadResponse = + serde_json::from_value(value).expect("failed to deserialize"); + let expected = get_asset("lib.dom.d.ts").unwrap(); + assert_eq!(actual.data, expected); + assert!(actual.version.is_some()); + assert_eq!(actual.script_kind, 3); + } + + #[tokio::test] + async fn test_load_tsbuildinfo() { + let mut state = setup( + Some(resolve_url_or_path("https://deno.land/x/mod.ts").unwrap()), + None, + Some("some content".to_string()), + ) + .await; + let actual = + op_load::call(&mut state, json!({ "specifier": "deno:///.tsbuildinfo"})) + .expect("should have invoked op"); + assert_eq!( + actual, + json!({ + "data": "some content", + "version": null, + "scriptKind": 0, + }) + ); + } + + #[tokio::test] + async fn test_load_missing_specifier() { + let mut state = setup(None, None, None).await; + let actual = op_load::call( + &mut state, + json!({ "specifier": "https://deno.land/x/mod.ts"}), + ) + .expect("should have invoked op"); + assert_eq!( + actual, + json!({ + "data": null, + "version": null, + "scriptKind": 0, + }) + ) + } + + #[tokio::test] + async fn test_resolve() { + let mut state = setup( + Some(resolve_url_or_path("https://deno.land/x/a.ts").unwrap()), + None, + None, + ) + .await; + let actual = op_resolve::call( + &mut state, + ResolveArgs { + base: "https://deno.land/x/a.ts".to_string(), + specifiers: vec!["./b.ts".to_string()], + }, + ) + .expect("should have invoked op"); + assert_eq!( + actual, + vec![("https://deno.land/x/b.ts".into(), ".ts".into())] + ); + } + + #[tokio::test] + async fn test_resolve_empty() { + let mut state = setup( + Some(resolve_url_or_path("https://deno.land/x/a.ts").unwrap()), + None, + None, + ) + .await; + let actual = op_resolve::call( + &mut state, + ResolveArgs { + base: "https://deno.land/x/a.ts".to_string(), + specifiers: vec!["./bad.ts".to_string()], + }, + ) + .expect("should have not errored"); + assert_eq!( + actual, + vec![("deno:///missing_dependency.d.ts".into(), ".d.ts".into())] + ); + } + + #[tokio::test] + async fn test_respond() { + let mut state = setup(None, None, None).await; + let actual = op_respond::call( + &mut state, + json!({ + "diagnostics": [ + { + "messageText": "Unknown compiler option 'invalid'.", + "category": 1, + "code": 5023 + } + ], + "stats": [["a", 12]] + }), + ) + .expect("should have invoked op"); + assert_eq!(actual, json!(true)); + let state = state.borrow::<State>(); + assert_eq!( + state.maybe_response, + Some(RespondArgs { + diagnostics: Diagnostics::new(vec![Diagnostic { + category: DiagnosticCategory::Error, + code: 5023, + start: None, + end: None, + message_text: Some( + "Unknown compiler option \'invalid\'.".to_string() + ), + message_chain: None, + source: None, + source_line: None, + file_name: None, + related_information: None, + }]), + stats: Stats(vec![("a".to_string(), 12)]) + }) + ); + } + + #[tokio::test] + async fn test_exec_basic() { + let specifier = resolve_url_or_path("https://deno.land/x/a.ts").unwrap(); + let actual = test_exec(&specifier) + .await + .expect("exec should not have errored"); + eprintln!("diagnostics {:#?}", actual.diagnostics); + assert!(actual.diagnostics.is_empty()); + assert!(actual.maybe_tsbuildinfo.is_some()); + assert_eq!(actual.stats.0.len(), 12); + } + + #[tokio::test] + async fn test_exec_reexport_dts() { + let specifier = resolve_url_or_path("file:///reexports.ts").unwrap(); + let actual = test_exec(&specifier) + .await + .expect("exec should not have errored"); + eprintln!("diagnostics {:#?}", actual.diagnostics); + assert!(actual.diagnostics.is_empty()); + assert!(actual.maybe_tsbuildinfo.is_some()); + assert_eq!(actual.stats.0.len(), 12); + } + + #[tokio::test] + async fn fix_lib_ref() { + let specifier = resolve_url_or_path("file:///libref.ts").unwrap(); + let actual = test_exec(&specifier) + .await + .expect("exec should not have errored"); + eprintln!("diagnostics {:#?}", actual.diagnostics); + assert!(actual.diagnostics.is_empty()); + } +} |