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 in it
and in the folders under it, runs each module's Init and then its Start in dependency
order, and later runs Stop in reverse. A ModuleScript's own children are never loaded.
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.
# wally.toml
[dependencies]
Lifecycle = "averyark/craftsman-lifecycle@2.0.0"
Install it with Wally. It requires its dependencies as siblings in Wally's
layout (./Promise), the same way Craftsman Kit and Craftsman Control do, so the three install
into one Packages folder. 1.0.0 required Ember's layout instead and cannot be installed by
Wally.
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.
Which modules are loaded
The loader walks Folders, and stops at a ModuleScript. A ModuleScript is one singleton, and anything inside it, ModuleScripts and Folders alike, belongs to it and is never looked at.
ServerScriptService.Features Folder, walked
Combat ModuleScript, loaded
Replication ModuleScript, Combat's own, not loaded
Shared Folder, walked
Wind ModuleScript, loaded
So a feature written as a folder with an init.luau is one module to the loader. Its parts
keep plain functions with whatever arguments they need, and its Start calls them in order.
Two features may each have a part named Types without clashing. A singleton that has to be
loaded on its own belongs in a Folder, not inside another ModuleScript.
This is TopLevelModulesOnly, which is true by default and set per loader, like
HangWarningSeconds. Set it to false to load every ModuleScript under the folder,
including ones inside other ModuleScripts, as 1.0.1 did:
Lifecycle.TopLevelModulesOnly = false
Lifecycle:LoadModulesAsync(folder):await()
Craftsman Control features
The loader answers whether a module's code is loaded and wired up, once at boot. Craftsman
Control answers whether a declared feature should be running, at any time. A module for a
feature declared in Control registers with Control in Start and does its work in the
handler, so switching the feature off in the console tears it down:
local Grass = {}
function Grass:Start()
self.Keeper:Add(Definitions:OnActivate(function(keeper)
-- Build. Everything made here goes into keeper, which Control
-- cleans every time the feature is switched off.
end))
end
The module's own Keeper holds the registration, so StopModulesAsync unregisters it and
cleans whatever the feature had running.
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. A ModuleScript's children are
skipped without it. 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
wally 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, nesting 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
2.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.
