Harden migration generator for legacy shakapacker setups#2901
Harden migration generator for legacy shakapacker setups#2901
Conversation
WalkthroughThis PR updates docs and generators to seed package manager info from lockfiles, remove duplicate Browserslist entries, detect JSX in Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant InstallGen as Install Generator
participant JSDepMgr as JS Dependency Manager
participant Shell as Shell/Open3
participant FS as File System
User->>InstallGen: run generator
InstallGen->>FS: inspect lockfiles & package.json
alt packageManager missing
InstallGen->>Shell: run <pm> --version
Shell-->>InstallGen: return version
InstallGen->>FS: write packageManager to package.json
end
InstallGen->>FS: check .browserslistrc & package.json
alt conflict found
InstallGen->>FS: remove browserslist key from package.json
end
InstallGen->>FS: scan app/javascript/**/*.js for JSX
alt JSX found & swc configured
InstallGen->>FS: update config/shakapacker.yml -> javascript_transpiler: "babel"
InstallGen->>JSDepMgr: add_packages(["babel-loader"], dev: true)
end
InstallGen->>JSDepMgr: setup_js_dependencies
JSDepMgr->>JSDepMgr: attempt package_json manager add/install
alt primary add fails or package_json unavailable
JSDepMgr->>Shell: system <pm> install args
Shell-->>JSDepMgr: success/failure
end
JSDepMgr-->>InstallGen: install result
InstallGen-->>User: complete (with warnings if any)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| return unless version | ||
|
|
||
| package_json_content["packageManager"] = "#{manager}@#{version}" | ||
| File.write("package.json", "#{JSON.pretty_generate(package_json_content)}\n") |
There was a problem hiding this comment.
JSON.pretty_generate reformats the entire package.json. This will normalize all whitespace, ordering, and trailing commas, creating a noisy diff and potentially losing any non-standard formatting the user intentionally has. The same issue applies to resolve_browserslist_conflict_after_shakapacker_install on line 669.
A safer approach would be to do a targeted string-level insertion (e.g. injecting "packageManager": "..." with a regex or a minimal JSON patch) so that only the required change appears in git diff, rather than a full reformat.
| def jsx_in_js_files_present? | ||
| Dir.glob("app/javascript/**/*.js").any? do |path| | ||
| content = File.read(path) | ||
| content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>\s*$/) |
There was a problem hiding this comment.
This regex has a meaningful false-positive risk. The pattern <\s*[A-Za-z][\w:-]*(\s|>|/) will match:
- HTML strings in test fixtures:
assert_match "<div class=...", response.body - HTML in comments:
# returns <Component/> - String templates:
fetch("/<div>") - Comparison operators that happen to align:
x < abc > 0(only if no space before<)
A false positive will silently switch the project from SWC (fast) to Babel (slower) without the user asking for it, which could be surprising in non-migration runs.
Consider restricting the scan to known JSX entry-point files (e.g. files that also contain import React or from 'react') to reduce false positives, or at minimum gate this only during initial migration generator invocations where --from-react-rails is set.
| say "⚙️ Detected JSX in .js files; switching shakapacker javascript_transpiler to babel for compatibility", | ||
| :yellow | ||
| set_javascript_transpiler_to_babel | ||
| add_packages(["babel-loader"], dev: true) |
There was a problem hiding this comment.
The return values of add_packages and install_js_dependencies are not checked here. If add_packages(["babel-loader"], dev: true) fails (returns false), install_js_dependencies is still called, and both failures are silently swallowed. At a minimum, it's worth surfacing a warning to the user when either step fails, so they know to manually add babel-loader.
| base_args + packages | ||
| end | ||
|
|
||
| def dev_flag_for(package_manager) |
There was a problem hiding this comment.
dev_flag_for returns nil when the case doesn't match. While fallback_package_manager currently guarantees only %w[npm yarn pnpm bun], the else-less case means any future caller that passes an unguarded value would silently append nil to the install_args array passed to system(), causing a TypeError at runtime. Add an explicit else branch that raises or returns a sensible default:
else "--save-dev" # safe fallback for unknown managers| install_args = build_install_args(package_manager, dev, packages_to_install) | ||
|
|
||
| system(*install_args) | ||
| rescue StandardError |
There was a problem hiding this comment.
All StandardErrors are silently swallowed here with no log output. This makes it very hard to diagnose why fallback installs fail. Even a simple GeneratorMessages.add_warning("⚠️ Fallback install failed: #{e.message}") before returning false would help users debug issues like Errno::ENOENT (command not found) or permission errors.
| GeneratorMessages.add_warning("⚠️ Failed to clean browserslist conflict: #{e.message}") | ||
| end | ||
|
|
||
| def detect_package_manager_from_lockfiles |
There was a problem hiding this comment.
Two separate package-manager detection mechanisms are now in play: detect_package_manager_from_lockfiles here (lockfile-based) and GeneratorMessages.detect_package_manager used by fallback_package_manager in js_dependency_manager.rb. These could return different values for the same project (e.g. the lockfile says yarn but GeneratorMessages.detect_package_manager returns npm if it uses a different heuristic). This could result in seed_package_manager_in_package_json_from_lockfile! writing [email protected] but the fallback installer then calling npm install.
Consider unifying both call sites to use the same detection method.
Review SummaryThe PR's goal is solid — making the install generator resilient for legacy shakapacker setups is a real pain point and the fallback approach is well-structured. A few issues worth addressing before merge: Medium priority
JSX regex false positives in Inconsistent package-manager detection (lines 677 vs Low priorityUnchecked return values in Silent error swallowing in
What looks good
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4bfa1fa9ac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| set_javascript_transpiler_to_babel | ||
| add_packages(["babel-loader"], dev: true) | ||
| install_js_dependencies |
There was a problem hiding this comment.
Add @babel/preset-react when switching SWC projects to Babel
This branch switches javascript_transpiler from SWC to Babel for JSX-in-.js, but it only adds babel-loader. In SWC-default installs, @babel/preset-react is not added on this path, yet the generated babel.config.js still references it, so compilation fails with a missing preset unless the app already had it installed. Please add the preset dependency when this compatibility switch runs.
Useful? React with 👍 / 👎.
| packages.reject do |package_spec| | ||
| package_name = package_name_from_spec(package_spec) | ||
| package_name && existing.include?(package_name) |
There was a problem hiding this comment.
Preserve versioned installs in fallback package filtering
The fallback installer treats a package as satisfied by name only, so versioned specs are dropped when a package already exists. In legacy setups where package_json is unavailable, requests like react-on-rails@<gem_version> or react@~19.0.4 are skipped if any react-on-rails/react entry exists, leaving stale versions in package.json and potentially mismatching required runtime versions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Switching to babel missing
@babel/preset-reactinstallation- When JSX-in-.js forces a switch from SWC to babel, the generator now also installs Babel React preset dependencies before running install.
- ✅ Fixed: Fallback package install missing exact version pinning flag
- Fallback install commands now include exact-version flags for npm, pnpm, yarn, and bun so dependency specs remain pinned.
- ✅ Fixed: Duplicated lockfile detection logic across three locations
- Lockfile-based package manager detection and supported-manager validation were centralized in GeneratorMessages and reused by the other generator code paths.
Or push these changes by commenting:
@cursor push c1d727b37c
Preview (c1d727b37c)
diff --git a/react_on_rails/lib/generators/react_on_rails/generator_messages.rb b/react_on_rails/lib/generators/react_on_rails/generator_messages.rb
--- a/react_on_rails/lib/generators/react_on_rails/generator_messages.rb
+++ b/react_on_rails/lib/generators/react_on_rails/generator_messages.rb
@@ -5,6 +5,7 @@
module GeneratorMessages
PRO_UPGRADE_HINT = "\n\n 💎 For RSC, streaming SSR, and 10-100x faster SSR, try React on Rails Pro:" \
"\n #{Rainbow('https://reactonrails.com/docs/pro/upgrading-to-pro/').cyan.underline}".freeze
+ SUPPORTED_PACKAGE_MANAGERS = %w[npm pnpm yarn bun].freeze
# rubocop:disable Metrics/ClassLength
class << self
@@ -106,17 +107,25 @@
# this while the current working directory is the target Rails app root.
def detect_package_manager
env_package_manager = ENV.fetch("REACT_ON_RAILS_PACKAGE_MANAGER", nil)&.strip&.downcase
- return env_package_manager if %w[npm pnpm yarn bun].include?(env_package_manager)
+ return env_package_manager if supported_package_manager?(env_package_manager)
- # Check for lock files to determine package manager
+ # Default to npm (Shakapacker 8.x default) - covers package-lock.json and no lockfile
+ detect_package_manager_from_lockfiles || "npm"
+ end
+
+ def detect_package_manager_from_lockfiles
return "yarn" if File.exist?("yarn.lock")
return "pnpm" if File.exist?("pnpm-lock.yaml")
return "bun" if File.exist?("bun.lock") || File.exist?("bun.lockb")
+ return "npm" if File.exist?("package-lock.json")
- # Default to npm (Shakapacker 8.x default) - covers package-lock.json and no lockfile
- "npm"
+ nil
end
+ def supported_package_manager?(package_manager)
+ SUPPORTED_PACKAGE_MANAGERS.include?(package_manager)
+ end
+
private
def build_render_example(component_name:, route:, rsc:)
diff --git a/react_on_rails/lib/generators/react_on_rails/install_generator.rb b/react_on_rails/lib/generators/react_on_rails/install_generator.rb
--- a/react_on_rails/lib/generators/react_on_rails/install_generator.rb
+++ b/react_on_rails/lib/generators/react_on_rails/install_generator.rb
@@ -250,6 +250,7 @@
:yellow
set_javascript_transpiler_to_babel
add_packages(["babel-loader"], dev: true)
+ add_babel_react_dependencies
install_js_dependencies
end
@@ -675,12 +676,7 @@
end
def detect_package_manager_from_lockfiles
- return "yarn" if File.exist?("yarn.lock")
- return "pnpm" if File.exist?("pnpm-lock.yaml")
- return "bun" if File.exist?("bun.lock") || File.exist?("bun.lockb")
- return "npm" if File.exist?("package-lock.json")
-
- nil
+ GeneratorMessages.detect_package_manager_from_lockfiles
end
def detect_package_manager_version(package_manager)
diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
--- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
+++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
@@ -516,18 +516,17 @@
def fallback_package_manager
package_manager = GeneratorMessages.detect_package_manager
- allowed_package_managers = %w[npm yarn pnpm bun].freeze
- return package_manager if allowed_package_managers.include?(package_manager)
+ return package_manager if GeneratorMessages.supported_package_manager?(package_manager)
"npm"
end
def build_install_args(package_manager, dev, packages)
base_commands = {
- "npm" => %w[npm install],
- "yarn" => %w[yarn add],
- "pnpm" => %w[pnpm add],
- "bun" => %w[bun add]
+ "npm" => %w[npm install --save-exact],
+ "yarn" => %w[yarn add --exact],
+ "pnpm" => %w[pnpm add --save-exact],
+ "bun" => %w[bun add --exact]
}
base_args = base_commands.fetch(package_manager).dup
diff --git a/react_on_rails/spec/react_on_rails/generators/generator_messages_spec.rb b/react_on_rails/spec/react_on_rails/generators/generator_messages_spec.rb
--- a/react_on_rails/spec/react_on_rails/generators/generator_messages_spec.rb
+++ b/react_on_rails/spec/react_on_rails/generators/generator_messages_spec.rb
@@ -96,6 +96,10 @@
end
describe ".detect_package_manager" do
+ before do
+ ENV.delete("REACT_ON_RAILS_PACKAGE_MANAGER")
+ end
+
it "returns bun when bun.lock exists" do
allow(File).to receive(:exist?).and_call_original
allow(File).to receive(:exist?).with("yarn.lock").and_return(false)
@@ -114,5 +118,16 @@
expect(described_class.detect_package_manager).to eq("bun")
end
+
+ it "returns npm when package-lock.json exists" do
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with("yarn.lock").and_return(false)
+ allow(File).to receive(:exist?).with("pnpm-lock.yaml").and_return(false)
+ allow(File).to receive(:exist?).with("bun.lock").and_return(false)
+ allow(File).to receive(:exist?).with("bun.lockb").and_return(false)
+ allow(File).to receive(:exist?).with("package-lock.json").and_return(true)
+
+ expect(described_class.detect_package_manager).to eq("npm")
+ end
end
end
diff --git a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
--- a/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
+++ b/react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
@@ -3109,10 +3109,11 @@
return <div>Hello</div>
}
JS
- allow(install_generator).to receive_messages(using_swc?: true, add_packages: true, install_js_dependencies: true)
+ allow(install_generator).to receive_messages(using_swc?: true, add_packages: true, add_babel_react_dependencies: true,
+ install_js_dependencies: true)
end
- it "switches to babel and installs babel-loader when JSX is found in .js files" do
+ it "switches to babel and installs babel dependencies when JSX is found in .js files" do
Dir.chdir(destination_root) do
install_generator.send(:ensure_jsx_in_js_compatibility)
end
@@ -3120,6 +3121,7 @@
shakapacker_yml = File.read(File.join(destination_root, "config/shakapacker.yml"))
expect(shakapacker_yml).to include('javascript_transpiler: "babel"')
expect(install_generator).to have_received(:add_packages).with(["babel-loader"], dev: true)
+ expect(install_generator).to have_received(:add_babel_react_dependencies)
expect(install_generator).to have_received(:install_js_dependencies)
end
end
diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
--- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
+++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
@@ -196,7 +196,7 @@
instance.add_npm_dependencies_result = false
result = instance.send(:add_packages, %w[package1])
expect(result).to be(true)
- expect(instance.system_calls).to include(%w[npm install package1])
+ expect(instance.system_calls).to include(%w[npm install --save-exact package1])
end
it "returns false when add_npm_dependencies and fallback both fail" do
@@ -238,7 +238,7 @@
instance.add_npm_dependencies_result = false
result = instance.send(:add_package, "some-package")
expect(result).to be(true)
- expect(instance.system_calls).to include(%w[npm install some-package])
+ expect(instance.system_calls).to include(%w[npm install --save-exact some-package])
end
it "returns false when fallback install fails" doYou can send follow-ups to this agent here.
Greptile SummaryThis PR hardens the Key changes:
Two P2 concerns:
Confidence Score: 5/5Safe to merge; all remaining findings are P2 improvement suggestions that do not block the primary migration use case. The PR solves a real, verified problem. Fallback logic is well-guarded, warning deduplication is correct, and the Ripper-validated RSpec injection fix is a clear improvement. Both P2 findings are non-breaking in the primary migration scenario. install_generator.rb (ensure_jsx_in_js_compatibility) and js_dependency_manager.rb (dev_flag_for) warrant a second look for the P2 improvements. Important Files Changed
Sequence DiagramsequenceDiagram
participant IG as InstallGenerator
participant SPI as seed_package_manager!
participant SHK as shakapacker:install
participant BLC as resolve_browserslist
participant SRD as setup_react_dependencies
participant EJX as ensure_jsx_in_js_compat
participant JDM as JsDependencyManager
participant PM as Package Manager CLI
participant PJG as package_json gem
IG->>SPI: seed_package_manager_in_package_json_from_lockfile!
SPI->>SPI: detect PM from lockfiles
SPI->>PM: pm --version (Open3.capture3)
PM-->>SPI: version string
SPI->>SPI: write packageManager field to package.json
IG->>SHK: bundle exec rails shakapacker:install
SHK-->>IG: success
IG->>BLC: resolve_browserslist_conflict_after_shakapacker_install
BLC->>BLC: if .browserslistrc AND package.json[browserslist] exists, remove key
IG->>SRD: setup_react_dependencies
SRD->>JDM: add_transpiler_dependencies (using_swc? cached true)
JDM->>PJG: add @swc/core swc-loader
PJG-->>JDM: success OR LoadError
JDM->>PM: fallback npm/yarn add if package_json unavailable
IG->>EJX: ensure_jsx_in_js_compatibility
EJX->>EJX: jsx_in_js_files_present?
EJX->>EJX: set_javascript_transpiler_to_babel
EJX->>JDM: add_packages babel-loader dev true
JDM->>PJG: add_npm_dependencies OR fallback PM CLI
EJX->>JDM: install_js_dependencies
Reviews (1): Last reviewed commit: "Harden migration generator for legacy sh..." | Re-trigger Greptile |
| def ensure_jsx_in_js_compatibility | ||
| return if options[:pretend] | ||
| return unless using_swc? | ||
| return unless jsx_in_js_files_present? | ||
|
|
||
| say "⚙️ Detected JSX in .js files; switching shakapacker javascript_transpiler to babel for compatibility", | ||
| :yellow | ||
| set_javascript_transpiler_to_babel | ||
| add_packages(["babel-loader"], dev: true) | ||
| install_js_dependencies | ||
| end |
There was a problem hiding this comment.
@babel/preset-react not added when switching from SWC to Babel
ensure_jsx_in_js_compatibility switches the transpiler to Babel and adds babel-loader, but does not add @babel/preset-react (the BABEL_REACT_DEPENDENCIES constant). babel-loader is only the Webpack loader that invokes Babel; processing JSX also requires @babel/preset-react (or an equivalent preset) in the Babel config.
In the primary migration scenario (from react-rails) this is typically safe — the user already has @babel/preset-react in package.json from their existing Babel setup. However, an app that arrived at SWC + JSX-in-.js via another path (e.g. upgrading Shakapacker to 9.3+ on a project that already had .js JSX files) would get a broken transpiler setup after this generator runs.
Consider calling add_babel_react_dependencies alongside the babel-loader install so the method is self-contained:
def ensure_jsx_in_js_compatibility
return if options[:pretend]
return unless using_swc?
return unless jsx_in_js_files_present?
say "⚙️ Detected JSX in .js files; switching shakapacker javascript_transpiler to babel for compatibility",
:yellow
set_javascript_transpiler_to_babel
add_packages(["babel-loader"], dev: true)
add_babel_react_dependencies # also adds @babel/preset-react
install_js_dependencies
end| def ensure_jsx_in_js_compatibility | ||
| return if options[:pretend] | ||
| return unless using_swc? | ||
| return unless jsx_in_js_files_present? | ||
|
|
||
| say "⚙️ Detected JSX in .js files; switching shakapacker javascript_transpiler to babel for compatibility", | ||
| :yellow | ||
| set_javascript_transpiler_to_babel | ||
| add_packages(["babel-loader"], dev: true) | ||
| install_js_dependencies | ||
| end |
There was a problem hiding this comment.
Stale
@using_swc cache leaves unused SWC packages after Babel switch
using_swc? in GeneratorHelper is memoised into @using_swc. By the time ensure_jsx_in_js_compatibility is reached, setup_react_dependencies → setup_js_dependencies → add_transpiler_dependencies has already been called, which:
- Evaluated
using_swc?→true(memoised) - Added
@swc/coreandswc-loadertopackage.json - Skipped
add_babel_react_dependencies
When ensure_jsx_in_js_compatibility subsequently changes shakapacker.yml to babel, @using_swc is still true. The SWC packages already added to package.json remain there, unused by Babel. Downstream code in the same generator instance that calls using_swc? would also get the stale value.
The fix is to invalidate the cache after the transpiler switch:
def set_javascript_transpiler_to_babel
shakapacker_config_path = "config/shakapacker.yml"
return unless File.exist?(shakapacker_config_path)
gsub_file(
shakapacker_config_path,
/^(\s*javascript_transpiler:\s*)["']?swc["']?(\s*(?:#.*)?)$/,
'\1"babel"\2'
)
# Invalidate the cached value so subsequent using_swc? calls reflect the change
@using_swc = false
end| def dev_flag_for(package_manager) | ||
| case package_manager | ||
| when "npm", "pnpm" then "--save-dev" | ||
| when "yarn", "bun" then "--dev" | ||
| end |
There was a problem hiding this comment.
dev_flag_for missing else branch — returns nil silently
The case statement has no else clause, so Ruby returns nil for any package manager not explicitly listed. If that happens, build_install_args appends nil to the args array (e.g., ["npm", "install", nil]), which would cause Kernel#system to misbehave.
fallback_package_manager already constrains the value to the four known managers, so this cannot fire today. Still, an explicit guard prevents silent breakage if the allowed list ever changes:
| def dev_flag_for(package_manager) | |
| case package_manager | |
| when "npm", "pnpm" then "--save-dev" | |
| when "yarn", "bun" then "--dev" | |
| end | |
| def dev_flag_for(package_manager) | |
| case package_manager | |
| when "npm", "pnpm" then "--save-dev" | |
| when "yarn", "bun" then "--dev" | |
| else | |
| raise ArgumentError, "Unknown package manager for dev flag: #{package_manager}" | |
| end | |
| end |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
react_on_rails/lib/generators/react_on_rails/install_generator.rb (1)
802-809: Consider relaxing the JSX fragment regex anchor.The fragment pattern
/<\s*>\s*$/only matches<>at the end of a line. Fragments like<>content</>or<>followed by content on the same line won't be detected.💡 Suggested fix to detect mid-line fragments
def jsx_in_js_files_present? Dir.glob("app/javascript/**/*.js").any? do |path| content = File.read(path) - content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>\s*$/) + content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>/) rescue StandardError false end end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@react_on_rails/lib/generators/react_on_rails/install_generator.rb` around lines 802 - 809, The regex that checks for JSX fragments in jsx_in_js_files_present? is too strict (/<\s*>\s*$/) because it only matches an opening fragment at line end; update that check to look for an opening fragment anywhere in the file (for example use an unanchored pattern like /<\s*>/ or a slightly more permissive /<\s*>/ that matches mid-line fragments such as <>content</>) so the method detects fragments like <>content</> or <> followed by inline content; modify the pattern used in the content.match? call inside jsx_in_js_files_present? accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/oss/migrating/migrating-from-react-rails.md`:
- Around line 12-14: Update the example for setting the packageManager field so
it is manager-agnostic: when referencing package.json and the packageManager
field alongside lockfiles (yarn.lock, pnpm-lock.yaml, bun.lock*), replace the
hardcoded example `[email protected]` with either a set of manager-specific examples
(e.g., npm@<version>, yarn@<version>, pnpm@<version>) or a clear instruction to
substitute the actual tool and exact version used by the project; ensure the
text explicitly lists npm, yarn, and pnpm as examples and shows the format
consumers should use for their chosen manager/version.
In `@react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb`:
- Around line 545-552: The current filter_missing_packages method incorrectly
treats versioned specs like "[email protected]" or "react@~19.0.4" as
satisfied if the package name exists; update filter_missing_packages to detect
when package_spec includes a version/range (use package_name_from_spec to get
the name and check package_spec for a version token or regex) and, in that case,
compare the spec's version/range against the installed version (look up
installed versions via existing_package_names/extend or an
existing_package_versions helper); only reject the spec if the installed version
satisfies the spec range, otherwise keep it so the installer will add/upgrade
the package.
- Around line 467-470: The add_npm_dependencies method currently swallows
pj.manager.add(...) results and returns true unconditionally, preventing
add_packages from falling back to install_packages_with_fallback; update
add_npm_dependencies to capture the boolean result of pj.manager.add(packages,
...) and return that value (so add_packages sees falsy and calls
install_packages_with_fallback), ensuring symbols to change are
add_npm_dependencies and the pj.manager.add(...) call used by
add_packages/install_packages_with_fallback.
---
Nitpick comments:
In `@react_on_rails/lib/generators/react_on_rails/install_generator.rb`:
- Around line 802-809: The regex that checks for JSX fragments in
jsx_in_js_files_present? is too strict (/<\s*>\s*$/) because it only matches an
opening fragment at line end; update that check to look for an opening fragment
anywhere in the file (for example use an unanchored pattern like /<\s*>/ or a
slightly more permissive /<\s*>/ that matches mid-line fragments such as
<>content</>) so the method detects fragments like <>content</> or <> followed
by inline content; modify the pattern used in the content.match? call inside
jsx_in_js_files_present? accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 520f6044-a31f-453b-aff3-e0b8d220dbd2
📒 Files selected for processing (8)
docs/oss/migrating/migrating-from-react-rails.mdreact_on_rails/lib/generators/react_on_rails/base_generator.rbreact_on_rails/lib/generators/react_on_rails/generator_helper.rbreact_on_rails/lib/generators/react_on_rails/install_generator.rbreact_on_rails/lib/generators/react_on_rails/js_dependency_manager.rbreact_on_rails/spec/react_on_rails/generators/base_generator_spec.rbreact_on_rails/spec/react_on_rails/generators/install_generator_spec.rbreact_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
| def filter_missing_packages(packages) | ||
| existing = existing_package_names | ||
| return packages if existing.empty? | ||
|
|
||
| packages.reject do |package_spec| | ||
| package_name = package_name_from_spec(package_spec) | ||
| package_name && existing.include?(package_name) | ||
| end |
There was a problem hiding this comment.
Don't treat versioned specs as already satisfied by name alone.
This filter drops the version/range before checking package.json, so react-on-rails@..., Pro package pins, and the RSC react@~19.0.4 pins are all skipped as soon as the bare package name exists. On legacy apps the fallback path can therefore return success while leaving an incompatible version in place.
♻️ Possible fix
def filter_missing_packages(packages)
existing = existing_package_names
return packages if existing.empty?
packages.reject do |package_spec|
package_name = package_name_from_spec(package_spec)
- package_name && existing.include?(package_name)
+ package_name == package_spec && existing.include?(package_name)
end
endPlease add a regression around a versioned spec such as react-on-rails@... or react@~19.0.4 so this does not silently regress.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb` around
lines 545 - 552, The current filter_missing_packages method incorrectly treats
versioned specs like "[email protected]" or "react@~19.0.4" as satisfied if
the package name exists; update filter_missing_packages to detect when
package_spec includes a version/range (use package_name_from_spec to get the
name and check package_spec for a version token or regex) and, in that case,
compare the spec's version/range against the installed version (look up
installed versions via existing_package_names/extend or an
existing_package_versions helper); only reject the spec if the installed version
satisfies the spec range, otherwise keep it so the installer will add/upgrade
the package.
| else | ||
| pj.manager.add(packages, exact: true) | ||
| end | ||
| !result.nil? |
There was a problem hiding this comment.
Fragile return-value check may suppress the fallback path
pj.manager.add(...) returns the parsed package.json Hash on success (always truthy/non-nil for a real write). But the check !result.nil? would return true even if the gem returned false for some reason—hiding a real failure and skipping the fallback install.
The previous code simply fell through to true after a successful call. A clearer approach:
| !result.nil? | |
| !result.nil? && result != false |
Or, since the gem either succeeds (returns something non-nil/non-false) or raises, the original true pattern was safer. At minimum the intent should be result (truthy check) rather than !result.nil?.
| def jsx_in_js_files_present? | ||
| Dir.glob("app/javascript/**/*.js").any? do |path| | ||
| content = File.read(path) | ||
| content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>/) |
There was a problem hiding this comment.
JSX detection regex has significant false-positive risk
The pattern %r{<\s*[A-Za-z][\w:-]*(\s|>|/)} will match things that aren't JSX:
- TypeScript-style generic annotations in
.js:new Array<Item>()matches as<Item> - Comparison operators:
if (x < ErrorHandler ...)— the space after the identifier satisfies\s - HTML in string literals or comments:
// returns <div> - Multi-word
<=won't match, but< SomeVarwill
A false positive silently switches the entire project from SWC to Babel—a meaningful, hard-to-reverse build change. Consider:
- Narrowing the heuristic by also requiring the match to be outside comments/strings (hard in regex alone).
- Requiring the pattern to appear at the beginning of an expression (e.g., preceded by
return,(,=,,, or newline) to reduce noise. - Asking for user confirmation before switching transpilers, since this is a consequential irreversible change.
At minimum the current heuristic should be documented so future maintainers understand why false positives can occur.
| return unless version | ||
|
|
||
| package_json_content["packageManager"] = "#{manager}@#{version}" | ||
| File.write("package.json", "#{JSON.pretty_generate(package_json_content)}\n") |
There was a problem hiding this comment.
JSON.pretty_generate silently reformats the entire package.json
JSON.pretty_generate always uses 2-space indentation. Many projects (particularly older ones being migrated) use 4-space indent, tabs, or have formatter-enforced styles. This will silently reformat package.json everywhere—creating a large noisy diff of unrelated whitespace changes in the user's repo.
The same issue occurs at line 676 in resolve_browserslist_conflict_after_shakapacker_install.
Consider detecting and preserving the original indentation before re-serialising:
original = File.read("package.json")
indent_str = original.match(/\n(\s+)/)&.[](1) || " "
File.write("package.json", "#{JSON.generate(package_json_content, indent: indent_str)}\n")Or use a narrowly-targeted string substitution that only adds/removes the one key rather than re-serialising the whole file.
|
|
||
| install_args = build_install_args(package_manager, dev, packages_to_install) | ||
|
|
||
| system(*install_args) |
There was a problem hiding this comment.
system() can return nil (not false) when the command isn't found
Kernel#system returns nil when the command cannot be executed (e.g., the package manager binary isn't on PATH). nil is falsy, so callers treat it as failure—which is correct—but no warning is generated for this case since rescue StandardError won't catch it.
Consider normalising the return value and emitting a warning:
| system(*install_args) | |
| result = system(*install_args) | |
| if result.nil? | |
| GeneratorMessages.add_warning("⚠️ Fallback package install failed: command not found — #{install_args.first}") | |
| return false | |
| end | |
| result |
|
Overall this is a well-motivated set of changes with good test coverage. The fallback path for missing package_json gem, deduplication of warning noise, and the browserslist/packageManager seeding all address real pain points in legacy migrations. Issues found (inline comments posted):
Minor observations:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed:
!result.nil?returns true for false results- Changed add_npm_dependencies to return !!result so a false manager.add result correctly propagates and fallback logic can run.
Or push these changes by commenting:
@cursor push b7a7538dc5
Preview (b7a7538dc5)
diff --git a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb
--- a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb
+++ b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb
@@ -34,7 +34,7 @@
else
pj.manager.add(packages, exact: true)
end
- !result.nil?
+ !!result
rescue StandardError => e
say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
say_status :warning, "Will fall back to direct npm commands.", :yellowYou can send follow-ups to this agent here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 754923e88f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| else | ||
| pj.manager.add(packages, exact: true) | ||
| end | ||
| !result.nil? |
There was a problem hiding this comment.
Return false when package_json add reports failure
add_npm_dependencies now returns !result.nil?, which treats false as success. In the failure mode where pj.manager.add returns false, callers will skip fallback installation and continue as if dependencies were added, leaving required packages missing for subsequent build/generator steps. This should return the real success boolean (not non-nil) so fallback logic is actually triggered.
Useful? React with 👍 / 👎.
| package_manager = GeneratorMessages.detect_package_manager | ||
| return package_manager if GeneratorMessages.supported_package_manager?(package_manager) |
There was a problem hiding this comment.
Verify detected fallback manager is installed
The fallback path chooses a package manager purely from lockfiles/env and only checks whether the name is supported, not whether the executable exists. In legacy installs (where package_json is unavailable), a stale lockfile can select yarn/pnpm even when only npm is installed, causing all fallback system(...) installs to fail even though a working manager is present. Add a CLI availability check and fall back to an installed manager.
Useful? React with 👍 / 👎.
| def jsx_in_js_files_present? | ||
| Dir.glob("app/javascript/**/*.js").any? do |path| | ||
| content = File.read(path) | ||
| content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>/) |
There was a problem hiding this comment.
Narrow JSX detection to actual JSX syntax
This pattern also matches normal comparison code such as a < b && ..., because it accepts < followed by an identifier and whitespace. That can incorrectly trigger the SWC→Babel switch in projects that do not use JSX in .js files, unexpectedly changing transpiler behavior and dependency installation. The detector should only match JSX-specific tokens like opening/closing tags or fragments.
Useful? React with 👍 / 👎.
|
Addressed the latest review feedback in 754923e:
CI is running on this commit now. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@react_on_rails/lib/generators/react_on_rails/install_generator.rb`:
- Around line 813-823: The method set_javascript_transpiler_to_babel currently
always sets `@using_swc` = false even when gsub_file made no change (e.g., SWC was
only the implicit Shakapacker default); change it to first read
shakapacker_config_path and test the same regex used for gsub_file to see if an
explicit javascript_transpiler: swc line exists, only call gsub_file and set
`@using_swc` = false when that match is found. Reference symbols:
set_javascript_transpiler_to_babel, shakapacker_config_path, gsub_file,
`@using_swc`, and detect_swc_configuration.
- Around line 252-255: Remove the redundant full install by deleting the call to
install_js_dependencies (and its assignment to dependencies_installed); leave
the targeted installs (babel_loader_added and babel_preset_added from
add_packages/add_babel_react_dependencies) and keep the existing return if those
targeted installs succeeded — i.e., eliminate the line that calls
install_js_dependencies so only the specific package installs are performed.
In `@react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb`:
- Around line 101-109: The method add_npm_dependencies currently returns
!result.nil? which turns a false result from manager.add into true; change the
return to propagate the actual boolean from manager.add (e.g., return result
directly or coerce with !!result) so that when mock_manager.add(packages, exact:
true) returns false, add_npm_dependencies also returns false and allows the
fallback installation to run.
In `@react_on_rails/spec/react_on_rails/generators/generator_messages_spec.rb`:
- Around line 99-101: The before hook deletes REACT_ON_RAILS_PACKAGE_MANAGER
which mutates global ENV; capture the original value (e.g. orig =
ENV["REACT_ON_RAILS_PACKAGE_MANAGER"]) before changing it in the before block
and add an after hook that restores it (set
ENV["REACT_ON_RAILS_PACKAGE_MANAGER"] = orig if orig; otherwise
ENV.delete("REACT_ON_RAILS_PACKAGE_MANAGER")). Update the spec in
generator_messages_spec.rb where the before hook runs so the env is always
restored after the example group.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e68db6f9-cd9c-4b45-9512-89146c41f6ca
📒 Files selected for processing (9)
docs/oss/migrating/migrating-from-react-rails.mdreact_on_rails/lib/generators/react_on_rails/generator_helper.rbreact_on_rails/lib/generators/react_on_rails/generator_messages.rbreact_on_rails/lib/generators/react_on_rails/install_generator.rbreact_on_rails/lib/generators/react_on_rails/js_dependency_manager.rbreact_on_rails/spec/react_on_rails/generators/generator_helper_spec.rbreact_on_rails/spec/react_on_rails/generators/generator_messages_spec.rbreact_on_rails/spec/react_on_rails/generators/install_generator_spec.rbreact_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
✅ Files skipped from review due to trivial changes (1)
- react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
🚧 Files skipped from review as they are similar to previous changes (4)
- react_on_rails/lib/generators/react_on_rails/generator_helper.rb
- docs/oss/migrating/migrating-from-react-rails.md
- react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
- react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
|
|
||
| install_args = build_install_args(package_manager, dev, packages_to_install) | ||
|
|
||
| system(*install_args) |
There was a problem hiding this comment.
Bug: system() can return nil, which silently propagates as a falsy non-false value.
Kernel#system returns true, false, or nil (when the command cannot be executed — e.g. the binary is missing or a permissions error). Returning nil here means no warning is shown to the user about what went wrong, unlike the false case which shows a clear message in install_js_dependencies.
| system(*install_args) | |
| result = system(*install_args) | |
| return result == true | |
Or explicitly handle nil:
case system(*install_args)
when true then true
when false
GeneratorMessages.add_warning("⚠️ Fallback install command failed (non-zero exit).")
false
else # nil
GeneratorMessages.add_warning("⚠️ Could not execute #{install_args.first} — is it on PATH?")
false
end| return unless version | ||
|
|
||
| package_json_content["packageManager"] = "#{manager}@#{version}" | ||
| File.write("package.json", "#{JSON.pretty_generate(package_json_content)}\n") |
There was a problem hiding this comment.
JSON.pretty_generate will reformat the entire package.json, creating noisy git diffs.
JSON.pretty_generate normalizes indentation (2 spaces) and key ordering, so even a file that previously used 4-space indentation, tabs, or different key order will be rewritten in full. This will produce a large diff in the user's working directory that has nothing to do with adding packageManager.
Consider a targeted string injection (e.g. inserting a line after "private": true) or using a JSON serializer that preserves original formatting. At a minimum, document the side effect in a comment so users aren't surprised by the reformatting:
# NOTE: JSON.pretty_generate reformats the entire file; this is intentional to
# ensure a canonical format before shakapacker:install.
File.write("package.json", "#{JSON.pretty_generate(package_json_content)}\n")The same issue exists at line 675 in resolve_browserslist_conflict_after_shakapacker_install.
| def jsx_in_js_files_present? | ||
| Dir.glob("app/javascript/**/*.js").any? do |path| | ||
| content = File.read(path) | ||
| content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>/) |
There was a problem hiding this comment.
JSX detection regex has false positive risk that could unnecessarily switch projects from SWC to Babel.
The pattern <\s*[A-Za-z][\w:-]*(\s|>|/) matches:
- Regular HTML strings in template literals or JS strings:
`<div class="foo">` - HTML comments in JS:
// <Component example> - Comparison operators mixed with capitalized identifiers in some edge cases
A developer with SWC set up, .js files containing HTML in strings or comments, but no actual JSX syntax would have their transpiler silently downgraded. This could mask build issues rather than surfacing them.
Consider making this opt-in via a generator flag (e.g., --auto-detect-jsx) rather than running it unconditionally, or at minimum logging what file and what match triggered the switch so users can audit it:
Dir.glob("app/javascript/**/*.js").each do |path|
content = File.read(path)
if content.match?(%r{<\s*[A-Za-z][\w:-]*(\s|>|/)}) || content.match?(/<\s*>/)
say " → #{path}", :yellow # show which file triggered the detection
return true
end
false
rescue StandardError
false
end| swc_transpiler_pattern, | ||
| '\1"babel"\2' | ||
| ) | ||
| @using_swc = false |
There was a problem hiding this comment.
Direct @using_swc assignment bypasses the memoization pattern used elsewhere.
@using_swc = false sets the raw ivar directly, but the caching/initialization convention (e.g. @using_swc.nil? ? ... : @using_swc) elsewhere means false and "not yet computed" are distinguishable. Directly assigning false here is intentional, but it tightly couples this method to the implementation details of using_swc?.
If using_swc? is ever refactored to use ||= or similar memoization, this manual reset could silently stop working. Consider using a dedicated reset method or a comment explaining the coupling:
# Reset the memoized SWC flag so subsequent using_swc? calls reflect the YAML change.
@using_swc = false| else | ||
| pj.manager.add(packages, exact: true) | ||
| end | ||
| result ? true : false |
There was a problem hiding this comment.
result ? true : false is equivalent to !!result. Both are valid, but either !!result or just returning result (since the caller already coerces to boolean) would be more idiomatic Ruby.
| result ? true : false | |
| result ? true : false |
→ !!result or simply return result if the callers all treat it as truthy/falsy.
| detect_package_manager_from_lockfiles || "npm" | ||
| end | ||
|
|
||
| def detect_package_manager_from_lockfiles |
There was a problem hiding this comment.
detect_package_manager_from_lockfiles is now a public class method but is called internally as an implementation detail of detect_package_manager.
Exposing it publicly means external callers could call it directly and get nil (when no lockfile exists) rather than the "npm" default that detect_package_manager provides. This nil return is silently consumed in seed_package_manager_in_package_json_from_lockfile! (which correctly returns early on nil), but a caller who forgets to handle nil could get unexpected behavior.
Consider either:
- Keeping it
privateand delegating frominstall_generator.rbthroughdetect_package_manager(which already provides the nil-safe default), or - Documenting the nil return contract clearly so callers know to guard against it.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Fallback install silently returns nil on system failure
- Updated fallback package installation to emit a warning with the attempted command and always return explicit false on system failure instead of propagating nil/false silently.
Or push these changes by commenting:
@cursor push 32e77c6b92
Preview (32e77c6b92)
diff --git a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
--- a/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
+++ b/react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb
@@ -511,7 +511,15 @@
install_args = build_install_args(package_manager, dev, packages_to_install)
- system(*install_args)
+ return true if system(*install_args)
+
+ GeneratorMessages.add_warning(<<~MSG.strip)
+ ⚠️ JavaScript package addition failed via #{package_manager}.
+
+ Please run manually:
+ #{install_args.join(' ')}
+ MSG
+ false
rescue StandardError => e
GeneratorMessages.add_warning("⚠️ Fallback package install failed: #{e.message}")
false
diff --git a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
--- a/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
+++ b/react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb
@@ -204,6 +204,9 @@
instance.system_result = false
result = instance.send(:add_packages, %w[package1])
expect(result).to be(false)
+ expect(warnings.size).to be > 0
+ expect(warnings.first.to_s).to include("JavaScript package addition failed via npm")
+ expect(warnings.first.to_s).to include("npm install --save-exact package1")
end
it "skips fallback install for packages already present in package.json" doYou can send follow-ups to this agent here.
|
|
||
| install_args = build_install_args(package_manager, dev, packages_to_install) | ||
|
|
||
| system(*install_args) |
There was a problem hiding this comment.
Fallback install silently returns nil on system failure
Medium Severity
install_packages_with_fallback returns the raw result of system(*install_args), which can be nil (command not found) or false (non-zero exit) without emitting any warning. This contrasts with install_js_dependencies, which explicitly checks the system return, emits a warning on failure, and returns false. The nil return also violates the documented @return [Boolean] contract of add_packages. When the detected package manager isn't installed, the fallback silently fails with no user-facing message about which command was attempted.
Additional Locations (1)
|
Review: Harden migration generator for legacy shakapacker setups. Overall this is a solid improvement - the fallback install path, deduplication of the package_json unavailable warning, and the indentation-safe RSpec helper injection are all good fixes. Test coverage was expanded meaningfully. A few issues to address before merging. BUGS: 1) system() returns nil silently in the fallback install path (js_dependency_manager.rb:514) - Kernel#system returns true, false, or nil. The nil case propagates out of install_packages_with_fallback with no user-visible warning. See inline comment. 2) JSON.pretty_generate rewrites the entire package.json (install_generator.rb:660 and 675) - both seed_package_manager_in_package_json_from_lockfile! and resolve_browserslist_conflict_after_shakapacker_install write back with JSON.pretty_generate, normalising all indentation and whitespace across the file and producing a noisy unrelated diff. Should be documented; ideally use a targeted string-level patch. DESIGN CONCERNS: 3) JSX detection regex can cause false positives silently downgrading SWC to Babel - the regex matches HTML in JS string literals, template literals, and comments. At minimum, log which file triggered the detection so users can audit it. 4) detect_package_manager_from_lockfiles is now public but returns nil when no lockfile is found, unlike detect_package_manager which falls back to npm. The nil contract is handled correctly today but is an easy footgun for future callers. MINOR: @using_swc = false in set_javascript_transpiler_to_babel directly assigns the ivar coupled to using_swc? caching convention - a comment would help. result ? true : false in generator_helper.rb:37 should be !!result. CONFIGURE_RSPEC_TO_COMPILE_ASSETS now names a fragment not a full block - mildly misleading. |
|
Addressed the latest actionable CodeRabbit comments in commit
Targeted specs and RuboCop pass locally; CI is running on the updated branch. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
react_on_rails/lib/generators/react_on_rails/install_generator.rb (1)
249-253:⚠️ Potential issue | 🔴 CriticalHandle implicit SWC defaults before announcing the Babel switch.
Line 817 still bails unless
config/shakapacker.ymlalready contains an explicitjavascript_transpiler: swc. For Shakapacker 9.3+ apps whereusing_swc?is true via the default instead of a YAML entry, Lines 249-253 will print the switch message and install Babel deps, but the config stays on SWC and JSX-in-.jscontinues to fail. Please inject an explicit Babel setting when the file relies on the implicit default, and add a regression example for that path.💡 Suggested fix
def ensure_jsx_in_js_compatibility return if options[:pretend] return unless using_swc? return unless jsx_in_js_files_present? + return unless set_javascript_transpiler_to_babel + say "⚙️ Detected JSX in .js files; switching shakapacker javascript_transpiler to babel for compatibility", :yellow - set_javascript_transpiler_to_babel babel_loader_added = add_packages(["babel-loader"], dev: true) babel_preset_added = add_babel_react_dependencies return if babel_loader_added && babel_preset_added @@ def set_javascript_transpiler_to_babel shakapacker_config_path = "config/shakapacker.yml" - return unless File.exist?(shakapacker_config_path) + return false unless File.exist?(shakapacker_config_path) swc_transpiler_pattern = /^(\s*javascript_transpiler:\s*)["']?swc["']?(\s*(?:#.*)?)$/ - return unless File.read(shakapacker_config_path).match?(swc_transpiler_pattern) + config = File.read(shakapacker_config_path) + + if config.match?(swc_transpiler_pattern) + gsub_file( + shakapacker_config_path, + swc_transpiler_pattern, + '\1"babel"\2' + ) + elsif config.match?(/^\s*default:.*$/) + inject_into_file( + shakapacker_config_path, + " javascript_transpiler: \"babel\"\n", + after: /^\s*default:.*$/ + ) + else + return false + end - gsub_file( - shakapacker_config_path, - swc_transpiler_pattern, - '\1"babel"\2' - ) `@using_swc` = false + true endExpected result:
ReactOnRails::Generators::InstallGenerator#detect_swc_configurationshould show the Shakapacker 9.3+ fallback path, while the current spec coverage should still only exercise an explicitjavascript_transpiler: "swc"fixture.#!/bin/bash set -euo pipefail rg -nC4 'def using_swc\?|def detect_swc_configuration|def set_javascript_transpiler_to_babel' react_on_rails/lib/generators/react_on_rails echo '---' rg -nC4 'describe "#ensure_jsx_in_js_compatibility"|javascript_transpiler:\s*"swc"' react_on_rails/spec/react_on_rails/generators/install_generator_spec.rbAlso applies to: 816-824
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@react_on_rails/lib/generators/react_on_rails/install_generator.rb` around lines 249 - 253, When switching to Babel in the install flow, handle implicit SWC defaults by detecting when using_swc? returns true but the YAML has no explicit javascript_transpiler entry (see detect_swc_configuration and using_swc?): in that case call set_javascript_transpiler_to_babel and also write an explicit javascript_transpiler: "babel" into the shakapacker config so the file no longer relies on the implicit default; update the install generator path that prints the switch (where babel_loader_added/babel_preset_added are set) to perform this write-before-announcing, and add a regression spec exercising the Shakapacker 9.3+ implicit-default path (a new case under describe "#ensure_jsx_in_js_compatibility") to assert the YAML now contains the explicit "babel" setting while keeping the existing explicit-SWC fixture tests unchanged.
🧹 Nitpick comments (1)
react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb (1)
325-328: Keep these assertions scoped to theRSpec.configureblock.Both
include(...)checks now pass even if the helper call is appended outsideRSpec.configure do |config|, which is the malformed-injection case this area is trying to catch. A block-scoped matcher keeps the test flexible without dropping placement coverage.🧪 Suggested matcher
- expect(contents).to include("ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)") + expect(contents).to match( + /RSpec\.configure do \|config\|.*^\s+ReactOnRails::TestHelper\.configure_rspec_to_compile_assets\(config\)/m + )Also applies to: 366-369
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb` around lines 325 - 328, The current assertion checks the entire spec/rails_helper.rb for the helper call and can pass if the call is injected outside the RSpec.configure block; update the test to extract the RSpec.configure block from contents (e.g., using a regexp matching /RSpec\.configure.*?end/m) and assert that the extracted block includes "ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)"; apply the same scoped-block change for the other assertion mentioned (the one around lines 366-369) so both checks verify placement inside the RSpec.configure block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@react_on_rails/lib/generators/react_on_rails/install_generator.rb`:
- Around line 249-253: When switching to Babel in the install flow, handle
implicit SWC defaults by detecting when using_swc? returns true but the YAML has
no explicit javascript_transpiler entry (see detect_swc_configuration and
using_swc?): in that case call set_javascript_transpiler_to_babel and also write
an explicit javascript_transpiler: "babel" into the shakapacker config so the
file no longer relies on the implicit default; update the install generator path
that prints the switch (where babel_loader_added/babel_preset_added are set) to
perform this write-before-announcing, and add a regression spec exercising the
Shakapacker 9.3+ implicit-default path (a new case under describe
"#ensure_jsx_in_js_compatibility") to assert the YAML now contains the explicit
"babel" setting while keeping the existing explicit-SWC fixture tests unchanged.
---
Nitpick comments:
In `@react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb`:
- Around line 325-328: The current assertion checks the entire
spec/rails_helper.rb for the helper call and can pass if the call is injected
outside the RSpec.configure block; update the test to extract the
RSpec.configure block from contents (e.g., using a regexp matching
/RSpec\.configure.*?end/m) and assert that the extracted block includes
"ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)"; apply the
same scoped-block change for the other assertion mentioned (the one around lines
366-369) so both checks verify placement inside the RSpec.configure block.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7813486a-e6ba-47c5-bb3b-1eabd9670aa1
📒 Files selected for processing (4)
react_on_rails/lib/generators/react_on_rails/generator_helper.rbreact_on_rails/lib/generators/react_on_rails/install_generator.rbreact_on_rails/spec/react_on_rails/generators/generator_messages_spec.rbreact_on_rails/spec/react_on_rails/generators/install_generator_spec.rb
🚧 Files skipped from review as they are similar to previous changes (1)
- react_on_rails/spec/react_on_rails/generators/generator_messages_spec.rb



Summary
Why
Legacy react-rails + older shakapacker combinations can fail one-shot migration because generator dependency insertion paths assume package_json is available and because fallback installs previously over-upgraded existing dependencies.
Validation
Note
Medium Risk
Touches core generator install paths (dependency installation, Shakapacker setup, and config rewriting), so mis-detection or edge cases could break installs for some apps, though changes are bounded to scaffolding/migration tooling.
Overview
Improves the
react_on_rails:installmigration experience for older/legacy Shakapacker setups by making JS dependency management resilient when thepackage_jsongem is missing, including package-manager-aware fallback installs that avoid reinstalling already-present packages.The generator now (a) detects/records the active package manager from lockfiles (including
package-lock.json), seedspackage.json#packageManagerwith the detected tool+version before runningshakapacker:install, cleans upbrowserslistinpackage.jsonwhen.browserslistrcexists, and auto-switchesjavascript_transpilerfrom SWC to Babel when JSX is found in.jsfiles (adding required Babel deps).Separately, RSpec helper injection is made indentation-safe, warning noise is reduced, migration docs add new preflight guidance, and specs are expanded to cover the new behaviors.
Written by Cursor Bugbot for commit 8850c37. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests