dependabot-npm_and_yarn 0.287.0 → 0.289.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a81729237750f9d53bf9197345ac3f563f267d9683ac202ebca543413bd912ef
4
- data.tar.gz: 5caefb54429a28a52aeff8b50ba8f80554f67baa70eaf6e5bef7040d52d7ebaa
3
+ metadata.gz: 3b61a2e379cb066af66a91f9cbfd25d89755129946f7093b2e1d5be7f3642133
4
+ data.tar.gz: 10995493f890b53c62af1c14f7d29a165ee32096b8b84a529f0822984d6f1480
5
5
  SHA512:
6
- metadata.gz: 2d4fca7ae33a0540e6940de6946420c08e06be84daf21b37c9efe7234af43050de4efe74191254ace8b12e7e977c74d66557067e9a9bdc07eaf5964bc1996060
7
- data.tar.gz: ab4bd3efd5fe75244c87ec30fb31e1585ce26ac586d42a4b9b8ea84fe86a4015d54dbdc515afe228fb319520fdedc1b1aaa1ab89f71f38bd5cf76683c4c780a9
6
+ metadata.gz: 3fb1619f2f8ba90e8bbe7945c7b1e179abd09004b940c1d2514d9f4290a7679955ad0fcc0db26ebcb4b810c5903e4d66ddb3f3d0b3ca0a265c9636f34f272519
7
+ data.tar.gz: ae15d52f683156e2df0ddf213c254e0b9cd6e9324da8eda5367dae8d1c59629497739be437e3c76c6597ce9ccbbbd06509196ba9efad51240e3e98a9c99d91f4
@@ -107,6 +107,7 @@ module Dependabot
107
107
  fetched_yarn_files << yarn_lock if yarn_lock
108
108
  fetched_yarn_files << yarnrc if yarnrc
109
109
  fetched_yarn_files << yarnrc_yml if yarnrc_yml
110
+ create_yarn_cache
110
111
  fetched_yarn_files
111
112
  end
112
113
 
@@ -244,6 +245,20 @@ module Dependabot
244
245
  return @pnpm_lock if defined?(@pnpm_lock)
245
246
 
246
247
  @pnpm_lock ||= T.let(fetch_file_if_present(PNPMPackageManager::LOCKFILE_NAME), T.nilable(DependencyFile))
248
+
249
+ return @pnpm_lock if @pnpm_lock || directory == "/"
250
+
251
+ # Loop through parent directories looking for a pnpm-lock
252
+ (1..directory.split("/").count).each do |i|
253
+ @pnpm_lock = fetch_file_from_host(("../" * i) + PNPMPackageManager::LOCKFILE_NAME)
254
+ .tap { |f| f.support_file = true }
255
+ break if @pnpm_lock
256
+ rescue Dependabot::DependencyFileNotFound
257
+ # Ignore errors (pnpm_lock.yaml may not be present)
258
+ nil
259
+ end
260
+
261
+ @pnpm_lock
247
262
  end
248
263
 
249
264
  sig { returns(T.nilable(DependencyFile)) }
@@ -655,6 +670,19 @@ module Dependabot
655
670
  rescue JSON::ParserError
656
671
  raise Dependabot::DependencyFileNotParseable, T.must(lerna_json).path
657
672
  end
673
+
674
+ sig { void }
675
+ def create_yarn_cache
676
+ if repo_contents_path.nil?
677
+ Dependabot.logger.info("Repository contents path is nil")
678
+ elsif Dir.exist?(T.must(repo_contents_path))
679
+ Dir.chdir(T.must(repo_contents_path)) do
680
+ FileUtils.mkdir_p(".yarn/cache")
681
+ end
682
+ else
683
+ Dependabot.logger.info("Repository contents path does not exist")
684
+ end
685
+ end
658
686
  end
659
687
  end
660
688
  end
@@ -11,6 +11,7 @@ require "dependabot/npm_and_yarn/helpers"
11
11
  require "dependabot/npm_and_yarn/native_helpers"
12
12
  require "dependabot/npm_and_yarn/version"
13
13
  require "dependabot/npm_and_yarn/requirement"
14
+ require "dependabot/npm_and_yarn/package_manager"
14
15
  require "dependabot/npm_and_yarn/registry_parser"
15
16
  require "dependabot/git_metadata_fetcher"
16
17
  require "dependabot/git_commit_checker"
@@ -83,7 +84,8 @@ module Dependabot
83
84
  @ecosystem ||= T.let(
84
85
  Ecosystem.new(
85
86
  name: ECOSYSTEM,
86
- package_manager: package_manager_helper.package_manager
87
+ package_manager: package_manager_helper.package_manager,
88
+ language: package_manager_helper.language
87
89
  ),
88
90
  T.nilable(Ecosystem)
89
91
  )
@@ -477,4 +479,4 @@ module Dependabot
477
479
  end
478
480
 
479
481
  Dependabot::FileParsers
