Repository: denoland/deno
Pull request: feat(lsp): support .npmrc

Closes #24040.

Changed files:
filename: cli/args/mod.rs
patch: @@ -554,12 +554,12 @@ fn discover_package_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(
+pub fn discover_npmrc(
   maybe_package_json_path: Option<PathBuf>,
   maybe_deno_json_path: Option<PathBuf>,
-) -> Result<Arc<ResolvedNpmRc>, AnyError> {
+) -> Result<(Arc<ResolvedNpmRc>, Option<PathBuf>), AnyError> {
   if !*DENO_FUTURE {
-    return Ok(create_default_npmrc());
+    return Ok((create_default_npmrc(), None));
   }
 
   const NPMRC_NAME: &str = ".npmrc";
@@ -599,21 +599,21 @@ fn discover_npmrc(
   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);
+        return try_to_parse_npmrc(source, &path).map(|r| (r, Some(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);
+        return try_to_parse_npmrc(source, &path).map(|r| (r, Some(path)));
       }
     }
   }
 
   log::debug!("No .npmrc file found");
-  Ok(create_default_npmrc())
+  Ok((create_default_npmrc(), None))
 }
 
 pub fn create_default_npmrc() -> Arc<ResolvedNpmRc> {
@@ -938,7 +938,7 @@ impl CliOptions {
     } else {
       maybe_package_json = discover_package_json(&flags, None, &initial_cwd)?;
     }
-    let npmrc = discover_npmrc(
+    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" {

filename: cli/lsp/config.rs
patch: @@ -1,6 +1,7 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 
 use super::logging::lsp_log;
+use crate::args::discover_npmrc;
 use crate::args::read_lockfile_at_path;
 use crate::args::ConfigFile;
 use crate::args::FmtOptions;
@@ -1175,10 +1176,12 @@ impl ConfigData {
         .entry(config_file.specifier.clone())
         .or_insert(ConfigWatchedFileType::DenoJson);
     }
-    let config_file_canonicalized_specifier = config_file
+    let config_file_path = config_file
       .as_ref()
-      .and_then(|c| c.specifier.to_file_path().ok())
-      .and_then(|p| canonicalize_path_maybe_not_exists(&p).ok())
+      .and_then(|c| specifier_to_file_path(&c.specifier).ok());
+    let config_file_canonicalized_specifier = config_file_path
+      .as_ref()
+      .and_then(|p| canonicalize_path_maybe_not_exists(p).ok())
       .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
     if let Some(specifier) = config_file_canonicalized_specifier {
       watched_files
@@ -1306,25 +1309,25 @@ 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) {
+    let package_json_path = specifier_to_file_path(scope)
+      .ok()
+      .map(|p| p.join("package.json"));
+    if let Some(path) = &package_json_path {
+      if let Ok(specifier) = ModuleSpecifier::from_file_path(path) {
         watched_files
           .entry(specifier)
           .or_insert(ConfigWatchedFileType::PackageJson);
       }
       let package_json_canonicalized_specifier =
-        canonicalize_path_maybe_not_exists(&path)
+        canonicalize_path_maybe_not_exists(path)
           .ok()
           .and_then(|p| ModuleSpecifier::from_file_path(p).ok());
       if let Some(specifier) = package_json_canonicalized_specifier {
         watched_files
           .entry(specifier)
           .or_insert(ConfigWatchedFileType::PackageJson);
       }
-      if let Ok(source) = std::fs::read_to_string(&path) {
+      if let Ok(source) = std::fs::read_to_string(path) {
         match PackageJson::load_from_string(path.clone(), source) {
           Ok(result) => {
             lsp_log!("  Resolved package.json: \"{}\"", path.display());
@@ -1340,6 +1343,17 @@ impl ConfigData {
         }
       }
     }
+    let npmrc = discover_npmrc(package_json_path, config_file_path)
+      .inspect(|(_, path)| {
+        if let Some(path) = path {
+          lsp_log!("  Resolved .npmrc: \"{}\"", path.display());
+        }
+      })
+      .inspect_err(|err| {
+        lsp_warn!("  Couldn't read .npmrc for \"{scope}\": {err}");
+      })
+      .map(|(r, _)| r)
+      .ok();
     let byonm = std::env::var("DENO_UNSTABLE_BYONM").is_ok()
       || config_file
         .as_ref()
@@ -1464,7 +1478,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),
+      npmrc,
       import_map: import_map.map(Arc::new),
       import_map_from_settings,
       watched_files,

filename: tests/integration/lsp_tests.rs
patch: @@ -8860,6 +8860,83 @@ fn lsp_tls_cert() {
   client.shutdown();
 }
 
+#[test]
+fn lsp_npmrc() {
+  let context = TestContextBuilder::new()
+    .use_http_server()
+    .use_temp_cwd()
+    .add_npm_env_vars()
+    .env("DENO_FUTURE", "1")
+    .build();
+  let temp_dir = context.temp_dir();
+  temp_dir.write(
+    temp_dir.path().join("deno.json"),
+    json!({
+      "nodeModulesDir": true,
+    })
+    .to_string(),
+  );
+  temp_dir.write(
+    temp_dir.path().join("package.json"),
+    json!({
+      "name": "npmrc_test",
+      "version": "0.0.1",
+      "dependencies": {
+        "@denotest/basic": "1.0.0",
+      },
+    })
+    .to_string(),
+  );
+  temp_dir.write(
+    temp_dir.path().join(".npmrc"),
+    "\
+@denotest:registry=http://127.0.0.1:4261/
+//127.0.0.1:4261/:_authToken=private-reg-token
+",
+  );
+  let file = source_file(
+    temp_dir.path().join("main.ts"),
+    r#"
+      import { getValue, setValue } from "@denotest/basic";
+      setValue(42);
+      const n: string = getValue();
+      console.log(n);
+    "#,
+  );
+  let mut client = context.new_lsp_command().build();
+  client.initialize_default();
+  client.write_request(
+    "workspace/executeCommand",
+    json!({
+      "command": "deno.cache",
+      "arguments": [[], file.uri()],
+    }),
+  );
+  let diagnostics = client.did_open_file(&file);
+  assert_eq!(
+    json!(diagnostics.all()),
+    json!([
+      {
+        "range": {
+          "start": {
+            "line": 3,
+            "character": 12,
+          },
+          "end": {
+            "line": 3,
+            "character": 13,
+          },
+        },
+        "severity": 1,
+        "code": 2322,
+        "source": "deno-ts",
+        "message": "Type 'number' is not assignable to type 'string'.",
+      },
+    ]),
+  );
+  client.shutdown();
+}
+
 #[test]
 fn lsp_diagnostics_warn_redirect() {
   let context = TestContextBuilder::new()