defmodule Svelixir.MixProject do use Mix.Project # Above `project/0`, not below it: a module attribute is read where it is # expanded, so declaring it after the function that uses it silently yields # nil — `mix hex.build` would then report the description as missing while # the source plainly shows one. @description "Scaffolder for Elixir/Phoenix projects with a Svelte 5 UI layer." # VERSION is the single source of truth; scripts/bump-version.sh writes it and # nothing else. Read here rather than duplicated as a literal, so the two can # never disagree. # # Resolved against __DIR__, never a bare "VERSION". A relative read resolves # against the CURRENT WORKING DIRECTORY, which is the project root only by # convention — it is not when this project is a path dependency, and it is # not for `mix cmd` from elsewhere. That is the same failure that made the # vendored typedstruct need a patch, and the reason Svelixir.Target exists. @version_path Path.join(__DIR__, "VERSION") @external_resource @version_path @version @version_path |> File.read!() |> String.trim() def project do [ app: :svelixir, version: @version, elixir: "~> 1.20", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, # Consolidation compiles a protocol's dispatch against the impls that # exist when the app compiles, and silently ignores any defined later. A # test that defines a section module gets `defimpl Vex.Extract` from # `use Vex.Struct`, but the test file compiles after consolidation, so # validating that struct raises Protocol.UndefinedError. Consolidation is # a production optimisation; :test is the one env that needs it off. consolidate_protocols: Mix.env() != :test, deps: deps(), description: @description, package: package(), test_coverage: [tool: ExCoveralls], dialyzer: [plt_add_apps: [:mix], ignore_warnings: ".dialyzer_ignore.exs"], docs: [main: "readme", extras: ["README.md"]] ] end # `files` is spelled out rather than left to the hex default, for two reasons # that both bite silently. # # The default list includes `priv` RECURSIVELY. priv/svelixir_new is a # gitignored STANDALONE git repository, so the default would package its # `.git` directory into a release; and priv/meta/*/{deps,_build} would ship # 2.5 MB of somebody else's compiled BEAMs. Only priv/templates, # priv/baselines.exs and the baseline SOURCES are ours to ship. # # `vendor` is not a hex default at all, and the package does not compile # without it: simple_enum, typedstruct and vex are on elixirc_paths from # there, not in deps/0. Omitting it produces a package that builds here and # fails for everyone else. defp package do [ licenses: ["MIT"], links: %{"GitHub" => "https://github.com/wimwian-org/svelixir"}, files: package_files() ] end # `**` is the WRONG fix for the priv/meta exclusion and the intuitive one: # `priv/meta/**/lib` matches priv/meta/x/_build/dev/lib and hex then pulls # that whole directory in (still 2.4 MB). There is no `!` exclusion syntax — # listing "!priv/..." fails the build with "Missing files:". Exclusion is only # achievable by enumerating includes. @meta_excluded ~r{^priv/meta/[^/]+/(deps|_build)/} # SORTED FILE paths, never directories — for every entry, not just priv/meta. # Given a directory entry hex writes its tar members in raw readdir order, # which is a filesystem name-hash and differs between APFS and ext4: the same # source tree would then produce a different package checksum on Linux CI. # Sorting pins it (D8). # VERSION is not optional here: mix.exs reads it at build time, so a package # without it does not compile for the consumer even though it builds fine in # this checkout. Same shape of bug as omitting vendor/. defp package_files do ~w(VERSION mix.exs README.md LICENSE.md CHANGELOG.md .formatter.exs priv/baselines.exs) |> Enum.concat(Path.wildcard("lib/**", match_dot: true)) |> Enum.concat(Path.wildcard("vendor/**", match_dot: true)) |> Enum.concat(Path.wildcard("priv/templates/**", match_dot: true)) |> Enum.concat(baseline_files()) |> Enum.reject(&File.dir?/1) |> Enum.sort() end defp baseline_files do Enum.reject(Path.wildcard("priv/meta/**", match_dot: true), &(&1 =~ @meta_excluded)) end # `preferred_cli_env` inside `def project` is deprecated at 1.20.3 and prints # a stack trace on every mix invocation — which would make "gates are clean" # false from commit one. def cli do [preferred_envs: [coveralls: :test, "coveralls.html": :test, "coveralls.json": :test]] end # :eex is inherited, not ours. Vendoring vex took its lib/ but not its # mix.exs, where `applications: [:eex]` was declared — so the app spec that # covered lib/vex/error_renderers/eex.ex came away with the file it applies # to. Without it dialyzer reports EEx.eval_string/2 as unknown, and a release # that selected vex's EEx error renderer would fail at runtime. def application, do: [extra_applications: [:logger, :eex]] # simple_enum, typedstruct and vex are vendored, not depended on — their lib/ # trees compile straight into this application. See vendor/README.md for the # versions and checksums. None has a runtime dependency of its own, which is # what makes this sound: there is no transitive graph to resolve. defp elixirc_paths(:test), do: ["lib", "test/support"] ++ vendor_paths() defp elixirc_paths(_), do: ["lib"] ++ vendor_paths() # Path.wildcard/1, NOT the literal "vendor/*/lib". Mix resolves each # elixirc_paths entry with :elixir_utils.read_file_type/1, which does not # expand globs — measured, a literal glob returns {:error, :enoent} and # contributes ZERO files with no error anywhere. The pre-existing # `["vendor/**/lib"]` in this file had the same defect and was never noticed # because vendor/ did not exist yet. defp vendor_paths, do: Path.wildcard("vendor/*/lib") # sourceror is deliberately absent: measured, `Sourceror.to_string/1` # reproduces comments verbatim, and stripping the comment nodes changes # blank-line rendering. `Macro.to_string/1` is correct. defp deps do [ {: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_doc, "~> 0.34", only: :dev, runtime: false}, {:excoveralls, "~> 0.18", only: :test}, {:ex_machina, "~> 2.8", only: :test}, {:faker, "~> 0.18", only: :test}, {:git_ops, "~> 2.6", only: :dev} ] end end