Forest Logo
search
package_2

goodloader

By @encodedlux

Roblox

Mirrored

GoodLoader

A lightweight and useful module loader for Roblox.


GoodLoader is a no-nonsense module loader for Roblox. Load, sort, dispatch, and bind — pick what fits your architecture and leave the rest.

Features

  • 📦 Module Loading — Load modules from multiple paths with optional filtering, priority sorting, and dependency ordering — all in a single call.
  • 📣 Method Dispatch — Call methods across all loaded modules sequentially or concurrently.
  • 🔗 Event Binding — Bind module methods to signals or custom event sources.
  • 🗂️ Module Registry — Register and retrieve module lists by ID for cross-system access.

Installation

GoodLoader is easy to install! You can copy the source code or install via Wally:

[dependencies]
GoodLoader = "encodedlux/goodloader@1.1.0"

Getting Started

First, create a loader script that will be the entry point for GoodLoader.

-- replace with path to GoodLoader
local GoodLoader = require(...)

-- Optional: set the name field for memory profiling.
GoodLoader.getModuleNameField = function(module)
    return module.name
end

local modules = GoodLoader:loadModules({
    paths = { script.Parent.Services:GetChildren() },
    filter = GoodLoader:matchesName("Service$"),
    getPriorityField = function(module) return module.priority end,
    getDependenciesField = function(module) return module.dependencies end,
})

GoodLoader:registerModules(modules, "Game")

GoodLoader:callMethod(modules, ":Init")
GoodLoader:spawnMethod(modules, ":Start")

Now, create modules in the Services folder.

local MyService = {
    name = "MyService",
    dependencies = { OtherService } -- OtherService will be loaded before MyService.
}

function MyService.Init(self: Self)
    -- Initialize your properties and everything that other modules may depend on here.
    -- For example, initialize variables, set up event connections, etc.
end

function MyService.Start(self: Self)
    -- Run your module logic here.
    -- For example, start a loop.
end

type Self = typeof(MyService)
return MyService

Module Loading

GoodLoader:loadModules(params)

Loads and requires all ModuleScripts from the given paths, returning the resulting module tables in a sorted order. Duplicate ModuleScripts across paths are automatically deduplicated.

At least one of paths or modules must be provided, otherwise an error is thrown.

local modules = GoodLoader:loadModules({
    paths = { script.Parent.Services:GetChildren() },
    modules = { otherModule },
    filter = GoodLoader:matchesName("Service$"),
    getPriorityField = function(module) return module.priority end,
    getDependenciesField = function(module) return module.dependencies end,
})

Parameters (LoadParams)

  • optional paths: A list of instance lists to scan (e.g. { folder:GetChildren() }). Only ModuleScript instances are loaded.
  • optional modules: A list of already-loaded module tables to include alongside the ones discovered from paths. Useful for injecting modules that don't live under the scanned paths.
  • optional filter: A function (moduleScript: ModuleScript) -> boolean evaluated before requiring. Modules that don't pass the filter are never loaded.
  • optional getPriorityField: A function (module) -> number? that returns the priority of a module. Lower values are loaded first; modules without a priority go last.
  • optional getDependenciesField: A function (module) -> { module }? that returns the dependencies of a module. When provided, modules are sorted in topological (dependency) order.

Note: When both getPriorityField and getDependenciesField are provided, the priority sort runs first and the topological sort preserves that order as a tie-breaker for modules at the same dependency depth.

Returns

A table of required modules, sorted according to the provided sorting options.


GoodLoader:matchesName(pattern)

A utility that creates a name-matching filter to use with loadModules.

local modules = GoodLoader:loadModules({
    paths = { script.Parent.Services:GetChildren() },
    filter = GoodLoader:matchesName("Service$"),
})

Parameters

  • pattern: A Lua pattern string matched against each ModuleScript's name.

Returns

A filter function (moduleScript: ModuleScript) -> boolean.


Method Dispatch

GoodLoader:callMethod(modules, methodName, ...)

Calls methodName sequentially on all modules. Use for initialization steps where order matters.

GoodLoader:callMethod(modules, ":init") -- passes self as first argument
GoodLoader:callMethod(modules, ".init") -- does not pass self
GoodLoader:callMethod(modules, "init")  -- same as ":init"

