A proof-of-concept integration of Vite.js (modern JS/assets bundler) with Phoenix + Liveview

Thanks for the detailed gist @thiagomajesk.
Observed that you have setup the latest bootstrap. It would have been nicer if you showed your app.scss and app.js as well. Does the bootstrap Javascript work normally with LiveView? For alpine, you need extra setup for it to work smoothly with LiveView. Does Bootstrap also need any extra configuration?

Hi @cvkmohan! Everything should work as expected for both app.scss and app.js, you just need to import your files as usual (eg: @import "bootstrap" and import 'bootstrap').

About LiveView, I haven’t tested how it interacts with other Javascript libraries. I can’t recall having problems with Bootstrap the last time I used it, but this was over a year ago. The problems you’re likely to encounter tend to be related to code initialization and keeping state, since LV will not reload the page.

I know there are some questions on the forum about this, perhaps you should take a look and see if you find better content on the subject there. Since I’m not actively trying to use LV with Alpine and Bootstrap I’m not the best person to answer that.

1 Like

Early on, I had the idea that I wanted separate CSS files for separate pages of my app*, and as a result of that, found so much success in just decoupling the styling from the bundling altogether, running brew install sass/sass/sass on my dev box, and then feeling the glorious, glorious lifting of a burden when I deleted so many CSS-based plugins from package.json and webpack.config.

Then Phoenix migrated everything over to esbuild, and rocky takeoff notwithstanding, I have everything running smoothly and snappily now.

# config/dev.exs
watchers: [
  sass: ~w[--watch --no-source-map assets/css/:priv/static/css],
  esbuild: {Esbuild, :install_and_run, [:default, ~w(--watch)]}
]

I’ve also got a mix task for wrapping all of my assets up in a nice, self-contained digest when it’s deploy time. I call it assets.deploy

defmodule Mix.Tasks.Assets.Deploy do
  use Mix.Task

  @shortdoc "Packages assets for the browser"

  @moduledoc """
  Packages assets for the browser.

  If the `DEV_MODE` environment variable is set, will not minify the CSS and JS,
  or create a sourcemap for either.
  """

  def run(_args) do
    dev_mode? = env("MIX_ENV") in [:dev, :test, nil]
    shell! "rm -rf priv/static/*"
    shell! "cp -R assets/static/* priv/static/"
    cmd "sass assets/css/app.scss:priv/static/css/app.css" <> (unless dev_mode?, do: " --source-map", else: "")
    cmd "mix esbuild default" <> (unless dev_mode?, do: " --minify --sourcemap", else: "")
    unless dev_mode? do
      cmd "mix phx.digest -o priv/digest"
      shell! "rm -rf priv/static/*"
      shell! "mv priv/digest/* priv/static/"
    end
  end

  defp cmd(command, collectible \\ IO.stream(:stdio, :line)) when is_binary(command) do
    [app | options] = String.split(command, ~r/\s+/)
    cmd(app, options, collectible)
  end
  defp cmd(app, options, collectible) do
    [app | options] |> IO.inspect
    System.cmd(app, options, into: collectible)
  end
  defp shell!(command, opts \\ [into: IO.stream()]) do
    command |> IO.inspect
    System.shell(command, opts)
  end

  defp env(var), do: System.get_env(var)
end

