Skip to content

Commit cbd4fc4

Browse files
ihabadhamclaude
authored andcommitted
fix: pin third-party npm deps in generator to prevent peer dep conflicts (#3083)
## Summary - Pin all third-party npm dependency constants in the install generator and `bin/switch-bundler` to `^major.0.0` version ranges - Fixes CI breakage from `@rspack/plugin-react-refresh@2.0.0` requiring `@rspack/core@^2.0.0-0` while `@rspack/core` latest is still `1.7.11` - Follows the same pinning pattern used by Shakapacker's own installer ## Why The generator listed all third-party npm packages as bare names, resolving to whatever `latest` pointed to at install time. When `@rspack/plugin-react-refresh` published 2.0.0 with a peer dep on `@rspack/core@^2.0.0-0` — while `@rspack/core` latest was still 1.7.11 — npm refused to install, breaking three rspack generator specs on main. Every comparable tool (Shakapacker, Webpacker, create-next-app, create-vite) pins dependencies. This PR closes the gap. ## What changed | Constant | Example pin | Notes | |----------|------------|-------| | `REACT_DEPENDENCIES` | `react@^19.0.0` | Current major | | `CSS_DEPENDENCIES` | `css-loader@^7.0.0` | Current major | | `RSPACK_DEPENDENCIES` | `@rspack/core@^1.0.0` | **The fix** | | `RSPACK_DEV_DEPENDENCIES` | `@rspack/plugin-react-refresh@^1.0.0` | **The fix** | | `TYPESCRIPT_DEPENDENCIES` | `typescript@^6.0.0` | Current major | | `SWC_DEPENDENCIES` | `@swc/core@^1.3.0` | Matches Shakapacker | | `DEV_DEPENDENCIES` | bare | Pre-1.0, `^0.x` too narrow | | `bin/switch-bundler` | All deps pinned | Same pattern | ## Verification Created test apps from both `main` and this branch: - **Basic install (no --rspack):** `diff package.json` → **identical**, zero behavior change - **`--rspack` on main:** fails with `ERESOLVE` peer dep conflict - **`--rspack` on this branch:** succeeds, all packages installed at expected 1.x versions ## Test plan - [ ] CI `rspec-package-tests` passes (the three failing rspack specs should be green) - [ ] CI `rspec-unit-tests` passes (updated constant assertions in `js_dependency_manager_spec.rb`) - [ ] Verify no regressions in other generator specs Closes #3082 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * npm dependencies are now pinned to caret-major ranges (e.g., ^major.0.0) to avoid peer dependency conflicts and prevent CI/bundler/SWC-related breakage. * **Documentation** * Changelog updated to describe the new dependency pinning approach and rationale. * **Installer** * Install/switch tooling now handles versioned npm specifiers correctly when updating project dependencies. * **Tests** * Test suite updated to verify the new versioned dependency behavior in generators. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2f70481 commit cbd4fc4

5 files changed

Lines changed: 157 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
2424

2525
### [Unreleased]
2626

27+
#### Fixed
28+
29+
- **Pin third-party npm dependency versions in generator**: All third-party npm dependencies installed by the `react_on_rails:install` generator and `bin/switch-bundler` are now pinned to `^major.0.0` version ranges, preventing peer dependency conflicts from uncontrolled major version bumps. Fixes CI breakage caused by `@rspack/plugin-react-refresh@2.0.0` requiring `@rspack/core@^2.0.0-0` while `@rspack/core` latest was still `1.7.11`. SWC dependency pins match Shakapacker's own version constraints. Closes [Issue 3082](https://github.com/shakacode/react_on_rails/issues/3082). [PR 3083](https://github.com/shakacode/react_on_rails/pull/3083) by [ihabadham](https://github.com/ihabadham).
30+
2731
### [16.6.0.rc.1] - 2026-04-07
2832

2933
#### Removed

react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,44 +52,55 @@ module Generators
5252
# Include this module in generator classes and call setup_js_dependencies
5353
# to handle all JS dependency installation via package_json gem.
5454
module JsDependencyManager
55+
# Third-party dependencies are pinned to ^major.0.0 ranges to prevent breaking
56+
# changes from uncontrolled major version bumps (e.g., peer dependency conflicts)
57+
# while still allowing minor/patch updates. Pre-1.0 packages are left bare since
58+
# ^0.x ranges pin to the minor version, which is too narrow.
59+
# Exception: SWC deps are pinned to match Shakapacker's own version constraints
60+
# (swc-loader@^0.2.0 is pre-1.0 but deliberately pinned for Shakapacker compat).
61+
#
62+
# Update these pins deliberately when adopting a new major version.
63+
5564
# Core React dependencies required for React on Rails
5665
# Note: @babel/preset-react is handled separately in BABEL_REACT_DEPENDENCIES
5766
# and is added only when SWC is not the active transpiler.
5867
REACT_DEPENDENCIES = %w[
59-
react
60-
react-dom
61-
prop-types
68+
react@^19.0.0
69+
react-dom@^19.0.0
70+
prop-types@^15.0.0
6271
].freeze
6372

6473
# Babel preset needed by the generated babel.config.js for non-SWC setups.
6574
BABEL_REACT_DEPENDENCIES = %w[
66-
@babel/preset-react
75+
@babel/preset-react@^7.0.0
6776
].freeze
6877

6978
# CSS processing dependencies for webpack
7079
CSS_DEPENDENCIES = %w[
71-
css-loader
72-
css-minimizer-webpack-plugin
73-
mini-css-extract-plugin
74-
style-loader
80+
css-loader@^7.0.0
81+
css-minimizer-webpack-plugin@^8.0.0
82+
mini-css-extract-plugin@^2.0.0
83+
style-loader@^4.0.0
7584
].freeze
7685

7786
# Development-only dependencies for hot reloading (Webpack)
87+
# Both packages are pre-1.0, so left bare (see pinning note above).
7888
DEV_DEPENDENCIES = %w[
7989
@pmmmwh/react-refresh-webpack-plugin
8090
react-refresh
8191
].freeze
8292

8393
# Rspack core dependencies (only installed when --rspack flag is used)
8494
RSPACK_DEPENDENCIES = %w[
85-
@rspack/core
86-
rspack-manifest-plugin
95+
@rspack/core@^1.0.0
96+
rspack-manifest-plugin@^5.0.0
8797
].freeze
8898

8999
# Rspack development dependencies for hot reloading
100+
# react-refresh is pre-1.0, so left bare (see pinning note above).
90101
RSPACK_DEV_DEPENDENCIES = %w[
91-
@rspack/cli
92-
@rspack/plugin-react-refresh
102+
@rspack/cli@^1.0.0
103+
@rspack/plugin-react-refresh@^1.0.0
93104
react-refresh
94105
].freeze
95106

@@ -100,16 +111,17 @@ module JsDependencyManager
100111
# - If users choose javascript_transpiler: 'babel', they should manually add @babel/preset-typescript
101112
# and configure it in their babel.config.js
102113
TYPESCRIPT_DEPENDENCIES = %w[
103-
typescript
104-
@types/react
105-
@types/react-dom
114+
typescript@^6.0.0
115+
@types/react@^19.0.0
116+
@types/react-dom@^19.0.0
106117
].freeze
107118

108119
# SWC transpiler dependencies (for Shakapacker 9.3.0+ default transpiler)
109120
# SWC is ~20x faster than Babel and is the default for new Shakapacker installations
121+
# Version ranges match Shakapacker's own constraints.
110122
SWC_DEPENDENCIES = %w[
111-
@swc/core
112-
swc-loader
123+
@swc/core@^1.3.0
124+
swc-loader@^0.2.0
113125
].freeze
114126

115127
# React on Rails Pro dependencies (only installed when --pro or --rsc flag is used)
@@ -222,7 +234,8 @@ def add_react_dependencies
222234
# RSC requires React 19.0.x specifically (not 19.1.x or later)
223235
# Pin to ~19.0.4 to allow patch updates while staying within 19.0.x
224236
react_deps = if respond_to?(:use_rsc?) && use_rsc?
225-
["react@#{RSC_REACT_VERSION_RANGE}", "react-dom@#{RSC_REACT_VERSION_RANGE}", "prop-types"]
237+
["react@#{RSC_REACT_VERSION_RANGE}", "react-dom@#{RSC_REACT_VERSION_RANGE}",
238+
"prop-types@^15.0.0"]
226239
else
227240
REACT_DEPENDENCIES
228241
end

react_on_rails/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ require "json"
66

77
# Script to switch between webpack and rspack bundlers
88
class BundlerSwitcher
9+
# Pinned to ^major.0.0 to prevent peer dependency conflicts from major version bumps.
10+
# Pre-1.0 packages are left bare since ^0.x pins to the minor version.
11+
# webpack-cli pinned to ^6 (not ^7) to stay within Shakapacker's supported peer range.
912
WEBPACK_DEPS = {
10-
dependencies: %w[webpack webpack-assets-manifest webpack-merge],
11-
dev_dependencies: %w[webpack-cli webpack-dev-server @pmmmwh/react-refresh-webpack-plugin]
13+
dependencies: %w[webpack@^5.0.0 webpack-assets-manifest@^6.0.0 webpack-merge@^6.0.0],
14+
dev_dependencies: %w[webpack-cli@^6.0.0 webpack-dev-server@^5.0.0 @pmmmwh/react-refresh-webpack-plugin]
1215
}.freeze
1316

1417
RSPACK_DEPS = {
15-
dependencies: %w[@rspack/core rspack-manifest-plugin],
16-
dev_dependencies: %w[@rspack/cli @rspack/plugin-react-refresh]
18+
dependencies: %w[@rspack/core@^1.0.0 rspack-manifest-plugin@^5.0.0],
19+
dev_dependencies: %w[@rspack/cli@^1.0.0 @rspack/plugin-react-refresh@^1.0.0]
1720
}.freeze
1821

1922
def initialize(target_bundler)
@@ -83,11 +86,14 @@ class BundlerSwitcher
8386
remove_deps = @target_bundler == "rspack" ? WEBPACK_DEPS : RSPACK_DEPS
8487

8588
# Remove old bundler dependencies
89+
# Strip version suffix (e.g., "webpack@^5.0.0" -> "webpack") since package.json keys are bare names.
90+
# The regex handles scoped (@scope/name@ver) and unscoped (name@ver) packages, and is a no-op for
91+
# bare names without a version suffix (e.g., "@pmmmwh/react-refresh-webpack-plugin" stays unchanged).
8692
remove_deps[:dependencies].each do |dep|
87-
package_json["dependencies"]&.delete(dep)
93+
package_json["dependencies"]&.delete(dep[%r{\A(@[^/]+/[^@]+|[^@]+)}])
8894
end
8995
remove_deps[:dev_dependencies].each do |dep|
90-
package_json["devDependencies"]&.delete(dep)
96+
package_json["devDependencies"]&.delete(dep[%r{\A(@[^/]+/[^@]+|[^@]+)}])
9197
end
9298

9399
puts "✅ Removed #{@target_bundler == 'rspack' ? 'webpack' : 'rspack'} dependencies"

react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,16 @@ class ActiveSupport::TestCase
664664
end
665665
end
666666

667+
it "switch-bundler has version-pinned deps and strips versions before deletion" do
668+
assert_file "bin/switch-bundler" do |content|
669+
# Version pins are present in the constants
670+
expect(content).to include("@rspack/core@^1.0.0")
671+
expect(content).to include("webpack@^5.0.0")
672+
# Version-stripping regex is used for package.json key deletion
673+
expect(content).to include('dep[%r{\A(@[^/]+/[^@]+|[^@]+)}]')
674+
end
675+
end
676+
667677
it "installs rspack dependencies in package.json" do
668678
assert_file "package.json" do |content|
669679
package_json = JSON.parse(content)

0 commit comments

Comments
 (0)