Let's take the relevant paragraph from the documentation:
require (modname)
[...]
Once a loader is found, require calls the loader with two arguments: modname and an extra value dependent on how it got the loader. (If the loader came from a file, this extra value is the file name.) If the loader returns any non-nil value, require assigns the returned value to package.loaded[modname]. If the loader does not return a non-nil value and has not assigned any value to package.loaded[modname], then require assigns true to this entry. In any case, require returns the final value of package.loaded[modname].
So, the way to go for a module with such recursive dependencies is:
- Get the module name
modname (first unnamed var-arg argument)
- Load modules needed for basic initializing
- Return if the module was fully set-up in 2 (recursive call)
- Set up the module-table under
package.loaded[modname]
- Set up enough scaffolding to start the sub-modules
require the sub-modules
- Do the rest of the modules setup.
- Return (No need to return anything due to step 4).
local _M, modname = {}, {...}[1]
local sub = require(modname..".sub")
if package.loaded[modname] then return end
package.loaded[modname] = _M
-- Populate _M with everything needed to set up more modules
_M.X = require(modname..".X")
--Do the rest of this modules setup and that's it
--return
That allows you to not create any globals at all, as is proper for modern modules.
If you do not need any submodules while setting up the main-module, consider using on-demand-loading instead:
setmetatable(_M, {__index =
function(t, k)
t[k] = require(modname.."."..k)
return t[k]
end})
Should your module only be structured into submodules for external consumption, you could put it all in the main-module and let it register the appropriate member-tables as sub-modules in their own right (See step 2).
In that case, the sub-module loaders would only require the main-module and return nothing.