effect-start 0.16.0 → 0.17.1

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 (172) hide show
  1. package/dist/Commander.d.ts +103 -0
  2. package/dist/Commander.js +333 -0
  3. package/dist/ContentNegotiation.d.ts +13 -0
  4. package/dist/ContentNegotiation.js +364 -0
  5. package/dist/Development.d.ts +34 -0
  6. package/dist/Development.js +52 -0
  7. package/dist/Entity.d.ts +47 -0
  8. package/dist/Entity.js +224 -0
  9. package/dist/FileRouter.d.ts +61 -0
  10. package/dist/FileRouter.js +203 -0
  11. package/dist/FileRouterCodegen.d.ts +19 -0
  12. package/dist/FileRouterCodegen.js +176 -0
  13. package/dist/FileRouterPattern.d.ts +9 -0
  14. package/dist/FileRouterPattern.js +35 -0
  15. package/dist/Http.d.ts +37 -0
  16. package/dist/Http.js +92 -0
  17. package/dist/HttpAppExtra.d.ts +7 -0
  18. package/dist/HttpAppExtra.js +320 -0
  19. package/dist/HttpUtils.d.ts +3 -0
  20. package/dist/HttpUtils.js +11 -0
  21. package/dist/PathPattern.d.ts +134 -0
  22. package/dist/PathPattern.js +415 -0
  23. package/dist/Random.d.ts +5 -0
  24. package/dist/Random.js +49 -0
  25. package/dist/Route.d.ts +98 -0
  26. package/dist/Route.js +81 -0
  27. package/dist/RouteBody.d.ts +53 -0
  28. package/dist/RouteBody.js +67 -0
  29. package/dist/RouteHook.d.ts +12 -0
  30. package/dist/RouteHook.js +45 -0
  31. package/dist/RouteHttp.d.ts +21 -0
  32. package/dist/RouteHttp.js +260 -0
  33. package/dist/RouteHttpTracer.d.ts +10 -0
  34. package/dist/RouteHttpTracer.js +62 -0
  35. package/dist/RouteMount.d.ts +119 -0
  36. package/dist/RouteMount.js +77 -0
  37. package/dist/RouteSchema.d.ts +65 -0
  38. package/dist/RouteSchema.js +155 -0
  39. package/dist/RouteSse.d.ts +21 -0
  40. package/dist/RouteSse.js +85 -0
  41. package/dist/RouteTree.d.ts +56 -0
  42. package/dist/RouteTree.js +91 -0
  43. package/dist/RouteTrie.d.ts +20 -0
  44. package/dist/RouteTrie.js +157 -0
  45. package/dist/RouterPattern.d.ts +118 -0
  46. package/dist/RouterPattern.js +269 -0
  47. package/dist/SchemaExtra.d.ts +7 -0
  48. package/dist/SchemaExtra.js +74 -0
  49. package/dist/Start.d.ts +19 -0
  50. package/dist/Start.js +23 -0
  51. package/dist/StartApp.d.ts +19 -0
  52. package/dist/StartApp.js +21 -0
  53. package/dist/StreamExtra.d.ts +28 -0
  54. package/dist/StreamExtra.js +100 -0
  55. package/dist/TuplePathPattern.d.ts +9 -0
  56. package/dist/TuplePathPattern.js +63 -0
  57. package/dist/Values.d.ts +26 -0
  58. package/dist/Values.js +30 -0
  59. package/dist/bun/BunBundle.d.ts +12 -0
  60. package/dist/bun/BunBundle.js +145 -0
  61. package/dist/bun/BunHttpServer.d.ts +44 -0
  62. package/dist/bun/BunHttpServer.js +187 -0
  63. package/dist/bun/BunHttpServer_web.d.ts +60 -0
  64. package/dist/bun/BunHttpServer_web.js +252 -0
  65. package/dist/bun/BunImportTrackerPlugin.d.ts +13 -0
  66. package/dist/bun/BunImportTrackerPlugin.js +71 -0
  67. package/dist/bun/BunRoute.d.ts +49 -0
  68. package/dist/bun/BunRoute.js +131 -0
  69. package/dist/bun/BunRuntime.d.ts +1 -0
  70. package/dist/bun/BunRuntime.js +26 -0
  71. package/dist/bun/BunVirtualFilesPlugin.d.ts +4 -0
  72. package/dist/bun/BunVirtualFilesPlugin.js +40 -0
  73. package/dist/bun/_BunEnhancedResolve.d.ts +45 -0
  74. package/dist/bun/_BunEnhancedResolve.js +104 -0
  75. package/dist/bun/index.d.ts +4 -0
  76. package/dist/bun/index.js +4 -0
  77. package/dist/bundler/Bundle.d.ts +60 -0
  78. package/dist/bundler/Bundle.js +48 -0
  79. package/dist/bundler/BundleFiles.d.ts +13 -0
  80. package/dist/bundler/BundleFiles.js +94 -0
  81. package/dist/bundler/BundleHttp.d.ts +45 -0
  82. package/dist/bundler/BundleHttp.js +176 -0
  83. package/dist/client/Overlay.d.ts +2 -0
  84. package/dist/client/Overlay.js +32 -0
  85. package/dist/client/ScrollState.d.ts +6 -0
  86. package/dist/client/ScrollState.js +98 -0
  87. package/dist/client/index.d.ts +6 -0
  88. package/dist/client/index.js +81 -0
  89. package/dist/experimental/EncryptedCookies.d.ts +51 -0
  90. package/dist/experimental/EncryptedCookies.js +243 -0
  91. package/dist/experimental/SseHttpResponse.d.ts +7 -0
  92. package/dist/experimental/SseHttpResponse.js +28 -0
  93. package/dist/experimental/index.d.ts +2 -0
  94. package/dist/experimental/index.js +2 -0
  95. package/dist/hyper/Hyper.d.ts +32 -0
  96. package/dist/hyper/Hyper.js +34 -0
  97. package/dist/hyper/HyperHtml.d.ts +23 -0
  98. package/dist/hyper/HyperHtml.js +144 -0
  99. package/dist/hyper/HyperNode.d.ts +14 -0
  100. package/dist/hyper/HyperNode.js +11 -0
  101. package/dist/hyper/HyperRoute.d.ts +8 -0
  102. package/dist/hyper/HyperRoute.js +32 -0
  103. package/dist/hyper/HyperRoute.test.d.ts +1 -0
  104. package/dist/hyper/HyperRoute.test.js +72 -0
  105. package/dist/hyper/index.d.ts +4 -0
  106. package/dist/hyper/index.js +4 -0
  107. package/dist/hyper/jsx-runtime.d.ts +7 -0
  108. package/dist/hyper/jsx-runtime.js +8 -0
  109. package/dist/index.d.ts +6 -0
  110. package/dist/index.js +6 -0
  111. package/dist/inference_check.d.ts +1 -0
  112. package/dist/inference_check.js +15 -0
  113. package/dist/middlewares/BasicAuthMiddleware.d.ts +8 -0
  114. package/dist/middlewares/BasicAuthMiddleware.js +22 -0
  115. package/dist/middlewares/index.d.ts +1 -0
  116. package/dist/middlewares/index.js +1 -0
  117. package/dist/node/FileSystem.d.ts +9 -0
  118. package/dist/node/FileSystem.js +440 -0
  119. package/dist/node/Utils.d.ts +1 -0
  120. package/dist/node/Utils.js +19 -0
  121. package/dist/repro_fail.d.ts +1 -0
  122. package/dist/repro_fail.js +14 -0
  123. package/dist/testing/TestHttpClient.d.ts +13 -0
  124. package/dist/testing/TestHttpClient.js +68 -0
  125. package/dist/testing/TestLogger.d.ts +13 -0
  126. package/dist/testing/TestLogger.js +29 -0
  127. package/dist/testing/index.d.ts +3 -0
  128. package/dist/testing/index.js +3 -0
  129. package/dist/testing/utils.d.ts +9 -0
  130. package/dist/testing/utils.js +39 -0
  131. package/dist/x/cloudflare/CloudflareTunnel.d.ts +13 -0
  132. package/dist/x/cloudflare/CloudflareTunnel.js +43 -0
  133. package/dist/x/cloudflare/index.d.ts +1 -0
  134. package/dist/x/cloudflare/index.js +1 -0
  135. package/dist/x/datastar/Datastar.d.ts +6 -0
  136. package/dist/x/datastar/Datastar.js +46 -0
  137. package/dist/x/datastar/index.d.ts +2 -0
  138. package/dist/x/datastar/index.js +2 -0
  139. package/dist/x/tailwind/TailwindPlugin.d.ts +23 -0
  140. package/dist/x/tailwind/TailwindPlugin.js +219 -0
  141. package/dist/x/tailwind/compile.d.ts +19 -0
  142. package/dist/x/tailwind/compile.js +156 -0
  143. package/dist/x/tailwind/plugin.d.ts +2 -0
  144. package/dist/x/tailwind/plugin.js +15 -0
  145. package/package.json +67 -14
  146. package/src/Development.test.ts +119 -0
  147. package/src/Development.ts +137 -0
  148. package/src/Entity.test.ts +1 -1
  149. package/src/Entity.ts +3 -6
  150. package/src/FileRouter.ts +2 -2
  151. package/src/Route.ts +4 -0
  152. package/src/RouteBody.test.ts +43 -45
  153. package/src/RouteBody.ts +137 -10
  154. package/src/RouteHttp.test.ts +4 -1
  155. package/src/RouteHttp.ts +22 -6
  156. package/src/RouteSse.test.ts +249 -0
  157. package/src/RouteSse.ts +195 -0
  158. package/src/Values.ts +9 -7
  159. package/src/bun/BunBundle.ts +0 -73
  160. package/src/bun/BunRoute.ts +0 -2
  161. package/src/hyper/HyperHtml.test.ts +119 -0
  162. package/src/hyper/HyperHtml.ts +10 -2
  163. package/src/hyper/HyperNode.ts +2 -0
  164. package/src/hyper/HyperRoute.test.tsx +199 -0
  165. package/src/hyper/HyperRoute.ts +61 -0
  166. package/src/hyper/index.ts +4 -0
  167. package/src/hyper/jsx.d.ts +15 -0
  168. package/src/index.ts +1 -0
  169. package/src/node/FileSystem.ts +8 -0
  170. package/src/x/tailwind/compile.ts +8 -2
  171. package/src/FileSystemExtra.test.ts +0 -242
  172. package/src/FileSystemExtra.ts +0 -66
