-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Dynamic version selector for Npm and Yarn (#10510)
* Changes for pnpm and yarn dynamic version selector
- Loading branch information
1 parent
efdfe8e
commit ae11a0e
Showing
14 changed files
with
488 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,20 +2,55 @@ | |
# frozen_string_literal: true | ||
|
||
require "dependabot/shared_helpers" | ||
require "dependabot/npm_and_yarn/version_selector" | ||
|
||
module Dependabot | ||
module NpmAndYarn | ||
class PackageManager | ||
extend T::Sig | ||
extend T::Helpers | ||
def initialize(package_json, lockfiles:) | ||
@package_json = package_json | ||
@lockfiles = lockfiles | ||
@package_manager = package_json.fetch("packageManager", nil) | ||
@engines = package_json.fetch("engines", nil) | ||
end | ||
|
||
# rubocop:disable Metrics/CyclomaticComplexity | ||
# rubocop:disable Metrics/PerceivedComplexity | ||
def setup(name) | ||
return unless @package_manager.nil? || @package_manager.start_with?("#{name}@") | ||
# we prioritize version mentioned in "packageManager" instead of "engines" | ||
# i.e. if { engines : "pnpm" : "6" } and { packageManager: "[email protected]" }, | ||
# we go for the specificity mentioned in packageManager (6.0.2) | ||
|
||
version = requested_version(name) | ||
if Dependabot::Experiments.enabled?(:enable_pnpm_yarn_dynamic_engine) | ||
|
||
unless @package_manager&.start_with?("#{name}@") || (@package_manager&.==name.to_s) || @package_manager.nil? | ||
return | ||
end | ||
|
||
if @engines && @package_manager.nil? | ||
# if "packageManager" doesn't exists in manifest file, | ||
# we check if we can extract "engines" information | ||
Dependabot.logger.info("No \"packageManager\" info found for \"#{name}\"") | ||
version = check_engine_version(name) | ||
|
||
elsif @package_manager&.==name.to_s | ||
# if "packageManager" is found but no version is specified (i.e. [email protected]), | ||
# we check if we can get "engines" info to override default version | ||
Dependabot.logger.info("Found \"packageManager\" : \"#{@package_manager}\"") | ||
version = check_engine_version(name) if @engines | ||
|
||
elsif @package_manager&.start_with?("#{name}@") | ||
# if "packageManager" info has version specification i.e. [email protected] | ||
# we go with the version in "packageManager" | ||
Dependabot.logger.info("Found \"packageManager\" : \"#{@package_manager}\". Skipped checking \"engines\".") | ||
end | ||
else | ||
return unless @package_manager.nil? || @package_manager&.start_with?("#{name}@") | ||
end | ||
|
||
version ||= requested_version(name) | ||
|
||
if version | ||
raise_if_unsupported!(name, version) | ||
|
@@ -33,6 +68,8 @@ def setup(name) | |
|
||
version | ||
end | ||
# rubocop:enable Metrics/CyclomaticComplexity | ||
# rubocop:enable Metrics/PerceivedComplexity | ||
|
||
private | ||
|
||
|
@@ -44,6 +81,8 @@ def raise_if_unsupported!(name, version) | |
end | ||
|
||
def install(name, version) | ||
Dependabot.logger.info("Installing \"#{name}@#{version}\"") | ||
|
||
SharedHelpers.run_shell_command( | ||
"corepack install #{name}@#{version} --global --cache-only", | ||
fingerprint: "corepack install <name>@<version> --global --cache-only" | ||
|
@@ -53,18 +92,35 @@ def install(name, version) | |
def requested_version(name) | ||
return unless @package_manager | ||
|
||
match = @package_manager.match(/#{name}@(?<version>\d+.\d+.\d+)/) | ||
match = @package_manager.match(/^#{name}@(?<version>\d+.\d+.\d+)/) | ||
return unless match | ||
|
||
Dependabot.logger.info("Requested version #{match['version']}") | ||
match["version"] | ||
end | ||
|
||
def guessed_version(name) | ||
lockfile = @lockfiles[name.to_sym] | ||
return unless lockfile | ||
|
||
Dependabot.logger.info("Estimating version") | ||
Helpers.send(:"#{name}_version_numeric", lockfile) | ||
end | ||
|
||
sig { params(name: T.untyped).returns(T.nilable(String)) } | ||
def check_engine_version(name) | ||
version_selector = VersionSelector.new | ||
engine_versions = version_selector.setup(@package_json, name) | ||
|
||
if engine_versions.empty? | ||
Dependabot.logger.info("No relevant (engines) info for \"#{name}\"") | ||
return | ||
end | ||
|
||
version = engine_versions[name] | ||
Dependabot.logger.info("Returned (engines) info \"#{name}\" : \"#{version}\"") | ||
version | ||
end | ||
end | ||
end | ||
end |
45 changes: 45 additions & 0 deletions
45
npm_and_yarn/lib/dependabot/npm_and_yarn/version_selector.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "dependabot/shared_helpers" | ||
|
||
module Dependabot | ||
module NpmAndYarn | ||
class VersionSelector | ||
extend T::Sig | ||
extend T::Helpers | ||
|
||
# For limited testing, allowing only specific versions defined in engines in package.json | ||
# such as "20.8.7", "8.1.2", "8.21.2", | ||
NODE_ENGINE_SUPPORTED_REGEX = /^\d+(?:\.\d+)*$/ | ||
|
||
sig { params(manifest_json: T::Hash[String, T.untyped], name: String).returns(T::Hash[Symbol, T.untyped]) } | ||
def setup(manifest_json, name) | ||
engine_versions = manifest_json["engines"] | ||
|
||
if engine_versions.nil? | ||
Dependabot.logger.info("No info (engines) found") | ||
return {} | ||
end | ||
|
||
# logs entries for analysis purposes | ||
log = engine_versions.select do |engine, _value| | ||
engine.to_s.match(name) | ||
end | ||
Dependabot.logger.info("Found engine info #{log}") unless log.empty? | ||
|
||
# Only keep matching specs versions i.e. "20.21.2", "7.1.2", | ||
# Additional specs can be added later | ||
engine_versions.delete_if { |_key, value| !valid_extracted_version?(value) } | ||
version = engine_versions.select { |engine, _value| engine.to_s.match(name) } | ||
|
||
version | ||
end | ||
|
||
sig { params(version: String).returns(T::Boolean) } | ||
def valid_extracted_version?(version) | ||
version.match?(NODE_ENGINE_SUPPORTED_REGEX) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.