Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ ReactOnRails.configure do |config|
#
config.server_bundle_js_file = "server-bundle.js"

# ⚠️ IMPORTANT: This must match output.path in config/webpack/serverWebpackConfig.js
#
# Both are currently set to 'ssr-generated' (relative to Rails.root)
# Keeping these in sync ensures React on Rails can find the server bundle at runtime.
#
# Configure where server bundles are output. Defaults to "ssr-generated".
# This should match your webpack configuration for server bundles.
# This path is relative to Rails.root and should point to a private directory
# (outside of public/) for security.
config.server_bundle_output_path = "ssr-generated"

# Enforce that server bundles are only loaded from private (non-public) directories.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,15 @@ const configureServer = () => {
};
serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));

// Custom output for the server-bundle that matches the config in
// config/initializers/react_on_rails.rb
// Server bundles are output to a private directory (not public) for security
// Custom output for the server-bundle
// ⚠️ IMPORTANT: This output.path must match server_bundle_output_path in
// config/initializers/react_on_rails.rb
//
// Both are currently set to 'ssr-generated' (relative to Rails.root)
// Keeping these in sync ensures React on Rails can find the server bundle at runtime.
//
// Server bundles are output to a private directory (not public) for security.
// This prevents server-side code from being exposed via the web server.
serverWebpackConfig.output = {
filename: 'server-bundle.js',
globalObject: 'this',
Expand Down
67 changes: 66 additions & 1 deletion lib/react_on_rails/doctor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ def check_react_on_rails_initializer
end
end

# rubocop:disable Metrics/CyclomaticComplexity
def analyze_server_rendering_config(content)
checker.add_info("\n🖥️ Server Rendering:")

Expand All @@ -675,6 +676,18 @@ def analyze_server_rendering_config(content)
checker.add_info(" server_bundle_js_file: server-bundle.js (default)")
end

# Server bundle output path
server_bundle_path_match = content.match(/config\.server_bundle_output_path\s*=\s*["']([^"']+)["']/)
rails_bundle_path = server_bundle_path_match ? server_bundle_path_match[1] : "ssr-generated"
checker.add_info(" server_bundle_output_path: #{rails_bundle_path}")

# Enforce private server bundles
enforce_private_match = content.match(/config\.enforce_private_server_bundles\s*=\s*([^\s\n,]+)/)
checker.add_info(" enforce_private_server_bundles: #{enforce_private_match[1]}") if enforce_private_match

# Validate webpack config matches Rails config
validate_server_bundle_path_sync(rails_bundle_path)

# RSC bundle file (Pro feature)
rsc_bundle_match = content.match(/config\.rsc_bundle_js_file\s*=\s*["']([^"']+)["']/)
if rsc_bundle_match
Expand All @@ -699,7 +712,7 @@ def analyze_server_rendering_config(content)

checker.add_info(" raise_on_prerender_error: #{raise_on_error_match[1]}")
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity

# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
def analyze_performance_config(content)
Expand Down Expand Up @@ -1144,6 +1157,58 @@ def safe_display_config_value(label, config, method_name)
checker.add_info(" #{label}: <error reading value: #{e.message}>")
end
end

# Validates that webpack serverWebpackConfig.js output.path matches
# React on Rails config.server_bundle_output_path
def validate_server_bundle_path_sync(rails_bundle_path)
webpack_config_path = "config/webpack/serverWebpackConfig.js"

unless File.exist?(webpack_config_path)
checker.add_info("\n ℹ️ Webpack server config not found - skipping path validation")
return
end

begin
webpack_content = File.read(webpack_config_path)

# Extract the path from webpack config
# Look for: path: require('path').resolve(__dirname, '../../ssr-generated')
path_regex = %r{path:\s*require\(['"]path['"]\)\.resolve\(__dirname,\s*['"]\.\./\.\./([^'"]+)['"]\)}
path_match = webpack_content.match(path_regex)

unless path_match
checker.add_info("\n ℹ️ Could not parse webpack server bundle path - skipping validation")
return
end

webpack_bundle_path = path_match[1]

# Compare the paths
if webpack_bundle_path == rails_bundle_path
checker.add_success("\n ✅ Webpack and Rails configs are in sync (both use '#{rails_bundle_path}')")
else
checker.add_warning(<<~MSG.strip)
\n ⚠️ Configuration mismatch detected!

React on Rails config (config/initializers/react_on_rails.rb):
server_bundle_output_path = "#{rails_bundle_path}"

Webpack config (#{webpack_config_path}):
output.path = "#{webpack_bundle_path}" (relative to Rails.root)

These must match for server rendering to work correctly.

To fix:
1. Update server_bundle_output_path in config/initializers/react_on_rails.rb, OR
2. Update output.path in #{webpack_config_path}

Make sure both point to the same directory relative to Rails.root.
MSG
end
rescue StandardError => e
checker.add_info("\n ℹ️ Could not validate webpack config: #{e.message}")
end
end
end
# rubocop:enable Metrics/ClassLength
end
Loading