Skip to content

Code Generation ​

Purpose: Generate the app-facing domain facade used by SDK, React, and agent code Audience: Developers who already have a working .mel domain 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.ts next to the .mel file
  • one domain interface with state, computed, and actions
  • 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.ts

Code 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 ​

bash
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:

typescript
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:

typescript
// @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:

typescript
// 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:

typescript
// 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.

typescript
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:

typescript
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.ts

Legacy TypeScript And Zod Artifacts ​

createTsPlugin() and createZodPlugin() remain available for migration and low-level build pipelines. Prefer createDomainPlugin() for new app integrations.

typescript
import { generate, createTsPlugin, createZodPlugin } from "@manifesto-ai/codegen";

const result = await generate({
  schema,
  outDir: "./generated",
  plugins: [createTsPlugin(), createZodPlugin()],
});
// Produces types.ts + base.ts

Handling Errors ​

When an error occurs (e.g., file collision), no files are written to disk:

typescript
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 DefinitionTypeScriptZod
{ kind: "primitive", type: "string" }stringz.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 interfacez.object({...})
{ kind: "union", types: [A, B] }A | Bz.union([A, B])
Union with null (T | null)T | nullz.nullable(T)
{ kind: "ref", name: "Other" }Otherz.lazy(() => OtherSchema)
Optional fieldfield?: T.optional()

CI Integration ​

Let your normal build run the compiler plugin, then verify generated files did not drift:

bash
pnpm build
pnpm typecheck
git diff --exit-code -- src/domain

This 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.

typescript
// 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 ​

typescript
// Wrong: both plugins write to the same file
plugins: [
  createTsPlugin({ typesFile: "output.ts" }),
  createZodPlugin({ schemasFile: "output.ts" }),
]
// -> Error diagnostic, no files written

Fix: Use distinct file names for each plugin.

Absolute or Traversal Paths ​

typescript
// Wrong: in a custom plugin
patches: [{ op: "set", path: "/etc/types.ts", content: "..." }]  // absolute
patches: [{ op: "set", path: "../escape.ts", content: "..." }]   // traversal

Fix: Use relative paths that stay within the output directory.


Next Steps ​


See Also ​