Skip to content

zuqini/zpack.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

93 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zpack.nvim

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

Requirements

  • Neovim 0.12.0+

Installation

-- install with vim.pack directly
vim.pack.add({ 'https://github.com/zuqini/zpack.nvim' })

Usage

-- 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()

Commands

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.

Directory Structure

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,
}

Configurations

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.

Importing Specs

-- 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 },
    ...
  },
})

Why zpack?

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.

Features

  • [zpack] is completely native
    • Install and manage your plugins (including zpack) all within vim.pack
  • [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.pack without 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

Examples

For more examples, refer to example config:

Lazy Load on Command

return {
  'nvim-tree/nvim-tree.lua',
  cmd = { 'NvimTreeToggle', 'NvimTreeFocus' },
  opts = {},
}

Lazy Load on Keymap

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 = {},
}

Lazy Load on Event

return {
  'windwp/nvim-autopairs',
  event = 'InsertEnter', -- Also supports 'VeryLazy'
  opts = { check_ts = true },
}

Lazy Load on Event with Pattern

-- 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 = {},
}

Lazy Load on FileType

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,
}

Conditional Loading

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 = {},
}

Build Hook

return {
  'nvim-telescope/telescope-fzf-native.nvim',
  build = 'make',
}

Dependencies

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.

Version Pinning

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.

Version Pinning for lazy.nvim compatibility
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',
}

Load Priority

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,
}

Custom Config Function

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,
}

Using Plugin Data in Hooks

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,
}

Explicit Main Module

If automatic module detection fails, specify the module explicitly with main:

return {
  'some/plugin-with-unusual-structure',
  main = 'plugin.core',
  opts = { enabled = true },
}

Multiple Plugins in One File

return {
  { 'nvim-lua/plenary.nvim' },
  { 'nvim-tree/nvim-web-devicons' },
  { 'nvim-lualine/lualine.nvim', opts = { theme = 'auto' } },
  { import = 'plugins.mini' },
}

Spec Reference

{
  -- 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
}

zpack.Plugin Reference

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
}

zpack.EventSpec Reference

{
  event = string|string[],        -- Event name(s) to trigger on
  pattern = string|string[],      -- Pattern(s) for the event (optional)
}

zpack.KeySpec Reference

{
  [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
}

Migrating from lazy.nvim

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 version field maps to zpack's sem_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

Snacks.nvim dashboard with zpack.nvim

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.

Acknowledgements

zpack's spec design and several features are inspired by lazy.nvim. Credit to folke for the excellent plugin manager that influenced this project.