Contributing
Contributions are welcome on any layer: the engine, a backend, the Flow A bridge, the Flow B compiler, the tooling, these docs. This page is the project-wide rules; the rest of the Internals section has the layer-specific ones, and the roadmap has what is planned and what is known to be broken.
Engine invariants
Three rules are not negotiable, because they are what keeps the engine portable to any MCU:
- No platform headers. Pure C99. No
stm32h7xx_hal.h, noesp_lcd.h, no<windows.h>. The engine uses only the C standard library headers. Hardware specifics live inbackends/. - No React, or any frontend, in the engine. It does not import React and makes no assumptions
about who calls
er_scene.h. Bindings to React, or to anything else, live inbridges/. That neutrality is what makes the two-flow design work. - No heap during rendering. Every scratch buffer is static, sized at compile time through the
ERUI_*flags. A render pass never allocates.
A change that needs to break one of these is a design conversation first, in an issue.
Code style
C is formatted by clang-format with the repository's .clang-format. The root CMakeLists.txt
exposes two targets; run the first before committing:
cmake -S . -B build
cmake --build build --target format # rewrite in place
cmake --build build --target format-check # dry run: --Werror, no edits
JavaScript, TypeScript and this site's Markdown are formatted by Prettier with the root
.prettierrc.cjs. The repository root has its own package.json that pins the Prettier version,
so install it once there and run it from there:
npm install # once, at the repository root
npm run format # rewrite in place
npm run format:check # dry-run
Markdown keeps its line breaks as written (Prettier's default proseWrap: "preserve"), and only
the tables are aligned. changelog.md and roadmap.md are copies synced from the root files, so
they are left out.
Neither formatter runs in CI, so run them before you push.
TypeScript. The Flow B compiler (aot/compile.mts and aot/compiler/) is TypeScript: .mts
files, checked strictly by npm run typecheck. Node runs them as they are by stripping their types,
so running the compiler or the JS tests from source needs Node 22.18 or later, and only erasable
syntax is allowed: no enum, namespace or parameter properties, a relative import names the real
file (./switch.mts), and a type-only import is written import type. The published package ships
JavaScript: tools/stage-npm-package.mjs compiles the .mts files when a release packs it.
Documentation conventions
C files are broken into sections with a banner:
/*----------------------------------------------------------------------------------------------------------------------
- Functions: Public
---------------------------------------------------------------------------------------------------------------------*/
Every function, static or public, carries a description, its parameters and its return value:
/**
* @brief What the function does.
*
* @param[in] param1 What it is.
* @param[in] param2 What it is.
*
* @return What comes back.
*/
Public APIs are documented in the header; static functions in the .c file that implements them.
The header comments are the reference the C engine page points at, so they
are worth keeping exact.
Comments say what the code does, not its history. A comment that cites an issue number or a review round goes stale the moment the code moves; the git log has the history.
Changes that need more than code
- Anything user-facing gets a changelog bullet under
## [Unreleased], short and high level, before the change is finished. See Releasing. - Anything that changes a version-bearing file (a manifest, an install pin) must go through
sync-version.mjs; CI checks for drift. - A new prop or event needs the bridge's tables, the package's type declarations and the AOT
compiler to agree;
npm testhas a parity test over the tables, and the AOT either lowers the prop or rejects it by name. - A new engine feature needs a CTest case, and if it adds a compile-time flag, a CI pass with the flag on. See Testing.
- A change that affects a board should be checked on that board; the board guides say how to build and flash each. Hardware results belong in the pull request, with the board named.
The pull request
master is protected: every change lands through a squash-merged pull request with a review and
the CI checks green (engine, bridge, JS tests and version drift, Flow B golden images, fuzzing, consumer smoke).
Keep a pull request to one concern. Describe what changed and why in a few sentences; the diff
shows the how.