Conventions
Coding and project conventions for contributing to Hone.
Perry-Safe TypeScript
All code that will be compiled by Perry must avoid the constrained patterns. The key rules:
- No optional chaining (
?.) — use explicit null checks. - No nullish coalescing (
??) — useif (x !== undefined). - No dynamic key access (
obj[variable]) — use if/else chains. - No
for...ofon arrays — use indexedforloops. - No regex — use
indexOfand character checks. - No ES6 shorthand properties — write
{ key: key }. - No
Array.map/filter/reduceon class fields — useforloops. - No closures capturing
this— use module-level state and named functions.
When in doubt, prefer explicit and verbose over concise and clever.
Database Schemas
All database identifiers use camelCase:
CREATE TABLE magicLinks (
id INT PRIMARY KEY AUTO_INCREMENT,
userId INT NOT NULL,
tokenHash VARCHAR(64) NOT NULL,
expiresAt BIGINT NOT NULL,
createdAt BIGINT NOT NULL
);
This applies to table names, column names, and index names.
Test Imports
Bun-based packages (hone-core, hone-editor, hone-terminal, hone-relay, hone-build) import test utilities from bun:test:
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
Do not import from vitest, jest, or @jest/globals in these packages.
Platform Detection
Use the __platform__ compile-time constant for platform-specific code:
declare const __platform__: number;
// 0 = macOS, 1 = iOS, 2 = Android
if (__platform__ === 0) {
// macOS-specific code
}
Non-matching branches are dead-code-eliminated at compile time.
Configuration Files
Service configuration uses KEY=VALUE format in .conf files:
PORT=8445
DB_HOST=webserver.skelpo.net
AUTH_SECRET=my-secret
No quotes around values. No section headers. One key-value pair per line.
Project Structure
- No Rust in hone-ide. All Rust code is in
../perry/(compiler, runtime, UI libraries) or inhone-editor/native/andhone-terminal/native/(FFI crates). The IDE package is pure TypeScript. - No workspace manager. Each package is independent with its own
package.json, its ownnode_modules, and its own test runner. - No shared dependencies. Packages do not hoist or share
node_modules. Install dependencies in each package individually.
Closure Rule
Perry captures closure variables by value at creation time. Mutations after closure creation are invisible to the closure.
// WRONG — Perry captures the initial value of count
let count = 0;
setInterval(() => { count++; }, 1000); // always increments from 0
// RIGHT — module-level named function reads current state
let _count = 0;
function tick() { _count++; }
setInterval(tick, 1000);
Store mutable state in module-level let variables. Access it through module-level named functions. Never capture this in a closure.
FFI Conventions
- Use
f64for numeric parameters,i64for string/pointer parameters. Neveri32. - Functions are exported as
__wrapper_<name>symbols. - All FFI functions must be declared in
package.jsonunderperry.nativeLibrary.functions.
See FFI Conventions for full details.