Packages

A Mix task for downloading Three.js assets and setting up imports in Phoenix projects. Automatically downloads Three.js files from CDN and configures app.js imports.

Current section

Files

Jump to
phoenix_three lib configure.ex
Raw

lib/configure.ex

defmodule Mix.Tasks.Threejs.Configure do
@moduledoc """
Configures Three.js imports in Phoenix app.js.
Adds Three.js import and global window assignment to assets/js/app.js.
## Usage
mix threejs.configure
## Options
--force Overwrite existing imports
"""
use Mix.Task
@shortdoc "Configures Three.js imports in Phoenix app.js"
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
switches: [force: :boolean],
aliases: [f: :force]
)
force = opts[:force] || false
Mix.shell().info("🔧 Configuring Three.js imports in app.js...")
update_app_js(force)
end
defp update_app_js(force) do
app_js_path = Path.join(["assets", "js", "app.js"])
if File.exists?(app_js_path) do
content = File.read!(app_js_path)
updated_content =
content
|> add_threejs_import(force)
|> add_threejs_global(force)
if content != updated_content do
File.write!(app_js_path, updated_content)
Mix.shell().info("✅ Updated app.js with Three.js imports and global assignment")
else
Mix.shell().info("⏭️ app.js already contains Three.js setup")
end
else
Mix.shell().error("❌ assets/js/app.js not found")
Mix.shell().info("💡 Make sure you're running this from your Phoenix project root")
end
end
defp add_threejs_import(content, force) do
import_line = ~s(import * as THREE from "../vendor/three/three.module.js";)
cond do
String.contains?(content, import_line) and not force ->
content
String.contains?(content, ~s(from "../vendor/three/three.module.js")) and not force ->
content
true ->
lines = String.split(content, "\n")
{before_imports, after_imports} = find_import_insertion_point(lines)
cleaned_lines =
if force do
Enum.reject(before_imports ++ after_imports, fn line ->
String.contains?(line, "three.module.js")
end)
else
before_imports ++ after_imports
end
{before_lines, after_lines} = find_import_insertion_point(cleaned_lines)
updated_lines = before_lines ++ [import_line] ++ after_lines
Enum.join(updated_lines, "\n")
end
end
defp add_threejs_global(content, force) do
global_assignment = "\n\n// Make THREE.js available globally for hooks\nwindow.THREE = THREE"
cond do
String.contains?(content, "window.THREE = THREE") and not force ->
content
String.contains?(content, "window.THREE") and not force ->
content
true ->
cleaned_content =
if force do
content
|> String.replace(
~r/\n\/\/ Make THREE\.js available globally.*\nwindow\.THREE = THREE/s,
""
)
|> String.replace(~r/window\.THREE = THREE/s, "")
else
content
end
# Find window.liveSocket assignment and add after it
case Regex.run(~r/^.*window\.liveSocket\s*=.*$/m, cleaned_content) do
[livesocket_line] ->
String.replace(cleaned_content, livesocket_line, livesocket_line <> global_assignment)
nil ->
# Fallback: add at the end of the file
cleaned_content <> global_assignment
end
end
end
defp find_import_insertion_point(lines) do
last_import_index =
lines
|> Enum.with_index()
|> Enum.reverse()
|> Enum.find_value(fn {line, index} ->
if String.match?(line, ~r/^\s*import\s+/) do
index
end
end)
case last_import_index do
nil ->
{[], lines}
index ->
{Enum.take(lines, index + 1), Enum.drop(lines, index + 1)}
end
end
end