-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
fix(lsp): improved npm specifier to import map entry mapping #22016
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,12 +25,14 @@ use deno_runtime::deno_node::NodeResolver; | |
use deno_runtime::deno_node::NpmResolver; | ||
use deno_runtime::deno_node::PathClean; | ||
use deno_runtime::permissions::PermissionsContainer; | ||
use deno_semver::npm::NpmPackageReqReference; | ||
use deno_semver::package::PackageReq; | ||
use import_map::ImportMap; | ||
use once_cell::sync::Lazy; | ||
use regex::Regex; | ||
use std::cmp::Ordering; | ||
use std::collections::HashMap; | ||
use std::collections::HashSet; | ||
use std::path::Path; | ||
use tower_lsp::lsp_types as lsp; | ||
use tower_lsp::lsp_types::Position; | ||
|
@@ -211,20 +213,31 @@ impl<'a> TsResponseImportMapper<'a> { | |
if !pkg_reqs.is_empty() { | ||
let sub_path = self.resolve_package_path(specifier); | ||
if let Some(import_map) = self.maybe_import_map { | ||
for pkg_req in &pkg_reqs { | ||
let paths = vec![ | ||
concat_npm_specifier("npm:", pkg_req, sub_path.as_deref()), | ||
concat_npm_specifier("npm:/", pkg_req, sub_path.as_deref()), | ||
]; | ||
for path in paths { | ||
if let Some(mapped_path) = ModuleSpecifier::parse(&path) | ||
.ok() | ||
.and_then(|s| import_map.lookup(&s, referrer)) | ||
let pkg_reqs = pkg_reqs.iter().collect::<HashSet<_>>(); | ||
let mut matches = Vec::new(); | ||
for entry in import_map.entries_for_referrer(referrer) { | ||
if let Some(value) = entry.raw_value { | ||
if let Ok(package_ref) = | ||
NpmPackageReqReference::from_str(value) | ||
{ | ||
return Some(mapped_path); | ||
if pkg_reqs.contains(package_ref.req()) { | ||
let sub_path = sub_path.as_deref().unwrap_or(""); | ||
let value_sub_path = package_ref.sub_path().unwrap_or(""); | ||
if let Some(key_sub_path) = | ||
sub_path.strip_prefix(value_sub_path) | ||
{ | ||
matches | ||
.push(format!("{}{}", entry.raw_key, key_sub_path)); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// select the shortest match | ||
matches.sort_by_key(|a| a.len()); | ||
if let Some(matched) = matches.first() { | ||
return Some(matched.to_string()); | ||
} | ||
} | ||
|
||
// if not found in the import map, return the first pkg req | ||
|
@@ -267,6 +280,22 @@ impl<'a> TsResponseImportMapper<'a> { | |
// a search for the .d.ts file instead | ||
if specifier_path.extension().and_then(|e| e.to_str()) == Some("js") { | ||
search_paths.insert(0, specifier_path.with_extension("d.ts")); | ||
} else if let Some(file_name) = | ||
specifier_path.file_name().and_then(|f| f.to_str()) | ||
{ | ||
// In some other cases, typescript will provide the .d.ts extension, but the | ||
// export might not have a .d.ts defined. In that case, look for the corresponding | ||
// JavaScript file after not being able to find the .d.ts file. | ||
if let Some(file_stem) = file_name.strip_suffix(".d.ts") { | ||
search_paths | ||
.push(specifier_path.with_file_name(format!("{}.js", file_stem))); | ||
} else if let Some(file_stem) = file_name.strip_suffix(".d.cts") { | ||
search_paths | ||
.push(specifier_path.with_file_name(format!("{}.cjs", file_stem))); | ||
} else if let Some(file_stem) = file_name.strip_suffix(".d.mts") { | ||
search_paths | ||
.push(specifier_path.with_file_name(format!("{}.mjs", file_stem))); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Found this situation while writing the test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, that's a tricky one |
||
} | ||
|
||
for search_path in search_paths { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export function entryC() { | ||
export function entryA() { | ||
return 12; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export function entryB(): "b"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function entryB() { | ||
return "b"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,9 @@ | |
}, | ||
"./entry-a": { | ||
"import": "./dist/entry-a.js" | ||
}, | ||
"./nested/entry-b": { | ||
"import": "./dist/entry-b.js" | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code now goes from import map entry to npm package req and from that figures out a match instead of going from pkg req -> specifier, then calling import_map.lookup on the possible specifiers. We can no longer rely on a mapping from PkgReq -> specifier because we normalize and eliminate duplicate PackageReqs based on their RangeSetAndTag since denoland/deno_semver@98f9174