Saltar al contenido

Módulos y require: runtimepath, caché y recarga

Cómo Lua carga un módulo y por qué Neovim sustituye el buscador estándar: el rastreo de los directorios lua de todo el runtimepath, la caché de package.loaded, el acelerador vim.loader y las condiciones reales para recargar un módulo en caliente sin engañarte.

require("config.keymaps") parece una línea inocente, y en realidad dispara una cadena de decisiones: buscar un archivo entre decenas de directorios, compilarlo, ejecutarlo una única vez, guardar su resultado y devolver siempre esa misma referencia. Si no conoces esa cadena, tarde o temprano te encontrarás editando un archivo, recargándolo y viendo que no pasa nada.

  • Escribir módulos que devuelvan una tabla y razonar sobre su ámbito privado.
  • Explicar cómo el buscador de Neovim traduce un nombre de módulo a una ruta del runtimepath.
  • Describir el papel de package.loaded y del acelerador vim.loader.
  • Diagnosticar los dos errores de carga clásicos: el módulo que no aparece y el que devuelve true.
  • Recargar un módulo en caliente entendiendo qué efectos secundarios no se deshacen.

Un módulo es un archivo que devuelve una tabla

No hay palabra clave module ni export. Un módulo es un archivo Lua normal cuyo valor de retorno se convierte en lo que recibe quien lo pide. El convenio universal es devolver una tabla, casi siempre llamada M, con lo público colgado de ella y lo privado como locales del archivo — el patrón de la lección 7.1, ahora con nombre y apellidos.

LUA
-- lua/mi/git.lualocal M = {}local cache_rama                        -- privado: nadie fuera lo velocal function ejecutar(cmd)            -- privado  return vim.system(cmd, { text = true }):wait()endfunction M.rama()  if cache_rama then return cache_rama end  local r = ejecutar({ "git", "branch", "--show-current" })  cache_rama = vim.trim(r.stdout or "")  return cache_ramaendfunction M.invalidar() cache_rama = nil endreturn M                                -- sin esto, require devuelve true

Ese return M final no es decorativo. Si lo olvidas, el archivo se ejecuta igual —sus efectos ocurren, sus locales nacen y mueren— pero require guarda true como valor del módulo, y la llamada siguiente falla con attempt to index a boolean value, uno de los mensajes más desconcertantes para quien empieza porque el error señala al consumidor, no al módulo culpable.

El nombre que pasas a require usa puntos, no barras ni extensión, y se traduce a una ruta relativa dentro de un directorio lua. Además, un directorio con init.lua se puede pedir por su nombre a secas — el mismo convenio que tu ~/.config/nvim/init.lua, una escala más abajo.

LUA
require("mi.git")        --> lua/mi/git.lua, o lua/mi/git/init.luarequire("mi")            --> lua/mi.lua, o lua/mi/init.lua

Cómo encuentra Neovim tu módulo

Aquí viene la diferencia que casi nadie explica. El Lua de fábrica busca módulos expandiendo las plantillas de package.path. Neovim añade su propio buscador delante: uno que recorre los directorios lua/ de cada entrada del runtimepath, en el orden en que las lista :echo nvim_list_runtime_paths() — tu configuración primero, luego cada plugin, luego el runtime de Neovim, y al final las variantes after/. Solo si esa ronda no encuentra nada se cae al mecanismo estándar de Lua.

LUA
:lua vim.print(vim.opt.runtimepath:get())-- ~/.config/nvim, los directorios de cada plugin, $VIMRUNTIME,-- ~/.local/share/nvim/site... y al final las variantes after/

