Skip to content

Commit c59bcf4

Browse files
justin808claude
andauthored
Improve SWC compiler detection and default dependencies (#2135)
## Summary - Add `using_swc?` helper method to detect SWC configuration in shakapacker.yml - Automatically install SWC dependencies (@swc/core, swc-loader) when SWC is configured - Update spec/dummy to include SWC packages ## Key Changes ### 1. Enhanced `using_swc?` Helper - Parses `shakapacker.yml` for `javascript_transpiler` setting - Returns `true` when SWC is explicitly configured - Returns `true` for Shakapacker 9.3.0+ when not specified (SWC is the default) - Returns `true` for fresh installations (SWC is recommended) ### 2. Generator Updates - Add `SWC_DEPENDENCIES` constant with @swc/core and swc-loader - Install SWC deps automatically when `using_swc?` returns true - Add to devDependencies since they're build-time tools ### 3. spec/dummy Updates - Add @swc/core and swc-loader to devDependencies - Matches the `javascript_transpiler: swc` setting already in shakapacker.yml ## Testing - Added comprehensive RSpec tests for `using_swc?` helper - All existing tests pass - RuboCop checks pass ## Test Plan - [ ] Test generator with Shakapacker 9.3.0+ - [ ] Verify SWC detection logic - [ ] Test fallback to Babel when explicitly configured Closes #1956 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automatic SWC transpiler detection using Shakapacker config and version heuristics * Conditional installation of SWC dependencies during project setup when applicable * Resilient config parsing with clear precedence and fallback behavior * **Tests** * Expanded test coverage for SWC detection, dependency handling, and edge/error scenarios <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <[email protected]>
1 parent 753b75f commit c59bcf4

File tree

6 files changed

+278
-0
lines changed

6 files changed

+278
-0
lines changed

react_on_rails/lib/generators/react_on_rails/generator_helper.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,75 @@ def shakapacker_version_9_or_higher?
124124
true
125125
end
126126
end
127+
128+
# Check if SWC is configured as the JavaScript transpiler in shakapacker.yml
129+
#
130+
# @return [Boolean] true if SWC is configured or should be used by default
131+
#
132+
# Detection logic:
133+
# 1. If shakapacker.yml exists and specifies javascript_transpiler: parse it
134+
# 2. For Shakapacker 9.3.0+, SWC is the default if not specified
135+
# 3. Returns true for fresh installations (SWC is recommended default)
136+
#
137+
# @note This method is used to determine whether to install SWC dependencies
138+
# (@swc/core, swc-loader) instead of Babel dependencies during generation.
139+
#
140+
# @note Caching: The result is memoized for the lifetime of the generator instance.
141+
# If shakapacker.yml changes during generator execution (unlikely), the cached
142+
# value will not update. This is acceptable since generators run quickly.
143+
def using_swc?
144+
return @using_swc if defined?(@using_swc)
145+
146+
@using_swc = detect_swc_configuration
147+
end
148+
149+
private
150+
151+
def detect_swc_configuration
152+
shakapacker_yml_path = File.join(destination_root, "config/shakapacker.yml")
153+
154+
if File.exist?(shakapacker_yml_path)
155+
config = parse_shakapacker_yml(shakapacker_yml_path)
156+
transpiler = config.dig("default", "javascript_transpiler")
157+
158+
# Explicit configuration takes precedence
159+
return transpiler == "swc" if transpiler
160+
161+
# For Shakapacker 9.3.0+, SWC is the default
162+
return shakapacker_version_9_3_or_higher?
163+
end
164+
165+
# Fresh install: SWC is recommended default for Shakapacker 9.3.0+
166+
shakapacker_version_9_3_or_higher?
167+
end
168+
169+
def parse_shakapacker_yml(path)
170+
require "yaml"
171+
# Use safe_load_file for security (defense-in-depth, even though this is user's own config)
172+
# permitted_classes: [Symbol] allows symbol keys which shakapacker.yml may use
173+
# aliases: true allows YAML anchors (&default, *default) commonly used in Rails configs
174+
YAML.safe_load_file(path, permitted_classes: [Symbol], aliases: true)
175+
rescue ArgumentError
176+
# Older Psych versions don't support all parameters - try without aliases
177+
begin
178+
YAML.safe_load_file(path, permitted_classes: [Symbol])
179+
rescue ArgumentError
180+
# Very old Psych - fall back to safe_load with File.read
181+
YAML.safe_load(File.read(path), permitted_classes: [Symbol]) # rubocop:disable Style/YAMLFileRead
182+
end
183+
rescue StandardError
184+
# If we can't parse the file, return empty config
185+
{}
186+
end
187+
188+
# Check if Shakapacker 9.3.0 or higher is available
189+
# This version made SWC the default JavaScript transpiler
190+
def shakapacker_version_9_3_or_higher?
191+
return true unless defined?(ReactOnRails::PackerUtils)
192+
193+
ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.3.0")
194+
rescue StandardError
195+
# If we can't determine version, assume latest (which uses SWC)
196+
true
197+
end
127198
end

react_on_rails/lib/generators/react_on_rails/js_dependency_manager.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ module JsDependencyManager
9999
@types/react-dom
100100
].freeze
101101

