Skip to main content

lib/mix/tasks/build.ex

defmodule Mix.Tasks.DicEx.Build do
  @moduledoc """
  Builds the dicEx frontend assets (Three.js + Rapier) into
  `priv/static/dic_ex.min.js` so they ship with the Hex package.

      mix dic_ex.build

  Run inside the package when you change anything under `assets/src`. Consumers
  of the published package only need the prebuilt file plus the LiveView hook.

  ## Prerequisites

  Requires Node.js and runs `pnpm install` (or `npm install`) on first use when
  `assets/node_modules` is missing.
  """

  use Mix.Task

  @shortdoc "Builds the dicEx 3D frontend bundle"

  @impl true
  def run(_args) do
    assets_dir = Path.join(File.cwd!(), "assets")

    ensure_deps(assets_dir)
    ensure_priv_static()

    Mix.shell().info("[dicEx] bundling assets -> priv/static/dic_ex.min.js")

    {time, _} =
      :timer.tc(fn ->
        System.cmd("node", ["build.mjs"],
          cd: assets_dir,
          stderr_to_stdout: true,
          into: IO.binstream(:stdio, :line)
        )
      end)

    Mix.shell().info("[dicEx] built in #{div(time, 1000)}ms")
  end

  defp ensure_deps(assets_dir) do
    node_modules = Path.join(assets_dir, "node_modules")

    unless File.dir?(node_modules) do
      {runner, args} = package_manager()
      Mix.shell().info("[dicEx] installing JS deps with #{runner}")

      System.cmd(runner, args ++ ["install"],
        cd: assets_dir,
        stderr_to_stdout: true,
        into: IO.binstream(:stdio, :line)
      )
    end
  end

  defp ensure_priv_static do
    File.mkdir_p!(Path.join(File.cwd!(), "priv/static"))
  end

  defp package_manager do
    cond do
      System.find_executable("pnpm") -> {"pnpm", []}
      System.find_executable("bun") -> {"bun", ["install"]}
      System.find_executable("npm") -> {"npm", []}
      true -> Mix.raise("no JS package manager found (pnpm/npm/bun)")
    end
  end
end