1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00
denoland-deno/cli/lsp/resolver.rs
David Sherret 147411e64b
feat: npm workspace and better Deno workspace support (#24334)
Adds much better support for the unstable Deno workspaces as well as
support for npm workspaces. npm workspaces is still lacking in that we
only install packages into the root node_modules folder. We'll make it
smarter over time in order for it to figure out when to add node_modules
folders within packages.

This includes a breaking change in config file resolution where we stop
searching for config files on the first found package.json unless it's
in a workspace. For the previous behaviour, the root deno.json needs to
be updated to be a workspace by adding `"workspace":
["./path-to-pkg-json-folder-goes-here"]`. See details in
https://github.com/denoland/deno_config/pull/66

Closes #24340
Closes #24159
Closes #24161
Closes #22020
Closes #18546
Closes #16106
Closes #24160
2024-07-04 00:54:33 +00:00

741 lines
22 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::args::create_default_npmrc;
use crate::args::CacheSetting;
use crate::args::CliLockfile;
use crate::args::PackageJsonInstallDepsProvider;
use crate::graph_util::CliJsrUrlProvider;
use crate::http_util::HttpClientProvider;
use crate::lsp::config::Config;
use crate::lsp::config::ConfigData;
use crate::lsp::logging::lsp_warn;
use crate::npm::create_cli_npm_resolver_for_lsp;
use crate::npm::CliNpmResolver;
use crate::npm::CliNpmResolverByonmCreateOptions;
use crate::npm::CliNpmResolverCreateOptions;
use crate::npm::CliNpmResolverManagedCreateOptions;
use crate::npm::CliNpmResolverManagedSnapshotOption;
use crate::npm::ManagedCliNpmResolver;
use crate::resolver::CliGraphResolver;
use crate::resolver::CliGraphResolverOptions;
use crate::resolver::CliNodeResolver;
use crate::resolver::SloppyImportsResolver;
use crate::resolver::WorkerCliNpmGraphResolver;
use crate::util::progress_bar::ProgressBar;
use crate::util::progress_bar::ProgressBarStyle;
use dashmap::DashMap;
use deno_ast::MediaType;
use deno_cache_dir::HttpCache;
use deno_config::workspace::PackageJsonDepResolution;
use deno_config::workspace::WorkspaceResolver;
use deno_core::error::AnyError;
use deno_core::url::Url;
use deno_graph::source::Resolver;
use deno_graph::GraphImport;
use deno_graph::ModuleSpecifier;
use deno_npm::NpmSystemInfo;
use deno_runtime::deno_fs;
use deno_runtime::deno_node::NodeResolution;
use deno_runtime::deno_node::NodeResolutionMode;
use deno_runtime::deno_node::NodeResolver;
use deno_runtime::deno_node::PackageJson;
use deno_runtime::fs_util::specifier_to_file_path;
use deno_semver::jsr::JsrPackageReqReference;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageNv;
use deno_semver::package::PackageReq;
use indexmap::IndexMap;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use super::cache::LspCache;
use super::jsr::JsrCacheResolver;
#[derive(Debug, Clone)]
struct LspScopeResolver {
graph_resolver: Arc<CliGraphResolver>,
jsr_resolver: Option<Arc<JsrCacheResolver>>,
npm_resolver: Option<Arc<dyn CliNpmResolver>>,
node_resolver: Option<Arc<CliNodeResolver>>,
redirect_resolver: Option<Arc<RedirectResolver>>,
graph_imports: Arc<IndexMap<ModuleSpecifier, GraphImport>>,
config_data: Option<Arc<ConfigData>>,
}
impl Default for LspScopeResolver {
fn default() -> Self {
Self {
graph_resolver: create_graph_resolver(None, None, None),
jsr_resolver: None,
npm_resolver: None,
node_resolver: None,
redirect_resolver: None,
graph_imports: Default::default(),
config_data: None,
}
}
}
impl LspScopeResolver {
async fn from_config_data(
config_data: Option<&Arc<ConfigData>>,
config: &Config,
cache: &LspCache,
http_client_provider: Option<&Arc<HttpClientProvider>>,
) -> Self {
let mut npm_resolver = None;
let mut node_resolver = None;
if let Some(http_client) = http_client_provider {
npm_resolver = create_npm_resolver(
config_data.map(|d| d.as_ref()),
cache,
http_client,
)
.await;
node_resolver = create_node_resolver(npm_resolver.as_ref());
}
let graph_resolver = create_graph_resolver(
config_data.map(|d| d.as_ref()),
npm_resolver.as_ref(),
node_resolver.as_ref(),
);
let jsr_resolver = Some(Arc::new(JsrCacheResolver::new(
cache.for_specifier(config_data.map(|d| &d.scope)),
config_data.map(|d| d.as_ref()),
config,
)));
let redirect_resolver = Some(Arc::new(RedirectResolver::new(
cache.for_specifier(config_data.map(|d| &d.scope)),
config_data.and_then(|d| d.lockfile.clone()),
)));
let npm_graph_resolver = graph_resolver.create_graph_npm_resolver();
let graph_imports = config_data
.and_then(|d| d.config_file.as_ref())
.and_then(|cf| cf.to_maybe_imports().ok())
.map(|imports| {
Arc::new(
imports
.into_iter()
.map(|(referrer, imports)| {
let graph_import = GraphImport::new(
&referrer,
imports,
&CliJsrUrlProvider,
Some(graph_resolver.as_ref()),
Some(&npm_graph_resolver),
);
(referrer, graph_import)
})
.collect(),
)
})
.unwrap_or_default();
Self {
graph_resolver,
jsr_resolver,
npm_resolver,
node_resolver,
redirect_resolver,
graph_imports,
config_data: config_data.cloned(),
}
}
fn snapshot(&self) -> Arc<Self> {
let npm_resolver =
self.npm_resolver.as_ref().map(|r| r.clone_snapshotted());
let node_resolver = create_node_resolver(npm_resolver.as_ref());
let graph_resolver = create_graph_resolver(
self.config_data.as_deref(),
npm_resolver.as_ref(),
node_resolver.as_ref(),
);
Arc::new(Self {
graph_resolver,
jsr_resolver: self.jsr_resolver.clone(),
npm_resolver,
node_resolver,
redirect_resolver: self.redirect_resolver.clone(),
graph_imports: self.graph_imports.clone(),
config_data: self.config_data.clone(),
})
}
}
#[derive(Debug, Default, Clone)]
pub struct LspResolver {
unscoped: Arc<LspScopeResolver>,
by_scope: BTreeMap<ModuleSpecifier, Arc<LspScopeResolver>>,
}
impl LspResolver {
pub async fn from_config(
config: &Config,
cache: &LspCache,
http_client_provider: Option<&Arc<HttpClientProvider>>,
) -> Self {
let mut by_scope = BTreeMap::new();
for (scope, config_data) in config.tree.data_by_scope().as_ref() {
by_scope.insert(
scope.clone(),
Arc::new(
LspScopeResolver::from_config_data(
Some(config_data),
config,
cache,
http_client_provider,
)
.await,
),
);
}
Self {
unscoped: Arc::new(
LspScopeResolver::from_config_data(
None,
config,
cache,
http_client_provider,
)
.await,
),
by_scope,
}
}
pub fn snapshot(&self) -> Arc<Self> {
Arc::new(Self {
unscoped: self.unscoped.snapshot(),
by_scope: self
.by_scope
.iter()
.map(|(s, r)| (s.clone(), r.snapshot()))
.collect(),
})
}
pub fn did_cache(&self) {
for resolver in
std::iter::once(&self.unscoped).chain(self.by_scope.values())
{
resolver.jsr_resolver.as_ref().inspect(|r| r.did_cache());
resolver
.redirect_resolver
.as_ref()
.inspect(|r| r.did_cache());
}
}
pub async fn set_npm_reqs(
&self,
reqs: &BTreeMap<Option<ModuleSpecifier>, BTreeSet<PackageReq>>,
) {
for (scope, resolver) in [(None, &self.unscoped)]
.into_iter()
.chain(self.by_scope.iter().map(|(s, r)| (Some(s), r)))
{
if let Some(npm_resolver) = resolver.npm_resolver.as_ref() {
if let Some(npm_resolver) = npm_resolver.as_managed() {
let reqs = reqs
.get(&scope.cloned())
.map(|reqs| reqs.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();
if let Err(err) = npm_resolver.set_package_reqs(&reqs).await {
lsp_warn!("Could not set npm package requirements: {:#}", err);
}
}
}
}
}
pub fn as_graph_resolver(
&self,
file_referrer: Option<&ModuleSpecifier>,
) -> &dyn Resolver {
let resolver = self.get_scope_resolver(file_referrer);
resolver.graph_resolver.as_ref()
}
pub fn create_graph_npm_resolver(
&self,
file_referrer: Option<&ModuleSpecifier>,
) -> WorkerCliNpmGraphResolver {
let resolver = self.get_scope_resolver(file_referrer);
resolver.graph_resolver.create_graph_npm_resolver()
}
pub fn maybe_managed_npm_resolver(
&self,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<&ManagedCliNpmResolver> {
let resolver = self.get_scope_resolver(file_referrer);
resolver.npm_resolver.as_ref().and_then(|r| r.as_managed())
}
pub fn graph_imports_by_referrer(
&self,
file_referrer: &ModuleSpecifier,
) -> IndexMap<&ModuleSpecifier, Vec<&ModuleSpecifier>> {
let resolver = self.get_scope_resolver(Some(file_referrer));
resolver
.graph_imports
.iter()
.map(|(s, i)| {
(
s,
i.dependencies
.values()
.flat_map(|d| d.get_type().or_else(|| d.get_code()))
.collect(),
)
})
.collect()
}
pub fn jsr_to_resource_url(
&self,
req_ref: &JsrPackageReqReference,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<ModuleSpecifier> {
let resolver = self.get_scope_resolver(file_referrer);
resolver.jsr_resolver.as_ref()?.jsr_to_resource_url(req_ref)
}
pub fn jsr_lookup_export_for_path(
&self,
nv: &PackageNv,
path: &str,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<String> {
let resolver = self.get_scope_resolver(file_referrer);
resolver
.jsr_resolver
.as_ref()?
.lookup_export_for_path(nv, path)
}
pub fn jsr_lookup_req_for_nv(
&self,
nv: &PackageNv,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<PackageReq> {
let resolver = self.get_scope_resolver(file_referrer);
resolver.jsr_resolver.as_ref()?.lookup_req_for_nv(nv)
}
pub fn npm_to_file_url(
&self,
req_ref: &NpmPackageReqReference,
referrer: &ModuleSpecifier,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<(ModuleSpecifier, MediaType)> {
let resolver = self.get_scope_resolver(file_referrer);
let node_resolver = resolver.node_resolver.as_ref()?;
Some(NodeResolution::into_specifier_and_media_type(
node_resolver
.resolve_req_reference(req_ref, referrer, NodeResolutionMode::Types)
.ok(),
))
}
pub fn in_node_modules(&self, specifier: &ModuleSpecifier) -> bool {
let resolver = self.get_scope_resolver(Some(specifier));
if let Some(npm_resolver) = &resolver.npm_resolver {
return npm_resolver.in_npm_package(specifier);
}
false
}
pub fn node_media_type(
&self,
specifier: &ModuleSpecifier,
) -> Option<MediaType> {
let resolver = self.get_scope_resolver(Some(specifier));
let node_resolver = resolver.node_resolver.as_ref()?;
let resolution = node_resolver
.url_to_node_resolution(specifier.clone())
.ok()?;
Some(NodeResolution::into_specifier_and_media_type(Some(resolution)).1)
}
pub fn get_closest_package_json(
&self,
referrer: &ModuleSpecifier,
) -> Result<Option<Arc<PackageJson>>, AnyError> {
let resolver = self.get_scope_resolver(Some(referrer));
let Some(node_resolver) = resolver.node_resolver.as_ref() else {
return Ok(None);
};
node_resolver.get_closest_package_json(referrer)
}
pub fn resolve_redirects(
&self,
specifier: &ModuleSpecifier,
file_referrer: Option<&ModuleSpecifier>,
) -> Option<ModuleSpecifier> {
let resolver = self.get_scope_resolver(file_referrer);
let Some(redirect_resolver) = resolver.redirect_resolver.as_ref() else {
return Some(specifier.clone());
};
redirect_resolver.resolve(specifier)
}
pub fn redirect_chain_headers(
&self,
specifier: &ModuleSpecifier,
file_referrer: Option<&ModuleSpecifier>,
) -> Vec<(ModuleSpecifier, Arc<HashMap<String, String>>)> {
let resolver = self.get_scope_resolver(file_referrer);
let Some(redirect_resolver) = resolver.redirect_resolver.as_ref() else {
return vec![];
};
redirect_resolver
.chain(specifier)
.into_iter()
.map(|(s, e)| (s, e.headers.clone()))
.collect()
}
fn get_scope_resolver(
&self,
file_referrer: Option<&ModuleSpecifier>,
) -> &LspScopeResolver {
let Some(file_referrer) = file_referrer else {
return self.unscoped.as_ref();
};
self
.by_scope
.iter()
.rfind(|(s, _)| file_referrer.as_str().starts_with(s.as_str()))
.map(|(_, r)| r.as_ref())
.unwrap_or(self.unscoped.as_ref())
}
}
async fn create_npm_resolver(
config_data: Option<&ConfigData>,
cache: &LspCache,
http_client_provider: &Arc<HttpClientProvider>,
) -> Option<Arc<dyn CliNpmResolver>> {
let mut byonm_dir = None;
if let Some(config_data) = config_data {
if config_data.byonm {
byonm_dir = Some(config_data.node_modules_dir.clone().or_else(|| {
specifier_to_file_path(&config_data.scope)
.ok()
.map(|p| p.join("node_modules/"))
})?)
}
}
let options = if let Some(byonm_dir) = byonm_dir {
CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions {
fs: Arc::new(deno_fs::RealFs),
root_node_modules_dir: byonm_dir,
})
} else {
CliNpmResolverCreateOptions::Managed(CliNpmResolverManagedCreateOptions {
http_client_provider: http_client_provider.clone(),
snapshot: match config_data.and_then(|d| d.lockfile.as_ref()) {
Some(lockfile) => {
CliNpmResolverManagedSnapshotOption::ResolveFromLockfile(
lockfile.clone(),
)
}
None => CliNpmResolverManagedSnapshotOption::Specified(None),
},
// Don't provide the lockfile. We don't want these resolvers
// updating it. Only the cache request should update the lockfile.
maybe_lockfile: None,
fs: Arc::new(deno_fs::RealFs),
npm_global_cache_dir: cache.deno_dir().npm_folder_path(),
// Use an "only" cache setting in order to make the
// user do an explicit "cache" command and prevent
// the cache from being filled with lots of packages while
// the user is typing.
cache_setting: CacheSetting::Only,
text_only_progress_bar: ProgressBar::new(ProgressBarStyle::TextOnly),
maybe_node_modules_path: config_data
.and_then(|d| d.node_modules_dir.clone()),
// only used for top level install, so we can ignore this
package_json_deps_provider: Arc::new(
PackageJsonInstallDepsProvider::empty(),
),
npmrc: config_data
.and_then(|d| d.npmrc.clone())
.unwrap_or_else(create_default_npmrc),
npm_system_info: NpmSystemInfo::default(),
})
};
Some(create_cli_npm_resolver_for_lsp(options).await)
}
fn create_node_resolver(
npm_resolver: Option<&Arc<dyn CliNpmResolver>>,
) -> Option<Arc<CliNodeResolver>> {
let npm_resolver = npm_resolver?;
let fs = Arc::new(deno_fs::RealFs);
let node_resolver_inner = Arc::new(NodeResolver::new(
fs.clone(),
npm_resolver.clone().into_npm_resolver(),
));
Some(Arc::new(CliNodeResolver::new(
None,
fs,
node_resolver_inner,
npm_resolver.clone(),
)))
}
fn create_graph_resolver(
config_data: Option<&ConfigData>,
npm_resolver: Option<&Arc<dyn CliNpmResolver>>,
node_resolver: Option<&Arc<CliNodeResolver>>,
) -> Arc<CliGraphResolver> {
let config_file = config_data.and_then(|d| d.config_file.as_deref());
let unstable_sloppy_imports =
config_file.is_some_and(|cf| cf.has_unstable("sloppy-imports"));
Arc::new(CliGraphResolver::new(CliGraphResolverOptions {
node_resolver: node_resolver.cloned(),
npm_resolver: npm_resolver.cloned(),
workspace_resolver: Arc::new(WorkspaceResolver::new_raw(
config_data.and_then(|d| d.import_map.as_ref().map(|i| (**i).clone())),
config_data
.and_then(|d| d.package_json.clone())
.into_iter()
.collect(),
if config_data.map(|d| d.byonm).unwrap_or(false) {
PackageJsonDepResolution::Disabled
} else {
// todo(dsherret): this should also be disabled for when using
// auto-install with a node_modules directory
PackageJsonDepResolution::Enabled
},
)),
maybe_jsx_import_source_config: config_file
.and_then(|cf| cf.to_maybe_jsx_import_source_config().ok().flatten()),
maybe_vendor_dir: config_data.and_then(|d| d.vendor_dir.as_ref()),
bare_node_builtins_enabled: config_file
.map(|cf| cf.has_unstable("bare-node-builtins"))
.unwrap_or(false),
sloppy_imports_resolver: unstable_sloppy_imports.then(|| {
SloppyImportsResolver::new_without_stat_cache(Arc::new(deno_fs::RealFs))
}),
}))
}
#[derive(Debug, Eq, PartialEq)]
struct RedirectEntry {
headers: Arc<HashMap<String, String>>,
target: Url,
destination: Option<Url>,
}
type GetHeadersFn =
Box<dyn Fn(&Url) -> Option<HashMap<String, String>> + Send + Sync>;
struct RedirectResolver {
get_headers: GetHeadersFn,
entries: DashMap<Url, Option<Arc<RedirectEntry>>>,
}
impl std::fmt::Debug for RedirectResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedirectResolver")
.field("get_headers", &"Box(|_| { ... })")
.field("entries", &self.entries)
.finish()
}
}
impl RedirectResolver {
fn new(
cache: Arc<dyn HttpCache>,
lockfile: Option<Arc<CliLockfile>>,
) -> Self {
let entries = DashMap::new();
if let Some(lockfile) = lockfile {
for (source, destination) in &lockfile.lock().content.redirects {
let Ok(source) = ModuleSpecifier::parse(source) else {
continue;
};
let Ok(destination) = ModuleSpecifier::parse(destination) else {
continue;
};
entries.insert(
source,
Some(Arc::new(RedirectEntry {
headers: Default::default(),
target: destination.clone(),
destination: Some(destination.clone()),
})),
);
entries.insert(destination, None);
}
}
Self {
get_headers: Box::new(move |specifier| {
let cache_key = cache.cache_item_key(specifier).ok()?;
cache.read_headers(&cache_key).ok().flatten()
}),
entries,
}
}
#[cfg(test)]
fn mock(get_headers: GetHeadersFn) -> Self {
Self {
get_headers,
entries: Default::default(),
}
}
fn resolve(&self, specifier: &Url) -> Option<Url> {
if !matches!(specifier.scheme(), "http" | "https") {
return Some(specifier.clone());
}
let mut current = specifier.clone();
let mut chain = vec![];
let destination = loop {
if let Some(maybe_entry) = self.entries.get(&current) {
break match maybe_entry.as_ref() {
Some(entry) => entry.destination.clone(),
None => Some(current),
};
}
let Some(headers) = (self.get_headers)(&current) else {
break None;
};
let headers = Arc::new(headers);
if let Some(location) = headers.get("location") {
if chain.len() > 10 {
break None;
}
let Ok(target) =
deno_core::resolve_import(location, specifier.as_str())
else {
break None;
};
chain.push((
current.clone(),
RedirectEntry {
headers,
target: target.clone(),
destination: None,
},
));
current = target;
} else {
self.entries.insert(current.clone(), None);
break Some(current);
}
};
for (specifier, mut entry) in chain {
entry.destination.clone_from(&destination);
self.entries.insert(specifier, Some(Arc::new(entry)));
}
destination
}
fn chain(&self, specifier: &Url) -> Vec<(Url, Arc<RedirectEntry>)> {
self.resolve(specifier);
let mut result = vec![];
let mut seen = HashSet::new();
let mut current = Cow::Borrowed(specifier);
loop {
let Some(maybe_entry) = self.entries.get(&current) else {
break;
};
let Some(entry) = maybe_entry.as_ref() else {
break;
};
result.push((current.as_ref().clone(), entry.clone()));
seen.insert(current.as_ref().clone());
if seen.contains(&entry.target) {
break;
}
current = Cow::Owned(entry.target.clone())
}
result
}
fn did_cache(&self) {
self.entries.retain(|_, entry| entry.is_some());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redirect_resolver() {
let redirect_resolver =
RedirectResolver::mock(Box::new(|specifier| match specifier.as_str() {
"https://foo/redirect_2.js" => Some(
[("location".to_string(), "./redirect_1.js".to_string())]
.into_iter()
.collect(),
),
"https://foo/redirect_1.js" => Some(
[("location".to_string(), "./file.js".to_string())]
.into_iter()
.collect(),
),
"https://foo/file.js" => Some([].into_iter().collect()),
_ => None,
}));
assert_eq!(
redirect_resolver.resolve(&Url::parse("https://foo/file.js").unwrap()),
Some(Url::parse("https://foo/file.js").unwrap())
);
assert_eq!(
redirect_resolver
.resolve(&Url::parse("https://foo/redirect_1.js").unwrap()),
Some(Url::parse("https://foo/file.js").unwrap())
);
assert_eq!(
redirect_resolver
.resolve(&Url::parse("https://foo/redirect_2.js").unwrap()),
Some(Url::parse("https://foo/file.js").unwrap())
);
assert_eq!(
redirect_resolver.resolve(&Url::parse("https://foo/unknown").unwrap()),
None
);
assert_eq!(
redirect_resolver
.chain(&Url::parse("https://foo/redirect_2.js").unwrap()),
vec![
(
Url::parse("https://foo/redirect_2.js").unwrap(),
Arc::new(RedirectEntry {
headers: Arc::new(
[("location".to_string(), "./redirect_1.js".to_string())]
.into_iter()
.collect()
),
target: Url::parse("https://foo/redirect_1.js").unwrap(),
destination: Some(Url::parse("https://foo/file.js").unwrap()),
})
),
(
Url::parse("https://foo/redirect_1.js").unwrap(),
Arc::new(RedirectEntry {
headers: Arc::new(
[("location".to_string(), "./file.js".to_string())]
.into_iter()
.collect()
),
target: Url::parse("https://foo/file.js").unwrap(),
destination: Some(Url::parse("https://foo/file.js").unwrap()),
})
),
]
);
}
}