Current section
Files
Jump to
Current section
Files
mix.exs
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