Skip to content

Commit 600af78

Browse files
justin808claude
andauthored
docs: clarify extensible precompile ownership (#3226)
## Summary - add a migration checklist for moving custom build steps out of `precompile_hook` - clarify that `bin/dev`, Procfiles, and build commands each own different lifecycle work - document a script-based pattern for keeping test and production build commands in sync without long duplicated strings Fixes #2347 ## Test plan - `pnpm exec prettier --check docs/oss/building-features/extensible-precompile-pattern.md` - `pnpm start format.listDifferent` - `script/check-docs-sidebar origin/main HEAD` - `git diff --check` - pre-commit hook: trailing-newlines, offline Markdown links, Prettier - pre-push hook: branch-lint, online Markdown links <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk documentation-only change; it may influence user build setups, but no runtime code is modified. > > **Overview** > **Clarifies how to migrate from `precompile_hook` to the extensible `bin/dev` precompile pattern** by adding a step-by-step checklist to avoid duplicate one-time build steps across `bin/dev`, Procfiles, CI, and production. > > **Expands build-command guidance** to explicitly cover both test and production lifecycles, adds a recommended wrapper-script pattern (`bin/build-react-on-rails`) to keep `build_test_command`/`build_production_command` in sync (including `node_modules_location` and cross-platform/executable-bit notes), and updates testing docs to note that Shakapacker derives `NODE_ENV` from `RAILS_ENV` for asset builds. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2816101. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added a migration checklist to consolidate one-time precompile steps, remove duplicated fragments and obsolete hooks, ensure test and production builds include required steps, and keep long-running watchers as separate processes. * Expanded build-commands guidance with a reusable, mode-aware build script pattern, explicit pre-build validation step, instructions for making it executable, and wiring it into app startup. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/shakacode/react_on_rails/pull/3226) <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fafa6af commit 600af78

2 files changed

Lines changed: 166 additions & 5 deletions

File tree

docs/oss/building-features/extensible-precompile-pattern.md

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,34 @@ Consider this approach if you:
2323

2424
## Implementation
2525

26+
### Migration Checklist
27+
28+
If you have not already completed Sections 1–4 below (start at [Section 1](#1-customize-bindev)), do that first so
29+
`bin/dev`, `config/shakapacker.yml`, your Procfiles, and your build commands are in place before you start removing
30+
duplicates.
31+
32+
When moving custom build work out of `precompile_hook`, make the ownership change in one commit so the same task cannot run twice. The checklist uses letters (A–E) so the steps are easy to distinguish from the numbered Implementation sections referenced above.
33+
34+
A. Add (or uncomment, if already present) custom one-time tasks to the `run_precompile_tasks` method in `bin/dev`
35+
(see [Section 1](#1-customize-bindev) for the generator-provided template).
36+
37+
B. Ensure `build_test_command` and `build_production_command` each include every one-time build task those lifecycles
38+
need, such as ReScript builds, TypeScript checks or compilation, and locale generation. `bin/dev` is not invoked in
39+
CI or production, so these commands are the only mechanism those lifecycles have.
40+
41+
C. After verifying the updated commands work locally, remove one-time build commands from individual Procfile process
42+
entries. If those same commands appear as standalone steps in CI/CD pipeline scripts, remove those duplicate
43+
invocations too. For example, remove a bare `yarn res:build` GitHub Actions step only after `build_test_command` or
44+
`build_production_command` includes it. Do not delete entire `.github/workflows`, `.circleci/config.yml`, or Heroku
45+
`app.json` files unless they exist solely for the migrated build step.
46+
47+
D. Confirm `precompile_hook` has been removed from `config/shakapacker.yml` (per
48+
[Section 2](#2-configure-shakapackeryml)) so the same task does not also run during webpack compiles.
49+
50+
E. Keep long-running watchers, such as `rescript: yarn res:watch`, as separate Procfile processes.
51+
52+
The goal is one owner per lifecycle: `bin/dev` owns development startup, Procfile processes own long-running watchers, and React on Rails build commands own test and production compilation.
53+
2654
### 1. Customize bin/dev
2755

2856
The React on Rails generator creates a `bin/dev` script with an extensible precompile pattern. Uncomment and customize the `run_precompile_tasks` method:
@@ -79,7 +107,7 @@ ReactOnRails::Dev::ServerManager.run_from_command_line(argv_with_defaults)
79107

80108
### 2. Configure shakapacker.yml
81109

82-
Remove or comment out the `precompile_hook` in `config/shakapacker.yml`, since `bin/dev` now handles precompile tasks directly:
110+
Remove the `precompile_hook` from `config/shakapacker.yml`, since `bin/dev` now handles precompile tasks directly:
83111

84112
**Before (default precompile_hook approach):**
85113

@@ -127,16 +155,145 @@ wp-server: SERVER_BUNDLE_ONLY=true bin/shakapacker --watch
127155

128156
### 4. Configure Build Commands
129157

130-
Handle production builds in `config/initializers/react_on_rails.rb`:
158+
Handle test and production builds in `config/initializers/react_on_rails.rb`. These commands must include every build step that production deploys and CI test runs require, because `bin/dev` is not part of those lifecycles:
159+
160+
In CI, ReactOnRails::TestHelper runs `build_test_command` when test assets need compilation. See
161+
[testing configuration](testing-configuration.md#quick-start) for the RSpec/Minitest wiring. During
162+
`assets:precompile`, React on Rails runs `build_production_command`.
163+
164+
Choose one of the following configuration styles. Use only one: Option A sets the commands directly, while Option B
165+
points both commands at the helper script.
166+
167+
#### Option A - Inline commands
131168

132169
```ruby
133170
ReactOnRails.configure do |config|
134-
# Build commands should include all necessary steps
171+
# Build commands should include all necessary steps.
172+
# Shakapacker auto-derives NODE_ENV from RAILS_ENV, so the test command leaves NODE_ENV implicit.
173+
# The production command sets NODE_ENV=production explicitly as a belt-and-suspenders safeguard
174+
# against any pre-shakapacker step (e.g. a custom yarn script) that reads NODE_ENV directly.
135175
config.build_test_command = "yarn res:build && RAILS_ENV=test bin/shakapacker"
136176
config.build_production_command = "yarn res:build && RAILS_ENV=production NODE_ENV=production bin/shakapacker"
137177
end
138178
```
139179

180+
#### Option B - Script wrapper (recommended for multi-step builds)
181+
182+
If your build needs more than one pre-shakapacker step, such as a TypeScript check and a ReScript compile, prefer a small
183+
Ruby script over a very long command string. Create `bin/build-react-on-rails`:
184+
185+
```ruby
186+
#!/usr/bin/env ruby
187+
# frozen_string_literal: true
188+
189+
require "rbconfig"
190+
191+
# Pin the working directory to the Rails application root so the relative paths below resolve correctly even when
192+
# `config.node_modules_location` is a subdirectory. React on Rails prepends `cd "<node_modules_location>"` to
193+
# `build_test_command` and `build_production_command`, which would otherwise leave the script looking for
194+
# `bin/shakapacker` under that subdirectory. See the note under the configuration example below for invoking the
195+
# wrapper itself from a custom `node_modules_location`.
196+
Dir.chdir(File.expand_path("..", __dir__))
197+
198+
mode = ARGV.first
199+
200+
unless %w[test production].include?(mode)
201+
abort "Usage: bin/build-react-on-rails test|production\nGot: #{mode.inspect}"
202+
end
203+
204+
# Add your app's pre-build step(s) here. They run for both test and production.
205+
# Leave this section empty if shakapacker is the only build step.
206+
# If a pre-build command needs env vars, pass them via a hash:
207+
# system({ "SOME_VAR" => "value" }, "yarn", "custom:build") || abort("custom:build failed")
208+
# The mode-specific env hashes below are intentionally scoped to each shakapacker call.
209+
# For example, to run TypeScript then ReScript:
210+
# # --noEmit type-checks only; ts-loader/babel-loader handle transpilation during webpack bundling.
211+
# system("yarn", "tsc", "--noEmit") || abort("tsc type-check failed")
212+
# system("yarn", "res:build") || abort("res:build failed")
213+
214+
# Mode-specific invocation below. `RbConfig.ruby` runs shakapacker with the same Ruby interpreter that launched this
215+
# wrapper, and the `Dir.chdir` above keeps `bin/shakapacker` resolvable from the Rails application root. The
216+
# shakapacker binstub is a Ruby file by convention, so passing it to `RbConfig.ruby` is portable; if your project
217+
# replaces `bin/shakapacker` with a shell wrapper, drop `RbConfig.ruby` and invoke the binstub directly (after
218+
# ensuring the right Ruby is on `PATH`). Add shared steps above, not inside the case blocks.
219+
case mode
220+
when "test"
221+
env = { "RAILS_ENV" => "test" }
222+
system(env, RbConfig.ruby, "bin/shakapacker") || abort("shakapacker (test) failed")
223+
when "production"
224+
env = { "RAILS_ENV" => "production", "NODE_ENV" => "production" }
225+
system(env, RbConfig.ruby, "bin/shakapacker") || abort("shakapacker (production) failed")
226+
end
227+
```
228+
229+
On Unix-like filesystems, make the script executable so it can run locally, then stage the file so Git records the
230+
executable bit for CI and other checkouts:
231+
232+
```bash
233+
chmod +x bin/build-react-on-rails
234+
git add bin/build-react-on-rails
235+
```
236+
237+
`git add --chmod=+x bin/build-react-on-rails` (Git 2.9 or newer) is a useful shortcut, but it only updates the
238+
executable bit in Git's index — the file mode on disk is left unchanged. The script will still not be runnable from
239+
this checkout until `chmod +x` is also applied. Use the shortcut when CI is the only consumer that needs the
240+
executable bit; otherwise keep the two-step `chmod +x` then `git add` so the file is executable both locally and in
241+
committed history.
242+
243+
<details>
244+
<summary>Windows and Docker bind mounts</summary>
245+
246+
On Windows or Docker bind mounts backed by a Windows filesystem, the filesystem may not preserve Unix modes, so `chmod`
247+
may not make the current checkout runnable. Record the executable bit in Git for CI and other checkouts, and invoke the
248+
script through Ruby when running it from that local filesystem:
249+
250+
```bash
251+
git update-index --chmod=+x bin/build-react-on-rails
252+
ruby bin/build-react-on-rails test
253+
```
254+
255+
If the file has not been staged yet, use `git update-index --add --chmod=+x bin/build-react-on-rails` instead. Use
256+
`production` instead of `test` for the production build command. The `git update-index` command only updates Git metadata;
257+
it does not change the current working-tree file mode.
258+
259+
Configure `react_on_rails.rb` once. Prefix the helper with `ruby` so the same commands work on Unix, macOS, CI, and
260+
Windows or Windows-backed Docker bind-mount checkouts without relying on the current filesystem's executable bit.
261+
The outer `ruby` is resolved via `PATH` when Rails runs the build command, which works under rbenv/asdf/mise and
262+
standard CI images. For hermetic environments where `PATH` may not select the project's interpreter (e.g. some Docker
263+
base images without active version-manager shims), invoke through Bundler or substitute an absolute Ruby path — for
264+
example `bundle exec ruby bin/build-react-on-rails test` or `$(rbenv which ruby) bin/build-react-on-rails test`.
265+
Inside the script, `RbConfig.ruby` already pins shakapacker to the same interpreter that launched the wrapper.
266+
267+
</details>
268+
269+
```ruby
270+
# config/initializers/react_on_rails.rb
271+
ReactOnRails.configure do |config|
272+
config.build_test_command = "ruby bin/build-react-on-rails test"
273+
config.build_production_command = "ruby bin/build-react-on-rails production"
274+
end
275+
```
276+
277+
> **If you set `config.node_modules_location`:** React on Rails prepends `cd "<node_modules_location>"` to both
278+
> build commands, so the bare `ruby bin/build-react-on-rails …` invocation above will not find the wrapper from a
279+
> subdirectory. Interpolate an absolute path from `Rails.root` so the script is locatable regardless of the
280+
> prepended `cd`. React on Rails passes these command strings to a shell, so escape the interpolated path with
281+
> `Shellwords.escape` (Ruby stdlib) — a bare `#{wrapper}` would break on any `Rails.root` containing spaces or
282+
> other shell metacharacters (e.g. `/Users/jane doe/my app`):
283+
>
284+
> ```ruby
285+
> require "shellwords"
286+
>
287+
> wrapper = Shellwords.escape(Rails.root.join("bin", "build-react-on-rails").to_s)
288+
> config.build_test_command = "ruby #{wrapper} test"
289+
> config.build_production_command = "ruby #{wrapper} production"
290+
> ```
291+
>
292+
> The `Dir.chdir` inside the wrapper then re-pins the working directory to the Rails root for the inner
293+
> `bin/shakapacker` call.
294+
295+
This keeps the migration reviewable and avoids duplicating custom build logic across `bin/dev`, Procfiles, and deploy scripts.
296+
140297
## Direct Ruby API Reference
141298
142299
### ReactOnRails::Locales.compile

docs/oss/building-features/testing-configuration.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ For most applications, the recommended approach is React on Rails TestHelper wit
99
```ruby
1010
# config/initializers/react_on_rails.rb
1111
ReactOnRails.configure do |config|
12-
config.build_test_command = "RAILS_ENV=test bin/shakapacker"
12+
config.build_test_command = "RAILS_ENV=test bin/shakapacker" # NODE_ENV is derived from RAILS_ENV by Shakapacker
1313
end
1414
```
1515

16+
Leave `NODE_ENV` unset for Shakapacker asset builds: Shakapacker automatically sets `NODE_ENV` to match `RAILS_ENV`,
17+
so `RAILS_ENV=test` is sufficient. Set `NODE_ENV=test` explicitly only when running a JavaScript test runner such as
18+
Jest directly.
19+
1620
Then wire TestHelper into your test framework. If your app uses both RSpec and Minitest, wire both files.
1721

1822
**RSpec** — add to `spec/rails_helper.rb`:
@@ -420,7 +424,7 @@ React on Rails supports two mutually exclusive approaches for compiling webpack
420424
```ruby
421425
# config/initializers/react_on_rails.rb
422426
ReactOnRails.configure do |config|
423-
config.build_test_command = "NODE_ENV=test RAILS_ENV=test bin/shakapacker"
427+
config.build_test_command = "RAILS_ENV=test bin/shakapacker" # NODE_ENV is derived from RAILS_ENV by Shakapacker
424428

425429
# Or use your project's package manager with a custom script:
426430
# config.build_test_command = "pnpm run build:test" # or: npm run build:test, yarn run build:test

0 commit comments

Comments
 (0)