@spfn/core 0.3.0-beta.5 → 0.3.0-beta.6
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 +132 -4
- package/dist/db/index.d.ts +173 -27
- package/dist/db/index.js +192 -57
- package/dist/db/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +18 -1
- package/dist/nextjs/index.js +40 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +34 -1
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/docs/file-upload.md +195 -333
- package/package.json +6 -5
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +344 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +736 -0
- package/src/job/README.md +514 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
|
@@ -0,0 +1,516 @@
|
|
|
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
|
+
- `ContractGeneratorError`, `ConditionalRegistrationError`, `assertUnconditionalRegistration`
|
|
56
|
+
- `RouteContractMapping`, `ResourceRoutes`, `ClientGenerationOptions`, `GenerationStats`
|
|
57
|
+
(legacy client-generation types — exported but not used by any shipped generator)
|
|
58
|
+
|
|
59
|
+
> **Renamed: `defineCodegenConfig` → `defineConfig`.** The old name does **not** exist.
|
|
60
|
+
> Older docs also show an `api-client` / `createApi`-emitting generator and
|
|
61
|
+
> `codegen.config.ts` — those are **removed**. The built-in generators today are
|
|
62
|
+
> `@spfn/core:route-map` and `@spfn/core:contract`, configured in `.spfnrc.ts`. Do not
|
|
63
|
+
> import `defineCodegenConfig` or configure a `name: 'api-client'` generator.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. Configure `.spfnrc.ts`
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
73
|
+
|
|
74
|
+
export default defineConfig({
|
|
75
|
+
generators: [
|
|
76
|
+
defineGenerator({
|
|
77
|
+
name: '@spfn/core:route-map',
|
|
78
|
+
routerPath: './src/server/router.ts',
|
|
79
|
+
outputPath: './src/generated/route-map.ts', // optional (this is the default)
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Generate
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
spfn codegen run # run all generators once
|
|
89
|
+
spfn dev # watch mode (regenerates on file change)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This writes `src/generated/route-map.ts`, which the RPC proxy imports.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Configuration resolution
|
|
97
|
+
|
|
98
|
+
`loadCodegenConfig(cwd)` checks these in order and returns the **first** that exists:
|
|
99
|
+
|
|
100
|
+
1. `.spfnrc.ts` — loaded with jiti (supports `defineConfig`/`defineGenerator` + TS types).
|
|
101
|
+
The default export (or the module itself) is the `CodegenConfig`.
|
|
102
|
+
2. `.spfnrc.json` — the config is read from the top-level **`codegen`** key:
|
|
103
|
+
`{ "codegen": { "generators": [...] } }`.
|
|
104
|
+
3. `package.json` — read from **`spfn.codegen`**: `{ "spfn": { "codegen": { ... } } }`.
|
|
105
|
+
|
|
106
|
+
If none exist (or parsing fails), you get `{ generators: [] }` and nothing runs.
|
|
107
|
+
|
|
108
|
+
> Precedence is "first file found wins", not a deep merge. A `.spfnrc.ts` fully shadows
|
|
109
|
+
> `.spfnrc.json` and `package.json`.
|
|
110
|
+
|
|
111
|
+
### Generator config entries (`GeneratorConfig`)
|
|
112
|
+
|
|
113
|
+
A `generators[]` entry is one of three shapes:
|
|
114
|
+
|
|
115
|
+
| Shape | Example | Meaning |
|
|
116
|
+
|-------|---------|---------|
|
|
117
|
+
| 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. |
|
|
118
|
+
| File generator | `{ path: './src/generators/x.ts' }` | A `.ts`/`.js` file whose default export is a `() => Generator` factory. `.ts` loaded via jiti. |
|
|
119
|
+
| Pre-built instance | `defineGenerator({...})` result that already has a `generate` fn | Pushed as-is (guards against accidentally calling a factory yourself). |
|
|
120
|
+
|
|
121
|
+
For package generators, `name` is split on `:` into `package:generatorName`. The loader
|
|
122
|
+
imports `${package}/codegen` and looks for `generators[generatorName]`, falling back to a
|
|
123
|
+
`create<Name>Generator` export. So `@spfn/core:route-map` → import `@spfn/core/codegen`,
|
|
124
|
+
call `generators['route-map'](config)`.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Built-in: `@spfn/core:route-map`
|
|
129
|
+
|
|
130
|
+
Parses your router + route files and emits a `routeName → {method, path}` map. This map is
|
|
131
|
+
what the **RPC proxy** (`createRpcProxy` in `@spfn/core/nextjs/server`) uses to turn
|
|
132
|
+
`api.getUser.call(...)` into an HTTP request — it needs `method` and `path` without
|
|
133
|
+
importing server code into the client bundle.
|
|
134
|
+
|
|
135
|
+
### `RouteMapGeneratorConfig`
|
|
136
|
+
|
|
137
|
+
| Field | Type | Required | Default | Description |
|
|
138
|
+
|-------|------|----------|---------|-------------|
|
|
139
|
+
| `name` | `'@spfn/core:route-map'` | yes | — | Generator identifier (literal). |
|
|
140
|
+
| `routerPath` | `string` | yes | — | Router file, relative to project root. Throws at construction if missing. |
|
|
141
|
+
| `outputPath` | `string` | no | `'./src/generated/route-map.ts'` | Where the map is written (parent dirs auto-created). |
|
|
142
|
+
| `additionalRouteDirs` | `string[]` | no | `[]` | Extra route dirs to watch (each becomes `${dir}/**/*.ts`). |
|
|
143
|
+
|
|
144
|
+
### What it parses
|
|
145
|
+
|
|
146
|
+
- **Router file** (`routerPath`): relative `import { ... } from './...'` statements, and the
|
|
147
|
+
route names listed inside the **first** `defineRouter({ ... })` call (top-level
|
|
148
|
+
identifiers, comments stripped).
|
|
149
|
+
- **Route files**: only the exact pattern
|
|
150
|
+
`export const <name> = route.<get|post|put|patch|delete>('<path>')…` (single/double/back
|
|
151
|
+
quotes). The leading `route.<method>('<path>')` call is what's matched.
|
|
152
|
+
- Only routes whose `name` also appears in `defineRouter({...})` end up in the output
|
|
153
|
+
(declared-but-unregistered routes are dropped).
|
|
154
|
+
|
|
155
|
+
### Generated output
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// src/generated/route-map.ts — DO NOT EDIT
|
|
159
|
+
import type { HttpMethod } from '@spfn/core/route';
|
|
160
|
+
|
|
161
|
+
export interface RouteInfo
|
|
162
|
+
{
|
|
163
|
+
method: HttpMethod;
|
|
164
|
+
path: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const routeMap: Record<string, RouteInfo> = {
|
|
168
|
+
getUser: { method: 'GET', path: '/users/:id' },
|
|
169
|
+
createUser: { method: 'POST', path: '/users' },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type RouteMap = typeof routeMap;
|
|
173
|
+
export type RouteName = keyof RouteMap;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`watchPatterns` are `[routerPath, 'src/server/routes/**/*.ts', ...additionalRouteDirs]` and
|
|
177
|
+
`runOn` is `['watch', 'manual', 'build']` — every trigger the CLI fires, so it runs in every mode.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Built-in: `@spfn/core:contract`
|
|
182
|
+
|
|
183
|
+
Writes `contracts/current.json` — every route carrying `.contract()` — and on a **build** compares
|
|
184
|
+
it against the newest released snapshot, refusing changes that would break a client already in the
|
|
185
|
+
field. Full behaviour lives in [`../contract/README.md`](../contract/README.md); this section is the
|
|
186
|
+
generator's own surface.
|
|
187
|
+
|
|
188
|
+
### `ContractGeneratorConfig`
|
|
189
|
+
|
|
190
|
+
| Field | Type | Required | Default | Description |
|
|
191
|
+
|-------|------|----------|---------|-------------|
|
|
192
|
+
| `name` | `'@spfn/core:contract'` | yes | — | Generator identifier (literal). |
|
|
193
|
+
| `routerPath` | `string` | yes | — | Router file, relative to project root. Throws at construction if missing. |
|
|
194
|
+
| `routerExport` | `string` | no | `appRouter` → `default` → `router` | Export holding the `defineRouter()` result. |
|
|
195
|
+
| `outputDir` | `string` | no | `'./contracts'` | Holds `current.json`, `released/`, `usage/`. |
|
|
196
|
+
| `additionalRouteDirs` | `string[]` | no | `[]` | Extra route dirs to watch. |
|
|
197
|
+
|
|
198
|
+
### How it differs from route-map
|
|
199
|
+
|
|
200
|
+
| | `route-map` | `contract` |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| Reads the router by | parsing the source | **loading the module** and walking `RouteDef`s |
|
|
203
|
+
| Covers | every registered route | only routes carrying `.contract()` |
|
|
204
|
+
| `runOn` | `watch`, `manual`, `build` | `watch`, `build`, `manual` |
|
|
205
|
+
| Can fail a build | no | **yes**, on the `build` trigger only |
|
|
206
|
+
|
|
207
|
+
Loading rather than parsing is what makes the contract correct: real routes build schemas from
|
|
208
|
+
imported values (`EmailSchema`, `FileSchema()`, constants) that a source parser cannot resolve. It
|
|
209
|
+
costs a module import and no infrastructure — `@spfn/auth`'s 43 routes load in ~0.6s with neither
|
|
210
|
+
`DATABASE_URL` nor `CACHE_URL` set.
|
|
211
|
+
|
|
212
|
+
### When it refuses
|
|
213
|
+
|
|
214
|
+
- The router file is missing, or the module will not load (it names the module and the cause —
|
|
215
|
+
it never skips quietly).
|
|
216
|
+
- No router export is found under the configured or default names.
|
|
217
|
+
- Two contracted routes share a name, or a contracted route has no method, path, `since` or
|
|
218
|
+
`response`.
|
|
219
|
+
- `defineRouter({...})` contains a computed spread (`...(flag ? { route } : {})`), which would make
|
|
220
|
+
the contract describe whichever way the generator happened to run.
|
|
221
|
+
- On the `build` trigger only: the contract breaks the newest released snapshot.
|
|
222
|
+
|
|
223
|
+
`spfn dev` generates but never refuses — being unable to hold a half-finished route mid-edit would
|
|
224
|
+
make the feature unusable.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## CLI
|
|
229
|
+
|
|
230
|
+
Provided by the `spfn` CLI (`@spfn/cli`), not by `@spfn/core` itself:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
spfn codegen init # scaffold a .spfnrc.ts
|
|
234
|
+
spfn codegen list # list resolved generators + their watch patterns (alias: ls)
|
|
235
|
+
spfn codegen run # run all generators once (CodegenOrchestrator.generateAll, trigger 'manual')
|
|
236
|
+
spfn dev # dev server + codegen in watch mode
|
|
237
|
+
spfn build # runs codegen once before building (trigger 'build'; a failure exits 1)
|
|
238
|
+
|
|
239
|
+
spfn contract check # regenerate the contract, compare against the newest released snapshot
|
|
240
|
+
spfn contract release X # write contracts/released/X.json
|
|
241
|
+
spfn contract list # released snapshots (alias: ls)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`spfn codegen run` has **no** `--name` flag — it always runs every configured generator.
|
|
245
|
+
(The `spfn init` project scaffold writes a `.spfnrc.ts` preconfigured with the route-map
|
|
246
|
+
generator and adds a `"codegen": "spfn codegen run"` npm script.)
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Programmatic usage
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import {
|
|
254
|
+
CodegenOrchestrator,
|
|
255
|
+
loadCodegenConfig,
|
|
256
|
+
createGeneratorsFromConfig,
|
|
257
|
+
} from '@spfn/core/codegen';
|
|
258
|
+
|
|
259
|
+
const cwd = process.cwd();
|
|
260
|
+
const config = loadCodegenConfig(cwd);
|
|
261
|
+
const generators = await createGeneratorsFromConfig(config, cwd);
|
|
262
|
+
|
|
263
|
+
const orchestrator = new CodegenOrchestrator({ generators, cwd, debug: true });
|
|
264
|
+
|
|
265
|
+
// Run once. Trigger defaults to 'manual'; only generators whose runOn includes it execute.
|
|
266
|
+
await orchestrator.generateAll(); // 'manual'
|
|
267
|
+
await orchestrator.generateAll('build'); // what `spfn build` dispatches
|
|
268
|
+
|
|
269
|
+
// Watch mode: runs an initial 'watch' pass, then returns a promise that stays pending
|
|
270
|
+
// (keeping the process alive) until close() is called.
|
|
271
|
+
await orchestrator.watch();
|
|
272
|
+
// ...later, to shut down: await orchestrator.close();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### `OrchestratorOptions`
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
interface OrchestratorOptions
|
|
279
|
+
{
|
|
280
|
+
generators: Generator[];
|
|
281
|
+
cwd?: string; // default: process.cwd()
|
|
282
|
+
debug?: boolean; // default: false
|
|
283
|
+
throwOnError?: boolean; // default: false
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`throwOnError` decides what a generator failure does. Off — the default — it is logged and the
|
|
288
|
+
run continues, which is what keeps watch mode alive through a half-edited file. `spfn build`
|
|
289
|
+
turns it on: a generator that refuses at build time (a broken route contract, a router that will
|
|
290
|
+
not load) has to reach the exit code, or the refusal scrolls past and the build ships anyway.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Custom generators
|
|
295
|
+
|
|
296
|
+
A generator is a plain object implementing the `Generator` interface. Register it by
|
|
297
|
+
`path` (file) or by exporting it from a package's `./codegen` entry.
|
|
298
|
+
|
|
299
|
+
### File-based
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// src/generators/admin-nav-generator.ts
|
|
303
|
+
import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
|
|
304
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
305
|
+
import { join, dirname } from 'path';
|
|
306
|
+
|
|
307
|
+
// Default export MUST be a zero-arg factory returning a Generator.
|
|
308
|
+
export default function createAdminNavGenerator(): Generator
|
|
309
|
+
{
|
|
310
|
+
return {
|
|
311
|
+
name: 'admin-nav',
|
|
312
|
+
watchPatterns: ['src/app/admin/**/nav.config.tsx'],
|
|
313
|
+
runOn: ['watch', 'manual', 'build'],
|
|
314
|
+
|
|
315
|
+
async generate(options: GeneratorOptions): Promise<void>
|
|
316
|
+
{
|
|
317
|
+
const out = join(options.cwd, 'src/lib/admin/nav-data.generated.tsx');
|
|
318
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
319
|
+
// ...scan source, write `out`
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// .spfnrc.ts
|
|
327
|
+
import { defineConfig } from '@spfn/core/codegen';
|
|
328
|
+
|
|
329
|
+
export default defineConfig({
|
|
330
|
+
generators: [
|
|
331
|
+
{ path: './src/generators/admin-nav-generator.ts' },
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Package-based
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// my-package/src/codegen/index.ts
|
|
340
|
+
import { createMyGenerator } from './my-generator';
|
|
341
|
+
|
|
342
|
+
// The loader looks up `generators[name]` by the part after the ':'.
|
|
343
|
+
export const generators = {
|
|
344
|
+
'my-generator': createMyGenerator, // factory: (config) => Generator
|
|
345
|
+
};
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```jsonc
|
|
349
|
+
// my-package/package.json — must expose ./codegen
|
|
350
|
+
{ "exports": { "./codegen": { "import": "./dist/codegen/index.js", "types": "./dist/codegen/index.d.ts" } } }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// consumer .spfnrc.ts
|
|
355
|
+
defineGenerator({ name: 'my-package:my-generator', enabled: true, /* ...opts */ });
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The factory receives the config object with `name` and `enabled` stripped out (everything
|
|
359
|
+
else is passed through as options).
|
|
360
|
+
|
|
361
|
+
### `Generator` interface
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
type GeneratorTrigger = 'watch' | 'manual' | 'build' | 'start';
|
|
365
|
+
|
|
366
|
+
interface Generator
|
|
367
|
+
{
|
|
368
|
+
name: string;
|
|
369
|
+
watchPatterns: string[]; // globs; orchestrator watches their base dirs
|
|
370
|
+
runOn?: GeneratorTrigger[]; // default ['watch', 'manual', 'build']
|
|
371
|
+
generate(options: GeneratorOptions): Promise<void>;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface GeneratorOptions
|
|
375
|
+
{
|
|
376
|
+
cwd: string;
|
|
377
|
+
debug?: boolean;
|
|
378
|
+
trigger?: {
|
|
379
|
+
type: GeneratorTrigger;
|
|
380
|
+
changedFile?: { path: string; event: 'add' | 'change' | 'unlink' }; // watch only
|
|
381
|
+
};
|
|
382
|
+
[key: string]: any;
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
For incremental rebuilds, check `options.trigger?.changedFile` and fall back to full
|
|
387
|
+
regeneration when it's absent (build/manual passes don't set it).
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Pitfalls & anti-patterns
|
|
392
|
+
|
|
393
|
+
- **`defineCodegenConfig` does not exist — use `defineConfig`.** Likewise there is no
|
|
394
|
+
`api-client` built-in generator, no `createApi`-emitting codegen, and no
|
|
395
|
+
`codegen.config.ts`. The current model is `.spfnrc.ts` + `@spfn/core:route-map`. Any doc
|
|
396
|
+
showing those is stale.
|
|
397
|
+
- **`.spfnrc.json` / `package.json` need the wrapper key.** JSON config lives under
|
|
398
|
+
`"codegen"` (in `.spfnrc.json`) or `"spfn": { "codegen": ... }` (in `package.json`). A
|
|
399
|
+
top-level `{ "generators": [...] }` in `.spfnrc.json` is ignored. (A `.spfnrc.ts` default
|
|
400
|
+
export, by contrast, *is* the config directly.)
|
|
401
|
+
- **Config precedence is first-found, not merged.** A `.spfnrc.ts` completely shadows the
|
|
402
|
+
JSON/package configs — they are not combined.
|
|
403
|
+
- **Run codegen before the client builds.** `src/generated/route-map.ts` is committed/built
|
|
404
|
+
output the RPC proxy imports. If it's missing or stale, `api.<route>.call()` resolves the
|
|
405
|
+
wrong (or no) `method`/`path`. `spfn build` runs codegen first; if you build by other
|
|
406
|
+
means, run `spfn codegen run` yourself. Treat the file as generated (it's marked
|
|
407
|
+
`DO NOT EDIT`).
|
|
408
|
+
- **route-map only matches a specific route syntax.** Routes must be
|
|
409
|
+
`export const x = route.<method>('<literal path>')`. Dynamic paths, methods other than
|
|
410
|
+
get/post/put/patch/delete, or routes not listed in `defineRouter({...})` won't appear.
|
|
411
|
+
- **A route must be in `defineRouter({...})` to be emitted.** Defining `export const foo =
|
|
412
|
+
route.get(...)` but not registering `foo` in the router drops it from the map.
|
|
413
|
+
- **Custom generators must default-export a factory.** `createGeneratorsFromConfig` calls
|
|
414
|
+
`module.default()` (a zero-arg function) for `{ path }` entries. Exporting the Generator
|
|
415
|
+
object directly, or a factory that needs args, won't load.
|
|
416
|
+
- **Don't watch your own output.** A generator whose `watchPatterns` match its `outputPath`
|
|
417
|
+
re-triggers itself. Write outputs outside the watched globs (the orchestrator serializes
|
|
418
|
+
runs and queues one pending re-run, but a self-match still loops).
|
|
419
|
+
- **`generate()` should not throw to signal "skip".** The orchestrator catches errors per
|
|
420
|
+
generator (one failure doesn't stop the others), but a throw is logged as a failure.
|
|
421
|
+
Return early instead (route-map logs a warning and returns when the router file is
|
|
422
|
+
absent).
|
|
423
|
+
- **`watch()` never resolves on its own.** It returns a promise that stays pending to keep
|
|
424
|
+
the process alive; call `close()` to resolve it and tear down the chokidar watcher.
|
|
425
|
+
- **Package generators need a `:` in `name`.** `{ name: 'route-map' }` (no colon) is
|
|
426
|
+
rejected as an invalid name — it must be `'@spfn/core:route-map'`.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Complete example
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
// .spfnrc.ts
|
|
434
|
+
import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
435
|
+
|
|
436
|
+
export default defineConfig({
|
|
437
|
+
generators: [
|
|
438
|
+
// Built-in: route-map for the RPC proxy
|
|
439
|
+
defineGenerator({
|
|
440
|
+
name: '@spfn/core:route-map',
|
|
441
|
+
routerPath: './src/server/router.ts',
|
|
442
|
+
outputPath: './src/generated/route-map.ts',
|
|
443
|
+
}),
|
|
444
|
+
// A project-local custom generator
|
|
445
|
+
{ path: './src/generators/admin-nav-generator.ts' },
|
|
446
|
+
],
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
// src/server/router.ts
|
|
452
|
+
import { defineRouter } from '@spfn/core/route';
|
|
453
|
+
import { getUser, createUser } from './routes/users';
|
|
454
|
+
|
|
455
|
+
export const router = defineRouter({
|
|
456
|
+
getUser,
|
|
457
|
+
createUser,
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
// src/server/routes/users.ts
|
|
463
|
+
import { route } from '@spfn/core/route';
|
|
464
|
+
|
|
465
|
+
export const getUser = route.get('/users/:id')/* ...contract/handler */;
|
|
466
|
+
export const createUser = route.post('/users')/* ...contract/handler */;
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// app/api/rpc/[routeName]/route.ts — RPC proxy consumes the generated map
|
|
471
|
+
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
472
|
+
import { routeMap } from '@/generated/route-map';
|
|
473
|
+
|
|
474
|
+
// routeMap (from src/generated/route-map.ts) lets the proxy resolve method + path
|
|
475
|
+
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
spfn codegen run # writes src/generated/route-map.ts
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Types reference
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
interface CodegenConfig { generators?: GeneratorConfig[]; }
|
|
488
|
+
|
|
489
|
+
type GeneratorConfig =
|
|
490
|
+
| { path: string } // file-based
|
|
491
|
+
| ({ name: string; enabled?: boolean } & Record<string, any>); // package-based
|
|
492
|
+
|
|
493
|
+
type GeneratorTrigger = 'watch' | 'manual' | 'build' | 'start';
|
|
494
|
+
|
|
495
|
+
interface Generator
|
|
496
|
+
{
|
|
497
|
+
name: string;
|
|
498
|
+
watchPatterns: string[];
|
|
499
|
+
runOn?: GeneratorTrigger[]; // default ['watch', 'manual', 'build']
|
|
500
|
+
generate(options: GeneratorOptions): Promise<void>;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface RouteMapGeneratorConfig
|
|
504
|
+
{
|
|
505
|
+
name: '@spfn/core:route-map';
|
|
506
|
+
routerPath: string;
|
|
507
|
+
outputPath?: string; // default './src/generated/route-map.ts'
|
|
508
|
+
additionalRouteDirs?: string[];
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Related
|
|
513
|
+
|
|
514
|
+
- [@spfn/core/route](../route/README.md) — `route.*` / `defineRouter` (the source parsed)
|
|
515
|
+
- [@spfn/core/nextjs](../nextjs/README.md) — RPC proxy (`createRpcProxy`) that consumes `routeMap`
|
|
516
|
+
- [@spfn/core/env](../env/README.md) — environment configuration
|