mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 07:14:47 -05:00
147411e64b
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
741 lines
22 KiB
Rust
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(¤t) {
|
|
break match maybe_entry.as_ref() {
|
|
Some(entry) => entry.destination.clone(),
|
|
None => Some(current),
|
|
};
|
|
}
|
|
let Some(headers) = (self.get_headers)(¤t) 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(¤t) 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()),
|
|
})
|
|
),
|
|
]
|
|
);
|
|
}
|
|
}
|