When adjusting webpacker.yml is not enough, it might be necessary to modify Webpacker's default webpack configuration. Configuring webpack is precisely the main job of Webpacker's NPM package, @rails/webpacker.

Subscribe to my newsletter, Joy of Rails, to get notified about new content.

This post provides an overview of how to customize its default settings for your Rails application. It's a followup to my last post on understanding webpacker.yml.

tl;dr

Use webpacker.yml to modify a limited number of settings, some of which are shared between Rails and webpack.

Modify the base @rails/webpacker configuration for any webpack-specific config options in JavaScript.

At a glance

In an ideal world, webpack configuration would be transparent to Rails developers. Let's face it, most developers would rather spend their time building features than configuring their asset compilation process. Still, the base webpack configuration provided by @rails/webpacker may not satisfy your application's needs, so modifications may be inevitable.

Some things you might want to modify or change:

So, where to start?

First, we'll take a look at the environment-specific JavaScript files Webpacker installs in the config/webpack/ directory within your Rails application:

$ tree config/webpack
config/webpack
├── development.js
├── environment.js
├── production.js
└── test.js

For experienced frontend developers wondering where is webpack.config.js?, it's here, as config/webpack/{development,test,production}.js; there is a separate config file for each Rails environment.

Running ./bin/webpack is similar to typing out one the following commands to run webpack directly, depending on your current RAILS_ENV:

RAILS_ENV=development yarn webpack --config ./config/webpack/development.js
RAILS_ENV=test yarn webpack --config ./config/webpack/test.js
RAILS_ENV=production yarn webpack --config ./config/webpack/production.js

These files are to webpack configuration what Ruby config files config/environments/{development,test,production}.rb are Rails configuration: the place to customize environment-specific needs. Just as config/application.rb is the shared configuration for all Rails environments, so is config/webpack/environment.js for each of the environment-specific webpack config files.

The config/webpack/environment.js file is where the default webpack configuration is imported via @rails/webpacker. The named import environment is an abstraction around the webpack config. It provides its own API to support modification and extension.

// config/webpack/environment.js
const { environment } = require('@rails/webpacker')

module.exports = environment

Each of the environment-specific files are more or less the same; they import the base environment object and must convert it to a JavaScript object that matches the webpack configuration schema:

// config/webpack/development.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development'

const environment = require('./environment')

module.exports = environment.toWebpackConfig()

Under the hood

There's a problem though with making changes through an abstraction layer; it's hard to see what you want to change. Since the API is not fully documented yet, you may need to do some digging.

To print it out on the command line, here's a handy one-line script:

$ RAILS_ENV=development node -e 'console.dir(require("./config/webpack/development"), { depth: null })'
# displays entire webpack config object

console.dir is a nice alternative to console.log for inspecting JavaScript objects.

To go deeper, you may want to checkout the source for the Environment class in the @rails/webpacker package.

An environment instance has loaders and plugins properties that are each implemented as bespoke ConfigList objects that subclass JavaScript's Array class (source).

Loaders in webpack define transforms based on file type or name; they are analogous to preprocessors in the Sprockets. Plugins in webpack support a wider range of tasks, like optimization or moving/copying assets, by leveraging webpack's exhaustive list of hooks to tap into the compilation process.

The environment.config property is also useful is you want to simply override defaults with a raw object matching a portion of the webpack config schema.

Examples

Here's the rub: Webpacker, in true Rails fashion, aims to provide convention over configuration, however, the design of webpack skews heavily in the other direction: it is extremely flexible and malleable through its support for plugins and a large number of configurable options. Webpack is built to support a broad range of use cases to meet the needs of a diverse frontend landscape. Webpacker's opinionated approach may leave out something you need.

This means there may come a time when you need to roll up your sleeves and peel back the abstraction layer and modify the base Webpacker environment object. At this point, it may help to read up on the Webpacker docs for modifying the webpack configuration. Below are just a few examples.

Providing jQuery as an import to legacy plugins and exposing to global scope

