feat: lazy.nvim spec parity (pin/optional/dev/specs/build/reload/sync)#28
Merged
Conversation
…shape)
Series of additive lazy.nvim spec parity fixes:
* build hook (P1 zpack_nvim-9zd, zpack_nvim-jc9): execute_build now
accepts arrays (iterated in declared order), distinguishes ':<ex>'
from shell commands (shell spawns async via vim.system inside
plugin.path), and skips on `build = false`. Mirrors
lazy.nvim/manage/task/plugin.lua B.cmd / B.shell dispatch. validate
widens `build` to `{ string, function, table, boolean }`.
* version = false (P2 zpack_nvim-9tm): utils.normalize_version returns
nil for `version = false` so a global default version doesn't pin a
plugin that explicitly opted out. validate widens `version` to
include boolean.
* Plugin object shape (P2 zpack_nvim-clj): callbacks now receive a
plugin with lazy.nvim's introspection fields populated additively:
`name` (alias for spec.name), `dir` (alias for path), and
`dependencies` (sorted list of resolved dep names). zpack's existing
`spec`/`path`/`main` are preserved.
* nested `specs` (P2 zpack_nvim-74a): import walks `spec.specs` after
registering the parent, treating entries as peer specs (not
dependencies, matching LazyPluginSpec.specs).
* `pin = true` (P2 zpack_nvim-gi5): bulk :ZPack update filters pinned
plugins from the explicit name list it passes to vim.pack.update.
When nothing is pinned the fast-path falls back to vim.pack.update's
default "everything" behavior. Single-name updates ignore pin
(explicit > policy).
* `optional = true` (P2 zpack_nvim-sg0): merge.resolve_all marks
entries whose every contributing spec carries `optional = true` as
disabled, piggybacking on the existing propagate/prune machinery.
Required dep registrations naturally keep the plugin alive.
* `dev = true` (P2 zpack_nvim-lkb): new `dev = { path, fallback }`
setup config. normalize_source resolves `dev = true` specs to
`config.dev.path/<derived-name>`; `fallback = true` falls through to
the regular source when the local directory is missing.
* cmd lazy proxy nargs (P3 zpack_nvim-7p7): after loading the real
command, consult its nargs and re-pack the proxy's whitespace-split
fargs into a single arg when nargs is '1' or '?'. Matches
lazy.nvim/handler/cmd.lua:44-47. Proxy stays `count = -1` (covers
the common `:5Telescope` count form via range translation, which
range=true would reject at parse time).
* `import = function()` (P3 zpack_nvim-fqs): import.lua now accepts a
function-form `import` field; invokes it inside pcall and treats a
table return as a spec list to recurse into.
validate.lua widens with new fields: `specs`, `pin`, `optional`, `dev`,
`virtual`, `deactivate`. types.lua documents the new spec fields and
the augmented Plugin shape.
Tests: nine new regressions (build :prefix, build shell dispatch,
build array, build = false, mixed-type array, cmd nargs=1, cmd
nargs='?'). 455/455 pass; luacheck clean.
Round two of the lazy.nvim spec parity closure:
* `:ZPack sync` (zpack_nvim-0sp): bulk update + clean in one step;
bang form `:ZPack! sync` force-applies. lazy.nvim parity for
`:Lazy sync`.
* `:ZPack check [plugin]` (zpack_nvim-xrx): delegates to
vim.pack.update without force=true (opens the confirmation buffer
as a "what would update" preview). lazy.nvim parity for `:Lazy check`.
* `:ZPack log {plugin}` (zpack_nvim-wwl): spawns `git log --oneline
-n 40` inside the plugin path via vim.system; renders in a botright
scratch buffer with filetype=git. lazy.nvim parity for `:Lazy log`.
* `:ZPack reload {plugin}` (zpack_nvim-dpl + zpack_nvim-aht): runs the
spec's `deactivate` hook (caught throw -> warn), drops
package.loaded entries for the resolved main module + submodules,
resets load_status to 'pending', and re-runs try_process_spec.
lazy.nvim parity for `:Lazy reload`. validate.lua now accepts
`deactivate = function`.
* `virtual = true` (zpack_nvim-fqt): merge.resolve_all marks virtual
entries and skips them from vim_packs. registration.lua synthesizes
the plugin object (path/dir=nil) so the same startup/lazy machinery
walks them. startup.lua and plugin_loader.lua skip packadd when
is_virtual. Dependencies still install; config/init still runs.
utils.resolve_main bails when plugin.path is nil — virtual plugins
that need auto-setup must declare `main` explicitly.
90b (lazy-lock.json import) closed as out-of-scope per
.claude/review-decisions.md — would couple zpack to lazy.nvim's
lockfile schema and bypass vim.pack's authoritative lockfile flow.
Docs:
* docs/spec.md: documents build's array/`:`/shell/false dispatch,
deactivate hook, version=false escape hatch, pin/optional/dev/
virtual/specs flags, import as function, augmented zpack.Plugin
shape (name/dir/dependencies).
* doc/zpack.txt: same coverage for vimdoc readers + new commands
(sync/check/log/reload) and `setup({ dev = { path, fallback } })`
config section.
* README.md: command-list bullets for the four new subcommands.
Tests: 17 new (lazy_parity_test.lua) covering version=false, Plugin
shape augmentation, nested specs, pin filter, optional pruning,
optional-as-dep survival, function-form import, sync, check, virtual
(2x), deactivate+reload, plus 2x nargs=1/? cmd proxy regressions
(lazy_cmd_test.lua) and a bare-string dependency regression
(dependencies_test.lua). 472/472 pass; luacheck clean.
Post-review pass on the lazy-parity branch.
Cut as out of scope:
* `virtual = true` (zpack_nvim-fqt): the only feature in the branch that
explicitly bypassed vim.pack.add — special-cased 5 modules and
synthesized a fake plugin object with path=nil. Same yardstick that
closed lazy-lock.json import: don't sidestep vim.pack's authoritative
flow. Same outcome is expressible via an import directory returning a
spec list with `dependencies` + a regular `config`.
Reload hardening (commands.lua Sub.reload):
* Refuse to reload mid-load — flipping `loading` -> `pending` would slip
past process_spec's circular-dep guard and double-run config.
* Scope `package.loaded` sweep via `package.searchpath` to modules whose
on-disk file lives under THIS plugin's lua/ dir — prevents collateral
damage to sibling plugins nested under the same namespace (e.g.
reloading `telescope` no longer drops telescope-fzf-native's
`telescope.extensions.fzf`).
Build hook hardening (hooks.lua):
* `build = true` now notifies (previously silent no-op since it matched
no dispatch branch).
* Array `build` steps chain via `on_done` so shell steps don't
interleave — `{ 'git submodule update --init', 'make' }` now runs
serially.
Dev (`dev = true`) hardening (import.lua):
* `vim.uv.fs_stat` accepts files; tightened to `stat.type == 'directory'`.
* Notify when `dev = true` lacks a source field to derive the local
checkout name from.
* Drop the dead `state.config` fallback — setup() assigns it before any
import_specs call.
Misc:
* `Sub.delete` uses `pack.spec.name` (canonical) instead of user-typed
name for case-insensitive FS safety.
* `names_for_bulk_update` skips `zpack.nvim` in the registry loop so
the update confirmation buffer doesn't list it twice.
* README: clarifies `dev = true` is source rewriting (not file-watch /
auto-reload) and `:ZPack reload` is a manual command — both consistent
with the README's "no dev mode / change-detection" non-goal.
Tests: 470/470 pass (down from 472; the 2 virtual=true tests went with
the feature). luacheck clean. lua-language-server unchanged.
Second post-review pass on the lazy-parity branch.
Cut as out of scope:
* :ZPack log (zpack_nvim-wwl): sidestepped vim.pack (spawned git log
directly on plugin.path) AND owned a UI seam (botright new + scratch
buffer with filetype=git) — the only :ZPack subcommand that opened a
window, in a manager whose README explicitly disclaims "UI dashboard".
Same yardstick as the virtual=true and lazy-lock.json import cuts.
* :ZPack check (zpack_nvim-xrx): strict subset of :ZPack update — both
delegated to run_pack_update(arg, nil, ...) (vim.pack.update without
force opens the confirm buffer, which IS the preview). Vocab-only
alias; the README/help can note the lazy.nvim mapping for muscle
memory.
:ZPack reload sweep fix (commands.lua Sub.reload):
* The previous sweep used package.searchpath(key, package.path) to
scope module clearing to the plugin's lua/ dir, but Neovim's lua
loader walks runtimepath rather than augmenting package.path — so
searchpath returned nil for every plugin module and the sweep never
cleared anything. Switch to a direct vim.uv.fs_stat check on
<lua_dir>/<key>.lua and <key>/init.lua. Existing test only checked
lifecycle order; new regression pins the sweep clears matching keys
and leaves siblings + unrelated keys alone.
* Guard plugin == nil before resolve_main / lua_dir derivation.
:ZPack sync semantics:
* Drop bang; sync always force-applies. vim.pack.update returns
immediately under no-force (opens confirm buffer), so chaining
clean_unused() synchronously would race ahead of the user's
confirm/cancel response — cleanup persists even on dismiss. For a
preview, use :ZPack update without !.
import = function() (import.lua):
* Per-setup() visited-function set guards against
f = function() return { { import = f } } end self-recursion.
Shell-form build (hooks.lua):
* Switch from \$SHELL / &shell to a hard-coded sh -c (cmd.exe /c on
Windows). The previous form crashed with ENOENT for users whose
&shell carried args (e.g. bash --login) since vim.system treats
argv[0] as a literal filename.
plugin.dependencies (registration.lua):
* Skip deps that prune_disabled dropped (e.g. optional = true with no
required reference) so callbacks don't see phantom dep names the
user can never load.
Misc:
* Drop dead command.nargs = info.nargs in lazy_trigger/cmd.lua
(verified: nvim_cmd ignores the field — the load-bearing read is the
info.nargs:find("[1?]") arg re-pack, which stays).
* names_for_bulk_update honors a user-spec'd pin = true on zpack.nvim
itself (previously seeded unconditionally).
* Doc/README: drop log/check entries; update :ZPack sync description;
fix doc/zpack.txt non-goals ("no dev mode" was contradicted by the
new dev = true field) and migration-dev advice.
New tests pinning the fixes:
* :ZPack sync force-applies (otherwise clean races confirm).
* :ZPack reload clears package.loaded for plugin modules; leaves
sibling/unrelated keys alone.
* dev = true happy path / fallback / no-source-field notify.
Tests: 473/473 pass; luacheck clean; lua-language-server check
unchanged (89 vs baseline 90 — dead nargs deletion cleared one warning).
Third-pass review on the lazy-parity branch.
* Nested `specs` propagated parent's `is_dependency` (import.lua):
`dependencies = { { 'A', specs = { 'B' } } }` silently marked B as a
dep, contradicting the inline doc-comment and lazy.nvim's peer
semantic. Reset `is_dependency = false` on the peer ctx.
* `:ZPack reload` keyed unloaded_plugin_names on user-typed name
(commands.lua): same class as the Sub.delete canonical-name fix in
0463771 — user-typed name can differ in case on case-insensitive FS,
while plugin_loader clears by pack.spec.name. Mirror the fix.
* :checkhealth zpack didn't surface dev mode (health.lua): when at
least one spec has `dev = true`, emit
`dev: N plugin(s) → <path> (fallback: <bool>)`. Skipped when no dev
plugins so non-users see no extra line.
* tests/validate_test.lua merged-config fixture missed the `dev`
default that init.lua now populates — added so the test mirrors what
:checkhealth actually passes.
* tests/lazy_parity_test.lua: new regression
"specs nested inside a dependencies chain stay peers, not deps"
pinning the import.lua fix above (the existing top-level test
doesn't exercise the nested-dep case).
Tests: 474/474 pass (was 473; +1 regression). luacheck clean.
lua-language-server unchanged at baseline 89.
- :ZPack reload re-runs init (fresh-load contract; matches :Lazy reload) - :ZPack reload skips deactivate when plugin is nil (never-loaded / install-failed window) and falls back to no-prefix package.loaded sweep when utils.resolve_main has cached a not-found result - :ZPack update bulk path seeds names from vim.pack.get so pinning one plugin no longer silently narrows the universe past the registry - resolve_dev_path coerces bad dev.path/dev.fallback to defaults and names the offending spec in the no-source-field notify - normalize_version cast clears the LLS warning the `version = false` early-return left behind - ProcessContext declares _imported_functions for the function-form import dedup set
* :ZPack reload (commands.lua Sub.reload): guard try_call_hook on
type(spec.init) == 'function'. Pre-fix, reload of a no-init spec
(the common case) emitted "expected init missing" ERROR — startup
pre-filters via ctx.src_with_init; reload now mirrors that.
* optional = true on table-form deps (merge.lua resolve_all): the
optional prune now treats _is_dependency = true as defeating
optional. Pre-fix, `dependencies = { { 'foo/x', optional = true } }`
pruned x and cascade-disabled its parent via
propagate_enabled_disable. normalize_dependencies only wraps string
deps into a fresh {deps} table — table-form deps pass through with
`optional` preserved, so the prune saw a single optional contributor.
A dep declaration IS a non-optional reference (lazy.nvim parity).
* dev = true derivation (import.lua resolve_dev_path):
- prefer spec.name over [1]/src/url/dir for the derived local dir
name (lazy.nvim parity for `me/myplugin.nvim` → ~/projects/myplugin)
- strip trailing slash from dev.path so `~/projects/` does not yield
`~/projects//<name>` registry keys that drift between sessions.
* import = function() returning non-table (import.lua): notifies WARN
on stray return values. Mirrors load_spec_module's notify; pre-fix
a `return 'oops'` or implicit nil return was silently dropped.
* docs/tips.md: replace stale dev mode advice
(`src = vim.fn.expand('~/projects/...')`) with the shipped
`dev = true` + `setup({ dev = { path } })` flow.
* docs/spec.md: move `-- version = false` next to canonical `version`
block, matching doc/zpack.txt grouping.
Tests: 479/479 pass (+2 regressions pinning the critical fixes).
luacheck clean. lua-language-server unchanged at baseline.
LLS sees the callback param as vim.pack's { spec, path } shape, so injecting
name/dir/dependencies fired four inject-field warnings in CI. Cast to
zpack.Plugin at the top of the callback — the type adds those fields and
doc-comments them as lazy.nvim parity additions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Close lazy.nvim spec-parity gaps so user-authored specs copy-pasted from upstream plugin READMEs work without silent field drops. Pure spec-layer + command-layer additions — no
vim.packsidestepping. README's "thin layer overvim.pack" charter preserved; file-watch / auto-reload / UI dashboard remain out of scope.Spec fields
pin = true— exclude from:ZPack updatebulk runsoptional = true— only install if also referenced non-optionallydev = true+setup({ dev = { path, fallback } })— rewrite source to local checkout under<path>/<derived-name>(source rewriter only; no file-watch)specs— companion plugin grouping (peers, not deps)version = false— opt out of versioning (lazy.nvim escape hatch)build— table form (serial array) +falseopt-outdeactivate— teardown hook for:ZPack reloadimport = function()— function form returning spec listzpack.Pluginshape additions (lazy.nvim parity)name,dir,dependencies,main(additive aliases; existingspec/pathpreserved)Commands
:ZPack sync— bulk update (force) + clean:ZPack reload {plugin}— runsdeactivate, clears matchingpackage.loadedentries, re-runsinit/configviaprocess_specBug fixes folded in
rangeand re-packs whitespace-split args for realnargs = '1' | '?'commands (previously rejected as "too many arguments" on first invocation)builduses hard-codedsh -c/cmd.exe /cinstead of$SHELL(which broke for users whose&shellcarried args likebash --login):ZPack deleteusespack.spec.name(canonical) for case-insensitive FS safety:checkhealth zpacksurfaces dev-mode plugin count + resolved pathOut-of-scope items cut during review
virtual = true(synthesized fake plugin objects bypassingvim.pack.add):ZPack log(owned a UI seam + sidesteppedvim.packto spawngit log):ZPack check(strict subset alias of:ZPack update)vim.pack's authoritative lockfile)Test plan
nvim -u NONE -l tests/busted.lua— 479/479 passluacheck lua/ tests/cleanlua-language-server --checkunchanged at baselinetests/lazy_parity_test.lua(552 LOC) covers every parity field,tests/lifecycle_test.luacovers:ZPack reload+deactivate,tests/lazy_cmd_test.luacovers proxy mods/range/nargs