Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 115 additions & 5 deletions lib/react_on_rails/version_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,36 @@ def package_json_location
"Package.json location: #{VersionChecker::NodePackageVersion.package_json_path}"
end

# rubocop:disable Metrics/ClassLength
class NodePackageVersion
attr_reader :package_json
attr_reader :package_json, :yarn_lock, :package_lock

def self.build
new(package_json_path)
new(package_json_path, yarn_lock_path, package_lock_path)
end

def self.package_json_path
Rails.root.join(ReactOnRails.configuration.node_modules_location, "package.json")
end

def initialize(package_json)
def self.yarn_lock_path
# Lockfiles are in the same directory as package.json
# If node_modules_location is empty, use Rails.root
base_dir = ReactOnRails.configuration.node_modules_location.presence || ""
Rails.root.join(base_dir, "yarn.lock").to_s
end

def self.package_lock_path
# Lockfiles are in the same directory as package.json
# If node_modules_location is empty, use Rails.root
base_dir = ReactOnRails.configuration.node_modules_location.presence || ""
Rails.root.join(base_dir, "package-lock.json").to_s
end

def initialize(package_json, yarn_lock = nil, package_lock = nil)
@package_json = package_json
@yarn_lock = yarn_lock
@package_lock = package_lock
end

def raw
Expand All @@ -238,10 +255,16 @@ def raw
deps = parsed["dependencies"]

# Check for react-on-rails-pro first (Pro takes precedence)
return @raw = deps["react-on-rails-pro"] if deps.key?("react-on-rails-pro")
if deps.key?("react-on-rails-pro")
@raw = resolve_version(deps["react-on-rails-pro"], "react-on-rails-pro")
return @raw
end

# Fall back to react-on-rails
return @raw = deps["react-on-rails"] if deps.key?("react-on-rails")
if deps.key?("react-on-rails")
@raw = resolve_version(deps["react-on-rails"], "react-on-rails")
return @raw
end

# Neither package found
msg = "No 'react-on-rails' or 'react-on-rails-pro' entry in the dependencies of " \
Expand Down Expand Up @@ -314,6 +337,92 @@ def parts

private

# Resolve version from lockfiles if available, otherwise use package.json version
def resolve_version(package_json_version, package_name)
# Try yarn.lock first
if yarn_lock && File.exist?(yarn_lock)
lockfile_version = version_from_yarn_lock(package_name)
return lockfile_version if lockfile_version
end

# Try package-lock.json
if package_lock && File.exist?(package_lock)
lockfile_version = version_from_package_lock(package_name)
return lockfile_version if lockfile_version
end

# Fall back to package.json version
package_json_version
end

# Parse version from yarn.lock
# Looks for entries like:
# react-on-rails@^16.1.1:
# version "16.1.1"
# The pattern ensures exact package name match to avoid matching similar names
# (e.g., "react-on-rails" won't match "react-on-rails-pro")
# rubocop:disable Metrics/CyclomaticComplexity
def version_from_yarn_lock(package_name)
return nil unless yarn_lock && File.exist?(yarn_lock)

