deno/cli/tsc/js.rs
Nathan Whitaker b252cc78c2
feat(unstable): typescript-go integration for deno check (#30920)
Integrated only with deno check (and test, `run --check`, etc)
currently. All spec tests for deno check pass except for 3, which i've
disabled

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Co-authored-by: Divy Srivastava <me@littledivy.com>
2025-10-20 09:59:54 -07:00

985 lines
26 KiB
Rust

// Copyright 2018-2025 the Deno authors. MIT license.
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use deno_core::FastString;
use deno_core::JsRuntime;
use deno_core::ModuleSpecifier;
use deno_core::OpState;
use deno_core::RuntimeOptions;
use deno_core::anyhow::Context;
use deno_core::located_script_name;
use deno_core::op2;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde_json::json;
use deno_core::url::Url;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
use deno_lib::util::hash::FastInsecureHasher;
use deno_lib::worker::create_isolate_create_params;
use deno_path_util::resolve_url_or_path;
use node_resolver::ResolutionMode;
use super::LAZILY_LOADED_STATIC_ASSETS;
use super::ResolveArgs;
use super::ResolveError;
use crate::args::TypeCheckMode;
use crate::tsc::Diagnostics;
use crate::tsc::ExecError;
use crate::tsc::LoadError;
use crate::tsc::Request;
use crate::tsc::RequestNpmState;
use crate::tsc::Response;
use crate::tsc::Stats;
use crate::tsc::get_hash;
#[op2]
#[string]
fn op_remap_specifier(
state: &mut OpState,
#[string] specifier: &str,
) -> Option<String> {
let state = state.borrow::<State>();
state
.maybe_remapped_specifier(specifier)
.map(|url| url.to_string())
}
#[op2]
#[serde]
fn op_libs() -> Vec<String> {
let mut out = Vec::with_capacity(LAZILY_LOADED_STATIC_ASSETS.len());
for (key, value) in LAZILY_LOADED_STATIC_ASSETS.iter() {
if !value.is_lib {
continue;
}
let lib = key
.replace("lib.", "")
.replace(".d.ts", "")
.replace("deno_", "deno.");
out.push(lib);
}
out
}
#[op2]
#[serde]
fn op_resolve(
state: &mut OpState,
#[string] base: String,
#[serde] specifiers: Vec<(bool, String)>,
) -> Result<Vec<(String, Option<&'static str>)>, ResolveError> {
op_resolve_inner(state, ResolveArgs { base, specifiers })
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TscConstants {
types_node_ignorable_names: Vec<&'static str>,
node_only_globals: Vec<&'static str>,
ignored_diagnostic_codes: Vec<u64>,
}
impl TscConstants {
pub fn new() -> Self {
Self {
types_node_ignorable_names: super::TYPES_NODE_IGNORABLE_NAMES.to_vec(),
node_only_globals: super::NODE_ONLY_GLOBALS.to_vec(),
ignored_diagnostic_codes: super::IGNORED_DIAGNOSTIC_CODES
.iter()
.copied()
.collect(),
}
}
}
#[op2]
#[serde]
fn op_tsc_constants() -> TscConstants {
TscConstants::new()
}
#[inline]
fn op_resolve_inner(
state: &mut OpState,
args: ResolveArgs,
) -> Result<Vec<(String, Option<&'static str>)>, ResolveError> {
let state = state.borrow_mut::<State>();
let mut resolved: Vec<(String, Option<&'static str>)> =
Vec::with_capacity(args.specifiers.len());
let referrer = if let Some(remapped_specifier) =
state.maybe_remapped_specifier(&args.base)
{
remapped_specifier.clone()
} else {
resolve_url_or_path(&args.base, &state.current_dir)?
};
let referrer_module = state.graph.get(&referrer);
for (is_cjs, specifier) in args.specifiers {
let result = super::resolve_specifier_for_tsc(
specifier,
&referrer,
&state.graph,
if is_cjs {
ResolutionMode::Require
} else {
ResolutionMode::Import
},
state.maybe_npm.as_ref(),
referrer_module,
&mut state.remapped_specifiers,
)?;
resolved.push(result);
}
Ok(resolved)
}
deno_core::extension!(deno_cli_tsc,
ops = [
op_create_hash,
op_emit,
op_is_node_file,
op_load,
op_remap_specifier,
op_resolve,
op_tsc_constants,
op_respond,
op_libs,
],
options = {
request: Request,
root_map: HashMap<String, Url>,
remapped_specifiers: HashMap<String, Url>,
},
state = |state, options| {
state.put(State::new(
options.request.graph,
options.request.hash_data,
options.request.maybe_npm,
options.request.maybe_tsbuildinfo,
options.root_map,
options.remapped_specifiers,
std::env::current_dir()
.context("Unable to get CWD")
.unwrap(),
));
},
customizer = |ext: &mut deno_core::Extension| {
use deno_core::ExtensionFileSource;
ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/99_main_compiler.js", crate::tsc::MAIN_COMPILER_SOURCE.as_str().into()));
ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/97_ts_host.js", crate::tsc::TS_HOST_SOURCE.as_str().into()));
ext.esm_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/98_lsp.js", crate::tsc::LSP_SOURCE.as_str().into()));
ext.js_files.to_mut().push(ExtensionFileSource::new_computed("ext:deno_cli_tsc/00_typescript.js", crate::tsc::TYPESCRIPT_SOURCE.as_str().into()));
ext.esm_entry_point = Some("ext:deno_cli_tsc/99_main_compiler.js");
}
);
// TODO(bartlomieju): this mechanism is questionable.
// Can't we use something more efficient here?
#[op2]
fn op_respond(state: &mut OpState, #[serde] args: RespondArgs) {
op_respond_inner(state, args)
}
#[inline]
fn op_respond_inner(state: &mut OpState, args: RespondArgs) {
let state = state.borrow_mut::<State>();
state.maybe_response = Some(args);
}
#[op2]
#[string]
fn op_create_hash(s: &mut OpState, #[string] text: &str) -> String {
op_create_hash_inner(s, text)
}
#[inline]
fn op_create_hash_inner(s: &mut OpState, text: &str) -> String {
let state = s.borrow_mut::<State>();
get_hash(text, state.hash_data)
}
#[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,
}
#[op2(fast)]
fn op_emit(
state: &mut OpState,
#[string] data: String,
#[string] file_name: String,
) -> bool {
op_emit_inner(state, EmitArgs { data, file_name })
}
#[inline]
fn op_emit_inner(state: &mut OpState, args: EmitArgs) -> bool {
let state = state.borrow_mut::<State>();
match args.file_name.as_ref() {
"internal:///.tsbuildinfo" => state.maybe_tsbuildinfo = Some(args.data),
_ => {
if cfg!(debug_assertions) {
panic!("Unhandled emit write: {}", args.file_name);
}
}
}
true
}
#[op2(fast)]
fn op_is_node_file(state: &mut OpState, #[string] path: &str) -> bool {
let state = state.borrow::<State>();
ModuleSpecifier::parse(path)
.ok()
.and_then(|specifier| {
state
.maybe_npm
.as_ref()
.map(|n| n.node_resolver.in_npm_package(&specifier))
})
.unwrap_or(false)
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
struct RespondArgs {
pub diagnostics: Diagnostics,
pub ambient_modules: Vec<String>,
pub stats: Stats,
}
impl super::LoadContent for FastString {
fn from_static(source: &'static str) -> Self {
FastString::from_static(source)
}
fn from_string(source: String) -> Self {
FastString::from(source)
}
fn from_arc_str(source: Arc<str>) -> Self {
FastString::from(source)
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct LoadResponse {
data: FastString,
version: Option<String>,
script_kind: i32,
is_cjs: bool,
}
#[op2]
#[serde]
fn op_load(
state: &mut OpState,
#[string] load_specifier: &str,
) -> Result<Option<LoadResponse>, LoadError> {
op_load_inner(state, load_specifier)
}
impl super::Mapper for State {
fn maybe_remapped_specifier(
&self,
specifier: &str,
) -> Option<&ModuleSpecifier> {
self.maybe_remapped_specifier(specifier)
}
}
fn op_load_inner(
state: &mut OpState,
load_specifier: &str,
) -> Result<Option<LoadResponse>, LoadError> {
let state = state.borrow::<State>();
Ok(
super::load_for_tsc::<FastString, _>(
load_specifier,
state.maybe_npm.as_ref(),
&state.current_dir,
&state.graph,
state.maybe_tsbuildinfo.as_deref(),
state.hash_data,
state,
)?
.map(|res| LoadResponse {
data: res.data,
version: res.version,
is_cjs: res.is_cjs,
script_kind: super::as_ts_script_kind(res.media_type),
}),
)
}
pub fn exec_request(
request: Request,
root_names: Vec<String>,
root_map: HashMap<String, ModuleSpecifier>,
remapped_specifiers: HashMap<String, ModuleSpecifier>,
code_cache: Option<Arc<dyn deno_runtime::code_cache::CodeCache>>,
) -> Result<Response, ExecError> {
let request_value = json!({
"config": request.config,
"debug": request.debug,
"rootNames": root_names,
"localOnly": request.check_mode == TypeCheckMode::Local,
});
let exec_source = format!("globalThis.exec({request_value})");
let mut extensions =
deno_runtime::snapshot_info::get_extensions_in_snapshot();
extensions.push(deno_cli_tsc::init(request, root_map, remapped_specifiers));
let extension_code_cache = code_cache.map(|cache| {
Rc::new(TscExtCodeCache::new(cache)) as Rc<dyn deno_core::ExtCodeCache>
});
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions,
create_params: create_isolate_create_params(&crate::sys::CliSys::default()),
startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
extension_code_cache,
..Default::default()
});
runtime
.execute_script(located_script_name!(), exec_source)
.map_err(ExecError::Js)?;
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 ambient_modules = response.ambient_modules;
let maybe_tsbuildinfo = state.maybe_tsbuildinfo;
let stats = response.stats;
Ok(Response {
diagnostics,
ambient_modules,
maybe_tsbuildinfo,
stats,
})
} else {
Err(ExecError::ResponseNotSet)
}
}
pub struct TscExtCodeCache {
cache: Arc<dyn deno_runtime::code_cache::CodeCache>,
}
impl TscExtCodeCache {
pub fn new(cache: Arc<dyn deno_runtime::code_cache::CodeCache>) -> Self {
Self { cache }
}
}
impl deno_core::ExtCodeCache for TscExtCodeCache {
fn get_code_cache_info(
&self,
specifier: &ModuleSpecifier,
code: &deno_core::ModuleSourceCode,
esm: bool,
) -> deno_core::SourceCodeCacheInfo {
use deno_runtime::code_cache::CodeCacheType;
let code_hash = FastInsecureHasher::new_deno_versioned()
.write_hashable(code)
.finish();
let data = self
.cache
.get_sync(
specifier,
if esm {
CodeCacheType::EsModule
} else {
CodeCacheType::Script
},
code_hash,
)
.map(Cow::from)
.inspect(|_| {
log::debug!(
"V8 code cache hit for Extension module: {specifier}, [{code_hash:?}]"
);
});
deno_core::SourceCodeCacheInfo {
hash: code_hash,
data,
}
}
fn code_cache_ready(
&self,
specifier: ModuleSpecifier,
source_hash: u64,
code_cache: &[u8],
esm: bool,
) {
use deno_runtime::code_cache::CodeCacheType;
log::debug!(
"Updating V8 code cache for Extension module: {specifier}, [{source_hash:?}]"
);
self.cache.set_sync(
specifier,
if esm {
CodeCacheType::EsModule
} else {
CodeCacheType::Script
},
source_hash,
code_cache,
);
}
}
// TODO(bartlomieju): we have similar struct in `tsc.rs` - maybe at least change
// the name of the struct to avoid confusion?
#[derive(Debug)]
struct State {
hash_data: u64,
graph: Arc<ModuleGraph>,
maybe_tsbuildinfo: Option<String>,
maybe_response: Option<RespondArgs>,
maybe_npm: Option<RequestNpmState>,
// todo(dsherret): it looks like the remapped_specifiers and
// root_map could be combined... what is the point of the separation?
remapped_specifiers: HashMap<String, ModuleSpecifier>,
root_map: HashMap<String, ModuleSpecifier>,
current_dir: PathBuf,
}
impl Default for State {
fn default() -> Self {
Self {
hash_data: Default::default(),
graph: Arc::new(ModuleGraph::new(GraphKind::All)),
maybe_tsbuildinfo: Default::default(),
maybe_response: Default::default(),
maybe_npm: Default::default(),
remapped_specifiers: Default::default(),
root_map: Default::default(),
current_dir: Default::default(),
}
}
}
impl State {
pub fn new(
graph: Arc<ModuleGraph>,
hash_data: u64,
maybe_npm: Option<RequestNpmState>,
maybe_tsbuildinfo: Option<String>,
root_map: HashMap<String, ModuleSpecifier>,
remapped_specifiers: HashMap<String, ModuleSpecifier>,
current_dir: PathBuf,
) -> Self {
State {
hash_data,
graph,
maybe_npm,
maybe_tsbuildinfo,
maybe_response: None,
remapped_specifiers,
root_map,
current_dir,
}
}
pub fn maybe_remapped_specifier(
&self,
specifier: &str,
) -> Option<&ModuleSpecifier> {
self
.remapped_specifiers
.get(specifier)
.or_else(|| self.root_map.get(specifier))
}
}
#[cfg(test)]
mod tests {
use deno_ast::MediaType;
use deno_core::OpState;
use deno_core::futures::future;
use deno_core::parking_lot::Mutex;
use deno_core::serde_json;
use deno_error::JsErrorBox;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
use deno_runtime::code_cache::CodeCacheType;
use test_util::PathRef;
use super::super::Diagnostic;
use super::super::DiagnosticCategory;
use super::*;
use crate::args::CompilerOptions;
use crate::tsc::MISSING_DEPENDENCY_SPECIFIER;
use crate::tsc::get_lazily_loaded_asset;
#[derive(Debug, Default)]
pub struct MockLoader {
pub fixtures: PathRef,
}
impl deno_graph::source::Loader for MockLoader {
fn load(
&self,
specifier: &ModuleSpecifier,
_options: deno_graph::source::LoadOptions,
) -> deno_graph::source::LoadFuture {
let specifier_text = specifier
.to_string()
.replace(":///", "_")
.replace("://", "_")
.replace('/', "-");
let source_path = self.fixtures.join(specifier_text);
let response = source_path
.read_to_bytes_if_exists()
.map(|c| {
Some(deno_graph::source::LoadResponse::Module {
specifier: specifier.clone(),
mtime: None,
maybe_headers: None,
content: c.into(),
})
})
.map_err(|e| {
deno_graph::source::LoadError::Other(Arc::new(JsErrorBox::generic(
e.to_string(),
)))
});
Box::pin(future::ready(response))
}
}
async fn setup(
maybe_specifier: Option<ModuleSpecifier>,
maybe_hash_data: Option<u64>,
maybe_tsbuildinfo: Option<String>,
) -> OpState {
let specifier = maybe_specifier
.unwrap_or_else(|| ModuleSpecifier::parse("file:///main.ts").unwrap());
let hash_data = maybe_hash_data.unwrap_or(0);
let fixtures = test_util::testdata_path().join("tsc2");
let loader = MockLoader { fixtures };
let mut graph = ModuleGraph::new(GraphKind::TypesOnly);
graph
.build(vec![specifier], Vec::new(), &loader, Default::default())
.await;
let state = State::new(
Arc::new(graph),
hash_data,
None,
maybe_tsbuildinfo,
HashMap::new(),
HashMap::new(),
std::env::current_dir()
.context("Unable to get CWD")
.unwrap(),
);
let mut op_state = OpState::new(None);
op_state.put(state);
op_state
}
async fn test_exec(
specifier: &ModuleSpecifier,
) -> Result<Response, ExecError> {
test_exec_with_cache(specifier, None).await
}
async fn test_exec_with_cache(
specifier: &ModuleSpecifier,
code_cache: Option<Arc<dyn deno_runtime::code_cache::CodeCache>>,
) -> Result<Response, ExecError> {
let hash_data = 123; // something random
let fixtures = test_util::testdata_path().join("tsc2");
let loader = MockLoader { fixtures };
let mut graph = ModuleGraph::new(GraphKind::TypesOnly);
graph
.build(
vec![specifier.clone()],
Vec::new(),
&loader,
Default::default(),
)
.await;
let config = Arc::new(CompilerOptions::new(json!({
"allowJs": true,
"checkJs": false,
"esModuleInterop": true,
"emitDecoratorMetadata": false,
"incremental": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
"lib": ["deno.window"],
"noEmit": true,
"outDir": "internal:///",
"strict": true,
"target": "esnext",
"tsBuildInfoFile": "internal:///.tsbuildinfo",
})));
let request = Request {
config,
debug: false,
graph: Arc::new(graph),
hash_data,
maybe_npm: None,
maybe_tsbuildinfo: None,
root_names: vec![(specifier.clone(), MediaType::TypeScript)],
check_mode: TypeCheckMode::All,
initial_cwd: std::env::current_dir().unwrap(),
};
crate::tsc::exec(request, code_cache, None)
}
#[tokio::test]
async fn test_create_hash() {
let mut state = setup(None, Some(123), None).await;
let actual = op_create_hash_inner(&mut state, "some sort of content");
assert_eq!(actual, "11905938177474799758");
}
#[tokio::test]
async fn test_hash_url() {
let specifier = deno_core::resolve_url(
"data:application/javascript,console.log(\"Hello%20Deno\");",
)
.unwrap();
assert_eq!(
crate::tsc::hash_url(&specifier, MediaType::JavaScript),
"data:///d300ea0796bd72b08df10348e0b70514c021f2e45bfe59cec24e12e97cd79c58.js"
);
}
#[tokio::test]
async fn test_emit_tsbuildinfo() {
let mut state = setup(None, None, None).await;
let actual = op_emit_inner(
&mut state,
EmitArgs {
data: "some file content".to_string(),
file_name: "internal:///.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(ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap()),
None,
Some("some content".to_string()),
)
.await;
let actual =
op_load_inner(&mut state, "https://deno.land/x/mod.ts").unwrap();
assert_eq!(
serde_json::to_value(actual).unwrap(),
json!({
"data": "console.log(\"hello deno\");\n",
"version": "7821807483407828376",
"scriptKind": 3,
"isCjs": false,
})
);
}
#[tokio::test]
async fn test_load_asset() {
let mut state = setup(
Some(ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap()),
None,
Some("some content".to_string()),
)
.await;
let actual = op_load_inner(&mut state, "asset:///lib.dom.d.ts")
.expect("should have invoked op")
.expect("load should have succeeded");
let expected = get_lazily_loaded_asset("lib.dom.d.ts").unwrap();
assert_eq!(actual.data.to_string(), expected.to_string());
assert!(actual.version.is_some());
assert_eq!(actual.script_kind, 3);
}
#[tokio::test]
async fn test_load_tsbuildinfo() {
let mut state = setup(
Some(ModuleSpecifier::parse("https://deno.land/x/mod.ts").unwrap()),
None,
Some("some content".to_string()),
)
.await;
let actual = op_load_inner(&mut state, "internal:///.tsbuildinfo")
.expect("should have invoked op")
.expect("load should have succeeded");
assert_eq!(
serde_json::to_value(actual).unwrap(),
json!({
"data": "some content",
"version": null,
"scriptKind": 0,
"isCjs": false,
})
);
}
#[tokio::test]
async fn test_load_missing_specifier() {
let mut state = setup(None, None, None).await;
let actual = op_load_inner(&mut state, "https://deno.land/x/mod.ts")
.expect("should have invoked op");
assert_eq!(serde_json::to_value(actual).unwrap(), json!(null));
}
#[tokio::test]
async fn test_resolve() {
let mut state = setup(
Some(ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap()),
None,
None,
)
.await;
let actual = op_resolve_inner(
&mut state,
ResolveArgs {
base: "https://deno.land/x/a.ts".to_string(),
specifiers: vec![(false, "./b.ts".to_string())],
},
)
.expect("should have invoked op");
assert_eq!(
actual,
vec![("https://deno.land/x/b.ts".into(), Some(".ts"))]
);
}
#[tokio::test]
async fn test_resolve_empty() {
let mut state = setup(
Some(ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap()),
None,
None,
)
.await;
let actual = op_resolve_inner(
&mut state,
ResolveArgs {
base: "https://deno.land/x/a.ts".to_string(),
specifiers: vec![(false, "./bad.ts".to_string())],
},
)
.expect("should have not errored");
assert_eq!(
actual,
vec![(MISSING_DEPENDENCY_SPECIFIER.into(), Some(".d.ts"))]
);
}
#[tokio::test]
async fn test_respond() {
let mut state = setup(None, None, None).await;
let args = serde_json::from_value(json!({
"diagnostics": [
{
"messageText": "Unknown compiler option 'invalid'.",
"category": 1,
"code": 5023
}
],
"stats": [["a", 12]],
"ambientModules": []
}))
.unwrap();
op_respond_inner(&mut state, args);
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,
original_source_start: None,
message_text: Some(
"Unknown compiler option \'invalid\'.".to_string()
),
message_chain: None,
source: None,
source_line: None,
file_name: None,
related_information: None,
reports_deprecated: None,
reports_unnecessary: None,
other: Default::default(),
missing_specifier: None,
}]),
ambient_modules: vec![],
stats: Stats(vec![("a".to_string(), 12)])
})
);
}
#[tokio::test]
async fn test_exec_basic() {
let specifier = ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap();
let actual = test_exec(&specifier)
.await
.expect("exec should not have errored");
assert!(!actual.diagnostics.has_diagnostic());
assert!(actual.maybe_tsbuildinfo.is_some());
assert_eq!(actual.stats.0.len(), 12);
}
#[tokio::test]
async fn test_exec_reexport_dts() {
let specifier = ModuleSpecifier::parse("file:///reexports.ts").unwrap();
let actual = test_exec(&specifier)
.await
.expect("exec should not have errored");
assert!(!actual.diagnostics.has_diagnostic());
assert!(actual.maybe_tsbuildinfo.is_some());
assert_eq!(actual.stats.0.len(), 12);
}
#[tokio::test]
async fn fix_lib_ref() {
let specifier = ModuleSpecifier::parse("file:///libref.ts").unwrap();
let actual = test_exec(&specifier)
.await
.expect("exec should not have errored");
assert!(!actual.diagnostics.has_diagnostic());
}
pub type SpecifierWithType = (ModuleSpecifier, CodeCacheType);
#[derive(Default)]
struct TestExtCodeCache {
cache: Mutex<HashMap<(SpecifierWithType, u64), Vec<u8>>>,
hits: Mutex<HashMap<SpecifierWithType, usize>>,
misses: Mutex<HashMap<SpecifierWithType, usize>>,
}
impl deno_runtime::code_cache::CodeCache for TestExtCodeCache {
fn get_sync(
&self,
specifier: &ModuleSpecifier,
code_cache_type: CodeCacheType,
source_hash: u64,
) -> Option<Vec<u8>> {
let result = self
.cache
.lock()
.get(&((specifier.clone(), code_cache_type), source_hash))
.cloned();
if result.is_some() {
*self
.hits
.lock()
.entry((specifier.clone(), code_cache_type))
.or_default() += 1;
} else {
*self
.misses
.lock()
.entry((specifier.clone(), code_cache_type))
.or_default() += 1;
}
result
}
fn set_sync(
&self,
specifier: ModuleSpecifier,
code_cache_type: CodeCacheType,
source_hash: u64,
data: &[u8],
) {
self
.cache
.lock()
.insert(((specifier, code_cache_type), source_hash), data.to_vec());
}
}
#[tokio::test]
async fn test_exec_code_cache() {
let code_cache = Arc::new(TestExtCodeCache::default());
let specifier = ModuleSpecifier::parse("https://deno.land/x/a.ts").unwrap();
let actual = test_exec_with_cache(&specifier, Some(code_cache.clone()))
.await
.expect("exec should not have errored");
assert!(!actual.diagnostics.has_diagnostic());
let expect = [
(
"ext:deno_cli_tsc/99_main_compiler.js",
CodeCacheType::EsModule,
),
("ext:deno_cli_tsc/98_lsp.js", CodeCacheType::EsModule),
("ext:deno_cli_tsc/97_ts_host.js", CodeCacheType::EsModule),
("ext:deno_cli_tsc/00_typescript.js", CodeCacheType::Script),
];
{
let mut files = HashMap::new();
for (((specifier, ty), _), _) in code_cache.cache.lock().iter() {
let specifier = specifier.to_string();
if files.contains_key(&specifier) {
panic!("should have only 1 entry per specifier");
}
files.insert(specifier, *ty);
}
// 99_main_compiler, 98_lsp, 97_ts_host, 00_typescript
assert_eq!(files.len(), 4);
assert_eq!(code_cache.hits.lock().len(), 0);
assert_eq!(code_cache.misses.lock().len(), 4);
for (specifier, ty) in &expect {
assert_eq!(files.get(*specifier), Some(ty));
}
code_cache.hits.lock().clear();
code_cache.misses.lock().clear();
}
{
let _ = test_exec_with_cache(&specifier, Some(code_cache.clone()))
.await
.expect("exec should not have errored");
// 99_main_compiler, 98_lsp, 97_ts_host, 00_typescript
assert_eq!(code_cache.hits.lock().len(), 4);
assert_eq!(code_cache.misses.lock().len(), 0);
for (specifier, ty) in expect {
let url = ModuleSpecifier::parse(specifier).unwrap();
assert_eq!(code_cache.hits.lock().get(&(url, ty)), Some(&1));
}
}
}
}