acrylblends 0.6.2 → 0.6.3

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 (181) hide show
  1. package/README.md +4 -1
  2. package/docs/framework/README.md +3 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/cordis/acryl-in-a-cordis-app.md +6 -0
  5. package/docs/framework/getting-started/basic.md +1 -0
  6. package/docs/framework/getting-started/desktop.md +41 -0
  7. package/docs/framework/guides/jsx-dom.md +0 -3
  8. package/docs/framework/guides/jsx.md +0 -3
  9. package/docs/framework/helpers/css.md +0 -3
  10. package/docs/framework/upstream.json +3 -3
  11. package/lib/accepts.d.ts +1 -1
  12. package/lib/acryl-base.d.ts +1 -1
  13. package/lib/adapter.d.ts +1 -1
  14. package/lib/{app-BdrVHBlJ.d.ts → app-wFwpg-bt.d.ts} +1 -1
  15. package/lib/base-BpQy2Rk7.d.ts +783 -0
  16. package/lib/base-ddiVmiE7.js +721 -0
  17. package/lib/basic-auth.d.ts +1 -1
  18. package/lib/bearer-auth.d.ts +1 -1
  19. package/lib/body-limit.d.ts +1 -1
  20. package/lib/cache.d.ts +1 -1
  21. package/lib/client.d.ts +2 -2
  22. package/lib/combine.d.ts +1 -1
  23. package/lib/common-D8Z4JRCw.d.ts +46 -0
  24. package/lib/components-Ce5FRyHU.js +268 -0
  25. package/lib/components-DYZdxJIJ.js +24 -0
  26. package/lib/compress.d.ts +1 -1
  27. package/lib/conninfo.d.ts +2 -2
  28. package/lib/constants-BR9jM9BC.js +9 -0
  29. package/lib/{context-B5Y1lLha.d.ts → context-DVQBjuns.d.ts} +1 -1
  30. package/lib/context-DxFdY_un.d.ts +250 -0
  31. package/lib/context-storage.d.ts +1 -1
  32. package/lib/{cookie-D9_nQbsM.d.ts → cookie-CCelEqky.d.ts} +1 -1
  33. package/lib/cookie.d.ts +3 -3
  34. package/lib/cors.d.ts +1 -1
  35. package/lib/csrf.d.ts +1 -1
  36. package/lib/css-D2jb7X8b.js +320 -0
  37. package/lib/css.d.ts +82 -0
  38. package/lib/css.js +134 -0
  39. package/lib/dev.d.ts +2 -2
  40. package/lib/etag.d.ts +1 -1
  41. package/lib/factory.d.ts +2 -2
  42. package/lib/hooks-Bq5q9IMl.js +762 -0
  43. package/lib/html-SllAe0YT.d.ts +46 -0
  44. package/lib/html.d.ts +1 -1
  45. package/lib/index-C850MeBA.d.ts +94 -0
  46. package/lib/{index-m12zcmph.d.ts → index-CUTfU69R.d.ts} +3 -3
  47. package/lib/{index-DtEoak1i.d.ts → index-Cqcdluyn.d.ts} +2 -2
  48. package/lib/{index-Kqou8Fjh.d.ts → index-CsvB-F9j.d.ts} +2 -2
  49. package/lib/index.d.ts +4 -4
  50. package/lib/ip-restriction.d.ts +2 -2
  51. package/lib/jsx/dom/client.d.ts +30 -0
  52. package/lib/jsx/dom/client.js +49 -0
  53. package/lib/jsx/dom/css.d.ts +84 -0
  54. package/lib/jsx/dom/css.js +2 -0
  55. package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
  56. package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
  57. package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
  58. package/lib/jsx/dom/jsx-runtime.js +2 -0
  59. package/lib/jsx/dom/server.d.ts +105 -0
  60. package/lib/jsx/dom/server.js +37 -0
  61. package/lib/jsx/dom.d.ts +5 -0
  62. package/lib/jsx/dom.js +74 -0
  63. package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
  64. package/lib/jsx/jsx-dev-runtime.js +14 -0
  65. package/lib/jsx/jsx-runtime.d.ts +10 -0
  66. package/lib/jsx/jsx-runtime.js +30 -0
  67. package/lib/jsx/streaming.d.ts +2 -0
  68. package/lib/jsx/streaming.js +2 -0
  69. package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
  70. package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
  71. package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
  72. package/lib/jsx.d.ts +3 -0
  73. package/lib/jsx.js +47 -0
  74. package/lib/jwk.d.ts +4 -4
  75. package/lib/{jwt-BA65QhnJ.d.ts → jwt-BARhotDz.d.ts} +1 -1
  76. package/lib/jwt.d.ts +4 -4
  77. package/lib/language.d.ts +1 -1
  78. package/lib/logger.d.ts +1 -1
  79. package/lib/loopback-only.d.ts +1 -1
  80. package/lib/method-not-allowed.d.ts +2 -2
  81. package/lib/method-override.d.ts +2 -2
  82. package/lib/mount.d.ts +1 -1
  83. package/lib/node-server/conninfo.d.ts +1 -1
  84. package/lib/node-server/serve-static.d.ts +1 -1
  85. package/lib/node-server/serve-static.js +1 -1
  86. package/lib/node-server.d.ts +1 -1
  87. package/lib/node-server.js +1 -1
  88. package/lib/powered-by.d.ts +1 -1
  89. package/lib/pretty-json.d.ts +1 -1
  90. package/lib/proxy.d.ts +1 -1
  91. package/lib/quick.d.ts +1 -1
  92. package/lib/request-id.d.ts +1 -1
  93. package/lib/request.d.ts +1 -1
  94. package/lib/route.d.ts +1 -1
  95. package/lib/schema-validator.d.ts +1 -1
  96. package/lib/secure-headers.d.ts +1 -1
  97. package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
  98. package/lib/serve-static.d.ts +1 -1
  99. package/lib/streaming.d.ts +1 -1
  100. package/lib/testing.d.ts +4 -4
  101. package/lib/timeout.d.ts +1 -1
  102. package/lib/timing.d.ts +1 -1
  103. package/lib/tiny.d.ts +1 -1
  104. package/lib/trailing-slash.d.ts +1 -1
  105. package/lib/{types-Bd29gh2o.d.ts → types-CQiGmsWW.d.ts} +3 -3
  106. package/lib/{types-D9DEXMjE.d.ts → types-Lqx5BxQF.d.ts} +8 -1
  107. package/lib/{types-ZM9gIiKh.d.ts → types-nEL8VPDC.d.ts} +1 -1
  108. package/lib/types.d.ts +1 -1
  109. package/lib/utils/body.d.ts +1 -1
  110. package/lib/utils/cookie.d.ts +1 -1
  111. package/lib/utils/crypto.d.ts +1 -1
  112. package/lib/utils/html.d.ts +1 -45
  113. package/lib/utils/ipaddr.d.ts +1 -1
  114. package/lib/utils/jwt.d.ts +1 -1
  115. package/lib/utils/types.d.ts +2 -2
  116. package/lib/validator.d.ts +2 -2
  117. package/lib/ws.d.ts +1 -1
  118. package/package.json +45 -1
  119. package/plugins/acryl-inspect/upstream.json +1 -1
  120. package/plugins/acryl-node-server/upstream.json +1 -1
  121. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
  122. package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
  123. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
  124. package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
  125. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
  126. package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
  127. package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
  128. package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
  129. package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
  130. package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
  131. package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
  132. package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
  133. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
  134. package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
  135. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
  136. package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
  137. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
  138. package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
  139. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
  140. package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
  141. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
  142. package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
  143. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
  144. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
  145. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
  146. package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
  147. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
  148. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
  149. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
  150. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
  151. package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
  152. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
  153. package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
  154. package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
  155. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
  156. package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
  157. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
  158. package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
  159. package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
  160. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
  161. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
  162. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
  163. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
  164. package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
  165. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
  166. package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
  167. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
  168. package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
  169. package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
  170. package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
  171. package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
  172. package/plugins/acryl-router/src/core/utils/types.ts +6 -0
  173. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  174. package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
  175. package/plugins/acryl-router/upstream.json +3 -3
  176. package/plugins/acryl-router-http/upstream.json +1 -1
  177. package/plugins/acryl-state/upstream.json +1 -1
  178. package/plugins/desktop/upstream.json +1 -1
  179. package/plugins/desktop-web/upstream.json +1 -1
  180. package/plugins/save/plugin.json +1 -1
  181. package/templates/todo/upstream.json +1 -1