Esto explica dos cosas que confunden a diario. La primera: cualquier plugin instalado puede exponer módulos, porque su carpeta entra en el runtimepath —eso hace vim.pack.add de la lección 2.2, entre otras cosas— y su subdirectorio lua/ pasa a ser visible para todos. Una entrada del runtimepath no solo aporta lua/: también plugin/, ftplugin/, colors/ y, desde 0.12, lsp/ con su variante after/lsp/ para configuraciones de servidores, como viste en el Nivel 3. La segunda: el orden importa, y como tu configuración va primero, un archivo tuyo en lua/telescope/init.lua haría sombra al plugin real: todo require("telescope") del sistema —incluido el del propio plugin— recibiría tu tabla. Los nombres de tus módulos deberían llevar prefijo propio precisamente por eso.

Cuando algo no aparece, hay dos herramientas de diagnóstico. El propio mensaje de error de require lista todas las rutas que se probaron, en orden: léelo entero una vez y la máquina dejará de ser opaca. Y vim.api.nvim_get_runtime_file responde qué archivos ve Neovim y en qué orden, con soporte de comodines:

LUA
:lua vim.print(vim.api.nvim_get_runtime_file("lua/mi/git.lua", true)):lua vim.print(vim.api.nvim_get_runtime_file("lua/*/init.lua", true))

require no es lo mismo que source

require busca en los directorios lua/ del runtimepath, cachea y devuelve un valor. dofile y :luafile ejecutan una ruta concreta del disco, sin caché y sin registrar nada. El comando :runtime busca en el runtimepath pero fuera de lua/, y ejecuta todo lo que coincida. Cada uno tiene su sitio: módulos con require, scripts sueltos con :luafile — la pareja completa la verás en la 7.5.

La caché: package.loaded y vim.loader

require ejecuta el archivo una sola vez por sesión. El valor devuelto se guarda en package.loaded bajo el nombre del módulo, y todas las llamadas posteriores devuelven esa misma referencia sin volver a tocar el disco. package.loaded es una tabla normal y puedes inspeccionarla:

LUA
:lua vim.print(vim.tbl_count(package.loaded))       -- cuantos modulos vivos:lua vim.print(package.loaded["mi.git"] ~= nil)     -- esta cargado

Esa identidad compartida es la propiedad más importante y la más olvidada, y es la semántica de referencia de la 7.1 a escala de proceso: si dos archivos hacen require("mi.git"), ambos manipulan la misma tabla. Un módulo es, de facto, un singleton, y su estado interno —el cache_rama del ejemplo— es estado global compartido por todo el que lo importe. Eso vale también para los módulos de los plugins: cuando tu configuración hace require("blink.cmp"), recibe la misma tabla que el propio plugin usa por dentro.

Encima de esa caché hay una segunda, en disco. vim.loader —todavía marcado como experimental en 0.12— reemplaza el buscador por uno que memoriza la resolución de nombres a rutas y guarda el bytecode compilado de cada archivo, de modo que en los arranques siguientes no se vuelve a parsear el fuente. En configuraciones con cientos de módulos recorta decenas de milisegundos, y 0.12 lo afinó: :packadd y vim.pack.add actualizan la caché de rutas en el sitio en vez de invalidarla entera.

LUA
vim.loader.enable()                         -- al principio del init.luavim.loader.reset(vim.fn.stdpath("config"))  -- invalida lo cacheado de una ruta

Recargar en caliente sin engañarte

Recargar es, en teoría, una línea: borra la entrada de la caché y vuelve a pedir el módulo.

LUA
local function recargar(nombre)  package.loaded[nombre] = nil  return require(nombre)endvim.api.nvim_create_user_command("R", function(o) recargar(o.args) end,  { nargs = 1, desc = "Recargar un modulo Lua" })

En la práctica, esa línea solo consigue una tabla nueva. Todo lo que el módulo hizo al ejecutarse la primera vez sigue ahí, porque el editor no lleva un registro de qué efectos produjo cada archivo:

Referencias colgadas

Quien hiciera local git = require("mi.git") conserva la tabla vieja. Solo se actualiza quien vuelva a pedir el módulo después de la recarga.

Efectos que persisten

Autocomandos, mapeos, comandos de usuario y temporizadores creados al cargar se duplican en cada recarga si no usas un augroup con clear (Nivel 9).

