├── test ├── support │ └── .keep ├── test_helper.exs └── web_dev_utils │ └── code_reloader_test.exs ├── .release-please-manifest.json ├── .rtx.toml ├── lib ├── web_dev_utils.ex └── web_dev_utils │ ├── filesystem.ex │ ├── live_reload.ex │ ├── code_reloader.ex │ ├── assets.ex │ └── components.ex ├── .formatter.exs ├── release-please-config.json ├── .github └── workflows │ ├── lint-commit.yaml │ ├── release.yaml │ └── ci.yaml ├── .gitignore ├── README.md ├── LICENSE ├── CHANGELOG.md ├── mix.exs └── mix.lock /test/support/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.3.0" 3 | } 4 | -------------------------------------------------------------------------------- /.rtx.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | elixir = "1.15.7-otp-26" 3 | erlang = "26.1.1" 4 | -------------------------------------------------------------------------------- /lib/web_dev_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/web_dev_utils/filesystem.ex: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils.FileSystem do 2 | @moduledoc """ 3 | The file watcher process. 4 | """ 5 | def child_spec(_) do 6 | %{ 7 | id: FileSystem, 8 | start: 9 | {FileSystem, :start_link, [[dirs: [Path.absname("")], name: :web_dev_utils_file_watcher]]} 10 | } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "packages": { 4 | ".": { 5 | "package-name": "web_dev_utils", 6 | "release-type": "elixir", 7 | "bump-minor-pre-major": true, 8 | "include-component-in-tag": false, 9 | "extra-files": [ 10 | "README.md" 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/lint-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Commit 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | - edited 9 | pull_request: 10 | types: 11 | - opened 12 | - reopened 13 | - synchronize 14 | - edited 15 | 16 | jobs: 17 | commitlint: 18 | runs-on: ubuntu-latest 19 | name: commitlint 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: amannn/action-semantic-pull-request@v5 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | subjectPattern: ^(?![A-Z]).+$ 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | web_dev_utils-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | /test/support/editable.ex 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebDevUtils 2 | 3 | WebDevUtils is a library to enable awesome local development for websites and web applications. 4 | 5 | ## Features 6 | 7 | - Asset watchers (run CSS/JS tooling when you boot up your server) 8 | - Live Reload (make the browser refresh when files change on disk) 9 | - Code Reloading (make your Elixir files recompile) 10 | 11 | ## Usage 12 | 13 | TODO 14 | 15 | ## Installation 16 | 17 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 18 | by adding `web_dev_utils` to your list of dependencies in `mix.exs`: 19 | 20 | 21 | 22 | ```elixir 23 | def deps do 24 | [ 25 | {:web_dev_utils, "~> 0.3.0"} 26 | ] 27 | end 28 | ``` 29 | 30 | 31 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 32 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 33 | be found at . 34 | -------------------------------------------------------------------------------- /lib/web_dev_utils/live_reload.ex: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils.LiveReload do 2 | @moduledoc """ 3 | Notifications (usually sent to a web browser) when files on disk change. 4 | """ 5 | require Logger 6 | 7 | def init(opts \\ []) do 8 | name = Keyword.get(opts, :name, :web_dev_utils_file_watcher) 9 | 10 | FileSystem.subscribe(name) 11 | end 12 | 13 | def reload!({:file_event, _watcher_pid, {path, _event}}, opts \\ []) do 14 | patterns = Keyword.get(opts, :patterns, []) 15 | debounce = Keyword.get(opts, :debounce, 100) 16 | 17 | Process.sleep(debounce) 18 | 19 | paths = flush([Path.relative_to_cwd(to_string(path))]) 20 | 21 | path = Enum.find(paths, fn path -> Enum.any?(patterns, &String.match?(path, &1)) end) 22 | 23 | if path do 24 | Logger.debug("Live reload: #{Path.relative_to_cwd(path)}") 25 | 26 | send(self(), :reload) 27 | end 28 | end 29 | 30 | defp flush(acc) do 31 | receive do 32 | {:file_event, _, {path, _event}} -> 33 | flush([Path.relative_to_cwd(to_string(path)) | acc]) 34 | after 35 | 0 -> acc 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mitchell Hanberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/web_dev_utils/code_reloader_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils.CodeReloaderTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.CaptureIO 4 | 5 | alias WebDevUtils.CodeReloader 6 | 7 | setup do 8 | on_exit(fn -> 9 | File.rm("test/support/editable.ex") 10 | end) 11 | 12 | :ok 13 | end 14 | 15 | test "noops when nothing has changed" do 16 | start_supervised!(CodeReloader) 17 | 18 | assert {_, []} = CodeReloader.reload() 19 | assert {:noop, []} == CodeReloader.reload() 20 | end 21 | 22 | test "recompiles code" do 23 | File.write!("test/support/editable.ex", """ 24 | defmodule Editable do 25 | end 26 | """) 27 | 28 | start_supervised!(CodeReloader) 29 | 30 | capture_io(:stderr, fn -> 31 | assert {:ok, []} == CodeReloader.reload() 32 | end) 33 | end 34 | 35 | test "recompiles code and something fails" do 36 | File.write!("test/support/editable.ex", """ 37 | defmodule Editable d 38 | end 39 | """) 40 | 41 | start_supervised!(CodeReloader) 42 | 43 | capture_io(:stderr, fn -> 44 | assert {:error, [%Mix.Task.Compiler.Diagnostic{}]} = CodeReloader.reload() 45 | end) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.3.0](https://github.com/elixir-tools/web_dev_utils/compare/v0.2.0...v0.3.0) (2025-03-04) 4 | 5 | 6 | ### Features 7 | 8 | * return compiler diagnostics from reload ([#6](https://github.com/elixir-tools/web_dev_utils/issues/6)) ([d553a40](https://github.com/elixir-tools/web_dev_utils/commit/d553a403f4019929d4947d4f93eec61a6f96211d)) 9 | 10 | ## [0.2.0](https://github.com/elixir-tools/web_dev_utils/compare/v0.1.1...v0.2.0) (2024-10-11) 11 | 12 | 13 | ### ⚠ BREAKING CHANGES 14 | 15 | * The live reload function now sends just the message 16 | 17 | ### Features 18 | 19 | * improve live reload ([#4](https://github.com/elixir-tools/web_dev_utils/issues/4)) ([be0e530](https://github.com/elixir-tools/web_dev_utils/commit/be0e5302204519ace2fe0a311721decf568e1a5b)) 20 | 21 | ## [0.1.1](https://github.com/elixir-tools/web_dev_utils/compare/v0.1.0...v0.1.1) (2024-10-11) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * bump file_system to v1.0 ([#2](https://github.com/elixir-tools/web_dev_utils/issues/2)) ([237f471](https://github.com/elixir-tools/web_dev_utils/commit/237f471d64d43eea8dd2c8dcbee208ed2ac26d0b)) 27 | 28 | ## 0.1.0 (2023-10-28) 29 | 30 | 31 | ### Features 32 | 33 | * assets, live reload, code reloadign ([418fe5d](https://github.com/elixir-tools/web_dev_utils/commit/418fe5d89db33b55675b86548bf804fbb14f1e58)) 34 | * make websocket url configureable ([608cc84](https://github.com/elixir-tools/web_dev_utils/commit/608cc846edd9417a4697b49c61f4ae25105c0ba6)) 35 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/elixir-tools/web_dev_utils" 5 | 6 | def project do 7 | [ 8 | app: :web_dev_utils, 9 | description: 10 | "Library to enable awesome local development for websites and web applications", 11 | version: "0.3.0", 12 | elixir: "~> 1.14", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | start_permanent: Mix.env() == :prod, 15 | package: package(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Run "mix help compile.app" to learn about applications. 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Run "mix help deps" to learn about dependencies. 32 | defp deps do 33 | [ 34 | {:file_system, "~> 1.0"}, 35 | {:ex_doc, ">= 0.0.0", only: :dev} 36 | 37 | # {:dep_from_hexpm, "~> 0.3.0"}, 38 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 39 | ] 40 | end 41 | 42 | defp package() do 43 | [ 44 | maintainers: ["Mitchell Hanberg"], 45 | licenses: ["MIT"], 46 | links: %{ 47 | GitHub: @source_url, 48 | Sponsor: "https://github.com/sponsors/mhanberg" 49 | }, 50 | files: ~w(lib LICENSE mix.exs README.md .formatter.exs) 51 | ] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/web_dev_utils/code_reloader.ex: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils.CodeReloader do 2 | @moduledoc """ 3 | The code reloader process. 4 | """ 5 | 6 | # Handles automatic code reloading. 7 | 8 | # Taken from [Still](https://github.com/still-ex/still/blob/277f4b546f0abf1ba56167a7ae894a49069b3c6c/lib/still/web/code_reloader.ex), which is taken from [Phoenix](https://github.com/phoenixframework/phoenix/blob/431c51e20d8840fa1f851160b659f78c6bb484c6/lib/phoenix/code_reloader/server.ex). 9 | use GenServer 10 | 11 | require Logger 12 | 13 | def start_link(_) do 14 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 15 | end 16 | 17 | @doc """ 18 | Reloads the code. 19 | """ 20 | def reload do 21 | GenServer.call(__MODULE__, :reload) 22 | end 23 | 24 | def init(_opts) do 25 | {:ok, :nostate} 26 | end 27 | 28 | def handle_call(:reload, _from, state) do 29 | froms = all_waiting([]) 30 | result = mix_compile(Code.ensure_loaded(Mix.Task)) 31 | Enum.each(froms, &GenServer.reply(&1, result)) 32 | 33 | {:reply, result, state} 34 | end 35 | 36 | defp all_waiting(acc) do 37 | receive do 38 | {:"$gen_call", from, :reload} -> all_waiting([from | acc]) 39 | after 40 | 0 -> acc 41 | end 42 | end 43 | 44 | defp mix_compile({:error, _reason}) do 45 | Logger.error("Could not find Mix") 46 | :error 47 | end 48 | 49 | defp mix_compile({:module, Mix.Task}) do 50 | Mix.Task.reenable("compile.elixir") 51 | Mix.Task.run("compile.elixir", ["--return-errors"]) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | release: 13 | name: release 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | otp: [26.1.1] 18 | elixir: [1.15.7] 19 | steps: 20 | - uses: googleapis/release-please-action@v4 21 | id: release 22 | 23 | 24 | - uses: actions/checkout@v3 25 | if: ${{ steps.release.outputs.release_created }} 26 | 27 | - uses: erlef/setup-beam@v1 28 | with: 29 | otp-version: ${{matrix.otp}} 30 | elixir-version: ${{matrix.elixir}} 31 | if: ${{ steps.release.outputs.release_created }} 32 | 33 | - uses: actions/cache@v3 34 | id: cache 35 | if: ${{ steps.release.outputs.release_created }} 36 | with: 37 | path: | 38 | deps 39 | _build 40 | key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 41 | restore-keys: | 42 | ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}- 43 | 44 | - name: Install Dependencies 45 | if: steps.release.outputs.release_created && steps.cache.outputs.cache-hit != 'true' 46 | run: mix deps.get 47 | 48 | - name: publish to hex 49 | if: ${{ steps.release.outputs.release_created }} 50 | env: 51 | HEX_API_KEY: ${{secrets.HEX_API_KEY}} 52 | run: | 53 | mix hex.publish --yes 54 | -------------------------------------------------------------------------------- /lib/web_dev_utils/assets.ex: -------------------------------------------------------------------------------- 1 | # module mostly taken from `Phoenix.Endpoint.Watcher` 2 | 3 | defmodule WebDevUtils.Assets do 4 | @moduledoc """ 5 | Task for starting arbitrary commands and Elixir modules (like `Tailwind` and `Esbuild`). 6 | """ 7 | require Logger 8 | 9 | def child_spec(args) do 10 | %{ 11 | id: make_ref(), 12 | start: {__MODULE__, :start_link, [args]}, 13 | restart: :transient 14 | } 15 | end 16 | 17 | def start_link({cmd, args}) do 18 | Task.start_link(__MODULE__, :watch, [to_string(cmd), args]) 19 | end 20 | 21 | def watch(_cmd, {mod, fun, args}) do 22 | try do 23 | apply(mod, fun, args) 24 | catch 25 | kind, reason -> 26 | # The function returned a non-zero exit code. 27 | # Sleep for a couple seconds before exiting to 28 | # ensure this doesn't hit the supervisor's 29 | # max_restarts/max_seconds limit. 30 | Process.sleep(2000) 31 | :erlang.raise(kind, reason, __STACKTRACE__) 32 | end 33 | end 34 | 35 | def watch(cmd, args) when is_list(args) do 36 | {args, opts} = Enum.split_while(args, &is_binary(&1)) 37 | opts = Keyword.merge([into: IO.stream(:stdio, :line), stderr_to_stdout: true], opts) 38 | 39 | try do 40 | System.cmd(cmd, args, opts) 41 | catch 42 | :error, :enoent -> 43 | relative = Path.relative_to_cwd(cmd) 44 | 45 | Logger.error( 46 | "Could not start watcher #{inspect(relative)} from #{inspect(cd(opts))}, executable does not exist" 47 | ) 48 | 49 | exit(:shutdown) 50 | else 51 | {_, 0} -> 52 | :ok 53 | 54 | {_, _} -> 55 | # System.cmd returned a non-zero exit code 56 | # sleep for a couple seconds before exiting to ensure this doesn't 57 | # hit the supervisor's max_restarts / max_seconds limit 58 | Process.sleep(2000) 59 | exit(:watcher_command_error) 60 | end 61 | end 62 | 63 | defp cd(opts), do: opts[:cd] || File.cwd!() 64 | end 65 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 3 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 4 | "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, 5 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 7 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: main 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | name: Test (${{matrix.elixir}}/${{matrix.otp}}) 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | elixir: [1.17.x, 1.18.x] 16 | otp: [26.x, 27.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: erlef/setup-beam@v1 21 | id: install 22 | with: 23 | otp-version: ${{matrix.otp}} 24 | elixir-version: ${{matrix.elixir}} 25 | - uses: actions/cache@v4 26 | id: cache 27 | with: 28 | path: | 29 | deps 30 | key: ${{ runner.os }}-mix-${{steps.install.outputs.otp-version}}-${{steps.install.outputs.elixir-version}}-${{ hashFiles('**/mix.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-mix-${{steps.install.outputs.otp-version}}-${{steps.install.outputs.elixir-version}}- 33 | 34 | - name: Install Dependencies 35 | if: steps.cache.outputs.cache-hit != 'true' 36 | run: mix deps.get 37 | 38 | - name: Run Tests 39 | run: mix test 40 | 41 | formatter: 42 | runs-on: ubuntu-latest 43 | name: Formatter (${{matrix.elixir}}/${{matrix.otp}}) 44 | 45 | strategy: 46 | matrix: 47 | otp: [27.x] 48 | elixir: [1.17.x] 49 | 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: erlef/setup-beam@v1 53 | id: install 54 | with: 55 | otp-version: ${{matrix.otp}} 56 | elixir-version: ${{matrix.elixir}} 57 | - uses: actions/cache@v4 58 | id: cache 59 | with: 60 | path: | 61 | deps 62 | _build 63 | key: ${{ runner.os }}-mix-${{steps.install.outputs.otp-version}}-${{steps.install.outputs.elixir-version}}-${{ hashFiles('**/mix.lock') }} 64 | restore-keys: | 65 | ${{ runner.os }}-mix-${{steps.install.outputs.otp-version}}-${{steps.install.outputs.elixir-version}}- 66 | 67 | - name: Install Dependencies 68 | if: steps.cache.outputs.cache-hit != 'true' 69 | run: mix deps.get 70 | 71 | - name: Run Formatter 72 | run: mix format --check-formatted 73 | -------------------------------------------------------------------------------- /lib/web_dev_utils/components.ex: -------------------------------------------------------------------------------- 1 | defmodule WebDevUtils.Components do 2 | @moduledoc """ 3 | Builtin components. 4 | """ 5 | require EEx 6 | 7 | @doc """ 8 | A component for triggering live reloading via a websocket. 9 | 10 | The url for the websocket connectino is controlled by the `:reload_url` applicaiton config key. 11 | 12 | ## Example 13 | 14 | ```elixir 15 | # config/config.exs 16 | 17 | config :web_dev_utils, :reload_url, "wss://sometunnelingdomain/ws" 18 | ``` 19 | """ 20 | EEx.function_from_string( 21 | :def, 22 | :live_reload, 23 | ~s''' 24 | 73 | ''', 74 | [:_] 75 | ) 76 | end 77 | --------------------------------------------------------------------------------