dependabot-npm_and_yarn 0.288.0 → 0.290.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef19e7ca0938baa0bee66d3db0500574f6d3b42b181fef8fe38d36396587a2f4
4
- data.tar.gz: 4905508434d0e5ac9ef7cbe1b71dcc10d466c5932291b8c9abb4473d53d854b1
3
+ metadata.gz: a8a094096ac6049e60970f2435585ab4cde21182030752133351f3b211f83d2b
4
+ data.tar.gz: 78e3e5f474091fa12526c005a4309a65d71420cbfd64d6a835cae7a1d387f031
5
5
  SHA512:
6
- metadata.gz: 9981d5b93d3b36479e9500d54fcfcc2d0de8aed94ac5d382a84b7189d570277348e5d421e77f0fc8fee9d20c18511352a60ce855cdc95c178e74ff94640fc175
7
- data.tar.gz: c8015a50c6732baf3435b7be9ebef4393f312c20d154bf15c516bcad80f8dc39d1fd093c09df6aeb70a59feabe02385dcf9ae15e4b46f3eb6a61916736f04f6a
6
+ metadata.gz: a264a72f6140fae85f8f5d03519381060c44093838cf8828130d30b8489bfe334790106fcf874d2805d62edfe295d2cc7963118e768806da61d879df5ff9863d
7
+ data.tar.gz: 82d5056fa18f3f7592a0471e4ce29d9feebb68108f5793f7e07b7603d63f6bba1294912ad1b56ba8bbfbc3e676ad74adfbda3d5a8446533e63463f88988569d8
@@ -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
 
@@ -206,7 +207,9 @@ module Dependabot
206
207
  @package_manager_helper ||= T.let(
207
208
  PackageManagerHelper.new(
208
209
  parsed_package_json,
209
- lockfiles: lockfiles
210
+ lockfiles,
211
+ registry_config_files,
212
+ credentials
210
213
  ), T.nilable(PackageManagerHelper)
211
214
  )
212
215
  end
@@ -220,6 +223,17 @@ module Dependabot
220
223
  }
221
224
  end
222
225
 
226
+ # Returns the .npmrc, and .yarnrc files for the repository.
227
+ # @return [Hash{Symbol => Dependabot::DependencyFile}]
228
+ sig { returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) }
229
+ def registry_config_files
230
+ {
231
+ npmrc: npmrc,
232
+ yarnrc: yarnrc,
233
+ yarnrc_yml: yarnrc_yml
234
+ }
235
+ end
236
+
223
237
  sig { returns(DependencyFile) }
224
238
  def package_json
225
239
  @package_json ||= T.let(fetch_file_from_host(MANIFEST_FILENAME), T.nilable(DependencyFile))
@@ -244,6 +258,20 @@ module Dependabot
244
258
  return @pnpm_lock if defined?(@pnpm_lock)
245
259
 
246
260
  @pnpm_lock ||= T.let(fetch_file_if_present(PNPMPackageManager::LOCKFILE_NAME), T.nilable(DependencyFile))
261
+
262
+ return @pnpm_lock if @pnpm_lock || directory == "/"
263
+
264
+ # Loop through parent directories looking for a pnpm-lock
265
+ (1..directory.split("/").count).each do |i|
266
+ @pnpm_lock = fetch_file_from_host(("../" * i) + PNPMPackageManager::LOCKFILE_NAME)
267
+ .tap { |f| f.support_file = true }
268
+ break if @pnpm_lock
269
+ rescue Dependabot::DependencyFileNotFound
270
+ # Ignore errors (pnpm_lock.yaml may not be present)
271
+ nil
272
+ end
273
+
274
+ @pnpm_lock
247
275
  end
248
276
 
249
277
  sig { returns(T.nilable(DependencyFile)) }
@@ -655,6 +683,19 @@ module Dependabot
655
683
  rescue JSON::ParserError
656
684
  raise Dependabot::DependencyFileNotParseable, T.must(lerna_json).path
657
685
  end
686
+
687
+ sig { void }
688
+ def create_yarn_cache
689
+ if repo_contents_path.nil?
690
+ Dependabot.logger.info("Repository contents path is nil")
691
+ elsif Dir.exist?(T.must(repo_contents_path))
692
+ Dir.chdir(T.must(repo_contents_path)) do
693
+ FileUtils.mkdir_p(".yarn/cache")
694
+ end
695
+ else
696
+ Dependabot.logger.info("Repository contents path does not exist")
697
+ end
698
+ end
658
699
  end
659
700
  end
660
701
  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
  )
@@ -96,7 +98,9 @@ module Dependabot
96
98
  @package_manager_helper ||= T.let(
97
99
  PackageManagerHelper.new(
98
100
  parsed_package_json,
99
- lockfiles: lockfiles
101
+ lockfiles,
102
+ registry_config_files,
103
+ credentials
100
104
  ), T.nilable(PackageManagerHelper)
101
105
  )
