Dance, Computer, Dance

by Ray Grasso

Running Webpack and Rails with Guard

3 November, 2015

A while ago I decided to graft React and Flux onto an existing Rails app using Webpack. I opted to avoid hacking on Sprockets and instead used Guard to smooth out the development process.

This is me finally writing about that process.

Node

I installed all the necessary node modules from the root of the Rails app.

Dependencies and scripts from package.json:

{
  "scripts": {
    "test": "PHANTOMJS_BIN=./node_modules/.bin/phantomjs ./node_modules/karma/bin/karma start karma.config.js",
    "test-debug": "./node_modules/karma/bin/karma start karma.debug.config.js",
    "build": "./node_modules/webpack/bin/webpack.js --config webpack.prod.config.js -c"
  },
  "dependencies": {
    "classnames": "^1.2.0",
    "eventemitter3": "^0.1.6",
    "flux": "^2.0.1",
    "keymirror": "^0.1.1",
    "lodash": "^3.5.0",
    "moment": "^2.9.0",
    "react": "^0.13.0",
    "react-bootstrap": "^0.19.1",
    "react-router": "^0.13.2",
    "react-router-bootstrap": "^0.12.1",
    "react-tools": "^0.13.1",
    "webpack": "^1.7.3",
    "whatwg-fetch": "^0.7.0"
  },
  "devDependencies": {
    "jasmine-core": "^2.2.0",
    "jsx-loader": "^0.12.2",
    "karma": "^0.12.31",
    "karma-jasmine": "^0.3.5",
    "karma-jasmine-matchers": "^2.0.0-beta1",
    "karma-mocha": "^0.1.10",
    "karma-mocha-reporter": "^1.0.2",
    "karma-phantomjs-launcher": "^0.1.4",
    "karma-webpack": "^1.5.0",
    "mocha": "^2.2.1",
    "node-inspector": "^0.9.2",
    "phantomjs": "^1.9.16",
    "react-hot-loader": "^1.2.3",
    "webpack-dev-server": "^1.7.0"
  }
}

Development Server

I wanted a single command to run my development server as per normal Rails development.

Firstly, I set up the Webpack config to read from, and build to app/assets/javascripts.

From webpack.config.js:

var webpack = require('webpack');

module.exports = {
  // Set the directory where webpack looks when you use 'require'
  context: __dirname + '/app/assets/javascripts',

  // Just one entry for this app
  entry: {
    main: [
      'webpack-dev-server/client?http://localhost:8080/assets',
      'webpack/hot/only-dev-server',
      './main.js'
    ]
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],

  output: {
    filename: '[name].bundle.js',
    // Save the bundle in the same directory as our other JS
    path: __dirname + '/app/assets/javascripts',
    // Required for webpack-dev-server
    publicPath: 'http://localhost:8080/assets'
  },

  // The only version of source maps that seemed to consistently work
  devtool: 'inline-source-map',

  // Make sure we can resolve requires to jsx files
  resolve: {
    extensions: ["", ".web.js", ".js", ".jsx"]
  },

  // Would make more sense to use Babel now
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: [ /node_modules/, /__tests__/ ],
        loaders: [ 'react-hot', 'jsx-loader?harmony' ]
      }
    ]
  }
}

Then the Rails app includes the built Webpack bundle.

From app/assets/javascripts/application.js:

//= require main.bundle

To get access to the Webpack Dev Server and React hot loader during development I added some asset URL rewrite hackery in development mode.

From config/environments/development.rb:

  # In development send *.bundle.js to the webpack-dev-server running on 8080
  config.action_controller.asset_host = Proc.new { |source|
    if source =~ /\.bundle\.js$/i
      "http://localhost:8080"
    end
  }

Then I kick it all off via Guard using guard-rails and guard-process.

Selections from Guardfile:

# Run the Rails server
guard :rails do
  watch('Gemfile.lock')
  watch(%r{^(config|lib)/.*})
end

# Run the Webpack dev server
guard :process, :name => "Webpack Dev Server", :command => "webpack-dev-server --config webpack.config.js --inline"

All Javascript and JSX files live in app/assets/javascripts and app/assets/javascripts/main.js is the application’s entry point.

To develop locally I run guard, hit http://localhost:3000 like normal, and have React hot swapping goodness when editing Javascript files.