package/src/Values.ts CHANGED
@@ -4,16 +4,18 @@ type JsonPrimitives =
4
4
  | boolean
5
5
  | null
6
6
 
7
+ export type JsonObject = {
8
+ [key: string]:
9
+ | Json
10
+ // undefined won't be included in JSON objects but this will allow
11
+ // to use Json type in functions that return object of multiple shapes
12
+ | undefined
13
+ }
14
+
7
15
  export type Json =
8
16
  | JsonPrimitives
9
17
  | Json[]
10
- | {
11
- [key: string]:
12
- | Json
13
- // undefined won't be included in JSON objects but this will allow
14
- // to use Json type in functions that return object of multiple shapes
15
- | undefined
16
- }
18
+ | JsonObject
17
19
 
18
20
  export function isPlainObject(
19
21
  value: unknown,
@@ -9,10 +9,7 @@ import {
9
9
  Iterable,
10
10
  Layer,
11
11
  pipe,
12
- PubSub,
13
12
  Record,
14
- Stream,
15
- SynchronizedRef,
16
13
  } from "effect"
17
14
  import * as NPath from "node:path"
18
15
  import type {
@@ -20,7 +17,6 @@ import type {
20
17
  BundleManifest,
21
18
  } from "../bundler/Bundle.ts"
22
19
  import * as Bundle from "../bundler/Bundle.ts"
23
- import * as FileSystemExtra from "../FileSystemExtra.ts"
24
20
  import { BunImportTrackerPlugin } from "./index.ts"
25
21
 
26
22
  export type BuildOptions = Omit<
@@ -125,75 +121,6 @@ export function layer<T>(
125
121
  return Layer.effect(tag, build(config))
126
122
  }
127
123
 
128
- export function layerDev<T>(
129
- tag: Context.Tag<T, BundleContext>,
130
- config: BuildOptions,
131
- ) {
132
- return Layer.scoped(
133
- tag,
134
- Effect.gen(function*() {
135
- const loadRefKey = "_loadRef"
136
- const sharedBundle = yield* build(config)
137
-
138
- const loadRef = yield* SynchronizedRef.make(null)
139
- sharedBundle[loadRefKey] = loadRef
140
- sharedBundle.events = yield* PubSub.unbounded<Bundle.BundleEvent>()
141
-
142
- yield* Effect.fork(
143
- pipe(
144
- FileSystemExtra.watchSource({
145
- filter: FileSystemExtra.filterSourceFiles,
146
- }),
147
- Stream.map(v =>
148
- ({
149
- _tag: "Change",
150
- path: v.path,
151
- }) as Bundle.BundleEvent
152
- ),
153
- Stream.onError(err =>
154
- Effect.logError("Error while watching files", err)
155
- ),
156
- Stream.runForEach((v) =>
157
- pipe(
158
- Effect.gen(function*() {
159
- yield* Effect.logDebug("Updating bundle: " + tag.key)
160
-
161
- const newBundle = yield* build(config)
162
-
163
- Object.assign(sharedBundle, newBundle)
164
-
165
- // Clean old loaded bundle
166
- yield* SynchronizedRef.update(loadRef, () => null)
167
-
168
- // publish event after the built
169
- if (sharedBundle.events) {
170
- yield* PubSub.publish(sharedBundle.events, v)
171
- }
172
- }),
173
- Effect.catchAll(err =>
174
- Effect.gen(function*() {
175
- yield* Effect.logError(
176
- "Error while updating bundle",
177
- err,
178
- )
179
- if (sharedBundle.events) {
180
- yield* PubSub.publish(sharedBundle.events, {
181
- _tag: "BuildError",
182
- error: String(err),
183
- })
184
- }
185
- })
186
- ),
187
- )
188
- ),
189
- ),
190
- )
191
-
192
- return sharedBundle
193
- }),
194
- )
195
- }
196
-
197
124
  /**
198
125
  * Finds common path prefix across provided paths.
199
126
  */
@@ -162,8 +162,6 @@ export function htmlBundle(
162
162
  }
163
163
  }
164
164
 
165
-
166
-
167
165
  type BunServerFetchHandler = (
168
166
  request: Request,
169
167
  server: Bun.Server<unknown>,
@@ -88,3 +88,122 @@ test.it("mixed boolean and string attributes", () => {
88
88
  .expect(html)
89
89
  .toBe("<input type=\"checkbox\" checked name=\"test\" value=\"on\">")
90
90
  })
91
+
92
+ test.it("data-* attributes with object values are JSON stringified", () => {
93
+ const node = HyperNode.make("div", {
94
+ "data-signals": {
95
+ draft: "",
96
+ pendingDraft: "",
97
+ username: "User123",
98
+ },
99
+ })
100
+
101
+ const html = HyperHtml.renderToString(node)
102
+
103
+ test
104
+ .expect(html)
105
+ .toBe(
106
+ "<div data-signals=\"{&quot;draft&quot;:&quot;&quot;,&quot;pendingDraft&quot;:&quot;&quot;,&quot;username&quot;:&quot;User123&quot;}\"></div>",
107
+ )
108
+ })
109
+
110
+ test.it("data-* attributes with array values are JSON stringified", () => {
111
+ const node = HyperNode.make("div", {
112
+ "data-items": [1, 2, 3],
113
+ })
114
+
115
+ const html = HyperHtml.renderToString(node)
116
+
117
+ test
118
+ .expect(html)
119
+ .toBe("<div data-items=\"[1,2,3]\"></div>")
120
+ })
121
+
122
+ test.it("data-* attributes with nested object values", () => {
123
+ const node = HyperNode.make("div", {
124
+ "data-config": {
125
+ user: { name: "John", active: true },
126
+ settings: { theme: "dark" },
127
+ },
128
+ })
129
+
130
+ const html = HyperHtml.renderToString(node)
131
+
132
+ test
133
+ .expect(html)
134
+ .toBe(
135
+ "<div data-config=\"{&quot;user&quot;:{&quot;name&quot;:&quot;John&quot;,&quot;active&quot;:true},&quot;settings&quot;:{&quot;theme&quot;:&quot;dark&quot;}}\"></div>",
136
+ )
137
+ })
138
+
139
+ test.it("data-* string values are not JSON stringified", () => {
140
+ const node = HyperNode.make("div", {
141
+ "data-value": "hello world",
142
+ })
143
+
144
+ const html = HyperHtml.renderToString(node)
145
+
146
+ test
147
+ .expect(html)
148
+ .toBe("<div data-value=\"hello world\"></div>")
149
+ })
150
+
151
+ test.it("non-data attributes with object values are not JSON stringified", () => {
152
+ const node = HyperNode.make("div", {
153
+ style: "color: red",
154
+ })
155
+
156
+ const html = HyperHtml.renderToString(node)
157
+
158
+ test
159
+ .expect(html)
160
+ .toBe("<div style=\"color: red\"></div>")
161
+ })
162
+
163
+ test.it("script with function child renders as IIFE", () => {
164
+ const handler = (window: Window) => {
165
+ console.log("Hello from", window.document.title)
166
+ }
167
+
168
+ const node = HyperNode.make("script", {
169
+ children: handler,
170
+ })
171
+
172
+ const html = HyperHtml.renderToString(node)
173
+
174
+ test
175
+ .expect(html)
176
+ .toBe(`<script>(${handler.toString()})(window)</script>`)
177
+ })
178
+
179
+ test.it("script with arrow function child renders as IIFE", () => {
180
+ const node = HyperNode.make("script", {
181
+ children: (window: Window) => {
182
+ window.alert("test")
183
+ },
184
+ })
185
+
186
+ const html = HyperHtml.renderToString(node)
187
+
188
+ test
189
+ .expect(html)
190
+ .toContain("<script>(")
191
+ test
192
+ .expect(html)
193
+ .toContain(")(window)</script>")
194
+ test
195
+ .expect(html)
196
+ .toContain("window.alert")
197
+ })
198
+
199
+ test.it("script with string child renders normally", () => {
200
+ const node = HyperNode.make("script", {
201
+ children: "console.log('hello')",
202
+ })
203
+
204
+ const html = HyperHtml.renderToString(node)
205
+
206
+ test
207
+ .expect(html)
208
+ .toBe("<script>console.log(&apos;hello&apos;)</script>")
209
+ })
@@ -120,8 +120,13 @@ export function renderToString(
120
120
  result += ` ${esc(key)}`
121
121
  } else {
122
122
  const resolvedKey = key === "className" ? "class" : key
123
+ const value = props[key]
123
124
 
124
- result += ` ${esc(resolvedKey)}="${esc(props[key])}"`
125
+ if (key.startsWith("data-") && typeof value === "object") {
126
+ result += ` ${esc(resolvedKey)}="${esc(JSON.stringify(value))}"`
127
+ } else {
128
+ result += ` ${esc(resolvedKey)}="${esc(value)}"`
129
+ }
125
130
  }
126
131
  }
127
132
  }
