- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 639
 
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/rescript-json-combinators@1.4.0 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-combinatorsUpstream: 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.jsCommit: 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.mdexplaining 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 
namedExportdefault 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: truedefault 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.