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.
- package/dist/components/errorBoundary.d.ts +7 -0
- package/dist/components/errorBoundary.d.ts.map +1 -0
- package/dist/components/errorBoundary.js +6 -0
- package/dist/components/errorBoundary.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/suspense.d.ts +33 -0
- package/dist/components/suspense.d.ts.map +1 -0
- package/dist/components/suspense.js +113 -0
- package/dist/components/suspense.js.map +1 -0
- package/dist/constants.d.ts +4 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -11
- package/dist/constants.js.map +1 -1
- package/dist/dom.js +1 -3
- package/dist/dom.js.map +1 -1
- package/dist/hooks/utils.d.ts.map +1 -1
- package/dist/hooks/utils.js +1 -0
- package/dist/hooks/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/renderToString.d.ts.map +1 -1
- package/dist/renderToString.js +92 -36
- package/dist/renderToString.js.map +1 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +47 -10
- package/dist/scheduler.js.map +1 -1
- package/dist/ssr/server.d.ts +4 -1
- package/dist/ssr/server.d.ts.map +1 -1
- package/dist/ssr/server.js +135 -64
- package/dist/ssr/server.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.utils.d.ts +7 -1
- package/dist/types.utils.d.ts.map +1 -1
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +15 -8
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/runtime.d.ts +1 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js.map +1 -1
- package/dist/utils/vdom.d.ts +3 -1
- package/dist/utils/vdom.d.ts.map +1 -1
- package/dist/utils/vdom.js +6 -2
- package/dist/utils/vdom.js.map +1 -1
- package/package.json +1 -1
- package/src/components/errorBoundary.ts +16 -0
- package/src/components/index.ts +2 -0
- package/src/components/suspense.ts +193 -0
- package/src/constants.ts +6 -12
- package/src/dom.ts +1 -3
- package/src/hooks/utils.ts +1 -0
- package/src/index.ts +1 -0
- package/src/renderToString.ts +106 -40
- package/src/scheduler.ts +59 -12
- package/src/ssr/server.ts +161 -69
- package/src/types.ts +11 -0
- package/src/types.utils.ts +8 -0
- package/src/utils/format.ts +16 -12
- package/src/utils/runtime.ts +1 -1
- package/src/utils/vdom.ts +11 -0
package/src/renderToString.ts
CHANGED
|
@@ -8,75 +8,141 @@ import {
|
|
|
8
8
|
assertValidElementProps,
|
|
9
9
|
} from "./utils/index.js"
|
|
10
10
|
import { Signal } from "./signals/base.js"
|
|
11
|
-
import {
|
|
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
|
|
19
|
-
const
|
|
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
|
|
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
|
-
):
|
|
29
|
-
if (el === null) return
|
|
30
|
-
if (el === undefined) return
|
|
31
|
-
if (typeof el === "boolean") return
|
|
32
|
-
if (typeof el === "string")
|
|
33
|
-
|
|
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.
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
108
|
+
renderToString_internal(ctx, children, el, idx)
|
|
109
|
+
return
|
|
57
110
|
}
|
|
58
111
|
|
|
59
112
|
if (typeof type !== "string") {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
37
|
-
if (typeof el === "
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
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
|
|
112
|
+
const { type, props = {} } = el
|
|
62
113
|
const children = props.children
|
|
63
|
-
|
|
64
|
-
if (type === "#text")
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
renderToStream_internal(
|
|
72
|
-
|
|
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
|
-
|
|
158
|
+
|
|
159
|
+
// other exotic types
|
|
160
|
+
return renderToStream_internal(ctx, children, el, idx)
|
|
76
161
|
}
|
|
77
162
|
|
|
78
163
|
if (typeof type !== "string") {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/types.utils.ts
CHANGED
|
@@ -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"] & {
|
package/src/utils/format.ts
CHANGED
|
@@ -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(
|
|
26
|
-
.replace(
|
|
27
|
-
.replace(
|
|
28
|
-
.replace(
|
|
29
|
-
.replace(
|
|
30
|
-
.replace(
|
|
29
|
+
.replace(REGEX_AMP, "&")
|
|
30
|
+
.replace(REGEX_LT, "<")
|
|
31
|
+
.replace(REGEX_GT, ">")
|
|
32
|
+
.replace(REGEX_DBLQT, """)
|
|
33
|
+
.replace(REGEX_SQT, "'")
|
|
34
|
+
.replace(REGEX_SLASH, "/")
|
|
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(
|
|
84
|
+
const cssKey = key.replace(REGEX_ALPHA_UPPER, "-$&").toLowerCase()
|
|
81
85
|
cssString += `${cssKey}:${obj[key]};`
|
|
82
86
|
}
|
|
83
87
|
return cssString
|
package/src/utils/runtime.ts
CHANGED
|
@@ -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
|
|
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) {
|