Current section

Files

Jump to
sagents_live_debugger lib sagents_live_debugger assets.ex
Raw

lib/sagents_live_debugger/assets.ex

defmodule SagentsLiveDebugger.Assets do
@moduledoc false
# Plug that serves self-contained CSS and JavaScript for the debugger.
#
# At compile time, this module reads the debugger CSS file and concatenates
# the pre-built JS files from phoenix, phoenix_html, and phoenix_live_view
# dependencies, plus a small LiveSocket initialization script. Assets are
# served via Plug routes with cache-busting MD5 hashes in the URL.
#
# This follows the same pattern as Phoenix.LiveDashboard.Assets.
import Plug.Conn
# Read CSS at compile time
css_path = Path.join(:code.priv_dir(:sagents_live_debugger), "static/debugger.css")
@external_resource css_path
@css File.read!(css_path)
@css_hash Base.encode16(:crypto.hash(:md5, @css), case: :lower)
# Read Phoenix dependency JS files at compile time
phoenix_js_paths =
for app <- [:phoenix, :phoenix_html, :phoenix_live_view] do
path = Application.app_dir(app, ["priv", "static", "#{app}.js"])
Module.put_attribute(__MODULE__, :external_resource, path)
path
end
# Minimal LiveSocket initialization script.
# After the concatenated Phoenix JS files load, `Phoenix` and `LiveView`
# are available as globals. This connects the LiveSocket so the page
# becomes interactive.
@init_js """
(function() {
var socketPath = document.querySelector("html").getAttribute("phx-socket") || "/live";
var csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
var liveSocket = new LiveView.LiveSocket(socketPath, Phoenix.Socket, {
params: function() {
var tz = "UTC";
try {
tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
} catch (e) {}
return {_csrf_token: csrfToken, time_zone: tz};
}
});
// Attempt WebSocket first, fall back to long polling on connection error
var rawSocket = liveSocket.socket;
var originalOnConnError = rawSocket.onConnError;
var firstConnect = true;
rawSocket.onOpen(function() { firstConnect = false; });
rawSocket.onConnError = function() {
if (firstConnect) {
firstConnect = false;
rawSocket.disconnect(null, 3000);
rawSocket.transport = Phoenix.LongPoll;
rawSocket.connect();
} else {
originalOnConnError.apply(rawSocket, arguments);
}
};
window.addEventListener("phx:page-loading-start", function() {});
window.addEventListener("phx:page-loading-stop", function() {});
liveSocket.connect();
window.liveSocket = liveSocket;
})();
"""
@js Enum.map_join(phoenix_js_paths, "\n", fn path ->
path |> File.read!() |> String.replace("//# sourceMappingURL=", "// ")
end) <> "\n" <> @init_js
@js_hash Base.encode16(:crypto.hash(:md5, @js), case: :lower)
def init(asset) when asset in [:js, :css], do: asset
def call(conn, :js) do
conn
|> put_resp_header("content-type", "text/javascript")
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> put_private(:plug_skip_csrf_protection, true)
|> send_resp(200, @js)
|> halt()
end
def call(conn, :css) do
conn
|> put_resp_header("content-type", "text/css")
|> put_resp_header("cache-control", "public, max-age=31536000, immutable")
|> put_private(:plug_skip_csrf_protection, true)
|> send_resp(200, @css)
|> halt()
end
@doc """
Returns the current MD5 hash for the given asset bundle.
"""
def current_hash(:js), do: @js_hash
def current_hash(:css), do: @css_hash
end