diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb index 8b813959f9..a3da496044 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb @@ -375,7 +375,11 @@ def library? return true if dependency_files.any? { |f| f.name == "lerna.json" } @library = - LibraryDetector.new(package_json_file: package_json).library? + LibraryDetector.new( + package_json_file: package_json, + credentials: credentials, + dependency_files: dependency_files + ).library? end def dependency_source_details diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb index 6244797e63..3f39cb5668 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb @@ -8,8 +8,10 @@ module Dependabot module NpmAndYarn class UpdateChecker class LibraryDetector - def initialize(package_json_file:) + def initialize(package_json_file:, credentials:, dependency_files:) @package_json_file = package_json_file + @credentials = credentials + @dependency_files = dependency_files end def library? @@ -20,7 +22,7 @@ def library? private - attr_reader :package_json_file + attr_reader :package_json_file, :credentials, :dependency_files def package_json_may_be_for_library? return false unless project_name @@ -36,7 +38,8 @@ def npm_response_matches_package_json? return false unless project_description # Check if the project is listed on npm. If it is, it's a library - @project_npm_response ||= Dependabot::RegistryClient.get(url: "https://registry.npmjs.org/#{escaped_project_name}") + url = "#{registry.chomp('/')}/#{escaped_project_name}" + @project_npm_response ||= Dependabot::RegistryClient.get(url: url) return false unless @project_npm_response.status == 200 @project_npm_response.body.force_encoding("UTF-8").encode. @@ -56,6 +59,15 @@ def escaped_project_name def parsed_package_json @parsed_package_json ||= JSON.parse(package_json_file.content) end + + def registry + NpmAndYarn::UpdateChecker::RegistryFinder.new( + dependency: nil, + credentials: credentials, + npmrc_file: dependency_files.find { |f| f.name.end_with?(".npmrc") }, + yarnrc_file: dependency_files.find { |f| f.name.end_with?(".yarnrc") } + ).registry_from_rc(project_name) + end end end end diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb index 90deb85b94..9faaa6152c 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb @@ -19,6 +19,10 @@ class RegistryFinder /^registry\s*=\s*['"]?(?.*?)['"]?$/.freeze YARN_GLOBAL_REGISTRY_REGEX = /^(?:--)?registry\s+['"](?.*)['"]/.freeze + NPM_SCOPED_REGISTRY_REGEX = + /^(?@[^:]+)\s*:registry\s*=\s*['"]?(?.*?)['"]?$/.freeze + YARN_SCOPED_REGISTRY_REGEX = + /['"](?@[^:]+):registry['"]\s['"](?.*)['"]/.freeze def initialize(dependency:, credentials:, npmrc_file: nil, yarnrc_file: nil) @@ -46,6 +50,13 @@ def self.central_registry?(registry) end end + def registry_from_rc(dependency_name) + return global_registry unless dependency_name.start_with?("@") && dependency_name.include?("/") + + scope = dependency_name.split("/").first + scoped_registry(scope) + end + private attr_reader :dependency, :credentials, :npmrc_file, :yarnrc_file @@ -64,7 +75,7 @@ def first_registry_with_dependency_details nil end&.fetch("registry") - @first_registry_with_dependency_details ||= global_registry + @first_registry_with_dependency_details ||= global_registry.sub(%r{/+$}, "").sub(%r{^.*?//}, "") end def registry_url @@ -194,22 +205,32 @@ def global_registry npmrc_file&.content.to_s.scan(NPM_GLOBAL_REGISTRY_REGEX) do next if Regexp.last_match[:registry].include?("${") - registry = Regexp.last_match[:registry].strip. - sub(%r{/+$}, ""). - sub(%r{^.*?//}, "") - return registry + return Regexp.last_match[:registry].strip end yarnrc_file&.content.to_s.scan(YARN_GLOBAL_REGISTRY_REGEX) do next if Regexp.last_match[:registry].include?("${") - registry = Regexp.last_match[:registry].strip. - sub(%r{/+$}, ""). - sub(%r{^.*?//}, "") - return registry + return Regexp.last_match[:registry].strip + end + + "https://registry.npmjs.org" + end + + def scoped_registry(scope) + npmrc_file&.content.to_s.scan(NPM_SCOPED_REGISTRY_REGEX) do + next if Regexp.last_match[:registry].include?("${") || Regexp.last_match[:scope] != scope + + return Regexp.last_match[:registry].strip + end + + yarnrc_file&.content.to_s.scan(YARN_SCOPED_REGISTRY_REGEX) do + next if Regexp.last_match[:registry].include?("${") || Regexp.last_match[:scope] != scope + + return Regexp.last_match[:registry].strip end - "registry.npmjs.org" + global_registry end # npm registries expect slashes to be escaped diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/library_detector_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/library_detector_spec.rb index 1e2472a4c5..136bbb8101 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/library_detector_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/library_detector_spec.rb @@ -5,10 +5,18 @@ require "dependabot/npm_and_yarn/update_checker/library_detector" RSpec.describe Dependabot::NpmAndYarn::UpdateChecker::LibraryDetector do - subject(:finder) { described_class.new(package_json_file: package_json_file) } + subject(:finder) do + described_class.new( + package_json_file: package_json_file, + credentials: credentials, + dependency_files: dependency_files + ) + end let(:package_json_file) do project_dependency_files(project_name).find { |f| f.name == "package.json" } end + let(:credentials) { {} } + let(:dependency_files) { project_dependency_files(project_name) } describe "library?" do subject { finder.library? } @@ -64,5 +72,69 @@ end end end + + context "with a custom global registry" do + let(:project_name) { "npm8/library_with_global_registry" } + + context "not listed in registry" do + before do + stub_request(:get, "http://example.com/dependabot/etag"). + to_return(status: 404) + end + + it { is_expected.to eq(false) } + end + + context "listed on registry" do + before do + stub_request(:get, "http://example.com/dependabot/etag"). + to_return(status: 200, body: body) + end + + context "with a description that matches" do + let(:body) { fixture("npm_responses", "etag.json") } + it { is_expected.to eq(true) } + end + + context "with a description that doesn't match" do + let(:body) do + fixture("npm_responses", "is_number.json") + end + it { is_expected.to eq(false) } + end + end + end + + context "with a custom scoped registry" do + let(:project_name) { "npm8/library_with_scoped_registry" } + + context "not listed in registry" do + before do + stub_request(:get, "http://example.com/dependabot/@dependabot%2Fetag"). + to_return(status: 404) + end + + it { is_expected.to eq(false) } + end + + context "listed on registry" do + before do + stub_request(:get, "http://example.com/dependabot/@dependabot%2Fetag"). + to_return(status: 200, body: body) + end + + context "with a description that matches" do + let(:body) { fixture("npm_responses", "etag.json") } + it { is_expected.to eq(true) } + end + + context "with a description that doesn't match" do + let(:body) do + fixture("npm_responses", "is_number.json") + end + it { is_expected.to eq(false) } + end + end + end end end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/registry_finder_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/registry_finder_spec.rb index a631cb3717..e21a63da98 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/registry_finder_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker/registry_finder_spec.rb @@ -37,6 +37,40 @@ end let(:source) { nil } + describe "registry_from_rc" do + subject { finder.registry_from_rc(dependency_name) } + + let(:dependency_name) { "some_dep" } + + it { is_expected.to eq("https://registry.npmjs.org") } + + context "with a global npm registry" do + let(:npmrc_file) { Dependabot::DependencyFile.new(name: ".npmrc", content: "registry=http://example.com") } + + it { is_expected.to eq("http://example.com") } + end + + context "with a global yarn registry" do + let(:yarnrc_file) { Dependabot::DependencyFile.new(name: ".yarnrc", content: 'registry "http://example.com"') } + + it { is_expected.to eq("http://example.com") } + end + + context "with a scoped npm registry" do + let(:dependency_name) { "@dependabot/some_dep" } + let(:npmrc_file) { Dependabot::DependencyFile.new(name: ".npmrc", content: "@dependabot:registry=http://example.com") } + + it { is_expected.to eq("http://example.com") } + end + + context "with a scoped yarn registry" do + let(:dependency_name) { "@dependabot/some_dep" } + let(:yarnrc_file) { Dependabot::DependencyFile.new(name: ".yarnrc", content: '"@dependabot:registry" "http://example.com"') } + + it { is_expected.to eq("http://example.com") } + end + end + describe "registry" do subject { finder.registry } diff --git a/npm_and_yarn/spec/fixtures/projects/npm8/library_with_global_registry/.npmrc b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_global_registry/.npmrc new file mode 100644 index 0000000000..a6b36cf454 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_global_registry/.npmrc @@ -0,0 +1 @@ +registry=http://example.com/dependabot diff --git a/npm_and_yarn/spec/fixtures/projects/npm8/library_with_global_registry/package.json b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_global_registry/package.json new file mode 100644 index 0000000000..3b3156d94a --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_global_registry/package.json @@ -0,0 +1,44 @@ +{ + "name": "etag", + "description": "Create simple ETags", + "version": "1.8.0", + "contributors": [ + "Douglas Christopher Wilson ", + "David Björklund " + ], + "license": "MIT", + "keywords": [ + "etag", + "http", + "res" + ], + "repository": "jshttp/etag", + "devDependencies": { + "beautify-benchmark": "0.2.4", + "benchmark": "2.1.3", + "eslint": "3.15.0", + "eslint-config-standard": "6.2.1", + "eslint-plugin-markdown": "1.0.0-beta.3", + "eslint-plugin-promise": "3.4.2", + "eslint-plugin-standard": "2.0.1", + "istanbul": "0.4.5", + "mocha": "1.21.5", + "seedrandom": "2.4.2" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "bench": "node benchmark/index.js", + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" + } +} diff --git a/npm_and_yarn/spec/fixtures/projects/npm8/library_with_scoped_registry/.npmrc b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_scoped_registry/.npmrc new file mode 100644 index 0000000000..182423a082 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_scoped_registry/.npmrc @@ -0,0 +1 @@ +@dependabot:registry=http://example.com/dependabot diff --git a/npm_and_yarn/spec/fixtures/projects/npm8/library_with_scoped_registry/package.json b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_scoped_registry/package.json new file mode 100644 index 0000000000..20e108dc9e --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm8/library_with_scoped_registry/package.json @@ -0,0 +1,44 @@ +{ + "name": "@dependabot/etag", + "description": "Create simple ETags", + "version": "1.8.0", + "contributors": [ + "Douglas Christopher Wilson ", + "David Björklund " + ], + "license": "MIT", + "keywords": [ + "etag", + "http", + "res" + ], + "repository": "jshttp/etag", + "devDependencies": { + "beautify-benchmark": "0.2.4", + "benchmark": "2.1.3", + "eslint": "3.15.0", + "eslint-config-standard": "6.2.1", + "eslint-plugin-markdown": "1.0.0-beta.3", + "eslint-plugin-promise": "3.4.2", + "eslint-plugin-standard": "2.0.1", + "istanbul": "0.4.5", + "mocha": "1.21.5", + "seedrandom": "2.4.2" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "bench": "node benchmark/index.js", + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" + } +}