Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions doc/zpack.txt
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ Plugin-level settings always take precedence over `defaults`.
-- or import from a custom directory e.g. `./lua/a/b/plugins/`
require('zpack').setup({ { import = 'a.b.plugins' } })

-- or point at a single spec file e.g. `./lua/plugins/telescope.lua`
require('zpack').setup({ { import = 'plugins.telescope' } })

-- or add your specs inline in setup
require('zpack').setup({
{ 'neovim/nvim-lspconfig', config = function() ... end },
Expand Down Expand Up @@ -856,9 +859,14 @@ module (boolean, optional)
*zpack-Spec.import*
import (string|function, optional)
Module path to import specs from (e.g., 'plugins' or
'plugins.lsp'). Imports all .lua files from lua/{path}/
and all subdirectories with init.lua (lua/{path}/*/init.lua).
Each file should return a spec or list of specs.
'plugins.lsp'). If lua/{path}.lua exists it is loaded as a
single spec module; otherwise zpack imports all .lua files
from lua/{path}/ and all subdirectories with init.lua
(lua/{path}/*/init.lua). A lua/{path}.lua file takes
precedence over a lua/{path}/ directory (mirroring Lua's own
`require`, where foo.lua shadows foo/init.lua); the directory
walk is skipped. Each file should return a spec or list of
specs.

lazy.nvim parity: `import` also accepts a function. The
function is called inside pcall, and a table return value
Expand Down
2 changes: 1 addition & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
specs = { { 'companion/plugin' } }, -- Companion plugin specs grouped with this one

-- Spec imports
import = "plugins.lsp", -- Import from lua/{path}/*.lua and lua/{path}/*/init.lua
import = "plugins.lsp", -- A single lua/{path}.lua spec module, else lua/{path}/*.lua and lua/{path}/*/init.lua
-- import = function() return { ... } end, -- Or a function returning a spec list (lazy.nvim parity)
}
```
Expand Down
10 changes: 8 additions & 2 deletions lua/zpack/import.lua
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ local load_spec_module = function(full_module, ctx)
end
end

---Import specs from a module directory
---@param module_path string Module path (e.g., 'plugins' imports from lua/plugins/*.lua)
---Import specs from a module file or directory (lazy.nvim parity).
---`import = 'plugins'` resolves `lua/plugins.lua` as a single spec module if it
---exists, otherwise walks `lua/plugins/*.lua` and `lua/plugins/*/init.lua`.
---@param module_path string Module path (e.g., 'plugins' imports from lua/plugins.lua or lua/plugins/)
---@param ctx zpack.ProcessContext
local import_from_module = function(module_path, ctx)
if imported_modules[module_path] then
Expand All @@ -187,6 +189,10 @@ local import_from_module = function(module_path, ctx)

local lua_path = vim.fn.stdpath('config') .. '/lua/' .. module_path:gsub('%.', '/')

-- A `lua/<path>.lua` file takes precedence over a `lua/<path>/` directory:
-- the file is the single, explicitly-named spec module and the directory walk
-- is skipped. This mirrors Lua's own `require` resolution (`foo.lua` shadows
-- `foo/init.lua`) so `import = 'plugins.telescope'` loads exactly that file.
if vim.uv.fs_stat(("%s.lua"):format(lua_path)) then
load_spec_module(module_path, ctx)
return
Expand Down
108 changes: 108 additions & 0 deletions tests/import_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,114 @@ describe("Spec Import", function()
package.loaded['test_plugins.mini'] = nil
end)

it("import resolves a single spec file directly (lua/<path>.lua)", function()
local utils = require('zpack.utils')
local original_lsdir = utils.lsdir
local original_stdpath = vim.fn.stdpath
local original_fs_stat = vim.uv.fs_stat
local lsdir_called = false
vim.fn.stdpath = function() return '/mock/config' end
utils.lsdir = function(path)
if path == '/mock/config/lua/test_plugins/telescope' then
lsdir_called = true
end
return {}
end
vim.uv.fs_stat = function(path)
if path == '/mock/config/lua/test_plugins/telescope.lua' then
return { type = 'file' }
end
return original_fs_stat(path)
end

package.loaded['test_plugins.telescope'] = { 'test/telescope' }

local state = require('zpack.state')
require('zpack').setup({ { import = 'test_plugins.telescope' } })
helpers.flush_pending()

assert.is_not_nil(state.spec_registry['https://github.com/test/telescope'],
"single-file import should register the file's spec")
assert.is_false(lsdir_called,
"a single-file import must not walk a same-named directory")

utils.lsdir = original_lsdir
vim.fn.stdpath = original_stdpath
vim.uv.fs_stat = original_fs_stat
package.loaded['test_plugins.telescope'] = nil
end)

it("a lua/<path>.lua file takes precedence over a lua/<path>/ directory", function()
local utils = require('zpack.utils')
local original_lsdir = utils.lsdir
local original_stdpath = vim.fn.stdpath
local original_fs_stat = vim.uv.fs_stat
vim.fn.stdpath = function() return '/mock/config' end
-- Both `lua/test_plugins.lua` and `lua/test_plugins/foo.lua` exist; the file
-- wins and the directory entry must be ignored.
utils.lsdir = function(path)
if path == '/mock/config/lua/test_plugins' then
return {
{ name = 'foo.lua', type = 'file' },
}
end
return {}
end
vim.uv.fs_stat = function(path)
if path == '/mock/config/lua/test_plugins.lua' then
return { type = 'file' }
end
return original_fs_stat(path)
end

package.loaded['test_plugins'] = { 'test/file-spec' }
package.loaded['test_plugins.foo'] = { 'test/dir-spec' }

local state = require('zpack.state')
require('zpack').setup({ { import = 'test_plugins' } })
helpers.flush_pending()

assert.is_not_nil(state.spec_registry['https://github.com/test/file-spec'],
"the file spec should be registered")
assert.is_nil(state.spec_registry['https://github.com/test/dir-spec'],
"the directory spec must be ignored when a same-named .lua file exists")

utils.lsdir = original_lsdir
vim.fn.stdpath = original_stdpath
vim.uv.fs_stat = original_fs_stat
package.loaded['test_plugins'] = nil
package.loaded['test_plugins.foo'] = nil
end)

it("single-file import with enabled=false skips import", function()
local utils = require('zpack.utils')
local original_lsdir = utils.lsdir
local original_stdpath = vim.fn.stdpath
local original_fs_stat = vim.uv.fs_stat
vim.fn.stdpath = function() return '/mock/config' end
utils.lsdir = function() return {} end
vim.uv.fs_stat = function(path)
if path == '/mock/config/lua/test_plugins/foo.lua' then
return { type = 'file' }
end
return original_fs_stat(path)
end

package.loaded['test_plugins.foo'] = { 'test/foo-plugin' }

local state = require('zpack.state')
require('zpack').setup({ { import = 'test_plugins.foo', enabled = false } })
helpers.flush_pending()

assert.is_nil(state.spec_registry['https://github.com/test/foo-plugin'],
"single-file import should not register when enabled=false")

utils.lsdir = original_lsdir
vim.fn.stdpath = original_stdpath
vim.uv.fs_stat = original_fs_stat
package.loaded['test_plugins.foo'] = nil
end)

it("import only goes 1 level deep for init.lua", function()
local utils = require('zpack.utils')
local original_lsdir = utils.lsdir
Expand Down
Loading