@spfn/core 0.3.0-beta.13 → 0.3.0-beta.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/core",
3
- "version": "0.3.0-beta.13",
3
+ "version": "0.3.0-beta.14",
4
4
  "description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
5
5
  "type": "module",
6
6
  "exports": {
@@ -153,9 +153,13 @@ way `registerRoutes` walks it. How the router is *written* does not matter: a on
153
153
  server registers them.
154
154
  - `method` and `path` come from the `RouteDef`, not from a pattern over the source. A
155
155
  route not registered in the router is absent, because there is nothing registering it.
156
- - **`.packages()` routers are left out**, at every depth. A package publishes its own
157
- route map and the app merges them (`{ ...routeMap, ...authRouteMap }`). Their *names*
158
- are still read, because that merge is what a collision turns on — see below.
156
+ - **`.packages()` routers are carried**, at every depth, provided the router publishes a
157
+ route map (`defineRouter`, not `defineUnmappedRouter`). A package ships a client that
158
+ names its own routes — `authApi.login`, `monitorApi.getStats` — and the app's
159
+ `createRpcProxy` resolves a name in the one map it was constructed with, so the app
160
+ merges nothing by hand: `{ ...routeMap, ...authRouteMap }` is a no-op today. They are
161
+ written after the app's own routes, under a comment line naming where they came from,
162
+ and the header says how many there are and which mount each came through.
159
163
  - A name that is not a JS identifier (`'get-user'`) is emitted quoted, and `__proto__` is
160
164
  emitted as a computed key, so the generated file parses and the map keeps every route as
161
165
  an own property.
@@ -202,16 +206,20 @@ Unlike the old source parser, which dropped what it could not read, the generato
202
206
  line in the diff rather than a route that vanishes at build.
203
207
  - Two routes reach the same name (the message names both places).
204
208
  - An **app route and a package route** reach the same name, where that package publishes a
205
- route map. Both register at runtime, and the app's proxy merges
206
- `{ ...routeMap, ...authRouteMap }` — so the package's route wins the name while the
207
- generated types still describe the app's. Two *package* routers sharing a name is not
208
- refused: which one wins is the app's merge order, which this generator neither sees nor
209
- writes. Nor is a collision with a router that publishes **no** map — one declared with
209
+ route map. Both register at runtime, and both want the single entry that name has in this
210
+ map — so one of the two paths becomes unreachable while the generated types still
211
+ describe the app's route. **Two package routers** reaching one name is refused too, and
212
+ names both mounts: the generator writes the order now, so the ambiguity is its to refuse.
213
+ The same route reached through two mounts is not two routes — a package exporting one set
214
+ of routes under a router and a sub-router an app mounts beside it hands both mounts the
215
+ same route object, and it is collected once. No first-party package ships that shape today;
216
+ the rule is keyed on object identity so that an app mounting both still builds when one
217
+ does, and the header line of the second mount says which mount got there first.
218
+ A collision with a router that publishes **no** map is not refused — one declared with
210
219
  `defineUnmappedRouter`, whose routes are reached by the URL something was handed rather
211
- than by name: the ops surface (`createOpsRouter`), `@spfn/monitor`, `@spfn/cms` and the
212
- tracking routes of `@spfn/notification`. Nothing of theirs merges over the app's map, so
213
- an app route named after one of their routes overwrites nothing and must not fail the
214
- build.
220
+ than by name: the ops surface (`createOpsRouter`) and the tracking routes of
221
+ `@spfn/notification`. Nothing of theirs is written here, so an app route named after one
222
+ of their routes keeps its own path and must not fail the build.
215
223
  - A route's `method` is not an `HttpMethod`. `registerRoutes` lowercases the method before
216
224
  registering, so `method: 'get'` serves fine and would emit a map that fails `tsc`.
217
225
 
@@ -222,8 +230,9 @@ registering a route before `.handler()` was called), and a router entry that is
222
230
  route nor a router. `registerRoutes` warns and skips both, so a map without them still names
