Post

Rails with Webpacker, a full setup

Create a new Rails application without Webpacker

Minimal application

1
2
$> rails new myapp --minimal
$> cd myapp

The minimal flag means “please install a default Rails app with the fewest possible dependencies”. See this article about the rails new command.

Add the bare minimum files

Now create a default route, controller, and view

1
2
3
4
5
# inside config/routes.rb  
Rails.application.routes.draw do  
  get "welcome/index"  
  root to: "welcome#index"  
end  
1
2
3
# inside app/controllers/welcome_controller.rb  
class WelcomeController < ApplicationController  
end  
1
2
<!-- inside app/views/welcome/index.html.erb -->  
<h1> Hello, Rails and Webpack ! </h1>

And run the Rails application

1
$/myapp> bin/rails server

Open the browser at localhost:3000, you should see “Hello, Rails and Webpack !”

Add the Webpacker gem to Rails

Open the Gemfile and add this line :

1
2
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 5.0'

Stop the server, and run

1
$myapp> bundle install

And

1
$myapp> bin/rails webpacker:install

Here is the list of what the last command does (extracted from output, and simplified) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
      create  config/webpacker.yml
      create  config/webpack
      create  config/webpack/development.js
      create  config/webpack/environment.js
      create  config/webpack/production.js
      create  config/webpack/test.js
      create  postcss.config.js
      create  babel.config.js
      create  .browserslistrc
      The JavaScript app source directory already exists (app/javascript)
      create    bin/webpack
      create    bin/webpack-dev-server
      append  .gitignore
      run  yarn add @rails/webpacker@5.2.1 from "."

And now our package.json look like this :

1
2
3
4
5
6
7
8
9
10
{
  "dependencies": {
    "@rails/webpacker": "5.3.0",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.11.2"
  }
}

This highlight the fact that webpacker is just a wrapper around webpack.

Change Rails default directory for Webpacker

The annoying thing with Rails and Webpack(er) is that the default integration considers that only JavaScript should be handled by Webpack, and other assets should be handled by an older gem (Sprockets). Now webpacker is fully mature, so let’s use it only.

Rename app/javascript to app/frontend, like this :

1
$/myapp> mv app/javascript app/frontend  

In config/webpacker.yml, change the name of the folder as follow :

1
2
3
4
# Inside webpacker.yml, first lines  
default: &default  
  source_path: https://res.cloudinary.com/bdavidxyz-com/image/upload/w_1600,h_836,q_100/l_text:Karla_72_bold:Rails%20with%20Webpacker%20%20a%20full%20setup,co_rgb:ffe4e6,c_fit,w_1400,h_240/fl_layer_apply,g_south_west,x_100,y_180/l_text:Karla_48:A%20Ruby-on-Rails%20tutorial,co_rgb:ffe4e680,c_fit,w_1400/fl_layer_apply,g_south_west,x_100,y_100/newblog/globals/bg_me.jpg
  source_entry_path: https://res.cloudinary.com/bdavidxyz-com/image/upload/w_1600,h_836,q_100/l_text:Karla_72_bold:Rails%20with%20Webpacker%20%20a%20full%20setup,co_rgb:ffe4e6,c_fit,w_1400,h_240/fl_layer_apply,g_south_west,x_100,y_180/l_text:Karla_48:A%20Ruby-on-Rails%20tutorial,co_rgb:ffe4e680,c_fit,w_1400/fl_layer_apply,g_south_west,x_100,y_100/newblog/globals/bg_me.jpg

Add a first frontend file

As you can guess from the configuration above, Webpack will turn into a “pack” (compiled file that could be referenced in a html page) any file under the app/frontend/packs directory.

So let’s add app/frontend/packs/mainjs.js

1
2
// inside app/frontend/packs/mainjs.js
console.log("Hello from mainjs")

Build (web)packs with Rails

Rails will automatically run some build when running a local server, but for this tutorial, we want to understand exactly what’s going on.

So far, you should have a /public folder at the root of your app, but not any /public/packs folder, which is the default build output of webpacker.