480
- .register("npm_and_yarn", Dependabot::NpmAndYarn::FileParser)
482
+ .register(Dependabot::NpmAndYarn::ECOSYSTEM, Dependabot::NpmAndYarn::FileParser)
@@ -1,9 +1,10 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency"
5
5
  require "dependabot/file_parsers"
6
6
  require "dependabot/file_parsers/base"
7
+ require "dependabot/shared_helpers"
7
8
  require "sorbet-runtime"
8
9
 
9
10
  module Dependabot
@@ -15,6 +16,7 @@ module Dependabot
15
16
  /^.*(?<error>The "yarn-path" option has been set \(in [^)]+\), but the specified location doesn't exist)/
16
17
 
17
18
  # NPM Version Constants
19
+ NPM_V10 = 10
18
20
  NPM_V8 = 8
19
21
  NPM_V6 = 6
20
22
  NPM_DEFAULT_VERSION = NPM_V8
@@ -39,6 +41,10 @@ module Dependabot
39
41
  # Otherwise, we are going to use old versionining npm 6
40
42
  sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
41
43
  def self.npm_version_numeric(lockfile)
44
+ if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
45
+ return npm_version_numeric_latest(lockfile)
46
+ end
47
+
42
48
  fallback_version_npm8 = Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6)
43
49
 
44
50
  return npm_version_numeric_npm8_or_higher(lockfile) if fallback_version_npm8
@@ -90,6 +96,36 @@ module Dependabot
90
96
  NPM_DEFAULT_VERSION # Fallback to default npm version if parsing fails
91
97
  end
92
98
 
99
+ # rubocop:disable Metrics/PerceivedComplexity
100
+ sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
101
+ def self.npm_version_numeric_latest(lockfile)
102
+ lockfile_content = lockfile&.content
103
+
104
+ # Return npm 10 as the default if the lockfile is missing or empty
105
+ return NPM_V10 if lockfile_content.nil? || lockfile_content.strip.empty?
106
+
107
+ # Parse the lockfile content to extract the `lockfileVersion`
108
+ parsed_lockfile = JSON.parse(lockfile_content)
109
+ lockfile_version = parsed_lockfile["lockfileVersion"]&.to_i
110
+
111
+ # Determine the appropriate npm version based on `lockfileVersion`
112
+ if lockfile_version.nil?
113
+ NPM_V10 # Use npm 10 if `lockfileVersion` is missing or nil
114
+ elsif lockfile_version >= 3
115
+ NPM_V10 # Use npm 10 for lockfileVersion 3 or higher
116
+ elsif lockfile_version >= 2
117
+ NPM_V8 # Use npm 8 for lockfileVersion 2
118
+ elsif lockfile_version >= 1
119
+ # Use npm 8 if the fallback version flag is enabled, otherwise use npm 6
120
+ Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6) ? NPM_V8 : NPM_V6
121
+ else
122
+ NPM_V10 # Default to npm 10 for unexpected or unsupported versions
123
+ end
124
+ rescue JSON::ParserError
125
+ NPM_V8 # Fallback to npm 8 if the lockfile content cannot be parsed
126
+ end
127
+ # rubocop:enable Metrics/PerceivedComplexity
128
+
93
129
  sig { params(yarn_lock: T.nilable(DependencyFile)).returns(Integer) }
94
130
  def self.yarn_version_numeric(yarn_lock)
95
131
  lockfile_content = yarn_lock&.content
@@ -110,9 +146,14 @@ module Dependabot
110
146
  def self.pnpm_version_numeric(pnpm_lock)
111
147
  lockfile_content = pnpm_lock&.content
112
148
 
113
- return PNPM_DEFAULT_VERSION if lockfile_content.nil? || lockfile_content.strip.empty?
149
+ return PNPM_DEFAULT_VERSION if !lockfile_content || lockfile_content.strip.empty?
150
+
151
+ pnpm_lockfile_version_str = pnpm_lockfile_version(pnpm_lock)
152
+
153
+ return PNPM_FALLBACK_VERSION unless pnpm_lockfile_version_str
154
+
155
+ pnpm_lockfile_version = pnpm_lockfile_version_str.to_f
114
156
 
115
- pnpm_lockfile_version = pnpm_lockfile_version(pnpm_lock).to_f
116
157
  return PNPM_V9 if pnpm_lockfile_version >= 9.0
117
158
  return PNPM_V8 if pnpm_lockfile_version >= 6.0
118
159
  return PNPM_V7 if pnpm_lockfile_version >= 5.4
@@ -120,6 +161,7 @@ module Dependabot
120
161
  PNPM_FALLBACK_VERSION
121
162
  end
122
163
 
164
+ sig { params(key: String, default_value: String).returns(T.untyped) }
123
165
  def self.fetch_yarnrc_yml_value(key, default_value)
124
166
  if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml"))
125
167
  yarnrc.fetch(key, default_value)
