Skip to content
DocspackagesDocumentation

@lunora/codegen

Emits _generated/{api,server,dataModel}.ts from your schema.ts.

PackagesCodegen

@lunora/codegen reads your backend and writes the types that make it type-safe end to end. It parses lunora/schema.ts plus every function file under lunora/, then writes lunora/_generated/ so the rest of your app gets typed access to its own queries, mutations, actions, and tables.

You rarely call this package directly. The @lunora/vite plugin runs it on every schema change, and the lunora codegen command runs it once on demand. Both share this same core, so the output is identical regardless of how it was produced.

What it generates

The core outputs, always written:

  • api.ts: the typed api.<file>.<function> (and internal.*) function references your client and server code call through.
  • server.ts: the query / mutation / action / internalQuery / internalMutation / internalAction / definePolicy procedure builders, bound to your schema.
  • dataModel.ts: the Doc<"messages"> / Id<"users"> types derived from your tables.

It also emits app.ts, functions.ts, shard.ts, and the Drizzle schemas below, plus crons.ts, containers.ts, workflows.ts, vectors.ts, seed.ts, and the OpenAPI / OpenRPC spec files, each only when you use the matching feature.

The Drizzle schemas

drizzle.shard.ts and drizzle.global.ts mirror your tables as Drizzle sqliteTable definitions, split by where the rows live: .global() tables (D1) go in drizzle.global.ts, everything else (the default root shard and .shardBy() tables) in drizzle.shard.ts. A project with no table on one side gets an empty module, not a missing file.

They exist for the escape hatch: ctx.db covers the typed path, and when you need raw Drizzle — a @lunora/d1 client.drizzle query against your global tables, a hand-written join, a Drizzle Studio config — you import the table objects instead of redeclaring them.

import { drizzle } from "drizzle-orm/d1";
import { eq } from "drizzle-orm";

import { users } from "./lunora/_generated/drizzle.global.js";

const rows = await drizzle(env.DB).select().from(users).where(eq(users.email, email));

searchIndexes are not emitted — Drizzle has no sqliteTable abstraction for FTS5 virtual tables, and the runtime handles that plumbing outside Drizzle. Nothing you write has to import these files; ignore them if you never reach for Drizzle directly.

When it runs

// vite.config.ts — codegen runs on startup and on every schema change
import { lunora } from "@lunora/vite";

export default defineConfig({ plugins: [lunora()] });

Under lunora dev, codegen runs in watch mode: save a function or change the schema and the regenerated types are in the module graph before you switch windows.

Don't edit _generated/

These files are derived from your schema and overwritten on every run, so any hand edit is lost the next time codegen runs. Change schema.ts or your function files instead, then let codegen catch up.

Commit lunora/_generated/. The output is deterministic, so a stale committed copy is a real diff — but no command checks that for you: lunora verify runs codegen as a dry run (writing nothing) and lunora prepare silently overwrites the files. The schema-drift gate both run measures a different artifact, the committed .lunora-schema.json baseline. To gate the generated types in CI, regenerate and diff:

lunora codegen
git diff --exit-code lunora/_generated

The .js extension exception

The emitted modules are consumed under NodeNext, where relative imports need a file extension. So codegen deliberately writes .js into the imports it generates:

import { api } from "./_generated/api.js";

This is the one place in a Lunora project where hand-written .js extensions are correct. Your own lunora/ source still omits extensions like the rest of the codebase.

Calling it programmatically

If you need codegen in your own tooling, runCodegen takes a single options object and returns the result, including any static schema advisories found this run:

import { formatAdvisories, runCodegen } from "@lunora/codegen";

const result = runCodegen({ projectRoot: process.cwd() });

console.log(`wrote _generated/ -> ${result.outputDirectory}`);

if (result.advisories.length > 0) {
    console.warn(formatAdvisories(result.advisories));
}

Common options:

  • projectRoot (required): the directory containing the lunora/ folder.
  • dryRun: run discovery and emit (so parse errors still surface) without writing files. outputDirectory is still the path that would have been written.
  • lint: run the static schema advisor (default true); when false, result.advisories is empty.
  • apiSpec: "openapi" (default), "openrpc", "both", or "none".
  • lunoraDirectory: override the lunora subdirectory name.