SvEx.Source.MixExs (SvEx v0.4.2)

Locates and patches constructs in a mix.exs, structurally (SDD R5, R6).

The one module that knows what a mix.exs looks like. Reading and writing live together deliberately: they were separate and they disagreed — the writer matched one of the four shapes mix accepts, the reader matched two, and each reported success on the shapes it could not handle.

Every function takes and returns SOURCE TEXT and touches no filesystem, so a caller supplies bytes from wherever it has them and this module is coverable without generating a project.

Placement is Sourceror.get_range/1 followed by Sourceror.patch_string/2, which splices at a range and copies every other byte through unchanged. Sourceror.to_string/1 is never called here: reprinting a file the project owns is a first-order R6 violation, and mix.exs's own comment records the measurement that rejected it.

Summary

Functions

Adds dep to source's dependency list.

The aliases/0 keyword list in source, as {name, [command source]} pairs.

Appends element to the literal list at list_range, as source text.

The dependency declarations in source, as the strings the author wrote.

The project/0 keyword list in source, as {key, source string} pairs.

Sets name to commands in source's aliases/0.

Sets key to value in source's project/0.

Types

error()

@type error() ::
  {:no_deps_list, :not_found | :not_a_literal}
  | {:no_project_function, :not_found}
  | {:no_aliases, :not_found}
  | {:unparseable, term()}

Functions

add_dep(source, dep)

@spec add_dep(binary(), binary()) :: {:ok, binary()} | {:error, error()}

Adds dep to source's dependency list.

Idempotent, decided by comparing PARSED declarations rather than text: a reflowed declaration is not an absent one, and duplicating it makes mix reject the file.

The dependency lands at the TOP of the list, which priv/meta/cache_component records and the round trip compares byte for byte. A single-line list stays single-line and a multi-line one stays multi-line — patch_string/2 indents continuation lines relative to the range start, so the change carries no leading whitespace of its own.

aliases(source)

@spec aliases(binary()) :: {:ok, keyword()} | {:error, error()}

The aliases/0 keyword list in source, as {name, [command source]} pairs.

{:error, {:no_aliases, :not_found}} when the project declares none, which every mix new project does.

append_element(source, list, list_range, element)

@spec append_element(binary(), [Macro.t()], SvEx.Vendor.Sourceror.Range.t(), binary()) ::
  binary()

Appends element to the literal list at list_range, as source text.

Shared with SvEx.Source.ApplicationEx, which appends a supervision child by exactly the same mechanics: patch at the LAST element's range so the author's formatting survives, or — for an empty list — at a zero-width point just inside the brackets, so that mix new's commented-out examples are not deleted along with the list they sit in.

add_dep/2 does NOT use this: a dependency goes at the top of the list, a position priv/meta/cache_component records and the round trip compares.

deps(source)

@spec deps(binary()) :: {:ok, [binary()]} | {:error, error()}

The dependency declarations in source, as the strings the author wrote.

Source strings rather than terms, so the manifest records only: and runtime: exactly as written (D10).

project_keys(source)

@spec project_keys(binary()) :: {:ok, keyword()} | {:error, error()}

The project/0 keyword list in source, as {key, source string} pairs.

Values are strings rather than terms for the same reason deps/1's are: what the author wrote is what the manifest must record.

put_alias(source, name, commands)

@spec put_alias(binary(), atom(), [binary()]) :: {:ok, binary()} | {:error, error()}

Sets name to commands in source's aliases/0.

Three cases, all of which the Svelte layer needs: the key is absent and is added; the key exists and its command list is REPLACED; aliases/0 does not exist and is created, with aliases: aliases() wired into project/0 — the ordinary case on a mix new project, where a function nothing calls would be inert, and silently so.

Replacement is why idempotency is structural here: after one apply the old commands are gone, so a textual presence test cannot tell "already applied" from "never applied".

put_project_key(source, key, value)

@spec put_project_key(binary(), atom(), binary()) ::
  {:ok, binary()} | {:error, error()}

Sets key to value in source's project/0.

value is SOURCE TEXT, not a term — "aliases()" is a call and ":renamed" an atom, and a plugin records what it wants written rather than a term this module would have to render back.