102
106
  end
@@ -110,6 +114,15 @@ module Dependabot
110
114
  }
111
115
  end
112
116
 
117
+ sig { returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) }
118
+ def registry_config_files
119
+ {
120
+ npmrc: npmrc,
121
+ yarnrc: yarnrc,
122
+ yarnrc_yml: yarnrc_yml
123
+ }
124
+ end
125
+
113
126
  sig { returns(T.untyped) }
114
127
  def parsed_package_json
115
128
  JSON.parse(T.must(package_json.content))
@@ -154,6 +167,27 @@ module Dependabot
154
167
  end, T.nilable(Dependabot::DependencyFile))
155
168
  end
156
169
 
170
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
171
+ def npmrc
172
+ @npmrc ||= T.let(dependency_files.find do |f|
173
+ f.name == NpmPackageManager::RC_FILENAME
174
+ end, T.nilable(Dependabot::DependencyFile))
175
+ end
176
+
177
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
178
+ def yarnrc
179
+ @yarnrc ||= T.let(dependency_files.find do |f|
180
+ f.name == YarnPackageManager::RC_FILENAME
181
+ end, T.nilable(Dependabot::DependencyFile))
182
+ end
183
+
184
+ sig { returns(T.nilable(DependencyFile)) }
185
+ def yarnrc_yml
186
+ @yarnrc_yml ||= T.let(dependency_files.find do |f|
187
+ f.name == YarnPackageManager::RC_YML_FILENAME
188
+ end, T.nilable(Dependabot::DependencyFile))
189
+ end
190
+
157
191
  sig { returns(Dependabot::FileParsers::Base::DependencySet) }
158
192
  def manifest_dependencies
159
193
  dependency_set = DependencySet.new
@@ -477,4 +511,4 @@ module Dependabot
477
511
  end
478
512
 
479
513
  Dependabot::FileParsers
480
- .register("npm_and_yarn", Dependabot::NpmAndYarn::FileParser)
514
+ .register(Dependabot::NpmAndYarn::ECOSYSTEM, Dependabot::NpmAndYarn::FileParser)
@@ -9,13 +9,14 @@ require "sorbet-runtime"
9
9
 
10
10
  module Dependabot
11
11
  module NpmAndYarn
12
- module Helpers
12
+ module Helpers # rubocop:disable Metrics/ModuleLength
13
13
  extend T::Sig
14
14
 
15
15
  YARN_PATH_NOT_FOUND =
16
16
  /^.*(?<error>The "yarn-path" option has been set \(in [^)]+\), but the specified location doesn't exist)/
17
17
 
18
18
  # NPM Version Constants
19
+ NPM_V10 = 10
19
20
  NPM_V8 = 8
20
21
  NPM_V6 = 6
21
22
  NPM_DEFAULT_VERSION = NPM_V8
@@ -40,6 +41,10 @@ module Dependabot
40
41
  # Otherwise, we are going to use old versionining npm 6
41
42
  sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) }
42
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
+
43
48
  fallback_version_npm8 = Dependabot::Experiments.enabled?(:npm_fallback_version_above_v6)
44
49
 
45
50
  return npm_version_numeric_npm8_or_higher(lockfile) if fallback_version_npm8
@@ -91,6 +96,36 @@ module Dependabot
91
96
  NPM_DEFAULT_VERSION # Fallback to default npm version if parsing fails
92
97
  end
93
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
+
94
129
  sig { params(yarn_lock: T.nilable(DependencyFile)).returns(Integer) }
95
130
  def self.yarn_version_numeric(yarn_lock)
96
131
  lockfile_content = yarn_lock&.content
@@ -139,6 +174,10 @@ module Dependabot
139
174
  def self.npm8?(package_lock)
140
175
  return true unless package_lock&.content
141
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
+
142
181
  npm_version_numeric(package_lock) == NPM_V8
143
182
  end
144
183
 
@@ -284,6 +323,37 @@ module Dependabot
284
323
  end
285
324
  end
