dependabot-npm_and_yarn 0.132.0 → 0.133.4

Sign up to get free protection for your applications and to get access to all the features.
data/helpers/package.json CHANGED
@@ -10,14 +10,14 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@dependabot/yarn-lib": "^1.21.1",
13
- "@npmcli/arborist": "^2.2.0",
13
+ "@npmcli/arborist": "^2.2.2",
14
14
  "detect-indent": "^6.0.0",
15
15
  "npm6": "npm:npm@6.14.11",
16
16
  "npm7": "npm:npm@7.4.0",
17
17
  "semver": "^7.3.4"
18
18
  },
19
19
  "devDependencies": {
20
- "eslint": "^7.18.0",
20
+ "eslint": "^7.20.0",
21
21
  "eslint-config-prettier": "^7.2.0",
22
22
  "jest": "^26.6.3",
23
23
  "prettier": "^2.2.1",
@@ -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,35 +17,22 @@ 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, lockfile.name)
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
37
  UNREACHABLE_GIT = /fatal: repository '(?<url>.*)' not found/.freeze
51
38
  FORBIDDEN_GIT = /fatal: Authentication failed for '(?<url>.*)'/.freeze
@@ -63,6 +50,21 @@ module Dependabot
63
50
  NPM7_MISSING_GIT_REF = /already exists and is not an empty directory/.freeze
64
51
  NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/.freeze
65
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
+
66
68
  def top_level_dependencies
67
69
  dependencies.select(&:top_level?)
68
70
  end
@@ -71,16 +73,14 @@ module Dependabot
71
73
  dependencies.reject(&:top_level?)
72
74
  end
73
75
 
74
- def updatable_dependencies(lockfile)
76
+ def updatable_dependencies
75
77
  dependencies.reject do |dependency|
76
- dependency_up_to_date?(lockfile, dependency) ||
77
- top_level_dependency_update_not_required?(dependency, lockfile)
78
+ dependency_up_to_date?(dependency) || top_level_dependency_update_not_required?(dependency)
78
79
  end
79
80
  end
80
81
 
81
- def lockfile_dependencies(lockfile)
82
- @lockfile_dependencies ||= {}
83
- @lockfile_dependencies[lockfile.name] ||=
82
+ def lockfile_dependencies
83
+ @lockfile_dependencies ||=
84
84
  NpmAndYarn::FileParser.new(
85
85
  dependency_files: [lockfile, *package_files],
86
86
  source: nil,
@@ -88,9 +88,8 @@ module Dependabot
88
88
  ).parse
89
89
  end
90
90
 
91
- def dependency_up_to_date?(lockfile, dependency)
92
- existing_dep = lockfile_dependencies(lockfile).
93
- find { |dep| dep.name == dependency.name }
91
+ def dependency_up_to_date?(dependency)
92
+ existing_dep = lockfile_dependencies.find { |dep| dep.name == dependency.name }
94
93
 
95
94
  # If the dependency is missing but top level it should be treated as
96
95
  # not up to date
@@ -106,93 +105,81 @@ module Dependabot
106
105
  # proj). npm 7 introduces workspace support so we explitly want to
107
106
  # update the root lockfile and check if the dependency is in the
108
107
  # lockfile
109
- def top_level_dependency_update_not_required?(dependency, lockfile)
110
- lockfile_dir = Pathname.new(lockfile.name).dirname.to_s
111
-
112
- requirements_for_path = dependency.requirements.select do |req|
113
- req_dir = Pathname.new(req[:file]).dirname.to_s
114
- req_dir == lockfile_dir
115
- end
116
-
117
- dependency_in_lockfile = lockfile_dependencies(lockfile).any? do |dep|
118
- dep.name == dependency.name
119
- end
120
-
121
- dependency.top_level? && requirements_for_path.empty? && !dependency_in_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)
122
112
  end
123
113
 
124
- def run_current_npm_update(lockfile_name:, lockfile_content:)
125
- top_level_dependency_updates = top_level_dependencies.map do |d|
126
- { name: d.name, version: d.version, requirements: d.requirements }
127
- end
128
-
129
- run_npm_updater(
130
- lockfile_name: lockfile_name,
131
- top_level_dependency_updates: top_level_dependency_updates,
132
- lockfile_content: lockfile_content
133
- )
114
+ def run_current_npm_update
115
+ run_npm_updater(top_level_dependencies: top_level_dependencies)
134
116
  end
