@supalive/codegen
v1.25.0
Published
Generate type-safe client code (Dart, and more) from a Supalive TypeScript router.
Maintainers
Readme
@supalive/codegen
Generate a type-safe client from a Supalive TypeScript router. The TypeScript router is the single source of truth: this tool reads its procedures, inputs, and output schemas and emits a strongly-typed client for another language.
Dart/Flutter is the first target (the supalive-dart binary). The package is
named generically so additional targets (e.g. Swift, Kotlin) can ship as
sibling binaries in future releases.
Install
npm install --save-dev @supalive/codegenThe generated Dart code depends on the supalive_client package — add it to
your Flutter/Dart app's pubspec.yaml.
CLI
npx supalive-dart <entry.ts> [options]<entry.ts> is a TypeScript file that exports your router.
| Option | Description | Default |
| --- | --- | --- |
| -o, --output <dir> | Output directory for generated Dart files | . |
| -r, --router <name> | Router export name | appRouter |
| -c, --client-class <name> | Generated client class name | AppClient |
| --config <name> | Config object export name | dartCodegen |
| --tsconfig <path> | tsconfig.json used to resolve types | — |
| --include-internal | Also generate internal: true procedures | off |
| -h, --help | Show help | — |
Example:
npx supalive-dart src/server/procedures.ts \
--router appRouter \
--client-class BasicClient \
--output lib/api \
--tsconfig tsconfig.jsonThis writes models.dart and client.dart into lib/api/. Treat that
directory as generated — never edit it by hand; re-run the command after
changing your procedures.
Config in the entry file
Instead of passing flags, export a dartCodegen object next to your router.
CLI flags take precedence over these values.
export const dartCodegen = {
output: "lib/api",
clientClassName: "BasicClient",
// router, includeInternal, tsconfig, numericOverrides also supported
};Only JSON-literal values are read statically; dynamic expressions are ignored.
Programmatic API
import { generateToDisk, generate } from "@supalive/codegen";
// Extract + emit + write files to `output`.
generateToDisk({
entry: "src/server/procedures.ts",
output: "lib/api",
clientClassName: "BasicClient",
tsconfig: "tsconfig.json",
});
// Or get the emitted files in memory without touching disk.
const { files } = generate({ entry: "src/server/procedures.ts" });extractClient (build the intermediate representation) and emit (turn the IR
into files) are also exported for advanced use.
Return type overrides
By default the codegen infers everything from TypeScript types: number →
double, bigint → BigInt, Date → DateTime, etc. The returns property
on procedure configs lets you override specific fields for the generated client
without changing your handler's return type.
Model naming uses the .modelName() Zod extension method — no extra imports
needed (it patches all Zod types automatically when you import z from @supalive/core).
Default (no returns)
Everything inferred from the handler's TS return type:
const listCards = query({
args: z.object({ limit: z.number().int().default(20) }),
handler: async (_ctx, _input): Promise<{ items: { id: string; name: string }[] }> => ({
items: [],
}),
});
// → ListCardsResult { items: List<ListCardsItem> }.modelName("Name") — rename a model
Give a generated model a custom name instead of the auto-derived one:
const listCards = query({
args: z.object({ limit: z.number().int().default(20) }),
returns: z.array(
z.object({ id: z.string(), name: z.string() }).modelName("CardItem")
),
handler: async (_ctx, _input) => [] as { id: string; name: string }[],
});
// → result type: List<CardItem> (not List<ListCardsResultItem>).modelName("Foo") on any Zod schema tells the codegen to rename the
model to Foo. Combine with field-level overrides like z.number().int() to
also patch the TS-inferred types.
Partial field overrides (implicit)
Only the fields you list in the returns schema are overridden; the rest stay
inferred from TS. No partial() wrapper needed:
const storeCards = query({
args: z.object({}),
returns: z.object({
total: z.number().int(), // override: number → int
// 'cards' is NOT listed → stays inferred from TS
}),
handler: async (_ctx, _input): Promise<{
cards: { id: string; name: string; balance: number }[];
total: number;
}> => ({ cards: [], total: 0 }),
});
// → StoreCardsResult { cards: List<...>, total: int }Combining .modelName() + .int()
The most common pattern: rename nested models and patch numeric types while keeping the parent result inferred from TS:
const storeCards = query({
args: z.object({}),
returns: z.object({
value: z.array(
z.object({
balanceCents: z.number().int(), // double → int
}).modelName("StoreCardItem")
),
}),
handler: async (ctx, input) => {
const cards = await ctx.db.query(GiftCardSchema).select().get();
return {
cursor: "...",
value: cards,
};
},
});
// → StoreCardsResult { cursor: String, value: List<StoreCardItem> }
// StoreCardItem { id, balanceCents: int, ... }Procedure-level list return with model naming
When a procedure returns a bare list (not wrapped in an object), use
z.array() at the top level of returns:
const cardLedger = query({
args: z.object({ cardId: z.string() }),
returns: z.array(
z.object({
amountCents: z.number().int(),
balanceAfterCents: z.number().int(),
}).modelName("CardLedgerItem")
),
handler: async (ctx, { cardId }) =>
ctx.db.query(CardTransactionSchema).select()
.where((f) => f.eq("cardId", cardId)).get(),
});
// → result type: List<CardLedgerItem>
// CardLedgerItem { ..., amountCents: int, balanceAfterCents: int, ... }Nested model overrides
Override fields inside a list element's model by nesting .modelName() +
field overrides:
const nestedOverride = query({
args: z.object({}),
returns: z.object({
items: z.array(
z.object({
id: z.string(),
balance: z.number().int(), // override int
name: z.string(),
}).modelName("NestedItem")
),
}),
handler: async (_ctx, _input) => ({
items: [] as { id: string; balance: number; name: string }[],
}),
});
// → NestedItem { id: String, balance: int, name: String }Summary of helpers
| Helper | Purpose | Example |
| --- | --- | --- |
| .modelName("Name") | Rename a model in generated output | z.object({ ... }).modelName("CardItem") |
| z.number().int() | Patch a field's type from double to int | z.object({ balance: z.number().int() }) |
| z.bigint() | Patch a field's type to BigInt | z.object({ revision: z.bigint() }) |
All returns helpers are purely for codegen — they have no effect at runtime.
The Zod schemas are not used for runtime validation of the handler's return
value.
License
MIT
