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