A companion plugin for mason.nvim that routes package installs through faster alternative package managers instead of the defaults (npm, pip) — and runs npm-sourced packages via bun instead of node
Compatibility: most npm packages run fine on bun, but the occasional rare package — e.g. native
node-gypaddons — may behave differently. Theenabledflags andrequire("swapson").restore()cover exactly this case; see Caveats
Each manager (npm, pip) is designed to fully override its legacy tool —
controlled by a single enabled toggle in your plugin opts/config. When
npm.enabled = true, mason's npm install/uninstall calls go through bun
entirely, and npm-installed packages also run on bun via the node shim (see
below) rather than a system node runtime. Same idea for pip/uv, though pip's
swap is install-time only — Python packages still run on the venv's own
interpreter, since there's no equivalent "python shim" concept here. The only
time the legacy tool runs instead is the automatic fallback: if the
configured tool isn't found on $PATH, swapson notifies you and lets mason
fall back to its default — that's a safety net, not the intended mode of
operation.
bun add is significantly faster than npm install for installing npm packages.
Since mason.nvim installs hundreds of LSP servers, linters, and formatters from
npm, using bun cuts install time dramatically on a fresh setup
uv pip install is significantly faster than pip install for Python packages,
and uv venv creates virtual environments much faster than python -m venv
mason.nvim's maintainers have (reasonably) declined to add native alternative toolchain support upstream, as it would introduce dependencies on external toolchains with overlapping but not identical semantics
swapson.nvim monkeypatches mason.nvim's internal manager modules at runtime,
replacing the init, install, and uninstall functions with tool-specific
alternatives
This is the same technique used by mason-lspconfig.nvim and mason-tool-installer.nvim to extend mason.nvim without forking it
The patches are applied to the module tables cached in package.loaded, which
every mason.nvim internal that requires the same module path shares. No files are
modified
Note — the node shim: When npm patching is enabled, swapson.nvim creates a shell wrapper at
<mason_install_root>/bin/nodethat delegates tobun, so npm-published packages with#!/usr/bin/env nodeshebangs run on bun rather than a real node runtime — regardless of whether a systemnodeis also present. It's gated onnpm.enabled(the same toggle that controls the install/uninstall patch), not a separate opt: setnpm.enabled = falseto install and run npm-sourced packages on stock npm/node. Re-runningsetup()with npm disabled (e.g. after a config change + restart) removes a previously-created shim, not justrestore()
bun installed and on $PATH (for npm swaps)uv installed and on $PATH (for pip swaps)The recommended shorthand is opts = {...}, since it's simpler when no extra
logic beyond setup() is needed:
{
"Senal-D-A-Gunaratna/swapson.nvim",
dependencies = {
"mason-org/mason.nvim", -- lazy.nvim spec field: ensures mason loads first
},
opts = {
npm = {
enabled = true, -- turn the npm -> bun patch on/off
tool = "bun", -- binary name/path swapson calls instead of npm
},
pip = {
enabled = true, -- turn the pip -> uv patch on/off
tool = "uv", -- binary name/path swapson calls instead of pip
},
},
},
dependencies isn't a swapson option — it's a lazy.nvim spec field that
guarantees mason.nvim loads before swapson.nvim, which is required since
swapson patches mason's already-loaded internal modulesnpm.enabled now controls the whole npm swap as one unit: install/uninstall,
version lookups (get_latest_version/get_all_versions, which normally
shell out to npm view --json, are replaced with direct HTTPS requests to
registry.npmjs.org), and the node shim. There's no separate toggle for any
of these — this matters specifically if you have no npm installed at
all, since without the version-lookup swap, lookups would still shell out
to npm even with everything else patchedThe opts form is safe to use regardless of load order. swapson.nvim's
setup() includes a load-order safety guard: it checks
require("mason").has_setup before applying any patches. If mason.nvim has not
completed its own setup yet, swapson defers patching via vim.schedule() and
retries once. This ensures patches are only applied against a fully initialized
mason.nvim state.
Omitting any field falls back to the defaults inside init.lua.
require("swapson").setup(opts) accepts an optional table:
require("swapson").setup({
npm = {
enabled = true, -- set false to skip npm->bun patching entirely
tool = "bun", -- the bun binary name/path
},
pip = {
enabled = true, -- set false to skip pip->uv patching
tool = "uv", -- the uv binary name/path
},
})
Each manager falls back gracefully to mason's default behavior if its
configured tool is not found on $PATH, with a vim.notify() warning
For pip packages, the virtual environment is created with
uv venv --python <requires-python>, so uv picks the interpreter that satisfies
the package's requires-python. Which interpreters uv considers (uv-managed or
system ones on $PATH) and whether it may download one follow your uv config,
e.g. python-preference and python-downloads in uv.toml. If nothing
satisfies the specifier, the install fails — most often because downloads are
disabled (python-downloads = "manual") or you're offline, since uv downloads a
matching interpreter by default. mason's :MasonInstall --force (a mason flag,
not uv's unrelated --force) falls back to uv's default interpreter.
Mason's pip.upgrade_pip setting is honoured as well: swapson passes --seed,
which adds pip — plus setuptools and wheel, which uv omits on Python 3.12+
— to the venv. That only matters if something other than uv pip install expects
pip to be there, since swapson drives uv directly for the actual install.
Run :checkhealth swapson to diagnose your swapson.nvim setup:
bun and uv binaries are on $PATHnode is present (informational only — it
no longer affects shim creation) and whether the bun-based node shim
is activebun path/tool config), distinguishing a shim that's missing,
foreign (no swapson marker), stale (drifted), or currentThe health check is read-only: it never creates, modifies, or removes files
Call require("swapson").restore() to restore all mason.nvim manager functions
to their originals. Useful for A/B testing or if a swapped install misbehaves
swapson.nvim patches private Lua modules internal to mason.nvim
(e.g. mason-core.installer.managers.npm,
mason-core.installer.managers.pypi, and optionally
mason.providers.client.npm). These modules are not part of mason.nvim's
public API. Future mason.nvim releases may refactor or rename them without a
semver-major bump, which could break the patch — swapson will vim.notify()
an error if the module can't be loaded, rather than failing silently, but the
patch itself won't apply until swapson is updated to match
bun's and uv's behavior isn't a drop-in match for npm/pip in every case:
node-gyp native addons, npm-specific
postinstall hooks, or deep scoped dependency trees are the most likely to
behave differently under bun than under npm$PATH for a python3.x matching the package's
requires-python, swapson hands the specifier to uv venv --python and lets
uv resolve it under your uv config. That is usually equivalent, but it can pick
a uv-managed or freshly downloaded interpreter where mason would have used a
system oneThis is expected to affect a small minority of packages, and the enabled
flags or require("swapson").restore() are there for exactly this case — if a
specific package misbehaves, you can revert to npm/pip for that install
If swapson.nvim stops working after a mason.nvim update, check mason.nvim's changelog for internal module changes and file an issue
MIT