135
117
 
136
- def run_previous_npm_update(lockfile_name:, lockfile_content:)
118
+ def run_previous_npm_update
137
119
  previous_top_level_dependencies = top_level_dependencies.map do |d|
138
- {
120
+ Dependabot::Dependency.new(
139
121
  name: d.name,
122
+ package_manager: d.package_manager,
140
123
  version: d.previous_version,
141
- requirements: d.previous_requirements
142
- }
124
+ previous_version: d.previous_version,
125
+ requirements: d.previous_requirements,
126
+ previous_requirements: d.previous_requirements
127
+ )
143
128
  end
144
129
 
145
- run_npm_updater(
146
- lockfile_name: lockfile_name,
147
- top_level_dependency_updates: previous_top_level_dependencies,
148
- lockfile_content: lockfile_content
149
- )
130
+ run_npm_updater(top_level_dependencies: previous_top_level_dependencies)
150
131
  end
151
132
 
152
- def run_npm_updater(lockfile_name:, top_level_dependency_updates:, lockfile_content:)
133
+ def run_npm_updater(top_level_dependencies:)
153
134
  SharedHelpers.with_git_configured(credentials: credentials) do
154
- if top_level_dependency_updates.any?
155
- run_npm_top_level_updater(
156
- lockfile_name: lockfile_name,
157
- top_level_dependency_updates: top_level_dependency_updates,
158
- lockfile_content: lockfile_content
159
- )
135
+ if top_level_dependencies.any?
136
+ run_npm_top_level_updater(top_level_dependencies: top_level_dependencies)
160
137
  else
161
- run_npm_subdependency_updater(lockfile_name: lockfile_name, lockfile_content: lockfile_content)
138
+ run_npm_subdependency_updater
162
139
  end
163
140
  end
164
141
  end
165
142
 
166
- def run_npm_top_level_updater(lockfile_name:, top_level_dependency_updates:, lockfile_content:)
167
- if npm7?(lockfile_content)
168
- run_npm_7_top_level_updater(
169
- lockfile_name: lockfile_name,
170
- top_level_dependency_updates: top_level_dependency_updates
171
- )
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)
172
146
  else
173
147
  SharedHelpers.run_helper_subprocess(
174
148
  command: NativeHelpers.helper_path,
175
149
  function: "npm6:update",
176
150
  args: [
177
151
  Dir.pwd,
178
- lockfile_name,
179
- top_level_dependency_updates
152
+ lockfile_basename,
153
+ top_level_dependencies.map(&:to_h)
180
154
  ]
181
155
  )
182
156
  end
183
157
  end
184
158
 
185
- def run_npm_7_top_level_updater(lockfile_name:, top_level_dependency_updates:)
186
- # - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
187
- # work around an issue in npm 6, we don't want that here
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
188
178
  # - `--force` ignores checks for platform (os, cpu) and engines
189
- # - `--ignore-scripts` disables prepare and prepack scripts which are run
190
- # when installing git dependencies
191
- flattenend_manifest_dependencies = flattenend_manifest_dependencies_for_lockfile_name(lockfile_name)
192
- install_args = npm_top_level_updater_args(
193
- top_level_dependency_updates: top_level_dependency_updates,
194
- flattenend_manifest_dependencies: flattenend_manifest_dependencies
195
- )
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
196
183
  command = [
197
184
  "npm",
198
185
  "install",
@@ -204,26 +191,27 @@ module Dependabot
204
191
  "--package-lock-only"
205
192
  ].join(" ")
206
193
  SharedHelpers.run_shell_command(command)
207
- { lockfile_name => File.read(lockfile_name) }
194
+ { lockfile_basename => File.read(lockfile_basename) }
208
195
  end
209
196
 
210
- def run_npm_subdependency_updater(lockfile_name:, lockfile_content:)
211
- if npm7?(lockfile_content)
212
- run_npm_7_subdependency_updater(lockfile_name: lockfile_name)
197
+ def run_npm_subdependency_updater
198
+ if npm7?
199
+ run_npm_7_subdependency_updater
213
200
  else
