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