Skip to content

Commit 2cf6004

Browse files
authored
Merge commit from fork
1 parent cf9a78d commit 2cf6004

File tree

2 files changed

+150
-11
lines changed

2 files changed

+150
-11
lines changed

src/jsx/components.test.tsx

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,67 @@ describe('ErrorBoundary', () => {
8686
errorBoundaryCounter--
8787
suspenseCounter--
8888
})
89+
90+
it('string content', async () => {
91+
const html = <ErrorBoundary fallback={<Fallback />}>{'< ok >'}</ErrorBoundary>
92+
93+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; ok &gt;')
94+
95+
errorBoundaryCounter--
96+
suspenseCounter--
97+
})
98+
99+
it('error: string content', async () => {
100+
const html = (
101+
<ErrorBoundary fallback={'< error >'}>
102+
<Component error={true} />
103+
</ErrorBoundary>
104+
)
105+
106+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
107+
108+
errorBoundaryCounter--
109+
suspenseCounter--
110+
})
111+
112+
it('error: Promise<string> from fallback', async () => {
113+
const html = (
114+
<ErrorBoundary fallback={Promise.resolve('< error >')}>
115+
<Component error={true} />
116+
</ErrorBoundary>
117+
)
118+
119+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
120+
121+
errorBoundaryCounter--
122+
suspenseCounter--
123+
})
124+
125+
it('error: string content from fallbackRender', async () => {
126+
const html = (
127+
<ErrorBoundary fallbackRender={() => '< error >'}>
128+
<Component error={true} />
129+
</ErrorBoundary>
130+
)
131+
132+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
133+
134+
errorBoundaryCounter--
135+
suspenseCounter--
136+
})
137+
138+
it('error: Promise<string> from fallbackRender', async () => {
139+
const html = (
140+
<ErrorBoundary fallbackRender={() => Promise.resolve('< error >')}>
141+
<Component error={true} />
142+
</ErrorBoundary>
143+
)
144+
145+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
146+
147+
errorBoundaryCounter--
148+
suspenseCounter--
149+
})
89150
})
90151

91152
describe('async', async () => {
@@ -123,6 +184,54 @@ describe('ErrorBoundary', () => {
123184

124185
suspenseCounter--
125186
})
187+
188+
it('error: string content', async () => {
189+
const html = (
190+
<ErrorBoundary fallback={'< error >'}>
191+
<Component error={true} />
192+
</ErrorBoundary>
193+
)
194+
195+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
196+
197+
suspenseCounter--
198+
})
199+
200+
it('error: Promise<string> from fallback', async () => {
201+
const html = (
202+
<ErrorBoundary fallback={Promise.resolve('< error >')}>
203+
<Component error={true} />
204+
</ErrorBoundary>
205+
)
206+
207+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
208+
209+
suspenseCounter--
210+
})
211+
212+
it('error: string content from fallbackRender', async () => {
213+
const html = (
214+
<ErrorBoundary fallbackRender={() => '< error >'}>
215+
<Component error={true} />
216+
</ErrorBoundary>
217+
)
218+
219+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
220+
221+
suspenseCounter--
222+
})
223+
224+
it('error: Promise<string> from fallbackRender', async () => {
225+
const html = (
226+
<ErrorBoundary fallbackRender={() => Promise.resolve('< error >')}>
227+
<Component error={true} />
228+
</ErrorBoundary>
229+
)
230+
231+
expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
232+
233+
suspenseCounter--
234+
})
126235
})
127236

128237
describe('async : nested', async () => {

src/jsx/components.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { raw } from '../helper/html'
22
import type { HtmlEscapedCallback, HtmlEscapedString } from '../utils/html'
33
import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html'
4+
import { jsx, Fragment } from './base'
45
import { DOM_RENDERER } from './constants'
56
import { useContext } from './context'
67
import { ErrorBoundary as ErrorBoundaryDomRenderer } from './dom/components'
@@ -25,6 +26,21 @@ export const childrenToString = async (children: Child[]): Promise<HtmlEscapedSt
2526
}
2627
}
2728

