Packages
phoenix_three
0.1.0
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
Current section
Files
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