react_on_rails 17.0.1 → 17.1.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/docs/agent/README.md +26 -0
  4. data/docs/agent/doctor-fix-loop.md +25 -0
  5. data/docs/agent/install-and-upgrade.md +162 -0
  6. data/docs/agent/rsc-adoption.md +31 -0
  7. data/docs/agent/streaming-debug.md +57 -0
  8. data/lib/generators/react_on_rails/base_generator.rb +4 -4
  9. data/lib/generators/react_on_rails/generator_helper.rb +12 -1
  10. data/lib/generators/react_on_rails/install_generator.rb +50 -5
  11. data/lib/generators/react_on_rails/js_dependency_manager.rb +1 -1
  12. data/lib/generators/react_on_rails/pro_generator.rb +5 -3
  13. data/lib/generators/react_on_rails/pro_setup.rb +51 -19
  14. data/lib/generators/react_on_rails/rsc_generator.rb +18 -1
  15. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +63 -25
  16. data/lib/generators/react_on_rails/rsc_setup.rb +141 -39
  17. data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +31 -3
  18. data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev-static-assets → Procfile.dev-static-assets.tt} +1 -1
  19. data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev → Procfile.dev.tt} +1 -1
  20. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +18 -42
  21. data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb +215 -0
  22. data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_skill.md +71 -0
  23. data/lib/react_on_rails/agent_guardrails.rb +223 -0
  24. data/lib/react_on_rails/dev/service_checker.rb +1 -1
  25. data/lib/react_on_rails/doctor.rb +880 -48
  26. data/lib/react_on_rails/doctor_schema.rb +192 -0
  27. data/lib/react_on_rails/engine.rb +2 -2
  28. data/lib/react_on_rails/helper.rb +17 -10
  29. data/lib/react_on_rails/length_prefixed_parser.rb +22 -14
  30. data/lib/react_on_rails/lenient_json.rb +0 -0
  31. data/lib/react_on_rails/prerender_error.rb +38 -8
  32. data/lib/react_on_rails/react_component/render_options.rb +1 -1
  33. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +168 -15
  34. data/lib/react_on_rails/version.rb +1 -1
  35. data/lib/tasks/agent_guardrails.rake +21 -0
  36. data/rakelib/run_rspec.rake +14 -0
  37. data/sig/react_on_rails/prerender_error.rbs +2 -1
  38. data/skills/doctor-fix-loop/SKILL.md +26 -0
  39. data/skills/install-and-upgrade/SKILL.md +24 -0
  40. data/skills/rsc-adoption/SKILL.md +28 -0
  41. data/skills/streaming-debug/SKILL.md +53 -0
  42. metadata +18 -4
  43. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0a69e6dd7950d3edc83c8ce77b7ad56419f243b3355878140e8fad6e1f56d54
4
- data.tar.gz: 2262184ed159c5e4efa4402e863d695c86cded69ff395dea8c7096b9fca73701
3
+ metadata.gz: 3c57ce4cf00568f0e471fb56202eda0a3483c1abdbcdd2ac75af3f7b0980fdcb
4
+ data.tar.gz: a5a7c6b5279f3274ece7bba232ad8d8367fab307532df3769fe222f4ad557b6b
5
5
  SHA512:
