Cleanly organizing modulescript instances can be difficult, so I took a look at how roblox does things and thought maybe it would look neat to create a module to make require less of a chore.
local modules = require(game.ReplicatedStorage.ServiceModules);
modules:AddService("ExampleService", game.ReplicatedStorage.ExampleModule, { present = true });
-- Equivalent to modules.ExampleService when present = true
local ExampleModule = modules:GetService("ExampleService");
The modules are only require()
'd if they are marked as present or retrieved with :GetService()
and the result from require is cached to make sure they only run once, like they would normally (this might be useless since I think this how require normally works anyway)
Of course, a downside to this is the loss of type information and autocomplete (if you export types from modules you have to require them directly), which can probably be worked around but it would have to really push LuaU type system to its limits and I haven’t worked it out that far yet.
But it can definitely make using multiple modulescripts less tedious, for example:
local ServiceA, ServiceB = modules:GetServices("ServiceA", "ServiceB");
ServiceA:Func();
ServiceB:Func();
The source for the module is here:
local module = {}
Loaded = {};
-- Modules parented to this module will become services automatically.
-- present: Whether or not the service is accessible via module.[name], and loaded as soon as this module loads.
-- move: If true, the ModuleScript passed is parented to the module instead of creating a reference.
function module:AddService(name: string, service: ModuleScript, properties: { present: boolean, move: boolean })
properties = properties or {};
if script:FindFirstChild(name) then
warn("Service module \"" .. name .. "\" already exists!");
return;
end
if not service then
error("Expecting ModuleScript for argument #2, got nil.");
end
if typeof(service) ~= "Instance" then
error(if typeof(service) ~= "Instance" then "Expecting ModuleScript for argument #2, got " .. typeof(service) else "Expecting ModuleScript for argument #2, got " .. module.ClassName)
end
if properties.move then
service.Name = name;
service.Parent = script;
else
local ref = Instance.new("ObjectValue");
ref.Name = name;
ref.Value = service;
ref.Parent = script;
end
if properties.present then
module[name] = service;
service:AddTag("Present");
end
end
function module:GetService(name: string)
if not name then
error("Expecting string for argument #1, got nil");
end
if Loaded[name] then
return Loaded[name];
end
local m = script:FindFirstChild(name);
if not m then
return;
end
-- AddService parents an ObjectValue to avoid cloning the script, so modules can still expect to stay where they are in the heirarchy.
if m:IsA("ObjectValue") then
m = m.Value;
end
local service = require(m);
Loaded[name] = service;
return service;
end
function module:GetServices(...: string)
local names = { ... };
local result = {};
for i, v in pairs(names) do
table.insert(result, module:GetService(v));
end
return unpack(result);
end
-- Load present modules.
for i,v in pairs(script:GetChildren()) do
if (v:IsA("ModuleScript") or v:IsA("ObjectValue")) and v:HasTag("Present") then
local service = if v:IsA("ModuleScript") then require(v) else require(v.Value);
Loaded[v.name] = service;
module[v.name] = service;
end
end
return module