@@ -132,6 +174,10 @@ module Dependabot
132
174
  def self.npm8?(package_lock)
133
175
  return true unless package_lock&.content
134
176
 
177
+ if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
178
+ return npm_version_numeric_latest(package_lock) >= NPM_V8
179
+ end
180
+
135
181
  npm_version_numeric(package_lock) == NPM_V8
136
182
  end
137
183
 
@@ -252,9 +298,12 @@ module Dependabot
252
298
  # set to false. Yarn commands should _not_ be ran outside of this helper
253
299
  # to ensure that postinstall scripts are never executed, as they could
254
300
  # contain malicious code.
301
+ sig { params(commands: T::Array[String]).void }
255
302
  def self.run_yarn_commands(*commands)
256
303
  setup_yarn_berry
257
- commands.each { |cmd, fingerprint| run_single_yarn_command(cmd, fingerprint: fingerprint) }
304
+ commands.each do |cmd, fingerprint|
305
+ run_single_yarn_command(cmd, fingerprint: fingerprint) if cmd
306
+ end
258
307
  end
259
308
 
260
309
  # Run single npm command returning stdout/stderr.
@@ -267,10 +316,44 @@ module Dependabot
267
316
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
268
317
  package_manager_run_command(NpmPackageManager::NAME, command, fingerprint: fingerprint)
269
318
  else
270
- SharedHelpers.run_shell_command("corepack npm #{command}", fingerprint: "corepack npm #{fingerprint}")
319
+ Dependabot::SharedHelpers.run_shell_command(
320
+ "corepack npm #{command}",
321
+ fingerprint: "corepack npm #{fingerprint}"
322
+ )
271
323
  end
272
324
  end
273
325
 
326
+ sig { returns(T.nilable(String)) }
327
+ def self.node_version
328
+ version = run_node_command("-v", fingerprint: "-v").strip
329
+
330
+ # Validate the output format (e.g., "v20.18.1" or "20.18.1")
331
+ if version.match?(/^v?\d+(\.\d+){2}$/)
332
+ version.strip.delete_prefix("v") # Remove the "v" prefix if present
333
+ end
334
+ rescue StandardError => e
335
+ puts "Error retrieving Node.js version: #{e.message}"
336
+ nil
337
+ end
338
+
339
+ sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
340
+ def self.run_node_command(command, fingerprint: nil)
341
+ full_command = "node #{command}"
342
+
343
+ Dependabot.logger.info("Running node command: #{full_command}")
344
+
345
+ result = Dependabot::SharedHelpers.run_shell_command(
346
+ full_command,
347
+ fingerprint: "node #{fingerprint || command}"
348
+ )
349
+
350
+ Dependabot.logger.info("Command executed successfully: #{full_command}")
351
+ result
352
+ rescue StandardError => e
353
+ Dependabot.logger.error("Error running node command: #{full_command}, Error: #{e.message}")
354
+ raise
355
+ end
356
+
274
357
  # Setup yarn and run a single yarn command returning stdout/stderr
275
358
  sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
276
359
  def self.run_yarn_command(command, fingerprint: nil)
@@ -284,7 +367,10 @@ module Dependabot
284
367
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
285
368
  package_manager_run_command(PNPMPackageManager::NAME, command, fingerprint: fingerprint)
286
369
  else
287
- SharedHelpers.run_shell_command("pnpm #{command}", fingerprint: "pnpm #{fingerprint || command}")
370
+ Dependabot::SharedHelpers.run_shell_command(
371
+ "pnpm #{command}",
372
+ fingerprint: "pnpm #{fingerprint || command}"
373
+ )
288
374
  end
289
375
  end
290
376
 
@@ -294,13 +380,16 @@ module Dependabot
294
380
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
295
381
  package_manager_run_command(YarnPackageManager::NAME, command, fingerprint: fingerprint)
296
382
  else
297
- SharedHelpers.run_shell_command("yarn #{command}", fingerprint: "yarn #{fingerprint || command}")
383
+ Dependabot::SharedHelpers.run_shell_command(
384
+ "yarn #{command}",
385
+ fingerprint: "yarn #{fingerprint || command}"
386
+ )
298
387
  end
299
388
  end
300
389
 
301
390
  # Install the package manager for specified version by using corepack
302
391
  # and prepare it for use by using corepack
303
- sig { params(name: String, version: String).void }
392
+ sig { params(name: String, version: String).returns(String) }
304
393
  def self.install(name, version)
305
394
  Dependabot.logger.info("Installing \"#{name}@#{version}\"")
306
395
 
@@ -309,30 +398,40 @@ module Dependabot
309
398
  installed_version = package_manager_version(name)
310
399
 
311
400
  Dependabot.logger.info("Installed version of #{name}: #{installed_version}")
401
+
402
+ installed_version
312
403
  end
313
404
 
314
405
  # Install the package manager for specified version by using corepack
315
406
  sig { params(name: String, version: String).void }
316
407
  def self.package_manager_install(name, version)