286
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
+ Dependabot.logger.error("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
+
287
357
  # Setup yarn and run a single yarn command returning stdout/stderr
288
358
  sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
289
359
  def self.run_yarn_command(command, fingerprint: nil)
@@ -318,42 +388,105 @@ module Dependabot
318
388
  end
319
389
 
320
390
  # Install the package manager for specified version by using corepack
321
- # and prepare it for use by using corepack
322
- sig { params(name: String, version: String).returns(String) }
323
- def self.install(name, version)
391
+ sig do
392
+ params(
393
+ name: String,
394
+ version: String,
395
+ env: T.nilable(T::Hash[String, String])
396
+ )
397
+ .returns(String)
398
+ end
399
+ def self.install(name, version, env: {})
324
400
  Dependabot.logger.info("Installing \"#{name}@#{version}\"")
325
401
 
326
- package_manager_install(name, version)
327
- package_manager_activate(name, version)
328
- installed_version = package_manager_version(name)
402
+ begin
403
+ # Try to install the specified version
404
+ output = package_manager_install(name, version, env: env)
405
+
406
+ # Confirm success based on the output
407
+ if output.match?(/Adding #{name}@.* to the cache/)
408
+ Dependabot.logger.info("#{name}@#{version} successfully installed.")
329
409
 
330
- Dependabot.logger.info("Installed version of #{name}: #{installed_version}")
410
+ Dependabot.logger.info("Activating currently installed version of #{name}: #{version}")
411
+ package_manager_activate(name, version)
412
+
413
+ else
414
+ Dependabot.logger.error("Corepack installation output unexpected: #{output}")
415
+ fallback_to_local_version(name)
416
+ end
417
+ rescue StandardError => e
418
+ Dependabot.logger.error("Error installing #{name}@#{version}: #{e.message}")
419
+ fallback_to_local_version(name)
420
+ end
421
+
422
+ # Verify the installed version
423
+ installed_version = package_manager_version(name)
331
424
 
332
425
  installed_version
333
426
  end
334
427
 
428
+ # Attempt to activate the local version of the package manager
429
+ sig { params(name: String).void }
430
+ def self.fallback_to_local_version(name)
431
+ Dependabot.logger.info("Falling back to activate the currently installed version of #{name}.")
432
+
433
+ # Fetch the currently installed version directly from the environment
434
+ current_version = local_package_manager_version(name)
435
+ Dependabot.logger.info("Activating currently installed version of #{name}: #{current_version}")
436
+
437
+ # Prepare the existing version
438
+ package_manager_activate(name, current_version)
439
+ end
440
+
335
441
  # Install the package manager for specified version by using corepack
336
- sig { params(name: String, version: String).void }
337
- def self.package_manager_install(name, version)
442
+ sig do
443
+ params(
444
+ name: String,
445
+ version: String,
446
+ env: T.nilable(T::Hash[String, String])
447
+ )
448
+ .returns(String)
449
+ end
450
+ def self.package_manager_install(name, version, env: {})
338
451
  Dependabot::SharedHelpers.run_shell_command(
339
452
  "corepack install #{name}@#{version} --global --cache-only",
340
- fingerprint: "corepack install <name>@<version> --global --cache-only"
453
+ fingerprint: "corepack install <name>@<version> --global --cache-only",
454
+ env: env
341
455
  ).strip
342
456
  end
343
457
 
344
458
  # Prepare the package manager for use by using corepack
345
- sig { params(name: String, version: String).void }
459
+ sig { params(name: String, version: String).returns(String) }
346
460
  def self.package_manager_activate(name, version)
347
461
  Dependabot::SharedHelpers.run_shell_command(
348
462
  "corepack prepare #{name}@#{version} --activate",
349
- fingerprint: "corepack prepare --activate"
463
+ fingerprint: "corepack prepare <name>@<version> --activate"
464
+ ).strip
465
+ end
466
+
467
+ # Fetch the currently installed version of the package manager directly
468
+ # from the system without involving Corepack
469
+ sig { params(name: String).returns(String) }
470
+ def self.local_package_manager_version(name)
471
+ Dependabot::SharedHelpers.run_shell_command(
472
+ "#{name} -v",
473
+ fingerprint: "#{name} -v"
350
474
  ).strip
351
475
  end
352
476
 
353
477
  # Get the version of the package manager by using corepack
354
478
  sig { params(name: String).returns(String) }
355
479
  def self.package_manager_version(name)
356
- package_manager_run_command(name, "-v")
480
+ Dependabot.logger.info("Fetching version for package manager: #{name}")
481
+
482
+ version = package_manager_run_command(name, "-v").strip
483
+
484
+ Dependabot.logger.info("Installed version of #{name}: #{version}")
485
+
486
+ version
487
+ rescue StandardError => e
488
+ Dependabot.logger.error("Error fetching version for package manager #{name}: #{e.message}")
489
+ raise
357
490
  end
358
491
 
359
492
  # Run single command on package manager returning stdout/stderr
@@ -365,11 +498,19 @@ module Dependabot
365
498
  ).returns(String)
366
499
  end
367
500
  def self.package_manager_run_command(name, command, fingerprint: nil)
368
- Dependabot::SharedHelpers.run_shell_command(
369
- "corepack #{name} #{command}",
501
+ full_command = "corepack #{name} #{command}"
502
+
503
+ result = Dependabot::SharedHelpers.run_shell_command(
504
+ full_command,
370
505
  fingerprint: "corepack #{name} #{fingerprint || command}"
371
506
  ).strip
507
+
508
+ result
509
+ rescue StandardError => e
510
+ Dependabot.logger.error("Error running package manager command: #{full_command}, Error: #{e.message}")
511
+ raise
372
512
  end
513
+
373
514
  private_class_method :run_single_yarn_command
374
515
 
375
516
  sig { params(pnpm_lock: DependencyFile).returns(T.nilable(String)) }
@@ -3,14 +3,46 @@
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"
8
+ require "dependabot/npm_and_yarn/registry_helper"
7
9
 
8
10
  module Dependabot
9
11
  module NpmAndYarn
10
12
  ECOSYSTEM = "npm_and_yarn"
11
13
  MANIFEST_FILENAME = "package.json"
12
14
  LERNA_JSON_FILENAME = "lerna.json"
13
- PACKAGE_MANAGER_VERSION_REGEX = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:-(?<pre_release>[a-zA-Z0-9.]+))?(?:\+(?<build>[a-zA-Z0-9.]+))?$/ # rubocop:disable Layout/LineLength
15
+ PACKAGE_MANAGER_VERSION_REGEX = /
16
+ ^ # Start of string
17
+ (?<major>\d+) # Major version (required, numeric)
18
+ \. # Separator between major and minor versions
19
+ (?<minor>\d+) # Minor version (required, numeric)
20
+ \. # Separator between minor and patch versions
21
+ (?<patch>\d+) # Patch version (required, numeric)
22
+ ( # Start pre-release section
23
+ -(?<pre_release>[a-zA-Z0-9.]+) # Pre-release label (optional, alphanumeric or dot-separated)
24
+ )?
25
+ ( # Start build metadata section
26
+ \+(?<build>[a-zA-Z0-9.]+) # Build metadata (optional, alphanumeric or dot-separated)
27
+ )?
28
+ $ # End of string
29
+ /x # Extended mode for readability
30
+
31
+ VALID_REQUIREMENT_CONSTRAINT = /
32
+ ^ # Start of string
33
+ (?<operator>=|>|>=|<|<=|~>|\\^) # Allowed operators
34
+ \s* # Optional whitespace
35
+ (?<major>\d+) # Major version (required)
36
+ (\.(?<minor>\d+))? # Minor version (optional)
37
+ (\.(?<patch>\d+))? # Patch version (optional)
38
+ ( # Start pre-release section
39
+ -(?<pre_release>[a-zA-Z0-9.]+) # Pre-release label (optional)
40
+ )?
41
+ ( # Start build metadata section
42
+ \+(?<build>[a-zA-Z0-9.]+) # Build metadata (optional)
43
+ )?
44
+ $ # End of string
45
+ /x # Extended mode for readability
14
46
 
15
47
  MANIFEST_PACKAGE_MANAGER_KEY = "packageManager"
16
48
  MANIFEST_ENGINES_KEY = "engines"
@@ -26,24 +58,32 @@ module Dependabot
26
58
  NPM_V7 = "7"
27
59
  NPM_V8 = "8"
28
60
  NPM_V9 = "9"
61
+ NPM_V10 = "10"
29
62
 
30
63
  # Keep versions in ascending order
31
64
  SUPPORTED_VERSIONS = T.let([
32
65
  Version.new(NPM_V6),
33
66
  Version.new(NPM_V7),
34
67
  Version.new(NPM_V8),
35
- Version.new(NPM_V9)
68
+ Version.new(NPM_V9),
69
+ Version.new(NPM_V10)
36
70
  ].freeze, T::Array[Dependabot::Version])
37
71
 
38
72
  DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
39
73
 
40
- sig { params(raw_version: String).void }
41
- def initialize(raw_version)
74
+ sig do
75
+ params(
76
+ raw_version: String,
77
+ requirement: T.nilable(Dependabot::NpmAndYarn::Requirement)
78
+ ).void
79
+ end
80
+ def initialize(raw_version, requirement: nil)
42
81
  super(
43
82
  NAME,
44
83
  Version.new(raw_version),
45
84
  DEPRECATED_VERSIONS,
46
- SUPPORTED_VERSIONS
85
+ SUPPORTED_VERSIONS,
86
+ requirement
47
87
  )
48
88
  end
49
89
 
@@ -77,13 +117,19 @@ module Dependabot
77
117
 
78
118
  DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
79
119
 
80
- sig { params(raw_version: String).void }
81
- def initialize(raw_version)
120
+ sig do
121
+ params(
122
+ raw_version: String,
123
+ requirement: T.nilable(Requirement)
124
+ ).void
125
+ end
126
+ def initialize(raw_version, requirement: nil)
82
127
  super(
83
128
  NAME,
84
129
  Version.new(raw_version),
85
130
  DEPRECATED_VERSIONS,
86
- SUPPORTED_VERSIONS
131
+ SUPPORTED_VERSIONS,
132
+ requirement
87
133
  )
88
134
  end
89
135
 
@@ -116,13 +162,19 @@ module Dependabot
116
162
 
117
163
  DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
118
164
 
119
- sig { params(raw_version: String).void }
120
- def initialize(raw_version)
165
+ sig do
166
+ params(
167
+ raw_version: String,
168
+ requirement: T.nilable(Requirement)
169
+ ).void
170
+ end
171
+ def initialize(raw_version, requirement: nil)
121
172
  super(
122
173
  NAME,
123
174
  Version.new(raw_version),
124
175
  DEPRECATED_VERSIONS,
125
- SUPPORTED_VERSIONS
176
+ SUPPORTED_VERSIONS,
177
+ requirement
126
178
  )
127
179
  end
128
180
 
@@ -175,7 +227,20 @@ module Dependabot
175
227
  # Defaults to npm if no package manager is detected
176
228
  sig { returns(String) }
177
229
  def detect_package_manager
178
- name_from_lockfiles || name_from_package_manager_attr || name_from_engines || DEFAULT_PACKAGE_MANAGER
230
+ package_manager = name_from_lockfiles ||
231
+ name_from_package_manager_attr ||
232
+ name_from_engines
233
+
234
+ if package_manager
235
+ Dependabot.logger.info("Detected package manager: #{package_manager}")
236
+ else
237
+ package_manager = DEFAULT_PACKAGE_MANAGER
238
+ Dependabot.logger.info("Default package manager used: #{package_manager}")
239
+ end
240
+ package_manager
241
+ rescue StandardError => e
242
+ Dependabot.logger.error("Error detecting package manager: #{e.message}")
243
+ DEFAULT_PACKAGE_MANAGER
179
244
  end
180
245
 
181
246
  private
@@ -205,6 +270,41 @@ module Dependabot
205
270
  end
206
271
  end
207
272
 
273
+ class Language < Ecosystem::VersionManager
274
+ extend T::Sig
275
+ NAME = "node"
276
+
277
+ SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
278
+
279
+ DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
280
+
281
+ sig do
282
+ params(
283
+ raw_version: T.nilable(String),
284
+ requirement: T.nilable(Requirement)
285
+ ).void
286
+ end
287
+ def initialize(raw_version, requirement: nil)
288
+ super(
289
+ NAME,
290
+ Version.new(raw_version),
291
+ DEPRECATED_VERSIONS,
292
+ SUPPORTED_VERSIONS,
293
+ requirement
294
+ )
295
+ end
296
+
297
+ sig { override.returns(T::Boolean) }
298
+ def deprecated?
299
+ false
300
+ end
301
+
302
+ sig { override.returns(T::Boolean) }
303
+ def unsupported?
304
+ false
305
+ end
306
+ end
307
+
208
308
  class PackageManagerHelper
209
309
  extend T::Sig
210
310
  extend T::Helpers
@@ -212,17 +312,27 @@ module Dependabot
212
312
  sig do
213
313
  params(
214
314
  package_json: T.nilable(T::Hash[String, T.untyped]),
215
- lockfiles: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]
315
+ lockfiles: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)],
316
+ registry_config_files: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)],
317
+ credentials: T.nilable(T::Array[Dependabot::Credential])
216
318
  ).void
217
319
  end
218
- def initialize(package_json, lockfiles:)
320
+ def initialize(package_json, lockfiles, registry_config_files, credentials)
219
321
  @package_json = package_json
220
322
  @lockfiles = lockfiles
323
+ @registry_helper = T.let(
324
+ RegistryHelper.new(registry_config_files, credentials),
325
+ Dependabot::NpmAndYarn::RegistryHelper
326
+ )
221
327
  @package_manager_detector = T.let(PackageManagerDetector.new(lockfiles, package_json), PackageManagerDetector)
222
328
  @manifest_package_manager = T.let(package_json&.fetch(MANIFEST_PACKAGE_MANAGER_KEY, nil), T.nilable(String))
223
329
  @engines = T.let(package_json&.fetch(MANIFEST_ENGINES_KEY, nil), T.nilable(T::Hash[String, T.untyped]))
224
330
 
225
331
  @installed_versions = T.let({}, T::Hash[String, String])
332
+ @registries = T.let({}, T::Hash[String, String])
333
+
334
+ @language = T.let(nil, T.nilable(Ecosystem::VersionManager))
335
+ @language_requirement = T.let(nil, T.nilable(Requirement))
226
336
  end
227
337
 
228
338
  sig { returns(Ecosystem::VersionManager) }
@@ -232,9 +342,53 @@ module Dependabot
232
342
  )
