Skip to content

Commit

Permalink
Serve static assets from the router (#406)
Browse files Browse the repository at this point in the history
Follow-up to the conversation from #386:

- Adds dashboard asset routes with resource-specific hashes

- Adds far-future response header for caching
  • Loading branch information
mcrumm authored Feb 14, 2023
1 parent 9ecbcb3 commit d07bde5
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 35 deletions.
2 changes: 1 addition & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Note: Phoenix JS dependencies are loaded
// from their Application directories by the LayoutView
// from their Application directories by the Assets module
import NProgress from "nprogress"
import PhxChartComponent from "./metrics_live"
import PhxRequestLoggerCookie from "./request_logger_cookie"
Expand Down
2 changes: 1 addition & 1 deletion dist/css/app.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions dist/js/app.js

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions lib/phoenix/live_dashboard/controllers/assets.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule Phoenix.LiveDashboard.Assets do
# Plug to serve dependency-specific assets for the dashboard.
@moduledoc false
import Plug.Conn

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

css_path = Path.join(__DIR__, "../../../../dist/css/app.css")
@external_resource css_path
@css File.read!(css_path)

js_path = Path.join(__DIR__, "../../../../dist/js/app.js")
@external_resource js_path

@js """
#{for path <- phoenix_js_paths, do: path |> File.read!() |> String.replace("//# sourceMappingURL=", "// ")}
#{File.read!(js_path)}
"""

@hashes %{
:css => Base.encode16(:crypto.hash(:md5, @css), case: :lower),
:js => Base.encode16(:crypto.hash(:md5, @js), case: :lower)
}

def init(asset) when asset in [:css, :js], do: asset

def call(conn, asset) do
{contents, content_type} = contents_and_type(asset)

conn
|> put_resp_header("content-type", content_type)
|> put_resp_header("cache-control", "public, max-age=31536000")
|> send_resp(200, contents)
|> halt()
end

defp contents_and_type(:css), do: {@css, "text/css"}
defp contents_and_type(:js), do: {@js, "text/javascript"}

@doc """
Returns the current hash for the given `asset`.
"""
def current_hash(:css), do: @hashes.css
def current_hash(:js), do: @hashes.js
end
50 changes: 23 additions & 27 deletions lib/phoenix/live_dashboard/layout_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,6 @@ defmodule Phoenix.LiveDashboard.LayoutView do

embed_templates "layouts/*"

phoenix_path = Application.app_dir(:phoenix, "priv/static/phoenix.js")

phoenix_html_path = Application.app_dir(:phoenix_html, "priv/static/phoenix_html.js")

phoenix_live_view_path =
Application.app_dir(:phoenix_live_view, "priv/static/phoenix_live_view.js")

js_path = Path.join(__DIR__, "../../../dist/js/app.js")
css_path = Path.join(__DIR__, "../../../dist/css/app.css")

@external_resource phoenix_path
@external_resource phoenix_html_path
@external_resource phoenix_live_view_path
@external_resource js_path
@external_resource css_path

@app_js """
#{File.read!(phoenix_html_path) |> String.replace("//# sourceMappingURL=", "// ")}
#{File.read!(phoenix_path) |> String.replace("//# sourceMappingURL=", "// ")}
#{File.read!(phoenix_live_view_path) |> String.replace("//# sourceMappingURL=", "// ")}
#{File.read!(js_path)}
"""
def render("app.js", _), do: @app_js

@app_css File.read!(css_path)
def render("app.css", _), do: @app_css

def render("dash.html", assigns), do: dash(assigns)

defp csp_nonce(conn, type) when type in [:script, :style, :img] do
Expand All @@ -41,4 +14,27 @@ defmodule Phoenix.LiveDashboard.LayoutView do
def live_socket_path(conn) do
[Enum.map(conn.script_name, &["/" | &1]) | conn.private.live_socket_path]
end

# TODO: Remove this and the conditional on Phoenix v1.7+
@compile {:no_warn_undefined, Phoenix.VerifiedRoutes}

defp asset_path(conn, asset) when asset in [:css, :js] do
hash = Phoenix.LiveDashboard.Assets.current_hash(asset)

if function_exported?(conn.private.phoenix_router, :__live_dashboard_prefix__, 0) do
prefix = conn.private.phoenix_router.__live_dashboard_prefix__()

Phoenix.VerifiedRoutes.unverified_path(
conn,
conn.private.phoenix_router,
"#{prefix}/#{asset}-#{hash}"
)
else
apply(
conn.private.phoenix_router.__helpers__(),
:live_dashboard_asset_path,
[conn, asset, hash]
)
end
end
end
4 changes: 2 additions & 2 deletions lib/phoenix/live_dashboard/layouts/dash.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no, user-scalable=no"/>
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<title><%= assigns[:page_title] || "Phoenix LiveDashboard" %></title>
<style nonce={csp_nonce(@conn, :style)}><%= raw(render("app.css", %{})) %></style>
<link rel="stylesheet" nonce={csp_nonce(@conn, :style)} href={asset_path(@conn, :css)}>
<script nonce={csp_nonce(@conn, :script)} src={asset_path(@conn, :js)} defer></script>
</head>
<body>
<div class="d-flex flex-column align-items-stretch layout-wrapper">
Expand All @@ -20,6 +21,5 @@
<a href="https://dockyard.com/" target="_blank"><img nonce={csp_nonce(@conn, :img)} src="data:image/png;base64, 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">DockYard</a>
</footer>
</div>
<script nonce={csp_nonce(@conn, :script)}><%= raw(render("app.js", %{})) %></script>
</body>
</html>
5 changes: 5 additions & 0 deletions lib/phoenix/live_dashboard/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,14 @@ defmodule Phoenix.LiveDashboard.Router do
{session_name, session_opts, route_opts} =
Phoenix.LiveDashboard.Router.__options__(opts)

import Phoenix.Router, only: [get: 4]
import Phoenix.LiveView.Router, only: [live: 4, live_session: 3]

live_session session_name, session_opts do
# LiveDashboard assets
get "/css-:md5", Phoenix.LiveDashboard.Assets, :css, as: :live_dashboard_asset
get "/js-:md5", Phoenix.LiveDashboard.Assets, :js, as: :live_dashboard_asset

# All helpers are public contracts and cannot be changed
live "/", Phoenix.LiveDashboard.PageLive, :home, route_opts
live "/:page", Phoenix.LiveDashboard.PageLive, :page, route_opts
Expand Down

0 comments on commit d07bde5

Please sign in to comment.