Packages

An Ash-native searchable select / combobox / faceted-search component for Phoenix LiveView.

Current section

Files

Jump to
flicker mix.exs
Raw

mix.exs

defmodule Flicker.MixProject do
  use Mix.Project

  @version "0.1.0"
  @source_url "https://github.com/team-alembic/flicker"

  def project do
    [
      app: :flicker,
      version: @version,
      elixir: "~> 1.17",
      # Needed for `Phoenix.LiveView.ColocatedHook`'s merged manifest
      # (`_build/#{Mix.env()}/phoenix-colocated/flicker/index.js`) to be
      # written at all — otherwise only the per-hook fragment files land in
      # `_build`, and nothing ever exports the `hooks` map a `LiveSocket`
      # needs. Real host apps normally get this setting from their own
      # Phoenix 1.8 boilerplate; flicker needs it itself for the dev
      # playground's `.Nav`/`.Palette`/`.FlickerSearchNav` hooks to mount
      # at all (Spec 005/007 — this was previously silently broken: the
      # playground rendered fine, but no client-side keyboard behaviour
      # ever ran, since `phx-hook="..."` had nothing to attach).
      compilers: [:phoenix_live_view] ++ Mix.compilers(),
      elixirc_paths: elixirc_paths(Mix.env()),
      consolidate_protocols: Mix.env() != :test,
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases(),
      package: package(),
      description: description(),
      name: "Flicker",
      source_url: @source_url,
      homepage_url: @source_url,
      docs: &docs/0,
      dialyzer: [
        # `:phoenix_test` — `Flicker.Test.search_select/3` calls it directly.
        # `:igniter_js` — `mix flicker.install` calls its parser; it's
        # `runtime: false`, so the app tree doesn't pull it into the PLT.
        plt_add_apps: [:mix, :ex_unit, :phoenix_test, :igniter_js],
        plt_core_path: "priv/plts",
        plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
      ],
      usage_rules: usage_rules()
    ]
  end

  def cli do
    [preferred_envs: [ci: :test, "test.coverage": :test]]
  end

  # `Dev.Application` (dev/application.ex) starts the dev playground's
  # Bandit-served endpoint (Spec 005) — wired as the `mod` callback only in
  # `:dev`, so `:test`/`:prod` builds never boot it even though `dev/` is on
  # `:test`'s `elixirc_paths` too (Spec 004's harness needs it compiled, not
  # running).
  def application do
    base = [extra_applications: [:logger]]
    if Mix.env() == :dev, do: base ++ [mod: {Dev.Application, []}], else: base
  end

  # `dev/` holds the seeded Ash demo domain (Spec 004's test harness) and the
  # dev playground built on it (Spec 005). It's Ash-dependent, so it's only
  # added to the test path when `ash` is actually in this build's deps — the
  # same `FLICKER_NO_ASH` switch `ash_deps/0` uses, so the no-ash CI leg
  # never compiles it. (Deps aren't compiled yet when `elixirc_paths/1` runs,
  # so `Code.ensure_loaded?/1` can't be used to detect this here.)
  defp elixirc_paths(:test) do
    if System.get_env("FLICKER_NO_ASH"), do: ["lib", "test/support"], else: ["lib", "test/support", "dev"]
  end

  defp elixirc_paths(:dev) do
    if System.get_env("FLICKER_NO_ASH"), do: ["lib"], else: ["lib", "dev"]
  end

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

  defp description do
    "An Ash-native searchable select / combobox / faceted-search component for Phoenix LiveView."
  end

  defp package do
    [
      maintainers: ["Team Alembic"],
      licenses: ["Apache-2.0"],
      links: %{
        "GitHub" => @source_url,
        "HexDocs" => "https://hexdocs.pm/flicker",
        "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md"
      },
      # `README.template.md` is the template-only file; rename.sh promotes
      # it to README.md before the first hex publish, so it isn't shipped.
      files: ~w(lib guides assets .formatter.exs mix.exs README.md LICENSE* CHANGELOG* usage-rules.md)
    ]
  end

  defp deps do
    [
      # Core — the only hard runtime dependency (ADR-006, ADR-008).
      {:phoenix_live_view, "~> 1.1"}
    ] ++ localize_deps() ++ ash_deps() ++ tooling_deps()
  end

  # CLDR display formatting (ADR-013): interval collapsing, number grouping,
  # plural-aware durations, list conjunctions, locale week rules. Optional —
  # without it `Flicker.Facet.Format` falls back to ISO 8601, plain digits and
  # English joins, which is the shipped behaviour for a host that doesn't
  # localise. Excluded entirely with `FLICKER_NO_LOCALIZE` set, mirroring
  # `FLICKER_NO_ASH`, so a CI leg can prove the fallback compiles and reads
  # correctly rather than taking it on trust.
  defp localize_deps do
    if System.get_env("FLICKER_NO_LOCALIZE") do
      []
    else
      [{:localize, "~> 1.2", optional: true}] ++ json_polyfill_deps()
    end
  end

  # `localize` needs OTP 27's `:json` module. The oldest CI leg runs the
  # OTP 26 floor (ADR-008), so the suite gets the polyfill there. A host on
  # OTP 26 adds it itself, as `localize`'s startup error tells it to.
  defp json_polyfill_deps do
    if String.to_integer(System.otp_release()) < 27 do
      [{:json_polyfill, "~> 0.2", only: [:dev, :test]}]
    else
      []
    end
  end

  # `ash`/`ash_phoenix` are optional runtime deps (ADR-006, ADR-008): the
  # built-in `Flicker.Providers.AshResource` needs them, core does not.
  # Excluded entirely with `FLICKER_NO_ASH` set — the no-ash CI leg runs
  # `mix deps.get` with that set so `ash` never lands in its lock file,
  # forcing the `Code.ensure_loaded?/1` compile boundary to hold for real.
  defp ash_deps do
    if System.get_env("FLICKER_NO_ASH") do
      []
    else
      [
        {:ash, "~> 3.0", optional: true},
        {:ash_phoenix, "~> 2.0", optional: true},
        # SAT solver `Ash.Policy.Authorizer` needs — only our own dev/test
        # harness (the policy-bearing `Dev.Music.Artist`) uses policies, so
        # this is dev/test-only, not part of the published optional deps.
        {:picosat_elixir, "~> 0.2", only: [:dev, :test], runtime: false},
        # Optional interop target (Spec 009, ADR-006 pattern): a `Cinder`
        # table/collection can be driven by a `Flicker.search` filter via
        # query composition. Nothing under `lib/` calls `Cinder.*` directly
        # — this is dev/test-only so the playground page and its test can
        # exercise the recipe; a real host adds `cinder` itself. Lives in
        # `ash_deps/0`, not `tooling_deps/0`, because `cinder` hard-depends
        # on `ash` itself — pulling it in unconditionally would drag `ash`
        # back into the no-ash CI leg's dependency tree.
        {:cinder, "~> 0.15", optional: true, only: [:dev, :test]}
      ]
    end
  end

  defp tooling_deps do
    [
      # Docs
      {:ex_doc, "~> 0.34", only: [:dev, :test], runtime: false},

      # Test support
      {:phoenix_test, "~> 0.11", only: [:dev, :test], runtime: false},
      # Property-testing the query parser (Spec 003) — a direct dep so it's
      # available on the no-ash CI leg too, where `ash` (which also brings
      # `stream_data` transitively) is deliberately absent. No `:only`
      # restriction: `ash` depends on it unrestricted (`env: :prod`), and
      # Mix requires matching `:only` constraints across the dep graph.
      {:stream_data, "~> 1.0", runtime: false},
      # `mix_audit`'s `req` and `phoenix_test`'s `plug` want `mime` in
      # different envs; pin it ourselves so the two don't diverge.
      {:mime, "~> 2.0", only: [:dev, :test], override: true},

      # Quality
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:doctor, "~> 0.21", only: [:dev, :test], runtime: false},
      {:ex_check, "~> 0.16", only: [:dev, :test], runtime: false},
      {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
      {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},

      # Dev QoL
      {:mix_test_watch, "~> 1.2", only: [:dev, :test], runtime: false},
      {:doctest_formatter, "~> 0.3", only: [:dev, :test], runtime: false},

      # Formatter plugin — rewrites code based on .credo.exs rules.
      {:quokka, "~> 2.12", only: [:dev, :test], runtime: false},

      # Ash-aware Credo checks (opt-in for Ash packages). Also enable the
      # plugin in .credo.exs and switch the `lint` alias to compile before
      # credo — the compiled-introspection checks need it.
      # {:ash_credo, "~> 0.7", only: [:dev, :test], runtime: false},

      # Release automation (conventional commits -> CHANGELOG + tag)
      {:git_ops, "~> 2.6", only: [:dev, :test], runtime: false},

      # Syncs usage-rules.md from deps into AGENTS.md or agent skills.
      {:usage_rules, "~> 1.1", only: [:dev], runtime: false},

      # Needed by `mix igniter.install` (the installer, downstream), our own
      # `mix usage_rules.sync`, and the installer's tests (`:test`) —
      # optional, not a hard runtime dep.
      {:igniter, "~> 0.6", optional: true, only: [:dev, :test], runtime: false},

      # AST-based JS codemods (Rust NIF parser) — the installer uses this to
      # wire Flicker's colocated hook into `assets/js/app.js` via a real JS
      # parse (`extend_hook_object`/`insert_imports`) rather than fragile
      # regex string-patching. Optional and dev/test-only, same as igniter.
      {:igniter_js, "~> 0.4", optional: true, only: [:dev, :test], runtime: false},

      # HTTP server for the dev playground's endpoint (Spec 005) — never
      # shipped. Also `:test`-only (not started there, just compiled) so the
      # browser suite (Spec 007) can boot `Dev.Endpoint` for real over HTTP
      # instead of `Phoenix.ConnTest`'s in-process dispatch, which axe-core
      # and Wallaby both need a rendered DOM to run against.
      {:bandit, "~> 1.0", only: [:dev, :test]},

      # The dev playground has no asset pipeline, so nothing else would notice
      # an edit to `dev/` or `lib/flicker/`: `mix dev` is a plain
      # `run --no-halt`, and a long-running server otherwise serves stale
      # markup indefinitely (which has already cost us two phantom
      # regressions). `:dev` only — the same endpoint is booted in `:test` by
      # Spec 007's browser suite, which must not recompile mid-request.
      {:phoenix_live_reload, "~> 1.5", only: :dev}
    ] ++ browser_deps()
  end

  # `FLICKER_NO_BROWSER` drops these from the oldest CI leg: Wallaby's HTTP client chain needs a newer OTP than flicker's floor, and that leg never runs the browser suite. The browser modules compile only when Wallaby is loaded.
  #
  # Browser-driven tests (Spec 007): axe-core accessibility scans and
  # client-side keyboard behaviour ExUnit/PhoenixTest can't reach (real
  # arrow-key/aria-activedescendant/focus-trap DOM behaviour). `a11y_audit`
  # vendors axe-core itself — no npm — and drives it through a Wallaby
  # session. `:test`-only, tagged `@moduletag :browser` and excluded from
  # the default `mix test` run (see `.github/workflows/elixir.yml`'s
  # `browser` job and `test/support/browser_case.ex`).
  defp browser_deps do
    if System.get_env("FLICKER_NO_BROWSER") do
      []
    else
      [
        {:a11y_audit, "~> 0.4", only: :test, runtime: false},
        {:wallaby, "~> 0.30", only: :test, runtime: false}
      ]
    end
  end

  defp aliases do
    [
      ci: [
        "deps.unlock --check-unused",
        "format --check-formatted",
        "credo --strict",
        "doctor --full --raise",
        "sobelow --config",
        "hex.audit",
        "deps.audit",
        "dialyzer",
        "test"
      ],
      credo: ["credo --strict"],
      # If you enable ash_credo, use `mix lint` instead of `mix credo` — the
      # compiled-introspection checks need modules to be compiled first.
      lint: ["compile", "credo --strict"],
      sobelow: ["sobelow --config"],
      # Starts the dev playground (Spec 005): seeded `Dev.Music` domain,
      # Bandit-served endpoint, no database.
      dev: ["run --no-halt"]
    ]
  end

  defp docs do
    [
      main: "readme",
      logo: "assets/flicker-logo.png",
      favicon: "assets/flicker-logo.png",
      source_ref: "v#{@version}",
      source_url_pattern: "#{@source_url}/blob/v#{@version}/%{path}#L%{line}",
      filter_modules: ~r/^Elixir\.(Flicker|Mix\.Tasks\.Flicker\.)/,
      skip_code_autolink_to: &String.starts_with?(&1, "Dev."),
      extra_section: "GUIDES",
      extras: extras(),
      groups_for_extras: [
        "Getting started": ["guides/getting-started.md"],
        Concepts: [
          "guides/providers.md",
          "guides/theming.md"
        ],
        Features: [
          "guides/faceted-search.md",
          "guides/command-palette.md"
        ],
        Accessibility: ["guides/accessibility.md"],
        Integrations: ["guides/cinder-integration.md"]
      ],
      groups_for_modules: [
        "Public API": [
          Flicker,
          Flicker.Facet,
          Flicker.Messages,
          Flicker.Provider,
          Flicker.Query,
          Flicker.Result,
          Flicker.Theme
        ],
        Providers: [
          Flicker.Providers.AshResource,
          Flicker.Providers.Static
        ],
        Integrations: [
          Flicker.AshPhoenixForm,
          Flicker.Integrations.Cinder
        ],
        Testing: [Flicker.Test],
        "Mix tasks": [Mix.Tasks.Flicker.Install],
        "Component internals": ~r/^Flicker\.(Components|CursorContext|FacetSuggest|Keyboard|Messages\.English)/
      ]
    ]
  end

  defp extras do
    ["README.md", "CHANGELOG.md", "usage-rules.md"] ++
      Path.wildcard("guides/**/*.{md,cheatmd,livemd}")
  end

  # `mix usage_rules.sync` materialises this config into committed
  # `.claude/skills/*/SKILL.md` files. Re-run after adding or removing deps.
  defp usage_rules do
    [
      skills: [
        location: ".claude/skills",

        # Auto-generate a `use-<pkg>` skill per listed dependency. Each skill
        # references that package's shipped `usage-rules.md` (if any).
        deps: [
          # :ash, :phoenix, :ecto, :req,
          # ~r/^ash_/,
        ],

        # Pre-built skills shipped by packages in their `usage-rules/skills/`
        # directory — opt in per package.
        package_skills: [
          # :ash, ~r/^ash_/,
        ],

        # Custom composed skills that pull rules from multiple deps and
        # ship them as a single SKILL.md.
        build: [
          # "ash-framework": [
          #   description: "Use when working with Ash Framework or any of its extensions.",
          #   usage_rules: [:ash, ~r/^ash_/]
          # ],
          # "phoenix-framework": [
          #   description: "Use when working with Phoenix — controllers, LiveViews, routes, views.",
          #   usage_rules: [:phoenix, ~r/^phoenix_/]
          # ]
        ]
      ]
    ]
  end
end