Skip to content

Commit cd1bf8c

Browse files
justin808claude
andcommitted
Add Rspack migration guide for Shakapacker 9
Closes #1863 Comprehensive guide documenting all challenges and solutions for migrating from Webpack to Rspack with Shakapacker 9: - CSS Modules breaking change (namedExport default) - Server bundle CSS extraction filter for Rspack - CSS Modules configuration preservation for SSR - Bundler auto-detection pattern for dual support - SWC React runtime considerations - ReScript module resolution Based on real-world migration experience from react-webpack-rails-tutorial. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 16b3908 commit cd1bf8c

File tree

2 files changed

+382
-0
lines changed

2 files changed

+382
-0
lines changed

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Jump straight to what you need
2727
### 🏗️ **Migrating from Other Solutions**
2828

2929
- **[From react-rails](./migrating/migrating-from-react-rails.md)** - Switch from the react-rails gem
30+
- **[From Webpack to Rspack](./migrating/migrating-to-rspack.md)** - Migrate to Rspack with Shakapacker 9
3031
- **[Upgrading React on Rails](./upgrading/upgrading-react-on-rails.md)** - Version upgrade guide
3132

3233
## 🎯 Popular Use Cases
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
# Migrating from Webpack to Rspack
2+
3+
This guide documents the process of migrating a React on Rails project from Webpack to Rspack using Shakapacker 9. It covers all known issues and their solutions based on real-world migrations.
4+
5+
## Prerequisites
6+
7+
- Shakapacker 9.0.0 or later (Rspack support was added in v9)
8+
- Node.js 18+ (Node.js 22+ recommended)
9+
- Working React on Rails application using Webpack
10+
11+
## Overview
12+
13+
Rspack is a high-performance bundler written in Rust that aims to be drop-in compatible with Webpack. While mostly compatible, there are several configuration differences and breaking changes in Shakapacker 9 that require attention during migration.
14+
15+
**Reference Implementation:** [react-webpack-rails-tutorial PR #680](https://github.com/shakacode/react-webpack-rails-tutorial/pull/680)
16+
17+
## Step 1: Update Dependencies
18+
19+
### Install Rspack
20+
21+
Add Rspack core package:
22+
23+
```bash
24+
yarn add -D @rspack/core
25+
```
26+
27+
### Update shakapacker.yml
28+
29+
Configure Shakapacker to use Rspack as the bundler:
30+
31+
```yaml
32+
# config/shakapacker.yml
33+
default: &default # ... existing configuration ...
34+
assets_bundler: rspack # Add this line
35+
```
36+
37+
## Step 2: Fix CSS Modules (Breaking Change)
38+
39+
> ⚠️ **CRITICAL**: Shakapacker 9 changed the default CSS Modules configuration
40+
41+
### The Problem
42+
43+
Shakapacker 9 defaults CSS Modules to use named exports (`namedExport: true`). This breaks existing code that imports CSS modules as default exports:
44+
45+
```javascript
46+
// This pattern breaks with Shakapacker 9 defaults
47+
import css from './Component.module.scss';
48+
console.log(css.someClass); // undefined!
49+
```
50+
51+
**Error messages you might see:**
52+
53+
- SSR: `Cannot read properties of undefined (reading 'someClassName')`
54+
- Build: `ESModulesLinkingWarning: export 'default' (imported as 'css') was not found in './Component.module.scss'`
55+
56+
### The Solution
57+
58+
Configure CSS loader to preserve default export behavior in your webpack config:
59+
60+
```javascript
61+
// config/webpack/commonWebpackConfig.js
62+
const { generateWebpackConfig, merge } = require('shakapacker');
63+
64+
const commonWebpackConfig = () => {
65+
const baseWebpackConfig = generateWebpackConfig();
66+
67+
// Fix CSS modules to use default exports for backward compatibility
68+
// Shakapacker 9 defaults to namedExport: true which breaks existing code
69+
baseWebpackConfig.module.rules.forEach((rule) => {
70+
if (rule.use && Array.isArray(rule.use)) {
71+
const cssLoader = rule.use.find((loader) => {
72+
const loaderName = typeof loader === 'string' ? loader : loader?.loader;
73+
return loaderName?.includes('css-loader');
74+
});
75+
76+
if (cssLoader?.options?.modules) {
77+
cssLoader.options.modules.namedExport = false;
78+
cssLoader.options.modules.exportLocalsConvention = 'camelCase';
79+
}
80+
}
81+
});
82+
83+
return merge({}, baseWebpackConfig, commonOptions);
84+
};
85+
86+
module.exports = commonWebpackConfig;
87+
```
88+
89+
> **Important:** This configuration must be inside the function so it applies to a fresh config each time the function is called.
90+
91+
## Step 3: Update Server Bundle Configuration
92+
93+
If you use server-side rendering (SSR), update your server webpack configuration.
94+
95+
### Fix CSS Extract Plugin Filtering
96+
97+
Rspack uses a different loader path for CSS extraction than Webpack:
98+
99+
- **Webpack:** `mini-css-extract-plugin`
100+
- **Rspack:** `@rspack/core/dist/cssExtractLoader.js`
101+
102+
Update your server config to filter both:
103+
104+
```javascript
105+
// config/webpack/serverWebpackConfig.js
106+
const configureServer = (clientConfig) => {
107+
// ... other configuration ...
108+
109+
serverConfig.module.rules.forEach((rule) => {
110+
if (rule.use && Array.isArray(rule.use)) {
111+
// Filter out CSS extraction loaders for server bundle
112+
rule.use = rule.use.filter((item) => {
113+
let testValue;
114+
if (typeof item === 'string') {
115+
testValue = item;
116+
} else if (typeof item.loader === 'string') {
117+
testValue = item.loader;
118+
}
119+
// Handle both Webpack and Rspack CSS extract loaders
120+
return !(
121+
testValue?.match(/mini-css-extract-plugin/) ||
122+
testValue?.includes('cssExtractLoader') || // Rspack loader
123+
testValue === 'style-loader'
124+
);
125+
});
126+
}
127+
});
128+
129+
return serverConfig;
130+
};
131+
```
132+
133+
### Preserve CSS Modules Configuration for SSR
134+
135+
When configuring CSS modules for SSR (using `exportOnlyLocals`), merge the settings instead of replacing them:
136+
137+
```javascript
138+
// Wrong - overwrites the namedExport setting
139+
if (cssLoader?.options) {
140+
cssLoader.options.modules = { exportOnlyLocals: true };
141+
}
142+
143+
// Correct - preserves namedExport: false from common config
144+
if (cssLoader?.options?.modules) {
145+
cssLoader.options.modules = {
146+
...cssLoader.options.modules, // Preserve existing settings
147+
exportOnlyLocals: true,
148+
};
149+
}
150+
```
151+
152+
## Step 4: Bundler Auto-Detection Pattern
153+
154+
For projects that need to support both Webpack and Rspack, use conditional logic:
155+
156+
```javascript
157+
// config/webpack/commonWebpackConfig.js
158+
const { config } = require('shakapacker');
159+
160+
// Auto-detect bundler from shakapacker config
161+
const bundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack');
162+
163+
// Use for plugins that differ between bundlers
164+
clientConfig.plugins.push(
165+
new bundler.ProvidePlugin({
166+
React: 'react',
167+
}),
168+
);
169+
170+
serverConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
171+
```
172+
173+
This approach:
174+
175+
- Keeps all configs in the same `config/webpack/` directory
176+
- Makes differences between bundlers explicit
177+
- Simplifies debugging and maintenance
178+
179+
## Step 5: Handle SWC React Runtime (If Using SWC)
180+
181+
If you use SWC for transpilation and server-side rendering, you may need to use the classic React runtime:
182+
183+
```javascript
184+
// config/swc.config.js
185+
const customConfig = {
186+
options: {
187+
jsc: {
188+
transform: {
189+
react: {
190+
runtime: 'classic', // Use 'classic' instead of 'automatic'
191+
refresh: env.isDevelopment && env.runningWebpackDevServer,
192+
},
193+
},
194+
},
195+
},
196+
};
197+
```
198+
199+
**Why?** React on Rails SSR detects render function signatures. The automatic runtime's transformed output may not be detected correctly, causing errors like:
200+
201+
```
202+
Invalid call to renderToString. Possibly you have a renderFunction,
203+
a function that already calls renderToString, that takes one parameter.
204+
```
205+
206+
## Step 6: Handle ReScript (If Applicable)
207+
208+
If your project uses ReScript:
209+
210+
### Add `.bs.js` Extension Resolution
211+
212+
```javascript
213+
// config/webpack/commonWebpackConfig.js
214+
const commonOptions = {
215+
resolve: {
216+
extensions: ['.css', '.ts', '.tsx', '.bs.js'],
217+
},
218+
};
219+
```
220+
221+
### Patch Broken Dependencies
222+
223+
Some ReScript packages may not include compiled `.bs.js` files. Use `patch-package`:
224+
225+
```bash
226+
yarn add -D patch-package postinstall-postinstall
227+
```
228+
229+
Add to `package.json`:
230+
231+
```json
232+
{
233+
"scripts": {
234+
"postinstall": "patch-package"
235+
}
236+
}
237+
```
238+
239+
## Complete Configuration Example
240+
241+
Here's a complete example of a dual Webpack/Rspack compatible configuration:
242+
243+
```javascript
244+
// config/webpack/commonWebpackConfig.js
245+
const { generateWebpackConfig, merge, config } = require('shakapacker');
246+
247+
// Auto-detect bundler
248+
const bundler = config.assets_bundler === 'rspack' ? require('@rspack/core') : require('webpack');
249+
250+
const commonOptions = {
251+
resolve: {
252+
extensions: ['.css', '.ts', '.tsx'],
253+
},
254+
};
255+
256+
/**
257+
* Generate common webpack configuration with CSS modules fix.
258+
* Must be called as a function to get fresh config each time.
259+
*/
260+
const commonWebpackConfig = () => {
261+
const baseWebpackConfig = generateWebpackConfig();
262+
263+
// Fix CSS modules for backward compatibility with Shakapacker 9
264+
baseWebpackConfig.module.rules.forEach((rule) => {
265+
if (rule.use && Array.isArray(rule.use)) {
266+
const cssLoader = rule.use.find((loader) => {
267+
const loaderName = typeof loader === 'string' ? loader : loader?.loader;
268+
return loaderName?.includes('css-loader');
269+
});
270+
271+
if (cssLoader?.options?.modules) {
272+
cssLoader.options.modules.namedExport = false;
273+
cssLoader.options.modules.exportLocalsConvention = 'camelCase';
274+
}
275+
}
276+
});
277+
278+
return merge({}, baseWebpackConfig, commonOptions);
279+
};
280+
281+
module.exports = commonWebpackConfig;
282+
module.exports.bundler = bundler;
283+
```
284+
285+
```javascript
286+
// config/webpack/serverWebpackConfig.js
287+
const { merge } = require('shakapacker');
288+
289+
/**
290+
* Configure server-side rendering bundle.
291+
* Handles both Webpack and Rspack CSS extraction loaders.
292+
*/
293+
const configureServer = (clientConfig) => {
294+
const serverConfig = merge({}, clientConfig);
295+
296+
serverConfig.module.rules.forEach((rule) => {
297+
if (rule.use && Array.isArray(rule.use)) {
298+
// Filter CSS extraction loaders (different paths for Webpack vs Rspack)
299+
rule.use = rule.use.filter((item) => {
300+
let testValue;
301+
if (typeof item === 'string') {
302+
testValue = item;
303+
} else if (typeof item.loader === 'string') {
304+
testValue = item.loader;
305+
}
306+
return !(
307+
testValue?.match(/mini-css-extract-plugin/) ||
308+
testValue?.includes('cssExtractLoader') ||
309+
testValue === 'style-loader'
310+
);
311+
});
312+
313+
// Configure CSS modules for SSR (exportOnlyLocals)
314+
const cssLoader = rule.use.find((loader) => {
315+
const loaderName = typeof loader === 'string' ? loader : loader?.loader;
316+
return loaderName?.includes('css-loader');
317+
});
318+
319+
if (cssLoader?.options?.modules) {
320+
cssLoader.options.modules = {
321+
...cssLoader.options.modules, // Preserve namedExport: false
322+
exportOnlyLocals: true,
323+
};
324+
}
325+
}
326+
});
327+
328+
return serverConfig;
329+
};
330+
331+
module.exports = configureServer;
332+
```
333+
334+
## Troubleshooting
335+
336+
### CSS Modules Return `undefined` in SSR
337+
338+
**Cause:** CSS extraction loader not filtered from server bundle, or CSS modules configuration being overwritten.
339+
340+
**Solution:**
341+
342+
1. Ensure `cssExtractLoader` is filtered (see Step 3)
343+
2. Ensure CSS modules config is merged, not replaced
344+
345+
### Tests Pass Locally But Fail Intermittently in CI
346+
347+
**Cause:** Incomplete CSS extraction filtering causes non-deterministic behavior.
348+
349+
**Solution:** Add the `cssExtractLoader` filter for Rspack (see Step 3).
350+
351+
### Module Not Found Errors
352+
353+
**Cause:** Rspack may have stricter module resolution.
354+
355+
**Solution:**
356+
357+
1. Check `resolve.extensions` in webpack config
358+
2. Ensure all required file extensions are listed
359+
3. For ReScript, add `.bs.js` extension
360+
361+
### Build Warnings About Named Exports
362+
363+
**Warning:** `export 'default' (imported as 'css') was not found`
364+
365+
**Cause:** Shakapacker 9's `namedExport: true` default.
366+
367+
**Solution:** Apply the CSS modules fix in Step 2.
368+
369+
## Performance Benefits
370+
371+
After migrating to Rspack, you should see significant build time improvements:
372+
373+
- **Development builds:** 2-5x faster
374+
- **Production builds:** 2-3x faster
375+
- **Hot Module Replacement:** Near-instant updates
376+
377+
## Additional Resources
378+
379+
- [Shakapacker Rspack Support Issue](https://github.com/shakacode/shakapacker/issues/693)
380+
- [Rspack Documentation](https://rspack.dev/)
381+
- [Shakapacker Documentation](https://github.com/shakacode/shakapacker)

0 commit comments

Comments
 (0)