-
-
Notifications
You must be signed in to change notification settings - Fork 638
Description
Summary
After successfully migrating react-webpack-rails-tutorial from Webpack to Rspack (with Shakapacker 9.1.0), I encountered several non-obvious issues. This documents ALL challenges faced during the migration for future reference.
Background
- Project: react-webpack-rails-tutorial (https://github.com/shakacode/react-webpack-rails-tutorial)
- Migration PR: Update to Shakapacker 9.1.0 and migrate to Rspack react-webpack-rails-tutorial#680
- Shakapacker Version: 9.0.0-beta.8 → 9.1.0
- Target: Webpack → Rspack
- Tech Stack: React 19, ReScript, CSS Modules, SSR, Rails 8
- Total commits to resolve: 11 commits across 3 days
Complete List of Issues & Solutions
1. CSS Modules: Named vs Default Exports ⚠️ CRITICAL
Problem: Shakapacker 9 changed the default CSS Modules configuration from default exports to named exports (namedExport: true
). Existing code importing CSS modules as import css from './file.module.scss'
breaks because css
becomes undefined
.
Error in SSR: Cannot read properties of undefined (reading 'elementEnter')
Error in build: ESModulesLinkingWarning: export 'default' (imported as 'css') was not found in './CommentBox.module.scss' (module has no exports)
Root Cause: Breaking change in Shakapacker 9's default CSS loader configuration
Solution: Configure CSS loader to use default exports in commonWebpackConfig.js
:
const commonWebpackConfig = () => {
const baseWebpackConfig = generateWebpackConfig();
// Fix CSS modules to use default exports for backward compatibility
baseWebpackConfig.module.rules.forEach((rule) => {
if (rule.use && Array.isArray(rule.use)) {
const cssLoader = rule.use.find((loader) => {
const loaderName = typeof loader === 'string' ? loader : loader?.loader;
return loaderName?.includes('css-loader');
});
if (cssLoader?.options?.modules) {
cssLoader.options.modules.namedExport = false;
cssLoader.options.modules.exportLocalsConvention = 'camelCase';
}
}
});
return merge({}, baseWebpackConfig, commonOptions);
};
Key Insight: This must be done INSIDE the function so it applies to fresh config each time.
Commits: 1685fb4
, 28014b2
2. Server Bundle: CSS Extract Plugin Filtering ⚠️ CRITICAL
Problem: Server-side rendering config removes CSS extraction loaders by filtering for mini-css-extract-plugin
, but Rspack uses a different loader path: cssExtractLoader.js
. This caused CSS extraction to remain in the server bundle, breaking CSS modules exports.
Error: Same as #1 - Cannot read properties of undefined (reading 'elementEnter')
but only intermittently, causing flaky tests.
Solution: Update filter in serverWebpackConfig.js
to handle both bundlers:
rule.use = rule.use.filter((item) => {
let testValue;
if (typeof item === 'string') {
testValue = item;
} else if (typeof item.loader === 'string') {
testValue = item.loader;
}
// Handle both Webpack and Rspack CSS extract loaders
return !(
testValue?.match(/mini-css-extract-plugin/) ||
testValue?.includes('cssExtractLoader') || // Rspack loader!
testValue === 'style-loader'
);
});
Key Insight: Rspack uses @rspack/core/dist/cssExtractLoader.js
instead of webpack's plugin.
Commit: 3da3dfc
(this was the final fix that made all tests pass consistently)
3. Server Bundle: CSS Modules Configuration Preservation ⚠️ CRITICAL
Problem: Server config was REPLACING CSS modules options instead of merging them, losing namedExport
and exportLocalsConvention
settings set in common config.
Original (broken) code:
if (cssLoader && cssLoader.options) {
cssLoader.options.modules = { exportOnlyLocals: true }; // OVERWRITES!
}
Solution: Merge instead of replace:
if (cssLoader && cssLoader.options && cssLoader.options.modules) {
// Preserve existing modules config but add exportOnlyLocals for SSR
cssLoader.options.modules = {
...cssLoader.options.modules, // Preserve namedExport: false!
exportOnlyLocals: true,
};
}
Key Insight: The spread operator is critical to preserve common config settings.
Commit: 3fe61f0
4. ReScript: Module Resolution
Problem: ReScript-compiled .bs.js
files weren't being resolved by Rspack.
Error: Module not found: Can't resolve './Actions.bs.js'
Solution: Add .bs.js
to resolve extensions:
const commonOptions = {
resolve: {
extensions: ['.css', '.ts', '.tsx', '.bs.js'], // Add .bs.js
},
};
Commit: fbc5781
5. ReScript Dependencies: Missing Compiled Files ⚠️ MAJOR
Problem: @glennsl/[email protected]
package ships with only .res
source files, not compiled .bs.js
files. Its bsconfig.json
lacks package-specs
configuration AND references a non-existent examples
directory.
Error:
Module not found: Can't resolve '@glennsl/rescript-json-combinators/src/Json.bs.js'
Solution: Create a patch using patch-package
:
- Install patch-package:
{
"scripts": {
"postinstall": "patch-package"
},
"devDependencies": {
"patch-package": "^8.0.0"
}
}
- Fix the package's
bsconfig.json
:
{
"name": "@glennsl/rescript-json-combinators",
"namespace": "JsonCombinators",
"sources": ["src"],
"package-specs": [
{
"module": "esmodule",
"in-source": true
}
],
"suffix": ".bs.js"
}
- Generate patch:
npx patch-package @glennsl/rescript-json-combinators
Upstream: Filed issue glennsl/rescript-json-combinators#9
Commits: 76921b8
, 012b0b7
6. SWC React Runtime for SSR
Problem: React on Rails SSR couldn't detect render functions with SWC's automatic runtime. The function signature detection logic expects a specific pattern.
Error:
Invalid call to renderToString. Possibly you have a renderFunction, a function that already
calls renderToString, that takes one parameter. You need to add an extra unused parameter...
Solution: Use classic React runtime in config/swc.config.js
:
const customConfig = {
options: {
jsc: {
transform: {
react: {
runtime: 'classic', // Changed from 'automatic'
refresh: env.isDevelopment && env.runningWebpackDevServer,
},
},
},
},
};
Commit: 5d85f15
7. Bundler Auto-Detection Pattern
Problem: Initial implementation created separate config/rspack/
directory, making it hard to compare configurations and see what's actually different.
Solution: Use conditional logic to support both Webpack and Rspack in same files:
const { config } = require('shakapacker');
// Auto-detect bundler from shakapacker config
const bundler = config.assets_bundler === 'rspack'
? require('@rspack/core')
: require('webpack');
// Use for plugins
clientConfig.plugins.push(
new bundler.ProvidePlugin({ /* ... */ })
);
serverConfig.plugins.unshift(
new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 })
);
Files updated: commonWebpackConfig.js
, serverWebpackConfig.js
, clientWebpackConfig.js
, client.js
, server.js
Benefits:
- Smaller diff (removed 371 lines, added 64)
- Easy to compare Webpack vs Rspack
- Clear what's different (just the conditionals)
Commits: 752919b
, 4c761bb
8. Generated i18n Files Committed
Problem: Mistakenly committed client/app/libs/i18n/default.js
and client/app/libs/i18n/translations.js
which are generated by rake react_on_rails:locale
and already in .gitignore
.
Solution: Remove from git:
git rm --cached client/app/libs/i18n/default.js client/app/libs/i18n/translations.js
Commit: 71b934a
9. Yarn Lockfile Not Updated
Problem: Added patch-package
and postinstall-postinstall
to package.json
but forgot to run yarn install
, causing CI to fail with:
Your lockfile needs to be updated, but yarn was run with --frozen-lockfile
Solution: Run yarn install
to update yarn.lock
Commit: 012b0b7
10. Node Version Incompatibility (Local Development)
Problem: Local development blocked by Node version mismatch (required >=22, had 20.18.0). This prevented local testing of bin/shakapacker
.
Solution: Relied on CI for verification. Not a blocker for the migration itself.
Note: This is why the migration took 11 commits - couldn't test locally.
11. Patch File Format Error
Problem: First attempt at creating patch file manually had incorrect format, causing patch-package to fail with:
Patch file patches/@glennsl+rescript-json-combinators+1.4.0.patch could not be parsed.
Solution: Use npx patch-package
to generate the patch instead of creating manually.
Commit: 012b0b7
Timeline of Resolution
- Initial attempt (commit
a951f4c
): Updated Shakapacker, added Rspack deps, created rspack config - First error: Module not found errors → created i18n stub files (commit
879d171
) - Ruby version mismatch: Updated Gemfile (commit
087ec70
) - SSR renderFunction error: Changed SWC runtime to classic (commit
5d85f15
) - CSS modules SSR error (1st attempt): Tried to fix CSS modules in server config (commit
3fe61f0
) - ReScript resolution: Added .bs.js extension (commit
fbc5781
) - ReScript dependency: Created patch for json-combinators (commits
76921b8
,012b0b7
) - CSS modules SSR error (2nd attempt): Moved CSS fix into function (commit
1685fb4
,28014b2
) - CSS modules SSR error (final fix): Added cssExtractLoader to filter (commit
3da3dfc
) ✅ - Cleanup: Removed generated i18n files (commit
71b934a
) - Consolidation: Moved rspack config into webpack directory (commits
752919b
,4c761bb
)
Configuration Structure
Final recommended approach:
- Keep all configs in
config/webpack/
directory - Use same filenames (webpack.config.js, commonWebpackConfig.js, etc.)
- Add bundler detection conditionals within files
- Avoid creating separate
config/rspack/
directory
Benefits:
- Easier to compare Webpack vs Rspack configurations
- All changes in one place with clear conditionals
- Smaller diff - shows exactly what's different for Rspack
- Follows react_on_rails best practices
Test Results
- ✅ All 3 CI test matrix jobs passing consistently
- ✅ SSR working correctly
- ✅ CSS Modules functioning in both JS and ReScript components
- ✅ Development, test, and production builds working
- ✅ RuboCop passing
Documentation Created
- Patches README:
patches/README.md
explaining why patches are needed - JSDoc comments: Added to
commonWebpackConfig()
andconfigureServer()
- Inline comments: Explaining critical CSS modules fixes
- Upstream issues:
Suggested React on Rails Documentation Additions
- Migration Guide: Add dedicated "Migrating to Rspack" section
- Breaking Changes: Document Shakapacker 9's CSS Modules
namedExport
default change - Troubleshooting Section:
- CSS modules returning undefined in SSR
- ReScript module resolution issues
- Test flakiness from CSS extraction in server bundle
- Configuration Patterns: Document bundler auto-detection pattern
- Example Implementation: Link to this PR as reference
Key Takeaways
- CSS Modules breaking change: Shakapacker 9's
namedExport: true
default is a breaking change that will affect most projects - Rspack CSS extraction: Different loader paths require explicit handling in server config
- Configuration timing: CSS fixes must be inside the function to apply to fresh config
- Test flakiness: Incomplete CSS extraction filtering causes non-deterministic failures
- ReScript ecosystem: Some packages ship source-only and need patches
- Local testing critical: Node version mismatch prevented local validation, slowing debugging
Impact
Without documentation, this migration required:
- 3 days of debugging
- 11 commits to resolve all issues
- Multiple CI iterations to identify test flakiness root cause
- Trial and error for CSS modules configuration placement
Clear documentation could reduce this to ~2-3 commits for a typical migration.