Parameters

  • modules: The table of modules to dispatch to.
  • methodName: The method to call. Prefix with : to pass self, . to not pass self. Defaults to : behavior.
  • optional ...: Additional arguments forwarded to the method.

GoodLoader:spawnMethod(modules, methodName, ...)

Calls methodName concurrently on all modules. Use when modules can run in parallel.

GoodLoader:spawnMethod(modules, ":start")
GoodLoader:spawnMethod(modules, ".start")
GoodLoader:spawnMethod(modules, "start")

Parameters

  • modules: The table of modules to dispatch to.
  • methodName: The method to call. Same prefix syntax as callMethod.
  • optional ...: Additional arguments forwarded to the method.

Event Binding

GoodLoader:bindToSignal(modules, method, signal)

Fires method on all modules whenever signal fires, passing along its arguments.

local disconnect = GoodLoader:bindToSignal(modules, "onHeartbeat", RunService.Heartbeat)

-- later
disconnect()

Parameters

  • modules: The table of modules to dispatch to.
  • method: The method to call on each module. Supports the same prefix syntax as callMethod.
  • signal: The RBXScriptSignal to listen to.

Returns

A cleanup function that disconnects the signal when called.


GoodLoader:bindToCallback(modules, method, callback)

Binds method to a custom event source. Useful for backfilling existing state (e.g. players already in the game).

local disconnect = GoodLoader:bindToCallback(modules, "onPlayerAdded", function(fire)
    local conn = Players.PlayerAdded:Connect(fire)

    for _, player in Players:GetPlayers() do
        fire(player) -- backfill existing players
    end

    return function()
        conn:Disconnect()
    end
end)

-- later
disconnect()

Parameters

  • modules: The table of modules to dispatch to.
  • method: The method to call on each module. Supports the same prefix syntax as callMethod.
  • callback: A function that receives a fire function and returns an optional cleanup function. Call fire(...) to dispatch the method across all modules.

Returns

A cleanup function that runs the callback's cleanup when called.


Module Registry

GoodLoader:registerModules(modules, id)

Associates a list of modules with a unique string ID so it can be retrieved anywhere with getModules.

local unregister = GoodLoader:registerModules(modules, "Game")

-- later
unregister()

Parameters

  • modules: The table of modules to register.
  • id: A unique string identifier.

Returns

A function that unregisters the modules when called.


GoodLoader:getModules(id)

Retrieves a previously registered module list by ID.

local modules = GoodLoader:getModules("Game")
GoodLoader:spawnMethod(modules, ":doSomething")

Parameters

  • id: The string identifier used when registering.

Returns

The registered table of modules.


GoodLoader:unregisterModules(id)

Removes a module list from the registry.

GoodLoader:unregisterModules("Game")

Parameters

  • id: The string identifier to remove.

Memory Profiling

GoodLoader.getModuleNameField

An optional callback that GoodLoader uses to read a module's name for debug.setmemorycategory. Set this to enable per-module memory profiling in the Roblox Developer Console.

GoodLoader.getModuleNameField = function(module)
    return module.name
end

If not set (defaults to nil), memory categories are not modified during dispatch.


Migrating from Knit

Still on Knit and looking for something more current? GoodLoader is a drop-in replacement for the loading layer — and the best part is you don't need to rewrite your services or controllers at all.

Knit services already have KnitInit, KnitStart, and a Name field. Just point callMethod and spawnMethod at the methods you already have.

Before (Knit):

Knit.Start():andThen(function()
    print("Knit started!")
end)

After (GoodLoader):

GoodLoader.getModuleNameField = function(module)
    return module.Name
end

local modules = GoodLoader:loadModules({
    paths = { script.Parent.Services:GetChildren() },
    filter = GoodLoader:matchesName("Service$")
})

GoodLoader:callMethod(modules, ":KnitInit")
GoodLoader:spawnMethod(modules, ":KnitStart")

print("Services started!")

Your services stay exactly as they are:

-- No changes needed to existing services
local MyService = { Name = "MyService" }

function MyService:KnitInit()
    print(self.Name, "initialized!")
end

function MyService:KnitStart()
    print(self.Name, "started!")
end

return MyService

From there you can adopt GoodLoader features gradually — add a priority field to control load order, declare dependencies for topological sorting, or bind signals with bindToSignal. None of it is required up front.


Made by EncodedLux

Package Details

Install command (Click to copy)


Version

1.1.0

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.