diff --git a/CHANGELOG.md b/CHANGELOG.md index 0deea13650..8306260775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Changes since the last non-beta release. #### Added +- **Rspack Support**: Added `--rspack` flag to `react_on_rails:install` generator for significantly faster builds (~20x improvement with SWC). Includes unified webpack/rspack configuration templates and `bin/switch-bundler` utility to switch between bundlers post-installation. [PR #1852](https://github.com/shakacode/react_on_rails/pull/1852) by [justin808](https://github.com/justin808). + - **Attribution Comment**: Added HTML comment attribution to Rails views containing React on Rails functionality. The comment automatically displays which version is in use (open source React on Rails or React on Rails Pro) and, for Pro users, shows the license status. This helps identify React on Rails usage across your application. [PR #1857](https://github.com/shakacode/react_on_rails/pull/1857) by [AbanoubGhadban](https://github.com/AbanoubGhadban). - **Improved Error Messages**: Error messages for version mismatches and package configuration issues now include package-manager-specific installation commands (npm, yarn, pnpm, bun). [PR #1881](https://github.com/shakacode/react_on_rails/pull/1881) by [AbanoubGhadban](https://github.com/AbanoubGhadban). diff --git a/README.md b/README.md index 68b6aeb067..4fb2fd83c0 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ **🚀 React on Rails v16.0 Released!** Major modernization with ESM support, enhanced React Server Components, and streamlined configuration. +- **⚡ Rspack Support**: New `--rspack` generator flag for ~20x faster builds! Use Rspack instead of Webpack for dramatically improved build performance. See [Rspack documentation](https://www.shakacode.com/react-on-rails/docs/api/generator-details#rspack-support) for details. - **ESM-only package**: Modern module system with better tree-shaking and performance - **React Server Components**: Improved rendering flow and new `RSCRoute` component for seamless SSR - **Performance improvements**: New async loading strategies and optimized bundle generation @@ -111,8 +112,9 @@ To provide a high-performance framework for integrating Ruby on Rails with React | Feature | Benefit | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ⚡ **Rspack Support** | [~20x faster builds](./docs/api-reference/generator-details.md#rspack-support) with Rspack bundler - dramatically reduce build times in development and CI | | 🎯 **Smart Bundle Loading** | [Automated bundle optimization](./docs/guides/auto-bundling-file-system-based-automated-bundle-generation.md) based on components used - no more manual `javascript_pack_tags` configuration | -| ⚡ **Server-Side Rendering** | Enhanced React Server Components support for better SEO and UX performance | +| 🌟 **Server-Side Rendering** | Enhanced React Server Components support for better SEO and UX performance | | 🚀 **Advanced Loading** | `sync`, `async`, and `defer` options for optimal performance based on your needs | | 🔥 **Hot Module Replacement** | Instant feedback during development with tight [Shakapacker](https://github.com/shakacode/shakapacker) integration | | 📦 **Easy Props Passing** | Direct Rails → React data flow without separate API calls | diff --git a/RSPACK_IMPLEMENTATION.md b/RSPACK_IMPLEMENTATION.md new file mode 100644 index 0000000000..4c4bc84126 --- /dev/null +++ b/RSPACK_IMPLEMENTATION.md @@ -0,0 +1,160 @@ +# Rspack Generator Option Implementation + +This document summarizes the implementation of the `--rspack` option for the React on Rails generator, based on the patterns from [PR #20 in react_on_rails-demos](https://github.com/shakacode/react_on_rails-demos/pull/20). + +## Overview + +The `--rspack` flag allows users to generate a React on Rails application using Rspack instead of Webpack as the bundler. Rspack provides significantly faster build times (~53-270ms vs typical webpack builds). + +## Changes Made + +### 1. Install Generator (`lib/generators/react_on_rails/install_generator.rb`) + +- **Added `--rspack` class option** (line 31-35): Boolean flag to enable Rspack bundler +- **Updated `invoke_generators`** (line 82-83): Pass rspack option to base generator +- **Added `add_rspack_dependencies` method** (line 499-513): Installs Rspack core packages: + - `@rspack/core` + - `rspack-manifest-plugin` +- **Updated `add_dev_dependencies`** (line 515-534): Conditionally installs rspack or webpack refresh plugins: + - Rspack: `@rspack/cli`, `@rspack/plugin-react-refresh`, `react-refresh` + - Webpack: `@pmmmwh/react-refresh-webpack-plugin`, `react-refresh` +- **Updated `add_js_dependencies`** (line 433): Calls `add_rspack_dependencies` when rspack flag is set + +### 2. Base Generator (`lib/generators/react_on_rails/base_generator.rb`) + +- **Added `--rspack` class option** (line 22-26): Boolean flag (passed from install generator) +- **Updated `copy_packer_config`** (line 85-100): Calls `configure_rspack_in_shakapacker` after copying config +- **Added `configure_rspack_in_shakapacker` method** (line 404-426): + - Adds `assets_bundler: 'rspack'` to shakapacker.yml default section + - Changes `webpack_loader` to `'swc'` (Rspack works best with SWC transpiler) + +### 3. Webpack Configuration Templates + +Updated webpack configuration templates to support both webpack and rspack bundlers with unified config approach: + +**development.js.tt**: + +- Added `config` to shakapacker require to access `assets_bundler` setting +- Conditional React Refresh plugin loading based on `config.assets_bundler`: + - Rspack: Uses `@rspack/plugin-react-refresh` + - Webpack: Uses `@pmmmwh/react-refresh-webpack-plugin` +- Prevents "window not found" errors when using rspack + +**serverWebpackConfig.js.tt**: + +- Added `bundler` variable that conditionally requires `@rspack/core` or `webpack` +- Changed `webpack.optimize.LimitChunkCountPlugin` to `bundler.optimize.LimitChunkCountPlugin` +- Enables same config to work with both bundlers without warnings +- Avoids hardcoding webpack-specific imports + +### 4. Bundler Switching Script (`lib/generators/react_on_rails/templates/base/base/bin/switch-bundler`) + +Created a new executable script that allows switching between webpack and rspack after installation: + +**Features:** + +- Updates `shakapacker.yml` with correct `assets_bundler` setting +- Switches `webpack_loader` between 'swc' (rspack) and 'babel' (webpack) +- Removes old bundler dependencies from package.json +- Installs new bundler dependencies +- Supports npm, yarn, and pnpm package managers +- Auto-detects package manager from lock files + +**Usage:** + +```bash +bin/switch-bundler rspack # Switch to Rspack +bin/switch-bundler webpack # Switch to Webpack +``` + +**Dependencies managed:** + +- **Webpack**: webpack, webpack-cli, webpack-dev-server, webpack-assets-manifest, webpack-merge, @pmmmwh/react-refresh-webpack-plugin +- **Rspack**: @rspack/core, @rspack/cli, @rspack/plugin-react-refresh, rspack-manifest-plugin + +## Usage + +### Generate new app with Rspack: + +```bash +rails generate react_on_rails:install --rspack +``` + +### Generate with Rspack and TypeScript: + +```bash +rails generate react_on_rails:install --rspack --typescript +``` + +### Generate with Rspack and Redux: + +```bash +rails generate react_on_rails:install --rspack --redux +``` + +### Switch existing app to Rspack: + +```bash +bin/switch-bundler rspack +``` + +## Configuration Changes + +When `--rspack` is used, the following configuration changes are applied to `config/shakapacker.yml`: + +```yaml +default: &default + source_path: app/javascript + assets_bundler: 'rspack' # Added + # ... other settings + webpack_loader: 'swc' # Changed from 'babel' +``` + +## Dependencies + +### Rspack-specific packages installed: + +**Production:** + +- `@rspack/core` - Core Rspack bundler +- `rspack-manifest-plugin` - Manifest generation for Rspack + +**Development:** + +- `@rspack/cli` - Rspack CLI tools +- `@rspack/plugin-react-refresh` - React Fast Refresh for Rspack +- `react-refresh` - React Fast Refresh runtime + +### Webpack packages NOT installed with --rspack: + +**Production:** + +- `webpack` +- `webpack-assets-manifest` +- `webpack-merge` + +**Development:** + +- `webpack-cli` +- `webpack-dev-server` +- `@pmmmwh/react-refresh-webpack-plugin` + +## Performance Benefits + +According to PR #20: + +- Build times: ~53-270ms with Rspack vs typical webpack builds +- Approximately 20x faster transpilation with SWC (used by Rspack) +- Faster development builds and CI runs + +## Testing + +The implementation follows existing generator patterns and passes RuboCop checks with zero offenses. + +## Compatibility + +- Works with existing webpack configuration files (unified config approach) +- Compatible with TypeScript option (`--typescript`) +- Compatible with Redux option (`--redux`) +- Supports all package managers (npm, yarn, pnpm) +- Reversible via `bin/switch-bundler` script diff --git a/docs/api-reference/generator-details.md b/docs/api-reference/generator-details.md index fd862f49e8..a17e6c40b6 100644 --- a/docs/api-reference/generator-details.md +++ b/docs/api-reference/generator-details.md @@ -10,6 +10,8 @@ Usage: Options: -R, [--redux], [--no-redux] # Install Redux package and Redux version of Hello World Example. Default: false + -T, [--typescript], [--no-typescript] # Generate TypeScript files and install TypeScript dependencies. Default: false + [--rspack], [--no-rspack] # Use Rspack instead of Webpack as the bundler. Default: false [--ignore-warnings], [--no-ignore-warnings] # Skip warnings. Default: false Runtime options: @@ -29,6 +31,18 @@ can pass the redux option if you'd like to have redux setup for you automaticall to integrate the Redux state container framework. The necessary node modules will be automatically included for you. +* TypeScript + + Passing the --typescript generator option generates TypeScript files (.tsx) + instead of JavaScript files (.jsx) and sets up TypeScript configuration. + +* Rspack + + Passing the --rspack generator option uses Rspack instead of Webpack as the + bundler, providing significantly faster builds (~20x improvement with SWC). + Includes unified configuration that works with both bundlers and a + bin/switch-bundler utility to switch between bundlers post-installation. + ******************************************************************************* @@ -106,6 +120,68 @@ rails generate react_on_rails:install --typescript This creates `.tsx` files instead of `.jsx` and adds TypeScript configuration. +### Rspack Support + +The generator supports a `--rspack` option for using Rspack instead of Webpack as the bundler: + +```bash +rails generate react_on_rails:install --rspack +``` + +**Benefits:** + +- **~20x faster builds** with SWC transpilation (build times of ~53-270ms vs typical webpack builds) +- **Unified configuration** - same webpack config files work for both bundlers +- **Easy switching** - includes `bin/switch-bundler` utility to switch between bundlers post-installation + +**What gets installed:** + +- Rspack core packages (`@rspack/core`, `@rspack/cli`) +- Rspack-specific plugins (`@rspack/plugin-react-refresh`, `rspack-manifest-plugin`) +- Shakapacker configured with `assets_bundler: 'rspack'` and `webpack_loader: 'swc'` + +**Switching bundlers after installation:** + +```bash +# Switch to Rspack +bin/switch-bundler rspack + +# Switch back to Webpack +bin/switch-bundler webpack +``` + +The switch-bundler script automatically: + +- Updates shakapacker.yml configuration +- Installs/removes appropriate dependencies +- Works with npm, yarn, and pnpm + +**Limitations of `bin/switch-bundler`:** + +The switch-bundler utility handles the standard configuration and dependencies, but has some limitations: + +- **Custom webpack plugins**: Does not modify custom webpack plugins or loaders in your config files +- **Manual updates needed**: If you have custom webpack configuration, you may need to update it to use unified patterns (see examples in [Webpack Configuration](../core-concepts/webpack-configuration.md#unified-configuration)) +- **Third-party dependencies**: Does not detect or update third-party webpack-specific packages you may have added +- **YAML formatting**: Uses YAML.dump which may change formatting/whitespace (but preserves functionality) + +For apps with custom webpack configurations, review the generated config templates to understand the unified configuration patterns that work with both bundlers. + +**Combining with other options:** + +```bash +# Rspack with TypeScript +rails generate react_on_rails:install --rspack --typescript + +# Rspack with Redux +rails generate react_on_rails:install --rspack --redux + +# All options combined +rails generate react_on_rails:install --rspack --typescript --redux +``` + +For more details on Rspack configuration, see the [Webpack Configuration](../core-concepts/webpack-configuration.md#rspack-vs-webpack) docs. + ### Auto-Bundling and Component Registration Modern React on Rails uses auto-bundling to eliminate manual webpack configuration. Components placed in the configured `components_subdirectory` (default: `ror_components`) are automatically: diff --git a/docs/core-concepts/webpack-configuration.md b/docs/core-concepts/webpack-configuration.md index b3057de6af..3955b14534 100644 --- a/docs/core-concepts/webpack-configuration.md +++ b/docs/core-concepts/webpack-configuration.md @@ -11,6 +11,73 @@ To get a deeper understanding of Shakapacker, watch [RailsConf 2020 CE - Webpacker, It-Just-Works, But How? by Justin Gordon](https://youtu.be/sJLoOpc5LD8). +## Rspack vs. Webpack + +[Rspack](https://rspack.dev/) is a high-performance JavaScript bundler written in Rust that provides significantly faster builds than Webpack (~20x improvement). React on Rails supports both bundlers through unified configuration. + +### Using Rspack + +Generate a new app with Rspack: + +```bash +rails generate react_on_rails:install --rspack +``` + +Or switch an existing app to Rspack: + +```bash +bin/switch-bundler rspack +``` + +### Performance Benefits + +- **Build times**: ~53-270ms with Rspack vs typical webpack builds +- **~20x faster transpilation** with SWC (used by Rspack) +- **Faster development** builds and CI runs + +### Unified Configuration + +React on Rails generates unified webpack configuration files that work with both bundlers: + +**config/webpack/development.js** - Conditional plugin loading: + +```javascript +const { config } = require('shakapacker'); + +if (config.assets_bundler === 'rspack') { + // Rspack uses @rspack/plugin-react-refresh + const ReactRefreshPlugin = require('@rspack/plugin-react-refresh'); + clientWebpackConfig.plugins.push(new ReactRefreshPlugin()); +} else { + // Webpack uses @pmmmwh/react-refresh-webpack-plugin + const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); + clientWebpackConfig.plugins.push(new ReactRefreshWebpackPlugin()); +} +``` + +**config/webpack/serverWebpackConfig.js** - Dynamic bundler detection: + +```javascript +const { config } = require('shakapacker'); + +const bundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack'); + +// Use bundler-specific APIs +serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); +``` + +### Configuration in shakapacker.yml + +Rspack configuration is controlled via `config/shakapacker.yml`: + +```yaml +default: &default + assets_bundler: 'rspack' # or 'webpack' + webpack_loader: 'swc' # Rspack works best with SWC +``` + +The `bin/switch-bundler` script automatically updates this configuration when switching bundlers. + Per the example repo [shakacode/react_on_rails_demo_ssr_hmr](https://github.com/shakacode/react_on_rails_demo_ssr_hmr), you should consider keeping your codebase mostly consistent with the defaults for [Shakapacker](https://github.com/shakacode/shakapacker). diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index d49b19fd07..8b7ac0b935 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -31,6 +31,9 @@ git add . && git commit -m "Add react_on_rails gem" # Run the installer bin/rails generate react_on_rails:install + +# Optional: Use Rspack for ~20x faster builds +# bin/rails generate react_on_rails:install --rspack ``` Take a look at the files created by the generator. @@ -41,6 +44,10 @@ Take a look at the files created by the generator. - A sample controller and view - Webpack configuration +> 💡 **Performance Tip:** Add the `--rspack` flag for significantly faster builds (~20x improvement). You can also switch bundlers later with `bin/switch-bundler rspack`. +> +> **Note on `bin/switch-bundler`:** This utility safely switches between webpack and rspack by updating `shakapacker.yml` and managing dependencies. However, it does not modify custom webpack configuration code. If you have custom webpack plugins or loaders, you may need to update those manually to work with rspack. See [Rspack documentation](../api-reference/generator-details.md#rspack-support) for details on unified configuration patterns. + ## 🎯 Step 2: Start the Development Server (1 minute) > **Note:** Ensure you have `overmind` or `foreman` installed to run `bin/dev`. diff --git a/lib/generators/react_on_rails/base_generator.rb b/lib/generators/react_on_rails/base_generator.rb index 6f8746a08e..64f643d417 100644 --- a/lib/generators/react_on_rails/base_generator.rb +++ b/lib/generators/react_on_rails/base_generator.rb @@ -19,6 +19,12 @@ class BaseGenerator < Rails::Generators::Base desc: "Install Redux package and Redux version of Hello World Example", aliases: "-R" + # --rspack + class_option :rspack, + type: :boolean, + default: false, + desc: "Use Rspack instead of Webpack as the bundler" + def add_hello_world_route route "get 'hello_world', to: 'hello_world#index'" end @@ -82,6 +88,7 @@ def copy_packer_config if File.exist?(".shakapacker_just_installed") puts "Skipping Shakapacker config copy (already installed by Shakapacker installer)" File.delete(".shakapacker_just_installed") # Clean up marker + configure_rspack_in_shakapacker if options.rspack? return end @@ -89,6 +96,7 @@ def copy_packer_config base_path = "base/base/" config = "config/shakapacker.yml" copy_file("#{base_path}#{config}", config) + configure_rspack_in_shakapacker if options.rspack? end def add_base_gems_to_gemfile @@ -392,6 +400,25 @@ def add_configure_rspec_to_compile_assets(helper_file) search_str = "RSpec.configure do |config|" gsub_file(helper_file, search_str, CONFIGURE_RSPEC_TO_COMPILE_ASSETS) end + + def configure_rspack_in_shakapacker + shakapacker_config_path = "config/shakapacker.yml" + return unless File.exist?(shakapacker_config_path) + + puts Rainbow("🔧 Configuring Shakapacker for Rspack...").yellow + + # Parse YAML config properly to avoid fragile regex manipulation + config = YAML.load_file(shakapacker_config_path) + + # Update default section + config["default"] ||= {} + config["default"]["assets_bundler"] = "rspack" + config["default"]["webpack_loader"] = "swc" + + # Write back as YAML + File.write(shakapacker_config_path, YAML.dump(config)) + puts Rainbow("✅ Updated shakapacker.yml for Rspack").green + end end end end diff --git a/lib/generators/react_on_rails/install_generator.rb b/lib/generators/react_on_rails/install_generator.rb index e67916aa6b..f39168bc37 100644 --- a/lib/generators/react_on_rails/install_generator.rb +++ b/lib/generators/react_on_rails/install_generator.rb @@ -28,6 +28,12 @@ class InstallGenerator < Rails::Generators::Base desc: "Generate TypeScript files and install TypeScript dependencies. Default: false", aliases: "-T" + # --rspack + class_option :rspack, + type: :boolean, + default: false, + desc: "Use Rspack instead of Webpack as the bundler. Default: false" + # --ignore-warnings class_option :ignore_warnings, type: :boolean, @@ -96,7 +102,8 @@ def invoke_generators create_css_module_types create_typescript_config end - invoke "react_on_rails:base", [], { typescript: options.typescript?, redux: options.redux? } + invoke "react_on_rails:base", [], + { typescript: options.typescript?, redux: options.redux?, rspack: options.rspack? } if options.redux? invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? } else @@ -447,6 +454,7 @@ def add_js_dependencies add_react_on_rails_package add_react_dependencies add_css_dependencies + add_rspack_dependencies if options.rspack? add_dev_dependencies end @@ -512,12 +520,36 @@ def add_css_dependencies handle_npm_failure("CSS dependencies", css_deps) unless success end + def add_rspack_dependencies + puts "Installing Rspack core dependencies..." + rspack_deps = %w[ + @rspack/core + rspack-manifest-plugin + ] + if add_npm_dependencies(rspack_deps) + @added_dependencies_to_package_json = true + return + end + + success = system("npm", "install", *rspack_deps) + @ran_direct_installs = true if success + handle_npm_failure("Rspack dependencies", rspack_deps) unless success + end + def add_dev_dependencies puts "Installing development dependencies..." - dev_deps = %w[ - @pmmmwh/react-refresh-webpack-plugin - react-refresh - ] + dev_deps = if options.rspack? + %w[ + @rspack/cli + @rspack/plugin-react-refresh + react-refresh + ] + else + %w[ + @pmmmwh/react-refresh-webpack-plugin + react-refresh + ] + end if add_npm_dependencies(dev_deps, dev: true) @added_dependencies_to_package_json = true return diff --git a/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler b/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler new file mode 100755 index 0000000000..1ead3e3d07 --- /dev/null +++ b/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler @@ -0,0 +1,145 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "fileutils" +require "yaml" +require "json" + +# Script to switch between webpack and rspack bundlers +class BundlerSwitcher + WEBPACK_DEPS = { + dependencies: %w[webpack webpack-assets-manifest webpack-merge], + dev_dependencies: %w[webpack-cli webpack-dev-server @pmmmwh/react-refresh-webpack-plugin] + }.freeze + + RSPACK_DEPS = { + dependencies: %w[@rspack/core rspack-manifest-plugin], + dev_dependencies: %w[@rspack/cli @rspack/plugin-react-refresh] + }.freeze + + def initialize(target_bundler) + @target_bundler = target_bundler.to_s.downcase + @shakapacker_config = "config/shakapacker.yml" + validate_bundler! + end + + def switch! + puts "🔄 Switching to #{@target_bundler}..." + + update_shakapacker_config + update_dependencies + install_dependencies + + puts "✅ Successfully switched to #{@target_bundler}!" + puts "\nNext steps:" + puts " 1. Review your webpack configuration files in config/webpack/" + puts " 2. Restart your development server" + end + + private + + def validate_bundler! + return if %w[webpack rspack].include?(@target_bundler) + + abort "❌ Invalid bundler: #{@target_bundler}. Use 'webpack' or 'rspack'" + end + + def update_shakapacker_config + abort "❌ #{@shakapacker_config} not found" unless File.exist?(@shakapacker_config) + + puts "📝 Updating #{@shakapacker_config}..." + config = YAML.load_file(@shakapacker_config) + + config["default"] ||= {} + config["default"]["assets_bundler"] = @target_bundler + + # Update webpack_loader based on bundler + # Rspack works best with SWC, webpack typically uses babel + config["default"]["webpack_loader"] = @target_bundler == "rspack" ? "swc" : "babel" + + File.write(@shakapacker_config, YAML.dump(config)) + puts "✅ Updated assets_bundler to '#{@target_bundler}'" + end + + # rubocop:disable Metrics/CyclomaticComplexity + def update_dependencies + puts "📦 Updating package.json dependencies..." + + package_json_path = "package.json" + unless File.exist?(package_json_path) + puts "⚠️ package.json not found, skipping dependency updates" + return + end + + package_json = JSON.parse(File.read(package_json_path)) + + remove_deps = @target_bundler == "rspack" ? WEBPACK_DEPS : RSPACK_DEPS + + # Remove old bundler dependencies + remove_deps[:dependencies].each do |dep| + package_json["dependencies"]&.delete(dep) + end + remove_deps[:dev_dependencies].each do |dep| + package_json["devDependencies"]&.delete(dep) + end + + puts "✅ Removed #{@target_bundler == 'rspack' ? 'webpack' : 'rspack'} dependencies" + File.write(package_json_path, JSON.pretty_generate(package_json)) + end + # rubocop:enable Metrics/CyclomaticComplexity + + # rubocop:disable Metrics/CyclomaticComplexity + def install_dependencies + puts "📥 Installing #{@target_bundler} dependencies..." + + deps = @target_bundler == "rspack" ? RSPACK_DEPS : WEBPACK_DEPS + + # Detect package manager + package_manager = detect_package_manager + + # Install dependencies using array form to prevent command injection + success = case package_manager + when "yarn" + system("yarn", "add", *deps[:dependencies]) + when "pnpm" + system("pnpm", "add", *deps[:dependencies]) + else + system("npm", "install", *deps[:dependencies]) + end + + abort("❌ Failed to install dependencies") unless success + + # Install dev dependencies using array form to prevent command injection + success = case package_manager + when "yarn" + system("yarn", "add", "-D", *deps[:dev_dependencies]) + when "pnpm" + system("pnpm", "add", "-D", *deps[:dev_dependencies]) + else + system("npm", "install", "--save-dev", *deps[:dev_dependencies]) + end + + abort("❌ Failed to install dev dependencies") unless success + + puts "✅ Installed #{@target_bundler} dependencies" + end + # rubocop:enable Metrics/CyclomaticComplexity + + def detect_package_manager + return "yarn" if File.exist?("yarn.lock") + return "pnpm" if File.exist?("pnpm-lock.yaml") + + "npm" + end +end + +# Main execution +if ARGV.empty? + puts "Usage: bin/switch-bundler [webpack|rspack]" + puts "\nExamples:" + puts " bin/switch-bundler rspack # Switch to Rspack" + puts " bin/switch-bundler webpack # Switch to Webpack" + exit 1 +end + +BundlerSwitcher.new(ARGV[0]).switch! diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt b/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt index e3c26a2319..ce783668d6 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt @@ -1,20 +1,26 @@ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/development.js") %> -const { devServer, inliningCss } = require('shakapacker'); +const { devServer, inliningCss, config } = require('shakapacker'); const generateWebpackConfigs = require('./generateWebpackConfigs'); const developmentEnvOnly = (clientWebpackConfig, _serverWebpackConfig) => { - // React Refresh (Fast Refresh) setup - only when webpack-dev-server is running (HMR mode) - // This matches the condition in generateWebpackConfigs.js and babel.config.js + // React Refresh (Fast Refresh) setup - only when dev server is running (HMR mode) if (process.env.WEBPACK_SERVE) { // eslint-disable-next-line global-require - const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); - clientWebpackConfig.plugins.push( - new ReactRefreshWebpackPlugin({ - // Use default overlay configuration for better compatibility - }), - ); + if (config.assets_bundler === 'rspack') { + // Rspack uses @rspack/plugin-react-refresh for React Fast Refresh + const ReactRefreshPlugin = require('@rspack/plugin-react-refresh'); + clientWebpackConfig.plugins.push(new ReactRefreshPlugin()); + } else { + // Webpack uses @pmmmwh/react-refresh-webpack-plugin + const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); + clientWebpackConfig.plugins.push( + new ReactRefreshWebpackPlugin({ + // Use default overlay configuration for better compatibility + }), + ); + } } }; diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt index deb724704d..ec6e527918 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt @@ -3,7 +3,9 @@ const { merge, config } = require('shakapacker'); const commonWebpackConfig = require('./commonWebpackConfig'); -const webpack = require('webpack'); +const bundler = config.assets_bundler === 'rspack' + ? require('@rspack/core') + : require('webpack'); const configureServer = () => { // We need to use "merge" because the clientConfigObject, EVEN after running @@ -40,7 +42,7 @@ const configureServer = () => { serverWebpackConfig.optimization = { minimize: false, }; - serverWebpackConfig.plugins.unshift(new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); + serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); // Custom output for the server-bundle that matches the config in // config/initializers/react_on_rails.rb diff --git a/spec/react_on_rails/generators/install_generator_spec.rb b/spec/react_on_rails/generators/install_generator_spec.rb index ca095cf8f3..1b3afd2fff 100644 --- a/spec/react_on_rails/generators/install_generator_spec.rb +++ b/spec/react_on_rails/generators/install_generator_spec.rb @@ -139,6 +139,99 @@ end end + context "with --rspack" do + before(:all) { run_generator_test_with_args(%w[--rspack], package_json: true) } + + include_examples "base_generator", application_js: true + include_examples "no_redux_generator" + + it "creates bin/switch-bundler script" do + assert_file "bin/switch-bundler" do |content| + expect(content).to include("class BundlerSwitcher") + expect(content).to include("RSPACK_DEPS") + expect(content).to include("WEBPACK_DEPS") + end + end + + it "installs rspack dependencies in package.json" do + assert_file "package.json" do |content| + package_json = JSON.parse(content) + expect(package_json["dependencies"]).to include("@rspack/core") + expect(package_json["dependencies"]).to include("rspack-manifest-plugin") + expect(package_json["devDependencies"]).to include("@rspack/cli") + expect(package_json["devDependencies"]).to include("@rspack/plugin-react-refresh") + end + end + + it "does not install webpack-specific dependencies" do + assert_file "package.json" do |content| + package_json = JSON.parse(content) + expect(package_json["dependencies"]).not_to include("webpack") + expect(package_json["devDependencies"]).not_to include("webpack-cli") + expect(package_json["devDependencies"]).not_to include("@pmmmwh/react-refresh-webpack-plugin") + end + end + + it "generates unified webpack config with bundler detection" do + assert_file "config/webpack/development.js" do |content| + expect(content).to include("const { devServer, inliningCss, config } = require('shakapacker')") + expect(content).to include("if (config.assets_bundler === 'rspack')") + expect(content).to include("@rspack/plugin-react-refresh") + expect(content).to include("@pmmmwh/react-refresh-webpack-plugin") + end + end + + it "generates server webpack config with bundler variable" do + assert_file "config/webpack/serverWebpackConfig.js" do |content| + expect(content).to include("const bundler = config.assets_bundler === 'rspack'") + expect(content).to include("? require('@rspack/core')") + expect(content).to include(": require('webpack')") + expect(content).to include("new bundler.optimize.LimitChunkCountPlugin") + end + end + end + + context "with --rspack --typescript" do + before(:all) { run_generator_test_with_args(%w[--rspack --typescript], package_json: true) } + + include_examples "base_generator_common", application_js: true + include_examples "no_redux_generator" + + it "creates TypeScript component files with .tsx extension" do + assert_file "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx" + assert_file "app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx" + end + + it "creates tsconfig.json file" do + assert_file "tsconfig.json" do |content| + config = JSON.parse(content) + expect(config["compilerOptions"]["jsx"]).to eq("react-jsx") + expect(config["compilerOptions"]["strict"]).to be true + expect(config["include"]).to include("app/javascript/**/*") + end + end + + it "installs both rspack and typescript dependencies" do + assert_file "package.json" do |content| + package_json = JSON.parse(content) + # Rspack dependencies + expect(package_json["dependencies"]).to include("@rspack/core") + expect(package_json["devDependencies"]).to include("@rspack/cli") + # TypeScript dependencies + expect(package_json["devDependencies"]).to include("typescript") + expect(package_json["devDependencies"]).to include("@types/react") + expect(package_json["devDependencies"]).to include("@types/react-dom") + end + end + + it "TypeScript component includes proper typing" do + assert_file "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx" do |content| + expect(content).to match(/interface HelloWorldProps/) + expect(content).to match(/React\.FC/) + end + end + end + context "with helpful message" do let(:expected) do GeneratorMessages.format_info(GeneratorMessages.helpful_message_after_installation)