This document describes everything needed to integrate Webpack with a Rails project. The Webpack configuration here could be simplified even more if React & Typescript support is not needed.
module Webpack
module Asset
extend self
def path(name)
manifest.fetch name
end
private
def manifest
return @manifest if @manifest && Config.cache_manifest?
@manifest = read_manifest
end
def read_manifest
manifest_path = Rails.root.join 'public', 'packs', 'manifest.json'
missing_manifest_error =
if Config.dev_server? && DevServer.running?
'Please, wait for Webpack dev server build to finish...'
else
'Please, run a Webpack build'
end
raise missing_manifest_error unless File.exist? manifest_path
JSON.parse File.read(manifest_path)
end
end
end
require 'open3'
module Webpack
module Compiler
extend self
def compile
logger = Logger.new STDOUT
logger.info 'Compiling assets...'
stdout, stderr, status = Open3.capture3 'yarn', 'webpack', '--config', './config/webpack/build.js'
if status.success?
logger.info 'Assets compiled successfuly'
else
logger.error "Asset compilation failed\n#{[stdout, stderr].delete_if(&:empty?).join("\n\n")}"
raise stderr
end
true
end
end
end
module Webpack
module DevServer
extend self
def running?
raise 'Webpack Dev Server configuration not specified' unless Config.dev_server?
Socket.tcp(Config.dev_server_host, Config.dev_server_port, connect_timeout: 0.01).close
true
rescue Errno::ECONNREFUSED
false
end
end
end
namespace :webpack do
desc 'Compile JavaScript packs using webpack for production with digests'
task compile: [:environment] do
Webpack::Compiler.compile
end
end
if Rake::Task.task_defined? 'assets:precompile'
Rake::Task['assets:precompile'].enhance ['webpack:compile']
else
Rake::Task.define_task 'assets:precompile' => ['webpack:compile']
end
module ApplicationHelper
def javascript_pack_tag(name, defer: false, **options)
if Webpack::Config.dev_server? && !Webpack::DevServer.running?
raise 'Webpack Dev Server is expected to be running in this environment'
end
javascript_include_tag Webpack::Asset.path("#{name}.js"), **options.tap { |o| o[:defer] = defer }
end
def stylesheet_pack_tag(name, **options)
return unless Webpack::Config.extract_css?
stylesheet_link_tag Webpack::Asset.path("#{name}.css"), **options
end
# ... rest of file ...
end
development:
cache_build: true
development_build: true
dev_server:
host: localhost
port: 3035
test:
cache_build: true
development_build: true
dev_server: false
production:
cache_build: false
development_build: false
dev_server: false
const path = require('path')
const {load} = require('js-yaml')
const {readFileSync} = require('fs')
const common = load(readFileSync(path.resolve(__dirname, './common.yml')), 'utf8')
const environments = Object.keys(common)
const railsEnv = environments.includes(process.env.RAILS_ENV) ? process.env.RAILS_ENV : 'development'
if (!process.env.NODE_ENV) {
if (railsEnv === 'development' || railsEnv === 'test') {
process.env.NODE_ENV = 'development'
} else {
process.env.NODE_ENV = 'production'
}
}
const config = common[railsEnv]
const ensure = (obj, field) => {
if (obj[field] === undefined) {
throw new Error(`${field} must be specified`)
}
}
ensure(config, 'cache_build')
ensure(config, 'development_build')
ensure(config, 'dev_server')
const rootPath = path.resolve(__dirname, '../../')
const bundles = {
application: [
path.join(rootPath, 'app/javascript/packs/application.js'),
path.join(rootPath, 'app/javascript/packs/application.scss')
],
}
const output = {
filename: 'js/[name]-[contenthash].js',
chunkFilename: 'js/[name]-[contenthash].chunk.js',
hotUpdateChunkFilename: 'js/[id]-[fullhash].hot-update.js',
path: path.join(rootPath, 'public/packs')
}
module.exports = {
extractCss: config.extract_css,
cacheBuild: config.cache_build,
isDevelopmentBuild: config.development_build,
devServer: config.dev_server,
bundles,
output
}
const WebpackAssetsManifest = require('webpack-assets-manifest')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const {cacheBuild, isDevelopmentBuild, bundles, output} = require('./env')
module.exports = {
mode: isDevelopmentBuild ? 'development' : 'production',
devtool: isDevelopmentBuild ? 'eval-source-map' : 'source-map',
cache: cacheBuild
? {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
: false,
entry: bundles,
output: {...output, publicPath: '/packs/'},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.(woff2)$/i,
type: 'asset/inline'
},
{
test: /\.(svg|png|jpg|gif)$/i,
type: 'asset'
},
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
options: {
cacheDirectory: cacheBuild,
cacheCompression: false,
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
corejs: '3.8',
modules: 'auto',
bugfixes: true,
loose: true,
exclude: ['transform-typeof-symbol']
}
],
'@babel/preset-react',
['@babel/preset-typescript', {isTSX: true, allExtensions: true}]
],
plugins: ['@babel/plugin-transform-runtime']
}
},
{
test: /\.m?js/,
resolve: {
fullySpecified: false
}
},
isDevelopmentBuild && {
test: /\.js$/,
enforce: 'pre',
use: ['source-map-loader']
}
].filter(Boolean)
},
plugins: [
new WebpackAssetsManifest({
writeToDisk: true,
output: 'manifest.json',
entrypointsUseAssets: true,
publicPath: true
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[id].[contenthash].css'
}),
!isDevelopmentBuild &&
new CompressionPlugin({
algorithm: 'gzip',
filename: '[path][base].gz',
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/
}),
!isDevelopmentBuild &&
new CompressionPlugin({
algorithm: 'brotliCompress',
filename: '[path][base].br',
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/
})
].filter(Boolean),
resolve: {
extensions: [
'.tsx',
'.ts',
'.jsx',
'.mjs',
'.js',
'.sass',
'.scss',
'.css',
'.png',
'.svg',
'.gif',
'.jpeg',
'.jpg'
],
plugins: [new TsconfigPathsPlugin()],
modules: ['app/javascript', 'node_modules']
}
}
const WebpackAssetsManifest = require('webpack-assets-manifest')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const {extractCss, cacheBuild, isDevelopmentBuild, devServer, bundles, output} = require('./env')
const analyzeBundle = !!process.env.BUNDLE_ANALYZER
if (!devServer) throw new Error('Webpack dev server configuration is not specified for this environment')
if (extractCss) throw new Error('Webpack dev server expects CSS to be served through the JS bundle')
module.exports = {
mode: 'development',
devtool: 'eval-source-map',
optimization: {
runtimeChunk: {
name: 'runtime'
}
},
cache: cacheBuild
? {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
: false,
entry: bundles,
output: {...output, publicPath: `http://${devServer.host}:${devServer.port}/packs/`},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(woff2)$/i,
type: 'asset/inline'
},
{
test: /\.(svg|png|gif|jpg)$/i,
type: 'asset'
},
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
options: {
cacheDirectory: cacheBuild,
cacheCompression: false,
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry',
corejs: '3.8',
modules: 'auto',
bugfixes: true,
loose: true,
exclude: ['transform-typeof-symbol']
}
],
'@babel/preset-react',
['@babel/preset-typescript', {isTSX: true, allExtensions: true}]
],
plugins: ['@babel/plugin-transform-runtime', 'react-refresh/babel']
}
},
{
test: /\.m?js/,
resolve: {
fullySpecified: false
}
},
isDevelopmentBuild && {
test: /\.js$/,
enforce: 'pre',
use: ['source-map-loader']
}
].filter(Boolean)
},
plugins: [
new WebpackAssetsManifest({
writeToDisk: true,
output: 'manifest.json',
entrypointsUseAssets: true,
publicPath: true
}),
new ReactRefreshWebpackPlugin(),
new ForkTsCheckerWebpackPlugin({
typescript: {
diagnosticOptions: {
semantic: true,
syntactic: true
}
}
}),
analyzeBundle && new BundleAnalyzerPlugin()
].filter(Boolean),
resolve: {
extensions: [
'.tsx',
'.ts',
'.jsx',
'.mjs',
'.js',
'.sass',
'.scss',
'.css',
'.png',
'.svg',
'.gif',
'.jpeg',
'.jpg'
],
plugins: [new TsconfigPathsPlugin()],
modules: ['app/javascript', 'node_modules']
},
devServer: {
client: {
overlay: true
},
static: {
publicPath: output.path
},
headers: {
'Access-Control-Allow-Origin': '*'
},
compress: true,
hot: true,
liveReload: false,
host: devServer.host,
port: devServer.port
},
watchOptions: {
aggregateTimeout: 600
}
}
#!/bin/bash
yarn webpack --config ./config/webpack/build.js
#!/bin/bash
yarn webpack-dev-server --config ./config/webpack/dev-build.js
#!/bin/bash
rm -rf public/packs
rm -rf node_modules/.cache