Code Generation
Purpose: Generate the app-facing domain facade used by SDK, React, and agent code Audience: Developers who already have a working
.meldomain Reading time: 10-15 minutes
Overview
@manifesto-ai/codegen turns a compiled MEL domain into generated TypeScript files. For app code, the default output is the app-facing domain facade:
<source>.domain.tsnext to the.melfile- one domain interface with
state,computed, andactions - a type that can be passed to
createManifesto<TDomain>(schema, effects)
The output is deterministic: same schema always produces byte-identical files. Generated files include a @generated header and should not be hand-edited.
counter.mel -> compiler plugin -> counter.domain.tsCode generation is not needed for the smallest no-build script, but it is the normal path for TypeScript app code. Writing ManifestoDomainShape manually is a fallback for small examples, migrations, or unusual build environments.
Where This Fits
Do not start here before you have a .mel file. Once you move from the tutorial scripts into React, server routes, or agent tools, use the generated facade instead of hand-writing the same state, computed, and actions shape in TypeScript.
The normal app path is:
todo.mel -> Vite compiler plugin -> todo.domain.ts -> createManifesto<TodoDomain>()You only need direct DomainSchema access if you are writing build scripts, custom output plugins, or repository-level tooling.
Installation
npm install @manifesto-ai/sdk @manifesto-ai/core
npm install -D @manifesto-ai/compiler @manifesto-ai/codegen@manifesto-ai/core is installed here to satisfy the codegen peer dependency. You do not need to import Core APIs in normal app code. App code imports the compiled .mel file and the generated <source>.domain.ts facade.
Quick Start: Generate The Todo Facade
After Building a Todo App, keep src/domain/todo.mel. For the React and agent docs, configure Vite to emit a TodoDomain facade beside that file:
import { defineConfig } from "vite";
import { melPlugin } from "@manifesto-ai/compiler/vite";
import { createCompilerCodegen, createDomainPlugin } from "@manifesto-ai/codegen";
export default defineConfig({
plugins: [
melPlugin({
codegen: createCompilerCodegen({
plugins: [createDomainPlugin({ interfaceName: "TodoDomain" })],
}),
}),
],
});By default, compiling src/domain/todo.mel emits src/domain/todo.domain.ts.
Generated src/domain/todo.domain.ts:
// @generated by @manifesto-ai/codegen — DO NOT EDIT
// Source: src/domain/todo.mel | Schema hash: ...
import type {
ActionArgs,
ActionHandle,
ActionInput,
ManifestoApp,
RuntimeMode,
} from "@manifesto-ai/sdk";
export interface Todo {
completed: boolean;
id: string;
title: string;
}
export interface TodoDomain {
readonly state: {
filterMode: "all" | "active" | "completed"
todos: Todo[]
}
readonly computed: {
activeCount: number
completedCount: number
hasCompleted: boolean
todoCount: number
}
readonly actions: {
addTodo: (title: string) => void
clearCompleted: () => void
removeTodo: (id: string) => void
setFilter: (newFilter: "all" | "active" | "completed") => void
toggleTodo: (id: string) => void
}
}
export type TodoActionInput<Name extends keyof TodoDomain["actions"] & string> =
ActionInput<TodoDomain, Name>;
export type TodoActionArgs<Name extends keyof TodoDomain["actions"] & string> =
ActionArgs<TodoDomain, Name>;
export type TodoActionSurface<TMode extends RuntimeMode> = {
readonly [Name in keyof TodoDomain["actions"] & string]:
ActionHandle<TodoDomain, Name, TMode>;
};
export type TodoActionSurfaceFromApp<TMode extends RuntimeMode> =
ManifestoApp<TodoDomain, TMode>["action"];
export type TodoApp<TMode extends RuntimeMode> =
ManifestoApp<TodoDomain, TMode>;Use that facade at activation:
// src/manifesto-app.ts
import { createManifesto } from "@manifesto-ai/sdk";
import TodoMel from "./domain/todo.mel";
import type { TodoDomain } from "./domain/todo.domain";
const app = createManifesto<TodoDomain>(TodoMel, {}).activate();Use it for app-owned aliases too:
// src/types.ts
import type { TodoDomain } from "./domain/todo.domain";
export type TodoState = TodoDomain["state"];
export type Todo = TodoState["todos"][number];
export type FilterMode = TodoState["filterMode"];
export type TodoComputed = TodoDomain["computed"];That is the handoff into React Integration and Web App + Agent. If your goal is app code, you can stop here and use the generated facade. The rest of this page is for custom facade names, build scripts, and low-level codegen usage.
App Path Notes
Default Facade Name
createCompilerCodegen() can also be called without options. In that case it uses the default domain plugin and emits <source>.domain.ts; import whatever interface name that generated file exports. The Todo learning path pins interfaceName: "TodoDomain" only so every guide uses the same type name.
Advanced Codegen And Build Scripts
Direct Generation
If you want deterministic control over when files are emitted, call generate() directly in build or CI after your MEL source has been compiled to a DomainSchema.
import { generate, createDomainPlugin } from "@manifesto-ai/codegen";
import type { DomainSchema } from "@manifesto-ai/core";
const schema: DomainSchema = await loadCompiledSchema();
const result = await generate({
schema,
outDir: "./generated",
sourceId: "src/domain/todo.mel",
plugins: [createDomainPlugin()],
});
console.log(result.files.map(f => f.path));
// -> ["src/domain/todo.domain.ts"]Custom Facade Name
Use createDomainPlugin() options when you want a specific interface name:
import { generate, createDomainPlugin } from "@manifesto-ai/codegen";
const result = await generate({
schema,
outDir: "./generated",
sourceId: "src/domain/counter.mel",
plugins: [createDomainPlugin({ interfaceName: "CounterDomain" })],
});
// Produces src/domain/counter.domain.tsLegacy TypeScript And Zod Artifacts
createTsPlugin() and createZodPlugin() remain available for migration and low-level build pipelines. Prefer createDomainPlugin() for new app integrations.
import { generate, createTsPlugin, createZodPlugin } from "@manifesto-ai/codegen";
const result = await generate({
schema,
outDir: "./generated",
plugins: [createTsPlugin(), createZodPlugin()],
});
// Produces types.ts + base.tsHandling Errors
When an error occurs (e.g., file collision), no files are written to disk:
const result = await generate({ schema, outDir, plugins });
const errors = result.diagnostics.filter(d => d.level === "error");
if (errors.length > 0) {
errors.forEach(e => console.error(`[${e.plugin}] ${e.message}`));
// No files were written to disk
}Type Mapping Reference
This table is for low-level codegen users who work with compiled schemas or custom plugins. App developers usually express these shapes as MEL type definitions and let the compiler/codegen path emit the facade.
| Schema Definition | TypeScript | Zod |
|---|---|---|
{ kind: "primitive", type: "string" } | string | z.string() |
{ kind: "literal", value: "active" } | "active" | z.literal("active") |
{ kind: "array", element: T } | T[] | z.array(T) |
{ kind: "record", key: K, value: V } | Record<K, V> | z.record(K, V) |
{ kind: "object", fields: {...} } | export interface | z.object({...}) |
{ kind: "union", types: [A, B] } | A | B | z.union([A, B]) |
Union with null (T | null) | T | null | z.nullable(T) |
{ kind: "ref", name: "Other" } | Other | z.lazy(() => OtherSchema) |
| Optional field | field?: T | .optional() |
CI Integration
Let your normal build run the compiler plugin, then verify generated files did not drift:
pnpm build
pnpm typecheck
git diff --exit-code -- src/domainThis ensures generated code is always committed and up-to-date.
Legacy Plugin Order Matters
Plugins run in array order. This matters only when you use the legacy TS/Zod plugin pair: the Zod plugin reads artifacts from the TS plugin to generate type annotations.
// Correct: TS first, then Zod
plugins: [createTsPlugin(), createZodPlugin()]
// Also valid: Zod alone (no type annotations, but functional)
plugins: [createZodPlugin()]When the Zod plugin runs without the TS plugin, it produces schemas without z.ZodType<T> annotations -- still valid for runtime validation, but without compile-time type checking.
Common Mistakes
File Name Collision
// Wrong: both plugins write to the same file
plugins: [
createTsPlugin({ typesFile: "output.ts" }),
createZodPlugin({ schemasFile: "output.ts" }),
]
// -> Error diagnostic, no files writtenFix: Use distinct file names for each plugin.
Absolute or Traversal Paths
// Wrong: in a custom plugin
patches: [{ op: "set", path: "/etc/types.ts", content: "..." }] // absolute
patches: [{ op: "set", path: "../escape.ts", content: "..." }] // traversalFix: Use relative paths that stay within the output directory.
Next Steps
- React Integration -- Use the generated facade in UI code
- Web App + Agent -- Share one runtime between UI and agent entry points
- AI Agents -- Go deeper on agent-only tool loops
- API Reference -- Direct package API for build scripts and tooling
See Also
- Tutorial -- Learn Manifesto from scratch
- MEL For App Developers -- Write the domain code that codegen reads
- Runnable Examples -- Check the generated facade in a working app
- Package GUIDE -- Custom plugin tutorial and advanced usage