feat(pack): introduce plugin manifest support

Problem: There is no agreed way for plugins to provide extra information
  about themselves. Like required Neovim version, install/update/delete
  hooks, and dependencies.

Solution: Introduce basic concept of plugin manifest file `pkg.json`
  based on https://packspec.org/.
This commit is contained in:
Evgeni Chasnovski
2026-07-31 11:28:11 +03:00
parent 02a25caa1b
commit 4cdd6d76c5
5 changed files with 140 additions and 0 deletions

View File

@@ -339,6 +339,7 @@ LUA
• |vim.log| provides a logging interface.
• |vim.pack.get()| output includes revision of a pending update.
• |vim.pack.get()| can fetch new updates before computing the output.
• |vim.pack| supports |vim.pack-manifest| of plugins.
• |vim.o| now accepts table style values for assignment.
• |vim.keycode()| returns structured info as return value 2.
• |Iter:count()| counts items in the iterator.

View File

@@ -423,6 +423,35 @@ These events can be used to execute plugin hooks. For example: >lua
vim.api.nvim_create_autocmd('PackChanged', { callback = hooks })
<
*vim.pack-manifest*
Plugins can come with a special top level `pkg.json` manifest file with extra
information. If present, `vim.pack` uses it for improved user experience:
• Running |:checkhealth| for `vim.pack` will perform extra checks to ensure
healthy plugin installation.
Full specification see at https://packspec.org/. See also |vim.pack.Manifest|.
A simple example: >json
{
"name": "best-plugin.nvim",
"description": "The best plugin for Neovim",
"engines": {
"nvim": ">=0.13.0",
"vim": ">=9.1.0"
}
}
<
*vim.pack.Manifest*
Fields: ~
• {description}? (`string`) Plugin description
• {engines}? (`table`) Supported engine versions. Values should be
|vim.version.range()| compatible specs.
• {nvim}? (`string`) Version range for Nvim.
• {vim}? (`string`) Version range for Vim.
• {name}? (`string`) Plugin name
*vim.pack.Spec*
@@ -506,6 +535,9 @@ get({names}, {opts}) *vim.pack.get()*
to current session.
• {branches}? (`string[]`) Available Git branches (first is default).
Missing if `info=false`.
• {manifest}? (`vim.pack.Manifest`) Data from the |vim.pack-manifest|.
Empty in case of reading error. Missing if `info=false`. See
|vim.pack.Manifest|.
• {path} (`string`) Plugin's path on disk.
• {rev} (`string`) Current Git revision. Taken from
|vim.pack-lockfile| if `info=false`.

View File

