@spfn/core 0.3.0-beta.3 → 0.3.0-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +301 -10
- package/dist/app-config/index.d.ts +39 -1
- package/dist/app-config/index.js.map +1 -1
- package/dist/authz/index.js +47 -391
- package/dist/authz/index.js.map +1 -1
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/codegen/index.d.ts +157 -16
- package/dist/codegen/index.js +772 -166
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js +4 -4
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +39 -16
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- package/dist/db/index.d.ts +230 -35
- package/dist/db/index.js +316 -160
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/error-CIZlYJlL.d.ts +157 -0
- package/dist/errors/index.d.ts +18 -1
- package/dist/errors/index.js +47 -391
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +202 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +94 -18
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +18 -5
- package/dist/event/sse/index.js +452 -114
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +131 -52
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js +52 -397
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +75 -14
- package/dist/nextjs/index.js +200 -84
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +46 -8
- package/dist/nextjs/server.js +15 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +61 -42
- package/dist/ops/index.js +648 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +5 -2
- package/dist/route/index.js +107 -28
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{router-Cy7rAfmj.d.ts → route-builder-DRW-MSCJ.d.ts} +51 -126
- package/dist/router-C57y-vz1.d.ts +184 -0
- package/dist/router-CX5uVjmU.d.ts +406 -0
- package/dist/server/index.d.ts +130 -7
- package/dist/server/index.js +1174 -164
- package/dist/server/index.js.map +1 -1
- package/dist/state/client.d.ts +206 -0
- package/dist/state/client.js +678 -0
- package/dist/state/client.js.map +1 -0
- package/dist/state/index.d.ts +443 -0
- package/dist/state/index.js +644 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/react.d.ts +49 -0
- package/dist/state/react.js +50 -0
- package/dist/state/react.js.map +1 -0
- package/dist/surface/index.d.ts +115 -0
- package/dist/surface/index.js +1013 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-CfYVhIQ9.d.ts → types-5Rl11_Nb.d.ts} +43 -4
- package/dist/types-CKH0D3Dy.d.ts +441 -0
- package/dist/{types-ZQODsBft.d.ts → types-CurMab2Q.d.ts} +52 -2
- package/dist/{types-2AbaW4Ie.d.ts → types-DncqXfWN.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +39 -9
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +721 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +442 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +845 -0
- package/src/env/README.md +787 -0
- package/src/errors/README.md +445 -0
- package/src/event/README.md +994 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +756 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- package/src/state/README.md +806 -0
- package/dist/token-manager-BT5EnUAR.d.ts +0 -278
- package/dist/types-9oszaJqp.d.ts +0 -250
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
# @spfn/core/codegen — Code generation (route-map & custom generators)
|
|
2
|
+
|
|
3
|
+
Pluggable code-generation system with a single shared file watcher. The orchestrator
|
|
4
|
+
runs a list of generators once (build / manual) or continuously (watch). The one built-in
|
|
5
|
+
generator is `@spfn/core:route-map`, which produces the `routeName → {method, path}` map
|
|
6
|
+
the RPC proxy needs.
|
|
7
|
+
|
|
8
|
+
## Import paths
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// Single entry point — everything is exported from here:
|
|
12
|
+
import {
|
|
13
|
+
defineConfig, defineGenerator,
|
|
14
|
+
loadCodegenConfig, createGeneratorsFromConfig,
|
|
15
|
+
CodegenOrchestrator,
|
|
16
|
+
} from '@spfn/core/codegen';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
CodegenConfig, GeneratorConfig,
|
|
20
|
+
Generator, GeneratorOptions, GeneratorTrigger,
|
|
21
|
+
OrchestratorOptions,
|
|
22
|
+
RouteMapGeneratorConfig,
|
|
23
|
+
} from '@spfn/core/codegen';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
There is no `@spfn/core/codegen/loader` or other sub-path — everything ships from
|
|
27
|
+
`@spfn/core/codegen`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Public API (complete)
|
|
32
|
+
|
|
33
|
+
Functions:
|
|
34
|
+
|
|
35
|
+
- `defineConfig(config: CodegenConfig): CodegenConfig` — identity helper for `.spfnrc.ts`.
|
|
36
|
+
- `defineGenerator<T>(config: T): T` — identity helper that carries a generator config's
|
|
37
|
+
type through to the array (so `name`/options are type-checked).
|
|
38
|
+
- `loadCodegenConfig(cwd: string): CodegenConfig` — reads `.spfnrc.ts` → `.spfnrc.json` →
|
|
39
|
+
`package.json` (first hit wins). Returns `{ generators: [] }` if none found.
|
|
40
|
+
- `createGeneratorsFromConfig(config, cwd): Promise<Generator[]>` — resolves each config
|
|
41
|
+
entry into a live `Generator` (loads packages / `.ts` files via jiti).
|
|
42
|
+
|
|
43
|
+
Class:
|
|
44
|
+
|
|
45
|
+
- `CodegenOrchestrator` — `new CodegenOrchestrator(options)`, then `generateAll(trigger?)`,
|
|
46
|
+
`watch()`, `close()`.
|
|
47
|
+
|
|
48
|
+
Types:
|
|
49
|
+
|
|
50
|
+
- `CodegenConfig`, `GeneratorConfig`
|
|
51
|
+
- `Generator`, `GeneratorOptions`, `GeneratorTrigger`
|
|
52
|
+
- `OrchestratorOptions`
|
|
53
|
+
- `RouteMapGeneratorConfig` (config shape for the built-in route-map generator)
|
|
54
|
+
- `ContractGeneratorConfig` (config shape for the built-in contract generator)
|
|
55
|
+
- `StatesGeneratorConfig` (config shape for the built-in states generator)
|
|
56
|
+
- `ContractGeneratorError`, `StatesGeneratorError`, `ConditionalRegistrationError`,
|
|
57
|
+
`assertUnconditionalRegistration`
|
|
58
|
+
- `RouteContractMapping`, `ResourceRoutes`, `ClientGenerationOptions`, `GenerationStats`
|
|
59
|
+
(legacy client-generation types — exported but not used by any shipped generator)
|
|
60
|
+
|
|
61
|
+
> **Renamed: `defineCodegenConfig` → `defineConfig`.** The old name does **not** exist.
|
|
62
|
+
> Older docs also show an `api-client` / `createApi`-emitting generator and
|
|
63
|
+
> `codegen.config.ts` — those are **removed**. The built-in generators today are
|
|
64
|
+
> `@spfn/core:route-map`, `@spfn/core:contract` and `@spfn/core:states`, configured in `.spfnrc.ts`. Do not
|
|
65
|
+
> import `defineCodegenConfig` or configure a `name: 'api-client'` generator.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
### 1. Configure `.spfnrc.ts`
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
75
|
+
|
|
76
|
+
export default defineConfig({
|
|
77
|
+
generators: [
|
|
78
|
+
defineGenerator({
|
|
79
|
+
name: '@spfn/core:route-map',
|
|
80
|
+
routerPath: './src/server/router.ts',
|
|
81
|
+
outputPath: './src/generated/route-map.ts', // optional (this is the default)
|
|
82
|
+
}),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. Generate
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
spfn codegen run # run all generators once
|
|
91
|
+
spfn dev # watch mode (regenerates on file change)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This writes `src/generated/route-map.ts`, which the RPC proxy imports.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Configuration resolution
|
|
99
|
+
|
|
100
|
+
`loadCodegenConfig(cwd)` checks these in order and returns the **first** that exists:
|
|
101
|
+
|
|
102
|
+
1. `.spfnrc.ts` — loaded with jiti (supports `defineConfig`/`defineGenerator` + TS types).
|
|
103
|
+
The default export (or the module itself) is the `CodegenConfig`.
|
|
104
|
+
2. `.spfnrc.json` — the config is read from the top-level **`codegen`** key:
|
|
105
|
+
`{ "codegen": { "generators": [...] } }`.
|
|
106
|
+
3. `package.json` — read from **`spfn.codegen`**: `{ "spfn": { "codegen": { ... } } }`.
|
|
107
|
+
|
|
108
|
+
If none exist (or parsing fails), you get `{ generators: [] }` and nothing runs.
|
|
109
|
+
|
|
110
|
+
> Precedence is "first file found wins", not a deep merge. A `.spfnrc.ts` fully shadows
|
|
111
|
+
> `.spfnrc.json` and `package.json`.
|
|
112
|
+
|
|
113
|
+
### Generator config entries (`GeneratorConfig`)
|
|
114
|
+
|
|
115
|
+
A `generators[]` entry is one of three shapes:
|
|
116
|
+
|
|
117
|
+
| Shape | Example | Meaning |
|
|
118
|
+
|-------|---------|---------|
|
|
119
|
+
| Package generator | `{ name: 'pkg:gen', enabled?: true, ...opts }` | Loaded from `${pkg}/codegen`. `name` **must** contain `:`. `enabled: false` skips it. Extra keys are passed to the factory. |
|
|
120
|
+
| File generator | `{ path: './src/generators/x.ts' }` | A `.ts`/`.js` file whose default export is a `() => Generator` factory. `.ts` loaded via jiti. |
|
|
121
|
+
| Pre-built instance | `defineGenerator({...})` result that already has a `generate` fn | Pushed as-is (guards against accidentally calling a factory yourself). |
|
|
122
|
+
|
|
123
|
+
For package generators, `name` is split on `:` into `package:generatorName`. The loader
|
|
124
|
+
imports `${package}/codegen` and looks for `generators[generatorName]`, falling back to a
|
|
125
|
+
`create<Name>Generator` export. So `@spfn/core:route-map` → import `@spfn/core/codegen`,
|
|
126
|
+
call `generators['route-map'](config)`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Built-in: `@spfn/core:route-map`
|
|
131
|
+
|
|
132
|
+
Loads your router and emits a `routeName → {method, path}` map. This map is
|
|
133
|
+
what the **RPC proxy** (`createRpcProxy` in `@spfn/core/nextjs/server`) uses to turn
|
|
134
|
+
`api.getUser.call(...)` into an HTTP request — it needs `method` and `path` without
|
|
135
|
+
importing server code into the client bundle.
|
|
136
|
+
|
|
137
|
+
### `RouteMapGeneratorConfig`
|
|
138
|
+
|
|
139
|
+
| Field | Type | Required | Default | Description |
|
|
140
|
+
|-------|------|----------|---------|-------------|
|
|
141
|
+
| `name` | `'@spfn/core:route-map'` | yes | — | Generator identifier (literal). |
|
|
142
|
+
| `routerPath` | `string` | yes | — | Router file, relative to project root. Throws at construction if missing. |
|
|
143
|
+
| `outputPath` | `string` | no | `'./src/generated/route-map.ts'` | Where the map is written (parent dirs auto-created). |
|
|
144
|
+
| `additionalRouteDirs` | `string[]` | no | `[]` | **Deprecated and ignored** for collection; still added to `watchPatterns` as `${dir}/**/*.ts`. |
|
|
145
|
+
|
|
146
|
+
### What it reads
|
|
147
|
+
|
|
148
|
+
The router module is loaded with jiti (`appRouter` → `default` → `router`) and walked the
|
|
149
|
+
way `registerRoutes` walks it. How the router is *written* does not matter: a one-line
|
|
150
|
+
`defineRouter({ createUser })`, an aliased key `create: createUser`, and a second
|
|
151
|
+
`defineRouter(` in the same file all produce the same map.
|
|
152
|
+
|
|
153
|
+
- A **nested router** recurses to any depth and its routes land in the map **flat, under
|
|
154
|
+
their own keys** — the parent's key names the grouping, not the route, exactly as the
|
|
155
|
+
server registers them.
|
|
156
|
+
- `method` and `path` come from the `RouteDef`, not from a pattern over the source. A
|
|
157
|
+
route not registered in the router is absent, because there is nothing registering it.
|
|
158
|
+
- **`.packages()` routers are carried**, at every depth, provided the router publishes a
|
|
159
|
+
route map (`defineRouter`, not `defineUnmappedRouter`). A package ships a client that
|
|
160
|
+
names its own routes — `authApi.login`, `monitorApi.getStats` — and the app's
|
|
161
|
+
`createRpcProxy` resolves a name in the one map it was constructed with, so the app
|
|
162
|
+
merges nothing by hand: `{ ...routeMap, ...authRouteMap }` is a no-op today. They are
|
|
163
|
+
written after the app's own routes, under a comment line naming where they came from,
|
|
164
|
+
and the header says how many there are and which mount each came through.
|
|
165
|
+
- A name that is not a JS identifier (`'get-user'`) is emitted quoted, and `__proto__` is
|
|
166
|
+
emitted as a computed key, so the generated file parses and the map keeps every route as
|
|
167
|
+
an own property.
|
|
168
|
+
- Imports are resolved by Node **and by the `compilerOptions.paths` of the nearest
|
|
169
|
+
`tsconfig.json` that declares them**, searched from the router file's own directory up to
|
|
170
|
+
the project root, `extends` chains included — so the `@/*` alias every spfn app has
|
|
171
|
+
resolves while the router loads, whether it lives in the root config (a Next.js app) or in
|
|
172
|
+
`src/server/tsconfig.json` (what `spfn init` scaffolds, and the config `spfn build`
|
|
173
|
+
compiles the server with). The search stops at the project root: a tsconfig above it
|
|
174
|
+
belongs to whatever contains the project. A config that declares no `paths` is passed over
|
|
175
|
+
rather than ending the search. `baseUrl` is honoured, so the scaffold's `"baseUrl": "../.."`
|
|
176
|
+
resolves against the project root as tsc resolves it. The failure names the specifier that
|
|
177
|
+
did not resolve.
|
|
178
|
+
- `NODE_ENV` is pinned to `production` when the shell left it unset, so the same router
|
|
179
|
+
does not load two ways depending on how codegen was invoked. A value the shell *did* set
|
|
180
|
+
is left alone.
|
|
181
|
+
|
|
182
|
+
### When it refuses
|
|
183
|
+
|
|
184
|
+
Unlike the old source parser, which dropped what it could not read, the generator throws
|
|
185
|
+
— logged always, and fatal on the `build` trigger and on `spfn codegen run`:
|
|
186
|
+
|
|
187
|
+
- The module will not load. An import that did not resolve names the specifier; anything
|
|
188
|
+
else names the cause and says a route module must be importable without side effects. It
|
|
189
|
+
never falls back to parsing.
|
|
190
|
+
- No router is found under `appRouter`, `default` or `router` (the message lists all three).
|
|
191
|
+
- The router file registers a route **conditionally** (`...(flag ? { admin } : {})`), which
|
|
192
|
+
would put the flag's value into the map and leave `api.admin.call()` 404ing wherever the
|
|
193
|
+
flag differs. This is the same guard `@spfn/core:contract` uses, and it reads **the router
|
|
194
|
+
the generator was pointed at** — the export the loader found it under, plus the routers
|
|
195
|
+
that export mounts by name from the same file, since a nested router's routes land in the
|
|
196
|
+
same flat map. A second `defineRouter(` the app router never reaches is not scanned: it
|
|
197
|
+
registers nothing in production. The source is read with TypeScript's parser, so a JSDoc
|
|
198
|
+
spelling `defineRouter({ ... })` in prose, a comment holding an old router and a brace
|
|
199
|
+
inside a string are not router source. Only a spread whose expression **reads a
|
|
200
|
+
condition** is refused — a ternary, `&&`, `||`, `??`. A call is allowed
|
|
201
|
+
(`...metadataRoutes(config, resource)`): loading yields exactly the routes it returns.
|
|
202
|
+
Past the guard, by construction: a conditional spread inside an imported module, a router
|
|
203
|
+
built by a factory, a condition hoisted to a variable before the spread, and a
|
|
204
|
+
`.packages()` list assembled conditionally. `NODE_ENV` pinning stands in for the guard on
|
|
205
|
+
those, but only when the shell left it unset: `spfn build` pins `production`, so what ships
|
|
206
|
+
is right, while `spfn dev` sets `development` and the watcher regenerates under it. The
|
|
207
|
+
generated header names the `NODE_ENV` the map was written under, so that difference is a
|
|
208
|
+
line in the diff rather than a route that vanishes at build.
|
|
209
|
+
- Two routes reach the same name (the message names both places).
|
|
210
|
+
- An **app route and a package route** reach the same name, where that package publishes a
|
|
211
|
+
route map. Both register at runtime, and both want the single entry that name has in this
|
|
212
|
+
map — so one of the two paths becomes unreachable while the generated types still
|
|
213
|
+
describe the app's route. **Two package routers** reaching one name is refused too, and
|
|
214
|
+
names both mounts: the generator writes the order now, so the ambiguity is its to refuse.
|
|
215
|
+
The same route reached through two mounts is not two routes — a package exporting one set
|
|
216
|
+
of routes under a router and a sub-router an app mounts beside it hands both mounts the
|
|
217
|
+
same route object, and it is collected once. No first-party package ships that shape today;
|
|
218
|
+
the rule is keyed on object identity so that an app mounting both still builds when one
|
|
219
|
+
does, and the header line of the second mount says which mount got there first.
|
|
220
|
+
A collision with a router that publishes **no** map is not refused — one declared with
|
|
221
|
+
`defineUnmappedRouter`, whose routes are reached by the URL something was handed rather
|
|
222
|
+
than by name: the ops surface (`createOpsRouter`) and the tracking routes of
|
|
223
|
+
`@spfn/notification`. Nothing of theirs is written here, so an app route named after one
|
|
224
|
+
of their routes keeps its own path and must not fail the build.
|
|
225
|
+
- A route's `method` is not an `HttpMethod`. `registerRoutes` lowercases the method before
|
|
226
|
+
registering, so `method: 'get'` serves fine and would emit a map that fails `tsc`.
|
|
227
|
+
|
|
228
|
+
A **missing router file** is still only a warning: the generator returns and writes nothing.
|
|
229
|
+
|
|
230
|
+
**Warned about and left out, not refused**: a route with no method or path (reachable by
|
|
231
|
+
registering a route before `.handler()` was called), and a router entry that is neither a
|
|
232
|
+
route nor a router. `registerRoutes` warns and skips both, so a map without them still names
|
|
233
|
+
every route the server answers — which is all a refusal would have been protecting.
|
|
234
|
+
|
|
235
|
+
A package route with no method or path is skipped the same way, and for the same reason.
|
|
236
|
+
What is refused is only what would write a file that does not compile or that answers to
|
|
237
|
+
the wrong path: a method that is not an `HttpMethod`, and the two collisions above.
|
|
238
|
+
|
|
239
|
+
### Generated output
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// src/generated/route-map.ts — DO NOT EDIT
|
|
243
|
+
import type { HttpMethod } from '@spfn/core/route';
|
|
244
|
+
|
|
245
|
+
export interface RouteInfo
|
|
246
|
+
{
|
|
247
|
+
method: HttpMethod;
|
|
248
|
+
path: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const routeMap: Record<string, RouteInfo> = {
|
|
252
|
+
getUser: { method: 'GET', path: '/users/:id' },
|
|
253
|
+
createUser: { method: 'POST', path: '/users' },
|
|
254
|
+
// From the package routers this app mounts with .packages(), not its own:
|
|
255
|
+
login: { method: 'POST', path: '/_auth/login' },
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export type RouteMap = typeof routeMap;
|
|
259
|
+
export type RouteName = keyof RouteMap;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`watchPatterns` are `[routerPath, 'src/server/routes/**/*.ts', ...additionalRouteDirs]` and
|
|
263
|
+
`runOn` is `['watch', 'manual', 'build']` — every trigger the CLI fires, so it runs in every mode.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Built-in: `@spfn/core:contract`
|
|
268
|
+
|
|
269
|
+
Writes `contracts/current.json` — every route carrying `.contract()`, and with `eventRouterPath`
|
|
270
|
+
every SSE event carrying `.contract()` — and on a **build** compares
|
|
271
|
+
it against the newest released snapshot, refusing changes that would break a client already in the
|
|
272
|
+
field. Full behaviour lives in [`../contract/README.md`](../contract/README.md); this section is the
|
|
273
|
+
generator's own surface.
|
|
274
|
+
|
|
275
|
+
### `ContractGeneratorConfig`
|
|
276
|
+
|
|
277
|
+
| Field | Type | Required | Default | Description |
|
|
278
|
+
|-------|------|----------|---------|-------------|
|
|
279
|
+
| `name` | `'@spfn/core:contract'` | yes | — | Generator identifier (literal). |
|
|
280
|
+
| `routerPath` | `string` | yes | — | Router file, relative to project root. Throws at construction if missing. |
|
|
281
|
+
| `routerExport` | `string` | no | `appRouter` → `default` → `router` | Export holding the `defineRouter()` result. |
|
|
282
|
+
| `eventRouterPath` | `string` | no | — | Event router file, relative to project root. Loaded like the router (same `NODE_ENV` pin, same tsconfig aliases) and watched; its contracted events become the document's `events` section. Unset → no section. |
|
|
283
|
+
| `eventRouterExport` | `string` | no | `eventRouter` → `default` | Export holding the `defineEventRouter()` result. |
|
|
284
|
+
| `outputDir` | `string` | no | `'./contracts'` | Holds `current.json`, `released/`, `usage/`. |
|
|
285
|
+
| `additionalRouteDirs` | `string[]` | no | `[]` | Extra route dirs to watch. |
|
|
286
|
+
|
|
287
|
+
### How it differs from route-map
|
|
288
|
+
|
|
289
|
+
| | `route-map` | `contract` |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| Reads the router by | loading the module and walking `RouteDef`s | the same |
|
|
292
|
+
| Covers | every registered route | only routes carrying `.contract()` |
|
|
293
|
+
| `runOn` | `watch`, `manual`, `build` | `watch`, `build`, `manual` |
|
|
294
|
+
| Can fail a build | **yes** — any refusal, on every trigger the CLI fails on | **yes**; a broken contract only on the `build` trigger |
|
|
295
|
+
|
|
296
|
+
Loading rather than parsing is what makes the contract correct: real routes build schemas from
|
|
297
|
+
imported values (`EmailSchema`, `FileSchema()`, constants) that a source parser cannot resolve. It
|
|
298
|
+
costs a module import and no infrastructure — `@spfn/auth`'s 43 routes load in ~0.6s with neither
|
|
299
|
+
`DATABASE_URL` nor `CACHE_URL` set.
|
|
300
|
+
|
|
301
|
+
### When it refuses
|
|
302
|
+
|
|
303
|
+
- The router file is missing, or the module will not load (it names the module and the cause —
|
|
304
|
+
it never skips quietly).
|
|
305
|
+
- No router export is found under the configured or default names.
|
|
306
|
+
- `eventRouterPath` is set and the file is missing, or holds no event router (an object with
|
|
307
|
+
`events` and `eventNames`) under the configured or default names — the message names the
|
|
308
|
+
candidates it tried.
|
|
309
|
+
- The event router contracts events without a router-level `.contract()`, or a contracted event
|
|
310
|
+
has no payload schema, no `since`, or the reserved key `connected` / `ping`.
|
|
311
|
+
- Two contracted routes share a name, or a contracted route has no method, path, `since` or
|
|
312
|
+
`response`.
|
|
313
|
+
- The router the generator was pointed at contains a conditional spread
|
|
314
|
+
(`...(flag ? { route } : {})`), which would make the contract describe whichever way the
|
|
315
|
+
generator happened to run. Same guard as `route-map` — see its bullet for exactly what is
|
|
316
|
+
scanned and what is allowed. The event router file gets the same scan for
|
|
317
|
+
`defineEventRouter({ ...(flag ? { event } : {}) })`.
|
|
318
|
+
- On the `build` trigger only: the contract breaks the newest released snapshot.
|
|
319
|
+
|
|
320
|
+
`spfn dev` generates but never refuses — being unable to hold a half-finished route mid-edit would
|
|
321
|
+
make the feature unusable.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Built-in: `@spfn/core:states`
|
|
326
|
+
|
|
327
|
+
Loads your router and emits the ids and the manifest of every state it mounts —
|
|
328
|
+
`src/generated/states.ts`, a file with no import that a browser root and a server component
|
|
329
|
+
can both execute. `createStateClient<typeof states>` is a type import, erased at build, and
|
|
330
|
+
importing the router itself is not an option (each state's `read()` closes over repositories
|
|
331
|
+
and the database), so this file is the only runtime record of which states the app declared.
|
|
332
|
+
Compare its `stateManifest` with the `manifest` of `GET /state`: an id the server's manifest
|
|
333
|
+
lacks is a deploy skew between the Next.js server and the API, reported at the root instead
|
|
334
|
+
of surfacing as `state.<id>` being `undefined` inside a screen. See
|
|
335
|
+
[`../state/README.md`](../state/README.md) → "Declared ids at runtime".
|
|
336
|
+
|
|
337
|
+
### `StatesGeneratorConfig`
|
|
338
|
+
|
|
339
|
+
| Field | Type | Required | Default | Description |
|
|
340
|
+
|-------|------|----------|---------|-------------|
|
|
341
|
+
| `name` | `'@spfn/core:states'` | yes | — | Generator identifier (literal). |
|
|
342
|
+
| `routerPath` | `string` | yes | — | Router file, relative to project root — the one `route-map` reads. Throws at construction if missing. |
|
|
343
|
+
| `outputPath` | `string` | no | `'./src/generated/states.ts'` | Where the file is written (parent dirs auto-created). |
|
|
344
|
+
|
|
345
|
+
### What it reads
|
|
346
|
+
|
|
347
|
+
The router module is loaded exactly as `route-map` loads it (`appRouter` → `default` →
|
|
348
|
+
`router`, tsconfig aliases, `NODE_ENV` pinned), then walked:
|
|
349
|
+
|
|
350
|
+
- **The marker.** `defineStateRouter` sets a non-enumerable `Router._stateManifest` on the
|
|
351
|
+
router it derives — the very object the snapshot route answers as `manifest` — so the ids
|
|
352
|
+
written here and the manifest the server serves come from one value. Each state router
|
|
353
|
+
contributes its entries in declaration order; `key` lists the key fields in schema order.
|
|
354
|
+
- **Mounted routers at every depth.** Every nested router under `routes` and every entry of
|
|
355
|
+
`_packageRouters`, recursively — app `routes` first, then `_packageRouters` in mount order.
|
|
356
|
+
A router reached by two trails (`.packages()` copies a mounted router's own package routers
|
|
357
|
+
up) is collected once: the walk is keyed on the router object.
|
|
358
|
+
- **Chained routers.** `.use()`, `.packages()` and `.contractVersion()` carry the marker onto
|
|
359
|
+
the router they rebuild, so `states.router.use([audit])` is still found.
|
|
360
|
+
- An id that is not a JS identifier is emitted quoted, through the same key rule `route-map`
|
|
361
|
+
uses.
|
|
362
|
+
|
|
363
|
+
### When it refuses
|
|
364
|
+
|
|
365
|
+
Throws `StatesGeneratorError` (or the loader's and the guard's errors) — logged on `watch`,
|
|
366
|
+
fatal on `build` and on `spfn codegen run`:
|
|
367
|
+
|
|
368
|
+
- The module will not load, or no router is found under the three export names.
|
|
369
|
+
- The router registers **conditionally** — same `ConditionalRegistrationError` guard as
|
|
370
|
+
`route-map`.
|
|
371
|
+
- **Two state routers declare the same id.** The message names both trails.
|
|
372
|
+
- **No state router is mounted** anywhere in the walk. The generator is configured but nothing
|
|
373
|
+
it reads mounts a `defineStateRouter(...).router`; the message names the router path. Mount
|
|
374
|
+
the state router or drop the generator.
|
|
375
|
+
|
|
376
|
+
A mounted state router that declares **no state** — `defineStateRouter({})` — is not refused: it
|
|
377
|
+
is an app that has declared none yet. The file is written with `stateIds = [] as const` and an
|
|
378
|
+
empty `stateManifest`, which matches the empty manifest the server serves.
|
|
379
|
+
|
|
380
|
+
A **missing router file** is only a warning: the generator returns and writes nothing.
|
|
381
|
+
|
|
382
|
+
### Generated output
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// src/generated/states.ts — DO NOT EDIT (header names the NODE_ENV it was written under)
|
|
386
|
+
export const stateIds = ['consent', 'member', 'workspaces'] as const;
|
|
387
|
+
|
|
388
|
+
export type StateId = (typeof stateIds)[number];
|
|
389
|
+
|
|
390
|
+
export const stateManifest: Readonly<Record<StateId, { readonly key: readonly string[]; readonly write: boolean }>> = {
|
|
391
|
+
consent: { key: [], write: true },
|
|
392
|
+
member: { key: ['workspaceId'], write: false },
|
|
393
|
+
workspaces: { key: [], write: false },
|
|
394
|
+
};
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`watchPatterns` are `[routerPath, 'src/server/states.ts', 'src/server/states/**/*.ts',
|
|
398
|
+
'src/server/routes/**/*.ts']` and `runOn` is `['watch', 'manual', 'build']`.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## CLI
|
|
403
|
+
|
|
404
|
+
Provided by the `spfn` CLI (`@spfn/cli`), not by `@spfn/core` itself:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
spfn codegen init # scaffold a .spfnrc.ts
|
|
408
|
+
spfn codegen list # list resolved generators + their watch patterns (alias: ls)
|
|
409
|
+
spfn codegen run # run all generators once (CodegenOrchestrator.generateAll, trigger 'manual'; a failure exits 1)
|
|
410
|
+
spfn dev # dev server + codegen in watch mode
|
|
411
|
+
spfn build # runs codegen once before building (trigger 'build'; a failure exits 1)
|
|
412
|
+
|
|
413
|
+
spfn contract check # regenerate the contract, compare against the newest released snapshot
|
|
414
|
+
spfn contract release X # write contracts/released/X.json
|
|
415
|
+
spfn contract list # released snapshots (alias: ls)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`spfn codegen run` has **no** `--name` flag — it always runs every configured generator.
|
|
419
|
+
(The `spfn init` project scaffold writes a `.spfnrc.ts` preconfigured with the route-map
|
|
420
|
+
generator and adds a `"codegen": "spfn codegen run"` npm script.)
|
|
421
|
+
|
|
422
|
+
`spfn codegen run` **exits 1 when a generator refuses**, and stops at the one that refused
|
|
423
|
+
— the run that just failed to regenerate a map must not look like the run that did. `spfn
|
|
424
|
+
dev` is unaffected: watch mode logs the failure and keeps going, because being unable to
|
|
425
|
+
hold a half-finished route mid-edit would make the feature unusable.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Programmatic usage
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import {
|
|
433
|
+
CodegenOrchestrator,
|
|
434
|
+
loadCodegenConfig,
|
|
435
|
+
createGeneratorsFromConfig,
|
|
436
|
+
} from '@spfn/core/codegen';
|
|
437
|
+
|
|
438
|
+
const cwd = process.cwd();
|
|
439
|
+
const config = loadCodegenConfig(cwd);
|
|
440
|
+
const generators = await createGeneratorsFromConfig(config, cwd);
|
|
441
|
+
|
|
442
|
+
const orchestrator = new CodegenOrchestrator({ generators, cwd, debug: true });
|
|
443
|
+
|
|
444
|
+
// Run once. Trigger defaults to 'manual'; only generators whose runOn includes it execute.
|
|
445
|
+
await orchestrator.generateAll(); // 'manual'
|
|
446
|
+
await orchestrator.generateAll('build'); // what `spfn build` dispatches
|
|
447
|
+
|
|
448
|
+
// Watch mode: runs an initial 'watch' pass, then returns a promise that stays pending
|
|
449
|
+
// (keeping the process alive) until close() is called.
|
|
450
|
+
await orchestrator.watch();
|
|
451
|
+
// ...later, to shut down: await orchestrator.close();
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### `OrchestratorOptions`
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
interface OrchestratorOptions
|
|
458
|
+
{
|
|
459
|
+
generators: Generator[];
|
|
460
|
+
cwd?: string; // default: process.cwd()
|
|
461
|
+
debug?: boolean; // default: false
|
|
462
|
+
throwOnError?: boolean; // default: false
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
`throwOnError` decides what a generator failure does. Off — the default — it is logged and the
|
|
467
|
+
run continues, which is what keeps watch mode alive through a half-edited file. `spfn build`
|
|
468
|
+
turns it on: a generator that refuses at build time (a broken route contract, a router that will
|
|
469
|
+
not load) has to reach the exit code, or the refusal scrolls past and the build ships anyway.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Custom generators
|
|
474
|
+
|
|
475
|
+
A generator is a plain object implementing the `Generator` interface. Register it by
|
|
476
|
+
`path` (file) or by exporting it from a package's `./codegen` entry.
|
|
477
|
+
|
|
478
|
+
### File-based
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// src/generators/admin-nav-generator.ts
|
|
482
|
+
import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
|
|
483
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
484
|
+
import { join, dirname } from 'path';
|
|
485
|
+
|
|
486
|
+
// Default export MUST be a zero-arg factory returning a Generator.
|
|
487
|
+
export default function createAdminNavGenerator(): Generator
|
|
488
|
+
{
|
|
489
|
+
return {
|
|
490
|
+
name: 'admin-nav',
|
|
491
|
+
watchPatterns: ['src/app/admin/**/nav.config.tsx'],
|
|
492
|
+
runOn: ['watch', 'manual', 'build'],
|
|
493
|
+
|
|
494
|
+
async generate(options: GeneratorOptions): Promise<void>
|
|
495
|
+
{
|
|
496
|
+
const out = join(options.cwd, 'src/lib/admin/nav-data.generated.tsx');
|
|
497
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
498
|
+
// ...scan source, write `out`
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// .spfnrc.ts
|
|
506
|
+
import { defineConfig } from '@spfn/core/codegen';
|
|
507
|
+
|
|
508
|
+
export default defineConfig({
|
|
509
|
+
generators: [
|
|
510
|
+
{ path: './src/generators/admin-nav-generator.ts' },
|
|
511
|
+
],
|
|
512
|
+
});
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Package-based
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
// my-package/src/codegen/index.ts
|
|
519
|
+
import { createMyGenerator } from './my-generator';
|
|
520
|
+
|
|
521
|
+
// The loader looks up `generators[name]` by the part after the ':'.
|
|
522
|
+
export const generators = {
|
|
523
|
+
'my-generator': createMyGenerator, // factory: (config) => Generator
|
|
524
|
+
};
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```jsonc
|
|
528
|
+
// my-package/package.json — must expose ./codegen
|
|
529
|
+
{ "exports": { "./codegen": { "import": "./dist/codegen/index.js", "types": "./dist/codegen/index.d.ts" } } }
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
// consumer .spfnrc.ts
|
|
534
|
+
defineGenerator({ name: 'my-package:my-generator', enabled: true, /* ...opts */ });
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The factory receives the config object with `name` and `enabled` stripped out (everything
|
|
538
|
+
else is passed through as options).
|
|
539
|
+
|
|
540
|
+
### `Generator` interface
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
type GeneratorTrigger = 'watch' | 'manual' | 'build' | 'start';
|
|
544
|
+
|
|
545
|
+
interface Generator
|
|
546
|
+
{
|
|
547
|
+
name: string;
|
|
548
|
+
watchPatterns: string[]; // globs; orchestrator watches their base dirs
|
|
549
|
+
runOn?: GeneratorTrigger[]; // default ['watch', 'manual', 'build']
|
|
550
|
+
generate(options: GeneratorOptions): Promise<void>;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
interface GeneratorOptions
|
|
554
|
+
{
|
|
555
|
+
cwd: string;
|
|
556
|
+
debug?: boolean;
|
|
557
|
+
trigger?: {
|
|
558
|
+
type: GeneratorTrigger;
|
|
559
|
+
changedFile?: { path: string; event: 'add' | 'change' | 'unlink' }; // watch only
|
|
560
|
+
};
|
|
561
|
+
[key: string]: any;
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
For incremental rebuilds, check `options.trigger?.changedFile` and fall back to full
|
|
566
|
+
regeneration when it's absent (build/manual passes don't set it).
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Pitfalls & anti-patterns
|
|
571
|
+
|
|
572
|
+
- **`defineCodegenConfig` does not exist — use `defineConfig`.** Likewise there is no
|
|
573
|
+
`api-client` built-in generator, no `createApi`-emitting codegen, and no
|
|
574
|
+
`codegen.config.ts`. The current model is `.spfnrc.ts` + `@spfn/core:route-map`. Any doc
|
|
575
|
+
showing those is stale.
|
|
576
|
+
- **`.spfnrc.json` / `package.json` need the wrapper key.** JSON config lives under
|
|
577
|
+
`"codegen"` (in `.spfnrc.json`) or `"spfn": { "codegen": ... }` (in `package.json`). A
|
|
578
|
+
top-level `{ "generators": [...] }` in `.spfnrc.json` is ignored. (A `.spfnrc.ts` default
|
|
579
|
+
export, by contrast, *is* the config directly.)
|
|
580
|
+
- **Config precedence is first-found, not merged.** A `.spfnrc.ts` completely shadows the
|
|
581
|
+
JSON/package configs — they are not combined.
|
|
582
|
+
- **Run codegen before the client builds.** `src/generated/route-map.ts` is committed/built
|
|
583
|
+
output the RPC proxy imports. If it's missing or stale, `api.<route>.call()` resolves the
|
|
584
|
+
wrong (or no) `method`/`path`. `spfn build` runs codegen first; if you build by other
|
|
585
|
+
means, run `spfn codegen run` yourself. Treat the file as generated (it's marked
|
|
586
|
+
`DO NOT EDIT`). **Upgrading a mounted package goes stale the same way and is quieter about
|
|
587
|
+
it**: the map carries that package's routes, but the upgrade changes no app source, so no
|
|
588
|
+
watcher fires and nothing else says the committed map is a version behind — re-run codegen
|
|
589
|
+
after an upgrade as you would after adding a route.
|
|
590
|
+
- **A route must be in `defineRouter({...})` to be emitted.** Defining `export const foo =
|
|
591
|
+
route.get(...)` but not registering `foo` in the router drops it from the map — the
|
|
592
|
+
generator walks the router, so an unregistered route does not exist as far as it is
|
|
593
|
+
concerned. How the registration is *spelled* no longer matters.
|
|
594
|
+
- **route-map loads your router, so route modules must import cleanly.** Reading a
|
|
595
|
+
required environment value at module scope now fails `spfn build` and `spfn codegen run`,
|
|
596
|
+
not just `spfn dev`. Move that read inside the handler. Top-level `await` in a route or
|
|
597
|
+
router module does not load either — jiti transforms to CJS — the same limit
|
|
598
|
+
`@spfn/core:contract` has always had.
|
|
599
|
+
- **A path alias resolves only if a tsconfig between the router and the project root maps
|
|
600
|
+
it.** Both loading generators read `compilerOptions.paths` from the nearest such
|
|
601
|
+
`tsconfig.json` (following `extends`) and hand it to jiti, so `@/server/routes/users`
|
|
602
|
+
resolves from the root config or from `src/server/tsconfig.json`. A mapping that lives
|
|
603
|
+
*above* the project root — a monorepo's solution-style config — is not read; a `paths`
|
|
604
|
+
entry whose `*` is not a trailing `/*` is skipped (jiti matches a prefix, not a template);
|
|
605
|
+
and a `baseUrl` with no `paths` resolves nothing (an alias map cannot say "try this
|
|
606
|
+
directory for every bare specifier"). Several targets for one pattern are supported: the
|
|
607
|
+
first that exists on disk is used, as tsc and tsup use it. Each skip is warned about, an
|
|
608
|
+
`extends` target TypeScript could not read is warned about instead of silently dropping
|
|
609
|
+
its `paths`, and an import that then fails to resolve names the specifier.
|
|
610
|
+
- **Custom generators must default-export a factory.** `createGeneratorsFromConfig` calls
|
|
611
|
+
`module.default()` (a zero-arg function) for `{ path }` entries. Exporting the Generator
|
|
612
|
+
object directly, or a factory that needs args, won't load.
|
|
613
|
+
- **Don't watch your own output.** A generator whose `watchPatterns` match its `outputPath`
|
|
614
|
+
re-triggers itself. Write outputs outside the watched globs (the orchestrator serializes
|
|
615
|
+
runs and queues one pending re-run, but a self-match still loops).
|
|
616
|
+
- **`generate()` should not throw to signal "skip".** The orchestrator catches errors per
|
|
617
|
+
generator (one failure doesn't stop the others), but a throw is logged as a failure.
|
|
618
|
+
Return early instead (route-map logs a warning and returns when the router file is
|
|
619
|
+
absent).
|
|
620
|
+
- **`watch()` never resolves on its own.** It returns a promise that stays pending to keep
|
|
621
|
+
the process alive; call `close()` to resolve it and tear down the chokidar watcher.
|
|
622
|
+
- **Package generators need a `:` in `name`.** `{ name: 'route-map' }` (no colon) is
|
|
623
|
+
rejected as an invalid name — it must be `'@spfn/core:route-map'`.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Complete example
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
// .spfnrc.ts
|
|
631
|
+
import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
632
|
+
|
|
633
|
+
export default defineConfig({
|
|
634
|
+
generators: [
|
|
635
|
+
// Built-in: route-map for the RPC proxy
|
|
636
|
+
defineGenerator({
|
|
637
|
+
name: '@spfn/core:route-map',
|
|
638
|
+
routerPath: './src/server/router.ts',
|
|
639
|
+
outputPath: './src/generated/route-map.ts',
|
|
640
|
+
}),
|
|
641
|
+
// A project-local custom generator
|
|
642
|
+
{ path: './src/generators/admin-nav-generator.ts' },
|
|
643
|
+
],
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// src/server/router.ts
|
|
649
|
+
import { defineRouter } from '@spfn/core/route';
|
|
650
|
+
import { getUser, createUser } from './routes/users';
|
|
651
|
+
|
|
652
|
+
export const router = defineRouter({
|
|
653
|
+
getUser,
|
|
654
|
+
createUser,
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
// src/server/routes/users.ts
|
|
660
|
+
import { route } from '@spfn/core/route';
|
|
661
|
+
|
|
662
|
+
export const getUser = route.get('/users/:id')/* ...contract/handler */;
|
|
663
|
+
export const createUser = route.post('/users')/* ...contract/handler */;
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
// app/api/rpc/[routeName]/route.ts — RPC proxy consumes the generated map
|
|
668
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
669
|
+
import { routeMap } from '@/generated/route-map';
|
|
670
|
+
|
|
671
|
+
// routeMap (from src/generated/route-map.ts) lets the proxy resolve method + path
|
|
672
|
+
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
spfn codegen run # writes src/generated/route-map.ts
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## Types reference
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
interface CodegenConfig { generators?: GeneratorConfig[]; }
|
|
685
|
+
|
|
686
|
+
type GeneratorConfig =
|
|
687
|
+
| { path: string } // file-based
|
|
688
|
+
| ({ name: string; enabled?: boolean } & Record<string, any>); // package-based
|
|
689
|
+
|
|
690
|
+
type GeneratorTrigger = 'watch' | 'manual' | 'build' | 'start';
|
|
691
|
+
|
|
692
|
+
interface Generator
|
|
693
|
+
{
|
|
694
|
+
name: string;
|
|
695
|
+
watchPatterns: string[];
|
|
696
|
+
runOn?: GeneratorTrigger[]; // default ['watch', 'manual', 'build']
|
|
697
|
+
generate(options: GeneratorOptions): Promise<void>;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
interface RouteMapGeneratorConfig
|
|
701
|
+
{
|
|
702
|
+
name: '@spfn/core:route-map';
|
|
703
|
+
routerPath: string;
|
|
704
|
+
outputPath?: string; // default './src/generated/route-map.ts'
|
|
705
|
+
additionalRouteDirs?: string[]; // deprecated: watched, never collected from
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
interface StatesGeneratorConfig
|
|
709
|
+
{
|
|
710
|
+
name: '@spfn/core:states';
|
|
711
|
+
routerPath: string;
|
|
712
|
+
outputPath?: string; // default './src/generated/states.ts'
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
## Related
|
|
717
|
+
|
|
718
|
+
- [@spfn/core/route](../route/README.md) — `route.*` / `defineRouter` (the router loaded and walked)
|
|
719
|
+
- [@spfn/core/nextjs](../nextjs/README.md) — RPC proxy (`createRpcProxy`) that consumes `routeMap`
|
|
720
|
+
- [@spfn/core/state](../state/README.md) — `defineStateRouter`, whose manifest `@spfn/core:states` writes
|
|
721
|
+
- [@spfn/core/env](../env/README.md) — environment configuration
|