dependabot-npm_and_yarn 0.350.0 → 0.351.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: 3c95e5a25dfbc888b7cbe14a06c8545a1dffa0223254983a4fea67b8e372a024
4
- data.tar.gz: a47db5408d030fbc18d62679d22e3ace24b5f85cbb8de6ae2d153fd46a2c2c9a
3
+ metadata.gz: 58fb5e0f894d8303fda1c4ee5a250a5153a209047fb47b99b827a042bdedcc43
4
+ data.tar.gz: 4f7dbc1168c7672fa786fbdb253def7cd208064a2722ce63e94b387ab306d117
5
5
  SHA512:
6
- metadata.gz: 14d440ca0f12543d02ac0b1d49b6f943c88fec8abb6f96851dfbcb26db1f6737360acc8747f5cd2ee17eefcea34fc57b3e3ef04f6c84999527ef3516c56a3d2d
7
- data.tar.gz: 64b4cd22bcbe18bc9d82231bce93291cce5dc75ebf43d9c8a40d5ee28f44b9cd2a1b8784b9dd919e5e72e1243bffe5f07bd471c9405d1dda1667ad30be3b2002
6
+ metadata.gz: a7a6a0aeb758960adc54fca50c16142de988e0feed5b719c6b1edd34af72469e7829d906016e6f020a5190e9b50288df1eb4d2cc0745197b11bc4da8e4dca6f5
7
+ data.tar.gz: ae780bf718e902b6a6c46658727ac4bdd7b94271474f11014450d4288abc05613fdb927d329b3e20176fd6fb8b322453bf7fd36132ad7f62fc815f7b17264bd3
@@ -261,9 +261,16 @@ module Dependabot
261
261
 
262
262
  full_version = Regexp.last_match(1)
263
263
 
264
- # Normalize version: if full_version does not have patch version, add ".0"
264
+ # Normalize version: ensure it has major.minor.patch format
265
265
  version_parts = T.must(full_version).split(".")
266
- full_version = "#{full_version}.0" if version_parts.length == 2
266
+ full_version = case version_parts.length
267
+ when 1
268
+ "#{full_version}.0.0" # major only -> major.0.0
269
+ when 2
270
+ "#{full_version}.0" # major.minor -> major.minor.0
271
+ else
272
+ full_version # already major.minor.patch
273
+ end
267
274
 
268
275
  _, major, minor = version_components(full_version)
269
276
  return nil if major.nil?
