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?, options?) 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, and for aliases either a branded type plus a constructor (over a string or numeric primitive) or a plaintypealias — see Aliases are newtypes; - 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 and a
contextId; each method wraps one this._mero.rpc.execute(...) call.
export class KVStoreClient { private _mero: MeroJs; private _contextId: string;
constructor(mero: MeroJs, contextId: string) { this._mero = mero; this._contextId = contextId; }
/** * 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, }); }}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) and returns the result directly, throwing on error — there is no{ output }wrapper to unwrap.- There is no executor argument. The node resolves the executor from its own owned identity for the context, so a client-supplied key was always ignored.
- 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. - A cross-context entry point is tagged
@xcall <callers>, and a declared migration entrypoint@migration from state version N to N+1. Both are documentation; the node enforces the caller policy and drives migrations. 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
(create-mero-app/src/index.ts). It copies the KV Store reference app out of
the calimero-network/apps monorepo into a new standalone project 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. - Sparse-clone
calimero-network/appsinto a temporary directory (os.tmpdir()) andsparse-checkout set apps/kv-store. There is one template,rust, so nothing is prompted. - Copy the app into the target directory, excluding
.git,.github,.gitignore,.gitattributes,.gitmodules, andnode_modules. - Detach it from the monorepo. Cone-mode sparse checkout also materializes the repo-root files, and those are what the inherited references resolve against — see the CLI reference for the full table.
- Write a project root (
package.json,pnpm-workspace.yaml) and a README with the standalone commands, then delete the temp clone. - Print next steps:
cd <dir>,pnpm install,pnpm logic:build,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│ │ └── 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 ← scaffolding CLI └── monorepo.ts ← resolves catalog:/workspace/extends refscodegen-example/ ← reference React app (not published)