aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua
diff options
context:
space:
mode:
authorJustin M. Keyes <justinkz@gmail.com>2026-08-13 08:53:49 -0400
committerGitHub <noreply@github.com>2026-08-13 08:53:49 -0400
commita1ea2f35be951673bd5ef100f96b756e9eec00f6 (patch)
treef281eaf8dd7c52ff8295c4aecb9776befcdc37e7 /runtime/lua
parent3934dabfbb9f4a185750b50a38efc56eac0138fc (diff)
parent9d12df0398499d22bb509ad5c209dd608095596d (diff)
downloadrneovim-a1ea2f35be951673bd5ef100f96b756e9eec00f6.tar.gz
rneovim-a1ea2f35be951673bd5ef100f96b756e9eec00f6.tar.bz2
rneovim-a1ea2f35be951673bd5ef100f96b756e9eec00f6.zip
Merge #41075 from echasnovski/pack-packspec-part1
Diffstat (limited to 'runtime/lua')
-rw-r--r--runtime/lua/vim/pack.lua106
-rw-r--r--runtime/lua/vim/pack/health.lua43
2 files changed, 149 insertions, 0 deletions
diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua
index 2b456a6dbb..0e986d3196 100644
--- a/runtime/lua/vim/pack.lua
+++ b/runtime/lua/vim/pack.lua
@@ -232,6 +232,39 @@
----- 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:
+---- Apply |:source| for scripts after triggering corresponding |vim.pack-events|.
+--- This allows plugins to define hooks that will be executed during plugin's lifetime.
+--- Sourcing is done with special context:
+--- - The |current-directory| is temporarily set to plugin's root (to make
+--- it easier to run |vim.system()| commands).
+--- - Plugin's path is temporarily ensured to be inside |'runtimepath'| (so script
+--- can use |require()| with plugin's module, possibly with explicit |package.loaded|
+--- reset inside `"update"` scripts).
+---- 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"
+--- },
+--- "scripts": {
+--- "install": "scripts/install.lua",
+--- "preupdate": "scripts/preupdate.vim",
+--- "update": "scripts/update.vim",
+--- "preuninstall": "scripts/preuninstall.lua"
+--- }
+---}
+---```
local api = vim.api
local uv = vim.uv
@@ -516,14 +549,84 @@ 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.ManifestScripts
+--- @inlinedoc
+--- @field install? string Post install script.
+--- @field preupdate? string Pre update script.
+--- @field update? string Post update script.
+--- @field preuninstall? string Pre delete script.
+
+--- @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
+--- Script locations (relative to plugin's root) to |:source| after triggering
+--- corresponding |vim.pack-events|.
+--- @field scripts? vim.pack.ManifestScripts
+
+--- @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 p vim.pack.Plug
+--- @param name string
+local function source_manifest_script(p, name)
+ local manifest = manifest_read(p.path)
+ if not (type(manifest) == 'table' and (manifest.scripts or {})[name]) then
+ return
+ end
+
+ local script_path = vim.fs.joinpath(p.path, (manifest.scripts or {})[name])
+ vim._with({ cwd = p.path, o = { runtimepath = vim.o.runtimepath } }, function()
+ vim.cmd.packadd({ p.spec.name, bang = true })
+ ---@diagnostic disable-next-line: no-unknown
+ local ok, err = pcall(vim.cmd.source, { script_path, magic = { file = false, bar = false } })
+ if not ok then
+ notify(err, 'WARN')
+ end
+ end)
+end
+
+local manifest_script_name_map = {
+ PackChangedPre = { update = 'preupdate', delete = 'preuninstall' },
+ PackChanged = { install = 'install', update = 'update' },
+}
+
--- @param plugs vim.pack.Plug[]
--- @param event_name 'PackChangedPre'|'PackChanged'
--- @param kind 'install'|'update'|'delete'
local function trigger_events(plugs, event_name, kind)
+ local manifest_script_name = manifest_script_name_map[event_name][kind]
for _, p in ipairs(plugs) do
local active = active_plugins[p.path] ~= nil
local data = { active = active, kind = kind, spec = vim.deepcopy(p.spec), path = p.path }
api.nvim_exec_autocmds(event_name, { pattern = p.path, data = data })
+
+ if manifest_script_name then
+ source_manifest_script(p, manifest_script_name)
+ end
end
end
@@ -1468,6 +1571,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 +1599,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)
diff --git a/runtime/lua/vim/pack/health.lua b/runtime/lua/vim/pack/health.lua
index 5045c58c8f..6e4749976a 100644
--- a/runtime/lua/vim/pack/health.lua
+++ b/runtime/lua/vim/pack/health.lua
@@ -200,6 +200,44 @@ local function check_lockfile()
end
end
+--- @param manifest vim.pack.Manifest
+local function check_manifest(manifest, plug_name, plug_path)
+ 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
+
+ local ok_scripts = true
+ ---@diagnostic disable-next-line: no-unknown
+ for name, script_path in pairs(manifest.scripts or {}) do
+ if vim.fn.filereadable(vim.fs.joinpath(plug_path, script_path)) == 0 then
+ health.warn(('Plugin %s has no %s script at %s path'):format(name_str, name, script_path))
+ ok_scripts = false
+ end
+ end
+ if not ok_scripts then
+ 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 +286,11 @@ local function check_installed_plugin(plug_name)
)
end
+ -- Manifest
+ if info[1].manifest then
+ return check_manifest(info[1].manifest, plug_name, plug_path)
+ end
+
return true
end