Senal-D-A-Gunaratna/swapson.nvim

github github
lsp
stars 11
issues 0
subscribers 0
forks 1
CREATED

UPDATED


swapson.nvim

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-gyp addons — may behave differently. The enabled flags and require("swapson").restore() cover exactly this case; see Caveats

Why?

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

How it works

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/node that delegates to bun, so npm-published packages with #!/usr/bin/env node shebangs run on bun rather than a real node runtime — regardless of whether a system node is also present. It's gated on npm.enabled (the same toggle that controls the install/uninstall patch), not a separate opt: set npm.enabled = false to install and run npm-sourced packages on stock npm/node. Re-running setup() with npm disabled (e.g. after a config change + restart) removes a previously-created shim, not just restore()

Requirements

  • Neovim >= 0.7.0
  • mason.nvim
  • bun installed and on $PATH (for npm swaps)
  • uv installed and on $PATH (for pip swaps)
  • Platform: Linux and macOS (both tested); Windows is not and will not be supported (node shim is POSIX shell only)

Installation (lazy.nvim)

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 modules
  • npm.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 patched

The 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.

Configuration

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.

Health check

Run :checkhealth swapson to diagnose your swapson.nvim setup:

  • Checks that mason.nvim is installed and loadable
  • Verifies bun and uv binaries are on $PATH
  • Reports whether each manager is currently patched
  • Reports whether a system node is present (informational only — it no longer affects shim creation) and whether the bun-based node shim is active
  • Verifies the on-disk node shim content still matches what would be generated today (byte-for-byte drift check against the current bun path/tool config), distinguishing a shim that's missing, foreign (no swapson marker), stale (drifted), or current
  • Shows whether version lookups are patched (registry API vs. shelling out to npm)

The health check is read-only: it never creates, modifies, or removes files

Reverting

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

Caveats

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:

  • npm side: packages with 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
  • pip side: interpreter selection is delegated to uv rather than reimplemented, so where mason probes $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 one

This 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

License

MIT