Codegen
Codegen is the compiler that turns your file-convention source, collections, globals, routes, config, into one typed .generated/ app. You write declarations, run `questpie generate`, and every layer (CRUD, admin, REST, OpenAPI, typed client) wires itself up in sync.
Codegen is what makes QUESTPIE's "define once, get everything" promise real. You drop files in conventional directories (collections/, globals/, routes/, jobs/, config/…), run one command, and codegen discovers every file, merges in your modules, and emits a fully typed .generated/ app: the app instance, the AppContext, the #questpie/factories builders, and the #questpie type barrel (CollectionDoc, CollectionWhere, App, AppConfig). There is no central registry to edit and nothing is wired by hand, the file system is the registry, and codegen is the compiler.
This page covers the pipeline (file convention → .generated/), the export-discovery rules, the questpie generate / dev / add commands, the committed outputs you get, and, for plugin and module authors, the full CodegenPlugin API that lets a package contribute its own file conventions without you touching questpie.config.ts.
What it does
- Compiles file convention into a typed app. Scans your conventional dirs, derives a key per file, and emits
.generated/index.ts(thecreateApp()call),.generated/factories.ts(thecollection()/global()builders that know your modules), and the#questpietype barrel, all in one shot. - Discovers, never registers. Add a file under
collections/, run codegen, and it appears onapp.collections.*, the admin, REST, OpenAPI, and the typed client. No import list, no manifest. - Supports flat or by-feature source layout. The same category can live at
collections/posts.tsorfeatures/blog/collections/articles.ts; both feed the same generated maps. - Folds in modules and their plugins.
modules.tsis read in a pre-pass; each module can contribute collections, globals, jobs, and its own codegen plugin, auto-wired before the rest of discovery runs. - Watches in dev.
questpie devregenerates on file add/remove (content-only edits are skipped,typeof importis stable), so the typed surface tracks your file tree live. - Scaffolds from the same registry.
questpie add <type> <name>writes the right boilerplate into every target that declares that scaffold, then re-runs codegen. - Is extensible to the core. Every built-in category (collections, globals, routes, …) is declared through the same
CodegenPluginAPI a third-party package uses. Modules addviews,blocks,config/admin.tsthe same way you would, there are no privileged internal codegen hooks.
Quick start
You rarely call codegen's API directly, you run the CLI. After adding or renaming a convention file, regenerate the typed app:
questpie generate # discover every file, merge modules, emit .generated/That command reads questpie.config.ts, runs the modules.ts pre-pass, discovers every convention file, and writes the .generated/ directory. For a normal app it produces:
src/questpie/server/.generated/
index.ts # the createApp() call + App / AppConfig / CollectionDoc / createContext
factories.ts # collection() / global() builders pre-wired with your module fields
entities.gen.ts # flat category maps (AppCollections, AppGlobals, AppRoutes, AppServices)
context.gen.ts # the AppContext builder + session / route / block types
names.gen.ts # the names-only entity-key registries (relation-target autocomplete)During development, run the watcher instead and forget about it, it regenerates whenever you add or remove a file:
questpie dev # same pipeline, in watch mode (100ms debounce)Codegen is what wires the `#questpie/*` imports
The #questpie/factories and #questpie imports your app code uses resolve
to codegen's output. Until you run questpie generate they don't exist, which
is why a fresh checkout or a CI build starts with questpie generate. Mode is
auto-detected from the config file shape (root app vs. module vs. package).
The pipeline
Codegen runs the same ordered pipeline every time, whatever the mode:
- Resolve the target graph. The built-in
coreCodegenPlugin()is always prepended, then your config plugins, then module-extracted plugins. All contributions to a given target (e.g."server") merge into oneResolvedTarget. - Discover files. Scan each category's
dirs(bothdir/andfeatures/{name}/dir/) plus the single-filediscoverpatterns. Each match becomes aDiscoveredFilewith a derivedkey,varName,importPath, and detectedexportType. - Run transforms. Each plugin's
transform(ctx)runs in order, accumulating extra imports / type declarations / runtime code into the context. - Generate. Emit
index.ts(root-appcreateApp()) ormodule.ts(static module def), plusfactories.ts, validating the output with Bun's transpiler. - Write atomically. Files are written to a temp path and renamed, a crashed run never leaves a half-written
.generated/.
`coreCodegenPlugin()` is the canonical category map
Every built-in category and scaffold lives in one place:
coreCodegenPlugin(). It declares collections (factory collection),
globals (factory global), jobs, routes (dirs routes + functions,
recursive, /-separated keys), messages, services, emails (emitted as
emailTemplates), migrations + seeds (array emit), and fieldTypes
(factory fieldType). It is prepended automatically, you never register it.
Reading it is the fastest way to see exactly which dirs are scanned.
By-feature folders
QUESTPIE supports two equivalent source layouts for category files. Use the flat
layout when the app is small; use features/<feature>/... when a domain slice
owns several kinds of files.
Requires the file-convention codegen line
This is a questpie@3.0.2+ convention. Older 1.x / 2.x projects do not have
the features/ discovery contract; migrate them to the v3 file-convention
codegen before using this layout.
src/questpie/server/
collections/posts.ts
routes/health.ts
features/blog/
collections/articles.ts
routes/webhooks/stripe.post.ts
jobs/publish-article.ts
seeds/demo.ts
features/shop/
collections/products.ts
services/catalog.tsThe contract is:
- A category declared with
dirsis scanned in both places:dir/andfeatures/<feature>/dir/. - The feature name is organizational only.
features/blog/collections/articles.tscontributesapp.collections.articles, notapp.collections.blog.articles. - Keys are unique per category across both layouts.
collections/posts.tsandfeatures/blog/collections/posts.tsconflict instead of overriding each other. - Recursive categories keep only the path below the category directory.
features/blog/routes/webhooks/stripe.post.tskeys aswebhooks/stripe:POST. - Private feature directories (
features/_internal/...), private files (_helpers.ts),index.ts, tests, and declaration files are skipped. questpie devwatchesfeatures/on the server target, so adding or removing a feature file regenerates the typed app.
The core server categories that support this are collections, globals,
jobs, routes / functions, messages, services, emails, migrations,
seeds, and fieldTypes. Plugin categories use the same rule when they declare
dirs or a directory/map pattern such as blocks/*.ts.
Single-file discover patterns are different: modules.ts, env.ts,
fields.ts, config/app.ts, config/auth.ts, and config/admin.ts are
root-level files. A plugin can opt into collecting root plus feature single files
with mergeStrategy: "spread" for array-shaped contributions such as
sidebar.ts or dashboard widgets; spread order is root first, then features in
alphabetical order.
Export & discovery rules
Codegen has to turn a file into a key and an import. The rules are deterministic, identical across every category, and worth knowing because they decide what your entity is named on app.*.
The key comes from the factory argument, then the file name
For categories that declare factoryFunctions (collections → ["collection"], globals → ["global"]), the string you pass to the factory is the key, kebab-cased to camelCase:
import { collection } from "#questpie/factories";
// Keyed as app.collections.blogPosts, the file name is only a fallback.
export const blogPosts = collection("blog-posts").fields(/* … */);collection("blog-posts") registers as blogPosts regardless of the file name. Keep the file name matching the argument by convention (collections/blog-posts.ts) so the two never drift, and keep one entity per file. Hyphens convert to camelCase; underscores pass through unchanged, collection and global names use the snake_case PostgreSQL convention, and that name is the canonical runtime identifier (e.g. for relation targets).
For files without a factory string argument, the fallback depends on the category:
- Non-factory categories (jobs, services, emails, …) always key off the file name, never the export name. A single-entity file resolves its key as
kebabToCamelCase(basename(path))unconditionally, sojobs/send-newsletter.tsissendNewsletterwhether youexport defaultorexport const sendNewsletter. The export name is used only as the import binding (import { sendNewsletter as _job_sendNewsletter }), not as the entity key. - Factory categories (collections, globals, …) fall back to the export name for a named factory export with no string argument, and to the file name for a default factory export.
Default OR named export, both are discovered
Codegen detects the export style from the file content (no AST, a fast content scan), in priority order: export default → default; export const X = { … } (an object literal, not a factory call) → a named bundle (spread into the parent); export const/function/class X → a single named export; export { X } → named.
// Both of these are picked up identically:
export const posts = collection("posts"); // named
export default collection("posts"); // defaultMulti-export files are allowed for factory categories
Categories with factoryFunctions use a two-pass scan that finds every
matching factory call in a file, so one factory-category file can hold several
collection(), global(), block(), or fieldType() definitions. Routes,
jobs, services, emails, migrations, and seeds do not use this mode; keep those
as one entity per file. Files with zero matching factory calls are skipped
entirely, that's how utility and type-only files in a scanned dir are ignored.
Entity key still comes from the factory's first string argument when present,
else the export name.
What codegen skips
Discovery only considers .ts / .tsx / .mts files and skips: index.ts / .mts / .tsx, *.d.ts / *.d.mts, anything starting with _, __tests__/, and *.test.* / *.spec.*. So a _helpers.ts next to your collections is invisible to codegen.
Committed outputs
.generated/ is committed to git, not gitignored. It is build output, but it's the typed contract the rest of your code imports, so it lives in version control: a clone, a CI job, or a teammate's editor all resolve #questpie/* immediately, and a diff in .generated/ is a visible, reviewable consequence of a schema change.
| File | What it is | You import it as |
|---|---|---|
index.ts | The createApp() call + the public type barrel (App, AppConfig, CollectionDoc, CollectionWhere, createContext) | #questpie (the type barrel) |
factories.ts | collection() / global() (and module factories like block()) pre-wired with your merged field defs | #questpie/factories |
entities.gen.ts | Flat category maps (AppCollections, AppGlobals, AppRoutes, AppServices) | (re-exported via #questpie) |
context.gen.ts | The AppContext builder + session / route / block types | (re-exported via #questpie) |
names.gen.ts | The names-only entity-key registries that power relation-target autocomplete | (ambient) |
The rule of thumb: import collection / global from #questpie/factories (they know your enabled modules, so f.richText() and friends appear on f), and import types from #questpie. Run questpie generate after any schema change and commit the result alongside your source.
How to see what is in the app
The generated files are the ground truth for the app surface. If an agent or developer needs to verify what exists, inspect these before inventing imports or names:
| Question | Ground truth |
|---|---|
| Which collections/globals/routes/services exist? | src/questpie/server/.generated/entities.gen.ts and the AppCollections, AppGlobals, AppRoutes, AppServices exports from #questpie |
| Which relation target keys autocomplete? | src/questpie/server/.generated/names.gen.ts |
| What is the handler context shape? | src/questpie/server/.generated/context.gen.ts and Questpie.AppContext |
| What is the auth/session shape? | AppSession, AppSessionUser, and AppConfig.auth from #questpie |
| Which builder methods/fields are enabled? | src/questpie/server/.generated/factories.ts |
Convention files starting with _, index.ts, tests, and declaration files do not appear here by design. If a collection, route, custom field, or auth role is missing from the generated ground truth, fix discovery/module/config first and rerun questpie generate; do not patch the call site with casts or duplicate registries.
Regenerate before you migrate, seed, or push
questpie.config.ts carries app.url but is not itself the built app, on a
CLI command it auto-resolves .generated/index.ts for the real instance and
errors with "Run questpie generate first" if that file is missing. A stale
or absent .generated/ is the most common cause of "my new collection isn't
on app.collections". Generate first.
The #questpie type barrel
index.ts re-exports a small, stable public type surface off the #questpie package root. These are the aliases you reach for instead of Record<string, any>:
import type {
CollectionDoc,
CollectionWhere,
GlobalDoc,
App,
AppConfig,
} from "#questpie";
type Post = CollectionDoc<"posts">; // full selected row for app.collections.posts
type PostFilter = CollectionWhere<"posts">; // typed `where` clause for posts
type Settings = GlobalDoc<"siteSettings">; // selected row for a global| Type | Resolves to | Use for |
|---|---|---|
CollectionDoc<K> | CollectionSelect<AppCollections[K]> | A typed document/row of collection K. |
CollectionWhere<K> | Where<AppCollections[K], AppConfig> | A where filter built dynamically before find / findOne. |
GlobalDoc<K> | GlobalSelect<AppGlobals[K]> | A typed row of global K. |
AppConfig | { collections, globals, routes, storage, auth } | The client APIs: createClient<AppConfig>(), createAdminAuthClient<AppConfig>(). |
App | typeof app | The fully typed app instance. |
Two more, AccessRuleContext<K> and HookRuleContext<K>, narrow ctx.data to a collection's row inside shared access/hook helpers. They carry a cycle rule: import them only from files not imported by a collection (routes, services, jobs, scripts); helpers a collection imports take the package-level AccessContext / HookContext from questpie instead.
The barrel also emits createContext(), a typed AppContext factory for scripts, tests, and standalone code that resolves all services and gives flat access to infrastructure.
CLI reference
Codegen is driven by three commands. The broader migrate / seed / push surface is covered by Configuration and Seeds.
questpie generate
questpie generate [-c, --config <path>] [--dry-run] [--verbose]Runs codegen once. The mode is auto-detected from the config file: a root-app runtimeConfig() runs all targets (index.ts + factories.ts, plus ../admin if the admin module is present); a { module: { name } } config runs module mode; a packageConfig() iterates modulesDir subdirectories. The root-app path runs the modules.ts plugin pre-pass first. Codegen sets QUESTPIE_SKIP_ENV_VALIDATION=1 for the run, it imports your user code (config, env, modules) and must not require a populated environment, and exits 1 on target or cross-target projection errors.
questpie dev
questpie dev [-c, --config <path>] [--verbose]Watch mode. Regenerates all targets on file add / remove, content-only edits are skipped because typeof import("…") is stable, so editing a collection's fields doesn't churn .generated/. A change to a config file triggers a full regen. It watches every target root (including ../admin) and ignores .generated/ and non-.ts/.tsx/.mts files, with a 100ms debounce.
questpie add
questpie add [type] [name] [-c, --config <path>] [--dry-run] [--list] [--target <target>]Plugin-driven scaffolding from each target's scaffolds. Built-in types (from coreCodegenPlugin()): collection, global, job, service, email (.tsx), route, seed, migration. Modules add more (the admin module contributes block). --list prints every scaffold type with its description and the targets that declare it. When a scaffold type is declared by multiple targets, questpie add writes a file in all of them (e.g. a server block definition and its admin-client renderer), pass --target to scope to one. Existing files are skipped with a warning, and codegen re-runs afterward. The name is expanded to kebab / camel / pascal / title casings and passed to each template. See Seeds for the generated seed file contract.
questpie add collection invoice # → collections/invoice.ts, then re-runs codegen
questpie add --list # see every scaffold type + which targets provide itModules and codegen
modules.ts default-exports the pre-built modules your app depends on. Codegen reads it in a dedicated pre-pass before the rest of discovery, so a module's collections, globals, jobs, and its own codegen plugin, are folded in first.
import { adminModule } from "@questpie/admin/modules/admin";
import { openApiModule } from "@questpie/openapi";
const modules = [adminModule, openApiModule] as const;
export default modules;A module is plain static data shaped like ModuleDefinition, with one special key for codegen:
import { module } from "questpie";
export const myModule = module({
name: "billing",
collections: { invoices: /* … */ },
plugin: myCodegenPlugin(), // CodegenPlugin | CodegenPlugin[], codegen-only
});The plugin key is auto-extracted at codegen time and excluded from the runtime module merge, it exists purely so a package ships its file conventions without you editing questpie.config.ts. Extraction is a depth-first walk (sub-modules before self), deduped by plugin name (first occurrence wins); config-registered plugins win over module-extracted ones on a name collision.
`module()` is type-only
module(def) is an identity function, it returns the definition unchanged and
has no runtime behavior. It exists so TypeScript captures the literal module
shape. Modules are static plain data; there are no factory functions inside
a module definition.
Extending codegen: the CodegenPlugin API
This is the part that makes "core = module = userland" literal. A CodegenPlugin declares the same category / discover / registry contributions the core plugin uses, that's how @questpie/admin adds views, blocks, components, and a config/admin.ts file without a single change to framework code. Import everything from questpie/codegen:
import type { CodegenPlugin } from "questpie/codegen";
export function billingPlugin(): CodegenPlugin {
return {
name: "questpie-billing", // unique; dedup key
targets: {
server: {
root: ".",
outputFile: "index.ts",
// Scan a new conventional directory:
categories: {
webhooks: {
dirs: ["webhooks"],
prefix: "wh",
factoryFunctions: ["webhook"],
},
},
// Claim a composite config file:
discover: {
billingConfig: { pattern: "config/billing.ts", configKey: "billing" },
},
},
},
};
}Register it on runtimeConfig({ plugins: [...] }) or ship it from a module's plugin key, both reach the same merge.
`questpie/codegen` is the plugin-author entry point
questpie/codegen re-exports the full surface: every codegen type
(CodegenPlugin, CodegenTargetContribution, CategoryDeclaration,
DiscoverPattern, RegistryExtension, SingletonFactory, BuilderFactory,
ScaffoldConfig, …) plus the emit helpers (categoryRecordEntry,
categoryTypeEntry, importStatement, safeKey, sortedValues,
sourceBasename). This is the only entry point that exposes the codegen
types, questpie/types does not re-export any of them.
CodegenPlugin shape
interface CodegenPlugin {
name: string; // unique; dedup key
targets: Record<string, CodegenTargetContribution>;
validators?: CrossTargetValidator[]; // run after ALL targets generate
}Well-known target IDs are "server" (emits index.ts + factories.ts) and "admin-client" (emits client.ts); plugins may add custom IDs.
CodegenTargetContribution
One plugin's contribution to one target. Multiple plugins' contributions to the same target ID are merged into a single ResolvedTarget.
| Field | Type | Purpose |
|---|---|---|
root | string | Discovery root, relative to the server root (".", or "../admin" for the admin target). |
outDir | string | Output dir within root. Default ".generated". |
outputFile | string | Primary output filename ("index.ts", "client.ts"). |
moduleRoot | string | Subdir within each module dir where this target discovers (admin uses "client"). |
categories | Record<string, CategoryDeclaration> | Directory-pattern categories to scan + emit. |
discover | Record<string, DiscoverPattern> | Single-file / spread / config patterns. |
transform | (ctx: CodegenContext) => void | Runs after discovery, before generate; mutates the context. |
registries | { collectionExtensions, globalExtensions, fieldExtensions, singletonFactories, builderFactories } | Typed factory methods / wrappers to generate into factories.ts. |
callbackParams | Record<string, CallbackParamDefinition> | Named factories for callback-context proxies. |
generate | (ctx) => CodegenTargetOutput | Promise<…> | Custom generator that replaces the default template for this target. |
scaffolds | Record<string, ScaffoldConfig> | questpie add templates this target provides. |
`root` / `outDir` / `outputFile` / `moduleRoot` must agree across contributors
When several plugins contribute to the same target ID, resolveTargetGraph()
merges them, but a conflict in root, outDir, outputFile, or moduleRoot
throws, and a second generate per target throws. Categories are
shallow-merged per key (with one exception: factoryImports arrays are
concatenated), and transforms run in plugin order.
CategoryDeclaration
The primary unit of the plugin system, one declaration scans a directory and emits its entities. The essentials:
interface CategoryDeclaration {
dirs: string[]; // scanned as dir/ AND features/{name}/dir/
prefix: string; // generated var prefix (varName = _<prefix>_<key>)
recursive?: boolean; // descend subdirs (routes)
keySeparator?: "." | "/"; // for recursive keys (routes: "webhooks/stripe")
emit?: "record" | "array"; // "record" (default) | flat array (migrations/seeds)
typeEmit?: "standard" | "services" | "emails" | "messages" | "none";
registryKey?: string | boolean; // include in Registry; string overrides the key
factoryFunctions?: string[]; // enable multi-export factory discovery
keyFromProperty?: string; // use a runtime prop as the key (views → "name")
keyFromSource?: "basename"; // use the source filename as the key
createAppKey?: string; // emit under a different createApp key (emails → emailTemplates)
factoryImports?: Array<{ name: string; from: string }>; // spread runtime values into factories.ts
// …includeInAppState, extractFromModules, placeholder, recordPlaceholder, typeRegistry…
}Key behaviors:
dirsscans bothdir/and everyfeatures/*/dir/, so feature folders contribute without a central importer.factoryFunctionsturns on multi-export discovery (each matching factory call → a separate entity; non-matching files skipped). Without it, one file = one entity.keyFromPropertyuses a runtime property as the object key, views key off.name, blocks off.state.name, emitted as[_var.prop]: _var.keyFromSource: "basename"keys off the source filename instead (a client-convention pattern).emit: "array"andtypeEmit: "messages" | "none"exclude the category from the names-only key registry that powers relation-target autocomplete.registryKeycontrols the value-typedRegistry:trueuses the category name; a string overrides it (e.g.fieldTypes→"~fieldTypes"; tilde-prefixed keys are intentional internal registry keys);false/omitted excludes it.factoryImportsinjects spread-merged runtime imports intofactories.ts(the admin module addsadminFieldsto the field defs this way). When two plugins target the same category, these arrays are concatenated.
DiscoverPattern
For single files and spreads rather than directory categories:
type DiscoverPattern =
| string // shorthand
| {
pattern: string;
resolve?: "default" | "named" | "all" | "auto"; // default "auto"
keyFrom?: "filename" | "exportName";
cardinality?: "single" | "map";
mergeStrategy?: "spread"; // collect root + every features/*/pattern
registryKey?: string; // augment Registry with typeof the single
configKey?: string; // emit the whole file as config.<key>
};The string shorthand: a pattern containing * (or with no file extension) is a map / directory pattern; a single file with an extension ("config/auth.ts", "sidebar.ts") is a single. mergeStrategy: "spread" collects the root file plus every features/*/pattern into an ordered array, that's how sidebar entries and dashboard widgets accumulate from many feature modules. configKey emits the whole file as one entry in the config bucket, the modern way to add a composite config/<name>.ts file.
Use `configKey` for config files
Use configKey, which maps the whole file to one key under the config bucket (AppStateConfig). The canonical recipe for a plugin's config file is discover: { xConfig: { pattern: "config/x.ts", configKey: "x" } } paired with declare module "questpie" { interface AppStateConfig { x?: XConfig } }. Modules contributing the same key are merged per sub-key.
Generating typed factory methods
Three registry declarations let a plugin generate typed wrappers into factories.ts:
RegistryExtension, a typed method oncollection()/global()/ a field instance.stateKeyis the builder.set()key (the admin module addsadmin,adminList,adminForm, …).isCallback+callbackContextParamsmake codegen emit runtime proxy helpers;configTypePlaceholdersswaps tokens ($COMPONENT_NAMES) for module-extracted type aliases;defaultswraps user config as{ ...defaults, ...userConfig }.SingletonFactory, a typed identity wrapper for a config file:export function branding<T extends ServerBrandingConfig>(config: T): T { return config }. Core declaresappConfigandauthConfighere.BuilderFactory, a builder factory that needs the wrapped field defs at construction (the admin module'sblock()): codegen emitsexport function block<TName extends string>(name) { return BlockBuilder.create(name, _allFieldDefs) }. Unlikecollection()/global()(always generated), builder factories are plugin-contributed.
CallbackParamDefinition ties callback contexts to a real exported factory, never an inline JS string. Core declares f: { factory: "createFieldNameProxy", from: "questpie/builders" }, which makes f.title resolve to the string "title" inside a .fields() callback. When an extension lists callbackContextParams: ["f"], codegen looks f up in the merged params and emits that proxy.
Scaffolds
A ScaffoldConfig powers questpie add <type>:
interface ScaffoldConfig {
dir: string; // relative to the target root
extension?: string; // default ".ts"
description?: string; // shown in `questpie add --list`
template: (ctx: ScaffoldContext) => string;
}
// ScaffoldContext = { kebab, camel, pascal, title, targetId }Declaring the same scaffold name on two targets is the point: questpie add block hero then writes a server block definition and an admin-client renderer in one command.
Custom generators
When a target needs more than the default templates, give it a generate(ctx) returning { code, additionalFiles? }. Transforms run before generate, so ctx.extraImports / extraTypeDeclarations / extraRuntimeCode / extraEntities are already populated; additionalFiles are written relative to the target outDir and each is syntax-validated. The admin module uses this for its admin-client target. Only one generate per target is allowed.
Cross-target validation
Register validators on the plugin (not on a target) to enforce consistency between targets after all have generated. A CrossTargetValidator receives the full result map and returns ProjectionError[]; a ProjectionError with severity: "error" fails codegen (exit 1). This is how the admin module catches a server file referencing a block / view / component the admin-client target never registered.
Programmatic API
You almost never need these, the CLI is the interface, but they're public for tooling:
runCodegen(options: CodegenOptions): Promise<CodegenResult>, single target. Resolves the graph (core plugin prepended), discovers, transforms, generatesindex.ts(root-app) ormodule.ts(module mode) +factories.ts, validates syntax, atomic-writes. Settingoptions.moduleswitches to module mode.runAllTargets(options): Promise<MultiTargetCodegenResult>, every resolved target, then all plugin validators. Per-target errors are collected (non-fatal);validationErrorswithseverity: "error"fail the CLI. Module mode is not supported here, userunCodegendirectly.generateModule(options: GenerateModuleOptions): Promise<CodegenResult>, convenience wrapper for npm packages that emit.generated/module.tsat build time.
Packages: the static-module pattern
A package that ships file conventions (its own collections, routes, etc.) uses packageConfig() in its dev-only questpie.config.ts. questpie generate then scans modulesDir/*, derives a module name ${modulePrefix}-${dirName}, and runs codegen in module mode per subdir, emitting a .generated/module.ts static module definition for each. Only the generated module.ts ships with the package; the dev config does not.
import { packageConfig } from "questpie/cli";
export default packageConfig({
modulesDir: "src/server/modules",
modulePrefix: "questpie", // module "admin" → name "questpie-admin"
});QUESTPIE's own core uses exactly this (modulesDir: "src/server/modules", modulePrefix: "questpie"). importRewriteMap (on the module options) rewrites self-package specifiers so generated imports resolve to source rather than stale dist/ types. packageConfig and config are exported from questpie/cli.
Importing `questpie/cli` must not start a command
The CLI is side-effect-free on import, command parsing is guarded by
import.meta.main. Importing questpie/cli just to grab packageConfig will
not kick off a command run, which matters in a workspace where a
src-vs-dist double-instance could otherwise corrupt .generated/.
TypeScript
The plugin-author types all come from questpie/codegen:
import type {
CodegenPlugin,
CodegenTargetContribution,
CategoryDeclaration,
DiscoverPattern,
RegistryExtension,
SingletonFactory,
BuilderFactory,
ScaffoldConfig,
ScaffoldContext,
CodegenContext,
CrossTargetValidator,
ProjectionError,
} from "questpie/codegen";
import {
categoryRecordEntry,
importStatement,
safeKey,
} from "questpie/codegen";App-level generated types, CollectionDoc<K>, CollectionWhere<K>, GlobalDoc<K>, App, AppConfig, come from #questpie (above). The codegen types are exposed only via questpie/codegen; questpie/types does not re-export them.
Related
- Configuration, the convention files codegen discovers (
questpie.config.ts,config/app.ts,config/auth.ts,modules.ts) and howruntimeConfigregistersplugins. - Environment,
env.ts/env.client.ts, also discovered by codegen, and boot-time validation. - Collections, the export/discovery rules in practice and the
#questpie/factoriesbuilder. - Blocks, a category contributed by the admin module's codegen plugin (the
block()builder factory). - Get started, where
questpie generatefirst appears in the workflow.
Building a plugin
A plugin is the unit of extension in QUESTPIE, it teaches codegen new file conventions, wraps the collection/global/field builders with typed methods, and ships everything as one npm package. The framework's own admin is just a plugin, and yours reaches the exact same seams.
Multi-tenancy
Serve many tenants, organizations, properties, cities, from one app by deriving the active scope from the request, isolating data with access rules and scoped globals, and letting admins switch tenants from the sidebar.