brustjs 0.1.24-alpha → 0.1.25-alpha
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/example/pokedex/actions.ts +10 -2
- package/example/pokedex/app.css +30 -30
- package/example/pokedex/components/AddToTeamButton.tsx +1 -2
- package/example/pokedex/components/AppLayout.tsx +5 -1
- package/example/pokedex/components/ThemeToggle.tsx +51 -0
- package/example/pokedex/lib/loaders.ts +14 -6
- package/example/pokedex/lib/pokeapi.ts +7 -5
- package/example/pokedex/lib/types.ts +1 -0
- package/package.json +9 -8
- package/runtime/cookies.ts +61 -0
- package/runtime/define-actions.ts +34 -7
- package/runtime/index.js +52 -52
- package/runtime/index.ts +9 -0
- package/runtime/loader-cache.ts +42 -0
- package/runtime/request-context.ts +35 -0
- package/runtime/routes.ts +96 -25
- package/runtime/treaty.ts +47 -10
- package/runtime/treaty.type-test.ts +69 -0
- package/runtime/tsconfig.typecheck.json +15 -0
package/runtime/treaty.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionsBuilder } from './define-actions.ts'
|
|
1
|
+
import type { ActionsBuilder, EndpointEntry } from './define-actions.ts'
|
|
2
2
|
|
|
3
3
|
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head'])
|
|
4
4
|
|
|
@@ -15,12 +15,6 @@ export interface ClientOptions {
|
|
|
15
15
|
fetch?: typeof fetch
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
type FirstSegment<S extends string> = S extends `/${infer Rest}`
|
|
19
|
-
? FirstSegment<Rest>
|
|
20
|
-
: S extends `${infer T}/${infer _U}`
|
|
21
|
-
? T
|
|
22
|
-
: S
|
|
23
|
-
|
|
24
18
|
export type PermissiveProxy = {
|
|
25
19
|
(arg?: any): PermissiveProxy
|
|
26
20
|
[key: string]: PermissiveProxy
|
|
@@ -33,10 +27,53 @@ export type PermissiveProxy = {
|
|
|
33
27
|
head: (options?: any) => Promise<TreatyResponse<any, any>>
|
|
34
28
|
}
|
|
35
29
|
|
|
30
|
+
// ─── Typed Treaty<App> (B2, descoped) ───────────────────────────────────────
|
|
31
|
+
// Static paths get fully-typed methods (output + discriminated error union);
|
|
32
|
+
// any param path (`{...}`) or unknown segment falls through to PermissiveProxy.
|
|
33
|
+
// Reference shape proven by spike5 (EXIT=0).
|
|
34
|
+
|
|
35
|
+
/** Per-method client signatures for one endpoint-entry map (the `{ GET, POST, … }`
|
|
36
|
+
* object for a single path). Bodyless methods (GET/HEAD) take only options. */
|
|
37
|
+
type Methods<E> = {
|
|
38
|
+
[M in keyof E & string as Lowercase<M>]: M extends 'GET' | 'HEAD'
|
|
39
|
+
? E[M] extends EndpointEntry
|
|
40
|
+
? (o?: {
|
|
41
|
+
query?: Record<string, string>
|
|
42
|
+
headers?: Record<string, string>
|
|
43
|
+
}) => Promise<TreatyResponse<E[M]['output'], E[M]['error']>>
|
|
44
|
+
: never
|
|
45
|
+
: E[M] extends EndpointEntry
|
|
46
|
+
? (
|
|
47
|
+
b?: E[M]['input'],
|
|
48
|
+
o?: { headers?: Record<string, string> },
|
|
49
|
+
) => Promise<TreatyResponse<E[M]['output'], E[M]['error']>>
|
|
50
|
+
: never
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A path is static iff it contains no `{param}` segment. */
|
|
54
|
+
type IsStatic<K extends string> = K extends `${string}{${string}}${string}` ? false : true
|
|
55
|
+
/** Keep only the static path keys of the accumulator. */
|
|
56
|
+
type StaticAcc<Acc> = {
|
|
57
|
+
[K in keyof Acc as K extends string ? (IsStatic<K> extends true ? K : never) : never]: Acc[K]
|
|
58
|
+
}
|
|
59
|
+
/** Given a path key `K` and the accumulated prefix `P`, the next segment after `P`. */
|
|
60
|
+
type Tail<K extends string, P extends string> = K extends `${P}/${infer Rest}`
|
|
61
|
+
? Rest extends `${infer H}/${string}`
|
|
62
|
+
? H
|
|
63
|
+
: Rest
|
|
64
|
+
: never
|
|
65
|
+
/** All immediate child segments under prefix `P`. */
|
|
66
|
+
type ChildSegs<SA, P extends string> = { [K in keyof SA]: Tail<K & string, P> }[keyof SA]
|
|
67
|
+
/** The entry map for the exact path `P`, if one exists. */
|
|
68
|
+
// biome-ignore lint/complexity/noBannedTypes: `{}` is the empty-methods identity when no exact endpoint matches the prefix
|
|
69
|
+
type ExactEntry<SA, P extends string> = P extends keyof SA ? SA[P] : {}
|
|
70
|
+
/** A treaty node: methods of the exact path + child segment nodes + permissive fallback. */
|
|
71
|
+
type TNode<SA, P extends string> = Methods<ExactEntry<SA, P>> & {
|
|
72
|
+
[Seg in ChildSegs<SA, P> & string]: TNode<SA, `${P}/${Seg}`>
|
|
73
|
+
} & PermissiveProxy
|
|
74
|
+
|
|
36
75
|
export type Treaty<App> =
|
|
37
|
-
App extends ActionsBuilder<infer Acc>
|
|
38
|
-
? { [K in FirstSegment<keyof Acc & string>]: PermissiveProxy } & PermissiveProxy
|
|
39
|
-
: PermissiveProxy
|
|
76
|
+
App extends ActionsBuilder<infer Acc> ? TNode<StaticAcc<Acc>, ''> : PermissiveProxy
|
|
40
77
|
|
|
41
78
|
function resolvePrefix(opts?: ClientOptions): string {
|
|
42
79
|
if (opts?.prefix) return opts.prefix
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Isolated TYPE TEST for the B2 typed treaty client. Not executed — every
|
|
2
|
+
// assertion is a pure type check inside an un-called async function. Run via:
|
|
3
|
+
// bun run typecheck:treaty → tsc -p tsconfig.typecheck.json --noEmit
|
|
4
|
+
//
|
|
5
|
+
// It exercises: (1) static-path methods are fully typed (output + error union),
|
|
6
|
+
// (2) the discriminated error union narrows on `code`, (3) accessing a
|
|
7
|
+
// non-existent output field is a type error, (4) a wrong error code / field is a
|
|
8
|
+
// type error, (5) param paths stay permissive (no type error, the fallback).
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import { defineActions } from './define-actions.ts'
|
|
11
|
+
import { client } from './treaty.ts'
|
|
12
|
+
|
|
13
|
+
const actions = defineActions()
|
|
14
|
+
.get('/team', () => ({ team: [{ id: 1 }], max: 6 }))
|
|
15
|
+
.post('/team', ({ body }: { body: { id: number } }) => ({ team: [{ id: body.id }], max: 6 }), {
|
|
16
|
+
body: z.object({ id: z.number() }),
|
|
17
|
+
errors: {
|
|
18
|
+
TEAM_FULL: z.object({ max: z.number() }),
|
|
19
|
+
DUPLICATE: z.object({ existingId: z.number() }),
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
.delete('/team/{id}', () => ({ ok: true }))
|
|
23
|
+
|
|
24
|
+
const api = client<typeof actions>()
|
|
25
|
+
|
|
26
|
+
// Never called — purely for type checking.
|
|
27
|
+
export async function _typeTest() {
|
|
28
|
+
// (1) static POST → output typed.
|
|
29
|
+
const r = await api.team.post({ id: 5 })
|
|
30
|
+
if (r.data) {
|
|
31
|
+
const max: number = r.data.max
|
|
32
|
+
void max
|
|
33
|
+
// (3) non-existent output field → must be a type error.
|
|
34
|
+
// @ts-expect-error `nope` is not a field of the typed output
|
|
35
|
+
void r.data.nope
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// (2) error union narrows on the discriminant `code` to the SPECIFIC branch,
|
|
39
|
+
// and that branch's `data` is typed (multi-member union: TEAM_FULL | DUPLICATE).
|
|
40
|
+
if (r.error && r.error.value.code === 'TEAM_FULL') {
|
|
41
|
+
const m: number = r.error.value.data.max
|
|
42
|
+
void m
|
|
43
|
+
// (4a) non-existent error-data field → must be a type error.
|
|
44
|
+
// @ts-expect-error `min` is not a field of the TEAM_FULL error data
|
|
45
|
+
void r.error.value.data.min
|
|
46
|
+
// (4c) narrowing is per-branch: DUPLICATE's `existingId` is NOT on TEAM_FULL.
|
|
47
|
+
// @ts-expect-error `existingId` belongs to the DUPLICATE branch, not TEAM_FULL
|
|
48
|
+
void r.error.value.data.existingId
|
|
49
|
+
}
|
|
50
|
+
if (r.error && r.error.value.code === 'DUPLICATE') {
|
|
51
|
+
const e: number = r.error.value.data.existingId
|
|
52
|
+
void e
|
|
53
|
+
}
|
|
54
|
+
// (4b) comparing against an undeclared error code → must be a type error.
|
|
55
|
+
if (r.error) {
|
|
56
|
+
// @ts-expect-error 'NOPE' is not a declared error code
|
|
57
|
+
void (r.error.value.code === 'NOPE')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// GET → output typed.
|
|
61
|
+
const g = await api.team.get()
|
|
62
|
+
if (g.data) {
|
|
63
|
+
const max2: number = g.data.max
|
|
64
|
+
void max2
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// (5) param path stays permissive — MUST NOT error (locks the fallback).
|
|
68
|
+
await api.team({ id: '5' }).delete()
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"skipLibCheck": true,
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"types": []
|
|
13
|
+
},
|
|
14
|
+
"files": ["treaty.ts", "define-actions.ts", "standard-schema.ts", "treaty.type-test.ts"]
|
|
15
|
+
}
|