tal/core/module.lua
2025-02-06 02:30:52 +02:00

325 lines
6.8 KiB
Lua

local exports = {};
local function mk_module(name, init, parent)
local module;
local cache = {};
parent.modules = parent.modules or {};
module = setmetatable({
name = name,
exports = {},
init = init,
env = setmetatable({}, { __index = parent.env }),
}, { __index = parent });
module.returns = { n = 1, module.exports };
function module.resolve(id, list)
if cache[id] ~= nil then return cache[id] end
for i = 1, #module.resolvers do
local res, files = module.resolvers[i](module, id);
if res then
cache[id] = res;
return res;
elseif list then
for i = 1, #files do
list[#list + 1] = files[i];
end
end
end
return nil;
end
function module.import(id)
local paths = {};
local mod = module.resolve(id, paths);
if mod == nil then
error(table.concat {
"Unable to resolve module '",
id,
"' - tried the following paths:\n- ",
table.concat(paths, "\n- ")
}, 2);
elseif type(mod.init) == "function" then
local init = mod.init;
mod.init = nil;
init(mod);
end
return (unpack or table.unpack)(mod.returns, 1, mod.returns.n);
end
function module.require(id)
local original_id = id;
local match = id:match "[^/]+$";
if match == id then
local start = id:match "^%.+" or "";
local rest = id:sub(#start + 1):gsub("%.", "/");
if #start == 0 then
id = rest;
elseif #start == 1 then
id = "./" .. rest;
else
id = ("../"):rep(#start - 1) .. rest;
end
else
id = id:sub(1, -#match - 1) .. match:gsub("%.", "/");
end
local paths = {};
local mod = module.resolve(id .. ".lua", paths);
if mod == nil then
mod = module.resolve(id .. "/init.lua", paths);
end
if mod == nil then
error(table.concat {
"Unable to resolve module '",
original_id,
"' - tried the following paths:\n- ",
table.concat(paths, "\n- ")
}, 2);
elseif type(mod.init) == "function" then
local init = mod.init;
mod.init = nil;
init(mod);
end
return (unpack or table.unpack)(mod.returns, 1, mod.returns.n);
end
function module.export(exports)
for k, v in next, exports do
module.exports[k] = v;
end
end
function module.mk(name, init)
if module.modules[name] ~= nil then
return module.modules[name], true;
else
local mod = mk_module(name, init, module);
module.modules[name] = mod;
return mod, false;
end
end
module.env.module = module;
module.env.import = module.import;
module.env.require = module.require;
module.env.export = module.export;
module.env.exports = module.exports;
module.env.package = setmetatable({}, {
__index = function()
error "This is a TAL module, please use 'module' instead";
end,
__newindex = function()
error "This is a TAL module, please use 'module' instead";
end,
});
return module;
end
local function try_file(self, cwd, id, base, suffixes)
suffixes = suffixes or { "" };
local file, found;
local files = {};
for i = 1, #suffixes do
file = cwd(base, id .. suffixes[i]);
local f = io.open(file, "r");
if f ~= nil then
f:close();
found = true;
break;
end
files[#files + 1] = file;
end
if not found then
return nil, files;
end
return self.mk(file, function (self)
local func = assert(loadfile(file, "t", self.env));
local function fin(...)
if select("#", ...) > 0 then
self.exports = ...;
self.returns = { n = select("#", ...), ... };
end
end
fin(func());
end), file;
end
function exports.file_resolver(cwd, join)
return function(self, id)
if id:match "^%./" or id:match "^%.%./" then
return try_file(self, cwd, id, join(self.name, "..", self.extensions), self.extensions);
else
local files = {};
for i = 1, #self.paths do
local res, file = try_file(self, cwd, id, self.paths[i], self.extensions);
if res ~= nil then
return res, file;
end
for i = 1, #file do
files[#files + 1] = file[i];
end
end
return nil, files;
end
end
end
function exports.lua_resolver(opts)
local returns = {
modules = {},
};
function returns.resolver(self, id)
if id:match "^lua:" then
id = id:sub(5);
end
if returns.modules[id] ~= nil then
return returns.modules[id], "lua:" ..id;
else
return nil, { "lua:" .. id };
end
end
function returns.register(name, ...)
returns.modules[name] = {
name = "lua:" .. name,
exports = ...,
returns = { n = select("#", ...), ... },
};
end
if opts.glob then
returns.register("global.lua", opts.glob);
end
if opts.expose then
local expose = opts.expose;
if expose == true then
expose = {
"utf8", "bit32", "bit",
"table", "string", "os", "math", "jit", "io", "ffi", "debug", "coroutine",
"table.new", "table.clear",
"string.buffer",
"jit.profile",
};
end
for i = 1, #expose do
local ok, mod = pcall(require, expose[i]);
if ok then
returns.register(expose[i]:gsub("%.", "/") .. ".lua", mod);
end
end
end
if opts.root then
returns.register("root.lua", opts.root);
end
returns.register("lua-resolver.lua", returns);
return returns;
end
function exports.mk_root(opts)
local run_loop;
local root = mk_module(
opts.join(opts.pwd, "/<root>"),
nil,
{
paths = {},
resolvers = {},
modules = {},
env = opts.glob,
}
);
root.paths[#root.paths + 1] = opts.pwd .. "/.tal_mod";
if opts.tal_path then
for i = 1, #opts.tal_path do
local p = opts.tal_path[i];
if p:match "^~/" then
root.paths[#root.paths + 1] = opts.join(opts.home, p:sub(2));
elseif p:match "^/" then
root.paths[#root.paths + 1] = opts.join(p);
else
root.paths[#root.paths + 1] = opts.join(opts.pwd, p);
end
end
end
if opts.lua_path then
for v in opts.lua_path:gmatch "[^;]+" do
local match = v:match "^(.+)/%?%.lua$";
if match ~= nil and match ~= "." then
root.paths[#root.paths + 1] = match;
end
end
end
if opts.resolvers then
root.resolvers[#root.resolvers + 1] = exports.file_resolver(opts.cwd, opts.join);
root.resolvers[#root.resolvers + 1] = exports.lua_resolver {
glob = opts.glob,
expose = true,
root = root,
}.resolver;
end
return root, run_loop;
end
function exports.init(opts, msg)
if opts.glob == nil then
---@diagnostic disable-next-line: deprecated
opts.glob = _ENV or _G or (getfenv and getfenv());
if opts.glob == nil then
error "Environment is not accessible";
end
end
local root = exports.mk_root {
tal_path = opts.tal_path,
lua_path = opts.lua_path or package.path,
resolvers = true,
evn_loop = true,
pwd = opts.pwd or opts.fs.pwd(),
home = opts.home or opts.fs.home(),
join = opts.join or opts.path.join,
cwd = opts.cwd or opts.path.cwd,
glob = opts.glob,
};
local require = root.require;
require "core"(opts.target_glob or root.env);
local run_loop = require "core.evn-loop";
root.env.promise = require "promise";
return root, run_loop;
end
return exports;