# Plugins
## Built-in Plugins
Volt includes built-in support for Vue, Svelte, and React. These are activated automatically when you import `.vue` or `.svelte` files, or configure `import_source: "react"`.
## Using Plugins
Add plugins to your Volt config:
```elixir
config :volt, plugins: [MyApp.MarkdownPlugin]
```
Plugins can also accept options as `{module, opts}` tuples:
```elixir
config :volt, plugins: [{MyApp.SassPlugin, output_style: :compressed}]
```
## Writing Plugins
Implement the `Volt.Plugin` behaviour. All callbacks except `name/0` are optional — implement only the hooks you need.
### Example: Markdown imports
Load `.md` files as HTML string modules:
```elixir
defmodule MyApp.MarkdownPlugin do
@behaviour Volt.Plugin
@impl true
def name, do: "markdown"
@impl true
def resolve(spec, _importer) do
if String.ends_with?(spec, ".md"), do: {:ok, spec}
end
@impl true
def load(path) do
if String.ends_with?(path, ".md") do
html = path |> File.read!() |> Earmark.as_html!()
{:ok, "export default #{Jason.encode!(html)};\n"}
end
end
def resolve(_, _), do: nil
def load(_), do: nil
end
```
```javascript
import readme from './README.md'
document.getElementById('content').innerHTML = readme
```
### Example: Banner injection
Use `render_chunk/2` to prepend a license banner to production output:
```elixir
defmodule MyApp.BannerPlugin do
@behaviour Volt.Plugin
@banner "/* © 2026 MyApp — MIT License */\n"
@impl true
def name, do: "banner"
@impl true
def render_chunk(code, %{type: :entry}), do: {:ok, @banner <> code}
def render_chunk(_code, _chunk_info), do: nil
end
```
### Example: Compile-time constants
Use `define/1` to inject build-time values:
```elixir
defmodule MyApp.BuildInfo do
@behaviour Volt.Plugin
@impl true
def name, do: "build-info"
@impl true
def define(_mode) do
{hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
%{
"__BUILD_HASH__" => Jason.encode!(String.trim(hash)),
"__BUILD_TIME__" => Jason.encode!(DateTime.utc_now() |> to_string())
}
end
end
```
```javascript
console.log(`Build ${__BUILD_HASH__} at ${__BUILD_TIME__}`)
```
### Example: Custom file compilation with OXC
Use `compile/3` to handle a custom file format, transforming output with OXC:
```elixir
defmodule MyApp.CSVPlugin do
@behaviour Volt.Plugin
@impl true
def name, do: "csv"
@impl true
def extensions(:compile), do: [".csv"]
def extensions(:resolve), do: [".csv"]
def extensions(_), do: []
@impl true
def resolve(spec, _importer) do
if String.ends_with?(spec, ".csv"), do: {:ok, spec}
end
def resolve(_, _), do: nil
@impl true
def compile(path, source, opts) do
if Path.extname(path) == ".csv" do
rows =
source
|> String.split("\n", trim: true)
|> Enum.map(&String.split(&1, ","))
js = "export default #{Jason.encode!(rows)};\n"
{:ok, %{code: js, sourcemap: nil, css: nil, hashes: nil}}
end
end
end
```
```javascript
import data from './prices.csv'
// data = [["name", "price"], ["Widget", "9.99"], ...]
```
### Example: AST transform with OXC
Use `transform/2` to modify compiled JavaScript. OXC provides `parse/2`, `postwalk/3`, and `patch_string/2` for AST-based transforms:
```elixir
defmodule MyApp.StripConsolePlugin do
@behaviour Volt.Plugin
@impl true
def name, do: "strip-console"
@impl true
def transform(code, _path) do
case OXC.parse(code, "module.js") do
{:ok, ast} ->
patches =
collect_console_calls(ast)
|> Enum.map(fn %{start: s, end: e} ->
%{start: s, end: e, change: "void 0"}
end)
if patches == [] do
nil
else
{:ok, OXC.patch_string(code, patches)}
end
{:error, _} ->
nil
end
end
defp collect_console_calls(ast) do
{_ast, calls} =
OXC.postwalk(ast, [], fn
%{
type: :call_expression,
callee: %{
type: :member_expression,
object: %{type: :identifier, name: "console"}
}
} = node, acc ->
{node, [node | acc]}
node, acc ->
{node, acc}
end)
calls
end
end
```
## Hooks
All hooks are optional. Return `nil` to pass to the next plugin.
| Hook | Purpose |
| --- | --- |
| `name/0` | Plugin identifier (required) |
| `extensions/1` | File extensions for `:compile`, `:resolve`, `:watch`, or `:scan` |
| `resolve/2` | Resolve import specifiers to file paths |
| `load/1` | Load file content for a resolved path |
| `compile/3` | Compile source into browser-ready JS + optional CSS |
| `extract_imports/3` | Extract import specifiers from source |
| `transform/2` | Transform compiled JS before serving or bundling |
| `define/1` | Compile-time variable replacements |
| `prebundle_alias/1` | Canonical prebundle specifier for an import |
| `prebundle_entry/1` | Generated prebundle entry module |
| `render_chunk/2` | Transform final output chunks |
### Hook execution order
During compilation, hooks run in this order:
1. **`resolve`** — map import specifier to a file path
2. **`load`** — read file content (override `File.read`)
3. **`compile`** — transform source into JS + CSS
4. **`extract_imports`** — find imports for dependency walking
5. **`transform`** — post-process compiled JS
6. **`render_chunk`** — modify final bundled output
`define/1` runs once at build start. `extensions/1` is checked throughout to determine which files a plugin handles.
### Plugin options
When configured as a `{module, opts}` tuple, the opts are passed as an extra argument to callbacks that support it. Define a callback with one additional arity to receive them:
```elixir
defmodule MyApp.SassPlugin do
@behaviour Volt.Plugin
@impl true
def name, do: "sass"
# 3-arg version (standard)
def compile(path, source, opts), do: compile(path, source, opts, [])
# 4-arg version receives plugin opts
def compile(path, source, opts, plugin_opts) do
style = Keyword.get(plugin_opts, :output_style, :expanded)
# ...
end
end
```
## JavaScript Runtimes
Plugins can run JavaScript build tools through `Volt.JS.Runtime`, which installs npm packages into Volt's cache and executes them in QuickBEAM without requiring Node.js in the host application:
```elixir
defmodule MyApp.SassPlugin do
@behaviour Volt.Plugin
@runtime_name __MODULE__.Runtime
@runtime_packages %{"sass" => "^1.80.0"}
@impl true
def name, do: "sass"
@impl true
def extensions(:compile), do: [".scss"]
def extensions(:resolve), do: [".scss"]
def extensions(_), do: []
@impl true
def compile(path, source, _opts) do
if Path.extname(path) == ".scss" do
runtime =
Volt.JS.Runtime.ensure!(
name: @runtime_name,
packages: @runtime_packages,
entry: {:volt_asset, "sass-runtime.ts"},
bundle: true
)
case Volt.JS.Runtime.call(runtime, "compileSass", [source, path]) do
{:ok, %{"css" => css}} ->
js = "var s = document.createElement('style'); s.textContent = #{Jason.encode!(css)}; document.head.appendChild(s);\n"
{:ok, %{code: js, sourcemap: nil, css: css, hashes: nil}}
{:error, _} = error ->
error
end
end
end
end
```
The runtime automatically installs npm packages on first use and caches the bundled entry script. Subsequent calls reuse the running QuickBEAM instance.