From 9d12df0398499d22bb509ad5c209dd608095596d Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 31 Jul 2026 15:17:00 +0300 Subject: 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). --- runtime/lua/vim/pack.lua | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) (limited to 'runtime/lua/vim/pack.lua') diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index a69684de75..0e986d3196 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -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 -- cgit