Let’s build our packs buy running

1
2
$/myapp> bin/rails assets:clobber
$/myapp> bin/rails webpacker:compile

The first line cleans any previous build, the second line runs the actual build.

Now let’s check what happened

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
**bin/rails webpacker:compile**

**Compiling...**

**Compiled all packs in /Users/david/workspace/myapp/public/packs**

**warning package.json: No license field**

  

**Hash: 242563483b6f56b94cc3**

**Version: webpack 4.46.0**

**Time: 1316ms**

**Built at: 04/12/2021 9:58:18 AM**

**Asset Size  Chunks Chunk Names**

**js/mainjs-266dceea7f600337d3ed.js  1 KiB 0  [emitted] [immutable]  mainjs**

**js/mainjs-266dceea7f600337d3ed.js.br  467 bytes  [emitted]**

**js/mainjs-266dceea7f600337d3ed.js.gz  528 bytes  [emitted]**

**js/mainjs-266dceea7f600337d3ed.js.map 4.67 KiB 0  [emitted] [dev]  mainjs**

**js/mainjs-266dceea7f600337d3ed.js.map.br 1.59 KiB  [emitted]**

**js/mainjs-266dceea7f600337d3ed.js.map.gz 1.78 KiB  [emitted]**

**manifest.json  329 bytes  [emitted]**

**manifest.json.br  122 bytes  [emitted]**

**manifest.json.gz  135 bytes  [emitted]**

**Entrypoint mainjs = js/mainjs-266dceea7f600337d3ed.js js/mainjs-266dceea7f600337d3ed.js.map**

**[0] ./app/frontend/packs/mainjs.js 33 bytes {0} [built]**

Webpack creates a bunch of files, even if we created so far a simple JS file with a simple console.log. Open the public/packs/js folder to notice it :

Many pages
Many files

Ok, for one file : six possible extensions.

Look also at the manifest.json file :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "entrypoints": {
    "mainjs": {
      "js": [
        "/packs/js/mainjs-266dceea7f600337d3ed.js"
      ],
      "js.map": [
        "/packs/js/mainjs-266dceea7f600337d3ed.js.map"
      ]
    }
  },
  "mainjs.js": "/packs/js/mainjs-266dceea7f600337d3ed.js",
  "mainjs.js.map": "/packs/js/mainjs-266dceea7f600337d3ed.js.map"
}

The manifest.json is here to help. If an HTML file is looking for a js file named “mainjs”, it will ask to the manifest.json where the exact location is.

Webpacker for JavaScript

The example above was with a JavaScript file, so good news : we already made half of the job.

Reference an existing pack

Now reference our mainjs file into the HTML by modifying the main layout (app/views/layouts/application.html.erb) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- inside app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>Rails and Webpack</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    
    <!-- Remove stylesheet_link_tag  -->
    <!-- Add the line below  -->
    <%= javascript_pack_tag 'mainjs' %>    
  </head>

  <body>
    <%= yield %>
  </body>
</html>

Stop and restart the local web server

1
$/myapp> bin/rails server

Open the browser at localhost:3000 to see if the log message is displayed in the browser’s dev tools.

Displayed log<
Displayed log

Using an es6 module

If you want a regular ES6 module that will not be a pack, simply add a folder app/frontend/js, like this :

1
2
3
4
5
// inside app/frontend/js/magicAdd.js
const  magicAdd = (a, b) => {
  return a + b;
}
export default  magicAdd;

And reference this file from the pack :

1
2
3
4
5
6
7
8
// inside app/frontend/packs/mainjs.js

import magicAdd from '../js/magicAdd.js'

let a = magicAdd(2, 4);

// remove old console.log, and replace by the one below
console.log(`From mainjs, magicAdd result is ${a}`)

Now open your browser and check the browser’s console to see if everything works properly.

Webpacker for Stylesheets

First, change every extract_css: false to extract_css: true inside config/webpacker.yml. Or webpack won’t be able to detect scss files as a possible pack.