102+
# SWC transpiler dependencies (for Shakapacker 9.3.0+ default transpiler)
103+
# SWC is ~20x faster than Babel and is the default for new Shakapacker installations
104+
SWC_DEPENDENCIES = %w[
105+
@swc/core
106+
swc-loader
107+
].freeze
108+
102109
private
103110

104111
def setup_js_dependencies
@@ -118,6 +125,8 @@ def add_js_dependencies
118125
add_css_dependencies
119126
# Rspack dependencies are only added when --rspack flag is used
120127
add_rspack_dependencies if respond_to?(:options) && options&.rspack?
128+
# SWC dependencies are only added when SWC is the configured transpiler
129+
add_swc_dependencies if using_swc?
121130
# Dev dependencies vary based on bundler choice
122131
add_dev_dependencies
123132
end
@@ -232,6 +241,26 @@ def add_rspack_dependencies
232241
MSG
233242
end
234243

244+
def add_swc_dependencies
245+
puts "Installing SWC transpiler dependencies (20x faster than Babel)..."
246+
return if add_packages(SWC_DEPENDENCIES, dev: true)
247+
248+
GeneratorMessages.add_warning(<<~MSG.strip)
249+
⚠️ Failed to add SWC dependencies.
250+
251+
SWC is the default JavaScript transpiler for Shakapacker 9.3.0+.
252+
You can install them manually by running:
253+
npm install --save-dev #{SWC_DEPENDENCIES.join(' ')}
254+
MSG
255+
rescue StandardError => e
256+
GeneratorMessages.add_warning(<<~MSG.strip)
257+
⚠️ Error adding SWC dependencies: #{e.message}
258+
259+
You can install them manually by running:
260+
npm install --save-dev #{SWC_DEPENDENCIES.join(' ')}
261+
MSG
262+
end
263+
235264
def add_typescript_dependencies
236265
puts "Installing TypeScript dependencies..."
237266
return if add_packages(TYPESCRIPT_DEPENDENCIES, dev: true)

react_on_rails/spec/dummy/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@playwright/test": "^1.55.1",
3737
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
3838
"@rescript/react": "^0.13.0",
39+
"@swc/core": "^1.7.0",
3940
"@types/react": "^19.0.0",
4041
"@types/react-dom": "^19.0.0",
4142
"@types/react-helmet": "^6.1.5",
@@ -55,6 +56,7 @@
5556
"sass-resources-loader": "^2.1.0",
5657
"shakapacker": "9.4.0",
5758
"style-loader": "^3.3.1",
59+
"swc-loader": "^0.2.6",
5860
"terser-webpack-plugin": "5.3.1",
5961
"url-loader": "^4.0.0",
6062
"webpack": "5.72.0",