(the cmd, shell!, and env patterns are brought over from my actual deployment-deploying task, sort of a bespoke take on Capistrano for publishing to gigalixir through its’ git endpoint… possibly from a tty box idk life’s wild sometimes. I call it giggitty. But I’m way off topic now!

EDIT: * I did, of course, realize the awful idea that would’ve been, and now roll all of my CSS, with the comparatively gigantic phoenix.css taffy monster beneath my app.scss file and everything, into one, huge, abomination.

3 Likes

Thanks so much for this. I was thinking of trying out a Vite based setup too, and you’ve done all the hard work for me.

Thanks a lot! I’m new to Elixir, your details were very helpful. :+1:

In complement, this article was useful to me integrating vite.js with phoenix 1.6 | moroz.dev

3 Likes

Should we disable the esbuild used in Phoenix (with the watcher) to avoid conflict with Vite.js?

I finally managed to port another project using Webpack to Vite and I made some changes to my vite.config.js file. The main reason for those changes are:


import { defineConfig } from "vite";

export default defineConfig((command) => {
  maybeCloseStdin(command)

  return {
    publicDir: "./static",
    build: {
      target: "es2018",
      minify: true,
      outDir: "../priv/static",
      emptyOutDir: true,
      assetsInlineLimit: 0,
      rollupOptions: {
        input: ["js/app.js", "css/app.scss"],
        output: {
          entryFileNames: "js/[name].js",
          chunkFileNames: "js/[name].js",
          assetFileNames: chunkAssets
        },
      }
    }
  }
})

/*
* Watches and closes stdin when the main process closes.
* This avoids zombie esbuild processes accumulating in the system.
*/
function maybeCloseStdin(command) {
  if (command == "build") return
  process.stdin.on("close", () => { process.exit(0) })
  process.stdin.resume()
}

/*
* Maps asset chunks to their corresponding folder.
* Keys are regexes that match the file name and values should be a asset folder.
* This first match is always used, so make sure to put the most specific regex first.
*/
const assetChunkMappings = {
  "\.webfonts/\.(woff2?|ttf|eot|svg)": "fonts/[name][extname]",
  "\.(woff2?|ttf|eot)$": "fonts/[name][extname]"
}

function chunkAssets(info) {
  return Object.entries(assetChunkMappings)
    .filter(([key, _value]) => info.name.match(key))
    .map(([_key, value]) => value)[0] || "[ext]/[name][extname]"
}
3 Likes

Just an important update on this solution:

If someone is having problems using it, just replace this line: export default defineConfig((command) with export default defineConfig(({command}). The destructuring syntax here is important because if not, the wrong object will be passed to maybeCloseStdin which will cause the vite build command to not exit properly, preventing other commands like mix phx.digest to execute correctly giving you lots of headaches in production :sweat_smile:.

3 Likes

I have an important update on this solution: It seems that there was some sort of regression and the provided solution to prevent zombie processes doesn’t work anymore (details here and here). If you are still having problems where Vite is leaving orphan processes behind, try changing your setup to use a wrapper script instead. The main idea is explained in the docs here: Port — Elixir v1.12.0-rc.0.

# mix.exs alias for asset deploy... 
# If your deploy process uses docker you can simply use npx here.
# However, if your deploy machine state is persistent, call the binary from the node_modules directly instead.
"assets.deploy": ["cmd --cd assets npx vite build", "phx.digest"]
# dev.exs config watchers
# Call the Vite binary directly in development, which is the most common use-case.
# It's important to call the binary directly because the wrapper will terminate only the direct children.
# phoenix [server] -> node -> esbuild [vite] (works properly when terminated)
# If we call 'npx' here, another node process will be spawned and the process tree will become something like:
# phoenix [server] -> node -> node [npx] -> esbuild [vite] (won't be terminated properly)
# To understand it better, you can observe this problem by echoing the PID in the wrapper script and checking which process in the tree is actually being terminated.
watchers: [
    "#{Path.expand("../assets/wrapper.sh", __DIR__)}": [
      "node_modules/.bin/vite",
      "build",
      "--watch",
      "--minify",
      "false",
      "--emptyOutDir",
      "false",
      "--clearScreen",
      "false",
      "--mode",
      "development",
      cd: Path.expand("../assets", __DIR__)
    ]

wrapper.sh

#!/usr/bin/env bash

# Start the program in the background
exec "$@" &
pid1=$!

# Silence warnings from here on
exec >/dev/null 2>&1

# Read from stdin in the background and
# kill running program when stdin closes
exec 0<&0 $(
  while read; do :; done
  kill -KILL $pid1
) &
pid2=$!

# Clean up
wait $pid1
ret=$?
kill -KILL $pid2
exit $ret
2 Likes

@thiagomajesk Any reason you went for vite build and then --watch it in dev mode? Default for dev would be just vite and with this you just have to make the assets available via the header through localhost e.g. phoenix-liveview-vite-demo/_preamble.html.heex at main · moroz/phoenix-liveview-vite-demo · GitHub

Would love to hear your opinion about it or if you just wanted to keep it closer to the deploy functionality?

best
denym_

Interesting take @denym. I’m not an expert on build tools but at first sight, I’d be wary of possible side-effects with vite hot reload. This might not be a problem at all, and if it’s working, go for it, less configuration is always better; in fact, this solution would make a nice integration lib (with the script tag replacement solution and all).

@thiagomajesk Just further followups on your approach:

  • @/some/path can’t be used and all imports in vue have to be relative
  • routing gets quite messy when using vue router since phoenix is going to resolve the paths first. Only solution I can think of is to manually except all used routes from vue in the phoenix routes.ex
  • the server log gets quite crowded and become hard to read due to js shouting left and right when developing

With just those issues I can only recommend using a clean external vite project besides your phoenix backend. Especially when you use phoenix as api backend.

Just my 2 cents & best regards
denym_

I really like your solution and also used it.

Just one addition, as I struggled with it. The current solution will deleted files like robots.txt in /priv/static. If you need them, change emptyOutDir to emptyOutDir: false.

IIRC, the publicDir: "./static config, copies everything from assets/static to priv/static. So there should be no problem in deleting those files in development. Make sure you are not mixing approaches like the one used with the new esbuild (where everything is put directly into priv/static).

What do you mean exactly? By changing that config above files are copied but the directory is not erased. For sure you could also move those files but I don’t see much of a difference of both solutions.

The emptyOutDir config empties the priv/static directory, which is the default behavior you get from Vite to make things easier to check while in development. But here we have to configure it manually to do that.

You mentioned that your robots.txt file was being deleted from priv/static, but you are not supposed to keep the original files there. Using the config I provided, everything from assets/static is copied over to priv/static (both files and directories). So, if you want to keep your robots.txt file, it should be placed inside assets/static - like we used to do before esbuild.

The emptyOutDir config defaults to true because otherwise, you would end up with old trash being kept in the outDir while you organize your files (eg: if you rename files or remove directories from the publicDir) which is not desirable in the majority of cases.

Could you kindly update this article to work with Phoenix 1.7? I am new to Phoenix, I have tried several options by reading a few articles online as well as consulting chatGPT but non is working.

Phoenix 1.7 uses esbuild, so you shouldn’t need Vite, unless you really want to. In that case, I recommend that you take a look at the getting started guide - it shouldn’t be that different.

I was talking about an article on how to integrate Bootstrap into phoenix 1.7. I tried out your instructions above but they didn’t work. I want to removed tailwind css and replace it with bootstrap

Have you checked this comment? You shouldn’t need anything special to integrate Bootstrap with Phoenix 1.7. I’d say that it’s even easier now that we have the Esbuild CLI built-in.

I think if you just follow the instructions on the docs you’ll get it to work, they even have a Vite-specific section there now: Get started with Bootstrap · Bootstrap v5.3.

About the thread, I won’t be updating it because the focus here is on ViteJs. However, I’m almost sure if you search the forum you’ll find threads like this one that goes step by step on how to configure Bootstrap itself: Phoenix 1.7 Bootstrap CSS for Core Components.

Cheers!