223
231
  every route the server answers — which is all a refusal would have been protecting.
224
232
 
225
- What a *package* router carries is never refused — an entry the app developer cannot fix
226
- is skipped, exactly as `registerRoutes` skips it.
233
+ A package route with no method or path is skipped the same way, and for the same reason.
234
+ What is refused is only what would write a file that does not compile or that answers to
235
+ the wrong path: a method that is not an `HttpMethod`, and the two collisions above.
227
236
 
228
237
  ### Generated output
229
238
 
@@ -240,6 +249,8 @@ export interface RouteInfo
240
249
  export const routeMap: Record<string, RouteInfo> = {
241
250
  getUser: { method: 'GET', path: '/users/:id' },
242
251
  createUser: { method: 'POST', path: '/users' },
252
+ // From the package routers this app mounts with .packages(), not its own:
253
+ login: { method: 'POST', path: '/_auth/login' },
243
254
  };
244
255
 
245
256
  export type RouteMap = typeof routeMap;
@@ -484,7 +495,10 @@ regeneration when it's absent (build/manual passes don't set it).
484
495
  output the RPC proxy imports. If it's missing or stale, `api.<route>.call()` resolves the
485
496
  wrong (or no) `method`/`path`. `spfn build` runs codegen first; if you build by other
486
497
  means, run `spfn codegen run` yourself. Treat the file as generated (it's marked
487
- `DO NOT EDIT`).
498
+ `DO NOT EDIT`). **Upgrading a mounted package goes stale the same way and is quieter about
499
+ it**: the map carries that package's routes, but the upgrade changes no app source, so no
500
+ watcher fires and nothing else says the committed map is a version behind — re-run codegen
501
+ after an upgrade as you would after adding a route.
488
502
  - **A route must be in `defineRouter({...})` to be emitted.** Defining `export const foo =
489
503
  route.get(...)` but not registering `foo` in the router drops it from the map — the
490
504
  generator walks the router, so an unregistered route does not exist as far as it is
@@ -292,24 +292,26 @@ type RouteMap = Record<string, RouteMapEntry>;
292
292
 
293
293
  ### `routeMap` — the proxy's source of truth
294
294
 
295
- `routeMap` comes from codegen (`@/generated/route-map`). Merge in route maps exported by
296
- SPFN packages so their endpoints are reachable through the same proxy:
295
+ `routeMap` comes from codegen (`@/generated/route-map`) and already holds the routes of every
296
+ package router the app router mounts with `.packages()` — `@spfn/auth`'s included, so
297
+ `{ ...routeMap, ...authRouteMap }` is a no-op and no longer needed. `eventRouteMap` is still
298
+ merged by hand: it is a hand-written constant, not a mounted router.
297
299
 
298
300
  ```typescript
299
301
  // app/api/rpc/[routeName]/route.ts
300
302
  import '@spfn/auth/nextjs/api'; // side-effect: registers auth interceptors
301
303
  import { createRpcProxy } from '@spfn/core/nextjs/server';
302
- import { authRouteMap } from '@spfn/auth';
303
304
  import { eventRouteMap } from '@spfn/core/event';
304
305
  import { routeMap } from '@/generated/route-map';
305
306
 
306
307
  export const { GET, POST } = createRpcProxy({
307
- routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },
308
+ routeMap: { ...routeMap, ...eventRouteMap },
308
309
  });
309
310
  ```
310
311
 
311
- A `routeName` absent from the merged `routeMap` returns **404** from the proxy (not from the
312
- backend). After adding routes, re-run codegen and clear `.spfn` cache if stale.
312
+ A `routeName` absent from `routeMap` returns **404** from the proxy (not from the backend).
313
+ After adding routes — or after upgrading a mounted package — re-run codegen and clear `.spfn`
314
+ cache if stale.
313
315
 
314
316
  ### Header forwarding & client IP
315
317
 
@@ -341,7 +343,7 @@ to your LB/nginx depth) rather than widening the header allowlist — forwarding
341
343
 
