0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-10-30 09:08:00 -04:00
denoland-deno/cli/resolver.rs
David Sherret 6233c0aff0
fix(npm): support bare specifiers in package.json having a path (#17903)
For example `import * as test from "package/path.js"`
2023-02-23 17:33:23 +00:00

269 lines
7.7 KiB
Rust

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::futures::future;
use deno_core::futures::future::LocalBoxFuture;
use deno_core::futures::FutureExt;
use deno_core::ModuleSpecifier;
use deno_graph::npm::NpmPackageNv;
use deno_graph::npm::NpmPackageReq;
use deno_graph::source::NpmResolver;
use deno_graph::source::Resolver;
use deno_graph::source::UnknownBuiltInNodeModuleError;
use deno_graph::source::DEFAULT_JSX_IMPORT_SOURCE_MODULE;
use deno_runtime::deno_node::is_builtin_node_module;
use import_map::ImportMap;
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::args::JsxImportSourceConfig;
use crate::npm::NpmRegistryApi;
use crate::npm::NpmResolution;
/// A resolver that takes care of resolution, taking into account loaded
/// import map, JSX settings.
#[derive(Debug, Clone)]
pub struct CliGraphResolver {
maybe_import_map: Option<Arc<ImportMap>>,
maybe_package_json_deps: Option<BTreeMap<String, NpmPackageReq>>,
maybe_default_jsx_import_source: Option<String>,
maybe_jsx_import_source_module: Option<String>,
no_npm: bool,
npm_registry_api: NpmRegistryApi,
npm_resolution: NpmResolution,
sync_download_semaphore: Option<Arc<tokio::sync::Semaphore>>,
}
impl Default for CliGraphResolver {
fn default() -> Self {
// This is not ideal, but necessary for the LSP. In the future, we should
// refactor the LSP and force this to be initialized.
let npm_registry_api = NpmRegistryApi::new_uninitialized();
let npm_resolution =
NpmResolution::new(npm_registry_api.clone(), None, None);
Self {
maybe_import_map: Default::default(),
maybe_default_jsx_import_source: Default::default(),
maybe_jsx_import_source_module: Default::default(),
no_npm: false,
npm_registry_api,
npm_resolution,
maybe_package_json_deps: Default::default(),
sync_download_semaphore: Self::create_sync_download_semaphore(),
}
}
}
impl CliGraphResolver {
pub fn new(
maybe_jsx_import_source_config: Option<JsxImportSourceConfig>,
maybe_import_map: Option<Arc<ImportMap>>,
no_npm: bool,
npm_registry_api: NpmRegistryApi,
npm_resolution: NpmResolution,
maybe_package_json_deps: Option<BTreeMap<String, NpmPackageReq>>,
) -> Self {
Self {
maybe_import_map,
maybe_default_jsx_import_source: maybe_jsx_import_source_config
.as_ref()
.and_then(|c| c.default_specifier.clone()),
maybe_jsx_import_source_module: maybe_jsx_import_source_config
.map(|c| c.module),
no_npm,
npm_registry_api,
npm_resolution,
maybe_package_json_deps,
sync_download_semaphore: Self::create_sync_download_semaphore(),
}
}
fn create_sync_download_semaphore() -> Option<Arc<tokio::sync::Semaphore>> {
if crate::npm::should_sync_download() {
Some(Arc::new(tokio::sync::Semaphore::new(1)))
} else {
None
}
}
pub fn as_graph_resolver(&self) -> &dyn Resolver {
self
}
pub fn as_graph_npm_resolver(&self) -> &dyn NpmResolver {
self
}
}
impl Resolver for CliGraphResolver {
fn default_jsx_import_source(&self) -> Option<String> {
self.maybe_default_jsx_import_source.clone()
}
fn jsx_import_source_module(&self) -> &str {
self
.maybe_jsx_import_source_module
.as_deref()
.unwrap_or(DEFAULT_JSX_IMPORT_SOURCE_MODULE)
}
fn resolve(
&self,
specifier: &str,
referrer: &ModuleSpecifier,
) -> Result<ModuleSpecifier, AnyError> {
if let Some(import_map) = &self.maybe_import_map {
return import_map
.resolve(specifier, referrer)
.map_err(|err| err.into());
}
if let Some(deps) = self.maybe_package_json_deps.as_ref() {
if let Some(specifier) = resolve_package_json_dep(specifier, deps)? {
return Ok(specifier);
}
if let Some(req) = deps.get(specifier) {
return Ok(ModuleSpecifier::parse(&format!("npm:{req}")).unwrap());
}
}
deno_graph::resolve_import(specifier, referrer).map_err(|err| err.into())
}
}
fn resolve_package_json_dep(
specifier: &str,
deps: &BTreeMap<String, NpmPackageReq>,
) -> Result<Option<ModuleSpecifier>, deno_core::url::ParseError> {
for (bare_specifier, req) in deps {
if specifier.starts_with(bare_specifier) {
if specifier.len() == bare_specifier.len() {
return ModuleSpecifier::parse(&format!("npm:{req}")).map(Some);
}
let path = &specifier[bare_specifier.len()..];
if path.starts_with('/') {
return ModuleSpecifier::parse(&format!("npm:/{req}{path}")).map(Some);
}
}
}
Ok(None)
}
impl NpmResolver for CliGraphResolver {
fn resolve_builtin_node_module(
&self,
specifier: &ModuleSpecifier,
) -> Result<Option<String>, UnknownBuiltInNodeModuleError> {
if specifier.scheme() != "node" {
return Ok(None);
}
let module_name = specifier.path().to_string();
if is_builtin_node_module(&module_name) {
Ok(Some(module_name))
} else {
Err(UnknownBuiltInNodeModuleError { module_name })
}
}
fn load_and_cache_npm_package_info(
&self,
package_name: &str,
) -> LocalBoxFuture<'static, Result<(), String>> {
if self.no_npm {
// return it succeeded and error at the import site below
return Box::pin(future::ready(Ok(())));
}
// this will internally cache the package information
let package_name = package_name.to_string();
let api = self.npm_registry_api.clone();
let mut maybe_sync_download_semaphore =
self.sync_download_semaphore.clone();
async move {
let result = if let Some(semaphore) = maybe_sync_download_semaphore.take()
{
let _permit = semaphore.acquire().await.unwrap();
api.package_info(&package_name).await
} else {
api.package_info(&package_name).await
};
result.map(|_| ()).map_err(|err| format!("{err:#}"))
}
.boxed()
}
fn resolve_npm(
&self,
package_req: &NpmPackageReq,
) -> Result<NpmPackageNv, AnyError> {
if self.no_npm {
bail!("npm specifiers were requested; but --no-npm is specified")
}
self
.npm_resolution
.resolve_package_req_for_deno_graph(package_req)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_resolve_package_json_dep() {
fn resolve(
specifier: &str,
deps: &BTreeMap<String, NpmPackageReq>,
) -> Result<Option<String>, String> {
resolve_package_json_dep(specifier, deps)
.map(|s| s.map(|s| s.to_string()))
.map_err(|err| err.to_string())
}
let deps = BTreeMap::from([
(
"package".to_string(),
NpmPackageReq::from_str("package@1.0").unwrap(),
),
(
"package-alias".to_string(),
NpmPackageReq::from_str("package@^1.2").unwrap(),
),
(
"@deno/test".to_string(),
NpmPackageReq::from_str("@deno/test@~0.2").unwrap(),
),
]);
assert_eq!(
resolve("package", &deps).unwrap(),
Some("npm:package@1.0".to_string()),
);
assert_eq!(
resolve("package/some_path.ts", &deps).unwrap(),
Some("npm:/package@1.0/some_path.ts".to_string()),
);
assert_eq!(
resolve("@deno/test", &deps).unwrap(),
Some("npm:@deno/test@~0.2".to_string()),
);
assert_eq!(
resolve("@deno/test/some_path.ts", &deps).unwrap(),
Some("npm:/@deno/test@~0.2/some_path.ts".to_string()),
);
// matches the start, but doesn't have the same length or a path
assert_eq!(resolve("@deno/testing", &deps).unwrap(), None,);
// alias
assert_eq!(
resolve("package-alias", &deps).unwrap(),
Some("npm:package@^1.2".to_string()),
);
// non-existent bare specifier
assert_eq!(resolve("non-existent", &deps).unwrap(), None);
}
}