317
- SharedHelpers.run_shell_command(
408
+ Dependabot::SharedHelpers.run_shell_command(
318
409
  "corepack install #{name}@#{version} --global --cache-only",
319
410
  fingerprint: "corepack install <name>@<version> --global --cache-only"
320
- )
411
+ ).strip
321
412
  end
322
413
 
323
414
  # Prepare the package manager for use by using corepack
324
415
  sig { params(name: String, version: String).void }
325
416
  def self.package_manager_activate(name, version)
326
- SharedHelpers.run_shell_command(
417
+ Dependabot::SharedHelpers.run_shell_command(
327
418
  "corepack prepare #{name}@#{version} --activate",
328
419
  fingerprint: "corepack prepare --activate"
329
- )
420
+ ).strip
330
421
  end
331
422
 
332
423
  # Get the version of the package manager by using corepack
333
424
  sig { params(name: String).returns(String) }
334
425
  def self.package_manager_version(name)
335
- package_manager_run_command(name, "-v")
426
+ Dependabot.logger.info("Fetching version for package manager: #{name}")
427
+
428
+ version = package_manager_run_command(name, "-v").strip
429
+
430
+ Dependabot.logger.info("Version for #{name}: #{version}")
431
+ version
432
+ rescue StandardError => e
433
+ Dependabot.logger.error("Error fetching version for package manager #{name}: #{e.message}")
434
+ raise
336
435
  end
337
436
 
338
437
  # Run single command on package manager returning stdout/stderr
@@ -344,15 +443,30 @@ module Dependabot
344
443
  ).returns(String)
345
444
  end
346
445
  def self.package_manager_run_command(name, command, fingerprint: nil)
347
- SharedHelpers.run_shell_command(
348
- "corepack #{name} #{command}",
446
+ full_command = "corepack #{name} #{command}"
447
+
448
+ Dependabot.logger.info("Running package manager command: #{full_command}")
449
+
450
+ result = Dependabot::SharedHelpers.run_shell_command(
451
+ full_command,
349
452
  fingerprint: "corepack #{name} #{fingerprint || command}"
350
- )
453
+ ).strip
454
+
455
+ Dependabot.logger.info("Command executed successfully: #{full_command}")
456
+ result
457
+ rescue StandardError => e
458
+ Dependabot.logger.error("Error running package manager command: #{full_command}, Error: #{e.message}")
459
+ raise
351
460
  end
461
+
352
462
  private_class_method :run_single_yarn_command
353
463
 
464
+ sig { params(pnpm_lock: DependencyFile).returns(T.nilable(String)) }
354
465
  def self.pnpm_lockfile_version(pnpm_lock)
