@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 typedapi.<file>.<function>(andinternal.*) function references your client and server code call through.server.ts: thequery/mutation/action/internalQuery/internalMutation/internalAction/definePolicyprocedure builders, bound to your schema.dataModel.ts: theDoc<"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/_generatedThe .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 thelunora/folder.dryRun: run discovery and emit (so parse errors still surface) without writing files.outputDirectoryis still the path that would have been written.lint: run the static schema advisor (defaulttrue); whenfalse,result.advisoriesis empty.apiSpec:"openapi"(default),"openrpc","both", or"none".lunoraDirectory: override thelunorasubdirectory name.