214
201
  SharedHelpers.run_helper_subprocess(
215
202
  command: NativeHelpers.helper_path,
216
203
  function: "npm6:updateSubdependency",
217
- args: [Dir.pwd, lockfile_name, sub_dependencies.map(&:to_h)]
204
+ args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)]
218
205
  )
219
206
  end
220
207
  end
221
208
 
222
- def run_npm_7_subdependency_updater(lockfile_name:)
209
+ def run_npm_7_subdependency_updater
223
210
  dependency_names = sub_dependencies.map(&:name)
211
+ # NOTE: npm options
212
+ # - `--force` ignores checks for platform (os, cpu) and engines
224
213
  # - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
225
214
  # work around an issue in npm 6, we don't want that here
226
- # - `--force` ignores checks for platform (os, cpu) and engines
227
215
  # - `--ignore-scripts` disables prepare and prepack scripts which are run
228
216
  # when installing git dependencies
229
217
  command = [
@@ -237,59 +225,64 @@ module Dependabot
237
225
  "--package-lock-only"
238
226
  ].join(" ")
239
227
  SharedHelpers.run_shell_command(command)
240
- { lockfile_name => File.read(lockfile_name) }
228
+ { lockfile_basename => File.read(lockfile_basename) }
241
229
  end
242
230
 
243
- # TODO: Update the npm 6 updater to use these args as we currently do
244
- # the same in the js updater helper, we've kept it seperate for the npm
245
- # 7 rollout
246
- def npm_top_level_updater_args(top_level_dependency_updates:, flattenend_manifest_dependencies:)
247
- top_level_dependency_updates.map do |dependency|
248
- # NOTE: For git dependencies we loose some information about the
249
- # requirement that's only available in the package.json, e.g. when
250
- # specifying a semver tag:
251
- # `dependabot/depeendabot-core#semver:^0.1` - this is required to
252
- # pass the correct install argument to `npm install`
253
- existing_version_requirement = flattenend_manifest_dependencies[dependency.fetch(:name)]
254
- npm_install_args(
255
- dependency.fetch(:name),
256
- dependency.fetch(:version),
257
- dependency.fetch(:requirements),
258
- existing_version_requirement
259
- )
260
- end
231
+ def updated_version_requirement_for_dependency(dependency)
232
+ flattenend_manifest_dependencies[dependency.name]
261
233
  end
262
234
 
263
- def flattenend_manifest_dependencies_for_lockfile_name(lockfile_name)
264
- package_json_content = updated_package_json_content_for_lockfile_name(lockfile_name)
265
- return {} unless package_json_content
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)
266
241
 
267
- parsed_package = JSON.parse(package_json_content)
268
- NpmAndYarn::FileParser::DEPENDENCY_TYPES.inject({}) do |deps, type|
269
- deps.merge(parsed_package[type] || {})
270
- end
242
+ @flattenend_manifest_dependencies =
243
+ NpmAndYarn::FileParser::DEPENDENCY_TYPES.inject({}) do |deps, type|
244
+ deps.merge(parsed_package_json[type] || {})
245
+ end
271
246
  end
272
247
 
273
- def npm_install_args(dep_name, desired_version, requirements, existing_version_requirement)
274
- git_requirement = requirements.find { |req| req[:source] && req[:source][:type] == "git" }
248
+ def npm_install_args(dependency)
249
+ git_requirement = dependency.requirements.find { |req| req[:source] && req[:source][:type] == "git" }
275
250
 
276
251
  if git_requirement
277
- existing_version_requirement ||= git_requirement[:source][:url]
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]
278
259
 
279
260
  # NOTE: Git is configured to auth over https while updating
