Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): jsr support with cache probing #22418

Merged
merged 5 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cli/lsp/documents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,10 @@ impl Documents {
bare_node_builtins_enabled: false,
sloppy_imports_resolver: None,
})),
jsr_resolver: Default::default(),
jsr_resolver: Arc::new(JsrResolver::from_cache_and_lockfile(
cache.clone(),
None,
)),
npm_specifier_reqs: Default::default(),
has_injected_types_node_package: false,
redirect_resolver: Arc::new(RedirectResolver::new(cache)),
Expand Down Expand Up @@ -1332,6 +1335,16 @@ impl Documents {
Ok(())
}

pub fn refresh_jsr_resolver(
&mut self,
lockfile: Option<Arc<Mutex<Lockfile>>>,
) {
self.jsr_resolver = Arc::new(JsrResolver::from_cache_and_lockfile(
self.cache.clone(),
lockfile,
));
}

pub fn update_config(&mut self, options: UpdateDocumentConfigOptions) {
#[allow(clippy::too_many_arguments)]
fn calculate_resolver_config_hash(
Expand Down
99 changes: 61 additions & 38 deletions cli/lsp/jsr_resolver.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use crate::args::jsr_url;
use dashmap::DashMap;
use deno_cache_dir::HttpCache;
use deno_core::parking_lot::Mutex;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::ModuleSpecifier;
use deno_graph::packages::JsrPackageInfo;
use deno_graph::packages::JsrPackageVersionInfo;
use deno_lockfile::Lockfile;
use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::package::PackageNv;
use deno_semver::package::PackageReq;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;

#[derive(Debug, Default)]
#[derive(Debug)]
pub struct JsrResolver {
nv_by_req: HashMap<PackageReq, PackageNv>,
nv_by_req: DashMap<PackageReq, Option<PackageNv>>,
/// The `module_graph` field of the version infos should be forcibly absent.
/// It can be large and we don't want to store it.
info_by_nv: HashMap<PackageNv, JsrPackageVersionInfo>,
info_by_nv: DashMap<PackageNv, Option<JsrPackageVersionInfo>>,
info_by_name: DashMap<String, Option<JsrPackageInfo>>,
cache: Arc<dyn HttpCache>,
}

impl JsrResolver {
pub fn from_cache_and_lockfile(
cache: Arc<dyn HttpCache>,
lockfile: Option<Arc<Mutex<Lockfile>>>,
) -> Self {
let mut nv_by_req = HashMap::new();
let mut info_by_nv = HashMap::new();
let nv_by_req = DashMap::new();
if let Some(lockfile) = lockfile {
for (req_url, nv_url) in &lockfile.lock().content.packages.specifiers {
let Some(req) = req_url.strip_prefix("jsr:") else {
Expand All @@ -44,40 +46,14 @@ impl JsrResolver {
let Ok(nv) = PackageNv::from_str(nv) else {
continue;
};
nv_by_req.insert(req, nv);
nv_by_req.insert(req, Some(nv));
}
}
for nv in nv_by_req.values() {
if info_by_nv.contains_key(nv) {
continue;
}
let Ok(meta_url) =
jsr_url().join(&format!("{}/{}_meta.json", &nv.name, &nv.version))
else {
continue;
};
let Ok(meta_cache_item_key) = cache.cache_item_key(&meta_url) else {
continue;
};
let Ok(Some(meta_bytes)) = cache.read_file_bytes(&meta_cache_item_key)
else {
continue;
};
// This is a roundabout way of deserializing `JsrPackageVersionInfo`,
// because we only want the `exports` field and `module_graph` is large.
let Ok(info) = serde_json::from_slice::<serde_json::Value>(&meta_bytes)
else {
continue;
};
let info = JsrPackageVersionInfo {
exports: json!(info.as_object().and_then(|o| o.get("exports"))),
module_graph: None,
};
info_by_nv.insert(nv.clone(), info);
}
Self {
nv_by_req,
info_by_nv,
info_by_nv: Default::default(),
info_by_name: Default::default(),
cache: cache.clone(),
}
}

Expand All @@ -86,15 +62,62 @@ impl JsrResolver {
specifier: &ModuleSpecifier,
) -> Option<ModuleSpecifier> {
let req_ref = JsrPackageReqReference::from_str(specifier.as_str()).ok()?;
let nv = self.nv_by_req.get(req_ref.req())?;
let info = self.info_by_nv.get(nv)?;
let req = req_ref.req().clone();
let maybe_nv = self.nv_by_req.entry(req.clone()).or_insert_with(|| {
let name = req.name.clone();
let maybe_package_info = self
.info_by_name
.entry(name.clone())
.or_insert_with(|| read_cached_package_info(&name, &self.cache));
let package_info = maybe_package_info.as_ref()?;
let version = package_info
.versions
.keys()
.find(|v| req.version_req.tag().is_none() && req.version_req.matches(v))
.cloned()?;
nayeemrmn marked this conversation as resolved.
Show resolved Hide resolved
Some(PackageNv { name, version })
});
let nv = maybe_nv.as_ref()?;
let maybe_info = self
.info_by_nv
.entry(nv.clone())
.or_insert_with(|| read_cached_package_version_info(nv, &self.cache));
let info = maybe_info.as_ref()?;
let path = info.export(&normalize_export_name(req_ref.sub_path()))?;
jsr_url()
.join(&format!("{}/{}/{}", &nv.name, &nv.version, &path))
.ok()
}
}

fn read_cached_package_info(
name: &str,
cache: &Arc<dyn HttpCache>,
) -> Option<JsrPackageInfo> {
let meta_url = jsr_url().join(&format!("{}/meta.json", name)).ok()?;
let meta_cache_item_key = cache.cache_item_key(&meta_url).ok()?;
let meta_bytes = cache.read_file_bytes(&meta_cache_item_key).ok()??;
serde_json::from_slice::<JsrPackageInfo>(&meta_bytes).ok()
}

fn read_cached_package_version_info(
nv: &PackageNv,
cache: &Arc<dyn HttpCache>,
) -> Option<JsrPackageVersionInfo> {
let meta_url = jsr_url()
.join(&format!("{}/{}_meta.json", &nv.name, &nv.version))
.ok()?;
let meta_cache_item_key = cache.cache_item_key(&meta_url).ok()?;
let meta_bytes = cache.read_file_bytes(&meta_cache_item_key).ok()??;
// This is a roundabout way of deserializing `JsrPackageVersionInfo`,
// because we only want the `exports` field and `module_graph` is large.
let info = serde_json::from_slice::<serde_json::Value>(&meta_bytes).ok()?;
Some(JsrPackageVersionInfo {
exports: json!(info.as_object().and_then(|o| o.get("exports"))),
nayeemrmn marked this conversation as resolved.
Show resolved Hide resolved
module_graph: None,
})
}

// TODO(nayeemrmn): This is duplicated from a private function in deno_graph
// 0.65.1. Make it public or cleanup otherwise.
fn normalize_export_name(sub_path: Option<&str>) -> Cow<str> {
Expand Down
22 changes: 5 additions & 17 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,23 +362,11 @@ impl LanguageServer {
.client
.show_message(MessageType::WARNING, err);
}
let mut lockfile_content_changed = false;
if let Some(lockfile) = self.0.read().await.config.maybe_lockfile() {
let lockfile = lockfile.lock();
let path = lockfile.filename.clone();
if let Ok(new_lockfile) = Lockfile::new(path, false) {
lockfile_content_changed = FastInsecureHasher::hash(&*lockfile)
!= FastInsecureHasher::hash(new_lockfile);
} else {
lockfile_content_changed = true;
}
}
if lockfile_content_changed {
// TODO(nayeemrmn): Remove this branch when the documents config no
// longer depends on the lockfile for JSR resolution.
self.0.write().await.refresh_documents_config().await;
} else {
self.0.write().await.refresh_npm_specifiers().await;
{
let mut inner = self.0.write().await;
let lockfile = inner.config.maybe_lockfile().cloned();
inner.documents.refresh_jsr_resolver(lockfile);
inner.refresh_npm_specifiers().await;
}
// now refresh the data in a read
self.0.read().await.post_cache(result.mark).await;
Expand Down
3 changes: 0 additions & 3 deletions tests/integration/lsp_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4664,9 +4664,6 @@ fn lsp_code_actions_deno_cache_jsr() {
.use_temp_cwd()
.build();
let temp_dir = context.temp_dir();
// TODO(nayeemrmn): JSR resolution currently depends on a lockfile being
// created on cache. Remove this when that's not the case.
temp_dir.write("deno.json", "{}");
nayeemrmn marked this conversation as resolved.
Show resolved Hide resolved
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
Expand Down
Loading