355
- pnpm_lock.content.match(/^lockfileVersion: ['"]?(?<version>[\d.]+)/)[:version]
466
+ match = T.must(pnpm_lock.content).match(/^lockfileVersion: ['"]?(?<version>[\d.]+)/)
467
+ return match[:version] if match
468
+
469
+ nil
356
470
  end
357
471
 
358
472
  sig { params(dependency_set: Dependabot::FileParsers::Base::DependencySet).returns(T::Array[Dependency]) }
@@ -1,8 +1,9 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/shared_helpers"
5
5
  require "dependabot/ecosystem"
6
+ require "dependabot/npm_and_yarn/requirement"
6
7
  require "dependabot/npm_and_yarn/version_selector"
7
8
 
8
9
  module Dependabot
@@ -10,6 +11,37 @@ module Dependabot
10
11
  ECOSYSTEM = "npm_and_yarn"
11
12
  MANIFEST_FILENAME = "package.json"
12
13
  LERNA_JSON_FILENAME = "lerna.json"
14
+ PACKAGE_MANAGER_VERSION_REGEX = /
15
+ ^ # Start of string
16
+ (?<major>\d+) # Major version (required, numeric)
17
+ \. # Separator between major and minor versions
18
+ (?<minor>\d+) # Minor version (required, numeric)
19
+ \. # Separator between minor and patch versions
20
+ (?<patch>\d+) # Patch version (required, numeric)
21
+ ( # Start pre-release section
22
+ -(?<pre_release>[a-zA-Z0-9.]+) # Pre-release label (optional, alphanumeric or dot-separated)
23
+ )?
24
+ ( # Start build metadata section
25
+ \+(?<build>[a-zA-Z0-9.]+) # Build metadata (optional, alphanumeric or dot-separated)
26
+ )?
27
+ $ # End of string
28
+ /x # Extended mode for readability
29
+
30
+ VALID_REQUIREMENT_CONSTRAINT = /
31
+ ^ # Start of string
32
+ (?<operator>=|>|>=|<|<=|~>|\\^) # Allowed operators
33
+ \s* # Optional whitespace
34
+ (?<major>\d+) # Major version (required)
35
+ (\.(?<minor>\d+))? # Minor version (optional)
36
+ (\.(?<patch>\d+))? # Patch version (optional)
37
+ ( # Start pre-release section
38
+ -(?<pre_release>[a-zA-Z0-9.]+) # Pre-release label (optional)
39
+ )?
40
+ ( # Start build metadata section
41
+ \+(?<build>[a-zA-Z0-9.]+) # Build metadata (optional)
42
+ )?
43
+ $ # End of string
44
+ /x # Extended mode for readability
13
45
 
14
46
  MANIFEST_PACKAGE_MANAGER_KEY = "packageManager"
15
47
  MANIFEST_ENGINES_KEY = "engines"
@@ -25,24 +57,32 @@ module Dependabot
25
57
  NPM_V7 = "7"
26
58
  NPM_V8 = "8"
27
59
  NPM_V9 = "9"
60
+ NPM_V10 = "10"
28
61
 
29
62
  # Keep versions in ascending order
30
63
  SUPPORTED_VERSIONS = T.let([
31
64
  Version.new(NPM_V6),
32
65
  Version.new(NPM_V7),
33
66
  Version.new(NPM_V8),
34
- Version.new(NPM_V9)
67
+ Version.new(NPM_V9),
68
+ Version.new(NPM_V10)
35
69
  ].freeze, T::Array[Dependabot::Version])
36
70
 
37
71
  DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
38
72
 
39
- sig { params(raw_version: String).void }
40
- def initialize(raw_version)
73
+ sig do
74
+ params(
75
+ raw_version: String,
76
+ requirement: T.nilable(Dependabot::NpmAndYarn::Requirement)
77
+ ).void
78
+ end
79
+ def initialize(raw_version, requirement: nil)
41
80
  super(
42
81
  NAME,
43
82
  Version.new(raw_version),
44
83
  DEPRECATED_VERSIONS,
45
- SUPPORTED_VERSIONS
84
+ SUPPORTED_VERSIONS,
85
+ requirement
46
86
  )
47
87
  end
48
88
 
@@ -76,13 +116,19 @@ module Dependabot
76
116
 
77
117
  DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
78
118
 
79
- sig { params(raw_version: String).void }
80
- def initialize(raw_version)
119
+ sig do
120
+ params(
121
+ raw_version: String,
122
+ requirement: T.nilable(Requirement)
123
+ ).void
124
+ end
125
+ def initialize(raw_version, requirement: nil)
81
126
  super(
82
127
  NAME,
83
128
  Version.new(raw_version),
84
129
  DEPRECATED_VERSIONS,
85
- SUPPORTED_VERSIONS
130
+ SUPPORTED_VERSIONS,
131
+ requirement
86
132
  )
87
133
  end
88
134
 
@@ -115,13 +161,19 @@ module Dependabot
115
161
 
116
162
  DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
117
163
 
118
- sig { params(raw_version: String).void }
119
- def initialize(raw_version)
164
+ sig do
165
+ params(
166
+ raw_version: String,
167
+ requirement: T.nilable(Requirement)
168
+ ).void
169
+ end
170
+ def initialize(raw_version, requirement: nil)
120
171
  super(
121
172
  NAME,
122
173
  Version.new(raw_version),
123
174
  DEPRECATED_VERSIONS,
124
- SUPPORTED_VERSIONS
175
+ SUPPORTED_VERSIONS,
176
+ requirement
125
177
  )
126
178
  end
127
179
 
@@ -138,11 +190,20 @@ module Dependabot
138
190
 
139
191
  DEFAULT_PACKAGE_MANAGER = NpmPackageManager::NAME
140
192
 
141
- PACKAGE_MANAGER_CLASSES = {
193
+ # Define a type alias for the expected class interface
194
+ NpmAndYarnPackageManagerClassType = T.type_alias do
195
+ T.any(
196
+ T.class_of(Dependabot::NpmAndYarn::NpmPackageManager),
197
+ T.class_of(Dependabot::NpmAndYarn::YarnPackageManager),
198
+ T.class_of(Dependabot::NpmAndYarn::PNPMPackageManager)
199
+ )
200
+ end
201
+
202
+ PACKAGE_MANAGER_CLASSES = T.let({
142
203
  NpmPackageManager::NAME => NpmPackageManager,
143
204
  YarnPackageManager::NAME => YarnPackageManager,
144
205
  PNPMPackageManager::NAME => PNPMPackageManager
145
- }.freeze
206
+ }.freeze, T::Hash[String, NpmAndYarnPackageManagerClassType])
146
207
 
147
208
  class PackageManagerDetector
148
209
  extend T::Sig
@@ -151,21 +212,34 @@ module Dependabot
151
212
  sig do
152
213
  params(
153
214
  lockfiles: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)],
154
- package_json: T::Hash[String, T.untyped]
215
+ package_json: T.nilable(T::Hash[String, T.untyped])
155
216
  ).void
156
217
  end
157
218
  def initialize(lockfiles, package_json)
158
219
  @lockfiles = lockfiles
159
220
  @package_json = package_json
160
- @manifest_package_manager = package_json["packageManager"]
161
- @engines = package_json.fetch(MANIFEST_ENGINES_KEY, nil)
221
+ @manifest_package_manager = T.let(package_json&.fetch(MANIFEST_PACKAGE_MANAGER_KEY, nil), T.nilable(String))
222
+ @engines = T.let(package_json&.fetch(MANIFEST_ENGINES_KEY, {}), T::Hash[String, T.untyped])
162
223
  end
163
224
 
164
225
  # Returns npm, yarn, or pnpm based on the lockfiles, package.json, and engines
165
226
  # Defaults to npm if no package manager is detected
166
227
  sig { returns(String) }
167
228
  def detect_package_manager
168
- name_from_lockfiles || name_from_package_manager_attr || name_from_engines || DEFAULT_PACKAGE_MANAGER
229
+ package_manager = name_from_lockfiles ||
230
+ name_from_package_manager_attr ||
231
+ name_from_engines
232
+
233
+ if package_manager
234
+ Dependabot.logger.info("Detected package manager: #{package_manager}")
235
+ else
236
+ package_manager = DEFAULT_PACKAGE_MANAGER
237
+ Dependabot.logger.info("Default package manager used: #{package_manager}")
238
+ end
239
+ package_manager
240
+ rescue StandardError => e
241
+ Dependabot.logger.error("Error detecting package manager: #{e.message}")
242
+ DEFAULT_PACKAGE_MANAGER
169
243
  end
170
244
 
171
245
  private
@@ -195,22 +269,62 @@ module Dependabot
195
269
  end
196
270
  end
197
271
 
272
+ class Language < Ecosystem::VersionManager
273
+ extend T::Sig
274
+ NAME = "node"
275
+
276
+ SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
277
+
278
+ DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
279
+
280
+ sig do
281
+ params(
282
+ raw_version: T.nilable(String),
283
+ requirement: T.nilable(Requirement)
284
+ ).void
285
+ end
286
+ def initialize(raw_version, requirement: nil)
287
+ super(
288
+ NAME,
289
+ Version.new(raw_version),
290
+ DEPRECATED_VERSIONS,
291
+ SUPPORTED_VERSIONS,
292
+ requirement
293
+ )
294
+ end
295
+
296
+ sig { override.returns(T::Boolean) }
297
+ def deprecated?
298
+ false
299
+ end
300
+
301
+ sig { override.returns(T::Boolean) }
302
+ def unsupported?
303
+ false
304
+ end
305
+ end
306
+
198
307
  class PackageManagerHelper
199
308
  extend T::Sig
200
309
  extend T::Helpers
201
310
 
202
311
  sig do
203
312
  params(
204
- package_json: T::Hash[String, T.untyped],
313
+ package_json: T.nilable(T::Hash[String, T.untyped]),
205
314
  lockfiles: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]
206
315
  ).void
207
316
  end
208
317
  def initialize(package_json, lockfiles:)
209
318
  @package_json = package_json
210
319
  @lockfiles = lockfiles
211
- @manifest_package_manager = package_json[MANIFEST_PACKAGE_MANAGER_KEY]
212
- @engines = package_json.fetch(MANIFEST_ENGINES_KEY, nil)
213
- @package_manager_detector = PackageManagerDetector.new(@lockfiles, @package_json)
320
+ @package_manager_detector = T.let(PackageManagerDetector.new(lockfiles, package_json), PackageManagerDetector)
321
+ @manifest_package_manager = T.let(package_json&.fetch(MANIFEST_PACKAGE_MANAGER_KEY, nil), T.nilable(String))
322
+ @engines = T.let(package_json&.fetch(MANIFEST_ENGINES_KEY, nil), T.nilable(T::Hash[String, T.untyped]))
323
+
324
+ @installed_versions = T.let({}, T::Hash[String, String])
325
+
326
+ @language = T.let(nil, T.nilable(Ecosystem::VersionManager))
327
+ @language_requirement = T.let(nil, T.nilable(Requirement))
214
328
  end
215
329
 
216
330
  sig { returns(Ecosystem::VersionManager) }
@@ -220,8 +334,54 @@ module Dependabot
220
334
  )