233
343
  end
234
344
 
345
+ sig { returns(Ecosystem::VersionManager) }
346
+ def language
347
+ @language ||= Language.new(
348
+ Helpers.node_version,
349
+ requirement: language_requirement
350
+ )
351
+ end
352
+
353
+ sig { returns(T.nilable(Requirement)) }
354
+ def language_requirement
355
+ @language_requirement ||= find_engine_constraints_as_requirement(Language::NAME)
356
+ end
357
+
358
+ sig { params(name: String).returns(T.nilable(Requirement)) }
359
+ def find_engine_constraints_as_requirement(name)
360
+ Dependabot.logger.info("Processing engine constraints for #{name}")
361
+
362
+ return nil unless @engines.is_a?(Hash) && @engines[name]
363
+
364
+ raw_constraint = @engines[name].to_s.strip
365
+ return nil if raw_constraint.empty?
366
+
367
+ raw_constraints = raw_constraint.split
368
+ constraints = raw_constraints.map do |constraint|
369
+ case constraint
370
+ when /^\d+$/
371
+ ">=#{constraint}.0.0 <#{constraint.to_i + 1}.0.0"
372
+ when /^\d+\.\d+$/
373
+ ">=#{constraint} <#{constraint.split('.').first.to_i + 1}.0.0"
374
+ when /^\d+\.\d+\.\d+$/
375
+ "=#{constraint}"
376
+ else
377
+ Dependabot.logger.warn("Unrecognized constraint format for #{name}: #{constraint}")
378
+ constraint
379
+ end
380
+ end
381
+
382
+ Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
383
+ Requirement.new(constraints)
384
+ rescue StandardError => e
385
+ Dependabot.logger.error("Error processing constraints for #{name}: #{e.message}")
386
+ nil
387
+ end
388
+
235
389
  # rubocop:disable Metrics/CyclomaticComplexity
