Skip to content
Merged
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ require('zpack').setup({
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)
lazy = false, -- make every spec lazy-load by default unless it sets lazy=false (lazy.nvim parity, default: false)
version = nil, -- default version applied when a spec has no version/sem_version/branch/tag/commit (lazy.nvim parity, default: nil)
},
performance = {
vim_loader = true, -- enables vim.loader for faster startup (default: true)
Expand Down
21 changes: 20 additions & 1 deletion doc/zpack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ command name is configurable via |zpack-setup-cmd_name| — a short name like
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)
lazy = false, -- make every spec lazy-load by default unless it sets lazy=false (lazy.nvim parity, default: false)
version = nil, -- default version applied when a spec has no version/sem_version/branch/tag/commit (lazy.nvim parity, default: nil)
},
performance = {
vim_loader = true, -- enables vim.loader for faster startup (default: true)
Expand Down Expand Up @@ -250,6 +252,13 @@ defaults (table, optional)
settings always take precedence over defaults.
- `confirm`: Skip vim.pack install prompts. Default: `true`
- `cond`: Global condition for all plugins.
- `lazy`: When `true`, every spec is treated as lazy
unless it sets `lazy = false`. lazy.nvim parity.
Default: `false`.
- `version`: Default version applied when a spec has no
`version`/`sem_version`/`branch`/`tag`/`commit`. A
per-spec `version = false` opts out. lazy.nvim parity.
Default: `nil`.

performance (table, optional)
Performance-related settings.
Expand Down Expand Up @@ -1082,7 +1091,17 @@ default lazy plugins lazy.nvim's community specs silently default
lazy triggers or a lazy parent. zpack respects
your specs as-written, so set `lazy = true`
explicitly on such specs if you want the same
default.
default, or set `defaults.lazy = true` in
`setup()` to apply it to every spec.

*zpack-migration-enabled-cond*
enabled vs cond lazy.nvim collapses both to a single "disabled"
state. zpack splits them: `enabled = false` skips
install entirely (the plugin is never cloned),
while `cond = false` installs the plugin but
skips its load (so it still shows up under
`vim.pack`'s data dir). Use `enabled` when you
want lazy.nvim's "don't install at all" behavior.

------------------------------------------------------------------------------
11.2 GOTCHAS *zpack-gotchas*
Expand Down
3 changes: 2 additions & 1 deletion docs/tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Most of your lazy.nvim plugin specs will work as-is with zpack. However, zpack f
- **version pinning**: lazy.nvim's `version` field maps to zpack's `sem_version`. See [Spec Reference](spec.md) and [version pinning examples](examples.md#version-pinning-for-lazynvim-compatibility)
- **dev mode**: Set `dev = true` on a spec and configure `setup({ dev = { path = '~/projects' } })` — the source is rewritten to `<path>/<plugin-name>`. Live file-watch / auto-reload is out of scope; use `:ZPack reload {plugin}` to manually re-source. See [Spec Reference](spec.md) for `dev`/`deactivate`
- **profiling**: Use `nvim --startuptime startuptime.log`. Also refer to example [Neovim Profiler script](https://gist.github.com/zuqini/35993710f81983fbfa6baca67bdb32ed)
- **default lazy plugins**: lazy.nvim's community specs silently default top-level specs for utility libraries like `plenary.nvim` to `lazy = true`, even without lazy triggers or a lazy parent. zpack respects your specs as-written, so set `lazy = true` explicitly on such specs if you want the same default
- **default lazy plugins**: lazy.nvim's community specs silently default top-level specs for utility libraries like `plenary.nvim` to `lazy = true`, even without lazy triggers or a lazy parent. zpack respects your specs as-written, so set `lazy = true` explicitly on such specs if you want the same default — or set `defaults.lazy = true` in [`setup()`](../README.md#configurations) to apply it to every spec
- **`enabled` vs `cond`**: lazy.nvim collapses both to a single "disabled" state. zpack splits them: `enabled = false` skips install entirely (the plugin is never cloned), while `cond = false` installs the plugin but skips its load (so it still shows up under `vim.pack`'s data dir). Use `enabled` when you want lazy.nvim's "don't install at all" behavior

## Gotchas

Expand Down
2 changes: 2 additions & 0 deletions lua/zpack/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ end
---@class zpack.Config.Defaults
---@field cond? boolean|(fun(plugin: zpack.Plugin):boolean)
---@field confirm? boolean
---@field lazy? boolean Treat every spec as lazy unless it sets `lazy = false` (lazy.nvim parity)
---@field version? string|vim.VersionRange|false Default version when none set; `false` means no default (lazy.nvim parity)

---@class zpack.Config.Performance
---@field vim_loader? boolean
Expand Down
47 changes: 30 additions & 17 deletions lua/zpack/lazy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,28 @@ local function is_dependency_only(src)
return true
end

---Check if any parent of a dependency is lazy (cached)
---@return boolean
local function default_lazy()
return (state.config and state.config.defaults and state.config.defaults.lazy) == true
end

---@param spec zpack.Spec
---@param plugin zpack.Plugin?
---@param src? string
---@return boolean
local function has_lazy_triggers(spec, plugin, src)
local event = utils.try_resolve_field(spec.event, plugin, src, 'event')
local cmd = utils.try_resolve_field(spec.cmd, plugin, src, 'cmd')
local ft = utils.try_resolve_field(spec.ft, plugin, src, 'ft')
local keys = utils.try_resolve_field(spec.keys, plugin, src, 'keys')

if event or cmd or ft or (keys and #keys > 0) then
return true
end
return false
end

---Check if any parent of a dependency is lazy (cached).
---@param dep_src string
---@return boolean
local function has_lazy_parent(dep_src)
Expand All @@ -43,20 +64,13 @@ local function has_lazy_parent(dep_src)
local parent_entry = state.spec_registry[parent_src]
if parent_entry and parent_entry.merged_spec then
local parent_spec = parent_entry.merged_spec --[[@as zpack.Spec]]
if parent_spec.lazy == true then
if parent_spec.lazy == true
or (parent_spec.lazy == nil
and has_lazy_triggers(parent_spec, parent_entry.plugin, parent_src))
then
state.lazy_parent_cache[dep_src] = true
return true
end
if parent_spec.lazy == nil then
local event = utils.try_resolve_field(parent_spec.event, parent_entry.plugin, parent_src, 'event')
local cmd = utils.try_resolve_field(parent_spec.cmd, parent_entry.plugin, parent_src, 'cmd')
local ft = utils.try_resolve_field(parent_spec.ft, parent_entry.plugin, parent_src, 'ft')
local keys = utils.try_resolve_field(parent_spec.keys, parent_entry.plugin, parent_src, 'keys')
if event or cmd or ft or (keys and #keys > 0) then
state.lazy_parent_cache[dep_src] = true
return true
end
end
end
end

Expand All @@ -73,12 +87,11 @@ M.is_lazy = function(spec, plugin, src)
return spec.lazy
end

local event = utils.try_resolve_field(spec.event, plugin, src, 'event')
local cmd = utils.try_resolve_field(spec.cmd, plugin, src, 'cmd')
local ft = utils.try_resolve_field(spec.ft, plugin, src, 'ft')
local keys = utils.try_resolve_field(spec.keys, plugin, src, 'keys')
if default_lazy() then
return true
end

if event or cmd or ft or (keys and #keys > 0) then
if has_lazy_triggers(spec, plugin, src) then
return true
end

Expand Down
9 changes: 5 additions & 4 deletions lua/zpack/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -320,16 +320,13 @@ M.is_semver_like = function(str)
or str:match('^%d+[%d%.]*$') ~= nil
end

---Normalize plugin version using priority: version > sem_version > branch > tag > commit.
---Normalize plugin version using priority: version > sem_version > branch > tag > commit > defaults.version.
---`version = false` is a lazy.nvim escape hatch meaning "no version constraint" —
---returns nil so vim.pack tracks the default branch even when a global default would
---otherwise pin a version.
---@param spec zpack.Spec
---@return string|vim.VersionRange|nil version
M.normalize_version = function(spec)
-- `version = false` is a lazy.nvim escape hatch ("no version") that we
-- treat as nil so vim.pack tracks the default branch. The early return
-- also narrows `spec.version` for the analyzer below.
if spec.version == false then
return nil
end
Expand All @@ -345,6 +342,10 @@ M.normalize_version = function(spec)
elseif spec.commit then
return spec.commit
end
local default_version = state.config and state.config.defaults and state.config.defaults.version
if type(default_version) == 'string' or type(default_version) == 'table' then
return default_version
end
return nil
end

Expand Down
3 changes: 3 additions & 0 deletions lua/zpack/validate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ function M.validate_config(opts)
if type(opts.defaults) == 'table' then
check(errors, 'defaults.cond', opts.defaults.cond, { 'boolean', 'function' })
check(errors, 'defaults.confirm', opts.defaults.confirm, 'boolean')
check(errors, 'defaults.lazy', opts.defaults.lazy, 'boolean')
-- `boolean` accepts `version = false`, the no-default opt-out handled in utils.normalize_version.
check(errors, 'defaults.version', opts.defaults.version, { 'string', 'table', 'boolean' })
end
if type(opts.performance) == 'table' then
check(errors, 'performance.vim_loader', opts.performance.vim_loader, 'boolean')
Expand Down
112 changes: 112 additions & 0 deletions tests/conditional_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,115 @@ describe("Conditional Loading", function()
assert.is_truthy(utils.check_enabled(both_true), "both-true functions should merge to true")
end)
end)

describe("defaults.lazy", function()
before_each(helpers.setup_test_env)
after_each(helpers.cleanup_test_env)

it("makes a triggerless spec lazy when defaults.lazy=true", function()
local state = require('zpack.state')

require('zpack').setup({
spec = { { 'test/plugin' } },
defaults = { confirm = false, lazy = true },
})
helpers.flush_pending()

local entry = state.spec_registry['https://github.com/test/plugin']
assert.is_not_nil(entry, "Plugin should be registered")
assert.is_true(entry.is_lazy_resolved, "Plugin should be lazy via defaults.lazy")
end)

it("spec.lazy=false overrides defaults.lazy=true", function()
local state = require('zpack.state')

require('zpack').setup({
spec = { { 'test/plugin', lazy = false } },
defaults = { confirm = false, lazy = true },
})
helpers.flush_pending()

local entry = state.spec_registry['https://github.com/test/plugin']
assert.is_not_nil(entry)
assert.is_false(entry.is_lazy_resolved, "Explicit lazy=false should win over defaults.lazy")
end)

it("defaults.lazy=false (the default) keeps a triggerless spec eager", function()
local state = require('zpack.state')

require('zpack').setup({
spec = { { 'test/plugin' } },
defaults = { confirm = false },
})
helpers.flush_pending()

local entry = state.spec_registry['https://github.com/test/plugin']
assert.is_not_nil(entry)
assert.is_false(entry.is_lazy_resolved, "Without defaults.lazy, a triggerless spec stays eager")
end)

it("a dep of a defaults.lazy=true parent is also resolved as lazy", function()
local state = require('zpack.state')

require('zpack').setup({
spec = {
{ 'test/parent', dependencies = { 'test/child' } },
},
defaults = { confirm = false, lazy = true },
})
helpers.flush_pending()

local parent = state.spec_registry['https://github.com/test/parent']
local child = state.spec_registry['https://github.com/test/child']
assert.is_not_nil(parent)
assert.is_not_nil(child)
assert.is_true(parent.is_lazy_resolved, "Parent should be lazy via defaults.lazy")
assert.is_true(child.is_lazy_resolved, "Dep-only child should be lazy under defaults.lazy")
end)

it("a dep-only child inherits laziness from a triggered parent (no defaults.lazy)", function()
local state = require('zpack.state')

require('zpack').setup({
spec = {
{ 'test/parent', event = 'BufRead', dependencies = { 'test/child' } },
},
defaults = { confirm = false },
})
helpers.flush_pending()

local parent = state.spec_registry['https://github.com/test/parent']
local child = state.spec_registry['https://github.com/test/child']
assert.is_not_nil(parent)
assert.is_not_nil(child)
assert.is_true(parent.is_lazy_resolved, "Parent should be lazy via its event trigger")
assert.is_true(child.is_lazy_resolved, "Dep-only child should inherit laziness from a triggered parent")
end)

it("a spec with an explicit event trigger stays lazy under defaults.lazy=true", function()
local state = require('zpack.state')

require('zpack').setup({
spec = { { 'test/plugin', event = 'BufRead' } },
defaults = { confirm = false, lazy = true },
})
helpers.flush_pending()

local entry = state.spec_registry['https://github.com/test/plugin']
assert.is_not_nil(entry)
assert.is_true(entry.is_lazy_resolved)
end)

it("enabled=false wins over defaults.lazy=true (plugin is never registered)", function()
local state = require('zpack.state')

require('zpack').setup({
spec = { { 'test/plugin', enabled = false } },
defaults = { confirm = false, lazy = true },
})
helpers.flush_pending()

assert.is_nil(state.spec_registry['https://github.com/test/plugin'],
"enabled=false should prune the plugin even under defaults.lazy=true")
end)
end)
38 changes: 38 additions & 0 deletions tests/validate_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ describe("Config Validation", function()
})
assert.are.equal(0, #errors)
end)

it("validate_config accepts defaults.lazy and defaults.version", function()
local validate = require('zpack.validate')
local errors = validate.validate_config({
defaults = { lazy = true, version = 'main' },
})
assert.are.equal(0, #errors)

errors = validate.validate_config({
defaults = { version = vim.version.range('^1') },
})
assert.are.equal(0, #errors)
end)

it("validate_config flags a non-boolean defaults.lazy", function()
local validate = require('zpack.validate')
local errors = validate.validate_config({ defaults = { lazy = 'yes' } })
assert.are.equal(1, #errors)
assert.is_truthy(errors[1]:find('defaults.lazy', 1, true) ~= nil,
"error should name defaults.lazy")
assert.is_truthy(errors[1]:find('boolean', 1, true) ~= nil,
"error should state expected boolean")
end)

it("validate_config flags a wrong-typed defaults.version", function()
local validate = require('zpack.validate')
local errors = validate.validate_config({ defaults = { version = 123 } })
assert.are.equal(1, #errors)
assert.is_truthy(errors[1]:find('defaults.version', 1, true) ~= nil,
"error should name defaults.version")
end)

it("validate_config accepts defaults.version=false (the no-default opt-out)", function()
local validate = require('zpack.validate')
local errors = validate.validate_config({ defaults = { version = false } })
assert.are.equal(0, #errors,
"defaults.version=false is the documented opt-out and must not error")
end)
end)

describe("Spec Validation", function()
Expand Down
Loading
Loading