Packages

Scaffolder for Elixir/Phoenix projects with a Svelte 5 UI layer.

Current section

Files

Jump to
svelixir mix.exs
Raw

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: docs()
]
end
# `filter_modules` WHITELISTS ours rather than blacklisting the vendored
# roots. simple_enum, typedstruct, vex and sourceror compile straight into
# this application from `vendor/`, so without a filter hexdocs publishes
# Vex.Validators.Length and Sourceror.Zipper under svelixir's name — other
# people's API, versioned and searchable as if it were ours.
#
# A blacklist would need editing every time something is vendored, and
# vendoring sourceror for structural placement is the proof that happens.
# The whitelist needs editing only when WE add a namespace, which is the
# change nobody forgets to make.
defp docs do
[
main: "readme",
extras: ["README.md", "CHANGELOG.md"],
filter_modules: ~r/^(Elixir\.)?(Svelixir|Mix\.Tasks\.Svelixir)/
]
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.
# Build artefacts inside a meta project, which must never enter the payload.
# `node_modules` earns its place the same way `deps` and `_build` do —
# measured, a meta project with its assets installed put 38,189 files into
# the file list and `mix hex.build` fell over. `priv/static/assets` and
# `priv/static/.vite` are Vite's output; `priv/static` itself is source.
# Svelixir.Baseline prunes the same set for the same reason.
@meta_excluded ~r{^priv/meta/[^/]+/(deps|_build)/|/node_modules/|^priv/meta/[^/]+/priv/static/(assets|\.vite)/}
# 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 VENDORED, not depended on — see vendor/README.md. Two uses,
# one adopted and one still rejected:
# REJECTED: `Sourceror.to_string/1` for rendering a declaration back to a
# string. Measured, it reproduces comments verbatim, and stripping the
# comment nodes changes blank-line rendering. `Macro.to_string/1` is
# correct and Svelixir.Component.MixChanges still uses it.
# ADOPTED: `get_range/1` + `patch_string/2` for LOCATING and SPLICING,
# which never reprints and is what R6 requires of a file the project owns.
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