Tests

I originally tried integrating Jest for Javascript tests but found it difficult to debug failing tests whilst using it. So, I switched to Karma and Jasmine and had Guard run the tests continually.

From Guardfile:

# Run Karma
guard :process, :name => "Javascript tests", :command => "npm test", dont_stop: true do
  watch(%r{Spec.js$})
  watch(%r{.jsx?$})
end

Like Jest, I keep tests next to application code in __tests__ directories. Karma will pick them all up based upon file suffixes.

A test-debug npm script1 runs the tests in a browser for easy debugging.

karma.config.js:

module.exports = function(config) {
  config.set({

    /**
     * These are the files required to run the tests.
     *
     * The `Function.prototype.bind` polyfill is required by PhantomJS
     * because it uses an older version of JavaScript.
     */
    files: [
      './app/assets/javascripts/test/polyfill.js',
      './app/assets/javascripts/**/__tests__/*Spec.js'
    ],

    /**
     * The actual tests are preprocessed by the karma-webpack plugin, so that
     * their source can be properly transpiled.
     */
    preprocessors: {
      './app/assets/javascripts/**/__tests__/*Spec.js': ['webpack'],
    },

    /* We want to run the tests using the PhantomJS headless browser. */
    browsers: ['PhantomJS'],

    frameworks: ['jasmine', 'jasmine-matchers'],

    reporters: ['mocha'],

    /**
     * The configuration for the karma-webpack plugin.
     *
     * This is very similar to the main webpack.local.config.js.
     */
    webpack: {
      context: __dirname + '/app/assets/javascripts',
      module: {
        loaders: [
          { test: /\.jsx?$/, exclude: /node_modules/, loader: "jsx-loader?harmony"}
        ]
      },
      resolve: {
        extensions: ['', '.js', '.jsx']
      }
    },

    /**
     * Configuration option to turn off verbose logging of webpack compilation.
     */
    webpackMiddleware: {
      noInfo: true
    },

    /**
     * Once the mocha test suite returns, we want to exit from the test runner as well.
     */
    singleRun: true,

    plugins: [
      'karma-webpack',
      'karma-jasmine',
      'karma-jasmine-matchers',
      'karma-phantomjs-launcher',
      'karma-mocha-reporter'
    ],
  });
}

Deployment

When deploying I use Capistrano to build the Javascript with Webpack before allowing Rails to precompile the assets as per normal.

From package.json:

{
  "scripts": {
    "build": "./node_modules/webpack/bin/webpack.js --config webpack.prod.config.js -c"
  }
}

The Webpack config for prod just has the development server and hot loader config stripped out.

webpack.prod.config.js:

var webpack = require('webpack');

module.exports = {
  // 'context' sets the directory where webpack looks for module files you list in
  // your 'require' statements
  context: __dirname + '/app/assets/javascripts',

  // 'entry' specifies the entry point, where webpack starts reading all
  // dependencies listed and bundling them into the output file.
  // The entrypoint can be anywhere and named anything - here we are storing it in
  // the 'javascripts' directory to follow Rails conventions.
  entry: {
    main: [
      './main.js'
    ]
  },

  // 'output' specifies the filepath for saving the bundled output generated by
  // wepback.
  // It is an object with options, and you can interpolate the name of the entry
  // file using '[name]' in the filename.
  // You will want to add the bundled filename to your '.gitignore'.
  output: {
    filename: '[name].bundle.js',
    // We want to save the bundle in the same directory as the other JS.
    path: __dirname + '/app/assets/javascripts',
  },

  // Make sure we can resolve requires to jsx files
  resolve: {
    extensions: ["", ".web.js", ".js", ".jsx"]
  },

  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: [ /node_modules/, /__tests__/ ],
        loaders: [ 'jsx-loader?harmony' ]
      }
    ]
  }
};

The Capistrano tasks in config/deploy.rb:

namespace :deploy do
  namespace :assets do
    desc "Build Javascript via webpack"
    task :webpack do
      on roles(:app) do
        execute("cd #{release_path} && npm install && npm run build")
      end
    end
  end
end

before "deploy:assets:precompile", "deploy:assets:webpack"

Finally

I’m not sure if there is a simpler way to incorporate Webpack into Rails nowadays but this approach worked pretty well for me.

  1. As shown in the package.json listing above.