effect-start 0.15.0 → 0.16.0

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/src/Http.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import * as Values from "./Values.ts"
2
+
1
3
  export type Method =
2
4
  | "GET"
3
5
  | "POST"
@@ -7,19 +9,159 @@ export type Method =
7
9
  | "HEAD"
8
10
  | "OPTIONS"
9
11
 
10
- export type WebHandler = (request: Request) => Response | Promise<Response>
12
+ type Respondable =
13
+ | Response
14
+ | Promise<Response>
15
+
16
+ export type WebHandler = (request: Request) => Respondable
11
17
 
12
- export function cloneRequest<T extends object>(
18
+ export type WebMiddleware = (
13
19
  request: Request,
14
- props: T,
15
- ): Request & T {
16
- const cloned = new Request(request.url, {
17
- method: request.method,
18
- headers: request.headers,
19
- body: request.body,
20
+ next: WebHandler,
21
+ ) => Respondable
22
+
23
+ export function fetch(
24
+ handler: WebHandler,
25
+ init:
26
+ & Omit<RequestInit, "body">
27
+ & (
28
+ | { url: string }
29
+ | { path: `/${string}` }
30
+ )
31
+ & { body?: RequestInit["body"] | Record<string, unknown> },
32
+ ): Promise<Response> {
33
+ const url = "path" in init
34
+ ? `http://localhost${init.path}`
35
+ : init.url
36
+
37
+ const isPlain = Values.isPlainObject(init.body)
38
+
39
+ const headers = new Headers(init.headers)
40
+ if (isPlain && !headers.has("Content-Type")) {
41
+ headers.set("Content-Type", "application/json")
42
+ }
43
+
44
+ const body = isPlain ? JSON.stringify(init.body) : init.body
45
+
46
+ const request = new Request(url, {
47
+ ...init,
48
+ headers,
49
+ body: body as BodyInit,
20
50
  })
21
- for (const [key, value] of Object.entries(props)) {
22
- ;(cloned as any)[key] = value
51
+ return Promise.resolve(handler(request))
52
+ }
53
+
54
+ export function createAbortableRequest(
55
+ init:
56
+ & Omit<RequestInit, "signal">
57
+ & (
58
+ | { url: string }
59
+ | { path: `/${string}` }
60
+ ),
61
+ ): { request: Request; abort: () => void } {
62
+ const url = "path" in init
63
+ ? `http://localhost${init.path}`
64
+ : init.url
65
+ const controller = new AbortController()
66
+ const request = new Request(url, { ...init, signal: controller.signal })
67
+ return { request, abort: () => controller.abort() }
68
+ }
69
+
70
+ export function mapHeaders(
71
+ headers: Headers,
72
+ ): Record<string, string | undefined> {
73
+ const result: Record<string, string | undefined> = {}
74
+ headers.forEach((value, key) => {
75
+ result[key.toLowerCase()] = value
76
+ })
77
+ return result
78
+ }
79
+
80
+ export function parseCookies(
81
+ cookieHeader: string | null,
82
+ ): Record<string, string | undefined> {
83
+ if (!cookieHeader) return {}
84
+ const result: Record<string, string | undefined> = {}
85
+ for (const part of cookieHeader.split(";")) {
86
+ const idx = part.indexOf("=")
87
+ if (idx === -1) {
88
+ // Cookie without value (e.g., "name" or just whitespace)
89
+ const key = part.trim()
90
+ if (key) {
91
+ result[key] = undefined
92
+ }
93
+ } else {
94
+ const key = part.slice(0, idx).trim()
95
+ const value = part.slice(idx + 1).trim()
96
+ if (key) {
97
+ result[key] = value
98
+ }
99
+ }
23
100
  }
24
- return cloned as Request & T
101
+ return result
102
+ }
103
+
104
+ export function mapUrlSearchParams(
105
+ params: URLSearchParams,
106
+ ): Record<string, string | ReadonlyArray<string> | undefined> {
107
+ const result: Record<string, string | ReadonlyArray<string> | undefined> = {}
108
+ for (const key of new Set(params.keys())) {
109
+ const values = params.getAll(key)
110
+ result[key] = values.length === 1 ? values[0] : values
111
+ }
112
+ return result
113
+ }
114
+
115
+ export interface FilePart {
116
+ readonly _tag: "File"
117
+ readonly key: string
118
+ readonly name: string
119
+ readonly contentType: string
120
+ readonly content: Uint8Array
121
+ }
122
+
123
+ export interface FieldPart {
124
+ readonly _tag: "Field"
125
+ readonly key: string
126
+ readonly value: string
127
+ }
128
+
129
+ export type MultipartPart = FilePart | FieldPart
130
+
131
+ export async function parseFormData(
132
+ request: Request,
133
+ ): Promise<
134
+ Record<string, ReadonlyArray<FilePart> | ReadonlyArray<string> | string>
135
+ > {
136
+ const formData = await request.formData()
137
+ const result: Record<
138
+ string,
139
+ ReadonlyArray<FilePart> | ReadonlyArray<string> | string
140
+ > = {}
141
+
142
+ for (const key of new Set(formData.keys())) {
143
+ const values = formData.getAll(key)
144
+ const first = values[0]
145
+
146
+ if (typeof first === "string") {
147
+ result[key] = values.length === 1 ? first : (values as string[])
148
+ } else {
149
+ const files: FilePart[] = []
150
+ for (const value of values) {
151
+ if (typeof value !== "string") {
152
+ const content = new Uint8Array(await value.arrayBuffer())
153
+ files.push({
154
+ _tag: "File",
155
+ key,
156
+ name: value.name,
157
+ contentType: value.type || "application/octet-stream",
158
+ content,
159
+ })
160
+ }
161
+ }
162
+ result[key] = files
163
+ }
164
+ }
165
+
166
+ return result
25
167
  }
@@ -477,7 +477,9 @@ export function toBun(path: string): PathPattern[] {
477
477
  : `:${optionalName}`
478
478
 
479
479
  const withOptionalSegments = [...before, requiredOptional, ...after]
480
- const withOptionalPath: PathPattern = `/${withOptionalSegments.map(formatSegment).join("/")}`
480
+ const withOptionalPath: PathPattern = `/${
481
+ withOptionalSegments.map(formatSegment).join("/")
482
+ }`
481
483
 
482
484
  return [...toBun(basePath), ...toBun(withOptionalPath)]
483
485
  }
package/src/Route.ts CHANGED
@@ -3,6 +3,7 @@ import type * as Effect from "effect/Effect"
3
3
  import * as Layer from "effect/Layer"
4
4
  import * as Pipeable from "effect/Pipeable"
5
5
  import * as Predicate from "effect/Predicate"
6
+ import type * as Entity from "./Entity.ts"
6
7
  import * as RouteBody from "./RouteBody.ts"
7
8
  import * as RouteTree from "./RouteTree.ts"
8
9
  import * as Values from "./Values.ts"
@@ -99,14 +100,8 @@ export namespace Route {
99
100
 
100
101
  export type Handler<B, A, E, R> = (
101
102
  context: B,
102
- next: (context: B) => Effect.Effect<A>,
103
- ) => Effect.Effect<A, E, R>
104
-
105
- // handler that cannot modify the context
106
- export type HandlerImmutable<B, A, E, R> = (
107
- context: B,
108
- next: () => Effect.Effect<A>,
109
- ) => Effect.Effect<A, E, R>
103
+ next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<A>,
104
+ ) => Effect.Effect<Entity.Entity<A>, E, R>
110
105
 
111
106
  /**
112
107
  * Extracts only the bindings (B) from routes, excluding descriptors.
@@ -246,8 +241,21 @@ export function descriptor<
246
241
  T extends RouteSet.Data<any, any, any>,
247
242
  >(
248
243
  self: T,
249
- ): T[typeof RouteDescriptor] {
250
- return self[RouteDescriptor]
244
+ ): T[typeof RouteDescriptor]
245
+ export function descriptor<
246
+ T extends RouteSet.Data<any, any, any>,
247
+ >(
248
+ self: Iterable<T>,
249
+ ): T[typeof RouteDescriptor][]
250
+ export function descriptor(
251
+ self:
252
+ | RouteSet.Data<any, any, any>
253
+ | Iterable<RouteSet.Data<any, any, any>>,
254
+ ): RouteDescriptor.Any | RouteDescriptor.Any[] {
255
+ if (RouteDescriptor in self) {
256
+ return self[RouteDescriptor]
257
+ }
258
+ return [...self].map((r) => r[RouteDescriptor])
251
259
  }
252
260
 
253
261
  export type ExtractBindings<
@@ -311,6 +319,10 @@ export const bytes = RouteBody.build<Uint8Array, "bytes">({
311
319
  format: "bytes",
312
320
  })
313
321
 
322
+ export {
323
+ render,
324
+ } from "./RouteBody.ts"
325
+
314
326
  export class Routes extends Context.Tag("effect-start/Routes")<
315
327
  Routes,
316
328
  RouteTree.RouteTree
@@ -1,15 +1,14 @@
1
1
  import * as test from "bun:test"
2
2
  import * as Effect from "effect/Effect"
3
+ import * as Stream from "effect/Stream"
4
+ import * as Entity from "./Entity.ts"
5
+ import * as Route from "./Route.ts"
3
6
  import * as RouteBody from "./RouteBody.ts"
4
7
  import * as RouteMount from "./RouteMount.ts"
5
8
 
6
- const text = RouteBody.build<string, "text">({
7
- format: "text",
8
- })
9
-
10
9
  test.it("infers parent descriptions", () => {
11
10
  RouteMount.get(
12
- text((ctx) =>
11
+ Route.text((ctx) =>
13
12
  Effect.gen(function*() {
14
13
  test
15
14
  .expectTypeOf(ctx)
@@ -24,13 +23,12 @@ test.it("infers parent descriptions", () => {
24
23
  )
25
24
  })
26
25
 
27
- test.it("cannot modify context", () => {
28
- text((ctx, next) =>
26
+ test.it("next is function returning Entity", () => {
27
+ Route.text((ctx, next) =>
29
28
  Effect.gen(function*() {
30
29
  test
31
30
  .expectTypeOf(next)
32
- .parameters
33
- .toEqualTypeOf<[]>()
31
+ .toExtend<() => Entity.Entity<string>>()
34
32
 
35
33
  return "Hello, world!"
36
34
  })
@@ -39,24 +37,66 @@ test.it("cannot modify context", () => {
39
37
 
40
38
  test.it("enforces result value", () => {
41
39
  // @ts-expect-error must return string
42
- text((ctx, next) =>
40
+ Route.text((ctx, next) =>
43
41
  Effect.gen(function*() {
44
42
  return 1337
45
43
  })
46
44
  )
47
45
  })
48
46
 
47
+ test.it("accepts text stream", () => {
48
+ RouteMount.get(
49
+ Route.text((ctx) =>
50
+ Effect.gen(function*() {
51
+ test
52
+ .expectTypeOf(ctx)
53
+ .toExtend<{
54
+ method: "GET"
55
+ format: "text"
56
+ }>()
57
+
58
+ return Stream.make("Hello", " ", "world!")
59
+ })
60
+ ),
61
+ )
62
+ })
63
+
64
+ test.it("accepts Effect<Stream<string>> for html format", () => {
65
+ RouteMount.get(
66
+ Route.html(function*() {
67
+ return Stream.make("<div>", "content", "</div>")
68
+ }),
69
+ )
70
+ })
71
+
72
+ test.it("accepts Effect<Stream<Uint8Array>> for bytes format", () => {
73
+ const encoder = new TextEncoder()
74
+
75
+ RouteMount.get(
76
+ Route.bytes(function*() {
77
+ return Stream.make(encoder.encode("chunk"))
78
+ }),
79
+ )
80
+ })
81
+
82
+ test.it("rejects Stream for json format", () => {
83
+ // @ts-expect-error Stream not allowed for json format
84
+ Route.json(function*() {
85
+ return Stream.make({ msg: "hello" })
86
+ })
87
+ })
88
+
49
89
  test.it("accepts value directly", () => {
50
90
  const value = "Hello, world!"
51
91
 
52
92
  test
53
- .expectTypeOf(text)
93
+ .expectTypeOf(Route.text)
54
94
  .toBeCallableWith(value)
55
95
  })
56
96
 
57
97
  test.describe(`${RouteBody.handle.name}()`, () => {
58
98
  const ctx = {}
59
- const next = () => Effect.succeed("next" as const)
99
+ const next = () => Entity.effect(Effect.succeed(Entity.make("next")))
60
100
 
61
101
  test.it("accepts all HandlerInput variants", () => {
62
102
  test
@@ -73,11 +113,12 @@ test.describe(`${RouteBody.handle.name}()`, () => {
73
113
  .expectTypeOf(handler)
74
114
  .returns
75
115
  .toEqualTypeOf<
76
- Effect.Effect<string, never, never>
116
+ Effect.Effect<Entity.Entity<string>, never, never>
77
117
  >()
78
118
 
79
119
  const result = await Effect.runPromise(handler(ctx, next))
80
- test.expect(result).toBe("hello")
120
+ test.expect(result.body).toBe("hello")
121
+ test.expect(result.status).toBe(200)
81
122
  })
82
123
 
83
124
  test.it("handles Effect directly", async () => {
@@ -87,13 +128,13 @@ test.describe(`${RouteBody.handle.name}()`, () => {
87
128
  .expectTypeOf(handler)
88
129
  .returns
89
130
  .toEqualTypeOf<
90
- Effect.Effect<string, never, never>
131
+ Effect.Effect<Entity.Entity<string>, never, never>
91
132
  >()
92
133
 
93
134
  const result = await Effect.runPromise(handler(ctx, next))
94
135
 
95
136
  test
96
- .expect(result)
137
+ .expect(result.body)
97
138
  .toBe("from effect")
98
139
  })
99
140
 
@@ -104,7 +145,7 @@ test.describe(`${RouteBody.handle.name}()`, () => {
104
145
  .expectTypeOf(handler)
105
146
  .returns
106
147
  .toEqualTypeOf<
107
- Effect.Effect<never, Error, never>
148
+ Effect.Effect<Entity.Entity<never>, Error, never>
108
149
  >()
109
150
  })
110
151
 
@@ -117,21 +158,27 @@ test.describe(`${RouteBody.handle.name}()`, () => {
117
158
  .expectTypeOf(handler)
118
159
  .parameters
119
160
  .toEqualTypeOf<
120
- [{ id: number }, () => Effect.Effect<number>]
161
+ [
162
+ { id: number },
163
+ (
164
+ context?: Partial<{ id: number }> & Record<string, unknown>,
165
+ ) => Entity.Entity<number>,
166
+ ]
121
167
  >()
122
168
  test
123
169
  .expectTypeOf(handler)
124
170
  .returns
125
171
  .toEqualTypeOf<
126
- Effect.Effect<number, never, never>
172
+ Effect.Effect<Entity.Entity<number>, never, never>
127
173
  >()
128
174
 
175
+ const numNext = () => Entity.effect(Effect.succeed(Entity.make(23)))
129
176
  const result = await Effect.runPromise(
130
- handler({ id: 42 }, () => Effect.succeed(23)),
177
+ handler({ id: 42 }, numNext),
131
178
  )
132
179
 
133
180
  test
134
- .expect(result)
181
+ .expect(result.body)
135
182
  .toBe(42)
136
183
  })
137
184
 
@@ -145,30 +192,35 @@ test.describe(`${RouteBody.handle.name}()`, () => {
145
192
  .expectTypeOf(handler)
146
193
  .parameters
147
194
  .toEqualTypeOf<
148
- [{ id: number }, () => Effect.Effect<number>]
195
+ [
196
+ { id: number },
197
+ (
198
+ context?: Partial<{ id: number }> & Record<string, unknown>,
199
+ ) => Entity.Entity<number>,
200
+ ]
149
201
  >()
150
202
 
151
203
  test
152
204
  .expectTypeOf(handler)
153
205
  .returns
154
206
  .toEqualTypeOf<
155
- Effect.Effect<number, never, never>
207
+ Effect.Effect<Entity.Entity<number>, never, never>
156
208
  >()
157
209
 
210
+ const numNext = () => Entity.effect(Effect.succeed(Entity.make(23)))
158
211
  const result = await Effect.runPromise(
159
- // TODO: we should accept Effect.void in next here
160
- handler({ id: 21 }, () => Effect.succeed(23)),
212
+ handler({ id: 21 }, numNext),
161
213
  )
162
214
 
163
215
  test
164
- .expect(result)
216
+ .expect(result.body)
165
217
  .toBe(42)
166
218
  })
167
219
 
168
220
  test.it("generator can call next", async () => {
169
221
  const handler = RouteBody.handle(
170
- function*(_ctx: {}, next: () => Effect.Effect<string>) {
171
- const fromNext = yield* next()
222
+ function*(_ctx: {}, next: () => Entity.Entity<string>) {
223
+ const fromNext = yield* next().text
172
224
  return `got: ${fromNext}`
173
225
  },
174
226
  )
@@ -176,7 +228,7 @@ test.describe(`${RouteBody.handle.name}()`, () => {
176
228
  const result = await Effect.runPromise(handler(ctx, next))
177
229
 
178
230
  test
179
- .expect(result)
231
+ .expect(result.body)
180
232
  .toBe("got: next")
181
233
  })
182
234
  })
package/src/RouteBody.ts CHANGED
@@ -1,52 +1,83 @@
1
1
  import * as Effect from "effect/Effect"
2
+ import type * as Stream from "effect/Stream"
2
3
  import type * as Utils from "effect/Utils"
4
+ import * as Entity from "./Entity.ts"
3
5
  import * as Route from "./Route.ts"
6
+ import type * as Values from "./Values.ts"
4
7
 
5
8
  export type Format =
6
9
  | "text"
7
10
  | "html"
8
11
  | "json"
9
12
  | "bytes"
13
+ | "*"
14
+
15
+ const formatToContentType: Record<Format, string | undefined> = {
16
+ text: "text/plain; charset=utf-8",
17
+ html: "text/html; charset=utf-8",
18
+ json: "application/json",
19
+ bytes: "application/octet-stream",
20
+ "*": undefined,
21
+ }
22
+
23
+ type UnwrapStream<T> = T extends Stream.Stream<infer V, any, any> ? V : T
10
24
 
11
25
  export type HandlerInput<B, A, E, R> =
12
26
  | A
13
- | Effect.Effect<A, E, R>
14
- | ((context: _Simplify<B>, next: () => Effect.Effect<A>) =>
15
- | Effect.Effect<A, E, R>
16
- | Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>)
27
+ | Entity.Entity<A>
28
+ | Effect.Effect<A | Entity.Entity<A>, E, R>
29
+ | ((
30
+ context: Values.Simplify<B>,
31
+ next: (
32
+ context?: Partial<B> & Record<string, unknown>,
33
+ ) => Entity.Entity<UnwrapStream<A>>,
34
+ ) =>
35
+ | Effect.Effect<A | Entity.Entity<A>, E, R>
36
+ | Generator<
37
+ Utils.YieldWrap<Effect.Effect<unknown, E, R>>,
38
+ A | Entity.Entity<A>,
39
+ unknown
40
+ >)
17
41
 
18
- export function handle<B, A, E, R>(
19
- handler: (context: B, next: () => Effect.Effect<A>) =>
20
- | Effect.Effect<A, E, R>
21
- | Generator<Utils.YieldWrap<Effect.Effect<any, E, R>>, A, any>,
22
- ): Route.Route.HandlerImmutable<B, A, E, R>
23
- export function handle<A, E, R>(
24
- handler: Effect.Effect<A, E, R>,
25
- ): Route.Route.HandlerImmutable<{}, A, E, R>
26
- export function handle<A>(
27
- handler: A,
28
- ): Route.Route.HandlerImmutable<{}, A, never, never>
29
42
  export function handle<B, A, E, R>(
30
43
  handler: HandlerInput<B, A, E, R>,
31
- ): Route.Route.HandlerImmutable<B, A, E, R> {
44
+ ): Route.Route.Handler<B, A, E, R> {
32
45
  if (typeof handler === "function") {
33
46
  return (
34
47
  context: B,
35
- next: () => Effect.Effect<A>,
36
- ): Effect.Effect<A, E, R> => {
48
+ next: (
49
+ context?: Partial<B> & Record<string, unknown>,
50
+ ) => Entity.Entity<A>,
51
+ ): Effect.Effect<Entity.Entity<A>, E, R> => {
37
52
  const result = (handler as Function)(context, next)
38
- if (Effect.isEffect(result)) {
39
- return result as Effect.Effect<A, E, R>
40
- }
41
- return Effect.gen(function*() {
42
- return yield* result
43
- }) as Effect.Effect<A, E, R>
53
+ const effect = Effect.isEffect(result)
54
+ ? result as Effect.Effect<A | Entity.Entity<A>, E, R>
55
+ : Effect.gen(function*() {
56
+ return yield* result
57
+ }) as Effect.Effect<A | Entity.Entity<A>, E, R>
58
+ return Effect.map(effect, normalizeToEntity)
44
59
  }
45
60
  }
46
61
  if (Effect.isEffect(handler)) {
47
- return (_context, _next) => handler
62
+ return (_context, _next) =>
63
+ Effect.map(handler, normalizeToEntity) as Effect.Effect<
64
+ Entity.Entity<A>,
65
+ E,
66
+ R
67
+ >
68
+ }
69
+ if (Entity.isEntity(handler)) {
70
+ return (_context, _next) => Effect.succeed(handler as Entity.Entity<A>)
48
71
  }
49
- return (_context, _next) => Effect.succeed(handler as A)
72
+ return (_context, _next) =>
73
+ Effect.succeed(normalizeToEntity(handler as A) as Entity.Entity<A>)
74
+ }
75
+
76
+ function normalizeToEntity<A>(value: A | Entity.Entity<A>): Entity.Entity<A> {
77
+ if (Entity.isEntity(value)) {
78
+ return value as Entity.Entity<A>
79
+ }
80
+ return Entity.make(value as A, { status: 200 })
50
81
  }
51
82
 
52
83
  export function build<
@@ -59,7 +90,8 @@ export function build<
59
90
  D extends Route.RouteDescriptor.Any,
60
91
  B extends {},
61
92
  I extends Route.Route.Tuple,
62
- A extends Value,
93
+ A extends F extends "json" ? Value
94
+ : Value | Stream.Stream<Value, any, any>,
63
95
  E = never,
64
96
  R = never,
65
97
  >(
@@ -75,8 +107,25 @@ export function build<
75
107
  return function(
76
108
  self: Route.RouteSet.RouteSet<D, B, I>,
77
109
  ) {
110
+ const contentType = formatToContentType[descriptors.format]
111
+ const baseHandler = handle(handler)
112
+ const wrappedHandler: Route.Route.Handler<
113
+ D & B & Route.ExtractBindings<I> & { format: F },
114
+ A,
115
+ E,
116
+ R
117
+ > = (ctx, next) =>
118
+ Effect.map(baseHandler(ctx as any, next as any), (entity) =>
119
+ entity.headers["content-type"]
120
+ ? entity
121
+ : Entity.make(entity.body, {
122
+ status: entity.status,
123
+ url: entity.url,
124
+ headers: { ...entity.headers, "content-type": contentType },
125
+ }))
126
+
78
127
  const route = Route.make<{ format: F }, {}, A, E, R>(
79
- handle(handler) as any,
128
+ wrappedHandler as any,
80
129
  descriptors,
81
130
  )
82
131
 
@@ -97,10 +146,48 @@ export function build<
97
146
  }
98
147
  }
99
148
 
100
- // used to simplify the context type passed to route handlers
101
- // for those who prefer to write code by hand :)
102
- type _Simplify<T> = {
103
- -readonly [K in keyof T]: T[K] extends object
104
- ? { -readonly [P in keyof T[K]]: T[K][P] }
105
- : T[K]
106
- } extends infer U ? { [K in keyof U]: U[K] } : never
149
+ export type RenderValue =
150
+ | string
151
+ | Uint8Array
152
+ | Stream.Stream<string | Uint8Array, any, any>
153
+
154
+ export function render<
155
+ D extends Route.RouteDescriptor.Any,
156
+ B extends {},
157
+ I extends Route.Route.Tuple,
158
+ A extends RenderValue,
159
+ E = never,
160
+ R = never,
161
+ >(
162
+ handler: HandlerInput<
163
+ NoInfer<
164
+ D & B & Route.ExtractBindings<I> & { format: "*" }
165
+ >,
166
+ A,
167
+ E,
168
+ R
169
+ >,
170
+ ) {
171
+ return function(
172
+ self: Route.RouteSet.RouteSet<D, B, I>,
173
+ ) {
174
+ const route = Route.make<{ format: "*" }, {}, A, E, R>(
175
+ handle(handler) as any,
176
+ { format: "*" },
177
+ )
178
+
179
+ const items: [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>] = [
180
+ ...Route.items(self),
181
+ route,
182
+ ]
183
+
184
+ return Route.set<
185
+ D,
186
+ B,
187
+ [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>]
188
+ >(
189
+ items,
190
+ Route.descriptor(self),
191
+ )
192
+ }
193
+ }