"""
end
end
PhoenixPlayground.start(live: TimelineLive)
================================================
FILE: examples/demo_live_test.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
Count: {@count}
"""
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end
Logger.configure(level: :warning)
ExUnit.start()
defmodule DemoLiveTest do
use ExUnit.Case
use PhoenixPlayground.Test, live: DemoLive
test "it works" do
{:ok, view, html} = live(build_conn(), "/")
assert html =~ "Count: 0"
assert render_click(view, :inc, %{}) =~ "Count: 1"
assert render_click(view, :inc, %{}) =~ "Count: 2"
end
end
================================================
FILE: examples/demo_plug.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
PhoenixPlayground.start(
plug: fn conn ->
Plug.Conn.send_resp(conn, 200, "Hello!")
end
)
================================================
FILE: examples/demo_router.exs
================================================
#!/usr/bin/env elixir
Mix.install([
{:phoenix_playground, "~> 0.1.8"}
])
defmodule CounterLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
{@count}
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, count: socket.assigns.count + 1)}
end
end
defmodule DemoRouter do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
live "/", CounterLive
end
end
PhoenixPlayground.start(plug: DemoRouter)
================================================
FILE: lib/phoenix_playground/application.ex
================================================
defmodule PhoenixPlayground.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
# suppress Phoenix warning about no endpoint configuration
Application.put_env(:phoenix_playground, PhoenixPlayground.Endpoint, [])
Application.put_env(:phoenix_live_view, :debug_heex_annotations, true)
Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__)
end
end
================================================
FILE: lib/phoenix_playground/code_reloader.ex
================================================
defmodule PhoenixPlayground.CodeReloader do
@moduledoc false
def reload(_endpoint, _options \\ []) do
if path = Application.get_env(:phoenix_playground, :file) do
case File.read(path) do
{:ok, contents} ->
old = Code.get_compiler_option(:ignore_module_conflict) == true
Code.put_compiler_option(:ignore_module_conflict, true)
Code.eval_string(contents, [], file: path)
Code.put_compiler_option(:ignore_module_conflict, old)
# ignore fs errors. (Seems like saving file in vim sometimes make it temp dissapear?)
{:error, _reason} ->
:ok
end
else
# in Livebook, path is nil
:ok
end
end
end
================================================
FILE: lib/phoenix_playground/endpoint.ex
================================================
defmodule PhoenixPlayground.Endpoint do
@moduledoc false
use Phoenix.Endpoint, otp_app: :phoenix_playground
@signing_salt "ll+Leuc4"
@session_options [
store: :cookie,
key: "_phoenix_playground_key",
signing_salt: @signing_salt,
same_site: "Lax",
# 14 days
max_age: 14 * 24 * 60 * 60
]
socket "/live", Phoenix.LiveView.Socket
plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix"
plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view"
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug :run_live_reload
# TODO:
# plug Phoenix.Ecto.CheckRepoStatus, otp_app: :phoenix_playground
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.Session, @session_options
plug PhoenixPlayground.Router
defp run_live_reload(conn, _options) do
if Application.get_env(:phoenix_playground, :live_reload) do
conn
|> Phoenix.LiveReloader.call([])
|> Phoenix.CodeReloader.call(reloader: &PhoenixPlayground.CodeReloader.reload/2)
else
conn
end
end
# See https://github.com/phoenixframework/phoenix/blob/v1.7.14/lib/phoenix/endpoint.ex#L484:L490
@plug_debugger [
banner: {Phoenix.Endpoint.RenderErrors, :__debugger_banner__, []},
style: [
primary: "#EB532D",
logo:
"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
]
]
# See https://github.com/elixir-plug/plug/blob/v1.16.1/lib/plug/debugger.ex#L129:L146
def call(conn, opts) do
if Application.get_env(:phoenix_playground, :debug_errors, false) do
try do
case conn do
%Plug.Conn{path_info: ["__plug__", "debugger", "action"], method: "POST"} ->
Plug.Debugger.run_action(conn)
%Plug.Conn{} ->
super(conn, opts)
end
rescue
e in Plug.Conn.WrapperError ->
%{conn: conn, kind: kind, reason: reason, stack: stack} = e
Plug.Debugger.__catch__(conn, kind, reason, stack, @plug_debugger)
catch
kind, reason ->
Plug.Debugger.__catch__(conn, kind, reason, __STACKTRACE__, @plug_debugger)
end
else
super(conn, opts)
end
end
end
================================================
FILE: lib/phoenix_playground/error_view.ex
================================================
defmodule PhoenixPlayground.ErrorView do
@moduledoc false
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
================================================
FILE: lib/phoenix_playground/layout.ex
================================================
defmodule PhoenixPlayground.Layout do
@moduledoc """
Built-in layout.
This is the layout used by `PhoenixPlayground.start/1` with option `:live` or `:controller`
To customize page title, set `:page_title` assign.
You can customize LiveSocket hooks and uploaders by changing `window.hooks` and `window.uploaders`.
"""
use Phoenix.Component
@doc false
def render(template, assigns)
def render("root.html", assigns) do
~H"""
<.live_title>
<%= assigns[:page_title] || "Phoenix Playground" %>
<%= @inner_content %>
"""
end
end
================================================
FILE: lib/phoenix_playground/router.ex
================================================
defmodule PhoenixPlayground.Router do
@moduledoc false
@behaviour Plug
@impl true
def init([]) do
[]
end
@impl true
def call(conn, []) do
endpoint = conn.private.phoenix_endpoint
options = endpoint.config(:phoenix_playground)
cond do
options[:live] ->
PhoenixPlayground.Router.LiveRouter.call(conn, [])
controller = options[:controller] ->
conn = put_in(conn.private[:phoenix_playground_controller], controller)
PhoenixPlayground.Router.ControllerRouter.call(conn, [])
options[:plug] ->
# always fetch plug from app env to allow code reloading anonymous functions
plug = Application.fetch_env!(:phoenix_playground, :plug)
case plug do
module when is_atom(module) ->
module.call(conn, module.init([]))
{module, options} when is_atom(module) ->
module.call(conn, module.init(options))
fun when is_function(fun, 1) ->
fun.(conn)
fun when is_function(fun, 2) ->
fun.(conn, [])
end
true ->
raise ArgumentError, "expected :live, :controller, or :plug, got: #{inspect(options)}"
end
end
defmodule ControllerRouter do
@moduledoc false
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html", "json"]
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
get "/", PhoenixPlayground.Router.DelegateController, :index
end
end
defmodule DelegateController do
@moduledoc false
def init(options) do
options
end
def call(conn, options) do
controller = conn.private.phoenix_playground_controller
controller.call(conn, controller.init(options))
end
end
defmodule LiveRouter do
@moduledoc false
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
plug :put_secure_browser_headers
end
scope "/" do
pipe_through :browser
live "/", PhoenixPlayground.Router.DelegateLive, :index
end
end
defmodule DelegateLive do
@moduledoc false
use Phoenix.LiveView
@impl true
def mount(params, session, socket) do
module().mount(params, session, socket)
end
@impl true
def render(assigns) do
module().render(assigns)
end
@impl true
def handle_params(unsigned_params, uri, socket) do
if function_exported?(module(), :handle_params, 3) do
module().handle_params(unsigned_params, uri, socket)
else
{:noreply, socket}
end
end
@impl true
def handle_event(event, params, socket) do
module().handle_event(event, params, socket)
end
@impl true
def handle_info(message, socket) do
module().handle_info(message, socket)
end
@impl true
def handle_async(message, result, socket) do
module().handle_async(message, result, socket)
end
def module do
Application.fetch_env!(:phoenix_playground, :live)
end
end
end
================================================
FILE: lib/phoenix_playground/test.ex
================================================
defmodule PhoenixPlayground.Test do
@moduledoc ~S'''
Conveniences for testing single-file Phoenix apps.
## Examples
Mix.install([
{:phoenix_playground, "~> 0.1.0"}
])
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
Count: <%= @count %>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end
Logger.configure(level: :info)
ExUnit.start()
defmodule DemoLiveTest do
use ExUnit.Case
use PhoenixPlayground.Test, live: DemoLive
test "it works" do
{:ok, view, html} = live(build_conn(), "/")
assert html =~ "Count: 0"
assert render_click(view, :inc, %{}) =~ "Count: 1"
assert render_click(view, :inc, %{}) =~ "Count: 2"
end
end
'''
@secret_key_base String.duplicate("a", 32)
@signing_salt "ll+Leuc4"
defmacro __using__(options) do
options =
Keyword.validate!(options, [
:live,
:controller,
endpoint: PhoenixPlayground.Endpoint
])
imports =
if options[:live] do
quote do
import(Phoenix.LiveViewTest)
end
end
quote do
import Phoenix.ConnTest
unquote(imports)
@endpoint unquote(options[:endpoint])
setup do
options = unquote(options)
if live = options[:live] do
Application.put_env(:phoenix_playground, :live, live)
end
start_supervised!(
{@endpoint,
secret_key_base: unquote(@secret_key_base),
live_view: [signing_salt: unquote(@signing_salt)],
phoenix_playground: options}
)
:ok
end
end
end
end
================================================
FILE: lib/phoenix_playground.ex
================================================
defmodule PhoenixPlayground do
@moduledoc """
Phoenix Playground makes it easy to create single-file Phoenix applications.
"""
require Logger
@secret_key_base [
then(:inet.gethostname(), fn {:ok, host} -> host end),
System.get_env("USER", ""),
System.version(),
:erlang.system_info(:version),
:erlang.system_info(:system_architecture)
]
|> :erlang.md5()
|> Base.url_encode64(padding: false)
@signing_salt "ll+Leuc4"
@doc """
Starts Phoenix Playground.
This functions starts Phoenix with a LiveView (`:live`), a controller (`:controller`),
or a router (`:router`).
## Options
* `:live` - a LiveView module.
Phoenix Playground adds the following conveniences to the given LiveView:
* a `:page_title` assign can be used to customise `` `` tag.
* a `window.hooks` object can be used to register hooks. See `examples/demo_hooks.exs`.
This LiveView will automatically use `PhoenixPlayground.Layout`.
* `:controller` - a controller module.
This controller will automatically use `PhoenixPlayground.Layout`.
* `:plug` - a plug.
* `:port` - port to listen on, defaults to: `4000`.
* `:endpoint_options` - additional Phoenix endpoint options, defaults to `[]`.
* `:debug_errors` - whether to use Phoenix error debugger, defaults to `true`.
* `:open_browser` - whether to open the browser on start, defaults to `true`.
* `:child_specs` - child specs to run in Phoenix Playground supervision tree. The playground
Phoenix endpoint is automatically added and is always the last child spec. Defaults to `[]`.
"""
def start(options) do
options =
case Application.fetch_env(:phoenix_playground, :file) do
{:ok, file} ->
Keyword.put(options, :file, file)
:error ->
file = get_file()
Application.put_env(:phoenix_playground, :file, file)
Keyword.put(options, :file, file)
end
options =
if router = options[:router] do
IO.warn("setting :router is deprecated in favour of setting :plug")
options
|> Keyword.delete(:router)
|> Keyword.put(:plug, router)
else
options
end
if plug = options[:plug] do
Application.put_env(:phoenix_playground, :plug, plug)
end
case Supervisor.start_child(PhoenixPlayground.Application, {PhoenixPlayground, options}) do
{:ok, pid} ->
{:ok, pid}
{:error, {:already_started, pid}} ->
# In Livebook, path is nil. Livebook does its own code reloading, this just refreshes LV.
unless options[:file] do
Phoenix.PubSub.broadcast(
PhoenixPlayground.PubSub,
"live_view",
{:phoenix_live_reload, :live_view, nil}
)
end
{:ok, pid}
{:error, {{:EXIT, {exception, _trace}}, _child_info}} ->
raise exception
other ->
other
end
end
defp get_file do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
get_file(stacktrace)
end
defp get_file([
{PhoenixPlayground, :start, 1, _},
{_, :__FILE__, 1, meta} | _
]) do
Path.expand(Keyword.fetch!(meta, :file))
end
defp get_file([_ | rest]) do
get_file(rest)
end
defp get_file([]) do
nil
end
@doc false
def child_spec(options) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [options]},
type: :supervisor
}
end
@doc false
def start_link(options) do
options =
Keyword.validate!(options, [
:live,
:controller,
:plug,
:file,
port: 4000,
host: "localhost",
live_reload: true,
ip: {127, 0, 0, 1},
endpoint: PhoenixPlayground.Endpoint,
endpoint_options: [],
debug_errors: true,
open_browser: true,
child_specs: []
])
child_specs = Keyword.fetch!(options, :child_specs)
path = options[:file]
Application.put_env(:phoenix_playground, :file, path)
if options[:debug_errors] do
Application.put_env(:phoenix_playground, :debug_errors, true)
end
if options[:open_browser] do
Application.put_env(:phoenix, :browser_open, true)
end
if live = options[:live] do
Application.put_env(:phoenix_playground, :live, live)
end
# in Livebook, path is nil
if path do
Application.put_env(:phoenix_live_reload, :dirs, [
Path.dirname(path)
])
end
Application.put_env(:phoenix_playground, :live_reload, options[:live_reload])
if options[:live_reload] do
# PhoenixLiveReload requires Hex
{:ok, _} = Application.ensure_all_started(:hex)
{:ok, _} = Application.ensure_all_started(:phoenix_live_reload)
end
live_reload_options =
if options[:live] &&
unquote(
# TODO: remove when depending on LV 1.0.
Version.match?(
to_string(Application.spec(:phoenix_live_view, :vsn)),
">= 1.0.0-rc.1"
)
) do
[
# In Livebook, path is nil
patterns:
if path do
# TODO: this should not be needed given we set :phoenix_live_reload :dirs
[~r/^does_not_matter$/]
else
[]
end,
notify: [
live_view: [
~r/^#{path}$/
]
]
]
else
[
patterns: [
~r/^#{path}$/
]
]
end
lr_options =
if options[:live_reload] do
[
web_console_logger: true,
debounce: 100,
reloader: {PhoenixPlayground.CodeReloader, :reload, []}
] ++ live_reload_options
else
[
notify: [
live_view: []
],
reloader: {PhoenixPlayground.CodeReloader, :reload, []}
]
end
# Some compile-time options are defined at the top of lib/phoenix_playground/endpoint.ex
endpoint_options =
[
adapter: Bandit.PhoenixAdapter,
http: [ip: options[:ip], port: options[:port]],
url: [host: options[:host]],
server: !!options[:port],
live_view: [signing_salt: @signing_salt],
secret_key_base: @secret_key_base,
pubsub_server: PhoenixPlayground.PubSub,
live_reload: lr_options,
phoenix_playground: Keyword.take(options, [:live, :controller, :plug])
]
|> Keyword.merge(Keyword.get(options, :endpoint_options, []))
children =
child_specs ++
[
{Phoenix.PubSub, name: PhoenixPlayground.PubSub},
{Keyword.fetch!(options, :endpoint), endpoint_options}
]
System.no_halt(true)
case Supervisor.start_link(children, strategy: :one_for_one) do
{:ok, pid} ->
{:ok, pid}
{:error, reason} ->
Logger.error(Exception.format_exit(reason))
{:error, reason}
end
end
end
================================================
FILE: mix.exs
================================================
defmodule PhoenixPlayground.MixProject do
use Mix.Project
@source_url "https://github.com/phoenix-playground/phoenix_playground"
@version "0.1.8"
def project do
[
app: :phoenix_playground,
version: @version,
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
package: package(),
aliases: aliases(),
docs: docs(),
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {PhoenixPlayground.Application, []}
]
end
defp package do
[
description: "Phoenix Playground makes it easy to create single-file Phoenix applications.",
licenses: ["Apache-2.0"],
links: %{
"GitHub" => @source_url,
"Changelog" => "https://hexdocs.pm/phoenix_playground/changelog.html"
}
]
end
defp aliases do
[
docs: ["docs.setup", "docs", "docs.teardown"],
"docs.setup": &docs_setup/1,
"docs.teardown": &docs_teardown/1
]
end
defp docs do
[
main: "readme",
source_url: @source_url,
source_ref: "v#{@version}",
extras: [
"README.md",
"CHANGELOG.md"
],
assets: %{"assets" => "assets"},
skip_code_autolink_to: [
"PhoenixPlayground.start_link/1"
]
]
end
defp docs_setup(_) do
tmp = "#{__DIR__}/tmp"
File.mkdir_p!(tmp)
File.cp!("#{__DIR__}/README.md", "#{tmp}/README.md")
File.write!("#{__DIR__}/README.md", [
File.read!("#{__DIR__}/README.md"),
"\n\n",
for path <- Path.wildcard("examples/*.exs") do
"[`#{path}`]: https://github.com/phoenix-playground/phoenix_playground/blob/v#{@version}/#{path}\n"
end
])
end
defp docs_teardown(_) do
tmp = "#{__DIR__}/tmp"
File.rename!("#{tmp}/README.md", "#{__DIR__}/README.md")
end
defp deps do
[
{:phoenix, "~> 1.0"},
{:phoenix_live_view, "~> 1.1"},
{:bandit, "~> 1.0"},
{:jason, "~> 1.0"},
{:lazy_html, "~> 0.1.3"},
# Don't start phoenix_live_reload in case someone just wants PhoenixPlayground.Test.
# Instead, manually start it in PhoenixPlayground.start_link/1.
{:phoenix_live_reload, "~> 1.5", runtime: false},
# dev-only
{:ex_doc, ">= 0.0.0", only: :dev}
]
end
end
================================================
FILE: test/examples_test.exs
================================================
defmodule Examples do
def run(path) do
{:ok, quoted} =
path
|> File.read!()
|> Code.string_to_quoted()
quoted
|> remove_mix_install()
|> Code.eval_quoted([], file: path)
end
defp remove_mix_install(quoted) do
Macro.prewalk(quoted, fn
{{:., _, [{:__aliases__, _, [:Mix]}, :install]}, _, _} ->
nil
ast ->
ast
end)
end
end
Examples.run("#{__DIR__}/../examples/demo_controller_test.exs")
Examples.run("#{__DIR__}/../examples/demo_live_test.exs")
================================================
FILE: test/phoenix_playground_test.exs
================================================
defmodule PhoenixPlaygroundTest do
use ExUnit.Case, async: true
end
================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()