280
- existing_version_requirement = existing_version_requirement.gsub(
261
+ updated_version_requirement = updated_version_requirement.gsub(
281
262
  %r{git\+ssh://git@(.*?)[:/]}, 'https://\1/'
282
263
  )
283
264
 
284
265
  # NOTE: Keep any semver range that has already been updated by the
285
266
  # PackageJsonUpdater when installing the new version
286
- if existing_version_requirement.include?(desired_version)
287
- "#{dep_name}@#{existing_version_requirement}"
267
+ if updated_version_requirement.include?(dependency.version)
268
+ "#{dependency.name}@#{updated_version_requirement}"
288
269
  else
289
- "#{dep_name}@#{existing_version_requirement.sub(/#.*/, '')}##{desired_version}"
270
+ "#{dependency.name}@#{updated_version_requirement.sub(/#.*/, '')}##{dependency.version}"
290
271
  end
291
272
  else
292
- "#{dep_name}@#{desired_version}"
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
293
286
  end
294
287
  end
295
288
 
@@ -297,14 +290,14 @@ module Dependabot
297
290
  # rubocop:disable Metrics/CyclomaticComplexity
298
291
  # rubocop:disable Metrics/PerceivedComplexity
299
292
  # rubocop:disable Metrics/MethodLength
300
- def handle_npm_updater_error(error, lockfile)
293
+ def handle_npm_updater_error(error)
301
294
  error_message = error.message
302
295
  if error_message.match?(MISSING_PACKAGE)
303
296
  package_name = error_message.match(MISSING_PACKAGE).
304
297
  named_captures["package_req"]
305
298
  sanitized_name = sanitize_package_name(package_name)
306
299
  sanitized_error = error_message.gsub(package_name, sanitized_name)
307
- handle_missing_package(sanitized_name, sanitized_error, lockfile)
300
+ handle_missing_package(sanitized_name, sanitized_error)
308
301
  end
309
302
 
310
303
  # Invalid package: When the package.json doesn't include a name or
@@ -316,7 +309,7 @@ module Dependabot
316
309
  if error_message.match?(INVALID_PACKAGE) ||
317
310
  error_message.include?("Invalid package name") ||
318
311
  error_message.include?(sub_dep_local_path_error)
319
- raise_resolvability_error(error_message, lockfile)
312
+ raise_resolvability_error(error_message)
320
313
  end
321
314
 
322
315
  # TODO: Move this logic to the version resolver and check if a new
@@ -340,7 +333,7 @@ module Dependabot
340
333
  # queries
341
334
  if error_message.include?("No matching vers") &&
342
335
  dependencies_in_error_message?(error_message) &&
343
- resolvable_before_update?(lockfile)
336
+ resolvable_before_update?
344
337
 
345
338
  # Raise a bespoke error so we can capture and ignore it if
346
339
  # we're trying to create a new PR (which will be created
@@ -353,7 +346,7 @@ module Dependabot
353
346
  named_captures["package_req"]
354
347
  sanitized_name = sanitize_package_name(package_name)
355
348
  sanitized_error = error_message.gsub(package_name, sanitized_name)
356
- handle_missing_package(sanitized_name, sanitized_error, lockfile)
349
+ handle_missing_package(sanitized_name, sanitized_error)
357
350
  end
358
351
 
359
352
  # Some private registries return a 403 when the user is readonly
@@ -362,7 +355,7 @@ module Dependabot
362
355
  named_captures["package_req"]
363
356
  sanitized_name = sanitize_package_name(package_name)
364
357
  sanitized_error = error_message.gsub(package_name, sanitized_name)
365
- handle_missing_package(sanitized_name, sanitized_error, lockfile)
358
+ handle_missing_package(sanitized_name, sanitized_error)
366
359
  end
367
360
 
368
361
  if (git_error = error_message.match(UNREACHABLE_GIT) || error_message.match(FORBIDDEN_GIT))
@@ -379,9 +372,8 @@ module Dependabot
379
372
  # people to re-generate their lockfiles (Future feature idea: add a
380
373
  # way to click-to-fix the lockfile from the issue)
381
374
  if error_message.include?("Cannot read property 'match' of ") &&
382
- !resolvable_before_update?(lockfile)
383
- raise_missing_lockfile_version_resolvability_error(error_message,
384
- lockfile)
375
+ !resolvable_before_update?
376
+ raise_missing_lockfile_version_resolvability_error(error_message)
385
377
  end
386
378
 
387
379
  if (error_message.include?("No matching vers") ||
@@ -390,8 +382,8 @@ module Dependabot
390
382
  error_message.include?("Invalid tag name") ||
391
383
  error_message.match?(NPM6_MISSING_GIT_REF) ||
392
384
  error_message.match?(NPM7_MISSING_GIT_REF)) &&
393
- !resolvable_before_update?(lockfile)
394
- raise_resolvability_error(error_message, lockfile)
385
+ !resolvable_before_update?
386
+ raise_resolvability_error(error_message)
395
387
  end
396
388
 
397
389
  # NOTE: This check was introduced in npm7/arborist
@@ -407,17 +399,15 @@ module Dependabot
407
399
  # rubocop:enable Metrics/PerceivedComplexity
408
400
  # rubocop:enable Metrics/MethodLength
409
401
 
410
- def raise_resolvability_error(error_message, lockfile)
402
+ def raise_resolvability_error(error_message)
411
403
  dependency_names = dependencies.map(&:name).join(", ")
412
404
  msg = "Error whilst updating #{dependency_names} in "\
413
405
  "#{lockfile.path}:\n#{error_message}"
414
406
  raise Dependabot::DependencyFileNotResolvable, msg
415
407
  end
416
408
 
417
- def raise_missing_lockfile_version_resolvability_error(error_message,
418
- lockfile)
419
- lockfile_dir = Pathname.new(lockfile.name).dirname
420
- 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")
421
411
  # NOTE: don't include the dependency names to prevent opening
422
412
  # multiple issues for each dependency that fails because we unique
423
413
  # issues on the error message (issue detail) on the backend
@@ -432,11 +422,10 @@ module Dependabot
432
422
  raise Dependabot::DependencyFileNotResolvable, msg
433
423
  end
434
424
 
435
- def handle_missing_package(package_name, error_message, lockfile)
436
- missing_dep = lockfile_dependencies(lockfile).
437
- 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 }
438
427
 
439
- raise_resolvability_error(error_message, lockfile) unless missing_dep
428
+ raise_resolvability_error(error_message) unless missing_dep
440
429
 
441
430
  reg = NpmAndYarn::UpdateChecker::RegistryFinder.new(
442
431
  dependency: missing_dep,
@@ -458,23 +447,14 @@ module Dependabot
458
447
  end
459
448
  end
460
449
 
461
- def resolvable_before_update?(lockfile)
462
- @resolvable_before_update ||= {}
463
- 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)
464
452
 
465
- @resolvable_before_update[lockfile.name] =
453
+ @resolvable_before_update =
466
454
  begin
467
455
  SharedHelpers.in_a_temporary_directory do
468
- write_temporary_dependency_files(
469
- lockfile.name,
470
- update_package_json: false
471
- )
472
-
473
- lockfile_name = Pathname.new(lockfile.name).basename.to_s
474
- path = Pathname.new(lockfile.name).dirname.to_s
475
- Dir.chdir(path) do
476
- run_previous_npm_update(lockfile_name: lockfile_name, lockfile_content: lockfile.content)
477
- end
456
+ write_temporary_dependency_files(update_package_json: false)
457
+ Dir.chdir(lockfile_directory) { run_previous_npm_update }
478
458
  end
479
459
 
480
460
  true
@@ -492,12 +472,10 @@ module Dependabot
492
472
  end
493
473
  end
494
474
 
495
- def write_temporary_dependency_files(lockfile_name,
496
- update_package_json: true)
497
- write_lockfiles(lockfile_name)
475
+ def write_temporary_dependency_files(update_package_json: true)
476
+ write_lockfiles
498
477
 
499
- dir = Pathname.new(lockfile_name).dirname
500
- File.write(File.join(dir, ".npmrc"), npmrc_content)
478
+ File.write(File.join(lockfile_directory, ".npmrc"), npmrc_content)
501
479
 
502
480
  package_files.each do |file|
503
481
  path = file.name
@@ -524,9 +502,9 @@ module Dependabot
524
502
  end
525
503
  end
526
504
 
527
- def write_lockfiles(lockfile_name)
505
+ def write_lockfiles
528
506
  excluded_lock =
529
- case lockfile_name
507
+ case lockfile.name
530
508
  when "package-lock.json" then "npm-shrinkwrap.json"
531
509
  when "npm-shrinkwrap.json" then "package-lock.json"
532
510
  end
@@ -627,22 +605,86 @@ module Dependabot
627
605
  @git_ssh_requirements_to_swap
628
606
  end
629
607
 
630
- def post_process_npm_lockfile(original_content, updated_content, lockfile_name)
631
- updated_content = replace_project_metadata(updated_content, original_content)
632
-
608
+ def post_process_npm_lockfile(updated_lockfile_content)
633
609
  # Switch SSH requirements back for git dependencies
634
- updated_content = replace_swapped_git_ssh_requirements(updated_content)
610
+ updated_lockfile_content = replace_swapped_git_ssh_requirements(updated_lockfile_content)
635
611
 
636
612
  # Switch from details back for git dependencies (they will have
637
613
  # changed because we locked them)
638
- updated_content = replace_locked_git_dependencies(updated_content)
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)
639
623
 
