Code Conventions
This page covers the coding standards enforced across the monorepo.
TypeScript
Compiler Settings
- Target: ES2022
- Module: ESNext with
bundlermodule resolution - Strict mode: Enabled with
noUncheckedIndexedAccess verbatimModuleSyntaxandisolatedModulesare enabled
Import Style
Use inline type imports:
// Correct
import { type Entity, createWorld } from '@web-engine-dev/ecs';
// Incorrect - separate type import statement
import type { Entity } from '@web-engine-dev/ecs';
import { createWorld } from '@web-engine-dev/ecs';File Extensions in Imports
All relative imports must include the .js extension (even for .ts source files). This is required by the verbatimModuleSyntax setting and ESM resolution:
// Correct
import { SelectionManager } from './SelectionManager.js';
// Incorrect
import { SelectionManager } from './SelectionManager';Named Exports Only
All packages use named exports exclusively. Default exports are not used:
// Correct
export function createWorld() { ... }
export class World { ... }
// Incorrect
export default class World { ... }Package Structure
Each package follows a consistent structure:
package-name/
├── src/
│ ├── index.ts # Public exports
│ └── *.test.ts # Co-located tests
├── tsup.config.ts # Build config (extends shared)
├── vitest.config.ts # Test config (extends shared)
├── tsconfig.json # TS config (extends shared/tsconfig.package.json)
├── package.json
└── README.mdPublic API is exported from src/index.ts. Internal utilities and helpers are not exported.
ESLint
The project uses the ESLint flat config format (eslint.config.mjs).
Key Rules
| Rule | Setting |
|---|---|
consistent-type-imports | Enforced with inline type specifiers |
no-console | Restricted (only console.warn and console.error allowed) |
| Unused variables | Allowed if prefixed with _ (e.g., _unused) |
| Import ordering | Enforced: builtin > external > internal > parent/sibling > index > type |
Import Order
Imports must be ordered by group, with each group separated by a blank line:
// 1. Node builtins
import { readFileSync } from 'node:fs';
// 2. External packages
import { describe, it, expect } from 'vitest';
// 3. Internal (workspace) packages
import { createWorld } from '@web-engine-dev/ecs';
// 4. Parent/sibling modules
import { SelectionManager } from '../selection/SelectionManager.js';
import { validateInput } from './helpers.js';
// 5. Types (inline is preferred, but type-only imports at the end)
import { type Entity } from '@web-engine-dev/ecs';Type-Aware Linting
Many type-checked ESLint rules are enabled as warnings for gradual adoption. Type-aware linting is disabled for packages/rendering/renderer/src/ for build performance reasons.
Naming Conventions
- Files: PascalCase for classes/components (
SelectionManager.ts), camelCase for utilities (helpers.ts) - Classes: PascalCase (
SelectionManager,HistoryManager) - Functions/methods: camelCase (
createWorld,getEntity) - Constants: UPPER_SNAKE_CASE for true constants (
MAX_ENTITIES,PANEL_IDS) - Interfaces/Types: PascalCase (
Entity,ComponentType,PanelDefinition) - Component definitions: PascalCase (
Transform3D,MeshHandle) - Test files: Same name as source with
.test.tssuffix (SelectionManager.test.ts)
ECS Conventions
Component Data is Copied
world.get(entity, Component) returns a copy, not a reference. Always call world.insert() to persist changes:
// Correct
const pos = world.get(entity, Position);
if (pos) {
world.insert(entity, Position, { x: pos.x + 1, y: pos.y });
}
// Incorrect - modifications are silently lost
const pos = world.get(entity, Position);
if (pos) {
pos.x += 1; // This change is never saved!
}Resources are References
world.getResource(Resource) returns a direct reference. Mutations persist without needing insertResource().
Documentation
- Public APIs require JSDoc with
@param,@returns,@throws,@example, and@remarkstags - Internal modules need at minimum a header doc explaining purpose and relationships
- Complex packages have architecture notes in their README files
Error Handling
- Use
invariant()assertions for precondition checks - Errors are expected control flow, not exceptional
- Every error path should be tested
- Validate inputs at system boundaries (user input, network, file I/O)
- Trust internal calls within a module after boundary validation
Performance Conventions
Since this is a game engine, hot paths have specific requirements:
- Zero allocations in per-frame code: Use pre-allocated buffers, object pools, SoA layouts
- Document algorithm complexity: Use
@remarks O(n log n)in JSDoc - Prefer SoA over AoS: Align with the ECS archetype storage model
- Determinism: Systems affecting gameplay must produce identical results given identical inputs