effect-start 0.33.0 → 0.35.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.
Files changed (134) hide show
  1. package/README.md +303 -36
  2. package/dist/Entity.d.ts +3 -1
  3. package/dist/Entity.d.ts.map +1 -1
  4. package/dist/Entity.js +23 -0
  5. package/dist/Entity.js.map +1 -1
  6. package/dist/Fetch.d.ts +1 -1
  7. package/dist/Fetch.js.map +1 -1
  8. package/dist/FileRouter.d.ts +1 -1
  9. package/dist/FileRouterCodegen.d.ts.map +1 -1
  10. package/dist/FileRouterCodegen.js +10 -4
  11. package/dist/FileRouterCodegen.js.map +1 -1
  12. package/dist/Job.d.ts +94 -0
  13. package/dist/Job.d.ts.map +1 -0
  14. package/dist/Job.js +157 -0
  15. package/dist/Job.js.map +1 -0
  16. package/dist/Password.d.ts +1 -1
  17. package/dist/Route.d.ts +20 -15
  18. package/dist/Route.d.ts.map +1 -1
  19. package/dist/Route.js +12 -0
  20. package/dist/Route.js.map +1 -1
  21. package/dist/RouteBody.d.ts +7 -7
  22. package/dist/RouteBody.d.ts.map +1 -1
  23. package/dist/RouteBody.js.map +1 -1
  24. package/dist/RouteHook.d.ts +1 -1
  25. package/dist/RouteHook.d.ts.map +1 -1
  26. package/dist/RouteHook.js.map +1 -1
  27. package/dist/RouteHttp.d.ts.map +1 -1
  28. package/dist/RouteHttp.js +55 -39
  29. package/dist/RouteHttp.js.map +1 -1
  30. package/dist/RouteLink.d.ts +16 -0
  31. package/dist/RouteLink.d.ts.map +1 -0
  32. package/dist/RouteLink.js +23 -0
  33. package/dist/RouteLink.js.map +1 -0
  34. package/dist/RouteMount.d.ts +37 -31
  35. package/dist/RouteMount.d.ts.map +1 -1
  36. package/dist/RouteMount.js.map +1 -1
  37. package/dist/RouteSchema.d.ts +81 -28
  38. package/dist/RouteSchema.d.ts.map +1 -1
  39. package/dist/RouteSchema.js +56 -101
  40. package/dist/RouteSchema.js.map +1 -1
  41. package/dist/RouteSse.d.ts +1 -1
  42. package/dist/RouteSse.d.ts.map +1 -1
  43. package/dist/RouteSse.js.map +1 -1
  44. package/dist/Socket.d.ts +1 -1
  45. package/dist/Start.js +1 -1
  46. package/dist/Start.js.map +1 -1
  47. package/dist/StaticFiles.d.ts +4 -10
  48. package/dist/StaticFiles.d.ts.map +1 -1
  49. package/dist/StaticFiles.js +10 -18
  50. package/dist/StaticFiles.js.map +1 -1
  51. package/dist/System.d.ts +1 -1
  52. package/dist/_Docker.d.ts +1 -1
  53. package/dist/_HtmlScanner.d.ts +42 -0
  54. package/dist/_HtmlScanner.d.ts.map +1 -0
  55. package/dist/_HtmlScanner.js +385 -0
  56. package/dist/_HtmlScanner.js.map +1 -0
  57. package/dist/_RouteLink.d.ts +16 -0
  58. package/dist/_RouteLink.d.ts.map +1 -0
  59. package/dist/_RouteLink.js +22 -0
  60. package/dist/_RouteLink.js.map +1 -0
  61. package/dist/bun/BunRoute.d.ts +4 -6
  62. package/dist/bun/BunRoute.d.ts.map +1 -1
  63. package/dist/bun/BunRoute.js +1 -1
  64. package/dist/bun/BunRoute.js.map +1 -1
  65. package/dist/bundler/Bundle.d.ts +1 -1
  66. package/dist/bundler/BundleRoute.d.ts +3 -4
  67. package/dist/bundler/BundleRoute.d.ts.map +1 -1
  68. package/dist/bundler/BundleRoute.js +5 -11
  69. package/dist/bundler/BundleRoute.js.map +1 -1
  70. package/dist/datastar/watchers/patchElements.js +1 -1
  71. package/dist/datastar/watchers/patchElements.js.map +1 -1
  72. package/dist/experimental/CsrfProtection.d.ts +67 -0
  73. package/dist/experimental/CsrfProtection.d.ts.map +1 -0
  74. package/dist/experimental/CsrfProtection.js +100 -0
  75. package/dist/experimental/CsrfProtection.js.map +1 -0
  76. package/dist/experimental/EncryptedCookies.d.ts +1 -1
  77. package/dist/experimental/KeyValueStore.d.ts +1 -1
  78. package/dist/lint/plugin.js +4 -0
  79. package/dist/lint/plugin.js.map +1 -1
  80. package/dist/sql/SqlClient.d.ts +1 -1
  81. package/dist/studio/Studio.d.ts +1 -1
  82. package/dist/studio/Studio.d.ts.map +1 -1
  83. package/dist/studio/Studio.js +4 -10
  84. package/dist/studio/Studio.js.map +1 -1
  85. package/dist/studio/routes/errors/route.d.ts +2 -2
  86. package/dist/studio/routes/errors/route.d.ts.map +1 -1
  87. package/dist/studio/routes/errors/route.js +3 -2
  88. package/dist/studio/routes/errors/route.js.map +1 -1
  89. package/dist/studio/routes/fiberDetail.d.ts +3 -7
  90. package/dist/studio/routes/fiberDetail.d.ts.map +1 -1
  91. package/dist/studio/routes/fibers/route.d.ts +2 -2
  92. package/dist/studio/routes/layout.d.ts +3 -3
  93. package/dist/studio/routes/logs/route.d.ts +2 -2
  94. package/dist/studio/routes/logs/route.d.ts.map +1 -1
  95. package/dist/studio/routes/logs/route.js +3 -2
  96. package/dist/studio/routes/logs/route.js.map +1 -1
  97. package/dist/studio/routes/metrics/route.d.ts +2 -2
  98. package/dist/studio/routes/route.d.ts +1 -1
  99. package/dist/studio/routes/routes/route.d.ts +1 -1
  100. package/dist/studio/routes/services/route.d.ts +1 -1
  101. package/dist/studio/routes/system/route.d.ts +2 -2
  102. package/dist/studio/routes/traceDetail.d.ts +3 -7
  103. package/dist/studio/routes/traceDetail.d.ts.map +1 -1
  104. package/dist/studio/routes/traces/route.d.ts +2 -2
  105. package/dist/studio/routes/traces/route.d.ts.map +1 -1
  106. package/dist/studio/routes/traces/route.js +3 -2
  107. package/dist/studio/routes/traces/route.js.map +1 -1
  108. package/dist/studio/routes/tree.d.ts +43 -51
  109. package/dist/studio/routes/tree.d.ts.map +1 -1
  110. package/package.json +4 -3
  111. package/src/Entity.ts +34 -5
  112. package/src/Fetch.ts +1 -1
  113. package/src/FileRouterCodegen.ts +10 -4
  114. package/src/Route.ts +55 -34
  115. package/src/RouteBody.ts +15 -15
  116. package/src/RouteHook.ts +3 -3
  117. package/src/RouteHttp.ts +55 -51
  118. package/src/RouteLink.ts +56 -0
  119. package/src/RouteMount.ts +44 -37
  120. package/src/RouteSchema.ts +299 -166
  121. package/src/RouteSse.ts +3 -3
  122. package/src/Start.ts +1 -1
  123. package/src/StaticFiles.ts +17 -34
  124. package/src/_HtmlScanner.ts +415 -0
  125. package/src/bun/BunRoute.ts +11 -11
  126. package/src/bundler/BundleRoute.ts +8 -19
  127. package/src/datastar/watchers/patchElements.ts +1 -1
  128. package/src/dev.d.ts +3 -0
  129. package/src/experimental/CsrfProtection.ts +153 -0
  130. package/src/lint/plugin.js +2 -0
  131. package/src/studio/Studio.ts +4 -14
  132. package/src/studio/routes/errors/route.tsx +3 -2
  133. package/src/studio/routes/logs/route.tsx +3 -2
  134. package/src/studio/routes/traces/route.tsx +3 -2
