Architecture & philosophy
The structural decisions that shape Meep — pure ECS, zero allocation, source-available, and code-first — and why each one was made.
Meep is built around a short list of non-negotiable properties. This page names them, explains the reasoning, and points at the parts of the engine where each one shows up. It assumes you’ve already read the ECS overview and the getting started guide.
Pure ECS, zero allocation
The ECS overview covers the data-oriented architecture in detail. What’s worth adding here is why the two properties travel together.
A pure ECS gives you predictable memory layout: components live in flat typed arrays, component queries are O(1) cached, and a system iterating ten entities costs the same per-element as one iterating ten million. But predictable layout only helps if you don’t throw it away every frame. The moment you start allocating new objects in your hot path — a new Vector3() here, a new Array() there — you hand control back to the garbage collector. On mobile, that means jank you can’t tune away.
The zero-allocation rule closes the loop. Math operations write into out-parameters; they don’t return new objects. Particle systems re-use emission buffers rather than growing and discarding them. Component pools are pre-allocated at scene load. The result is a simulation that the GC mostly never sees: memory turns over in the handful of permanent pools, and the per-frame allocation rate is near zero.
These are not independent features. The ECS makes zero-allocation straightforward to enforce; zero-allocation makes the ECS layout worth having.
4,500+ runtime assertions
Meep makes heavy use of a single internal assert module — src/core/assert.js. It exports assert, assert.equal, assert.defined, assert.isInstanceOf, assert.greaterThan, and around twenty other variants. Every invariant the engine can express at call time is expressed: wrong component type passed to a method, index out of range, NaN fed to a physics solver, entity used after it’s been destroyed.
In development these throw immediately at the source of the violation. You get a stack trace pointing at the line that broke the rule, not at whatever crashed downstream.
In production none of them exist. Every example in the repository configures @rollup/plugin-strip to remove all assert.* calls at build time:
// vite.config.js (first-person example, and every other example)
import strip from "@rollup/plugin-strip";
export default defineConfig({
plugins: [
{ ...strip(), apply: "build" },
],
});
The default pattern (assert.*) covers the full call surface. After stripping, the functions and their arguments are gone — the call sites compile to nothing. No wrapper overhead, no boolean guards, no dead branches. See the installation guide for the bundler-specific setup.
The flip side is that the assertions are only useful if you run a dev build. Shipping the un-stripped engine in production is not catastrophic — it still works — but it wastes bundle bytes and CPU on validation code that has no place in a shipped game.
4,000+ handwritten tests
The engine ships with a Jest test suite covering critical algorithms, edge cases, and architectural invariants. The spec files are excluded from the published npm package (the files field in package.json excludes src/**/*.spec.js), so they don’t bloat installs. The test runner is configured via jest.conf.json and coverage is collected against the source directly. The test script is npm run test:ci.
The intent behind “handwritten” is that these are not snapshot tests or generated fuzz runs — they’re authored assertions about specific behaviors that have been wrong at some point, or that encode a contract the engine depends on being true. Tests for the BVH traversal, the physics solver substep arithmetic, the ECS query cache invalidation, and the binary serializer round-trips are in this category.
Source-available
The full engine source ships under src/ inside the npm package. When you run npm install @woosh/meep-engine, you receive approximately 3,250 JavaScript source files alongside prebuilt bundles under build/. There are no compiled blobs, no obfuscated code, no WASM modules that wrap an opaque binary.
This matters in two practical ways:
Debuggability. When something in the engine behaves unexpectedly, you can read the code. Source maps point at real files on disk. You can set a breakpoint in the engine’s EntityManager.js or PhysicsSystem.js the same way you’d break in your own code.
No lock-in. If you need to understand why a query returns a particular result, or how the physics solver handles a degenerate contact, the answer is in the source you already have. You don’t need to file a support ticket and wait.
The license is proprietary — see FAQ for the distinction between source-available and open source. But the distribution model is deliberately transparent: no black boxes.
JSDoc-typed JavaScript with generated TypeScript declarations
The engine is authored in JavaScript with JSDoc type annotations. TypeScript declarations are generated by the build (npm run generate-types) and ship alongside the source. Every class, function, and type in the public API has a corresponding .d.ts file that TypeScript tooling picks up automatically.
The reason for JavaScript rather than TypeScript as the authoring language is explained in the FAQ: TypeScript can consume JavaScript cleanly, but JavaScript cannot consume TypeScript without a compile step. Authoring in JS keeps the engine usable from both worlds without friction. JSDoc gives most of the same editor benefits — autocomplete, inline type errors, documentation on hover — while the source stays as plain JavaScript that any bundler can handle without a TS build step.
Fine-grained ES modules
The engine is distributed as approximately 3,000 individual ES modules. There is no monolithic root export — you import exactly what you need by path:
import { EntityManager } from "@woosh/meep-engine/src/engine/ecs/EntityManager.js";
import { Transform } from "@woosh/meep-engine/src/engine/ecs/component/transform/Transform.js";
Tree-shaking removes everything you don’t reference. A demo that does nothing but import a lerp utility compiles to four lines. A demo that pulls in the full renderer, physics, AI, and terrain system still ships a smaller runtime than most monolithic alternatives, because every module is individually droppable.
The granularity also makes it straightforward to find the code behind any given feature: one module = one concept, named to match what it does.
Optional Three.js peer dependency
Three.js is a peer dependency marked optional in package.json:
"peerDependenciesMeta": {
"three": { "optional": true }
}
The graphics subsystem — renderer, materials, mesh systems — depends on Three.js. Nothing else does. An AI simulation, a headless physics server, a bot running on Node, a turn-based game with a pure DOM UI: none of these import the graphics modules, so Three.js never enters the bundle. You install it when you need it:
npm install three
The minimum version the engine declares a peer against is >=0.135.0. See installation for details.
No editor — code first
There is no Meep editor. There is no proprietary scene format, no binary project file, no GUI you have to open to configure a component. Everything is code.
An entity is created by calling new Entity().add(component).build(ecd). A scene is a function that builds entities. A level is a system that knows when to load and unload scenes. The state of your world at any point in time is the result of running your code — not something stored in an opaque file that the engine parses on startup.
This is a deliberate trade. You can’t click your way to a game; you have to understand what you’re building well enough to write it. In exchange, your project is a directory of plain source files, version-controlled like the rest of your code, reviewable in a diff, and rebuildable from scratch. Nothing about the architecture is hidden behind tooling you don’t own.
Where to go next
- ECS overview — the data-oriented architecture in depth.
- Installation — bundler configuration and the strip plugin.
- FAQ — source-available vs open source, TypeScript compatibility, and why not WebAssembly.
- Physics overview — the deterministic rigid-body engine built on the ECS.