Skip to content

Commit 51ba058

Browse files
justin808claude
andauthored
Add Shakapacker 9.0+ private_output_path integration for server bundles (#2028)
## Summary This PR integrates Shakapacker 9.0+ `private_output_path` configuration with React on Rails, eliminating the need for manual `server_bundle_output_path` configuration and providing automatic synchronization between webpack and Rails configs. ## Key Changes ### Auto-Detection - **New**: Automatically detects `private_output_path` from `shakapacker.yml` (Shakapacker 9.0+) - Falls back to default `"ssr-generated"` if not configured - Gracefully handles older Shakapacker versions ### Generator Updates - **Templates**: Updated to use `private_output_path` for Shakapacker 9.0+ - **Version-aware**: Detects Shakapacker version and generates appropriate config - **Backward compatible**: Generates hardcoded paths for older versions ### Doctor Enhancements - **Detection**: Identifies Shakapacker version and `private_output_path` support - **Validation**: Checks for configuration mismatches - **Recommendations**: Provides upgrade guidance and migration steps - **Clear messaging**: Success/warning/info messages for all scenarios ### Documentation - Updated configuration docs with Shakapacker 9.0+ examples - Added benefits section explaining advantages - Clear migration path from older versions ## Benefits 1. **Single Source of Truth**: Server bundle path configured once in `shakapacker.yml` 2. **Automatic Sync**: No manual coordination between webpack and Rails configs 3. **Better Maintainability**: Reduces configuration duplication 4. **Graceful Degradation**: Works with older Shakapacker versions 5. **Clear Diagnostics**: Doctor command validates configuration ## Testing - ✅ Comprehensive RSpec tests for auto-detection logic - ✅ Tests for path normalization edge cases - ✅ Doctor diagnostic tests for all scenarios - ✅ Generator tests for version-aware templates - ✅ All existing tests pass ## Migration Path For users on Shakapacker 9.0+: 1. Add `private_output_path: ssr-generated` to `config/shakapacker.yml` 2. Remove `config.server_bundle_output_path` from React on Rails initializer (optional - auto-detected) 3. Run `rails react_on_rails:doctor` to verify For users on older Shakapacker: - No changes required - continues to work as before ## Related Supersedes #1967 (rebased for cleaner history) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- Reviewable:start --> - - - This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/shakacode/react_on_rails/2028) <!-- Reviewable:end --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Auto-detection/validation of server bundle locations with new configuration options for private server bundles and SSR behavior. * **Documentation** * Major expansion of configuration docs and guides for server rendering, bundle organization, Shakapacker integration, and migration examples. * **Tools & Diagnostics** * Enhanced doctor diagnostics with targeted guidance when bundle paths or private-output settings mismatch. * **Tests** * Added comprehensive tests for Shakapacker integration and path-normalization scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d5b13e0 commit 51ba058

File tree

13 files changed

+876
-10
lines changed

13 files changed

+876
-10
lines changed

docs/api-reference/configuration.md

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,252 @@ ReactOnRails.configure do |config|
9494
# This controls what command is run to build assets during tests
9595
################################################################################
9696
config.build_test_command = "RAILS_ENV=test bin/shakapacker"
97+
98+
#
99+
# React Server Components and Streaming SSR are React on Rails Pro features.
100+
# For detailed configuration of RSC and streaming features, see:
101+
# https://github.com/shakacode/react_on_rails/blob/master/react_on_rails_pro/docs/configuration.md
102+
#
103+
# Key Pro configurations (configured in ReactOnRailsPro.configure block):
104+
# - rsc_bundle_js_file: Path to RSC bundle
105+
# - react_client_manifest_file: Client component manifest for RSC
106+
# - react_server_client_manifest_file: Server manifest for RSC
107+
# - enable_rsc_support: Enable React Server Components
108+
#
109+
# See Pro documentation for complete setup instructions.
110+
111+
################################################################################
112+
# SERVER BUNDLE SECURITY AND ORGANIZATION
113+
################################################################################
114+
115+
# ⚠️ RECOMMENDED: Use Shakapacker 9.0+ for Automatic Configuration
116+
#
117+
# For Shakapacker 9.0+, add to config/shakapacker.yml:
118+
# private_output_path: ssr-generated
119+
#
120+
# React on Rails will automatically detect and use this value, eliminating the need
121+
# to configure server_bundle_output_path here. This provides a single source of truth.
122+
#
123+
# For older Shakapacker versions or custom setups, manually configure:
124+
# This configures the directory (relative to the Rails root) where the server bundle will be output.
125+
# By default, this is "ssr-generated". If set to nil, the server bundle will be loaded from the same
126+
# public directory as client bundles. For enhanced security, use this option in conjunction with
127+
# `enforce_private_server_bundles` to ensure server bundles are only loaded from private directories
128+
# config.server_bundle_output_path = "ssr-generated"
129+
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.
138+
config.enforce_private_server_bundles = false
139+
140+
################################################################################
141+
# BUNDLE ORGANIZATION EXAMPLES
142+
################################################################################
143+
#
144+
# This configuration creates a clear separation between client and server assets:
145+
#
146+
# CLIENT BUNDLES (Public, Web-Accessible):
147+
# Location: public/webpack/[environment]/ or public/packs/ (According to your shakapacker.yml configuration)
148+
# Files: application.js, manifest.json, CSS files
149+
# Served by: Web server directly
150+
# Access: ReactOnRails::Utils.public_bundles_full_path
151+
#
152+
# SERVER BUNDLES (Private, Server-Only):
153+
# Location: ssr-generated/ (when server_bundle_output_path configured)
154+
# Files: server-bundle.js, rsc-bundle.js
155+
# Served by: Never served to browsers
156+
# Access: ReactOnRails::Utils.server_bundle_js_file_path
157+
#
158+
# Example directory structure with recommended configuration:
159+
# app/
160+
# ├── ssr-generated/ # Private server bundles
161+
# │ ├── server-bundle.js
162+
# │ └── rsc-bundle.js
163+
# └── public/
164+
# └── webpack/development/ # Public client bundles
165+
# ├── application.js
166+
# ├── manifest.json
167+
# └── styles.css
168+
#
169+
################################################################################
170+
171+
# `prerender` means server-side rendering
172+
# default is false. This is an option for view helpers `render_component` and `render_component_hash`.
173+
# Set to true to change the default value to true.
174+
config.prerender = false
175+
176+
# THE BELOW OPTIONS FOR SERVER-SIDE RENDERING RARELY NEED CHANGING
177+
#
178+
# This value only affects server-side rendering when using the webpack-dev-server
179+
# If you are hashing the server bundle and you want to use the same bundle for client and server,
180+
# you'd set this to `true` so that React on Rails reads the server bundle from the webpack-dev-server.
181+
# Normally, you have different bundles for client and server, thus, the default is false.
182+
# Furthermore, if you are not hashing the server bundle (not in the manifest.json), then React on Rails
183+
# will only look for the server bundle to be created in the typical file location, typically by
184+
# a `shakapacker --watch` process.
185+
# If true, ensure that in config/shakapacker.yml that you have both dev_server.hmr and
186+
# dev_server.inline set to false.
187+
config.same_bundle_for_client_and_server = false
188+
189+
# If set to true, this forces Rails to reload the server bundle if it is modified
190+
# Default value is Rails.env.development?
191+
# You probably will never change this.
192+
config.development_mode = Rails.env.development?
193+
194+
# For server rendering so that the server-side console replays in the browser console.
195+
# This can be set to false so that server side messages are not displayed in the browser.
196+
# Default is true. Be cautious about turning this off, as it can make debugging difficult.
197+
# Default value is true
198+
config.replay_console = true
199+
200+
# Default is true. Logs server rendering messages to Rails.logger.info. If false, you'll only
201+
# see the server rendering messages in the browser console.
202+
config.logging_on_server = true
203+
204+
# Default is true only for development? to raise exception on server if the JS code throws for
205+
# server rendering. The reason is that the server logs will show the error and force you to fix
206+
# any server rendering issues immediately during development.
207+
config.raise_on_prerender_error = Rails.env.development?
208+
209+
# This configuration allows logic to be applied to client rendered props, such as stripping props that are only used during server rendering.
210+
# Add a module with an adjust_props_for_client_side_hydration method that expects the component's name & props hash
211+
# See below for an example definition of RenderingPropsExtension
212+
config.rendering_props_extension = RenderingPropsExtension
213+
214+
################################################################################
215+
# Server Renderer Configuration for ExecJS
216+
################################################################################
217+
# The default server rendering is ExecJS, by default using Node.js runtime
218+
# If you wish to use an alternative Node server rendering for higher performance,
219+
# contact justin@shakacode.com for details.
220+
#
221+
# For ExecJS:
222+
# You can configure your pool of JS virtual machines and specify where it should load code:
223+
# On MRI, use `node.js` runtime for the best performance
224+
# (see https://github.com/shakacode/react_on_rails/issues/1438)
225+
# Also see https://github.com/shakacode/react_on_rails/issues/1457#issuecomment-1165026717 if using `mini_racer`
226+
# On MRI, you'll get a deadlock with `pool_size` > 1
227+
# If you're using JRuby, you can increase `pool_size` to have real multi-threaded rendering.
228+
config.server_renderer_pool_size = 1 # increase if you're on JRuby
229+
config.server_renderer_timeout = 20 # seconds
230+
231+
################################################################################
232+
################################################################################
233+
# FILE SYSTEM BASED COMPONENT REGISTRY
234+
# `render_component` and `render_component_hash` view helper methods can
235+
# auto-load the bundle for the generated component, to avoid having to specify the
236+
# bundle manually for each view with the component.
237+
#
238+
# SHAKAPACKER VERSION REQUIREMENTS:
239+
# - Basic pack generation: Shakapacker 6.5.1+
240+
# - Advanced auto-registration with nested entries: Shakapacker 7.0.0+
241+
# - Async loading support: Shakapacker 8.2.0+
242+
#
243+
# Feature Compatibility Matrix:
244+
# | Shakapacker Version | Basic Pack Generation | Auto-Registration | Nested Entries | Async Loading |
245+
# |-------------------|----------------------|-------------------|----------------|---------------|
246+
# | 6.5.1 - 6.9.x | ✅ Yes | ❌ No | ❌ No | ❌ No |
247+
# | 7.0.0 - 8.1.x | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No |
248+
# | 8.2.0+ | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
249+
#
250+
################################################################################
251+
# components_subdirectory is the name of the subdirectory matched to detect and register components automatically
252+
# The default is nil. You can enable the feature by updating it in the next line.
253+
config.components_subdirectory = nil
254+
# Change to a value like this example to enable this feature
255+
# config.components_subdirectory = "ror_components"
256+
257+
# Default is false.
258+
# The default can be overridden as an option in calls to view helpers
259+
# `render_component` and `render_component_hash`. You may set to true to change the default to auto loading.
260+
# NOTE: Requires Shakapacker 6.5.1+ for basic functionality, 7.0.0+ for full auto-registration features.
261+
# See version requirements matrix above for complete feature compatibility.
262+
config.auto_load_bundle = false
263+
264+
# Default is false
265+
# Set this to true & instead of trying to import the generated server components into your existing
266+
# server bundle entrypoint, the PacksGenerator will create a server bundle entrypoint using
267+
# config.server_bundle_js_file for the filename.
268+
config.make_generated_server_bundle_the_entrypoint = false
269+
270+
# Configuration for how generated component packs are loaded.
271+
# Options: :sync, :async, :defer
272+
# - :sync (default for Shakapacker < 8.2.0): Loads scripts synchronously
273+
# - :async (default for Shakapacker ≥ 8.2.0): Loads scripts asynchronously for better performance
274+
# - :defer: Defers script execution until after page load
275+
config.generated_component_packs_loading_strategy = :async
276+
277+
# DEPRECATED: Use `generated_component_packs_loading_strategy` instead.
278+
# Migration: `defer_generated_component_packs: true` → `generated_component_packs_loading_strategy: :defer`
279+
# Migration: `defer_generated_component_packs: false` → `generated_component_packs_loading_strategy: :sync`
280+
# See [16.0.0 Release Notes](docs/release-notes/16.0.0.md) for more details.
281+
# config.defer_generated_component_packs = false
282+
283+
################################################################################
284+
# DEPRECATED CONFIGURATION
285+
################################################################################
286+
# 🚫 DEPRECATED: immediate_hydration is no longer used
287+
#
288+
# This configuration option has been removed. Immediate hydration is now
289+
# automatically enabled for React on Rails Pro users and cannot be disabled.
290+
#
291+
# If you still have this in your config, it will log a deprecation warning:
292+
# config.immediate_hydration = false # ⚠️ Logs warning, has no effect
293+
#
294+
# Action Required: Remove this line from your config/initializers/react_on_rails.rb
295+
# See CHANGELOG.md for migration instructions.
296+
#
297+
# Historical Context:
298+
# Previously controlled whether Pro components hydrated immediately upon their
299+
# server-rendered HTML reaching the client, vs waiting for full page load.
300+
301+
################################################################################
302+
# I18N OPTIONS
303+
################################################################################
304+
# Replace the following line to the location where you keep translation.js & default.js for use
305+
# by the npm packages react-intl. Be sure this directory exists!
306+
# config.i18n_dir = Rails.root.join("client", "app", "libs", "i18n")
307+
#
308+
# If not using the i18n feature, then leave this section commented out or set the value
309+
# of config.i18n_dir to nil.
310+
#
311+
# Replace the following line to the location where you keep your client i18n yml files
312+
# that will source for automatic generation on translations.js & default.js
313+
# By default(without this option) all yaml files from Rails.root.join("config", "locales")
314+
# and installed gems are loaded
315+
config.i18n_yml_dir = Rails.root.join("config", "locales")
316+
317+
# Possible output formats are js and json
318+
# The default format is json
319+
config.i18n_output_format = 'json'
320+
321+
# Possible YAML.safe_load options pass-through for locales
322+
# config.i18n_yml_safe_load_options = { permitted_classes: [Symbol] }
323+
324+
################################################################################
325+
################################################################################
326+
# TEST CONFIGURATION OPTIONS
327+
# Below options are used with the use of this test helper:
328+
# ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
329+
#
330+
# NOTE:
331+
# Instead of using this test helper, you may ensure fresh test files using Shakapacker via:
332+
# 1. Have `config/webpack/test.js` exporting an array of objects to configure both client and server bundles.
333+
# 2. Set the compile option to true in config/shakapacker.yml for env test
334+
################################################################################
335+
336+
# If you are using this in your spec_helper.rb (or rails_helper.rb):
337+
#
338+
# ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)
339+
#
340+
# with rspec then this controls what yarn command is run
341+
# to automatically refresh your Webpack assets on every test run.
342+
#
97343
end
98344
```
99345

docs/core-concepts/webpack-configuration.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,38 @@ default: &default
7878
7979
The `bin/switch-bundler` script automatically updates this configuration when switching bundlers.
8080

81+
### Server Bundle Configuration (Shakapacker 9.0+)
82+
83+
**Recommended**: For Shakapacker 9.0+, use `private_output_path` in `shakapacker.yml` for server bundles:
84+
85+
```yaml
86+
default: &default # ... other config ...
87+
private_output_path: ssr-generated
88+
```
89+
90+
This provides a single source of truth for server bundle location. React on Rails automatically detects this configuration, eliminating the need to set `server_bundle_output_path` in your React on Rails initializer.
91+
92+
In your `config/webpack/serverWebpackConfig.js`:
93+
94+
```javascript
95+
const { config } = require('shakapacker');
96+
97+
serverWebpackConfig.output = {
98+
filename: 'server-bundle.js',
99+
globalObject: 'this',
100+
path: config.privateOutputPath, // Automatically uses shakapacker.yml value
101+
};
102+
```
103+
104+
**Benefits:**
105+
106+
- Single source of truth in `shakapacker.yml`
107+
- Automatic synchronization between webpack and React on Rails
108+
- No configuration duplication
109+
- Better maintainability
110+
111+
**For older Shakapacker versions:** Use hardcoded paths and manual configuration as shown in the generator templates.
112+
81113
Per the example repo [shakacode/react_on_rails_demo_ssr_hmr](https://github.com/shakacode/react_on_rails_demo_ssr_hmr),
82114
you should consider keeping your codebase mostly consistent with the defaults for [Shakapacker](https://github.com/shakacode/shakapacker).
83115

lib/generators/react_on_rails/base_generator.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ def copy_packer_config
101101
puts "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
102102
base_path = "base/base/"
103103
config = "config/shakapacker.yml"
104-
copy_file("#{base_path}#{config}", config)
104+
# Use template to enable version-aware configuration
105+
template("#{base_path}#{config}.tt", config)
105106
configure_rspack_in_shakapacker if options.rspack?
106107
end
107108

lib/generators/react_on_rails/generator_helper.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,33 @@ def add_documentation_reference(message, source)
9595
def component_extension(options)
9696
options.typescript? ? "tsx" : "jsx"
9797
end
98+
99+
# Check if Shakapacker 9.0 or higher is available
100+
# 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.
113+
def shakapacker_version_9_or_higher?
114+
return @shakapacker_version_9_or_higher if defined?(@shakapacker_version_9_or_higher)
115+
116+
@shakapacker_version_9_or_higher = begin
117+
# If Shakapacker is not available yet (fresh install), default to true
118+
# since we're likely installing the latest version
119+
return true unless defined?(ReactOnRails::PackerUtils)
120+
121+
ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.0.0")
122+
rescue StandardError
123+
# If we can't determine version, assume latest
124+
true
125+
end
126+
end
98127
end

lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,25 @@ ReactOnRails.configure do |config|
1212
# Set to "" if you're not using server rendering
1313
config.server_bundle_js_file = "server-bundle.js"
1414

15+
# ⚠️ RECOMMENDED: Use Shakapacker 9.0+ private_output_path instead
16+
#
17+
# If using Shakapacker 9.0+, add to config/shakapacker.yml:
18+
# private_output_path: ssr-generated
19+
#
20+
# React on Rails will auto-detect this value, eliminating the need to set it here.
21+
# This keeps your webpack and Rails configs in sync automatically.
22+
#
23+
# For older Shakapacker versions or custom setups, manually configure:
24+
# config.server_bundle_output_path = "ssr-generated"
25+
#
26+
# The path is relative to Rails.root and should point to a private directory
27+
# (outside of public/) for security. Run 'rails react_on_rails:doctor' to verify.
28+
29+
# Enforce that server bundles are only loaded from private (non-public) directories.
30+
# When true, server bundles will only be loaded from the configured server_bundle_output_path.
31+
# This is recommended for production to prevent server-side code from being exposed.
32+
config.enforce_private_server_bundles = true
33+
1534
################################################################################
1635
# Test Configuration (Optional)
1736
################################################################################

lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml renamed to lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml.tt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ default: &default
2929
# Location for manifest.json, defaults to {public_output_path}/manifest.json if unset
3030
# manifest_path: public/packs/manifest.json
3131

32+
# Location for private server-side bundles (e.g., for SSR)
33+
# These bundles are not served publicly, unlike public_output_path
34+
# Shakapacker 9.0+ feature - automatically detected by React on Rails
35+
<% if shakapacker_version_9_or_higher? -%>
36+
private_output_path: ssr-generated
37+
<% else -%>
38+
# private_output_path: ssr-generated # Uncomment to enable (requires Shakapacker 9.0+)
39+
<% end -%>
40+
3241
# Additional paths webpack should look up modules
3342
# ['app/assets', 'engine/foo/app/assets']
3443
additional_paths: []

0 commit comments

Comments
 (0)