Packages

Seamless Three.js integration for Phoenix LiveView.

Current section

Files

Jump to
live_three lib mix tasks live_three.install.ex
Raw

lib/mix/tasks/live_three.install.ex

defmodule Mix.Tasks.LiveThree.Install do
use Mix.Task
@shortdoc "Installs LiveThree JS dependencies and provides setup instructions"
@min_lv_version "0.20.0"
@moduledoc """
This task helps you set up LiveThree in your Phoenix project.
It performs the following:
1. Checks for a `package.json` in the assets directory.
2. Runs `npm install three` (or your preferred manager).
3. Provides the JS Hook registration code.
"""
def run(_args) do
Mix.shell().info([:green, "* installing live_three setup..."])
case check_liveview_version() do
:ok ->
perform_install()
{:error, reason} ->
Mix.shell().error("✘ Compatibility Error: #{reason}")
Mix.shell().info("LiveThree requires Phoenix LiveView >= #{@min_lv_version}")
end
end
defp install_npm_deps(path) do
Mix.shell().info([:blue, "Checking for npm dependencies..."])
# Detect package manager
{cmd, args} =
cond do
File.exists?(Path.join(path, "pnpm-lock.yaml")) -> {"pnpm", ["add", "three"]}
File.exists?(Path.join(path, "yarn.lock")) -> {"yarn", ["add", "three"]}
true -> {"npm", ["install", "--save", "three"]}
end
case System.cmd(cmd, args, cd: path, into: IO.stream(:stdio, :line)) do
{_, 0} ->
Mix.shell().info([:green, "✔ Successfully installed three.js"])
_ ->
Mix.shell().error(
"✘ Failed to install three.js. Please run '#{cmd} #{Enum.join(args, " ")}' manually in ./assets"
)
end
end
defp check_liveview_version do
case Application.loaded_applications() |> List.keyfind(:phoenix_live_view, 0) do
{:phoenix_live_view, _desc, version} ->
v_string = List.to_string(version)
if Version.match?(v_string, ">= #{@min_lv_version}") do
:ok
else
{:error, "Found LiveView #{v_string}, but we need #{@min_lv_version}+"}
end
nil ->
{:error, "Phoenix LiveView not found in loaded applications."}
end
end
defp perform_install do
assets_path = Path.expand("assets")
if File.dir?(assets_path) do
Mix.shell().info([:green, "* Configuring assets..."])
# 1. Inject the "file:../deps/live_three" dependency
case add_npm_path_dep(assets_path) do
:ok ->
# 2. Run the actual install (npm/yarn/pnpm)
install_npm_deps(assets_path)
Mix.shell().info("""
#{IO.ANSI.green()}✔ LiveThree is now linked to your assets!#{IO.ANSI.reset()}
You can now use: #{IO.ANSI.cyan()}import { LiveThreeHook } from "live_three"#{IO.ANSI.reset()}
""")
print_instructions()
{:error, reason} ->
Mix.shell().error("✘ Could not update package.json: #{reason}")
end
else
Mix.shell().error("✘ Could not find 'assets' directory. Are you in a Phoenix project root?")
end
end
defp add_npm_path_dep(assets_path) do
package_json_path = Path.join(assets_path, "package.json")
if File.exists?(package_json_path) do
try do
content = File.read!(package_json_path) |> Jason.decode!()
# We insert the path to the deps folder so esbuild can find the JS
updated_content =
put_in(
content,
["dependencies", "live_three"],
"file:../deps/live_three"
)
File.write!(package_json_path, Jason.encode_to_iodata!(updated_content, pretty: true))
:ok
rescue
_ -> {:error, "Invalid package.json format."}
end
else
{:error, "package.json not found."}
end
end
defp print_instructions do
Mix.shell().info("""
#{IO.ANSI.bright()}Next Steps:#{IO.ANSI.reset()}
1. Register the Hook in your #{IO.ANSI.cyan()}assets/js/app.js#{IO.ANSI.reset()}:
import { LiveThreeHook } from "live_three"
let Hooks = { LiveThreeHook }
// ... add to your LiveSocket
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
hooks: Hooks
})
2. Use the component in your LiveView template:
<.three_scene id="my-scene" effect="starfield" />
Enjoy building in 3D!
""")
end
end