effect-start 0.32.0 → 0.34.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 (200) hide show
  1. package/dist/Entity.d.ts +6 -1
  2. package/dist/Entity.d.ts.map +1 -1
  3. package/dist/Entity.js +43 -5
  4. package/dist/Entity.js.map +1 -1
  5. package/dist/Fetch.js.map +1 -1
  6. package/dist/FileRouterCodegen.js +2 -2
  7. package/dist/FileRouterCodegen.js.map +1 -1
  8. package/dist/Html.d.ts +5 -11
  9. package/dist/Html.d.ts.map +1 -1
  10. package/dist/Html.js +21 -1
  11. package/dist/Html.js.map +1 -1
  12. package/dist/KeyValueStore.d.ts +37 -0
  13. package/dist/KeyValueStore.d.ts.map +1 -0
  14. package/dist/KeyValueStore.js +99 -0
  15. package/dist/KeyValueStore.js.map +1 -0
  16. package/dist/Route.d.ts +2 -2
  17. package/dist/Route.d.ts.map +1 -1
  18. package/dist/Route.js +1 -1
  19. package/dist/Route.js.map +1 -1
  20. package/dist/RouteBody.d.ts +2 -2
  21. package/dist/RouteBody.d.ts.map +1 -1
  22. package/dist/RouteHttp.d.ts.map +1 -1
  23. package/dist/RouteHttp.js +45 -35
  24. package/dist/RouteHttp.js.map +1 -1
  25. package/dist/RouteMount.d.ts +20 -31
  26. package/dist/RouteMount.d.ts.map +1 -1
  27. package/dist/RouteMount.js +0 -15
  28. package/dist/RouteMount.js.map +1 -1
  29. package/dist/Start.d.ts.map +1 -1
  30. package/dist/Start.js +4 -0
  31. package/dist/Start.js.map +1 -1
  32. package/dist/StaticFiles.d.ts +2 -2
  33. package/dist/StaticFiles.d.ts.map +1 -1
  34. package/dist/StaticFiles.js +7 -8
  35. package/dist/StaticFiles.js.map +1 -1
  36. package/dist/bun/BunRoute.d.ts.map +1 -1
  37. package/dist/bun/BunRoute.js +90 -78
  38. package/dist/bun/BunRoute.js.map +1 -1
  39. package/dist/bun/BunServer.d.ts +1 -1
  40. package/dist/bun/BunServer.d.ts.map +1 -1
  41. package/dist/bun/BunServer.js +8 -1
  42. package/dist/bun/BunServer.js.map +1 -1
  43. package/dist/bundler/BundleRoute.d.ts +4 -4
  44. package/dist/bundler/BundleRoute.d.ts.map +1 -1
  45. package/dist/datastar/attributes/computed.js +3 -3
  46. package/dist/datastar/attributes/computed.js.map +1 -1
  47. package/dist/datastar/attributes/on.js +11 -36
  48. package/dist/datastar/attributes/on.js.map +1 -1
  49. package/dist/datastar/engine.d.ts +9 -7
  50. package/dist/datastar/engine.d.ts.map +1 -1
  51. package/dist/datastar/engine.js +45 -29
  52. package/dist/datastar/engine.js.map +1 -1
  53. package/dist/datastar/jsx.d.ts +70 -0
  54. package/dist/datastar/jsx.d.ts.map +1 -0
  55. package/dist/datastar/jsx.js +2 -0
  56. package/dist/datastar/jsx.js.map +1 -0
  57. package/dist/datastar/window.d.ts +8 -0
  58. package/dist/datastar/window.d.ts.map +1 -0
  59. package/dist/datastar/window.js +4 -0
  60. package/dist/datastar/window.js.map +1 -0
  61. package/dist/experimental/KeyValueStore.d.ts +37 -0
  62. package/dist/experimental/KeyValueStore.d.ts.map +1 -0
  63. package/dist/experimental/KeyValueStore.js +99 -0
  64. package/dist/experimental/KeyValueStore.js.map +1 -0
  65. package/dist/experimental/SqlCache.d.ts +19 -0
  66. package/dist/experimental/SqlCache.d.ts.map +1 -0
  67. package/dist/experimental/SqlCache.js +35 -0
  68. package/dist/experimental/SqlCache.js.map +1 -0
  69. package/dist/experimental/SqlIntrospect.d.ts +92 -0
  70. package/dist/experimental/SqlIntrospect.d.ts.map +1 -0
  71. package/dist/experimental/SqlIntrospect.js +478 -0
  72. package/dist/experimental/SqlIntrospect.js.map +1 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/jsx-runtime.d.ts +2 -2
  78. package/dist/jsx-runtime.d.ts.map +1 -1
  79. package/dist/jsx.d.ts +3216 -0
  80. package/dist/jsx.d.ts.map +1 -0
  81. package/dist/jsx.js +6 -0
  82. package/dist/jsx.js.map +1 -0
  83. package/dist/lint/plugin.d.ts +4 -3
  84. package/dist/lint/plugin.js +56 -17
  85. package/dist/lint/plugin.js.map +1 -1
  86. package/dist/sql/index.d.ts +0 -2
  87. package/dist/sql/index.d.ts.map +1 -1
  88. package/dist/sql/index.js +0 -2
  89. package/dist/sql/index.js.map +1 -1
  90. package/dist/studio/StudioLogger.d.ts.map +1 -1
  91. package/dist/studio/StudioLogger.js +2 -1
  92. package/dist/studio/StudioLogger.js.map +1 -1
  93. package/dist/studio/StudioStore.d.ts +3 -0
  94. package/dist/studio/StudioStore.d.ts.map +1 -1
  95. package/dist/studio/StudioStore.js +13 -0
  96. package/dist/studio/StudioStore.js.map +1 -1
  97. package/dist/studio/_Pretty.d.ts +4 -0
  98. package/dist/studio/_Pretty.d.ts.map +1 -0
  99. package/dist/studio/_Pretty.js +56 -0
  100. package/dist/studio/_Pretty.js.map +1 -0
  101. package/dist/studio/routes/errors/route.d.ts +2 -2
  102. package/dist/studio/routes/fiberDetail.d.ts +2 -2
  103. package/dist/studio/routes/fibers/route.d.ts +2 -2
  104. package/dist/studio/routes/layout.d.ts +2 -0
  105. package/dist/studio/routes/layout.d.ts.map +1 -1
  106. package/dist/studio/routes/layout.html +3 -12
  107. package/dist/studio/routes/layout.js +6 -1
  108. package/dist/studio/routes/layout.js.map +1 -1
  109. package/dist/studio/routes/logs/route.d.ts +2 -2
  110. package/dist/studio/routes/metrics/route.d.ts +2 -2
  111. package/dist/studio/routes/route.d.ts +2 -2
  112. package/dist/studio/routes/routes/route.d.ts +2 -2
  113. package/dist/studio/routes/services/route.d.ts +2 -2
  114. package/dist/studio/routes/system/route.d.ts +2 -2
  115. package/dist/studio/routes/traceDetail.d.ts +2 -2
  116. package/dist/studio/routes/traces/route.d.ts +2 -2
  117. package/dist/studio/routes/traces/route.d.ts.map +1 -1
  118. package/dist/studio/routes/traces/route.js +5 -2
  119. package/dist/studio/routes/traces/route.js.map +1 -1
  120. package/dist/studio/routes/tree.d.ts +22 -0
  121. package/dist/studio/routes/tree.d.ts.map +1 -1
  122. package/dist/studio/ui/Errors.d.ts +1 -1
  123. package/dist/studio/ui/Errors.js +1 -1
  124. package/dist/studio/ui/Errors.js.map +1 -1
  125. package/dist/studio/ui/Fibers.d.ts +2 -2
  126. package/dist/studio/ui/Fibers.d.ts.map +1 -1
  127. package/dist/studio/ui/Fibers.js +4 -3
  128. package/dist/studio/ui/Fibers.js.map +1 -1
  129. package/dist/studio/ui/Logs.d.ts +1 -1
  130. package/dist/studio/ui/Logs.d.ts.map +1 -1
  131. package/dist/studio/ui/Logs.js +2 -1
  132. package/dist/studio/ui/Logs.js.map +1 -1
  133. package/dist/studio/ui/Metrics.d.ts +1 -1
  134. package/dist/studio/ui/Routes.d.ts +1 -1
  135. package/dist/studio/ui/Routes.d.ts.map +1 -1
  136. package/dist/studio/ui/Services.d.ts +1 -1
  137. package/dist/studio/ui/Services.d.ts.map +1 -1
  138. package/dist/studio/ui/Shell.d.ts +2 -2
  139. package/dist/studio/ui/Shell.d.ts.map +1 -1
  140. package/dist/studio/ui/System.d.ts +1 -1
  141. package/dist/studio/ui/Traces.d.ts +3 -3
  142. package/dist/studio/ui/Traces.d.ts.map +1 -1
  143. package/dist/studio/ui/Traces.js +5 -11
  144. package/dist/studio/ui/Traces.js.map +1 -1
  145. package/dist/studio/ui/_PrettyValue.d.ts +10 -0
  146. package/dist/studio/ui/_PrettyValue.d.ts.map +1 -0
  147. package/dist/studio/ui/_PrettyValue.js +27 -0
  148. package/dist/studio/ui/_PrettyValue.js.map +1 -0
  149. package/dist/tailwind/TailwindPlugin.d.ts.map +1 -1
  150. package/dist/tailwind/TailwindPlugin.js +89 -62
  151. package/dist/tailwind/TailwindPlugin.js.map +1 -1
  152. package/dist/ts/import-plugin.cjs +388 -0
  153. package/dist/ts/import-plugin.cjs.map +1 -0
  154. package/dist/ts/import-plugin.d.cts +87 -0
  155. package/dist/ts/import-plugin.d.cts.map +1 -0
  156. package/dist/ts/import-plugin.d.ts +87 -0
  157. package/dist/ts/import-plugin.d.ts.map +1 -0
  158. package/dist/ts/import-plugin.js +390 -0
  159. package/dist/ts/import-plugin.js.map +1 -0
  160. package/package.json +109 -8
  161. package/src/Entity.ts +62 -8
  162. package/src/Fetch.ts +1 -1
  163. package/src/FileRouterCodegen.ts +2 -2
  164. package/src/Html.ts +28 -21
  165. package/src/Route.ts +2 -2
  166. package/src/RouteBody.ts +2 -2
  167. package/src/RouteHttp.ts +45 -47
  168. package/src/RouteMount.ts +23 -65
  169. package/src/Start.ts +4 -0
  170. package/src/StaticFiles.ts +7 -10
  171. package/src/bun/BunRoute.ts +117 -95
  172. package/src/bun/BunServer.ts +9 -2
  173. package/src/datastar/README.md +24 -8
  174. package/src/datastar/attributes/computed.ts +3 -3
  175. package/src/datastar/attributes/on.ts +11 -37
  176. package/src/datastar/engine.ts +61 -37
  177. package/src/datastar/jsx.d.ts +12 -26
  178. package/src/datastar/types.d.ts +8 -0
  179. package/src/experimental/KeyValueStore.ts +161 -0
  180. package/src/{sql → experimental}/SqlCache.ts +1 -1
  181. package/src/{sql → experimental}/SqlIntrospect.ts +1 -1
  182. package/src/index.ts +1 -0
  183. package/src/jsx-runtime.ts +1 -1
  184. package/src/jsx.d.ts +17 -2
  185. package/src/lint/plugin.js +54 -19
  186. package/src/sql/index.ts +0 -2
  187. package/src/studio/StudioLogger.ts +2 -1
  188. package/src/studio/StudioStore.ts +18 -0
  189. package/src/studio/_Pretty.ts +59 -0
  190. package/src/studio/routes/layout.html +3 -12
  191. package/src/studio/routes/layout.tsx +9 -1
  192. package/src/studio/routes/traces/route.tsx +5 -1
  193. package/src/studio/ui/Errors.tsx +1 -1
  194. package/src/studio/ui/Fibers.tsx +14 -10
  195. package/src/studio/ui/Logs.tsx +15 -10
  196. package/src/studio/ui/Traces.tsx +40 -68
  197. package/src/studio/ui/_PrettyValue.tsx +34 -0
  198. package/src/tailwind/TailwindPlugin.ts +102 -75
  199. package/src/RouteTrie.ts +0 -205
  200. package/src/experimental/index.ts +0 -1