6
- metadata.gz: abfc807dddaed6cc958329c40bece87518247237aea46f1f8fbbe7f5c7bfc7738658f80a2656de0755788cfaa861b1369ccda45c0de44fef225e37686cb6040a
7
- data.tar.gz: 630f17042cc9b1df2ce8cdaebe33883eeee4baa21ab041de75590984a5710751c0e5d7c53eedea1580823efbe62850d23c2e79d58f8baa1263d1c2aa9f1b4e3b
6
+ metadata.gz: 7f42ff98daf96f7f8e5a32a7d790790b1a61e3f5fdcbe18d32962b43ea77863a2061b4b14b58a59f4a6b8e9b5ee555ce18353dcccdf591776ba9480827237e14
7
+ data.tar.gz: 79891caa83aea2acaa89f37a5351b22a6ac8ac30c80f458d9ad4c4bc1bb2df461e2fa212297d03ad7f06beef37b29cd7ec3dd723c477c5cbb2192eeb08a4985d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (17.0.1)
4
+ react_on_rails (17.1.0.rc.0)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -0,0 +1,26 @@
1
+ # React on Rails bundled agent reference
2
+
3
+ These guides and skills ship with the installed React on Rails version. Read them before relying on
4
+ training data or hosted documentation, which may describe a different release.
5
+
6
+ | Workflow | Skill | Guide |
7
+ | ----------------------------------- | ------------------------------------------------------------------ | --------------------------------- |
8
+ | Install or upgrade | [`install-and-upgrade`](../../skills/install-and-upgrade/SKILL.md) | [Guide](./install-and-upgrade.md) |
9
+ | Adopt React Server Components (Pro) | [`rsc-adoption`](../../skills/rsc-adoption/SKILL.md) | [Guide](./rsc-adoption.md) |
10
+ | Debug streaming SSR (Pro) | [`streaming-debug`](../../skills/streaming-debug/SKILL.md) | [Guide](./streaming-debug.md) |
11
+ | Iterate with the doctor | [`doctor-fix-loop`](../../skills/doctor-fix-loop/SKILL.md) | [Guide](./doctor-fix-loop.md) |
12
+
13
+ The gem and npm package contain byte-identical copies. Use the installed gem directory as the
14
+ canonical, reliable lookup:
15
+
16
+ ```bash
17
+ bundle show react_on_rails
18
+ ```
19
+
20
+ Read `docs/agent/README.md` and the `skills/` paths from the returned directory. An optional
21
+ direct-dependency path is `node_modules/react-on-rails/`, but use it only when that directory
22
+ exists. Do not assume it exists: Pro installations may omit the direct base npm dependency, and
23
+ pnpm may isolate a transitive copy.
24
+
25
+ For broader explanation after reading the installed references, use the hosted documentation:
26
+ https://reactonrails.com/docs/.
@@ -0,0 +1,25 @@
1
+ # Doctor-driven fix loop
2
+
3
+ Use the stable JSON form for agent and CI workflows:
4
+
5
+ ```bash
6
+ bin/rails react_on_rails:doctor FORMAT=json
7
+ ```
8
+
9
+ Standard output is one JSON document. The top-level `status`, `checks`, and `summary` describe the
10
+ run. Every check exposes a stable `id`, `status`, `severity`, `message`, `fix_command`, `docs_url`,
11
+ `remediation`, and `details` contract.
12
+
13
+ For every non-passing check, in array order:
14
+
15
+ 1. Record `id` and `severity`.
16
+ 2. Inspect `message`, `remediation.files`, and the current app state.
17
+ 3. Use `remediation.prompt` and `remediation.expected_end_state` as bounded guidance.
18
+ 4. Review the smallest corrective diff.
19
+ 5. Rerun the JSON doctor and confirm the check passes without creating another failure.
20
+
21
+ Exit code `1` means at least one error remains. Warnings keep exit code `0`, so read `status` or
22
+ `summary.warn` when warning-free output matters. A `null` `fix_command` is expected; never infer an
23
+ unsafe global repair command from the check ID.
24
+
25
+ Secondary reference: https://reactonrails.com/docs/api-reference/doctor.
@@ -0,0 +1,162 @@
1
+ # Install and upgrade
2
+
3
+ Use this guide for an existing Rails app. Keep app-specific choices and custom configuration intact.
4
+
5
+ ## Preflight
6
+
7
+ - Record the Rails, Ruby, Node, Shakapacker, React on Rails gem, npm package, and package-manager state.
8
+ - Identify both Ruby and JavaScript lockfiles and start from a reviewable working tree.
9
+ - Keep the gem and npm package on the same React on Rails release. A Pro app must also update its Pro
10
+ gem and JavaScript packages together.
11
+
12
+ ## Install
13
+
14
+ Add Shakapacker and React on Rails through Bundler.
15
+ Choose exactly one stack flag before running the install generator.
16
+ Use `--standard-only` for OSS, `--pro` for Pro, or `--rsc` for Pro with RSC.
17
+ Do not rely on an interactive TTY prompt; replace `<STACK_FLAG>` explicitly in every command.
18
+ Choose the language explicitly too: replace `<LANGUAGE_CHOICE>` with `--typescript` for TypeScript,
19
+ or remove that placeholder for JavaScript. TypeScript is optional; omitting its flag preserves JavaScript.
20
+
21
+ ```bash
22
+ bundle add shakapacker --strict
23
+ bundle add react_on_rails --strict
24
+ ```
25
+
26
+ For `--pro` or `--rsc`, React on Rails Pro is free for evaluation and non-production use;
27
+ production use requires a subscription. Review [Pro licensing and setup](https://reactonrails.com/docs/pro/upgrading-to-pro/)
28
+ before proceeding.
29
+
30
+ Skip this Pro-gem preparation for `--standard-only`.
31
+ For `--pro` or `--rsc` only, derive the exact installed base-gem version before adding the Pro gem:
32
+
33
+ ```bash
34
+ ROR_GEM_VERSION="$(bundle exec ruby -rreact_on_rails/version -e 'print ReactOnRails::VERSION')"
35
+ bundle add react_on_rails_pro --version="${ROR_GEM_VERSION}" --strict
36
+ ```
37
+
38
+ If that exact version is an unpublished prerelease, require a matching local/path `react_on_rails_pro` gem
39
+ and use the same exact version instead of the registry command:
40
+
41
+ ```bash
42
+ bundle add react_on_rails_pro --path="<path-to-matching-react_on_rails_pro>" --version="${ROR_GEM_VERSION}" --strict
43
+ ```
44
+
45
+ Never fall back silently to a stable Pro gem.
46
+ For `--pro` or `--rsc` only, after either Pro source command succeeds, remove the direct base-gem declaration:
47
+
48
+ ```bash
49
+ bundle remove react_on_rails
50
+ ```
51
+
52
+ This cleanup removes only the direct `react_on_rails` declaration; `react_on_rails_pro` retains the matching base gem
53
+ transitively. Do not run this cleanup for `--standard-only`.
54
+
55
+ After the conditional Pro preparation, run the generator with the explicit stack flag:
56
+
57
+ ```bash
58
+ bundle exec rails generate react_on_rails:install <LANGUAGE_CHOICE> <STACK_FLAG>
59
+ ```
60
+
61
+ Use the app's declared JavaScript package manager if the generator prints a manual install command.
62
+ Review the generated initializer, Shakapacker configuration, scripts, routes, and sample files.
63
+
64
+ ## Upgrade
65
+
66
+ 1. Record the installed Ruby gem version as `CURRENT_VERSION`:
67
+
68
+ ```bash
69
+ CURRENT_VERSION="$(bundle exec ruby -rreact_on_rails/version -e 'puts ReactOnRails::VERSION')"
70
+ printf '%s\n' "$CURRENT_VERSION"
71
+ ```
72
+
73
+ 2. Choose and verify explicit Ruby and npm target versions before editing dependency pins. Record
74
+ the Ruby/tag form as `GEM_TARGET_VERSION` and the npm semver form as `NPM_TARGET_VERSION`.
75
+ Ruby prereleases use dot notation (for example, `17.0.0.rc.6`); npm prereleases use hyphen notation
76
+ (for example, `17.0.0-rc.6`). Choose both explicitly rather than deriving either from
77
+ `CURRENT_VERSION` or from the other target.
78
+
79
+ ```bash
80
+ GEM_TARGET_VERSION="<Ruby/tag version you selected>"
81
+ NPM_TARGET_VERSION="<npm semver version you selected>"
82
+ git ls-remote --exit-code --tags https://github.com/shakacode/react_on_rails.git \
83
+ "refs/tags/v${GEM_TARGET_VERSION}"
84
+ ```
85
+
86
+ 3. Read the changelog and upgrade guidance from the immutable repository tag `v<GEM_TARGET_VERSION>`:
87
+ `https://github.com/shakacode/react_on_rails/tree/v<GEM_TARGET_VERSION>`. Compare the complete
88
+ `CURRENT_VERSION..GEM_TARGET_VERSION` release range before changing dependencies:
89
+ `https://github.com/shakacode/react_on_rails/compare/v<CURRENT_VERSION>...v<GEM_TARGET_VERSION>`.
90
+
91
+ ## JavaScript target matrix
92
+
93
+ Use exactly the row matching the detected stack. Verify every relevant package version before
94
+ pinning it; checking only the base package is insufficient. Use the app's declared package manager's
95
+ registry-inspection command for every exact package spec below. Do not assume pnpm, npm, Yarn, or Bun
96
+ is available merely because another package manager is installed.
97
+
98
+ - **OSS (`--standard-only`)**: pin and verify `react-on-rails@${NPM_TARGET_VERSION}`.
99
+
100
+ - **Pro (all renderers)**: pin and verify `react-on-rails-pro@${NPM_TARGET_VERSION}`. Also pin and verify
101
+ `react-on-rails-pro-node-renderer@${NPM_TARGET_VERSION}` only when the app uses the standalone NodeRenderer.
102
+ Do not add a direct `react-on-rails` dependency to Pro or RSC apps.
103
+
104
+ - **RSC (`--rsc`)**: follow the Pro rule, including its conditional standalone NodeRenderer package
105
+ and prohibition on a direct base dependency. Select `RSC_TARGET_VERSION` independently from the
106
+ target release guidance or the target tag's
107
+ `ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN`:
108
+ `https://github.com/shakacode/react_on_rails/blob/v<GEM_TARGET_VERSION>/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb`.
109
+ Never derive `RSC_TARGET_VERSION` from `NPM_TARGET_VERSION`. Pin and verify the exact
110
+ `react-on-rails-rsc@${RSC_TARGET_VERSION}` release.
111
+
112
+ ```bash
113
+ RSC_TARGET_VERSION="<exact RSC pin from target guidance>"
114
+ ```
115
+
116
+ 4. Pin Ruby gems with `GEM_TARGET_VERSION` and regenerate the Bundler lockfile.
117
+ Apply exactly the matching JavaScript matrix row with the app's declared package manager, pin each
118
+ package to the verified exact version, and regenerate the JavaScript lockfile.
119
+ 5. Detect the app's existing stack and current generator choices from its dependencies and
120
+ configuration. Use the one preserving mapping that matches the existing stack:
121
+ - Use `--standard-only` for an existing OSS stack.
122
+ - Use `--pro` only for an existing Pro + NodeRenderer stack.
123
+ - Use `--rsc` for an existing RSC stack.
124
+
125
+ Pro + ExecJS has no preserving install-generator flag.
126
+ For that stack, skip both generator preview and apply steps; upgrade dependencies and configuration manually.
127
+ Preserve the ExecJS renderer setup. For the three preserving mappings, preserve the current stack.
128
+ Preserve every other current generator choice, such as TypeScript, Redux, or generated
129
+ test-framework choices.
130
+
131
+ 6. For a stack with a preserving mapping, first rerun the generator as a preview, substituting the
132
+ selected stack flag and the app's other current choices:
133
+
134
+ ```bash
135
+ bundle exec rails generate react_on_rails:install --pretend <STACK_FLAG> <OTHER_CURRENT_CHOICES>
136
+ ```
137
+
138
+ `--pretend` omits dependency installation and script effects; it is not a complete mutation audit.
139
+ Review the complete preview without overwriting app-owned configuration. Never prescribe or run a
140
+ bare, non-TTY generator rerun.
141
+
142
+ 7. Only for a stack with a preserving mapping, and only after reviewing and accepting the preview,
143
+ apply the same choices without `--pretend` in a clean branch or worktree:
144
+
145
+ ```bash
146
+ bundle exec rails generate react_on_rails:install <STACK_FLAG> <OTHER_CURRENT_CHOICES>
147
+ ```
148
+
149
+ Before accepting any generated change, audit `package.json` and the JavaScript lockfile against
150
+ their pre-run state. The apply step installs the generator's current dependency defaults even
151
+ though `--pretend` skipped them; preserve intentional app pins and reject unintended upgrades.
152
+
153
+ 8. Run the doctor loop, compile assets, and run focused application tests.
154
+
155
+ ```bash
156
+ bin/rails react_on_rails:doctor FORMAT=json
157
+ bundle exec rails shakapacker:compile
158
+ ```
159
+
160
+ Secondary references:
161
+ https://reactonrails.com/docs/getting-started/existing-rails-app and
162
+ https://reactonrails.com/docs/upgrading/upgrading-react-on-rails.
@@ -0,0 +1,31 @@
1
+ # React Server Components adoption (Pro)
2
+
3
+ React Server Components require React on Rails Pro. Use the generator and installed package contracts;
4
+ do not reconstruct the RSC build or renderer protocol from memory.
5
+
6
+ ## Adoption sequence
7
+
8
+ 1. Confirm the Pro gem/npm packages, Node renderer, React, React DOM, and RSC package are compatible.
9
+ 2. Preserve the app's language when running the generator: use
10
+ `bundle exec rails generate react_on_rails:rsc --typescript` for a TypeScript app and
11
+ `bundle exec rails generate react_on_rails:rsc` for a JavaScript app. Review the generated client,
12
+ server, and RSC bundle configuration.
13
+ 3. Move one leaf at a time. The `'use client'` directive marks the server-to-client boundary, not
14
+ every module that will execute on the client. Modules imported below that boundary remain client code
15
+ even when they lack the directive; place it only where a subtree needs browser APIs, state, effects,
16
+ or event handlers.
17
+ 4. Keep `.client.` and `.server.` suffixes conceptually separate: they select bundle placement and do
18
+ not classify React Server Components.
19
+ 5. Keep Rails-owned data and security decisions in Rails. Supply data through props or async props;
20
+ server components do not gain in-process access to Rails models, sessions, or cookies.
21
+ 6. Install the app safety guardrails and review the generated skill and advisory hook:
22
+
23
+ ```bash
24
+ bundle exec rake react_on_rails:install_rsc_agent_guardrails
25
+ bin/rails react_on_rails:doctor FORMAT=json
26
+ ```
27
+
28
+ Before completion, protect any RSC payload route, keep the Node renderer private, avoid sensitive
29
+ logged props or URLs, build all three bundles, and exercise navigation and hydration in a browser.
30
+
31
+ Secondary reference: https://reactonrails.com/docs/pro/react-server-components.
@@ -0,0 +1,57 @@
1
+ # Streaming SSR debugging (Pro)
2
+
3
+ Streaming requires React on Rails Pro and the Node renderer. First determine whether the selected
4
+ helper is progressive or intentionally buffered.
5
+
6
+ ## Debug in boundary order
7
+
8
+ 1. Run `bin/rails react_on_rails:doctor FORMAT=json` and resolve configuration or dependency errors.
9
+ 2. Identify the exact helper before applying streaming prerequisites.
10
+
11
+ ## Progressive helpers
12
+
13
+ For `stream_react_component`, `cached_stream_react_component`, and
14
+ `stream_react_component_with_async_props`:
15
+
16
+ 1. Confirm the controller includes `ReactOnRailsPro::Stream`, the action enters the view through
17
+ `stream_view_containing_react_components` instead of ordinary `render`, and the Node renderer
18
+ is reachable.
19
+ 2. When Suspense content should reveal progressively, confirm the boundary's child actually suspends.
20
+ 3. For `stream_react_component_with_async_props` only, confirm
21
+ `ReactOnRailsPro.configuration.enable_rsc_support` is enabled by setting
22
+ `config.enable_rsc_support = true` in the Pro initializer.
23
+ This is not a prerequisite for `stream_react_component` or `cached_stream_react_component`.
24
+ 4. For async props, start slow work inside the streaming block, emit only JSON-serializable values,
25
+ and emit or reject every prop the renderer requests.
26
+
27
+ ## Buffered helpers
28
+
29
+ For `buffered_stream_react_component`, `cached_buffered_stream_react_component`, and
30
+ `cached_static_rsc_component`:
31
+
32
+ 1. Confirm the Node renderer is reachable and inspect the complete buffered result.
33
+ 2. These helpers do not require the streaming controller wrapper: they do not require
34
+ `ReactOnRailsPro::Stream`, `stream_view_containing_react_components`, or a suspending boundary.
35
+ 3. For `cached_static_rsc_component`, set `config.enable_rsc_support = true`; disabling RSC support
36
+ omits the RSC payload generation that gives this helper its static-RSC behavior.
37
+ This setting is not a prerequisite for `buffered_stream_react_component` or
38
+ `cached_buffered_stream_react_component`.
39
+ 4. Expect the browser to receive the page only after the renderer has produced the complete result;
40
+ early shell flush and progressive Suspense reveal are not buffered-helper behavior.
41
+
42
+ ## Debug either path
43
+
44
+ Probe the origin with buffering disabled in the client, then compare the same route through each
45
+ reverse proxy or CDN. Inspect response headers, chunk arrival, compression, Rails logs, Node
46
+ renderer logs, and browser timing marks.
47
+
48
+ A useful transport probe is:
49
+
50
+ ```bash
51
+ curl --no-buffer --dump-header - https://your-app.example/streaming-route
52
+ ```
53
+
54
+ If the origin streams but the public route buffers, fix the proxy/CDN boundary. If neither streams,
55
+ reduce to one Suspense boundary and one known-slow value before changing application architecture.
56
+
57
+ Secondary reference: https://reactonrails.com/docs/pro/streaming-ssr.
@@ -251,9 +251,7 @@ module ReactOnRails
251
251
  ensure_new_app_root_route_initialized
252
252
 
253
253
  base_path = "base/base/"
254
- base_files = %w[Procfile.dev
255
- Procfile.dev-static-assets
256
- Procfile.dev-prod-assets
254
+ base_files = %w[Procfile.dev-prod-assets
257
255
  .dev-services.yml.example
258
256
  .env.example
259
257
  bin/shakapacker-precompile-hook]
@@ -262,7 +260,9 @@ module ReactOnRails
262
260
  # Exception: Redux still needs the HelloWorld controller even with RSC
263
261
  base_files << "app/controllers/hello_world_controller.rb" unless use_rsc? && !options.redux?
264
262
  base_files << "app/controllers/home_controller.rb" if generate_new_app_home_page?
265
- base_templates = %w[config/initializers/react_on_rails.rb]
263
+ base_templates = %w[Procfile.dev
264
+ Procfile.dev-static-assets
265
+ config/initializers/react_on_rails.rb]
266
266
  base_files.each { |file| copy_file("#{base_path}#{file}", file) }
267
267
  copy_react_on_rails_default_layout(base_path)
268
268
  warn_existing_hello_world_tailwind_layout
@@ -85,7 +85,7 @@ module GeneratorHelper
85
85
  end
86
86
 
87
87
  def add_documentation_reference(message, source)
88
- "#{message} \n#{source}"
88
+ "#{message}\n#{source}"
89
89
  end
90
90
 
91
91
  def print_generator_messages
@@ -492,6 +492,17 @@ module GeneratorHelper
492
492
  nil
493
493
  end
494
494
 
495
+ # Prefer Shakapacker's optional watcher binstub when the application has an executable copy. Older
496
+ # supported Shakapacker installations can still run watch mode through the required
497
+ # bin/shakapacker binstub, so React on Rails does not need to vendor a fallback watcher.
498
+ def shakapacker_watch_command
499
+ watch_binstub = File.join(destination_root, "bin/shakapacker-watch")
500
+ watch_binstub_available = File.file?(watch_binstub) && File.executable?(watch_binstub)
501
+ executable = watch_binstub_available ? "bin/shakapacker-watch" : "bin/shakapacker"
502
+
503
+ "#{executable} --watch"
504
+ end
505
+
495
506
  # Check if Shakapacker 9.0 or higher is available
496
507
  # Returns true if Shakapacker >= 9.0, false otherwise
497
508
  #
@@ -86,16 +86,22 @@ module ReactOnRails
86
86
  desc: "Write AI-agent guidance files (AGENTS.md + editor pointers). Default: true"
87
87
 
88
88
  # --pro
89
+ # No static default: prompt suppression needs to distinguish an omitted flag from --no-pro.
89
90
  class_option :pro,
90
91
  type: :boolean,
91
- default: false,
92
- desc: "Install React on Rails Pro with Node Renderer. Default: false"
92
+ desc: "Install React on Rails Pro with Node Renderer"
93
93
 
94
94
  # --rsc
95
+ # No static default: prompt suppression needs to distinguish an omitted flag from --no-rsc.
95
96
  class_option :rsc,
97
+ type: :boolean,
98
+ desc: "Install React Server Components support (includes Pro)"
99
+
100
+ # --standard-only
101
+ class_option :standard_only,
96
102
  type: :boolean,
97
103
  default: false,
98
- desc: "Install React Server Components support (includes Pro). Default: false"
104
+ desc: "Install only the open-source package; cannot be combined with --pro or --rsc"
99
105
 
100
106
  # Hidden option: allows tests (and advanced users) to signal that Shakapacker
101
107
  # was just installed, triggering force-overwrite of shakapacker.yml with RoR's template.
@@ -196,6 +202,7 @@ module ReactOnRails
196
202
  # This is inherited by all invoked generators and persists through Rails initialization
197
203
  # See lib/react_on_rails/engine.rb for the validation skip logic
198
204
  ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true"
205
+ prompt_for_pro_features_if_applicable
199
206
 
200
207
  if installation_prerequisites_met? || options.ignore_warnings?
201
208
  invoke_generators
@@ -230,6 +237,40 @@ module ReactOnRails
230
237
 
231
238
  private
232
239
 
240
+ def use_pro?
241
+ return @interactive_pro_selection if defined?(@interactive_pro_selection)
242
+
243
+ !!super
244
+ end
245
+
246
+ def prompt_for_pro_features_if_applicable
247
+ validate_product_stack_choice!
248
+ return if options.new_app? || explicit_product_stack_choice? || !interactive_install_session?
249
+
250
+ say "React on Rails Pro is free for evaluation; production use requires a subscription."
251
+ say "Learn more: https://reactonrails.com/docs/pro/upgrading-to-pro/"
252
+ answer = ask(
253
+ "Enable React on Rails Pro features (Node Renderer and streaming SSR; RSC available separately)? [Y/n]",
254
+ :cyan
255
+ )
256
+ normalized_answer = answer.to_s.strip
257
+ @interactive_pro_selection = normalized_answer.empty? || normalized_answer.match?(/\A(?:y|yes)\z/i)
258
+ end
259
+
260
+ def explicit_product_stack_choice?
261
+ options.key?(:pro) || options.key?(:rsc) || options.standard_only?
262
+ end
263
+
264
+ def interactive_install_session?
265
+ !ReactOnRails::GitUtils.truthy_env?(ENV.fetch("CI", nil)) && $stdin.tty? && $stdout.tty?
266
+ end
267
+
268
+ def validate_product_stack_choice!
269
+ return unless options.standard_only? && (options.pro? || options.rsc?)
270
+
271
+ raise Thor::Error, "--standard-only cannot be combined with --pro or --rsc"
272
+ end
273
+
233
274
  # Fresh-install context: default to Rspack (when Shakapacker supports it) unless the
234
275
  # app already declares a bundler. See GeneratorHelper#fresh_install_rspack_default.
235
276
  # NOTE: BaseGenerator#rspack_bundler_default is an intentional twin of this override
@@ -866,8 +907,12 @@ module ReactOnRails
866
907
  end
867
908
 
868
909
  def product_stack_install_flag
869
- return "--rsc" if options.rsc?
870
- return "--pro" if options.pro?
910
+ return "--rsc" if use_rsc?
911
+ return "--pro" if use_pro?
912
+ return "--standard-only" if options.standard_only?
913
+ return "--no-rsc" if options.key?(:rsc)
914
+ return "--no-pro" if options.key?(:pro)
915
+ return "--standard-only" if defined?(@interactive_pro_selection)
871
916
 
872
917
  nil
873
918
  end
@@ -177,7 +177,7 @@ module ReactOnRails
177
177
  end
178
178
 
179
179
  def add_js_dependencies
180
- using_pro = respond_to?(:use_pro?) && use_pro?
180
+ using_pro = respond_to?(:use_pro?, true) && use_pro?
181
181
  using_rsc = respond_to?(:use_rsc?) && use_rsc?
182
182
  # Pro package includes react-on-rails, so skip base package when using Pro
183
183
  add_react_on_rails_package unless using_pro
@@ -40,9 +40,11 @@ module ReactOnRails
40
40
  )
41
41
 
42
42
  setup_pro
43
- add_pro_npm_dependencies
44
- update_imports_to_pro_package unless options[:invoked_by_install]
45
- print_success_message unless options[:invoked_by_install]
43
+ unless options[:invoked_by_install]
44
+ add_pro_npm_dependencies
45
+ update_imports_to_pro_package
46
+ print_success_message
47
+ end
46
48
  else
47
49
  GeneratorMessages.add_error(<<~MSG.strip)
48
50
  🚫 React on Rails Pro generator prerequisites not met!
@@ -33,6 +33,38 @@ module ReactOnRails
33
33
  AUTO_INSTALL_TIMEOUT = 120
34
34
  TERMINATION_GRACE_PERIOD = 5
35
35
 
36
+ # Loader helpers emitted by
37
+ # templates/base/base/config/webpack/serverWebpackConfig.js.tt.
38
+ #
39
+ # Keep both blocks byte-identical to that template: a fresh `--pro` install renders
40
+ # the template while a standalone Pro upgrade patches an existing base config, and
41
+ # the two must produce the same source text so drift is detectable (issue #4786).
42
+ GET_LOADER_PATH_JS = <<~JS
43
+ // Normalizes an entry of a webpack/rspack `rule.use` array to its loader path.
44
+ // Entries may be a bare string, a `{ loader, options }` object, or null.
45
+ function getLoaderPath(item) {
46
+ if (typeof item === 'string') return item;
47
+ if (item && typeof item.loader === 'string') return item.loader;
48
+ return '';
49
+ }
50
+ JS
51
+
52
+ EXTRACT_LOADER_JS = <<~JS
53
+ function extractLoader(rule, loaderName) {
54
+ if (!Array.isArray(rule.use)) return null;
55
+ return rule.use.find((item) => getLoaderPath(item).includes(loaderName));
56
+ }
57
+ JS
58
+
59
+ BUNDLER_REQUIRE_PATTERN =
60
+ %r{(const bundler = config\.assets_bundler.*\n.*require\('@rspack/core'\).*\n.*: require\('webpack'\);)}
61
+
62
+ # Matches any declaration of the getLoaderPath symbol, however it is written. The
63
+ # emitted extractLoader calls getLoaderPath, so we must never add a second
64
+ # declaration: `function` next to an existing `const` is a SyntaxError, not a
65
+ # silent shadow, and the generated config would fail to parse in Node.
66
+ GET_LOADER_PATH_DECLARATION = /(?:function\s+getLoaderPath\s*\(|(?:const|let|var)\s+getLoaderPath\s*=)/
67
+
36
68
  # Main entry point for Pro setup.
37
69
  # Orchestrates creation of all Pro-related files and configuration.
38
70
  #
@@ -488,25 +520,25 @@ module ReactOnRails
488
520
  # Skip if extractLoader already exists
489
521
  return if content.include?("function extractLoader")
490
522
 
491
- extract_loader_code = <<~JS.chomp
492
-
493
-
494
- function extractLoader(rule, loaderName) {
495
- if (!Array.isArray(rule.use)) return null;
496
- return rule.use.find((item) => {
497
- if (!item) return false;
498
- const testValue = typeof item === 'string' ? item : (typeof item.loader === 'string' ? item.loader : '');
499
- return testValue.includes(loaderName);
500
- });
501
- }
502
- JS
503
-
504
- # Insert after bundler require line
505
- gsub_file(
506
- webpack_config,
507
- %r{(const bundler = config\.assets_bundler.*\n.*require\('@rspack/core'\).*\n.*: require\('webpack'\);)},
508
- "\\1#{extract_loader_code}"
509
- )
523
+ if content.include?(GET_LOADER_PATH_JS)
524
+ # Config rendered by the current base template: append extractLoader directly after
525
+ # the shared getLoaderPath helper so the result matches the template's Pro output.
526
+ gsub_file(webpack_config, GET_LOADER_PATH_JS, "#{GET_LOADER_PATH_JS}\n#{EXTRACT_LOADER_JS}")
527
+ elsif content.match?(GET_LOADER_PATH_DECLARATION)
528
+ # The app already declares getLoaderPath but has customized it (reformatted, recommented,
529
+ # or rewritten as an arrow function). Reuse whatever is there and emit extractLoader only.
530
+ # Emitting our own copy would redeclare the identifier, which is a SyntaxError next to an
531
+ # existing const/let and silent shadowing next to another function declaration.
532
+ gsub_file(webpack_config, BUNDLER_REQUIRE_PATTERN, "\\1\n\n#{EXTRACT_LOADER_JS}".chomp)
533
+ else
534
+ # Base config generated before getLoaderPath existed: emit both helpers after the
535
+ # bundler require so extractLoader's dependency is present.
536
+ gsub_file(
537
+ webpack_config,
538
+ BUNDLER_REQUIRE_PATTERN,
539
+ "\\1\n\n#{GET_LOADER_PATH_JS}\n#{EXTRACT_LOADER_JS}".chomp
540
+ )
541
+ end
510
542
  end
511
543
 
512
544
  def add_babel_ssr_caller_to_server_config(webpack_config, content)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "react_on_rails/agent_guardrails"
4
5
  require_relative "generator_helper"
5
6
  require_relative "generator_messages"
6
7
  require_relative "js_dependency_manager"
@@ -52,7 +53,8 @@ module ReactOnRails
52
53
  if options[:invoked_by_install] || prerequisites_met?
53
54
  warn_about_react_version_for_rsc(force: true)
54
55
  setup_rsc
55
- add_rsc_npm_dependencies
56
+ add_rsc_npm_dependencies unless options[:invoked_by_install]
57
+ install_agent_guardrails
56
58
  print_success_message unless options[:invoked_by_install]
57
59
  else
58
60
  GeneratorMessages.add_error(<<~MSG.strip)
@@ -120,6 +122,21 @@ module ReactOnRails
120
122
  say "✅ RSC npm dependencies added", :green
121
123
  end
122
124
 
125
+ def install_agent_guardrails
126
+ if options[:pretend]
127
+ say_status :pretend, ".claude/ (RSC agent guardrails)", :yellow
128
+ return
129
+ end
130
+
131
+ say "🛡️ Installing RSC agent guardrails (rsc-app-safety skill + advisory hook)...", :yellow
132
+ ReactOnRails::AgentGuardrails.install(destination_root, skip_existing: options[:skip]).each do |action|
133
+ say " #{action}"
134
+ end
135
+ say "✅ RSC agent guardrails installed", :green
136
+ rescue ReactOnRails::AgentGuardrails::Error, SystemCallError => e
137
+ say "⚠️ RSC agent guardrail installation incomplete: #{e.message}", :yellow
138
+ end
139
+
123
140
  def print_success_message
124
141
  GeneratorMessages.add_info(<<~MSG)
125
142
  Next steps: