dependabot-npm_and_yarn 0.91.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +14 -0
  3. data/helpers/npm/.eslintrc +14 -0
  4. data/helpers/npm/bin/run.js +34 -0
  5. data/helpers/npm/lib/helpers.js +25 -0
  6. data/helpers/npm/lib/peer-dependency-checker.js +102 -0
  7. data/helpers/npm/lib/subdependency-updater.js +48 -0
  8. data/helpers/npm/lib/updater.js +101 -0
  9. data/helpers/npm/package-lock.json +8868 -0
  10. data/helpers/npm/package.json +17 -0
  11. data/helpers/npm/test/fixtures/npm-left-pad.json +1 -0
  12. data/helpers/npm/test/fixtures/updater/original/package-lock.json +16 -0
  13. data/helpers/npm/test/fixtures/updater/original/package.json +9 -0
  14. data/helpers/npm/test/fixtures/updater/updated/package-lock.json +16 -0
  15. data/helpers/npm/test/helpers.js +7 -0
  16. data/helpers/npm/test/updater.test.js +50 -0
  17. data/helpers/npm/yarn.lock +6176 -0
  18. data/helpers/yarn/.eslintrc +14 -0
  19. data/helpers/yarn/bin/run.js +36 -0
  20. data/helpers/yarn/lib/fix-duplicates.js +78 -0
  21. data/helpers/yarn/lib/helpers.js +5 -0
  22. data/helpers/yarn/lib/lockfile-parser.js +21 -0
  23. data/helpers/yarn/lib/peer-dependency-checker.js +130 -0
  24. data/helpers/yarn/lib/replace-lockfile-declaration.js +57 -0
  25. data/helpers/yarn/lib/subdependency-updater.js +69 -0
  26. data/helpers/yarn/lib/updater.js +266 -0
  27. data/helpers/yarn/package.json +17 -0
  28. data/helpers/yarn/test/fixtures/updater/original/package.json +6 -0
  29. data/helpers/yarn/test/fixtures/updater/original/yarn.lock +11 -0
  30. data/helpers/yarn/test/fixtures/updater/updated/yarn.lock +12 -0
  31. data/helpers/yarn/test/fixtures/updater/with-version-comments/package.json +5 -0
  32. data/helpers/yarn/test/fixtures/updater/with-version-comments/yarn.lock +13 -0
  33. data/helpers/yarn/test/fixtures/yarnpkg-is-positive.json +1 -0
  34. data/helpers/yarn/test/fixtures/yarnpkg-left-pad.json +1 -0
  35. data/helpers/yarn/test/helpers.js +7 -0
  36. data/helpers/yarn/test/updater.test.js +93 -0
  37. data/helpers/yarn/yarn.lock +4760 -0
  38. data/lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb +146 -0
  39. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +332 -0
  40. data/lib/dependabot/npm_and_yarn/file_parser.rb +397 -0
  41. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +527 -0
  42. data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +190 -0
  43. data/lib/dependabot/npm_and_yarn/file_updater/package_json_preparer.rb +87 -0
  44. data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +218 -0
  45. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +471 -0
  46. data/lib/dependabot/npm_and_yarn/file_updater.rb +189 -0
  47. data/lib/dependabot/npm_and_yarn/metadata_finder.rb +217 -0
  48. data/lib/dependabot/npm_and_yarn/native_helpers.rb +28 -0
  49. data/lib/dependabot/npm_and_yarn/requirement.rb +145 -0
  50. data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +340 -0
  51. data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +67 -0
  52. data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +224 -0
  53. data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +193 -0
  54. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +223 -0
  55. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +495 -0
  56. data/lib/dependabot/npm_and_yarn/update_checker.rb +282 -0
  57. data/lib/dependabot/npm_and_yarn/version.rb +34 -0
  58. data/lib/dependabot/npm_and_yarn.rb +11 -0
  59. metadata +226 -0
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/npm_and_yarn/file_updater"
4
+
5
+ module Dependabot
6
+ module NpmAndYarn
7
+ class FileUpdater
8
+ # Build a .npmrc file from the lockfile content, credentials, and any
9
+ # committed .npmrc
10
+ class NpmrcBuilder
11
+ CENTRAL_REGISTRIES = %w(
12
+ registry.npmjs.org
13
+ registry.yarnpkg.com
14
+ ).freeze
15
+
16
+ def initialize(dependency_files:, credentials:)
17
+ @dependency_files = dependency_files
18
+ @credentials = credentials
19
+ end
20
+
21
+ def npmrc_content
22
+ initial_content =
23
+ if npmrc_file then complete_npmrc_from_credentials
24
+ elsif yarnrc_file then build_npmrc_from_yarnrc
25
+ else build_npmrc_content_from_lockfile
26
+ end
27
+
28
+ return initial_content || "" unless registry_credentials.any?
29
+
30
+ ([initial_content] + credential_lines_for_npmrc).compact.join("\n")
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :dependency_files, :credentials
36
+
37
+ def build_npmrc_content_from_lockfile
38
+ return unless yarn_lock || package_lock
39
+ return unless global_registry
40
+
41
+ "registry = https://#{global_registry['registry']}\n"\
42
+ "#{global_registry_auth_line}\n"\
43
+ "always-auth = true"
44
+ end
45
+
46
+ def global_registry
47
+ @global_registry ||=
48
+ registry_credentials.find do |cred|
49
+ next false if CENTRAL_REGISTRIES.include?(cred["registry"])
50
+
51
+ # If all the URLs include this registry, it's global
52
+ if dependency_urls.all? { |url| url.include?(cred["registry"]) }
53
+ next true
54
+ end
55
+
56
+ # If any unscoped URLs include this registry, it's global
57
+ dependency_urls.
58
+ reject { |u| u.include?("@") || u.include?("%40") }.
59
+ any? { |url| url.include?(cred["registry"]) }
60
+ end
61
+ end
62
+
63
+ def global_registry_auth_line
64
+ token = global_registry.fetch("token")
65
+
66
+ if token.include?(":")
67
+ encoded_token = Base64.encode64(token).delete("\n")
68
+ "_auth = #{encoded_token}"
69
+ elsif Base64.decode64(token).ascii_only? &&
70
+ Base64.decode64(token).include?(":")
71
+ "_auth = #{token.delete("\n")}"
72
+ else
73
+ "_authToken = #{token}"
74
+ end
75
+ end
76
+
77
+ def dependency_urls
78
+ if package_lock
79
+ parsed_package_lock.fetch("dependencies", {}).
80
+ map { |_, details| details["resolved"] }.compact.
81
+ select { |url| url.is_a?(String) }.
82
+ reject { |url| url.start_with?("git") }
83
+ elsif yarn_lock
84
+ yarn_lock.content.scan(/ resolved "(.*?)"/).flatten
85
+ end
86
+ end
87
+
88
+ def complete_npmrc_from_credentials
89
+ initial_content = npmrc_file.content.
90
+ gsub(/^.*\$\{.*\}.*/, "").strip + "\n"
91
+ return initial_content unless yarn_lock || package_lock
92
+ return initial_content unless global_registry
93
+
94
+ initial_content +
95
+ "registry = https://#{global_registry['registry']}\n"\
96
+ "#{global_registry_auth_line}\n"\
97
+ "always-auth = true\n"
98
+ end
99
+
100
+ def build_npmrc_from_yarnrc
101
+ yarnrc_global_registry =
102
+ yarnrc_file.content.
103
+ lines.find { |line| line.match?(/^\s*registry\s/) }&.
104
+ match(/^\s*registry\s+"(?<registry>[^"]+)"/)&.
105
+ named_captures&.fetch("registry")
106
+
107
+ if yarnrc_global_registry
108
+ return "registry = #{yarnrc_global_registry}\n"
109
+ end
110
+
111
+ build_npmrc_content_from_lockfile
112
+ end
113
+
114
+ def credential_lines_for_npmrc
115
+ lines = []
116
+ registry_credentials.each do |cred|
117
+ registry = cred.fetch("registry")
118
+
119
+ lines << registry_scope(registry) if registry_scope(registry)
120
+
121
+ token = cred.fetch("token")
122
+ if token.include?(":")
123
+ encoded_token = Base64.encode64(token).delete("\n")
124
+ lines << "//#{registry}/:_auth=#{encoded_token}"
125
+ elsif Base64.decode64(token).ascii_only? &&
126
+ Base64.decode64(token).include?(":")
127
+ lines << %(//#{registry}/:_auth=#{token.delete("\n")})
128
+ else
129
+ lines << "//#{registry}/:_authToken=#{token}"
130
+ end
131
+ end
132
+
133
+ return lines unless lines.any? { |str| str.include?("auth=") }
134
+
135
+ # Work around a suspected yarn bug
136
+ ["always-auth = true"] + lines
137
+ end
138
+
139
+ def registry_scope(registry)
140
+ # Central registries don't just apply to scopes
141
+ return if CENTRAL_REGISTRIES.include?(registry)
142
+
143
+ return unless dependency_urls
144
+
145
+ affected_urls = dependency_urls.
146
+ select { |url| url.include?(registry) }
147
+
148
+ scopes = affected_urls.map do |url|
149
+ url.split(/\%40|@/)[1]&.split(%r{\%2F|/})&.first
150
+ end
151
+
152
+ # Registry used for unscoped packages
153
+ return if scopes.include?(nil)
154
+
155
+ # This just seems unlikely
156
+ return unless scopes.uniq.count == 1
157
+
158
+ "@#{scopes.first}:registry=https://#{registry}/"
159
+ end
160
+
161
+ def registry_credentials
162
+ credentials.select { |cred| cred.fetch("type") == "npm_registry" }
163
+ end
164
+
165
+ def parsed_package_lock
166
+ @parsed_package_lock ||= JSON.parse(package_lock.content)
167
+ end
168
+
169
+ def npmrc_file
170
+ @npmrc_file ||= dependency_files.
171
+ find { |f| f.name.end_with?(".npmrc") }
172
+ end
173
+
174
+ def yarnrc_file
175
+ @yarnrc_file ||= dependency_files.
176
+ find { |f| f.name.end_with?(".yarnrc") }
177
+ end
178
+
179
+ def yarn_lock
180
+ @yarn_lock ||= dependency_files.find { |f| f.name == "yarn.lock" }
181
+ end
182
+
183
+ def package_lock
184
+ @package_lock ||=
185
+ dependency_files.find { |f| f.name == "package-lock.json" }
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/npm_and_yarn/file_updater"
4
+ require "dependabot/npm_and_yarn/file_parser"
5
+
6
+ module Dependabot
7
+ module NpmAndYarn
8
+ class FileUpdater
9
+ class PackageJsonPreparer
10
+ def initialize(package_json_content:)
11
+ @package_json_content = package_json_content
12
+ end
13
+
14
+ def prepared_content
15
+ content = package_json_content
16
+ content = replace_ssh_sources(content)
17
+ content = remove_workspace_path_prefixes(content)
18
+ content = remove_invalid_characters(content)
19
+ content
20
+ end
21
+
22
+ def replace_ssh_sources(content)
23
+ updated_content = content
24
+
25
+ git_ssh_requirements_to_swap.each do |req|
26
+ new_req = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'https://\1/')
27
+ updated_content = updated_content.gsub(req, new_req)
28
+ end
29
+
30
+ updated_content
31
+ end
32
+
33
+ # A bug prevents Yarn recognising that a directory is part of a
34
+ # workspace if it is specified with a `./` prefix.
35
+ def remove_workspace_path_prefixes(content)
36
+ json = JSON.parse(content)
37
+ return content unless json.key?("workspaces")
38
+
39
+ workspace_object = json.fetch("workspaces")
40
+ paths_array =
41
+ if workspace_object.is_a?(Hash)
42
+ workspace_object.values_at("packages", "nohoist").
43
+ flatten.compact
44
+ elsif workspace_object.is_a?(Array) then workspace_object
45
+ else raise "Unexpected workspace object"
46
+ end
47
+
48
+ paths_array.each { |path| path.gsub!(%r{^\./}, "") }
49
+
50
+ json.to_json
51
+ end
52
+
53
+ def remove_invalid_characters(content)
54
+ content.
55
+ gsub(/\{\{.*?\}\}/, "something"). # {{ name }} syntax not allowed
56
+ gsub(/(?<!\\)\\ /, " "). # escaped whitespace not allowed
57
+ gsub(%r{^\s*//.*}, " ") # comments are not allowed
58
+ end
59
+
60
+ def swapped_ssh_requirements
61
+ git_ssh_requirements_to_swap
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :package_json_content
67
+
68
+ def git_ssh_requirements_to_swap
69
+ return @git_ssh_requirements_to_swap if @git_ssh_requirements_to_swap
70
+
71
+ @git_ssh_requirements_to_swap = []
72
+
73
+ NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |t|
74
+ JSON.parse(package_json_content).fetch(t, {}).each do |_, req|
75
+ next unless req.start_with?("git+ssh:")
76
+
77
+ req = req.split("#").first
78
+ @git_ssh_requirements_to_swap << req
79
+ end
80
+ end
81
+
82
+ @git_ssh_requirements_to_swap
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/npm_and_yarn/file_updater"
4
+
5
+ module Dependabot
6
+ module NpmAndYarn
7
+ class FileUpdater
8
+ class PackageJsonUpdater
9
+ def initialize(package_json:, dependencies:)
10
+ @package_json = package_json
11
+ @dependencies = dependencies
12
+ end
13
+
14
+ def updated_package_json
15
+ updated_file = package_json.dup
16
+ updated_file.content = updated_package_json_content
17
+ updated_file
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :package_json, :dependencies
23
+
24
+ def updated_package_json_content
25
+ dependencies.reduce(package_json.content.dup) do |content, dep|
26
+ updated_requirements(dep).each do |new_req|
27
+ old_req = old_requirement(dep, new_req)
28
+
29
+ new_content = update_package_json_declaration(
30
+ package_json_content: content,
31
+ dependency_name: dep.name,
32
+ old_req: old_req,
33
+ new_req: new_req
34
+ )
35
+
36
+ raise "Expected content to change!" if content == new_content
37
+
38
+ content = new_content
39
+ end
40
+
41
+ new_requirements(dep).each do |new_req|
42
+ old_req = old_requirement(dep, new_req)
43
+
44
+ content = update_package_json_resolutions(
45
+ package_json_content: content,
46
+ new_req: new_req,
47
+ dependency: dep,
48
+ old_req: old_req
49
+ )
50
+ end
51
+
52
+ content
53
+ end
54
+ end
55
+
56
+ def old_requirement(dependency, new_requirement)
57
+ dependency.previous_requirements.
58
+ select { |r| r[:file] == package_json.name }.
59
+ find { |r| r[:groups] == new_requirement[:groups] }
60
+ end
61
+
62
+ def new_requirements(dependency)
63
+ dependency.requirements.select { |r| r[:file] == package_json.name }
64
+ end
65
+
66
+ def updated_requirements(dependency)
67
+ new_requirements(dependency).
68
+ reject { |r| dependency.previous_requirements.include?(r) }
69
+ end
70
+
71
+ def update_package_json_declaration(package_json_content:, new_req:,
72
+ dependency_name:, old_req:)
73
+ original_line = declaration_line(
74
+ dependency_name: dependency_name,
75
+ dependency_req: old_req,
76
+ content: package_json_content
77
+ )
78
+
79
+ replacement_line = replacement_declaration_line(
80
+ original_line: original_line,
81
+ old_req: old_req,
82
+ new_req: new_req
83
+ )
84
+
85
+ groups = new_req.fetch(:groups)
86
+
87
+ update_package_json_sections(
88
+ groups,
89
+ package_json_content,
90
+ original_line,
91
+ replacement_line
92
+ )
93
+ end
94
+
95
+ # For full details on how Yarn resolutions work, see
96
+ # https://github.com/yarnpkg/rfcs/blob/master/implemented/
97
+ # 0000-selective-versions-resolutions.md
98
+ def update_package_json_resolutions(package_json_content:, new_req:,
99
+ dependency:, old_req:)
100
+ dep = dependency
101
+ resolutions =
102
+ JSON.parse(package_json_content).fetch("resolutions", {}).
103
+ reject { |_, v| v != old_req && v != dep.previous_version }.
104
+ select { |k, _| k == dep.name || k.end_with?("/#{dep.name}") }
105
+
106
+ return package_json_content unless resolutions.any?
107
+
108
+ content = package_json_content
109
+ resolutions.each do |_, resolution|
110
+ original_line = declaration_line(
111
+ dependency_name: dep.name,
112
+ dependency_req: { requirement: resolution },
113
+ content: content
114
+ )
115
+
116
+ new_resolution = resolution == old_req ? new_req : dep.version
117
+
118
+ replacement_line = replacement_declaration_line(
119
+ original_line: original_line,
120
+ old_req: { requirement: resolution },
121
+ new_req: { requirement: new_resolution }
122
+ )
123
+
124
+ content = update_package_json_sections(
125
+ ["resolutions"], content, original_line, replacement_line
126
+ )
127
+ end
128
+ content
129
+ end
130
+
131
+ def declaration_line(dependency_name:, dependency_req:, content:)
132
+ git_dependency = dependency_req.dig(:source, :type) == "git"
133
+
134
+ unless git_dependency
135
+ requirement = dependency_req.fetch(:requirement)
136
+ return content.match(/"#{Regexp.escape(dependency_name)}"\s*:\s*
137
+ "#{Regexp.escape(requirement)}"/x).to_s
138
+ end
139
+
140
+ username, repo =
141
+ dependency_req.dig(:source, :url).split("/").last(2)
142
+
143
+ content.match(
144
+ %r{"#{Regexp.escape(dependency_name)}"\s*:\s*
145
+ ".*?#{Regexp.escape(username)}/#{Regexp.escape(repo)}.*"}x
146
+ ).to_s
147
+ end
148
+
149
+ def replacement_declaration_line(original_line:, old_req:, new_req:)
150
+ was_git_dependency = old_req.dig(:source, :type) == "git"
151
+ now_git_dependency = new_req.dig(:source, :type) == "git"
152
+
153
+ unless was_git_dependency
154
+ return original_line.gsub(
155
+ %("#{old_req.fetch(:requirement)}"),
156
+ %("#{new_req.fetch(:requirement)}")
157
+ )
158
+ end
159
+
160
+ unless now_git_dependency
161
+ return original_line.gsub(
162
+ /(?<=\s").*[^\\](?=")/,
163
+ new_req.fetch(:requirement)
164
+ )
165
+ end
166
+
167
+ if original_line.include?("semver:")
168
+ return original_line.gsub(
169
+ %(semver:#{old_req.fetch(:requirement)}"),
170
+ %(semver:#{new_req.fetch(:requirement)}")
171
+ )
172
+ end
173
+
174
+ original_line.gsub(
175
+ %(\##{old_req.dig(:source, :ref)}"),
176
+ %(\##{new_req.dig(:source, :ref)}")
177
+ )
178
+ end
179
+
180
+ def update_package_json_sections(sections, content, old_line,
181
+ new_line)
182
+ # Currently, Dependabot doesn't update peerDependencies. However,
183
+ # if a development dependency is being updated and its requirement
184
+ # matches the requirement on a peer dependency we probably want to
185
+ # update the peer too.
186
+ #
187
+ # TODO: Move this logic to the UpdateChecker (and parse peer deps)
188
+ sections += ["peerDependencies"]
189
+ sections_regex = /#{sections.join("|")}/
190
+
191
+ declaration_blocks = []
192
+
193
+ content.scan(/['"]#{sections_regex}['"]\s*:\s*\{/m) do
194
+ mtch = Regexp.last_match
195
+ declaration_blocks <<
196
+ mtch.to_s +
197
+ mtch.post_match[0..closing_bracket_index(mtch.post_match)]
198
+ end
199
+
200
+ declaration_blocks.reduce(content.dup) do |new_content, block|
201
+ updated_block = block.sub(old_line, new_line)
202
+ new_content.sub!(block, updated_block)
203
+ end
204
+ end
205
+
206
+ def closing_bracket_index(string)
207
+ closes_required = 1
208
+
209
+ string.chars.each_with_index do |char, index|
210
+ closes_required += 1 if char == "{"
211
+ closes_required -= 1 if char == "}"
212
+ return index if closes_required.zero?
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end