Skip to content

Commit e25fd1f

Browse files
justin808claude
andauthored
refactor: unify renderer cache staging with mode: :copy / :symlink (#3167)
## Summary Collapses `ReactOnRailsPro::PreSeedRendererCache` (copy) and `ReactOnRailsPro::PrepareNodeRenderBundles` (symlink) into a single entry point: `PreSeedRendererCache.call(mode: :copy | :symlink)`. Both modes produce the same \`<cache>/<bundleHash>/<bundleHash>.js\` layout — only the file operation differs. Stacked on #3124 (PR A). Target base is \`jg/3122-preseed-renderer-cache\`; will be rebased onto \`master\` once PR A merges. Refs #3122. ## Changes - **Unified API**: \`PreSeedRendererCache.call(mode: :copy)\` for Docker/image builds (default); \`(mode: :symlink)\` for same-filesystem workflows (local dev, CI, Heroku-style same-dyno deploys, bundle-caching restores). Unknown modes → \`ArgumentError\`. - **Non-dev env-var guard (copy mode only)**: When neither \`RENDERER_SERVER_BUNDLE_CACHE_PATH\` nor \`RENDERER_BUNDLE_PATH\` is set in a non-dev/test environment, \`mode: :copy\` raises a clear error. Motivation: the Node renderer's default cache-path resolution can differ from the Ruby side (falling back to \`/tmp\` when \`cwd\` is outside the app tree), so silent fallback is a misconfig footgun that pre-seeds bundles into a directory the renderer never reads. - **Rake task**: \`react_on_rails_pro:pre_seed_renderer_cache\` accepts \`MODE=copy\` (default) or \`MODE=symlink\`. - **Deprecations (soft)**: \`PrepareNodeRenderBundles\` and the \`pre_stage_bundle_for_node_renderer\` rake task remain as thin shims emitting once-per-process deprecation warnings; both delegate to the unified API with \`mode: :symlink\`. No behavior change for existing users. - **Auto-invocation**: \`AssetsPrecompile.call\` now calls \`PreSeedRendererCache.call(mode: :symlink)\` after precompile (previously \`PrepareNodeRenderBundles.call\` — same effect). - **Doctor check**: \`react_on_rails:doctor\` scans common deploy-script locations (\`Procfile*\`, \`Dockerfile\`, \`bin/deploy\`, \`bin/release\`, \`bin/docker-entrypoint\`) for references to the deprecated task and surfaces a migration warning. - **Docs**: \`docs/pro/node-renderer.md\` updated to describe both modes, the non-dev env-var requirement for copy, and the deprecation story. - **CHANGELOG**: new \`Changed\` entry. ## Why this shape - The two classes shared ~90% of their logic after #3124 (both use \`RendererCacheHelpers\` for bundle validation, asset collection, and required-RSC-asset path construction). Keeping them separate forced readers to understand two entry points with parallel semantics. - Single class with \`mode:\` keeps the public surface obvious: the user picks copy vs symlink based on whether bundles need to survive Docker layer boundaries. Everything else is shared. - Soft deprecation avoids breaking users with \`Procfile\`/\`Dockerfile\`/\`bin/\` entries pinned to the old names. The doctor check gives them a clear migration nudge. ## Test plan - [x] 26 dummy specs pass for both \`pre_seed_renderer_cache_spec.rb\` and \`prepare_node_renderer_bundles_spec.rb\` (+6 new: mode validation, symlink mode via unified API, raise-in-non-dev guard, symlink mode opt-out from guard, env-var bypass for guard, deprecation warning on the shim). - [x] 2 new doctor specs for \`check_deprecated_renderer_cache_task\`: Procfile-reference warning + no-match silence. - [x] 163 total doctor specs pass (1 pre-existing unrelated failure in \`auto_fix_versions\`, not introduced by this PR). - [x] \`bundle exec rubocop\` clean on all changed files. - [ ] CI (will verify after push). ## Migration path for users - Existing \`Procfile\` / \`Dockerfile\` / \`bin/*\` entries that call \`pre_stage_bundle_for_node_renderer\` keep working unchanged — they emit a one-time deprecation warning and delegate to \`MODE=symlink\`. - Doctor surfaces affected files with migration guidance. - No changes needed for users relying on \`AssetsPrecompile.call\` auto-invocation (same behavior). - Docker builds that were using the old task name should migrate to \`MODE=copy\` (the default) for image-baked caches. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches Pro deployment/SSR renderer-cache staging behavior and introduces new env-var enforcement in `MODE=copy`, which could break misconfigured non-dev environments; changes are mitigated by deprecation shims and added doctor warnings/tests. > > **Overview** > **Unifies Node Renderer cache staging in Pro** by making `ReactOnRailsPro::PreSeedRendererCache.call(mode: :copy | :symlink)` the single entry point; both modes produce the same `<cache>/<bundleHash>/<bundleHash>.js` layout and the rake task `react_on_rails_pro:pre_seed_renderer_cache` now accepts `MODE=copy` (default) or `MODE=symlink`. > > **Deprecates legacy paths**: `react_on_rails_pro:pre_stage_bundle_for_node_renderer` and `ReactOnRailsPro::PrepareNodeRenderBundles` now warn and delegate to `mode: :symlink`, while `assets:precompile` auto-stages via `mode: :symlink`. > > **Adds safety/migration checks**: `MODE=copy` now raises in non-dev/test when neither `RENDERER_SERVER_BUNDLE_CACHE_PATH` nor `RENDERER_BUNDLE_PATH` is set, and `react_on_rails:doctor` scans common deploy scripts for the deprecated task and suggests the correct replacement. > > Docs/CHANGELOG are updated for the new modes and deprecations; specs are expanded for the new behavior, and the link checker ignore list adds `guavapass.com`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f511867. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3638bd9 commit e25fd1f

13 files changed

Lines changed: 623 additions & 88 deletions

File tree

.lychee.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ exclude = [
8686
'^https://(www\.)?estately\.com', # Returns 403
8787
'^https://hiring\.careerbuilder\.com', # Returns 403
8888
'^https://(www\.)?yourmechanic\.com', # Returns 403/503
89+
'^https://(www\.)?guavapass\.com', # TLS handshake failures from CI
8990
'^https?://(www\.)?hvmn\.com', # Returns 503 from CI
9091
'^https://(www\.)?hawaiichee\.com/?$', # Intermittent 500 from CI
9192

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
3434

3535
#### Changed
3636

37+
- **[Pro]** **Unified renderer cache staging**: `ReactOnRailsPro::PreSeedRendererCache.call(mode: :copy | :symlink)` is now the single entry point for staging the Node Renderer cache. Both modes produce the same `<cache>/<bundleHash>/<bundleHash>.js` layout. The `react_on_rails_pro:pre_seed_renderer_cache` rake task accepts `MODE=copy` (default; Docker/image builds) or `MODE=symlink` (same-filesystem). `MODE=copy` now raises a clear error when neither `RENDERER_SERVER_BUNDLE_CACHE_PATH` nor `RENDERER_BUNDLE_PATH` is set in non-dev/test environments, because the Node renderer's default lookup can differ from the Ruby side and would silently drop pre-seeded bundles in the wrong directory. The legacy `react_on_rails_pro:pre_stage_bundle_for_node_renderer` task and `ReactOnRailsPro::PrepareNodeRenderBundles` class remain as deprecated shims that emit a once-per-process warning and delegate to `mode: :symlink`. `react_on_rails:doctor` flags deploy scripts that still reference the deprecated task. [PR 3167](https://github.com/shakacode/react_on_rails/pull/3167) by [justin808](https://github.com/justin808).
3738
- **[Pro]** **Pro generator now creates the Node Renderer at `renderer/node-renderer.js`**: The canonical location for the Node Renderer entry point is now a dedicated top-level `renderer/` directory instead of `client/`, making it straightforward to exclude from production Docker builds that strip JS sources after bundling. Docs and Pro `spec/dummy` now use the new path consistently. Existing apps are unaffected — the generator skips files that already exist (including a legacy `client/node-renderer.js`). Fixes [Issue 3073](https://github.com/shakacode/react_on_rails/issues/3073). [PR 3165](https://github.com/shakacode/react_on_rails/pull/3165) by [justin808](https://github.com/justin808).
3839
- **Rspack install scaffolding now targets Rspack v2**: `react_on_rails:install --rspack` and `bin/switch-bundler` now generate the Rspack v2 package line (`@rspack/core@^2.0.0-0`, `@rspack/cli@^2.0.0-0`, `@rspack/plugin-react-refresh@^2.0.0`) while keeping `rspack-manifest-plugin@^5.0.0`, which is already compatible. Closes [Issue 3082](https://github.com/shakacode/react_on_rails/issues/3082). [PR 3084](https://github.com/shakacode/react_on_rails/pull/3084) by [justin808](https://github.com/justin808).
3940

docs/oss/building-features/node-renderer/js-configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ Deprecated options:
6060

6161
### Testing example:
6262

63-
[react_on_rails_pro/spec/dummy/renderer/node-renderer.js](https://github.com/shakacode/react_on_rails/blob/main/react_on_rails_pro/spec/dummy/renderer/node-renderer.js)
63+
The repository's dummy app keeps a full integration-test launcher at
64+
[`react_on_rails_pro/spec/dummy/renderer/node-renderer.js`](https://github.com/shakacode/react_on_rails/blob/main/react_on_rails_pro/spec/dummy/renderer/node-renderer.js).
6465

6566
### Simple example:
6667

docs/pro/node-renderer.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,26 +119,35 @@ When a new container starts, the Node Renderer has an empty bundle cache. The fi
119119

120120
### Pre-seeding the bundle cache
121121

122-
The `pre_seed_renderer_cache` rake task copies compiled server bundles directly into the renderer's cache directory during your Docker build, so the renderer finds them immediately on startup:
122+
The `pre_seed_renderer_cache` rake task stages compiled server bundles directly into the renderer's cache directory, so the renderer finds them immediately on startup.
123+
124+
It supports two modes, both producing the same on-disk cache layout (`<cache>/<bundleHash>/<bundleHash>.js`):
125+
126+
- **`MODE=copy`** (default) — copies files. Use in Docker/image builds so the cache is baked into an immutable artifact.
127+
- **`MODE=symlink`** — creates relative symlinks. For same-filesystem workflows (local dev, CI, Heroku-style same-dyno deploys, bundle-caching restores).
123128

124129
```dockerfile
125-
# After webpack/assets build step
130+
# After webpack/assets build step (Docker image build)
131+
ENV RENDERER_SERVER_BUNDLE_CACHE_PATH=/app/.node-renderer-bundles
126132
RUN bundle exec rake react_on_rails_pro:pre_seed_renderer_cache
127133
```
128134

129-
This copies the bundle into the renderer's expected directory structure (`<cache>/<bundleHash>/<bundleHash>.js`), including any configured `assets_to_copy` and RSC bundles when RSC support is enabled.
135+
Both modes stage the server bundle, any configured `assets_to_copy`, and (when RSC is enabled) the RSC bundle and its companion manifests.
130136

131-
This is the preferred path for Docker and other image-build workflows. React on Rails Pro has long supported runtime bundle uploads and the older `react_on_rails_pro:pre_stage_bundle_for_node_renderer` task for same-filesystem deployments; `pre_seed_renderer_cache` is the copy-based variant that fits immutable artifacts while using the same bundle-hash cache layout.
137+
The `pre_seed_renderer_cache` task is also invoked automatically at the end of `assets:precompile` with `MODE=symlink`, so the local/CI/Heroku path has zero new configuration.
138+
139+
> [!NOTE]
140+
> The older `react_on_rails_pro:pre_stage_bundle_for_node_renderer` rake task and `ReactOnRailsPro::PrepareNodeRenderBundles` class are deprecated in favor of the unified API. Both remain available as thin shims that emit a deprecation warning and delegate to `MODE=symlink`. `react_on_rails:doctor` flags deploy scripts that still reference the deprecated task.
132141
133142
### Configuration
134143

135144
The task follows the same environment-variable precedence as the Node Renderer, while the default fallback can differ between Ruby and standalone Node environments:
136145

137146
1. `RENDERER_SERVER_BUNDLE_CACHE_PATH` environment variable (preferred)
138147
2. `RENDERER_BUNDLE_PATH` environment variable (deprecated — emits a warning)
139-
3. `Rails.root.join(".node-renderer-bundles")` (Rails-side default when env vars are unset)
148+
3. `Rails.root.join(".node-renderer-bundles")` (Rails-side default when env vars are unset, only accepted for `MODE=symlink` and in dev/test)
140149

141-
Set `RENDERER_SERVER_BUNDLE_CACHE_PATH` in your Dockerfile to match the renderer's configuration:
150+
In **`MODE=copy`** (Docker image builds) the task requires one of the env vars above to be set in non-dev/test environments. "Non-dev/test" means any `RAILS_ENV` other than `development` or `test` — including custom environments like `staging`, `review`, or `ci` — so set `RENDERER_SERVER_BUNDLE_CACHE_PATH` wherever you run `MODE=copy` outside of local/CI-test runs. Because the Node renderer's own default can differ (e.g., falling back to `/tmp/react-on-rails-pro-node-renderer-bundles` when its `cwd` sits outside the app tree), relying on the silent fallback risks pre-seeded bundles landing in a directory the renderer never reads. The task raises a clear error if the env var is missing:
142151

143152
```dockerfile
144153
ENV RENDERER_SERVER_BUNDLE_CACHE_PATH=/app/.node-renderer-bundles

react_on_rails/lib/react_on_rails/doctor.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ class Doctor
5858
SERVER_BUNDLE_SOURCE_EXTENSIONS = %w[.js .jsx .ts .tsx .mjs .cjs].freeze
5959
CUSTOM_LAUNCHER_INDICATOR_FILES = %w[dev].freeze
6060

61+
# Deprecated-renderer-cache scan (used by check_deprecated_renderer_cache_task):
62+
# look for references to the old pre_stage_bundle_for_node_renderer task in
63+
# common deploy-script locations so users on older Procfile/Dockerfile entries
64+
# get a migration nudge before the task is removed.
65+
DEPRECATED_RENDERER_CACHE_TASK = "pre_stage_bundle_for_node_renderer"
66+
RENDERER_CACHE_DEPLOY_SCRIPT_PATHS = [
67+
"Procfile",
68+
"Procfile.dev",
69+
"Procfile.dev-static-assets",
70+
"Procfile.production",
71+
"Dockerfile",
72+
"Dockerfile.production",
73+
"Dockerfile.staging",
74+
"Dockerfile.review",
75+
"bin/deploy",
76+
"bin/release",
77+
"bin/docker-entrypoint"
78+
].freeze
79+
RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES = 1_048_576
80+
6181
def initialize(verbose: false, fix: false)
6282
@verbose = verbose
6383
@fix = fix
@@ -2717,6 +2737,7 @@ def check_pro_setup
27172737
ensure_rails_environment_loaded
27182738
check_pro_renderer_mode
27192739
check_base_package_imports
2740+
check_deprecated_renderer_cache_task
27202741
end
27212742

27222743
def check_pro_initializer_existence
@@ -2746,6 +2767,50 @@ def check_pro_renderer_mode
27462767
checker.add_warning("⚠️ Could not detect Pro renderer mode: #{e.message}")
27472768
end
27482769

2770+
def check_deprecated_renderer_cache_task
2771+
# Resolve against Rails.root (not Dir.pwd) so the scan still fires when
2772+
# doctor is invoked from a subdirectory — otherwise the checks silently
2773+
# find nothing and the deprecation warning never surfaces.
2774+
#
2775+
# Substring match is intentional: a comment line in a Procfile/Dockerfile
2776+
# that mentions the old task name will also trigger the warning. That is
2777+
# acceptable — the worst case is a benign migration nudge on a file that's
2778+
# already been migrated but still references the old name in a comment.
2779+
matches = RENDERER_CACHE_DEPLOY_SCRIPT_PATHS.select do |path|
2780+
full_path = Rails.root.join(path)
2781+
next false unless full_path.file?
2782+
# Skip files larger than 1 MB; deploy scripts should be tiny.
2783+
next false if full_path.size > RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES
2784+
2785+
full_path.read.include?(DEPRECATED_RENDERER_CACHE_TASK)
2786+
end
2787+
2788+
return if matches.empty?
2789+
2790+
checker.add_warning(<<~MSG.strip)
2791+
⚠️ Deprecated rake task '#{DEPRECATED_RENDERER_CACHE_TASK}' referenced in:
2792+
#{matches.map { |p| " • #{p}#{renderer_cache_migration_suggestion(p)}" }.join("\n")}
2793+
2794+
The unified 'pre_seed_renderer_cache' task uses MODE=copy by default (for
2795+
Docker/image builds) and MODE=symlink for same-filesystem workflows.
2796+
This scan also matches comments; remove stale mentions after migrating.
2797+
MSG
2798+
rescue StandardError => e
2799+
checker.add_warning("⚠️ Could not scan for deprecated renderer-cache task references: #{e.message}")
2800+
end
2801+
2802+
def renderer_cache_migration_suggestion(path)
2803+
# Dockerfile* entries are RUN steps during image build, so copy mode bakes
2804+
# the cache into the layer. Procfile, bin/*, and other runtime scripts run
2805+
# inside the already-booted container or dyno, where both the app and
2806+
# renderer share the same filesystem, so symlink mode is correct.
2807+
if path.start_with?("Dockerfile")
2808+
"rake react_on_rails_pro:pre_seed_renderer_cache"
2809+
else
2810+
"rake react_on_rails_pro:pre_seed_renderer_cache MODE=symlink"
2811+
end
2812+
end
2813+
27492814
# The base 'react-on-rails' npm package is a transitive dependency of 'react-on-rails-pro',
27502815
# so `import ... from 'react-on-rails'` resolves silently — loading the base package instead
27512816
# of Pro. Components registered through the base package won't have Pro features (streaming,

react_on_rails/spec/lib/react_on_rails/doctor_spec.rb

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,6 +2468,171 @@ class << self
24682468
end
24692469
end
24702470

2471+
describe "check_deprecated_renderer_cache_task" do
2472+
let(:doctor) { described_class.new(verbose: false, fix: false) }
2473+
let(:checker) { doctor.instance_variable_get(:@checker) }
2474+
2475+
context "when a Procfile references the deprecated task" do
2476+
let(:tmpdir) { Dir.mktmpdir }
2477+
2478+
before do
2479+
File.write(
2480+
File.join(tmpdir, "Procfile"),
2481+
"web: bundle exec rake react_on_rails_pro:pre_stage_bundle_for_node_renderer && bundle exec puma\n"
2482+
)
2483+
allow(Rails).to receive(:root).and_return(Pathname.new(tmpdir))
2484+
end
2485+
2486+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2487+
2488+
it "warns with migration guidance" do
2489+
doctor.send(:check_deprecated_renderer_cache_task)
2490+
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
2491+
expect(warning_msgs.any? { |m| m[:content].include?("pre_stage_bundle_for_node_renderer") }).to be(true)
2492+
expect(warning_msgs.any? { |m| m[:content].include?("MODE=symlink") }).to be(true)
2493+
end
2494+
end
2495+
2496+
context "when a Dockerfile variant references the deprecated task" do
2497+
let(:tmpdir) { Dir.mktmpdir }
2498+
2499+
before do
2500+
File.write(
2501+
File.join(tmpdir, "Dockerfile.production"),
2502+
"RUN bundle exec rake react_on_rails_pro:pre_stage_bundle_for_node_renderer\n"
2503+
)
2504+
allow(Rails).to receive(:root).and_return(Pathname.new(tmpdir))
2505+
end
2506+
2507+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2508+
2509+
it "suggests the copy-mode task without MODE=symlink" do
2510+
doctor.send(:check_deprecated_renderer_cache_task)
2511+
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
2512+
expect(warning_msgs).not_to be_empty
2513+
suggestion_line = warning_msgs
2514+
.flat_map { |m| m[:content].split("\n") }
2515+
.find { |line| line.include?("Dockerfile.production →") }
2516+
expect(suggestion_line).not_to be_nil
2517+
expect(suggestion_line).to include("pre_seed_renderer_cache")
2518+
expect(suggestion_line).not_to include("MODE=symlink")
2519+
end
2520+
end
2521+
2522+
context "when no deploy scripts reference the deprecated task" do
2523+
let(:tmpdir) { Dir.mktmpdir }
2524+
2525+
before { allow(Rails).to receive(:root).and_return(Pathname.new(tmpdir)) }
2526+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2527+
2528+
it "adds no warnings" do
2529+
doctor.send(:check_deprecated_renderer_cache_task)
2530+
expect(checker.messages.select { |m| m[:type] == :warning }).to be_empty
2531+
end
2532+
end
2533+
2534+
context "when a configured deploy-script path is a directory" do
2535+
let(:tmpdir) { Dir.mktmpdir }
2536+
2537+
before do
2538+
FileUtils.mkdir_p(File.join(tmpdir, "bin/deploy"))
2539+
File.write(
2540+
File.join(tmpdir, "Procfile"),
2541+
"web: bundle exec rake react_on_rails_pro:pre_stage_bundle_for_node_renderer\n"
2542+
)
2543+
allow(Rails).to receive(:root).and_return(Pathname.new(tmpdir))
2544+
end
2545+
2546+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2547+
2548+
it "skips the directory and continues scanning real files" do
2549+
doctor.send(:check_deprecated_renderer_cache_task)
2550+
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
2551+
2552+
expect(warning_msgs.any? { |m| m[:content].include?("Procfile") }).to be(true)
2553+
expect(warning_msgs.none? do |m|
2554+
m[:content].include?("Could not scan for deprecated renderer-cache task")
2555+
end).to be(true)
2556+
end
2557+
end
2558+
2559+
context "when a deploy-script file exceeds the size gate" do
2560+
let(:tmpdir) { Dir.mktmpdir }
2561+
2562+
before do
2563+
# Stub the cap so we do not have to write a real 1 MB file — the gate
2564+
# logic is what we are exercising, not the specific threshold.
2565+
stub_const("ReactOnRails::Doctor::RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES", 64)
2566+
padding = "x" * 128
2567+
File.write(
2568+
File.join(tmpdir, "Procfile"),
2569+
"web: bundle exec rake react_on_rails_pro:pre_stage_bundle_for_node_renderer\n#{padding}"
2570+
)
2571+
allow(Rails).to receive(:root).and_return(Pathname.new(tmpdir))
2572+
end
2573+
2574+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2575+
2576+
it "silently skips the file and emits no warning" do
2577+
doctor.send(:check_deprecated_renderer_cache_task)
2578+
expect(checker.messages.select { |m| m[:type] == :warning }).to be_empty
2579+
end
2580+
end
2581+
2582+
context "when a deploy-script file is exactly the size gate" do
2583+
let(:tmpdir) { Dir.mktmpdir }
2584+
let(:script_content) do
2585+
"web: bundle exec rake react_on_rails_pro:pre_stage_bundle_for_node_renderer\n"
2586+
end
2587+
2588+
before do
2589+
stub_const("ReactOnRails::Doctor::RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES", script_content.bytesize)
2590+
File.write(File.join(tmpdir, "Procfile"), script_content)
2591+
allow(Rails).to receive(:root).and_return(Pathname.new(tmpdir))
2592+
end
2593+
2594+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2595+
2596+
it "still scans the file" do
2597+
doctor.send(:check_deprecated_renderer_cache_task)
2598+
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
2599+
expect(warning_msgs.any? { |m| m[:content].include?("pre_stage_bundle_for_node_renderer") }).to be(true)
2600+
end
2601+
end
2602+
2603+
context "when reading a deploy-script file raises an unexpected error" do
2604+
let(:tmpdir) { Dir.mktmpdir }
2605+
let(:procfile_path) { File.join(tmpdir, "Procfile") }
2606+
2607+
before do
2608+
File.write(
2609+
procfile_path,
2610+
"web: bundle exec rake react_on_rails_pro:pre_stage_bundle_for_node_renderer\n"
2611+
)
2612+
root_path = Pathname.new(tmpdir)
2613+
allow(Rails).to receive(:root).and_return(root_path)
2614+
2615+
# Simulate a filesystem error (e.g. transient EIO or a permissions race)
2616+
# on the actual Pathname receiver used by the doctor scan.
2617+
failing_procfile = instance_double(Pathname)
2618+
allow(failing_procfile).to receive_messages(file?: true, size: File.size(procfile_path))
2619+
allow(failing_procfile).to receive(:read).and_raise(Errno::EIO, "simulated read failure")
2620+
allow(root_path).to receive(:join).and_call_original
2621+
allow(root_path).to receive(:join).with("Procfile").and_return(failing_procfile)
2622+
end
2623+
2624+
after { FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) }
2625+
2626+
it "captures the error as a warning instead of failing the doctor check" do
2627+
expect { doctor.send(:check_deprecated_renderer_cache_task) }.not_to raise_error
2628+
warning_msgs = checker.messages.select { |m| m[:type] == :warning }
2629+
expect(warning_msgs.any? do |m|
2630+
m[:content].include?("Could not scan for deprecated renderer-cache task")
2631+
end).to be(true)
2632+
end
2633+
end
2634+
end
2635+
24712636
describe "check_base_package_imports" do
24722637
let(:doctor) { described_class.new(verbose: false, fix: false) }
24732638
let(:checker) { doctor.instance_variable_get(:@checker) }

react_on_rails_pro/lib/react_on_rails_pro/assets_precompile.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ def build_bundles
6363
def self.call
6464
instance.build_or_fetch_bundles
6565

66-
ReactOnRailsPro::PrepareNodeRenderBundles.call if ReactOnRailsPro.configuration.node_renderer?
66+
# Auto-stage via symlink after asset precompile (same-filesystem default).
67+
# Docker/image builds should invoke `rake react_on_rails_pro:pre_seed_renderer_cache`
68+
# (MODE=copy, the default) as a separate step.
69+
ReactOnRailsPro::PreSeedRendererCache.call(mode: :symlink) if ReactOnRailsPro.configuration.node_renderer?
6770
end
6871

6972
def build_or_fetch_bundles

0 commit comments

Comments
 (0)