{"id":"encodedlux/goodloader","name":"goodloader","scope":"encodedlux","platform":"roblox","description":"A lightweight and useful module loader for Roblox.","version":"1.1.0","latest":"1.1.0","versions":["0.1.0","1.0.0","1.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"102220b80614b7045b014c608d74d0ceec0be1da6ef8f709e777c9cbe2d255bf","likes":0,"downloads":0,"install":"forest install encodedlux/goodloader","url":"https://forest.dev/p/roblox/encodedlux/goodloader","files":"https://api.forest.dev/ai/package/roblox/encodedlux/goodloader/files","readme":"<div align=\"center\">\n\n# GoodLoader\n\n*A lightweight and useful module loader for Roblox.*\n\n</div>\n\n---\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## Features\n\n- 📦 **Module Loading** — Load modules from multiple paths with optional filtering, priority sorting, and dependency ordering — all in a single call.\n- 📣 **Method Dispatch** — Call methods across all loaded modules sequentially or concurrently.\n- 🔗 **Event Binding** — Bind module methods to signals 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/goodloader@1.1.0\"\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-- Optional: set the name field for memory profiling.\nGoodLoader.getModuleNameField = function(module)\n    return module.name\nend\n\nlocal modules = GoodLoader:loadModules({\n    paths = { script.Parent.Services:GetChildren() },\n    filter = GoodLoader:matchesName(\"Service$\"),\n    getPriorityField = function(module) return module.priority end,\n    getDependenciesField = function(module) return module.dependencies end,\n})\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:loadModules(params)`\n\nLoads and requires all ModuleScripts from the given paths, returning the resulting module tables in a sorted order. Duplicate ModuleScripts across paths are automatically deduplicated.\n\nAt least one of `paths` or `modules` must be provided, otherwise an error is thrown.\n\n```lua\nlocal modules = GoodLoader:loadModules({\n    paths = { script.Parent.Services:GetChildren() },\n    modules = { otherModule },\n    filter = GoodLoader:matchesName(\"Service$\"),\n    getPriorityField = function(module) return module.priority end,\n    getDependenciesField = function(module) return module.dependencies end,\n})\n```\n\n#### Parameters (`LoadParams`)\n\n- **optional** `paths`: A list of instance lists to scan (e.g. `{ folder:GetChildren() }`). Only `ModuleScript` instances are loaded.\n- **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.\n- **optional** `filter`: A function `(moduleScript: ModuleScript) -> boolean` evaluated **before** requiring. Modules that don't pass the filter are never loaded.\n- **optional** `getPriorityField`: A function `(module) -> number?` that returns the priority of a module. Lower values are loaded first; modules without a priority go last.\n- **optional** `getDependenciesField`: A function `(module) -> { module }?` that returns the dependencies of a module. When provided, modules are sorted in topological (dependency) order.\n\n> **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.\n\n#### Returns\n\nA table of required modules, sorted according to the provided sorting options.\n\n---\n\n### `GoodLoader:matchesName(pattern)`\n\nA utility that creates a name-matching filter to use with `loadModules`.\n\n```lua\nlocal modules = GoodLoader:loadModules({\n    paths = { script.Parent.Services:GetChildren() },\n    filter = GoodLoader:matchesName(\"Service$\"),\n})\n```\n\n#### Parameters\n\n- `pattern`: A Lua pattern string matched against each `ModuleScript`'s name.\n\n#### Returns\n\nA filter function `(moduleScript: ModuleScript) -> boolean`.\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. 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## Memory Profiling\n\n### `GoodLoader.getModuleNameField`\n\nAn 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.\n\n```lua\nGoodLoader.getModuleNameField = function(module)\n    return module.name\nend\n```\n\nIf not set (defaults to `nil`), memory categories are not modified during dispatch.\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. 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.getModuleNameField = function(module)\n    return module.Name\nend\n\nlocal modules = GoodLoader:loadModules({\n    paths = { script.Parent.Services:GetChildren() },\n    filter = GoodLoader:matchesName(\"Service$\")\n})\n\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}