defmodule WebpackStatic.Plug do @moduledoc """ Phoenix plug to proxy a locally running instance of the webpack dev server.
This plug will only serve assets when the env parameter has the value of `:dev`.
Phoenix will be allowed a chance to resolve any assets not resolved by webpack.
## Installation ``` defp deps do [ {:WebpackStaticPlug, "~> 0.1.0"} ] end ``` And run: $ mix deps.get ## Usage Add WebpackStatic.Plug as a plug in the phoenix project's endpoint. ## Arguments * **port** - *(required)* The port that the webpack dev server is listening on. * **webpack_assets** - *(required)* a list of the paths in the static folder that webpack will for serve. The plug will ignore requests to any other path. * **env** - *(required)* the current environment the project is running under. * **manifest_path** - *(optional)* relative path that will resolve from the static folder of the webpack manifest file. ## Example in `endpoint.ex` ``` plug WebpackStatic.Plug, port: 9000, webpack_assets: ~w(css fonts images js), env: Mix.env, manifest_path: "/manifest.json" ``` """ alias HTTPotion, as: Http alias Plug.Conn, as: Conn require Poison @doc false def init([port, webpack_assets, env, manifest_path]) do [port, webpack_assets, env, manifest_path] end @doc false def call(conn, [ {:port, port}, {:webpack_assets, assets}, {:env, env}, {:manifest_path, manifest_path} ]) do if env == :dev do manifest_task = Task.async(fn -> get_manifest(manifest_path, port) end) serve_asset(conn, port, assets, Task.await(manifest_task)) else conn end end defp get_manifest(path, port) do url = "http://localhost:#{port}" |> URI.merge(path) |> URI.to_string() case Http.get(url, headers: [Accept: "application/json"]) do %HTTPotion.Response{status_code: code} when code == 400 -> raise "404 could not find: #{url}" %HTTPotion.Response{body: body} -> Poison.decode!(body) %HTTPotion.ErrorResponse{message: message} -> raise "Error fetching manifest: #{message}" end end defp serve_asset( conn = %Plug.Conn{ path_info: [uri, file_name], req_headers: req_headers }, port, assets, manifest ) do requested_path = "#{uri}/#{file_name}" actual_path = case manifest do %{^requested_path => value} -> value _ -> file_name end url = "http://localhost:#{port}" |> URI.merge(actual_path) |> URI.to_string() asset_type = uri |> String.split("/") |> hd if Enum.any?(assets, &(&1 == asset_type)) do Http.get( url, stream_to: self(), headers: req_headers ) receive_response(conn) else conn end end defp serve_asset(conn = %Plug.Conn{}, _, _, _), do: conn defp receive_response(conn) do receive do %HTTPotion.AsyncChunk{chunk: chunk} -> case Conn.chunk(conn, chunk) do {:ok, conn} -> receive_response(conn) {:error, reason} -> raise reason end %HTTPotion.AsyncHeaders{status_code: status} when status == 404 -> conn %HTTPotion.AsyncHeaders{headers: %HTTPotion.Headers{hdrs: headers}} -> headers |> Map.to_list() |> Enum.reduce(conn, fn {key, value}, acc -> Conn.put_resp_header(acc, key, value) end) |> Conn.send_chunked(200) |> receive_response() %HTTPotion.AsyncEnd{} -> Conn.halt(conn) %HTTPotion.ErrorResponse{message: message} -> raise "Error fetching webpack resource: #{message}" after 15_000 -> raise "Error fetching webpack resource: Timeout exceeded" end end end