# Declaring parsers (/advanced/lock)

A parser is a dependency. You add it, your package manager pins it, and it is
there — or you did not add it, and it is not.

Declaring parsers makes output reproducible. The set is in a file you already
review, pinned by a lock you already have, so every machine renders the same
document with the same parsers, and the lock records which parser produced it.

For an Elixir application that downloaded parsers before 0.9, see
[Upgrading to 0.9](/migrate/elixir#upgrading-to-09).

## One rule, one place to look

Every runtime resolves parsers the same way: **only the ones the project
installed**, with no fallback that fetches anything else. A parser is a
dependency, declared where that runtime's dependencies already live.

| Runtime | Declares its parsers in | Pinned by |
| --- | --- | --- |
| Rust | `Cargo.toml` features | `Cargo.lock` |
| JavaScript / TypeScript | `package.json` dependencies | your package manager's lockfile |
| JavaScript in a browser | the packages it imports | the lockfile behind your bundle |
| Elixir | `mix.exs` dependencies | `mix.lock` |
| CLI | nothing | — |

Rust links parsers statically. JavaScript installs `@lumis-sh/wasm-*`, and a
browser, which has no project to read, declares by what it imports. Elixir
depends on `lumis_wasm_*`, whose `priv/parsers` holds the bytes — so they travel
into a release like any other dependency's assets, with no data directory to
prepare and nothing to copy.

The CLI is the exception, deliberately. It has no project and downloads what it
needs, because a viewer is not the application whose output a declaration
governs, and a highlighter that behaved differently depending on the directory
you ran it in would be worse than one that does not.

### Elixir

Add the parsers you want to `mix.exs`. There are no Lumis-specific commands:
`mix deps.get`, `mix deps.update` and `mix.lock` already are them.

```elixir title="mix.exs"
defp deps do
  [
    {:lumis, "~> 0.10"},
    {:lumis_wasm_rust, "~> 0.26.0"},
    {:lumis_wasm_bundle_web, "~> 0.1"}
  ]
end
```

A bundle is a package whose dependencies are its members, so it widens when it
gains a language and `mix deps.update` picks that up — a visible `mix.lock`
diff, not a silent change.

```bash title="Move a pinned version"
mix deps.update lumis_wasm_rust
```

```bash title="Stop using it"
# remove the line from mix.exs, then
mix deps.unlock --unused
```

A project that vendors parsers rather than depending on them can point at the
directories instead. Same layout; only how they got there differs.

```elixir title="config/config.exs"
config :lumis, parser_dirs: ["priv/parsers"]
```

### JavaScript / TypeScript

Install the parsers you want. Your package manager's lockfile pins them, so
there is no separate Lumis lock.

```bash
npm install @lumis-sh/wasm-rust @lumis-sh/wasm-json
```

Rust and JSON now highlight. A Markdown file with a Haskell block renders that
block as plain text — Lumis will not go and fetch `haskell`.

Install nothing and nothing highlights. There is no fallback that fetches a
language the project did not install, the same way Rust cannot use a language
that was not compiled in.

Install a bundle for a set of them, and prefer a focused one:
[loading the full set can exhaust](/languages#bundle-full-is-not-the-safe-default)
the 128 MB Wasm store every language shares. `bundle-full` also leaves out
`systemverilog`; install `@lumis-sh/wasm-systemverilog` if you need it.

A bundle installs a set in one package:

```bash
npm install @lumis-sh/wasm-bundle-web
```

Parsers are read out of `node_modules` — the manifest through each package's
`./lumis.json` export — so nothing is fetched at runtime.

A browser has no `node_modules`, so there the packages you import are the
declaration. `import rust from "@lumis-sh/wasm-rust"` works in both, and
nothing is fetched in a browser either; see
[Where parsers come from](/usage/javascript#where-parsers-come-from).

Supplying your own resolver is a declaration too, and takes precedence: a
caller that said where parsers come from has answered the question the
dependency list otherwise answers.

### Rust

Parsers are Cargo features, so `Cargo.lock` and the `lumis` version pin them.
Nothing is fetched at runtime.

```toml title="Cargo.toml"
lumis = { version = "0.16", default-features = false, features = ["lang-rust", "lang-elixir"] }
```


## What a language you did not add does

Naming a language explicitly is your decision, so one you never depended on is
an error rather than a silent download:

```elixir
Lumis.Languages.load("haskell")
#=> {:error, :not_installed}
```

A language an *injection* names is content rather than a decision — a fenced
block in a README, say — so that block renders plain and the document is fine.
A missing parser costs one block, not the page.

## Releases

Nothing to prepare. Parsers are dependencies, so `mix release` carries them the
way it carries any dependency's `priv/`:

```
_build/prod/rel/my_app/lib/lumis_wasm_elixir-0.26.3/priv/parsers/
  elixir.lumis.json
  tree-sitter-elixir-0.26.3-....wasm
```

The released node reads them from there. There is no data directory to stage, no
copy step, and no network at boot or at render time.

`config :lumis, :data_dir` still has a job, a smaller one: it holds the compiled
WASM module cache, which is a speed-up rather than a source. Point it somewhere
writable and persistent and a restart skips recompiling; lose it and the first
render of each language is slower and nothing else changes.

## Verification

npm and Hex check every package they fetch against its published checksum, which
your lockfile records, so a parser that arrives through a package manager is
already verified. Lumis loads it as installed, beside the `lumis.json` that
describes it, and refuses a package outside the Tree-sitter series this Lumis
speaks. It checks a parser against the SHA-256 in `lumis.json` only when it
downloads the bytes itself, through a resolver or the CLI.