Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a951f4c
Update to Shakapacker 9.1.0 and migrate to Rspack
justin808 Oct 9, 2025
879d171
Add missing i18n translation files
justin808 Oct 10, 2025
087ec70
Fix Ruby version mismatch for CI
justin808 Oct 10, 2025
5d85f15
Fix SSR by using classic React runtime in SWC
justin808 Oct 10, 2025
3fe61f0
Fix CSS modules config for server bundle
justin808 Oct 10, 2025
fbc5781
Add .bs.js extension to resolve extensions for ReScript
justin808 Oct 11, 2025
76921b8
Add patch for rescript-json-combinators to generate .bs.js files
justin808 Oct 11, 2025
012b0b7
Fix yarn.lock and patch file for rescript-json-combinators
justin808 Oct 11, 2025
1685fb4
Fix CSS modules to use default exports for ReScript compatibility
justin808 Oct 11, 2025
28014b2
Move CSS modules fix into function to ensure it applies on each call
justin808 Oct 11, 2025
3da3dfc
Fix server bundle to properly filter Rspack CSS extract loader
justin808 Oct 12, 2025
71b934a
Remove generated i18n files that should be gitignored
justin808 Oct 12, 2025
752919b
Consolidate Rspack config into webpack directory with conditionals
justin808 Oct 12, 2025
4c761bb
Add bundler auto-detection to all webpack config files
justin808 Oct 12, 2025
431a8ee
Add comprehensive documentation and address code review feedback
justin808 Oct 12, 2025
2e03f56
Add performance benchmarks to README
justin808 Oct 12, 2025
5f92988
Correct performance benchmarks to show actual bundler times
justin808 Oct 12, 2025
0ab9eac
Refactor bundler detection and improve documentation
justin808 Oct 12, 2025
a32ebff
Add test coverage and improve documentation
justin808 Oct 13, 2025
84311cc
Fix CI failure and add bundler validation improvements
justin808 Oct 13, 2025
660aab3
Fix YAML alias parsing in RSpec bundler tests
justin808 Oct 13, 2025
2bdc624
Remove heavyweight RSpec bundler integration test
justin808 Oct 13, 2025
2af9d6f
Fix ESLint violations in bundlerUtils test
justin808 Oct 14, 2025
4d9d19e
Fix CSS plugin filtering and add cache immutability docs
justin808 Oct 14, 2025
13449f0
Migrate to modern ReScript .res.js suffix and remove patch
justin808 Oct 14, 2025
b7171e5
Add ror_components wrapper for ReScript component
justin808 Oct 14, 2025
8bae4aa
Remove generated .res.js files from git and add to .gitignore
justin808 Oct 14, 2025
ab8bd51
Add .bs.js to .gitignore for completeness
justin808 Oct 14, 2025
921844d
Remove patches/README.md
justin808 Oct 14, 2025
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ client/app/bundles/comments/rescript/**/*.bs.js
# Generated React on Rails packs
**/generated/**

# Generated ReScript files (compiled from .res source files)
**/*.res.js
**/*.res.mjs
**/*.bs.js

.claude/
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.4.6"

gem "react_on_rails", "16.1.1"
gem "shakapacker", "9.0.0.beta.8"
gem "shakapacker", "9.1.0"

