dependabot-npm_and_yarn 0.131.2 → 0.133.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/npm6/updater.js +1 -0
  3. data/helpers/lib/npm7/index.js +5 -0
  4. data/helpers/lib/npm7/peer-dependency-checker.js +77 -0
  5. data/helpers/package.json +1 -1
  6. data/helpers/test/npm7/conflicting-dependency-parser.test.js +67 -0
  7. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/deeply-nested/package-lock.json +1291 -0
  8. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/deeply-nested/package.json +14 -0
  9. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/nested/package-lock.json +411 -0
  10. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/nested/package.json +14 -0
  11. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/simple/package-lock.json +64 -0
  12. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/simple/package.json +14 -0
  13. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency/package-lock.json +290 -0
  14. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency/package.json +23 -0
  15. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency_multiple/package-lock.json +965 -0
  16. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency_multiple/package.json +10 -0
  17. data/helpers/test/npm7/helpers.js +21 -0
  18. data/helpers/test/npm7/peer-dependency-checker.test.js +107 -0
  19. data/helpers/yarn.lock +1258 -1271
  20. data/lib/dependabot/npm_and_yarn/dependency_files_filterer.rb +3 -3
  21. data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +40 -16
  22. data/lib/dependabot/npm_and_yarn/file_updater.rb +9 -19
  23. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +409 -167
  24. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +1 -2
  25. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +1 -2
  26. metadata +19 -4
@@ -20,7 +20,7 @@ module Dependabot
20
20
  dependency_files.select do |file|
21
21
  package_files_requiring_update.include?(file) ||
22
22
  package_required_lockfile?(file) ||
23
- yarn_workspaces_lockfile?(file)
23
+ workspaces_lockfile?(file)
24
24
  end
25
25
  end
26
26
  end
@@ -51,8 +51,8 @@ module Dependabot
51
51
  end
52
52
  end
53
53
 
54
- def yarn_workspaces_lockfile?(lockfile)
55
- return false unless lockfile.name == "yarn.lock"
54
+ def workspaces_lockfile?(lockfile)
55
+ return false unless ["yarn.lock", "package-lock.json"].include?(lockfile.name)
56
56
  return false unless parsed_root_package_json["workspaces"]
57
57
 
58
58
  updated_dependencies_in_lockfile?(lockfile)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "dependabot/dependency_file"
4
4
  require "dependabot/npm_and_yarn/file_parser"
5
+ require "dependabot/npm_and_yarn/helpers"
5
6
 
6
7
  module Dependabot
7
8
  module NpmAndYarn
@@ -23,23 +24,9 @@ module Dependabot
23
24
  potential_lockfiles_for_manifest(manifest_name).each do |lockfile|
24
25
  details =
25
26
  if [*package_locks, *shrinkwraps].include?(lockfile)
26
- parsed_lockfile = parse_package_lock(lockfile)
27
- parsed_lockfile.dig("dependencies", dependency_name)
27
+ npm_lockfile_details(lockfile, dependency_name, manifest_name)
28
28
  else
29
- parsed_yarn_lock = parse_yarn_lock(lockfile)
30
- details_candidates =
31
- parsed_yarn_lock.
32
- select { |k, _| k.split(/(?<=\w)\@/)[0] == dependency_name }
33
-
34
- # If there's only one entry for this dependency, use it, even if
35
- # the requirement in the lockfile doesn't match
36
- if details_candidates.one?
37
- details_candidates.first.last
38
- else
39
- details_candidates.find do |k, _|
40
- k.split(/(?<=\w)\@/)[1..-1].join("@") == requirement
41
- end&.last
42
- end
29
+ yarn_lockfile_details(lockfile, dependency_name, requirement, manifest_name)
43
30
  end
44
31
 
45
32
  return details if details
@@ -65,6 +52,43 @@ module Dependabot
65
52
  compact
66
53
  end
67
54
 
55
+ def npm_lockfile_details(lockfile, dependency_name, manifest_name)
56
+ parsed_lockfile = parse_package_lock(lockfile)
57
+
58
+ if Helpers.npm_version(lockfile.content) == "npm7"
59
+ parsed_lockfile.dig(
60
+ "packages",
61
+ node_modules_path(manifest_name, dependency_name)
62
+ )&.slice("version", "resolved", "integrity", "dev")
63
+ else
64
+ parsed_lockfile.dig("dependencies", dependency_name)
65
+ end
66
+ end
67
+
68
+ def yarn_lockfile_details(lockfile, dependency_name, requirement, _manifest_name)
69
+ parsed_yarn_lock = parse_yarn_lock(lockfile)
70
+ details_candidates =
71
+ parsed_yarn_lock.
72
+ select { |k, _| k.split(/(?<=\w)\@/)[0] == dependency_name }
73
+
74
+ # If there's only one entry for this dependency, use it, even if
75
+ # the requirement in the lockfile doesn't match
76
+ if details_candidates.one?
77
+ details_candidates.first.last
78
+ else
79
+ details_candidates.find do |k, _|
80
+ k.split(/(?<=\w)\@/)[1..-1].join("@") == requirement
81
+ end&.last
82
+ end
83
+ end
84
+
85
+ def node_modules_path(manifest_name, dependency_name)
86
+ return "node_modules/#{dependency_name}" if manifest_name == "package.json"
87
+
88
+ workspace_path = manifest_name.gsub("/package.json", "")
89
+ File.join(workspace_path, "node_modules", dependency_name)
90
+ end
91
+
68
92
  def yarn_lock_dependencies
69
93
  dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
70
94
 
@@ -117,11 +117,11 @@ module Dependabot
117
117
  end
118
118
 
119
119
  def package_lock_changed?(package_lock)
120
- package_lock.content != updated_package_lock_content(package_lock)
120
+ package_lock.content != updated_lockfile_content(package_lock)
121
121
  end
122
122
 
123
123
  def shrinkwrap_changed?(shrinkwrap)
124
- shrinkwrap.content != updated_package_lock_content(shrinkwrap)
124
+ shrinkwrap.content != updated_lockfile_content(shrinkwrap)
125
125
  end
126
126
 
127
127
  def updated_manifest_files
@@ -150,7 +150,7 @@ module Dependabot
150
150
 
151
151
  updated_files << updated_file(
152
152
  file: package_lock,
153
- content: updated_package_lock_content(package_lock)
153
+ content: updated_lockfile_content(package_lock)
154
154
  )
155
155
  end
156
156
 
@@ -159,7 +159,7 @@ module Dependabot
159
159
 
160
160
  updated_files << updated_file(
161
161
  file: shrinkwrap,
162
- content: updated_shrinkwrap_content(shrinkwrap)
162
+ content: updated_lockfile_content(shrinkwrap)
163
163
  )
164
164
  end
165
165
 
@@ -181,25 +181,15 @@ module Dependabot
181
181
  )
182
182
  end
183
183
 
184
- def updated_package_lock_content(package_lock)
185
- @updated_package_lock_content ||= {}
186
- @updated_package_lock_content[package_lock.name] ||=
187
- npm_lockfile_updater.updated_lockfile_content(package_lock)
188
- end
189
-
190
- def updated_shrinkwrap_content(shrinkwrap)
191
- @updated_shrinkwrap_content ||= {}
192
- @updated_shrinkwrap_content[shrinkwrap.name] ||=
193
- npm_lockfile_updater.updated_lockfile_content(shrinkwrap)
194
- end
195
-
196
- def npm_lockfile_updater
197
- @npm_lockfile_updater ||=
184
+ def updated_lockfile_content(file)
185
+ @updated_lockfile_content ||= {}
186
+ @updated_lockfile_content[file.name] ||=
198
187
  NpmLockfileUpdater.new(
188
+ lockfile: file,
199
189
  dependencies: dependencies,
200
190
  dependency_files: dependency_files,
201
191
  credentials: credentials
202
- )
192
+ ).updated_lockfile.content
203
193
  end
204
194
 
205
195
  def updated_package_json_content(file)
@@ -17,45 +17,54 @@ module Dependabot
17
17
  require_relative "npmrc_builder"
18
18
  require_relative "package_json_updater"
19
19
 
20
- def initialize(dependencies:, dependency_files:, credentials:)
20
+ def initialize(lockfile:, dependencies:, dependency_files:, credentials:)
21
+ @lockfile = lockfile
21
22
  @dependencies = dependencies
22
23
  @dependency_files = dependency_files
23
24
  @credentials = credentials
24
25
  end
25
26
 
26
- def updated_lockfile_content(lockfile)
27
- return lockfile.content if npmrc_disables_lockfile?
28
- return lockfile.content if updatable_dependencies(lockfile).empty?
29
-
30
- @updated_lockfile_content ||= {}
31
- @updated_lockfile_content[lockfile.name] ||=
32
- SharedHelpers.in_a_temporary_directory do
33
- path = Pathname.new(lockfile.name).dirname.to_s
34
- lockfile_name = Pathname.new(lockfile.name).basename.to_s
35
- write_temporary_dependency_files(lockfile.name)
36
- updated_files = Dir.chdir(path) do
37
- run_current_npm_update(lockfile_name: lockfile_name, lockfile_content: lockfile.content)
38
- end
39
- updated_content = updated_files.fetch(lockfile_name)
40
- post_process_npm_lockfile(lockfile.content, updated_content)
41
- end
42
- rescue SharedHelpers::HelperSubprocessFailed => e
43
- handle_npm_updater_error(e, lockfile)
27
+ def updated_lockfile
28
+ updated_file = lockfile.dup
29
+ updated_file.content = updated_lockfile_content
30
+ updated_file
44
31
  end
45
32
 
46
33
  private
47
34
 
48
- attr_reader :dependencies, :dependency_files, :credentials
35
+ attr_reader :lockfile, :dependencies, :dependency_files, :credentials
49
36
 