@@ -139,7 +144,10 @@ export function renderToString(
139
144
  result += html
140
145
  } else {
141
146
  const children = props.children
142
- if (Array.isArray(children)) {
147
+
148
+ if (type === "script" && typeof children === "function") {
149
+ result += `(${children.toString()})(window)`
150
+ } else if (Array.isArray(children)) {
143
151
  for (let i = children.length - 1; i >= 0; i--) {
144
152
  stack.push(children[i])
145
153
  }
@@ -12,6 +12,8 @@ export type Props = {
12
12
  | Primitive
13
13
  | HyperNode
14
14
  | Iterable<Primitive | HyperNode>
15
+ | Record<string, unknown>
16
+ | ((window: Window) => void)
15
17
  }
16
18
 
17
19
  export type HyperComponent = (
@@ -0,0 +1,199 @@
1
+ /** @jsxImportSource effect-start */
2
+ import * as test from "bun:test"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Entity from "../Entity.ts"
5
+ import * as Http from "../Http.ts"
6
+ import * as Route from "../Route.ts"
7
+ import * as RouteHttp from "../RouteHttp.ts"
8
+ import * as HyperRoute from "./HyperRoute.ts"
9
+
10
+ test.describe("HyperRoute.html", () => {
11
+ test.it("renders JSX to HTML string", async () => {
12
+ const handler = RouteHttp.toWebHandler(
13
+ Route.get(
14
+ HyperRoute.html(
15
+ <div>
16
+ Hello World
17
+ </div>,
18
+ ),
19
+ ),
20
+ )
21
+
22
+ const response = await Http.fetch(handler, { path: "/" })
23
+
24
+ test.expect(response.status).toBe(200)
25
+ test.expect(response.headers.get("Content-Type")).toBe(
26
+ "text/html; charset=utf-8",
27
+ )
28
+ test.expect(await response.text()).toBe("<div>Hello World</div>")
29
+ })
30
+
31
+ test.it("renders nested JSX elements", async () => {
32
+ const handler = RouteHttp.toWebHandler(
33
+ Route.get(
34
+ HyperRoute.html(
35
+ <div class="container">
36
+ <h1>
37
+ Title
38
+ </h1>
39
+ <p>
40
+ Paragraph
41
+ </p>
42
+ </div>,
43
+ ),
44
+ ),
45
+ )
46
+
47
+ const response = await Http.fetch(handler, { path: "/" })
48
+
49
+ test.expect(await response.text()).toBe(
50
+ "<div class=\"container\"><h1>Title</h1><p>Paragraph</p></div>",
51
+ )
52
+ })
53
+
54
+ test.it("renders JSX from Effect", async () => {
55
+ const handler = RouteHttp.toWebHandler(
56
+ Route.get(
57
+ HyperRoute.html(
58
+ Effect.succeed(
59
+ <span>
60
+ From Effect
61
+ </span>,
62
+ ),
63
+ ),
64
+ ),
65
+ )
66
+
67
+ const response = await Http.fetch(handler, { path: "/" })
68
+
69
+ test.expect(await response.text()).toBe("<span>From Effect</span>")
70
+ })
71
+
72
+ test.it("renders JSX from generator function", async () => {
73
+ const handler = RouteHttp.toWebHandler(
74
+ Route.get(
75
+ HyperRoute.html(
76
+ Effect.gen(function*() {
77
+ const name = yield* Effect.succeed("World")
78
+ return (
79
+ <div>
80
+ Hello {name}
81
+ </div>
82
+ )
83
+ }),
84
+ ),
85
+ ),
86
+ )
87
+
88
+ const response = await Http.fetch(handler, { path: "/" })
89
+
90
+ test.expect(await response.text()).toBe("<div>Hello World</div>")
91
+ })
92
+
93
+ test.it("renders JSX from handler function", async () => {
94
+ const handler = RouteHttp.toWebHandler(
95
+ Route.get(
96
+ HyperRoute.html((context) =>
97
+ Effect.succeed(
98
+ <div>
99
+ Request received
100
+ </div>,
101
+ )
102
+ ),
103
+ ),
104
+ )
105
+
106
+ const response = await Http.fetch(handler, { path: "/" })
107
+
108
+ test.expect(await response.text()).toBe("<div>Request received</div>")
109
+ })
110
+
111
+ test.it("renders JSX with dynamic content", async () => {
112
+ const items = ["Apple", "Banana", "Cherry"]
113
+
114
+ const handler = RouteHttp.toWebHandler(
115
+ Route.get(
116
+ HyperRoute.html(
117
+ <ul>
118
+ {items.map((item) => (
119
+ <li>
120
+ {item}
121
+ </li>
122
+ ))}
123
+ </ul>,
124
+ ),
125
+ ),
126
+ )
127
+
128
+ const response = await Http.fetch(handler, { path: "/" })
129
+
130
+ test.expect(await response.text()).toBe(
131
+ "<ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>",
132
+ )
133
+ })
134
+
135
+ test.it("handles Entity with JSX body", async () => {
136
+ const handler = RouteHttp.toWebHandler(
137
+ Route.get(
138
+ HyperRoute.html(
139
+ Entity.make(
140
+ <div>
141
+ With Entity
142
+ </div>,
143
+ { status: 201 },
144
+ ),
145
+ ),
146
+ ),
147
+ )
148
+
149
+ const response = await Http.fetch(handler, { path: "/" })
150
+
151
+ test.expect(response.status).toBe(201)
152
+ test.expect(await response.text()).toBe("<div>With Entity</div>")
153
+ })
154
+
155
+ test.it("renders data-* attributes with object values as JSON", async () => {
156
+ const handler = RouteHttp.toWebHandler(
157
+ Route.get(
158
+ HyperRoute.html(
159
+ <div
160
+ data-signals={{
161
+ draft: "",
162
+ pendingDraft: "",
163
+ username: "User123",
164
+ }}
165
+ >
166
+ Content
167
+ </div>,
168
+ ),
169
+ ),
170
+ )
171
+
172
+ const response = await Http.fetch(handler, { path: "/" })
173
+
174
+ test.expect(await response.text()).toBe(
175
+ "<div data-signals=\"{&quot;draft&quot;:&quot;&quot;,&quot;pendingDraft&quot;:&quot;&quot;,&quot;username&quot;:&quot;User123&quot;}\">Content</div>",
176
+ )
177
+ })
178
+
179
+ test.it("renders script with function child as IIFE", async () => {
180
+ const handler = RouteHttp.toWebHandler(
181
+ Route.get(
182
+ HyperRoute.html(
183
+ <script>
184
+ {(window) => {
185
+ console.log("Hello from", window.document.title)
186
+ }}
187
+ </script>,
188
+ ),
189
+ ),
190
+ )
191
+
192
+ const response = await Http.fetch(handler, { path: "/" })
193
+ const text = await response.text()
194
+
195
+ test.expect(text).toContain("<script>(")
196
+ test.expect(text).toContain(")(window)</script>")
197
+ test.expect(text).toContain("window.document.title")
198
+ })
199
+ })
@@ -0,0 +1,61 @@
1
+ import * as Effect from "effect/Effect"
2
+ import * as Entity from "../Entity.ts"
3
+ import * as Route from "../Route.ts"
4
+ import type * as RouteBody from "../RouteBody.ts"
5
+ import * as HyperHtml from "./HyperHtml.ts"
6
+ import type { JSX } from "./jsx.d.ts"
7
+
8
+ function renderValue(
9
+ value: JSX.Children | Entity.Entity<JSX.Children>,
10
+ ): string | Entity.Entity<string> {
11
+ if (Entity.isEntity(value)) {
12
+ return Entity.make(HyperHtml.renderToString(value.body), {
13
+ status: value.status,
14
+ url: value.url,
15
+ headers: value.headers,
16
+ })
17
+ }
18
+ return HyperHtml.renderToString(value)
19
+ }
20
+
21
+ function normalizeToEffect<B, A, E, R>(
22
+ handler: RouteBody.HandlerInput<B, A, E, R>,
23
+ context: B,
24
+ next: (context?: Partial<B> & Record<string, unknown>) => Entity.Entity<A>,
25
+ ): Effect.Effect<A | Entity.Entity<A>, E, R> {
26
+ if (Effect.isEffect(handler)) {
27
+ return handler
28
+ }
29
+ if (typeof handler === "function") {
30
+ const result = (handler as Function)(context, next)
31
+ if (Effect.isEffect(result)) {
32
+ return result as Effect.Effect<A | Entity.Entity<A>, E, R>
33
+ }
34
+ return Effect.gen(function*() {
35
+ return yield* result
36
+ }) as Effect.Effect<A | Entity.Entity<A>, E, R>
37
+ }
38
+ return Effect.succeed(handler as A | Entity.Entity<A>)
39
+ }
40
+
41
+ export function html<
42
+ D extends Route.RouteDescriptor.Any,
43
+ B extends {},
44
+ I extends Route.Route.Tuple,
45
+ E = never,
46
+ R = never,
47
+ >(
48
+ handler: RouteBody.HandlerInput<
49
+ NoInfer<D & B & Route.ExtractBindings<I> & { format: "html" }>,
50
+ JSX.Children,
51
+ E,
52
+ R
53
+ >,
54
+ ) {
55
+ return Route.html<D, B, I, string, E, R>((context, next) =>
56
+ Effect.map(
57
+ normalizeToEffect(handler, context, next as never),
58
+ renderValue,
59
+ )
60
+ )
61
+ }
@@ -0,0 +1,4 @@
1
+ export * as Hyper from "./Hyper.ts"
2
+ export * as HyperHtml from "./HyperHtml.ts"
3
+ export * as HyperNode from "./HyperNode.ts"
4
+ export * as HyperRoute from "./HyperRoute.ts"
@@ -1818,6 +1818,21 @@ export namespace JSX {
1818
1818
  event?: string | undefined
1819
1819
  /** @deprecated */
1820
1820
  language?: string | undefined
1821
+
1822
+ children?: Children
1823
+ }
1824
+
1825
+ // Separate interface for script elements with function children.
1826
+ // This enables TypeScript to infer the `window` parameter type.
1827
+ //
1828
+ // Using a union in a single interface (`children?: Function | Children`)
1829
+ // doesn't work because TS can't infer callback parameter types from unions.
1830
+ // By splitting into two interfaces, TS can discriminate based on children
1831
+ interface ScriptHTMLAttributesWithHandler<T>
1832
+ extends Omit<ScriptHTMLAttributes<T>, "children" | "type">
1833
+ {
1834
+ children: (window: Window) => void
1835
+ type?: never
1821
1836
  }
1822
1837
  interface SelectHTMLAttributes<T> extends HTMLAttributes<T> {
1823
1838
  autocomplete?: HTMLAutocomplete | undefined
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as Bundle from "./bundler/Bundle.ts"
2
+ export * as Development from "./Development.ts"
2
3
  export * as Entity from "./Entity.ts"
3
4
  export * as FileRouter from "./FileRouter.ts"
4
5
  export * as Route from "./Route.ts"
@@ -1,3 +1,6 @@
1
+ /*
2
+ * Adapted from @effect/platform
3
+ */
1
4
  import { effectify } from "@effect/platform/Effectify"
2
5
  import * as Error from "@effect/platform/Error"
3
6
  import type {
@@ -715,6 +718,11 @@ const makeFileSystem = Effect.map(
715
718
 
716
719
  export const layer = Layer.effect(FileSystem.FileSystem, makeFileSystem)
717
720
 
721
+ export {
722
+ Error,
723
+ FileSystem,
724
+ }
725
+
718
726
  export function handleErrnoException(
719
727
  module: SystemError["module"],
720
728
  method: string,
@@ -88,13 +88,19 @@ async function ensureSourceDetectionRootExists(compiler: {
88
88
  }
89
89
  }
90
90
 
91
- export async function compileAst(ast: AstNode[], options: CompileOptions) {
91
+ export async function compileAst(
92
+ ast: AstNode[],
93
+ options: CompileOptions,
94
+ ): ReturnType<typeof _compileAst> {
92
95
  let compiler = await _compileAst(ast, createCompileOptions(options))
93
96
  await ensureSourceDetectionRootExists(compiler)
94
97
  return compiler
95
98
  }
96
99
 
97
- export async function compile(css: string, options: CompileOptions) {
100
+ export async function compile(
101
+ css: string,
102
+ options: CompileOptions,
103
+ ): ReturnType<typeof _compile> {
98
104
  let compiler = await _compile(css, createCompileOptions(options))
99
105
  await ensureSourceDetectionRootExists(compiler)
100
106
  return compiler