defmodule Mix.Tasks.Compile.Surface do @moduledoc """ Generate CSS and JS assets for components. """ use Mix.Task @recursive true @default_hooks_output_dir "assets/js/_hooks" @hooks_extension ".hooks.js" @doc false def run(_args) do case generate_files() do "" -> {:noop, []} _ -> {:ok, []} end end defp generate_files() do opts = Application.get_env(:surface, :compiler, []) hooks_output_dir = Keyword.get(opts, :hooks_output_dir, @default_hooks_output_dir) js_output_dir = Path.join([File.cwd!(), hooks_output_dir]) index_file = Path.join([js_output_dir, "index.js"]) {js_files, _css_files} = get_colocated_assets() File.mkdir_p!(js_output_dir) delete_unused_hooks_files!(js_output_dir, js_files) index_file_time = case File.stat(index_file) do {:ok, %File.Stat{mtime: time}} -> time _ -> nil end update_index? = for {src_file, dest_file} <- js_files, {:ok, %File.Stat{mtime: time}} <- [File.stat(src_file)], !index_file_time or !File.exists?(dest_file) or time > index_file_time, reduce: false do _ -> File.cp!(src_file, Path.join(js_output_dir, dest_file)) true end if update_index? do File.write!(index_file, index_content(js_files)) end end defp get_colocated_assets() do for mod <- Mix.Phoenix.modules(), Code.ensure_loaded?(mod), function_exported?(mod, :component_type, 0), reduce: {[], []} do {js_files, css_files} -> base_file = mod.module_info() |> get_in([:compile, :source]) |> Path.rootname() js_file = "#{base_file}#{@hooks_extension}" base_name = inspect(mod) dest_js_file = "#{base_name}#{@hooks_extension}" css_file = "#{base_file}.css" dest_css_file = "#{base_name}.css" js_files = if File.exists?(js_file), do: [{js_file, dest_js_file} | js_files], else: js_files css_files = if File.exists?(css_file), do: [{css_file, dest_css_file} | css_files], else: css_files {js_files, css_files} end end defp index_content([]) do """ /* This file was generated by the Surface compiler */ export default {} """ end defp index_content(js_files) do files = js_files |> Enum.sort() |> Enum.with_index(1) {hooks, imports} = for {{_file, dest_file}, index} <- files, reduce: {[], []} do {hooks, imports} -> namespace = Path.basename(dest_file, @hooks_extension) var = "c#{index}" hook = ~s[ns(#{var}, "#{namespace}")] imp = ~s[import * as #{var} from "./#{namespace}.hooks"] {[hook | hooks], [imp | imports]} end hooks = Enum.reverse(hooks) imports = Enum.reverse(imports) """ /* This file was generated by the Surface compiler */ function ns(hooks, nameSpace) { const updatedHooks = {} Object.keys(hooks).map(function(key) { updatedHooks[`${nameSpace}#${key}`] = hooks[key] }) return updatedHooks } #{Enum.join(imports, "\n")} let hooks = Object.assign( #{Enum.join(hooks, ",\n ")} ) export default hooks """ end defp delete_unused_hooks_files!(js_output_dir, js_files) do used_files = Enum.map(js_files, fn {_, dest_file} -> Path.join(js_output_dir, dest_file) end) all_files = js_output_dir |> Path.join("*#{@hooks_extension}") |> Path.wildcard() unsused_files = all_files -- used_files Enum.each(unsused_files, &File.rm!/1) end end