Appearance
Module structure conventions
One rule shapes every package:
txt
one autonomous element = one folder = one public indexA mechanic, a plugin, a storage backend — each lives in its own folder with a public index.ts, and is imported from its own package subpath. The structure scales by addition: new bricks get a new folder, and nothing else moves.
Folder anatomy
txt
packages/mechanics/src/
index.ts # package barrel (re-exports every mechanic)
producers/
index.ts # the public surface of the mechanic
extension.ts # the Extension<T> factory
types.ts # public option/def/result types
projects/
index.ts
extension.ts
manager.ts # ProjectManager — public via index.ts only
crafting/
index.ts
extension.ts
types.ts
resources.ts # ResourceBag helpers — public via index.ts only
economy/
index.ts # optional bridge to @idlekitjs/economy (subpath-only)
...
packages/economy/src/
index.ts # main facade
accessors/
index.ts # public subpath: @idlekitjs/economy/accessors
cost-curves/
index.ts # public subpath: @idlekitjs/economy/cost-curves
resources/
amounts/
requirements/
transactions/
format/Internal files exist as the module needs them (registry.ts, manager.ts, scheduler.ts); no artificial empty files. The package.json exports map points each subpath at the folder's index.
Import rules
ts
// ✅ public subpaths — the preferred form
import { producers } from "@idlekitjs/mechanics/producers";
import { economyPurchase } from "@idlekitjs/mechanics/producers/economy";
import { projects, ProjectManager } from "@idlekitjs/mechanics/projects";
import { createEconomy, stateKey } from "@idlekitjs/economy";
import { geometric } from "@idlekitjs/economy/cost-curves";
import { autosave, SaveScheduler } from "@idlekitjs/plugins/autosave";
import { LocalStorageAdapter } from "@idlekitjs/storage/local-storage";
// ✅ the package barrel — fine for convenience
import { producers, projects } from "@idlekitjs/mechanics";ts
// ❌ internal detail promoted to a subpath — does not exist
import { ProjectManager } from "@idlekitjs/mechanics/projectManager";
import { SaveScheduler } from "@idlekitjs/plugins/saveScheduler";
// ❌ deep import into a module's internals — not public API
import { producers } from "@idlekitjs/mechanics/producers/extension";The principle behind both ❌ cases: helpers are public through the module that owns them, or not at all. ProjectManager belongs to the projects mechanic; SaveScheduler belongs to autosave; the ResourceBag helpers belong to crafting. Their home subpath is their only address — internals stay free to be reorganized without breaking anyone.
Economy bridges are a deliberate variant of the same rule. A bridge belongs to the mechanic whose state shape it adapts, so it is public at @idlekitjs/mechanics/<mechanic>/economy. It is not re-exported from the @idlekitjs/mechanics barrel and it does not live in @idlekitjs/economy/adapters. That keeps the core mechanic importable without Economy and prevents the cycle economy -> mechanics -> economy.
Import boundaries
Every export — value or type — has exactly one home package, and that is where you import it from:
ts
// The engine and its contracts
import { createEngine, SaveManager, type Extension, type EngineContext } from "@idlekitjs/core";
// DOM rendering: renderer and bindings
import { Renderer, bindText, type Binding } from "@idlekitjs/dom";
// Browser runtime bridges: frames, page lifecycle, screen info
import { createRafScheduler } from "@idlekitjs/browser/raf-scheduler";
import { pageLifecycle } from "@idlekitjs/browser/page-lifecycle";
// Gameplay primitives: each mechanic owns its types
import { crafting, type RecipeDef, type CraftingJob } from "@idlekitjs/mechanics/crafting";
import { recipeCost } from "@idlekitjs/mechanics/crafting/economy";
import { boosts, type BoostDef, type ActiveBoost } from "@idlekitjs/mechanics/boosts";
import { createEconomy, type Transaction } from "@idlekitjs/economy";
import { projects, type Project } from "@idlekitjs/mechanics/projects";
// Plugins and storage backends own theirs too
import { autosave, type AutosaveOptions } from "@idlekitjs/plugins/autosave";
import { LocalStorageAdapter } from "@idlekitjs/storage/local-storage";What @idlekitjs/core re-exports is deliberately limited to engine-level contracts (Extension, EngineContext, System, SaveAdapter, ... from @idlekitjs/types). It never re-exports mechanic, plugin, DOM, browser or storage types: Project comes from @idlekitjs/mechanics/projects, Binding from @idlekitjs/dom, createRafScheduler from @idlekitjs/browser/raf-scheduler, LocalStorageAdapter from @idlekitjs/storage/local-storage. Core is the engine's facade, not a catalog of everything IdleKit ships — and no deep imports, ever (see above).
Economy-owned types (ResourceDef, ResourceId, Transaction, Requirement, CostCurve) come from @idlekitjs/economy. They do not belong in core. Mechanic-owned bridge helpers (economyPurchase, projectFromTransaction, cardResources, recipeCost, activateBoost) come from their mechanic's /economy subpath.
The platform rule of thumb: writes to the DOM → dom; reads or bridges a browser API → browser; consumes only engine events → plugins.
Deliberate exceptions
@idlekitjs/corehas internal sub-domains (loop/,save/,state/,events/,numbers/,random/,format/) but a single public facade. Core is one engine, not a bag of bricks — no public subpaths.@idlekitjs/economyhas a main facade plus two convenience public subpaths:@idlekitjs/economy/accessorsand@idlekitjs/economy/cost-curves. Its internal domains (resources/,amounts/,requirements/,transactions/,format/) remain public via the main facade only.@idlekitjs/domhas a main facade plus the@idlekitjs/dom/bind-eachconvenience subpath for keyed list binding.@idlekitjs/utilsand@idlekitjs/typesare small, flat, single-facade packages.@idlekitjs/browserfollows the standard brick layout from day one (page-lifecycle/,raf-scheduler/,screen/).
Adding a new brick
- Create
packages/<pkg>/src/<brick>/with anindex.tsexposing the public surface. - Add
"./<brick>": "./src/<brick>/index.ts"to the package'sexports. - Re-export from the package barrel, except for deliberate subpath-only bridges such as
@idlekitjs/mechanics/<mechanic>/economy. - Add
packages/<pkg>/tests/<brick>.test.tsimporting the public surface. - Document it.
That is the whole procedure for a public brick.
Why it matters
- Predictability — knowing one module's layout means knowing all of them.
- Lean bundles — subpath imports pull one mechanic, not the catalog.
- Safe refactors — internals aren't addressable, so they can change.
- Package surface clarity — the
exportsmap is the public API boundary.