Module: Dependabot::NpmAndYarn::Helpers
- Extended by:
- T::Sig
- Defined in:
- lib/dependabot/npm_and_yarn/helpers.rb
Overview
rubocop:disable Metrics/ModuleLength
Constant Summary collapse
- YARN_PATH_NOT_FOUND =
/^.*(?<error>The "yarn-path" option has been set \(in [^)]+\), but the specified location doesn't exist)/- NPM_V10 =
NPM Version Constants
10- NPM_V8 =
8- NPM_V6 =
6- NPM_DEFAULT_VERSION =
NPM_V10- PNPM_V10 =
PNPM Version Constants
10- PNPM_V9 =
9- PNPM_V8 =
8- PNPM_V7 =
7- PNPM_V6 =
6- PNPM_DEFAULT_VERSION =
PNPM_V10- PNPM_FALLBACK_VERSION =
PNPM_V6- YARN_V3 =
YARN Version Constants
3- YARN_V2 =
2- YARN_V1 =
1- YARN_DEFAULT_VERSION =
YARN_V3- YARN_FALLBACK_VERSION =
YARN_V1- SUPPORTED_COREPACK_PACKAGE_MANAGERS =
corepack supported package managers
%w(npm yarn pnpm).freeze
Class Method Summary collapse
- .command_observer(output) ⇒ Object
- .corepack_supported_package_manager?(name) ⇒ Boolean
- .dependencies_with_all_versions_metadata(dependency_set) ⇒ Object
- .fallback_to_local_version(name) ⇒ Object
- .fetch_yarnrc_yml_value(key, default_value) ⇒ Object
- .handle_subprocess_failure(error) ⇒ Object
- .install(name, version, env: {}) ⇒ Object
- .local_package_manager_version(name) ⇒ Object
- .node_version ⇒ Object
- .npm_version_numeric(lockfile) ⇒ Object
- .package_manager_activate(name, version) ⇒ Object
- .package_manager_install(name, version, env: {}) ⇒ Object
- .package_manager_run_command(name, command, fingerprint: nil, output_observer: nil, env: nil) ⇒ Object
- .package_manager_version(name) ⇒ Object
- .parse_npm8?(package_lock) ⇒ Boolean
- .pnpm_lockfile_version(pnpm_lock) ⇒ Object
- .pnpm_version_numeric(pnpm_lock) ⇒ Object
- .run_node_command(command, fingerprint: nil) ⇒ Object
- .run_npm_command(command, fingerprint: command, env: nil) ⇒ Object
- .run_pnpm_command(command, fingerprint: nil) ⇒ Object
- .run_yarn_command(command, fingerprint: nil) ⇒ Object
- .run_yarn_commands(*commands) ⇒ Object
- .setup_yarn_berry ⇒ Object
- .yarn_4_or_higher? ⇒ Boolean
- .yarn_berry?(yarn_lock) ⇒ Boolean
- .yarn_berry_args ⇒ Object
- .yarn_berry_disable_scripts? ⇒ Boolean
- .yarn_berry_skip_build? ⇒ Boolean
- .yarn_major_version ⇒ Object
- .yarn_offline_cache? ⇒ Boolean
- .yarn_version_numeric(yarn_lock) ⇒ Object
- .yarn_zero_install? ⇒ Boolean
Class Method Details
.command_observer(output) ⇒ Object
291 292 293 294 295 296 297 298 299 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 291 def self.command_observer(output) # Observe the output for specific error return {} unless output.include?("npm ERR! ERESOLVE") { gracefully_stop: true, # value must be a String reason: "NPM Resolution Error" } end |
.corepack_supported_package_manager?(name) ⇒ Boolean
532 533 534 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 532 def self.corepack_supported_package_manager?(name) SUPPORTED_COREPACK_PACKAGE_MANAGERS.include?(name) end |
.dependencies_with_all_versions_metadata(dependency_set) ⇒ Object
524 525 526 527 528 529 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 524 def self.(dependency_set) dependency_set.dependencies.map do |dependency| dependency.[:all_versions] = dependency_set.all_versions_for_name(dependency.name) dependency end end |
.fallback_to_local_version(name) ⇒ Object
405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 405 def self.fallback_to_local_version(name) return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name) Dependabot.logger.info("Falling back to activate the currently installed version of #{name}.") # Fetch the currently installed version directly from the environment current_version = local_package_manager_version(name) Dependabot.logger.info("Activating currently installed version of #{name}: #{current_version}") # Prepare the existing version package_manager_activate(name, current_version) end |
.fetch_yarnrc_yml_value(key, default_value) ⇒ Object
115 116 117 118 119 120 121 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 115 def self.fetch_yarnrc_yml_value(key, default_value) if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml")) yarnrc.fetch(key, default_value) else default_value end end |
.handle_subprocess_failure(error) ⇒ Object
169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 169 def self.handle_subprocess_failure(error) = error. if YARN_PATH_NOT_FOUND.match?() error = T.must(T.must(YARN_PATH_NOT_FOUND.match())[:error]).sub(Dir.pwd, ".") raise MisconfiguredTooling.new("Yarn", error) end if .include?("Internal Error") && .include?(".yarnrc.yml") raise MisconfiguredTooling.new("Invalid .yarnrc.yml file", ) end raise end |
.install(name, version, env: {}) ⇒ Object
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 374 def self.install(name, version, env: {}) Dependabot.logger.info("Installing \"#{name}@#{version}\"") begin # Try to install the specified version output = package_manager_install(name, version, env: env) # Confirm success based on the output if output.match?(/Adding #{name}@.* to the cache/) Dependabot.logger.info("#{name}@#{version} successfully installed.") Dependabot.logger.info("Activating currently installed version of #{name}: #{version}") package_manager_activate(name, version) else Dependabot.logger.error("Corepack installation output unexpected: #{output}") fallback_to_local_version(name) end rescue StandardError => e Dependabot.logger.error("Error installing #{name}@#{version}: #{e.message}") fallback_to_local_version(name) end # Verify the installed version installed_version = package_manager_version(name) installed_version end |
.local_package_manager_version(name) ⇒ Object
451 452 453 454 455 456 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 451 def self.local_package_manager_version(name) Dependabot::SharedHelpers.run_shell_command( "#{name} -v", fingerprint: "#{name} -v" ).strip end |
.node_version ⇒ Object
302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 302 def self.node_version version = run_node_command("-v", fingerprint: "-v").strip # Validate the output format (e.g., "v20.18.1" or "20.18.1") if version.match?(/^v?\d+(\.\d+){2}$/) version.strip.delete_prefix("v") # Remove the "v" prefix if present end rescue StandardError => e Dependabot.logger.error("Error retrieving Node.js version: #{e.message}") nil end |
.npm_version_numeric(lockfile) ⇒ Object
44 45 46 47 48 49 50 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 44 def self.npm_version_numeric(lockfile) detected_npm_version = detect_npm_version(lockfile) return NPM_DEFAULT_VERSION if detected_npm_version.nil? || detected_npm_version == NPM_V6 detected_npm_version end |
.package_manager_activate(name, version) ⇒ Object
439 440 441 442 443 444 445 446 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 439 def self.package_manager_activate(name, version) return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name) Dependabot::SharedHelpers.run_shell_command( "corepack prepare #{name}@#{version} --activate", fingerprint: "corepack prepare <name>@<version> --activate" ).strip end |
.package_manager_install(name, version, env: {}) ⇒ Object
427 428 429 430 431 432 433 434 435 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 427 def self.package_manager_install(name, version, env: {}) return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name) Dependabot::SharedHelpers.run_shell_command( "corepack install #{name}@#{version} --global --cache-only", fingerprint: "corepack install <name>@<version> --global --cache-only", env: env ).strip end |
.package_manager_run_command(name, command, fingerprint: nil, output_observer: nil, env: nil) ⇒ Object
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 483 def self.package_manager_run_command( name, command, fingerprint: nil, output_observer: nil, env: nil ) full_command = "corepack #{name} #{command}" fingerprint = "corepack #{name} #{fingerprint || command}" if output_observer return Dependabot::SharedHelpers.run_shell_command( full_command, fingerprint: fingerprint, output_observer: output_observer, env: env ).strip else Dependabot::SharedHelpers.run_shell_command(full_command, fingerprint: fingerprint) end.strip rescue StandardError => e Dependabot.logger.error("Error running package manager command: #{full_command}, Error: #{e.message}") if e..match?(/Response Code.*:.*404.*\(Not Found\)/) && e..include?("The remote server failed to provide the requested resource") raise RegistryError.new(404, "The remote server failed to provide the requested resource") end raise end |
.package_manager_version(name) ⇒ Object
460 461 462 463 464 465 466 467 468 469 470 471 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 460 def self.package_manager_version(name) Dependabot.logger.info("Fetching version for package manager: #{name}") version = package_manager_run_command(name, "-v").strip Dependabot.logger.info("Installed version of #{name}: #{version}") version rescue StandardError => e Dependabot.logger.error("Error fetching version for package manager #{name}: #{e.message}") raise end |
.parse_npm8?(package_lock) ⇒ Boolean
124 125 126 127 128 129 130 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 124 def self.parse_npm8?(package_lock) return true unless package_lock&.content detected_npm = detect_npm_version(package_lock) # For conversion reading properly from npm 6 lockfile we need to check if detected version is npm 6 detected_npm.nil? || detected_npm != NPM_V6 end |
.pnpm_lockfile_version(pnpm_lock) ⇒ Object
516 517 518 519 520 521 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 516 def self.pnpm_lockfile_version(pnpm_lock) match = T.must(pnpm_lock.content).match(/^lockfileVersion: ['"]?(?<version>[\d.]+)/) return match[:version] if match nil end |
.pnpm_version_numeric(pnpm_lock) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 96 def self.pnpm_version_numeric(pnpm_lock) lockfile_content = pnpm_lock&.content return PNPM_DEFAULT_VERSION if !lockfile_content || lockfile_content.strip.empty? pnpm_lockfile_version_str = pnpm_lockfile_version(pnpm_lock) return PNPM_FALLBACK_VERSION unless pnpm_lockfile_version_str pnpm_lockfile_version = pnpm_lockfile_version_str.to_f return PNPM_V10 if pnpm_lockfile_version >= 9.0 return PNPM_V8 if pnpm_lockfile_version >= 6.0 return PNPM_V7 if pnpm_lockfile_version >= 5.4 PNPM_FALLBACK_VERSION end |
.run_node_command(command, fingerprint: nil) ⇒ Object
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 315 def self.run_node_command(command, fingerprint: nil) full_command = "node #{command}" Dependabot.logger.info("Running node command: #{full_command}") result = Dependabot::SharedHelpers.run_shell_command( full_command, fingerprint: "node #{fingerprint || command}" ) Dependabot.logger.info("Command executed successfully: #{full_command}") result rescue StandardError => e Dependabot.logger.error("Error running node command: #{full_command}, Error: #{e.message}") raise end |
.run_npm_command(command, fingerprint: command, env: nil) ⇒ Object
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 269 def self.run_npm_command(command, fingerprint: command, env: nil) if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn) package_manager_run_command( NpmPackageManager::NAME, command, fingerprint: fingerprint, output_observer: ->(output) { command_observer(output) }, env: env ) else Dependabot::SharedHelpers.run_shell_command( "npm #{command}", fingerprint: "npm #{fingerprint}", output_observer: ->(output) { command_observer(output) } ) end end |
.run_pnpm_command(command, fingerprint: nil) ⇒ Object
341 342 343 344 345 346 347 348 349 350 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 341 def self.run_pnpm_command(command, fingerprint: nil) if Dependabot::Experiments.enabled?(:enable_corepack_for_npm_and_yarn) package_manager_run_command(PNPMPackageManager::NAME, command, fingerprint: fingerprint) else Dependabot::SharedHelpers.run_shell_command( "pnpm #{command}", fingerprint: "pnpm #{fingerprint || command}" ) end end |
.run_yarn_command(command, fingerprint: nil) ⇒ Object
334 335 336 337 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 334 def self.run_yarn_command(command, fingerprint: nil) setup_yarn_berry run_single_yarn_command(command, fingerprint: fingerprint) end |
.run_yarn_commands(*commands) ⇒ Object
250 251 252 253 254 255 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 250 def self.run_yarn_commands(*commands) setup_yarn_berry commands.each do |cmd, fingerprint| run_single_yarn_command(cmd, fingerprint: fingerprint) if cmd end end |
.setup_yarn_berry ⇒ Object
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 223 def self.setup_yarn_berry # Always disable immutable installs so yarn's CI detection doesn't prevent updates. run_single_yarn_command("config set enableImmutableInstalls false") # Do not generate a cache if offline cache disabled. Otherwise side effects may confuse further checks run_single_yarn_command("config set enableGlobalCache true") unless yarn_berry_skip_build? # We never want to execute postinstall scripts, either set this config or mode=skip-build must be set run_single_yarn_command("config set enableScripts false") if yarn_berry_disable_scripts? if (http_proxy = ENV.fetch("HTTP_PROXY", false)) run_single_yarn_command("config set httpProxy #{http_proxy}", fingerprint: "config set httpProxy <proxy>") end if (https_proxy = ENV.fetch("HTTPS_PROXY", false)) run_single_yarn_command("config set httpsProxy #{https_proxy}", fingerprint: "config set httpsProxy <proxy>") end return unless (ca_file_path = ENV.fetch("NODE_EXTRA_CA_CERTS", false)) if yarn_4_or_higher? run_single_yarn_command("config set httpsCaFilePath #{ca_file_path}") else run_single_yarn_command("config set caFilePath #{ca_file_path}") end end |
.yarn_4_or_higher? ⇒ Boolean
218 219 220 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 218 def self.yarn_4_or_higher? yarn_major_version >= 4 end |
.yarn_berry?(yarn_lock) ⇒ Boolean
133 134 135 136 137 138 139 140 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 133 def self.yarn_berry?(yarn_lock) return false if yarn_lock.nil? || yarn_lock.content.nil? yaml = YAML.safe_load(T.must(yarn_lock.content)) yaml.key?("__metadata") rescue StandardError false end |
.yarn_berry_args ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 195 def self.yarn_berry_args if yarn_major_version == 2 "" elsif yarn_berry_skip_build? "--mode=skip-build" else # We only want this mode if the cache is not being updated/managed # as this improperly leaves old versions in the cache "--mode=update-lockfile" end end |
.yarn_berry_disable_scripts? ⇒ Boolean
213 214 215 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 213 def self.yarn_berry_disable_scripts? yarn_major_version == YARN_V2 || !yarn_zero_install? end |
.yarn_berry_skip_build? ⇒ Boolean
208 209 210 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 208 def self.yarn_berry_skip_build? yarn_major_version >= YARN_V3 && (yarn_zero_install? || yarn_offline_cache?) end |
.yarn_major_version ⇒ Object
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 143 def self.yarn_major_version retries = 0 output = run_single_yarn_command("--version") Version.new(output).major rescue Dependabot::SharedHelpers::HelperSubprocessFailed => e # Should never happen, can probably be removed once this settles raise "Failed to replace ENV, not sure why" if T.must(retries).positive? = e. missing_env_var_regex = %r{Environment variable not found \((?:[^)]+)\) in #{Dir.pwd}/(?<path>\S+)} if .match?(missing_env_var_regex) match = T.must(.match(missing_env_var_regex)) path = T.must(match.named_captures["path"]) File.write(path, File.read(path).gsub(/\$\{[^}-]+\}/, "")) retries = T.must(retries) + 1 retry end handle_subprocess_failure(e) end |
.yarn_offline_cache? ⇒ Boolean
189 190 191 192 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 189 def self.yarn_offline_cache? yarn_cache_dir = fetch_yarnrc_yml_value("cacheFolder", ".yarn/cache") File.exist?(yarn_cache_dir) && (fetch_yarnrc_yml_value("nodeLinker", "") == "node-modules") end |
.yarn_version_numeric(yarn_lock) ⇒ Object
80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 80 def self.yarn_version_numeric(yarn_lock) lockfile_content = yarn_lock&.content return YARN_DEFAULT_VERSION if lockfile_content.nil? || lockfile_content.strip.empty? if yarn_berry?(yarn_lock) YARN_DEFAULT_VERSION else YARN_FALLBACK_VERSION end end |
.yarn_zero_install? ⇒ Boolean
184 185 186 |
# File 'lib/dependabot/npm_and_yarn/helpers.rb', line 184 def self.yarn_zero_install? File.exist?(".pnp.cjs") end |