{"id":"encodedlux/good-loader","name":"good-loader","scope":"encodedlux","platform":"roblox","description":"A lightweight and useful module loader for Roblox.","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"b754aa361bd00f345d52183cab49bcf4bdcff91759a274b584561d4dcf08c3c0","likes":0,"downloads":0,"install":"forest install encodedlux/good-loader","url":"https://forest.dev/p/roblox/encodedlux/good-loader","files":"https://api.forest.dev/ai/package/roblox/encodedlux/good-loader/files","readme":"<div align=\"center\">\n\n# GoodLoader\n\n*A lightweight and useful module loader for Roblox.*\n\n[![Luau](https://img.shields.io/badge/Made%20with-Luau-blue?style=for-the-badge)](https://luau-lang.org/)\n[![GitHub License](https://img.shields.io/github/license/encodedlux/good-loader?style=for-the-badge)](LICENSE.md)\n\n</div>\n\n**GoodLoader** is a no-nonsense module loader for Roblox. Load, sort, dispatch, and bind — pick what fits your architecture and leave the rest.\n\n---\n\n## Features\n\n- 📦 **Module Loading** — Load modules from children, descendants, or multiple paths with optional filtering.\n- 🔢 **Sorting** — Sort modules by priority or dependency order.\n- 📣 **Method Dispatch** — Call methods across all loaded modules.\n- 🔗 **Event Binding** — Bind module methods to `RBXScriptSignal`s or custom event sources.\n- 🗂️ **Module Registry** — Register and retrieve module lists by ID for cross-system access.\n\n---\n\n## Installation\n\nGoodLoader is easy to install! You can copy the source code or install via Wally:\n\n```toml\n[dependencies]\nGoodLoader = \"encodedlux/good-loader@VERSION\"\n```\n\n---\n\n## Getting Started\n\nFirst, create a loader script that will be the entry point for GoodLoader.\n```lua\n-- replace with path to GoodLoader\nlocal GoodLoader = require(...)\n\n-- The name field will be used for memory profiling.\n-- It's optional, but highly recommended to use.\nGoodLoader:setFields({ name = \"name\" })\n\nlocal modules = GoodLoader:loadDescendants(script.Parent:WaitForChild(\"Services\"), GoodLoader.matchesName(\"Service$\"))\n\nGoodLoader:prioritySort(modules, function(m)\n    return m.priority\nend)\n\nGoodLoader:topoSort(modules, function(m)\n    return m.dependencies\nend)\n\nGoodLoader:registerModules(modules, \"Game\")\n\nGoodLoader:callMethod(modules, \":Init\")\nGoodLoader:spawnMethod(modules, \":Start\")\n```\n\nNow, create modules in the `Services` folder.\n```lua\nlocal MyService = {\n    name = \"MyService\",\n    dependencies = { OtherService } -- OtherService will be loaded before MyService.\n}\n\nfunction MyService.Init(self: Self)\n    -- Initialize your properties and everything that other modules may depend on here.\n    -- For example, initialize variables, set up event connections, etc.\nend\n\nfunction MyService.Start(self: Self)\n    -- Run your module logic here.\n    -- For example, start a loop.\nend\n\ntype Self = typeof(MyService)\nreturn MyService\n```\n\n---\n\n## 📦 Module Loading\n\n### `GoodLoader:loadChildren(parent, predicate?)`\n\nLoads all `ModuleScript` children from a given `Instance`.\n\n```lua\n-- Load all ModuleScripts:\nlocal modules = GoodLoader:loadChildren(script.Parent)\n\n-- Load only modules whose name ends with \"Service\":\nlocal modules = GoodLoader:loadChildren(script.Parent, function(moduleScript)\n    return moduleScript.Name:match(\"Service$\") ~= nil\nend)\n```\n\n#### Parameters\n\n- `parent`: The `Instance` whose children will be scanned for `ModuleScript`s.\n- **optional** `predicate`: A function `(moduleScript: ModuleScript) -> boolean`. If provided, a module is only loaded if it returns `true`.\n\n#### Returns\n\nA table of required modules.\n\n---\n\n### `GoodLoader:loadDescendants(parent, predicate?)`\n\nLoads all `ModuleScript` descendants from a given `Instance`.\n\n```lua\n-- Load all ModuleScripts:\nlocal modules = GoodLoader:loadDescendants(script.Parent)\n\n-- Load only modules whose name ends with \"Service\":\nlocal modules = GoodLoader:loadDescendants(script.Parent, function(moduleScript)\n    return moduleScript.Name:match(\"Service$\") ~= nil\nend)\n```\n\n#### Parameters\n\n- `parent`: The `Instance` whose descendants will be scanned for `ModuleScript`s.\n- **optional** `predicate`: A function `(moduleScript: ModuleScript) -> boolean`. If provided, a module is only loaded if it returns `true`.\n\n#### Returns\n\nA table of required modules.\n\n---\n\n### `GoodLoader:loadPaths(paths, predicate?)`\n\nLoads modules from multiple lists of instances at once.\n\n```lua\nlocal modules = GoodLoader:loadPaths({\n    script.Parent:GetChildren(),\n    ReplicatedStorage.Shared.Modules:GetChildren(),\n})\n```\n\n#### Parameters\n\n- `paths`: An array of instance lists (e.g. from `:GetChildren()`). Each list is iterated and its `ModuleScript`s are loaded.\n- **optional** `predicate`: A function `(moduleScript: ModuleScript) -> boolean`. If provided, a module is only loaded if it returns `true`.\n\n#### Returns\n\nA table of required modules.\n\n---\n\n### `GoodLoader.matchesName(pattern)`\n\nA utility that creates a name-matching predicate to use with the loading functions.\n\n```lua\nlocal modules = GoodLoader:loadDescendants(script.Parent, GoodLoader.matchesName(\"Service$\"))\n```\n\n#### Parameters\n\n- `pattern`: A Lua pattern string matched against each `ModuleScript`'s name.\n\n#### Returns\n\nA predicate function `(moduleScript: ModuleScript) -> boolean`.\n\n---\n\n## 🔢 Sorting\n\n### `GoodLoader:prioritySort(modules, getPriority)`\n\nSorts modules by priority in ascending order (1,2,3...).\n\n```lua\nGoodLoader:prioritySort(modules, function(module)\n    return module.priority\nend)\n```\n\n#### Parameters\n\n- `modules`: The table of modules to sort. Sorted in-place.\n- `getPriority`: A function `(module) -> number?` that returns the priority of a module.\n\n---\n\n### `GoodLoader:topoSort(modules, getDependencies)`\n\nSorts modules by topological dependency order. A module that another depends on will always load first.\n\n```lua\nGoodLoader:topoSort(modules, function(module)\n    return module.dependencies\nend)\n```\n\n#### Parameters\n\n- `modules`: The table of modules to sort. Sorted in-place.\n- `getDependencies`: A function `(module) -> { module }?` that returns the dependencies of a module.\n\n---\n\n## 📣 Method Dispatch\n\n### `GoodLoader:callMethod(modules, methodName, ...)`\n\nCalls `methodName` **sequentially** on all modules. Use for initialization steps where order matters.\n\n```lua\nGoodLoader:callMethod(modules, \":init\") -- passes self as first argument\nGoodLoader:callMethod(modules, \".init\") -- does not pass self\nGoodLoader:callMethod(modules, \"init\")  -- same as \":init\"\n```\n\n#### Parameters\n\n- `modules`: The table of modules to dispatch to.\n- `methodName`: The method to call. Prefix with `:` to pass `self`, `.` to not pass `self`. Defaults to `:` behavior.\n- **optional** `...`: Additional arguments forwarded to the method.\n\n---\n\n### `GoodLoader:spawnMethod(modules, methodName, ...)`\n\nCalls `methodName` **concurrently** on all modules via `task.spawn`. Use when modules can run in parallel.\n\n```lua\nGoodLoader:spawnMethod(modules, \":start\")\nGoodLoader:spawnMethod(modules, \".start\")\nGoodLoader:spawnMethod(modules, \"start\")\n```\n\n#### Parameters\n\n- `modules`: The table of modules to dispatch to.\n- `methodName`: The method to call. Same prefix syntax as `callMethod`.\n- **optional** `...`: Additional arguments forwarded to the method.\n\n---\n\n## 🔗 Event Binding\n\n### `GoodLoader:bindToSignal(modules, method, signal)`\n\nFires `method` on all modules whenever `signal` fires, passing along its arguments.\n\n```lua\nlocal disconnect = GoodLoader:bindToSignal(modules, \"onHeartbeat\", RunService.Heartbeat)\n\n-- later\ndisconnect()\n```\n\n#### Parameters\n\n- `modules`: The table of modules to dispatch to.\n- `method`: The method to call on each module. Supports the same prefix syntax as `callMethod`.\n- `signal`: The `RBXScriptSignal` to listen to.\n\n#### Returns\n\nA cleanup function that disconnects the signal when called.\n\n---\n\n### `GoodLoader:bindToCallback(modules, method, callback)`\n\nBinds `method` to a custom event source. Useful for backfilling existing state (e.g. players already in the game).\n\n```lua\nlocal disconnect = GoodLoader:bindToCallback(modules, \"onPlayerAdded\", function(fire)\n    local conn = Players.PlayerAdded:Connect(fire)\n\n    for _, player in Players:GetPlayers() do\n        fire(player) -- backfill existing players\n    end\n\n    return function()\n        conn:Disconnect()\n    end\nend)\n\n-- later\ndisconnect()\n```\n\n#### Parameters\n\n- `modules`: The table of modules to dispatch to.\n- `method`: The method to call on each module. Supports the same prefix syntax as `callMethod`.\n- `callback`: A function that receives a `fire` function and returns an optional cleanup function. Call `fire(...)` to dispatch the method across all modules.\n\n#### Returns\n\nA cleanup function that runs the callback's cleanup when called.\n\n---\n\n## 🗂️ Module Registry\n\n### `GoodLoader:registerModules(modules, id)`\n\nAssociates a list of modules with a unique string ID so it can be retrieved anywhere with `getModules`.\n\n```lua\nlocal unregister = GoodLoader:registerModules(modules, \"Game\")\n\n-- later\nunregister()\n```\n\n#### Parameters\n\n- `modules`: The table of modules to register.\n- `id`: A unique string identifier.\n\n#### Returns\n\nA function that unregisters the modules when called.\n\n---\n\n### `GoodLoader:getModules(id)`\n\nRetrieves a previously registered module list by ID.\n\n```lua\nlocal modules = GoodLoader:getModules(\"Game\")\nGoodLoader:spawnMethod(modules, \":doSomething\")\n```\n\n#### Parameters\n\n- `id`: The string identifier used when registering.\n\n#### Returns\n\nThe registered table of modules.\n\n---\n\n### `GoodLoader:unregisterModules(id)`\n\nRemoves a module list from the registry.\n\n```lua\nGoodLoader:unregisterModules(\"Game\")\n```\n\n#### Parameters\n\n- `id`: The string identifier to remove.\n\n---\n\n## ⚙️ Field Setup\n\n### `GoodLoader:setFields(fields)`\n\nConfigures the field names GoodLoader reads from modules. Currently only `name` is supported, which is used for memory profiling. Defaults to `\"Name\"`.\n\n```lua\nGoodLoader:setFields({ name = \"name\" })\n```\n\n#### Parameters\n\n- `fields`: A table with optional keys:\n  - **optional** `name`: The field GoodLoader reads to identify a module by name.\n\n---\n\n## 🔀 Migrating from Knit\n\nStill 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.\n\nKnit services already have `KnitInit`, `KnitStart`, and a `Name` field. GoodLoader reads `Name` by default, so just point `callMethod` and `spawnMethod` at the methods you already have.\n\n**Before (Knit):**\n```lua\nKnit.Start():andThen(function()\n    print(\"Knit started!\")\nend)\n```\n\n**After (GoodLoader):**\n```lua\nGoodLoader:callMethod(modules, \":KnitInit\")\nGoodLoader:spawnMethod(modules, \":KnitStart\")\n\nprint(\"Services started!\")\n```\n\nYour services stay exactly as they are:\n\n```lua\n-- No changes needed to existing services\nlocal MyService = { Name = \"MyService\" }\n\nfunction MyService:KnitInit()\n    print(self.Name, \"initialized!\")\nend\n\nfunction MyService:KnitStart()\n    print(self.Name, \"started!\")\nend\n\nreturn MyService\n```\n\nFrom 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.\n\n---\n\n<div align=\"center\">\n\n*Made by EncodedLux*\n</div>\n","readmeTruncated":false}