Codegen
Codegen scans your convention directories and writes one typed app into .generated/. Run it after adding a file, and the app object, the routes and the types all match what is on disk.
You drop a file in collections/. Nothing imports it. It still turns up on
app.collections. Codegen is what closes that gap. This page is how it works,
and the commands that drive it.
Run it
questpie generateThe command reads questpie.config.ts and scans the directories beside it.
Codegen follows a root config that only re-exports a deeper one. The starters do
that, so they scan beside src/questpie/server/questpie.config.ts. The
#questpie and #questpie/factories imports in your code resolve into
.generated/. Before the first run they do not exist. That is why a fresh clone
and a CI build both start here.
Write a file:
import { collection } from "#questpie/factories";
export const invoices = collection("invoices").fields(({ f }) => ({
number: f.text(40).required(),
total: f.number(),
}));Generate, then use it:
questpie generateimport { app } from "#questpie";
const { docs } = await app.collections.invoices.find({});No import list. No manifest. The directory is the registry.
What it reads
Each of these is a directory beside questpie.config.ts. Put a file in one and
it is found on the next run.
| Directory | Lands on | Rule |
|---|---|---|
collections/ | app.collections | the file must call collection() |
globals/ | app.globals | the file must call global() |
channels/ | ctx.channels | the file must call channel() |
routes/, functions/ | app.routes, an HTTP endpoint | subfolders are scanned too |
jobs/ | ctx.queue.<key> | one job per file |
services/ | ctx.services.<key> | one service per file |
emails/ | ctx.email.sendTemplate | one template per file |
fields/ | f.<name> in .fields() | the file must call fieldType() |
messages/ | backend translations | one file per locale, en.ts |
migrations/, seeds/ | the lists the CLI runs | one per file |
A handful of single files are read the same way: modules.ts, env.ts,
env.client.ts, fields.ts, config/app.ts and config/auth.ts. Modules add
their own, so @questpie/admin also brings views/, blocks/, components/
and config/admin.ts.
What it writes
| File | What it holds |
|---|---|
.generated/index.ts | The shared app instance, env and the public types |
.generated/app-factory.ts | createAppForRuntime(), one fresh app per call |
.generated/factories.ts | collection() and global(), wired to your enabled field types |
.generated/entities.gen.ts | The flat category maps, AppCollections and friends |
.generated/context.gen.ts | The AppContext shape and the session types |
.generated/names.gen.ts | The key registries that make relation targets autocomplete |
app-factory.ts is for tests. createAppForRuntime(runtime) builds a new app
from the runtime config you pass it. index.ts shares one app, so every
import of #questpie gets the same one. Import from #questpie/app-factory.
Import collection and global from #questpie/factories. Import everything
else from #questpie. Run codegen after a schema change and commit the result.
.generated/ belongs in git. It is build output, but your code imports it, so a
teammate, CI and your editor all resolve it with no build step first.
Codegen owns the output directory
Each run that writes deletes .generated/ and recreates it. A file you put
there by hand is gone on the next generate. Each file is written to a temp
path and renamed, so a killed run never leaves a truncated file behind.
What becomes the key
collection("invoices") is keyed as invoices. The string you pass the factory
is the key, whatever the file is called. Hyphens become camelCase. Underscores
stay put. So collection("barber_services") in collections/barber-services.ts
keys as barber_services.
Files without a factory call are keyed off the file name instead.
jobs/send-newsletter.ts is sendNewsletter, default export or named.
Codegen skips index.ts, *.d.ts, test and spec files, and anything starting
with an underscore. So a _helpers.ts sitting next to your collections is
invisible.
Keep it running
questpie devWatch mode. It regenerates when a file is added or removed, and when the config
changes. Editing the body of a collection does nothing. The generated file
imports your module by path, and that import line does not change when the
file's contents do. Both commands take -c for a config path and --verbose.
generate also takes --dry-run to print the output instead of writing it.
Scaffold a file
questpie add collection invoice # writes collections/invoice.ts, then generates
questpie add --list # every type, and which target provides itThe built-in types are collection, global, channel, job, service,
email, route, seed and migration. Modules add more. When two targets
declare the same type, questpie add writes a file in both. So
questpie add block hero gives you the server definition and the admin
renderer at once. Pass --target to pick one. An existing file is skipped with
a warning.
Modules bring their own directories
modules.ts lists the packages your app depends on. Codegen reads it before
anything else, because a module can carry its own codegen plugin. That plugin is
what adds views/, blocks/ and config/admin.ts when you install
@questpie/admin.
import { adminModule } from "@questpie/admin/modules/admin";
import { openApiModule } from "@questpie/openapi";
const modules = [adminModule, openApiModule] as const;
export default modules;You do not register the plugin. Installing the module is enough. The plugin key is read at codegen time and dropped before the runtime merge, so it never reaches the app.
Types you import from #questpie
import type { CollectionDoc, CollectionWhere, App, AppConfig } from "#questpie";
type Invoice = CollectionDoc<"invoices">;
type InvoiceFilter = CollectionWhere<"invoices">;| Type | Use it for |
|---|---|
CollectionDoc<K> | One row of collection K |
CollectionWhere<K> | A where clause you build before calling find |
GlobalDoc<K> | One row of global K |
AppConfig | The client APIs, as in createClient<AppConfig>() |
App | The typed app instance, typeof app |
#questpie also exports createContext(). It builds a typed AppContext for
scripts and tests. AppSession and AppSessionUser come from there too.
Generate before you migrate, seed or push
A CLI command resolves .generated/index.ts for the real app instance. When
that file is missing it tells you to run questpie generate first. A stale
.generated/ is the usual reason a new collection is not on
app.collections.
Where each topic lives
| Topic | Page |
|---|---|
| Every rule that turns a file into an entity | Discovery |
| The plugin contract that adds a convention | Plugins |
| Writing a plugin, start to finish | Building a plugin |
| Generating a module inside an npm package | Publishing |
questpie.config.ts, config/app.ts, modules.ts | Configuration |
env.ts and env.client.ts | Environment |
Next
Modules is the bundle side of this. A module ships collections, routes and jobs, and codegen folds them in before it scans a single directory of yours.