@@ -0,0 +1,214 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/dependency_file"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/npm_and_yarn/helpers"
9
+ require "dependabot/npm_and_yarn/package_manager"
10
+ require "dependabot/npm_and_yarn/file_updater/npmrc_builder"
11
+
12
+ module Dependabot
13
+ module NpmAndYarn
14
+ class DependencyGrapher < Dependabot::DependencyGraphers::Base
15
+ class LockfileGenerator
16
+ extend T::Sig
17
+
18
+ sig do
19
+ params(
20
+ dependency_files: T::Array[Dependabot::DependencyFile],
21
+ package_manager: String,
22
+ credentials: T::Array[Dependabot::Credential]
23
+ ).void
24
+ end
25
+ def initialize(dependency_files:, package_manager:, credentials:)
26
+ @dependency_files = dependency_files
27
+ @package_manager = package_manager
28
+ @credentials = credentials
29
+ end
30
+
31
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
32
+ def generate
33
+ SharedHelpers.in_a_temporary_directory do
34
+ write_temporary_files
35
+ run_lockfile_generation
36
+ read_generated_lockfile
37
+ end
38
+ rescue SharedHelpers::HelperSubprocessFailed => e
39
+ handle_generation_error(e)
40
+ nil
41
+ end
42
+
43
+ private
44
+
45
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
46
+ attr_reader :dependency_files
47
+
48
+ sig { returns(String) }
49
+ attr_reader :package_manager
50
+
51
+ sig { returns(T::Array[Dependabot::Credential]) }
52
+ attr_reader :credentials
53
+
54
+ sig { void }
55
+ def write_temporary_files
56
+ write_package_files
57
+ write_npmrc
58
+ write_yarnrc if yarn?
59
+ end
60
+
61
+ sig { void }
62
+ def write_package_files
63
+ dependency_files.each do |file|
64
+ next unless file.name.end_with?(
65
+ "package.json", ".npmrc", ".yarnrc", ".yarnrc.yml", "pnpm-workspace.yaml"
66
+ )
67
+
68
+ path = file.name
69
+ FileUtils.mkdir_p(File.dirname(path))
70
+ File.write(path, file.content)
71
+ end
72
+ end
73
+
74
+ sig { void }
75
+ def write_npmrc
76
+ # Skip if .npmrc already exists in dependency files (already written above)
77
+ return if dependency_files.any? { |f| f.name.end_with?(".npmrc") }
78
+
79
+ # Use NpmrcBuilder to generate npmrc content from credentials
80
+ npmrc_content = FileUpdater::NpmrcBuilder.new(
81
+ credentials: credentials,
82
+ dependency_files: dependency_files
83
+ ).npmrc_content
84
+
85
+ return if npmrc_content.empty?
86
+
87
+ File.write(".npmrc", npmrc_content)
88
+ end
89
+
90
+ sig { void }
91
+ def write_yarnrc
92
+ return unless yarn_berry?
93
+
94
+ # For Yarn Berry, set up the environment properly
95
+ Helpers.setup_yarn_berry
96
+ end
97
+
98
+ sig { void }
99
+ def run_lockfile_generation
100
+ Dependabot.logger.info("Generating lockfile using #{package_manager}")
101
+
102
+ case package_manager
103
+ when NpmPackageManager::NAME
104
+ run_npm_lockfile_generation
105
+ when YarnPackageManager::NAME
106
+ run_yarn_lockfile_generation
107
+ when PNPMPackageManager::NAME
108
+ run_pnpm_lockfile_generation
109
+ else
110
+ raise "Unknown package manager: #{package_manager}"
111
+ end
112
+ end
113
+
114
+ sig { void }
115
+ def run_npm_lockfile_generation
116
+ # Use --package-lock-only to generate lockfile without installing node_modules
117
+ # Use --ignore-scripts to prevent running any scripts
118
+ # Use --force to ignore platform checks
119
+ # Use --dry-run false because global .npmrc may have dry-run: true set
120
+ command = "install --package-lock-only --ignore-scripts --force --dry-run false"
121
+ Helpers.run_npm_command(command, fingerprint: command)
122
+ end
123
+
124
+ sig { void }
125
+ def run_yarn_lockfile_generation
126
+ if yarn_berry?
127
+ # Yarn Berry (2+) uses different commands
128
+ Helpers.run_yarn_command("install --mode update-lockfile")
129
+ else
130
+ # Yarn Classic (1.x)
131
+ SharedHelpers.run_shell_command(
132
+ "yarn install --ignore-scripts --frozen-lockfile=false",
133
+ fingerprint: "yarn install --ignore-scripts --frozen-lockfile=false"
134
+ )
135
+ end
136
+ end
137
+
138
+ sig { void }
139
+ def run_pnpm_lockfile_generation
140
+ # pnpm uses --lockfile-only to generate lockfile without installing
141
+ command = "install --lockfile-only --ignore-scripts"
142
+ Helpers.run_pnpm_command(command, fingerprint: command)
143
+ end
144
+
145
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
146
+ def read_generated_lockfile
147
+ lockfile_name = expected_lockfile_name
148
+
149
+ unless File.exist?(lockfile_name)
150
+ Dependabot.logger.warn("Lockfile #{lockfile_name} was not generated")
151
+ return nil
152
+ end
153
+
154
+ content = File.read(lockfile_name)
155
+
156
+ Dependabot::DependencyFile.new(
157
+ name: lockfile_name,
158
+ content: content,
159
+ directory: "/"
160
+ )
161
+ end
162
+
163
+ sig { returns(String) }
164
+ def expected_lockfile_name
165
+ case package_manager
166
+ when NpmPackageManager::NAME
167
+ NpmPackageManager::LOCKFILE_NAME
168
+ when YarnPackageManager::NAME
169
+ YarnPackageManager::LOCKFILE_NAME
170
+ when PNPMPackageManager::NAME
171
+ PNPMPackageManager::LOCKFILE_NAME
172
+ else
173
+ "package-lock.json"
174
+ end
175
+ end
176
+
177
+ sig { returns(T::Boolean) }
178
+ def yarn?
179
+ package_manager == YarnPackageManager::NAME
180
+ end
181
+
182
+ sig { returns(T::Boolean) }
183
+ def yarn_berry?
184
+ return false unless yarn?
185
+
186
+ # Check for .yarnrc.yml which indicates Yarn Berry
187
+ dependency_files.any? { |f| f.name.end_with?(".yarnrc.yml") }
188
+ end
189
+
190
+ sig { params(error: SharedHelpers::HelperSubprocessFailed).void }
191
+ def handle_generation_error(error)
192
+ Dependabot.logger.error(
193
+ "Failed to generate lockfile with #{package_manager}: #{error.message}"
194
+ )
195
+
196
+ # Log more details for debugging
197
+ if error.message.include?("ERESOLVE")
198
+ Dependabot.logger.error(
199
+ "Dependency resolution failed. This may be due to conflicting peer dependencies."
200
+ )
201
+ elsif error.message.include?("ENOTFOUND") || error.message.include?("ETIMEDOUT")
202
+ Dependabot.logger.error(
203
+ "Network error while generating lockfile. Registry may be unreachable."
204
+ )
205
+ elsif error.message.include?("401") || error.message.include?("403")
206
+ Dependabot.logger.error(
207
+ "Authentication error. Check that credentials are configured correctly."
208
+ )
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,174 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/dependency_graphers"
7
+ require "dependabot/dependency_graphers/base"
8
+ require "dependabot/npm_and_yarn/file_parser"
9
+ require "dependabot/npm_and_yarn/package_manager"
10
+ require "dependabot/npm_and_yarn/helpers"
11
+
12
+ module Dependabot
13
+ module NpmAndYarn
14
+ class DependencyGrapher < Dependabot::DependencyGraphers::Base
15
+ extend T::Sig
16
+
17
+ require_relative "dependency_grapher/lockfile_generator"
18
+
19
+ sig { override.returns(Dependabot::DependencyFile) }
20
+ def relevant_dependency_file
21
+ # Prefer lockfile if present, otherwise use package.json
22
+ lockfile || package_json
23
+ end
24
+
25
+ sig { override.void }
26
+ def prepare!
27
+ if lockfile.nil?
28
+ Dependabot.logger.info("No lockfile found, generating ephemeral lockfile for dependency graphing")
29
+ generate_ephemeral_lockfile!
30
+ emit_missing_lockfile_warning!
31
+ end
32
+ super
33
+ end
34
+
35
+ private
36
+
37
+ sig { returns(Dependabot::DependencyFile) }
38
+ def package_json
39
+ return T.must(@package_json) if defined?(@package_json)
40
+
41
+ T.must(
42
+ @package_json = T.let(
43
+ T.must(dependency_files.find { |f| f.name.end_with?(MANIFEST_FILENAME) }),
44
+ T.nilable(Dependabot::DependencyFile)
45
+ )
46
+ )
47
+ end
48
+
49
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
50
+ def lockfile
51
+ return @lockfile if defined?(@lockfile)
52
+
53
+ @lockfile = T.let(
54
+ dependency_files.find do |f|
55
+ f.name.end_with?(
56
+ NpmPackageManager::LOCKFILE_NAME,
57
+ YarnPackageManager::LOCKFILE_NAME,
58
+ PNPMPackageManager::LOCKFILE_NAME
59
+ )
60
+ end,
61
+ T.nilable(Dependabot::DependencyFile)
62
+ )
63
+ end
64
+
65
+ sig { returns(T::Hash[String, T.untyped]) }
66
+ def parsed_package_json
67
+ @parsed_package_json ||= T.let(
68
+ JSON.parse(T.must(package_json.content)),
69
+ T.nilable(T::Hash[String, T.untyped])
70
+ )
71
+ rescue JSON::ParserError
72
+ {}
73
+ end
74
+
75
+ sig { returns(T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)]) }
76
+ def lockfiles_hash
77
+ {
78
+ npm: dependency_files.find { |f| f.name.end_with?(NpmPackageManager::LOCKFILE_NAME) },
79
+ yarn: dependency_files.find { |f| f.name.end_with?(YarnPackageManager::LOCKFILE_NAME) },
80
+ pnpm: dependency_files.find { |f| f.name.end_with?(PNPMPackageManager::LOCKFILE_NAME) }
81
+ }
82
+ end
83
+
84
+ sig { returns(String) }
85
+ def detected_package_manager
86
+ @detected_package_manager ||= T.let(
87
+ PackageManagerDetector.new(
88
+ lockfiles_hash,
89
+ parsed_package_json
90
+ ).detect_package_manager,
91
+ T.nilable(String)
92
+ )
93
+ end
94
+
95
+ sig { void }
96
+ def generate_ephemeral_lockfile!
97
+ generator = LockfileGenerator.new(
98
+ dependency_files: dependency_files,
99
+ package_manager: detected_package_manager,
100
+ credentials: file_parser.credentials
101
+ )
102
+
103
+ ephemeral_lockfile = generator.generate
104
+ return unless ephemeral_lockfile
105
+
106
+ # Inject the ephemeral lockfile into the dependency files
107
+ # so the file parser can use it
108
+ inject_ephemeral_lockfile(ephemeral_lockfile)
109
+
110
+ Dependabot.logger.info(
111
+ "Successfully generated ephemeral #{ephemeral_lockfile.name} for dependency graphing"
112
+ )
113
+ rescue StandardError => e
114
+ Dependabot.logger.warn(
115
+ "Failed to generate ephemeral lockfile: #{e.message}. " \
116
+ "Dependency versions may not be resolved."
117
+ )
118
+ end
119
+
120
+ sig { params(ephemeral_lockfile: Dependabot::DependencyFile).void }
121
+ def inject_ephemeral_lockfile(ephemeral_lockfile)
122
+ dependency_files << ephemeral_lockfile
123
+
124
+ # Clear our cached lockfile reference so it picks up the new one
125
+ @lockfile = T.let(nil, T.nilable(Dependabot::DependencyFile))
126
+
127
+ # Clear the FileParser's memoized lockfile references so it will
128
+ # find the newly injected lockfile when parse is called
129
+ file_parser.instance_variable_set(:@package_lock, nil)
130
+ file_parser.instance_variable_set(:@yarn_lock, nil)
131
+ file_parser.instance_variable_set(:@pnpm_lock, nil)
132
+ # Also clear the lockfile_parser which parses lockfile content
133
+ file_parser.instance_variable_set(:@lockfile_parser, nil)
134
+ # Also clear the package_manager_helper which caches lockfile info
135
+ file_parser.instance_variable_set(:@package_manager_helper, nil)
136
+ end
137
+
138
+ sig { void }
139
+ def emit_missing_lockfile_warning!
140
+ Dependabot.logger.warn(
141
+ "No lockfile was found in this repository. " \
142
+ "Dependabot generated a temporary lockfile to determine exact dependency versions.\n\n" \
143
+ "To ensure consistent builds and security scanning, we recommend:\n" \
144
+ " - Committing your package-lock.json (npm), yarn.lock (yarn), or pnpm-lock.yaml (pnpm)\n" \
145
+ " - Setting up a scheduled Dependabot graph job to periodically scan for changes\n\n" \
146
+ "Without a committed lockfile, resolved dependency versions may change between scans " \
147
+ "due to new package releases."
148
+ )
149
+ end
150
+
151
+ # Fetches subdependencies for a given dependency.
152
+ # For npm/yarn/pnpm, we can extract this from the lockfile parser if available.
153
+ sig { override.params(dependency: Dependabot::Dependency).returns(T::Array[String]) }
154
+ def fetch_subdependencies(dependency)
155
+ # Check if the parser has attached depends_on metadata
156
+ dependency.metadata.fetch(:depends_on, [])
157
+ end
158
+
159
+ sig { override.params(_dependency: Dependabot::Dependency).returns(String) }
160
+ def purl_pkg_for(_dependency)
161
+ "npm"
162
+ end
163
+
164
+ # npm packages use the package name as-is for the purl
165
+ sig { params(dependency: Dependabot::Dependency).returns(String) }
166
+ def purl_name_for(dependency)
167
+ # Handle scoped packages: @scope/name -> %40scope/name (URL encoded @)
168
+ dependency.name.sub(/^@/, "%40")
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ Dependabot::DependencyGraphers.register("npm_and_yarn", Dependabot::NpmAndYarn::DependencyGrapher)
@@ -284,14 +284,18 @@ module Dependabot
284
284
  # TODO: Update the npm 6 updater to use these args as we currently
