Skip to content

Commit bf5681a

Browse files
Add strict version validation for React on Rails packages
This commit implements fail-fast validation for package.json configuration to prevent runtime errors and ensure proper package/gem compatibility. Changes: - Add new validation method that raises errors instead of logging warnings - Validate package.json file exists before checking versions - Detect conflicting package installations (both base and Pro) - Enforce exact version matching (no semver wildcards) - Validate Pro gem/package compatibility - Remove deprecated log_if_gem_and_node_package_versions_differ method Implementation details: - Created validate_version_and_package_compatibility! method with 4 validations: 1. validate_package_json_exists! - Ensures package.json is present 2. validate_package_gem_compatibility! - Checks Pro/base package conflicts 3. validate_exact_version! - Enforces exact versions (no ^, ~, >, <, *) 4. validate_version_match! - Ensures gem and package versions match - Added package detection methods (react_on_rails_package?, react_on_rails_pro_package?) - Applied DRY principle with shared package_installed? helper - Integrated validation into Rails engine initializer (config.after_initialize) - Added comprehensive test coverage (53 tests, all passing) - Included helpful error messages with package.json location Fixes #1876 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 661312e commit bf5681a

File tree

6 files changed

+477
-97
lines changed

6 files changed

+477
-97
lines changed

lib/react_on_rails/engine.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@
44

55
module ReactOnRails
66
class Engine < ::Rails::Engine
7+
# Validate package versions and compatibility on Rails startup
8+
# This ensures the application fails fast if versions don't match or packages are misconfigured
9+
initializer "react_on_rails.validate_version_and_package_compatibility" do
10+
config.after_initialize do
11+
Rails.logger.info "[React on Rails] Validating package version and compatibility..."
12+
VersionChecker.build.validate_version_and_package_compatibility!
13+
Rails.logger.info "[React on Rails] Package validation successful"
14+
end
15+
end
16+
717
config.to_prepare do
8-
VersionChecker.build.log_if_gem_and_node_package_versions_differ
918
ReactOnRails::ServerRenderingPool.reset_pool
1019
end
1120

lib/react_on_rails/version_checker.rb

Lines changed: 173 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,142 @@ def initialize(node_package_version)
1717
@node_package_version = node_package_version
1818
end
1919

20-
# For compatibility, the gem and the node package versions should always match,
21-
# unless the user really knows what they're doing. So we will give a
22-
# warning if they do not.
23-
def log_if_gem_and_node_package_versions_differ
24-
return if node_package_version.raw.nil? || node_package_version.local_path_or_url?
25-
return log_node_semver_version_warning if node_package_version.semver_wildcard?
26-
27-
log_differing_versions_warning unless node_package_version.parts == gem_version_parts
20+
# Validates version and package compatibility.
21+
# Raises ReactOnRails::Error if:
22+
# - package.json file is not found
23+
# - Both react-on-rails and react-on-rails-pro packages are installed
24+
# - Pro gem is installed but using react-on-rails package
25+
# - Pro package is installed but Pro gem is not installed
26+
# - Non-exact version is used
27+
# - Versions don't match
28+
def validate_version_and_package_compatibility!
29+
validate_package_json_exists!
30+
validate_package_gem_compatibility!
31+
validate_exact_version!
32+
validate_version_match!
2833
end
2934

3035
private
3136

32-
def common_error_msg
33-
<<-MSG.strip_heredoc
34-
Detected: #{node_package_version.raw}
35-
gem: #{gem_version}
36-
Ensure the installed version of the gem is the same as the version of
37-
your installed Node package. Do not use >= or ~> in your Gemfile for react_on_rails.
38-
Do not use ^, ~, or other non-exact versions in your package.json for react-on-rails.
39-
Run `yarn add react-on-rails --exact` in the directory containing folder node_modules.
37+
def validate_package_json_exists!
38+
return if File.exist?(node_package_version.package_json)
39+
40+
raise ReactOnRails::Error, <<~MSG.strip
41+
**ERROR** ReactOnRails: package.json file not found.
42+
43+
Expected location: #{node_package_version.package_json}
44+
45+
React on Rails requires a package.json file with either 'react-on-rails' or
46+
'react-on-rails-pro' package installed.
47+
48+
Fix:
49+
1. Ensure you have a package.json in your project root
50+
2. Run: yarn add react-on-rails@#{gem_version} --exact
51+
52+
Or if using React on Rails Pro:
53+
Run: yarn add react-on-rails-pro@#{gem_version} --exact
4054
MSG
4155
end
4256

