Skip to content

Commit

Permalink
FUTURE: initial support for .npmrc file (#23560)
Browse files Browse the repository at this point in the history
This commit adds initial support for ".npmrc" files.

Currently we only discover ".npmrc" files next to "package.json" files
and discovering these files in user home dir is left for a follow up.

This pass supports "_authToken" and "_auth" configuration
for providing authentication.

LSP support has been left for a follow up PR.

Towards #16105
  • Loading branch information
bartlomieju authored May 23, 2024
1 parent 5de30c5 commit 959739f
Show file tree
Hide file tree
Showing 65 changed files with 611 additions and 164 deletions.
98 changes: 98 additions & 0 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use ::import_map::ImportMap;
use deno_ast::SourceMapOption;
use deno_core::resolve_url_or_path;
use deno_graph::GraphKind;
use deno_npm::npm_rc::NpmRc;
use deno_npm::npm_rc::ResolvedNpmRc;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_npm::NpmSystemInfo;
use deno_runtime::deno_tls::RootCertStoreProvider;
Expand Down Expand Up @@ -546,6 +548,83 @@ fn discover_package_json(
Ok(None)
}

/// Discover `.npmrc` file - currently we only support it next to `package.json`
/// or next to `deno.json`.
///
/// In the future we will need to support it in user directory or global directory
/// as per https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#files.
fn discover_npmrc(
maybe_package_json_path: Option<PathBuf>,
maybe_deno_json_path: Option<PathBuf>,
) -> Result<Arc<ResolvedNpmRc>, AnyError> {
if !*DENO_FUTURE {
return Ok(create_default_npmrc());
}

const NPMRC_NAME: &str = ".npmrc";

fn get_env_var(var_name: &str) -> Option<String> {
std::env::var(var_name).ok()
}

fn try_to_read_npmrc(
dir: &Path,
) -> Result<Option<(String, PathBuf)>, AnyError> {
let path = dir.join(NPMRC_NAME);
let maybe_source = match std::fs::read_to_string(&path) {
Ok(source) => Some(source),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => {
bail!("Error loading .npmrc at {}. {:#}", path.display(), err)
}
};

Ok(maybe_source.map(|source| (source, path)))
}

fn try_to_parse_npmrc(
source: String,
path: &Path,
) -> Result<Arc<ResolvedNpmRc>, AnyError> {
let npmrc = NpmRc::parse(&source, &get_env_var).with_context(|| {
format!("Failed to parse .npmrc at {}", path.display())
})?;
let resolved = npmrc
.as_resolved(npm_registry_url())
.context("Failed to resolve .npmrc options")?;
Ok(Arc::new(resolved))
}

if let Some(package_json_path) = maybe_package_json_path {
if let Some(package_json_dir) = package_json_path.parent() {
if let Some((source, path)) = try_to_read_npmrc(package_json_dir)? {
return try_to_parse_npmrc(source, &path);
}
}
}

if let Some(deno_json_path) = maybe_deno_json_path {
if let Some(deno_json_dir) = deno_json_path.parent() {
if let Some((source, path)) = try_to_read_npmrc(deno_json_dir)? {
return try_to_parse_npmrc(source, &path);
}
}
}

log::debug!("No .npmrc file found");
Ok(create_default_npmrc())
}

pub fn create_default_npmrc() -> Arc<ResolvedNpmRc> {
Arc::new(ResolvedNpmRc {
default_config: deno_npm::npm_rc::RegistryConfigWithUrl {
registry_url: npm_registry_url().clone(),
config: Default::default(),
},
scopes: Default::default(),
})
}

struct CliRootCertStoreProvider {
cell: OnceCell<RootCertStore>,
maybe_root_path: Option<PathBuf>,
Expand Down Expand Up @@ -722,6 +801,7 @@ pub struct CliOptions {
maybe_vendor_folder: Option<PathBuf>,
maybe_config_file: Option<ConfigFile>,
maybe_package_json: Option<PackageJson>,
npmrc: Arc<ResolvedNpmRc>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
overrides: CliOptionOverrides,
maybe_workspace_config: Option<WorkspaceConfig>,
Expand All @@ -736,6 +816,7 @@ impl CliOptions {
maybe_config_file: Option<ConfigFile>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
maybe_package_json: Option<PackageJson>,
npmrc: Arc<ResolvedNpmRc>,
force_global_cache: bool,
) -> Result<Self, AnyError> {
if let Some(insecure_allowlist) =
Expand Down Expand Up @@ -798,6 +879,7 @@ impl CliOptions {
maybe_config_file,
maybe_lockfile,
maybe_package_json,
npmrc,
maybe_node_modules_folder,
maybe_vendor_folder,
overrides: Default::default(),
Expand Down Expand Up @@ -851,6 +933,16 @@ impl CliOptions {
} else {
maybe_package_json = discover_package_json(&flags, None, &initial_cwd)?;
}
let npmrc = discover_npmrc(
maybe_package_json.as_ref().map(|p| p.path.clone()),
maybe_config_file.as_ref().and_then(|cf| {
if cf.specifier.scheme() == "file" {
Some(cf.specifier.to_file_path().unwrap())
} else {
None
}
}),
)?;

let maybe_lock_file = lockfile::discover(
&flags,
Expand All @@ -863,6 +955,7 @@ impl CliOptions {
maybe_config_file,
maybe_lock_file.map(|l| Arc::new(Mutex::new(l))),
maybe_package_json,
npmrc,
false,
)
}
Expand Down Expand Up @@ -1172,6 +1265,7 @@ impl CliOptions {
maybe_vendor_folder: self.maybe_vendor_folder.clone(),
maybe_config_file: self.maybe_config_file.clone(),
maybe_package_json: self.maybe_package_json.clone(),
npmrc: self.npmrc.clone(),
maybe_lockfile: self.maybe_lockfile.clone(),
maybe_workspace_config: self.maybe_workspace_config.clone(),
overrides: self.overrides.clone(),
Expand Down Expand Up @@ -1303,6 +1397,10 @@ impl CliOptions {
&self.maybe_package_json
}

pub fn npmrc(&self) -> &Arc<ResolvedNpmRc> {
&self.npmrc
}

pub fn maybe_package_json_deps(&self) -> Option<PackageJsonDeps> {
if matches!(
self.flags.subcommand,
Expand Down
2 changes: 1 addition & 1 deletion cli/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ impl CliFactory {
self.package_json_deps_provider().clone(),
),
npm_system_info: self.options.npm_system_info(),
npm_registry_url: crate::args::npm_registry_url().to_owned(),
npmrc: self.options.npmrc().clone()
})
}).await
}.boxed_local())
Expand Down
33 changes: 27 additions & 6 deletions cli/http_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use deno_runtime::deno_fetch::reqwest::header::LOCATION;
use deno_runtime::deno_fetch::reqwest::Response;
use deno_runtime::deno_fetch::CreateHttpClientOptions;
use deno_runtime::deno_tls::RootCertStoreProvider;
use reqwest::header::HeaderName;
use reqwest::header::HeaderValue;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
Expand Down Expand Up @@ -294,7 +296,7 @@ impl HttpClient {
&self,
url: U,
) -> Result<Vec<u8>, AnyError> {
let maybe_bytes = self.inner_download(url, None).await?;
let maybe_bytes = self.inner_download(url, None, None).await?;
match maybe_bytes {
Some(bytes) => Ok(bytes),
None => Err(custom_error("Http", "Not found.")),
Expand All @@ -304,17 +306,21 @@ impl HttpClient {
pub async fn download_with_progress<U: reqwest::IntoUrl>(
&self,
url: U,
maybe_header: Option<(HeaderName, HeaderValue)>,
progress_guard: &UpdateGuard,
) -> Result<Option<Vec<u8>>, AnyError> {
self.inner_download(url, Some(progress_guard)).await
self
.inner_download(url, maybe_header, Some(progress_guard))
.await
}

async fn inner_download<U: reqwest::IntoUrl>(
&self,
url: U,
maybe_header: Option<(HeaderName, HeaderValue)>,
progress_guard: Option<&UpdateGuard>,
) -> Result<Option<Vec<u8>>, AnyError> {
let response = self.get_redirected_response(url).await?;
let response = self.get_redirected_response(url, maybe_header).await?;

if response.status() == 404 {
return Ok(None);
Expand All @@ -339,15 +345,30 @@ impl HttpClient {
pub async fn get_redirected_response<U: reqwest::IntoUrl>(
&self,
url: U,
mut maybe_header: Option<(HeaderName, HeaderValue)>,
) -> Result<Response, AnyError> {
let mut url = url.into_url()?;
let mut response = self.get_no_redirect(url.clone())?.send().await?;

let mut builder = self.get_no_redirect(url.clone())?;
if let Some((header_name, header_value)) = maybe_header.as_ref() {
builder = builder.header(header_name, header_value);
}
let mut response = builder.send().await?;
let status = response.status();
if status.is_redirection() {
for _ in 0..5 {
let new_url = resolve_redirect_from_response(&url, &response)?;
let new_response =
self.get_no_redirect(new_url.clone())?.send().await?;
let mut builder = self.get_no_redirect(new_url.clone())?;

if new_url.origin() == url.origin() {
if let Some((header_name, header_value)) = maybe_header.as_ref() {
builder = builder.header(header_name, header_value);
}
} else {
maybe_header = None;
}

let new_response = builder.send().await?;
let status = new_response.status();
if status.is_redirection() {
response = new_response;
Expand Down
5 changes: 5 additions & 0 deletions cli/lsp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::ModuleSpecifier;
use deno_lockfile::Lockfile;
use deno_npm::npm_rc::ResolvedNpmRc;
use deno_runtime::deno_node::PackageJson;
use deno_runtime::fs_util::specifier_to_file_path;
use deno_runtime::permissions::PermissionsContainer;
Expand Down Expand Up @@ -1090,6 +1091,7 @@ pub struct ConfigData {
pub vendor_dir: Option<PathBuf>,
pub lockfile: Option<Arc<Mutex<Lockfile>>>,
pub package_json: Option<Arc<PackageJson>>,
pub npmrc: Option<Arc<ResolvedNpmRc>>,
pub import_map: Option<Arc<ImportMap>>,
pub import_map_from_settings: bool,
watched_files: HashMap<ModuleSpecifier, ConfigWatchedFileType>,
Expand Down Expand Up @@ -1274,6 +1276,8 @@ impl ConfigData {

// Load package.json
let mut package_json = None;
// TODO(bartlomieju): support discovering .npmrc
let npmrc = None;
if let Ok(path) = specifier_to_file_path(scope) {
let path = path.join("package.json");
if let Ok(specifier) = ModuleSpecifier::from_file_path(&path) {
Expand Down Expand Up @@ -1429,6 +1433,7 @@ impl ConfigData {
vendor_dir,
lockfile: lockfile.map(Mutex::new).map(Arc::new),
package_json: package_json.map(Arc::new),
npmrc: npmrc.map(Arc::new),
import_map: import_map.map(Arc::new),
import_map_from_settings,
watched_files,
Expand Down
4 changes: 4 additions & 0 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ use super::tsc::ChangeKind;
use super::tsc::GetCompletionDetailsArgs;
use super::tsc::TsServer;
use super::urls;
use crate::args::create_default_npmrc;
use crate::args::get_root_cert_store;
use crate::args::CaData;
use crate::args::CacheSetting;
Expand Down Expand Up @@ -3318,6 +3319,9 @@ impl Inner {
config_data.and_then(|d| d.config_file.as_deref().cloned()),
config_data.and_then(|d| d.lockfile.clone()),
config_data.and_then(|d| d.package_json.as_deref().cloned()),
config_data
.and_then(|d| d.npmrc.clone())
.unwrap_or_else(create_default_npmrc),
force_global_cache,
)?;

Expand Down
6 changes: 5 additions & 1 deletion cli/lsp/resolver.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

use crate::args::create_default_npmrc;
use crate::args::package_json;
use crate::args::CacheSetting;
use crate::cache::FastInsecureHasher;
Expand Down Expand Up @@ -363,7 +364,10 @@ async fn create_npm_resolver(
// do not install while resolving in the lsp—leave that to the cache command
package_json_installer:
CliNpmResolverManagedPackageJsonInstallerOption::NoInstall,
npm_registry_url: crate::args::npm_registry_url().to_owned(),
npmrc: config_data
.npmrc
.clone()
.unwrap_or_else(create_default_npmrc),
npm_system_info: NpmSystemInfo::default(),
})
};
Expand Down
Loading

0 comments on commit 959739f

Please sign in to comment.