in_package_block = false
File.foreach(yarn_lock) do |line|
# Check if we're starting the block for our package
# Pattern: optionally quoted package name, followed by @, ensuring it's not followed by more word chars
# This prevents "react-on-rails" from matching "react-on-rails-pro"
if line.match?(/^"?#{Regexp.escape(package_name)}@/)
in_package_block = true
next
end

# If we're in the package block, look for the version line
if in_package_block
# Version line looks like: version "16.1.1"
if (match = line.match(/^\s+version\s+"([^"]+)"/))
return match[1]
end

# If we hit a blank line or new package, we've left the block
break if line.strip.empty? || (line[0] != " " && line[0] != "\t")
end
end

nil
end
# rubocop:enable Metrics/CyclomaticComplexity

# Parse version from package-lock.json
# Supports both v1 (dependencies) and v2/v3 (packages) formats
# rubocop:disable Metrics/CyclomaticComplexity
def version_from_package_lock(package_name)
return nil unless package_lock && File.exist?(package_lock)

begin
parsed = JSON.parse(File.read(package_lock))

# Try v2/v3 format first (packages)
if parsed["packages"]
# Look for node_modules/package-name entry
node_modules_key = "node_modules/#{package_name}"
package_data = parsed["packages"][node_modules_key]
return package_data["version"] if package_data&.key?("version")
end

# Fall back to v1 format (dependencies)
if parsed["dependencies"]
dependency_data = parsed["dependencies"][package_name]
# In v1, the dependency can be a hash with a "version" key
return dependency_data["version"] if dependency_data.is_a?(Hash) && dependency_data.key?("version")
end
rescue JSON::ParserError
# If we can't parse the lockfile, fall back to package.json version
nil
end

nil
end
# rubocop:enable Metrics/CyclomaticComplexity

def package_installed?(package_name)
return false unless File.exist?(package_json)

Expand Down Expand Up @@ -348,5 +457,6 @@ def parsed_package_contents
end
end
end
# rubocop:enable Metrics/ClassLength
end
end
5 changes: 5 additions & 0 deletions spec/react_on_rails/fixtures/malformed_package-lock.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "test-app",
"version": "1.0.0",
"this is not valid JSON because of the trailing comma",
}
5 changes: 5 additions & 0 deletions spec/react_on_rails/fixtures/malformed_yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

This is not a valid yarn.lock format
Just some random text here
27 changes: 27 additions & 0 deletions spec/react_on_rails/fixtures/pro_semver_caret_package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions spec/react_on_rails/fixtures/pro_semver_caret_yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


react-on-rails-pro@^16.1.1:
version "16.1.1"
resolved "https://registry.yarnpkg.com/react-on-rails-pro/-/react-on-rails-pro-16.1.1.tgz"
integrity sha512-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz=
49 changes: 49 additions & 0 deletions spec/react_on_rails/fixtures/semver_caret_package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions spec/react_on_rails/fixtures/semver_caret_package-lock_v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "test-app",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"babel": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/babel/-/babel-6.23.0.tgz",
"integrity": "sha1-0NHn2APpdHZb7qMjLU4VPA77kPQ="
},
"react-on-rails": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/react-on-rails/-/react-on-rails-1.2.3.tgz",
"integrity": "sha512-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz="
},
"webpack": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-1.15.0.tgz",
"integrity": "sha1-v4SbvGJWkYqkKVBKjNJlJQQNqZg="
}
}
}
18 changes: 18 additions & 0 deletions spec/react_on_rails/fixtures/semver_caret_yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


babel@^6.3.26:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel/-/babel-6.23.0.tgz"
integrity sha1-0NHn2APpdHZb7qMjLU4VPA77kPQ=

react-on-rails@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/react-on-rails/-/react-on-rails-1.2.3.tgz"
integrity sha512-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz=

webpack@^1.12.8:
version "1.15.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-1.15.0.tgz"
integrity sha1-v4SbvGJWkYqkKVBKjNJlJQQNqZg=
27 changes: 27 additions & 0 deletions spec/react_on_rails/fixtures/semver_exact_package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions spec/react_on_rails/fixtures/semver_exact_package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": {
"react-on-rails": "16.1.1"
}
}
8 changes: 8 additions & 0 deletions spec/react_on_rails/fixtures/semver_exact_yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


react-on-rails@16.1.1:
version "16.1.1"
resolved "https://registry.yarnpkg.com/react-on-rails/-/react-on-rails-16.1.1.tgz"
integrity sha512-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz=
6 changes: 6 additions & 0 deletions spec/react_on_rails/fixtures/similar_packages_package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": {
"react-on-rails": "^1.2.3",
"react-on-rails-pro": "^16.1.1"
}
}
13 changes: 13 additions & 0 deletions spec/react_on_rails/fixtures/similar_packages_yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


react-on-rails-pro@^16.1.1:
version "16.1.1"
resolved "https://registry.yarnpkg.com/react-on-rails-pro/-/react-on-rails-pro-16.1.1.tgz"
integrity sha512-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz=

react-on-rails@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/react-on-rails/-/react-on-rails-1.2.3.tgz"
integrity sha512-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz=
Loading
Loading