effect-start 0.29.0 → 0.30.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 (228) hide show
  1. package/README.md +1 -1
  2. package/dist/{_Development.d.ts → Development.d.ts} +2 -1
  3. package/dist/Development.d.ts.map +1 -0
  4. package/dist/{_Development.js → Development.js} +2 -1
  5. package/dist/Development.js.map +1 -0
  6. package/dist/Fetch.d.ts +0 -8
  7. package/dist/Fetch.d.ts.map +1 -1
  8. package/dist/Fetch.js +6 -22
  9. package/dist/Fetch.js.map +1 -1
  10. package/dist/FileRouter.js +1 -1
  11. package/dist/FileRouter.js.map +1 -1
  12. package/dist/GlobalLayer.d.ts.map +1 -1
  13. package/dist/GlobalLayer.js +1 -1
  14. package/dist/GlobalLayer.js.map +1 -1
  15. package/dist/Html.d.ts +32 -0
  16. package/dist/Html.d.ts.map +1 -0
  17. package/dist/{hyper/HyperHtml.js → Html.js} +45 -26
  18. package/dist/Html.js.map +1 -0
  19. package/dist/Route.d.ts +20 -7
  20. package/dist/Route.d.ts.map +1 -1
  21. package/dist/Route.js +24 -3
  22. package/dist/Route.js.map +1 -1
  23. package/dist/RouteBody.d.ts +13 -6
  24. package/dist/RouteBody.d.ts.map +1 -1
  25. package/dist/RouteBody.js +38 -27
  26. package/dist/RouteBody.js.map +1 -1
  27. package/dist/RouteHttp.d.ts.map +1 -1
  28. package/dist/RouteHttp.js +18 -1
  29. package/dist/RouteHttp.js.map +1 -1
  30. package/dist/RouteMount.js +1 -1
  31. package/dist/RouteMount.js.map +1 -1
  32. package/dist/System.d.ts +1 -1
  33. package/dist/System.d.ts.map +1 -1
  34. package/dist/System.js.map +1 -1
  35. package/dist/_ChildProcess.d.ts +1 -1
  36. package/dist/_ChildProcess.d.ts.map +1 -1
  37. package/dist/_ChildProcess.js.map +1 -1
  38. package/dist/bun/BunRoute.d.ts +1 -1
  39. package/dist/bun/BunRoute.d.ts.map +1 -1
  40. package/dist/bun/BunRoute.js +102 -33
  41. package/dist/bun/BunRoute.js.map +1 -1
  42. package/dist/bun/BunServer.d.ts.map +1 -1
  43. package/dist/bun/BunServer.js.map +1 -1
  44. package/dist/cloudflare/CloudflareTunnel.d.ts +12 -0
  45. package/dist/cloudflare/CloudflareTunnel.d.ts.map +1 -0
  46. package/dist/{x/cloudflare → cloudflare}/CloudflareTunnel.js +1 -1
  47. package/dist/cloudflare/CloudflareTunnel.js.map +1 -0
  48. package/dist/cloudflare/index.d.ts.map +1 -0
  49. package/dist/cloudflare/index.js.map +1 -0
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +3 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/jsx-runtime.d.ts +8 -0
  55. package/dist/jsx-runtime.d.ts.map +1 -0
  56. package/dist/{hyper/jsx-runtime.js → jsx-runtime.js} +2 -2
  57. package/dist/jsx-runtime.js.map +1 -0
  58. package/dist/studio/routes/errors/route.d.ts.map +1 -1
  59. package/dist/studio/routes/errors/route.js +3 -4
  60. package/dist/studio/routes/errors/route.js.map +1 -1
  61. package/dist/studio/routes/fiberDetail.d.ts.map +1 -1
  62. package/dist/studio/routes/fiberDetail.js +1 -2
  63. package/dist/studio/routes/fiberDetail.js.map +1 -1
  64. package/dist/studio/routes/fibers/route.d.ts.map +1 -1
  65. package/dist/studio/routes/fibers/route.js +3 -4
  66. package/dist/studio/routes/fibers/route.js.map +1 -1
  67. package/dist/studio/routes/logs/route.d.ts.map +1 -1
  68. package/dist/studio/routes/logs/route.js +3 -4
  69. package/dist/studio/routes/logs/route.js.map +1 -1
  70. package/dist/studio/routes/metrics/route.d.ts.map +1 -1
  71. package/dist/studio/routes/metrics/route.js +3 -4
  72. package/dist/studio/routes/metrics/route.js.map +1 -1
  73. package/dist/studio/routes/route.d.ts +1 -1
  74. package/dist/studio/routes/routes/route.d.ts.map +1 -1
  75. package/dist/studio/routes/routes/route.js +1 -2
  76. package/dist/studio/routes/routes/route.js.map +1 -1
  77. package/dist/studio/routes/services/route.d.ts.map +1 -1
  78. package/dist/studio/routes/services/route.js +1 -2
  79. package/dist/studio/routes/services/route.js.map +1 -1
  80. package/dist/studio/routes/system/route.d.ts.map +1 -1
  81. package/dist/studio/routes/system/route.js +3 -4
  82. package/dist/studio/routes/system/route.js.map +1 -1
  83. package/dist/studio/routes/traceDetail.d.ts.map +1 -1
  84. package/dist/studio/routes/traceDetail.js +1 -2
  85. package/dist/studio/routes/traceDetail.js.map +1 -1
  86. package/dist/studio/routes/traces/route.d.ts.map +1 -1
  87. package/dist/studio/routes/traces/route.js +3 -4
  88. package/dist/studio/routes/traces/route.js.map +1 -1
  89. package/dist/studio/routes/tree.d.ts +1 -1
  90. package/dist/studio/ui/Errors.d.ts +1 -1
  91. package/dist/studio/ui/Errors.d.ts.map +1 -1
  92. package/dist/studio/ui/Fibers.d.ts +2 -2
  93. package/dist/studio/ui/Fibers.d.ts.map +1 -1
  94. package/dist/studio/ui/Logs.d.ts +1 -1
  95. package/dist/studio/ui/Logs.d.ts.map +1 -1
  96. package/dist/studio/ui/Metrics.d.ts +1 -1
  97. package/dist/studio/ui/Metrics.d.ts.map +1 -1
  98. package/dist/studio/ui/Routes.d.ts +1 -1
  99. package/dist/studio/ui/Routes.d.ts.map +1 -1
  100. package/dist/studio/ui/Services.d.ts +1 -1
  101. package/dist/studio/ui/Services.d.ts.map +1 -1
  102. package/dist/studio/ui/Shell.d.ts +2 -2
  103. package/dist/studio/ui/Shell.d.ts.map +1 -1
  104. package/dist/studio/ui/System.d.ts +1 -1
  105. package/dist/studio/ui/System.d.ts.map +1 -1
  106. package/dist/studio/ui/Traces.d.ts +3 -3
  107. package/dist/studio/ui/Traces.d.ts.map +1 -1
  108. package/dist/tailscale/TailscaleTunnel.d.ts +16 -0
  109. package/dist/tailscale/TailscaleTunnel.d.ts.map +1 -0
  110. package/dist/{x/tailscale → tailscale}/TailscaleTunnel.js +2 -2
  111. package/dist/tailscale/TailscaleTunnel.js.map +1 -0
  112. package/dist/tailscale/index.d.ts.map +1 -0
  113. package/dist/tailscale/index.js.map +1 -0
  114. package/dist/tailwind/TailwindPlugin.d.ts.map +1 -0
  115. package/dist/tailwind/TailwindPlugin.js.map +1 -0
  116. package/dist/tailwind/compile.d.ts.map +1 -0
  117. package/dist/{x/tailwind → tailwind}/compile.js +1 -1
  118. package/dist/tailwind/compile.js.map +1 -0
  119. package/dist/tailwind/index.d.ts +3 -0
  120. package/dist/tailwind/index.d.ts.map +1 -0
  121. package/dist/tailwind/index.js +3 -0
  122. package/dist/tailwind/index.js.map +1 -0
  123. package/dist/tailwind/plugin.d.ts.map +1 -0
  124. package/dist/{x/tailwind → tailwind}/plugin.js +1 -1
  125. package/dist/tailwind/plugin.js.map +1 -0
  126. package/package.json +37 -37
  127. package/src/{_Development.ts → Development.ts} +5 -0
  128. package/src/Fetch.ts +10 -37
  129. package/src/FileRouter.ts +1 -1
  130. package/src/GlobalLayer.ts +3 -1
  131. package/src/{hyper/HyperHtml.ts → Html.ts} +90 -30
  132. package/src/Route.ts +67 -11
  133. package/src/RouteBody.ts +87 -62
  134. package/src/RouteHttp.ts +19 -1
  135. package/src/RouteMount.ts +1 -1
  136. package/src/System.ts +1 -1
  137. package/src/_ChildProcess.ts +1 -1
  138. package/src/bun/BunRoute.ts +125 -37
  139. package/src/bun/BunServer.ts +1 -0
  140. package/src/{x/cloudflare → cloudflare}/CloudflareTunnel.ts +1 -1
  141. package/src/index.ts +3 -1
  142. package/src/jsx-runtime.ts +15 -0
  143. package/src/{hyper/jsx.d.ts → jsx.d.ts} +3 -3
  144. package/src/studio/routes/errors/route.tsx +3 -4
  145. package/src/studio/routes/fiberDetail.tsx +1 -2
  146. package/src/studio/routes/fibers/route.tsx +3 -4
  147. package/src/studio/routes/logs/route.tsx +3 -4
  148. package/src/studio/routes/metrics/route.tsx +3 -4
  149. package/src/studio/routes/routes/route.tsx +1 -2
  150. package/src/studio/routes/services/route.tsx +1 -2
  151. package/src/studio/routes/system/route.tsx +3 -4
  152. package/src/studio/routes/traceDetail.tsx +1 -2
  153. package/src/studio/routes/traces/route.tsx +3 -4
  154. package/src/{x/tailscale → tailscale}/TailscaleTunnel.ts +2 -2
  155. package/src/{x/tailwind → tailwind}/compile.ts +1 -1
  156. package/src/tailwind/index.ts +2 -0
  157. package/src/{x/tailwind → tailwind}/plugin.ts +1 -1
  158. package/dist/_Development.d.ts.map +0 -1
  159. package/dist/_Development.js.map +0 -1
  160. package/dist/hyper/Hyper.d.ts +0 -26
  161. package/dist/hyper/Hyper.d.ts.map +0 -1
  162. package/dist/hyper/Hyper.js +0 -24
  163. package/dist/hyper/Hyper.js.map +0 -1
  164. package/dist/hyper/HyperHtml.d.ts +0 -24
  165. package/dist/hyper/HyperHtml.d.ts.map +0 -1
  166. package/dist/hyper/HyperHtml.js.map +0 -1
  167. package/dist/hyper/HyperHtml.test.d.ts +0 -2
  168. package/dist/hyper/HyperHtml.test.d.ts.map +0 -1
  169. package/dist/hyper/HyperHtml.test.js +0 -283
  170. package/dist/hyper/HyperHtml.test.js.map +0 -1
  171. package/dist/hyper/HyperNode.d.ts +0 -14
  172. package/dist/hyper/HyperNode.d.ts.map +0 -1
  173. package/dist/hyper/HyperNode.js +0 -12
  174. package/dist/hyper/HyperNode.js.map +0 -1
  175. package/dist/hyper/HyperRoute.d.ts +0 -9
  176. package/dist/hyper/HyperRoute.d.ts.map +0 -1
  177. package/dist/hyper/HyperRoute.js +0 -33
  178. package/dist/hyper/HyperRoute.js.map +0 -1
  179. package/dist/hyper/HyperRoute.test.d.ts +0 -2
  180. package/dist/hyper/HyperRoute.test.d.ts.map +0 -1
  181. package/dist/hyper/HyperRoute.test.js +0 -84
  182. package/dist/hyper/HyperRoute.test.js.map +0 -1
  183. package/dist/hyper/html.d.ts +0 -12
  184. package/dist/hyper/html.d.ts.map +0 -1
  185. package/dist/hyper/html.js +0 -31
  186. package/dist/hyper/html.js.map +0 -1
  187. package/dist/hyper/index.d.ts +0 -7
  188. package/dist/hyper/index.d.ts.map +0 -1
  189. package/dist/hyper/index.js +0 -6
  190. package/dist/hyper/index.js.map +0 -1
  191. package/dist/hyper/jsx-runtime.d.ts +0 -8
  192. package/dist/hyper/jsx-runtime.d.ts.map +0 -1
  193. package/dist/hyper/jsx-runtime.js.map +0 -1
  194. package/dist/x/cloudflare/CloudflareTunnel.d.ts +0 -12
  195. package/dist/x/cloudflare/CloudflareTunnel.d.ts.map +0 -1
  196. package/dist/x/cloudflare/CloudflareTunnel.js.map +0 -1
  197. package/dist/x/cloudflare/index.d.ts.map +0 -1
  198. package/dist/x/cloudflare/index.js.map +0 -1
  199. package/dist/x/tailscale/TailscaleTunnel.d.ts +0 -16
  200. package/dist/x/tailscale/TailscaleTunnel.d.ts.map +0 -1
  201. package/dist/x/tailscale/TailscaleTunnel.js.map +0 -1
  202. package/dist/x/tailscale/index.d.ts.map +0 -1
  203. package/dist/x/tailscale/index.js.map +0 -1
  204. package/dist/x/tailwind/TailwindPlugin.d.ts.map +0 -1
  205. package/dist/x/tailwind/TailwindPlugin.js.map +0 -1
  206. package/dist/x/tailwind/compile.d.ts.map +0 -1
  207. package/dist/x/tailwind/compile.js.map +0 -1
  208. package/dist/x/tailwind/plugin.d.ts.map +0 -1
  209. package/dist/x/tailwind/plugin.js.map +0 -1
  210. package/src/hyper/Hyper.ts +0 -55
  211. package/src/hyper/HyperHtml.test.tsx +0 -395
  212. package/src/hyper/HyperNode.ts +0 -33
  213. package/src/hyper/HyperRoute.test.tsx +0 -166
  214. package/src/hyper/HyperRoute.ts +0 -59
  215. package/src/hyper/html.ts +0 -47
  216. package/src/hyper/index.ts +0 -6
  217. package/src/hyper/jsx-runtime.ts +0 -15
  218. /package/dist/{x/cloudflare → cloudflare}/index.d.ts +0 -0
  219. /package/dist/{x/cloudflare → cloudflare}/index.js +0 -0
  220. /package/dist/{x/tailscale → tailscale}/index.d.ts +0 -0
  221. /package/dist/{x/tailscale → tailscale}/index.js +0 -0
  222. /package/dist/{x/tailwind → tailwind}/TailwindPlugin.d.ts +0 -0
  223. /package/dist/{x/tailwind → tailwind}/TailwindPlugin.js +0 -0
  224. /package/dist/{x/tailwind → tailwind}/compile.d.ts +0 -0
  225. /package/dist/{x/tailwind → tailwind}/plugin.d.ts +0 -0
  226. /package/src/{x/cloudflare → cloudflare}/index.ts +0 -0
  227. /package/src/{x/tailscale → tailscale}/index.ts +0 -0
  228. /package/src/{x/tailwind → tailwind}/TailwindPlugin.ts +0 -0
