Architecture
This page walks the two tools end to end and maps each stage to the code that implements it, so you can jump straight to the source.
The abi-codegen pipeline
Section titled “The abi-codegen pipeline”calimero-abi-codegen is a straight-line pipeline: read the manifest, validate
it, walk it, and print TypeScript. There is no intermediate build step and no
config file — the CLI (src/cli.ts) orchestrates two library calls.
1. Parse & validate — parse.ts
Section titled “1. Parse & validate — parse.ts”loadAbiManifestFromFile(path) reads the JSON and hands it to
parseAbiManifest(json), which:
- validates it against the embedded JSON Schema (
schema/wasm-abi-v1.schema.json) using AJV, with all formats registered; - confirms
schema_version === 'wasm-abi/1'; - enforces invariants beyond the schema — every
$refmust resolve to a defined type or a recognized raw Rust type,state_rootmust name a defined type, and map keys must bestring; - rejects duplicate method, event, or type names;
- returns a deeply frozen
AbiManifest, so downstream code cannot mutate it.
Any failure throws a descriptive Error rather than returning a partial result.
2. Model — model.ts
Section titled “2. Model — model.ts”model.ts is pure types: TypeScript interfaces that mirror the JSON schema 1:1
— AbiManifest, AbiMethod, AbiEvent, AbiParameter, AbiTypeRef, and every
type-definition variant (AbiRecord, AbiVariantDef, AbiAlias, bytes types).
It carries no logic; it is the shared vocabulary the parser and generators speak.
3. Generate — generate/client.ts
Section titled “3. Generate — generate/client.ts”generateClient(manifest, clientName?, importPath?) walks the manifest and
returns a single TypeScript source string. In order, it emits:
- a generated-file banner and an
import { MeroJs } from '<importPath>'; - one declaration per named type —
interfacefor records, a string-literal union or a discriminated-unionPayloadtype (plus a factoryconst) for variants,typealiases for aliases; - error-code / error-union types for any method that declares
errors, and<Event>Payloadtypes plus anAbiEventunion for events; - the
CalimeroByteshelper class and byte-conversion helpers — only if the manifest actually usesbytestypes; - the client class itself.
4. Emit helpers — generate/emit.ts
Section titled “4. Emit helpers — generate/emit.ts”emit.ts holds the naming utilities the generator leans on: formatIdentifier
(escapes reserved words and illegal characters), toCamelCase (method names),
sanitizeClassName and deriveClientNameFromPath (client-class naming), and
mapRustTypeToTs (maps raw Rust type strings like Vec<u32>, Option<T>,
Result<T, E>, and tuples to TypeScript).
The generated client class
Section titled “The generated client class”The emitted class is thin. Its constructor takes a MeroJs runtime, a
contextId, and an executor public key; each method wraps one
this._mero.rpc.execute(...) call.
export class KVStoreClient { private _mero: MeroJs; private _contextId: string; private _executorPublicKey: string;
constructor(mero: MeroJs, contextId: string, executorPublicKey: string) { this._mero = mero; this._contextId = contextId; this._executorPublicKey = executorPublicKey; }
/** * set * * @intent mutating */ public async set(params: { key: string; value: string }): Promise<void> { const response = await this._mero.rpc.execute({ contextId: this._contextId, method: 'set', argsJson: params, executorPublicKey: this._executorPublicKey, }); }}Note the shape, because it differs from older hand-written clients:
- All arguments go in a single
paramsobject — a method with parameters isset(params: { key, value }), never positionalset(key, value). A method with no parameters takes no arguments. - The first constructor argument is the
MeroJsinstance itself, notmero.rpc. rpc.executeis called with a named-field object (contextId,method,argsJson,executorPublicKey) and returns the result directly, throwing on error — there is no{ output }wrapper to unwrap.- A declared method
intent(e.g.read_only) is surfaced as a@intentJSDoc tag only. It is documentation; there is no separate read transport yet, so read and write methods are called identically. bytes-typed values are represented by the generatedCalimeroBytesclass (CalimeroBytes.fromHex(...),.toUint8Array()), converted to/from plain arrays at the RPC boundary.
The create-mero-app scaffolding flow
Section titled “The create-mero-app scaffolding flow”create-mero-app is an independent CLI built on commander and prompts
(create-mero-app/src/index.ts). It clones a starter template from GitHub into
a new directory.
Step by step:
- Resolve the target directory from the optional
[project-name]argument (defaults to the current directory) and validate its basename withvalidate-npm-package-name. A non-empty existing directory is rejected. - Pick the template — from
-t, --template <name>if given, otherwise via an interactiveselectprompt. The two templates arerust(calimero-network/kv-store) andjavascript(calimero-network/kv-store-js). git clone --depth 1the template repo into a temporary directory (os.tmpdir()).- Copy the tree into the target directory, excluding
.git,.github,.gitignore,.gitattributes,.gitmodules, andnode_modules; then delete the temp clone. - Rewrite the
namefield of the new project’spackage.jsonto the app name. - Print next steps:
cd <dir>,pnpm install,pnpm dev.
Package layout
Section titled “Package layout”abi-codegen/├── src/│ ├── cli.ts ← calimero-abi-codegen CLI entrypoint│ ├── parse.ts ← loadAbiManifestFromFile, parseAbiManifest│ ├── model.ts ← TypeScript model of the ABI manifest│ ├── index.ts ← package entry: re-exports model + parse│ ├── generate/│ │ ├── client.ts ← generateClient() — full source emitter│ │ ├── types.ts ← generateTypes() — types-only emitter (library)│ │ └── emit.ts ← naming utilities, Rust→TS mapping│ ├── schema/│ │ └── wasm-abi-v1.schema.json ← embedded JSON Schema (AJV)│ └── utils/ ← deepFreeze, AJV error formattingcreate-mero-app/└── src/index.ts ← interactive scaffolding CLIcodegen-example/ ← reference React app (not published)