Identidad rota

Una metatabla recreada ya no es la misma tabla: los objetos construidos antes de recargar siguen apuntando a la vieja y no ven los métodos nuevos.

De ahí la disciplina que hace la recarga fiable: que cargar un módulo sea idempotente. Declara los autocomandos dentro de un grupo creado con clear = true, define los mapeos en una función setup en vez de en el cuerpo del archivo, guarda el estado que deba sobrevivir en un sitio estable, y mientras estés iterando sobre un módulo prefiere require("mi.git").rama() —que pasa por la caché y ve la versión vigente— a capturar el módulo en una local de larga vida.

El singleton silencioso que gobierna tu configuración

La idea que hay que llevarse de esta lección es que require no es una directiva de importación: es una memoización con efectos secundarios. Cada módulo se ejecuta una sola vez, y ese momento único es cuando registra autocomandos, crea comandos, arranca temporizadores y toca opciones globales. La tabla que devuelve no es una copia, es la única instancia, compartida por todo el proceso, sin frontera entre tu configuración y el código de cualquier plugin: en el espacio Lua de Neovim todos habitáis el mismo package.loaded, el mismo _G y el mismo runtimepath, y el orden de ese runtimepath decide quién gana ante un nombre repetido. Comprender esto cambia cómo escribes. Deja de importar por costumbre y empieza a preguntarte qué ocurre en el instante exacto de la primera carga, porque ese instante es tu única oportunidad de ser ordenado. Separa definir de activar: que el cuerpo del archivo solo construya la tabla y que todo efecto secundario viva en una función setup que puedas volver a invocar sin acumular basura. Ese es el motivo real por el que la comunidad entera adoptó el patrón setup, y por el que la carga perezosa que ofrecen los gestores de plugins es posible: si el efecto está separado de la definición, puedes retrasarlo, repetirlo o cancelarlo. Un módulo bien escrito no es el que hace muchas cosas al cargarse, sino el que se puede volver a cargar sin que el editor se entere.

Drills

El mensaje lista cada ruta probada: los lua/no/existe.lua de cada entrada del runtimepath, las bibliotecas compartidas, las plantillas de package.path. Es el mapa completo del buscador, impreso gratis cada vez que algo falla.

true: identidad, no igualdad de contenido. La segunda llamada ni miró el disco: salió de package.loaded. Con cualquier módulo tuyo pasa exactamente lo mismo.

Decenas o cientos, y solo has abierto el editor. Cada plugin, cada módulo de vim.* cargado bajo demanda, cada archivo de tu configuración: todos conviven en la misma tabla.

Lista vacía si aún no existe; con la ruta en cuanto crees el archivo. La misma consulta con comodines (lua/*/init.lua) te enseña el bosque entero de módulos instalados.

Domina el ciclo de carga

  1. Crea lua/mi/probe.lua en tu configuración con una local privada, una función pública y su return M; comprueba desde :lua que require("mi.probe") == require("mi.probe") es true.
  2. Usa nvim_get_runtime_file con un comodín para listar dónde buscaría Neovim tus módulos, y explica el orden que ves comparándolo con vim.opt.runtimepath:get().
  3. Quita a propósito el return M, ejecuta :lua =package.loaded["mi.probe"] tras un require, y observa el true cacheado; provoca después el attempt to index a boolean value y devuelve el return a su sitio.
  4. Edita la función pública, comprueba que require sigue sirviendo la versión vieja, y recárgala con package.loaded["mi.probe"] = nil seguido de un nuevo require.
  5. Implementa el comando :R de esta lección y amplíalo para que, antes de recargar, borre un augroup del módulo; justifica por qué sin eso cada recarga duplicaría los autocomandos.
  6. Activa vim.loader.enable() al principio de tu init.lua, mide el arranque con nvim --startuptime /tmp/log antes y después, y localiza en el log qué partidas se encogieron.