285
285
  # do the same in the js updater helper, we've kept it separate for
286
286
  # the npm 7 rollout
287
- install_args = top_level_dependencies.map { |dependency| npm_install_args(dependency) }
288
287
 
289
- run_npm_install_lockfile_only(install_args)
288
+ top_level_dependencies.each do |dependency|
289
+ install_args = [npm_install_args(dependency)]
290
+ is_optional = optional_dependency?(dependency)
291
+ run_npm_install_lockfile_only(install_args, has_optional_dependencies: is_optional)
292
+ end
290
293
 
291
294
  unless dependencies_in_current_package_json
292
295
  File.write(T.must(package_json).name, previous_package_json)
293
296
 
294
- run_npm_install_lockfile_only
297
+ # Run final install without specific dependencies
298
+ run_npm_install_lockfile_only([], has_optional_dependencies: false)
295
299
  end
296
300
 
297
301
  { lockfile_basename => File.read(lockfile_basename) }
@@ -339,9 +343,11 @@ module Dependabot
339
343
  # to work around an issue in npm 6, we don't want that here
340
344
  # - `--ignore-scripts` disables prepare and prepack scripts which are
341
345
  # run when installing git dependencies
342
- sig { params(install_args: T::Array[String]).returns(String) }
343
- def run_npm_install_lockfile_only(install_args = [])
344
- command = [
346
+ # - `--save-optional` when updating optional dependencies to ensure they
347
+ # stay in optionalDependencies section and allow version upgrades
348
+ sig { params(install_args: T::Array[String], has_optional_dependencies: T::Boolean).returns(String) }
349
+ def run_npm_install_lockfile_only(install_args = [], has_optional_dependencies: false)
350
+ command_args = [
345
351
  "install",
346
352
  *install_args,
347
353
  "--force",
@@ -349,9 +355,13 @@ module Dependabot
349
355
  "false",
350
356
  "--ignore-scripts",
351
357
  "--package-lock-only"
352
- ].join(" ")
358
+ ]
359
+
360
+ command_args << "--save-optional" if has_optional_dependencies
353
361
 