221
335
  end
222
336
 
337
+ sig { returns(Ecosystem::VersionManager) }
338
+ def language
339
+ @language ||= Language.new(
340
+ Helpers.node_version,
341
+ requirement: language_requirement
342
+ )
343
+ end
344
+
345
+ sig { returns(T.nilable(Requirement)) }
346
+ def language_requirement
347
+ @language_requirement ||= find_engine_constraints_as_requirement(Language::NAME)
348
+ end
349
+
350
+ sig { params(name: String).returns(T.nilable(Requirement)) }
351
+ def find_engine_constraints_as_requirement(name)
352
+ Dependabot.logger.info("Processing engine constraints for #{name}")
353
+
354
+ return nil unless @engines.is_a?(Hash) && @engines[name]
355
+
356
+ raw_constraint = @engines[name].to_s.strip
357
+ return nil if raw_constraint.empty?
358
+
359
+ raw_constraints = raw_constraint.split
360
+ constraints = raw_constraints.map do |constraint|
361
+ case constraint
362
+ when /^\d+$/
363
+ ">=#{constraint}.0.0 <#{constraint.to_i + 1}.0.0"
364
+ when /^\d+\.\d+$/
365
+ ">=#{constraint} <#{constraint.split('.').first.to_i + 1}.0.0"
366
+ when /^\d+\.\d+\.\d+$/
367
+ "=#{constraint}"
368
+ else
369
+ Dependabot.logger.warn("Unrecognized constraint format for #{name}: #{constraint}")
370
+ constraint
371
+ end
372
+ end
373
+
374
+ Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
375
+ Requirement.new(constraints)
376
+ rescue StandardError => e
377
+ Dependabot.logger.error("Error processing constraints for #{name}: #{e.message}")
378
+ nil
379
+ end
380
+
223
381
  # rubocop:disable Metrics/CyclomaticComplexity
