PostsAMA
Everything Posts Notes
mikker·over 8 years ago

Getting a project's Ruby version from the Gemfile

It used to be that every project had its own .ruby_version file. But you’d also have to specify the same version in the Gemfile. I say nay to .ruby_version and instead get it from one place:

alias ruby-vers="cat Gemfile | grep '^ruby' | sed -E \"s/.*[\\\"'](.+)[\\\"']/\1/"\"

Now you can $ chruby `ruby-vers` your way to success!

mikker·over 8 years ago

Using Preact in production with Rails' Webpacker

Preact is like React but smaller in size. I think? Anyway, so I’ve been told. It’s also different in some ways and I’m not entirely sure how, but they have this thing called preact-compat that’ll plug right into your Webpack config and use Preact instead of React which seems like free filesize savings. Right on!

First:

$ yarn add preact-compat webpack-merge

In config/webpack/production.js:

const environment = require("./environment");
const merge = require("webpack-merge");

module.exports = merge(environment.toWebpackConfig(), {
  resolve: {
    alias: {
      react: "preact-compat",
      "react-dom": "preact-compat",
    },
  },
});

I saved 2 whole megabytes with just this change. No, of course that’s not true.

mikker·about 9 years ago

Disable long-press for Hyper on macOS

Hyper is great and even though I still cling to iTerm I sometimes try and shake things up by using Hyper for a few days.

macOS has a feature where if you press a character like u and hold it, a popup menu appears that’ll let you pick out a ü instead. Great if you’re writing German prose. Not great if you are undoing a whole day’s work in vim.

So let’s disable that feature only for Hyper. Put this in your term and smoke it:

defaults write co.zeit.hyper ApplePressAndHoldEnabled -bool false

If you want to disable it system wide then use the same command but without the co.zeit.hyper part.

mikker·about 9 years ago

Conditionally load local plugs in vim

If you’re using vim (you should) and vim-plug for plugins (you should) and you have some plugins locally (you might) but still want your setup to be portable (you should) then you might want to only load plugins locally if they exist (here’s how).

function! s:maybeLocalPlug(args)
  let l:localPath = $HOME . "/dev/" . expand(a:args)

  if isdirectory(l:localPath)
    Plug l:localPath
  else
    Plug 'mikker/' . expand(a:args)
  endif
endfunction

call s:maybeLocalPlug('lightline-theme-pencil')
call s:maybeLocalPlug('vim-rerunner')
call s:maybeLocalPlug('vim-dimcil')
call s:maybeLocalPlug('vim-colors-paramount')

This checks to see if the plugin exists locally at ~/dev/plugin-name and if it doesn’t it loads from good old Github.

mikker·about 9 years ago

Sprinkling React

A beautiful thing about jQuery was how you could sprinkle it on top of an already working site. With all of today’s SPAs and what nots this seems like a distant past. We’ve all drunk the Kool Aid and the benefits of React (and the like) are just too huge to ever look back.

Still, there’s no reason we can’t still sprinkle instead SPA-ing. That was a horrible sentence. Let’s move on.

Here’s what I do on 10er.dk.

The api is basically as follows. You have a div with some data- props that we’ll catch later and switch in the magic.

<article>
  <div data-react="NumberWang" data-props='{"numbers":[1,2,3,5,68]}'></div>
</article>

Ok, so that means look up the component called NumberWang, render it with these props and throw it back in here.

Here’s sprinkleComponents.js. It uses Webpack and its require.context:

import React from "react";
import { render } from "react-dom";

let context = require.context("./components", true, /\.(\/index)?js$/);

export default function sprinkleComponents() {
  const nodes = document.querySelectorAll("[data-react]");

  for (const node of nodes) {
    const regexp = new RegExp(node.dataset.react + "(/index)?\\.js");
    const match = context.keys().find((path) => path.match(regexp));
    const Comp = context(match).default;
    const props = node.dataset.reactProps
      ? JSON.parse(node.dataset.reactProps)
      : {};
    props.children = node.innerHTML;

    render(<Comp {...props} />, node);
  }
}

// We can even get hot module reload (so dx am I right?)
if (module.hot) {
  module.hot.accept(context.id, (upd) => {
    context = require.context("./components", true, /\.(\/index)?js$/);
    sprinkleComponents();
  });
}

This expects to find the component NumberWang at either ./components/NumberWang.js or ./components/NumberWang/index.js relative to sprinkleComponents.js.

Let’s make it – ./components/NumberWang.js:

import React from "react";

export default ({ numbers }) => (
  <div>{numbers[Math.floor(Math.random() * numbers.length)]}</div>
);

And finally in your main.js or index.js or client.js or whatever:

document.addEventListener("DOMContentLoaded", () => {
  sprinkleComponents();
});

This is the technique that I use to selectively apply some js sauce on 10er.dk, a mostly server rendered Rails app.

mikker·over 9 years ago

Serving a Keybase.io claim with Next

I recently moved my personal website to next.js because why not. The people at ▲ Zeit are on a roll and I want in on it.

Keybase quickly started complaining about me removing my claim. You’re supposed to host a file at /keybase.txt with specific content to claim a domain is truly yours. So how do we do that with Next?

Here’s pages/keybase.txt.js:

import React, { Component } from "react";

export default class KeybaseTxt extends Component {
  static getInitialProps({ res }) {
    res.end(claim);
  }

  render() {
    return <div>We'll never get this far</div>;
  }
}

const claim = `=====...long long claim thing...`;

getInitialProps is Next’s way of fetching data before render so it works both on the server and the client. During server render it’s passed req and res from node and apparently we can just end it right there. So dumb it’s smart.

mikker·over 9 years ago

Control-h in NeoVim on macOS

Apparently <c-h> sends <BS> to xterm256-screen terminals and your binding doesn’t work in Neovim.

