Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions RSPACK_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions lib/generators/react_on_rails/base_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,13 +88,15 @@ 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

puts "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} 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
Expand Down Expand Up @@ -392,6 +400,30 @@ 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

# Read the current config
config_content = File.read(shakapacker_config_path)

# Update assets_bundler to rspack in default section
unless config_content.include?("assets_bundler:")
# Add assets_bundler after source_path in default section
config_content.gsub!(/^default: &default\n(\s+source_path:.*\n)/) do
"default: &default\n#{Regexp.last_match(1)} assets_bundler: 'rspack'\n"
end
end

# Update webpack_loader to swc (rspack works best with SWC)
config_content.gsub!(/^\s*webpack_loader:.*$/, " webpack_loader: 'swc'")

File.write(shakapacker_config_path, config_content)
puts Rainbow("✅ Updated shakapacker.yml for Rspack").green
end
Comment on lines +404 to +426
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Refactor to use YAML parsing instead of string manipulation.

The current implementation uses string manipulation (regex and gsub!) on YAML content, which is fragile and error-prone. This approach has several issues:

  1. Line 414: The unless config_content.include?("assets_bundler:") check won't update an existing assets_bundler setting (e.g., if it's already set to 'webpack').
  2. Lines 416-418: The regex assumes specific whitespace formatting and may not match variations.
  3. Line 422: The gsub!(/^\s*webpack_loader:.*$/, ...) updates ALL occurrences globally, not just in the default section, which could affect environment-specific overrides.

Apply this diff to use proper YAML parsing:

 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

-  # Read the current config
-  config_content = File.read(shakapacker_config_path)
-
-  # Update assets_bundler to rspack in default section
-  unless config_content.include?("assets_bundler:")
-    # Add assets_bundler after source_path in default section
-    config_content.gsub!(/^default: &default\n(\s+source_path:.*\n)/) do
-      "default: &default\n#{Regexp.last_match(1)}  assets_bundler: 'rspack'\n"
-    end
-  end
-
-  # Update webpack_loader to swc (rspack works best with SWC)
-  config_content.gsub!(/^\s*webpack_loader:.*$/, "  webpack_loader: 'swc'")
-
-  File.write(shakapacker_config_path, config_content)
+  # Parse YAML config
+  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

Note: This approach is already used correctly in the switch-bundler script (lines 50-59), so applying the same pattern here maintains consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
# Read the current config
config_content = File.read(shakapacker_config_path)
# Update assets_bundler to rspack in default section
unless config_content.include?("assets_bundler:")
# Add assets_bundler after source_path in default section
config_content.gsub!(/^default: &default\n(\s+source_path:.*\n)/) do
"default: &default\n#{Regexp.last_match(1)} assets_bundler: 'rspack'\n"
end
end
# Update webpack_loader to swc (rspack works best with SWC)
config_content.gsub!(/^\s*webpack_loader:.*$/, " webpack_loader: 'swc'")
File.write(shakapacker_config_path, config_content)
puts Rainbow("✅ Updated shakapacker.yml for Rspack").green
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
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
42 changes: 37 additions & 5 deletions lib/generators/react_on_rails/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -73,7 +79,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
Expand Down Expand Up @@ -424,6 +431,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

Expand Down Expand Up @@ -489,12 +497,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
Expand Down
Loading
Loading