236
- # rubocop:disable Metrics/PerceivedComplexity
237
390
  # rubocop:disable Metrics/AbcSize
391
+ # rubocop:disable Metrics/PerceivedComplexity
238
392
  sig { params(name: String).returns(T.nilable(T.any(Integer, String))) }
239
393
  def setup(name)
240
394
  # we prioritize version mentioned in "packageManager" instead of "engines"
@@ -292,21 +446,36 @@ module Dependabot
292
446
  end
293
447
  version
294
448
  end
449
+ # rubocop:enable Metrics/CyclomaticComplexity
450
+ # rubocop:enable Metrics/AbcSize
451
+ # rubocop:enable Metrics/PerceivedComplexity
295
452
 
296
453
  sig { params(name: T.nilable(String)).returns(Ecosystem::VersionManager) }
297
454
  def package_manager_by_name(name)
298
- name = ensure_valid_package_manager(name)
455
+ Dependabot.logger.info("Resolving package manager for: #{name || 'default'}")
299
456
 
457
+ name = ensure_valid_package_manager(name)
300
458
  package_manager_class = T.must(PACKAGE_MANAGER_CLASSES[name])
301
459
 
302
460
  installed_version = installed_version(name)
461
+ Dependabot.logger.info("Installed version for #{name}: #{installed_version}")
303
462
 
304
- package_manager_class.new(installed_version)
463
+ package_manager_requirement = find_engine_constraints_as_requirement(name)
464
+ if package_manager_requirement
465
+ Dependabot.logger.info("Version requirement for #{name}: #{package_manager_requirement}")
466
+ else
467
+ Dependabot.logger.info("No version requirement found for #{name}")
468
+ end
469
+
470
+ package_manager_class.new(
471
+ installed_version,
472
+ requirement: package_manager_requirement
473
+ )
474
+ rescue StandardError => e
475
+ Dependabot.logger.error("Error resolving package manager for #{name || 'default'}: #{e.message}")
476
+ raise
305
477
  end
306
478
 
307
- # rubocop:enable Metrics/CyclomaticComplexity
308
- # rubocop:enable Metrics/PerceivedComplexity
309
- # rubocop:enable Metrics/AbcSize
310
479
  # Retrieve the installed version of the package manager by executing
311
480
  # the "corepack <name> -v" command and using the output.
312
481
  # If the output does not match the expected version format (PACKAGE_MANAGER_VERSION_REGEX),
@@ -346,7 +515,12 @@ module Dependabot
346
515
  sig { params(name: String, version: T.nilable(String)).void }
347
516
  def install(name, version)
348
517
  if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn)
349
- return Helpers.install(name, version.to_s)
518
+ env = {}
519
+ if Dependabot::Experiments.enabled?(:enable_private_registry_for_corepack)
520
+ env = @registry_helper.find_corepack_env_variables
521
+ end
522
+ # Use the Helpers.install method to install the package manager
523
+ return Helpers.install(name, version.to_s, env: env)
350
524
  end
351
525
 
352
526
  Dependabot.logger.info("Installing \"#{name}@#{version}\"")