50
- UNREACHABLE_GIT =
51
- /ls-remote (?:(-h -t)|(--tags --heads)) (?<url>.*)/.freeze
52
- FORBIDDEN_PACKAGE =
53
- %r{(?<package_req>[^/]+) - (Forbidden|Unauthorized)}.freeze
37
+ UNREACHABLE_GIT = /fatal: repository '(?<url>.*)' not found/.freeze
38
+ FORBIDDEN_GIT = /fatal: Authentication failed for '(?<url>.*)'/.freeze
39
+ FORBIDDEN_PACKAGE = %r{(?<package_req>[^/]+) - (Forbidden|Unauthorized)}.freeze
54
40
  FORBIDDEN_PACKAGE_403 = %r{^403\sForbidden\s
55
41
  -\sGET\shttps?://(?<source>[^/]+)/(?<package_req>[^/\s]+)}x.freeze
56
42
  MISSING_PACKAGE = %r{(?<package_req>[^/]+) - Not found}.freeze
57
43
  INVALID_PACKAGE = /Can't install (?<package_req>.*): Missing/.freeze
58
44
 
45
+ # TODO: look into fixing this in npm, seems like a bug in the git
46
+ # downloader introduced in npm 7
47
+ #
48
+ # NOTE: error message returned from arborist/npm 7 when trying to
49
+ # fetching a invalid/non-existent git ref
50
+ NPM7_MISSING_GIT_REF = /already exists and is not an empty directory/.freeze
51
+ NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/.freeze
52
+
53
+ def updated_lockfile_content
54
+ return lockfile.content if npmrc_disables_lockfile?
55
+ return lockfile.content unless updatable_dependencies.any?
56
+
57
+ @updated_lockfile_content ||=
58
+ SharedHelpers.in_a_temporary_directory do
59
+ write_temporary_dependency_files
60
+ updated_files = Dir.chdir(lockfile_directory) { run_current_npm_update }
61
+ updated_lockfile_content = updated_files.fetch(lockfile_basename)
62
+ post_process_npm_lockfile(updated_lockfile_content)
63
+ end
64
+ rescue SharedHelpers::HelperSubprocessFailed => e
65
+ handle_npm_updater_error(e)
66
+ end
67
+
59
68
  def top_level_dependencies
60
69
  dependencies.select(&:top_level?)
61
70
  end
@@ -64,18 +73,14 @@ module Dependabot
64
73
  dependencies.reject(&:top_level?)
65
74
  end
66
75
 
67
- def updatable_dependencies(lockfile)
68
- lockfile_dir = Pathname.new(lockfile.name).dirname.to_s
76
+ def updatable_dependencies
69
77
  dependencies.reject do |dependency|
70
- dependency_up_to_date?(lockfile, dependency) ||
71
- top_level_dependency_update_not_required?(dependency,
72
- lockfile_dir)
78
+ dependency_up_to_date?(dependency) || top_level_dependency_update_not_required?(dependency)
73
79
  end
74
80
  end
75
81
 
76
- def lockfile_dependencies(lockfile)
77
- @lockfile_dependencies ||= {}
78
- @lockfile_dependencies[lockfile.name] ||=
82
+ def lockfile_dependencies
83
+ @lockfile_dependencies ||=
79
84
  NpmAndYarn::FileParser.new(
80
85
  dependency_files: [lockfile, *package_files],
81
86
  source: nil,
@@ -83,9 +88,8 @@ module Dependabot
83
88
  ).parse
84
89
  end
85
90
 
86
- def dependency_up_to_date?(lockfile, dependency)
87
- existing_dep = lockfile_dependencies(lockfile).
88
- find { |dep| dep.name == dependency.name }
91
+ def dependency_up_to_date?(dependency)
92
+ existing_dep = lockfile_dependencies.find { |dep| dep.name == dependency.name }
89
93
 
90
94
  # If the dependency is missing but top level it should be treated as
91
95
  # not up to date
@@ -96,99 +100,204 @@ module Dependabot
96
100
  existing_dep&.version == dependency.version
97
101
  end
98
102
 
99
- # Prevent changes to the lockfile when the dependency has been
103
+ # NOTE: Prevent changes to npm 6 lockfiles when the dependency has been
100
104
  # required in a package.json outside the current folder (e.g. lerna
101
- # proj)
102
- def top_level_dependency_update_not_required?(dependency,
103
- lockfile_dir)
104
- requirements_for_path = dependency.requirements.select do |req|
105
- req_dir = Pathname.new(req[:file]).dirname.to_s
106
- req_dir == lockfile_dir
107
- end
108
-
109
- dependency.top_level? && requirements_for_path.empty?
105
+ # proj). npm 7 introduces workspace support so we explitly want to
106
+ # update the root lockfile and check if the dependency is in the
107
+ # lockfile
108
+ def top_level_dependency_update_not_required?(dependency)
109
+ dependency.top_level? &&
110
+ !dependency_in_package_json?(dependency) &&
111
+ !dependency_in_lockfile?(dependency)
110
112
  end
111
113
 
112
- def run_current_npm_update(lockfile_name:, lockfile_content:)
113
- top_level_dependency_updates = top_level_dependencies.map do |d|
114
- { name: d.name, version: d.version, requirements: d.requirements }
115
- end
116
-
117
- run_npm_updater(
118
- lockfile_name: lockfile_name,
119
- top_level_dependency_updates: top_level_dependency_updates,
120
- lockfile_content: lockfile_content
121
- )
114
+ def run_current_npm_update
115
+ run_npm_updater(top_level_dependencies: top_level_dependencies)
122
116
  end
123
117
 
124
- def run_previous_npm_update(lockfile_name:, lockfile_content:)
118
+ def run_previous_npm_update
125
119
  previous_top_level_dependencies = top_level_dependencies.map do |d|
126
- {
120
+ Dependabot::Dependency.new(
127
121
  name: d.name,
122
+ package_manager: d.package_manager,
128
123
  version: d.previous_version,
129
- requirements: d.previous_requirements
130
- }
124
+ previous_version: d.previous_version,
125
+ requirements: d.previous_requirements,
126
+ previous_requirements: d.previous_requirements
127
+ )
131
128
  end
132
129
 
133
- run_npm_updater(
134
- lockfile_name: lockfile_name,
135
- top_level_dependency_updates: previous_top_level_dependencies,
136
- lockfile_content: lockfile_content
137
- )
130
+ run_npm_updater(top_level_dependencies: previous_top_level_dependencies)
138
131
  end
139
132
 
140
- def run_npm_updater(lockfile_name:, top_level_dependency_updates:, lockfile_content:)
133
+ def run_npm_updater(top_level_dependencies:)
141
134
  SharedHelpers.with_git_configured(credentials: credentials) do
142
- if top_level_dependency_updates.any?
143
- run_npm_top_level_updater(
144
- lockfile_name: lockfile_name,
145
- top_level_dependency_updates: top_level_dependency_updates,
146
- lockfile_content: lockfile_content
147
- )
135
+ if top_level_dependencies.any?
136
+ run_npm_top_level_updater(top_level_dependencies: top_level_dependencies)
148
137
  else
149
- run_npm_subdependency_updater(lockfile_name: lockfile_name, lockfile_content: lockfile_content)
138
+ run_npm_subdependency_updater
150
139
  end
151
140
  end
152
141
  end
153
142
 
154
- def run_npm_top_level_updater(lockfile_name:, top_level_dependency_updates:, lockfile_content:)
155
- npm_version = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content)
156
- Dependabot.logger.info(npm_version)
143
+ def run_npm_top_level_updater(top_level_dependencies:)
144
+ if npm7?
145
+ run_npm_7_top_level_updater(top_level_dependencies: top_level_dependencies)
146
+ else
147
+ SharedHelpers.run_helper_subprocess(
148
+ command: NativeHelpers.helper_path,
149
+ function: "npm6:update",
150
+ args: [
151
+ Dir.pwd,
152
+ lockfile_basename,
153
+ top_level_dependencies.map(&:to_h)
154
+ ]
155
+ )
156
+ end
157
+ end
157
158
 
