Skip to content

Code Conventions

This page covers the coding standards enforced across the monorepo.

TypeScript

Compiler Settings

  • Target: ES2022
  • Module: ESNext with bundler module resolution
  • Strict mode: Enabled with noUncheckedIndexedAccess
  • verbatimModuleSyntax and isolatedModules are enabled

Import Style

Use inline type imports:

typescript
// 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:

typescript
// 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:

typescript
// 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.md

Public 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

RuleSetting
consistent-type-importsEnforced with inline type specifiers
no-consoleRestricted (only console.warn and console.error allowed)
Unused variablesAllowed if prefixed with _ (e.g., _unused)
Import orderingEnforced: builtin > external > internal > parent/sibling > index > type

Import Order

Imports must be ordered by group, with each group separated by a blank line:

typescript
// 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.ts suffix (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:

typescript
// 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 @remarks tags
  • 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

Proprietary software. All rights reserved.