Skip to content

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
games   ← anything (they are the composition layer)

Stated as prohibitions:

  • core never imports dom, browser, mechanics, plugins or storage — and never touches a browser global.
  • economy never imports core, mechanics, dom, browser or storage — it is pure economic vocabulary plus transaction execution.
  • mechanics never import dom, browser or storage — gameplay is platform-agnostic and persistence-agnostic.
  • mechanics core modules do not import @idlekitjs/economy; only official adapter subpaths under packages/mechanics/src/*/economy/ may do that.
  • plugins never import dom or browser. autosave includes browser lifecycle triggers in its own implementation; generic browser bridges live in @idlekitjs/browser.
  • utils and types import no @idlekitjs package at all.
  • nothing imports a game.

The headless-core guard

The core rule is enforced mechanically, not by convention:

bash
npm run check:headless-core

fails 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:

NeedContract (@idlekitjs/types / core)Implementation
Frame drivingFrameSchedulercreateRafScheduler() (browser), manualScheduler() (core, headless)
RenderingRenderTargetRenderer (dom) — or your own
PersistenceSaveAdapterMemoryAdapter, LocalStorageAdapter (storage)
Tab lifecyclepause() / resume() on the gamepageLifecycle() (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

games
-> concrete wiring, balancing, game-specific rules

ResourceDef 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/economy

They are not re-exported by the @idlekitjs/mechanics barrel. Core mechanic files remain usable without Economy.

Recommended guardrail: an automated boundary check should fail on any @idlekitjs/economy import under packages/mechanics/src unless the file path contains /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 document to 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 Economymechanics/<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 backendstorage — new folder + subpath
A DOM rendering concerndom (or your own renderer package)
A pure helperutils (if platform-free) or the module that owns it
A contract shared across packagestypes
Anything with your game's meaning in ityour game

Layout rules for the new folder: module conventions.