From f645cbfb8f5c2b43a70a93b993ef390d731bb8b3 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 1 Oct 2026 12:36:48 +0400 Subject: [PATCH 1/3] Fetch icons a stale cached set lacks, and scan Markdown A set cached before an icon was added made new icons fail with Icon not found. Icons missing from the cached set are now fetched one by one from the Iconify API, which also reports unknown names instead of raising. Markdown files matched by the default globs were skipped; they are now scanned like HEEx. Closes #2. --- CHANGELOG.md | 6 +++ lib/mix/tasks/compile/phoenix_iconify.ex | 34 +--------------- lib/phoenix_iconify/cache.ex | 37 ++++++++++++++++++ lib/phoenix_iconify/scanner.ex | 3 +- test/phoenix_iconify/scanner_test.exs | 27 +++++++++++++ test/phoenix_iconify/stale_cache_test.exs | 47 +++++++++++++++++++++++ 6 files changed, 120 insertions(+), 34 deletions(-) create mode 100644 test/phoenix_iconify/stale_cache_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index d995216..4acc16f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +- Fetch icons that a cached icon set lacks from the Iconify API. A set cached before an icon was added to the collection made new icons fail with "Icon not found" (#2). +- Report unknown icon names as not found instead of raising when fetching from the Iconify API. +- Scan Markdown files for icon components. v0.3.5 added `content/**/*.md` to the default globs but skipped those files. + ## v0.3.5 - Add configurable scanner source globs diff --git a/lib/mix/tasks/compile/phoenix_iconify.ex b/lib/mix/tasks/compile/phoenix_iconify.ex index db94b6c..9e25a7e 100644 --- a/lib/mix/tasks/compile/phoenix_iconify.ex +++ b/lib/mix/tasks/compile/phoenix_iconify.ex @@ -103,43 +103,11 @@ defmodule Mix.Tasks.Compile.PhoenixIconify do end defp fetch_prefix_icons(prefix, full_names) do - icon_names = Enum.map(full_names, &icon_name!/1) - - case Cache.fetch_set(prefix) do - {:ok, set} -> fetch_icons_from_set(prefix, icon_names, set) - {:error, _} -> fetch_icons_from_api(prefix, icon_names) - end + Cache.get_icons(prefix, Enum.map(full_names, &icon_name!/1)) end defp icon_name!(name) do {:ok, _prefix, icon_name} = Iconify.parse_name(name) icon_name end - - defp fetch_icons_from_set(prefix, icon_names, set) do - Enum.flat_map(icon_names, fn icon_name -> - case Iconify.Set.get(set, icon_name) do - {:ok, icon} -> - full_name = "#{prefix}:#{icon_name}" - [{full_name, %{icon | name: full_name}}] - - :error -> - [] - end - end) - end - - defp fetch_icons_from_api(prefix, icon_names) do - case Iconify.Fetcher.fetch_icons(prefix, icon_names) do - {:ok, icons} -> - Enum.map(icons, fn {name, icon} -> - full_name = "#{prefix}:#{name}" - {full_name, %{icon | name: full_name}} - end) - - {:error, reason} -> - Mix.shell().error("PhoenixIconify: Failed to fetch #{prefix} icons: #{inspect(reason)}") - [] - end - end end diff --git a/lib/phoenix_iconify/cache.ex b/lib/phoenix_iconify/cache.ex index 99744ae..14a1b71 100644 --- a/lib/phoenix_iconify/cache.ex +++ b/lib/phoenix_iconify/cache.ex @@ -102,6 +102,43 @@ defmodule PhoenixIconify.Cache do end end + @doc """ + Gets icons of one set, keyed by their full `prefix:name`. + + Icons come from the cached set when it has them. A cached set can predate icons + added to the collection since it was downloaded, so the rest are fetched one by + one with `fetch_icon`. Icons that exist in neither are left out. + """ + @spec get_icons(String.t(), [String.t()], (String.t(), String.t() -> + {:ok, Iconify.Icon.t()} | {:error, term()})) :: + [{String.t(), Iconify.Icon.t()}] + def get_icons(prefix, icon_names, fetch_icon \\ &Iconify.Fetcher.fetch_icon/2) do + cached = + case fetch_set(prefix) do + {:ok, set} -> + for name <- icon_names, + {:ok, icon} <- [Iconify.Set.get(set, name)], + into: %{}, + do: {name, icon} + + {:error, _} -> + %{} + end + + for name <- icon_names, + {:ok, icon} <- [cached_or_fetched(cached, prefix, name, fetch_icon)] do + full_name = "#{prefix}:#{name}" + {full_name, %{icon | name: full_name}} + end + end + + defp cached_or_fetched(cached, prefix, name, fetch_icon) do + case Map.fetch(cached, name) do + {:ok, icon} -> {:ok, icon} + :error -> fetch_icon.(prefix, name) + end + end + @doc """ Clears all cached icon sets. """ diff --git a/lib/phoenix_iconify/scanner.ex b/lib/phoenix_iconify/scanner.ex index 16ae8fc..d053ae1 100644 --- a/lib/phoenix_iconify/scanner.ex +++ b/lib/phoenix_iconify/scanner.ex @@ -54,7 +54,8 @@ defmodule PhoenixIconify.Scanner do content = File.read!(path) cond do - Path.extname(path) in [".heex", ".astral"] -> scan_heex(content, path) + # Markdown in Astral and similar tools can use HEEx components. + Path.extname(path) in [".heex", ".astral", ".md"] -> scan_heex(content, path) Path.extname(path) == ".ex" -> scan_ex(content) true -> [] end diff --git a/test/phoenix_iconify/scanner_test.exs b/test/phoenix_iconify/scanner_test.exs index 9aa705d..462ccb2 100644 --- a/test/phoenix_iconify/scanner_test.exs +++ b/test/phoenix_iconify/scanner_test.exs @@ -28,6 +28,33 @@ defmodule PhoenixIconify.ScannerTest do end end + test "scans icon components in Markdown", %{tmp_dir: tmp} do + File.mkdir_p!(Path.join(tmp, "content")) + + File.write!(Path.join(tmp, "content/post.md"), """ + # Post + + Reach me by <.icon name="lucide:mail" /> email, or <.nav_link icon="lucide:rss">the feed. + + ```elixir + if a < b, do: :ok + ``` + """) + + previous = Application.get_env(:phoenix_iconify, :source_globs) + Application.put_env(:phoenix_iconify, :source_globs, ["content/**/*.md"]) + + try do + assert File.cd!(tmp, &Scanner.scan/0) == ["lucide:mail", "lucide:rss"] + after + if previous do + Application.put_env(:phoenix_iconify, :source_globs, previous) + else + Application.delete_env(:phoenix_iconify, :source_globs) + end + end + end + describe "scan_heex_content/1" do test "extracts icon names from heex content" do content = """ diff --git a/test/phoenix_iconify/stale_cache_test.exs b/test/phoenix_iconify/stale_cache_test.exs new file mode 100644 index 0000000..1d80649 --- /dev/null +++ b/test/phoenix_iconify/stale_cache_test.exs @@ -0,0 +1,47 @@ +defmodule PhoenixIconify.StaleCacheTest do + use ExUnit.Case, async: false + + alias PhoenixIconify.Cache + + @moduletag :tmp_dir + + setup %{tmp_dir: tmp_dir} do + previous = Application.get_env(:phoenix_iconify, :cache_dir) + Application.put_env(:phoenix_iconify, :cache_dir, tmp_dir) + + on_exit(fn -> + if previous, + do: Application.put_env(:phoenix_iconify, :cache_dir, previous), + else: Application.delete_env(:phoenix_iconify, :cache_dir) + end) + + # A set cached before "layer-arrow-up" was added to Lucide. + Cache.save_set( + "lucide", + ~s({"prefix":"lucide","icons":{"mail":{"body":""}},"width":24,"height":24}) + ) + end + + test "fetches icons a stale cached set lacks, and skips icons that exist nowhere" do + parent = self() + + fetch_icon = fn + "lucide", "layer-arrow-up" = name -> + send(parent, {:fetched, name}) + {:ok, %Iconify.Icon{name: name, body: "", width: 24, height: 24}} + + "lucide", name -> + send(parent, {:fetched, name}) + {:error, :not_found} + end + + icons = Cache.get_icons("lucide", ["mail", "layer-arrow-up", "no-such-icon"], fetch_icon) + + assert Enum.map(icons, &elem(&1, 0)) == ["lucide:mail", "lucide:layer-arrow-up"] + assert Enum.all?(icons, fn {full_name, icon} -> icon.name == full_name end) + + refute_received {:fetched, "mail"} + assert_received {:fetched, "layer-arrow-up"} + assert_received {:fetched, "no-such-icon"} + end +end From bba63ead982bb9f34e16ab90f8ba2fd70833f8cc Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 1 Oct 2026 12:45:26 +0400 Subject: [PATCH 2/3] Scan Astral sources only with Astral, mirror lib/ in tests, add e2e tests Default globs include Astral templates and Markdown only when Astral is a dependency. The get_icons tests move into cache_test.exs, and an e2e test against the Iconify API, run with mix test.e2e, reproduces #2. Requires iconify 0.3.1, which reports unknown names instead of raising. --- CHANGELOG.md | 3 +- README.md | 2 +- lib/phoenix_iconify/scanner.ex | 18 ++++++--- mix.exs | 5 ++- test/e2e/phoenix_iconify/cache_test.exs | 32 +++++++++++++++ test/phoenix_iconify/cache_test.exs | 46 +++++++++++++++++++++- test/phoenix_iconify/scanner_test.exs | 30 +++++++++++++++ test/phoenix_iconify/stale_cache_test.exs | 47 ----------------------- test/test_helper.exs | 2 +- 9 files changed, 126 insertions(+), 59 deletions(-) create mode 100644 test/e2e/phoenix_iconify/cache_test.exs delete mode 100644 test/phoenix_iconify/stale_cache_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4acc16f..d80cc3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ ## Unreleased - Fetch icons that a cached icon set lacks from the Iconify API. A set cached before an icon was added to the collection made new icons fail with "Icon not found" (#2). -- Report unknown icon names as not found instead of raising when fetching from the Iconify API. +- Report unknown icon names as not found instead of raising when fetching from the Iconify API. Requires `iconify` 0.3.1. - Scan Markdown files for icon components. v0.3.5 added `content/**/*.md` to the default globs but skipped those files. +- Include Astral templates and Markdown in the default globs only when Astral is a dependency, so other projects don't scan their Markdown. ## v0.3.5 diff --git a/README.md b/README.md index 104e2c6..7d81f54 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ config :phoenix_iconify, Options: - `:extra_icons` - icons to include even when they are not found by static discovery -- `:source_globs` - source globs to scan for literal icon usage. Defaults include Phoenix HEEx files and Astral `pages/**/*.astral`, `components/**/*.astral`, `layouts/**/*.astral`, and `content/**/*.md`. +- `:source_globs` - source globs to scan for literal icon usage. Defaults to Phoenix sources (`lib/**/*.ex`, `lib/**/*.heex`, `priv/**/*.heex`). In projects that depend on [Astral](https://hex.pm/packages/astral), the defaults also include its templates and Markdown: `pages/**/*.{astral,md}`, `components/**/*.astral`, `layouts/**/*.astral`, and `content/**/*.md`. - `:fallback` - icon to render when a requested icon is missing - `:warn_on_missing` - log missing icon warnings, enabled by default diff --git a/lib/phoenix_iconify/scanner.ex b/lib/phoenix_iconify/scanner.ex index d053ae1..761e25c 100644 --- a/lib/phoenix_iconify/scanner.ex +++ b/lib/phoenix_iconify/scanner.ex @@ -13,11 +13,11 @@ defmodule PhoenixIconify.Scanner do alias Phoenix.LiveView.TagEngine alias Phoenix.LiveView.Tokenizer, as: LegacyTokenizer - @default_source_globs [ - "lib/**/*.ex", - "lib/**/*.heex", - "priv/**/*.heex", - "pages/**/*.astral", + @phoenix_source_globs ["lib/**/*.ex", "lib/**/*.heex", "priv/**/*.heex"] + + # Astral renders HEEx from its templates and Markdown, so those hold icons too. + @astral_source_globs [ + "pages/**/*.{astral,md}", "components/**/*.astral", "layouts/**/*.astral", "content/**/*.md" @@ -45,11 +45,17 @@ defmodule PhoenixIconify.Scanner do defp source_paths do :phoenix_iconify - |> Application.get_env(:source_globs, @default_source_globs) + |> Application.get_env(:source_globs, default_source_globs()) |> List.wrap() |> Enum.flat_map(&Path.wildcard/1) end + defp default_source_globs do + if Code.ensure_loaded?(Astral), + do: @phoenix_source_globs ++ @astral_source_globs, + else: @phoenix_source_globs + end + defp scan_file(path) do content = File.read!(path) diff --git a/mix.exs b/mix.exs index 9ad26b8..571a8ed 100644 --- a/mix.exs +++ b/mix.exs @@ -42,16 +42,17 @@ defmodule PhoenixIconify.MixProject do if path = System.get_env("ICONIFY_PATH") do [path: path] else - "~> 0.3.0" + "~> 0.3.1" end end def cli do - [preferred_envs: [ci: :test]] + [preferred_envs: [ci: :test, "test.e2e": :test]] end defp aliases do [ + "test.e2e": ["test --only e2e"], ci: [ "compile --warnings-as-errors", "format --check-formatted", diff --git a/test/e2e/phoenix_iconify/cache_test.exs b/test/e2e/phoenix_iconify/cache_test.exs new file mode 100644 index 0000000..d0c11a4 --- /dev/null +++ b/test/e2e/phoenix_iconify/cache_test.exs @@ -0,0 +1,32 @@ +defmodule PhoenixIconify.E2E.CacheTest do + # Talks to the Iconify API. Run with `mix test.e2e`. + use ExUnit.Case, async: false + + alias PhoenixIconify.Cache + + @moduletag :e2e + @moduletag :tmp_dir + + setup %{tmp_dir: tmp_dir} do + {:ok, _} = Application.ensure_all_started(:req) + previous = Application.get_env(:phoenix_iconify, :cache_dir) + Application.put_env(:phoenix_iconify, :cache_dir, tmp_dir) + + on_exit(fn -> + if previous, + do: Application.put_env(:phoenix_iconify, :cache_dir, previous), + else: Application.delete_env(:phoenix_iconify, :cache_dir) + end) + end + + test "fetches an icon added to Lucide after its set was cached (#2)" do + Cache.save_set( + "lucide", + ~s({"prefix":"lucide","icons":{"mail":{"body":""}},"width":24,"height":24}) + ) + + assert [{"lucide:layer-arrow-up", icon}] = Cache.get_icons("lucide", ["layer-arrow-up"]) + assert icon.body =~ " + if previous, + do: Application.put_env(:phoenix_iconify, :cache_dir, previous), + else: Application.delete_env(:phoenix_iconify, :cache_dir) + end) + + # A set cached before "layer-arrow-up" was added to Lucide. + Cache.save_set( + "lucide", + ~s({"prefix":"lucide","icons":{"mail":{"body":""}},"width":24,"height":24}) + ) + end + + test "fetches icons a stale cached set lacks, and skips icons that exist nowhere" do + parent = self() + + fetch_icon = fn + "lucide", "layer-arrow-up" = name -> + send(parent, {:fetched, name}) + {:ok, %Iconify.Icon{name: name, body: "", width: 24, height: 24}} + + "lucide", name -> + send(parent, {:fetched, name}) + {:error, :not_found} + end + + icons = Cache.get_icons("lucide", ["mail", "layer-arrow-up", "no-such-icon"], fetch_icon) + + assert Enum.map(icons, &elem(&1, 0)) == ["lucide:mail", "lucide:layer-arrow-up"] + assert Enum.all?(icons, fn {full_name, icon} -> icon.name == full_name end) + + refute_received {:fetched, "mail"} + assert_received {:fetched, "layer-arrow-up"} + assert_received {:fetched, "no-such-icon"} + end + end end diff --git a/test/phoenix_iconify/scanner_test.exs b/test/phoenix_iconify/scanner_test.exs index 462ccb2..d524b73 100644 --- a/test/phoenix_iconify/scanner_test.exs +++ b/test/phoenix_iconify/scanner_test.exs @@ -55,6 +55,36 @@ defmodule PhoenixIconify.ScannerTest do end end + describe "default source globs" do + setup %{tmp_dir: tmp} do + File.mkdir_p!(Path.join(tmp, "content")) + File.write!(Path.join(tmp, "content/post.md"), ~s(Mail <.icon name="lucide:mail" />.)) + + previous = Application.get_env(:phoenix_iconify, :source_globs) + Application.delete_env(:phoenix_iconify, :source_globs) + + on_exit(fn -> + if previous, do: Application.put_env(:phoenix_iconify, :source_globs, previous) + end) + end + + test "skip Markdown without Astral", %{tmp_dir: tmp} do + refute Code.ensure_loaded?(Astral) + assert File.cd!(tmp, &Scanner.scan/0) == [] + end + + test "include Astral templates and Markdown when Astral is present", %{tmp_dir: tmp} do + Code.compile_string("defmodule Astral do end") + + on_exit(fn -> + :code.purge(Astral) + :code.delete(Astral) + end) + + assert File.cd!(tmp, &Scanner.scan/0) == ["lucide:mail"] + end + end + describe "scan_heex_content/1" do test "extracts icon names from heex content" do content = """ diff --git a/test/phoenix_iconify/stale_cache_test.exs b/test/phoenix_iconify/stale_cache_test.exs deleted file mode 100644 index 1d80649..0000000 --- a/test/phoenix_iconify/stale_cache_test.exs +++ /dev/null @@ -1,47 +0,0 @@ -defmodule PhoenixIconify.StaleCacheTest do - use ExUnit.Case, async: false - - alias PhoenixIconify.Cache - - @moduletag :tmp_dir - - setup %{tmp_dir: tmp_dir} do - previous = Application.get_env(:phoenix_iconify, :cache_dir) - Application.put_env(:phoenix_iconify, :cache_dir, tmp_dir) - - on_exit(fn -> - if previous, - do: Application.put_env(:phoenix_iconify, :cache_dir, previous), - else: Application.delete_env(:phoenix_iconify, :cache_dir) - end) - - # A set cached before "layer-arrow-up" was added to Lucide. - Cache.save_set( - "lucide", - ~s({"prefix":"lucide","icons":{"mail":{"body":""}},"width":24,"height":24}) - ) - end - - test "fetches icons a stale cached set lacks, and skips icons that exist nowhere" do - parent = self() - - fetch_icon = fn - "lucide", "layer-arrow-up" = name -> - send(parent, {:fetched, name}) - {:ok, %Iconify.Icon{name: name, body: "", width: 24, height: 24}} - - "lucide", name -> - send(parent, {:fetched, name}) - {:error, :not_found} - end - - icons = Cache.get_icons("lucide", ["mail", "layer-arrow-up", "no-such-icon"], fetch_icon) - - assert Enum.map(icons, &elem(&1, 0)) == ["lucide:mail", "lucide:layer-arrow-up"] - assert Enum.all?(icons, fn {full_name, icon} -> icon.name == full_name end) - - refute_received {:fetched, "mail"} - assert_received {:fetched, "layer-arrow-up"} - assert_received {:fetched, "no-such-icon"} - end -end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..8965455 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start() +ExUnit.start(exclude: [:e2e]) From 4c530451360aa87cd670c61d749b5470e27ecc38 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 1 Oct 2026 13:02:37 +0400 Subject: [PATCH 3/3] Update iconify to 0.3.1 --- mix.lock | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mix.lock b/mix.lock index f6aa425..2154478 100644 --- a/mix.lock +++ b/mix.lock @@ -6,16 +6,18 @@ "ex_dna": {:hex, :ex_dna, "1.5.1", "c5181067eba53bfe7509fc8a1a5c018659d2be87e4b3bf682c9ce570c7d62f62", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: true]}, {:gen_lsp, "~> 0.11", [hex: :gen_lsp, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: true]}], "hexpm", "1b0653d3e8af5870063541a897b9f70937540053fb4555db5b81ec404428824f"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, - "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "iconify": {:hex, :iconify, "0.3.0", "c6652941484621d9400ad119d885c3c4914d906dbd1cab8b8fbf4fe48193fc05", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "4365d7838e0092affb9c9cf2049e900e491d092256294d5c48d352b0e6f8e954"}, + "finch": {:hex, :finch, "0.24.0", "4022b6194e907b6d9b597c168001cac38e488a35fe36cef9d8297e3159782510", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "33ba40069c3587c2f99f9125b766e19dad87d6d54be3c6961db2304df04cef00"}, + "hpax": {:hex, :hpax, "1.1.0", "782931867cc23217c68fb5f68fe1a11f5e7544c7fda82c8a7019a5df5a4a1cdf", [:mix], [], "hexpm", "0b8d0f05832f55571d65ac720f79bf8994138ffbb133209dc4685eae0ad456a8"}, + "iconify": {:hex, :iconify, "0.3.1", "2332a72d42bcfaf463c6b3be6d42a5ed2ed5859b357999d03bb181a4ba2b6eb2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:json_codec, "~> 0.3", [hex: :json_codec, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "7a77f60b523ae0187a45f188f7a746703e62a13a30c141af80103ebdab5e968e"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "json_codec": {:hex, :json_codec, "0.3.1", "81f738e212308c85344e11da00af6d8b52bd486224ee24c1f51b95df0604aeae", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:json_spec, "~> 1.2", [hex: :json_spec, repo: "hexpm", optional: false]}], "hexpm", "dffea3198d3f1fe9a39aa176ab0d6988b8c80dd85a67e913b323d4ec7c57ca9d"}, + "json_spec": {:hex, :json_spec, "1.2.0", "c67f73cdc4166e955b2f1095d3f44dd78c197337038f1a5301fb526b6c87ab04", [:mix], [], "hexpm", "3639961babad7d6bc45f3a2d96a49c18ec70614761bb84ce58b64e9fad9354de"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "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"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, + "mint": {:hex, :mint, "1.11.0", "a713551624815c0435237b93d90ea8b9b14254690c66f732d0ef8930f76ff1d9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "c6279ba2d6aa3a383a1d4cfbe7b59f42e6efd400f58d8e2acfeac48a438693ab"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, @@ -24,10 +26,10 @@ "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.3", "e8abe7f2acb96d98695e9290b35cb5a01a696cc2086d4ff2e0b3aa8b9c74675f", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "449affd6aea24daaa2f6b43748fc1e2c6a87610df996cc1f54e7b19a7a18e638"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, + "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, "reach": {:hex, :reach, "2.5.0", "c6b2116d0bab5454597479e7b0b6bc224fb3a262b9e15bb16877b4fdd42c3ada", [:mix], [{:boxart, "~> 0.3.3", [hex: :boxart, repo: "hexpm", optional: true]}, {:ex_ast, "~> 0.12.0", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:ex_dna, "~> 1.5", [hex: :ex_dna, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:libgraph, "~> 0.16.0", [hex: :libgraph, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: true]}, {:makeup_js, "~> 0.1", [hex: :makeup_js, repo: "hexpm", optional: true]}, {:quickbeam, "~> 0.10", [hex: :quickbeam, repo: "hexpm", optional: true]}], "hexpm", "b7fa8a1b5b8a1a55b16a0063ddbcedc63b747423b1cf4c26671919c3f56bbaf3"}, - "req": {:hex, :req, "0.5.18", "48e6431cb4135e8a7815e745177485369a9b4a9924d5fe68ca00eb09ceaed1ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21.0 or ~> 0.22.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "fa03812c440a9754bf34355e0c5d4f3ed316458db62e3284b7a352ef8dc0b996"}, + "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},