ward.module
ward.module is Ward’s external Lua module manager. It downloads third-party
Lua code into Ward’s data directory and makes it available to the current
process by extending package.path, so you can require("<name>.<submodule>")
(and other repo-local module names) directly. Downloads are stored in a
content-addressed layout so multiple versions can coexist and be reused across runs.
Import
local module = require("ward.module")
Where modules live
Ward keeps external modules under an “externals” directory in the user data home:
- Base directory:
${XDG_DATA_HOME}/ward/externals- If
XDG_DATA_HOMEis not set:~/.local/share/ward/externals
- If
- Content-addressed store:
${base}/.store/<id>
The store id is derived from the normalized source URL and the selected
revision (for git sources), so changing tag/branch/rev produces a
different <id>.
Ward maintains a per-process binding map so that once you bind <name>
to an id, future require(...) calls in that same run resolve consistently.
The binding operation also injects the selected store root into package.path
(idempotently), so standard Lua resolution works. Rebinding is rejected unless
you pass force=true, in which case Ward also clears matching package.loaded
entries and updates package.path to point at the new store root.
Naming rules
If you don’t specify a name, it is derived from the URL:
- Basename is taken from the URL path; common suffixes are removed
(
.git,.lua,.zip,.tar.gz,.tgz), and query/fragment parts are ignored. - The resulting name is canonicalized:
- lowercase
- non-alphanumeric separators collapse to
_ - names starting with a digit are prefixed with
_
Submodules are supported:
require("<name>.<submodule>")
(Each segment must be alphanumeric or _.)
API
module.dir() -> string
Returns the absolute path to the externals base directory.
local module = require("ward.module")
print(module.dir())
module.git(url, opts?) ->
Fetches a git repository, checks out a selected revision, and binds it as
<name> for this process.
Arguments
url(string): git repository URL (HTTPS/SSH, etc.)opts(table, optional):name(string): override derived module name / binding name- Exactly one selector:
rev(string): commit hashbranch(string): branch nametag(string): tag name- If none provided, defaults to “head”
force(boolean, defaultfalse): allow rebinding<name>to a different id (also clears cachedrequire)depth(integer): shallow clone depthrecursive(boolean): fetch submodulestimeout(number, seconds): overall fetch timeoutmax_bytes(integer): abort if transfer exceeds this sizefilter_blobs(boolean): request partial clone with blob filtering (when supported)
Return fields
ok(boolean): whether fetch/checkout succeededname(string): canonicalized namerequire(string): require target (e.g.my_lib)path/store_path(string): checkout directory pathid(string): content-addressed id for this URL+selector
Example
local module = require("ward.module")
local result = module.git("https://github.com/org/repo.git", {
name = "my_lib",
tag = "v1.2.3",
depth = 1,
recursive = true,
})
assert(result.ok, "git fetch failed")
-- Require a submodule from the checked out repo
local api = require(result.require .. ".api")
print("loaded", result.require, "from", result.path, "id", result.id)
module.url(url, opts?) -> { ok, name, require, path, store_path, id }
Downloads a single Lua file and binds it as <name>. The downloaded
content is stored as init.lua inside the store directory for its id.
Arguments
url(string): HTTP(S) URL to a.luafile (or any endpoint returning Lua content)opts(table, optional):name(string): override derived module nameinsecure(boolean, defaultfalse): allow invalid TLS certificatesfollow_redirects(boolean, defaulttrue)retries(integer, default5, minimum1)retry_delay(integer, milliseconds, default2000)force(boolean, defaultfalse): allow rebinding<name>to a new idtimeout(number, seconds): overall request timeoutmax_bytes(integer): abort if download exceeds this sizemethod(string): HTTP method (defaultGET)headers(table<string, string>): additional request headers
Return fields Same shape as module.git.
Example
local module = require("ward.module")
local result = module.url("https://example.com/plugin.lua", {
headers = { Authorization = "Bearer TOKEN" },
method = "POST",
retry_delay = 500,
})
assert(result.ok, "download failed")
local plugin = require(result.require)
print("plugin loaded:", plugin.version and plugin.version() or "<no version()>")
Notes and best practices
- Prefer pinning git modules with
tagorrevto get reproducible behavior. - Use
force=trueonly when you intentionally want to rebind a name within a single run. - If you need multiple versions at once, bind them under different names
(e.g.
my_lib_v1,my_lib_v2) rather than forcing rebinding.