diff options
Diffstat (limited to 'cli')
29 files changed, 568 insertions, 92 deletions
diff --git a/cli/build.rs b/cli/build.rs index c88c00376..8e15ef443 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 a8bd40e69..5ea92447b 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<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", ]; diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 42f3c0202..92858ba31 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 ca2a535ef..0d3bf748e 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<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(), }) } diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 491e402ad..00f49b05d 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<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 ¶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 74e1a6a60..37f8b6bce 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<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 } diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 359f1f24b..83958fde2 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::<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,7 +1916,7 @@ 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) @@ -1884,6 +1924,23 @@ fn op_dispose( #[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 { specifier: String, old_length: u32, @@ -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( diff --git a/cli/main.rs b/cli/main.rs index 92abf9778..20f9131bf 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 770f8b872..8ea8b7f88 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<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. diff --git a/cli/ops/runtime_compiler.rs b/cli/ops/runtime_compiler.rs index c2d5582e7..099f2d555 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 668c73058..3d4d67f53 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 00116e7e1..b9a08d5ca 100644 --- a/cli/tests/compiler_api_test.ts +++ b/cli/tests/compiler_api_test.ts @@ -93,6 +93,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() { const { diagnostics, files, ignoredOptions, stats } = await Deno.emit( diff --git a/cli/tests/config_types.ts b/cli/tests/config_types.ts new file mode 100644 index 000000000..f1a8d6583 --- /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 000000000..417b7b537 --- /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 000000000..3810d4534 --- /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 000000000..745bb7b20 --- /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 081ea40e5..af3c9167c 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 afc93764a..04c66625c 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(); @@ -123,6 +129,85 @@ fn lsp_init_tsconfig() { } #[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"); did_open( diff --git a/cli/tests/lsp/a.d.ts b/cli/tests/lsp/a.d.ts new file mode 100644 index 000000000..7f587e144 --- /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 000000000..9d4b96cb8 --- /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 000000000..ba7f3344d --- /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 000000000..105e23b37 --- /dev/null +++ b/cli/tests/reference_types.ts @@ -0,0 +1,3 @@ +/// <reference types="./subdir/types.d.ts" /> + +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 000000000..417b7b537 --- /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 000000000..2e7a80098 --- /dev/null +++ b/cli/tests/reference_types_remote.ts @@ -0,0 +1,3 @@ +/// <reference types="http://localhost:4545/cli/tests/subdir/types.d.ts" /> + +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 000000000..417b7b537 --- /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 000000000..7f587e144 --- /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 5794b494f..b1ab0174f 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 f87682fca..bfd5e8dbe 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<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(); @@ -301,6 +322,25 @@ fn op_emit(state: &mut State, args: Value) -> Result<Value, AnyError> { } #[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. specifier: String, @@ -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)], }; diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index be0ed012a..a2f3af176 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()"); |