224
382
  # rubocop:disable Metrics/PerceivedComplexity
383
+ # rubocop:disable Metrics/AbcSize
384
+ sig { params(name: String).returns(T.nilable(T.any(Integer, String))) }
225
385
  def setup(name)
226
386
  # we prioritize version mentioned in "packageManager" instead of "engines"
227
387
  # i.e. if { engines : "pnpm" : "6" } and { packageManager: "pnpm@6.0.2" },
@@ -257,13 +417,13 @@ module Dependabot
257
417
 
258
418
  if version
259
419
  raise_if_unsupported!(name, version.to_s)
260
- install(name, version)
420
+ install(name, version.to_s)
261
421
  end
262
422
  else
263
423
  version ||= requested_version(name)
264
424
 
265
425
  if version
266
- raise_if_unsupported!(name, version)
426
+ raise_if_unsupported!(name, version.to_s)
267
427
 
268
428
  install(name, version)
269
429
  else
@@ -272,30 +432,74 @@ module Dependabot
272
432
  if version
273
433
  raise_if_unsupported!(name, version.to_s)
274
434
 
275
- install(name, version) if name == PNPMPackageManager::NAME
435
+ install(name, version.to_s) if name == PNPMPackageManager::NAME
276
436
  end
277
437
  end
278
438
  end
279
439
  version
280
440
  end
281
- # rubocop:enable Metrics/CyclomaticComplexity
282
- # rubocop:enable Metrics/PerceivedComplexity
283
-
284
- private
285
441
 
286
442
  sig { params(name: T.nilable(String)).returns(Ecosystem::VersionManager) }
287
443
  def package_manager_by_name(name)
288
- name = DEFAULT_PACKAGE_MANAGER if name.nil? || PACKAGE_MANAGER_CLASSES[name].nil?
444
+ Dependabot.logger.info("Resolving package manager for: #{name || 'default'}")
289
445
 
290
- package_manager_class = PACKAGE_MANAGER_CLASSES[name]
446
+ name = ensure_valid_package_manager(name)
447
+ package_manager_class = T.must(PACKAGE_MANAGER_CLASSES[name])
291
448
 
292
- package_manager_class ||= PACKAGE_MANAGER_CLASSES[DEFAULT_PACKAGE_MANAGER]
449
+ installed_version = installed_version(name)
450
+ Dependabot.logger.info("Installed version for #{name}: #{installed_version}")
293
451
 
294
- version = Helpers.send(:"#{name}_version_numeric", @lockfiles[name.to_sym])
452
+ package_manager_requirement = find_engine_constraints_as_requirement(name)
453
+ if package_manager_requirement
454
+ Dependabot.logger.info("Version requirement for #{name}: #{package_manager_requirement}")
455
+ else
456
+ Dependabot.logger.info("No version requirement found for #{name}")
457
+ end
295
458
 
296
- package_manager_class.new(version.to_s)
459
+ package_manager_instance = package_manager_class.new(
460
+ installed_version,
461
+ requirement: package_manager_requirement
462
+ )
463
+
464
+ Dependabot.logger.info("Package manager resolved for #{name}: #{package_manager_instance}")
465
+ package_manager_instance
466
+ rescue StandardError => e
467
+ Dependabot.logger.error("Error resolving package manager for #{name || 'default'}: #{e.message}")
468
+ raise
297
469
  end
298
470
 
