Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
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
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)
9 changes: 9 additions & 0 deletions client/app/libs/i18n/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Default locale and messages for i18n
export const defaultLocale = 'en';

export const defaultMessages = {
'app.name': 'React Webpack Rails Tutorial',
'comment.form.name_label': 'Name',
'comment.form.text_label': 'Text',
'comment.form.submit': 'Submit',
};
15 changes: 15 additions & 0 deletions client/app/libs/i18n/translations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Translation messages for different locales
export const translations = {
en: {
'app.name': 'React Webpack Rails Tutorial',
'comment.form.name_label': 'Name',
'comment.form.text_label': 'Text',
'comment.form.submit': 'Submit',
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Eliminate code duplication with defaultMessages.

The English translations here duplicate defaultMessages from client/app/libs/i18n/default.js. This violates the DRY principle and creates a maintenance burden—updating English text requires changes in two places.

Apply this refactor to import and reuse defaultMessages:

+import { defaultMessages } from './default';
+
 // Translation messages for different locales
 export const translations = {
-  en: {
-    'app.name': 'React Webpack Rails Tutorial',
-    'comment.form.name_label': 'Name',
-    'comment.form.text_label': 'Text',
-    'comment.form.submit': 'Submit',
-  },
+  en: defaultMessages,
   es: {
     'app.name': 'Tutorial de React Webpack Rails',
     'comment.form.name_label': 'Nombre',
     'comment.form.text_label': 'Texto',
     'comment.form.submit': 'Enviar',
   },
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const translations = {
en: {
'app.name': 'React Webpack Rails Tutorial',
'comment.form.name_label': 'Name',
'comment.form.text_label': 'Text',
'comment.form.submit': 'Submit',
},
import { defaultMessages } from './default';
// Translation messages for different locales
export const translations = {
en: defaultMessages,
es: {
'app.name': 'Tutorial de React Webpack Rails',
'comment.form.name_label': 'Nombre',
'comment.form.text_label': 'Texto',
'comment.form.submit': 'Enviar',
},
};
🤖 Prompt for AI Agents
In client/app/libs/i18n/translations.js around lines 2 to 8, the English
translations duplicate strings from client/app/libs/i18n/default.js; import
defaultMessages from that file and reuse it rather than hardcoding the same keys
here (e.g., replace the inline en object with a spread/merge of defaultMessages
into translations.en), ensuring any additional or overridden keys remain after
the spread so updates live in one place.

es: {
'app.name': 'Tutorial de React Webpack Rails',
'comment.form.name_label': 'Nombre',
'comment.form.text_label': 'Texto',
'comment.form.submit': 'Enviar',
},
};
9 changes: 9 additions & 0 deletions config/rspack/alias.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const { resolve } = require('path');

module.exports = {
resolve: {
alias: {
Assets: resolve(__dirname, '..', '..', 'client', 'app', 'assets'),
},
},
};
24 changes: 24 additions & 0 deletions config/rspack/clientRspackConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const rspack = require('@rspack/core');
const commonRspackConfig = require('./commonRspackConfig');

const configureClient = () => {
const clientConfig = commonRspackConfig();

clientConfig.plugins.push(
new rspack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
ActionCable: '@rails/actioncable',
}),
);

// server-bundle is special and should ONLY be built by the serverConfig
// In case this entry is not deleted, a very strange "window" not found
// error shows referring to window["webpackJsonp"]. That is because the
// client config is going to try to load chunks.
delete clientConfig.entry['server-bundle'];

return clientConfig;
};

module.exports = configureClient;
79 changes: 79 additions & 0 deletions config/rspack/commonRspackConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Common configuration applying to client and server configuration
const { generateWebpackConfig, merge } = require('shakapacker');

const commonOptions = {
resolve: {
extensions: ['.css', '.ts', '.tsx', '.bs.js'],
},
};

// add sass resource loader
const sassLoaderConfig = {
loader: 'sass-resources-loader',
options: {
resources: './client/app/assets/styles/app-variables.scss',
},
};

const ignoreWarningsConfig = {
ignoreWarnings: [/Module not found: Error: Can't resolve 'react-dom\/client'/],
};

// Copy the object using merge b/c the baseClientRspackConfig and commonOptions are mutable globals
const commonRspackConfig = () => {
const baseClientRspackConfig = generateWebpackConfig();

// Fix all CSS-related loaders to use default exports instead of named exports
// Shakapacker 9 defaults to namedExport: true, but existing code expects default exports
baseClientRspackConfig.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';
}
}
});

const scssConfigIndex = baseClientRspackConfig.module.rules.findIndex((config) =>
'.scss'.match(config.test) && config.use,
);

if (scssConfigIndex === -1) {
console.warn('No SCSS rule with use array found in rspack config');
} else {
// Configure sass-loader to use the modern API
const scssRule = baseClientRspackConfig.module.rules[scssConfigIndex];
const sassLoaderIndex = scssRule.use.findIndex((loader) => {
if (typeof loader === 'string') {
return loader.includes('sass-loader');
}
return loader.loader && loader.loader.includes('sass-loader');
});

if (sassLoaderIndex !== -1) {
const sassLoader = scssRule.use[sassLoaderIndex];
if (typeof sassLoader === 'string') {
scssRule.use[sassLoaderIndex] = {
loader: sassLoader,
options: {
api: 'modern'
}
};
} else {
sassLoader.options = sassLoader.options || {};
sassLoader.options.api = 'modern';
}
}

baseClientRspackConfig.module.rules[scssConfigIndex].use.push(sassLoaderConfig);
}

return merge({}, baseClientRspackConfig, commonOptions, ignoreWarningsConfig);
};

module.exports = commonRspackConfig;
25 changes: 25 additions & 0 deletions config/rspack/development.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

const { devServer, inliningCss } = require('shakapacker');

const rspackConfig = require('./rspackConfig');

const developmentEnvOnly = (clientRspackConfig, _serverRspackConfig) => {
// plugins
if (inliningCss) {
// Note, when this is run, we're building the server and client bundles in separate processes.
// Thus, this plugin is not applied to the server bundle.

// eslint-disable-next-line global-require
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
clientRspackConfig.plugins.push(
new ReactRefreshWebpackPlugin({
overlay: {
sockPort: devServer.port,
},
}),
);
}
};

module.exports = rspackConfig(developmentEnvOnly);
9 changes: 9 additions & 0 deletions config/rspack/production.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
process.env.NODE_ENV = process.env.NODE_ENV || 'production';

const rspackConfig = require('./rspackConfig');

const productionEnvOnly = (_clientRspackConfig, _serverRspackConfig) => {
// place any code here that is for production only
};

module.exports = rspackConfig(productionEnvOnly);
15 changes: 15 additions & 0 deletions config/rspack/rspack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { env, generateWebpackConfig } = require('shakapacker');
const { existsSync } = require('fs');
const { resolve } = require('path');

const envSpecificConfig = () => {
const path = resolve(__dirname, `${env.nodeEnv}.js`);
if (existsSync(path)) {
console.log(`Loading ENV specific rspack configuration file ${path}`);
return require(path);
}

return generateWebpackConfig();
};

module.exports = envSpecificConfig();
34 changes: 34 additions & 0 deletions config/rspack/rspackConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const clientRspackConfig = require('./clientRspackConfig');
const serverRspackConfig = require('./serverRspackConfig');

const rspackConfig = (envSpecific) => {
const clientConfig = clientRspackConfig();
const serverConfig = serverRspackConfig();

if (envSpecific) {
envSpecific(clientConfig, serverConfig);
}

let result;
// For HMR, need to separate the the client and server rspack configurations
if (process.env.WEBPACK_SERVE || process.env.CLIENT_BUNDLE_ONLY) {
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the client bundles.');
result = clientConfig;
} else if (process.env.SERVER_BUNDLE_ONLY) {
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating only the server bundle.');
result = serverConfig;
} else {
// default is the standard client and server build
// eslint-disable-next-line no-console
console.log('[React on Rails] Creating both client and server bundles.');
result = [clientConfig, serverConfig];
}

// To debug, uncomment next line and inspect "result"
// debugger
return result;
};

module.exports = rspackConfig;
Loading