@@ -1,27 +1,46 @@
1
- /**
2
- * Renders Hyper JSX nodes to HTML.
3
- *
4
- * Effect Start comes with {@link Hyper} and {@link JsxRuntime} to enable
5
- * JSX support. The advantage of using JSX over HTML strings or templates
6
- * is type safety and better editor support.
7
- *
8
- * JSX nodes are compatible with React's and Solid's.
9
-
10
- * You can enable JSX support by updating `tsconfig.json`:
11
- *
12
- * {
13
- * compilerOptions: {
14
- * jsx: "react-jsx",
15
- * jsxImportSource: "effect-start" | "react" | "praect" // etc.
16
- * }
17
- * }
18
- */
19
-
20
- import type * as Hyper from "./Hyper.ts"
21
- import type * as HyperNode from "./HyperNode.ts"
22
- import type * as JsxRuntime from "./jsx-runtime.ts"
23
1
  import type { JSX } from "./jsx.d.ts"
24
2
 
3
+ export const TypeId = "~effect-start/HyperNode" as const
4
+
5
+ const NoChildren: ReadonlyArray<never> = Object.freeze([])
6
+
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
+
27
+ export function make(type: ElementType, props: ElemenetProps): Element {
28
+ return {
29
+ type,
30
+ props: {
31
+ ...props,
32
+ children: props.children ?? NoChildren,
33
+ },
34
+ }
35
+ }
36
+
37
+ export function isGenericJsxObject(value: unknown): value is {
38
+ type: any
39
+ props: any
40
+ } {
41
+ return typeof value === "object" && value !== null && "type" in value && "props" in value
42
+ }
43
+
25
44
  const EMPTY_TAGS = [
26
45
  "area",
27
46
  "base",
@@ -53,12 +72,11 @@ let map = {
53
72
 
54
73
  const RAW_TEXT_TAGS = ["script", "style"]
55
74
 
56
- // Prevents closing html tags in embedded css/js source
57
75
  const escapeRawText = (text: string) => text.replaceAll("</", "<\\/")
58
76
 
59
77
  export function renderToString(
60
78
  node: JSX.Children,
61
- hooks?: { onNode?: (node: HyperNode.HyperNode) => void },
79
+ hooks?: { onNode?: (node: Element) => void },
62
80
  ): string {
63
81
  const stack: Array<any> = [node]
64
82
  let result = ""
@@ -68,7 +86,6 @@ export function renderToString(
68
86
 
69
87
  if (typeof current === "string") {
70
88
  if (current.startsWith("<") && current.endsWith(">")) {
71
- // This is a closing tag, don't escape it
72
89
  result += current
73
90
  } else {
74
91
  result += esc(current)
@@ -82,17 +99,14 @@ export function renderToString(
82
99
  }
83
100
 
84
101
  if (typeof current === "boolean") {
85
- // React-like behavior: booleans render nothing
86
102
  continue
87
103
  }
88
104
 
89
105
  if (current === null || current === undefined) {
90
- // React-like behavior: null/undefined render nothing
91
106
  continue
92
107
  }
93
108
 
94
109
  if (Array.isArray(current)) {
95
- // Handle arrays by pushing all items to stack in reverse order
96
110
  for (let i = current.length - 1; i >= 0; i--) {
97
111
  stack.push(current[i])
98
112
  }
@@ -165,10 +179,56 @@ export function renderToString(
165
179
  }
166
180
  }
167
181
  } else if (current && typeof current === "object") {
168
- // Handle objects without type property - convert to string or ignore
169
- // This prevents [object Object] from appearing
170
182
  continue
171
183
  }
172
184
  }
173
185
  return result
174
186
  }
187
+
188
+ const HtmlStringSymbol = Symbol.for("HtmlString")
189
+
190
+ export interface HtmlString {
191
+ readonly [HtmlStringSymbol]: true
192
+ readonly value: string
193
+ }
194
+
195
+ const makeHtmlString = (value: string): HtmlString => ({
196
+ [HtmlStringSymbol]: true,
197
+ value,
198
+ })
199
+
200
+ const isHtmlString = (value: unknown): value is HtmlString =>
201
+ typeof value === "object" && value !== null && HtmlStringSymbol in value
202
+
203
+ type HtmlValue =
204
+ | string
205
+ | number
206
+ | bigint
207
+ | boolean
208
+ | null
209
+ | undefined
210
+ | HtmlString
211
+ | Function
212
+ | Record<string, unknown>
213
+ | ReadonlyArray<HtmlValue>
214
+
215
+ const resolveValue = (value: HtmlValue): string => {
216
+ if (value === null || value === undefined || value === false || value === true) return ""
217
+ if (isHtmlString(value)) return value.value
218
+ if (Array.isArray(value)) return (value as Array<HtmlValue>).map(resolveValue).join("")
219
+ if (typeof value === "function") return value.toString()
220
+ if (typeof value === "object") return JSON.stringify(value)
221
+ if (typeof value === "string") return value
222
+ return String(value)
223
+ }
224
+
225
+ export const html = (strings: TemplateStringsArray, ...values: Array<HtmlValue>): HtmlString => {
226
+ let result = strings[0]
227
+ for (let i = 0; i < values.length; i++) {
228
+ result += resolveValue(values[i])
229
+ result += strings[i + 1]
230
+ }
231
+ return makeHtmlString(result)
232
+ }
233
+
234
+ html.raw = (value: string): HtmlString => makeHtmlString(value)
package/src/Route.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import * as Context from "effect/Context"
2
2
  import * as Effect from "effect/Effect"
3
3
  import * as Layer from "effect/Layer"
4
+ import * as Option from "effect/Option"
4
5
  import * as Pipeable from "effect/Pipeable"
5
6
  import * as Predicate from "effect/Predicate"
7
+ import * as Development from "./Development.ts"
6
8
  import * as Entity from "./Entity.ts"
7
9
  import * as RouteBody from "./RouteBody.ts"
8
10
  import * as RouteTree from "./RouteTree.ts"
9
11
  import type * as Values from "./_Values.ts"
12
+ import * as Html from "./Html.ts"
13
+ import type { JSX } from "./jsx.d.ts"
14
+
15
+ export const render = RouteBody.render
10
16
 
11
17
  export const RouteItems: unique symbol = Symbol()
12
18
  export const RouteDescriptor: unique symbol = Symbol()
@@ -162,6 +168,7 @@ export function items<T extends RouteSet.Data<any, any, any>>(self: T): RouteSet
162
168
  export function descriptor<T extends RouteSet.Data<any, any, any>>(
163
169
  self: T,
164
170
  ): T[typeof RouteDescriptor]
171
+ export function descriptor<E extends RouteDescriptor.Any>(self: RouteSet.Data<any, any, any>): E
165
172
  export function descriptor<T extends RouteSet.Data<any, any, any>>(
166
173
  self: Iterable<T>,
167
174
  ): Array<T[typeof RouteDescriptor]>
@@ -203,8 +210,10 @@ export const text = RouteBody.build<string, "text">({
203
210
  format: "text",
204
211
  })
205
212
 
206
- export const html = RouteBody.build<string, "html">({
213
+ export const html = RouteBody.build<string | JSX.Children, string, "html">({
207
214
  format: "html",
215
+ handle: (body) =>
216
+ typeof body === "string" ? body : Html.renderToString(body as JSX.Children),
208
217
  })
209
218
 
210
219
  export const json = RouteBody.build<Values.Json, "json">({
@@ -215,20 +224,31 @@ export const bytes = RouteBody.build<Uint8Array, "bytes">({
215
224
  format: "bytes",
216
225
  })
217
226
 
218
- export { render } from "./RouteBody.ts"
219
-
220
227
  export { sse } from "./RouteSse.ts"
221
228
 
222
- export function redirect(
229
+ export function redirect<D extends RouteDescriptor.Any, B, I extends Route.Tuple>(
223
230
  url: string | URL,
224
231
  options?: { status?: 301 | 302 | 303 | 307 | 308 },
225
- ): Entity.Entity<""> {
226
- return Entity.make("", {
227
- status: options?.status ?? 302,
228
- headers: {
229
- location: url instanceof URL ? url.href : url,
230
- },
231
- })
232
+ ): (
233
+ self: RouteSet.RouteSet<D, B, I>,
234
+ ) => RouteSet.RouteSet<D, B, [...I, Route.Route<{}, {}, "", never, never>]> {
235
+ const route = make<{}, {}, "">(
236
+ () =>
237
+ Effect.succeed(
238
+ Entity.make("", {
239
+ status: options?.status ?? 302,
240
+ headers: {
241
+ location: url instanceof URL ? url.href : url,
242
+ },
243
+ }),
244
+ ),
245
+ )
246
+
247
+ return (self) =>
248
+ set<D, B, [...I, Route.Route<{}, {}, "", never, never>]>(
249
+ [...items(self), route],
250
+ descriptor(self),
251
+ )
232
252
  }
233
253
 
234
254
  export class Routes extends Context.Tag("effect-start/Routes")<Routes, RouteTree.RouteTree>() {}
@@ -237,6 +257,42 @@ export function layer(routes: RouteTree.RouteMap | RouteTree.RouteTree) {
237
257
  return Layer.sync(Routes, () => (RouteTree.isRouteTree(routes) ? routes : RouteTree.make(routes)))
238
258
  }
239
259
 
260
+ /**
261
+ * Creates a route that short-curcits in development.
262
+ *
263
+ * Note that when we convert the routes to web handles in {@link import("./RouteHttp.ts")},
264
+ * we exclude them altogeteher in development.
265
+ */
266
+ export function devOnly<D extends RouteDescriptor.Any, B, I extends Route.Tuple>(
267
+ self: RouteSet.RouteSet<D, B, I>,
268
+ ): RouteSet.RouteSet<D, B, [...I, Route.Route<{ dev: true }, { dev: true }, unknown, any, any>]> {
269
+ const route: Route.Route<{ dev: true }, { dev: true }, unknown, any, any> = make<
270
+ { dev: true },
271
+ { dev: true },
272
+ unknown,
273
+ any,
274
+ any
275
+ >(
276
+ (context, next) =>
277
+ Effect.flatMap(Development.option, (developmentOption) =>
278
+ Option.isSome(developmentOption)
279
+ ? Effect.succeed(next({ ...context, dev: true }))
280
+ : Effect.succeed(Entity.make("", { status: 404 })),
281
+ ),
282
+ { dev: true },
283
+ )
284
+
285
+ const nextItems: [...I, Route.Route<{ dev: true }, { dev: true }, unknown, any, any>] = [
286
+ ...items(self),
287
+ route,
288
+ ]
289
+
290
+ return set<D, B, [...I, Route.Route<{ dev: true }, { dev: true }, unknown, any, any>]>(
291
+ nextItems,
292
+ descriptor(self),
293
+ )
294
+ }
295
+
240
296
  export { make as tree } from "./RouteTree.ts"
241
297
 
242
298
  export function lazy<T extends RouteSet.Any>(
package/src/RouteBody.ts CHANGED
@@ -3,6 +3,7 @@ import type * as Stream from "effect/Stream"
3
3
  import type * as Utils from "effect/Utils"
4
4
  import * as Entity from "./Entity.ts"
5
5
  import * as Route from "./Route.ts"
6
+ import * as StreamExtra from "./_StreamExtra.ts"
6
7
  import type * as Values from "./_Values.ts"
7
8
 
8
9
  export type Format = "text" | "html" | "json" | "bytes" | "*"
@@ -21,21 +22,36 @@ type YieldError<T> = T extends Utils.YieldWrap<Effect.Effect<any, infer E, any>>
21
22
 
22
23
  type YieldContext<T> = T extends Utils.YieldWrap<Effect.Effect<any, any, infer R>> ? R : never
23
24
 
25
+ type Next<B, A> = (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<UnwrapStream<A>>
26
+
27
+ type HandlerReturn<A> =
28
+ | A
29
+ | Entity.Entity<A>
30
+ | ((self: Route.RouteSet.Any) => Route.RouteSet.Any)
31
+
32
+ type HandlerFunction<B, A, E, R> = (
33
+ context: Values.Simplify<B>,
34
+ next: Next<B, A>,
35
+ ) =>
36
+ | Effect.Effect<HandlerReturn<A>, E, R>
37
+ | Generator<Utils.YieldWrap<Effect.Effect<unknown, E, R>>, HandlerReturn<A>, unknown>
38
+
24
39
  export type GeneratorHandler<B, A, Y> = (
25
40
  context: Values.Simplify<B>,
26
- next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<UnwrapStream<A>>,
27
- ) => Generator<Y, A | Entity.Entity<A>, never>
41
+ next: Next<B, A>,
42
+ ) => Generator<Y, HandlerReturn<A>, never>
28
43
 
29
44
  export type HandlerInput<B, A, E, R> =
30
45
  | A
31
46
  | Entity.Entity<A>
32
- | Effect.Effect<A | Entity.Entity<A>, E, R>
33
- | ((
34
- context: Values.Simplify<B>,
35
- next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<UnwrapStream<A>>,
36
- ) =>
37
- | Effect.Effect<A | Entity.Entity<A>, E, R>
38
- | Generator<Utils.YieldWrap<Effect.Effect<unknown, E, R>>, A | Entity.Entity<A>, unknown>)
47
+ | Effect.Effect<HandlerReturn<A>, E, R>
48
+ | HandlerFunction<B, A, E, R>
49
+
50
+ function isHandlerFunction<B, A, E, R>(
51
+ handler: HandlerInput<B, A, E, R>,
52
+ ): handler is HandlerFunction<B, A, E, R> {
53
+ return typeof handler === "function"
54
+ }
39
55
 
40
56
  export function handle<B, A, Y extends Utils.YieldWrap<Effect.Effect<any, any, any>>>(
41
57
  handler: GeneratorHandler<B, A, Y>,
@@ -46,38 +62,44 @@ export function handle<B, A, E, R>(
46
62
  export function handle<B, A, E, R>(
47
63
  handler: HandlerInput<B, A, E, R>,
48
64
  ): Route.Route.Handler<B, A, E, R> {
49
- if (typeof handler === "function") {
50
- return (
51
- context: B,
52
- next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<A>,
53
- ): Effect.Effect<Entity.Entity<A>, E, R> => {
54
- const result = (handler as Function)(context, next)
65
+ if (isHandlerFunction(handler)) {
66
+ return ((context: any, next: any) => {
67
+ const result = handler(context, next)
55
68
  const effect = Effect.isEffect(result)
56
- ? (result as Effect.Effect<A | Entity.Entity<A>, E, R>)
57
- : (Effect.gen(function* () {
69
+ ? result
70
+ : Effect.gen(function* () {
58
71
  return yield* result
59
- }) as Effect.Effect<A | Entity.Entity<A>, E, R>)
60
- return Effect.map(effect, normalizeToEntity)
61
- }
72
+ })
73
+ return Effect.flatMap(effect, normalizeToEntity)
74
+ }) as Route.Route.Handler<B, A, E, R>
62
75
  }
63
76
  if (Effect.isEffect(handler)) {
64
- return (_context, _next) =>
65
- Effect.map(handler, normalizeToEntity) as Effect.Effect<Entity.Entity<A>, E, R>
77
+ return ((_context: any, _next: any) =>
78
+ Effect.flatMap(handler, normalizeToEntity)) as Route.Route.Handler<B, A, E, R>
66
79
  }
67
80
  if (Entity.isEntity(handler)) {
68
81
  return (_context, _next) => Effect.succeed(handler as Entity.Entity<A>)
69
82
  }
70
- return (_context, _next) => Effect.succeed(normalizeToEntity(handler as A) as Entity.Entity<A>)
83
+ return ((_context: any, _next: any) =>
84
+ normalizeToEntity(handler)) as Route.Route.Handler<B, A, E, R>
71
85
  }
72
86
 
73
- function normalizeToEntity<A>(value: A | Entity.Entity<A>): Entity.Entity<A> {
87
+ function normalizeToEntity(value: unknown): Effect.Effect<Entity.Entity<any>> {
88
+ if (typeof value === "function") {
89
+ const result = (value as (self: Route.RouteSet.Any) => Route.RouteSet.Any)(Route.empty)
90
+ const routes = Route.items(result)
91
+ const route = routes[0]
92
+ if (route) {
93
+ return route.handler({}, () => Entity.make("")) as Effect.Effect<Entity.Entity<any>>
94
+ }
95
+ }
74
96
  if (Entity.isEntity(value)) {
75
- return value as Entity.Entity<A>
97
+ return Effect.succeed(value)
76
98
  }
77
- return Entity.make(value as A, { status: 200 })
99
+ return Effect.succeed(Entity.make(value, { status: 200 }))
78
100
  }
79
101
 
80
- export interface BuildReturn<Value, F extends Format> {
102
+ export interface BuildReturn<Value, F extends Format, Body = never> {
81
103
  <
82
104
  D extends Route.RouteDescriptor.Any,
83
105
  B,
@@ -91,7 +113,7 @@ export interface BuildReturn<Value, F extends Format> {
91
113
  ) => Route.RouteSet.RouteSet<
92
114
  D,
93
115
  B,
94
- [...I, Route.Route.Route<{ format: F }, {}, A, YieldError<Y>, YieldContext<Y>>]
116
+ [...I, Route.Route.Route<{ format: F }, {}, [Body] extends [never] ? A : Body, YieldError<Y>, YieldContext<Y>>]
95
117
  >
96
118
 
97
119
  <
@@ -105,50 +127,54 @@ export interface BuildReturn<Value, F extends Format> {
105
127
  handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: F }>, A, E, R>,
106
128
  ): (
107
129
  self: Route.RouteSet.RouteSet<D, B, I>,
108
- ) => Route.RouteSet.RouteSet<D, B, [...I, Route.Route.Route<{ format: F }, {}, A, E, R>]>
130
+ ) => Route.RouteSet.RouteSet<D, B, [...I, Route.Route.Route<{ format: F }, {}, [Body] extends [never] ? A : Body, E, R>]>
109
131
  }
110
132
 
111
- export function build<Value, F extends Format>(descriptors: { format: F }) {
133
+ export function build<Value, F extends Format>(options: { format: F }): BuildReturn<Value, F>
134
+ export function build<Value, Body, F extends Format>(options: {
135
+ format: F
136
+ handle: (body: Value) => Body
137
+ }): BuildReturn<Value, F, Body>
138
+ export function build<Value, F extends Format>(options: {
139
+ format: F
140
+ handle?: (body: any) => any
141
+ }): any {
142
+ const { handle: handleBody, ...descriptors } = options
112
143
  return function <
113
144
  D extends Route.RouteDescriptor.Any,
114
- B extends {},
145
+ B,
115
146
  I extends Route.Route.Tuple,
116
147
  A extends F extends "json" ? Value : Value | Stream.Stream<Value, any, any>,
117
148
  E = never,
118
149
  R = never,
119
150
  >(handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: F }>, A, E, R>) {
120
- return function (self: Route.RouteSet.RouteSet<D, B, I>) {
151
+ return (self: Route.RouteSet.RouteSet<D, B, I>) => {
121
152
  const contentType = formatToContentType[descriptors.format]
122
153
  const baseHandler = handle(handler)
123
- const wrappedHandler: Route.Route.Handler<
124
- D & B & Route.ExtractBindings<I> & { format: F },
125
- A,
126
- E,
127
- R
128
- > = (ctx, next) =>
129
- Effect.map(baseHandler(ctx as any, next as any), (entity) =>
130
- entity.headers["content-type"]
131
- ? entity
132
- : Entity.make(entity.body, {
133
- status: entity.status,
134
- url: entity.url,
135
- headers: { ...entity.headers, "content-type": contentType },
136
- }),
154
+ const wrappedHandler: Route.Route.Handler<{ format: F }, A, E, R> = (ctx, next) =>
155
+ baseHandler(ctx as D & B & Route.ExtractBindings<I> & { format: F }, next).pipe(
156
+ Effect.map((entity) => {
157
+ const body = handleBody && !StreamExtra.isStream(entity.body) ? handleBody(entity.body) : entity.body
158
+ if (body === entity.body && (entity.headers["content-type"] || contentType === undefined))
159
+ return entity
160
+ return Entity.make(body as A, {
161
+ status: entity.status,
162
+ url: entity.url,
163
+ headers: entity.headers["content-type"] || contentType === undefined
164
+ ? entity.headers
165
+ : { ...entity.headers, "content-type": contentType },
166
+ })
167
+ }),
137
168
  )
138
169
 
139
- const route = Route.make<{ format: F }, {}, A, E, R>(wrappedHandler as any, descriptors)
140
-
141
- const items: [...I, Route.Route.Route<{ format: F }, {}, A, E, R>] = [
142
- ...Route.items(self),
143
- route,
144
- ]
170
+ const route = Route.make<{ format: F }, {}, A, E, R>(wrappedHandler, descriptors)
145
171
 
146
172
  return Route.set<D, B, [...I, Route.Route.Route<{ format: F }, {}, A, E, R>]>(
147
- items,
173
+ [...Route.items(self), route],
148
174
  Route.descriptor(self),
149
175
  )
150
176
  }
151
- } as unknown as BuildReturn<Value, F>
177
+ } as BuildReturn<Value, F>
152
178
  }
153
179
 
154
180
  export type RenderValue = string | Uint8Array | Stream.Stream<string | Uint8Array, any, any>
@@ -188,16 +214,15 @@ export function render<
188
214
  E = never,
189
215
  R = never,
190
216
  >(handler: HandlerInput<NoInfer<D & B & Route.ExtractBindings<I> & { format: "*" }>, A, E, R>) {
191
- return function (self: Route.RouteSet.RouteSet<D, B, I>) {
192
- const route = Route.make<{ format: "*" }, {}, A, E, R>(handle(handler) as any, { format: "*" })
193
-
194
- const items: [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>] = [
195
- ...Route.items(self),
196
- route,
197
- ]
217
+ return (self: Route.RouteSet.RouteSet<D, B, I>) => {
218
+ const baseHandler = handle(handler)
219
+ const route = Route.make<{ format: "*" }, {}, A, E, R>(
220
+ (ctx, next) => baseHandler(ctx as D & B & Route.ExtractBindings<I> & { format: "*" }, next),
221
+ { format: "*" },
222
+ )
198
223
 
199
224
  return Route.set<D, B, [...I, Route.Route.Route<{ format: "*" }, {}, A, E, R>]>(
200
- items,
225
+ [...Route.items(self), route],
201
226
  Route.descriptor(self),
202
227
  )
203
228
  }
package/src/RouteHttp.ts CHANGED
@@ -8,6 +8,7 @@ import type * as ParseResult from "effect/ParseResult"
8
8
  import * as Runtime from "effect/Runtime"
9
9
  import * as Stream from "effect/Stream"
10
10
  import * as ContentNegotiation from "./_ContentNegotiation.ts"
11
+ import * as Development from "./Development.ts"
11
12
  import * as Entity from "./Entity.ts"
12
13
  import type * as Http from "./_Http.ts"
13
14
  import * as Route from "./Route.ts"
@@ -388,9 +389,26 @@ export function* walkHandles(
388
389
  runtime: Runtime.Runtime<never> = Runtime.defaultRuntime,
389
390
  ): Generator<[path: string, handler: Http.WebHandler]> {
390
391
  const pathGroups = new Map<string, Array<RouteMount.MountedRoute>>()
392
+ const runSync = Runtime.runSync(runtime)
393
+ const inDevelopment = Option.isSome(runSync(Development.option))
394
+ const developmentPaths = new Set<string>()
391
395
 
392
396
  for (const route of RouteTree.walk(tree)) {
393
- const path = Route.descriptor(route).path
397
+ const descriptor = Route.descriptor<{ path: string; dev?: boolean }>(route)
398
+ if (descriptor.dev === true) {
399
+ developmentPaths.add(descriptor.path)
400
+ }
401
+ }
402
+
403
+ for (const route of RouteTree.walk(tree)) {
404
+ const descriptor = Route.descriptor<{ path: string; dev?: boolean }>(route)
405
+ if (descriptor.dev === true) {
406
+ continue
407
+ }
408
+ const path = descriptor.path
409
+ if (!inDevelopment && developmentPaths.has(path)) {
410
+ continue
411
+ }
394
412
  const group = pathGroups.get(path) ?? []
395
413
  group.push(route)
396
414
  pathGroups.set(path, group)
package/src/RouteMount.ts CHANGED
@@ -83,7 +83,7 @@ function makeMethodDescriber<M extends RouteMount.Method>(method: M): RouteMount
83
83
  const result = f(methodSet)
84
84
  const resultItems = Route.items(result)
85
85
 
86
- // Items are already flat (only Routes), just merge method into each descriptor
86
+ // Items are already flat (only Routes), merge method into each descriptor
87
87
  const flattenedItems = resultItems.map((item) => {
88
88
  const itemDescriptor = Route.descriptor(item)
89
89
  const newDescriptor = { method, ...itemDescriptor }
package/src/System.ts CHANGED
@@ -67,7 +67,7 @@ export const which = (name: string): Effect.Effect<string, SystemError> =>
67
67
  )
68
68
 
69
69
  export const spawn = (
70
- cmd: readonly [string, ...Array<string>],
70
+ cmd: [string, ...Array<string>] | string[],
71
71
  options?: ChildProcess.Command.Options,
72
72
  ): Effect.Effect<
73
73
  ChildProcess.ChildProcessHandle,
@@ -57,7 +57,7 @@ const CommandProto = {
57
57
  export const isCommand = (u: unknown): u is Command => Predicate.hasProperty(u, TypeId)
58
58
 
59
59
  export const make = (
60
- cmd: readonly [string, ...Array<string>],
60
+ cmd: [string, ...Array<string>] | string[],
61
61
  options?: Command.Options,
62
62
  ): Command =>
63
63
  Object.assign(Object.create(CommandProto), {