Skip to content

Commit f434a13

Browse files
authored
Merge pull request #147 from phoenixframework/cm-web-logger
Add web console logger and open file from client support
2 parents 13d89ca + 80e6b5d commit f434a13

File tree

9 files changed

+388
-52
lines changed

9 files changed

+388
-52
lines changed

README.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ You can use `phoenix_live_reload` in your projects by adding it to your `mix.exs
66

77
```elixir
88
def deps do
9-
[{:phoenix_live_reload, "~> 1.3"}]
9+
[{:phoenix_live_reload, "~> 1.5"}]
1010
end
1111
```
1212

@@ -23,6 +23,67 @@ config :my_app, MyAppWeb.Endpoint,
2323

2424
The default interval is 100ms.
2525

26+
27+
## Streaming serving logs to the web console
28+
29+
Streaming server logs that you see in the terminal when running `mix phx.server` can be useful to have on the client during development, especially when debugging with SPA fetch callbacks, GraphQL queries, or LiveView actions in the browsers web console. You can enable log streaming to collocate client and server logs in the web console with the `web_console_logger` configuration in your `config/dev.exs`:
30+
31+
```elixir
32+
config :my_app, MyAppWeb.Endpoint,
33+
live_reload: [
34+
interval: 1000,
35+
patterns: [...],
36+
web_console_logger: true
37+
]
38+
```
39+
40+
Next, you'll need to listen for the `"phx:live_reload:attached"` event and enable client logging by calling the reloader's `enableServerLogs()` function, for example:
41+
42+
```javascript
43+
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
44+
// enable server log streaming to client.
45+
// disable with reloader.disableServerLogs()
46+
reloader.enableServerLogs()
47+
})
48+
```
49+
50+
## Jumping to HEEx function definitions
51+
52+
Many times it's useful to inspect the HTML DOM tree to find where markup is being rendered from within your application. HEEx supports annotating rendered HTML with HTML comments that give you the file/line of a HEEx function component and caller. `:phoenix_live_reload` will look for the `PLUG_EDITOR` environment export (used by the plug debugger page to link to source code) to launch a configured URL of your choice to open your code editor to the file-line of the HTML annotation. For example, the following export on your system would open vscode at the correct file/line:
53+
54+
```
55+
export PLUG_EDITOR="vscode://file/__FILE__:__LINE__"
56+
```
57+
58+
The `vscode://` protocol URL will open vscode with placeholders of `__FILE__:__LINE__` substited at runtime. Check your editor's documentation on protocol URL support. To open your configured editor URL when an element is clicked, say with alt-click, you can wire up an event listener within your `"phx:live_reload:attached"` callback and make use of the reloader's `openEditorAtCaller` and `openEditorAtDef` functions, passing the event target as the DOM node to reference for HEEx file:line annotation information. For example:
59+
60+
```javascript
61+
window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
62+
// Enable server log streaming to client. Disable with reloader.disableServerLogs()
63+
reloader.enableServerLogs()
64+
65+
// Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
66+
//
67+
// * click with "c" key pressed to open at caller location
68+
// * click with "d" key pressed to open at function component definition location
69+
let keyDown
70+
window.addEventListener("keydown", e => keyDown = e.key)
71+
window.addEventListener("keyup", e => keyDown = null)
72+
window.addEventListener("click", e => {
73+
if(keyDown === "c"){
74+
e.preventDefault()
75+
e.stopImmediatePropagation()
76+
reloader.openEditorAtCaller(e.target)
77+
} else if(keyDown === "d"){
78+
e.preventDefault()
79+
e.stopImmediatePropagation()
80+
reloader.openEditorAtDef(e.target)
81+
}
82+
}, true)
83+
window.liveReloader = reloader
84+
})
85+
```
86+
2687
## Backends
2788

2889
This project uses [`FileSystem`](https://github.com/falood/file_system) as a dependency to watch your filesystem whenever there is a change and it supports the following operating systems:

lib/phoenix_live_reload/application.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ defmodule Phoenix.LiveReloader.Application do
22
use Application
33
require Logger
44

5+
alias Phoenix.LiveReloader.WebConsoleLogger
6+
57
def start(_type, _args) do
6-
children = [%{id: __MODULE__, start: {__MODULE__, :start_link, []}}]
8+
# note we always attach and start the logger as :phoenix_live_reload should only
9+
# be started in dev via user's `only: :dev` entry.
10+
WebConsoleLogger.attach_logger()
11+
12+
children = [
13+
WebConsoleLogger,
14+
%{id: __MODULE__, start: {__MODULE__, :start_link, []}}
15+
]
16+
717
Supervisor.start_link(children, strategy: :one_for_one)
818
end
919

lib/phoenix_live_reload/channel.ex

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,30 @@ defmodule Phoenix.LiveReloader.Channel do
55
use Phoenix.Channel
66
require Logger
77

8+
alias Phoenix.LiveReloader.WebConsoleLogger
9+
10+
@logs :logs
11+
812
def join("phoenix:live_reload", _msg, socket) do
913
{:ok, _} = Application.ensure_all_started(:phoenix_live_reload)
1014

1115
if Process.whereis(:phoenix_live_reload_file_monitor) do
1216
FileSystem.subscribe(:phoenix_live_reload_file_monitor)
17+
18+
if web_console_logger_enabled?(socket) do
19+
WebConsoleLogger.subscribe(@logs)
20+
end
21+
1322
config = socket.endpoint.config(:live_reload)
1423

1524
socket =
1625
socket
1726
|> assign(:patterns, config[:patterns] || [])
1827
|> assign(:debounce, config[:debounce] || 0)
1928
|> assign(:notify_patterns, config[:notify] || [])
29+
|> assign(:deps_paths, deps_paths())
2030

21-
{:ok, socket}
31+
{:ok, join_info(), socket}
2232
else
2333
{:error, %{message: "live reload backend not running"}}
2434
end
@@ -28,7 +38,7 @@ defmodule Phoenix.LiveReloader.Channel do
2838
%{
2939
patterns: patterns,
3040
debounce: debounce,
31-
notify_patterns: notify_patterns,
41+
notify_patterns: notify_patterns
3242
} = socket.assigns
3343

3444
if matches_any_pattern?(path, patterns) do
@@ -47,13 +57,34 @@ defmodule Phoenix.LiveReloader.Channel do
4757
socket.pubsub_server,
4858
to_string(topic),
4959
{:phoenix_live_reload, topic, path}
50-
)
60+
)
5161
end
5262
end
5363