Now, create a pack, but this time, it should be a scss file (app/frontend/packs/mainstyle.scss) :

1
2
3
4
5
6
// inside app/frontend/packs/mainstyle.scss  
 
// Just a quick ugly style to see if our CSS works  
h1 {  
  text-decoration: underline;  
}  

Just one file will not be enough in production-ready app, so, like javascript, let’s see how to reference another file :

1
2
3
4
// inside app/frontend/css/mycomponent.scss
h1 {  
  font-style: italic;
}  

Update mainstyle.scss to reference this new custom component :

1
2
3
4
5
6
7
// inside app/frontend/packs/mainstyle.scss  
@import "../css/mycomponent.scss"; 
 
// Just a quick ugly style to see if our CSS works  
h1 {  
  text-decoration: underline;  
}  

Good ! So now let’s tell the main layout that we want to use webpack for stylesheets :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- inside app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>My title</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    
    <!-- Add line below, stylesheed_pack_tag -->
    <%= stylesheet_pack_tag 'mainstyle' %>
    <%= javascript_pack_tag 'mainjs' %>
  </head>

  <body>
    <%= yield %>

  </body>
</html>

Stop your local web server, and run

1
2
$/myapp> bin/rails assets:clobber
$/myapp> bin/rails webpacker:compile

Check your manifest.json file, and run your local web server again, you should see a title that is displayed both italic and underlined.

Webpacker for font family

Download the “Asap” font here, unzip it, and copy/paste the file named Asap-VariableFont_wght.ttf into app/frontend/font/Asap/Asap-VariableFont_wght.ttf

Then add a file font_faces.scss at app/frontend/css/font_faces.scss

1
2
3
4
5
6
@font-face {
  font-family: 'Asap';
  src: url('../font/Asap/Asap-VariableFont_wght.ttf') format('woff2 supports variations'),
       url('../font/Asap/Asap-VariableFont_wght.ttf') format('woff2-variations');
  font-weight: 100 1000;
}

Update mainstyle.scss to reference the font face :

1
2
3
4
5
6
7
8
9
10
11
// inside app/frontend/packs/mainstyle.scss  
// Add the line below on the top of the file
@import '../css/font_faces';
@import "../css/mycomponent.scss"; 
 
// Just a quick ugly style to see if our CSS works  
h1 {  
  // And add this line below to ensure font-family works
  font-family: 'Asap';
  text-decoration: underline;  
}  

Again, stop your local web server, and run

1
2
$/myapp> bin/rails assets:clobber
$/myapp> bin/rails webpacker:compile

Check your manifest.json file, and run your local web server again, you should see a title that is displayed italic, underlined… and with the font-face named “Asap”.

Webpacker for images

Create a SVG file named rectangle.svg into app/frontend/img/rectangle.svg

1
2
3
4
<!-- inside app/frontend/img/rectangle.svg -->
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
  <path d="M10 10 H 90 V 90 H 10 Z" fill="red" stroke="black"/>
</svg>

Then add one line to the main JS file, like this :

1
2
3
4
5
6
// inside app/frontend/packs/mainjs.js

// Add the line below
const images = require.context('../img', true)

// Everything else remain the same

Now the image can be referenced from any other file

1
2
3
<!-- inside app/views/welcome/index.html.erb -->  
<h1> Hello, Rails and Webpack ! </h1>
<%= image_pack_tag 'media/img/rectangle.svg', alt: 'A rectangle' %>

Please note that webpack adds the “media” prefix to the path.

Again, stop your local web server, and run

1
2
$/myapp> bin/rails assets:clobber
$/myapp> bin/rails webpacker:compile

Check your manifest.json file, and run your local web server again, open the browser, and ta-da : you should now see the rectangle that appears.

Conclusion

We have seen how to make Webpacker work with Rails. For Javascript files, stylesheets, images, and fonts. With a real example, from scratch. Don’t worry, you won’t have to compile assets and restart the server on each change. This was needed for this tutorial only because we added a fresh new kind of asset at each step.

Enjoy !

This post is licensed under CC BY 4.0 by the author.