640
624
  # Switch back npm 7 lockfile "pacakages" requirements from the package.json
641
- updated_content = restore_locked_package_dependencies(lockfile_name, updated_content)
625
+ updated_lockfile_content = restore_locked_package_dependencies(
626
+ updated_lockfile_content, parsed_updated_lockfile_content
627
+ )
642
628
 
643
629
  # Switch back the protocol of tarball resolutions if they've changed
644
630
  # (fixes an npm bug, which appears to be applied inconsistently)
645
- replace_tarball_urls(updated_content)
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')
646
688
  end
647
689
 
648
690
  # NOTE: This is a workaround to "sync" what's in package.json
@@ -654,43 +696,38 @@ module Dependabot
654
696
  # `package.json` requirement for eslint at `^1.0.0`, in which case we
655
697
  # need to copy this from the manifest to the lockfile after the update
656
698
  # has finished.
657
- def restore_locked_package_dependencies(lockfile_name, lockfile_content)
658
- return lockfile_content unless npm7?(lockfile_content)
659
-
660
- original_package = updated_package_json_content_for_lockfile_name(lockfile_name)
661
- return lockfile_content unless original_package
699
+ def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated_lockfile_content)
700
+ return updated_lockfile_content unless npm7?
662
701
 
663
- parsed_package = JSON.parse(original_package)
664
- parsed_lockfile = JSON.parse(lockfile_content)
665
702
  dependency_names_to_restore = (dependencies.map(&:name) + git_dependencies_to_lock.keys).uniq
666
703
 
667
704
  NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |type|
668
- parsed_package.fetch(type, {}).each do |dependency_name, original_requirement|
705
+ parsed_package_json.fetch(type, {}).each do |dependency_name, original_requirement|
669
706
  next unless dependency_names_to_restore.include?(dependency_name)
670
707
 
671
- locked_requirement = parsed_lockfile.dig("packages", "", type, dependency_name)
708
+ locked_requirement = parsed_updated_lockfile_content.dig("packages", "", type, dependency_name)
672
709
  next unless locked_requirement
673
710
 
674
711
  locked_req = %("#{dependency_name}": "#{locked_requirement}")
675
712
  original_req = %("#{dependency_name}": "#{original_requirement}")
676
- lockfile_content = lockfile_content.gsub(locked_req, original_req)
713
+ updated_lockfile_content = updated_lockfile_content.gsub(locked_req, original_req)
677
714
  end
678
715
  end
679
716
 
680
- lockfile_content
717
+ updated_lockfile_content
681
718
  end
682
719
 
683
- def replace_swapped_git_ssh_requirements(lockfile_content)
720
+ def replace_swapped_git_ssh_requirements(updated_lockfile_content)
684
721
  git_ssh_requirements_to_swap.each do |req|
685
722
  new_r = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'git+https://\1/')
686
723
  old_r = req.gsub(%r{git@(.*?)[:/]}, 'git@\1/')
687
- lockfile_content = lockfile_content.gsub(new_r, old_r)
724
+ updated_lockfile_content = updated_lockfile_content.gsub(new_r, old_r)
688
725
  end
689
726
 