@@ -0,0 +1,936 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /** @jsxImportSource ./ */
3
+ import { JSDOM } from 'jsdom'
4
+ import { raw } from '../helper/html/index.ts'
5
+ import type { HtmlEscapedString } from '../utils/html.ts'
6
+ import { HtmlEscapedCallbackPhase, resolveCallback as rawResolveCallback } from '../utils/html.ts'
7
+ import { ErrorBoundary } from './components.ts'
8
+ import { Suspense, renderToReadableStream, StreamingContext } from './streaming.ts'
9
+
10
+ const unsafeHtml = '<img src=x onerror=alert(1)>'
11
+ const escapedUnsafeHtml = '&lt;img src=x onerror=alert(1)&gt;'
12
+
13
+ function resolveCallback(template: string | HtmlEscapedString) {
14
+ return rawResolveCallback(template, HtmlEscapedCallbackPhase.Stream, false, {})
15
+ }
16
+
17
+ function replacementResult(html: string) {
18
+ const document = new JSDOM(html, { runScripts: 'dangerously' }).window.document
19
+ document.querySelectorAll('template, script').forEach((e) => e.remove())
20
+ return document.body.innerHTML
21
+ }
22
+
23
+ async function stringify(node: { toString(): string | Promise<string> }): Promise<string> {
24
+ return String(
25
+ await rawResolveCallback(await node.toString(), HtmlEscapedCallbackPhase.Stringify, false, {})
26
+ )
27
+ }
28
+
29
+ async function drainStream(stream: ReadableStream<Uint8Array>): Promise<string> {
30
+ const decoder = new TextDecoder()
31
+ let result = ''
32
+ for await (const chunk of stream) {
33
+ result += decoder.decode(chunk)
34
+ }
35
+ return result
36
+ }
37
+
38
+ const Fallback = () => <div>Out Of Service</div>
39
+
40
+ describe('ErrorBoundary', () => {
41
+ let errorBoundaryCounter = 0
42
+ let suspenseCounter = 0
43
+ afterEach(() => {
44
+ errorBoundaryCounter++
45
+ suspenseCounter++
46
+ })
47
+
48
+ describe('sync', async () => {
49
+ const Component = ({ error }: { error?: boolean }) => {
50
+ if (error) {
51
+ throw new Error('Error')
52
+ }
53
+ return <div>Hello</div>
54
+ }
55
+
56
+ it('no error', async () => {
57
+ const html = (
58
+ <ErrorBoundary fallback={<Fallback />}>
59
+ <Component />
60
+ </ErrorBoundary>
61
+ )
62
+
63
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('<div>Hello</div>')
64
+
65
+ errorBoundaryCounter--
66
+ suspenseCounter--
67
+ })
68
+
69
+ it('error', async () => {
70
+ const html = (
71
+ <ErrorBoundary fallback={<Fallback />}>
72
+ <Component error={true} />
73
+ </ErrorBoundary>
74
+ )
75
+
76
+ expect((await resolveCallback(await html.toString())).toString()).toEqual(
77
+ '<div>Out Of Service</div>'
78
+ )
79
+
80
+ suspenseCounter--
81
+ })
82
+
83
+ it('nullish', async () => {
84
+ const html = (
85
+ <div>
86
+ <ErrorBoundary fallback={<Fallback />}>{[null, undefined]}</ErrorBoundary>
87
+ </div>
88
+ )
89
+
90
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('<div></div>')
91
+
92
+ errorBoundaryCounter--
93
+ suspenseCounter--
94
+ })
95
+
96
+ it('boolean', async () => {
97
+ const html = (
98
+ <div>
99
+ <ErrorBoundary fallback={<Fallback />}>{[true, false]}</ErrorBoundary>
100
+ </div>
101
+ )
102
+
103
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('<div></div>')
104
+
105
+ errorBoundaryCounter--
106
+ suspenseCounter--
107
+ })
108
+
109
+ it('string content', async () => {
110
+ const html = <ErrorBoundary fallback={<Fallback />}>{'< ok >'}</ErrorBoundary>
111
+
112
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; ok &gt;')
113
+
114
+ errorBoundaryCounter--
115
+ suspenseCounter--
116
+ })
117
+
118
+ it('error: string content', async () => {
119
+ const html = (
120
+ <ErrorBoundary fallback={'< error >'}>
121
+ <Component error={true} />
122
+ </ErrorBoundary>
123
+ )
124
+
125
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
126
+
127
+ errorBoundaryCounter--
128
+ suspenseCounter--
129
+ })
130
+
131
+ it('error: Promise<string> from fallback', async () => {
132
+ const html = (
133
+ <ErrorBoundary fallback={Promise.resolve('< error >')}>
134
+ <Component error={true} />
135
+ </ErrorBoundary>
136
+ )
137
+
138
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
139
+
140
+ errorBoundaryCounter--
141
+ suspenseCounter--
142
+ })
143
+
144
+ it('error: string content from fallbackRender', async () => {
145
+ const html = (
146
+ <ErrorBoundary fallbackRender={() => '< error >'}>
147
+ <Component error={true} />
148
+ </ErrorBoundary>
149
+ )
150
+
151
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
152
+
153
+ errorBoundaryCounter--
154
+ suspenseCounter--
155
+ })
156
+
157
+ it('error: Promise<string> from fallbackRender', async () => {
158
+ const html = (
159
+ <ErrorBoundary fallbackRender={() => Promise.resolve('< error >')}>
160
+ <Component error={true} />
161
+ </ErrorBoundary>
162
+ )
163
+
164
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
165
+
166
+ errorBoundaryCounter--
167
+ suspenseCounter--
168
+ })
169
+ })
170
+
171
+ describe('async', async () => {
172
+ const Component = async ({ error }: { error?: boolean }) => {
173
+ await new Promise((resolve) => setTimeout(resolve, 10))
174
+ if (error) {
175
+ throw new Error('Error')
176
+ }
177
+ return <div>Hello</div>
178
+ }
179
+
180
+ it('no error', async () => {
181
+ const html = (
182
+ <ErrorBoundary fallback={<Fallback />}>
183
+ <Component />
184
+ </ErrorBoundary>
185
+ )
186
+
187
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('<div>Hello</div>')
188
+
189
+ errorBoundaryCounter--
190
+ suspenseCounter--
191
+ })
192
+
193
+ it('error', async () => {
194
+ const html = (
195
+ <ErrorBoundary fallback={<Fallback />}>
196
+ <Component error={true} />
197
+ </ErrorBoundary>
198
+ )
199
+
200
+ expect((await resolveCallback(await html.toString())).toString()).toEqual(
201
+ '<div>Out Of Service</div>'
202
+ )
203
+
204
+ suspenseCounter--
205
+ })
206
+
207
+ it('error: string content', async () => {
208
+ const html = (
209
+ <ErrorBoundary fallback={'< error >'}>
210
+ <Component error={true} />
211
+ </ErrorBoundary>
212
+ )
213
+
214
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
215
+
216
+ suspenseCounter--
217
+ })
218
+
219
+ it('error: Promise<string> from fallback', async () => {
220
+ const html = (
221
+ <ErrorBoundary fallback={Promise.resolve('< error >')}>
222
+ <Component error={true} />
223
+ </ErrorBoundary>
224
+ )
225
+
226
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
227
+
228
+ suspenseCounter--
229
+ })
230
+
231
+ it('error: string content from fallbackRender', async () => {
232
+ const html = (
233
+ <ErrorBoundary fallbackRender={() => '< error >'}>
234
+ <Component error={true} />
235
+ </ErrorBoundary>
236
+ )
237
+
238
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
239
+
240
+ suspenseCounter--
241
+ })
242
+
243
+ it('error: Promise<string> from fallbackRender', async () => {
244
+ const html = (
245
+ <ErrorBoundary fallbackRender={() => Promise.resolve('< error >')}>
246
+ <Component error={true} />
247
+ </ErrorBoundary>
248
+ )
249
+
250
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('&lt; error &gt;')
251
+
252
+ suspenseCounter--
253
+ })
254
+ })
255
+
256
+ describe('async : nested', async () => {
257
+ const handlers: Record<number, { resolve: (value: unknown) => void; reject: () => void }> = {}
258
+ const Component = async ({ id }: { id: number }) => {
259
+ await new Promise((resolve, reject) => (handlers[id] = { resolve, reject }))
260
+ return <div>{id}</div>
261
+ }
262
+
263
+ it('no error', async () => {
264
+ const html = (
265
+ <ErrorBoundary fallback={<Fallback />}>
266
+ <Component id={1} />
267
+ <ErrorBoundary fallback={<Fallback />}>
268
+ <Component id={2} />
269
+ </ErrorBoundary>
270
+ </ErrorBoundary>
271
+ ).toString()
272
+
273
+ Object.values(handlers).forEach(({ resolve }) => resolve(undefined))
274
+
275
+ expect((await resolveCallback(await html)).toString()).toEqual('<div>1</div><div>2</div>')
276
+
277
+ errorBoundaryCounter++
278
+ suspenseCounter--
279
+ })
280
+
281
+ it('error in parent', async () => {
282
+ const html = (
283
+ <ErrorBoundary fallback={<Fallback />}>
284
+ <Component id={1} />
285
+ <ErrorBoundary fallback={<Fallback />}>
286
+ <Component id={2} />
287
+ </ErrorBoundary>
288
+ </ErrorBoundary>
289
+ ).toString()
290
+
291
+ handlers[2].resolve(undefined)
292
+ handlers[1].reject()
293
+
294
+ expect((await resolveCallback(await html)).toString()).toEqual('<div>Out Of Service</div>')
295
+
296
+ errorBoundaryCounter++
297
+ suspenseCounter--
298
+ })
299
+
300
+ it('error in child', async () => {
301
+ const html = (
302
+ <ErrorBoundary fallback={<Fallback />}>
303
+ <Component id={1} />
304
+ <ErrorBoundary fallback={<Fallback />}>
305
+ <Component id={2} />
306
+ </ErrorBoundary>
307
+ </ErrorBoundary>
308
+ ).toString()
309
+
310
+ handlers[1].resolve(undefined)
311
+ handlers[2].reject()
312
+
313
+ expect((await resolveCallback(await html)).toString()).toEqual(
314
+ '<div>1</div><div>Out Of Service</div>'
315
+ )
316
+
317
+ errorBoundaryCounter++
318
+ suspenseCounter--
319
+ })
320
+ })
321
+
322
+ describe('async : setTimeout', async () => {
323
+ const TimeoutSuccessComponent = async () => {
324
+ await new Promise((resolve) => setTimeout(resolve, 10))
325
+ return <div>OK</div>
326
+ }
327
+ const TimeoutErrorComponent = async () => {
328
+ await new Promise((resolve) => setTimeout(resolve, 0))
329
+ throw new Error('Error')
330
+ }
331
+
332
+ it('fallback', async () => {
333
+ const html = (
334
+ <>
335
+ <TimeoutSuccessComponent />
336
+ <ErrorBoundary fallback={<Fallback />}>
337
+ <TimeoutErrorComponent />
338
+ </ErrorBoundary>
339
+ </>
340
+ ).toString()
341
+
342
+ expect((await resolveCallback(await html)).toString()).toEqual(
343
+ '<div>OK</div><div>Out Of Service</div>'
344
+ )
345
+
346
+ suspenseCounter--
347
+ })
348
+ })
349
+
350
+ describe('streaming', async () => {
351
+ const Component = async ({ error }: { error?: boolean }) => {
352
+ await new Promise((resolve) => setTimeout(resolve, 10))
353
+ if (error) {
354
+ throw new Error('Error')
355
+ }
356
+ return <div>Hello</div>
357
+ }
358
+
359
+ it('no error', async () => {
360
+ const stream = renderToReadableStream(
361
+ <ErrorBoundary fallback={<Fallback />}>
362
+ <Suspense fallback={<p>Loading...</p>}>
363
+ <Component />
364
+ </Suspense>
365
+ </ErrorBoundary>
366
+ )
367
+ const chunks = []
368
+ const textDecoder = new TextDecoder()
369
+ for await (const chunk of stream as any) {
370
+ chunks.push(textDecoder.decode(chunk))
371
+ }
372
+
373
+ expect(chunks).toEqual([
374
+ `<template id="E:${errorBoundaryCounter}"></template><!--E:${errorBoundaryCounter}-->`,
375
+ `<template data-acryl-target="E:${errorBoundaryCounter}"><template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$--></template><script>
376
+ ((d,c) => {
377
+ c=d.currentScript.previousSibling
378
+ d=d.getElementById('E:${errorBoundaryCounter}')
379
+ if(!d)return
380
+ d.parentElement.insertBefore(c.content,d.nextSibling)
381
+ })(document)
382
+ </script>`,
383
+ `<template data-acryl-target="H:${suspenseCounter}"><div>Hello</div></template><script>
384
+ ((d,c,n) => {
385
+ c=d.currentScript.previousSibling
386
+ d=d.getElementById('H:${suspenseCounter}')
387
+ if(!d)return
388
+ do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
389
+ d.replaceWith(c.content)
390
+ })(document)
391
+ </script><script>
392
+ ((d,c,n) => {
393
+ d=d.getElementById('E:${errorBoundaryCounter}')
394
+ if(!d)return
395
+ n=d.nextSibling
396
+ while(n.nodeType!=8||n.nodeValue!='E:${errorBoundaryCounter}'){n=n.nextSibling}
397
+ n.remove()
398
+ d.remove()
399
+ })(document)
400
+ </script>`,
401
+ ])
402
+
403
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
404
+ '<div>Hello</div>'
405
+ )
406
+ })
407
+
408
+ it('with StreamingContext', async () => {
409
+ const stream = renderToReadableStream(
410
+ <StreamingContext value={{ scriptNonce: 'test-nonce' }}>
411
+ <ErrorBoundary fallback={<Fallback />}>
412
+ <Suspense fallback={<p>Loading...</p>}>
413
+ <Component />
414
+ </Suspense>
415
+ </ErrorBoundary>
416
+ </StreamingContext>
417
+ )
418
+ const chunks = []
419
+ const textDecoder = new TextDecoder()
420
+ for await (const chunk of stream as any) {
421
+ chunks.push(textDecoder.decode(chunk))
422
+ }
423
+
424
+ expect(chunks).toEqual([
425
+ `<template id="E:${errorBoundaryCounter}"></template><!--E:${errorBoundaryCounter}-->`,
426
+ `<template data-acryl-target="E:${errorBoundaryCounter}"><template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$--></template><script nonce="test-nonce">
427
+ ((d,c) => {
428
+ c=d.currentScript.previousSibling
429
+ d=d.getElementById('E:${errorBoundaryCounter}')
430
+ if(!d)return
431
+ d.parentElement.insertBefore(c.content,d.nextSibling)
432
+ })(document)
433
+ </script>`,
434
+ `<template data-acryl-target="H:${suspenseCounter}"><div>Hello</div></template><script nonce="test-nonce">
435
+ ((d,c,n) => {
436
+ c=d.currentScript.previousSibling
437
+ d=d.getElementById('H:${suspenseCounter}')
438
+ if(!d)return
439
+ do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
440
+ d.replaceWith(c.content)
441
+ })(document)
442
+ </script><script>
443
+ ((d,c,n) => {
444
+ d=d.getElementById('E:${errorBoundaryCounter}')
445
+ if(!d)return
446
+ n=d.nextSibling
447
+ while(n.nodeType!=8||n.nodeValue!='E:${errorBoundaryCounter}'){n=n.nextSibling}
448
+ n.remove()
449
+ d.remove()
450
+ })(document)
451
+ </script>`,
452
+ ])
453
+
454
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
455
+ '<div>Hello</div>'
456
+ )
457
+ })
458
+
459
+ it('error', async () => {
460
+ const html = (
461
+ <ErrorBoundary fallback={<Fallback />}>
462
+ <Component error={true} />
463
+ </ErrorBoundary>
464
+ )
465
+
466
+ expect((await resolveCallback(await html.toString())).toString()).toEqual(
467
+ '<div>Out Of Service</div>'
468
+ )
469
+ })
470
+ })
471
+
472
+ describe('streaming : contains multiple suspense', async () => {
473
+ const handlers: Record<number, { resolve: (value: unknown) => void; reject: () => void }> = {}
474
+ const Component = async ({ id }: { id: number }) => {
475
+ await new Promise((resolve, reject) => (handlers[id] = { resolve, reject }))
476
+ return <div>{id}</div>
477
+ }
478
+
479
+ it('no error', async () => {
480
+ const stream = renderToReadableStream(
481
+ <ErrorBoundary fallback={<Fallback />}>
482
+ <Suspense fallback={<p>Loading...</p>}>
483
+ <Component id={1} />
484
+ </Suspense>
485
+ <Suspense fallback={<p>Loading...</p>}>
486
+ <Component id={2} />
487
+ </Suspense>
488
+ <Suspense fallback={<p>Loading...</p>}>
489
+ <Component id={3} />
490
+ </Suspense>
491
+ </ErrorBoundary>
492
+ )
493
+
494
+ Object.values(handlers).forEach(({ resolve }) => resolve(undefined))
495
+
496
+ const chunks = []
497
+ const textDecoder = new TextDecoder()
498
+ for await (const chunk of stream as any) {
499
+ chunks.push(textDecoder.decode(chunk))
500
+ }
501
+
502
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
503
+ '<div>1</div><div>2</div><div>3</div>'
504
+ )
505
+ })
506
+
507
+ it('error', async () => {
508
+ const stream = renderToReadableStream(
509
+ <ErrorBoundary fallback={<Fallback />}>
510
+ <Suspense fallback={<p>Loading...</p>}>
511
+ <Component id={1} />
512
+ </Suspense>
513
+ <Suspense fallback={<p>Loading...</p>}>
514
+ <Component id={2} />
515
+ </Suspense>
516
+ <Suspense fallback={<p>Loading...</p>}>
517
+ <Component id={3} />
518
+ </Suspense>
519
+ </ErrorBoundary>
520
+ )
521
+
522
+ handlers[1].resolve(undefined)
523
+ handlers[2].resolve(undefined)
524
+ handlers[3].reject()
525
+
526
+ const chunks = []
527
+ const textDecoder = new TextDecoder()
528
+ for await (const chunk of stream as any) {
529
+ chunks.push(textDecoder.decode(chunk))
530
+ }
531
+
532
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
533
+ '<div>Out Of Service</div>'
534
+ )
535
+ })
536
+
537
+ it('renders fallback and reports error once when multiple children reject', async () => {
538
+ let fallbackRenderCount = 0
539
+ let onErrorCount = 0
540
+ const stream = renderToReadableStream(
541
+ <ErrorBoundary
542
+ fallbackRender={() => {
543
+ fallbackRenderCount++
544
+ return <Fallback />
545
+ }}
546
+ onError={() => {
547
+ onErrorCount++
548
+ }}
549
+ >
550
+ <Suspense fallback={<p>Loading...</p>}>
551
+ <Component id={1} />
552
+ </Suspense>
553
+ <Suspense fallback={<p>Loading...</p>}>
554
+ <Component id={2} />
555
+ </Suspense>
556
+ <Suspense fallback={<p>Loading...</p>}>
557
+ <Component id={3} />
558
+ </Suspense>
559
+ </ErrorBoundary>
560
+ )
561
+
562
+ handlers[1].reject()
563
+ handlers[2].reject()
564
+ handlers[3].resolve(undefined)
565
+
566
+ const chunks = []
567
+ const textDecoder = new TextDecoder()
568
+ for await (const chunk of stream as any) {
569
+ chunks.push(textDecoder.decode(chunk))
570
+ }
571
+
572
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
573
+ '<div>Out Of Service</div>'
574
+ )
575
+ expect(fallbackRenderCount).toBe(1)
576
+ expect(onErrorCount).toBe(1)
577
+ })
578
+
579
+ it('preserves callbacks from a thenable fallback', async () => {
580
+ const fallback = Promise.resolve(
581
+ raw('<span>Recovering</span>', [
582
+ ({ phase }) =>
583
+ phase === HtmlEscapedCallbackPhase.Stream
584
+ ? Promise.resolve('<span>Recovered</span>')
585
+ : undefined,
586
+ ])
587
+ )
588
+ const stream = renderToReadableStream(
589
+ <ErrorBoundary fallback={fallback}>
590
+ <Suspense fallback={<p>Loading...</p>}>
591
+ <Component id={1} />
592
+ </Suspense>
593
+ </ErrorBoundary>
594
+ )
595
+
596
+ handlers[1].reject()
597
+
598
+ const chunks = []
599
+ const textDecoder = new TextDecoder()
600
+ for await (const chunk of stream as any) {
601
+ chunks.push(textDecoder.decode(chunk))
602
+ }
603
+
604
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
605
+ '<span>Recovering</span><span>Recovered</span>'
606
+ )
607
+ })
608
+
609
+ it('waits for an in-flight async fallback when multiple children reject', async () => {
610
+ let resolveFallback!: (value: HtmlEscapedString | PromiseLike<HtmlEscapedString>) => void
611
+ const fallback = new Promise<HtmlEscapedString>((resolve) => {
612
+ resolveFallback = resolve
613
+ })
614
+
615
+ const stream = renderToReadableStream(
616
+ <ErrorBoundary fallback={fallback}>
617
+ <Suspense fallback={<p>Loading...</p>}>
618
+ <Component id={1} />
619
+ </Suspense>
620
+ <Suspense fallback={<p>Loading...</p>}>
621
+ <Component id={2} />
622
+ </Suspense>
623
+ <Suspense fallback={<p>Loading...</p>}>
624
+ <Component id={3} />
625
+ </Suspense>
626
+ </ErrorBoundary>
627
+ )
628
+
629
+ handlers[1].reject()
630
+ handlers[2].reject()
631
+ handlers[3].resolve(undefined)
632
+ await Promise.resolve()
633
+ resolveFallback(<Fallback />)
634
+
635
+ const chunks = []
636
+ const textDecoder = new TextDecoder()
637
+ for await (const chunk of stream as any) {
638
+ chunks.push(textDecoder.decode(chunk))
639
+ }
640
+
641
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
642
+ '<div>Out Of Service</div>'
643
+ )
644
+ })
645
+ })
646
+
647
+ describe('streaming : nested', async () => {
648
+ const handlers: Record<number, { resolve: (value: unknown) => void; reject: () => void }> = {}
649
+ const Component = async ({ id }: { id: number }) => {
650
+ await new Promise((resolve, reject) => (handlers[id] = { resolve, reject }))
651
+ return <div>{id}</div>
652
+ }
653
+
654
+ it('no error', async () => {
655
+ const stream = renderToReadableStream(
656
+ <ErrorBoundary fallback={<Fallback />}>
657
+ <Suspense fallback={<p>Loading...</p>}>
658
+ <Component id={1} />
659
+ </Suspense>
660
+ <ErrorBoundary fallback={<Fallback />}>
661
+ <Suspense fallback={<p>Loading...</p>}>
662
+ <Component id={2} />
663
+ </Suspense>
664
+ </ErrorBoundary>
665
+ </ErrorBoundary>
666
+ )
667
+
668
+ Object.values(handlers).forEach(({ resolve }) => resolve(undefined))
669
+
670
+ const chunks = []
671
+ const textDecoder = new TextDecoder()
672
+ for await (const chunk of stream as any) {
673
+ chunks.push(textDecoder.decode(chunk))
674
+ }
675
+
676
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
677
+ '<div>1</div><div>2</div>'
678
+ )
679
+ })
680
+
681
+ it('error in parent', async () => {
682
+ const stream = renderToReadableStream(
683
+ <ErrorBoundary fallback={<Fallback />}>
684
+ <Suspense fallback={<p>Loading...</p>}>
685
+ <Component id={1} />
686
+ </Suspense>
687
+ <ErrorBoundary fallback={<Fallback />}>
688
+ <Suspense fallback={<p>Loading...</p>}>
689
+ <Component id={2} />
690
+ </Suspense>
691
+ </ErrorBoundary>
692
+ </ErrorBoundary>
693
+ )
694
+
695
+ handlers[2].resolve(undefined)
696
+ handlers[1].reject()
697
+
698
+ const chunks = []
699
+ const textDecoder = new TextDecoder()
700
+ for await (const chunk of stream as any) {
701
+ chunks.push(textDecoder.decode(chunk))
702
+ }
703
+
704
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
705
+ '<div>Out Of Service</div>'
706
+ )
707
+ })
708
+
709
+ it('error in child', async () => {
710
+ const stream = renderToReadableStream(
711
+ <ErrorBoundary fallback={<Fallback />}>
712
+ <Suspense fallback={<p>Loading...</p>}>
713
+ <Component id={1} />
714
+ </Suspense>
715
+ <ErrorBoundary fallback={<Fallback />}>
716
+ <Suspense fallback={<p>Loading...</p>}>
717
+ <Component id={2} />
718
+ </Suspense>
719
+ </ErrorBoundary>
720
+ </ErrorBoundary>
721
+ )
722
+
723
+ handlers[1].resolve(undefined)
724
+ handlers[2].reject()
725
+
726
+ const chunks = []
727
+ const textDecoder = new TextDecoder()
728
+ for await (const chunk of stream as any) {
729
+ chunks.push(textDecoder.decode(chunk))
730
+ }
731
+
732
+ expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
733
+ '<div>1</div><div>Out Of Service</div>'
734
+ )
735
+ })
736
+ })
737
+
738
+ describe('onError', async () => {
739
+ const Component = ({ error }: { error?: boolean }) => {
740
+ if (error) {
741
+ throw new Error('Error')
742
+ }
743
+ return <div>Hello</div>
744
+ }
745
+
746
+ it('no error', async () => {
747
+ const errors: Error[] = []
748
+ const html = (
749
+ <ErrorBoundary fallback={<Fallback />} onError={(err) => errors.push(err)}>
750
+ <Component />
751
+ </ErrorBoundary>
752
+ )
753
+
754
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('<div>Hello</div>')
755
+
756
+ errorBoundaryCounter--
757
+ suspenseCounter--
758
+
759
+ expect(errors).toEqual([])
760
+ })
761
+
762
+ it('error', async () => {
763
+ const errors: Error[] = []
764
+ const html = (
765
+ <ErrorBoundary fallback={<Fallback />} onError={(err) => errors.push(err)}>
766
+ <Component error={true} />
767
+ </ErrorBoundary>
768
+ )
769
+
770
+ expect((await resolveCallback(await html.toString())).toString()).toEqual(
771
+ '<div>Out Of Service</div>'
772
+ )
773
+
774
+ suspenseCounter--
775
+
776
+ expect(errors[0]).toEqual(new Error('Error'))
777
+ })
778
+ })
779
+
780
+ describe('fallbackRender', async () => {
781
+ const fallbackRenderer = (error: Error) => <div data-error>{error.message}</div>
782
+ const Component = ({ error }: { error?: boolean }) => {
783
+ if (error) {
784
+ throw new Error('Error')
785
+ }
786
+ return <div>Hello</div>
787
+ }
788
+
789
+ it('no error', async () => {
790
+ const errors: Error[] = []
791
+ const html = (
792
+ <ErrorBoundary fallbackRender={fallbackRenderer}>
793
+ <Component />
794
+ </ErrorBoundary>
795
+ )
796
+
797
+ expect((await resolveCallback(await html.toString())).toString()).toEqual('<div>Hello</div>')
798
+
799
+ errorBoundaryCounter--
800
+ suspenseCounter--
801
+
802
+ expect(errors).toEqual([])
803
+ })
804
+
805
+ it('error', async () => {
806
+ const html = (
807
+ <ErrorBoundary fallbackRender={fallbackRenderer}>
808
+ <Component error={true} />
809
+ </ErrorBoundary>
810
+ )
811
+
812
+ expect((await resolveCallback(await html.toString())).toString()).toEqual(
813
+ '<div data-error="true">Error</div>'
814
+ )
815
+
816
+ suspenseCounter--
817
+ })
818
+ })
819
+
820
+ describe('escaping', () => {
821
+ const Broken = () => {
822
+ throw new Error('boom')
823
+ }
824
+
825
+ it('escapes strings when an asynchronous sibling is present', async () => {
826
+ const Async = async () => <span>done</span>
827
+ const node = (
828
+ <ErrorBoundary fallback='error'>
829
+ {unsafeHtml}
830
+ <Async />
831
+ </ErrorBoundary>
832
+ )
833
+
834
+ expect(await stringify(node)).toBe(`${escapedUnsafeHtml}<span>done</span>`)
835
+ const streamed = await drainStream(renderToReadableStream(node))
836
+ expect(streamed).toContain(escapedUnsafeHtml)
837
+ expect(streamed).not.toContain(unsafeHtml)
838
+ })
839
+
840
+ it('escapes untrusted object fallbacks', async () => {
841
+ const object = { toString: () => unsafeHtml }
842
+ const asyncObject = { toString: async () => unsafeHtml }
843
+ const rawObject = { toString: () => raw(unsafeHtml) }
844
+
845
+ for (const fallback of [object, asyncObject, rawObject]) {
846
+ expect(
847
+ await stringify(
848
+ <ErrorBoundary fallback={fallback as never}>
849
+ <Broken />
850
+ </ErrorBoundary>
851
+ )
852
+ ).toBe(escapedUnsafeHtml)
853
+ }
854
+ })
855
+
856
+ it('preserves an explicitly trusted fallback', async () => {
857
+ expect(
858
+ await stringify(
859
+ <ErrorBoundary fallback={raw('<strong>trusted</strong>')}>
860
+ <Broken />
861
+ </ErrorBoundary>
862
+ )
863
+ ).toBe('<strong>trusted</strong>')
864
+ })
865
+
866
+ it('does not interpret replacement patterns in content', async () => {
867
+ const content = "literal $& $` $'"
868
+ const Async = async () => <span>done</span>
869
+
870
+ expect(
871
+ await stringify(
872
+ <ErrorBoundary fallback='error'>
873
+ {content}
874
+ <Async />
875
+ </ErrorBoundary>
876
+ )
877
+ ).toBe('literal $&amp; $` $&#39;<span>done</span>')
878
+ })
879
+
880
+ it('escapes untrusted object children', async () => {
881
+ const object = { toString: () => unsafeHtml }
882
+ const asyncObject = { toString: async () => unsafeHtml }
883
+ const rawObject = { toString: () => raw(unsafeHtml) }
884
+
885
+ for (const child of [object, asyncObject, rawObject]) {
886
+ const node = <ErrorBoundary fallback='error'>{child as never}</ErrorBoundary>
887
+ expect(await stringify(node)).toBe(escapedUnsafeHtml)
888
+ const streamed = await drainStream(renderToReadableStream(node))
889
+ expect(streamed).toContain(escapedUnsafeHtml)
890
+ expect(streamed).not.toContain(unsafeHtml)
891
+ }
892
+ })
893
+
894
+ it('preserves explicitly trusted children', async () => {
895
+ expect(
896
+ await stringify(
897
+ <ErrorBoundary fallback='error'>{raw('<strong>trusted</strong>')}</ErrorBoundary>
898
+ )
899
+ ).toBe('<strong>trusted</strong>')
900
+ })
901
+
902
+ it('keeps benign object stringification visible', async () => {
903
+ expect(await stringify(<ErrorBoundary fallback='error'>{{} as never}</ErrorBoundary>)).toBe(
904
+ '[object Object]'
905
+ )
906
+ })
907
+
908
+ it('escapes nested fallback arrays', async () => {
909
+ expect(
910
+ await stringify(
911
+ <ErrorBoundary fallback={[[unsafeHtml]]}>
912
+ <Broken />
913
+ </ErrorBoundary>
914
+ )
915
+ ).toBe(escapedUnsafeHtml)
916
+ })
917
+
918
+ it('keeps the existing falsy fallbackRender behavior', async () => {
919
+ expect(
920
+ await stringify(
921
+ <ErrorBoundary fallbackRender={() => 0}>
922
+ <Broken />
923
+ </ErrorBoundary>
924
+ )
925
+ ).toBe('')
926
+ })
927
+
928
+ it('keeps a direct Promise child unchanged', async () => {
929
+ expect(
930
+ await stringify(
931
+ <ErrorBoundary fallback='error'>{Promise.resolve('resolved')}</ErrorBoundary>
932
+ )
933
+ ).toBe('[object Promise]')
934
+ })
935
+ })
936
+ })