Current section

Files

Jump to
mob mix.exs
Raw

mix.exs

defmodule Mob.MixProject do
  use Mix.Project

  def project do
    [
      app: :mob,
      version: "0.9.4",
      elixir: "~> 1.19",
      start_permanent: Mix.env() == :prod,
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: compilers(Mix.env()) ++ Mix.compilers(),
      deps: deps(),
      aliases: aliases(),
      unused: [
        ignore: [
          # GenServer / behaviour callbacks (mix_unused can't see them).
          {:_, :init, 1},
          {:_, :handle_call, 3},
          {:_, :handle_cast, 2},
          {:_, :handle_info, 2},
          {:_, :handle_continue, 2},
          {:_, :terminate, 2},
          {:_, :code_change, 3},
          # Mob's public render-tree API — every component module exposes
          # render/1 callable from app code. mix_unused sees no internal
          # caller because it lives in user apps.
          {~r/^Mob\..+/, :render, 1},
          # Screen lifecycle callbacks dispatched by the Mob.Screen
          # GenServer wrapper.
          {~r/^Mob\..+/, :mount, 3},
          {~r/^Mob\..+/, :on_start, 0},
          {~r/^Mob\..+/, :on_resume, 1},
          # Screen state persistence — generated by `use Mob.Screen, vsn: N`.
          {~r/^Mob\..+/, :__mob_vsn__, 0},
          {~r/^Mob\..+/, :__mob_persist__, 0},
          {~r/^Mob\..+/, :dump_state, 1},
          {~r/^Mob\..+/, :load_state, 2},
          {~r/^Mob\..+/, :screen_key, 1},
          # NIF stub functions in src/mob_nif.erl (apply'd via load_nif).
          {:mob_nif, :_, :_},
          # Mob.Test diagnostic API surface — IEx-driven, no internal
          # caller in mob itself.
          {~r/^Mob\.Test$/, :_, :_},
          # Theme accessors — used by user apps via Mob.Theme.<name>.
          {~r/^Mob\.Theme\..+/, :_, :_}
        ]
      ],
      description: "BEAM-on-device mobile framework for Elixir",
      source_url: "https://github.com/genericjam/mob",
      homepage_url: "https://hexdocs.pm/mob",
      package: package(),
      docs: docs()
    ]
  end

  def application do
    [
      # :public_key is needed by Mob.Certs at runtime; Elixir 1.19+ strips
      # unused OTP applications from the code path, so it must be declared
      # here even though mob doesn't *start* it directly.
      extra_applications: [:logger, :public_key]
    ]
  end

  # Two hexdocs-only injections: (1) classify unstyled <pre><code> blocks as
  # Elixir so they get syntax-highlighted, and (2) render ```mermaid fenced
  # blocks as SVG (ex_doc has no built-in mermaid support; GitHub renders
  # them natively, so READMEs work there without this).
  defp before_closing_body_tag(:html) do
    """
    <script>
      // Default unstyled code blocks to Elixir highlighting.
      document.querySelectorAll("pre code").forEach(el => {
        if (!el.className) el.className = "language-elixir";
      });
    </script>
    <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
    <script>
      document.addEventListener("DOMContentLoaded", function () {
        // ex_doc marks dark mode by toggling `dark` on <body> (its stylesheet
        // is written against `body.dark`). Reading the class rather than
        // prefers-color-scheme covers all three of ex_doc's settings —
        // light, dark, and system — because ex_doc re-toggles the class
        // itself when the OS scheme changes under "system".
        const isDark = () => document.body.classList.contains("dark");

        // Capture each diagram's source BEFORE the first render replaces the
        // <pre>, otherwise a later theme switch has nothing to re-render from.
        const diagrams = [];
        document.querySelectorAll("pre code.mermaid").forEach((codeEl, i) => {
          const host = document.createElement("div");
          host.className = "mermaid-diagram";
          codeEl.closest("pre").replaceWith(host);
          diagrams.push({ host: host, source: codeEl.textContent, id: "mermaid-graph-" + i });
        });

        if (diagrams.length === 0) return;

        // Renders are async, so a fast light→dark→light toggle can land its
        // results out of order. Stamp each pass and drop stale ones.
        let pass = 0;
        function renderAll() {
          const token = ++pass;
          mermaid.initialize({ startOnLoad: false, theme: isDark() ? "dark" : "default" });
          for (const d of diagrams) {
            // mermaid requires a unique graph id per render call.
            mermaid.render(d.id + "-" + token, d.source).then(({ svg, bindFunctions }) => {
              if (token !== pass) return;
              d.host.innerHTML = svg;
              if (bindFunctions) bindFunctions(d.host);
            });
          }
        }

        renderAll();

        // <body>'s class list also churns for search focus and sticky scroll,
        // so only re-render when the theme itself actually flipped.
        let wasDark = isDark();
        new MutationObserver(function () {
          if (isDark() === wasDark) return;
          wasDark = isDark();
          renderAll();
        }).observe(document.body, { attributes: true, attributeFilter: ["class"] });
      });
    </script>
    """
  end

  defp before_closing_body_tag(_), do: ""

  defp docs do
    [
      main: "readme",
      before_closing_body_tag: &before_closing_body_tag/1,
      logo: "assets/logo/logo_full_color.png",
      source_url: "https://github.com/genericjam/mob",
      source_url_pattern: "https://github.com/genericjam/mob/blob/master/%{path}#L%{line}",
      extras: [
        "README.md": [title: "Mob"],
        "CHANGELOG.md": [title: "Changelog"],
        "MOB_PLUGINS.md": [title: "Plugins — Manifest Reference"],
        "MOB_PLUGIN_SECURITY.md": [title: "Plugins — Security & Trust"],
        "MOB_STYLES.md": [title: "Styles — Manifest Reference"],
        "MOB_FONTS.md": [title: "Fonts — Design Reference"],
        "guides/why_beam.md": [title: "Why the BEAM?"],
        "guides/getting_started.md": [title: "Getting Started"],
        "guides/packages.md": [title: "First-Party Packages"],
        "guides/architecture.md": [title: "Architecture & Prior Art"],
        "guides/screen_lifecycle.md": [title: "Screen Lifecycle"],
        "guides/events.md": [title: "Events"],
        "guides/event_model.md": [title: "Event Model"],
        "guides/background_execution.md": [title: "Background Execution"],
        "guides/components.md": [title: "Components"],
        "guides/styling.md": [title: "Styling & Native Rendering"],
        "guides/theming.md": [title: "Theming"],
        "guides/navigation.md": [title: "Navigation"],
        "guides/device_capabilities.md": [title: "Device Capabilities"],
        "guides/mobile_surface_matrix.md": [title: "Mobile Surface Matrix"],
        "guides/permissions.md": [title: "Permissions"],
        "guides/native_extensions.md": [title: "Native Extensions (NIFs, features)"],
        "guides/plugins.md": [title: "Writing a Plugin"],
        "guides/dns_on_ios.md": [title: "DNS on iOS"],
        "guides/push_notifications.md": [title: "Push Notifications"],
        "guides/data.md": [title: "Data & Persistence"],
        "guides/testing.md": [title: "Testing"],
        "guides/tooling.md": [title: "Tooling & Formatting"],
        "guides/publishing.md": [title: "Publishing to App Store / TestFlight"],
        "guides/troubleshooting.md": [title: "Troubleshooting"],
        "guides/support_matrix.md": [title: "Device Support Matrix"],
        "guides/liveview.md": [title: "LiveView Mode"],
        "guides/ios_physical_device.md": [title: "iOS Physical Devices"],
        "guides/agentic_coding.md": [title: "Agentic Coding"]
      ],
      groups_for_extras: [
        Guides: ~r/guides\/.*/,
        Plugins: ["MOB_PLUGINS.md", "MOB_PLUGIN_SECURITY.md", "MOB_STYLES.md", "MOB_FONTS.md"]
      ],
      groups_for_modules: [
        Core: [Mob, Mob.App, Mob.Screen, Mob.ScreenState, Mob.Socket, Mob.State],
        UI: [
          Mob.UI,
          Mob.Style,
          Mob.Renderer,
          Mob.Composite,
          Mob.Theme,
          Mob.Theme.Light,
          Mob.Theme.Dark,
          Mob.Theme.Adaptive
        ],
        Navigation: [Mob.Nav, Mob.Nav.Registry, Mob.Router],
        "Runtime Processes": [Mob.Screen.Server, Mob.Listener, Mob.Sender],
        Plugins: [Mob.Plugins, Mob.Plugins.Supervisor, Mob.Plugins.Lifecycle],
        "Device APIs": [
          Mob.Haptic,
          Mob.Clipboard,
          Mob.Share,
          Mob.Permissions,
          Mob.Files,
          Mob.Audio,
          Mob.Motion
        ],
        "Testing & Debugging": [Mob.Test, Mob.ScreenCase, Mob.RenderStats],
        Tooling: [Mob.Formatter],
        Internals: [Mob.Dist, Mob.NativeLogger, Mob.List, Mob.Sigil]
      ]
    ]
  end

  defp elixirc_paths(:test),
    do: ["lib", "test/support", "test/onboarding", "test/onboarding/support"]

  defp elixirc_paths(_), do: ["lib"]

  defp package do
    [
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/genericjam/mob"},
      # `lib/mix/tasks/mob.flake.ex` is deliberately absent: it is a
      # repo-internal tool that shells out to `mix test` in whatever project
      # loads it, and its docs quote this repo's own bisect history. Every app
      # depending on mob would otherwise gain a `mix mob.flake`.
      exclude_patterns: ["lib/mix/tasks/mob.flake.ex"],
      files: ~w(
        lib src priv
        android ios assets
        mix.exs mix.lock
        README.md CHANGELOG.md LICENSE
      )
    ]
  end

  defp aliases do
    # `mix setup` after cloning installs deps and activates the shared git
    # hooks (.githooks): format / Credo --strict / compile run on every push
    # and the full suite when mix.exs changes — the same gate CI enforces.
    [setup: ["deps.get", "cmd git config core.hooksPath .githooks"]]
  end

  defp deps do
    [
      {:ex_ast, "~> 0.12", only: [:dev, :test], runtime: false},
      {:reach, "~> 2.7", only: [:dev, :test], runtime: false},
      {:recon, "~> 2.5", only: [:dev, :test]},
      # HTML/HEEx template engine — same one Phoenix uses
      # {:phoenix_live_view, "~> 1.0", optional: true},  # add when HEEx rendering lands
      {:nimble_parsec, "~> 1.0"},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:jump_credo_checks, "~> 0.1.0", only: [:dev, :test], runtime: false},
      # ex_slop — Credo check that catches AI-generated Elixir patterns
      # (blanket rescue, narrator-style docs, redundant Enum chains, etc).
      # Wired in via .credo.exs as `{ExSlop, []}` in the enabled list.
      {:ex_slop, "~> 0.4", only: [:dev, :test], runtime: false},
      {:erlfmt, "~> 1.8", only: :dev, runtime: false},
      # mix_audit — CVE scan over mix.lock against the Erlef advisory feed.
      # Invocation note: `mix deps.audit` alone fails with
      # `YamlElixir.read_from_file/1 is undefined` because mix_audit doesn't
      # ensure_all_started its yaml_elixir transitive dep before parsing
      # the advisory files. CI works around this with `mix do app.start +
      # deps.audit` (the app.start prefix starts the host app, which
      # transitively starts yaml_elixir via the runtime tree).
      {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
      # Known Elixir 1.20-rc.4 dep warning (cosmetic, dev-only):
      #   lib/mix_unused/filter.ex:61 — `_.._ inside match is deprecated`.
      # No upstream fix shipped yet (0.4.1 is latest, from 2024). Bump
      # this version + drop this comment once mix_unused ships a 1.20-clean
      # release.
      {:mix_unused, "~> 0.4", only: :dev, runtime: false},
      {:ecto_sqlite3, "~> 0.18", only: :test}
    ]
  end

  # `:unused` only runs in dev. Keeps test/CI logs clean.
  defp compilers(:dev), do: [:unused]
  defp compilers(_), do: []
end