158
- SharedHelpers.run_helper_subprocess(
159
- command: NativeHelpers.helper_path,
160
- function: "npm6:update",
161
- args: [
162
- Dir.pwd,
163
- lockfile_name,
164
- top_level_dependency_updates
165
- ]
166
- )
159
+ def run_npm_7_top_level_updater(top_level_dependencies:)
160
+ dependencies_in_current_package_json = top_level_dependencies.any? do |dependency|
161
+ dependency_in_package_json?(dependency)
162
+ end
163
+
164
+ # NOTE: When updating a dependency in a nested workspace project we
165
+ # need to run `npm install` without any arguments to update the root
166
+ # level lockfile after having updated the nested packages package.json
167
+ # requirement, otherwise npm will add the dependency as a new
168
+ # top-level dependency to the root lockfile.
169
+ install_args = ""
170
+ if dependencies_in_current_package_json
171
+ # TODO: Update the npm 6 updater to use these args as we currently
172
+ # do the same in the js updater helper, we've kept it seperate for
173
+ # the npm 7 rollout
174
+ install_args = top_level_dependencies.map { |dependency| npm_install_args(dependency) }
175
+ end
176
+
177
+ # NOTE: npm options
178
+ # - `--force` ignores checks for platform (os, cpu) and engines
179
+ # - `--dry-run=false` the updater sets a global .npmrc with dry-run:
180
+ # true to work around an issue in npm 6, we don't want that here
181
+ # - `--ignore-scripts` disables prepare and prepack scripts which are
182
+ # run when installing git dependencies
183
+ command = [
184
+ "npm",
185
+ "install",
186
+ *install_args,
187
+ "--force",
188
+ "--dry-run",
189
+ "false",
190
+ "--ignore-scripts",
191
+ "--package-lock-only"
192
+ ].join(" ")
193
+ SharedHelpers.run_shell_command(command)
194
+ { lockfile_basename => File.read(lockfile_basename) }
195
+ end
196
+
197
+ def run_npm_subdependency_updater
198
+ if npm7?
199
+ run_npm_7_subdependency_updater
200
+ else
201
+ SharedHelpers.run_helper_subprocess(
202
+ command: NativeHelpers.helper_path,
203
+ function: "npm6:updateSubdependency",
204
+ args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)]
205
+ )
206
+ end
167
207
  end
168
208
 
169
- def run_npm_subdependency_updater(lockfile_name:, lockfile_content:)
170
- npm_version = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content)
171
- Dependabot.logger.info(npm_version)
209
+ def run_npm_7_subdependency_updater
210
+ dependency_names = sub_dependencies.map(&:name)
211
+ # NOTE: npm options
212
+ # - `--force` ignores checks for platform (os, cpu) and engines
213
+ # - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
214
+ # work around an issue in npm 6, we don't want that here
215
+ # - `--ignore-scripts` disables prepare and prepack scripts which are run
216
+ # when installing git dependencies
217
+ command = [
218
+ "npm",
219
+ "update",
220
+ *dependency_names,
221
+ "--force",
222
+ "--dry-run",
223
+ "false",
224
+ "--ignore-scripts",
225
+ "--package-lock-only"
226
+ ].join(" ")
227
+ SharedHelpers.run_shell_command(command)
228
+ { lockfile_basename => File.read(lockfile_basename) }
229
+ end
230
+
231
+ def updated_version_requirement_for_dependency(dependency)
232
+ flattenend_manifest_dependencies[dependency.name]
233
+ end
234
+
235
+ # TODO: Add the raw updated requirement to the Dependency instance
236
+ # instead of fishing it out of the updated package json, we need to do
237
+ # this because we don't store the same requirement in
238
+ # Dependency#requirements for git dependencies - see PackageJsonUpdater
239
+ def flattenend_manifest_dependencies
240
+ return @flattenend_manifest_dependencies if defined?(@flattenend_manifest_dependencies)
241
+
242
+ @flattenend_manifest_dependencies =
243
+ NpmAndYarn::FileParser::DEPENDENCY_TYPES.inject({}) do |deps, type|
244
+ deps.merge(parsed_package_json[type] || {})
245
+ end
246
+ end
172
247
 
