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.

perimeterrepositorywhat it is
1 — the packagewimwian-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 archivewimwian-org/svelixir_newthe 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.

tagwhat it needshow it runs
:toolchainthe external toolchain only (mix new, phx_new) — safe anywheremix test --include toolchain
:determinisma subprocess probe re-run under a reversed atom-interning ordermix test --include determinism
:archivepriv/svelixir_new, to build and install the archiveruns 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 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.

LibraryVersionLicenceAuthorUsed for
sourceror1.12.2Apache-2.0doorganstructural placement — parse and patch by range
vex0.9.2MITBruce Williamsvalidating svelixir.exs sections
typedstruct0.5.4MITJean-Philippe Cugnet and contributorsthe config and manifest structs
simple_enum1.0.0MITDarkyZ aka NotAVirusenumerated 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.