From 8dd9d5f5239f9f842f7096a540f866bd4f10b72c Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 10 Jul 2023 17:45:09 -0400 Subject: [PATCH] refactor(lsp): move config file related code to config.rs (#19790) Will make #19788 easier. --- cli/cache/common.rs | 6 +- cli/cache/incremental.rs | 13 +-- cli/cache/node.rs | 5 +- cli/cache/parsed_source.rs | 2 +- cli/emit.rs | 4 +- cli/lsp/config.rs | 92 +++++++++++++++++ cli/lsp/documents.rs | 4 +- cli/lsp/language_server.rs | 200 +++++++++++-------------------------- cli/tools/check.rs | 10 +- cli/tsc/mod.rs | 9 +- 10 files changed, 176 insertions(+), 169 deletions(-) diff --git a/cli/cache/common.rs b/cli/cache/common.rs index 93ff91d507..3e2e862aa9 100644 --- a/cli/cache/common.rs +++ b/cli/cache/common.rs @@ -11,6 +11,10 @@ impl FastInsecureHasher { Self::default() } + pub fn hash(hashable: impl std::hash::Hash) -> u64 { + Self::new().write_hashable(hashable).finish() + } + pub fn write_str(&mut self, text: &str) -> &mut Self { self.write(text.as_bytes()); self @@ -33,7 +37,7 @@ impl FastInsecureHasher { pub fn write_hashable( &mut self, - hashable: &impl std::hash::Hash, + hashable: impl std::hash::Hash, ) -> &mut Self { hashable.hash(&mut self.0); self diff --git a/cli/cache/incremental.rs b/cli/cache/incremental.rs index c50b876fa9..04ac4243b9 100644 --- a/cli/cache/incremental.rs +++ b/cli/cache/incremental.rs @@ -72,9 +72,8 @@ impl IncrementalCacheInner { state: &TState, initial_file_paths: &[PathBuf], ) -> Self { - let state_hash = FastInsecureHasher::new() - .write_str(&serde_json::to_string(state).unwrap()) - .finish(); + let state_hash = + FastInsecureHasher::hash(serde_json::to_string(state).unwrap()); let sql_cache = SqlIncrementalCache::new(db, state_hash); Self::from_sql_incremental_cache(sql_cache, initial_file_paths) } @@ -114,15 +113,13 @@ impl IncrementalCacheInner { pub fn is_file_same(&self, file_path: &Path, file_text: &str) -> bool { match self.previous_hashes.get(file_path) { - Some(hash) => { - *hash == FastInsecureHasher::new().write_str(file_text).finish() - } + Some(hash) => *hash == FastInsecureHasher::hash(file_text), None => false, } } pub fn update_file(&self, file_path: &Path, file_text: &str) { - let hash = FastInsecureHasher::new().write_str(file_text).finish(); + let hash = FastInsecureHasher::hash(file_text); if let Some(previous_hash) = self.previous_hashes.get(file_path) { if *previous_hash == hash { return; // do not bother updating the db file because nothing has changed @@ -270,7 +267,7 @@ mod test { let sql_cache = SqlIncrementalCache::new(conn, 1); let file_path = PathBuf::from("/mod.ts"); let file_text = "test"; - let file_hash = FastInsecureHasher::new().write_str(file_text).finish(); + let file_hash = FastInsecureHasher::hash(file_text); sql_cache.set_source_hash(&file_path, file_hash).unwrap(); let cache = IncrementalCacheInner::from_sql_incremental_cache( sql_cache, diff --git a/cli/cache/node.rs b/cli/cache/node.rs index 298d81e2f1..825bdfef4a 100644 --- a/cli/cache/node.rs +++ b/cli/cache/node.rs @@ -49,10 +49,7 @@ impl NodeAnalysisCache { } pub fn compute_source_hash(text: &str) -> String { - FastInsecureHasher::new() - .write_str(text) - .finish() - .to_string() + FastInsecureHasher::hash(text).to_string() } fn ensure_ok(res: Result) -> T { diff --git a/cli/cache/parsed_source.rs b/cli/cache/parsed_source.rs index 6f9c2f38fe..e231753d5a 100644 --- a/cli/cache/parsed_source.rs +++ b/cli/cache/parsed_source.rs @@ -262,7 +262,7 @@ impl deno_graph::ModuleAnalyzer for ParsedSourceCacheModuleAnalyzer { } fn compute_source_hash(bytes: &[u8]) -> String { - FastInsecureHasher::new().write(bytes).finish().to_string() + FastInsecureHasher::hash(bytes).to_string() } #[cfg(test)] diff --git a/cli/emit.rs b/cli/emit.rs index b71ca97ab2..e81d2e83c6 100644 --- a/cli/emit.rs +++ b/cli/emit.rs @@ -26,9 +26,7 @@ impl Emitter { parsed_source_cache: Arc, emit_options: deno_ast::EmitOptions, ) -> Self { - let emit_options_hash = FastInsecureHasher::new() - .write_hashable(&emit_options) - .finish(); + let emit_options_hash = FastInsecureHasher::hash(&emit_options); Self { emit_cache, parsed_source_cache, diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 0a25e2b992..4d008a290c 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1,16 +1,22 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use super::logging::lsp_log; +use crate::args::ConfigFile; +use crate::lsp::logging::lsp_warn; +use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::path::specifier_to_file_path; use deno_core::error::AnyError; +use deno_core::parking_lot::Mutex; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::Value; use deno_core::ModuleSpecifier; +use deno_lockfile::Lockfile; use lsp::Url; use std::collections::BTreeMap; use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use tower_lsp::lsp_types as lsp; @@ -427,6 +433,16 @@ pub struct Settings { pub workspace: WorkspaceSettings, } +/// Contains the config file and dependent information. +#[derive(Debug)] +struct LspConfigFileInfo { + pub config_file: ConfigFile, + /// An optional deno.lock file, which is resolved relative to the config file. + pub maybe_lockfile: Option>>, + /// The canonicalized node_modules directory, which is found relative to the config file. + pub maybe_node_modules_dir: Option, +} + #[derive(Debug)] pub struct Config { pub client_capabilities: ClientCapabilities, @@ -434,6 +450,9 @@ pub struct Config { pub root_uri: Option, settings: Settings, pub workspace_folders: Option>, + /// An optional configuration file which has been specified in the client + /// options along with some data that is computed after the config file is set. + maybe_config_file_info: Option, } impl Config { @@ -445,9 +464,40 @@ impl Config { root_uri: None, settings: Default::default(), workspace_folders: None, + maybe_config_file_info: None, } } + pub fn maybe_node_modules_dir_path(&self) -> Option<&PathBuf> { + self + .maybe_config_file_info + .as_ref() + .and_then(|p| p.maybe_node_modules_dir.as_ref()) + } + + pub fn maybe_config_file(&self) -> Option<&ConfigFile> { + self.maybe_config_file_info.as_ref().map(|c| &c.config_file) + } + + pub fn maybe_lockfile(&self) -> Option<&Arc>> { + self + .maybe_config_file_info + .as_ref() + .and_then(|c| c.maybe_lockfile.as_ref()) + } + + pub fn clear_config_file(&mut self) { + self.maybe_config_file_info = None; + } + + pub fn set_config_file(&mut self, config_file: ConfigFile) { + self.maybe_config_file_info = Some(LspConfigFileInfo { + maybe_lockfile: resolve_lockfile_from_config(&config_file), + maybe_node_modules_dir: resolve_node_modules_dir(&config_file), + config_file, + }); + } + pub fn workspace_settings(&self) -> &WorkspaceSettings { &self.settings.workspace } @@ -662,6 +712,48 @@ impl Config { } } +fn resolve_lockfile_from_config( + config_file: &ConfigFile, +) -> Option>> { + let lockfile_path = match config_file.resolve_lockfile_path() { + Ok(Some(value)) => value, + Ok(None) => return None, + Err(err) => { + lsp_warn!("Error resolving lockfile: {:#}", err); + return None; + } + }; + resolve_lockfile_from_path(lockfile_path) +} + +fn resolve_node_modules_dir(config_file: &ConfigFile) -> Option { + // For the language server, require an explicit opt-in via the + // `nodeModulesDir: true` setting in the deno.json file. This is to + // reduce the chance of modifying someone's node_modules directory + // without them having asked us to do so. + if config_file.node_modules_dir() != Some(true) { + return None; + } + if config_file.specifier.scheme() != "file" { + return None; + } + let file_path = config_file.specifier.to_file_path().ok()?; + let node_modules_dir = file_path.parent()?.join("node_modules"); + canonicalize_path_maybe_not_exists(&node_modules_dir).ok() +} + +fn resolve_lockfile_from_path( + lockfile_path: PathBuf, +) -> Option>> { + match Lockfile::new(lockfile_path, false) { + Ok(value) => Some(Arc::new(Mutex::new(value))), + Err(err) => { + lsp_warn!("Error loading lockfile: {:#}", err); + None + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 11662a8fcd..1ac5934ffb 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -1192,7 +1192,7 @@ impl Documents { maybe_package_json_deps: Option<&PackageJsonDeps>, ) -> u64 { let mut hasher = FastInsecureHasher::default(); - hasher.write_hashable(&document_preload_limit); + hasher.write_hashable(document_preload_limit); hasher.write_hashable(&{ // ensure these are sorted so the hashing is deterministic let mut enabled_urls = enabled_urls.to_vec(); @@ -1203,7 +1203,7 @@ impl Documents { hasher.write_str(&import_map.to_json()); hasher.write_str(import_map.base_url().as_str()); } - hasher.write_hashable(&maybe_jsx_config); + hasher.write_hashable(maybe_jsx_config); if let Some(package_json_deps) = &maybe_package_json_deps { // We need to ensure the hashing is deterministic so explicitly type // this in order to catch if the type of package_json_deps ever changes diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index d8fa7a7b4f..0f1fc2460f 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -4,7 +4,6 @@ use deno_ast::MediaType; use deno_core::anyhow::anyhow; use deno_core::anyhow::Context; use deno_core::error::AnyError; -use deno_core::parking_lot::Mutex; use deno_core::resolve_url; use deno_core::serde_json; use deno_core::serde_json::json; @@ -101,7 +100,6 @@ use crate::npm::NpmCacheDir; use crate::npm::NpmResolution; use crate::tools::fmt::format_file; use crate::tools::fmt::format_parsed_source; -use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::fs::remove_dir_all_if_exists; use crate::util::path::specifier_to_file_path; use crate::util::progress_bar::ProgressBar; @@ -135,51 +133,15 @@ struct LspNpmConfigHash(u64); impl LspNpmConfigHash { pub fn from_inner(inner: &Inner) -> Self { let mut hasher = FastInsecureHasher::new(); - hasher.write_hashable(&inner.maybe_node_modules_dir_path()); + hasher.write_hashable(inner.config.maybe_node_modules_dir_path()); hasher.write_hashable(&inner.maybe_cache_path); - hash_lockfile_with_hasher(&mut hasher, inner.maybe_lockfile()); + if let Some(lockfile) = inner.config.maybe_lockfile() { + hasher.write_hashable(&*lockfile.lock()); + } Self(hasher.finish()) } } -fn hash_lockfile(maybe_lockfile: Option<&Arc>>) -> u64 { - let mut hasher = FastInsecureHasher::new(); - hash_lockfile_with_hasher(&mut hasher, maybe_lockfile); - hasher.finish() -} - -fn hash_lockfile_with_hasher( - hasher: &mut FastInsecureHasher, - maybe_lockfile: Option<&Arc>>, -) { - if let Some(lockfile) = &maybe_lockfile { - let lockfile = lockfile.lock(); - hasher.write_hashable(&*lockfile); - } -} - -fn resolve_lockfile_from_path( - lockfile_path: PathBuf, -) -> Option>> { - match Lockfile::new(lockfile_path, false) { - Ok(value) => Some(Arc::new(Mutex::new(value))), - Err(err) => { - lsp_warn!("Error loading lockfile: {:#}", err); - None - } - } -} - -/// Contains the config file and dependent information. -#[derive(Debug)] -struct LspConfigFileInfo { - config_file: ConfigFile, - /// An optional deno.lock file, which is resolved relative to the config file. - maybe_lockfile: Option>>, - /// The canonicalized node_modules directory, which is found relative to the config file. - maybe_node_modules_dir: Option, -} - #[derive(Debug, Clone)] pub struct LanguageServer(Arc>); @@ -219,9 +181,6 @@ pub struct Inner { /// An optional path to the DENO_DIR which has been specified in the client /// options. maybe_cache_path: Option, - /// An optional configuration file which has been specified in the client - /// options along with some data that is computed after the config file is set. - maybe_config_file_info: Option, /// An optional import map which is used to resolve modules. maybe_import_map: Option>, /// The URL for the import map which is used to determine relative imports. @@ -520,22 +479,6 @@ impl LanguageServer { } } -fn resolve_node_modules_dir(config_file: &ConfigFile) -> Option { - // For the language server, require an explicit opt-in via the - // `nodeModulesDir: true` setting in the deno.json file. This is to - // reduce the chance of modifying someone's node_modules directory - // without them having asked us to do so. - if config_file.node_modules_dir() != Some(true) { - return None; - } - if config_file.specifier.scheme() != "file" { - return None; - } - let file_path = config_file.specifier.to_file_path().ok()?; - let node_modules_dir = file_path.parent()?.join("node_modules"); - canonicalize_path_maybe_not_exists(&node_modules_dir).ok() -} - fn create_npm_api_and_cache( dir: &DenoDir, http_client: Arc, @@ -651,7 +594,6 @@ impl Inner { documents, http_client, maybe_cache_path: None, - maybe_config_file_info: None, maybe_import_map: None, maybe_import_map_uri: None, maybe_package_json: None, @@ -836,7 +778,7 @@ impl Inner { &self, tsconfig: &mut TsConfig, ) -> Result<(), AnyError> { - if let Some(config_file) = self.maybe_config_file() { + if let Some(config_file) = self.config.maybe_config_file() { let (value, maybe_ignored_options) = config_file.to_compiler_options()?; tsconfig.merge(&value); if let Some(ignored_options) = maybe_ignored_options { @@ -854,7 +796,7 @@ impl Inner { let npm_resolution = Arc::new(NpmResolution::new( self.npm.api.clone(), self.npm.resolution.snapshot(), - self.maybe_lockfile().cloned(), + self.config.maybe_lockfile().cloned(), )); let node_fs = Arc::new(deno_fs::RealFs); let npm_resolver = Arc::new(CliNpmResolver::new( @@ -866,10 +808,10 @@ impl Inner { &ProgressBar::new(ProgressBarStyle::TextOnly), self.npm.api.base_url().clone(), npm_resolution, - self.maybe_node_modules_dir_path().cloned(), + self.config.maybe_node_modules_dir_path().cloned(), NpmSystemInfo::default(), ), - self.maybe_lockfile().cloned(), + self.config.maybe_lockfile().cloned(), )); let node_resolver = Arc::new(NodeResolver::new(node_fs, npm_resolver.clone())); @@ -980,7 +922,7 @@ impl Inner { registry_url, &progress_bar, ); - let maybe_snapshot = match self.maybe_lockfile() { + let maybe_snapshot = match self.config.maybe_lockfile() { Some(lockfile) => { match snapshot_from_lockfile(lockfile.clone(), &self.npm.api).await { Ok(snapshot) => Some(snapshot), @@ -998,7 +940,7 @@ impl Inner { progress_bar, self.npm.api.clone(), self.npm.cache.clone(), - self.maybe_node_modules_dir_path().cloned(), + self.config.maybe_node_modules_dir_path().cloned(), maybe_snapshot, ); @@ -1035,7 +977,7 @@ impl Inner { ) -> Result { resolve_import_map_from_specifier( import_map_url, - self.maybe_config_file(), + self.config.maybe_config_file(), &self.create_file_fetcher(cache_setting), ) .await @@ -1076,7 +1018,7 @@ impl Inner { "Setting import map from workspace settings: \"{}\"", import_map_str ); - if let Some(config_file) = self.maybe_config_file() { + if let Some(config_file) = self.config.maybe_config_file() { if let Some(import_map_path) = config_file.to_import_map_path() { lsp_log!("Warning: Import map \"{}\" configured in \"{}\" being ignored due to an import map being explicitly configured in workspace settings.", import_map_path, config_file.specifier); } @@ -1102,7 +1044,7 @@ impl Inner { import_map_str )); } - } else if let Some(config_file) = self.maybe_config_file() { + } else if let Some(config_file) = self.config.maybe_config_file() { if config_file.is_an_import_map() { lsp_log!( "Setting import map defined in configuration file: \"{}\"", @@ -1165,7 +1107,7 @@ impl Inner { } async fn update_config_file(&mut self) -> Result<(), AnyError> { - self.maybe_config_file_info = None; + self.config.clear_config_file(); self.fmt_options = Default::default(); self.lint_options = Default::default(); if let Some(config_file) = self.get_config_file()? { @@ -1186,11 +1128,7 @@ impl Inner { anyhow!("Unable to update formatter configuration: {:?}", err) })?; - self.maybe_config_file_info = Some(LspConfigFileInfo { - maybe_lockfile: self.resolve_lockfile_from_config(&config_file), - maybe_node_modules_dir: resolve_node_modules_dir(&config_file), - config_file, - }); + self.config.set_config_file(config_file); self.lint_options = lint_options; self.fmt_options = fmt_options; } @@ -1198,45 +1136,12 @@ impl Inner { Ok(()) } - fn resolve_lockfile_from_config( - &self, - config_file: &ConfigFile, - ) -> Option>> { - let lockfile_path = match config_file.resolve_lockfile_path() { - Ok(Some(value)) => value, - Ok(None) => return None, - Err(err) => { - lsp_warn!("Error resolving lockfile: {:#}", err); - return None; - } - }; - resolve_lockfile_from_path(lockfile_path) - } - - fn maybe_node_modules_dir_path(&self) -> Option<&PathBuf> { - self - .maybe_config_file_info - .as_ref() - .and_then(|p| p.maybe_node_modules_dir.as_ref()) - } - - fn maybe_config_file(&self) -> Option<&ConfigFile> { - self.maybe_config_file_info.as_ref().map(|c| &c.config_file) - } - - fn maybe_lockfile(&self) -> Option<&Arc>> { - self - .maybe_config_file_info - .as_ref() - .and_then(|c| c.maybe_lockfile.as_ref()) - } - /// Updates the package.json. Always ensure this is done after updating /// the configuration file as the resolution of this depends on that. fn update_package_json(&mut self) -> Result<(), AnyError> { self.maybe_package_json = None; self.maybe_package_json = - self.get_package_json(self.maybe_config_file())?; + self.get_package_json(self.config.maybe_config_file())?; Ok(()) } @@ -1393,10 +1298,7 @@ impl Inner { .workspace_settings() .document_preload_limit, maybe_import_map: self.maybe_import_map.clone(), - maybe_config_file: self - .maybe_config_file_info - .as_ref() - .map(|c| &c.config_file), + maybe_config_file: self.config.maybe_config_file(), maybe_package_json: self.maybe_package_json.as_ref(), npm_registry_api: self.npm.api.clone(), npm_resolution: self.npm.resolution.clone(), @@ -1556,6 +1458,30 @@ impl Inner { &mut self, params: DidChangeWatchedFilesParams, ) { + fn has_lockfile_changed( + lockfile: &Lockfile, + changed_urls: &HashSet, + ) -> bool { + let lockfile_path = lockfile.filename.clone(); + let Ok(specifier) = ModuleSpecifier::from_file_path(&lockfile_path) else { + return false; + }; + if !changed_urls.contains(&specifier) { + return false; + } + match Lockfile::new(lockfile_path, false) { + Ok(new_lockfile) => { + // only update if the lockfile has changed + FastInsecureHasher::hash(lockfile) + != FastInsecureHasher::hash(new_lockfile) + } + Err(err) => { + lsp_warn!("Error loading lockfile: {:#}", err); + false + } + } + } + let mark = self .performance .mark("did_change_watched_files", Some(¶ms)); @@ -1567,8 +1493,14 @@ impl Inner { .collect(); // if the current deno.json has changed, we need to reload it - if let Some(config_file) = self.maybe_config_file() { - if changes.contains(&config_file.specifier) { + if let Some(config_file) = self.config.maybe_config_file() { + if changes.contains(&config_file.specifier) + || self + .config + .maybe_lockfile() + .map(|l| has_lockfile_changed(&l.lock(), &changes)) + .unwrap_or(false) + { if let Err(err) = self.update_config_file().await { self.client.show_message(MessageType::WARNING, err); } @@ -1579,25 +1511,6 @@ impl Inner { } } - if let Some(config_info) = self.maybe_config_file_info.as_mut() { - if let Some(lockfile) = config_info.maybe_lockfile.as_ref() { - let lockfile_path = lockfile.lock().filename.clone(); - let maybe_specifier = - ModuleSpecifier::from_file_path(&lockfile_path).ok(); - if let Some(specifier) = maybe_specifier { - if changes.contains(&specifier) { - let lockfile_hash = hash_lockfile(Some(lockfile)); - let new_lockfile = resolve_lockfile_from_path(lockfile_path); - // only update if the lockfile has changed - if lockfile_hash != hash_lockfile(new_lockfile.as_ref()) { - config_info.maybe_lockfile = new_lockfile; - touched = true; - } - } - } - } - } - if let Some(package_json) = &self.maybe_package_json { // always update the package json if the deno config changes if touched || changes.contains(&package_json.specifier()) { @@ -3424,14 +3337,16 @@ impl Inner { unsafely_ignore_certificate_errors: workspace_settings .unsafely_ignore_certificate_errors .clone(), - node_modules_dir: Some(self.maybe_node_modules_dir_path().is_some()), + node_modules_dir: Some( + self.config.maybe_node_modules_dir_path().is_some(), + ), // bit of a hack to force the lsp to cache the @types/node package type_check_mode: crate::args::TypeCheckMode::Local, ..Default::default() }, std::env::current_dir().with_context(|| "Failed getting cwd.")?, - self.maybe_config_file().cloned(), - self.maybe_lockfile().cloned(), + self.config.maybe_config_file().cloned(), + self.config.maybe_lockfile().cloned(), self.maybe_package_json.clone(), )?; cli_options.set_import_map_specifier(self.maybe_import_map_uri.clone()); @@ -3464,7 +3379,12 @@ impl Inner { } fn get_tasks(&self) -> LspResult> { - Ok(self.maybe_config_file().and_then(|cf| cf.to_lsp_tasks())) + Ok( + self + .config + .maybe_config_file() + .and_then(|cf| cf.to_lsp_tasks()), + ) } async fn inlay_hint( diff --git a/cli/tools/check.rs b/cli/tools/check.rs index 4464802e6e..99d891e5d0 100644 --- a/cli/tools/check.rs +++ b/cli/tools/check.rs @@ -120,12 +120,10 @@ impl TypeChecker { // to make tsc build info work, we need to consistently hash modules, so that // tsc can better determine if an emit is still valid or not, so we provide // that data here. - let hash_data = { - let mut hasher = FastInsecureHasher::new(); - hasher.write(&ts_config.as_bytes()); - hasher.write_str(version::deno()); - hasher.finish() - }; + let hash_data = FastInsecureHasher::new() + .write(&ts_config.as_bytes()) + .write_str(version::deno()) + .finish(); let response = tsc::exec(tsc::Request { config: ts_config, diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs index a4d6640f7c..52883a0b31 100644 --- a/cli/tsc/mod.rs +++ b/cli/tsc/mod.rs @@ -242,10 +242,11 @@ fn get_maybe_hash( } fn get_hash(source: &str, hash_data: u64) -> String { - let mut hasher = FastInsecureHasher::new(); - hasher.write_str(source); - hasher.write_u64(hash_data); - hasher.finish().to_string() + FastInsecureHasher::new() + .write_str(source) + .write_u64(hash_data) + .finish() + .to_string() } /// Hash the URL so it can be sent to `tsc` in a supportable way