Here's an example of how to "provide" a jQuery import to a legacy package that doesn't understand modules and to "expose" the $ variable for the global scope (so you can use $(...) expressions in raw <script> tags).

$ yarn add expose-loader
// config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

// Adds `var jQuery = require('jquery') to legacy jQuery plugins
environment.plugins.append(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
  }),
)

// Adds window.$ = require('jquery')
environment.loaders.append('jquery', {
  test: require.resolve('jquery'),
  use: [
    {
      loader: 'expose-loader',
      options: '$',
    },
  ],
})

module.exports = environment

This StackOverflow post provides more general context on making legacy jQuery play nice with webpack.

Loading dotenv ENV vars in webpack

A nice feature of webpack and the default webpack configuration is that it will make ENV vars available to the build process. For example, using process.ENV.MY_API_KEY will be compiled to "my-api-key-value" in your webpack build. To emulate the behavior of the popular dotenv-rails project, which can load ENV vars defined in .env* files, you could add configuration as follows:

yarn add dotenv
// config/webpack/environment.js

const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
const dotenv = require('dotenv')

const dotenvFiles = [
  `.env.${process.env.NODE_ENV}.local`,
  '.env.local',
  `.env.${process.env.NODE_ENV}`,
  '.env',
]
dotenvFiles.forEach((dotenvFile) => {
  dotenv.config({ path: dotenvFile, silent: true })
})

module.exports = environment

Original source in the Webpacker docs.

Enabling webpack optimization

The splitChunks API instructs webpack to share dependencies across bundles. Using this optimization step must be combined with different view helpers; see theWebpacker docs for more info.

// config/webpack/environment.js

// Enable the default config
environment.splitChunks()

Tip: Use the splitChunks API for solving the "page-specific JavaScript" problem with Webpacker.

See the webpack splitChunks docs for more info.

Using module aliases

The Webpacker environment API also supports a config.merge function to override raw webpack config options. This example would allow you to import images from the app/assets directory using import 'images/path/to/image.jpg'.

// config/webpack/environment.js
const { resolve } = require('path')

// Enable the default config
environment.config.merge({
  resolve: {
    alias: {
      images: resolve('app/assets/images'),
    },
  },
})

Learn more in the webpack resolve docs.

Overriding the default options for compiling CSS modules

This change involves modifying an existing loader, which can be accessed using environment.loaders.get(key) and replacing its options property.

// config/webpack/environment.js
const { environment } = require('@rails/webpacker')

const myCssLoaderOptions = {
  modules: {
    localIdentName: '[name]__[local]___[hash:base64:10]',
  },
  sourceMap: true,
}

const CSSLoader = environment.loaders
  .get('moduleSass')
  .use.find((el) => el.loader === 'css-loader')

CSSLoader.options = { ...CSSLoader.options, ...myCssLoaderOptions }

module.exports = environment

Original source in the Webpacker docs.

Wrapping up

In this post, I've attempted to shed some light on the role of the @rails/webpacker project in your Rails app. We demonstrated how the Webpacker wraps the default webpack configuration along with some examples to illustrate how one might modify and extend the config for selected use cases.

For readers who need to go even further, there's no better place to go next than webpack's Getting Started guide.

Discuss it on Twitter · Published on Apr 18, 2020

More posts

Why does Rails 6 include both Webpacker and Sprockets?

A new Rails 6 application will install both Webpacker and Sprockets by default. Don't they solve the same problem? This article dives into why Sprockets lives on even though webpack has surpassed most of its features and why you might want to choose one over the other.

How to debug webpack on Rails

Understanding your Rails webpack configuration and build output can be a little confusing, especially if you're new to either Rails or webpack. This post contains a few tips for debugging your Webpacker setup, some specific to Rails Webpacker, some generally applicable to webpack.

Understanding webpacker.yml

Configuring Webpacker can be a daunting task. In this guide, we will take a look at the options provided via the webpacker.yml file and supported environment variable overrides.

Photo by Ingo Doerrie on Unsplash