refactor(require): improve module resolution and debugging info

- Improved error messages.
- Include checked file paths on failed module resolution.
- Moved repeated code into separate functions.
- Workaround for incorrect "circular-dependency" error.
- Added additional package.path "./?/init.lua".
- Added support for package.searchers.
This commit is contained in:
Linden
2024-04-05 18:01:06 +11:00
parent 77c34b8699
commit 56b99a7657

View File

@@ -1,30 +1,30 @@
local loaded = {} local loaded = {}
local _require = require
package = { package = {
path = './?.lua;./?/init.lua',
preload = {},
loaded = setmetatable({}, { loaded = setmetatable({}, {
__index = loaded, __index = loaded,
__newindex = noop, __newindex = noop,
__metatable = false, __metatable = false,
}), })
path = './?.lua;'
} }
local _require = require ---@param modName string
---@return string
---@param modpath string ---@return string
---@param modname? string local function getModuleInfo(modName)
---@return string, string?
local function getModuleInfo(modpath, modname)
local resourceSrc local resourceSrc
if not modpath:find('^@') then if not modName:find('^@') then
local idx = 1 local idx = 1
while true do while true do
local di = debug.getinfo(idx, 'S') local di = debug.getinfo(idx, 'S')
if di then if di then
if not di.short_src:find('^@ox_lib/imports/require') and not di.short_src:find('^%[C%]') and not di.short_src:find('^citizen') and di.short_src ~= '?' then if not di.short_src ~= '?' and di.source:match('^@([^%s]+)') and not di.short_src:find('^@ox_lib/imports/require') and not di.short_src:find('^citizen') then
resourceSrc = di.source:gsub('^%@+([^/]+)/.+', '%1') resourceSrc = di.source:gsub('^%@+([^/]+)/.+', '%1')
break break
end end
@@ -35,138 +35,139 @@ local function getModuleInfo(modpath, modname)
idx += 1 idx += 1
end end
else
if modname and resourceSrc ~= cache.resource then resourceSrc = modName:gsub('^@(.-)/.+', '%1')
modname = ('@%s.%s'):format(resourceSrc, modname) modName = modName:sub(#resourceSrc + 3)
end
end end
return resourceSrc, modname return resourceSrc, modName
end end
local tempData = {}
---@param name string
---@param path string
---@return string? filename
---@return string? errmsg
function package.searchpath(name, path)
local resourceSrc, modName = getModuleInfo(name:gsub('%.', '/'))
local tried = {}
for template in path:gmatch('[^;]+') do
local fileName = template:gsub('^%./', ''):gsub('?', modName:gsub('%.', '/') or modName)
local file = LoadResourceFile(resourceSrc, fileName)
if file then
tempData[1] = file
tempData[2] = resourceSrc
return fileName
end
tried[#tried + 1] = fileName
end
return nil, table.concat(tried, "\n\t")
end
---Attempts to load a module at the given path relative to the resource root directory.\
---Returns a function to load the module chunk, or a string containing all tested paths.
---@param modName string
---@param env? table
local function loadModule(modName, env)
local fileName, err = package.searchpath(modName, package.path)
if fileName then
local file = tempData[1]
local resource = tempData[2]
table.wipe(tempData)
return assert(load(file, ('@@%s/%s'):format(resource, modName), 't', env or _ENV))
end
return nil, err or 'unknown error'
end
package.searchers = {
function(modName) return package.preload[modName] end,
function(modName)
local ok, result = pcall(_require, modName)
if ok then return result end
return ok, result
end,
function(modName) return loadModule(modName) end,
}
---@param filePath string ---@param filePath string
---@param env? table ---@param env? table
---@return any ---@return unknown
---Loads and runs a Lua file at the given path. Unlike require, the chunk is not cached for future use. ---Loads and runs a Lua file at the given path. Unlike require, the chunk is not cached for future use.
function lib.load(filePath, env) function lib.load(filePath, env)
local resourceSrc if type(filePath) ~= 'string' then
local modpath = filePath:gsub('%.', '/') error(("file path must be a string (received '%s')"):format(filePath), 2)
if not modpath:find('^@') then
resourceSrc = getModuleInfo(modpath)
end end
if not resourceSrc then local result, err = loadModule(filePath, env)
resourceSrc = modpath:gsub('^@(.-)/.+', '%1')
modpath = modpath:sub(#resourceSrc + 3)
end
for path in package.path:gmatch('[^;]+') do if result then return result() end
local scriptPath = path:gsub('?', modpath):gsub('%.+%/+', '')
local resourceFile = LoadResourceFile(resourceSrc, scriptPath)
if resourceFile then error(err)
local chunk, err = load(resourceFile, ('@@%s/%s'):format(resourceSrc, modpath), 't', env or _ENV)
if not chunk or err then
error(err or 'an unknown error occurred', 2)
end
return chunk()
end
end
error(('cannot load file at path %s'):format(modpath))
end end
---@param filePath string ---@param filePath string
---@return table ---@return table
---Loads and decodes a json file at the given path. ---Loads and decodes a json file at the given path.
function lib.loadJson(filePath) function lib.loadJson(filePath)
local resourceSrc if type(filePath) ~= 'string' then
local modpath = filePath:gsub('%.', '/') error(("file path must be a string (received '%s')"):format(filePath), 2)
if not modpath:find('^@') then
resourceSrc = getModuleInfo(modpath)
end end
if not resourceSrc then local resourceSrc, modPath = getModuleInfo(filePath)
resourceSrc = modpath:gsub('^@(.-)/.+', '%1') local resourceFile = LoadResourceFile(resourceSrc, ('%s.json'):format(modPath))
modpath = modpath:sub(#resourceSrc + 3)
end
local scriptPath = ('%s.json'):format(modpath)
local resourceFile = LoadResourceFile(resourceSrc, scriptPath)
if resourceFile then if resourceFile then
return json.decode(resourceFile) return json.decode(resourceFile)
end end
error(('cannot load json file at path %s'):format(modpath)) error(('cannot load json file at path %s'):format(modPath))
end end
---Loads the given module inside the current resource, returning any values returned by the file or `true` when `nil`. ---Loads the given module, returns any value returned by the seacher (`true` when `nil`).\
---@param modname string ---Passing `@resourceName.modName` loads a module from a remote resource.\
---@param modName string
---@return unknown ---@return unknown
function lib.require(modname) function lib.require(modName)
if type(modname) ~= 'string' then return end if type(modName) ~= 'string' then
error(("module name must be a string (received '%s')"):format(modName), 3)
local modpath = modname:gsub('%.', '/')
local module = loaded[modname]
if module then return module end
local success, result = pcall(_require, modname)
if success then
loaded[modname] = result
return result
end end
local resourceSrc local module = loaded[modName]
if not modpath:find('^@') then if module == '__loading' then
resourceSrc, modname = getModuleInfo(modpath, modname) --[[@as string]] error(("^1circular-dependency occurred when loading module '%s'^0"):format(modName), 2)
end end
if not module then if module ~= nil then return module end
if module == false then
error(("^1circular-dependency occurred when loading module '%s'^0"):format(modname), 2) loaded[modName] = '__loading'
local err = {}
for i = 1, #package.searchers do
local result, errMsg = package.searchers[i](modName)
if result then
if type(result) == 'function' then result = result() end
loaded[modName] = result or result == nil
return loaded[modName]
end end
if not resourceSrc then err[#err + 1] = errMsg
resourceSrc = modpath:gsub('^@(.-)/.+', '%1')
modpath = modpath:sub(#resourceSrc + 3)
end
for path in package.path:gmatch('[^;]+') do
local scriptPath = path:gsub('?', modpath):gsub('%.+%/+', '')
local resourceFile = LoadResourceFile(resourceSrc, scriptPath)
if resourceFile then
loaded[modname] = false
scriptPath = ('@@%s/%s'):format(resourceSrc, scriptPath)
local chunk, err = load(resourceFile, scriptPath)
if err or not chunk then
loaded[modname] = nil
return error(err or ("unable to load module '%s'"):format(modname), 3)
end
local result = chunk(modname)
module = result or result == nil
loaded[modname] = module
return module
end
end
return error(result, 2)
end end
return module error(("%s"):format(table.concat(err, "\n\t")))
end end
return lib.require return lib.require