react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,147 @@ def self.read
109109
end
110110
end
111111
end
112+
113+
describe "#using_swc?" do
114+
let(:shakapacker_yml_path) { File.join(destination_root, "config/shakapacker.yml") }
115+
116+
before do
117+
# Clear memoized value before each test
118+
remove_instance_variable(:@using_swc) if instance_variable_defined?(:@using_swc)
119+
FileUtils.mkdir_p(File.join(destination_root, "config"))
120+
end
121+
122+
after do
123+
FileUtils.rm_rf(File.join(destination_root, "config"))
124+
end
125+
126+
context "when shakapacker.yml exists with javascript_transpiler: swc" do
127+
before do
128+
File.write(shakapacker_yml_path, <<~YAML)
129+
default: &default
130+
javascript_transpiler: swc
131+
YAML
132+
end
133+
134+
it "returns true" do
135+
expect(using_swc?).to be true
136+
end
137+
end
138+
139+
context "when shakapacker.yml exists with javascript_transpiler: babel" do
140+
before do
141+
File.write(shakapacker_yml_path, <<~YAML)
142+
default: &default
143+
javascript_transpiler: babel
144+
YAML
145+
end
146+
147+
it "returns false" do
148+
expect(using_swc?).to be false
149+
end
150+
end
151+
152+
context "when shakapacker.yml exists without javascript_transpiler setting" do
153+
before do
154+
File.write(shakapacker_yml_path, <<~YAML)
155+
default: &default
156+
source_path: app/javascript
157+
YAML
158+
# Stub to simulate Shakapacker 9.3.0+ where SWC is default
159+
stub_const("ReactOnRails::PackerUtils", Class.new do
160+
def self.shakapacker_version_requirement_met?(version)
161+
version == "9.3.0"
162+
end
163+
end)
164+
end
165+
166+
it "returns true for Shakapacker 9.3.0+ (SWC is default)" do
167+
expect(using_swc?).to be true
168+
end
169+
end
170+
171+
context "when shakapacker.yml does not exist" do
172+
before do
173+
FileUtils.rm_f(shakapacker_yml_path)
174+
# Stub to simulate Shakapacker 9.3.0+ where SWC is default
175+
stub_const("ReactOnRails::PackerUtils", Class.new do
176+
def self.shakapacker_version_requirement_met?(version)
177+
version == "9.3.0"
178+
end
179+
end)
180+
end
181+
182+
it "returns true for fresh installations with Shakapacker 9.3.0+" do
183+
expect(using_swc?).to be true
184+
end
185+
end
186+
187+
context "when shakapacker.yml has parse errors" do
188+
before do
189+
File.write(shakapacker_yml_path, "invalid: yaml: [}")
190+
# Stub to simulate Shakapacker 9.3.0+ where SWC is default
191+
stub_const("ReactOnRails::PackerUtils", Class.new do
192+
def self.shakapacker_version_requirement_met?(version)
193+
version == "9.3.0"
194+
end
195+
end)
196+
end
197+
198+
it "returns true (assumes latest Shakapacker with SWC default)" do
199+
expect(using_swc?).to be true
200+
end
201+
end
202+
203+
context "with version boundary scenarios" do
204+
before do
205+
File.write(shakapacker_yml_path, <<~YAML)
206+
default: &default
207+
source_path: app/javascript
208+
YAML
209+
end
210+
211+
context "when Shakapacker version is 9.3.0+ (SWC default)" do
212+
before do
213+
stub_const("ReactOnRails::PackerUtils", Class.new do
214+
def self.shakapacker_version_requirement_met?(version)
215+
version == "9.3.0"
216+
end
217+
end)
218+
end
219+
220+
it "returns true when no transpiler is specified" do
221+
expect(using_swc?).to be true
222+
end
223+
end
224+
225+
context "when Shakapacker version is below 9.3.0 (Babel default)" do
226+
before do
227+
stub_const("ReactOnRails::PackerUtils", Class.new do
228+
def self.shakapacker_version_requirement_met?(version)
229+
# Only meets requirements for versions below 9.3.0
230+
version != "9.3.0"
231+
end
232+
end)
233+
end
234+
235+
it "returns false when no transpiler is specified" do
236+
expect(using_swc?).to be false
237+
end
238+
end
239+
240+
context "when PackerUtils raises an error during version check" do
241+
before do
242+
stub_const("ReactOnRails::PackerUtils", Class.new do
243+
def self.shakapacker_version_requirement_met?(_version)
244+
raise StandardError, "Cannot determine version"
245+
end
246+
end)
247+
end
248+
249+
it "defaults to true (assumes latest Shakapacker)" do
250+
expect(using_swc?).to be true
251+
end
252+
end
253+
end
254+
end
112255
end

react_on_rails/spec/react_on_rails/generators/install_generator_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@
1212

1313
include_examples "base_generator", application_js: true
1414
include_examples "no_redux_generator"
15+
16+
it "installs appropriate transpiler dependencies based on Shakapacker version" do
17+
assert_file "package.json" do |content|
18+
package_json = JSON.parse(content)
19+
# This test verifies the generator adapts to the Shakapacker version in the current environment.
20+
# CI runs with both minimum (Shakapacker 8.x) and latest (Shakapacker 9.x) configurations,
21+
# so this test validates correct behavior for whichever version is installed.
22+
# SWC is the default transpiler for Shakapacker 9.3.0+; Babel is the default for older versions.
23+
swc_is_default = ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.3.0")
24+
25+
if swc_is_default
26+
expect(package_json["devDependencies"]).to include("@swc/core")
27+
expect(package_json["devDependencies"]).to include("swc-loader")
28+
else
29+
# For older Shakapacker versions, SWC is NOT installed by default
30+
# (Babel is the default, but we don't install Babel deps since Shakapacker handles it)
31+
expect(package_json["devDependencies"]).not_to include("@swc/core")
32+
end
33+
end
34+
end
1535
end
1636

1737
context "with --redux" do

react_on_rails/spec/react_on_rails/generators/js_dependency_manager_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ def destination_root
2626
"/test/path"
2727
end
2828

29+
# Mock using_swc? from GeneratorHelper (defaults to true for SWC testing)
30+
def using_swc?
31+
@using_swc.nil? ? true : @using_swc
32+
end
33+
34+
attr_writer :using_swc
35+
2936
# Test helpers
3037
attr_writer :add_npm_dependencies_result
3138

@@ -109,6 +116,12 @@ def errors
109116
])
110117
end
111118

119+
it "defines SWC_DEPENDENCIES" do
120+
expect(ReactOnRails::Generators::JsDependencyManager::SWC_DEPENDENCIES).to(
121+
eq(%w[@swc/core swc-loader])
122+
)
123+
end
124+
112125
it "does not include Babel presets in REACT_DEPENDENCIES" do
113126
expect(ReactOnRails::Generators::JsDependencyManager::REACT_DEPENDENCIES).not_to include(
114127
"@babel/preset-react"

0 commit comments

Comments
 (0)