From df332a8bcaf2306ad760a169e65a0c8acc0d7d92 Mon Sep 17 00:00:00 2001 From: zuqini Date: Mon, 15 Jun 2026 11:29:41 -0700 Subject: [PATCH] test(import): cover single-file import path, document precedence PR #30 added single-file import (import = 'plugins.foo' resolving lua/plugins/foo.lua) but landed without tests or docs. This adds: - tests for the single-file path, file-over-directory precedence, and enabled=false gating on a single-file import - a code comment making the file-shadows-directory precedence intentional - vimdoc (doc/zpack.txt) coverage of the single-file form and precedence - corrected the import comment in docs/spec.md Closes zpack_nvim-thi --- doc/zpack.txt | 14 ++++-- docs/spec.md | 2 +- lua/zpack/import.lua | 10 +++- tests/import_test.lua | 108 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 6 deletions(-) diff --git a/doc/zpack.txt b/doc/zpack.txt index 1b0f0da..356211c 100644 --- a/doc/zpack.txt +++ b/doc/zpack.txt @@ -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 }, @@ -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 diff --git a/docs/spec.md b/docs/spec.md index f089586..4db5b6c 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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) } ``` diff --git a/lua/zpack/import.lua b/lua/zpack/import.lua index bcb8fa3..ca709c5 100644 --- a/lua/zpack/import.lua +++ b/lua/zpack/import.lua @@ -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 @@ -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/.lua` file takes precedence over a `lua//` 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 diff --git a/tests/import_test.lua b/tests/import_test.lua index fdc3759..0cb0504 100644 --- a/tests/import_test.lua +++ b/tests/import_test.lua @@ -110,6 +110,114 @@ describe("Spec Import", function() package.loaded['test_plugins.mini'] = nil end) + it("import resolves a single spec file directly (lua/.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/.lua file takes precedence over a lua// 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