defmodule WebPack.Events do def child_spec(_) do Registry.child_spec(keys: :duplicate, name: __MODULE__) end @dispatch_key :events def register! do {:ok, _} = Registry.register(__MODULE__,@dispatch_key,nil) end def dispatch(event) do Registry.dispatch(__MODULE__, @dispatch_key, fn entries -> for {pid, nil} <- entries, do: send(pid,{:event,event}) end) end import Plug.Conn def stream_chunks(conn) do register!() conn = Stream.repeatedly(fn-> receive do {:event,event}-> event end end) |> Enum.reduce_while(conn, fn event, conn -> io = "event: #{event.event}\ndata: #{Poison.encode!(event)}\n\n" case chunk(conn,io) do {:ok,conn}->{:cont,conn};{:error,:closed}->{:halt,conn} end end) halt(conn) end end defmodule WebPack.Plug.Static do @moduledoc """ This plug API is the same as plug.static, but wrapped to : - wait file if compiling before serving them - add server side event endpoint for webpack build events - add webpack "stats" JSON getter, and stats static analyser app """ use Plug.Router plug :match plug :dispatch plug Plug.Static, at: "/webpack/static", from: :reaxt plug :wait_compilation def init(static_opts), do: Plug.Static.init(static_opts) def call(conn, opts) do conn = plug_builder_call(conn, opts) if !conn.halted, do: static_plug(conn,opts), else: conn end def wait_compilation(conn,_) do if Application.get_env(:reaxt,:hot) do try do :ok = GenServer.call(WebPack.EventManager,:wait?,30_000) catch :exit,{:timeout,_} -> :ok end end conn end def static_plug(conn,static_opts) do Plug.Static.call(conn,static_opts) end get "/webpack/stats.json" do conn |> put_resp_content_type("application/json") |> send_file(200,"#{WebPack.Util.web_priv}/webpack.stats.json") |> halt end get "/webpack", do: %{conn|path_info: ["webpack","static","index.html"]} get "/webpack/events" do conn=conn |> put_resp_header("content-type", "text/event-stream") |> send_chunked(200) hot? = Application.get_env(:reaxt,:hot) if hot? == :client, do: chunk(conn, "event: hot\ndata: nothing\n\n") if hot? do WebPack.Events.stream_chunks(conn) else conn end end get "/webpack/client.js" do conn |> put_resp_content_type("application/javascript") |> send_file(200,"#{WebPack.Util.web_app}/node_modules/reaxt/webpack_client.js") |> halt end match _, do: conn end defmodule WebPack.StartBlocker do @moduledoc """ this process blocks application start to ensure that when the next application starts the reaxt render is ready (js is compiled) """ def child_spec(arg) do %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]}, restart: :temporary} end def start_link(timeout) do :proc_lib.start_link(__MODULE__,:wait, [timeout]) end def wait(timeout) do :ok = GenServer.call(WebPack.EventManager,:wait?,timeout) :proc_lib.init_ack({:ok,self()}) end end defmodule WebPack.EventManager do use GenServer require Logger def start_link(_) do GenServer.start_link(__MODULE__,[], name: __MODULE__) end def init([]) do WebPack.Events.register!() {:ok,%{init: true,pending: [], compiling: false, compiled: false}} end def handle_call(:wait?,_from,%{compiling: false}=state), do: {:reply,:ok,state} def handle_call(:wait?,from,state), do: {:noreply,%{state|pending: [from|state.pending]}} def handle_info({:event,%{event: "client_done"}=ev},state) do Logger.info("[reaxt-webpack] client done, build_stats") WebPack.Util.build_stats if(!state.init) do Logger.info("[reaxt-webpack] client done, restart servers") :ok = Supervisor.terminate_child(Reaxt.App, Reaxt.App.PoolsSup) {:ok,_} = Supervisor.restart_child(Reaxt.App, Reaxt.App.PoolsSup) end if ev[:error] do Logger.error("[reaxt-webpack] error compiling server_side JS #{ev[:error]}") if ev[:error] != "soft fail", do: System.halt(1) end for {_idx,build}<-WebPack.stats, error<-build.errors, do: Logger.warn(error) for {_idx,build}<-WebPack.stats, warning<-build.warnings, do: Logger.warn(warning) {:noreply,done(state)} end def handle_info({:event,%{event: "client_invalid"}},%{compiling: false}=state) do Logger.info("[reaxt-webpack] detect client file change") {:noreply,%{state|compiling: true, compiled: false}} end def handle_info({:event,%{event: "done"}},state) do Logger.info("[reaxt-webpack] both done !") {:noreply,state} end def handle_info({:event,ev},state) do Logger.info("[reaxt-webpack] event : #{ev[:event]}") {:noreply,state} end def done(state) do for from<-state.pending do GenServer.reply(from,:ok) end WebPack.Events.dispatch(%{event: "done"}) %{state| pending: [], init: false, compiling: false, compiled: true} end end defmodule WebPack.Compiler do def child_spec(arg) do %{id: __MODULE__, start: {__MODULE__, :start_link, [arg]} } end def start_link(_) do cmd = "node ./node_modules/reaxt/webpack_server #{WebPack.Util.webpack_config}" hot_arg = if Application.get_env(:reaxt,:hot) == :client, do: " hot",else: "" Exos.Proc.start_link(cmd<>hot_arg,[],[cd: WebPack.Util.web_app],[name: __MODULE__],&WebPack.Events.dispatch/1) end end defmodule WebPack.Util do def webpack_config do Application.get_env(:reaxt,:webpack_config,"webpack.config.js") end def web_priv do case Application.get_env :reaxt, :otp_app, :no_app_specified do :no_app_specified -> :no_app_specified web_app -> :code.priv_dir(web_app) end end def web_app do Application.get_env :reaxt, :web_app, "web" end def parallel_build do ## We can't have parallel AND hot reload together Application.get_env(:reaxt, :hot, false) == false && Application.get_env(:reaxt, :parallel, false) end def build_stats do if File.exists?("#{web_priv()}/webpack.stats.json") do all_stats = Poison.decode!(File.read!("#{web_priv()}/webpack.stats.json")) #The format of the stats from parallel-webpack is different than the raw one from webpack stats_array = if WebPack.Util.parallel_build, do: all_stats, else: all_stats["children"] stats = Enum.map(stats_array,fn stats-> %{assetsByChunkName: stats["assetsByChunkName"], errors: stats["errors"], warnings: stats["warnings"]} end) _ = Code.compiler_options(ignore_module_conflict: true) defmodule Elixir.WebPack do @stats stats def stats, do: @stats def file_of(name) do r = Enum.find_value(WebPack.stats, fn %{assetsByChunkName: assets}-> assets["#{name}"] end) case r do [f|_]->f f -> f end end @header_script if(Application.get_env(:reaxt,:hot), do: ~s()) @header_global Poison.encode!(Application.get_env(:reaxt,:global_config)) def header, do: "\n#{@header_script}" end _ = Code.compiler_options(ignore_module_conflict: false) end end end defmodule Elixir.WebPack do def stats, do: %{assetsByChunkName: %{}} def file_of(_), do: nil def header, do: "" end