lib/foundry/context/router_introspector.ex

defmodule Foundry.Context.RouterIntrospector do
  @moduledoc """
  Introspects Phoenix router modules to extract LiveView route metadata.

  Used by Foundry to discover page nodes from `Router.__routes__()` without
  requiring per-LiveView annotations. Extracts route path, module, and dynamic flag.
  """

  @doc """
  Extract all LiveView routes from a router module.

  Returns a list of maps with keys: `:module`, `:path`, `:dynamic`, `:helper`.
  """
  @spec liveview_routes(module()) :: [map()]
  def liveview_routes(router_module) do
    router_module.__routes__()
    |> Enum.filter(&(&1.plug == Phoenix.LiveView.Plug))
    |> Enum.map(fn route ->
      %{
        module: route.plug_opts,
        path: route.path,
        dynamic: String.contains?(route.path, ":"),
        helper: route.helper
      }
    end)
  rescue
    _ -> []
  end

  @doc """
  Find the router module for a given app by scanning compiled BEAM files for `__routes__/0`.

  Returns the module atom or nil if not found.
  """
  @spec find_router(atom(), String.t()) :: module() | nil
  def find_router(app_name, project_root) do
    underscored = Macro.underscore(to_string(app_name))

    ebin_path =
      ["dev", "test"]
      |> Enum.map(&Path.join([project_root, "_build", &1, "lib", underscored, "ebin"]))
      |> Enum.find(&File.dir?/1)

    case ebin_path do
      nil ->
        nil

      path ->
        Code.append_path(path)

        Path.wildcard(Path.join(path, "*.beam"))
        |> Enum.map(&(&1 |> Path.basename(".beam") |> String.to_atom()))
        |> Enum.find(fn mod ->
          try do
            Code.ensure_loaded?(mod) and function_exported?(mod, :__routes__, 0)
          rescue
            _ -> false
          end
        end)
    end
  end
end