package/src/Route.ts CHANGED
@@ -27,16 +27,16 @@ export namespace RouteDescriptor {
27
27
  }
28
28
  }
29
29
 
30
- export namespace RouteSet {
31
- export type RouteSet<
32
- D extends RouteDescriptor.Any = {},
33
- B = {},
34
- M extends Route.Tuple = [],
35
- > = Data<D, B, M> & {
36
- [TypeId]: typeof TypeId
37
- } & Pipeable.Pipeable &
38
- Iterable<M[number]>
30
+ export type RouteSet<
31
+ D extends RouteDescriptor.Any = {},
32
+ B = {},
33
+ M extends Route.Tuple = [],
34
+ > = RouteSet.Data<D, B, M> & {
35
+ [TypeId]: typeof TypeId
36
+ } & Pipeable.Pipeable &
37
+ Iterable<M[number]>
39
38
 
39
+ export namespace RouteSet {
40
40
  export type Data<D extends RouteDescriptor.Any = {}, B = {}, M extends Route.Tuple = []> = {
41
41
  [RouteItems]: M
42
42
  [RouteDescriptor]: D
@@ -44,7 +44,7 @@ export namespace RouteSet {
44
44
  }
45
45
 
46
46
  export type Proto = Pipeable.Pipeable &
47
- Iterable<Route.Route<any, any, any, any, any>> & {
47
+ Iterable<Route<any, any, any, any, any>> & {
48
48
  [TypeId]: typeof TypeId
49
49
  }
50
50
 
@@ -58,17 +58,17 @@ export namespace RouteSet {
58
58
  T extends Data<infer D, any, any> ? D : never
59
59
  }
60
60
 
61
- export namespace Route {
62
- export interface Route<
63
- D extends RouteDescriptor.Any = {},
64
- B = {},
65
- A = any,
66
- E = never,
67
- R = never,
68
- > extends RouteSet.RouteSet<D, {}, [Route<D, B, A, E, R>]> {
69
- readonly handler: Handler<B & D, A, E, R>
70
- }
61
+ export interface Route<
62
+ D extends RouteDescriptor.Any = {},
63
+ B = {},
64
+ A = any,
65
+ E = never,
66
+ R = never,
67
+ > extends RouteSet<D, {}, [Route<D, B, A, E, R>]> {
68
+ readonly handler: Route.Handler<B & D, A, E, R>
69
+ }
71
70
 
71
+ export namespace Route {
72
72
  export type With<D extends RouteDescriptor.Any> = Route<any, any, any, any, any> & {
73
73
  [RouteDescriptor]: D
74
74
  }
@@ -125,26 +125,26 @@ export function isRouteSet(input: unknown): input is RouteSet.Any {
125
125
  return Predicate.hasProperty(input, TypeId)
126
126
  }
127
127
 
128
- export function isRoute(input: unknown): input is Route.Route {
128
+ export function isRoute(input: unknown): input is Route {
129
129
  return isRouteSet(input) && Predicate.hasProperty(input, "handler")
130
130
  }
131
131
 
132
132
  export function set<D extends RouteDescriptor.Any = {}, B = {}, I extends Route.Tuple = []>(
133
133
  items: I = [] as unknown as I,
134
134
  descriptor: D = {} as D,
135
- ): RouteSet.RouteSet<D, B, I> {
135
+ ): RouteSet<D, B, I> {
136
136
  return Object.assign(Object.create(Proto), {
137
137
  [RouteItems]: items,
138
138
  [RouteDescriptor]: descriptor,
139
- }) as RouteSet.RouteSet<D, B, I>
139
+ }) as RouteSet<D, B, I>
140
140
  }
141
141
 
142
142
  export function make<D extends RouteDescriptor.Any, B, A, E = never, R = never>(
143
143
  handler: Route.Handler<B & D, A, E, R>,
144
144
  descriptor?: D,
145
- ): Route.Route<D, B, A, E, R> {
145
+ ): Route<D, B, A, E, R> {
146
146
  const items: any = []
147
- const route: Route.Route<D, B, A, E, R> = Object.assign(Object.create(Proto), {
147
+ const route: Route<D, B, A, E, R> = Object.assign(Object.create(Proto), {
148
148
  [RouteItems]: items,
149
149
  [RouteDescriptor]: descriptor,
150
150
  handler,
@@ -185,7 +185,7 @@ export type ExtractBindings<M extends Route.Tuple> = M extends [
185
185
  infer Head,
186
186
  ...infer Tail extends Route.Tuple,
187
187
  ]
188
- ? Head extends Route.Route<any, infer B, any, any, any>
188
+ ? Head extends Route<any, infer B, any, any, any>
189
189
  ? ShallowMerge<B, ExtractBindings<Tail>>
190
190
  : ExtractBindings<Tail>
191
191
  : {}
@@ -206,6 +206,11 @@ export * from "./RouteSchema.ts"
206
206
 
207
207
  export { del, get, head, options, patch, post, put, use } from "./RouteMount.ts"
208
208
 
209
+ export class Request extends Context.Tag("effect-start/Route/Request")<
210
+ Request,
211
+ globalThis.Request
212
+ >() {}
213
+
209
214
  export const text = RouteBody.build<string, "text">({
210
215
  format: "text",
211
216
  })
@@ -229,8 +234,8 @@ export function redirect<D extends RouteDescriptor.Any, B, I extends Route.Tuple
229
234
  url: string | URL,
230
235
  options?: { status?: 301 | 302 | 303 | 307 | 308 },
231
236
  ): (
232
- self: RouteSet.RouteSet<D, B, I>,
233
- ) => RouteSet.RouteSet<D, B, [...I, Route.Route<{}, {}, "", never, never>]> {
237
+ self: RouteSet<D, B, I>,
238
+ ) => RouteSet<D, B, [...I, Route<{}, {}, "", never, never>]> {
234
239
  const route = make<{}, {}, "">(() =>
235
240
  Effect.succeed(
236
241
  Entity.make("", {
@@ -243,7 +248,7 @@ export function redirect<D extends RouteDescriptor.Any, B, I extends Route.Tuple
243
248
  )
244
249
 
245
250
  return (self) =>
246
- set<D, B, [...I, Route.Route<{}, {}, "", never, never>]>(
251
+ set<D, B, [...I, Route<{}, {}, "", never, never>]>(
247
252
  [...items(self), route],
248
253
  descriptor(self),
249
254
  )
@@ -255,6 +260,20 @@ export function layer(routes: RouteTree.RouteMap | RouteTree.RouteTree) {
255
260
  return Layer.sync(Routes, () => (RouteTree.isRouteTree(routes) ? routes : RouteTree.make(routes)))
256
261
  }
257
262
 
263
+ export function layerMerge(routes: RouteTree.InputRouteMap | RouteTree.RouteTree) {
264
+ return Layer.effect(
265
+ Routes,
266
+ Effect.gen(function* () {
267
+ const existing = yield* Effect.serviceOption(Routes).pipe(
268
+ Effect.andThen(Option.getOrUndefined),
269
+ )
270
+ const tree = RouteTree.isRouteTree(routes) ? routes : RouteTree.make(routes)
271
+ if (!existing) return tree
272
+ return RouteTree.merge(existing, tree)
273
+ }),
274
+ )
275
+ }
276
+
258
277
  /**
259
278
  * Creates a route that short-curcits in development.
260
279
  *
@@ -262,9 +281,9 @@ export function layer(routes: RouteTree.RouteMap | RouteTree.RouteTree) {
262
281
  * we exclude them altogeteher in development.
263
282
  */
264
283
  export function devOnly<D extends RouteDescriptor.Any, B, I extends Route.Tuple>(
265
- self: RouteSet.RouteSet<D, B, I>,
266
- ): RouteSet.RouteSet<D, B, [...I, Route.Route<{ dev: true }, { dev: true }, unknown, any, any>]> {
267
- const route: Route.Route<{ dev: true }, { dev: true }, unknown, any, any> = make<
284
+ self: RouteSet<D, B, I>,
285
+ ): RouteSet<D, B, [...I, Route<{ dev: true }, { dev: true }, unknown, any, any>]> {
286
+ const route: Route<{ dev: true }, { dev: true }, unknown, any, any> = make<
268
287
  { dev: true },
269
288
  { dev: true },
270
289
  unknown,
@@ -280,12 +299,12 @@ export function devOnly<D extends RouteDescriptor.Any, B, I extends Route.Tuple>
280
299
  { dev: true },
281
300
  )
282
301
 
283
- const nextItems: [...I, Route.Route<{ dev: true }, { dev: true }, unknown, any, any>] = [
302
+ const nextItems: [...I, Route<{ dev: true }, { dev: true }, unknown, any, any>] = [
284
303
  ...items(self),
285
304
  route,
286
305
  ]
287
306
 
288
- return set<D, B, [...I, Route.Route<{ dev: true }, { dev: true }, unknown, any, any>]>(
307
+ return set<D, B, [...I, Route<{ dev: true }, { dev: true }, unknown, any, any>]>(
289
308
  nextItems,
290
309
  descriptor(self),
291
310
  )
@@ -309,3 +328,5 @@ export function lazy<T extends RouteSet.Any>(
309
328
  )
310
329
  })
311
330
  }
331
+
332
+ export { link } from "./RouteLink.ts"
package/src/RouteBody.ts CHANGED
@@ -111,13 +111,13 @@ export interface BuildReturn<Value, F extends Format, Body = never> {
111
111
  >(
112
112
  handler: GeneratorHandler<NoInfer<D & B & Route.ExtractBindings<I> & { format: F }>, A, Y>,
113
113
  ): (
114
- self: Route.RouteSet.RouteSet<D, B, I>,
115
- ) => Route.RouteSet.RouteSet<
114
+ self: Route.RouteSet<D, B, I>,
115
+ ) => Route.RouteSet<
116
116
  D,
117
117
  B,
118
118
  [
119
119
  ...I,
120
- Route.Route.Route<
120
+ Route.Route<
121
121
  { format: F },
122
122
  {},
123
123
  [Body] extends [never] ? A : Body,
@@ -137,11 +137,11 @@ export interface BuildReturn<Value, F extends Format, Body = never> {
137
137
  >(
138
138
  handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: F }>, A, E, R>,
139
139
  ): (
140
- self: Route.RouteSet.RouteSet<D, B, I>,
141
- ) => Route.RouteSet.RouteSet<
140
+ self: Route.RouteSet<D, B, I>,
141
+ ) => Route.RouteSet<
142
142
  D,
143
143
  B,
144
- [...I, Route.Route.Route<{ format: F }, {}, [Body] extends [never] ? A : Body, E, R>]
144
+ [...I, Route.Route<{ format: F }, {}, [Body] extends [never] ? A : Body, E, R>]
145
145
  >
146
146
  }
147
147
 
@@ -163,7 +163,7 @@ export function build<Value, F extends Format>(options: {
163
163
  E = never,
164
164
  R = never,
165
165
  >(handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: F }>, A, E, R>) {
166
- return (self: Route.RouteSet.RouteSet<D, B, I>) => {
166
+ return (self: Route.RouteSet<D, B, I>) => {
167
167
  const contentType = formatToContentType[descriptors.format]
168
168
  const baseHandler = handle(handler)
169
169
  const wrappedHandler: Route.Route.Handler<{ format: F }, A, E, R> = (ctx, next) =>
@@ -191,7 +191,7 @@ export function build<Value, F extends Format>(options: {
191
191
 
192
192
  const route = Route.make<{ format: F }, {}, A, E, R>(wrappedHandler, descriptors)
193
193
 
194
- return Route.set<D, B, [...I, Route.Route.Route<{ format: F }, {}, A, E, R>]>(
194
+ return Route.set<D, B, [...I, Route.Route<{ format: F }, {}, A, E, R>]>(
195
195
  [...Route.items(self), route],
196
196
  Route.descriptor(self),
197
197
  )
@@ -210,11 +210,11 @@ export function render<
210
210
  >(
211
211
  handler: GeneratorHandler<NoInfer<D & B & Route.ExtractBindings<I> & { format: "*" }>, A, Y>,
212
212
  ): (
213
- self: Route.RouteSet.RouteSet<D, B, I>,
214
- ) => Route.RouteSet.RouteSet<
213
+ self: Route.RouteSet<D, B, I>,
214
+ ) => Route.RouteSet<
215
215
  D,
216
216
  B,
217
- [...I, Route.Route.Route<{ format: "*" }, {}, A, YieldError<Y>, YieldContext<Y>>]
217
+ [...I, Route.Route<{ format: "*" }, {}, A, YieldError<Y>, YieldContext<Y>>]
218
218
  >
219
219
  export function render<
220
220
  D extends Route.RouteDescriptor.Any,
@@ -226,8 +226,8 @@ export function render<
226
226
  >(
227
227
  handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: "*" }>, A, E, R>,
228
228
  ): (
229
- self: Route.RouteSet.RouteSet<D, B, I>,
230
- ) => Route.RouteSet.RouteSet<D, B, [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>]>
229
+ self: Route.RouteSet<D, B, I>,
230
+ ) => Route.RouteSet<D, B, [...I, Route.Route<{ format: "*" }, {}, A, E, R>]>
231
231
  export function render<
232
232
  D extends Route.RouteDescriptor.Any,
233
233
  B extends {},
@@ -236,14 +236,14 @@ export function render<
236
236
  E = never,
237
237
  R = never,
238
238
  >(handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: "*" }>, A, E, R>) {
239
- return (self: Route.RouteSet.RouteSet<D, B, I>) => {
239
+ return (self: Route.RouteSet<D, B, I>) => {
240
240
  const baseHandler = handle(handler)
241
241
  const route = Route.make<{ format: "*" }, {}, A, E, R>(
242
242
  (ctx, next) => baseHandler(ctx as D & B & Route.ExtractBindings<I> & { format: "*" }, next),
243
243
  { format: "*" },
244
244
  )
245
245
 
246
- return Route.set<D, B, [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>]>(
246
+ return Route.set<D, B, [...I, Route.Route<{ format: "*" }, {}, A, E, R>]>(
247
247
  [...Route.items(self), route],
248
248
  Route.descriptor(self),
249
249
  )
package/src/RouteHook.ts CHANGED
@@ -25,8 +25,8 @@ export function filter<
25
25
  const normalized = normalizeFilterHandler(filterHandler)
26
26
 
27
27
  return function (
28
- self: Route.RouteSet.RouteSet<D, SB, P>,
29
- ): Route.RouteSet.RouteSet<D, SB, [...P, Route.Route.Route<{}, BOut, unknown, E, R>]> {
28
+ self: Route.RouteSet<D, SB, P>,
29
+ ): Route.RouteSet<D, SB, [...P, Route.Route<{}, BOut, unknown, E, R>]> {
30
30
  const route = Route.make<{}, BOut, unknown, E, R>(
31
31
  (context: BOut, next: (ctx?: Partial<BOut>) => Entity.Entity<unknown>) =>
32
32
  Effect.gen(function* () {
@@ -39,7 +39,7 @@ export function filter<
39
39
  )
40
40
 
41
41
  return Route.set(
42
- [...Route.items(self), route] as [...P, Route.Route.Route<{}, BOut, unknown, E, R>],
42
+ [...Route.items(self), route] as [...P, Route.Route<{}, BOut, unknown, E, R>],
43
43
  Route.descriptor(self),
44
44
  )
45
45
  }
package/src/RouteHttp.ts CHANGED
@@ -76,7 +76,7 @@ const respondError = (
76
76
 
77
77
  function streamResponse(
78
78
  stream: Stream.Stream<unknown, unknown, unknown>,
79
- headers: Record<string, string | null | undefined>,
79
+ headers: globalThis.Headers,
80
80
  status: number,
81
81
  runtime: Runtime.Runtime<any>,
82
82
  ): Response {
@@ -92,10 +92,27 @@ function streamResponse(
92
92
  )
93
93
  return new Response(Stream.toReadableStreamRuntime(byteStream, runtime), {
94
94
  status,
95
- headers: headers as Record<string, string>,
95
+ headers,
96
96
  })
97
97
  }
98
98
 
99
+ function toHeaders(entityHeaders: Entity.Headers, contentType: string): globalThis.Headers {
100
+ const headers = new Headers()
101
+ for (const key in entityHeaders) {
102
+ const value = entityHeaders[key]
103
+ if (value == null) continue
104
+ if (typeof value === "string") {
105
+ headers.set(key, value)
106
+ } else {
107
+ for (const v of value) headers.append(key, v)
108
+ }
109
+ }
110
+ if (!headers.has("content-type")) {
111
+ headers.set("content-type", contentType)
112
+ }
113
+ return headers
114
+ }
115
+
99
116
  function toResponse(
100
117
  entity: Entity.Entity<any>,
101
118
  format: string | undefined,
@@ -103,7 +120,7 @@ function toResponse(
103
120
  ): Effect.Effect<Response, ParseResult.ParseError> {
104
121
  const contentType = Entity.type(entity)
105
122
  const status = entity.status ?? 200
106
- const headers = { ...entity.headers, "content-type": contentType }
123
+ const headers = toHeaders(entity.headers, contentType)
107
124
 
108
125
  if (StreamExtra.isStream(entity.body)) {
109
126
  return Effect.succeed(streamResponse(entity.body, headers, status, runtime))
@@ -168,66 +185,49 @@ function determineSelectedFormat(
168
185
 
169
186
  export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
170
187
  const runFork = Runtime.runFork(runtime)
188
+ const runSync = Runtime.runSync(runtime)
189
+ const inDevelopment = Option.isSome(runSync(Development.option))
171
190
 
172
191
  return (routes: Iterable<UnboundedRouteWithMethod>): Http.WebHandler => {
173
- const grouped = Object.groupBy(
174
- routes,
175
- (route) => Route.descriptor(route).method?.toUpperCase() ?? "*",
176
- )
177
- const wildcards = grouped["*"] ?? []
178
- const methodGroups: {
179
- [method in Http.Method]?: Array<UnboundedRouteWithMethod>
180
- } = {
181
- GET: undefined,
182
- POST: undefined,
183
- PUT: undefined,
184
- PATCH: undefined,
185
- DELETE: undefined,
186
- HEAD: undefined,
187
- OPTIONS: undefined,
192
+ const allRoutes = Array.from(routes)
193
+ const methods = new Set<string>()
194
+ for (const route of allRoutes) {
195
+ const m = Route.descriptor(route).method?.toUpperCase()
196
+ if (m && m !== "*") methods.add(m)
188
197
  }
189
-
190
- for (const method in grouped) {
191
- if (method !== "*") {
192
- methodGroups[method] = grouped[method]
193
- }
198
+ if (methods.has("GET") && !methods.has("HEAD")) {
199
+ methods.add("HEAD")
194
200
  }
195
-
196
- if (methodGroups["GET"] !== undefined && methodGroups["HEAD"] === undefined) {
197
- methodGroups["HEAD"] = methodGroups["GET"]
198
- }
199
-
200
- const allowedMethods = Object.keys(methodGroups)
201
- .filter((m) => methodGroups[m] !== undefined && methodGroups[m]!.length > 0)
202
- .join(", ")
201
+ const allowedMethods = Array.from(methods).join(", ")
203
202
 
204
203
  return (request) =>
205
204
  new Promise((resolve) => {
206
205
  const method = request.method.toUpperCase()
207
206
  const accept = request.headers.get("accept")
208
- const methodRoutes = methodGroups[method] ?? []
207
+ const matchingRoutes = allRoutes.filter((route) => {
208
+ const m = Route.descriptor(route).method?.toUpperCase()
209
+ return m === "*" || m === method || (method === "HEAD" && m === "GET")
210
+ })
209
211
 
210
- if (method === "OPTIONS" && methodRoutes.length === 0 && wildcards.length === 0) {
211
- return resolve(
212
- new Response(null, {
213
- status: 204,
214
- headers: { allow: allowedMethods },
215
- }),
216
- )
217
- }
212
+ if (matchingRoutes.length === 0) {
213
+ if (method === "OPTIONS" || methods.size === 0) {
214
+ return resolve(
215
+ new Response(null, {
216
+ status: 204,
217
+ headers: { allow: allowedMethods },
218
+ }),
219
+ )
220
+ }
218
221
 
219
- if (methodRoutes.length === 0 && wildcards.length === 0) {
220
222
  return resolve(
221
223
  respondError({ status: 405, message: "method not allowed" }, { allow: allowedMethods }),
222
224
  )
223
225
  }
224
-
225
- const allRoutes = [...wildcards, ...methodRoutes]
226
- const selectedFormat = determineSelectedFormat(accept, allRoutes)
226
+ const selectedFormat = determineSelectedFormat(accept, matchingRoutes)
227
227
 
228
228
  const specificFormats = new Set<string>()
229
229
  let hasWildcardFormatRoutes = false
230
- for (const r of allRoutes) {
230
+ for (const r of matchingRoutes) {
231
231
  const format = Route.descriptor(r).format
232
232
  if (format === "*") hasWildcardFormatRoutes = true
233
233
  else if (format) specificFormats.add(format)
@@ -249,13 +249,13 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
249
249
  currentContext = passedContext
250
250
  }
251
251
 
252
- if (index >= allRoutes.length) {
252
+ if (index >= matchingRoutes.length) {
253
253
  return Effect.succeed(
254
254
  Entity.make({ status: 404, message: "route not found" }, { status: 404 }),
255
255
  )
256
256
  }
257
257
 
258
- const route = allRoutes[index++]
258
+ const route = matchingRoutes[index++]
259
259
  const descriptor = Route.descriptor(route)
260
260
  const format = descriptor.format
261
261
  const handler = route.handler as unknown as Handler
@@ -292,7 +292,7 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
292
292
 
293
293
  const url = new URL(request.url)
294
294
 
295
- const innerEffect = Effect.gen(function* () {
295
+ const innerEffect = Effect.provideService(Effect.gen(function* () {
296
296
  const result = yield* createChain({ request, selectedFormat })
297
297
 
298
298
  const entity = Entity.isEntity(result) ? result : Entity.make(result, { status: 200 })
@@ -306,7 +306,7 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
306
306
  response.headers.set("vary", "Accept")
307
307
  }
308
308
  return response
309
- })
309
+ }), Route.Request, request)
310
310
 
311
311
  if (tracerDisabled) {
312
312
  return innerEffect
@@ -356,7 +356,9 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
356
356
  Effect.gen(function* () {
357
357
  yield* Effect.logError(cause)
358
358
  const status = getStatusFromCause(cause)
359
- const message = Cause.pretty(cause, { renderErrorCause: true })
359
+ const message = inDevelopment
360
+ ? Cause.pretty(cause, { renderErrorCause: true })
361
+ : "Internal Server Error"
360
362
  return respondError({ status, message })
361
363
  }),
362
364
  ),
@@ -384,7 +386,9 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
384
386
  resolve(respondError({ status: 499, message: "client closed request" }))
385
387
  } else {
386
388
  const status = getStatusFromCause(exit.cause)
387
- const message = Cause.pretty(exit.cause, { renderErrorCause: true })
389
+ const message = inDevelopment
390
+ ? Cause.pretty(exit.cause, { renderErrorCause: true })
391
+ : "Internal Server Error"
388
392
  resolve(respondError({ status, message }))
389
393
  }
390
394
  })
@@ -0,0 +1,56 @@
1
+ import type * as PathPattern from "./_PathPattern.ts"
2
+ import type * as Route from "./Route.ts"
3
+ import type { Routes as DevRoutes } from "effect-start/dev"
4
+
5
+ export type LinkParams<
6
+ Routes,
7
+ P extends string,
8
+ B = P extends keyof Routes
9
+ ? Routes[P] extends [...any[], infer L] // last route
10
+ ? L extends () => Promise<{ default: infer R extends Route.RouteSet.Any }>
11
+ ? Route.Route.Bindings<R>
12
+ : {}
13
+ : {}
14
+ : {},
15
+ > = {
16
+ [K in keyof PathPattern.Params<P>]: B extends { pathParams: infer S }
17
+ ? K extends keyof S
18
+ ? S[K]
19
+ : string | number
20
+ : string | number
21
+ } & (B extends { searchParams: infer S } ? { [K in keyof S]?: S[K] } : {})
22
+
23
+ export function link<
24
+ Routes = DevRoutes,
25
+ P extends keyof Routes extends never
26
+ ? // Falls back to any string when no routes are registered
27
+ string
28
+ : keyof Routes & string = keyof Routes extends never ? string : keyof Routes & string,
29
+ >(
30
+ ...args: {} extends LinkParams<Routes, P>
31
+ ? [path: P, params?: LinkParams<Routes, P>]
32
+ : [path: P, params: LinkParams<Routes, P>]
33
+ ): string {
34
+ const path = args[0] as string
35
+ // Substitute path params, deleting used keys so the rest become search params
36
+ const remaining = { ...args[1] } as Record<string, string | number | undefined>
37
+ const result = path.replace(
38
+ /\/:(\w+)([?*+])?/g,
39
+ (_, name: string, modifier: string | undefined) => {
40
+ const value = remaining[name]
41
+ delete remaining[name]
42
+ if (value == null) {
43
+ if (modifier === "?" || modifier === "*") return ""
44
+ return "/"
45
+ }
46
+ return "/" + encodeURIComponent(String(value))
47
+ },
48
+ )
49
+
50
+ const search = new URLSearchParams()
51
+ for (const key in remaining) {
52
+ if (remaining[key] != null) search.set(key, String(remaining[key]))
53
+ }
54
+ const qs = search.toString()
55
+ return qs ? result + "?" + qs : result
56
+ }