Running Webpack and Rails with Guard
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.
-
As shown in the
package.json
listing above. ↩︎