{"id":"averyark/craftsman-lifecycle","name":"craftsman-lifecycle","scope":"averyark","platform":"roblox","description":"The Craftsman module loader: dependency-ordered Init, Start and Stop for singleton modules.","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":["The package archive does not include its license text; the license is declared in its manifest metadata."],"licenseVerified":false,"dependencies":{"averyark/keeper":{"version":"^1.10.2","alias":"Keeper"},"howmanysmall/typed-promise":{"version":"^4.0.6","alias":"Promise"}},"integrity":"37d7557257d61be1fb779ea26183bc648f375d0a8f9c515244eb060a544fa004","likes":0,"downloads":0,"install":"forest install averyark/craftsman-lifecycle","url":"https://forest.dev/p/roblox/averyark/craftsman-lifecycle","files":"https://api.forest.dev/ai/package/roblox/averyark/craftsman-lifecycle/files","readme":"# Craftsman Lifecycle\n\nThe Craftsman module loader. Point it at a folder, and it requires every ModuleScript inside,\nruns each module's `Init` and then its `Start` in dependency order, and later runs `Stop` in\nreverse.\n\nIt is `Craftsman.Component` in Craftsman Kit 0.9.0, moved out and fixed. It lives in its own\npackage so a game can use the lifecycle without Craftsman Kit, and Craftsman Kit without the\nlifecycle.\n\n```toml\n# ember.toml\n[indices]\nwally = \"https://github.com/UpliftGames/wally-index\"\n\n[dependencies]\nLifecycle = { name = \"averyark/craftsman-lifecycle\", version = \"^1.0.0\", index = \"wally\" }\n```\n\nInstall it with [Ember](https://luaupm.com). It requires its dependencies in Ember's layout\n(`./packages/roblox/Promise`), so the Wally CLI cannot install it.\n\n## A module\n\n```lua\nlocal Inventory = require(\"./Inventory\")\n\nlocal Shop = {\n\tDependencies = { Inventory },\n}\n\nfunction Shop:Init()\n\t-- Synchronous. Set up state. Inventory:Init has already run.\nend\n\nfunction Shop:Start()\n\t-- Connect events and loops. Inventory:Start has already finished.\n\tself.Keeper:Add(workspace.ChildAdded:Connect(function() end))\nend\n\nfunction Shop:Stop()\n\t-- Runs before Inventory:Stop. Shop's Keeper is destroyed after this returns.\nend\n\nreturn Shop\n```\n\n`Dependencies` holds the module tables themselves, not their names. `Init`, `Start` and\n`Stop` are all optional. A module that is not a table is required and registered, and is\notherwise left alone.\n\n## Booting\n\n```lua\nlocal ServerScriptService = game:GetService(\"ServerScriptService\")\nlocal Lifecycle = require(\"@game/ReplicatedStorage/Packages/Lifecycle\")\n\nLifecycle:LoadModulesAsync(ServerScriptService.Craftsman.Modules):await()\n```\n\n`Lifecycle` is itself a loader, so a game can use it directly. `Lifecycle.Loader()` makes an\nindependent one with its own modules, which is what a test uses so that stopping its fixtures\ndoes not stop the game.\n\nLoading more than one folder into the same loader is supported: a module in a later folder may\ndepend on a module from an earlier one, and one `StopModulesAsync` stops them all.\n\n## What it guarantees\n\n| Phase | Order | When something fails |\n| :--- | :--- | :--- |\n| require | Every module, concurrently, before any `Init` | The module is reported, and nothing it would have run runs. The rest still load. |\n| `Init` | One at a time, each after every module it depends on | Its dependents skip `Init` and `Start`. Unrelated modules carry on. |\n| `Start` | Concurrently, each after every module it depends on has finished `Start` | Its dependents skip `Start`. |\n| `Stop` | Each after every module that depends on it has stopped | Reported. Its Keeper is still destroyed. |\n\n`LoadModulesAsync` resolves once every `Start` has finished or failed. It rejects, before\nrunning any `Init`, when two modules share a name or when modules depend on each other in a\ncycle. Name clashes are refused before anything is required.\n\nEvery loaded table gets a `Keeper` (from `averyark/keeper`) unless it already has one, and\nthat Keeper is destroyed after the module's `Stop`.\n\n### Warnings\n\n- **`Init` yielded.** `Init` must not yield. The loader waits for it anyway, so the order\n  still holds, and names the module so the waiting can move into `Start`.\n- **`Start` or `Stop` has not finished.** After `HangWarningSeconds` (10 by default, set per\n  loader), a `Start` or `Stop` that is still running is reported along with the modules\n  waiting on it.\n\n## Skipping a folder\n\nEverything under an instance with the `CraftsmanLifecycleIgnore` attribute set to `true` is\nskipped, as is any ModuleScript whose name ends in `.spec`. With Rojo, mark a folder with an\n`init.meta.json`:\n\n```json\n{\n  \"attributes\": {\n    \"CraftsmanLifecycleIgnore\": true\n  }\n}\n```\n\nAnything that has to be loaded some other way belongs outside the loaded folder or under an\nignored one. That includes Craftsman Control's `ServerScriptService.Craftsman.Control` and\n`ServerScriptService.Craftsman.Verify`: Control opens its stores when `Stores` is required,\nbecause a Luau Execution session loads the place without running any Script, so no `Init` or\n`Start` would ever run there.\n\n## Moving from `Craftsman.Component`\n\n| Before | After |\n| :--- | :--- |\n| `Craftsman.Component:LoadModulesAsync(folder)` | `Lifecycle:LoadModulesAsync(folder)` |\n| `Craftsman.Component:StopModulesAsync()` | `Lifecycle:StopModulesAsync()` |\n| `Craftsman.Component.new(module)` | `Lifecycle.new(module)` |\n| A scratch loader via `setmetatable({ Modules = {}, LoadedModules = {} }, { __index = Craftsman.Component })` | `Lifecycle.Loader()` |\n| `Config.MODULE_LOAD_ORDER` | `Dependencies` on the modules themselves |\n\n`CHANGELOG.md` lists every behaviour that changed.\n\n## Development\n\n```\nrokit install\nembr install\nlune setup\nlune run test\n./scripts/analyze.ps1\nrojo build test.project.json -o Lifecycle.rbxl\n```\n\n`lune run test` runs the loader against fake instances. The place built from\n`test.project.json` runs the ordering, ignore and Stop checks over real ModuleScripts, and its\n`Smoke` script prints the result to the server output.\n","readmeTruncated":false}