main 681a0eda7fea cached
27 files
43.0 KB
14.2k tokens
87 symbols
1 requests
Download .txt
Repository: phoenix-playground/phoenix_playground
Branch: main
Commit: 681a0eda7fea
Files: 27
Total size: 43.0 KB

Directory structure:
gitextract__377zb3b/

├── .formatter.exs
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── examples/
│   ├── demo_controller.exs
│   ├── demo_controller_test.exs
│   ├── demo_endpoint.exs
│   ├── demo_hooks.exs
│   ├── demo_live.exs
│   ├── demo_live.livemd
│   ├── demo_live_pubsub.exs
│   ├── demo_live_test.exs
│   ├── demo_plug.exs
│   └── demo_router.exs
├── lib/
│   ├── phoenix_playground/
│   │   ├── application.ex
│   │   ├── code_reloader.ex
│   │   ├── endpoint.ex
│   │   ├── error_view.ex
│   │   ├── layout.ex
│   │   ├── router.ex
│   │   └── test.ex
│   └── phoenix_playground.ex
├── mix.exs
└── test/
    ├── examples_test.exs
    ├── phoenix_playground_test.exs
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
[
  import_deps: [:phoenix],
  inputs: ["{mix,.formatter}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-22.04
    env:
      MIX_ENV: test
    strategy:
      fail-fast: false
      matrix:
        include:
          - pair:
              elixir: "1.15"
              otp: "24.3.4.10"
          - pair:
              elixir: "1.18"
              otp: "27.3.4.2"
            lint: lint
    steps:
      - uses: actions/checkout@v4

      - uses: erlef/setup-beam@main
        with:
          otp-version: ${{ matrix.pair.otp }}
          elixir-version: ${{ matrix.pair.elixir }}
          version-type: strict

      - uses: actions/cache@v4
        with:
          path: deps
          key: mix-deps-${{ hashFiles('**/mix.lock') }}

      - run: mix deps.get --check-locked

      - run: mix format --check-formatted
        if: ${{ matrix.lint }}

      - run: mix deps.unlock --check-unused
        if: ${{ matrix.lint }}

      - run: mix deps.compile

      - run: mix compile --warnings-as-errors
        if: ${{ matrix.lint }}

      - run: mix test
        if: ${{ ! matrix.lint }}

      - run: mix test --warnings-as-errors
        if: ${{ matrix.lint }}


================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
phoenix_playground-*.tar

# Temporary files, for example, from tests.
/tmp/


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## v0.1.8 (2025-07-30)

  * Use Phoenix LiveView v1.1.

## v0.1.7 (2024-09-24)

  * Delegate LiveView `handle_async/3`.

## v0.1.6 (2024-09-03)

  * Always use PhoenixPlayground.CodeReloader

  * Add blank favicon to layout

  * Add `:endpoint` option

## v0.1.5 (2024-08-14)

  * Fix live reloading.

  * Add `:debug_errors` option.

  * Document `PhoenixPlayground.Layout`.

  * Add `examples/demo_router.exs`.

## v0.1.4 (2024-07-18)

  * Delegate LiveView `handle_params/3`.

  * Add support for LiveView hooks.

  * Add support for LiveView uploaders.

  * Add support for `:page_title` assign.

  * Add `:endpoint_options` option

## v0.1.3 (2024-05-16)

  * Use state-preserving live reload. (Requires Phoenix LiveView 1.0.0-rc.1+.)

  * Display errors using `Plug.Debugger`.

## v0.1.2 (2024-04-29)

  * Deprecate setting `:router` in favour of `:plug`.

## v0.1.1 (2024-04-23)

  * Use local javascript assets.

  * Update secret key base.

  * Replace `PhoenixPlayground.start_link/1` with `start/1`.

  * Fix opening up component definition/caller: set `:debug_heex_annotations` on application boot.

  * Add `:child_specs` option to specify additional processes to run in the supervision tree.

  * Prevent VM from halting when playground is started.

## v0.1.0 (2024-04-18)

  * Initial release.


================================================
FILE: README.md
================================================
# Phoenix Playground

[![CI](https://github.com/phoenix-playground/phoenix_playground/actions/workflows/ci.yml/badge.svg)](https://github.com/phoenix-playground/phoenix_playground/actions/workflows/ci.yml)
[![Version](https://img.shields.io/hexpm/v/phoenix_playground.svg)](https://hex.pm/packages/phoenix_playground)
[![Hex Docs](https://img.shields.io/badge/documentation-gray.svg)](https://hexdocs.pm/phoenix_playground)

Phoenix Playground makes it easy to create single-file [Phoenix](https://www.phoenixframework.org) applications.

## Examples

[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fphoenix-playground%2Fphoenix_playground%2Fblob%2Fmain%2Fexamples%2Fdemo_live.livemd)

Create a `demo_live.exs` file:

```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"""
    <span>{@count}</span>
    <button phx-click="inc">+</button>

    <style type="text/css">
      body { padding: 1em; }
    </style>
    """
  end

  def handle_event("inc", _params, socket) do
    {:noreply, assign(socket, count: socket.assigns.count + 1)}
  end
end

PhoenixPlayground.start(live: DemoLive)
```

and run it:

```
$ iex demo_live.exs
```

<img width="1195" alt="image" src="assets/demo.png">


See more examples below:

  * [`examples/demo_live.exs`]
  * [`examples/demo_live_test.exs`]
  * [`examples/demo_controller.exs`]
  * [`examples/demo_controller_test.exs`]
  * [`examples/demo_plug.exs`]
  * [`examples/demo_router.exs`]
  * [`examples/demo_hooks.exs`]
  * [`examples/demo_endpoint.exs`]
  * [`examples/demo_live_pubsub.exs`]

## License

Copyright (c) 2024 Wojtek Mach

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

[`examples/demo_live.exs`]: examples/demo_live.exs
[`examples/demo_live_test.exs`]: examples/demo_live_test.exs
[`examples/demo_controller.exs`]: examples/demo_controller.exs
[`examples/demo_controller_test.exs`]: examples/demo_controller_test.exs
[`examples/demo_plug.exs`]: examples/demo_plug.exs
[`examples/demo_router.exs`]: examples/demo_router.exs
[`examples/demo_hooks.exs`]: examples/demo_hooks.exs
[`examples/demo_endpoint.exs`]: examples/demo_endpoint.exs
[`examples/demo_live_pubsub.exs`]: examples/demo_live_pubsub.exs


================================================
FILE: examples/demo_controller.exs
================================================
#!/usr/bin/env elixir
Mix.install([
  {:phoenix_playground, "~> 0.1.8"}
])

defmodule DemoController do
  use Phoenix.Controller, formats: [:html]
  use Phoenix.Component
  plug :put_layout, false
  plug :put_view, __MODULE__

  def index(conn, params) do
    count =
      case Integer.parse(params["count"] || "") do
        {n, ""} -> n
        _ -> 0
      end

    render(conn, :index, count: count)
  end

  def index(assigns) do
    ~H"""
    <span>{@count}</span>
    <button onclick={"window.location.href=\"/?count=#{@count + 1}\""}>+</button>

    <style type="text/css">
      body { padding: 1em; }
    </style>
    """
  end
end

PhoenixPlayground.start(controller: DemoController)


================================================
FILE: examples/demo_controller_test.exs
================================================
#!/usr/bin/env elixir
Mix.install([
  {:phoenix_playground, "~> 0.1.8"}
])

defmodule DemoController do
  use Phoenix.Controller, formats: [:html]
  use Phoenix.Component
  plug :put_layout, false
  plug :put_view, __MODULE__

  def index(conn, params) do
    count =
      case Integer.parse(params["count"] || "") do
        {n, ""} -> n
        _ -> 0
      end

    render(conn, :index, count: count)
  end

  def index(assigns) do
    ~H"""
    <span>Count: {@count}</span>
    <button onclick={"window.location.href='/?count=#{@count + 1}'"}>+</button>
    """
  end
end

Logger.configure(level: :warning)
ExUnit.start()

defmodule DemoControllerTest do
  use ExUnit.Case
  use PhoenixPlayground.Test, controller: DemoController

  test "it works" do
    conn = get(build_conn(), "/")
    assert html_response(conn, 200) =~ "Count: 0"

    conn = get(build_conn(), "/?count=1")
    assert html_response(conn, 200) =~ "Count: 1"
  end
end


================================================
FILE: examples/demo_endpoint.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"""
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket, {})
      liveSocket.connect()
    </script>

    <span>Count: {@count}</span>
    <button phx-click="inc">+</button>
    """
  end

  def handle_event("inc", _params, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}
  end
end

defmodule Demo.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug :put_root_layout, html: {PhoenixPlayground.Layout, :root}
  end

  scope "/" do
    pipe_through :browser
    live "/", DemoLive
  end
end

defmodule Demo.Endpoint do
  use Phoenix.Endpoint, otp_app: :phoenix_playground
  plug Plug.Logger
  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 Phoenix.LiveReloader
  plug Phoenix.CodeReloader, reloader: &PhoenixPlayground.CodeReloader.reload/2
  plug Demo.Router
end

PhoenixPlayground.start(endpoint: Demo.Endpoint)


================================================
FILE: examples/demo_hooks.exs
================================================
#!/usr/bin/env elixir
Mix.install([
  {:phoenix_playground, "~> 0.1.8"}
])

defmodule DemoHooks do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <span id="hook" phx-hook="MyHook">LiveView</span>

    <script>
    window.hooks.MyHook = {
      mounted() {
        this.el.innerHTML += " rocks";
        setInterval(() => this.el.innerHTML += "!", 1000);
      }
    }
    </script>

    <style type="text/css">
      body { padding: 1em; }
    </style>
    """
  end
end

PhoenixPlayground.start(live: DemoHooks)


================================================
FILE: examples/demo_live.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"""
    <span>{@count}</span>
    <button phx-click="inc">+</button>

    <style type="text/css">
      body { padding: 1em; }
    </style>
    """
  end

  def handle_event("inc", _params, socket) do
    {:noreply, assign(socket, count: socket.assigns.count + 1)}
  end
end

PhoenixPlayground.start(live: DemoLive)


================================================
FILE: examples/demo_live.livemd
================================================
# Demo

```elixir
Mix.install([
  {:phoenix_playground, "~> 0.1.8"}
])
```

## Section

```elixir
defmodule DemoLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  def render(assigns) do
    ~H"""
    <span>{@count}</span>
    <button phx-click="inc">+</button>

    <style type="text/css">
      body { padding: 1em; }
      span { font-family: monospace; }
    </style>
    """
  end

  def handle_event("inc", _params, socket) do
    {:noreply, assign(socket, count: socket.assigns.count + 1)}
  end
end

PhoenixPlayground.start(live: DemoLive)
```


================================================
FILE: examples/demo_live_pubsub.exs
================================================
Mix.install([
  {:phoenix_playground, "~> 0.1.8"}
])

defmodule TimelineLive do
  use Phoenix.LiveView

  @topic "timeline"

  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(PhoenixPlayground.PubSub, @topic)
    end

    socket =
      socket
      |> assign(temporary_assigns: [form: nil])
      |> stream(:posts, [])
      |> assign(:form, to_form(%{"content" => ""}))

    {:ok, socket}
  end

  def handle_event("create_post", %{"content" => content}, socket) do
    post = %{
      id: System.unique_integer([:positive]),
      content: content,
      inserted_at: DateTime.utc_now()
    }

    Phoenix.PubSub.broadcast(PhoenixPlayground.PubSub, @topic, {:new_post, post})

    {:noreply,
     socket
     |> assign(:form, to_form(%{"content" => ""}))}
  end

  def handle_event("delete_post", %{"dom_id" => dom_id}, socket) do
    Phoenix.PubSub.broadcast(PhoenixPlayground.PubSub, @topic, {:delete_post, dom_id})

    {:noreply, socket}
  end

  def handle_info({:new_post, post}, socket) do
    {:noreply, stream_insert(socket, :posts, post, at: 0)}
  end

  def handle_info({:delete_post, dom_id}, socket) do
    {:noreply, stream_delete_by_dom_id(socket, :posts, dom_id)}
  end

  def render(assigns) do
    ~H"""
    <div class="timeline">
      <h1>Timeline</h1>

      <.form for={@form} phx-submit="create_post" id="post-form">
        <textarea name="content" placeholder="What's on your mind?" id="content" phx-hook="CmdEnterSubmit"><%= @form.params["content"] %></textarea>
        <button type="submit">Post</button>
      </.form>

      <div class="posts" phx-update="stream" id="posts">
        <div :for={{dom_id, post} <- @streams.posts} id={dom_id} class="post">
          <div class="post-content">
            <p><%= post.content %></p>
            <small>{DateTime.truncate(post.inserted_at, :second)}</small>
          </div>
          <button phx-click="delete_post" phx-value-dom_id={dom_id} class="delete-btn">Delete</button>
        </div>
      </div>
    </div>

    <script>
      window.hooks.CmdEnterSubmit = {
        mounted() {
        this.el.addEventListener("keydown", (e) => {
          if (e.metaKey && e.key === 'Enter') {
            this.el.form.dispatchEvent(
              new Event('submit', {bubbles: true, cancelable: true}));
          }
        })
        }
      }
    </script>

    <style>
      * {
        box-sizing: border-box;
      }

      .timeline {
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      }

      h1 {
        color: #2c3e50;
        font-size: 2.5em;
        margin-bottom: 1em;
        text-align: center;
      }

      .post {
        border: 1px solid #e1e8ed;
        border-radius: 12px;
        padding: 16px;
        margin: 16px 0;
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        background: white;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
        transition: all 0.2s ease;
      }

      .post:hover {
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        transform: translateY(-2px);
      }

      .post-content {
        flex: 1;
        margin-right: 16px;
      }

      .post-content p {
        color: #2c3e50;
        font-size: 1.1em;
        line-height: 1.5;
        margin: 0 0 8px 0;
        word-wrap: break-word;
        overflow-wrap: break-word;
      }

      .post-content small {
        color: #8795a1;
        font-size: 0.9em;
        display: block;
        word-wrap: break-word;
        overflow-wrap: break-word;
      }

      form {
        margin: 20px 0;
        background: white;
        padding: 20px;
        border-radius: 12px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
        width: 100%;
      }

      textarea {
        width: 100%;
        min-height: 3ch;
        margin-bottom: 16px;
        padding: 12px;
        border: 2px solid #e1e8ed;
        border-radius: 8px;
        font-size: 1em;
        resize: vertical;
        transition: border-color 0.2s ease;
      }

      textarea:focus {
        outline: none;
        border-color: #4a9eff;
      }

      button {
        padding: 10px 20px;
        background: #4a9eff;
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 1em;
        font-weight: 500;
        transition: all 0.2s ease;
      }

      button:hover {
        background: #357abd;
        transform: translateY(-1px);
      }

      .delete-btn {
        background: #ff4a4a;
        margin-left: 10px;
        padding: 8px 16px;
        font-size: 0.9em;
        opacity: 0.8;
      }

      .delete-btn:hover {
        background: #bd3535;
        opacity: 1;
      }

      body {
        background: #f8fafc;
        margin: 0;
        padding: 20px;
      }
    </style>
    """
  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"""
    <span>Count: {@count}</span>
    <button phx-click="inc">+</button>
    """
  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"""
    <span>{@count}</span>
    <button phx-click="inc">+</button>

    <style type="text/css">
      body { padding: 1em; }
    </style>
    """
  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"""
    <!DOCTYPE html>
    <html lang="en" class="h-full">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <.live_title>
          <%= assigns[:page_title] || "Phoenix Playground" %>
        </.live_title>
        <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wcAAgAB/ajN8ioAAAAASUVORK5CYII=">
      </head>
      <body>
        <script src="/assets/phoenix/phoenix.js"></script>
        <script src="/assets/phoenix_live_view/phoenix_live_view.js"></script>
        <script>
          // Set global hooks and uploaders objects to be used by the LiveSocket,
          // so they can be overwritten in user provided templates.
          window.hooks = {}
          window.uploaders = {}

          let liveSocket =
            new window.LiveView.LiveSocket(
              "/live",
              window.Phoenix.Socket,
              { hooks, uploaders }
            )
          liveSocket.connect()

          window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
            // Enable server log streaming to client. Disable with reloader.disableServerLogs()
            reloader.enableServerLogs()

            // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
            //
            //   * click with "c" key pressed to open at caller location
            //   * click with "d" key pressed to open at function component definition location
            let keyDown
            window.addEventListener("keydown", e => keyDown = e.key)
            window.addEventListener("keyup", e => keyDown = null)
            window.addEventListener("click", e => {
              if(keyDown === "c"){
                e.preventDefault()
                e.stopImmediatePropagation()
                reloader.openEditorAtCaller(e.target)
              } else if(keyDown === "d"){
                e.preventDefault()
                e.stopImmediatePropagation()
                reloader.openEditorAtDef(e.target)
              }
            }, true)

            window.liveReloader = reloader
          })
        </script>

        <%= @inner_content %>
      </body>
    </html>
    """
  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"""
          <span>Count: <%= @count %></span> <button phx-click="inc">+</button>
          """
        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 `<head>` `<title>` 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()
Download .txt
gitextract__377zb3b/

├── .formatter.exs
├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── examples/
│   ├── demo_controller.exs
│   ├── demo_controller_test.exs
│   ├── demo_endpoint.exs
│   ├── demo_hooks.exs
│   ├── demo_live.exs
│   ├── demo_live.livemd
│   ├── demo_live_pubsub.exs
│   ├── demo_live_test.exs
│   ├── demo_plug.exs
│   └── demo_router.exs
├── lib/
│   ├── phoenix_playground/
│   │   ├── application.ex
│   │   ├── code_reloader.ex
│   │   ├── endpoint.ex
│   │   ├── error_view.ex
│   │   ├── layout.ex
│   │   ├── router.ex
│   │   └── test.ex
│   └── phoenix_playground.ex
├── mix.exs
└── test/
    ├── examples_test.exs
    ├── phoenix_playground_test.exs
    └── test_helper.exs
Download .txt
SYMBOL INDEX (87 symbols across 19 files)

FILE: examples/demo_controller.exs
  class DemoController (line 6) | defmodule DemoController
    method index (line 12) | def index(conn, params) do
    method index (line 22) | def index(assigns) do

FILE: examples/demo_controller_test.exs
  class DemoController (line 6) | defmodule DemoController
    method index (line 12) | def index(conn, params) do
    method index (line 22) | def index(assigns) do
  class DemoControllerTest (line 33) | defmodule DemoControllerTest

FILE: examples/demo_endpoint.exs
  class DemoLive (line 6) | defmodule DemoLive
    method mount (line 9) | def mount(_params, _session, socket) do
    method render (line 13) | def render(assigns) do
    method handle_event (line 25) | def handle_event("inc", _params, socket) do
  class Demo.Router (line 30) | defmodule Demo.Router
  class Demo.Endpoint (line 44) | defmodule Demo.Endpoint

FILE: examples/demo_hooks.exs
  class DemoHooks (line 6) | defmodule DemoHooks
    method mount (line 9) | def mount(_params, _session, socket) do
    method render (line 13) | def render(assigns) do

FILE: examples/demo_live.exs
  class DemoLive (line 6) | defmodule DemoLive
    method mount (line 9) | def mount(_params, _session, socket) do
    method render (line 13) | def render(assigns) do
    method handle_event (line 24) | def handle_event("inc", _params, socket) do

FILE: examples/demo_live_pubsub.exs
  class TimelineLive (line 5) | defmodule TimelineLive
    method mount (line 10) | def mount(_params, _session, socket) do
    method handle_event (line 24) | def handle_event("create_post", %{"content" => content}, socket) do
    method handle_event (line 38) | def handle_event("delete_post", %{"dom_id" => dom_id}, socket) do
    method handle_info (line 44) | def handle_info({:new_post, post}, socket) do
    method handle_info (line 48) | def handle_info({:delete_post, dom_id}, socket) do
    method render (line 52) | def render(assigns) do

FILE: examples/demo_live_test.exs
  class DemoLive (line 6) | defmodule DemoLive
    method mount (line 9) | def mount(_params, _session, socket) do
    method render (line 13) | def render(assigns) do
    method handle_event (line 20) | def handle_event("inc", _params, socket) do
  class DemoLiveTest (line 28) | defmodule DemoLiveTest

FILE: examples/demo_router.exs
  class CounterLive (line 6) | defmodule CounterLive
    method mount (line 9) | def mount(_params, _session, socket) do
    method render (line 13) | def render(assigns) do
    method handle_event (line 24) | def handle_event("inc", _params, socket) do
  class DemoRouter (line 29) | defmodule DemoRouter

FILE: lib/phoenix_playground.ex
  class PhoenixPlayground (line 1) | defmodule PhoenixPlayground
    method start (line 55) | def start(options) do
    method get_file (line 106) | defp get_file do
    method get_file (line 111) | defp get_file([
    method get_file (line 118) | defp get_file([_ | rest]) do
    method get_file (line 122) | defp get_file([]) do
    method child_spec (line 127) | def child_spec(options) do
    method start_link (line 136) | def start_link(options) do

FILE: lib/phoenix_playground/application.ex
  class PhoenixPlayground.Application (line 1) | defmodule PhoenixPlayground.Application
    method start (line 7) | def start(_type, _args) do

FILE: lib/phoenix_playground/code_reloader.ex
  class PhoenixPlayground.CodeReloader (line 1) | defmodule PhoenixPlayground.CodeReloader
    method reload (line 4) | def reload(_endpoint, _options \\ []) do

FILE: lib/phoenix_playground/endpoint.ex
  class PhoenixPlayground.Endpoint (line 1) | defmodule PhoenixPlayground.Endpoint
    method run_live_reload (line 38) | defp run_live_reload(conn, _options) do
    method call (line 59) | def call(conn, opts) do

FILE: lib/phoenix_playground/error_view.ex
  class PhoenixPlayground.ErrorView (line 1) | defmodule PhoenixPlayground.ErrorView
    method render (line 4) | def render(template, _), do: Phoenix.Controller.status_message_from_te...

FILE: lib/phoenix_playground/layout.ex
  class PhoenixPlayground.Layout (line 1) | defmodule PhoenixPlayground.Layout
    method render (line 15) | def render(template, assigns)
    method render (line 17) | def render("root.html", assigns) do

FILE: lib/phoenix_playground/router.ex
  class PhoenixPlayground.Router (line 1) | defmodule PhoenixPlayground.Router
    method init (line 7) | def init([]) do
    method call (line 12) | def call(conn, []) do
  class ControllerRouter (line 48) | defmodule ControllerRouter
  class DelegateController (line 69) | defmodule DelegateController
    method init (line 72) | def init(options) do
    method call (line 76) | def call(conn, options) do
  class LiveRouter (line 82) | defmodule LiveRouter
  class DelegateLive (line 102) | defmodule DelegateLive
    method mount (line 107) | def mount(params, session, socket) do
    method render (line 112) | def render(assigns) do
    method handle_params (line 117) | def handle_params(unsigned_params, uri, socket) do
    method handle_event (line 126) | def handle_event(event, params, socket) do
    method handle_info (line 131) | def handle_info(message, socket) do
    method handle_async (line 136) | def handle_async(message, result, socket) do
    method module (line 140) | def module do

FILE: lib/phoenix_playground/test.ex
  class PhoenixPlayground.Test (line 1) | defmodule PhoenixPlayground.Test

FILE: mix.exs
  class PhoenixPlayground.MixProject (line 1) | defmodule PhoenixPlayground.MixProject
    method project (line 7) | def project do
    method application (line 20) | def application do
    method package (line 27) | defp package do
    method aliases (line 38) | defp aliases do
    method docs (line 46) | defp docs do
    method docs_setup (line 62) | defp docs_setup(_) do
    method docs_teardown (line 76) | defp docs_teardown(_) do
    method deps (line 81) | defp deps do

FILE: test/examples_test.exs
  class Examples (line 1) | defmodule Examples
    method run (line 2) | def run(path) do
    method remove_mix_install (line 13) | defp remove_mix_install(quoted) do

FILE: test/phoenix_playground_test.exs
  class PhoenixPlaygroundTest (line 1) | defmodule PhoenixPlaygroundTest
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (48K chars).
[
  {
    "path": ".formatter.exs",
    "chars": 133,
    "preview": "# Used by \"mix format\"\n[\n  import_deps: [:phoenix],\n  inputs: [\"{mix,.formatter}.exs\", \"{config,examples,lib,test}/**/*."
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1130,
    "preview": "name: CI\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-22.04\n    env:\n      MIX_ENV: test\n    strategy:\n  "
  },
  {
    "path": ".gitignore",
    "chars": 640,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1322,
    "preview": "# Changelog\n\n## v0.1.8 (2025-07-30)\n\n  * Use Phoenix LiveView v1.1.\n\n## v0.1.7 (2024-09-24)\n\n  * Delegate LiveView `hand"
  },
  {
    "path": "README.md",
    "chars": 2914,
    "preview": "# Phoenix Playground\n\n[![CI](https://github.com/phoenix-playground/phoenix_playground/actions/workflows/ci.yml/badge.svg"
  },
  {
    "path": "examples/demo_controller.exs",
    "chars": 696,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoController do\n  use Phoenix.Co"
  },
  {
    "path": "examples/demo_controller_test.exs",
    "chars": 944,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoController do\n  use Phoenix.Co"
  },
  {
    "path": "examples/demo_endpoint.exs",
    "chars": 1393,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoLive do\n  use Phoenix.LiveView"
  },
  {
    "path": "examples/demo_hooks.exs",
    "chars": 596,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoHooks do\n  use Phoenix.LiveVie"
  },
  {
    "path": "examples/demo_live.exs",
    "chars": 554,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoLive do\n  use Phoenix.LiveView"
  },
  {
    "path": "examples/demo_live.livemd",
    "chars": 619,
    "preview": "# Demo\n\n```elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n```\n\n## Section\n\n```elixir\ndefmodule DemoLive do\n"
  },
  {
    "path": "examples/demo_live_pubsub.exs",
    "chars": 5034,
    "preview": "Mix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule TimelineLive do\n  use Phoenix.LiveView\n\n  @topic \"timeli"
  },
  {
    "path": "examples/demo_live_test.exs",
    "chars": 796,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule DemoLive do\n  use Phoenix.LiveView"
  },
  {
    "path": "examples/demo_plug.exs",
    "chars": 173,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\nPhoenixPlayground.start(\n  plug: fn conn ->\n"
  },
  {
    "path": "examples/demo_router.exs",
    "chars": 903,
    "preview": "#!/usr/bin/env elixir\nMix.install([\n  {:phoenix_playground, \"~> 0.1.8\"}\n])\n\ndefmodule CounterLive do\n  use Phoenix.LiveV"
  },
  {
    "path": "lib/phoenix_playground/application.ex",
    "chars": 423,
    "preview": "defmodule PhoenixPlayground.Application do\n  @moduledoc false\n\n  use Application\n\n  @impl true\n  def start(_type, _args)"
  },
  {
    "path": "lib/phoenix_playground/code_reloader.ex",
    "chars": 705,
    "preview": "defmodule PhoenixPlayground.CodeReloader do\n  @moduledoc false\n\n  def reload(_endpoint, _options \\\\ []) do\n    if path ="
  },
  {
    "path": "lib/phoenix_playground/endpoint.ex",
    "chars": 6691,
    "preview": "defmodule PhoenixPlayground.Endpoint do\n  @moduledoc false\n\n  use Phoenix.Endpoint, otp_app: :phoenix_playground\n\n  @sig"
  },
  {
    "path": "lib/phoenix_playground/error_view.ex",
    "chars": 154,
    "preview": "defmodule PhoenixPlayground.ErrorView do\n  @moduledoc false\n\n  def render(template, _), do: Phoenix.Controller.status_me"
  },
  {
    "path": "lib/phoenix_playground/layout.ex",
    "chars": 2738,
    "preview": "defmodule PhoenixPlayground.Layout do\n  @moduledoc \"\"\"\n  Built-in layout.\n\n  This is the layout used by `PhoenixPlaygrou"
  },
  {
    "path": "lib/phoenix_playground/router.ex",
    "chars": 3345,
    "preview": "defmodule PhoenixPlayground.Router do\n  @moduledoc false\n\n  @behaviour Plug\n\n  @impl true\n  def init([]) do\n    []\n  end"
  },
  {
    "path": "lib/phoenix_playground/test.ex",
    "chars": 1976,
    "preview": "defmodule PhoenixPlayground.Test do\n  @moduledoc ~S'''\n  Conveniences for testing single-file Phoenix apps.\n\n  ## Exampl"
  },
  {
    "path": "lib/phoenix_playground.ex",
    "chars": 7194,
    "preview": "defmodule PhoenixPlayground do\n  @moduledoc \"\"\"\n  Phoenix Playground makes it easy to create single-file Phoenix applica"
  },
  {
    "path": "mix.exs",
    "chars": 2308,
    "preview": "defmodule PhoenixPlayground.MixProject do\n  use Mix.Project\n\n  @source_url \"https://github.com/phoenix-playground/phoeni"
  },
  {
    "path": "test/examples_test.exs",
    "chars": 524,
    "preview": "defmodule Examples do\n  def run(path) do\n    {:ok, quoted} =\n      path\n      |> File.read!()\n      |> Code.string_to_qu"
  },
  {
    "path": "test/phoenix_playground_test.exs",
    "chars": 70,
    "preview": "defmodule PhoenixPlaygroundTest do\n  use ExUnit.Case, async: true\nend\n"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 15,
    "preview": "ExUnit.start()\n"
  }
]

About this extraction

This page contains the full source code of the phoenix-playground/phoenix_playground GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (43.0 KB), approximately 14.2k tokens, and a symbol index with 87 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!