5464
{:noreply, socket}
5565
end
5666

67+
def handle_info({@logs, %{level: level, msg: msg, meta: meta}}, socket) do
68+
push(socket, "log", %{
69+
level: to_string(level),
70+
msg: msg,
71+
file: meta[:file],
72+
line: meta[:line]
73+
})
74+
75+
{:noreply, socket}
76+
end
77+
78+
def handle_in("full_path", %{"rel_path" => rel_path, "app" => app}, socket) do
79+
case socket.assigns.deps_paths do
80+
%{^app => dep_path} ->
81+
{:reply, {:ok, %{full_path: Path.join(dep_path, rel_path)}}, socket}
82+
83+
%{} ->
84+
{:reply, {:ok, %{full_path: Path.join(File.cwd!(), rel_path)}}, socket}
85+
end
86+
end
87+
5788
defp debounce(0, _exts, _patterns), do: []
5889

5990
defp debounce(time, exts, patterns) when is_integer(time) and time > 0 do
@@ -87,4 +118,24 @@ defmodule Phoenix.LiveReloader.Channel do
87118

88119
defp remove_leading_dot("." <> rest), do: rest
89120
defp remove_leading_dot(rest), do: rest
121+
122+
defp web_console_logger_enabled?(socket) do
123+
socket.endpoint.config(:live_reload)[:web_console_logger] == true
124+
end
125+
126+
defp join_info do
127+
if url = System.get_env("PLUG_EDITOR") do
128+
%{editor_url: url}
129+
else
130+
%{}
131+
end
132+
end
133+
134+
defp deps_paths do
135+
if Code.loaded?(Mix.Project) do
136+
for {app, path} <- Mix.Project.deps_paths(), into: %{}, do: {to_string(app), path}
137+
else
138+
%{}
139+
end
140+
end
90141
end

lib/phoenix_live_reload/live_reloader.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ defmodule Phoenix.LiveReloader do
7070
Useful when class names are determined at runtime, for example when
7171
working with CSS modules. Defaults to false.
7272
73+
* `:web_console_logger` - If true, the live reloader will log messages
74+
to the web console in your browser. Defaults to false.
75+
*Note*: your appplication javascript bundle will need to enable logs.
76+
See the README for more information.
77+
7378
In an umbrella app, if you want to enable live reloading based on code
7479
changes in sibling applications, set the `reloadable_apps` option on your
7580
endpoint to ensure the code will be recompiled, then add the dirs to
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
defmodule Phoenix.LiveReloader.WebConsoleLogger do
2+
@moduledoc false
3+
4+
@registry Phoenix.LiveReloader.WebConsoleLoggerRegistry
5+
6+
def attach_logger do
7+
if function_exported?(Logger, :default_formatter, 0) do
8+
:ok =
9+
:logger.add_handler(__MODULE__, __MODULE__, %{
10+
formatter: Logger.default_formatter(colors: [enabled: false])
11+
})
12+
end
13+
end
14+
15+
def child_spec(_args) do
16+
Registry.child_spec(name: @registry, keys: :duplicate)
17+
end
18+
19+
def subscribe(prefix) do
20+
{:ok, _} = Registry.register(@registry, :all, prefix)
21+
:ok
22+
end
23+
24+
# Erlang/OTP log handler
25+
def log(%{meta: meta, level: level} = event, config) do
26+
%{formatter: {formatter_mod, formatter_config}} = config
27+
iodata = formatter_mod.format(event, formatter_config)
28+
msg = IO.iodata_to_binary(iodata)
29+
30+
Registry.dispatch(@registry, :all, fn entries ->
31+
for {pid, prefix} <- entries,
32+
do: send(pid, {prefix, %{level: level, msg: msg, meta: meta}})
33+
end)
34+
end
35+
end

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
%{
22
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
33
"ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"},
4-
"file_system": {:hex, :file_system, "0.2.1", "c4bec8f187d2aabace4beb890f0d4e468f65ca051593db768e533a274d0df587", [:mix], [], "hexpm", "ba49dc1647b30a1ae0ab320198b82dbe41f594c41eaaabd7a2ba14ac38faa578"},
4+
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
55
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
66
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
77
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},

0 commit comments

Comments
 (0)