There’s a few fixes, but I like this one that does it in iTerm:

  1. Edit -> Preferences -> Keys
  2. Press +
  3. Press Ctrl+h as Keyboard Shortcut
  4. Choose Send Escape Sequence as Action
  5. Type [104;5u for Esc+
mikker·over 9 years ago

Read marks with Ecto and Phoenix

As always, Rails has an easy, pluggable way to do this with unread but it isn’t too hard to add the same to your Phoenix app. Let’s try!

First we’ll generate the table and schema that holds the read marks. This assumes you have two tables: users and stories.

mix phoenix.gen.model ReadMark read_marks \
  user_id:references:users \
  story_id:references:stories

Open up the newly generated web/models/read_mark.ex and set up the associations:

defmodule MyApp.ReadMark do
  use MyApp.Web, :model

  schema "read_marks" do
    belongs_to :user, MyApp.User
    belongs_to :story, MyApp.Story

    timestamps()
  end
end

And in the existing models:

defmodule MyApp.User do
  schema "users" do
    has_many :read_marks, MyApp.ReadMark, on_delete: :delete_all
  end
end

defmodule MyApp.Story do
  schema "stories" do
    has_many :read_marks, MyApp.ReadMark, on_delete: :delete_all
  end
end

This is all the setup we need.

We can now get unread stories for a user with a query like:

defmodule MyApp.Story do
  #...

  def unread_by(query, user) do
    from s in query,
      left_join: rm in assoc(s, :read_marks),
      on: rm.user_id == ^user.id,
      where: is_nil(rm.id)
  end
  def unread_by(user), do: unread_by(DRBot.Story, user)
end

And use it like:

user = Repo.get(MyApp.User, 1)
unread_stories = MyApp.Story.unread_by(user) |> Repo.all

We can even chain it with other queries:

query = from s in MyApp.Story, where: s.published_on == ^today

unread_stories_published_today =
  MyApp.Story.unread_by(query, user)
  |> Repo.all

# or the other way around

query = MyApp.Story.unread_by(user)

unread_stories_published_today =
  (from s in query, where: s.published_on == ^today)
  |> Repo.all
mikker·over 9 years ago

Podcast Mixtapes

Here’s an idea for a web thing that you should make and afterwards figure out how to make money from. Bonus points if it doesn’t include VC or ads. (Minus points if both.)

One day, sitting at work, you laugh out loud because Greg the host of your favourite podcast “Tim & Greg’s Lazy Hour” says he’s been biking during the weekend and, well, if one knows Greg like you do, that would be what Greg would have been up to, so you simply cannot hold it back. You crack open with a half spitting / half laughing noise.

Seconds later coming back to reality you look up and notice your coworker looking at you, tainted by your spit, hinting at you to take off your headphones.

“What are you listening to?” she asks. Oh, where to begin.

Podcasts come in feeds. This is one of the best things about them. Over time you build a relationship with each one. You are there when all the internal jokes come to life and by now you feel like you and the hosts should just rent a cabin some time and be best pals because it feels like you already are.

But you can’t instruct your coworker to listen to the prior 136 episodes and then she’ll know. You have to point her to something that will ease her into it.

podcastmixtapes.sexy

Luckily there’s podcastmixtapes.sexy (not a real thing. Yet.)

  1. Coworker wants in on the funnies.
  2. You create a mixtape, pick the 4–5 episodes that’ll give her the best intro, give it the title “Up to speed on Tim & Greg”.
  3. After the first episode there are a few things that you should probably explain so you type in a few curator’s notes. These get added to the feed in between the episodes.
  4. You share the address of the mixtape with coworker.
  5. Coworker subscribes to the mixtape with her podcast player (like a decent person) or listens to it directly in her browser (like an animal.)

Your coworker is now better suited to either subscribe to the real deal - or judge you and your weird ass humour.

Other mixtape ideas:

  • Episodes that’ve made me cry in public
  • Intro to Roderick on the Line and the #supertrain movement
  • Best of This American Life pre episode 500
  • You Look Nice Today episodes mentioning Adam’s drumming
  • For Lisa ❤️🎧 (ongoing, made by Lisa’s boyfriend)
  • Siracusa Setting Things Straight (3+ new episodes each week)

Now go seize this fortune that I’ve left in your hands.

mikker·almost 10 years ago

Using legacy Devise records in Phoenix

If you, like me, are having fun with rebuilding a Rails app in Phoenix then you might also have to deal with User records made with devise. Here’s how to use them with no update to the data required.

First we have our user.ex schema file:

defmodule MyApp.User do
  use MyApp.Web, :model

  schema "users" do
    field :email, :string
    field :encrypted_password, :string
    field :password, :string, virtual: true
    field :password_confirmation, :string, virtual: true

    timestamps inserted_at: :created_at
  end

  @allowed [:email, :password, :password_confirmation]
  @required [:email, :password, :password_confirmation]

  def changeset(model attrs \\ %{}) do
    model
    |> cast(attrs, @allowed)
    |> validate_required(@required)
    |> MyApp.Crypto.encrypt_password
  end
end

Notice the two virtual fields and the last function in the changeset pipeline. This will encrypt whatever’s in password and save it to encrypted_password. Let’s see how it looks in web/crypto.ex.

defmodule MyApp.Crypto
  import Ecto.Changeset, only: [put_change: 3]

  def encrypt_password(changeset) do
    password = changeset.data.password || changeset.chages.password
    put_change(changeset, :encrypted_password, encrypt(password))
  end

  defp encrypt(password) do
    pepper = Application.get_env(:my_app, :pepper)
    Comeonin.Bcrypt.haspwsalt(password <> pepper)
  end
end

A few things: Devise uses both a salt and a pepper. We use Comeonin for actually bcrypting the string.

Now to authenticate users when they log in we make a Session module in web/session.ex.

defmoudle MyApp.Session do
  alias MyApp.Repo

  def authenticate(schema, %{email: email, password: password}) do
    case get_resource(schema, email) do
      {:ok, resource} -> check_password(resource, password)
      {:error, _} -> {:error, nil}
    end
  end

  defp check_password(resource, password) do
    case Comeonin.Bcrypt.checkpw(password <> pepper, resource.encrypted_password)
      true -> {:ok, resource}
      _ -> {:error, nil}
    end
  end

  defp pepper do
    Applicaion.get_env(:my_app, :pepper)
  end

  defp get_resource(schema, email) do
    case Repo.get_by(schema, email: email) do
      nil -> {:error, nil}
      resource -> {:ok, resource}
    end
  end
end

Use them like MyApp.Repo.insert(MyApp.User.changeset(%{ ... })) and MyApp.Session.authenticate(User, %{email: ..., password: ...}).

This is actually all it takes. Now you can both create new users using the same techiques as devise and authenticate everybody, both new and old.

mikker·almost 10 years ago

Sign in as a user in a Phoenix controller test

First I spent a few hours piecing this together. And then I spent a few months searching through projects several times to find where I had used it because I needed it again. So here it goes now, into my public scratch pad.

To put something into a conn session in a Phoenix test you first have to go through you app’s router and then back. So for example if you want to sign in as a user before hitting an endpoint use this:

def sign_in conn, user do
  conn
  |> bypass_through(MyApp.Router, :browser)
  |> get("/")
  |> put_session(:current_user, user.id)
  |> send_resp(:ok, "")
  |> recycle()
end

To have it available in all your ConnCases put it in test/support/conn_case.ex.

defmodule MyApp.ConnCase do
  # ...
  using do
    quote do
      # ...
      def sign_in conn, user do
        # ...
      end
    end
  end
end
mikker·almost 10 years ago

-webkit-user-select: none; disables input and Javascript events

I could’ve spared two hours of googling if I had just known this.

I had this in my css file to disable text selection in my mobile-targeted web app:

* {
  -webkit-user-select: none;
}

This is nice and makes what is actually a website behave more like apps do. What I didn’t know was that this also disables text input on iOS.

Fast forward two hours: Either remove the * rule above or re-enable user-select for input fields:

input {
  -webkit-user-select: auto;
}
mikker·almost 10 years ago

Cycle an array using ES2015

ES2015 is fun and allows you to write more concise Javascript.

Here’s how to cycle an array of values. Use it to zebra stripe a table or whatever:

function cycle(...args) {
  return (i) => args[i % args.length];
}

And we can still make it shorter:

const cycle =
  (...args) =>
  (i) =>
    args[i % args.length];

Use it in React like this:

function Table({ posts }) {
  const cls = cycle("odd", "even");

  return (
    <table>
      {posts.map((post, i) => (
        <tr key={i} className={cls(i)}>
          <td>{post.title}</td>
        </tr>
      ))}
    </table>
  );
}

Compare it to regular ES5:

function cycle() {
  var args = Array.prototype.slice.call(arguments);

  return function (i) {
    return args[i % args.length];
  };
}

Not that much longer but definitely less fun. We can have fun.

mikker·about 10 years ago

Updated Elixir Phoenix 1.1.4 with Webpack and React Hot Module Reload

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
mikker·about 10 years ago

Bookmarklet to control Netflix' playback rate

Who has time to watch ANYTHING at regular speed? Not this guy.

Netflix’ player is just a HTML5 <video> element and those have a playbackRate property.

Thank God for Javascript, right: Use this bookmarklet to control Netflix’ playback rate and never look back.

NetflixSpeed ← Drag this to your bookmarks bar

mikker·over 10 years ago

Upgrading PostgreSQL data from 9.4 to 9.5 with homebrew

I got this in my server.log:

The data directory was initialized by PostgreSQL version 9.4, which is not compatible with this version 9.5.0.

So we have to upgrade our data. Luckily there’s pb_upgrade.

First we need to have a version of 9.4 installed.

# move 9.5 out of the way
# - don't worry - your data will not be removed
$ brew uninstall postgres

# the last version of the postgres bottle with 9.4
$ brew install https://github.com/Homebrew/homebrew/raw/f8509e62904a055f085579aed47fca1faa7a810f/Library/Formula/postgresql.rb

# move 9.4 away but keep it
$ brew unlink postgres

# install 9.5
$ brew install postgres

Now we can follow Keita’s instructions:

# initialize a new 9.5 db
$ initdb /usr/local/var/postgres9.5 -E utf8

# upgrade our data
$ pg_upgrade -d /usr/local/var/postgres -D /usr/local/var/postgres9.5 -b /usr/local/Cellar/postgresql/9.4.5_2/bin/ -B /usr/local/Cellar/postgresql/9.5.0/bin/ -v

# move 9.4 data away and the new data into place
$ mv /usr/local/var/postgres /usr/local/var/postgres9.4
$ mv /usr/local/var/postgres9.5 /usr/local/var/postgres

# remove 9.4
$ brew cleanup

And for good measure:

$ gem uninstall pg
$ gem install pg
mikker·over 10 years ago

Recreating DuckDuckGo's "I'm Feeling Lucky" bang method on Google

DuckDuckGo has a slew of bang methods that let you do all sorts of things with your search terms. One of them is just the bang (!) that’ll automatically go to the topmost result. Like Google’s “I’m Feeling Lucky”.

How can we do that on Google aswell? Using dotjs! Long story short, dotjs let’s you write javascript files that run after sites load eg. ~/.js/google.com.js in this instance.

(function (window, document) {
  "use strict";

  console.log("dotjs");

  // Parse the search string to an object
  var params = window.location.search
    .replace(/^\?/, "")
    .split("&")
    .reduce(function (params, kv) {
      kv = kv.split("=");
      params[kv[0]] = kv[1];
      return params;
    }, {});

  // See if the ?q param contains a bang
  if (params.q.match(/\!/)) {
    // Click the topmost result
    document.querySelector("h3.r a").click();
  }
})(window, document);
mikker·over 10 years ago

El Capitan GM update notes

UPDATE 2015-10-11: Turns out, on a clean install /usr/local is still writable – even with SIP turned on.


I’ve been running the beta of OS X 10.11 El Capitan for some months and there’ve been very few hiccups. So full sail ahead on the update I say.

Upgrading to the GM broke my homebrew though. I think the days of using /usr/local might be over as El Capitan does some stuff to enforce even stricter permissions than a simple chown can get rid of.

So I’m moving out! Someone (forgot who or where – sorry!) mentioned in a Github issue thread how he’d been running his homebrew out of /Users/Shared/Developer for some months with no problems, so that’s what I did:

Installing Homebrew outside of /usr/local

$ git clone https://github.com/Homebrew/homebrew.git /Users/Shared/Developer

Now, that directory’s bin directory isn’t in your $PATH (like /usr/local/bin is automatically) so we need to add it. Open up ~/.bashrc or ~/.zshrc – whatever your preference – and add this:

export BREW_PATH=/Users/Shared/Developer
export PATH="$BREW_PATH/bin:$PATH"

Done. Run brew doctor to confirm.

Install taglib-ruby gem with Homebrew outside /usr/local

$ brew install taglib
$ gem install taglib-ruby -- --with-tag-dir=$BREW_PATH/Cellar/taglib/1.9.1/
mikker·over 10 years ago

Use webpack-dev-server and react-hot-loader with Phoenix

NB: Here’s an updated setup.

Phoenix has built-in livereload and it works right out of the box. But if you’ve ever had the joy of working with React and react-hot-loader you know you need to have that with you anywhere.

Phoenix uses Brunch to build it’s assets so we’ll have to pull that out and jam in a webpack-dev-server wherever it was. Luckily that’s quite easy.

The completed example is available on Github.

First, let’s create a new app:

$ mix phoenix.new my_app
$ cd my_app

We could’ve generated the app without Brunch but let’s keep it in, to see what we’re actually substituting.
Let’s start by pulling out Brunch and it’s dependencies and then add our new ones:

$ npm uninstall --save babel-brunch brunch clean-css-brunch css-brunch javascript-brunch sass-brunch uglify-js-brunch
$ npm install --save babel-loader react react-hot-loader webpack webpack-dev-server

Now, webpack needs to be told what to build and how, so let’s make a config file. Here’s webpack.config.js:

var path = require("path");
var webpack = require("webpack");

var env = process.env.MIX_ENV || "dev";
var prod = env === "prod";

var entry = "./web/static/js/bundle.js";
var plugins = [new webpack.NoErrorsPlugin()];
var loaders = ["babel"];
var publicPath = "http://localhost:4001/";

if (prod) {
  plugins.push(new webpack.optimize.UglifyJsPlugin());
} else {
  plugins.push(new webpack.HotModuleReplacementPlugin());
  loaders.unshift("react-hot");
}

module.exports = {
  devtool: prod ? null : "eval-sourcemaps",
  entry: prod
    ? entry
    : [
        "webpack-dev-server/client?" + publicPath,
        "webpack/hot/only-dev-server",
        entry,
      ],
  output: {
    path: path.join(__dirname, "./priv/static/js"),
    filename: "bundle.js",
    publicPath: publicPath,
  },
  plugins: plugins,
  module: {
    loaders: [{ test: /\.jsx?/, loaders: loaders, exclude: /node_modules/ }],
  },
};

I will not go into too much detail but notice the address http://localhost:4001. That’s where webpack-dev-server will be running from when we’re developing.

Next, let’s set up the dev server in webpack.devserver.js:

#!/usr/bin/env node
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var config = require("./webpack.config");

new WebpackDevServer(webpack(config), {
  contentBase: "http://localhost:4001",
  publicPath: config.output.publicPath,
  hot: true,
}).listen(4001, "0.0.0.0", function (err, result) {
  if (err) console.error(err);
  console.log("webpack-dev-server running on port 4001");
});

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

That last last bit is there to make the process shut down properly when Phoenix shuts down. (A big thank you to josevalim for guiding me in figuring that out, for being such a seemingly nice guy and of course for making Elixir!)

Don’t forget to make it executable:

$ chmod +x webpack.devserver.js

We need to tell Phoenix to run this instead of Brunch, so open up config/dev.exs and the change watchers: line to include our dev server instead:

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

And let’s just cut Phoenix some slack and tell it not to watch the assets:

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

OK, let’s make our entry file, web/static/js/bundle.js:

import React from "react";
import App from "./App";

React.render(<App />, document.getElementById("root"));

This renders our App (that we haven’t made yet) to an element with the id root (that we also haven’t made yet). Splitting your React components into separate files let’s react-hot-loader reload them independently from each other.

Here’s a simple web/static/js/App.js:

import React, { Component } from "react";

export default class App extends Component {
  render() {
    return (
      <div>
        <h1>This app is hot!</h1>
      </div>
    );
  }
}

So far, so good. Now we just need some html document to render it in.

Edit web/templates/layout/app.html.eex:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Hello Phoenix!</title>
  </head>
  <body>
    <%= @inner %>
    <%= if Mix.env == :dev do %>
      <script src='http://localhost:4001/bundle.js'></script>
    <% else %>
      <script src="<%= static_path(@conn, "/js/bundle.js") %>"></script>
    <% end %>
  </body>
</html>

Remember that our dev server is running on :4001. We want to use it’s bundle.js in dev and a built one in production.

The only thing left is the element with id root. Let’s put it in web/templates/pages/index.html.eex:

<div id="root"></div>

And we’re done! Now go open http://localhost:4000, edit App.js and behold the magic of our hot reloading gods!

Addendum: Production

Phoenix compiles all it’s assets with the task phoenix.digest which you’re supposed to run before deploying. We can just remember to run webpack beforehand — or we can make our own digest task.

Here’s lib/mix/tasks/digest.ex:

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

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

Let’s be fancy and override the original task so new developers or deployment scripts don’t need to know about our special setup. Open mix.exs and alias the original to our new task:

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

Try mix phoenix.digest and see that webpack runs first.

mikker·almost 11 years ago

Force-update text shortcuts in OS X Yosemite

My text shortcuts never synced when I recently set up my new Macbook and so I was left inserting all my emojis using ctrl+space and using my mouse like you would if you weren’t communicating primarily by thumbs-up emoji like me 👍🏼

So, to force a refresh:

  1. Turn off iCloud Drive
  2. rm -rf ~Library/Mobile Documents/com~apple~TextInput
  3. Turn iCloud Drive back on
Loading more posts...