354
- fingerprint = [
362
+ command = command_args.join(" ")
363
+
364
+ fingerprint_args = [
355
365
  "install",
356
366
  install_args.empty? ? "" : "<install_args>",
357
367
  "--force",
@@ -359,9 +369,31 @@ module Dependabot
359
369
  "false",
360
370
  "--ignore-scripts",
361
371
  "--package-lock-only"
362
- ].join(" ")
372
+ ]
373
+
374
+ fingerprint_args << "--save-optional" if has_optional_dependencies
375
+
376
+ fingerprint = fingerprint_args.join(" ")
377
+
378
+ env = build_registry_env_variables
379
+
380
+ Helpers.run_npm_command(command, fingerprint: fingerprint, env: env)
381
+ end
363
382
 
364
- Helpers.run_npm_command(command, fingerprint: fingerprint)
383
+ sig { returns(T.nilable(T::Hash[String, String])) }
384
+ def build_registry_env_variables
385
+ return nil unless Dependabot::Experiments.enabled?(:enable_private_registry_for_corepack)
386
+
387
+ registry_helper = NpmAndYarn::RegistryHelper.new(
388
+ {
389
+ npmrc: dependency_files.find { |f| f.name.end_with?(".npmrc") },
390
+ yarnrc: dependency_files.find { |f| f.name.end_with?(".yarnrc") && !f.name.end_with?(".yarnrc.yml") },
391
+ yarnrc_yml: dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") }
392
+ },
393
+ credentials
394
+ )
395
+
396
+ registry_helper.find_corepack_env_variables
365
397
  end
