Skip to content

Instantly share code, notes, and snippets.

@chrismccord
Last active Sep 25, 2021
Embed
What would you like to do?

Phoenix 1.5.x to 1.6 upgrade instructions

Update your deps

In mix.exs, update your phoenix, phoenix_html, telemetry_metrics, telemetry_poller and phoenix_live_dashboard deps, and add phoenix_live_view:

def deps do
    [
      {:phoenix, "~> 1.6.0"},
      ...
      {:phoenix_html, "~> 3.0"},
      {:phoenix_live_view, "~> 0.16.4"},
      {:phoenix_live_dashboard, "~> 0.5"},
      {:telemetry_metrics, "~> 0.6"},
      {:telemetry_poller, "~> 0.5"},
      ...
    ]
end

Next, run mix deps.get to grab your new deps.

Rename your .html.eex and .html.leex templates to .html.heex (optional)

While leex templates have been deprecated, this step is optional. For the most part, existing templates should continue to work, but the HTML-aware HEEx engine will enforce valid markup and is more strict in the elixir expressions that appear within an open and closing tag. For example, the following code will raise:

<div id="<%= @id %>">

Instead of the standard <%= %> EEx expressions, elixir expressions inside tags can only appear withing {}, such as:

<div id={@id}>

<%= %> expressions remain valid outside of HTML tags in the EEx engine.

To update your existing templates, rename all your .html.eex and .html.leex templats to .html.heex and follow the parser errors to find any tags that require the new {} attribute form.

Also be sure to review the HEEx documentation for more information on features.

Migrate to esbuild for js and css bundling (optional)

Phoenix's watchers configuration is build-tool agnostic, so you may continue to enjoy your existing webpack configurations generated by phoenix 1.5 or earlier. If only have basic js and css needs and you would like to take advantage of our new esbuild usage, for a dependency-free asset builder powered by a portably binary, follow these steps:

First delete your webpack config and related node files:

$ rm assets/webpack.config.js assets/package.json assets/package-lock.json assets/.babelrc
$ rm -rf assetes/node_modules

Next, add the esbuild mix dep to your mix.exs deps:

def deps do
  [
    ...
    {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
  ]
end

Next, configure esbuild in config/config.exs:

# Configure esbuild (the version is required)
config :esbuild,
  version: "0.12.18",
  default: [
    args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

Next, replace the node watcher with esbuild in your endpoint watcher config in config/dev.exs:

config :demo, DemoWeb.Endpoint,
  ...,
  watchers: [
    # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
    esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
  ]

Next, add a new assets.deploy mix alias in your mix.exs for easy asset building:

  defp aliases do
    [
      ...,
      "assets.deploy": ["esbuild default --minify", "phx.digest"]
    ]
  end

Running $ mix assets.deploy will download the esbuild binary on first run and then build your assets:

$ mix assets.deploy
21:40:37.588 [debug] Downloading esbuild from https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.12.18.tgz

  ../priv/static/assets/app.css  9.7kb
  ../priv/static/assets/app.js   1.3kb

⚡ Done in 12ms
Check your digested files at "priv/static"

Next, update your layouts, such as app.html.heex or root.html.heex to use the new assets prefix instead of js/app.js and css/app.css:

    <link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
    <script defer phx-track-static type="text/javascript" src={Routes.static_path(@conn, "/assets/app.js")}></script>

Finally, update your Plug.Static :only options in your lib/app_web/endpoint.ex to be aware of the new assets directory:

  plug Plug.Static,
    at: "/",
    from: :my_app,
    gzip: false,
    only: ~w(assets fonts images favicon.ico robots.txt)
@spunkedy

This comment has been minimized.

Copy link

@spunkedy spunkedy commented Aug 27, 2021

rm -rf assetes/node_modules

should be

rm -rf assets/node_modules
@stevehill1981

This comment has been minimized.

Copy link

@stevehill1981 stevehill1981 commented Aug 27, 2021

In a (demo) app upgraded from 1.5.10, I'm seeing an error around topbar. Fixed by removing those lines from my assets/js/app.js file for now.

@pdawczak

This comment has been minimized.

Copy link

@pdawczak pdawczak commented Aug 27, 2021

Looks like the link for "HEEx documentation" is invalid! 😬

@shahryarjb

This comment has been minimized.

Copy link

@shahryarjb shahryarjb commented Aug 27, 2021

In a (demo) app upgraded from 1.5.10, I'm seeing an error around topbar. Fixed by removing those lines from my assets/js/app.js file for now.

Create a fresh phoenix and copy vendor folder for topbar and change app.js like fresh installation

see my post:
https://elixirforum.com/t/problem-to-asset-deploy-with-esbuild/41945

@pjo336

This comment has been minimized.

Copy link

@pjo336 pjo336 commented Aug 28, 2021

Note that if you are sticking to the current npm/webpack setup from 1.5, after upgrading the mix.exs, you will have to run npm i --prefix assets to update the javascript libs as well

@stefanchrobot

This comment has been minimized.

Copy link

@stefanchrobot stefanchrobot commented Aug 28, 2021

It seems it would make sense to add the changes around the SQL sandbox into the guide.

@atomkirk

This comment has been minimized.

Copy link

@atomkirk atomkirk commented Aug 31, 2021

topbar Error

If you get the following error:

[watch] build started (change: "js")
> js/app.js:27:19: error: Could not resolve "../vendor/topbar"
27 │ import topbar from "../vendor/topbar"

Copy this (copied from a brand new phx 1.6 project) into assets/js/app.js

// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
import "../css/app.css"

// If you want to use Phoenix channels, run `mix help phx.gen.channel`
// to get started and then uncomment the line below.
// import "./user_socket.js"

// You can include dependencies in two ways.
//
// The simplest option is to put them in assets/vendor and
// import them using relative paths:
//
//     import "./vendor/some-package.js"
//
// Alternatively, you can `npm install some-package` and import
// them using a path starting with the package name:
//
//     import "some-package"
//

// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

Copy this into assets/vendor/topbar.js

/**
 * @license MIT
 * topbar 1.0.0, 2021-01-06
 * http://buunguyen.github.io/topbar
 * Copyright (c) 2021 Buu Nguyen
 */
(function (window, document) {
  "use strict";

  // https://gist.github.com/paulirish/1579671
  (function () {
    var lastTime = 0;
    var vendors = ["ms", "moz", "webkit", "o"];
    for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
      window.requestAnimationFrame =
        window[vendors[x] + "RequestAnimationFrame"];
      window.cancelAnimationFrame =
        window[vendors[x] + "CancelAnimationFrame"] ||
        window[vendors[x] + "CancelRequestAnimationFrame"];
    }
    if (!window.requestAnimationFrame)
      window.requestAnimationFrame = function (callback, element) {
        var currTime = new Date().getTime();
        var timeToCall = Math.max(0, 16 - (currTime - lastTime));
        var id = window.setTimeout(function () {
          callback(currTime + timeToCall);
        }, timeToCall);
        lastTime = currTime + timeToCall;
        return id;
      };
    if (!window.cancelAnimationFrame)
      window.cancelAnimationFrame = function (id) {
        clearTimeout(id);
      };
  })();

  var canvas,
    progressTimerId,
    fadeTimerId,
    currentProgress,
    showing,
    addEvent = function (elem, type, handler) {
      if (elem.addEventListener) elem.addEventListener(type, handler, false);
      else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
      else elem["on" + type] = handler;
    },
    options = {
      autoRun: true,
      barThickness: 3,
      barColors: {
        0: "rgba(26,  188, 156, .9)",
        ".25": "rgba(52,  152, 219, .9)",
        ".50": "rgba(241, 196, 15,  .9)",
        ".75": "rgba(230, 126, 34,  .9)",
        "1.0": "rgba(211, 84,  0,   .9)",
      },
      shadowBlur: 10,
      shadowColor: "rgba(0,   0,   0,   .6)",
      className: null,
    },
    repaint = function () {
      canvas.width = window.innerWidth;
      canvas.height = options.barThickness * 5; // need space for shadow

      var ctx = canvas.getContext("2d");
      ctx.shadowBlur = options.shadowBlur;
      ctx.shadowColor = options.shadowColor;

      var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
      for (var stop in options.barColors)
        lineGradient.addColorStop(stop, options.barColors[stop]);
      ctx.lineWidth = options.barThickness;
      ctx.beginPath();
      ctx.moveTo(0, options.barThickness / 2);
      ctx.lineTo(
        Math.ceil(currentProgress * canvas.width),
        options.barThickness / 2
      );
      ctx.strokeStyle = lineGradient;
      ctx.stroke();
    },
    createCanvas = function () {
      canvas = document.createElement("canvas");
      var style = canvas.style;
      style.position = "fixed";
      style.top = style.left = style.right = style.margin = style.padding = 0;
      style.zIndex = 100001;
      style.display = "none";
      if (options.className) canvas.classList.add(options.className);
      document.body.appendChild(canvas);
      addEvent(window, "resize", repaint);
    },
    topbar = {
      config: function (opts) {
        for (var key in opts)
          if (options.hasOwnProperty(key)) options[key] = opts[key];
      },
      show: function () {
        if (showing) return;
        showing = true;
        if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
        if (!canvas) createCanvas();
        canvas.style.opacity = 1;
        canvas.style.display = "block";
        topbar.progress(0);
        if (options.autoRun) {
          (function loop() {
            progressTimerId = window.requestAnimationFrame(loop);
            topbar.progress(
              "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
            );
          })();
        }
      },
      progress: function (to) {
        if (typeof to === "undefined") return currentProgress;
        if (typeof to === "string") {
          to =
            (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
              ? currentProgress
              : 0) + parseFloat(to);
        }
        currentProgress = to > 1 ? 1 : to;
        repaint();
        return currentProgress;
      },
      hide: function () {
        if (!showing) return;
        showing = false;
        if (progressTimerId != null) {
          window.cancelAnimationFrame(progressTimerId);
          progressTimerId = null;
        }
        (function loop() {
          if (topbar.progress("+.1") >= 1) {
            canvas.style.opacity -= 0.05;
            if (canvas.style.opacity <= 0.05) {
              canvas.style.display = "none";
              fadeTimerId = null;
              return;
            }
          }
          fadeTimerId = window.requestAnimationFrame(loop);
        })();
      },
    };

  if (typeof module === "object" && typeof module.exports === "object") {
    module.exports = topbar;
  } else if (typeof define === "function" && define.amd) {
    define(function () {
      return topbar;
    });
  } else {
    this.topbar = topbar;
  }
}.call(this, window, document));
@APB9785

This comment has been minimized.

Copy link

@APB9785 APB9785 commented Aug 31, 2021

If anyone else is getting errors on running mix assets.deploy, here's a brief guide summarizing the changes you must apply within the assets folder when upgrading from 1.5 to 1.6

https://github.com/APB9785/APB9785.github.io/blob/master/blogs/upgrade_phoenix_1_6_esbuild_errors.md

@petemcfarlane

This comment has been minimized.

Copy link

@petemcfarlane petemcfarlane commented Sep 1, 2021

I found I needed to add the lib/project_web/endpoint.ex Plug.Static config slightly, adding assets to the list of only paths. I think the suggested upgrade guide dumps the app.css/app.js directly in the priv/static/assets directory, previously mine were put into priv/static/css/ and priv/static/js respectively. Hoping this may help someone new to the framework like me.

@aj-foster

This comment has been minimized.

Copy link

@aj-foster aj-foster commented Sep 23, 2021

If you previously used the standalone phx.gen.auth and followed along with https://github.com/aaronrenner/phx_gen_auth_output to keep your code up-to-date, be sure to take note of this commit which occurred during the standalone->bundled transition.

It may be why your test test require_authenticated_user/2 stores the path to redirect to on GET is failing.

@hlship

This comment has been minimized.

Copy link

@hlship hlship commented Sep 25, 2021

== Compilation error in file lib/sk_web/endpoint.ex ==
** (SyntaxError) lib/sk_web/endpoint.ex:29:57: unexpectedly reached end of line. The current expression is invalid or incomplete
    (elixir 1.12.3) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
~/workspaces/elixir/speaker-karaoke > mix assets.deploy
Compiling 1 file (.ex)
 > js/app.js:13:7: error: No matching export in "../deps/phoenix_live_view/priv/static/phoenix_live_view.esm.js" for import "default"
    13 │ import LiveSocket from "phoenix_live_view"
       ╵        ~~~~~~~~~~

Getting this on upgrade to 1.6.0, despite previous upgrade to esbuild.

Despite this, the dashboard works, but I don't like errors in my build (warnings I can stomach).

Update: error is harmless locally, but prevents my Dokku-based production build from starting up.

@hlship

This comment has been minimized.

Copy link

@hlship hlship commented Sep 25, 2021

Figured it out, the line should be:

import {LiveSocket} from "phoenix_live_view"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment