zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Build an application

Compiler code generationSupported

zmdb codegen compiles validators and schemas ahead of time without a bundler. The single executable comes from @zmdb/cli and delegates to @zmdb/compiler.

yarn add --dev @zmdb/cli@1.0.0-beta.2 typescript@^7.0.2
yarn zmdb codegen --project tsconfig.json
yarn zmdb codegen --project tsconfig.json --check
yarn zmdb codegen --project tsconfig.json --watch

--check writes nothing and exits with code 1 when artifacts are stale. --watch retains one compiler session until interruption; it cannot be combined with --check or --json. Config is optional for this command: an explicit --project wins, then the loaded config's project, then tsconfig.json in the working directory. A supplied config also provides the naming strategy.

The compiler package owns the same compileProject and writeCompileResult APIs for custom tools. Install @zmdb/compiler@1.0.0-beta.2 with typescript@^7.0.2 when only the library is needed.

A project compiler script#

Create scripts/compile-zmdb.mjs:

import { fileURLToPath } from 'node:url';

import { compileProject, writeCompileResult } from '@zmdb/compiler';
import { loadConfig } from '@zmdb/compiler/config';

const check = process.argv.includes('--check');
const config = await loadConfig({ optional: true });
const project = config?.project ?? fileURLToPath(new URL('../tsconfig.json', import.meta.url));

const result = await compileProject({
  project,
  ...(config === undefined ? {} : { naming: config.resolvedNaming }),
});

for (const diagnostic of result.diagnostics) {
  console.error(`${diagnostic.code}: ${diagnostic.message}`);
}

if (result.diagnostics.length > 0) {
  process.exitCode = 1;
} else {
  const materialised = await writeCompileResult(result, { check });
  if (check && materialised.stale.length > 0) {
    for (const path of materialised.stale) console.error(`stale ${path}`);
    process.exitCode = 1;
  }
}

Run it with:

node scripts/compile-zmdb.mjs
node scripts/compile-zmdb.mjs --check

compileProject reads and compiles in a disposable shadow; it does not modify the application. writeCompileResult is the only writer. With check: true, it reports every stale, missing, or orphaned artifact and writes or deletes nothing.

What it writes#

For each source file that calls one of the seventeen generic entry points with a type argument — is, isShallow, equals, assert, assertShallow, assertEquals, validate, validateShallow, random, toJsonSchema, schemaOf, toolFor, protoDescriptor, protoDecode, protoEncode, grpcDescriptor, loadGrpcService — project compilation writes three files beside it and edits the call. The five protobuf/gRPC calls must resolve to @zmdb/protobuf; local or foreign functions with the same name are left alone.

src/handlers.ts                      your source; the call is rewritten
src/handlers.zmdb.witness.ts         the type argument, kept and checked by TypeScript
src/handlers.zmdb.generated.js       the compiled runtime code
src/handlers.zmdb.generated.d.ts     its signatures
// before
import { is, schemaOf } from '@zmdb/core';
if (is<User>(body)) { … }
const users = defineRepository(schemaOf<User>(), driver);
// after
import { zmdbIsUser, zmdbSchemaUser } from './handlers.zmdb.generated.js';
if (zmdbIsUser(body)) { … }
const users = defineRepository(zmdbSchemaUser(), driver);

Commit all four files. A fresh clone then runs the generated application code without loading TypeScript, @zmdb/compiler, a build plugin, or a schema interpreter.

Why three generated files#

The rewrite is destructive: after compilation, the original type argument no longer appears in the source. The witness preserves it in a TypeScript module checked by the application's own project. A renamed or deleted type therefore becomes a build error instead of leaving behind an ungrounded validator.

The emitted helpers are untyped JavaScript, so they live in .js; the adjacent .d.ts carries their signatures. Generated runtime JavaScript imports only runtime helpers such as @zmdb/validator/errors and @zmdb/protobuf/wire. It never imports @zmdb/compiler.

Selecting files#

Omit files to compile the project source set, or pass exact project members:

const result = await compileProject({
  project: '/workspace/app/tsconfig.json',
  files: ['/workspace/app/src/orders.ts'],
});

A requested file that is outside the project, duplicated after normalisation, or itself generated is returned as a compiler diagnostic rather than silently skipped.

Which route should I use?#

Build shapeCompiler route
Vite, Rollup, esbuild, webpack, or Rspackconfigured @zmdb/compiler plugin; explicit options via @zmdb/compiler/unplugin
React Native or Expo@zmdb/compiler/metro
project compilation before running the applicationzmdb codegen, or compileProject + writeCompileResult
a library that ships generated validatorsproject compilation; commit the generated files

All routes reuse the same reflection, transform, and emission implementation.

---

See also: AOT Setup · JIT vs AOT · Schema Declaration · CLI Overview