342
344
  ```typescript
343
345
  export const { GET, POST } = createRpcProxy({
344
- routeMap: { ...routeMap, ...authRouteMap },
346
+ routeMap: { ...routeMap, ...eventRouteMap },
345
347
  apiUrl: process.env.SPFN_API_URL,
346
348
  timeout: 60000,
347
349
  debug: true,
@@ -595,9 +597,10 @@ export async function createUser(formData: FormData) {
595
597
  - **The proxy resolves the real HTTP method from `routeMap`, not from the client call.** The
596
598
  client only ever sends GET (no body) or POST (body/formData) to `/api/rpc/...`. A PUT /
597
599
  PATCH / DELETE route still works — the backend method comes from `routeMap[routeName]`.
598
- - **A missing `routeName` in `routeMap` is a proxy 404, not a backend 404.** Merge package
599
- route maps (`authRouteMap`, `eventRouteMap`, …) and re-run codegen after adding routes;
600
- delete `.spfn` if the map looks stale.
600
+ - **A missing `routeName` in `routeMap` is a proxy 404, not a backend 404.** Re-run codegen
601
+ after adding routes or upgrading a mounted package — the generated map carries the mounted
602
+ packages' routes — merge `eventRouteMap` if you use SSE, and delete `.spfn` if the map
603
+ looks stale.
601
604
  - **Interceptor `pathPattern` matches the resolved backend path** (e.g. `/users/123`,
602
605
  `/_auth/login`) — *not* the `/api/rpc/{routeName}` URL the client hit.
603
606
  - **Two same-named interceptor type pairs.** `RequestInterceptor`/`ResponseInterceptor` from
@@ -458,19 +458,24 @@ export const appRouter = defineRouter({ getRoot, getStatus })
458
458
 
459
459
  `.packages()` also flattens any nested package routers the given routers themselves declared.
460
460
 
461
+ A mounted package's routes are written into the app's generated route map, so the app merges
462
+ nothing by hand — `createRpcProxy({ routeMap })` resolves `authApi.login` as it resolves the
463
+ app's own names.
464
+
461
465
  **A name a package route already uses is not available to an app route.** Both register, at
462
- their own paths, and the app's proxy merges the two maps as `{ ...routeMap, ...authRouteMap }`
463
- — so the package entry wins the name while the generated types still describe the app's
464
- route. `spfn codegen run` refuses rather than generating that, naming both sides.
466
+ their own paths, but the map holds one entry per name: one of the two paths becomes
467
+ unreachable while the generated types still describe the app's route. `spfn codegen run`
468
+ refuses rather than generating that, naming both sides. Two package routers reaching one
469
+ name is refused the same way.
465
470
 
466
471
  A package that publishes **no** route map is the exception, and declares itself one with
467
472
  `defineUnmappedRouter`: its routes are reached by the URL something was handed rather than by
468
- name, so nothing merges over the app's map and an app route may share a name with one of them
469
- freely. `createOpsRouter` builds the ops surface that way — `spfn ops` invokes a command over
470
- its URL — as do `@spfn/monitor`, `@spfn/cms` and the tracking routes of `@spfn/notification`.
471
- `defineRouter` is the default because the obligation runs the other way: a package that does
472
- publish a map must not declare itself unmapped, or a collision that really would overwrite an
473
- app route generates silently.
473
+ name, so nothing of theirs is written into the app's map and an app route may share a name
474
+ with one of them freely. `createOpsRouter` builds the ops surface that way — `spfn ops`
475
+ invokes a command over its URL — as do the tracking routes of `@spfn/notification`.
476
+ `defineRouter` is the default because the obligation runs the other way: a package whose
477
+ client names its own routes (`@spfn/auth`, `@spfn/monitor`, `@spfn/cms`) must not declare
478
+ itself unmapped, or those names land in nobody's map and resolve nowhere.
474
479
 
475
480
  ### `.use([...])` — router-level global middleware
476
481