Svelixir
Scaffolder for Elixir/Phoenix projects with a Svelte 5 UI layer.
Svelixir generates a project and then keeps it upgradable. svelixir.exs records
what the project asked for; fireside.exs records what was actually written, and
with which content hashes, so a later regeneration can tell an untouched file
from one the developer has edited. Every reader here works against an explicit
project root rather than the current working directory — Svelixir.Target
exists for exactly that reason, and Svelixir.BoundaryGuard enforces it
mechanically over lib/.
The two perimeters
The work is split across two repositories, and the split is structural rather than organisational.
| perimeter | repository | what it is |
|---|---|---|
| 1 — the package | wimwian-org/svelixir (this one) | the library a generated project depends on: Svelixir.Target, Svelixir.Config, Svelixir.Manifest, Svelixir.Exs, Svelixir.Hash, Svelixir.Baseline |
| 2 — the archive | wimwian-org/svelixir_new | the mix svelixir.new Mix archive, checked out at priv/svelixir_new |
Perimeter 2 is its own project because a Mix archive has to ship with zero hex
requirements: it is unpacked straight onto the developer's code path, where a
dependency of its own would be unresolvable. A separate Mix project is what lets
mix archive.build produce that, and it is also what keeps the two code paths
disjoint — neither perimeter's ebin is ever on the other's, so a change to one
cannot quietly compile against the other.
The two meet exactly once: the archive writes a svelixir.exs and this package
reads it back with Svelixir.Config.read!/1. There is no shared encoder and no
digest pinned in two files.
priv/svelixir_new is gitignored here. Clone it alongside before running the
cross-perimeter tests:
git clone git@github.com:wimwian-org/svelixir_new.git priv/svelixir_new
Everything degrades visibly without it rather than silently: test/test_helper.exs
excludes the :archive tag when priv/svelixir_new/mix.exs is absent,
bin/check skips that perimeter's gates, and CI annotates the run with a
::warning naming what did not run.
Running the checks
bin/check is the commit gate — format, credo --strict, coveralls at
minimum_coverage: 100, doctor and dialyzer, in both perimeters:
bin/check
It does not run the tagged tests, and a green bin/check alone is therefore
not a green suite. Three tags are excluded by default, and they are disjoint by
dependency: no test carries more than one, because ExUnit's include filter
beats its exclude filter per tag, so a doubly-tagged test would run in an
environment that cannot support it.
| tag | what it needs | how it runs |
|---|---|---|
:toolchain | the external toolchain only (mix new, phx_new) — safe anywhere | mix test --include toolchain |
:determinism | a subprocess probe re-run under a reversed atom-interning order | mix test --include determinism |
:archive | priv/svelixir_new, to build and install the archive | runs in a plain mix test when the perimeter is present; excluded when it is not |
The full local sequence:
bin/check
mix test --include toolchain --include determinism
(cd priv/svelixir_new && mix test --include toolchain)
Svelixir.TagCoverageTest asserts mechanically that every tag the suite excludes
is named by an --include on an unguarded CI step, so a newly excluded
tag cannot go unnamed by every workflow step. It reads ci.yml as text, so what
it proves is that a step exists which would run the tag — never that the step
ran. :archive is exempted from the unguarded half by name: its CI step is
guarded on the perimeter 2 checkout, which reads a private repository through the
SVELIXIR_NEW_TOKEN secret and 404s whenever that secret is absent, expired or
withheld — so :archive coverage in CI is contingent on a credential, and the
::warning in the run summary is what makes its absence visible. That failure
mode is the one this project calls F4: tests that silently did not run.
The hex dependency source
mix svelixir.new splices {:svelixir, "~> 0.1", only: [:dev], runtime: false}
into the generated project by default, and accepts --svelixir-path to splice a
path dependency instead. The default resolves: the package is published, and
~> 0.1 admits every 0.x release.
Both end-to-end tests still pass --svelixir-path, so the {:hex, requirement}
branch of the generator's dep_line/1 stays uncovered end to end. That is
deliberate rather than an oversight: the honest test needs the network, and as an
untagged test it would turn bin/check red for any offline developer, for reasons
unrelated to their change. What publishing changed is the failure mode — the
branch is no longer guaranteed to fail, so it is untested rather than known-broken.
Installation
def deps do
[
{:svelixir, "~> 0.1", only: [:dev], runtime: false}
]
end
only: [:dev] and runtime: false are deliberate rather than incidental:
Svelixir is a build-time scaffolder, and nothing it defines is needed once the
generated application is running.
To work against an unreleased checkout, depend on it by path instead — this is
what --svelixir-path generates:
{:svelixir, path: "../svelixir", only: [:dev], runtime: false}
Documentation is generated with ExDoc
and published at https://hexdocs.pm/svelixir. The vendored libraries in
vendor/ compile into this application but are filtered out of the docs by
filter_modules — their API is not ours to document.
Acknowledgements
Prior art
Svelixir's central idea — that a generated project should stay upgradable, which means telling a file the generator wrote from a file a human has since edited — is not original to it.
- Fireside by Ihor Barakaiev (MIT)
is where
fireside.exsgets both its name and its shape. Fireside imports code components into an existing project together with their dependencies and can upgrade them later, which is the same problem this project solves for a whole scaffold. Thesvelixir.exs/fireside.exssplit — intent in one file, what was actually written in the other — is Fireside's insight applied to a generator. The name is kept deliberately rather than rebranded: it should be obvious where the idea came from. - Igniter by Zach Daniel and the
Ash team (MIT) is the code-generation and project-patching framework Fireside
is built on, and the reference for what patching an existing project well looks
like — composable tasks over a parsed project rather than string surgery.
Svelixir does not depend on Igniter; the structural-placement modules
(
Svelixir.MixExs,Svelixir.ConfigExs,Svelixir.ApplicationEx) solve a deliberately narrower problem against a vendored Sourceror. Igniter is the right tool for the general case. - Sourceror by doorgan (Apache-2.0)
is what makes structural placement possible at all, and it is vendored here
(see below).
get_range/1pluspatch_string/2edits a range of an existing file and leaves every other byte untouched — as opposed to reprinting the AST, which reformats code the user never asked to change. Everymix.exs,config/*.exsandapplication.exedit this project makes goes through it.
Fireside depends on Igniter, and Igniter depends on Sourceror. Svelixir sits at the end of that chain and owes all three.
Vendored libraries
Four libraries are compiled directly into this application from vendor/ rather
than resolved as Hex dependencies, so that the generated archive perimeter can
carry zero Hex requirements. Each directory holds the upstream lib/ tree and
the upstream licence file, unmodified except for two recorded one-line patches.
Full provenance, checksums and patch notes are in
vendor/README.md.
| Library | Version | Licence | Author | Used for |
|---|---|---|---|---|
sourceror | 1.12.2 | Apache-2.0 | doorgan | structural placement — parse and patch by range |
vex | 0.9.2 | MIT | Bruce Williams | validating svelixir.exs sections |
typedstruct | 0.5.4 | MIT | Jean-Philippe Cugnet and contributors | the config and manifest structs |
simple_enum | 1.0.0 | MIT | DarkyZ aka NotAVirus | enumerated section values |
These are other people's work carrying other people's licences. Nothing in
vendor/ is formatted, linted or documented by this project's gates, and the
vendored modules are filtered out of the published docs — their API is theirs,
not ours.
Generated projects
Generated projects get their UI components from
sv5ui, a skin over the
bits-ui headless components, with the
sveltic theme template. The project
skeleton itself comes from Phoenix's own
phx.new, which Svelixir composes on top of rather than replaces.