690
- lockfile_content
727
+ updated_lockfile_content
691
728
  end
692
729
 
693
- def replace_locked_git_dependencies(lockfile_content)
730
+ def replace_locked_git_dependencies(updated_lockfile_content)
694
731
  # Switch from details back for git dependencies (they will have
695
732
  # changed because we locked them)
696
733
  git_dependencies_to_lock.each do |dependency_name, details|
@@ -701,44 +738,33 @@ module Dependabot
701
738
  # updates the lockfile "from" field to the new git commit when we
702
739
  # run npm install
703
740
  original_from = %("from": "#{details[:from]}")
704
- if npm7?(lockfile_content)
741
+ if npm7?
705
742
  # NOTE: The `from` syntax has changed in npm 7 to inclued the dependency name
706
743
  npm7_locked_from = %("from": "#{dependency_name}@#{details[:version]}")
707
- lockfile_content = lockfile_content.gsub(npm7_locked_from, original_from)
744
+ updated_lockfile_content = updated_lockfile_content.gsub(npm7_locked_from, original_from)
708
745
  else
709
746
  npm6_locked_from = %("from": "#{details[:version]}")
710
- lockfile_content = lockfile_content.gsub(npm6_locked_from, original_from)
747
+ updated_lockfile_content = updated_lockfile_content.gsub(npm6_locked_from, original_from)
711
748
  end
712
749
  end
713
750
 
714
- lockfile_content
751
+ updated_lockfile_content
715
752
  end
716
753
 
717
- def replace_tarball_urls(lockfile_content)
754
+ def replace_tarball_urls(updated_lockfile_content)
718
755
  tarball_urls.each do |url|
719
756
  trimmed_url = url.gsub(/(\d+\.)*tgz$/, "")
720
757
  incorrect_url = if url.start_with?("https")
721
758
  trimmed_url.gsub(/^https:/, "http:")
722
759
  else trimmed_url.gsub(/^http:/, "https:")
723
760
  end
724
- lockfile_content = lockfile_content.gsub(
761
+ updated_lockfile_content = updated_lockfile_content.gsub(
725
762
  /#{Regexp.quote(incorrect_url)}(?=(\d+\.)*tgz")/,
726
763
  trimmed_url
727
764
  )
728
765
  end
729
766
 
730
- lockfile_content
731
- end
732
-
733
- def replace_project_metadata(new_content, old_content)
734
- old_name = old_content.match(/(?<="name": ").*(?=",)/)&.to_s
735
-
736
- if old_name
737
- new_content = new_content.
738
- sub(/(?<="name": ").*(?=",)/, old_name)
739
- end
740
-
741
- new_content
767
+ updated_lockfile_content
742
768
  end
743
769
 
744
770
  def tarball_urls
@@ -765,15 +791,6 @@ module Dependabot
765
791
  ).npmrc_content
766
792
  end
767
793
 
768
- def updated_package_json_content_for_lockfile_name(lockfile_name)
769
- lockfile_basename = Pathname.new(lockfile_name).basename.to_s
770
- package_name = lockfile_name.sub(lockfile_basename, "package.json")
771
- package_json = package_files.find { |f| f.name == package_name }
772
- return unless package_json
773
-
774
- updated_package_json_content(package_json)
775
- end
776
-
777
794
  def updated_package_json_content(file)
778
795
  @updated_package_json_content ||= {}
779
796
  @updated_package_json_content[file.name] ||=
@@ -787,8 +804,10 @@ module Dependabot
787
804
  npmrc_content.match?(/^package-lock\s*=\s*false/)
788
805
  end
789
806
 
790
- def npm7?(lockfile_content)
791
- Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content) == "npm7"
807
+ def npm7?
808
+ return @npm7 if defined?(@npm7)
809
+
810
+ @npm7 = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile.content) == "npm7"
792
811
  end
793
812
 
794
813
  def sanitized_package_json_content(content)
@@ -802,6 +821,30 @@ module Dependabot
802
821
  package_name.gsub("%2f", "/").gsub("%2F", "/")
803
822
  end
804
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
+
805
848
  def package_locks
806
849
  @package_locks ||=
807
850
  dependency_files.