Start typing to search packages!
craftsman-lifecycle
By @averyark
Roblox
Mirrored from WallyCraftsman Lifecycle
The Craftsman module loader. Point it at a folder, and it requires every ModuleScript inside,
runs each module's Init and then its Start in dependency order, and later runs Stop in
reverse.
It is Craftsman.Component in Craftsman Kit 0.9.0, moved out and fixed. It lives in its own
package so a game can use the lifecycle without Craftsman Kit, and Craftsman Kit without the
lifecycle.
# ember.toml
[indices]
wally = "https://github.com/UpliftGames/wally-index"
[dependencies]
Lifecycle = { name = "averyark/craftsman-lifecycle", version = "^1.0.0", index = "wally" }
Install it with Ember. It requires its dependencies in Ember's layout
(./packages/roblox/Promise), so the Wally CLI cannot install it.
A module
local Inventory = require("./Inventory")
local Shop = {
Dependencies = { Inventory },
}
function Shop:Init()
-- Synchronous. Set up state. Inventory:Init has already run.
end
function Shop:Start()
-- Connect events and loops. Inventory:Start has already finished.
self.Keeper:Add(workspace.ChildAdded:Connect(function() end))
end
function Shop:Stop()
-- Runs before Inventory:Stop. Shop's Keeper is destroyed after this returns.
end
return Shop
Dependencies holds the module tables themselves, not their names. Init, Start and
Stop are all optional. A module that is not a table is required and registered, and is
otherwise left alone.
Booting
local ServerScriptService = game:GetService("ServerScriptService")
local Lifecycle = require("@game/ReplicatedStorage/Packages/Lifecycle")
Lifecycle:LoadModulesAsync(ServerScriptService.Craftsman.Modules):await()
Lifecycle is itself a loader, so a game can use it directly. Lifecycle.Loader() makes an
independent one with its own modules, which is what a test uses so that stopping its fixtures
does not stop the game.
Loading more than one folder into the same loader is supported: a module in a later folder may
depend on a module from an earlier one, and one StopModulesAsync stops them all.
What it guarantees
| Phase | Order | When something fails |
|---|---|---|
| require | Every module, concurrently, before any Init | The module is reported, and nothing it would have run runs. The rest still load. |
Init | One at a time, each after every module it depends on | Its dependents skip Init and Start. Unrelated modules carry on. |
Start | Concurrently, each after every module it depends on has finished Start | Its dependents skip Start. |
Stop | Each after every module that depends on it has stopped | Reported. Its Keeper is still destroyed. |
LoadModulesAsync resolves once every Start has finished or failed. It rejects, before
running any Init, when two modules share a name or when modules depend on each other in a
cycle. Name clashes are refused before anything is required.
Every loaded table gets a Keeper (from averyark/keeper) unless it already has one, and
that Keeper is destroyed after the module's Stop.
Warnings
Inityielded.Initmust not yield. The loader waits for it anyway, so the order still holds, and names the module so the waiting can move intoStart.StartorStophas not finished. AfterHangWarningSeconds(10 by default, set per loader), aStartorStopthat is still running is reported along with the modules waiting on it.
Skipping a folder
Everything under an instance with the CraftsmanLifecycleIgnore attribute set to true is
skipped, as is any ModuleScript whose name ends in .spec. With Rojo, mark a folder with an
init.meta.json:
{
"attributes": {
"CraftsmanLifecycleIgnore": true
}
}
Anything that has to be loaded some other way belongs outside the loaded folder or under an
ignored one. That includes Craftsman Control's ServerScriptService.Craftsman.Control and
ServerScriptService.Craftsman.Verify: Control opens its stores when Stores is required,
because a Luau Execution session loads the place without running any Script, so no Init or
Start would ever run there.
Moving from Craftsman.Component
| Before | After |
|---|---|
Craftsman.Component:LoadModulesAsync(folder) | Lifecycle:LoadModulesAsync(folder) |
Craftsman.Component:StopModulesAsync() | Lifecycle:StopModulesAsync() |
Craftsman.Component.new(module) | Lifecycle.new(module) |
A scratch loader via setmetatable({ Modules = {}, LoadedModules = {} }, { __index = Craftsman.Component }) | Lifecycle.Loader() |
Config.MODULE_LOAD_ORDER | Dependencies on the modules themselves |
CHANGELOG.md lists every behaviour that changed.
Development
rokit install
embr install
lune setup
lune run test
./scripts/analyze.ps1
rojo build test.project.json -o Lifecycle.rbxl
lune run test runs the loader against fake instances. The place built from
test.project.json runs the ordering, ignore and Stop checks over real ModuleScripts, and its
Smoke script prints the result to the server output.
Package Details
Install command (Click to copy)
Version
1.0.0
License
MIT
Safe for commercial use
The package archive does not include its license text; the license is declared in its manifest metadata.
Automated license review — not legal advice.
