From 281c4cd8fcf5fd54f558a6922736def2c7804529 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Tue, 22 Jun 2021 07:18:32 +1000 Subject: [PATCH] feat(cli): support "types" when type checking (#10999) Fixes #10677 --- cli/build.rs | 4 + cli/config_file.rs | 9 +- cli/dts/lib.deno.unstable.d.ts | 20 ++- cli/lsp/config.rs | 5 + cli/lsp/language_server.rs | 33 +++- cli/lsp/sources.rs | 3 + cli/lsp/tsc.rs | 181 +++++++++++++++----- cli/main.rs | 12 ++ cli/module_graph.rs | 117 ++++++++++--- cli/ops/runtime_compiler.rs | 3 + cli/program_state.rs | 2 + cli/tests/compiler_api_test.ts | 50 ++++++ cli/tests/config_types.ts | 1 + cli/tests/config_types.ts.out | 1 + cli/tests/config_types.tsconfig.json | 7 + cli/tests/config_types_remote.tsconfig.json | 7 + cli/tests/integration_tests.rs | 25 ++- cli/tests/integration_tests_lsp.rs | 85 +++++++++ cli/tests/lsp/a.d.ts | 1 + cli/tests/lsp/b.d.ts | 1 + cli/tests/lsp/types.tsconfig.json | 7 + cli/tests/reference_types.ts | 3 + cli/tests/reference_types.ts.out | 1 + cli/tests/reference_types_remote.ts | 3 + cli/tests/reference_types_remote.ts.out | 1 + cli/tests/subdir/types.d.ts | 1 + cli/tools/doc.rs | 3 + cli/tsc.rs | 55 +++++- cli/tsc/99_main_compiler.js | 19 +- docs/typescript/configuration.md | 7 + docs/typescript/types.md | 61 +++++++ 31 files changed, 636 insertions(+), 92 deletions(-) create mode 100644 cli/tests/config_types.ts create mode 100644 cli/tests/config_types.ts.out create mode 100644 cli/tests/config_types.tsconfig.json create mode 100644 cli/tests/config_types_remote.tsconfig.json create mode 100644 cli/tests/lsp/a.d.ts create mode 100644 cli/tests/lsp/b.d.ts create mode 100644 cli/tests/lsp/types.tsconfig.json create mode 100644 cli/tests/reference_types.ts create mode 100644 cli/tests/reference_types.ts.out create mode 100644 cli/tests/reference_types_remote.ts create mode 100644 cli/tests/reference_types_remote.ts.out create mode 100644 cli/tests/subdir/types.d.ts diff --git a/cli/build.rs b/cli/build.rs index c88c003760..8e15ef4433 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -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( diff --git a/cli/config_file.rs b/cli/config_file.rs index a8bd40e699..5ea92447b6 100644 --- a/cli/config_file.rs +++ b/cli/config_file.rs @@ -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>, +} + /// 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", ]; diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 42f3c0202a..92858ba311 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -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 diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index ca2a535ef2..0d3bf748ee 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -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, pub settings: Settings, + pub workspace_folders: Option>, } impl ConfigSnapshot { @@ -218,6 +220,7 @@ pub struct Config { pub root_uri: Option, settings: Arc>, tx: mpsc::Sender, + pub workspace_folders: Option>, } 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(), }) } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 491e402ad3..00f49b05d3 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -70,6 +70,7 @@ pub struct StateSnapshot { pub assets: Assets, pub config: ConfigSnapshot, pub documents: DocumentCache, + pub maybe_config_uri: Option, 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, /// An optional URL which provides the location of a TypeScript configuration /// file which will be used by the Deno LSP. maybe_config_uri: Option, @@ -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 ¶ms.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 diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs index 74e1a6a606..37f8b6bcef 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -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, + maybe_config_file: &Option, ) -> 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 } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 359f1f24be..83958fde2f 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -61,14 +61,18 @@ impl TsServer { pub fn new() -> Self { let (tx, mut rx) = mpsc::unbounded_channel::(); 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 { - 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 = 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 { - 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 { - 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 { - 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 { - 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, state_snapshot: StateSnapshot, snapshots: HashMap<(ModuleSpecifier, Cow<'a, str>), String>, + specifiers: HashMap, } 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>( + &mut self, + specifier: S, + ) -> Result { + 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>( + specifier: S, +) -> Result { + 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(op_fn: F) -> Box 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 { + 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, 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 { +fn load() -> Result { let mut runtime = JsRuntime::new(RuntimeOptions { startup_snapshot: Some(tsc::compiler_snapshot()), ..Default::default() @@ -2164,20 +2235,36 @@ pub fn start(debug: bool) -> Result { } 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::>(), }), 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 { 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_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( diff --git a/cli/main.rs b/cli/main.rs index 92abf9778b..20f9131bfb 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -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 { diff --git a/cli/module_graph.rs b/cli/module_graph.rs index 770f8b8722..8ea8b7f886 100644 --- a/cli/module_graph.rs +++ b/cli/module_graph.rs @@ -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>, + ) -> Result<(), AnyError> { + if let Some(user_config) = maybe_compiler_options { + if let Some(value) = user_config.get("types") { + let types: Vec = 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, + ) -> 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. diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index c2d5582e7c..099f2d5559 100644 --- a/cli/ops/runtime_compiler.rs +++ b/cli/ops/runtime_compiler.rs @@ -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, diff --git a/cli/program_state.rs b/cli/program_state.rs index 668c730580..3d4d67f53a 100644 --- a/cli/program_state.rs +++ b/cli/program_state.rs @@ -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(); diff --git a/cli/tests/compiler_api_test.ts b/cli/tests/compiler_api_test.ts index 00116e7e1d..b9a08d5caf 100644 --- a/cli/tests/compiler_api_test.ts +++ b/cli/tests/compiler_api_test.ts @@ -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": `/// + 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() { diff --git a/cli/tests/config_types.ts b/cli/tests/config_types.ts new file mode 100644 index 0000000000..f1a8d65831 --- /dev/null +++ b/cli/tests/config_types.ts @@ -0,0 +1 @@ +console.log(globalThis.a); diff --git a/cli/tests/config_types.ts.out b/cli/tests/config_types.ts.out new file mode 100644 index 0000000000..417b7b5370 --- /dev/null +++ b/cli/tests/config_types.ts.out @@ -0,0 +1 @@ +undefined diff --git a/cli/tests/config_types.tsconfig.json b/cli/tests/config_types.tsconfig.json new file mode 100644 index 0000000000..3810d45341 --- /dev/null +++ b/cli/tests/config_types.tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": [ + "./subdir/types.d.ts" + ] + } +} diff --git a/cli/tests/config_types_remote.tsconfig.json b/cli/tests/config_types_remote.tsconfig.json new file mode 100644 index 0000000000..745bb7b20f --- /dev/null +++ b/cli/tests/config_types_remote.tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": [ + "http://localhost:4545/cli/tests/subdir/types.d.ts" + ] + } +} diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 081ea40e51..af3c9167cc 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -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", diff --git a/cli/tests/integration_tests_lsp.rs b/cli/tests/integration_tests_lsp.rs index afc93764a7..04c66625cd 100644 --- a/cli/tests/integration_tests_lsp.rs +++ b/cli/tests/integration_tests_lsp.rs @@ -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": "/// \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"); diff --git a/cli/tests/lsp/a.d.ts b/cli/tests/lsp/a.d.ts new file mode 100644 index 0000000000..7f587e1449 --- /dev/null +++ b/cli/tests/lsp/a.d.ts @@ -0,0 +1 @@ +declare var a: string; diff --git a/cli/tests/lsp/b.d.ts b/cli/tests/lsp/b.d.ts new file mode 100644 index 0000000000..9d4b96cb86 --- /dev/null +++ b/cli/tests/lsp/b.d.ts @@ -0,0 +1 @@ +declare var b: string; diff --git a/cli/tests/lsp/types.tsconfig.json b/cli/tests/lsp/types.tsconfig.json new file mode 100644 index 0000000000..ba7f3344d3 --- /dev/null +++ b/cli/tests/lsp/types.tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "types": [ + "./a.d.ts" + ] + } +} diff --git a/cli/tests/reference_types.ts b/cli/tests/reference_types.ts new file mode 100644 index 0000000000..105e23b37e --- /dev/null +++ b/cli/tests/reference_types.ts @@ -0,0 +1,3 @@ +/// + +console.log(globalThis.a); diff --git a/cli/tests/reference_types.ts.out b/cli/tests/reference_types.ts.out new file mode 100644 index 0000000000..417b7b5370 --- /dev/null +++ b/cli/tests/reference_types.ts.out @@ -0,0 +1 @@ +undefined diff --git a/cli/tests/reference_types_remote.ts b/cli/tests/reference_types_remote.ts new file mode 100644 index 0000000000..2e7a80098c --- /dev/null +++ b/cli/tests/reference_types_remote.ts @@ -0,0 +1,3 @@ +/// + +console.log(globalThis.a); diff --git a/cli/tests/reference_types_remote.ts.out b/cli/tests/reference_types_remote.ts.out new file mode 100644 index 0000000000..417b7b5370 --- /dev/null +++ b/cli/tests/reference_types_remote.ts.out @@ -0,0 +1 @@ +undefined diff --git a/cli/tests/subdir/types.d.ts b/cli/tests/subdir/types.d.ts new file mode 100644 index 0000000000..7f587e1449 --- /dev/null +++ b/cli/tests/subdir/types.d.ts @@ -0,0 +1 @@ +declare var a: string; diff --git a/cli/tools/doc.rs b/cli/tools/doc.rs index 5794b494f4..b1ab0174f9 100644 --- a/cli/tools/doc.rs +++ b/cli/tools/doc.rs @@ -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); diff --git a/cli/tsc.rs b/cli/tsc.rs index f87682fcab..bfd5e8dbe1 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -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>, pub hash_data: Vec>, + pub maybe_config_specifier: Option, pub maybe_tsbuildinfo: Option, /// A vector of strings that represent the root/entry point modules for the /// program. @@ -203,6 +205,7 @@ struct State { hash_data: Vec>, emitted_files: Vec, graph: Arc>, + maybe_config_specifier: Option, maybe_tsbuildinfo: Option, maybe_response: Option, root_map: HashMap, @@ -212,6 +215,7 @@ impl State { pub fn new( graph: Arc>, hash_data: Vec>, + maybe_config_specifier: Option, maybe_tsbuildinfo: Option, root_map: HashMap, data_url_map: HashMap, @@ -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(op_fn: F) -> Box +fn normalize_specifier(specifier: &str) -> Result { + resolve_url_or_path(&specifier.replace(".d.ts.d.ts", ".d.ts")) + .map_err(|err| err.into()) +} + +fn op(op_fn: F) -> Box where - F: Fn(&mut State, Value) -> Result + 'static, + F: Fn(&mut State, V) -> Result + 'static, + V: de::DeserializeOwned, + R: Serialize + 'static, { op_sync(move |s, args, _: ()| { let state = s.borrow_mut::(); @@ -255,6 +267,15 @@ fn op_create_hash(state: &mut State, args: Value) -> Result { Ok(json!({ "hash": hash })) } +fn op_cwd(state: &mut State, _args: Value) -> Result { + 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 { } 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 { 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 { + 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 { 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 = None; let mut media_type = MediaType::Unknown; @@ -372,7 +412,7 @@ fn op_resolve(state: &mut State, args: Value) -> Result { } 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 { 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)], }; diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index be0ed012a1..a2f3af176d 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -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()"); diff --git a/docs/typescript/configuration.md b/docs/typescript/configuration.md index ed5af56d78..752b6da71c 100644 --- a/docs/typescript/configuration.md +++ b/docs/typescript/configuration.md @@ -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). diff --git a/docs/typescript/types.md b/docs/typescript/types.md index 88317cf257..ba5462dc74 100644 --- a/docs/typescript/types.md +++ b/docs/typescript/types.md @@ -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 +/// +``` + +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 +/// +``` + +#### 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