@spfn/core 0.3.0-beta.28 → 0.3.0-beta.29
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/dist/codegen/index.d.ts +65 -2
- package/dist/codegen/index.js +160 -15
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/{error-TsUIjp_x.d.ts → error-CIZlYJlL.d.ts} +1 -1
- package/dist/ops/index.d.ts +3 -3
- package/dist/ops/index.js +15 -5
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +2 -2
- package/dist/route/index.js +15 -5
- package/dist/route/index.js.map +1 -1
- package/dist/{router-4YoIy5fN.d.ts → router-C57y-vz1.d.ts} +12 -0
- package/dist/server/index.js +15 -5
- package/dist/server/index.js.map +1 -1
- package/dist/state/index.js +8 -3
- package/dist/state/index.js.map +1 -1
- package/dist/surface/index.d.ts +3 -3
- package/dist/surface/index.js +15 -5
- package/dist/surface/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen/README.md +89 -2
- package/src/state/README.md +67 -0
package/src/codegen/README.md
CHANGED
|
@@ -52,14 +52,16 @@ Types:
|
|
|
52
52
|
- `OrchestratorOptions`
|
|
53
53
|
- `RouteMapGeneratorConfig` (config shape for the built-in route-map generator)
|
|
54
54
|
- `ContractGeneratorConfig` (config shape for the built-in contract generator)
|
|
55
|
-
- `
|
|
55
|
+
- `StatesGeneratorConfig` (config shape for the built-in states generator)
|
|
56
|
+
- `ContractGeneratorError`, `StatesGeneratorError`, `ConditionalRegistrationError`,
|
|
57
|
+
`assertUnconditionalRegistration`
|
|
56
58
|
- `RouteContractMapping`, `ResourceRoutes`, `ClientGenerationOptions`, `GenerationStats`
|
|
57
59
|
(legacy client-generation types — exported but not used by any shipped generator)
|
|
58
60
|
|
|
59
61
|
> **Renamed: `defineCodegenConfig` → `defineConfig`.** The old name does **not** exist.
|
|
60
62
|
> Older docs also show an `api-client` / `createApi`-emitting generator and
|
|
61
63
|
> `codegen.config.ts` — those are **removed**. The built-in generators today are
|
|
62
|
-
> `@spfn/core:route-map` and `@spfn/core:
|
|
64
|
+
> `@spfn/core:route-map`, `@spfn/core:contract` and `@spfn/core:states`, configured in `.spfnrc.ts`. Do not
|
|
63
65
|
> import `defineCodegenConfig` or configure a `name: 'api-client'` generator.
|
|
64
66
|
|
|
65
67
|
---
|
|
@@ -320,6 +322,83 @@ make the feature unusable.
|
|
|
320
322
|
|
|
321
323
|
---
|
|
322
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
|
+
|
|
323
402
|
## CLI
|
|
324
403
|
|
|
325
404
|
Provided by the `spfn` CLI (`@spfn/cli`), not by `@spfn/core` itself:
|
|
@@ -625,10 +704,18 @@ interface RouteMapGeneratorConfig
|
|
|
625
704
|
outputPath?: string; // default './src/generated/route-map.ts'
|
|
626
705
|
additionalRouteDirs?: string[]; // deprecated: watched, never collected from
|
|
627
706
|
}
|
|
707
|
+
|
|
708
|
+
interface StatesGeneratorConfig
|
|
709
|
+
{
|
|
710
|
+
name: '@spfn/core:states';
|
|
711
|
+
routerPath: string;
|
|
712
|
+
outputPath?: string; // default './src/generated/states.ts'
|
|
713
|
+
}
|
|
628
714
|
```
|
|
629
715
|
|
|
630
716
|
## Related
|
|
631
717
|
|
|
632
718
|
- [@spfn/core/route](../route/README.md) — `route.*` / `defineRouter` (the router loaded and walked)
|
|
633
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
|
|
634
721
|
- [@spfn/core/env](../env/README.md) — environment configuration
|
package/src/state/README.md
CHANGED
|
@@ -196,6 +196,11 @@ With the default `path` `/state`:
|
|
|
196
196
|
|
|
197
197
|
`data` is present exactly when `.data()` is declared, in every response and frame.
|
|
198
198
|
|
|
199
|
+
The derived router also carries the manifest as a non-enumerable `Router._stateManifest` — the
|
|
200
|
+
object `getStates` answers as `manifest`, carried through `.use()` / `.packages()` /
|
|
201
|
+
`.contractVersion()` — which `@spfn/core:states` reads to write the declared ids
|
|
202
|
+
([Declared ids at runtime](#declared-ids-at-runtime)).
|
|
203
|
+
|
|
199
204
|
Route names are `get<Id>State` / `put<Id>State` with the id's first letter upper-cased, so an
|
|
200
205
|
id must match `^[a-z][a-zA-Z0-9]*$` — `defineState` refuses another.
|
|
201
206
|
|
|
@@ -498,6 +503,68 @@ The manifest may also come from `initial` — the snapshot the server rendered w
|
|
|
498
503
|
client knows it from construction: nothing above waits, a refused `subscribe` goes to `onError` at
|
|
499
504
|
once, and the first `subscribe` opens the stream without reading the snapshot first.
|
|
500
505
|
|
|
506
|
+
### Declared ids at runtime
|
|
507
|
+
|
|
508
|
+
The manifest says what the **server** serves; nothing the browser executes says what the **app**
|
|
509
|
+
declared — `typeof states` is erased at build, and importing the router would pull every state's
|
|
510
|
+
`read()`, with its repositories and database, into the layout. The `@spfn/core:states` generator
|
|
511
|
+
writes those ids and their manifest into a file with no import:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
// .spfnrc.ts
|
|
515
|
+
import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
516
|
+
import type { StatesGeneratorConfig } from '@spfn/core/codegen';
|
|
517
|
+
|
|
518
|
+
export default defineConfig({
|
|
519
|
+
generators: [
|
|
520
|
+
defineGenerator<StatesGeneratorConfig>({
|
|
521
|
+
name: '@spfn/core:states',
|
|
522
|
+
routerPath: './src/server/router.ts', // the router route-map reads
|
|
523
|
+
outputPath: './src/generated/states.ts', // the default
|
|
524
|
+
}),
|
|
525
|
+
],
|
|
526
|
+
});
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
An app lists its `@spfn/core:route-map` generator in the same `generators` array.
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// src/generated/states.ts — DO NOT EDIT
|
|
533
|
+
export const stateIds = ['consent', 'member', 'workspaces'] as const;
|
|
534
|
+
export type StateId = (typeof stateIds)[number];
|
|
535
|
+
export const stateManifest: Readonly<Record<StateId, { readonly key: readonly string[]; readonly write: boolean }>> = {
|
|
536
|
+
consent: { key: [], write: true },
|
|
537
|
+
member: { key: ['workspaceId'], write: false },
|
|
538
|
+
workspaces: { key: [], write: false },
|
|
539
|
+
};
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
An app root compares it with the snapshot it read before rendering, so a deploy skew between the
|
|
543
|
+
Next.js server and the API is reported there instead of as `state.<id>` being `undefined` in a screen:
|
|
544
|
+
|
|
545
|
+
```tsx
|
|
546
|
+
const snapshot = await api.getStates.call({ query });
|
|
547
|
+
const skewed = stateIds.filter(id => JSON.stringify(snapshot.manifest[id]) !== JSON.stringify(stateManifest[id]));
|
|
548
|
+
|
|
549
|
+
if (skewed.length > 0)
|
|
550
|
+
{
|
|
551
|
+
return <DeploySkew states={skewed} />;
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
That goes from declared to served. The reverse — an id the server serves that this build lacks —
|
|
556
|
+
indexes `stateManifest` with a `string`, which does not compile under `strict`.
|
|
557
|
+
`Array.prototype.includes` is not a type guard, so declare one:
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
const isStateId = (id: string): id is StateId => (stateIds as readonly string[]).includes(id);
|
|
561
|
+
|
|
562
|
+
const unknownToThisBuild = Object.keys(snapshot.manifest).filter(id => !isStateId(id));
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
The generator refuses a router that mounts no state router, and two state routers declaring one
|
|
566
|
+
id; see [`../codegen/README.md`](../codegen/README.md) → "Built-in: `@spfn/core:states`".
|
|
567
|
+
|
|
501
568
|
### `createStateClient<typeof states>({ sse, api?, onError?, retryDelay?, maxRetries?, initial? })`
|
|
502
569
|
|
|
503
570
|
`initial: { snapshot, query? }` is the snapshot a server render read — `api.getStates`'s answer —
|