package/src/Entity.ts CHANGED
@@ -20,7 +20,10 @@ function isBinary(v: unknown): v is Uint8Array | ArrayBuffer {
20
20
  * Header keys are guaranteed to be lowercase.
21
21
  */
22
22
  export type Headers = {
23
- [header: string]: string | null | undefined
23
+ // `set-cookie` is the only header that supports multiple values (as an array),
24
+ // since it requires separate `Set-Cookie` headers per cookie (RFC 6265).
25
+ readonly "set-cookie"?: string | ReadonlyArray<string> | null
26
+ readonly [header: string]: string | ReadonlyArray<string> | null | undefined
24
27
  }
25
28
 
26
29
  export interface Entity<T = unknown, E = never> extends Effect.Effect<Entity<T, E>, E> {
@@ -50,6 +53,9 @@ export interface Entity<T = unknown, E = never> extends Effect.Effect<Entity<T,
50
53
  : [T] extends [Values.Json]
51
54
  ? Effect.Effect<T, ParseResult.ParseError | E>
52
55
  : Effect.Effect<unknown, ParseResult.ParseError | E>
56
+ readonly schemaJson: <A, I, R>(
57
+ schema: Schema.Schema<A, I, R>,
58
+ ) => Effect.Effect<A, ParseResult.ParseError | E, R>
53
59
  readonly bytes: Effect.Effect<Uint8Array, ParseResult.ParseError | E>
54
60
  readonly stream: T extends Stream.Stream<infer A, infer E1, any>
55
61
  ? Stream.Stream<A, ParseResult.ParseError | E | E1>
@@ -76,6 +82,16 @@ function parseJson(s: string): Effect.Effect<unknown, ParseResult.ParseError> {
76
82
  }
77
83
  }
78
84
 
85
+ function isDirectJson(value: unknown): value is Exclude<Values.Json, string> {
86
+ return (
87
+ value === null ||
88
+ typeof value === "number" ||
89
+ typeof value === "boolean" ||
90
+ Array.isArray(value) ||
91
+ Values.isPlainObject(value)
92
+ )
93
+ }
94
+
79
95
  function getText(
80
96
  self: Entity<unknown, unknown>,
81
97
  ): Effect.Effect<string, ParseResult.ParseError | unknown> {
@@ -123,7 +139,7 @@ function getJson(
123
139
  if (isEntity(inner)) {
124
140
  return inner.json
125
141
  }
126
- if (typeof inner === "object" && inner !== null && !isBinary(inner)) {
142
+ if (isDirectJson(inner)) {
127
143
  return Effect.succeed(inner)
128
144
  }
129
145
  if (typeof inner === "string") {
@@ -136,7 +152,7 @@ function getJson(
136
152
  },
137
153
  )
138
154
  }
139
- if (typeof v === "object" && v !== null && !isBinary(v)) {
155
+ if (isDirectJson(v)) {
140
156
  return Effect.succeed(v)
141
157
  }
142
158
  if (typeof v === "string") {
@@ -188,13 +204,20 @@ function getBytes(
188
204
  if (typeof v === "string") {
189
205
  return Effect.succeed(textEncoder.encode(v))
190
206
  }
191
- // Allows entity.stream to work when body is a JSON object
192
- if (typeof v === "object" && v !== null && !isBinary(v)) {
207
+ // Allows entity.stream to work when body is a JSON value.
208
+ if (isDirectJson(v)) {
193
209
  return Effect.succeed(textEncoder.encode(JSON.stringify(v)))
194
210
  }
195
211
  return Effect.fail(mismatch(Schema.Uint8ArrayFromSelf, v))
196
212
  }
197
213
 
214
+ export function schemaJson<T, E, A, I, R>(
215
+ self: Entity<T, E>,
216
+ schema: Schema.Schema<A, I, R>,
217
+ ): Effect.Effect<A, ParseResult.ParseError | E, R> {
218
+ return Effect.flatMap(self.json, Schema.decodeUnknown(schema))
219
+ }
220
+
198
221
  function getStream<A, E1, E2>(
199
222
  self: Entity<Stream.Stream<A, E1, never>, E2>,
200
223
  ): Stream.Stream<A, ParseResult.ParseError | E1 | E2>
@@ -234,6 +257,11 @@ const Proto: Proto = Object.defineProperties(Object.create(Effectable.CommitProt
234
257
  return getJson(this)
235
258
  },
236
259
  },
260
+ schemaJson: {
261
+ value(this: Entity<unknown, unknown>, schema: Schema.Schema.Any) {
262
+ return schemaJson(this, schema)
263
+ },
264
+ },
237
265
  bytes: {
238
266
  get(this: Entity<unknown, unknown>) {
239
267
  return getBytes(this)
@@ -278,6 +306,32 @@ export function effect<A, E, R>(body: Effect.Effect<Entity<A> | A, E, R>): Entit
278
306
  return make(body) as unknown as Entity<A, E>
279
307
  }
280
308
 
309
+ function mergeSetCookie(
310
+ existing: string | ReadonlyArray<string> | null | undefined,
311
+ incoming: string | ReadonlyArray<string> | null | undefined,
312
+ ): string | ReadonlyArray<string> | undefined {
313
+ if (incoming == null) return existing ?? undefined
314
+ if (existing == null) return incoming
315
+ const a = Array.isArray(existing) ? existing : [existing]
316
+ const b = Array.isArray(incoming) ? incoming : [incoming]
317
+ return [...a, ...b]
318
+ }
319
+
320
+ export function merge<T, E>(entity: Entity<T, E>, options: Options): Entity<T, E> {
321
+ const headers: Headers = options.headers
322
+ ? {
323
+ ...entity.headers,
324
+ ...options.headers,
325
+ "set-cookie": mergeSetCookie(entity.headers["set-cookie"], options.headers["set-cookie"]),
326
+ }
327
+ : entity.headers
328
+ return make(entity.body, {
329
+ headers,
330
+ status: options.status ?? entity.status,
331
+ url: options.url ?? entity.url,
332
+ }) as Entity<T, E>
333
+ }
334
+
281
335
  export function resolve<A, E>(entity: Entity<A, E>): Effect.Effect<Entity<A, E>, E, never> {
282
336
  const body = entity.body
283
337
  if (Effect.isEffect(body)) {
@@ -297,13 +351,13 @@ export function resolve<A, E>(entity: Entity<A, E>): Effect.Effect<Entity<A, E>,
297
351
  export function type(self: Entity): string {
298
352
  const h = self.headers
299
353
  if (h["content-type"]) {
300
- return h["content-type"]
354
+ return h["content-type"] as string
301
355
  }
302
356
  const v = self.body
303
357
  if (typeof v === "string") {
304
358
  return "text/plain"
305
359
  }
306
- if (typeof v === "object" && v !== null && !isBinary(v)) {
360
+ if (isDirectJson(v)) {
307
361
  return "application/json"
308
362
  }
309
363
  return "application/octet-stream"
@@ -312,7 +366,7 @@ export function type(self: Entity): string {
312
366
  export function length(self: Entity): number | undefined {
313
367
  const h = self.headers
314
368
  if (h["content-length"]) {
315
- return parseInt(h["content-length"], 10)
369
+ return parseInt(h["content-length"] as string, 10)
316
370
  }
317
371
  const v = self.body
318
372
  if (typeof v === "string") {
package/src/Fetch.ts CHANGED
@@ -232,7 +232,7 @@ export function followRedirects(options?: { readonly maxRedirects?: number }): M
232
232
  return entity
233
233
  }
234
234
 
235
- const location = entity.headers["location"]
235
+ const location = entity.headers["location"] as string | undefined
236
236
  if (!location) {
237
237
  return entity
238
238
  }
@@ -137,10 +137,10 @@ export function generateCode(fileRoutes: FileRouter.OrderedFileRoutes): string |
137
137
  currentPath = parentPath || "/"
138
138
  }
139
139
 
140
- // Order: route first, then layers from innermost to outermost
140
+ // Order: layers from outermost to innermost, then route
141
141
  const loaders: Array<string> = [
142
+ ...allLayers.map((layer) => `() => import(".${layer.modulePath}")`),
142
143
  `() => import(".${route.modulePath}")`,
143
- ...allLayers.reverse().map((layer) => `() => import(".${layer.modulePath}")`),
144
144
  ]
145
145
 
146
146
  entries.push({ path, loaders })
package/src/Html.ts CHANGED
@@ -1,28 +1,14 @@
1
- import type { JSX } from "./jsx.d.ts"
1
+ import type { JSX, HtmlElement, HtmlElemenetProps, HtmlElementType, HtmlComponent } from "../src/jsx.d.ts"
2
+
3
+ export type ElementType = HtmlElementType
4
+ export type ElemenetProps = HtmlElemenetProps
5
+ export type Component = HtmlComponent
6
+ export type Element = HtmlElement
2
7
 
3
8
  export const TypeId = "~effect-start/HyperNode" as const
4
9
 
5
10
  const NoChildren: ReadonlyArray<never> = Object.freeze([])
6
11
 
7
- type Primitive = string | number | boolean | null | undefined
8
-
9
- export type ElementType = string | Component
10
-
11
- export type ElemenetProps = {
12
- [key: string]:
13
- | Primitive
14
- | Element
15
- | Iterable<Primitive | Element>
16
- | Record<string, unknown>
17
- | ((window: Window) => void)
18
- }
19
-
20
- export type Component = (props: ElemenetProps) => Element | Primitive
21
-
22
- export interface Element {
23
- type: ElementType
24
- props: ElemenetProps
25
- }
26
12
 
27
13
  export function make(type: ElementType, props: ElemenetProps): Element {
28
14
  return {
@@ -74,6 +60,27 @@ const RAW_TEXT_TAGS = ["script", "style"]
74
60
 
75
61
  const escapeRawText = (text: string) => text.replaceAll("</", "<\\/")
76
62
 
63
+ const serializeObjectProperty = (value: unknown): string | undefined => {
64
+ if (value === undefined) return undefined
65
+ if (typeof value === "function") return value.toString()
66
+ if (Array.isArray(value)) {
67
+ return `[${value.map((item) => serializeObjectProperty(item) ?? "null").join(",")}]`
68
+ }
69
+ if (value && typeof value === "object") {
70
+ const props = Object.entries(value)
71
+ .flatMap(([key, prop]) => {
72
+ const serialized = serializeObjectProperty(prop)
73
+ return serialized === undefined ? [] : [`${JSON.stringify(key)}:${serialized}`]
74
+ })
75
+ .join(",")
76
+ return `{${props}}`
77
+ }
78
+ return JSON.stringify(value)
79
+ }
80
+
81
+ const serializeDataAttributeObject = (key: string, value: Record<string, unknown>): string =>
82
+ key === "data-computed" ? serializeObjectProperty(value)! : JSON.stringify(value)
83
+
77
84
  export function renderToString(
78
85
  node: JSX.Children,
79
86
  hooks?: { onNode?: (node: Element) => void },
@@ -143,7 +150,7 @@ export function renderToString(
143
150
  if (key.startsWith("data-") && typeof value === "function") {
144
151
  result += ` ${esc(resolvedKey)}="${esc(value.toString())}"`
145
152
  } else if (key.startsWith("data-") && typeof value === "object") {
146
- result += ` ${esc(resolvedKey)}='${escSQ(JSON.stringify(value))}'`
153
+ result += ` ${esc(resolvedKey)}='${escSQ(serializeDataAttributeObject(key, value))}'`
147
154
  } else {
148
155
  result += ` ${esc(resolvedKey)}="${esc(value)}"`
149
156
  }
package/src/Route.ts CHANGED
@@ -10,7 +10,7 @@ import * as RouteBody from "./RouteBody.ts"
10
10
  import * as RouteTree from "./RouteTree.ts"
11
11
  import type * as Values from "./_Values.ts"
12
12
  import * as Html from "./Html.ts"
13
- import type { JSX } from "./jsx.d.ts"
13
+ import type { JSX } from "../src/jsx.d.ts"
14
14
 
15
15
  export const render = RouteBody.render
16
16
 
@@ -204,7 +204,7 @@ export type ExtractContext<
204
204
  export * from "./RouteHook.ts"
205
205
  export * from "./RouteSchema.ts"
206
206
 
207
- export { add, del, get, head, options, patch, post, put, use } from "./RouteMount.ts"
207
+ export { del, get, head, options, patch, post, put, use } from "./RouteMount.ts"
208
208
 
209
209
  export const text = RouteBody.build<string, "text">({
210
210
  format: "text",
package/src/RouteBody.ts CHANGED
@@ -25,7 +25,7 @@ type YieldContext<T> = T extends Utils.YieldWrap<Effect.Effect<any, any, infer R
25
25
 
26
26
  type Next<B, A> = (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<UnwrapStream<A>>
27
27
 
28
- type HandlerReturn<A> = A | Entity.Entity<A> | ((self: Route.RouteSet.Any) => Route.RouteSet.Any)
28
+ type HandlerReturn<A> = A | Entity.Entity<A, any> | ((self: Route.RouteSet.Any) => Route.RouteSet.Any)
29
29
 
30
30
  type HandlerFunction<B, A, E, R> = (
31
31
  context: Values.Simplify<B>,
@@ -41,7 +41,7 @@ export type GeneratorHandler<B, A, Y> = (
41
41
 
42
42
  export type HandlerInput<B, A, E, R> =
43
43
  | A
44
- | Entity.Entity<A>
44
+ | Entity.Entity<A, any>
45
45
  | Effect.Effect<HandlerReturn<A>, E, R>
46
46
  | HandlerFunction<B, A, E, R>
47
47
 
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))
@@ -170,64 +187,45 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
170
187
  const runFork = Runtime.runFork(runtime)
171
188
 
172
189
  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,
188
- }
189
-
190
- for (const method in grouped) {
191
- if (method !== "*") {
192
- methodGroups[method] = grouped[method]
193
- }
190
+ const allRoutes = Array.from(routes)
191
+ const methods = new Set<string>()
192
+ for (const route of allRoutes) {
193
+ const m = Route.descriptor(route).method?.toUpperCase()
194
+ if (m && m !== "*") methods.add(m)
194
195
  }
195
-
196
- if (methodGroups["GET"] !== undefined && methodGroups["HEAD"] === undefined) {
197
- methodGroups["HEAD"] = methodGroups["GET"]
196
+ if (methods.has("GET") && !methods.has("HEAD")) {
197
+ methods.add("HEAD")
198
198
  }
199
-
200
- const allowedMethods = Object.keys(methodGroups)
201
- .filter((m) => methodGroups[m] !== undefined && methodGroups[m]!.length > 0)
202
- .join(", ")
199
+ const allowedMethods = Array.from(methods).join(", ")
203
200
 
204
201
  return (request) =>
205
202
  new Promise((resolve) => {
206
203
  const method = request.method.toUpperCase()
207
204
  const accept = request.headers.get("accept")
208
- const methodRoutes = methodGroups[method] ?? []
205
+ const matchingRoutes = allRoutes.filter((route) => {
206
+ const m = Route.descriptor(route).method?.toUpperCase()
207
+ return m === "*" || m === method || (method === "HEAD" && m === "GET")
208
+ })
209
209
 
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
- }
210
+ if (matchingRoutes.length === 0) {
211
+ if (method === "OPTIONS" || methods.size === 0) {
212
+ return resolve(
213
+ new Response(null, {
214
+ status: 204,
215
+ headers: { allow: allowedMethods },
216
+ }),
217
+ )
218
+ }
218
219
 
219
- if (methodRoutes.length === 0 && wildcards.length === 0) {
220
220
  return resolve(
221
221
  respondError({ status: 405, message: "method not allowed" }, { allow: allowedMethods }),
222
222
  )
223
223
  }
224
-
225
- const allRoutes = [...wildcards, ...methodRoutes]
226
- const selectedFormat = determineSelectedFormat(accept, allRoutes)
224
+ const selectedFormat = determineSelectedFormat(accept, matchingRoutes)
227
225
 
228
226
  const specificFormats = new Set<string>()
229
227
  let hasWildcardFormatRoutes = false
230
- for (const r of allRoutes) {
228
+ for (const r of matchingRoutes) {
231
229
  const format = Route.descriptor(r).format
232
230
  if (format === "*") hasWildcardFormatRoutes = true
233
231
  else if (format) specificFormats.add(format)
@@ -249,13 +247,13 @@ export const toWebHandlerRuntime = <R>(runtime: Runtime.Runtime<R>) => {
249
247
  currentContext = passedContext
250
248
  }
251
249
 
252
- if (index >= allRoutes.length) {
250
+ if (index >= matchingRoutes.length) {
253
251
  return Effect.succeed(
254
252
  Entity.make({ status: 404, message: "route not found" }, { status: 404 }),
255
253
  )
256
254
  }
257
255
 
258
- const route = allRoutes[index++]
256
+ const route = matchingRoutes[index++]
259
257
  const descriptor = Route.descriptor(route)
260
258
  const format = descriptor.format
261
259
  const handler = route.handler as unknown as Handler
package/src/RouteMount.ts CHANGED
@@ -10,7 +10,7 @@ const RouteSetTypeId = "~effect-start/RouteSet" as const
10
10
  // oxlint-disable-next-line import/first, typescript/consistent-type-imports -- typeof import() is not an import statement
11
11
  type Module = typeof import("./RouteMount.ts")
12
12
 
13
- export type Self = RouteMount.Builder | Module
13
+ export type Self = RouteMount.Builder<any, any> | Omit<RouteMount.Builder<any, any>, "use"> | Module
14
14
 
15
15
  export const use = makeMethodDescriber("*")
16
16
  export const get = makeMethodDescriber("GET")
@@ -21,28 +21,6 @@ export const patch = makeMethodDescriber("PATCH")
21
21
  export const head = makeMethodDescriber("HEAD")
22
22
  export const options = makeMethodDescriber("OPTIONS")
23
23
 
24
- export const add: RouteMount.Add = function (
25
- this: Self,
26
- path: string,
27
- routes: Route.RouteSet.Any | ((self: RouteMount.Builder<{}, []>) => Route.RouteSet.Any),
28
- ) {
29
- const baseItems = Route.isRouteSet(this) ? Route.items(this) : ([] as const)
30
-
31
- const routeSet = typeof routes === "function" ? routes(make<{}, []>([])) : routes
32
- const routeItems = Route.items(routeSet)
33
- const newItems = routeItems.map((item) => {
34
- const itemDescriptor = Route.descriptor(item) as { path?: string }
35
- const concatenatedPath =
36
- typeof itemDescriptor?.path === "string" ? path + itemDescriptor.path : path
37
- const newDescriptor = { ...itemDescriptor, path: concatenatedPath }
38
- return Route.isRoute(item)
39
- ? Route.make(item.handler as Route.Route.Handler<any, any, any, any>, newDescriptor)
40
- : Route.set(Route.items(item), newDescriptor)
41
- })
42
-
43
- return make([...baseItems, ...newItems] as any)
44
- }
45
-
46
24
  const Proto = Object.assign(Object.create(null), {
47
25
  [RouteSetTypeId]: RouteSetTypeId,
48
26
  *[Symbol.iterator](this: Route.RouteSet.Any) {
@@ -56,7 +34,6 @@ const Proto = Object.assign(Object.create(null), {
56
34
  patch,
57
35
  head,
58
36
  options,
59
- add,
60
37
  })
61
38
 
62
39
  function make<
@@ -116,7 +93,19 @@ export namespace RouteMount {
116
93
  export type MountSet = Route.RouteSet.RouteSet<{ method: Method }, {}, Route.Route.Tuple>
117
94
 
118
95
  export interface Builder<D extends {} = {}, I extends Route.Route.Tuple = []>
119
- extends Route.RouteSet.RouteSet<D, {}, I>, Module {}
96
+ extends Route.RouteSet.RouteSet<D, {}, I> {
97
+ use: Describer<"*">
98
+ get: Describer<"GET">
99
+ post: Describer<"POST">
100
+ put: Describer<"PUT">
101
+ del: Describer<"DELETE">
102
+ patch: Describer<"PATCH">
103
+ head: Describer<"HEAD">
104
+ options: Describer<"OPTIONS">
105
+ }
106
+
107
+ export type BuilderAfter<M extends Method, I extends Route.Route.Tuple> =
108
+ M extends "*" ? Builder<{}, I> : Omit<Builder<{}, I>, "use">
120
109
 
121
110
  export type EmptySet<M extends Method, B = {}> = Route.RouteSet.RouteSet<{ method: M }, B, []>
122
111
 
@@ -142,37 +131,6 @@ export namespace RouteMount {
142
131
  }[number]
143
132
  >
144
133
 
145
- type PrefixPathItem<Prefix extends string, T> =
146
- T extends Route.Route.Route<infer D, infer B, infer A, infer E, infer R>
147
- ? D extends { path: infer P extends string }
148
- ? Route.Route.Route<Omit<D, "path"> & { path: `${Prefix}${P}` }, B, A, E, R>
149
- : Route.Route.Route<D & { path: Prefix }, B, A, E, R>
150
- : T
151
-
152
- export type PrefixPath<Prefix extends string, I extends Route.Route.Tuple> = {
153
- [K in keyof I]: PrefixPathItem<Prefix, I[K]>
154
- } extends infer R extends Route.Route.Tuple
155
- ? R
156
- : never
157
-
158
- export interface Add {
159
- <S extends Self, P extends string, R extends Route.RouteSet.Any>(
160
- this: S,
161
- path: P,
162
- routes: R,
163
- ): Builder<{}, [...Items<S>, ...PrefixPath<P, Route.RouteSet.Items<R>>]>
164
-
165
- <S extends Self, P extends string, R extends Route.RouteSet.Any>(
166
- this: S,
167
- path: P,
168
- /**
169
- * Callback form provides a builder seeded with higher-level bindings so
170
- * nested routes can type-infer outer context when mounting.
171
- */
172
- routes: (self: Builder<{}, []>) => R,
173
- ): Builder<{}, [...Items<S>, ...PrefixPath<P, Route.RouteSet.Items<R>>]>
174
- }
175
-
176
134
  // Flatten items: merge method into descriptor and accumulate bindings through the chain
177
135
  // `request` is omitted from bindings since it's implicit (always available)
178
136
  export type FlattenItems<M extends Method, B, I extends Route.Route.Tuple> = I extends [
@@ -195,13 +153,13 @@ export namespace RouteMount {
195
153
  <S extends Self, A extends Route.RouteSet.Any>(
196
154
  this: S,
197
155
  ab: (a: EmptySet<M, BuilderBindings<S>>) => A,
198
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<A>>]>
156
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<A>>]>
199
157
 
200
158
  <S extends Self, A extends Route.RouteSet.Any, B extends Route.RouteSet.Any>(
201
159
  this: S,
202
160
  ab: (a: EmptySet<M, BuilderBindings<S>>) => A,
203
161
  bc: (b: A) => B,
204
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<B>>]>
162
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<B>>]>
205
163
 
206
164
  <
207
165
  S extends Self,
@@ -213,7 +171,7 @@ export namespace RouteMount {
213
171
  ab: (a: EmptySet<M, BuilderBindings<S>>) => A,
214
172
  bc: (b: A) => B,
215
173
  cd: (c: B) => C,
216
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<C>>]>
174
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<C>>]>
217
175
 
218
176
  <
219
177
  S extends Self,
@@ -227,7 +185,7 @@ export namespace RouteMount {
227
185
  bc: (b: A) => B,
228
186
  cd: (c: B) => C,
229
187
  de: (d: C) => D,
230
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<D>>]>
188
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<D>>]>
231
189
 
232
190
  <
233
191
  S extends Self,
@@ -243,7 +201,7 @@ export namespace RouteMount {
243
201
  cd: (c: B) => C,
244
202
  de: (d: C) => D,
245
203
  ef: (e: D) => E,
246
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<E>>]>
204
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<E>>]>
247
205
 
248
206
  <
249
207
  S extends Self,
@@ -261,7 +219,7 @@ export namespace RouteMount {
261
219
  de: (d: C) => D,
262
220
  ef: (e: D) => E,
263
221
  fg: (f: E) => F,
264
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<F>>]>
222
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<F>>]>
265
223
 
266
224
  <
267
225
  S extends Self,
@@ -281,7 +239,7 @@ export namespace RouteMount {
281
239
  ef: (e: D) => E,
282
240
  fg: (f: E) => F,
283
241
  gh: (g: F) => G,
284
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<G>>]>
242
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<G>>]>
285
243
 
286
244
  <
287
245
  S extends Self,
@@ -303,7 +261,7 @@ export namespace RouteMount {
303
261
  fg: (f: E) => F,
304
262
  gh: (g: F) => G,
305
263
  hi: (h: G) => H,
306
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<H>>]>
264
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<H>>]>
307
265
 
308
266
  <
309
267
  S extends Self,
@@ -327,6 +285,6 @@ export namespace RouteMount {
327
285
  gh: (g: F) => G,
328
286
  hi: (h: G) => H,
329
287
  ij: (i: H) => I,
330
- ): Builder<{}, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<I>>]>
288
+ ): BuilderAfter<M, [...Items<S>, ...FlattenItems<M, BuilderBindings<S>, Route.RouteSet.Items<I>>]>
331
289
  }
332
290
  }
package/src/Start.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type * as FileSystem from "./FileSystem.ts"
2
+ import * as Cause from "effect/Cause"
2
3
  import * as Context from "effect/Context"
3
4
  import * as Deferred from "effect/Deferred"
4
5
  import * as Effect from "effect/Effect"
@@ -140,6 +141,9 @@ export function build<const Layers extends readonly [Layer.Layer.Any, ...Array<L
140
141
  map.delete(layer)
141
142
  return map
142
143
  })
144
+ if (Cause.isDie(exit.cause) || Cause.isInterruptedOnly(exit.cause)) {
145
+ return yield* exit
146
+ }
143
147
  }
144
148
  }
145
149
  }
@@ -41,17 +41,14 @@ export const make = (directory: string) =>
41
41
  return emptyNotFound
42
42
  }
43
43
 
44
- const headers: Entity.Headers = {
45
- "content-length": String(info.size),
46
- "content-type": Mime.fromPath(relativePath),
47
- }
48
-
49
- if (Option.isSome(info.mtime)) {
50
- headers["last-modified"] = info.mtime.value.toUTCString()
51
- }
52
-
53
44
  const bytes = yield* fs.readFile(absolutePath)
54
- return Entity.make(bytes, { headers })
45
+ return Entity.make(bytes, {
46
+ headers: {
47
+ "content-length": String(info.size),
48
+ "content-type": Mime.fromPath(relativePath),
49
+ ...(Option.isSome(info.mtime) ? { "last-modified": info.mtime.value.toUTCString() } : {}),
50
+ },
51
+ })
55
52
  }),
56
53
  )
57
54