feat(pack): add support for sourcing manifest scripts

Problem: No way for plugins to define hooks that would be executed
  during plugin lifecycle. Like after install/update or before
  update/delete.

Solution: Automatically source scripts defined in plugin manifest file
  (if any) after triggering corresponding `PackChanged{,Pre}` events.
  During sourcing make some termporary adjustments:
  - Current directory is set to plugin's root to simplify execution of
    CLI calls like `make build`.
  - Plugin's path is ensured to be inside 'runtimepath' to allow using
    `require('plugin-module')` inside manifest scripts.

  The reason to execute after triggering event is so that there is a
  possibility for users to execute code both before the script (exactly
  on event) and after the script (more-or-less via `vim.schedule` called
  on the event).
This commit is contained in:
Evgeni Chasnovski
2026-07-31 15:17:00 +03:00
parent 4cdd6d76c5
commit 9d12df0398
4 changed files with 242 additions and 2 deletions

View File

@@ -236,6 +236,14 @@
---
---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.
---
@@ -248,6 +256,12 @@
--- "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"
--- }
---}
---```
@@ -540,11 +554,21 @@ local n_active_plugins = 0
--- @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?
@@ -566,14 +590,43 @@ local function manifest_read(path)
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