A thin layer on top of Neovim's native vim.pack, adding support for lazy-loading and the widely adopted lazy.nvim-like declarative spec.
-- ./lua/plugins/fundo.lua
return {
'kevinhwang91/nvim-fundo',
dependencies = { "kevinhwang91/promise-async" },
cond = not vim.g.vscode,
version = 'main',
build = function() require('fundo').install() end,
opts = {},
config = function(_, opts)
vim.o.undofile = true
require('fundo').setup(opts)
end,
}Why zpack? | Examples | Spec Reference | Migrating from lazy.nvim
- Neovim 0.12.0+
-- install with vim.pack directly
vim.pack.add({ 'https://github.com/zuqini/zpack.nvim' })-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading zpack.nvim so that mappings are correct.
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
-- automatically import specs from `./lua/plugins/`
require('zpack').setup()zpack provides the following commands (default prefix: Z, customizable via cmd_prefix option):
:ZUpdate [plugin]- Update all plugins, or a specific plugin if provided (supports tab completion). See:h vim.pack.update():ZClean- Remove plugins that are no longer in your spec:ZBuild[!] [plugin]- Run build hook for a specific plugin, or all plugins with!(supports tab completion):ZLoad[!] [plugin]- Load a specific unloaded plugin, or all unloaded plugins with!(supports tab completion):ZDelete[!] [plugin]- Remove a specific plugin, or all plugins with!(supports tab completion)- Deleting active plugins in your spec can result in errors in your current session. Restart Neovim to re-install them.
Under the default setting, create plugin specs in lua/plugins/:
lua/
plugins/
treesitter.lua
telescope.lua
lsp.lua
Each file returns a spec or list of specs (see examples or spec reference):
-- lua/plugins/telescope.lua
return {
'nvim-telescope/telescope.nvim',
cmd = 'Telescope',
keys = {
{ '<leader>ff', function() require('telescope.builtin').find_files() end, desc = 'Find files' },
},
opts = {},
-- config is executed when the plugin loads. The default implementation will automatically run require(MAIN).setup(opts) if opts or config = true is set.
config = function()
...
end,
}require('zpack').setup({
-- { import = 'plugins' } -- default import spec if not explicitly passed in via [1] or spec
defaults = {
confirm = true, -- set to false to skip vim.pack install prompts (default: true)
cond = nil, -- global condition for all plugins, e.g. not vim.g.is_vscode (default: nil)
},
performance = {
vim_loader = true, -- enables vim.loader for faster startup (default: true)
},
cmd_prefix = 'Z', -- command prefix: :ZUpdate, :ZClean, etc. (default: 'Z')
})Plugin-level settings always take precedence over defaults.
-- automatically import specs from `./lua/plugins/`
require('zpack').setup()
-- or import from a custom directory e.g. `./lua/a/b/plugins/`
require('zpack').setup({ { import = 'a.b.plugins' } })
-- or add your specs inline in setup
require('zpack').setup({
{ 'neovim/nvim-lspconfig', config = function() ... end },
...
{ import = 'plugins.mini' }, -- or additionally import from `./lua/plugins/mini/`
})
-- or via the spec field
require('zpack').setup({
spec = {
{ 'neovim/nvim-lspconfig', config = function() ... end },
...
},
})Neovim 0.12+ includes a built-in package manager (vim.pack) that handles plugin installation, updates, and version management. zpack is a thin layer that adds lazy-loading capabilities and support for a lazy.nvim-like declarative spec while completely leveraging the native infrastructure.
- [zpack] is completely native
- Install and manage your plugins (including zpack) all within
vim.pack
- Install and manage your plugins (including zpack) all within
- [
pack] is "batteries included"
- Add plugins using the same lazy.nvim spec provided by plugin authors you know and love
- Minimal configurations necessary
- [đź’¤pack] powers up
vim.packwithout the frills- Powerful lazy-loading triggers
- Build triggers for installation/updates
- Basic plugin management commands
zpack might be for you if:
- you're a lazy.nvim user, love its declarative spec, and its wide adoption by plugin authors, but you don't need most of its advanced features
- you're a lazy.nvim user, want to migrate to
vim.pack, but don't want to rewrite your entire plugins spec from scratch - you want to use
vim.pack, but still looking for a few core quality of life features like:- run build commands only when plugin installs/updates
- a minimalist set of commands and tools to manage your plugin's lifecycle e.g. updates, cleaning, and builds
- lazy-loading triggers for a faster startup on slower machines
- lazy.nvim's declarative plugin spec support to keep your main neovim config neat and tidy
As a thin layer, zpack does not provide:
- UI dashboard for your plugins
- Advanced profiling, dev mode, change-detection, etc.
If you're a lazy.nvim user, see Migrating from lazy.nvim
For more examples, refer to example config:
return {
'nvim-tree/nvim-tree.lua',
cmd = { 'NvimTreeToggle', 'NvimTreeFocus' },
opts = {},
}return {
'folke/flash.nvim',
keys = {
{ 's', function() require('flash').jump() end, mode = { 'n', 'x', 'o' }, desc = 'Flash' },
{ 'S', function() require('flash').treesitter() end, mode = { 'n', 'x', 'o' }, desc = 'Flash Treesitter' },
},
opts = {},
}return {
'windwp/nvim-autopairs',
event = 'InsertEnter', -- Also supports 'VeryLazy'
opts = { check_ts = true },
}-- Inline pattern (same as lazy.nvim)
return {
'rust-lang/rust.vim',
event = 'BufReadPre *.rs',
init = function() vim.g.rustfmt_autosave = 1 end,
}
-- Or using EventSpec for multiple patterns
return {
'polyglot-plugin',
event = {
event = 'BufReadPre',
pattern = { '*.lua', '*.rs' },
},
opts = {},
}Load plugin when opening files of specific types. Automatically re-triggers BufReadPre, BufReadPost, and FileType events to ensure LSP clients and Treesitter attach properly:
return {
'rust-lang/rust.vim',
ft = { 'rust', 'toml' },
init = function() vim.g.rustfmt_autosave = 1 end,
}Use enabled to skip vim.pack.add entirely, or cond to conditionally load after calling vim.pack.add:
return {
'project-specific-plugin',
enabled = vim.fn.has('linux') == 1, -- skip installation
cond = function() return vim.fn.filereadable('.project-marker') == 1 end, -- skip loading
opts = {},
}return {
'nvim-telescope/telescope-fzf-native.nvim',
build = 'make',
}return {
'nvim-telescope/telescope.nvim',
cmd = 'Telescope',
dependencies = {
'nvim-lua/plenary.nvim',
{ 'nvim-tree/nvim-web-devicons', opts = {} },
},
}Dependencies are automatically loaded before the parent plugin when the parent's lazy trigger fires.
vim.pack.add expects version to be string|vim.VersionRange:
return {
'mrcjkb/rustaceanvim',
version = vim.version.range('^6'), -- semver version
-- version = 'main', -- branch
-- version = 'v1.0.0', -- tag
-- version = 'abc123', -- commit
}See :h vim.pack.Spec, :h vim.version.range(), and :h vim.VersionRange.
return {
'mrcjkb/rustaceanvim',
sem_version = '^6', -- corresponds to lazy.nvim spec's `version`, auto-wrapped to vim.version.range()
-- branch = 'main',
-- tag = 'v1.0.0',
-- commit = 'abc123',
}Control plugin load order with priority (higher values load first; default: 50):
-- Startup plugin: load colorscheme early
return {
'folke/tokyonight.nvim',
priority = 1000,
config = function() vim.cmd('colorscheme tokyonight') end,
}When you need custom configuration logic, use a config function. The resolved opts table is passed as the second argument:
return {
'nvim-lualine/lualine.nvim',
opts = { theme = 'tokyonight' },
config = function(_, opts)
opts.sections = { lualine_a = { 'mode' } }
require('lualine').setup(opts)
end,
}All lifecycle hooks (init, config, build, cond) and lazy-loading triggers (event, cmd, keys, ft) can be functions that receive a zpack.Plugin object containing the resolved plugin path and spec:
return {
'some/plugin',
build = function(plugin)
-- plugin.path: absolute path to the plugin directory
-- plugin.spec: the vim.pack.Spec with resolved name, src, version
vim.fn.system({ 'make', '-C', plugin.path })
end,
}If automatic module detection fails, specify the module explicitly with main:
return {
'some/plugin-with-unusual-structure',
main = 'plugin.core',
opts = { enabled = true },
}return {
{ 'nvim-lua/plenary.nvim' },
{ 'nvim-tree/nvim-web-devicons' },
{ 'nvim-lualine/lualine.nvim', opts = { theme = 'auto' } },
{ import = 'plugins.mini' },
}{
-- Plugin source (provide exactly one)
[1] = "user/repo", -- Plugin short name. Expands to https://github.com/{user/repo}
src = "https://...", -- Custom git URL or local path
dir = "/path/to/plugin", -- Local plugin directory (lazy.nvim compat, ~ expanded, mapped to src)
url = "https://...", -- Custom git URL (lazy.nvim compat, mapped to src)
-- Dependencies
dependencies = string|string[]|zpack.Spec|zpack.Spec[], -- Plugin dependencies
-- Loading control
enabled = true|false|function, -- Enable/disable plugin
cond = true|false|function(plugin), -- Condition to load plugin
lazy = true|false, -- Force eager loading when false (auto-detected)
priority = 50, -- Load priority (higher = earlier, default: 50)
-- Plugin configuration
opts = {}, -- Options passed to setup(), triggers auto-setup
-- opts = function(plugin, opts) return {} end, -- Can also be a function
-- Lifecycle hooks
init = function(plugin) end, -- Runs before plugin loads, useful for certain vim plugins
config = function(plugin, opts) end, -- Runs after plugin loads, receives resolved opts
-- config = true, -- Calls require(main).setup({})
build = string|function(plugin), -- Build command or function
-- Lazy loading triggers (auto-sets lazy=true unless overridden)
-- All triggers can also be functions that receive zpack.Plugin and return the respective type
event = string|string[]|zpack.EventSpec|(string|zpack.EventSpec)[]|function(plugin), -- Autocommand event(s). Supports 'VeryLazy' and inline patterns: "BufReadPre *.lua"
pattern = string|string[], -- Global fallback pattern(s) for all events
cmd = string|string[]|function(plugin), -- Command(s) to create
keys = zpack.KeySpec|zpack.KeySpec[]|function(plugin), -- Keymap(s) to create
ft = string|string[]|function(plugin), -- FileType(s) to lazy load on
-- Source control (version for `vim.pack.add`, string|vim.VersionRange)
version = "main", -- Git branch, tag, or commit
-- version = vim.version.range("1.*"), -- Or semver range via vim.version.range()
-- Source control (lazy.nvim compat, mapped to version)
sem_version = "^1.0.0", -- Semver string (corresponds to lazy.nvim spec's version), auto-wrapped to vim.version.range()
branch = "main", -- Git branch
tag = "v1.0.0", -- Git tag
commit = "abc123", -- Git commit
-- Plugin metadata
name = "my-plugin", -- Custom plugin name (optional, overrides auto-derived name)
main = "module.name", -- Explicit main module (auto-detected if not set)
module = false, -- Disable module-based lazy loading for this plugin
-- Spec imports
import = "plugins.lsp", -- Import from lua/{path}/*.lua and lua/{path}/*/init.lua
}The plugin data object passed to hooks and trigger functions:
{
spec = vim.pack.Spec, -- The resolved vim.pack spec (name, src, version)
path = string, -- Absolute path to the plugin directory
}{
event = string|string[], -- Event name(s) to trigger on
pattern = string|string[], -- Pattern(s) for the event (optional)
}{
[1] = "<leader>ff", -- LHS keymap (required)
[2] = function() end, -- RHS function
desc = "description", -- Keymap description
mode = "n"|{"n","v"}, -- Mode(s), default: "n"
remap = true|false, -- Allow remapping, default: false
nowait = true|false, -- Default: false
}Most of your lazy.nvim plugin specs will work as-is with zpack. However, zpack follows vim.pack conventions over lazy.nvim conventions, and is missing a few advanced features:
- version pinning: lazy.nvim's
versionfield maps to zpack'ssem_version. See Version Pinning - dev mode: Use
src = vim.fn.expand('~/projects/my_plugin.nvim')for local development - profiling: Use
nvim --startuptime startuptime.log. Also refer to example Neovim Profiler script
The default Snacks.nvim dashboard configuration includes a startup time section that has a hard dependency on lazy.nvim. This will cause errors with any other plugin manager, not just zpack.
To work around this, remove the startup section from your dashboard configuration:
require('snacks').setup({
dashboard = {
sections = {
{ section = "header" },
{ section = "keys", gap = 1, padding = 1 },
-- { section = "startup" }, -- Remove this line (depends on lazy.nvim)
},
}
})See snacks.nvim#1778 for more details.
zpack's spec design and several features are inspired by lazy.nvim. Credit to folke for the excellent plugin manager that influenced this project.