Forest Logo
search
package_2

craftsman-lifecycle

By @averyark

Roblox

Mirrored from Wally

Craftsman 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

PhaseOrderWhen something fails
requireEvery module, concurrently, before any InitThe module is reported, and nothing it would have run runs. The rest still load.
InitOne at a time, each after every module it depends onIts dependents skip Init and Start. Unrelated modules carry on.
StartConcurrently, each after every module it depends on has finished StartIts dependents skip Start.
StopEach after every module that depends on it has stoppedReported. 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

  • Init yielded. Init must not yield. The loader waits for it anyway, so the order still holds, and names the module so the waiting can move into Start.
  • Start or Stop has not finished. After HangWarningSeconds (10 by default, set per loader), a Start or Stop that 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

BeforeAfter
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_ORDERDependencies 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

check_circle

Safe for commercial use

infoThe package archive does not include its license text; the license is declared in its manifest metadata.

Automated license review — not legal advice.