# Bundle edge Rails instead: gem "rails", github: "rails/rails"
gem "listen"
Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ GEM
websocket (~> 1.0)
semantic_range (3.1.0)
sexp_processor (4.17.1)
shakapacker (9.0.0.beta.8)
shakapacker (9.1.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
Expand Down Expand Up @@ -493,7 +493,7 @@ DEPENDENCIES
scss_lint
sdoc
selenium-webdriver (~> 4)
shakapacker (= 9.0.0.beta.8)
shakapacker (= 9.1.0)
spring
spring-commands-rspec
stimulus-rails (~> 1.3)
Expand All @@ -502,7 +502,7 @@ DEPENDENCIES
web-console

RUBY VERSION
ruby 3.4.6p54
ruby 3.4.6p32

BUNDLED WITH
2.4.17
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,46 @@ See package.json and Gemfile for versions
+ **Testing Mode**: When running tests, it is useful to run `foreman start -f Procfile.spec` in order to have webpack automatically recompile the static bundles. Rspec is configured to automatically check whether or not this process is running. If it is not, it will automatically rebuild the webpack bundle to ensure you are not running tests on stale client code. This is achieved via the `ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)`
line in the `rails_helper.rb` file. If you are using this project as an example and are not using RSpec, you may want to implement similar logic in your own project.

## Webpack
## Webpack and Rspack

_Converted to use Shakapacker webpack configuration_.
_Converted to use Shakapacker with support for both Webpack and Rspack bundlers_.
Comment on lines +168 to +170
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the Table of Contents anchor

Renaming the heading to “Webpack and Rspack” means the TOC link [Webpack](#webpack) no longer resolves. Please update the TOC entry to match the new slug (#webpack-and-rspack) to avoid a broken navigation link.

-+ [Webpack](#webpack)
++ [Webpack and Rspack](#webpack-and-rspack)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In README.md around lines 168 to 170, the Table of Contents still links to
[Webpack](#webpack) but the heading was renamed to "Webpack and Rspack", so
update the TOC entry to use the new anchor `#webpack-and-rspack` (or rename the
TOC label to match) so the link resolves; ensure the TOC text and slug match
exactly the heading (lowercase, spaces to hyphens) and update any other
occurrences of the old `#webpack` anchor.


This project supports both Webpack and Rspack as JavaScript bundlers via [Shakapacker](https://github.com/shakacode/shakapacker). Switch between them by changing the `assets_bundler` setting in `config/shakapacker.yml`:

```yaml
# Use Rspack (default - faster builds)
assets_bundler: rspack

# Or use Webpack (classic, stable)
assets_bundler: webpack
```

### Performance Comparison

Measured bundler compile times for this project (client + server bundles):

| Build Type | Webpack | Rspack | Improvement |
|------------|---------|--------|-------------|
| Development | ~3.1s | ~1.0s | **~3x faster** |
| Production (cold) | ~22s | ~10.7s | **~2x faster** |

**Benefits of Rspack:**
- 67% faster development builds (saves ~2.1s per incremental build)
- 51% faster production builds (saves ~11s on cold builds)
- Faster incremental rebuilds during development
- Reduced CI build times
- Drop-in replacement - same configuration files work for both bundlers

_Note: These are actual bundler compile times. Total build times including package manager overhead may vary._

### Configuration Files

All bundler configuration is in `config/webpack/`:
- `webpack.config.js` - Main entry point (auto-detects Webpack or Rspack)
- `commonWebpackConfig.js` - Shared configuration
- `clientWebpackConfig.js` - Client bundle settings
- `serverWebpackConfig.js` - Server-side rendering bundle
- `development.js`, `production.js`, `test.js` - Environment-specific settings

### Additional Resources
- [Webpack Docs](https://webpack.js.org/)
Expand Down
8 changes: 3 additions & 5 deletions bin/shakapacker
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

ENV["RAILS_ENV"] ||= "development"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
ENV["APP_ROOT"] ||= File.expand_path("..", __dir__)

require "bundler/setup"
require "shakapacker"
require "shakapacker/webpack_runner"
require "shakapacker/runner"

APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Shakapacker::WebpackRunner.run(ARGV)
end
Shakapacker::Runner.run(ARGV)
2 changes: 1 addition & 1 deletion bsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
}
],
"bsc-flags": ["-open JsonCombinators", "-open Belt"],
"suffix": ".bs.js",
"suffix": ".res.js",
"bs-dependencies": [
"@rescript/react",
"@rescript/core",
Expand Down
144 changes: 144 additions & 0 deletions client/__tests__/webpack/bundlerUtils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/* eslint-disable max-classes-per-file */
/* eslint-disable global-require */
/**
* Unit tests for bundlerUtils.js
* Tests bundler auto-detection and helper functions
*
* Note: These tests verify the bundler selection logic without actually
* loading Rspack (which requires Node.js globals not available in jsdom).
* We use require() inside tests to ensure proper mocking order.
*/

// Mock the bundler packages to avoid loading them
jest.mock('webpack', () => ({
ProvidePlugin: class MockProvidePlugin {},
optimize: { LimitChunkCountPlugin: class MockLimitChunkCount {} },
}));

jest.mock('@rspack/core', () => ({
ProvidePlugin: class MockRspackProvidePlugin {},
CssExtractRspackPlugin: class MockCssExtractRspackPlugin {},
optimize: { LimitChunkCountPlugin: class MockRspackLimitChunkCount {} },
}));

jest.mock('mini-css-extract-plugin', () => class MiniCssExtractPlugin {});

describe('bundlerUtils', () => {
let mockConfig;

beforeEach(() => {
// Reset module cache
jest.resetModules();

// Create fresh mock config
mockConfig = { assets_bundler: 'webpack' };
});

afterEach(() => {
jest.clearAllMocks();
});

describe('getBundler()', () => {
it('returns webpack when assets_bundler is webpack', () => {
mockConfig.assets_bundler = 'webpack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

const bundler = utils.getBundler();

expect(bundler).toBeDefined();
expect(bundler.ProvidePlugin).toBeDefined();
expect(bundler.ProvidePlugin.name).toBe('MockProvidePlugin');
});

it('returns rspack when assets_bundler is rspack', () => {
mockConfig.assets_bundler = 'rspack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

const bundler = utils.getBundler();

expect(bundler).toBeDefined();
// Rspack has CssExtractRspackPlugin
expect(bundler.CssExtractRspackPlugin).toBeDefined();
expect(bundler.CssExtractRspackPlugin.name).toBe('MockCssExtractRspackPlugin');
});
});

describe('isRspack()', () => {
it('returns false when assets_bundler is webpack', () => {
mockConfig.assets_bundler = 'webpack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

expect(utils.isRspack()).toBe(false);
});

it('returns true when assets_bundler is rspack', () => {
mockConfig.assets_bundler = 'rspack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

expect(utils.isRspack()).toBe(true);
});
});

describe('getCssExtractPlugin()', () => {
it('returns mini-css-extract-plugin when using webpack', () => {
mockConfig.assets_bundler = 'webpack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

const plugin = utils.getCssExtractPlugin();

expect(plugin).toBeDefined();
expect(plugin.name).toBe('MiniCssExtractPlugin');
});

it('returns CssExtractRspackPlugin when using rspack', () => {
mockConfig.assets_bundler = 'rspack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

const plugin = utils.getCssExtractPlugin();

expect(plugin).toBeDefined();
// Rspack plugin class name
expect(plugin.name).toBe('MockCssExtractRspackPlugin');
});
});

describe('Edge cases and error handling', () => {
it('defaults to webpack when assets_bundler is undefined', () => {
mockConfig.assets_bundler = undefined;
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

const bundler = utils.getBundler();

expect(bundler).toBeDefined();
expect(bundler.ProvidePlugin.name).toBe('MockProvidePlugin');
});

it('throws error for invalid bundler type', () => {
mockConfig.assets_bundler = 'invalid-bundler';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

expect(() => utils.getBundler()).toThrow('Invalid assets_bundler: "invalid-bundler"');
expect(() => utils.getBundler()).toThrow('Must be one of: webpack, rspack');
});

it('returns cached bundler on subsequent calls', () => {
mockConfig.assets_bundler = 'webpack';
jest.doMock('shakapacker', () => ({ config: mockConfig }));
const utils = require('../../../config/webpack/bundlerUtils');

const bundler1 = utils.getBundler();
const bundler2 = utils.getBundler();

// Should return same instance (memoized)
expect(bundler1).toBe(bundler2);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Wrapper for ReScript component to work with react_on_rails auto-registration
// react_on_rails looks for components in ror_components/ subdirectories

import RescriptShow from '../../ReScriptShow.res.js';

export default RescriptShow;
Loading