Skip to content

Commit 024b0e2

Browse files
authored
Upgrade Shakapacker from 8.2.0 to 9.0.0 (#1904)
Upgrade Shakapacker to 9.0.0 with minimal changes (#1904) Why Enable Shakapacker 9.0 features while maintaining full backward compatibility with existing React on Rails applications. Summary Upgraded from Shakapacker 8.2.0 to 9.0.0, configured Babel transpiler for compatibility, added precompile hook support, and resolved CI issues. Key improvements - Configured Babel instead of default SWC for PropTypes compatibility - Added bin/shakapacker-precompile-hook for ReScript and pack generation - Fixed CSS Modules to use default exports (backward compatible) Impact Existing: No code changes required, maintains all current functionality New: Access to Shakapacker 9.0 features and precompile hook system Risks None. Babel configuration ensures compatibility with existing code.
1 parent 1313252 commit 024b0e2

File tree

12 files changed

+187
-22
lines changed

12 files changed

+187
-22
lines changed

.github/workflows/main.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ jobs:
6969
uses: actions/setup-node@v4
7070
with:
7171
node-version: ${{ matrix.node-version }}
72-
cache: yarn
72+
# Disable cache for Node 22 due to V8 bug in 22.21.0
73+
# https://github.com/nodejs/node/issues/56010
74+
cache: ${{ matrix.node-version != '22' && 'yarn' || '' }}
7375
cache-dependency-path: '**/yarn.lock'
7476
- name: Print system information
7577
run: |
@@ -148,7 +150,9 @@ jobs:
148150
uses: actions/setup-node@v4
149151
with:
150152
node-version: ${{ matrix.node-version }}
151-
cache: yarn
153+
# Disable cache for Node 22 due to V8 bug in 22.21.0
154+
# https://github.com/nodejs/node/issues/56010
155+
cache: ${{ matrix.node-version != '22' && 'yarn' || '' }}
152156
cache-dependency-path: '**/yarn.lock'
153157
- name: Print system information
154158
run: |

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ Changes since the last non-beta release.
3131

3232
- **Improved RSC Payload Error Handling**: Errors that happen during generation of RSC payload are transferred properly to rails side and logs the error message and stack. [PR #1888](https://github.com/shakacode/react_on_rails/pull/1888) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
3333

34+
#### Changed
35+
36+
- **Shakapacker 9.0.0 Upgrade**: Upgraded Shakapacker from 8.2.0 to 9.0.0 with Babel transpiler configuration for compatibility. Key changes include:
37+
- Configured `javascript_transpiler: babel` in shakapacker.yml (Shakapacker 9.0 defaults to SWC which has PropTypes handling issues)
38+
- Added precompile hook support via `bin/shakapacker-precompile-hook` for ReScript builds and pack generation
39+
- Configured CSS Modules to use default exports (`namedExport: false`) for backward compatibility with existing `import styles from` syntax
40+
- Fixed webpack configuration to process SCSS rules and CSS loaders in a single pass for better performance
41+
[PR 1904](https://github.com/shakacode/react_on_rails/pull/1904) by [justin808](https://github.com/justin808).
42+
3443
#### Bug Fixes
3544

3645
- **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2).

Gemfile.development_dependencies

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
gem "shakapacker", "8.2.0"
3+
gem "shakapacker", "9.0.0"
44
gem "bootsnap", require: false
55
gem "rails", "~> 7.1"
66

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ GEM
342342
rubyzip (>= 1.2.2, < 3.0)
343343
websocket (~> 1.0)
344344
semantic_range (3.1.0)
345-
shakapacker (8.2.0)
345+
shakapacker (9.0.0)
346346
activesupport (>= 5.2)
347347
package_json
348348
rack-proxy (>= 0.6.1)
@@ -440,7 +440,7 @@ DEPENDENCIES
440440
scss_lint
441441
sdoc
442442
selenium-webdriver (= 4.9.0)
443-
shakapacker (= 8.2.0)
443+
shakapacker (= 9.0.0)
444444
spring (~> 4.0)
445445
sprockets (~> 4.0)
446446
sqlite3 (~> 1.6)

knip.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const config: KnipConfig = {
1010
ignoreBinaries: [
1111
// Has to be installed globally
1212
'yalc',
13+
// Used in package.json scripts (devDependency, so unlisted in production mode)
1314
'nps',
1415
// Pro package binaries used in Pro workflows
1516
'playwright',
@@ -109,8 +110,9 @@ const config: KnipConfig = {
109110
'bin/.*',
110111
],
111112
ignoreDependencies: [
112-
// Knip thinks it can be a devDependency, but it's supposed to be in dependencies.
113+
// Build-time dependencies not detected by Knip in any mode
113114
'@babel/runtime',
115+
'mini-css-extract-plugin',
114116
// There's no ReScript plugin for Knip
115117
'@rescript/react',
116118
// The Babel plugin fails to detect it
@@ -120,17 +122,15 @@ const config: KnipConfig = {
120122
'node-libs-browser',
121123
// The below dependencies are not detected by the Webpack plugin
122124
// due to the config issue.
123-
'css-loader',
124125
'expose-loader',
125126
'file-loader',
126127
'imports-loader',
127-
'mini-css-extract-plugin',
128128
'null-loader',
129-
'sass',
130-
'sass-loader',
131129
'sass-resources-loader',
132130
'style-loader',
133131
'url-loader',
132+
// Transitive dependency of shakapacker but listed as direct dependency
133+
'webpack-merge',
134134
],
135135
},
136136
},

spec/dummy/Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ GEM
346346
rubyzip (>= 1.2.2, < 3.0)
347347
websocket (~> 1.0)
348348
semantic_range (3.1.0)
349-
shakapacker (8.2.0)
349+
shakapacker (9.0.0)
350350
activesupport (>= 5.2)
351351
package_json
352352
rack-proxy (>= 0.6.1)
@@ -441,7 +441,7 @@ DEPENDENCIES
441441
scss_lint
442442
sdoc
443443
selenium-webdriver (= 4.9.0)
444-
shakapacker (= 8.2.0)
444+
shakapacker (= 9.0.0)
445445
spring (~> 4.0)
446446
sprockets (~> 4.0)
447447
sqlite3 (~> 1.6)

spec/dummy/babel.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
const defaultConfigFunc = require('shakapacker/package/babel/preset');
1+
// eslint-disable-next-line import/extensions
2+
const defaultConfigFunc = require('shakapacker/package/babel/preset.js');
23

34
module.exports = function createBabelConfig(api) {
45
const resultConfig = defaultConfigFunc(api);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Shakapacker precompile hook
5+
# This script runs before Shakapacker compilation in both development and production.
6+
# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md
7+
8+
require "fileutils"
9+
10+
# Find Rails root by walking upward looking for config/environment.rb
11+
def find_rails_root
12+
dir = Dir.pwd
13+
loop do
14+
return dir if File.exist?(File.join(dir, "config", "environment.rb"))
15+
16+
parent = File.dirname(dir)
17+
return nil if parent == dir # Reached filesystem root
18+
19+
dir = parent
20+
end
21+
end
22+
23+
# Build ReScript if needed
24+
def build_rescript_if_needed
25+
# Check for both old (bsconfig.json) and new (rescript.json) config files
26+
return unless File.exist?("bsconfig.json") || File.exist?("rescript.json")
27+
28+
puts "🔧 Building ReScript..."
29+
30+
# Cross-platform package manager detection
31+
yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL)
32+
npm_available = system("npm", "--version", out: File::NULL, err: File::NULL)
33+
34+
success = if yarn_available
35+
system("yarn", "build:rescript")
36+
elsif npm_available
37+
system("npm", "run", "build:rescript")
38+
else
39+
warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build."
40+
return
41+
end
42+
43+
if success
44+
puts "✅ ReScript build completed successfully"
45+
else
46+
warn "❌ ReScript build failed"
47+
exit 1
48+
end
49+
end
50+
51+
# Generate React on Rails packs if needed
52+
# rubocop:disable Metrics/CyclomaticComplexity
53+
def generate_packs_if_needed
54+
# Find Rails root directory
55+
rails_root = find_rails_root
56+
return unless rails_root
57+
58+
# Check if React on Rails initializer exists
59+
initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb")
60+
return unless File.exist?(initializer_path)
61+
62+
# Check if auto-pack generation is configured (match actual config assignments, not comments)
63+
config_file = File.read(initializer_path)
64+
# Match uncommented configuration lines only (lines not starting with #)
65+
has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/
66+
has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/
67+
return unless has_auto_load || has_components_subdir
68+
69+
puts "📦 Generating React on Rails packs..."
70+
71+
# Cross-platform bundle availability check
72+
bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL)
73+
return unless bundle_available
74+
75+
# Check if rake task exists (use array form for security)
76+
task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read)
77+
return unless task_list.include?("react_on_rails:generate_packs")
78+
79+
# Use array form for better cross-platform support
80+
success = system("bundle", "exec", "rails", "react_on_rails:generate_packs")
81+
82+
if success
83+
puts "✅ Pack generation completed successfully"
84+
else
85+
warn "❌ Pack generation failed"
86+
exit 1
87+
end
88+
end
89+
# rubocop:enable Metrics/CyclomaticComplexity
90+
91+
# Main execution
92+
begin
93+
build_rescript_if_needed
94+
generate_packs_if_needed
95+
96+
exit 0
97+
rescue StandardError => e
98+
warn "❌ Precompile hook failed: #{e.message}"
99+
warn e.backtrace.join("\n")
100+
exit 1
101+
end

spec/dummy/config/shakapacker.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ default: &default
55
source_entry_path: packs
66
public_root_path: public
77

8+
# Use Babel instead of SWC (Shakapacker 9.0 default) for better compatibility
9+
# SWC has issues with PropTypes handling
10+
javascript_transpiler: babel
11+
12+
# Hook to run before compilation (e.g., for ReScript builds, pack generation)
13+
# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md
14+
precompile_hook: bin/shakapacker-precompile-hook
15+
816
cache_path: tmp/cache/shakapacker
917
webpack_compile_output: false
1018
ensure_consistent_versioning: true

spec/dummy/config/webpack/commonWebpackConfig.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,37 @@ const sassLoaderConfig = {
2121
},
2222
};
2323

24-
const scssConfigIndex = baseClientWebpackConfig.module.rules.findIndex((config) =>
25-
'.scss'.match(config.test),
26-
);
27-
baseClientWebpackConfig.module.rules[scssConfigIndex]?.use.push(sassLoaderConfig);
24+
// Process webpack rules in single pass for efficiency
25+
baseClientWebpackConfig.module.rules.forEach((rule) => {
26+
if (Array.isArray(rule.use)) {
27+
// Add sass-resources-loader to all SCSS rules (both .scss and .module.scss)
28+
if (rule.test && '.scss'.match(rule.test)) {
29+
rule.use.push(sassLoaderConfig);
30+
}
31+
32+
// Configure CSS Modules to use default exports (Shakapacker 9.0 compatibility)
33+
// Shakapacker 9.0 defaults to namedExport: true, but we use default imports
34+
// To restore backward compatibility with existing code using `import styles from`
35+
rule.use.forEach((loader) => {
36+
if (
37+
loader &&
38+
typeof loader === 'object' &&
39+
loader.loader &&
40+
typeof loader.loader === 'string' &&
41+
loader.loader.includes('css-loader') &&
42+
loader.options &&
43+
typeof loader.options === 'object' &&
44+
loader.options.modules &&
45+
typeof loader.options.modules === 'object'
46+
) {
47+
// eslint-disable-next-line no-param-reassign
48+
loader.options.modules.namedExport = false;
49+
// eslint-disable-next-line no-param-reassign
50+
loader.options.modules.exportLocalsConvention = 'camelCase';
51+
}
52+
});
53+
}
54+
});
2855

2956
// add jquery
3057
const exposeJQuery = {

0 commit comments

Comments
 (0)