feat(cli): support "types" when type checking (#10999)

Fixes #10677
This commit is contained in:
Kitson Kelly 2021-06-22 07:18:32 +10:00 committed by GitHub
parent cda15f2a98
commit 281c4cd8fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 636 additions and 92 deletions

View file

@ -163,6 +163,10 @@ fn create_compiler_snapshot(
}))
}),
);
js_runtime.register_op(
"op_cwd",
op_sync(move |_state, _args: Value, _: ()| Ok(json!("cache:///"))),
);
// using the same op that is used in `tsc.rs` for loading modules and reading
// files, but a slightly different implementation at build time.
js_runtime.register_op(

View file

@ -31,6 +31,14 @@ pub struct EmitConfigOptions {
pub jsx_fragment_factory: String,
}
/// There are certain compiler options that can impact what modules are part of
/// a module graph, which need to be deserialized into a structure for analysis.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
pub types: Option<Vec<String>>,
}
/// A structure that represents a set of options that were ignored and the
/// path those options came from.
#[derive(Debug, Clone, PartialEq)]
@ -90,7 +98,6 @@ pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[
"sourceMap",
"sourceRoot",
"target",
"types",
"useDefineForClassFields",
];

View file

@ -422,11 +422,25 @@ declare namespace Deno {
| "es2019"
| "es2020"
| "esnext";
/** List of names of type definitions to include. Defaults to `undefined`.
/** List of names of type definitions to include when type checking.
* Defaults to `undefined`.
*
* The type definitions are resolved according to the normal Deno resolution
* irrespective of if sources are provided on the call. Like other Deno
* modules, there is no "magical" resolution. For example:
* irrespective of if sources are provided on the call. In addition, unlike
* passing the `--config` option on startup, there is no base to resolve
* relative specifiers, so the specifiers here have to be fully qualified
* URLs or paths. For example:
*
* ```ts
* Deno.emit("./a.ts", {
* compilerOptions: {
* types: [
* "https://deno.land/x/pkg/types.d.ts",
* "/Users/me/pkg/types.d.ts",
* ]
* }
* });
* ```
*/
types?: string[];
/** Emit class fields with ECMAScript-standard semantics. Defaults to

View file

@ -10,6 +10,7 @@ use deno_core::serde_json::Value;
use deno_core::url::Url;
use deno_core::ModuleSpecifier;
use log::error;
use lsp::WorkspaceFolder;
use lspower::lsp;
use std::collections::BTreeMap;
use std::collections::HashMap;
@ -188,6 +189,7 @@ pub struct ConfigSnapshot {
pub client_capabilities: ClientCapabilities,
pub root_uri: Option<Url>,
pub settings: Settings,
pub workspace_folders: Option<Vec<lsp::WorkspaceFolder>>,
}
impl ConfigSnapshot {
@ -218,6 +220,7 @@ pub struct Config {
pub root_uri: Option<Url>,
settings: Arc<RwLock<Settings>>,
tx: mpsc::Sender<ConfigRequest>,
pub workspace_folders: Option<Vec<WorkspaceFolder>>,
}
impl Config {
@ -319,6 +322,7 @@ impl Config {
root_uri: None,
settings,
tx,
workspace_folders: None,
}
}
@ -343,6 +347,7 @@ impl Config {
.try_read()
.map_err(|_| anyhow!("Error reading settings."))?
.clone(),
workspace_folders: self.workspace_folders.clone(),
})
}

View file

@ -70,6 +70,7 @@ pub struct StateSnapshot {
pub assets: Assets,
pub config: ConfigSnapshot,
pub documents: DocumentCache,
pub maybe_config_uri: Option<ModuleSpecifier>,
pub module_registries: registries::ModuleRegistry,
pub performance: Performance,
pub sources: Sources,
@ -92,6 +93,9 @@ pub(crate) struct Inner {
module_registries: registries::ModuleRegistry,
/// The path to the module registries cache
module_registries_location: PathBuf,
/// An optional configuration file which has been specified in the client
/// options.
maybe_config_file: Option<ConfigFile>,
/// An optional URL which provides the location of a TypeScript configuration
/// file which will be used by the Deno LSP.
maybe_config_uri: Option<Url>,
@ -138,6 +142,7 @@ impl Inner {
config,
diagnostics_server,
documents: Default::default(),
maybe_config_file: Default::default(),
maybe_config_uri: Default::default(),
maybe_import_map: Default::default(),
maybe_import_map_uri: Default::default(),
@ -326,6 +331,7 @@ impl Inner {
LspError::internal_error()
})?,
documents: self.documents.clone(),
maybe_config_uri: self.maybe_config_uri.clone(),
module_registries: self.module_registries.clone(),
performance: self.performance.clone(),
sources: self.sources.clone(),
@ -477,6 +483,7 @@ impl Inner {
};
let (value, maybe_ignored_options) = config_file.as_compiler_options()?;
tsconfig.merge(&value);
self.maybe_config_file = Some(config_file);
self.maybe_config_uri = Some(config_url);
if let Some(ignored_options) = maybe_ignored_options {
// TODO(@kitsonk) turn these into diagnostics that can be sent to the
@ -2281,20 +2288,28 @@ impl Inner {
if !params.uris.is_empty() {
for identifier in &params.uris {
let specifier = self.url_map.normalize_url(&identifier.uri);
sources::cache(&specifier, &self.maybe_import_map)
.await
.map_err(|err| {
error!("{}", err);
LspError::internal_error()
})?;
}
} else {
sources::cache(&referrer, &self.maybe_import_map)
sources::cache(
&specifier,
&self.maybe_import_map,
&self.maybe_config_file,
)
.await
.map_err(|err| {
error!("{}", err);
LspError::internal_error()
})?;
}
} else {
sources::cache(
&referrer,
&self.maybe_import_map,
&self.maybe_config_file,
)
.await
.map_err(|err| {
error!("{}", err);
LspError::internal_error()
})?;
}
// now that we have dependencies loaded, we need to re-analyze them and
// invalidate some diagnostics

View file

@ -4,6 +4,7 @@ use super::analysis;
use super::text::LineIndex;
use super::tsc;
use crate::config_file::ConfigFile;
use crate::file_fetcher::get_source_from_bytes;
use crate::file_fetcher::map_content_type;
use crate::file_fetcher::SUPPORTED_SCHEMES;
@ -33,6 +34,7 @@ use tsc::NavigationTree;
pub async fn cache(
specifier: &ModuleSpecifier,
maybe_import_map: &Option<ImportMap>,
maybe_config_file: &Option<ConfigFile>,
) -> Result<(), AnyError> {
let program_state = Arc::new(ProgramState::build(Default::default()).await?);
let handler = Arc::new(Mutex::new(FetchHandler::new(
@ -41,6 +43,7 @@ pub async fn cache(
Permissions::allow_all(),
)?));
let mut builder = GraphBuilder::new(handler, maybe_import_map.clone(), None);
builder.analyze_config_file(maybe_config_file).await?;
builder.add(specifier, false).await
}

View file

@ -61,14 +61,18 @@ impl TsServer {
pub fn new() -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<Request>();
let _join_handle = thread::spawn(move || {
// TODO(@kitsonk) we need to allow displaying diagnostics here, but the
// current compiler snapshot sends them to stdio which would totally break
// the language server...
let mut ts_runtime = start(false).expect("could not start tsc");
let mut ts_runtime = load().expect("could not load tsc");
let runtime = create_basic_runtime();
runtime.block_on(async {
let mut started = false;
while let Some((req, state_snapshot, tx)) = rx.recv().await {
if !started {
// TODO(@kitsonk) need to reflect the debug state of the lsp here
start(&mut ts_runtime, false, &state_snapshot)
.expect("could not start tsc");
started = true;
}
let value = request(&mut ts_runtime, state_snapshot, req);
if tx.send(value).is_err() {
warn!("Unable to send result to client.");
@ -572,7 +576,7 @@ impl DocumentSpan {
line_index: &LineIndex,
language_server: &mut language_server::Inner,
) -> Option<lsp::LocationLink> {
let target_specifier = resolve_url(&self.file_name).unwrap();
let target_specifier = normalize_specifier(&self.file_name).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier.clone())
.await
@ -773,7 +777,7 @@ impl ImplementationLocation {
line_index: &LineIndex,
language_server: &mut language_server::Inner,
) -> lsp::Location {
let specifier = resolve_url(&self.document_span.file_name).unwrap();
let specifier = normalize_specifier(&self.document_span.file_name).unwrap();
let uri = language_server
.url_map
.normalize_specifier(&specifier)
@ -819,7 +823,7 @@ impl RenameLocations {
let mut text_document_edit_map: HashMap<Url, lsp::TextDocumentEdit> =
HashMap::new();
for location in self.locations.iter() {
let specifier = resolve_url(&location.document_span.file_name)?;
let specifier = normalize_specifier(&location.document_span.file_name)?;
let uri = language_server.url_map.normalize_specifier(&specifier)?;
// ensure TextDocumentEdit for `location.file_name`.
@ -982,7 +986,7 @@ impl FileTextChanges {
&self,
language_server: &mut language_server::Inner,
) -> Result<lsp::TextDocumentEdit, AnyError> {
let specifier = resolve_url(&self.file_name)?;
let specifier = normalize_specifier(&self.file_name)?;
let line_index = language_server.get_line_index(specifier.clone()).await?;
let edits = self
.text_changes
@ -1102,7 +1106,7 @@ impl ReferenceEntry {
line_index: &LineIndex,
language_server: &mut language_server::Inner,
) -> lsp::Location {
let specifier = resolve_url(&self.document_span.file_name).unwrap();
let specifier = normalize_specifier(&self.document_span.file_name).unwrap();
let uri = language_server
.url_map
.normalize_specifier(&specifier)
@ -1134,7 +1138,7 @@ impl CallHierarchyItem {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyItem> {
let target_specifier = resolve_url(&self.file).unwrap();
let target_specifier = normalize_specifier(&self.file).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier)
.await
@ -1153,7 +1157,7 @@ impl CallHierarchyItem {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> lsp::CallHierarchyItem {
let target_specifier = resolve_url(&self.file).unwrap();
let target_specifier = normalize_specifier(&self.file).unwrap();
let uri = language_server
.url_map
.normalize_specifier(&target_specifier)
@ -1234,7 +1238,7 @@ impl CallHierarchyIncomingCall {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyIncomingCall> {
let target_specifier = resolve_url(&self.from.file).unwrap();
let target_specifier = normalize_specifier(&self.from.file).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier)
.await
@ -1269,7 +1273,7 @@ impl CallHierarchyOutgoingCall {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyOutgoingCall> {
let target_specifier = resolve_url(&self.to.file).unwrap();
let target_specifier = normalize_specifier(&self.to.file).unwrap();
let target_line_index = language_server
.get_line_index(target_specifier)
.await
@ -1803,6 +1807,7 @@ struct State<'a> {
response: Option<Response>,
state_snapshot: StateSnapshot,
snapshots: HashMap<(ModuleSpecifier, Cow<'a, str>), String>,
specifiers: HashMap<String, String>,
}
impl<'a> State<'a> {
@ -1812,8 +1817,36 @@ impl<'a> State<'a> {
response: None,
state_snapshot,
snapshots: HashMap::default(),
specifiers: HashMap::default(),
}
}
/// If a normalized version of the specifier has been stored for tsc, this
/// will "restore" it for communicating back to the tsc language server,
/// otherwise it will just convert the specifier to a string.
fn denormalize_specifier(&self, specifier: &ModuleSpecifier) -> String {
let specifier_str = specifier.to_string();
self
.specifiers
.get(&specifier_str)
.unwrap_or(&specifier_str)
.to_string()
}
/// In certain situations, tsc can request "invalid" specifiers and this will
/// normalize and memoize the specifier.
fn normalize_specifier<S: AsRef<str>>(
&mut self,
specifier: S,
) -> Result<ModuleSpecifier, AnyError> {
let specifier_str = specifier.as_ref().replace(".d.ts.d.ts", ".d.ts");
if specifier_str != specifier.as_ref() {
self
.specifiers
.insert(specifier_str.clone(), specifier.as_ref().to_string());
}
ModuleSpecifier::parse(&specifier_str).map_err(|err| err.into())
}
}
/// If a snapshot is missing from the state cache, add it.
@ -1846,6 +1879,13 @@ fn cache_snapshot(
Ok(())
}
fn normalize_specifier<S: AsRef<str>>(
specifier: S,
) -> Result<ModuleSpecifier, AnyError> {
resolve_url(specifier.as_ref().replace(".d.ts.d.ts", ".d.ts").as_str())
.map_err(|err| err.into())
}
// buffer-less json_sync ops
fn op<F, V, R>(op_fn: F) -> Box<OpFn>
where
@ -1876,12 +1916,29 @@ fn op_dispose(
.state_snapshot
.performance
.mark("op_dispose", Some(&args));
let specifier = resolve_url(&args.specifier)?;
let specifier = state.normalize_specifier(&args.specifier)?;
state.snapshots.remove(&(specifier, args.version.into()));
state.state_snapshot.performance.measure(mark);
Ok(true)
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct SpecifierArgs {
specifier: String,
}
fn op_exists(state: &mut State, args: SpecifierArgs) -> Result<bool, AnyError> {
let mark = state
.state_snapshot
.performance
.mark("op_exists", Some(&args));
let specifier = state.normalize_specifier(args.specifier)?;
let result = state.state_snapshot.sources.contains_key(&specifier);
state.state_snapshot.performance.measure(mark);
Ok(result)
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct GetChangeRangeArgs {
@ -1901,7 +1958,7 @@ fn op_get_change_range(
.state_snapshot
.performance
.mark("op_get_change_range", Some(&args));
let specifier = resolve_url(&args.specifier)?;
let specifier = state.normalize_specifier(&args.specifier)?;
cache_snapshot(state, &specifier, args.version.clone())?;
let r = if let Some(current) = state
.snapshots
@ -1948,7 +2005,7 @@ fn op_get_length(
.state_snapshot
.performance
.mark("op_get_length", Some(&args));
let specifier = resolve_url(&args.specifier)?;
let specifier = state.normalize_specifier(args.specifier)?;
let r = if let Some(Some(asset)) = state.state_snapshot.assets.get(&specifier)
{
Ok(asset.length)
@ -1981,7 +2038,7 @@ fn op_get_text(
.state_snapshot
.performance
.mark("op_get_text", Some(&args));
let specifier = resolve_url(&args.specifier)?;
let specifier = state.normalize_specifier(args.specifier)?;
let content =
if let Some(Some(content)) = state.state_snapshot.assets.get(&specifier) {
content.text.clone()
@ -1997,6 +2054,20 @@ fn op_get_text(
Ok(text::slice(&content, args.start..args.end).to_string())
}
fn op_load(
state: &mut State,
args: SpecifierArgs,
) -> Result<Option<String>, AnyError> {
let mark = state
.state_snapshot
.performance
.mark("op_load", Some(&args));
let specifier = state.normalize_specifier(args.specifier)?;
let result = state.state_snapshot.sources.get_source(&specifier);
state.state_snapshot.performance.measure(mark);
Ok(result)
}
fn op_resolve(
state: &mut State,
args: ResolveArgs,
@ -2006,7 +2077,7 @@ fn op_resolve(
.performance
.mark("op_resolve", Some(&args));
let mut resolved = Vec::new();
let referrer = resolve_url(&args.base)?;
let referrer = state.normalize_specifier(&args.base)?;
let sources = &mut state.state_snapshot.sources;
if state.state_snapshot.documents.contains_key(&referrer) {
@ -2124,7 +2195,7 @@ fn op_script_version(
.state_snapshot
.performance
.mark("op_script_version", Some(&args));
let specifier = resolve_url(&args.specifier)?;
let specifier = state.normalize_specifier(args.specifier)?;
let r = if specifier.scheme() == "asset" {
if state.state_snapshot.assets.contains_key(&specifier) {
Ok(Some("1".to_string()))
@ -2151,7 +2222,7 @@ fn op_script_version(
/// Create and setup a JsRuntime based on a snapshot. It is expected that the
/// supplied snapshot is an isolate that contains the TypeScript language
/// server.
pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
fn load() -> Result<JsRuntime, AnyError> {
let mut runtime = JsRuntime::new(RuntimeOptions {
startup_snapshot: Some(tsc::compiler_snapshot()),
..Default::default()
@ -2164,20 +2235,36 @@ pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
}
runtime.register_op("op_dispose", op(op_dispose));
runtime.register_op("op_exists", op(op_exists));
runtime.register_op("op_get_change_range", op(op_get_change_range));
runtime.register_op("op_get_length", op(op_get_length));
runtime.register_op("op_get_text", op(op_get_text));
runtime.register_op("op_load", op(op_load));
runtime.register_op("op_resolve", op(op_resolve));
runtime.register_op("op_respond", op(op_respond));
runtime.register_op("op_script_names", op(op_script_names));
runtime.register_op("op_script_version", op(op_script_version));
runtime.sync_ops_cache();
let init_config = json!({ "debug": debug });
Ok(runtime)
}
/// Instruct a language server runtime to start the language server and provide
/// it with a minimal bootstrap configuration.
fn start(
runtime: &mut JsRuntime,
debug: bool,
state_snapshot: &StateSnapshot,
) -> Result<(), AnyError> {
let root_uri = state_snapshot
.config
.root_uri
.clone()
.unwrap_or_else(|| Url::parse("cache:///").unwrap());
let init_config = json!({ "debug": debug, "rootUri": root_uri });
let init_src = format!("globalThis.serverInit({});", init_config);
runtime.execute("[native code]", &init_src)?;
Ok(runtime)
runtime.execute("[native code]", &init_src)
}
#[derive(Debug, Serialize)]
@ -2369,7 +2456,7 @@ pub enum RequestMethod {
}
impl RequestMethod {
pub fn to_value(&self, id: usize) -> Value {
fn to_value(&self, state: &State, id: usize) -> Value {
match self {
RequestMethod::Configure(config) => json!({
"id": id,
@ -2386,7 +2473,7 @@ impl RequestMethod {
json!({
"id": id,
"method": "findRenameLocations",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
"findInStrings": find_in_strings,
"findInComments": find_in_comments,
@ -2406,7 +2493,7 @@ impl RequestMethod {
)) => json!({
"id": id,
"method": "getCodeFixes",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"startPosition": start_pos,
"endPosition": end_pos,
"errorCodes": error_codes,
@ -2414,7 +2501,7 @@ impl RequestMethod {
RequestMethod::GetCombinedCodeFix((specifier, fix_id)) => json!({
"id": id,
"method": "getCombinedCodeFix",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"fixId": fix_id,
}),
RequestMethod::GetCompletionDetails(args) => json!({
@ -2426,7 +2513,7 @@ impl RequestMethod {
json!({
"id": id,
"method": "getCompletions",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
"preferences": preferences,
})
@ -2434,13 +2521,13 @@ impl RequestMethod {
RequestMethod::GetDefinition((specifier, position)) => json!({
"id": id,
"method": "getDefinition",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
}),
RequestMethod::GetDiagnostics(specifiers) => json!({
"id": id,
"method": "getDiagnostics",
"specifiers": specifiers,
"specifiers": specifiers.iter().map(|s| state.denormalize_specifier(s)).collect::<Vec<String>>(),
}),
RequestMethod::GetDocumentHighlights((
specifier,
@ -2449,7 +2536,7 @@ impl RequestMethod {
)) => json!({
"id": id,
"method": "getDocumentHighlights",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
"filesToSearch": files_to_search,
}),
@ -2457,43 +2544,43 @@ impl RequestMethod {
json!({
"id": id,
"method": "getEncodedSemanticClassifications",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"span": span,
})
}
RequestMethod::GetImplementation((specifier, position)) => json!({
"id": id,
"method": "getImplementation",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
}),
RequestMethod::GetNavigationTree(specifier) => json!({
"id": id,
"method": "getNavigationTree",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
}),
RequestMethod::GetOutliningSpans(specifier) => json!({
"id": id,
"method": "getOutliningSpans",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
}),
RequestMethod::GetQuickInfo((specifier, position)) => json!({
"id": id,
"method": "getQuickInfo",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
}),
RequestMethod::GetReferences((specifier, position)) => json!({
"id": id,
"method": "getReferences",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
}),
RequestMethod::GetSignatureHelpItems((specifier, position, options)) => {
json!({
"id": id,
"method": "getSignatureHelpItems",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position,
"options": options,
})
@ -2502,7 +2589,7 @@ impl RequestMethod {
json!({
"id": id,
"method": "getSmartSelectionRange",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position
})
}
@ -2514,7 +2601,7 @@ impl RequestMethod {
json!({
"id": id,
"method": "prepareCallHierarchy",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position
})
}
@ -2525,7 +2612,7 @@ impl RequestMethod {
json!({
"id": id,
"method": "provideCallHierarchyIncomingCalls",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position
})
}
@ -2536,7 +2623,7 @@ impl RequestMethod {
json!({
"id": id,
"method": "provideCallHierarchyOutgoingCalls",
"specifier": specifier,
"specifier": state.denormalize_specifier(specifier),
"position": position
})
}
@ -2551,15 +2638,15 @@ pub fn request(
method: RequestMethod,
) -> Result<Value, AnyError> {
let performance = state_snapshot.performance.clone();
let id = {
let request_params = {
let op_state = runtime.op_state();
let mut op_state = op_state.borrow_mut();
let state = op_state.borrow_mut::<State>();
state.state_snapshot = state_snapshot;
state.last_id += 1;
state.last_id
let id = state.last_id;
method.to_value(state, id)
};
let request_params = method.to_value(id);
let mark = performance.mark("request", Some(request_params.clone()));
let request_src = format!("globalThis.serverRequest({});", request_params);
runtime.execute("[native_code]", &request_src)?;
@ -2632,7 +2719,9 @@ mod tests {
let temp_dir = TempDir::new().expect("could not create temp dir");
let location = temp_dir.path().join("deps");
let state_snapshot = mock_state_snapshot(sources, &location);
let mut runtime = start(debug).expect("could not start server");
let mut runtime = load().expect("could not start server");
start(&mut runtime, debug, &state_snapshot)
.expect("could not start server");
let ts_config = TsConfig::new(config);
assert_eq!(
request(

View file

@ -427,6 +427,9 @@ async fn info_command(
program_state.lockfile.clone(),
);
builder.add(&specifier, false).await?;
builder
.analyze_config_file(&program_state.maybe_config_file)
.await?;
let graph = builder.get_graph();
let info = graph.info()?;
@ -575,6 +578,9 @@ async fn create_module_graph_and_maybe_check(
program_state.lockfile.clone(),
);
builder.add(&module_specifier, false).await?;
builder
.analyze_config_file(&program_state.maybe_config_file)
.await?;
let module_graph = builder.get_graph();
if !program_state.flags.no_check {
@ -813,6 +819,9 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<(), AnyError> {
program_state.lockfile.clone(),
);
builder.add(&main_module, false).await?;
builder
.analyze_config_file(&program_state.maybe_config_file)
.await?;
let module_graph = builder.get_graph();
// Find all local files in graph
@ -1024,6 +1033,9 @@ async fn test_command(
for specifier in test_modules.iter() {
builder.add(specifier, false).await?;
}
builder
.analyze_config_file(&program_state.maybe_config_file)
.await?;
let graph = builder.get_graph();
for specifier in doc_modules {

View file

@ -7,6 +7,7 @@ use crate::ast::Location;
use crate::ast::ParsedModule;
use crate::checksum;
use crate::colors;
use crate::config_file::CompilerOptions;
use crate::config_file::ConfigFile;
use crate::config_file::IgnoredCompilerOptions;
use crate::config_file::TsConfig;
@ -31,11 +32,13 @@ use deno_core::error::AnyError;
use deno_core::error::Context;
use deno_core::futures::stream::FuturesUnordered;
use deno_core::futures::stream::StreamExt;
use deno_core::resolve_import;
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::url::Url;
@ -890,11 +893,20 @@ impl Graph {
vec![config.as_bytes(), version::deno().as_bytes().to_owned()];
let graph = Arc::new(Mutex::new(self));
let maybe_config_specifier =
if let Some(config_file) = &options.maybe_config_file {
ModuleSpecifier::from_file_path(&config_file.path).ok()
} else {
None
};
debug!("maybe_config_specifier: {:?}", maybe_config_specifier);
let response = tsc::exec(tsc::Request {
config: config.clone(),
debug: options.debug,
graph: graph.clone(),
hash_data,
maybe_config_specifier,
maybe_tsbuildinfo,
root_names,
})?;
@ -958,6 +970,11 @@ impl Graph {
})
}
/// Indicates if the module graph contains the supplied specifier or not.
pub fn contains(&self, specifier: &ModuleSpecifier) -> bool {
matches!(self.get_module(specifier), ModuleSlot::Module(_))
}
/// Emit the module graph in a specific format. This is specifically designed
/// to be an "all-in-one" API for access by the runtime, allowing both
/// emitting single modules as well as bundles, using Deno module resolution
@ -1025,6 +1042,7 @@ impl Graph {
debug: options.debug,
graph: graph.clone(),
hash_data,
maybe_config_specifier: None,
maybe_tsbuildinfo: None,
root_names,
})?;
@ -1829,26 +1847,7 @@ impl GraphBuilder {
specifier: &ModuleSpecifier,
is_dynamic: bool,
) -> Result<(), AnyError> {
self.fetch(specifier, &None, is_dynamic);
loop {
match self.pending.next().await {
Some(Err((specifier, err))) => {
self
.graph
.modules
.insert(specifier, ModuleSlot::Err(Arc::new(err)));
}
Some(Ok(cached_module)) => {
let is_root = &cached_module.specifier == specifier;
self.visit(cached_module, is_root, is_dynamic)?;
}
_ => {}
}
if self.pending.is_empty() {
break;
}
}
self.insert(specifier, is_dynamic).await?;
if !self.graph.roots.contains(specifier) {
self.graph.roots.push(specifier.clone());
@ -1862,6 +1861,53 @@ impl GraphBuilder {
Ok(())
}
/// Analyze compiler options, identifying any specifiers that need to be
/// resolved and added to the graph.
pub async fn analyze_compiler_options(
&mut self,
maybe_compiler_options: &Option<HashMap<String, Value>>,
) -> Result<(), AnyError> {
if let Some(user_config) = maybe_compiler_options {
if let Some(value) = user_config.get("types") {
let types: Vec<String> = serde_json::from_value(value.clone())?;
for specifier in types {
if let Ok(specifier) = resolve_url_or_path(&specifier) {
self.insert(&specifier, false).await?;
}
}
}
}
Ok(())
}
/// Analyze a config file, identifying any specifiers that need to be resolved
/// and added to the graph.
pub async fn analyze_config_file(
&mut self,
maybe_config_file: &Option<ConfigFile>,
) -> Result<(), AnyError> {
if let Some(config_file) = maybe_config_file {
let referrer = ModuleSpecifier::from_file_path(&config_file.path)
.map_err(|_| {
anyhow!("Could not convert file path: \"{:?}\"", config_file.path)
})?;
if let Some(compiler_options) = &config_file.json.compiler_options {
let compiler_options: CompilerOptions =
serde_json::from_value(compiler_options.clone())?;
if let Some(types) = compiler_options.types {
for specifier in types {
if let Ok(specifier) =
resolve_import(&specifier, &referrer.to_string())
{
self.insert(&specifier, false).await?;
}
}
}
}
}
Ok(())
}
/// Request a module to be fetched from the handler and queue up its future
/// to be awaited to be resolved.
fn fetch(
@ -1882,6 +1928,37 @@ impl GraphBuilder {
}
}
/// An internal method that fetches the specifier and recursively fetches any
/// of the dependencies, adding them to the graph.
async fn insert(
&mut self,
specifier: &ModuleSpecifier,
is_dynamic: bool,
) -> Result<(), AnyError> {
self.fetch(specifier, &None, is_dynamic);
loop {
match self.pending.next().await {
Some(Err((specifier, err))) => {
self
.graph
.modules
.insert(specifier, ModuleSlot::Err(Arc::new(err)));
}
Some(Ok(cached_module)) => {
let is_root = &cached_module.specifier == specifier;
self.visit(cached_module, is_root, is_dynamic)?;
}
_ => {}
}
if self.pending.is_empty() {
break;
}
}
Ok(())
}
/// Visit a module that has been fetched, hydrating the module, analyzing its
/// dependencies if required, fetching those dependencies, and inserting the
/// module into the graph.

View file

@ -108,6 +108,9 @@ async fn op_emit(
&root_specifier
))
})?;
builder
.analyze_compiler_options(&args.compiler_options)
.await?;
let bundle_type = match args.bundle {
Some(RuntimeBundleType::Module) => BundleType::Module,
Some(RuntimeBundleType::Classic) => BundleType::Classic,

View file

@ -174,6 +174,7 @@ impl ProgramState {
for specifier in specifiers {
builder.add(&specifier, false).await?;
}
builder.analyze_config_file(&self.maybe_config_file).await?;
let mut graph = builder.get_graph();
let debug = self.flags.log_level == Some(log::Level::Debug);
@ -248,6 +249,7 @@ impl ProgramState {
let mut builder =
GraphBuilder::new(handler, maybe_import_map, self.lockfile.clone());
builder.add(&specifier, is_dynamic).await?;
builder.analyze_config_file(&self.maybe_config_file).await?;
let mut graph = builder.get_graph();
let debug = self.flags.log_level == Some(log::Level::Debug);
let maybe_config_file = self.maybe_config_file.clone();

View file

@ -92,6 +92,56 @@ Deno.test({
},
});
Deno.test({
name: "Deno.emit() - type references can be loaded",
async fn() {
const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
"file:///a.ts",
{
sources: {
"file:///a.ts": `/// <reference types="./b.d.ts" />
const b = new B();
console.log(b.b);`,
"file:///b.d.ts": `declare class B {
b: string;
}`,
},
},
);
assertEquals(diagnostics.length, 0);
assert(!ignoredOptions);
assertEquals(stats.length, 12);
const keys = Object.keys(files).sort();
assertEquals(keys, ["file:///a.ts.js", "file:///a.ts.js.map"]);
},
});
Deno.test({
name: "Deno.emit() - compilerOptions.types",
async fn() {
const { diagnostics, files, ignoredOptions, stats } = await Deno.emit(
"file:///a.ts",
{
compilerOptions: {
types: ["file:///b.d.ts"],
},
sources: {
"file:///a.ts": `const b = new B();
console.log(b.b);`,
"file:///b.d.ts": `declare class B {
b: string;
}`,
},
},
);
assertEquals(diagnostics.length, 0);
assert(!ignoredOptions);
assertEquals(stats.length, 12);
const keys = Object.keys(files).sort();
assertEquals(keys, ["file:///a.ts.js", "file:///a.ts.js.map"]);
},
});
Deno.test({
name: "Deno.emit() - import maps",
async fn() {

View file

@ -0,0 +1 @@
console.log(globalThis.a);

View file

@ -0,0 +1 @@
undefined

View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": [
"./subdir/types.d.ts"
]
}
}

View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": [
"http://localhost:4545/cli/tests/subdir/types.d.ts"
]
}
}

View file

@ -3421,7 +3421,19 @@ console.log("finish");
output: "config.ts.out",
});
itest!(emtpy_typescript {
itest!(config_types {
args:
"run --reload --quiet --config config_types.tsconfig.json config_types.ts",
output: "config_types.ts.out",
});
itest!(config_types_remote {
http_server: true,
args: "run --reload --quiet --config config_types_remote.tsconfig.json config_types.ts",
output: "config_types.ts.out",
});
itest!(empty_typescript {
args: "run --reload subdir/empty.ts",
output_str: Some("Check file:[WILDCARD]tests/subdir/empty.ts\n"),
});
@ -4123,6 +4135,17 @@ console.log("finish");
output: "redirect_cache.out",
});
itest!(reference_types {
args: "run --reload --quiet reference_types.ts",
output: "reference_types.ts.out",
});
itest!(references_types_remote {
http_server: true,
args: "run --reload --quiet reference_types_remote.ts",
output: "reference_types_remote.ts.out",
});
itest!(deno_doc_types_header_direct {
args: "doc --reload http://127.0.0.1:4545/xTypeScriptTypes.js",
output: "doc/types_header.out",

View file

@ -21,6 +21,12 @@ fn load_fixture(path: &str) -> Value {
serde_json::from_str(&fixture_str).unwrap()
}
fn load_fixture_str(path: &str) -> String {
let fixtures_path = root_path().join("cli/tests/lsp");
let path = fixtures_path.join(path);
fs::read_to_string(path).unwrap()
}
fn init(init_path: &str) -> LspClient {
let deno_exe = deno_exe_path();
let mut client = LspClient::new(&deno_exe).unwrap();
@ -122,6 +128,85 @@ fn lsp_init_tsconfig() {
shutdown(&mut client);
}
#[test]
fn lsp_tsconfig_types() {
let mut params: lsp::InitializeParams =
serde_json::from_value(load_fixture("initialize_params.json")).unwrap();
let temp_dir = TempDir::new().expect("could not create temp dir");
let tsconfig =
serde_json::to_vec_pretty(&load_fixture("types.tsconfig.json")).unwrap();
fs::write(temp_dir.path().join("types.tsconfig.json"), tsconfig).unwrap();
let a_dts = load_fixture_str("a.d.ts");
fs::write(temp_dir.path().join("a.d.ts"), a_dts).unwrap();
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
if let Some(Value::Object(mut map)) = params.initialization_options {
map.insert("config".to_string(), json!("./types.tsconfig.json"));
params.initialization_options = Some(Value::Object(map));
}
let deno_exe = deno_exe_path();
let mut client = LspClient::new(&deno_exe).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
client.write_notification("initialized", json!({})).unwrap();
let diagnostics = did_open(
&mut client,
json!({
"textDocument": {
"uri": Url::from_file_path(temp_dir.path().join("test.ts")).unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(a);\n"
}
}),
);
let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics);
assert_eq!(diagnostics.count(), 0);
shutdown(&mut client);
}
#[test]
fn lsp_triple_slash_types() {
let mut params: lsp::InitializeParams =
serde_json::from_value(load_fixture("initialize_params.json")).unwrap();
let temp_dir = TempDir::new().expect("could not create temp dir");
let a_dts = load_fixture_str("a.d.ts");
fs::write(temp_dir.path().join("a.d.ts"), a_dts).unwrap();
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
let deno_exe = deno_exe_path();
let mut client = LspClient::new(&deno_exe).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
client.write_notification("initialized", json!({})).unwrap();
let diagnostics = did_open(
&mut client,
json!({
"textDocument": {
"uri": Url::from_file_path(temp_dir.path().join("test.ts")).unwrap(),
"languageId": "typescript",
"version": 1,
"text": "/// <reference types=\"./a.d.ts\" />\n\nconsole.log(a);\n"
}
}),
);
let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics);
assert_eq!(diagnostics.count(), 0);
shutdown(&mut client);
}
#[test]
fn lsp_hover() {
let mut client = init("initialize_params.json");

1
cli/tests/lsp/a.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare var a: string;

1
cli/tests/lsp/b.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare var b: string;

View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"types": [
"./a.d.ts"
]
}
}

View file

@ -0,0 +1,3 @@
/// <reference types="./subdir/types.d.ts" />
console.log(globalThis.a);

View file

@ -0,0 +1 @@
undefined

View file

@ -0,0 +1,3 @@
/// <reference types="http://localhost:4545/cli/tests/subdir/types.d.ts" />
console.log(globalThis.a);

View file

@ -0,0 +1 @@
undefined

1
cli/tests/subdir/types.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare var a: string;

View file

@ -125,6 +125,9 @@ pub async fn print_docs(
program_state.lockfile.clone(),
);
builder.add(&root_specifier, false).await?;
builder
.analyze_config_file(&program_state.maybe_config_file)
.await?;
let graph = builder.get_graph();
let doc_parser = doc::DocParser::new(Box::new(graph), private);

View file

@ -12,6 +12,7 @@ use deno_core::error::AnyError;
use deno_core::error::Context;
use deno_core::op_sync;
use deno_core::resolve_url_or_path;
use deno_core::serde::de;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde_json;
@ -179,6 +180,7 @@ pub struct Request {
pub debug: bool,
pub graph: Arc<Mutex<Graph>>,
pub hash_data: Vec<Vec<u8>>,
pub maybe_config_specifier: Option<ModuleSpecifier>,
pub maybe_tsbuildinfo: Option<String>,
/// A vector of strings that represent the root/entry point modules for the
/// program.
@ -203,6 +205,7 @@ struct State {
hash_data: Vec<Vec<u8>>,
emitted_files: Vec<EmittedFile>,
graph: Arc<Mutex<Graph>>,
maybe_config_specifier: Option<ModuleSpecifier>,
maybe_tsbuildinfo: Option<String>,
maybe_response: Option<RespondArgs>,
root_map: HashMap<String, ModuleSpecifier>,
@ -212,6 +215,7 @@ impl State {
pub fn new(
graph: Arc<Mutex<Graph>>,
hash_data: Vec<Vec<u8>>,
maybe_config_specifier: Option<ModuleSpecifier>,
maybe_tsbuildinfo: Option<String>,
root_map: HashMap<String, ModuleSpecifier>,
data_url_map: HashMap<String, ModuleSpecifier>,
@ -221,6 +225,7 @@ impl State {
hash_data,
emitted_files: Default::default(),
graph,
maybe_config_specifier,
maybe_tsbuildinfo,
maybe_response: None,
root_map,
@ -228,9 +233,16 @@ impl State {
}
}
fn op<F>(op_fn: F) -> Box<OpFn>
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())
}
fn op<F, V, R>(op_fn: F) -> Box<OpFn>
where
F: Fn(&mut State, Value) -> Result<Value, AnyError> + 'static,
F: Fn(&mut State, V) -> Result<R, AnyError> + 'static,
V: de::DeserializeOwned,
R: Serialize + 'static,
{
op_sync(move |s, args, _: ()| {
let state = s.borrow_mut::<State>();
@ -255,6 +267,15 @@ fn op_create_hash(state: &mut State, args: Value) -> Result<Value, AnyError> {
Ok(json!({ "hash": hash }))
}
fn op_cwd(state: &mut State, _args: Value) -> Result<String, AnyError> {
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 {
@ -285,7 +306,7 @@ fn op_emit(state: &mut State, args: Value) -> Result<Value, AnyError> {
} else if let Some(remapped_specifier) = state.root_map.get(s) {
remapped_specifier.clone()
} else {
resolve_url_or_path(s).unwrap()
normalize_specifier(s).unwrap()
}
})
.collect();
@ -300,6 +321,25 @@ fn op_emit(state: &mut State, args: Value) -> Result<Value, AnyError> {
Ok(json!(true))
}
#[derive(Debug, Deserialize)]
struct ExistsArgs {
/// The fully qualified specifier that should be loaded.
specifier: String,
}
fn op_exists(state: &mut State, args: ExistsArgs) -> Result<bool, AnyError> {
if let Ok(specifier) = normalize_specifier(&args.specifier) {
if specifier.scheme() == "asset" || specifier.scheme() == "data" {
Ok(true)
} else {
let graph = state.graph.lock().unwrap();
Ok(graph.contains(&specifier))
}
} else {
Ok(false)
}
}
#[derive(Debug, Deserialize)]
struct LoadArgs {
/// The fully qualified specifier that should be loaded.
@ -309,7 +349,7 @@ struct LoadArgs {
fn op_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 = resolve_url_or_path(&v.specifier)
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;
@ -372,7 +412,7 @@ fn op_resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
} else if let Some(remapped_base) = state.root_map.get(&v.base) {
remapped_base.clone()
} else {
resolve_url_or_path(&v.base).context(
normalize_specifier(&v.base).context(
"Error converting a string module specifier for \"op_resolve\".",
)?
};
@ -490,14 +530,17 @@ pub fn exec(request: Request) -> Result<Response, AnyError> {
op_state.put(State::new(
request.graph.clone(),
request.hash_data.clone(),
request.maybe_config_specifier.clone(),
request.maybe_tsbuildinfo.clone(),
root_map,
data_url_map,
));
}
runtime.register_op("op_cwd", op(op_cwd));
runtime.register_op("op_create_hash", op(op_create_hash));
runtime.register_op("op_emit", op(op_emit));
runtime.register_op("op_exists", op(op_exists));
runtime.register_op("op_load", op(op_load));
runtime.register_op("op_resolve", op(op_resolve));
runtime.register_op("op_respond", op(op_respond));
@ -573,6 +616,7 @@ mod tests {
State::new(
graph,
hash_data,
None,
maybe_tsbuildinfo,
HashMap::new(),
HashMap::new(),
@ -614,6 +658,7 @@ mod tests {
debug: false,
graph,
hash_data,
maybe_config_specifier: None,
maybe_tsbuildinfo: None,
root_names: vec![(specifier.clone(), MediaType::TypeScript)],
};

View file

@ -19,6 +19,9 @@ delete Object.prototype.__proto__;
let logDebug = false;
let logSource = "JS";
/** @type {string=} */
let cwd;
// The map from the normalized specifier to the original.
// TypeScript normalizes the specifier in its internal processing,
// but the original specifier is needed when looking up the source from the runtime.
@ -130,7 +133,6 @@ delete Object.prototype.__proto__;
// analysis in Rust operates on fully resolved URLs,
// it makes sense to use the same scheme here.
const ASSETS = "asset:///";
const CACHE = "cache:///";
/** Diagnostics that are intentionally ignored when compiling TypeScript in
* Deno, as they provide misleading or incorrect information. */
@ -251,9 +253,10 @@ delete Object.prototype.__proto__;
*
* @type {ts.CompilerHost & ts.LanguageServiceHost} */
const host = {
fileExists(fileName) {
debug(`host.fileExists("${fileName}")`);
return false;
fileExists(specifier) {
debug(`host.fileExists("${specifier}")`);
specifier = normalizedToOriginalMap.get(specifier) ?? specifier;
return core.opSync("op_exists", { specifier });
},
readFile(specifier) {
debug(`host.readFile("${specifier}")`);
@ -317,7 +320,8 @@ delete Object.prototype.__proto__;
);
},
getCurrentDirectory() {
return CACHE;
debug(`host.getCurrentDirectory()`);
return cwd ?? core.opSync("op_cwd", null);
},
getCanonicalFileName(fileName) {
return fileName;
@ -787,12 +791,13 @@ delete Object.prototype.__proto__;
}
}
/** @param {{ debug: boolean; }} init */
function serverInit({ debug: debugFlag }) {
/** @param {{ debug: boolean; rootUri?: string; }} init */
function serverInit({ debug: debugFlag, rootUri }) {
if (hasStarted) {
throw new Error("The language server has already been initialized.");
}
hasStarted = true;
cwd = rootUri;
languageService = ts.createLanguageService(host);
setLogDebug(debugFlag, "TSLS");
debug("serverInit()");

View file

@ -197,3 +197,10 @@ The biggest "danger" when doing something like this, is that the type checking
is significantly looser, and there is no way to validate that you are doing
sufficient and effective feature detection in your code, which may lead to what
could be trivial errors becoming runtime errors.
### Using the "types" property
The `"types"` property in `"compilerOptions"` can be used to specify arbitrary
type definitions to include when type checking a programme. For more information
on this see
[Using ambient or global types](./types#using-ambient-or-global-types).

View file

@ -101,6 +101,67 @@ When seeing this header, Deno would attempt to retrieve
`https://example.com/coolLib.d.ts` and use that when type checking the original
module.
### Using ambient or global types
Overall it is better to use module/UMD type definitions with Deno, where a
module expressly imports the types it depends upon. Modular type definitions can
express
[augmentation of the global scope](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html)
via the `declare global` in the type definition. For example:
```ts
declare global {
var AGlobalString: string;
}
```
This would make `AGlobalString` available in the global namespace when importing
the type definition.
In some cases though, when leveraging other existing type libraries, it may not
be possible to leverage modular type definitions. Therefore there are ways to
include arbitrary type definitions when type checking programmes.
#### Using a triple-slash directive
This option couples the type definitions to the code itself. By adding a
triple-slash `types` directive near the type of a module, type checking the file
will include the type definition. For example:
```ts
/// <reference types="./types.d.ts" />
```
The specifier provided is resolved just like any other specifier in Deno, which
means it requires an extension, and is relative to the module referencing it. It
can be a fully qualified URL as well:
```ts
/// <reference types="https://deno.land/x/pkg@1.0.0/types.d.ts" />
```
#### Using a `tsconfig.json` file
Another option is to use a `tsconfig.json` file that is configured to include
the type definitions, by supplying a `"types"` value to the `"compilerOptions"`.
For example:
```json
{
"compilerOptions": {
"types": [
"./types.d.ts",
"https://deno.land/x/pkg@1.0.0/types.d.ts",
"/Users/me/pkg/types.d.ts"
]
}
}
```
Like the triple-slash reference above, the specifier supplied in the `"types"`
array will be resolved like other specifiers in Deno. In the case of relative
specifiers, it will be resolved relative to the path to the `tsconfig.json`.
### Type Checking Web Workers
When Deno loads a TypeScript module in a web worker, it will automatically type