Appearance
Package boundaries
The boundaries are the architecture. This page states the rules, what enforces them, and what they buy you when you build on (or contribute to) IdleKit.
The dependency rules
txt
types ← depends on nothing
utils ← depends on nothing
core ← types
economy ← utils
storage ← types
dom ← types, core, utils
browser ← types, core, utils
mechanics ← types, core, utils
mechanics/*/economy ← mechanics + economy
plugins ← types, core
devtools ← types, core, utils
games ← anything (they are the composition layer)Stated as prohibitions:
- core never imports
dom,browser,mechanics,pluginsorstorage— and never touches a browser global. - economy never imports
core,mechanics,dom,browserorstorage— it is pure economic vocabulary plus transaction execution. - mechanics never import
dom,browserorstorage— gameplay is platform-agnostic and persistence-agnostic. - mechanics core modules do not import
@idlekitjs/economy; only official adapter subpaths underpackages/mechanics/src/*/economy/may do that. - plugins never import
domorbrowser.autosaveincludes browser lifecycle triggers in its own implementation; generic browser bridges live in@idlekitjs/browser. - devtools is a development overlay package. It may mount browser DOM UI behind runtime guards, but it is not a gameplay or production rendering layer.
- utils and types import no
@idlekitjspackage at all. - nothing imports a game.
The headless-core guard
The core rule is enforced mechanically, not by convention:
bash
pnpm check:headless-corefails if any executable code in packages/core/src references document, window, HTMLElement, requestAnimationFrame, cancelAnimationFrame, localStorage or navigator. Comments are exempt (the boundary may be documented in core, never used).
How each concern crosses the boundary
Core needs frames, rendering, persistence and lifecycle — it gets them through contracts defined in types, implemented above:
| Need | Contract (@idlekitjs/types / core) | Implementation |
|---|---|---|
| Frame driving | FrameScheduler | createRafScheduler() (browser), manualScheduler() (core, headless) |
| Rendering | RenderTarget | Renderer (dom) — or your own |
| Persistence | SaveAdapter | MemoryAdapter, LocalStorageAdapter (storage) |
| Tab lifecycle | pause() / resume() on the game | pageLifecycle() (browser) |
This is dependency inversion doing real work: each row is a place where you can swap the platform without touching the simulation.
Economy boundary
@idlekitjs/economy is not a second core and not an engine extension. It declares explicit resources, reads and writes state through declared accessors, normalizes costs/rewards, checks requirements, previews/executes transactions, calculates cost curves and returns formatting view models. It does not own state, scan state, schedule frames, run dt, persist saves or simulate mechanics.
Package responsibilities stay split this way:
txt
core
-> engine, loop, state, events, save, extension lifecycle, scheduler contracts
economy
-> resources, accessors, amounts, requirements, transactions, cost-curves, formatting
mechanics
-> producers, crafting, projects, collections, boosts, containers, timers, pickups
mechanics/*/economy
-> official bridges between mechanics and economy
devtools
-> development overlay and runtime metrics
games
-> concrete wiring, balancing, game-specific rulesResourceDef belongs to Economy, not core. Economy does not depend on any mechanic. The bridge direction is deliberately one-way: adapters live beside the mechanics that own the domain shape, so @idlekitjs/economy never needs to learn what a producer, card, recipe, project or boost is.
Current adapter subpaths are public and subpath-only:
txt
@idlekitjs/mechanics/producers/economy
@idlekitjs/mechanics/collections/economy
@idlekitjs/mechanics/crafting/economy
@idlekitjs/mechanics/projects/economy
@idlekitjs/mechanics/boosts/economy
@idlekitjs/mechanics/containers/economy
@idlekitjs/mechanics/pickups/economyThey are not re-exported by the @idlekitjs/mechanics barrel. Core mechanic files remain usable without Economy.
Why so strict?
- Testability — the headless testing story exists because core cannot accidentally require a browser.
- Portability — a native/canvas renderer or an SQLite backend is an implementation of an existing contract, not a fork.
- Reviewability — "does this belong here?" has a mechanical answer. A PR adding
documentto a mechanic is wrong by rule, not by taste. - Bundle honesty — a headless simulation build pulls zero DOM code; games that skip a mechanic never pay for it.
Where a new thing goes
| You're adding… | It goes in… |
|---|---|
| A pure economic concept (resource, amount, transaction, cost curve) | economy |
| A gameplay primitive (generic across games) | mechanics — new folder + subpath |
| A bridge between one mechanic and Economy | mechanics/<mechanic>/economy — subpath-only |
| A platform-agnostic engine policy (saving, catch-up rules) | plugins |
| A browser API bridge (lifecycle, notifications, PWA, wake lock) | browser — new folder + subpath |
| A persistence backend | storage — new folder + subpath |
| A DOM rendering concern | dom (or your own renderer package) |
| A pure helper | utils (if platform-free) or the module that owns it |
| A contract shared across packages | types |
| Anything with your game's meaning in it | your game |
Layout rules for the new folder: module conventions.