Skip to content

Documentation: Add guide for migrating to Rspack with Shakapacker 9 #1863

@justin808

Description

@justin808

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

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:

  1. Install patch-package:
{
  "scripts": {
    "postinstall": "patch-package"
  },
  "devDependencies": {
    "patch-package": "^8.0.0"
  }
}
  1. 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"
}
  1. 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

  1. Initial attempt (commit a951f4c): Updated Shakapacker, added Rspack deps, created rspack config
  2. First error: Module not found errors → created i18n stub files (commit 879d171)
  3. Ruby version mismatch: Updated Gemfile (commit 087ec70)
  4. SSR renderFunction error: Changed SWC runtime to classic (commit 5d85f15)
  5. CSS modules SSR error (1st attempt): Tried to fix CSS modules in server config (commit 3fe61f0)
  6. ReScript resolution: Added .bs.js extension (commit fbc5781)
  7. ReScript dependency: Created patch for json-combinators (commits 76921b8, 012b0b7)
  8. CSS modules SSR error (2nd attempt): Moved CSS fix into function (commit 1685fb4, 28014b2)
  9. CSS modules SSR error (final fix): Added cssExtractLoader to filter (commit 3da3dfc) ✅
  10. Cleanup: Removed generated i18n files (commit 71b934a)
  11. 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

  1. Patches README: patches/README.md explaining why patches are needed
  2. JSDoc comments: Added to commonWebpackConfig() and configureServer()
  3. Inline comments: Explaining critical CSS modules fixes
  4. Upstream issues:

Suggested React on Rails Documentation Additions

  1. Migration Guide: Add dedicated "Migrating to Rspack" section
  2. Breaking Changes: Document Shakapacker 9's CSS Modules namedExport default change
  3. Troubleshooting Section:
    • CSS modules returning undefined in SSR
    • ReScript module resolution issues
    • Test flakiness from CSS extraction in server bundle
  4. Configuration Patterns: Document bundler auto-detection pattern
  5. Example Implementation: Link to this PR as reference

Key Takeaways

  1. CSS Modules breaking change: Shakapacker 9's namedExport: true default is a breaking change that will affect most projects
  2. Rspack CSS extraction: Different loader paths require explicit handling in server config
  3. Configuration timing: CSS fixes must be inside the function to apply to fresh config
  4. Test flakiness: Incomplete CSS extraction filtering causes non-deterministic failures
  5. ReScript ecosystem: Some packages ship source-only and need patches
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions