pack a webpack into a rails app

programming ruby rails js

✨ UPDATE: rails has some support for this out of the box now, check it out! It’s much better and easier than what i wrote below. ✨

i have a little app that’s built with npm and webpack, and talks to an api built on rails. they each live independent of each other. this setup has some downsides for me:

because my api is built on rails, i would like to do server rendering there. this should simplify my deployment and system tests too. webpack does all the packaging and filename hashing so i don’t even need to depend on the asset pipeline.

so i’ve documented this and wrote it down as a walk through. it gets deployed to heroku at the end. you can the result as an example app on github.

create a new rails app, with or without the asset pipeline:

# scaffold a new rails app and go to it, use postgres if you want it on heroku
rails new webpackin -d postgresql
cd webpackin

# scaffold a controller so we can look at something 
bin/rails g controller pages
echo "<h1>hello, webpack</h1>" > app/views/pages/index.html.erb

change config/routes.rb so you can see your page:

Rails.application.routes.draw do
  root to: "pages#index"
end

rails setup is complete!

set up npm:

npm init -y
npm install webpack 
echo "node_modules" >> .gitignore

make a directory to keep all of your webpack stuff in. this could be in app but i prefer to keep webpack stuff separate from rails.

i put mine in client:

mkdir -p client/src
echo "alert('howdy')" > client/src/app.js

change client/webpack.config.js to be the minimum amount of code you need to take our client/src, and output a bundle into public/webpack:

module.exports = {
  entry: "./client/src/app.js",
  output: { 
    path: __dirname + "/../public/webpack",
    publicPath: "/webpack/",
    filename: "bundle.js"
  }
}

edit package.json and add build and watch scripts. this will run the local copy of webpack we installed before:

{
  
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config client/webpack.config.js",
    "watch": "webpack --config client/webpack.config.js --watch"
  },
  
}

now you can run npm run build or npm run watch and webpack will generate a javascript file in public/webpack/bundle.js.

keep this out of our git repo:

echo "/public/webpack" >> .gitignore

at this point we could reference the generated bundle.js from the asset pipeline and you’d be done. you would probably need to send it somewhere the asset pipeline can find it, e.g. vendor/assets/javascripts instead of public/webpack.

however i want to be able to cache files and not rely on the asset pipeline, which will mean we need to include a hash in the filename with webpack. with the generated hash in the filename, we will not know how to reference this file from the rails views. so we’ll generate a small view with the <script> tags with webpack, and then we can render that file with rails.

i’ll use the html webpack plugin to generate the html view:

npm install html-webpack-plugin

by default it generates an entire html document, but i just want the script and link tags. the easiest way i’ve found to do this is to use a blank file as a template:

touch client/blank.html

and change webpack.config.js to include a hash in the filename and to use the html webpack plugin.

var HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
  entry: "./client/src/app.js",
  output: { 
    path: __dirname + "/../public/webpack",
    publicPath: "/webpack/",
    filename: "bundle.[hash].js"
  },
  plugins: [
    new HtmlWebpackPlugin({ 
      template: "./client/blank.html", 
      filename: "views/_webpack_head.html" 
    })
  ]
}

now running npm run build will create some different files:

public/webpack/bundle.0a991e449145ef40afaa.js
public/webpack/views/_webpack_head.html

_webpack_head.html will look like this:

<script type="text/javascript" src="/webpack/bundle.0a991e449145ef40afaa.js"></script>

we want to be able to render this like a normal rails view, so we need to put it on the view path. update config/application.rb to add this to the views path:

module Webpackin
  class Application < Rails::Application
    config.paths["app/views"] << Rails.root.join("public", "webpack", "views")
    # …

and then restart your rails dev server, make sure npm run watch is running. change app/views/layouts/application.html.erb to render your generated partial:

<%= render "/webpack_head" %>

now when you reload the page, webpack_head should be rendered and you should see the alert from main.js 🙌. the <script> tag should reference a hashed filename. webpack is set up, great! you can edit main.js, refresh, and see any changes.

now lets put this thing on heroku.
(i forgot to use postgres when i did this in the example code, so i removed sqlite and active record)

heroku create webpackin
git push heroku

if you heroku open your app at this point, rails will choke with this error:

ActionView::Template::Error (Missing partial /_webpack_head …

this is because we haven’t added the webpack build step on heroku to produce that file. crack open package.json and add this script that heroku will automatically run:

{
  
  "scripts": {
    
    "postinstall": "npm run build"
  }
}

we also need to tell it to use the node buildpack, and for it to run before the ruby one which was automatically detected on the first deploy:

heroku buildpacks:add --index 1 heroku/nodejs

commit this, and send it off to heroku. heroku open and it should work. congratulations! you have a rails app that works with webpack. other than the fact that you have to keep npm run watch running in the background in addition to your rails dev server i think this should generally work.

onto css.

this is more in the scope of how-to-use webpack, but i was interested in getting stylesheets working, and having it referenced from a traditional <link> instead of having it inside the javascript bundle. to do this you’ll need to install some webpack plugins:

npm i style-loader css-loader extract-text-webpack-plugin

and add some stuff to client/webpack.config.js:

var ExtractTextPlugin = require("extract-text-webpack-plugin")
…
module.exports = {
  …
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract("style-loader", "css-loader")
      }
      // use this instead to have js generate a <style> tag:
      // { test: /\.css$/, loader: "style-loader!css-loader" }
    ]
  },
  plugins: [
    new ExtractTextPlugin("styles.[hash].css"),
    …
  ]
}

now we can create a stylesheet in client/src/styles.css:

body { background: red }

and because webpack works like a dependency graph, we need to require it from our client/src/app.js

require "./styles.css"
…

now when you reload the page, you should see a red background and see a <link> tag referencing a generated stylesheet. you can use @import statements, or involve css preprocessors as you wish.

that’s it! i hope this works as well as i expect it to.

other stuff:



home