Updated instructions for Phoenix 1.1.4 and custom Webpack dev server

Generate your new app without brunch:

$ mix phoenix.new my_app --no-brunch
$ cd my_app

This is file that we want to be able to compile, web/static/js/index.js:

// Phoenix' dependencies
import '../../../deps/phoenix/priv/static/phoenix'
import '../../../deps/phoenix_html/priv/static/phoenix_html'

// Shiny new, hot React component
import React, { Component } from 'react'
import { render } from 'react-dom'

class Root extends Component {
  render () {
    return <h1>omg so hot</h1>
  }
}

render(<Root />, document.getElementById('root'))

Install the required js dependencies using npm:

$ echo '{"private": true}' > package.json
$ npm install --save babel-core babel-polyfill babel-loader babel-preset-es2015 babel-preset-react react react-dom webpack
$ npm install --save-dev webpack-dev-middleware webpack-hot-middleware express cors babel-preset-react-hmre babel-preset-stage-0 babel-preset-es2015

A lot of stuff, right?

We need a webpack config. Here’s webpack.config.js:

var path = require('path')
var webpack = require('webpack')
var publicPath = 'http://localhost:4001/'

var env = process.env.MIX_ENV || 'dev'
var prod = env === 'prod'

var entry = './web/static/js/index.js'
var hot = 'webpack-hot-middleware/client?path=' +
  publicPath + '__webpack_hmr'

var plugins = [
  new webpack.optimize.OccurrenceOrderPlugin(),
  new webpack.NoErrorsPlugin(),
  new webpack.DefinePlugin({
    __PROD: prod,
    __DEV: env === 'dev'
  })
]

if (env === 'dev') {
  plugins.push(new webpack.HotModuleReplacementPlugin())
}

module.exports = {
  devtool: prod ? null : 'cheap-module-eval-source-map',
  entry: prod ? entry : [hot, entry],
  output: {
    path: path.resolve(__dirname) + '/priv/static/js',
    filename: 'index.bundle.js',
    publicPath: publicPath
  },
  plugins: plugins,
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loaders: ['babel'],
        exclude: path.resolve(__dirname, 'node_modules')
      }
    ]
  }
}

And we need a node server for development, webpack.dev.js:

#!/usr/bin/env node
var express = require('express')
var webpack = require('webpack')
var config = require('./webpack.config')

var compiler = webpack(config)
var app = express()
app.use(require('cors')())

app.use(require('webpack-dev-middleware')(compiler, {
  noInfo: true,
  publicPath: config.output.publicPath
}))

app.use(require('webpack-hot-middleware')(compiler, {
  log: console.log
}))

app.listen(4001, 'localhost', function (err) {
  if (err) return console.error(err)
  console.log('dev server running on localhost:4001')
})

// Exit on end of STDIN
process.stdin.resume()
process.stdin.on('end', function () {
  process.exit(0)
})

Allow it to be executed:

$ chmod +x webpack.dev.js

Babel 6 needs a .babelrc so let’s add it:

{
  "presets": ["es2015", "react", "stage-0"],
  "env": {
    "development": {
      "presets": ["react-hmre"]
    }
  }
}

Make your app run that script as a watcher in dev. config/dev.exs:

config :speaker, MyApp.Endpoint,
  http: [port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [{Path.expand("webpack.dev.js"), []}]

# ...

config :speaker, MyApp.Endpoint,
  live_reload: [
    patterns: [
      # ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{web/views/.*(ex)$},
      ~r{web/templates/.*(eex)$}
    ]
  ]

And finally add this to the bottom of web/templates/layout/app.html.eex:

<%= if Mix.env == :dev do %>
  <script src='http://localhost:4001/index.bundle.js'></script>
<% else %>
  <script src="<%= static_path(@conn, "/js/index.bundle.js") %>"></script>
<% end %>

And web/templates/page/index.html.eex is just:

<div id='root'></div>

Building for production

Add lib/mix/tasks/digest.ex:

defmodule Mix.Tasks.MyApp.Digest do
  use Mix.Task

  def run(args) do
    Mix.Shell.IO.cmd "NODE_ENV=production ./node_modules/webpack/bin/webpack.js -p"
    :ok = Mix.Tasks.Phoenix.Digest.run(args)
  end
end

And in mix.exs:

defmodule MyApp.Mixfile do
  # ...
  def project do
    [ # ...
      aliases: ["phoenix.digest": "my_app.digest"]]
  end
  # ...
end