Skip to content

Repository files navigation

Chief

CI lpm

A strictly and fully typed, tested Luau module framework for Roblox, inspired by Sleitnick's Knit and by Featherfall's Quill. Modules are much like Knit's services and controllers: each has an Init and Start lifecycle, and more lifecycles can be added with the Lifecycles extension.

The core is intentionally small — it loads modules, runs the Init/Start boot barrier, and fires hooks. And like Quill, everything else (Roblox event lifecycles, dependency injection, etc.) lives in extensions that attach to those hooks. The core has no knowledge of RunService events, players, or dependency graphs.

Packages

Package Description
chief/core The framework core: loads modules, runs the Init/Start barrier, and fires hooks for extensions.
chief/lifecycles Binds events (PlayerAdded, Heartbeat, custom) to module methods.
chief/dependencies Dependency injection with transitive loading and topological Init ordering.
chief/traits Binds per-instance behavior to CollectionService tags, with typed attribute and child contracts.
chief/bin A small cleanup container: connections, instances, threads, and functions, emptied in reverse insertion order.

Installation

Install with LPM:

lpm add chief/core
lpm add chief/lifecycles   # optional
lpm add chief/dependencies # optional
lpm add chief/traits       # optional
lpm add chief/bin          # optional; standalone cleanup container, also used by traits
lpm i

Chief is LPM-first: packages are published to LPM primarily. Pesde is also supported. If you'd like Wally support, open an issue.

Quick start

A bootstrap script creates a Chief instance, adds modules and extensions, then starts:

--!strict

local Chief = require('./roblox_packages/core')
local Dependencies = require('./roblox_packages/dependencies')
local Lifecycles = require('./roblox_packages/lifecycles')

Chief.new()
	:AddModules(script.Modules:GetChildren())
	:AddExtensions({
		Dependencies.new(),
		Lifecycles.new({
			Lifecycles.PlayerAdded,
			Lifecycles.Heartbeat,
		}),
	})
	:Start()

A module is a plain table with optional Init and Start methods:

--!strict

local DataModule = {}

function DataModule.Init (self: Self)
	-- Runs synchronously, in order, before any module's Start. Yielding here errors.
end

function DataModule.Start (self: Self)
	-- Spawned concurrently after every module's Init has completed.
end

type Self = typeof(DataModule)

return DataModule

Boot sequence

Chief:Start() runs the following phases in order:

PreLoad → Load → PostLoad → PreInit → Init → PostInit → PreStart → Start → PostStart
  • Every module's Init runs synchronously and in order (the "init barrier" — yielding during Init errors).
  • Every module's Start is spawned concurrently afterward.
  • Modules are sorted by Priority (higher first); ties break by insertion order.

Extensions

Extensions attach behavior through two hooks:

  • onPhase(chief, phase, entries) — fired at each boot phase with every entry loaded so far.
  • onModule(chief, event, entry) — fired per module as it is Loaded, Initialized, and Started.

During PostLoad, extensions may call chief:LoadModule to pull in additional modules; loading closes after PostLoad. Extensions may reorder entries in place during PostLoad or PreInit — after PreInit the order is final.

See the core README for the full extension contract.

Development

This repository is a pesde workspace (workspace_members = ["packages/*"]). CI runs each check as its own job, so a red badge points at the step that broke.

  • Format: StyLua (stylua.toml)
  • Lint: selene (selene.toml)
  • Typecheck: luau-lsp with the new solver (.luaurc), covering the packages and the tests — packages/traits derives its bound-object types with type function, which only the new solver understands
  • Test: specs under tests/, executed in real Roblox by CI via the Open Cloud Luau Execution API
  • roblox_packages/ and pesde.lock are generated and gitignored — never edit them.

Tests

Specs live in tests/ as *.spec ModuleScripts and run on a small in-repo runner, tests/Runner.luau, which exposes a Jest-shaped API:

--!strict

local Runner = require('./Runner')

local describe = Runner.describe
local expect = Runner.expect
local it = Runner.it

describe('addition', function ()
	it('adds', function ()
		expect(1 + 1).toBe(2)
	end)
end)

return {}

Matchers: toBe, toBeTruthy, toBeFalsy, toBeNil, toContain, and toThrow('BadArgument') for Chief's prefixed errors. beforeEach/afterEach nest with describe, and a test body may yield.

Chief uses its own runner rather than TestEZ (archived, and its injected globals force --!nocheck on specs) or Jest Lua (its sandbox rejects require-by-string, which pesde's generated shims depend on, so only dependency-free packages are requirable under it). The runner loads specs with a plain require, so every package is testable, and it exports its API instead of injecting globals, so specs typecheck under --!strict.

To run the suite locally, against the same Open Cloud path CI uses:

rojo build test.project.json --output test-place.rbxl
ROBLOX_API_KEY=... ROBLOX_UNIVERSE_ID=... ROBLOX_PLACE_ID=... \
	python3 scripts/upload_and_run_task.py test-place.rbxl tasks/run-tests.luau

The API key needs the universe.place:write and universe.place.luau-execution-session:write scopes. Each run uploads a new saved version of the test place.

License

MIT

About

A strictly and fully typed, tested Luau module framework for Roblox.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages