kiru 0.49.0 → 0.50.0-preview.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 (65) hide show
  1. package/dist/components/errorBoundary.d.ts +7 -0
  2. package/dist/components/errorBoundary.d.ts.map +1 -0
  3. package/dist/components/errorBoundary.js +6 -0
  4. package/dist/components/errorBoundary.js.map +1 -0
  5. package/dist/components/index.d.ts +2 -0
  6. package/dist/components/index.d.ts.map +1 -1
  7. package/dist/components/index.js +2 -0
  8. package/dist/components/index.js.map +1 -1
  9. package/dist/components/suspense.d.ts +33 -0
  10. package/dist/components/suspense.d.ts.map +1 -0
  11. package/dist/components/suspense.js +113 -0
  12. package/dist/components/suspense.js.map +1 -0
  13. package/dist/constants.d.ts +4 -11
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/constants.js +4 -11
  16. package/dist/constants.js.map +1 -1
  17. package/dist/dom.js +1 -3
  18. package/dist/dom.js.map +1 -1
  19. package/dist/hooks/utils.d.ts.map +1 -1
  20. package/dist/hooks/utils.js +1 -0
  21. package/dist/hooks/utils.js.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/renderToString.d.ts.map +1 -1
  27. package/dist/renderToString.js +92 -36
  28. package/dist/renderToString.js.map +1 -1
  29. package/dist/scheduler.d.ts.map +1 -1
  30. package/dist/scheduler.js +47 -10
  31. package/dist/scheduler.js.map +1 -1
  32. package/dist/ssr/server.d.ts +4 -1
  33. package/dist/ssr/server.d.ts.map +1 -1
  34. package/dist/ssr/server.js +135 -64
  35. package/dist/ssr/server.js.map +1 -1
  36. package/dist/types.d.ts +10 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/types.utils.d.ts +7 -1
  39. package/dist/types.utils.d.ts.map +1 -1
  40. package/dist/utils/format.d.ts.map +1 -1
  41. package/dist/utils/format.js +15 -8
  42. package/dist/utils/format.js.map +1 -1
  43. package/dist/utils/runtime.d.ts +1 -1
  44. package/dist/utils/runtime.d.ts.map +1 -1
  45. package/dist/utils/runtime.js.map +1 -1
  46. package/dist/utils/vdom.d.ts +3 -1
  47. package/dist/utils/vdom.d.ts.map +1 -1
  48. package/dist/utils/vdom.js +6 -2
  49. package/dist/utils/vdom.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/errorBoundary.ts +16 -0
  52. package/src/components/index.ts +2 -0
  53. package/src/components/suspense.ts +193 -0
  54. package/src/constants.ts +6 -12
  55. package/src/dom.ts +1 -3
  56. package/src/hooks/utils.ts +1 -0
  57. package/src/index.ts +1 -0
  58. package/src/renderToString.ts +106 -40
  59. package/src/scheduler.ts +59 -12
  60. package/src/ssr/server.ts +161 -69
  61. package/src/types.ts +11 -0
  62. package/src/types.utils.ts +8 -0
  63. package/src/utils/format.ts +16 -12
  64. package/src/utils/runtime.ts +1 -1
  65. package/src/utils/vdom.ts +11 -0
@@ -8,75 +8,141 @@ import {
8
8
  assertValidElementProps,
9
9
  } from "./utils/index.js"
10
10
  import { Signal } from "./signals/base.js"
11
- import { $HYDRATION_BOUNDARY, voidElements } from "./constants.js"
11
+ import {
12
+ $ERROR_BOUNDARY,
13
+ $HYDRATION_BOUNDARY,
14
+ voidElements,
15
+ } from "./constants.js"
12
16
  import { HYDRATION_BOUNDARY_MARKER } from "./ssr/hydrationBoundary.js"
13
17
  import { __DEV__ } from "./env.js"
18
+ import { ErrorBoundaryNode } from "./types.utils.js"
19
+ import { isSuspenseThrowValue } from "./components/suspense.js"
20
+
21
+ interface StringRenderContext {
22
+ write(chunk: string): void
23
+ beginNewBoundary(): number
24
+ resetBoundary(idx: number): void
25
+ }
14
26
 
15
27
  export function renderToString(element: JSX.Element) {
16
28
  const prev = renderMode.current
17
29
  renderMode.current = "string"
18
- const rootNode = Fragment({ children: element })
19
- const res = renderToString_internal(rootNode, null, 0)
30
+ const parts: string[] = [""]
31
+ const ctx: StringRenderContext = {
32
+ write(chunk) {
33
+ parts[parts.length - 1] += chunk
34
+ },
35
+ beginNewBoundary() {
36
+ parts.push("")
37
+ return parts.length - 1
38
+ },
39
+ resetBoundary(idx) {
40
+ parts[idx] = ""
41
+ },
42
+ }
43
+ renderToString_internal(ctx, Fragment({ children: element }), null, 0)
20
44
  renderMode.current = prev
21
- return res
45
+ return parts.join("")
22
46
  }
23
47
 
24
48
  function renderToString_internal(
49
+ ctx: StringRenderContext,
25
50
  el: unknown,
26
51
  parent: Kiru.VNode | null,
27
52
  idx: number
28
- ): string {
29
- if (el === null) return ""
30
- if (el === undefined) return ""
31
- if (typeof el === "boolean") return ""
32
- if (typeof el === "string") return encodeHtmlEntities(el)
33
- if (typeof el === "number" || typeof el === "bigint") return el.toString()
53
+ ): void {
54
+ if (el === null) return
55
+ if (el === undefined) return
56
+ if (typeof el === "boolean") return
57
+ if (typeof el === "string") {
58
+ return ctx.write(encodeHtmlEntities(el))
59
+ }
60
+ if (typeof el === "number" || typeof el === "bigint") {
61
+ return ctx.write(el.toString())
62
+ }
34
63
  if (el instanceof Array) {
35
- return el.map((c, i) => renderToString_internal(c, parent, i)).join("")
64
+ return el.forEach((c, i) => renderToString_internal(ctx, c, parent, i))
65
+ }
66
+ if (Signal.isSignal(el)) {
67
+ return ctx.write(String(el.peek()))
68
+ }
69
+ if (!isVNode(el)) {
70
+ return ctx.write(String(el))
36
71
  }
37
- if (Signal.isSignal(el)) return String(el.peek())
38
- if (!isVNode(el)) return String(el)
39
72
  el.parent = parent
40
73
  el.depth = (parent?.depth ?? -1) + 1
41
74
  el.index = idx
42
- const props = el.props ?? {}
43
- const type = el.type
44
- if (type === "#text") return encodeHtmlEntities(props.nodeValue ?? "")
75
+ const { type, props = {} } = el
76
+ if (type === "#text") {
77
+ return ctx.write(encodeHtmlEntities(props.nodeValue ?? ""))
78
+ }
45
79
 
46
80
  const children = props.children
47
81
  if (isExoticType(type)) {
48
82
  if (type === $HYDRATION_BOUNDARY) {
49
- return `<!--${HYDRATION_BOUNDARY_MARKER}-->${renderToString_internal(
50
- children,
51
- el,
52
- idx
53
- )}<!--/${HYDRATION_BOUNDARY_MARKER}-->`
83
+ ctx.write(`<!--${HYDRATION_BOUNDARY_MARKER}-->`)
84
+ renderToString_internal(ctx, children, el, idx)
85
+ ctx.write(`<!--/${HYDRATION_BOUNDARY_MARKER}-->`)
86
+ return
87
+ }
88
+
89
+ if (type === $ERROR_BOUNDARY) {
90
+ const boundaryIdx = ctx.beginNewBoundary()
91
+ try {
92
+ renderToString_internal(ctx, children, el, idx)
93
+ } catch (error) {
94
+ if (isSuspenseThrowValue(error)) {
95
+ throw error
96
+ }
97
+ ctx.resetBoundary(boundaryIdx)
98
+ const e = error instanceof Error ? error : new Error(String(error))
99
+ const { fallback, onError } = props as ErrorBoundaryNode["props"]
100
+ onError?.(e)
101
+ const fallbackContent =
102
+ typeof fallback === "function" ? fallback(e) : fallback
103
+ renderToString_internal(ctx, fallbackContent, el, 0)
104
+ }
105
+ return
54
106
  }
55
107
 
56
- return renderToString_internal(children, el, idx)
108
+ renderToString_internal(ctx, children, el, idx)
109
+ return
57
110
  }
58
111
 
59
112
  if (typeof type !== "string") {
60
- node.current = el
61
- const res = type(props)
62
- node.current = null
63
- return renderToString_internal(res, el, idx)
113
+ try {
114
+ node.current = el
115
+ const res = type(props)
116
+ renderToString_internal(ctx, res, el, idx)
117
+ return
118
+ } catch (error) {
119
+ if (isSuspenseThrowValue(error)) {
120
+ return renderToString_internal(ctx, error.fallback, el, 0)
121
+ }
122
+ throw error
123
+ } finally {
124
+ node.current = null
125
+ }
64
126
  }
65
127
 
66
- if (__DEV__) {
67
- assertValidElementProps(el)
68
- }
128
+ if (__DEV__) assertValidElementProps(el)
69
129
  const attrs = propsToElementAttributes(props)
70
- const inner =
71
- "innerHTML" in props
72
- ? Signal.isSignal(props.innerHTML)
73
- ? props.innerHTML.peek()
74
- : props.innerHTML
75
- : Array.isArray(children)
76
- ? children.map((c, i) => renderToString_internal(c, el, i)).join("")
77
- : renderToString_internal(children, el, 0)
130
+ ctx.write(`<${type}${attrs.length ? ` ${attrs}` : ""}>`)
131
+
132
+ if (voidElements.has(type)) return
78
133
 
79
- return `<${type}${attrs.length ? ` ${attrs}` : ""}>${
80
- voidElements.has(type) ? "" : `${inner}</${type}>`
81
- }`
134
+ if ("innerHTML" in props) {
135
+ ctx.write(
136
+ String(
137
+ Signal.isSignal(props.innerHTML)
138
+ ? props.innerHTML.peek()
139
+ : props.innerHTML
140
+ )
141
+ )
142
+ } else if (Array.isArray(children)) {
143
+ children.forEach((c, i) => renderToString_internal(ctx, c, el, i))
144
+ } else {
145
+ renderToString_internal(ctx, children, el, 0)
146
+ }
147
+ ctx.write(`</${type}>`)
82
148
  }