366
398
 
367
399
  sig { params(dependency: Dependabot::Dependency).returns(String) }
@@ -408,6 +440,13 @@ module Dependabot
408
440
  end
409
441
  end
410
442
 
443
+ sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
444
+ def optional_dependency?(dependency)
445
+ dependency.requirements.any? do |req|
446
+ req[:groups]&.include?("optionalDependencies")
447
+ end
448
+ end
449
+
411
450
  # rubocop:disable Metrics/AbcSize
412
451
  # rubocop:disable Metrics/CyclomaticComplexity
413
452
  # rubocop:disable Metrics/PerceivedComplexity
@@ -10,6 +10,7 @@ require "dependabot/npm_and_yarn/file_updater"
10
10
  require "dependabot/npm_and_yarn/metadata_finder"
11
11
  require "dependabot/npm_and_yarn/requirement"
12
12
  require "dependabot/npm_and_yarn/version"
13
+ require "dependabot/npm_and_yarn/dependency_grapher"
13
14
 
14
15
  require "dependabot/pull_request_creator/labeler"
15
16
  Dependabot::PullRequestCreator::Labeler
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.350.0
4
+ version: 0.351.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.350.0
18
+ version: 0.351.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.350.0
25
+ version: 0.351.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -313,6 +313,8 @@ files:
313
313
  - lib/dependabot/npm_and_yarn.rb
314
314
  - lib/dependabot/npm_and_yarn/constraint_helper.rb
315
315
  - lib/dependabot/npm_and_yarn/dependency_files_filterer.rb
316
+ - lib/dependabot/npm_and_yarn/dependency_grapher.rb
317
+ - lib/dependabot/npm_and_yarn/dependency_grapher/lockfile_generator.rb
316
318
  - lib/dependabot/npm_and_yarn/file_fetcher.rb
317
319
  - lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb
318
320
  - lib/dependabot/npm_and_yarn/file_parser.rb
@@ -359,7 +361,7 @@ licenses:
359
361
  - MIT
360
362
  metadata:
361
363
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
362
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.350.0
364
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.351.0
363
365
  rdoc_options: []
364
366
  require_paths:
365
367
  - lib