29+
const resolveChildEarly = (c: Child): HtmlEscapedString | Promise<HtmlEscapedString> => {
30+
if (c == null || typeof c === 'boolean') {
31+
return '' as HtmlEscapedString
32+
} else if (typeof c === 'string') {
33+
return c as HtmlEscapedString
34+
} else {
35+
const str = c.toString()
36+
if (!(str instanceof Promise)) {
37+
return raw(str)
38+
} else {
39+
return str as Promise<HtmlEscapedString>
40+
}
41+
}
42+
}
43+
2844
export type ErrorHandler = (error: Error) => void
2945
export type FallbackRender = (error: Error) => Child
3046

@@ -51,37 +67,51 @@ export const ErrorBoundary: FC<
5167
const nonce = useContext(StreamingContext)?.scriptNonce
5268

5369
let fallbackStr: string | undefined
54-
const fallbackRes = (error: Error): HtmlEscapedString => {
70+
const resolveFallbackStr = async () => {
71+
const awaitedFallback = await fallback
72+
if (typeof awaitedFallback === 'string') {
73+
fallbackStr = awaitedFallback
74+
} else {
75+
fallbackStr = await awaitedFallback?.toString()
76+
if (typeof fallbackStr === 'string') {
77+
// should not apply `raw` if fallbackStr is undefined, null, or boolean
78+
fallbackStr = raw(fallbackStr)
79+
}
80+
}
81+
}
82+
const fallbackRes = (error: Error): HtmlEscapedString | Promise<HtmlEscapedString> => {
5583
onError?.(error)
56-
return (fallbackStr || fallbackRender?.(error) || '').toString() as HtmlEscapedString
84+
return (fallbackStr ||
85+
(fallbackRender && jsx(Fragment, {}, fallbackRender(error) as HtmlEscapedString)) ||
86+
'') as HtmlEscapedString
5787
}
5888
let resArray: HtmlEscapedString[] | Promise<HtmlEscapedString[]>[] = []
5989
try {
60-
resArray = children.map((c) =>
61-
c == null || typeof c === 'boolean' ? '' : c.toString()
62-
) as HtmlEscapedString[]
90+
resArray = children.map(resolveChildEarly) as unknown as HtmlEscapedString[]
6391
} catch (e) {
64-
fallbackStr = await fallback?.toString()
92+
await resolveFallbackStr()
6593
if (e instanceof Promise) {
6694
resArray = [
6795
e.then(() => childrenToString(children as Child[])).catch((e) => fallbackRes(e)),
6896
] as Promise<HtmlEscapedString[]>[]
6997
} else {
70-
resArray = [fallbackRes(e as Error)]
98+
resArray = [fallbackRes(e as Error) as HtmlEscapedString]
7199
}
72100
}
73101

74102
if (resArray.some((res) => (res as {}) instanceof Promise)) {
75-
fallbackStr ||= await fallback?.toString()
103+
await resolveFallbackStr()
76104
const index = errorBoundaryCounter++
77105
const replaceRe = RegExp(`(<template id="E:${index}"></template>.*?)(.*?)(<!--E:${index}-->)`)
78106
const caught = false
79-
const catchCallback = ({ error, buffer }: { error: Error; buffer?: [string] }) => {
107+
const catchCallback = async ({ error, buffer }: { error: Error; buffer?: [string] }) => {
80108
if (caught) {
81109
return ''
82110
}
83111

84-
const fallbackResString = fallbackRes(error)
112+
const fallbackResString = await Fragment({
113+
children: fallbackRes(error),
114+
}).toString()
85115
if (buffer) {
86116
buffer[0] = buffer[0].replace(replaceRe, fallbackResString)
87117
}
@@ -195,7 +225,7 @@ d.remove()
195225
},
196226
])
197227
} else {
198-
return raw(resArray.join(''))
228+
return Fragment({ children: resArray as Child[] })
199229
}
200230
}
201231
;(ErrorBoundary as HasRenderToDom)[DOM_RENDERER] = ErrorBoundaryDomRenderer

0 commit comments

Comments
 (0)