173
- SharedHelpers.run_helper_subprocess(
174
- command: NativeHelpers.helper_path,
175
- function: "npm6:updateSubdependency",
176
- args: [Dir.pwd, lockfile_name, sub_dependencies.map(&:to_h)]
177
- )
248
+ def npm_install_args(dependency)
249
+ git_requirement = dependency.requirements.find { |req| req[:source] && req[:source][:type] == "git" }
250
+
251
+ if git_requirement
252
+ # NOTE: For git dependencies we loose some information about the
253
+ # requirement that's only available in the package.json, e.g. when
254
+ # specifying a semver tag:
255
+ # `dependabot/depeendabot-core#semver:^0.1` - this is required to
256
+ # pass the correct install argument to `npm install`
257
+ updated_version_requirement = updated_version_requirement_for_dependency(dependency)
258
+ updated_version_requirement ||= git_requirement[:source][:url]
259
+
260
+ # NOTE: Git is configured to auth over https while updating
261
+ updated_version_requirement = updated_version_requirement.gsub(
262
+ %r{git\+ssh://git@(.*?)[:/]}, 'https://\1/'
263
+ )
264
+
265
+ # NOTE: Keep any semver range that has already been updated by the
266
+ # PackageJsonUpdater when installing the new version
267
+ if updated_version_requirement.include?(dependency.version)
268
+ "#{dependency.name}@#{updated_version_requirement}"
269
+ else
270
+ "#{dependency.name}@#{updated_version_requirement.sub(/#.*/, '')}##{dependency.version}"
271
+ end
272
+ else
273
+ "#{dependency.name}@#{dependency.version}"
274
+ end
275
+ end
276
+
277
+ def dependency_in_package_json?(dependency)
278
+ dependency.requirements.any? do |req|
279
+ req[:file] == package_json.name
280
+ end
281
+ end
282
+
283
+ def dependency_in_lockfile?(dependency)
284
+ lockfile_dependencies.any? do |dep|
285
+ dep.name == dependency.name
286
+ end
178
287
  end
179
288
 
180
289
  # rubocop:disable Metrics/AbcSize
181
290
  # rubocop:disable Metrics/CyclomaticComplexity
182
291
  # rubocop:disable Metrics/PerceivedComplexity
183
292
  # rubocop:disable Metrics/MethodLength
184
- def handle_npm_updater_error(error, lockfile)
293
+ def handle_npm_updater_error(error)
185
294
  error_message = error.message
186
295
  if error_message.match?(MISSING_PACKAGE)
187
296
  package_name = error_message.match(MISSING_PACKAGE).
188
297
  named_captures["package_req"]
189
298
  sanitized_name = sanitize_package_name(package_name)
190
299
  sanitized_error = error_message.gsub(package_name, sanitized_name)
191
- handle_missing_package(sanitized_name, sanitized_error, lockfile)
300
+ handle_missing_package(sanitized_name, sanitized_error)
192
301
  end
193
302
 
194
303
  # Invalid package: When the package.json doesn't include a name or
@@ -198,9 +307,9 @@ module Dependabot
198
307
  # workspace project)
199
308
  sub_dep_local_path_error = "does not contain a package.json file"
200
309
  if error_message.match?(INVALID_PACKAGE) ||
201
- error_message.start_with?("Invalid package name") ||
310
+ error_message.include?("Invalid package name") ||
202
311
  error_message.include?(sub_dep_local_path_error)
203
- raise_resolvability_error(error_message, lockfile)
312
+ raise_resolvability_error(error_message)
204
313
  end
205
314
 
206
315
  # TODO: Move this logic to the version resolver and check if a new
@@ -222,9 +331,9 @@ module Dependabot
222
331
  # This happens if a new version has been published but npm is having
223
332
  # consistency issues and the version isn't fully available on all
224
333
  # queries
225
- if error_message.start_with?("No matching vers") &&
334
+ if error_message.include?("No matching vers") &&
226
335
  dependencies_in_error_message?(error_message) &&
227
- resolvable_before_update?(lockfile)
336
+ resolvable_before_update?
228
337
 
229
338
  # Raise a bespoke error so we can capture and ignore it if
230
339
  # we're trying to create a new PR (which will be created
@@ -237,7 +346,7 @@ module Dependabot
237
346
  named_captures["package_req"]
238
347
  sanitized_name = sanitize_package_name(package_name)
239
348
  sanitized_error = error_message.gsub(package_name, sanitized_name)
240
- handle_missing_package(sanitized_name, sanitized_error, lockfile)
349
+ handle_missing_package(sanitized_name, sanitized_error)
241
350
  end
242
351
 
243
352
  # Some private registries return a 403 when the user is readonly
@@ -246,13 +355,11 @@ module Dependabot
246
355
  named_captures["package_req"]
247
356
  sanitized_name = sanitize_package_name(package_name)
248
357
  sanitized_error = error_message.gsub(package_name, sanitized_name)
249
- handle_missing_package(sanitized_name, sanitized_error, lockfile)
358
+ handle_missing_package(sanitized_name, sanitized_error)
250
359
  end
251
360
 
252
- if error_message.match?(UNREACHABLE_GIT)
253
- dependency_url =
254
- error_message.match(UNREACHABLE_GIT).
255
- named_captures.fetch("url")
361
+ if (git_error = error_message.match(UNREACHABLE_GIT) || error_message.match(FORBIDDEN_GIT))
362
+ dependency_url = git_error.named_captures.fetch("url")
256
363
 
257
364
  raise Dependabot::GitDependenciesNotReachable, dependency_url
258
365
  end
@@ -265,17 +372,24 @@ module Dependabot
265
372
  # people to re-generate their lockfiles (Future feature idea: add a
266
373
  # way to click-to-fix the lockfile from the issue)
267
374
  if error_message.include?("Cannot read property 'match' of ") &&
268
- !resolvable_before_update?(lockfile)
269
- raise_missing_lockfile_version_resolvability_error(error_message,
270
- lockfile)
375
+ !resolvable_before_update?
376
+ raise_missing_lockfile_version_resolvability_error(error_message)
271
377
  end
272
378
 
273
- if (error_message.start_with?("No matching vers", "404 Not Found") ||
274
- error_message.include?("not match any file(s) known to git") ||
379
+ if (error_message.include?("No matching vers") ||
380
+ error_message.include?("404 Not Found") ||
275
381
  error_message.include?("Non-registry package missing package") ||
276
- error_message.include?("Invalid tag name")) &&
277
- !resolvable_before_update?(lockfile)
278
- raise_resolvability_error(error_message, lockfile)
382
+ error_message.include?("Invalid tag name") ||
383
+ error_message.match?(NPM6_MISSING_GIT_REF) ||
384
+ error_message.match?(NPM7_MISSING_GIT_REF)) &&
385
+ !resolvable_before_update?
386
+ raise_resolvability_error(error_message)
387
+ end
388
+
389
+ # NOTE: This check was introduced in npm7/arborist
390
+ if error_message.include?("must provide string spec")
391
+ msg = "Error parsing your package.json manifest: the version requirement must be a string"
392
+ raise Dependabot::DependencyFileNotParseable, msg
279
393
  end
280
394
 
281
395
  raise error
@@ -285,17 +399,15 @@ module Dependabot
285
399
  # rubocop:enable Metrics/PerceivedComplexity
286
400
  # rubocop:enable Metrics/MethodLength
287
401
 
288
- def raise_resolvability_error(error_message, lockfile)
402
+ def raise_resolvability_error(error_message)
289
403
  dependency_names = dependencies.map(&:name).join(", ")
290
404
  msg = "Error whilst updating #{dependency_names} in "\
291
405
  "#{lockfile.path}:\n#{error_message}"
292
406
  raise Dependabot::DependencyFileNotResolvable, msg
293
407
  end
294
408
 
295
- def raise_missing_lockfile_version_resolvability_error(error_message,
296
- lockfile)
297
- lockfile_dir = Pathname.new(lockfile.name).dirname
298
- modules_path = lockfile_dir.join("node_modules")
409
+ def raise_missing_lockfile_version_resolvability_error(error_message)
410
+ modules_path = File.join(lockfile_directory, "node_modules")
299
411
  # NOTE: don't include the dependency names to prevent opening
300
412
  # multiple issues for each dependency that fails because we unique
301
413
  # issues on the error message (issue detail) on the backend
@@ -310,11 +422,10 @@ module Dependabot
310
422
  raise Dependabot::DependencyFileNotResolvable, msg
311
423
  end
312
424
 
313
- def handle_missing_package(package_name, error_message, lockfile)
314
- missing_dep = lockfile_dependencies(lockfile).
315
- find { |dep| dep.name == package_name }
425
+ def handle_missing_package(package_name, error_message)
426
+ missing_dep = lockfile_dependencies.find { |dep| dep.name == package_name }
316
427
 
317
- raise_resolvability_error(error_message, lockfile) unless missing_dep
428
+ raise_resolvability_error(error_message) unless missing_dep
318
429
 
319
430
  reg = NpmAndYarn::UpdateChecker::RegistryFinder.new(
320
431
  dependency: missing_dep,
@@ -336,23 +447,14 @@ module Dependabot
336
447
  end
337
448
  end
338
449
 
339
- def resolvable_before_update?(lockfile)
340
- @resolvable_before_update ||= {}
341
- return @resolvable_before_update[lockfile.name] if @resolvable_before_update.key?(lockfile.name)
450
+ def resolvable_before_update?
451
+ return @resolvable_before_update if defined?(@resolvable_before_update)
342
452
 
343
- @resolvable_before_update[lockfile.name] =
453
+ @resolvable_before_update =
344
454
  begin
345
455
  SharedHelpers.in_a_temporary_directory do
346
- write_temporary_dependency_files(
347
- lockfile.name,
348
- update_package_json: false
349
- )
350
-
351
- lockfile_name = Pathname.new(lockfile.name).basename.to_s
352
- path = Pathname.new(lockfile.name).dirname.to_s
353
- Dir.chdir(path) do
354
- run_previous_npm_update(lockfile_name: lockfile_name, lockfile_content: lockfile.content)
355
- end
456
+ write_temporary_dependency_files(update_package_json: false)
457
+ Dir.chdir(lockfile_directory) { run_previous_npm_update }
356
458
  end
357
459
 
358
460
  true
@@ -370,12 +472,10 @@ module Dependabot
370
472
  end
371
473
  end
372
474
 
373
- def write_temporary_dependency_files(lockfile_name,
374
- update_package_json: true)
375
- write_lockfiles(lockfile_name)
475
+ def write_temporary_dependency_files(update_package_json: true)
476
+ write_lockfiles
376
477
 
377
- dir = Pathname.new(lockfile_name).dirname
378
- File.write(File.join(dir, ".npmrc"), npmrc_content)
478
+ File.write(File.join(lockfile_directory, ".npmrc"), npmrc_content)
379
479
 
380
480
  package_files.each do |file|
381
481
  path = file.name
@@ -388,8 +488,11 @@ module Dependabot
388
488
  file.content
389
489
  end
390
490
 
391
- # When updating a package-lock.json we have to manually lock all
392
- # git dependencies, otherwise npm will (unhelpfully) update them
491
+ # TODO: Figure out if we need to lock git deps for npm 7 and can
492
+ # start deprecating this hornets nest
493
+ #
494
+ # NOTE: When updating a package-lock.json we have to manually lock
495
+ # all git dependencies, otherwise npm will (unhelpfully) update them
393
496
  updated_content = lock_git_deps(updated_content)
394
497
  updated_content = replace_ssh_sources(updated_content)
395
498
  updated_content = lock_deps_with_latest_reqs(updated_content)
@@ -399,9 +502,9 @@ module Dependabot
399
502
  end
400
503
  end
401
504
 
402
- def write_lockfiles(lockfile_name)
505
+ def write_lockfiles
403
506
  excluded_lock =
404
- case lockfile_name
507
+ case lockfile.name
405
508
  when "package-lock.json" then "npm-shrinkwrap.json"
406
509
  when "npm-shrinkwrap.json" then "package-lock.json"
407
510
  end
@@ -502,57 +605,166 @@ module Dependabot
502
605
  @git_ssh_requirements_to_swap
503
606
  end
504
607
 
505
- def post_process_npm_lockfile(original_content, updated_content)
506
- updated_content =
507
- replace_project_metadata(updated_content, original_content)
508
-
608
+ def post_process_npm_lockfile(updated_lockfile_content)
509
609
  # Switch SSH requirements back for git dependencies
610
+ updated_lockfile_content = replace_swapped_git_ssh_requirements(updated_lockfile_content)
611
+
612
+ # Switch from details back for git dependencies (they will have
613
+ # changed because we locked them)
614
+ updated_lockfile_content = replace_locked_git_dependencies(updated_lockfile_content)
615
+
616
+ parsed_updated_lockfile_content = JSON.parse(updated_lockfile_content)
617
+
618
+ # Restore lockfile name attribute from the original lockfile
619
+ updated_lockfile_content = replace_project_name(updated_lockfile_content, parsed_updated_lockfile_content)
620
+
621
+ # Restore npm 7 "packages" "name" entry from package.json if previously set
622
+ updated_lockfile_content = restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_content)
623
+
624
+ # Switch back npm 7 lockfile "pacakages" requirements from the package.json
625
+ updated_lockfile_content = restore_locked_package_dependencies(
626
+ updated_lockfile_content, parsed_updated_lockfile_content
627
+ )
628
+
629
+ # Switch back the protocol of tarball resolutions if they've changed
630
+ # (fixes an npm bug, which appears to be applied inconsistently)
631
+ replace_tarball_urls(updated_lockfile_content)
632
+ end
633
+
634
+ def replace_project_name(updated_lockfile_content, parsed_updated_lockfile_content)
635
+ current_name = parsed_updated_lockfile_content["name"]
636
+ original_name = parsed_lockfile["name"]
637
+ if original_name
638
+ updated_lockfile_content = replace_lockfile_name_attribute(
639
+ current_name, original_name, updated_lockfile_content
640
+ )
641
+ end
642
+ updated_lockfile_content
643
+ end
644
+
645
+ def restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_content)
646
+ return updated_lockfile_content unless npm7?
647
+
648
+ current_name = parsed_updated_lockfile_content.dig("packages", "", "name")
649
+ original_name = parsed_lockfile.dig("packages", "", "name")
650
+
651
+ # TODO: Submit a patch to npm fixing this issue making `npm install`
652
+ # consistent with `npm install --package-lock-only`
653
+ #
654
+ # NOTE: This is a workaround for npm adding a `name` attribute to the
655
+ # packages section in the lockfile because we install using
656
+ # `--package-lock-only`
657
+ if !original_name
658
+ updated_lockfile_content = remove_lockfile_packages_name_attribute(
659
+ current_name, updated_lockfile_content
660
+ )
661
+ elsif original_name && original_name != current_name
662
+ updated_lockfile_content = replace_lockfile_packages_name_attribute(
663
+ current_name, original_name, updated_lockfile_content
664
+ )
665
+ end
666
+
667
+ updated_lockfile_content
668
+ end
669
+
670
+ def replace_lockfile_name_attribute(current_name, original_name, updated_lockfile_content)
671
+ updated_lockfile_content.sub(
672
+ /"name":\s"#{current_name}"/,
673
+ "\"name\": \"#{original_name}\""
674
+ )
675
+ end
676
+
677
+ def replace_lockfile_packages_name_attribute(current_name, original_name, updated_lockfile_content)
678
+ packages_key_line = '"": {'
679
+ updated_lockfile_content.sub(
680
+ /(#{packages_key_line}[\n\s]+"name":\s)"#{current_name}"/,
681
+ '\1"' + original_name + '"'
682
+ )
683
+ end
684
+
685
+ def remove_lockfile_packages_name_attribute(current_name, updated_lockfile_content)
686
+ packages_key_line = '"": {'
687
+ updated_lockfile_content.gsub(/(#{packages_key_line})[\n\s]+"name":\s"#{current_name}",/, '\1')
688
+ end
689
+
690
+ # NOTE: This is a workaround to "sync" what's in package.json
691
+ # requirements and the `packages.""` entry in npm 7 v2 lockfiles. These
692
+ # get out of sync because we lock git dependencies (that are not being
693
+ # updated) to a specific sha to prevent unrelated updates and the way we
694
+ # invoke the `npm install` cli, where we might tell npm to install a
695
+ # specific versionm e.g. `npm install eslint@1.1.8` but we keep the
696
+ # `package.json` requirement for eslint at `^1.0.0`, in which case we
697
+ # need to copy this from the manifest to the lockfile after the update
698
+ # has finished.
699
+ def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated_lockfile_content)
700
+ return updated_lockfile_content unless npm7?
701
+
702
+ dependency_names_to_restore = (dependencies.map(&:name) + git_dependencies_to_lock.keys).uniq
703
+
704
+ NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |type|
705
+ parsed_package_json.fetch(type, {}).each do |dependency_name, original_requirement|
706
+ next unless dependency_names_to_restore.include?(dependency_name)
707
+
708
+ locked_requirement = parsed_updated_lockfile_content.dig("packages", "", type, dependency_name)
709
+ next unless locked_requirement
710
+
711
+ locked_req = %("#{dependency_name}": "#{locked_requirement}")
712
+ original_req = %("#{dependency_name}": "#{original_requirement}")
713
+ updated_lockfile_content = updated_lockfile_content.gsub(locked_req, original_req)
714
+ end
715
+ end
716
+
717
+ updated_lockfile_content
718
+ end
719
+
720
+ def replace_swapped_git_ssh_requirements(updated_lockfile_content)
510
721
  git_ssh_requirements_to_swap.each do |req|
511
722
  new_r = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'git+https://\1/')
512
723
  old_r = req.gsub(%r{git@(.*?)[:/]}, 'git@\1/')
513
- updated_content = updated_content.gsub(new_r, old_r)
724
+ updated_lockfile_content = updated_lockfile_content.gsub(new_r, old_r)
514
725
  end
515
726
 
727
+ updated_lockfile_content
728
+ end
729
+
730
+ def replace_locked_git_dependencies(updated_lockfile_content)
516
731
  # Switch from details back for git dependencies (they will have
517
732
  # changed because we locked them)
518
- git_dependencies_to_lock.each do |_, details|
733
+ git_dependencies_to_lock.each do |dependency_name, details|
519
734
  next unless details[:version] && details[:from]
520
735
 
521
736
  # When locking git dependencies in package.json we set the version
522
737
  # to be the git commit from the lockfile "version" field which
523
738
  # updates the lockfile "from" field to the new git commit when we
524
739
  # run npm install
525
- locked_from = %("from": "#{details[:version]}")
526
740
  original_from = %("from": "#{details[:from]}")
527
- updated_content = updated_content.gsub(locked_from, original_from)
741
+ if npm7?
742
+ # NOTE: The `from` syntax has changed in npm 7 to inclued the dependency name
743
+ npm7_locked_from = %("from": "#{dependency_name}@#{details[:version]}")
744
+ updated_lockfile_content = updated_lockfile_content.gsub(npm7_locked_from, original_from)
745
+ else
746
+ npm6_locked_from = %("from": "#{details[:version]}")
747
+ updated_lockfile_content = updated_lockfile_content.gsub(npm6_locked_from, original_from)
748
+ end
528
749
  end
529
750
 
530
- # Switch back the protocol of tarball resolutions if they've changed
531
- # (fixes an npm bug, which appears to be applied inconsistently)
751
+ updated_lockfile_content
752
+ end
753
+
754
+ def replace_tarball_urls(updated_lockfile_content)
532
755
  tarball_urls.each do |url|
533
756
  trimmed_url = url.gsub(/(\d+\.)*tgz$/, "")
534
757
  incorrect_url = if url.start_with?("https")
535
758
  trimmed_url.gsub(/^https:/, "http:")
536
759
  else trimmed_url.gsub(/^http:/, "https:")
537
760
  end
538
- updated_content = updated_content.gsub(
761
+ updated_lockfile_content = updated_lockfile_content.gsub(
539
762
  /#{Regexp.quote(incorrect_url)}(?=(\d+\.)*tgz")/,
540
763
  trimmed_url
541
764
  )
542
765
  end
543
766
 
544
- updated_content
545
- end
546
-
547
- def replace_project_metadata(new_content, old_content)
548
- old_name = old_content.match(/(?<="name": ").*(?=",)/)&.to_s
549
-
550
- if old_name
551
- new_content = new_content.
552
- sub(/(?<="name": ").*(?=",)/, old_name)
553
- end
554
-
555
- new_content
767
+ updated_lockfile_content
556
768
  end
557
769
 
558
770
  def tarball_urls
@@ -592,6 +804,12 @@ module Dependabot
592
804
  npmrc_content.match?(/^package-lock\s*=\s*false/)
593
805
  end
594
806
 
807
+ def npm7?
808
+ return @npm7 if defined?(@npm7)
809
+
810
+ @npm7 = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile.content) == "npm7"
811
+ end
812
+
595
813
  def sanitized_package_json_content(content)
596
814
  content.
597
815
  gsub(/\{\{[^\}]*?\}\}/, "something"). # {{ nm }} syntax not allowed
@@ -603,6 +821,30 @@ module Dependabot
603
821
  package_name.gsub("%2f", "/").gsub("%2F", "/")
604
822
  end
605
823
 
824
+ def lockfile_directory
825
+ Pathname.new(lockfile.name).dirname.to_s
826
+ end
827
+
828
+ def lockfile_basename
829
+ Pathname.new(lockfile.name).basename.to_s
830
+ end
831
+
832
+ def parsed_lockfile
833
+ @parsed_lockfile ||= JSON.parse(lockfile.content)
834
+ end
835
+
836
+ def parsed_package_json
837
+ return {} unless package_json
838
+ return @parsed_package_json if defined?(@parsed_package_json)
839
+
840
+ @parsed_package_json = JSON.parse(updated_package_json_content(package_json))
841
+ end
842
+
843
+ def package_json
844
+ package_name = lockfile.name.sub(lockfile_basename, "package.json")
845
+ package_files.find { |f| f.name == package_name }
846
+ end
847
+
606
848
  def package_locks
607
849
  @package_locks ||=
608
850
  dependency_files.