@@ -232,6 +232,25 @@
----- To act on install from lockfile, run before very first `vim.pack.add()`
---vim.api.nvim_create_autocmd('PackChanged', { callback = hooks })
---```
---[vim.pack-manifest]()
---
---Plugins can come with a special top level `pkg.json` manifest file with extra
---information. If present, `vim.pack` uses it for improved user experience:
---- Running |:checkhealth| for `vim.pack` will perform extra checks to ensure
--- healthy plugin installation.
---
---Full specification see at https://packspec.org/. See also |vim.pack.Manifest|.
---A simple example:
---```json
---{
--- "name": "best-plugin.nvim",
--- "description": "The best plugin for Neovim",
--- "engines": {
--- "nvim": ">=0.13.0",
--- "vim": ">=9.1.0"
--- }
---}
---```
local api = vim.api
local uv = vim.uv
@@ -516,6 +535,37 @@ end
local active_plugins = {}
local n_active_plugins = 0
--- @class vim.pack.ManifestEngines
--- @inlinedoc
--- @field nvim? string Version range for Nvim.
--- @field vim? string Version range for Vim.
--- @class vim.pack.Manifest
--- @field name? string Plugin name
--- @field description? string Plugin description
--- Supported engine versions. Values should be |vim.version.range()| compatible specs.
--- @field engines? vim.pack.ManifestEngines
--- @param path string
--- @return vim.pack.Manifest?
local function manifest_read(path)
local manifest_path = vim.fs.joinpath(path, 'pkg.json')
local stat = uv.fs_stat(manifest_path)
if not stat then
return nil
end
local fd = uv.fs_open(manifest_path, 'r', 438)
if not fd then
return {}
end
local data = assert(uv.fs_read(fd, stat.size, 0))
assert(uv.fs_close(fd))
local ok, res = pcall(vim.json.decode, data)
return (ok and type(res) == 'table') and res or {}
end
--- @param plugs vim.pack.Plug[]
--- @param event_name 'PackChangedPre'|'PackChanged'
--- @param kind 'install'|'update'|'delete'
@@ -1468,6 +1518,8 @@ end
--- @class vim.pack.PlugData
--- @field active boolean Whether plugin was added via |vim.pack.add()| to current session.
--- @field branches? string[] Available Git branches (first is default). Missing if `info=false`.
--- Data from the |vim.pack-manifest|. Empty in case of reading error. Missing if `info=false`.
--- @field manifest? vim.pack.Manifest
--- @field path string Plugin's path on disk.
--- @field rev string Current Git revision. Taken from |vim.pack-lockfile| if `info=false`.
--- Git revision of a pending update. The same as used during |vim.pack.update()| and which
@@ -1494,6 +1546,7 @@ local function add_p_data_info(p_data_list, offline)
funs[i] = function()
p_data.branches = git_get_branches(path)
p_data.tags = git_get_tags(path)
p_data.manifest = manifest_read(path)
if not offline then
git_fetch(path)

View File

@@ -200,6 +200,31 @@ local function check_lockfile()
end
end
local function check_manifest(manifest, plug_name)
local name_str = vim.inspect(plug_name)
if vim.tbl_count(manifest) == 0 then
health.warn(('Plugin %s has empty or malformed manifest file'):format(name_str))
return false
end
local nvim_engine = (manifest.engines or {}).nvim or '*'
local ok_version, nvim_version_range = pcall(vim.version.range, nvim_engine)
if not ok_version then
health.warn(('Plugin %s has malformed `engines.nvim` in manifest file'):format(name_str))
return false
end
--- @cast nvim_version_range vim.VersionRange
if not nvim_version_range:has(vim.version()) then
health.warn(
('Plugin %s Nvim version requirement %s'):format(name_str, tostring(nvim_version_range))
.. (' does not match current version %s'):format(tostring(vim.version()))
)
return false
end
return true
end
--- @return boolean Whether a check is successful
local function check_installed_plugin(plug_name)
local name_str = vim.inspect(plug_name)
@@ -248,6 +273,11 @@ local function check_installed_plugin(plug_name)
)
end
-- Manifest
if info[1].manifest then
return check_manifest(info[1].manifest, plug_name)
end
return true
end

View File

@@ -244,6 +244,21 @@ function repos_setup.with_subs()
git_add_commit('Second commit for "with_subs"', 'with_subs')
end
function repos_setup.with_manifest()
init_test_repo('manifest')
repo_write_file('manifest', 'lua/manifest.lua', 'return "manifest init"')
local manifest_tbl = {
name = 'plug',
description = 'Nvim plugin',
engine = { nvim = '>=0.12.0', vim = '>=9.1.0' },
}
repo_write_file('manifest', 'pkg.json', vim.json.encode(manifest_tbl))
git_add_commit('Initial commit', 'manifest')
end
-- Utility --------------------------------------------------------------------
--- Execute `vim.pack.add()` inside `testnvim` instance
@@ -2227,6 +2242,15 @@ describe('vim.pack', function()
)
eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" }, { info = false })'))
eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })'))
-- Reports manifest
vim_pack_add({ repos_src.manifest })
local manifest_tbl = {
name = 'plug',
description = 'Nvim plugin',
engine = { nvim = '>=0.12.0', vim = '>=9.1.0' },
}
eq(manifest_tbl, exec_lua('return vim.pack.get({ "manifest" })[1].manifest'))
end)
it('reports potential revision after update', function()