@@ -0,0 +1,188 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+ require "dependabot/dependency_file"
6
+ require "sorbet-runtime"
7
+
8
+ module Dependabot
9
+ module NpmAndYarn
10
+ class RegistryHelper
11
+ extend T::Sig
12
+
13
+ # Keys for configurations
14
+ REGISTRY_KEY = "registry"
15
+ AUTH_KEY = "authToken"
16
+
17
+ # Yarn-specific keys
18
+ NPM_AUTH_TOKEN_KEY_FOR_YARN = "npmAuthToken"
19
+ NPM_SCOPE_KEY_FOR_YARN = "npmScopes"
20
+ NPM_REGISTER_KEY_FOR_YARN = "npmRegistryServer"
21
+
22
+ # Environment variable keys
23
+ COREPACK_NPM_REGISTRY_ENV = "COREPACK_NPM_REGISTRY"
24
+ COREPACK_NPM_TOKEN_ENV = "COREPACK_NPM_TOKEN"
25
+
26
+ sig do
27
+ params(
28
+ registry_config_files: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)],
29
+ credentials: T.nilable(T::Array[Dependabot::Credential])
30
+ ).void
31
+ end
32
+ def initialize(registry_config_files, credentials)
33
+ @registry_config_files = T.let(registry_config_files, T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)])
34
+ @credentials = T.let(credentials, T.nilable(T::Array[Dependabot::Credential]))
35
+ end
36
+
37
+ sig { returns(T::Hash[String, String]) }
38
+ def find_corepack_env_variables
39
+ registry_info = find_registry_and_token
40
+
41
+ env_variables = {}
42
+ env_variables[COREPACK_NPM_REGISTRY_ENV] = registry_info[:registry] if registry_info[:registry]
43
+ env_variables[COREPACK_NPM_TOKEN_ENV] = registry_info[:auth_token] if registry_info[:auth_token]
44
+
45
+ env_variables
46
+ end
47
+
48
+ private
49
+
50
+ sig { returns(T::Hash[Symbol, T.nilable(String)]) }
51
+ def find_registry_and_token
52
+ # Step 1: Check dependabot.yml configuration
53
+ dependabot_config = config_npm_registry_and_token
54
+ return dependabot_config if dependabot_config[:registry]
55
+
56
+ # Step 2: Check .npmrc
57
+ npmrc_config = @registry_config_files[:npmrc]
58
+ npmrc_result = parse_registry_from_npmrc_yarnrc(npmrc_config, "=", "npm")
59
+
60
+ return npmrc_result if npmrc_result[:registry]
61
+
62
+ # Step 3: Check .yarnrc
63
+ yarnrc_config = @registry_config_files[:yarnrc]
64
+ yarnrc_result = parse_registry_from_npmrc_yarnrc(yarnrc_config, " ", "npm")
65
+ return yarnrc_result if yarnrc_result[:registry]
66
+
67
+ # Step 4: Check yarnrc.yml
68
+ yarnrc_yml_config = @registry_config_files[:yarnrc_yml]
69
+ yarnrc_yml_result = parse_npm_from_yarnrc_yml(yarnrc_yml_config)
70
+ return yarnrc_yml_result if yarnrc_yml_result[:registry]
71
+
72
+ # Default values if no registry is found
73
+ {}
74
+ end
75
+
76
+ sig { returns(T::Hash[Symbol, T.nilable(String)]) }
77
+ def config_npm_registry_and_token
78
+ registries = {}
79
+
80
+ return registries unless @credentials&.any?
81
+
82
+ @credentials.each do |cred|
83
+ next unless cred["type"] == "npm_registry" # Skip if not an npm registry
84
+ next unless cred["replaces-base"] # Skip if not a reverse-proxy registry
85
+
86
+ # Set the registry if it's not already set
87
+ registries[:registry] ||= cred["registry"]
88
+
89
+ # Set the token if it's not already set
90
+ registries[:auth_token] ||= cred["token"]
91
+ end
92
+ registries
93
+ end
94
+
95
+ # Find registry and token in .npmrc or .yarnrc file
96
+ sig do
97
+ params(
98
+ file: T.nilable(Dependabot::DependencyFile),
99
+ separator: String
100
+ ).returns(T::Hash[Symbol, T.nilable(String)])
101
+ end
102
+ def parse_npm_from_npm_or_yarn_rc(file, separator = "=")
103
+ parse_registry_from_npmrc_yarnrc(file, separator, NpmPackageManager::NAME)
104
+ end
105
+
106
+ # Find registry and token in .npmrc or .yarnrc file
107
+ sig do
108
+ params(
109
+ file: T.nilable(Dependabot::DependencyFile),
110
+ separator: String,
111
+ scope: T.nilable(String)
112
+ ).returns(T::Hash[Symbol, T.nilable(String)])
113
+ end
114
+ def parse_registry_from_npmrc_yarnrc(file, separator = "=", scope = nil)
115
+ content = file&.content
116
+ return { registry: nil, auth_token: nil } unless content
117
+
118
+ global_registry = T.let(nil, T.nilable(String))
119
+ scoped_registry = T.let(nil, T.nilable(String))
120
+ auth_token = T.let(nil, T.nilable(String))
121
+
122
+ content.split("\n").each do |line|
123
+ # Split using the provided separator
124
+ key, value = line.strip.split(separator, 2)
125
+ next unless key && value
126
+
127
+ # Remove surrounding quotes from keys and values
128
+ cleaned_key = key.strip.gsub(/\A["']|["']\z/, "")
129
+ cleaned_value = value.strip.gsub(/\A["']|["']\z/, "")
130
+
131
+ case cleaned_key
132
+ when "registry"
133
+ # Case 1: Found a global registry
134
+ global_registry = cleaned_value
135
+ when "_authToken"
136
+ # Case 2: Found an auth token
137
+ auth_token = cleaned_value
138
+ else
139
+ # Handle scoped registry if a scope is provided
140
+ scoped_registry = cleaned_value if scope && cleaned_key == "@#{scope}:registry"
141
+ end
142
+ end
143
+
144
+ # Determine the registry to return (global first, fallback to scoped)
145
+ registry = global_registry || scoped_registry
146
+
147
+ { registry: registry, auth_token: auth_token }
148
+ end
149
+
150
+ # rubocop:disable Metrics/PerceivedComplexity
151
+ sig { params(file: T.nilable(Dependabot::DependencyFile)).returns(T::Hash[Symbol, T.nilable(String)]) }
152
+ def parse_npm_from_yarnrc_yml(file)
153
+ content = file&.content
154
+ return { registry: nil, auth_token: nil } unless content
155
+
156
+ result = {}
157
+ yaml_data = safe_load_yaml(content)
158
+
159
+ # Step 1: Extract global registry and auth token
160
+ result[:registry] = yaml_data[NPM_REGISTER_KEY_FOR_YARN] if yaml_data.key?(NPM_REGISTER_KEY_FOR_YARN)
161
+ result[:auth_token] = yaml_data[NPM_AUTH_TOKEN_KEY_FOR_YARN] if yaml_data.key?(NPM_AUTH_TOKEN_KEY_FOR_YARN)
162
+
163
+ # Step 2: Fallback to any scoped registry and auth token if global is missing
164
+ if result[:registry].nil? && yaml_data.key?(NPM_SCOPE_KEY_FOR_YARN)
165
+ yaml_data[NPM_SCOPE_KEY_FOR_YARN].each do |_current_scope, config|
166
+ next unless config.is_a?(Hash)
167
+
168
+ result[:registry] ||= config[NPM_REGISTER_KEY_FOR_YARN]
169
+ result[:auth_token] ||= config[NPM_AUTH_TOKEN_KEY_FOR_YARN]
170
+ end
171
+ end
172
+
173
+ result
174
+ end
175
+ # rubocop:enable Metrics/PerceivedComplexity
176
+
177
+ # Safely loads the YAML content and logs any parsing errors
178
+ sig { params(content: String).returns(T::Hash[String, T.untyped]) }
179
+ def safe_load_yaml(content)
180
+ YAML.safe_load(content, permitted_classes: [Symbol, String]) || {}
181
+ rescue Psych::SyntaxError => e
182
+ # Log the error instead of raising it
183
+ Dependabot.logger.error("YAML parsing error: #{e.message}")
184
+ {}
185
+ end
186
+ end
187
+ end
188
+ end
@@ -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.288.0
4
+ version: 0.290.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-21 00:00:00.000000000 Z
11
+ date: 2024-12-12 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.288.0
19
+ version: 0.290.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.288.0
26
+ version: 0.290.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -326,6 +326,7 @@ files:
326
326
  - lib/dependabot/npm_and_yarn/native_helpers.rb
327
327
  - lib/dependabot/npm_and_yarn/package_manager.rb
328
328
  - lib/dependabot/npm_and_yarn/package_name.rb
329
+ - lib/dependabot/npm_and_yarn/registry_helper.rb
329
330
  - lib/dependabot/npm_and_yarn/registry_parser.rb
330
331
  - lib/dependabot/npm_and_yarn/requirement.rb
331
332
  - lib/dependabot/npm_and_yarn/sub_dependency_files_filterer.rb
@@ -346,8 +347,8 @@ licenses:
346
347
  - MIT
347
348
  metadata:
348
349
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
349
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.288.0
350
- post_install_message:
350
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.290.0
351
+ post_install_message:
351
352
  rdoc_options: []
352
353
  require_paths:
353
354
  - lib
@@ -363,7 +364,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
363
364
  version: 3.1.0
364
365
  requirements: []
365
366
  rubygems_version: 3.5.9
366
- signing_key:
367
+ signing_key:
367
368
  specification_version: 4
368
369
  summary: Provides Dependabot support for Javascript (npm and yarn)
369
370
  test_files: []