package/src/scheduler.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type {
2
2
  ContextProviderNode,
3
3
  DomVNode,
4
+ ErrorBoundaryNode,
4
5
  FunctionVNode,
5
6
  } from "./types.utils"
6
7
  import {
7
8
  $CONTEXT_PROVIDER,
9
+ $ERROR_BOUNDARY,
8
10
  CONSECUTIVE_DIRTY_LIMIT,
9
11
  FLAG_DELETION,
10
12
  FLAG_DIRTY,
@@ -30,6 +32,7 @@ import {
30
32
  traverseApply,
31
33
  isExoticType,
32
34
  getVNodeAppContext,
35
+ findParentErrorBoundary,
33
36
  } from "./utils/index.js"
34
37
  import type { AppContext } from "./appContext"
35
38
 
@@ -202,21 +205,10 @@ function doWork(): void {
202
205
  function performUnitOfWork(vNode: VNode): VNode | void {
203
206
  let renderChild = true
204
207
  try {
205
- const { props } = vNode
206
208
  if (typeof vNode.type === "string") {
207
209
  updateHostComponent(vNode as DomVNode)
208
210
  } else if (isExoticType(vNode.type)) {
209
- if (vNode?.type === $CONTEXT_PROVIDER) {
210
- const {
211
- props: { dependents, value },
212
- prev,
213
- } = vNode as ContextProviderNode<unknown>
214
-
215
- if (dependents.size && prev && prev.props.value !== value) {
216
- dependents.forEach(queueUpdate)
217
- }
218
- }
219
- vNode.child = reconcileChildren(vNode, props.children)
211
+ updateExoticComponent(vNode)
220
212
  } else {
221
213
  renderChild = updateFunctionComponent(vNode as FunctionVNode)
222
214
  }
@@ -229,6 +221,18 @@ function performUnitOfWork(vNode: VNode): VNode | void {
229
221
  )
230
222
  }
231
223
 
224
+ const handler = findParentErrorBoundary(vNode)
225
+ if (handler) {
226
+ const e = (handler.error =
227
+ error instanceof Error ? error : new Error(String(error)))
228
+
229
+ handler.props.onError?.(e)
230
+ if (handler.depth < currentWorkRoot!.depth) {
231
+ currentWorkRoot = handler
232
+ }
233
+ return handler
234
+ }
235
+
232
236
  if (KiruError.isKiruError(error)) {
233
237
  if (error.customNodeStack) {
234
238
  setTimeout(() => {
@@ -279,6 +283,35 @@ function performUnitOfWork(vNode: VNode): VNode | void {
279
283
  }
280
284
  }
281
285
 
286
+ function updateExoticComponent(vNode: VNode) {
287
+ const { props, type } = vNode
288
+ let children = props.children
289
+
290
+ if (type === $CONTEXT_PROVIDER) {
291
+ const {
292
+ props: { dependents, value },
293
+ prev,
294
+ } = vNode as ContextProviderNode<unknown>
295
+
296
+ if (dependents.size && prev && prev.props.value !== value) {
297
+ dependents.forEach(queueUpdate)
298
+ }
299
+ } else if (type === $ERROR_BOUNDARY) {
300
+ const n = vNode as ErrorBoundaryNode
301
+ const { error } = n
302
+ if (error) {
303
+ children =
304
+ typeof props.fallback === "function"
305
+ ? props.fallback(error)
306
+ : props.fallback
307
+
308
+ delete n.error
309
+ }
310
+ }
311
+
312
+ vNode.child = reconcileChildren(vNode, children)
313
+ }
314
+
282
315
  function updateFunctionComponent(vNode: FunctionVNode) {
283
316
  const { type, props, subs, prev, flags } = vNode
284
317
  if (flags & FLAG_MEMO) {
@@ -318,6 +351,20 @@ function updateFunctionComponent(vNode: FunctionVNode) {
318
351
 
319
352
  if (__DEV__) {
320
353
  newChild = latest(type)(props)
354
+
355
+ if (vNode.hmrUpdated && vNode.hooks && vNode.hookSig) {
356
+ const len = vNode.hooks.length
357
+ if (hookIndex.current < len) {
358
+ // clean up any hooks that were removed
359
+ for (let i = hookIndex.current; i < len; i++) {
360
+ const hook = vNode.hooks[i]
361
+ hook.cleanup?.()
362
+ }
363
+ vNode.hooks.length = hookIndex.current
364
+ vNode.hookSig.length = hookIndex.current
365
+ }
366
+ }
367
+
321
368
  delete vNode.hmrUpdated
322
369
  if (++renderTryCount > CONSECUTIVE_DIRTY_LIMIT) {
323
370
  throw new KiruError({
package/src/ssr/server.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Readable } from "node:stream"
2
2
  import { Fragment } from "../element.js"
3
- import { renderMode, node } from "../globals.js"
3
+ import { renderMode, node, hookIndex } from "../globals.js"
4
4
  import {
5
5
  isVNode,
6
6
  encodeHtmlEntities,
@@ -9,102 +9,194 @@ import {
9
9
  assertValidElementProps,
10
10
  } from "../utils/index.js"
11
11
  import { Signal } from "../signals/base.js"
12
- import { $HYDRATION_BOUNDARY, voidElements } from "../constants.js"
12
+ import {
13
+ $HYDRATION_BOUNDARY,
14
+ $ERROR_BOUNDARY,
15
+ PREFETCHED_DATA_EVENT,
16
+ voidElements,
17
+ } from "../constants.js"
13
18
  import { HYDRATION_BOUNDARY_MARKER } from "./hydrationBoundary.js"
14
19
  import { __DEV__ } from "../env.js"
20
+ import type { ErrorBoundaryNode } from "../types.utils"
21
+ import { isSuspenseThrowValue } from "../components/suspense.js"
15
22
 
16
- export function renderToReadableStream(element: JSX.Element): Readable {
17
- const prev = renderMode.current
18
- renderMode.current = "stream"
19
- const stream = new Readable()
23
+ interface ServerRenderContext {
24
+ write: (chunk: string) => void
25
+ queuePendingData: (data: Kiru.StatefulPromise<unknown>[]) => void
26
+ }
27
+
28
+ const PREFETCH_EVENTS_SETUP = `
29
+ <script type="text/javascript">
30
+ const d = document,
31
+ m = (window["${PREFETCHED_DATA_EVENT}"] ??= new Map());
32
+ d.querySelectorAll("[x-data]").forEach((p) => {
33
+ const id = p.getAttribute("id");
34
+ const { data, error } = JSON.parse(p.innerHTML);
35
+ m.set(id, { data, error });
36
+ const event = new CustomEvent("${PREFETCHED_DATA_EVENT}", { detail: { id, data, error } });
37
+ window.dispatchEvent(event);
38
+ p.remove();
39
+ });
40
+ d.currentScript.remove()
41
+ </script>
42
+ `
43
+
44
+ export function renderToReadableStream(element: JSX.Element): {
45
+ immediate: string
46
+ stream: Readable
47
+ } {
48
+ const stream = new Readable({ read() {} })
20
49
  const rootNode = Fragment({ children: element })
50
+ const prefetchPromises = new Set<Kiru.StatefulPromise<unknown>>()
51
+ const pendingWritePromises: Promise<unknown>[] = []
21
52
 
22
- renderToStream_internal(stream, rootNode, null, 0)
23
- stream.push(null)
53
+ let immediate = ""
54
+
55
+ const ctx: ServerRenderContext = {
56
+ write: (chunk) => (immediate += chunk),
57
+ queuePendingData(data) {
58
+ for (const promise of data) {
59
+ if (prefetchPromises.has(promise)) continue
60
+ prefetchPromises.add(promise)
61
+
62
+ const writePromise = promise
63
+ .then(() => ({ data: promise.value }))
64
+ .catch(() => ({ error: promise.error?.message }))
65
+ .then((value) => {
66
+ const content = JSON.stringify(value)
67
+ stream.push(
68
+ `<script id="${promise.id}" x-data type="application/json">${content}</script>`
69
+ )
70
+ })
71
+
72
+ pendingWritePromises.push(writePromise)
73
+ }
74
+ },
75
+ }
76
+
77
+ const prev = renderMode.current
78
+ renderMode.current = "stream"
79
+ renderToStream_internal(ctx, rootNode, null, 0)
24
80
  renderMode.current = prev
25
81
 
26
- return stream
82
+ if (pendingWritePromises.length > 0) {
83
+ Promise.all(pendingWritePromises).then(() => {
84
+ stream.push(PREFETCH_EVENTS_SETUP)
85
+ stream.push(null)
86
+ })
87
+ } else {
88
+ stream.push(null)
89
+ }
90
+
91
+ return { immediate, stream }
27
92
  }
28
93
 
29
94
  function renderToStream_internal(
30
- stream: Readable,
95
+ ctx: ServerRenderContext,
31
96
  el: unknown,
32
97
  parent: Kiru.VNode | null,
33
98
  idx: number
34
99
  ): void {
35
- if (el === null) return
36
- if (el === undefined) return
37
- if (typeof el === "boolean") return
38
- if (typeof el === "string") {
39
- stream.push(encodeHtmlEntities(el))
40
- return
41
- }
42
- if (typeof el === "number" || typeof el === "bigint") {
43
- stream.push(el.toString())
44
- return
45
- }
46
- if (el instanceof Array) {
47
- el.forEach((c, i) => renderToStream_internal(stream, c, parent, i))
48
- return
49
- }
50
- if (Signal.isSignal(el)) {
51
- stream.push(String(el.peek()))
52
- return
53
- }
54
- if (!isVNode(el)) {
55
- stream.push(String(el))
56
- return
57
- }
100
+ if (el === null || el === undefined || typeof el === "boolean") return
101
+ if (typeof el === "string") return ctx.write(encodeHtmlEntities(el))
102
+ if (typeof el === "number" || typeof el === "bigint")
103
+ return ctx.write(el.toString())
104
+ if (el instanceof Array)
105
+ return el.forEach((c, i) => renderToStream_internal(ctx, c, parent, i))
106
+ if (Signal.isSignal(el)) return ctx.write(String(el.peek()))
107
+ if (!isVNode(el)) return ctx.write(String(el))
108
+
58
109
  el.parent = parent
59
110
  el.depth = (parent?.depth ?? -1) + 1
60
111
  el.index = idx
61
- const props = el.props ?? {}
112
+ const { type, props = {} } = el
62
113
  const children = props.children
63
- const type = el.type
64
- if (type === "#text") {
65
- stream.push(encodeHtmlEntities(props.nodeValue ?? ""))
66
- return
67
- }
114
+
115
+ if (type === "#text")
116
+ return ctx.write(encodeHtmlEntities(props.nodeValue ?? ""))
117
+
68
118
  if (isExoticType(type)) {
69
119
  if (type === $HYDRATION_BOUNDARY) {
70
- stream.push(`<!--${HYDRATION_BOUNDARY_MARKER}-->`)
71
- renderToStream_internal(stream, children, el, idx)
72
- stream.push(`<!--/${HYDRATION_BOUNDARY_MARKER}-->`)
120
+ ctx.write(`<!--${HYDRATION_BOUNDARY_MARKER}-->`)
121
+ renderToStream_internal(ctx, children, el, idx)
122
+ ctx.write(`<!--/${HYDRATION_BOUNDARY_MARKER}-->`)
123
+ return
124
+ }
125
+
126
+ if (type === $ERROR_BOUNDARY) {
127
+ let boundaryBuffer = ""
128
+ const localPromises = new Set<Kiru.StatefulPromise<unknown>>()
129
+
130
+ const boundaryCtx: ServerRenderContext = {
131
+ write(chunk) {
132
+ boundaryBuffer += chunk
133
+ },
134
+ queuePendingData(data) {
135
+ data.forEach((p) => localPromises.add(p))
136
+ },
137
+ }
138
+
139
+ try {
140
+ renderToStream_internal(boundaryCtx, children, el, idx)
141
+ // flush successful render
142
+ ctx.write(boundaryBuffer)
143
+ // merge local promises into global queue
144
+ ctx.queuePendingData([...localPromises])
145
+ } catch (error) {
146
+ if (isSuspenseThrowValue(error)) {
147
+ throw error
148
+ }
149
+ const e = error instanceof Error ? error : new Error(String(error))
150
+ const { fallback, onError } = props as ErrorBoundaryNode["props"]
151
+ onError?.(e)
152
+ const fallbackContent =
153
+ typeof fallback === "function" ? fallback(e) : fallback
154
+ renderToStream_internal(ctx, fallbackContent, el, 0)
155
+ }
73
156
  return
74
157
  }
75
- return renderToStream_internal(stream, children, el, idx)
158
+
159
+ // other exotic types
160
+ return renderToStream_internal(ctx, children, el, idx)
76
161
  }
77
162
 
78
163
  if (typeof type !== "string") {
79
- node.current = el
80
- const res = type(props)
81
- node.current = null
82
- return renderToStream_internal(stream, res, parent, idx)
164
+ try {
165
+ hookIndex.current = 0
166
+ node.current = el
167
+ const res = type(props)
168
+ return renderToStream_internal(ctx, res, el, idx)
169
+ } catch (error) {
170
+ if (isSuspenseThrowValue(error)) {
171
+ const { fallback, pendingData } = error
172
+ if (pendingData) ctx.queuePendingData(pendingData)
173
+ renderToStream_internal(ctx, fallback, el, 0)
174
+ return
175
+ }
176
+ throw error
177
+ } finally {
178
+ node.current = null
179
+ }
83
180
  }
84
181
 
85
- if (__DEV__) {
86
- assertValidElementProps(el)
87
- }
182
+ if (__DEV__) assertValidElementProps(el)
88
183
  const attrs = propsToElementAttributes(props)
89
- stream.push(`<${type}${attrs.length ? ` ${attrs}` : ""}>`)
90
-
91
- if (!voidElements.has(type)) {
92
- if ("innerHTML" in props) {
93
- stream.push(
94
- String(
95
- Signal.isSignal(props.innerHTML)
96
- ? props.innerHTML.peek()
97
- : props.innerHTML
98
- )
99
- )
100
- } else {
101
- if (Array.isArray(children)) {
102
- children.forEach((c, i) => renderToStream_internal(stream, c, el, i))
103
- } else {
104
- renderToStream_internal(stream, children, el, 0)
105
- }
106
- }
184
+ ctx.write(`<${type}${attrs.length ? ` ${attrs}` : ""}>`)
107
185
 
108
- stream.push(`</${type}>`)
186
+ if (voidElements.has(type)) return
187
+
188
+ if ("innerHTML" in props) {
189
+ ctx.write(
190
+ String(
191
+ Signal.isSignal(props.innerHTML)
192
+ ? props.innerHTML.peek()
193
+ : props.innerHTML
194
+ )
195
+ )
196
+ } else if (Array.isArray(children)) {
197
+ children.forEach((c, i) => renderToStream_internal(ctx, c, el, i))
198
+ } else {
199
+ renderToStream_internal(ctx, children, el, 0)
109
200
  }
201
+ ctx.write(`</${type}>`)
110
202
  }
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@ import type { ReadonlySignal, Signal as SignalClass } from "./signals"
2
2
  import type {
3
3
  $CONTEXT,
4
4
  $CONTEXT_PROVIDER,
5
+ $ERROR_BOUNDARY,
5
6
  $FRAGMENT,
6
7
  $HYDRATION_BOUNDARY,
7
8
  } from "./constants"
@@ -167,6 +168,15 @@ declare global {
167
168
 
168
169
  type Ref<T> = RefCallback<T> | RefObject<T> | null | undefined
169
170
 
171
+ interface PromiseState<T> {
172
+ id: string
173
+ state: "pending" | "fulfilled" | "rejected"
174
+ value?: T
175
+ error?: Error
176
+ }
177
+
178
+ interface StatefulPromise<T> extends Promise<T>, PromiseState<T> {}
179
+
170
180
  type RenderMode = "dom" | "hydrate" | "string" | "stream"
171
181
 
172
182
  type StateSetter<T> = T | ((prev: T) => T)
@@ -176,6 +186,7 @@ declare global {
176
186
  type ExoticSymbol =
177
187
  | typeof $FRAGMENT
178
188
  | typeof $CONTEXT_PROVIDER
189
+ | typeof $ERROR_BOUNDARY
179
190
  | typeof $HYDRATION_BOUNDARY
180
191
 
181
192
  interface VNode {
@@ -1,10 +1,12 @@
1
1
  import type {
2
2
  $CONTEXT_PROVIDER,
3
+ $ERROR_BOUNDARY,
3
4
  $FRAGMENT,
4
5
  $HYDRATION_BOUNDARY,
5
6
  } from "./constants"
6
7
  import type { HydrationBoundaryMode } from "./ssr/hydrationBoundary"
7
8
  import type { Signal } from "./signals"
9
+ import type { ErrorBoundaryProps } from "./components/errorBoundary"
8
10
 
9
11
  export type SomeElement = HTMLElement | SVGElement
10
12
  export type SomeDom = HTMLElement | SVGElement | Text
@@ -33,6 +35,12 @@ export interface ContextProviderNode<T> extends Kiru.VNode {
33
35
  }
34
36
  }
35
37
 
38
+ export interface ErrorBoundaryNode extends Kiru.VNode {
39
+ type: typeof $ERROR_BOUNDARY
40
+ props: ErrorBoundaryProps
41
+ error?: Error
42
+ }
43
+
36
44
  export interface HydrationBoundaryNode extends Kiru.VNode {
37
45
  type: typeof $HYDRATION_BOUNDARY
38
46
  props: Kiru.VNode["props"] & {
@@ -1,9 +1,5 @@
1
1
  import { unwrap } from "../signals/index.js"
2
- import {
3
- booleanAttributes,
4
- REGEX_UNIT,
5
- snakeCaseAttributes,
6
- } from "../constants.js"
2
+ import { booleanAttributes, snakeCaseAttributes } from "../constants.js"
7
3
 
8
4
  export {
9
5
  className,
@@ -16,18 +12,26 @@ export {
16
12
  safeStringify,
17
13
  }
18
14
 
15
+ const REGEX_AMP = /&/g
16
+ const REGEX_LT = /</g
17
+ const REGEX_GT = />/g
18
+ const REGEX_SQT = /'/g
19
+ const REGEX_DBLQT = /"/g
20
+ const REGEX_SLASH = /\//g
21
+ const REGEX_ALPHA_UPPER = /[A-Z]/g
22
+
19
23
  function className(...classes: (string | false | null | undefined)[]): string {
20
24
  return classes.filter(Boolean).join(" ")
21
25
  }
22
26
 
23
27
  function encodeHtmlEntities(text: string): string {
24
28
  return text
25
- .replace(REGEX_UNIT.AMP_G, "&amp;")
26
- .replace(REGEX_UNIT.LT_G, "&lt;")
27
- .replace(REGEX_UNIT.GT_G, "&gt;")
28
- .replace(REGEX_UNIT.DBLQT_G, "&quot;")
29
- .replace(REGEX_UNIT.SQT_G, "&#039;")
30
- .replace(REGEX_UNIT.SLASH_G, "&#47;")
29
+ .replace(REGEX_AMP, "&amp;")
30
+ .replace(REGEX_LT, "&lt;")
31
+ .replace(REGEX_GT, "&gt;")
32
+ .replace(REGEX_DBLQT, "&quot;")
33
+ .replace(REGEX_SQT, "&#039;")
34
+ .replace(REGEX_SLASH, "&#47;")
31
35
  }
32
36
 
33
37
  const propFilters = {
@@ -77,7 +81,7 @@ function propToHtmlAttr(key: string): string {
77
81
  function styleObjectToString(obj: Partial<CSSStyleDeclaration>): string {
78
82
  let cssString = ""
79
83
  for (const key in obj) {
80
- const cssKey = key.replace(REGEX_UNIT.ALPHA_UPPER_G, "-$&").toLowerCase()
84
+ const cssKey = key.replace(REGEX_ALPHA_UPPER, "-$&").toLowerCase()
81
85
  cssString += `${cssKey}:${obj[key]};`
82
86
  }
83
87
  return cssString
@@ -7,7 +7,7 @@ export { latest, sideEffectsEnabled }
7
7
  * This is a no-op in production. It is used to get the latest
8
8
  * iteration of a component or signal after HMR has happened.
9
9
  */
10
- function latest<T>(thing: T): T {
10
+ function latest<T extends Exclude<object, null>>(thing: T): T {
11
11
  let tgt: any = thing
12
12
  if (__DEV__) {
13
13
  while ("__next" in tgt) {