Generator
- Category: Engine
The generator scans your source for points, then writes a small set of
files the app imports at boot: a server points aggregator, a client one, a typed
route table, a metadata file, and ambient asset typings. You don't hand-write
any of them — you point each output at a path in the engine config, and
point0 generate fills it in.
// engine.ts — every generated file is opt-in
export const engine = Engine.create({
file: import.meta.url,
pointsGlob: '**/*.{ts,tsx,mdx}',
generate: {
meta: './generated/point0/meta.ts',
assetsTypes: './generated/point0/assets.d.ts',
},
server: {
points: async () => await import('./generated/point0/points.server'),
generate: { points: './generated/point0/points.server.ts' },
},
client: {
points: async () => await import('./generated/point0/points.client'),
generate: {
points: './generated/point0/points.client.ts',
routes: {
outfile: './generated/point0/routes.ts',
origin: 'process.env.CLIENT_URL',
},
},
},
})Run it with the CLI:
point0 generate # one-shot: scan points, write the files
point0 generate -w # ...and keep watching for changesdev and build run it for you — point0 dev generates once at startup and
watches; point0 build always generates first. The rest of this page covers
each generated file, when it reruns, and the config that controls it.
The generated files
The generator writes a separate file per output, and only the outputs you name
in generate — there are no implicit defaults. The example apps put them all
under src/generated/point0/, but the path is yours.
points.server.ts — server points
The server's aggregator: a flat array of every server-side point, static-imported so it all lands in the server bundle.
// generated points.server.ts
import type { PointsDefinition } from '@point0/core'
import { root as root_0, page as page_1, layout as layout_2 } from './file0.js'
export default [root_0, page_1, layout_2] as PointsDefinition<
(typeof root_0)['Infer']['RequiredCtx'],
(typeof root_0)['Infer']['Error']
>A point lands here when it's the root of the scope or has an HTTP
endpoint — a query, mutation, or action, or any
other point (page, layout, component,
provider) that has a .loader (and pages also come
whenever ssr: true). The app feeds it back to the engine:
// engine.ts → server
points: async () => await import('./generated/point0/points.server'),
generate: { points: './generated/point0/points.server.ts' },SSR changes the contents. With ssr: true every page comes to the server
(it gets server-rendered). With ssr: false only pages that have a .loader
come — they have an endpoint to fetch; loader-less pages are client-only and
stay out.
points.client.ts — client points
The client's aggregator: pages, layouts, and the root for the SPA. By default each point is a lazy dynamic import, so it becomes its own chunk loaded on navigation:
// generated points.client.ts (lazy — the default)
import type { PointsDefinition } from '@point0/core'
import { root as root_0 } from './file0.js'
export default [
root_0,
{
type: 'page',
name: 'mypage',
route: '/news',
polh: false, // prefetch-on-link-hover (from .prefetchPageOnLinkHover)
point: async () => (await import('./file0.js')).page,
},
{
type: 'layout',
name: 'mylayout',
route: '/layout',
point: async () => (await import('./file0.js')).layout,
},
] as PointsDefinition<
(typeof root_0)['Infer']['RequiredCtx'],
(typeof root_0)['Infer']['Error']
>Each page record carries its route, its polh flag (the hover-prefetch
setting, boolean or a debounce in ms), and any layouts it sits under. The
root is always imported statically — it has to be present from the first render.
Set lazy: false and the client file becomes the same static-import shape as
the server one — every point in a single bundle, no per-page chunks:
// engine.ts → client
generate: { points: { outfile: './generated/point0/points.client.ts', lazy: false } },There is no per-page .lazy() method; lazy-vs-ready is a whole-file switch in
the codegen config. See the page authoring notes for the trade-off.
routes.ts — the typed route table
Every page route, collected into a route0 table you import for
<Link>s and navigate:
// generated routes.ts
import { Routes } from '@1gr14/route0'
export const routes = Routes.create({
mypage: '/news',
})import { routes } from '@/generated/point0/routes'Only pages with a route are included. A page's .basePath
prefix is already folded into its route string. Set an origin and it rides
along on the table — a raw expression when it starts with process.env. /
import.meta.env. / a backtick, otherwise a quoted string:
generate: { routes: { outfile: './generated/point0/routes.ts', origin: 'process.env.CLIENT_URL' } }
// => Routes.create({ mypage: '/news' }, { origin: process.env.CLIENT_URL })Route keys are quoted only when they aren't a valid JS identifier (mypage:
bare, 'my-page': quoted) — matching Prettier's quoteProps: 'as-needed', so
diffs stay clean.
NOTE — routes are emitted as bare path strings, untyped. Typed-search routes are intentionally disabled to avoid a
routes.ts → page → Link → routes.tstype cycle, so a page that declares.search()still emits a plain string here.
meta.ts — point metadata for tooling
A full description of every point — including invalid ones — for tools that need to reason about your app without importing it. The project MCP reads it.
// generated meta.ts (engine block)
export default {
engine: {
file: '<file>',
import: async () =>
(await Engine.findAndImportSelf({ engineFile: '<file>' })).engine,
server: { scope: 'myroot' },
clients: [{ scope: 'myroot' }],
},
points: [
{
scope: 'myroot',
type: 'page',
name: 'mypage',
id: 'myroot:page:mypage',
tags: ['ideas'],
description: `...`,
route: undefined, // or Route0.create(...) when the point has a route
endpoint: undefined, // or { method, route } for query/mutation/action
pos: { file: '<file>', line: 5, column: 20 }, // source position
import: async () => (await import('./file0.js')).page,
valid: true,
errors: [],
ssr: false,
parents: [],
layouts: [],
},
// ...one entry per point, invalid and plugin points included
],
}Each entry carries the point's scope, type, name, id, tags, description, route,
endpoint, source position, a lazy import, validity, and its linked parents and
layouts. The project MCP bin consumes it:
point0-project-mcp --meta ./src/generated/point0/meta.tsThe MCP re-reads the file on every call, so it never serves stale points after a
point0 generate.
assets.d.ts — ambient asset typings
Ambient declarations that type imported static assets, so
import logo from './logo.png' and its ?url / ?file / ?text / ?raw /
?react forms type correctly:
generate: {
assetsTypes: './generated/point0/assets.d.ts'
}// generated assets.d.ts (excerpt)
declare module '*.png' {
const src: string
export default src
}
declare module '*.png?url' {
const src: string
export default src
}
declare module '*.svg?react' {
import type { FC, SVGProps } from 'react'
const ReactComponent: FC<SVGProps<SVGSVGElement>>
export default ReactComponent
}The extension list and the bare-import type both default from the general
compiler.assets config (defaultMode: 'url' out of the box) — one
source of truth — and you can override them per output:
generate: { assetsTypes: { outfile: './generated/point0/assets.d.ts', extensions: ['png', 'svg', 'pdf'] } }Reference the file from your tsconfig types or include, or with a
/// <reference path="..." />. The asset pipeline as a whole is on
Assets.
When it regenerates
point0 generate # one-shot
point0 generate -w # watch and regenerate on changepoint0 devgenerates once at startup, then runs a watcher in parallel.-G/--no-generateskips generation;-W/--no-watchdisables watching.point0 buildalways generates first — there is no "build without generate" (skipping it would leave a stale aggregator on disk).- The watcher follows
pointsGlob. Add, edit, or delete a point file and it logsadd: page.mypage/remove: page.mypageand rewrites only the affected files.
Two things keep the output stable:
- Idempotent writes. A file is rewritten only when its content actually
changes, and points are sorted deterministically — so unrelated edits produce
no diff. Writes are atomic (temp file under
node_modules/.cache, then renamed). - Safe on errors. A point with parse or collection errors is logged but doesn't break generation; the previous good point set is kept, so a broken edit never blows away your aggregators.
Hot reload does not need a regenerate — the dev server resolves points from the engine's source, not from the generated file. Generation matters when a point is added or removed, which is exactly what the watcher catches.
NOTE: the generator's watcher is
pointsGlob, separate from the server'sdevWatchGlob(restart trigger) andbuildWatchGlob. Vite/expo excludemeta.tsfromdevWatchGlobbecause meta changes on every point edit and would otherwise restart the server needlessly.
Gitignored output
Generated code is gitignored — the example apps ignore src/generated
wholesale. So a fresh checkout, worktree, or CI run must generate before
typechecking, or imports like ./generated/point0/points.client won't resolve.
That's what bun run setup does at the repo level (it runs point0 generate
per app); per-app, the generate script is wired to point0 generate.
examples/expo is the exception: it has no client generate and commits its
points.server.ts, ignoring only meta.ts and the Prisma client.
NOTE:
setupis not a CLI command.bun run setupis repo/apppackage.jsonorchestration (Prisma generate +point0 generate); the only generator CLI verb ispoint0 generate.
Reference
CLI
| Command | Flag | Effect |
|---|---|---|
point0 generate | — | scan points, write the configured files once |
point0 generate | -w, --watch | regenerate on every change |
point0 generate | --engine <path> | engine file (absolute or relative to cwd) |
point0 dev | -G, --no-generate | skip generation in dev |
point0 dev | -W, --no-watch | don't restart/regenerate on change |
point0 build always generates; it has no skip flag.
Where each output is configured
generate lives in three places, each carrying different outputs:
| Output | Config location | Key |
|---|---|---|
points.server.ts | server.generate | points |
points.client.ts | client.generate | points (lazy?) |
routes.ts | client.generate | routes (origin?) |
meta.ts | generate (general) | meta |
assets.d.ts | generate (general) | assetsTypes (extensions?) |
Each value is either a string (the outfile path) or an object with outfile
plus extra keys. Omit generate and nothing is generated — it defaults to
[].
Per-output options
| Option | Type | Default | Effect |
|---|---|---|---|
points.outfile (server/client) | string | — | aggregator path |
points.banner | string | null | none | text prepended to the file |
points.lazy (client) | boolean | true | lazy per-page chunks vs one static bundle |
routes.outfile | string | — | route table path |
routes.origin | string | null | none | route origin; raw if process.env. / import.meta.env. / backtick prefix |
routes.banner | string | null | none | prepended text |
meta.outfile | string | — | metadata path |
meta.banner | string | null | none | prepended text |
assetsTypes.outfile | string | — | .d.ts path |
assetsTypes.extensions | string[] | compiler.assets.extensions, else point0's built-in asset extension list | managed extensions |
assetsTypes.banner | string | null | none | prepended text |
The bare-import type (defaultMode — 'url' | 'file' | 'text' |
'react' | false, where false omits the bare declaration) is not a key
on the simple object config; in that form it always comes from
compiler.assets ('url' out of the box). To set it per output you
need the raw array form:
generate: [{ what: 'assetsTypes', outfile, defaultMode: 'file' }].
A page record needs a root in the scope; the points and routes
outputs throw Root point not found for scope <scope> without one.
Custom outputs
Beyond the named outputs, generate accepts custom tasks for generating your
own files from the point set:
{ what: 'customFile', handler, outfile }—handlerreturns the file content and the generator writes it tooutfileatomically, just like the built-ins.{ what: 'customControlled', handler }—handlerwrites its own files (nooutfile); use it when one task emits several files or a path you compute yourself.
Both handlers receive the same options object and run on every (re)generation:
type Handler = (options: {
points: CompilerPoint[] // the valid points, after collection
cwd: string // engine cwd — resolve your output paths against it
log: LogFn // the generator's logger
tempDir: string // scratch dir for atomic temp files
emitPointsImports: (options: {
points: CompilerPoint[]
// customControlled also passes the target `outfile` so imports resolve relative to it
}) => EmitNamedImportsResult // ready-to-write import lines for the given points
}) => /* customFile */ string | Promise<string>
// /* customControlled */ | void | Promise<void>customFile's handler returns a string (or Promise<string>) — the full
file content. customControlled's handler returns void (or Promise<void>)
and is responsible for writing whatever files it needs itself.
Each point is a CompilerPoint (exported from @point0/compiler). The fields
a handler reads to describe or import a point: id (scope:type:name),
scope, type, name, route (or undefined), endpoint
({ method, route } for any point reachable over HTTP — an SSR page always
(GET), plus any point with a .loader(): layouts (GET), queries / mutations
(POST), infinite-queries, and actions (their declared method); undefined
otherwise), tags, description, ssr, layouts, parents, valid (always
true here — points holds only valid points), pos
({ file, line, column }), and file (with file.abs, the absolute source
path).
// engine.ts → generate
generate: {
custom: [
{
what: 'customFile',
outfile: './generated/point0/point-ids.ts',
handler: ({ points }) =>
`export const pointIds = ${JSON.stringify(points.map((p) => p.id))}`,
},
],
},Supply them either as the full raw array form
(generate: [{ what: 'customFile', … }]) or via a custom array inside the
simple object config (generate: { custom: [...] }, and likewise under
server.generate / client.generate).
Importing points with emitPointsImports
To emit a file that re-exports or wires up the points, call emitPointsImports
instead of writing import lines by hand — it resolves each point's source path
relative to your outfile and gives every point a unique local binding. It
returns an EmitNamedImportsResult:
| Field | Type | What it is |
|---|---|---|
importLines | string[] | the import { x as x_0 } from './file.js' statements, one per source file |
importedPoints | Array<{ point, index, renamedExportName }> | each imported point with its unique local binding (renamedExportName) and its position |
rootSingleImportLine | string | null | a standalone import line for just the scope root, or null if no root was in points |
hasNotRootPoints | boolean | whether points held anything beyond the root |
Push importLines into the file, then reference each point through its
renamedExportName:
// engine.ts → generate
generate: {
custom: [
{
what: 'customFile',
outfile: './generated/point0/my-points.ts',
handler: ({ points, emitPointsImports }) => {
const { importLines, importedPoints } = emitPointsImports({ points })
const lines = [...importLines]
for (const { point, renamedExportName } of importedPoints) {
lines.push(`export const ${point.name}_${point.type} = ${renamedExportName}`)
}
return lines.join('\n') + '\n'
},
},
],
},Under customControlled the handler also passes the target outfile to
emitPointsImports so import paths resolve relative to the file you write.
Enjoying Point0?
Star Point0
Start0
YouTube
Discord
Telegram
of the Lord Jesus Christ ☦️
With love for developers
of all backgrounds around the world ❤️