Packages

Route policy and runtime contracts for Phoenix apps that go mobile.

Current section

Files

Jump to
crosswake mix.exs
Raw

mix.exs

defmodule Crosswake.MixProject do
  use Mix.Project

  # x-release-please-version
  @version "0.2.1"
  @source_url "https://github.com/szTheory/crosswake"

  def project do
    [
      app: :crosswake,
      version: @version,
      name: "crosswake",
      elixir: "~> 1.19",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      aliases: aliases(),
      description: description(),
      source_url: @source_url,
      homepage_url: @source_url,
      package: package(),
      docs: docs()
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Shared test fixtures (test/support/*.ex) are compiled once as project
  # modules in the test env, instead of being loaded per-file via
  # Code.require_file/2. The latter races during parallel test compilation —
  # multiple test files requiring the same fixture concurrently intermittently
  # fails with {:error, :enoent}.
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    # D-21: No companion-conditional dep blocks. Core names no companion in any env.
    # The conditional env-hack blocks were deleted in Phase 130 — companions now live
    # as standalone Hex packages (crosswake_rulestead in Phase 130, crosswake_rindle in
    # Phase 132, crosswake_sigra in Phase 137, crosswake_chimeway in Phase 138). Their
    # dependencies are declared in the companion's own mix.exs. The dress-rehearsal for
    # each companion runs via `mix companions.test` alias (runs each package's own test
    # lane separately).
    [
      {:jason, "~> 1.4"},
      {:nimble_options, "~> 1.1"},
      {:phoenix, "~> 1.8"},
      {:phoenix_live_view, "~> 1.1"},
      {:telemetry, "~> 1.0"},
      {:ex_doc, "~> 0.38", only: :dev, runtime: false},
      # Phase 154: required by Phoenix.LiveViewTest (element/2, render/1 parsing) the
      # moment core ships a real Phoenix.LiveViewTest round trip (test/support/
      # bridge_live_view_case.ex) — phoenix_live_view's own installed runtime raises
      # naming this exact package if it is absent.
      {:lazy_html, ">= 0.1.0", only: :test}
    ]
  end

  # D-26: Root aliases so contributors never need a bare `cd`.
  # mix companions.test — runs each companion's own test lane (rulestead + rindle + sigra + chimeway + threadline).
  #   Each lane's default tag exclusions apply (engine-present advisory + example-host
  #   tests are excluded). Adapter-behavior tests run with the engine in the lane's lock.
  # mix verify — runs companions.test + core hermetic test lane (excludes advisory tags).
  defp aliases do
    [
      "companions.test": [
        # deps.get each package first so the alias is self-sufficient on a fresh checkout
        # (the lanes only fetch one package's deps; `mix test` does not auto-fetch).
        "cmd --cd packages/crosswake_rulestead mix deps.get --check-locked",
        "cmd --cd packages/crosswake_rulestead mix test",
        "cmd --cd packages/crosswake_rindle mix deps.get --check-locked",
        "cmd --cd packages/crosswake_rindle mix test",
        "cmd --cd packages/crosswake_sigra mix deps.get --check-locked",
        "cmd --cd packages/crosswake_sigra mix test",
        "cmd --cd packages/crosswake_chimeway mix deps.get --check-locked",
        "cmd --cd packages/crosswake_chimeway mix test",
        "cmd --cd packages/crosswake_threadline mix deps.get --check-locked",
        "cmd --cd packages/crosswake_threadline mix test"
      ],
      verify: [
        "companions.test",
        "cmd mix test --only phase41_nested_process --seed 748644 --max-cases 1",
        "cmd mix test --exclude phase41_nested_process --exclude requires_example_host --exclude advisory_only --seed 748644 --max-cases 8"
      ]
    ]
  end

  defp description do
    "Route policy and runtime contracts for Phoenix apps that go mobile."
  end

  defp package do
    [
      name: "crosswake",
      licenses: ["Apache-2.0"],
      links: %{
        "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md",
        "Documentation" => "https://hexdocs.pm/crosswake",
        "GitHub" => @source_url
      },
      files: ~w(lib priv mix.exs README.md LICENSE CHANGELOG.md guides),
      exclude_patterns: ["brandbook"]
    ]
  end

  defp docs do
    [
      logo: "brandbook/logo/crosswake-mark.svg",
      favicon: "brandbook/logo/favicon.svg",
      main: "readme",
      source_ref: "v#{@version}",
      source_url: @source_url,
      formatters: ["html"],
      before_closing_head_tag: &before_closing_head_tag/1,
      before_closing_body_tag: &before_closing_body_tag/1,
      extras: [
        "README.md",
        "guides/see_it_run.md",
        "guides/architecture.md",
        "guides/code-walkthrough.md",
        "guides/physical_iphone_handoff.md",
        "CHANGELOG.md",
        "LICENSE",
        "guides/install.md",
        "guides/route_policy.md",
        "guides/web_to_mobile_migration.md",
        "guides/troubleshooting.md",
        "guides/support_matrix.md",
        "guides/capability_map.md",
        "guides/adopter_profiles.md",
        "guides/adoption.md",
        "guides/user_flows.md",
        "guides/capabilities.md",
        "guides/bridge.md",
        "guides/offline.md",
        "guides/tokens.md",
        "guides/commerce.md",
        "guides/companions.md",
        "guides/companion_contract.md",
        "guides/companion_compatibility.md",
        "guides/compatibility.md",
        "guides/native_shell.md",
        "guides/native_shell_upgrade.md",
        "guides/android_uat.md",
        "guides/packs.md",
        "guides/threadline.md",
        "guides/telemetry.md"
      ],
      groups_for_modules: [
        Policy: [Crosswake.Policy, Crosswake.Router],
        Bridge: ~r/Crosswake\.Bridge(\.|$)/,
        Manifest: [Crosswake.Manifest],
        Capabilities: ~r/Crosswake\.(Commerce|Offline|Packs)/,
        "Companion Contract": [
          Crosswake.Companion,
          Crosswake.Companion.State,
          Crosswake.Compatibility.Finding,
          Crosswake.Compatibility.Target,
          Crosswake.Manifest.Types.RouteEntry
        ],
        Telemetry: [
          Crosswake.Telemetry,
          Crosswake.Offline.Telemetry
        ]
      ],
      groups_for_extras: [
        Start: [
          "README.md",
          "guides/see_it_run.md",
          "guides/architecture.md",
          "guides/code-walkthrough.md",
          "guides/route_policy.md",
          "guides/install.md",
          "guides/physical_iphone_handoff.md"
        ],
        Adopt: [
          "guides/web_to_mobile_migration.md",
          "guides/user_flows.md",
          "guides/adopter_profiles.md",
          "guides/adoption.md"
        ],
        "Runtime Owners": [
          "guides/native_shell.md",
          "guides/bridge.md",
          "guides/capabilities.md",
          "guides/offline.md",
          "guides/packs.md",
          "guides/tokens.md",
          "guides/commerce.md"
        ],
        Truth: [
          "guides/troubleshooting.md",
          "guides/support_matrix.md",
          "guides/capability_map.md",
          "guides/compatibility.md",
          "guides/android_uat.md"
        ],
        Telemetry: [
          "guides/telemetry.md"
        ],
        "Extension Authors": [
          "guides/companion_contract.md",
          "guides/companion_compatibility.md"
        ],
        "Advanced/Companions": [
          "guides/companions.md",
          "guides/threadline.md",
          "CHANGELOG.md",
          "LICENSE"
        ]
      ]
    ]
  end

  defp before_closing_head_tag(:html) do
    ~S"""
    <script defer src="https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/mermaid.min.js" integrity="sha384-yQ4mmBBT+vhTAwjFH0toJXNYJ6O4usWnt6EPIdWwrRvx2V/n5lXuDZQwQFeSFydF" crossorigin="anonymous"></script>
    <style>
      .crosswake-mermaid {
        max-width: 100%;
        overflow-x: auto;
        padding-block: 0.75rem;
        background: transparent;
      }

      .crosswake-mermaid svg {
        display: block;
        min-width: 38rem;
        max-width: 100%;
        height: auto;
        margin-inline: auto;
        background: transparent;
      }

      @media (prefers-reduced-motion: reduce) {
        .crosswake-mermaid,
        .crosswake-mermaid * {
          scroll-behavior: auto !important;
          transition-duration: 0.01ms !important;
          animation-duration: 0.01ms !important;
          animation-iteration-count: 1 !important;
        }
      }
    </style>
    """
  end

  defp before_closing_head_tag(_formatter), do: ""

  defp before_closing_body_tag(:html) do
    ~S"""
    <script>
      (() => {
        if (window.crosswakeMermaidController) return;

        const controller = {
          queue: Promise.resolve(),
          counter: 0,
          dark: null,
          observer: null,
          warned: false
        };

        const themeVariables = (dark) => dark ? {
          background: "#09141A",
          primaryColor: "#0F1E26",
          primaryTextColor: "#F7F1E6",
          primaryBorderColor: "#254855",
          secondaryColor: "#162B35",
          secondaryTextColor: "#F7F1E6",
          secondaryBorderColor: "#254855",
          tertiaryColor: "#0F1E26",
          tertiaryTextColor: "#F7F1E6",
          tertiaryBorderColor: "#254855",
          lineColor: "#4E9A8E",
          textColor: "#F7F1E6",
          mainBkg: "#0F1E26",
          nodeBorder: "#254855",
          clusterBkg: "#162B35",
          clusterBorder: "#254855",
          titleColor: "#F7F1E6",
          edgeLabelBackground: "#09141A"
        } : {
          background: "#F7F1E6",
          primaryColor: "#F7F1E6",
          primaryTextColor: "#09141A",
          primaryBorderColor: "#254855",
          secondaryColor: "#EFE6D6",
          secondaryTextColor: "#09141A",
          secondaryBorderColor: "#254855",
          tertiaryColor: "#C9D4CF",
          tertiaryTextColor: "#09141A",
          tertiaryBorderColor: "#254855",
          lineColor: "#2B756A",
          textColor: "#09141A",
          mainBkg: "#F7F1E6",
          nodeBorder: "#254855",
          clusterBkg: "#EFE6D6",
          clusterBorder: "#254855",
          titleColor: "#09141A",
          edgeLabelBackground: "#F7F1E6"
        };

        const warnOnce = (message, error) => {
          if (controller.warned) return;
          controller.warned = true;
          console.warn(`[crosswake docs] ${message}; showing Mermaid source instead.`, error);
        };

        const fallback = (source, output, error) => {
          if (output) output.remove();
          source.hidden = false;
          warnOnce("Mermaid rendering failed", error);
        };

        const renderAll = async () => {
          const sources = Array.from(document.querySelectorAll("pre > code.mermaid"))
            .map((code) => ({code, source: code.parentElement}));

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

          if (!window.mermaid) {
            sources.forEach(({source}) => {
              const output = source.nextElementSibling;
              fallback(
                source,
                output && output.classList.contains("crosswake-mermaid") ? output : null,
                new Error("pinned Mermaid renderer unavailable")
              );
            });
            return;
          }

          const dark = document.body.classList.contains("dark");
          window.mermaid.initialize({
            startOnLoad: false,
            securityLevel: "strict",
            theme: "base",
            flowchart: {htmlLabels: false},
            themeVariables: themeVariables(dark)
          });

          for (const {code, source} of sources) {
            let output = source.nextElementSibling;
            if (!output || !output.classList.contains("crosswake-mermaid")) {
              output = document.createElement("div");
              output.className = "crosswake-mermaid";
              source.insertAdjacentElement("afterend", output);
            }

            try {
              const id = `crosswake-mermaid-${Date.now()}-${++controller.counter}`;
              const rendered = await window.mermaid.render(id, code.textContent);
              output.innerHTML = rendered.svg;
              output.dataset.theme = dark ? "dark" : "light";
              const svg = output.querySelector("svg");
              if (svg) svg.setAttribute("role", "img");
              if (rendered.bindFunctions) rendered.bindFunctions(output);
              source.hidden = true;
            } catch (error) {
              fallback(source, output, error);
            }
          }
        };

        const queueRender = () => {
          controller.queue = controller.queue.catch(() => undefined).then(renderAll);
          return controller.queue;
        };

        const observeTheme = () => {
          if (controller.observer || !document.body) return;
          controller.dark = document.body.classList.contains("dark");
          controller.observer = new MutationObserver(() => {
            const dark = document.body.classList.contains("dark");
            if (dark !== controller.dark) {
              controller.dark = dark;
              queueRender();
            }
          });
          controller.observer.observe(document.body, {attributes: true, attributeFilter: ["class"]});
        };

        const boot = () => {
          observeTheme();
          queueRender();
        };

        window.crosswakeMermaidController = controller;
        window.addEventListener("exdoc:loaded", boot);

        if (document.readyState === "loading") {
          document.addEventListener("DOMContentLoaded", boot, {once: true});
        } else {
          boot();
        }
      })();
    </script>
    """
  end

  defp before_closing_body_tag(_formatter), do: ""
end