471
+ # rubocop:enable Metrics/CyclomaticComplexity
472
+ # rubocop:enable Metrics/PerceivedComplexity
473
+ # rubocop:enable Metrics/AbcSize
474
+ # Retrieve the installed version of the package manager by executing
475
+ # the "corepack <name> -v" command and using the output.
476
+ # If the output does not match the expected version format (PACKAGE_MANAGER_VERSION_REGEX),
477
+ # fall back to the version inferred from the dependency files.
478
+ sig { params(name: String).returns(String) }
479
+ def installed_version(name)
480
+ # Return the memoized version if it has already been computed
481
+ return T.must(@installed_versions[name]) if @installed_versions.key?(name)
482
+
483
+ # Attempt to get the installed version through the package manager version command
484
+ @installed_versions[name] = Helpers.package_manager_version(name)
485
+
486
+ # If we can't get the installed version, we need to install the package manager and get the version
487
+ unless @installed_versions[name]&.match?(PACKAGE_MANAGER_VERSION_REGEX)
488
+ setup(name)
489
+ @installed_versions[name] = Helpers.package_manager_version(name)
490
+ end
491
+
492
+ # If we can't get the installed version or the version is invalid, we need to get inferred version
493
+ unless @installed_versions[name]&.match?(PACKAGE_MANAGER_VERSION_REGEX)
494
+ @installed_versions[name] = Helpers.public_send(:"#{name}_version_numeric", @lockfiles[name.to_sym]).to_s
495
+ end
496
+
497
+ T.must(@installed_versions[name])
498
+ end
499
+
500
+ private
501
+
502
+ sig { params(name: String, version: String).void }
299
503
  def raise_if_unsupported!(name, version)
300
504
  return unless name == PNPMPackageManager::NAME
301
505
  return unless Version.new(version) < Version.new("7")
@@ -303,6 +507,7 @@ module Dependabot
303
507
  raise ToolVersionNotSupported.new(PNPMPackageManager::NAME.upcase, version, "7.*, 8.*")
304
508
  end
305
509
 
510
+ sig { params(name: String, version: T.nilable(String)).void }
306
511
  def install(name, version)
307
512
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
308
513
  return Helpers.install(name, version.to_s)
@@ -316,6 +521,13 @@ module Dependabot
316
521
  )
317
522
  end
318
523
 
524
+ sig { params(name: T.nilable(String)).returns(String) }
525
+ def ensure_valid_package_manager(name)
526
+ name = DEFAULT_PACKAGE_MANAGER if name.nil? || PACKAGE_MANAGER_CLASSES[name].nil?
527
+ name
528
+ end
529
+
530
+ sig { params(name: String).returns(T.nilable(String)) }
319
531
  def requested_version(name)
320
532
  return unless @manifest_package_manager
321
533
 
@@ -326,6 +538,7 @@ module Dependabot
326
538
  match["version"]
327
539
  end
328
540
 
541
+ sig { params(name: String).returns(T.nilable(T.any(Integer, String))) }
329
542
  def guessed_version(name)
330
543
  lockfile = @lockfiles[name.to_sym]
331
544
  return unless lockfile
@@ -339,6 +552,8 @@ module Dependabot
339
552
 
340
553
  sig { params(name: T.untyped).returns(T.nilable(String)) }
341
554
  def check_engine_version(name)
555
+ return if @package_json.nil?
556
+
342
557
  version_selector = VersionSelector.new
343
558
  engine_versions = version_selector.setup(@package_json, name)
344
559
 
@@ -70,8 +70,8 @@ module Dependabot
70
70
  run_yarn_updater(path, lockfile_name)
71
71
  elsif lockfile.name.end_with?("pnpm-lock.yaml")
72
72
  run_pnpm_updater(path, lockfile_name)
73
- elsif Helpers.npm8?(lockfile)
74
- run_npm8_updater(path, lockfile_name)
73
+ elsif !Helpers.npm8?(lockfile)
74
+ run_npm6_updater(path, lockfile_name)
75
75
  else
76
76
  run_npm_updater(path, lockfile_name)
77
77
  end
@@ -143,7 +143,7 @@ module Dependabot
143
143
  end
144
144
  end
145
145
 
146
- def run_npm8_updater(path, lockfile_name)
146
+ def run_npm_updater(path, lockfile_name)
147
147
  SharedHelpers.with_git_configured(credentials: credentials) do
148
148
  Dir.chdir(path) do
149
149
  NativeHelpers.run_npm8_subdependency_update_command([dependency.name])
@@ -153,7 +153,7 @@ module Dependabot
153
153
  end
154
154
  end
155
155
 
156
- def run_npm_updater(path, lockfile_name)
156
+ def run_npm6_updater(path, lockfile_name)
157
157
  SharedHelpers.with_git_configured(credentials: credentials) do
158
158
  Dir.chdir(path) do
159
159
  SharedHelpers.run_helper_subprocess(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.287.0
4
+ version: 0.289.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-19 00:00:00.000000000 Z
11
+ date: 2024-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.287.0
19
+ version: 0.289.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.287.0
26
+ version: 0.289.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -346,7 +346,7 @@ licenses:
346
346
  - MIT
347
347
  metadata:
348
348
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
349
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.287.0
349
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.289.0
350
350
  post_install_message:
351
351
  rdoc_options: []
352
352
  require_paths: