Skip to content

Commit 58a717b

Browse files
justin808claude
andcommitted
Address code review feedback: improve robustness and code quality
Code Review Improvements: - Enhanced auto-detection to warn when user explicitly configures server_bundle_output_path differently from Shakapacker's private_output_path - Improved webpack config template readability by extracting path logic to named constant instead of inline ternary - Added warning for edge case of absolute paths outside Rails.root - Enhanced documentation for shakapacker_version_9_or_higher? method explaining optimistic default behavior and fallback logic - Fixed long line in configuration.md documentation (187 chars -> multi-line) Test Coverage: - Added 5 tests for auto-detection warning functionality - Added 2 tests for absolute path warning in normalize_to_relative_path - All tests pass, zero RuboCop offenses Changes maintain backward compatibility while providing better user guidance through actionable warning messages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 855b452 commit 58a717b

File tree

7 files changed

+156
-18
lines changed

7 files changed

+156
-18
lines changed

docs/api-reference/configuration.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,14 @@ ReactOnRails.configure do |config|
127127
# `enforce_private_server_bundles` to ensure server bundles are only loaded from private directories
128128
# config.server_bundle_output_path = "ssr-generated"
129129

130-
# When set to true, React on Rails will only load server bundles from private, explicitly configured directories (such as `ssr-generated`), and will raise an error if a server bundle is found in a public or untrusted location. This helps prevent accidental or malicious execution of untrusted JavaScript on the server, and is strongly recommended for production environments. And prevent leakage of server-side code to the client (Especially in the case of RSC).
131-
# Default is false for backward compatibility, but enabling this option is a best practice for security.
130+
# When set to true, React on Rails will only load server bundles from private, explicitly
131+
# configured directories (such as `ssr-generated`), and will raise an error if a server
132+
# bundle is found in a public or untrusted location. This helps prevent accidental or
133+
# malicious execution of untrusted JavaScript on the server, and is strongly recommended
134+
# for production environments. Also prevents leakage of server-side code to the client
135+
# (especially important for React Server Components).
136+
# Default is false for backward compatibility, but enabling this option is a best practice
137+
# for security.
132138
config.enforce_private_server_bundles = false
133139

134140
################################################################################

lib/generators/react_on_rails/generator_helper.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ def component_extension(options)
9898

9999
# Check if Shakapacker 9.0 or higher is available
100100
# Returns true if Shakapacker >= 9.0, false otherwise
101+
#
102+
# This method is used during code generation to determine which configuration
103+
# patterns to use in generated files (e.g., config.privateOutputPath vs hardcoded paths).
104+
#
105+
# @return [Boolean] true if Shakapacker 9.0+ is available or likely to be installed
106+
#
107+
# @note Default behavior: Returns true when Shakapacker is not yet installed
108+
# Rationale: During fresh installations, we optimistically assume users will install
109+
# the latest Shakapacker version. This ensures new projects get best-practice configs.
110+
# If users later install an older version, the generated webpack config includes
111+
# fallback logic (e.g., `config.privateOutputPath || hardcodedPath`) that prevents
112+
# breakage, and validation warnings guide them to fix any misconfigurations.
101113
def shakapacker_version_9_or_higher?
102114
return @shakapacker_version_9_or_higher if defined?(@shakapacker_version_9_or_higher)
103115

lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,22 @@ const configureServer = () => {
4848
<% if shakapacker_version_9_or_higher? -%>
4949
// Using Shakapacker 9.0+ privateOutputPath for automatic sync with shakapacker.yml
5050
// This eliminates manual path configuration and keeps configs in sync.
51+
// Falls back to hardcoded path if private_output_path is not configured.
52+
const serverBundleOutputPath = config.privateOutputPath ||
53+
require('path').resolve(__dirname, '../../ssr-generated');
5154
<% else -%>
5255
// Using hardcoded path (Shakapacker < 9.0)
5356
// For Shakapacker 9.0+, consider using config.privateOutputPath instead
5457
// to automatically sync with shakapacker.yml private_output_path.
58+
const serverBundleOutputPath = require('path').resolve(__dirname, '../../ssr-generated');
5559
<% end -%>
60+
5661
serverWebpackConfig.output = {
5762
filename: 'server-bundle.js',
5863
globalObject: 'this',
5964
// If using the React on Rails Pro node server renderer, uncomment the next line
6065
// libraryTarget: 'commonjs2',
61-
path: <%= shakapacker_version_9_or_higher? ? "(config.privateOutputPath != null ? config.privateOutputPath : require('path').resolve(__dirname, '../../ssr-generated'))" : "require('path').resolve(__dirname, '../../ssr-generated')" %>,
66+
path: serverBundleOutputPath,
6267
// No publicPath needed since server bundles are not served via web
6368
// https://webpack.js.org/configuration/output/#outputglobalobject
6469
};
@@ -77,9 +82,8 @@ const configureServer = () => {
7782
// 1. Ensure config/initializers/react_on_rails.rb has: config.server_bundle_output_path = "ssr-generated"
7883
// 2. Run: rails react_on_rails:doctor to verify configuration
7984
const fs = require('fs');
80-
const serverBundlePath = serverWebpackConfig.output.path;
81-
if (!fs.existsSync(serverBundlePath)) {
82-
console.warn(`⚠️ Server bundle output directory does not exist: ${serverBundlePath}`);
85+
if (!fs.existsSync(serverBundleOutputPath)) {
86+
console.warn(`⚠️ Server bundle output directory does not exist: ${serverBundleOutputPath}`);
8387
console.warn(' It will be created during build, but ensure React on Rails is configured:');
8488
console.warn(' config.server_bundle_output_path = "ssr-generated" in config/initializers/react_on_rails.rb');
8589
console.warn(' Run: rails react_on_rails:doctor to validate your configuration');

lib/react_on_rails/configuration.rb

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,36 +260,56 @@ def validate_enforce_private_server_bundles
260260
end
261261

262262
# Auto-detect server_bundle_output_path from Shakapacker 9.0+ private_output_path
263-
# Only sets if user hasn't explicitly configured server_bundle_output_path
263+
# Checks if user explicitly set a value and warns them to use auto-detection instead
264264
def auto_detect_server_bundle_path_from_shakapacker
265-
# Skip if user explicitly set server_bundle_output_path to something other than default
266-
return if server_bundle_output_path != ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
267-
268265
# Skip if Shakapacker is not available
269266
return unless defined?(::Shakapacker)
270267

271268
# Check if Shakapacker config has private_output_path method (9.0+)
272269
return unless ::Shakapacker.config.respond_to?(:private_output_path)
273270

274-
apply_shakapacker_private_output_path
275-
end
276-
277-
def apply_shakapacker_private_output_path
271+
# Get the private_output_path from Shakapacker
278272
private_path = ::Shakapacker.config.private_output_path
279273
return unless private_path
280274

281-
# Convert from Pathname to relative string path
282275
relative_path = ReactOnRails::Utils.normalize_to_relative_path(private_path)
283-
self.server_bundle_output_path = relative_path
284276

285-
Rails.logger&.info("ReactOnRails: Auto-detected server_bundle_output_path from " \
286-
"shakapacker.yml private_output_path: '#{relative_path}'")
277+
# Check if user explicitly configured server_bundle_output_path
278+
if server_bundle_output_path != ReactOnRails::DEFAULT_SERVER_BUNDLE_OUTPUT_PATH
279+
warn_about_explicit_configuration(relative_path)
280+
return
281+
end
282+
283+
apply_shakapacker_private_output_path(relative_path)
287284
rescue StandardError => e
288285
# Fail gracefully - if auto-detection fails, keep the default
289286
Rails.logger&.debug("ReactOnRails: Could not auto-detect server bundle path from " \
290287
"Shakapacker: #{e.message}")
291288
end
292289

290+
def warn_about_explicit_configuration(shakapacker_path)
291+
# Normalize both paths for comparison
292+
normalized_config = server_bundle_output_path.to_s.chomp("/")
293+
normalized_shakapacker = shakapacker_path.to_s.chomp("/")
294+
295+
# Only warn if there's a mismatch
296+
return if normalized_config == normalized_shakapacker
297+
298+
Rails.logger&.warn(
299+
"ReactOnRails: server_bundle_output_path is explicitly set to '#{server_bundle_output_path}' " \
300+
"but shakapacker.yml private_output_path is '#{shakapacker_path}'. " \
301+
"Consider removing server_bundle_output_path from your React on Rails initializer " \
302+
"to use the auto-detected value from shakapacker.yml."
303+
)
304+
end
305+
306+
def apply_shakapacker_private_output_path(relative_path)
307+
self.server_bundle_output_path = relative_path
308+
309+
Rails.logger&.debug("ReactOnRails: Auto-detected server_bundle_output_path from " \
310+
"shakapacker.yml private_output_path: '#{relative_path}'")
311+
end
312+
293313
def check_minimum_shakapacker_version
294314
ReactOnRails::PackerUtils.raise_shakapacker_version_incompatible_for_basic_pack_generation unless
295315
ReactOnRails::PackerUtils.supports_basic_pack_generation?

lib/react_on_rails/utils.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,14 @@ def self.normalize_to_relative_path(path)
479479
path_str.sub(%r{^#{Regexp.escape(rails_root_str)}/?}, "")
480480
else
481481
# Path is already relative or doesn't contain Rails.root
482+
# Warn if it's an absolute path outside Rails.root (edge case)
483+
if path_str.start_with?("/") && !path_str.start_with?(rails_root_str)
484+
Rails.logger&.warn(
485+
"ReactOnRails: Detected absolute path outside Rails.root: '#{path_str}'. " \
486+
"Server bundles are typically stored within Rails.root. " \
487+
"Verify this is intentional."
488+
)
489+
end
482490
path_str
483491
end
484492
end

spec/react_on_rails/configuration_spec.rb

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,82 @@ module ReactOnRails
550550
end
551551
end
552552
end
553+
554+
describe "auto_detect_server_bundle_path_from_shakapacker" do
555+
let(:shakapacker_module) { Module.new }
556+
let(:shakapacker_config) { double("ShakapackerConfig") } # rubocop:disable RSpec/VerifiedDoubles
557+
558+
before do
559+
config = shakapacker_config
560+
stub_const("::Shakapacker", shakapacker_module)
561+
shakapacker_module.define_singleton_method(:config) { config }
562+
allow(shakapacker_config).to receive(:respond_to?).with(:private_output_path).and_return(true)
563+
end
564+
565+
context "when user explicitly set server_bundle_output_path to different value" do
566+
it "warns about configuration mismatch" do
567+
allow(shakapacker_config).to receive(:private_output_path)
568+
.and_return(Pathname.new("/fake/rails/root/shakapacker-bundles"))
569+
570+
expect(Rails.logger).to receive(:warn).with(
571+
/server_bundle_output_path is explicitly set.*shakapacker\.yml private_output_path/
572+
)
573+
574+
ReactOnRails.configure do |config|
575+
config.server_bundle_output_path = "custom-path"
576+
end
577+
end
578+
579+
it "does not warn when paths match after normalization" do
580+
allow(shakapacker_config).to receive(:private_output_path)
581+
.and_return(Pathname.new("/fake/rails/root/ssr-generated"))
582+
583+
expect(Rails.logger).not_to receive(:warn)
584+
585+
ReactOnRails.configure do |config|
586+
config.server_bundle_output_path = "ssr-generated"
587+
end
588+
end
589+
end
590+
591+
context "when user has not explicitly set server_bundle_output_path" do
592+
it "auto-detects from Shakapacker private_output_path" do
593+
allow(shakapacker_config).to receive(:private_output_path)
594+
.and_return(Pathname.new("/fake/rails/root/shakapacker-bundles"))
595+
596+
config = nil
597+
ReactOnRails.configure do |c|
598+
config = c
599+
end
600+
601+
expect(config.server_bundle_output_path).to eq("shakapacker-bundles")
602+
end
603+
604+
it "logs debug message on successful auto-detection" do
605+
allow(shakapacker_config).to receive(:private_output_path)
606+
.and_return(Pathname.new("/fake/rails/root/auto-detected"))
607+
608+
expect(Rails.logger).to receive(:debug).with(
609+
/Auto-detected server_bundle_output_path.*auto-detected/
610+
)
611+
612+
ReactOnRails.configure { |_config| } # rubocop:disable Lint/EmptyBlock
613+
end
614+
end
615+
616+
context "when Shakapacker private_output_path is nil" do
617+
it "keeps default value" do
618+
allow(shakapacker_config).to receive(:private_output_path).and_return(nil)
619+
620+
config = nil
621+
ReactOnRails.configure do |c|
622+
config = c
623+
end
624+
625+
expect(config.server_bundle_output_path).to eq("ssr-generated")
626+
end
627+
end
628+
end
553629
end
554630
end
555631

spec/react_on_rails/utils_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,18 @@ def self.configuration=(config)
966966
expect(described_class.normalize_to_relative_path("/other/path/ssr-generated"))
967967
.to eq("/other/path/ssr-generated")
968968
end
969+
970+
it "logs warning for absolute path outside Rails.root" do
971+
expect(Rails.logger).to receive(:warn).with(
972+
%r{ReactOnRails: Detected absolute path outside Rails\.root: '/other/path/ssr-generated'}
973+
)
974+
described_class.normalize_to_relative_path("/other/path/ssr-generated")
975+
end
976+
977+
it "does not warn for relative paths" do
978+
expect(Rails.logger).not_to receive(:warn)
979+
described_class.normalize_to_relative_path("ssr-generated")
980+
end
969981
end
970982

971983
context "with path containing Rails.root as substring" do

0 commit comments

Comments
 (0)