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 index bcb9bf6e57..4ba82227e9 100644 --- a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb +++ b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb @@ -124,4 +124,64 @@ def shakapacker_version_9_or_higher? true end end + + # Check if SWC is configured as the JavaScript transpiler in shakapacker.yml + # + # @return [Boolean] true if SWC is configured or should be used by default + # + # Detection logic: + # 1. If shakapacker.yml exists and specifies javascript_transpiler: parse it + # 2. For Shakapacker 9.3.0+, SWC is the default if not specified + # 3. Returns true for fresh installations (SWC is recommended default) + # + # @note This method is used to determine whether to install SWC dependencies + # (@swc/core, swc-loader) instead of Babel dependencies during generation. + def using_swc? + return @using_swc if defined?(@using_swc) + + @using_swc = detect_swc_configuration + end + + private + + def detect_swc_configuration + shakapacker_yml_path = File.join(destination_root, "config/shakapacker.yml") + + if File.exist?(shakapacker_yml_path) + config = parse_shakapacker_yml(shakapacker_yml_path) + transpiler = config.dig("default", "javascript_transpiler") + + # Explicit configuration takes precedence + return transpiler == "swc" if transpiler + + # For Shakapacker 9.3.0+, SWC is the default + return shakapacker_version_9_3_or_higher? + end + + # Fresh install: SWC is recommended default for Shakapacker 9.3.0+ + shakapacker_version_9_3_or_higher? + end + + def parse_shakapacker_yml(path) + require "yaml" + # Support both old and new Psych versions + YAML.load_file(path, aliases: true) + rescue ArgumentError + # Older Psych versions don't support the aliases parameter + YAML.load_file(path) + rescue StandardError + # If we can't parse the file, return empty config + {} + end + + # Check if Shakapacker 9.3.0 or higher is available + # This version made SWC the default JavaScript transpiler + def shakapacker_version_9_3_or_higher? + return true unless defined?(ReactOnRails::PackerUtils) + + ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.3.0") + rescue StandardError + # If we can't determine version, assume latest (which uses SWC) + true + end end 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 index 8f5f5ade1e..81dc21ebe0 100644 --- 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 @@ -99,6 +99,13 @@ module JsDependencyManager @types/react-dom ].freeze + # SWC transpiler dependencies (for Shakapacker 9.3.0+ default transpiler) + # SWC is ~20x faster than Babel and is the default for new Shakapacker installations + SWC_DEPENDENCIES = %w[ + @swc/core + swc-loader + ].freeze + private def setup_js_dependencies @@ -118,6 +125,8 @@ def add_js_dependencies add_css_dependencies # Rspack dependencies are only added when --rspack flag is used add_rspack_dependencies if respond_to?(:options) && options&.rspack? + # SWC dependencies are only added when SWC is the configured transpiler + add_swc_dependencies if using_swc? # Dev dependencies vary based on bundler choice add_dev_dependencies end @@ -232,6 +241,26 @@ def add_rspack_dependencies MSG end + def add_swc_dependencies + puts "Installing SWC transpiler dependencies (20x faster than Babel)..." + return if add_packages(SWC_DEPENDENCIES, dev: true) + + GeneratorMessages.add_warning(<<~MSG.strip) + ⚠️ Failed to add SWC dependencies. + + SWC is the default JavaScript transpiler for Shakapacker 9.3.0+. + You can install them manually by running: + npm install --save-dev #{SWC_DEPENDENCIES.join(' ')} + MSG + rescue StandardError => e + GeneratorMessages.add_warning(<<~MSG.strip) + ⚠️ Error adding SWC dependencies: #{e.message} + + You can install them manually by running: + npm install --save-dev #{SWC_DEPENDENCIES.join(' ')} + MSG + end + def add_typescript_dependencies puts "Installing TypeScript dependencies..." return if add_packages(TYPESCRIPT_DEPENDENCIES, dev: true) diff --git a/react_on_rails/spec/dummy/package.json b/react_on_rails/spec/dummy/package.json index e9998a52db..5920cb3832 100644 --- a/react_on_rails/spec/dummy/package.json +++ b/react_on_rails/spec/dummy/package.json @@ -36,6 +36,7 @@ "@playwright/test": "^1.55.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.1", "@rescript/react": "^0.13.0", + "@swc/core": "^1.7.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/react-helmet": "^6.1.5", @@ -55,6 +56,7 @@ "sass-resources-loader": "^2.1.0", "shakapacker": "9.4.0", "style-loader": "^3.3.1", + "swc-loader": "^0.2.6", "terser-webpack-plugin": "5.3.1", "url-loader": "^4.0.0", "webpack": "5.72.0", diff --git a/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb b/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb index eb8b447549..79e3328d14 100644 --- a/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb +++ b/react_on_rails/spec/react_on_rails/generators/generator_helper_spec.rb @@ -109,4 +109,78 @@ def self.read end end end + + describe "#using_swc?" do + let(:shakapacker_yml_path) { File.join(destination_root, "config/shakapacker.yml") } + + before do + # Clear memoized value before each test + remove_instance_variable(:@using_swc) if instance_variable_defined?(:@using_swc) + FileUtils.mkdir_p(File.join(destination_root, "config")) + end + + after do + FileUtils.rm_rf(File.join(destination_root, "config")) + end + + context "when shakapacker.yml exists with javascript_transpiler: swc" do + before do + File.write(shakapacker_yml_path, <<~YAML) + default: &default + javascript_transpiler: swc + YAML + end + + it "returns true" do + expect(using_swc?).to be true + end + end + + context "when shakapacker.yml exists with javascript_transpiler: babel" do + before do + File.write(shakapacker_yml_path, <<~YAML) + default: &default + javascript_transpiler: babel + YAML + end + + it "returns false" do + expect(using_swc?).to be false + end + end + + context "when shakapacker.yml exists without javascript_transpiler setting" do + before do + File.write(shakapacker_yml_path, <<~YAML) + default: &default + source_path: app/javascript + YAML + end + + it "returns true for Shakapacker 9.3.0+ (SWC is default)" do + # The method assumes latest Shakapacker when version detection fails + expect(using_swc?).to be true + end + end + + context "when shakapacker.yml does not exist" do + before do + FileUtils.rm_f(shakapacker_yml_path) + end + + it "returns true for fresh installations (SWC is recommended)" do + expect(using_swc?).to be true + end + end + + context "when shakapacker.yml has parse errors" do + before do + File.write(shakapacker_yml_path, "invalid: yaml: [}") + end + + it "returns true (assumes latest Shakapacker with SWC default)" do + expect(using_swc?).to be true + end + end + 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 index 0c087b8b0c..08c0b7ea38 100644 --- 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 @@ -26,6 +26,13 @@ def destination_root "/test/path" end + # Mock using_swc? from GeneratorHelper (defaults to true for SWC testing) + def using_swc? + @using_swc.nil? ? true : @using_swc + end + + attr_writer :using_swc + # Test helpers attr_writer :add_npm_dependencies_result @@ -109,6 +116,12 @@ def errors ]) end + it "defines SWC_DEPENDENCIES" do + expect(ReactOnRails::Generators::JsDependencyManager::SWC_DEPENDENCIES).to( + eq(%w[@swc/core swc-loader]) + ) + end + it "does not include Babel presets in REACT_DEPENDENCIES" do expect(ReactOnRails::Generators::JsDependencyManager::REACT_DEPENDENCIES).not_to include( "@babel/preset-react"