43-
def log_differing_versions_warning
44-
msg = "**WARNING** ReactOnRails: ReactOnRails gem and Node package versions do not match\n#{common_error_msg}"
45-
Rails.logger.warn(msg)
57+
def validate_package_gem_compatibility!
58+
has_base_package = node_package_version.react_on_rails_package?
59+
has_pro_package = node_package_version.react_on_rails_pro_package?
60+
is_pro_gem = ReactOnRails::Utils.react_on_rails_pro?
61+
62+
# Error: Both packages installed
63+
if has_base_package && has_pro_package
64+
raise ReactOnRails::Error, <<~MSG.strip
65+
**ERROR** ReactOnRails: Both 'react-on-rails' and 'react-on-rails-pro' packages are installed.
66+
67+
If you're using React on Rails Pro, only install the 'react-on-rails-pro' package.
68+
The Pro package already includes all functionality from the base package.
69+
70+
Fix:
71+
1. Remove 'react-on-rails' from your package.json dependencies
72+
2. Run: yarn remove react-on-rails
73+
3. Keep only: react-on-rails-pro
74+
75+
#{package_json_location}
76+
MSG
77+
end
78+
79+
# Error: Pro gem but using base package
80+
if is_pro_gem && !has_pro_package
81+
raise ReactOnRails::Error, <<~MSG.strip
82+
**ERROR** ReactOnRails: You have the Pro gem installed but are using the base 'react-on-rails' package.
83+
84+
When using React on Rails Pro, you must use the 'react-on-rails-pro' npm package.
85+
86+
Fix:
87+
1. Remove the base package: yarn remove react-on-rails
88+
2. Install the Pro package: yarn add react-on-rails-pro@#{gem_version} --exact
89+
90+
#{package_json_location}
91+
MSG
92+
end
93+
94+
# Error: Pro package but not Pro gem
95+
return unless !is_pro_gem && has_pro_package
96+
97+
raise ReactOnRails::Error, <<~MSG.strip
98+
**ERROR** ReactOnRails: You have the 'react-on-rails-pro' package installed but the Pro gem is not installed.
99+
100+
The Pro npm package requires the Pro gem to function.
101+
102+
Fix:
103+
1. Install the Pro gem by adding to your Gemfile:
104+
gem 'react_on_rails_pro'
105+
2. Run: bundle install
106+
107+
Or if you meant to use the base version:
108+
1. Remove the Pro package: yarn remove react-on-rails-pro
109+
2. Install the base package: yarn add react-on-rails@#{gem_version} --exact
110+
111+
#{package_json_location}
112+
MSG
113+
end
114+
115+
def validate_exact_version!
116+
return if node_package_version.raw.nil? || node_package_version.local_path_or_url?
117+
118+
return unless node_package_version.semver_wildcard?
119+
120+
package_name = node_package_version.package_name
121+
raise ReactOnRails::Error, <<~MSG.strip
122+
**ERROR** ReactOnRails: The '#{package_name}' package version is not an exact version.
123+
124+
Detected: #{node_package_version.raw}
125+
Gem: #{gem_version}
126+
127+
React on Rails requires exact version matching between the gem and npm package.
128+
Do not use ^, ~, >, <, *, or other semver ranges.
129+
130+
Fix:
131+
Run: yarn add #{package_name}@#{gem_version} --exact
132+
133+
#{package_json_location}
134+
MSG
46135
end
47136

48-
def log_node_semver_version_warning
49-
msg = "**WARNING** ReactOnRails: Your Node package version for react-on-rails is not an exact version\n" \
50-
"#{common_error_msg}"
51-
Rails.logger.warn(msg)
137+
def validate_version_match!
138+
return if node_package_version.raw.nil? || node_package_version.local_path_or_url?
139+
140+
return if node_package_version.parts == gem_version_parts
141+
142+
package_name = node_package_version.package_name
143+
raise ReactOnRails::Error, <<~MSG.strip
144+
**ERROR** ReactOnRails: The '#{package_name}' package version does not match the gem version.
145+
146+
Package: #{node_package_version.raw}
147+
Gem: #{gem_version}
148+
149+
The npm package and gem versions must match exactly for compatibility.
150+
151+
Fix:
152+
Run: yarn add #{package_name}@#{gem_version} --exact
153+
154+
#{package_json_location}
155+
MSG
52156
end
53157

54158
def gem_version
@@ -59,6 +163,10 @@ def gem_version_parts
59163
gem_version.match(VERSION_PARTS_REGEX)&.captures&.compact
60164
end
61165

166+
def package_json_location
167+
"Package.json location: #{VersionChecker::NodePackageVersion.package_json_path}"
168+
end
169+
62170
class NodePackageVersion
63171
attr_reader :package_json
64172

@@ -77,19 +185,41 @@ def initialize(package_json)
77185
def raw
78186
return @raw if defined?(@raw)
79187

80-
if File.exist?(package_json)
81-
parsed_package_contents = JSON.parse(package_json_contents)
82-
if parsed_package_contents.key?("dependencies") &&
83-
parsed_package_contents["dependencies"].key?("react-on-rails")
84-
return @raw = parsed_package_contents["dependencies"]["react-on-rails"]
85-
end
86-
end
87-
msg = "No 'react-on-rails' entry in the dependencies of #{NodePackageVersion.package_json_path}, " \
88-
"which is the expected location according to ReactOnRails.configuration.node_modules_location"
188+
return @raw = nil unless File.exist?(package_json)
189+
190+
parsed = parsed_package_contents
191+
return @raw = nil unless parsed.key?("dependencies")
192+
193+
deps = parsed["dependencies"]
194+
195+
# Check for react-on-rails-pro first (Pro takes precedence)
196+
return @raw = deps["react-on-rails-pro"] if deps.key?("react-on-rails-pro")
197+
198+
# Fall back to react-on-rails
199+
return @raw = deps["react-on-rails"] if deps.key?("react-on-rails")
200+
201+
# Neither package found
202+
msg = "No 'react-on-rails' or 'react-on-rails-pro' entry in the dependencies of " \
203+
"#{NodePackageVersion.package_json_path}, which is the expected location according to " \
204+
"ReactOnRails.configuration.node_modules_location"
89205
Rails.logger.warn(msg)
90206
@raw = nil
91207
end
92208

209+
def react_on_rails_package?
210+
package_installed?("react-on-rails")
211+
end
212+
213+
def react_on_rails_pro_package?
214+
package_installed?("react-on-rails-pro")
215+
end
216+
217+
def package_name
218+
return "react-on-rails-pro" if react_on_rails_pro_package?
219+
220+
"react-on-rails"
221+
end
222+
93223
def semver_wildcard?
94224
# See https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies
95225
# We want to disallow all expressions other than exact versions
@@ -117,9 +247,20 @@ def parts
117247

118248
private
119249

250+
def package_installed?(package_name)
251+
return false unless File.exist?(package_json)
252+
253+
parsed = parsed_package_contents
254+
parsed.dig("dependencies", package_name).present?
255+
end
256+
120257
def package_json_contents
121258
@package_json_contents ||= File.read(package_json)
122259
end
260+
261+
def parsed_package_contents
262+
@parsed_package_contents ||= JSON.parse(package_json_contents)
263+
end
123264
end
124265
end
125266
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"dependencies": {
3+
"babel": "^6.3.26",
4+
"react-on-rails": "16.1.1",
5+
"react-on-rails-pro": "16.1.1",
6+
"webpack": "^1.12.8"
7+
},
8+
"devDependencies": {
9+
"babel-eslint": "^5.0.0-beta6"
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"dependencies": {
3+
"babel": "^6.3.26",
4+
"react-on-rails-pro": "16.1.1",
5+
"webpack": "^1.12.8"
6+
},
7+
"devDependencies": {
8+
"babel-eslint": "^5.0.0-beta6"
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"dependencies": {
3+
"babel": "^6.3.26",
4+
"react-on-rails-pro": "^16.1.1",
5+
"webpack": "^1.12.8"
6+
},
7+
"devDependencies": {
8+
"babel-eslint": "^5.0.0-beta6"
9+
}
10+
}

0 commit comments

Comments
 (0)