1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-15 16:43:44 -05:00
denoland-deno/cli/tsc.rs

809 lines
24 KiB
Rust
Raw Normal View History

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
use crate::diagnostics::Diagnostics;
use crate::media_type::MediaType;
use crate::module_graph::Graph;
use crate::module_graph::Stats;
use crate::tsc_config::TsConfig;
use deno_core::error::anyhow;
use deno_core::error::bail;
use deno_core::error::AnyError;
use deno_core::error::Context;
use deno_core::json_op_sync;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::JsRuntime;
use deno_core::ModuleSpecifier;
use deno_core::OpFn;
use deno_core::RuntimeOptions;
use deno_core::Snapshot;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
2020-12-29 23:17:17 -05:00
use std::sync::Arc;
use std::sync::Mutex;
/// Provide static assets that are not preloaded in the compiler snapshot.
pub fn get_asset(asset: &str) -> Option<&'static str> {
macro_rules! inc {
($e:expr) => {
Some(include_str!(concat!("dts/", $e)))
};
}
match asset {
"lib.dom.d.ts" => inc!("lib.dom.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.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"),
_ => None,
}
}
fn get_maybe_hash(
maybe_source: &Option<String>,
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
}
}
/// 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 url = specifier.as_url();
let path = if url.scheme() == "file" {
if let Ok(path) = url.to_file_path() {
path
} else {
PathBuf::from(url.path())
}
} else {
PathBuf::from(url.path())
};
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("tsx") => MediaType::TSX,
Some("js") => MediaType::JavaScript,
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,
2020-12-29 23:17:17 -05:00
pub graph: Arc<Mutex<Graph>>,
pub hash_data: Vec<Vec<u8>>,
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,
/// Any files that were emitted during the check.
pub emitted_files: Vec<EmittedFile>,
/// If there was any build info associated with the exec request.
pub maybe_tsbuildinfo: Option<String>,
/// Statistics from the check.
pub stats: Stats,
}
struct State {
hash_data: Vec<Vec<u8>>,
emitted_files: Vec<EmittedFile>,
2020-12-29 23:17:17 -05:00
graph: Arc<Mutex<Graph>>,
maybe_tsbuildinfo: Option<String>,
maybe_response: Option<RespondArgs>,
root_map: HashMap<String, ModuleSpecifier>,
}
impl State {
pub fn new(
2020-12-29 23:17:17 -05:00
graph: Arc<Mutex<Graph>>,
hash_data: Vec<Vec<u8>>,
maybe_tsbuildinfo: Option<String>,
root_map: HashMap<String, ModuleSpecifier>,
) -> Self {
State {
hash_data,
emitted_files: Vec::new(),
graph,
maybe_tsbuildinfo,
maybe_response: None,
root_map,
}
}
}
fn op<F>(op_fn: F) -> Box<OpFn>
where
F: Fn(&mut State, Value) -> Result<Value, AnyError> + 'static,
{
json_op_sync(move |s, args, _bufs| {
let state = s.borrow_mut::<State>();
op_fn(state, args)
})
}
#[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,
}
fn create_hash(state: &mut State, args: Value) -> Result<Value, AnyError> {
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 }))
}
#[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,
/// A string representation of the specifier that was associated with a
/// module. This should be present on every module that represents a module
/// that was requested to be transformed.
maybe_specifiers: Option<Vec<String>>,
}
fn emit(state: &mut State, args: Value) -> Result<Value, AnyError> {
let v: EmitArgs = serde_json::from_value(args)
.context("Invalid request from JavaScript for \"op_emit\".")?;
match v.file_name.as_ref() {
"deno:///.tsbuildinfo" => state.maybe_tsbuildinfo = Some(v.data),
_ => state.emitted_files.push(EmittedFile {
data: v.data,
maybe_specifiers: if let Some(specifiers) = &v.maybe_specifiers {
let specifiers = specifiers
.iter()
.map(|s| {
if let Some(remapped_specifier) = state.root_map.get(s) {
remapped_specifier.clone()
} else {
ModuleSpecifier::resolve_url_or_path(s).unwrap()
}
})
.collect();
Some(specifiers)
} else {
None
},
media_type: MediaType::from(&v.file_name),
}),
}
Ok(json!(true))
}
#[derive(Debug, Deserialize)]
struct LoadArgs {
/// The fully qualified specifier that should be loaded.
specifier: String,
}
fn load(state: &mut State, args: Value) -> Result<Value, AnyError> {
let v: LoadArgs = serde_json::from_value(args)
.context("Invalid request from JavaScript for \"op_load\".")?;
let specifier = ModuleSpecifier::resolve_url_or_path(&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 data = if &v.specifier == "deno:///.tsbuildinfo" {
state.maybe_tsbuildinfo.clone()
// 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:///none.d.ts" {
hash = Some("1".to_string());
media_type = MediaType::TypeScript;
Some("declare var a: any;\nexport = a;\n".to_string())
} else if v.specifier.starts_with("asset:///") {
let name = v.specifier.replace("asset:///", "");
let maybe_source = get_asset(&name).map(|s| s.to_string());
hash = get_maybe_hash(&maybe_source, &state.hash_data);
media_type = MediaType::from(&v.specifier);
maybe_source
} else {
2020-12-29 23:17:17 -05:00
let graph = state.graph.lock().unwrap();
let specifier =
if let Some(remapped_specifier) = state.root_map.get(&v.specifier) {
remapped_specifier.clone()
} else {
specifier
};
let maybe_source = graph.get_source(&specifier);
media_type = if let Some(media_type) = graph.get_media_type(&specifier) {
media_type
} else {
MediaType::Unknown
};
hash = get_maybe_hash(&maybe_source, &state.hash_data);
maybe_source
};
Ok(
json!({ "data": data, "hash": hash, "scriptKind": media_type.as_ts_script_kind() }),
)
}
#[derive(Debug, Deserialize)]
#[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>,
}
fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
let v: ResolveArgs = serde_json::from_value(args)
.context("Invalid request from JavaScript for \"op_resolve\".")?;
let mut resolved: Vec<(String, String)> = Vec::new();
let referrer = if let Some(remapped_base) = state.root_map.get(&v.base) {
remapped_base.clone()
} else {
ModuleSpecifier::resolve_url_or_path(&v.base).context(
"Error converting a string module specifier for \"op_resolve\".",
)?
};
for specifier in &v.specifiers {
if specifier.starts_with("asset:///") {
resolved.push((
specifier.clone(),
MediaType::from(specifier).as_ts_extension().to_string(),
));
} else {
2020-12-29 23:17:17 -05:00
let graph = state.graph.lock().unwrap();
match graph.resolve(specifier, &referrer, true) {
Ok(resolved_specifier) => {
let media_type = if let Some(media_type) =
graph.get_media_type(&resolved_specifier)
{
media_type
} else {
bail!(
"Unable to resolve media type for specifier: \"{}\"",
resolved_specifier
)
};
resolved.push((
resolved_specifier.to_string(),
media_type.as_ts_extension(),
));
}
// in certain situations, like certain dynamic imports, we won't have
// the source file in the graph, so we will return a fake module to
// make tsc happy.
Err(_) => {
resolved.push(("deno:///none.d.ts".to_string(), ".d.ts".to_string()));
}
}
}
}
Ok(json!(resolved))
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
struct RespondArgs {
pub diagnostics: Diagnostics,
pub stats: Stats,
}
fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> {
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(
snapshot: Snapshot,
request: Request,
) -> Result<Response, AnyError> {
let mut runtime = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(snapshot),
..Default::default()
});
// 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 root_names: Vec<String> = request
.root_names
.iter()
.map(|(s, mt)| {
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 op_state = runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put(State::new(
request.graph.clone(),
request.hash_data.clone(),
request.maybe_tsbuildinfo.clone(),
root_map,
));
}
runtime.register_op("op_create_hash", op(create_hash));
runtime.register_op("op_emit", op(emit));
runtime.register_op("op_load", op(load));
runtime.register_op("op_resolve", op(resolve));
runtime.register_op("op_respond", op(respond));
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("[native code]", startup_source)
.context("Could not properly start the compiler runtime.")?;
runtime.execute("[native_code]", &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 emitted_files = state.emitted_files;
let maybe_tsbuildinfo = state.maybe_tsbuildinfo;
let stats = response.stats;
Ok(Response {
diagnostics,
emitted_files,
maybe_tsbuildinfo,
stats,
})
} else {
Err(anyhow!("The response for the exec request was not set."))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::Diagnostic;
use crate::diagnostics::DiagnosticCategory;
use crate::js;
use crate::module_graph::tests::MockSpecifierHandler;
use crate::module_graph::GraphBuilder;
use crate::tsc_config::TsConfig;
use std::env;
use std::path::PathBuf;
2020-12-29 23:17:17 -05:00
use std::sync::Mutex;
async fn setup(
maybe_specifier: Option<ModuleSpecifier>,
maybe_hash_data: Option<Vec<Vec<u8>>>,
maybe_tsbuildinfo: Option<String>,
) -> State {
let specifier = maybe_specifier.unwrap_or_else(|| {
ModuleSpecifier::resolve_url_or_path("file:///main.ts").unwrap()
});
let hash_data = maybe_hash_data.unwrap_or_else(|| vec![b"".to_vec()]);
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let fixtures = c.join("tests/tsc2");
2020-12-29 23:17:17 -05:00
let handler = Arc::new(Mutex::new(MockSpecifierHandler {
fixtures,
..MockSpecifierHandler::default()
}));
let mut builder = GraphBuilder::new(handler.clone(), None, None);
builder
.add(&specifier, false)
.await
.expect("module not inserted");
2020-12-29 23:17:17 -05:00
let graph = Arc::new(Mutex::new(builder.get_graph()));
State::new(graph, hash_data, maybe_tsbuildinfo, HashMap::new())
}
async fn test_exec(
specifier: &ModuleSpecifier,
) -> Result<Response, AnyError> {
let hash_data = vec![b"something".to_vec()];
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let fixtures = c.join("tests/tsc2");
2020-12-29 23:17:17 -05:00
let handler = Arc::new(Mutex::new(MockSpecifierHandler {
fixtures,
..Default::default()
}));
let mut builder = GraphBuilder::new(handler.clone(), None, None);
builder.add(&specifier, false).await?;
2020-12-29 23:17:17 -05:00
let graph = Arc::new(Mutex::new(builder.get_graph()));
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,
hash_data,
maybe_tsbuildinfo: None,
root_names: vec![(specifier.clone(), MediaType::TypeScript)],
};
exec(js::compiler_isolate_init(), request)
}
#[tokio::test]
async fn test_create_hash() {
let mut state = setup(None, Some(vec![b"something".to_vec()]), None).await;
let actual =
create_hash(&mut state, json!({ "data": "some sort of content" }))
.expect("could not invoke op");
assert_eq!(
actual,
json!({"hash": "ae92df8f104748768838916857a1623b6a3c593110131b0a00f81ad9dac16511"})
);
}
#[test]
fn test_get_tsc_media_type() {
let fixtures = vec![
("file:///a.ts", MediaType::TypeScript),
("file:///a.tsx", MediaType::TSX),
("file:///a.d.ts", MediaType::Dts),
("file:///a.js", MediaType::JavaScript),
("file:///a.jsx", MediaType::JSX),
("file:///a.cjs", MediaType::Unknown),
("file:///a.mjs", MediaType::Unknown),
("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 = ModuleSpecifier::resolve_url_or_path(specifier).unwrap();
assert_eq!(get_tsc_media_type(&specifier), media_type);
}
}
#[tokio::test]
async fn test_emit() {
let mut state = setup(None, None, None).await;
let actual = emit(
&mut state,
json!({
"data": "some file content",
"fileName": "cache:///some/file.js",
"maybeSpecifiers": ["file:///some/file.ts"]
}),
)
.expect("should have invoked op");
assert_eq!(actual, json!(true));
assert_eq!(state.emitted_files.len(), 1);
assert!(state.maybe_tsbuildinfo.is_none());
assert_eq!(
state.emitted_files[0],
EmittedFile {
data: "some file content".to_string(),
maybe_specifiers: Some(vec![ModuleSpecifier::resolve_url_or_path(
"file:///some/file.ts"
)
.unwrap()]),
media_type: MediaType::JavaScript,
}
);
}
#[tokio::test]
async fn test_emit_tsbuildinfo() {
let mut state = setup(None, None, None).await;
let actual = emit(
&mut state,
json!({
"data": "some file content",
"fileName": "deno:///.tsbuildinfo",
}),
)
.expect("should have invoked op");
assert_eq!(actual, json!(true));
assert_eq!(state.emitted_files.len(), 0);
assert_eq!(
state.maybe_tsbuildinfo,
Some("some file content".to_string())
);
}
#[tokio::test]
async fn test_load() {
let mut state = setup(
Some(
ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts")
.unwrap(),
),
None,
Some("some content".to_string()),
)
.await;
let actual = load(
&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",
"hash": "149c777056afcc973d5fcbe11421b6d5ddc57b81786765302030d7fc893bf729",
"scriptKind": 3,
})
);
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LoadResponse {
data: String,
hash: Option<String>,
script_kind: i64,
}
#[tokio::test]
async fn test_load_asset() {
let mut state = setup(
Some(
ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts")
.unwrap(),
),
None,
Some("some content".to_string()),
)
.await;
let value =
load(&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.hash.is_some());
assert_eq!(actual.script_kind, 3);
}
#[tokio::test]
async fn test_load_tsbuildinfo() {
let mut state = setup(
Some(
ModuleSpecifier::resolve_url_or_path("https://deno.land/x/mod.ts")
.unwrap(),
),
None,
Some("some content".to_string()),
)
.await;
let actual =
load(&mut state, json!({ "specifier": "deno:///.tsbuildinfo"}))
.expect("should have invoked op");
assert_eq!(
actual,
json!({
"data": "some content",
"hash": null,
"scriptKind": 0,
})
);
}
#[tokio::test]
async fn test_load_missing_specifier() {
let mut state = setup(None, None, None).await;
let actual = load(
&mut state,
json!({ "specifier": "https://deno.land/x/mod.ts"}),
)
.expect("should have invoked op");
assert_eq!(
actual,
json!({
"data": null,
"hash": null,
"scriptKind": 0,
})
)
}
#[tokio::test]
async fn test_resolve() {
let mut state = setup(
Some(
ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts")
.unwrap(),
),
None,
None,
)
.await;
let actual = resolve(
&mut state,
json!({ "base": "https://deno.land/x/a.ts", "specifiers": [ "./b.ts" ]}),
)
.expect("should have invoked op");
assert_eq!(actual, json!([["https://deno.land/x/b.ts", ".ts"]]));
}
#[tokio::test]
async fn test_resolve_empty() {
let mut state = setup(
Some(
ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts")
.unwrap(),
),
None,
None,
)
.await;
let actual = resolve(
&mut state,
json!({ "base": "https://deno.land/x/a.ts", "specifiers": [ "./bad.ts" ]}),
).expect("should have not errored");
assert_eq!(actual, json!([["deno:///none.d.ts", ".d.ts"]]));
}
#[tokio::test]
async fn test_respond() {
let mut state = setup(None, None, None).await;
let actual = respond(
&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));
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 =
ModuleSpecifier::resolve_url_or_path("https://deno.land/x/a.ts").unwrap();
let actual = test_exec(&specifier)
.await
.expect("exec should not have errored");
assert!(actual.diagnostics.is_empty());
assert!(actual.emitted_files.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 =
ModuleSpecifier::resolve_url_or_path("file:///reexports.ts").unwrap();
let actual = test_exec(&specifier)
.await
.expect("exec should not have errored");
assert!(actual.diagnostics.is_empty());
assert!(actual.emitted_files.is_empty());
assert!(actual.maybe_tsbuildinfo.is_some());
assert_eq!(actual.stats.0.len(), 12);
}
#[tokio::test]
async fn fix_lib_ref() {
let specifier =
ModuleSpecifier::resolve_url_or_path("file:///libref.ts").unwrap();
let actual = test_exec(&specifier)
.await
.expect("exec should not have errored");
assert!(actual.diagnostics.is_empty());
}
}