acrylblends 0.6.3 → 0.6.4

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 (269) hide show
  1. package/README.md +3 -2
  2. package/docs/framework/README.md +2 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/guides/jsx.md +24 -0
  5. package/docs/framework/helpers/ssg.md +3 -6
  6. package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
  7. package/docs/framework/upstream.json +2 -2
  8. package/lib/accepts.d.ts +1 -1
  9. package/lib/acryl-base.d.ts +1 -1
  10. package/lib/adapter.d.ts +1 -1
  11. package/lib/{app-wFwpg-bt.d.ts → app-D8riatvv.d.ts} +1 -1
  12. package/lib/basic-auth.d.ts +1 -1
  13. package/lib/bearer-auth.d.ts +1 -1
  14. package/lib/body-limit.d.ts +1 -1
  15. package/lib/cache.d.ts +1 -1
  16. package/lib/cache.js +4 -4
  17. package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
  18. package/lib/client.d.ts +2 -2
  19. package/lib/client.js +2 -1
  20. package/lib/combine.d.ts +1 -1
  21. package/lib/{components-Ce5FRyHU.js → components-0o0CBYNv.js} +2 -2
  22. package/lib/{components-DYZdxJIJ.js → components-DdI7-ns7.js} +1 -1
  23. package/lib/compress.d.ts +1 -1
  24. package/lib/conninfo.d.ts +2 -2
  25. package/lib/{context-DVQBjuns.d.ts → context-DS3fYyG-.d.ts} +1 -1
  26. package/lib/context-storage.d.ts +1 -1
  27. package/lib/cookie.d.ts +1 -1
  28. package/lib/cors.d.ts +1 -1
  29. package/lib/csrf.d.ts +1 -1
  30. package/lib/dev.d.ts +2 -2
  31. package/lib/etag.d.ts +1 -1
  32. package/lib/factory.d.ts +2 -2
  33. package/lib/{hooks-Bq5q9IMl.js → hooks-BMw6BAyP.js} +5 -5
  34. package/lib/{index-CUTfU69R.d.ts → index-Bb7YJKCq.d.ts} +2 -2
  35. package/lib/{index-CsvB-F9j.d.ts → index-BvFh1Gfx.d.ts} +1 -1
  36. package/lib/{index-Cqcdluyn.d.ts → index-CTy4N7ng.d.ts} +2 -2
  37. package/lib/index.d.ts +4 -4
  38. package/lib/ip-restriction.d.ts +2 -2
  39. package/lib/jsx/dom/client.js +1 -1
  40. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  41. package/lib/jsx/dom/jsx-runtime.js +1 -1
  42. package/lib/jsx/dom/server.js +1 -1
  43. package/lib/jsx/dom.js +3 -3
  44. package/lib/jsx/streaming.js +1 -1
  45. package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-D18Y35TI.js} +1 -1
  46. package/lib/jsx-renderer.d.ts +75 -0
  47. package/lib/jsx-renderer.js +99 -0
  48. package/lib/jsx.js +3 -3
  49. package/lib/jwk.d.ts +1 -1
  50. package/lib/jwt.d.ts +1 -1
  51. package/lib/language.d.ts +1 -1
  52. package/lib/logger.d.ts +1 -1
  53. package/lib/loopback-only.d.ts +1 -1
  54. package/lib/method-not-allowed.d.ts +2 -2
  55. package/lib/method-override.d.ts +2 -2
  56. package/lib/mount.d.ts +1 -1
  57. package/lib/node-server/conninfo.d.ts +1 -1
  58. package/lib/node-server/serve-static.d.ts +1 -1
  59. package/lib/node-server/serve-static.js +1 -1
  60. package/lib/node-server.d.ts +1 -1
  61. package/lib/node-server.js +1 -1
  62. package/lib/powered-by.d.ts +1 -1
  63. package/lib/pretty-json.d.ts +1 -1
  64. package/lib/quick.d.ts +1 -1
  65. package/lib/request-id.d.ts +1 -1
  66. package/lib/request.d.ts +1 -1
  67. package/lib/route.d.ts +1 -1
  68. package/lib/schema-validator.d.ts +1 -1
  69. package/lib/secure-headers.d.ts +1 -1
  70. package/lib/{serve-static-Cw23shXv.js → serve-static-B14MF1Va.js} +1 -0
  71. package/lib/serve-static.d.ts +1 -1
  72. package/lib/ssg-BwjcA-Ge.js +389 -0
  73. package/lib/ssg.d.ts +156 -0
  74. package/lib/ssg.js +2 -0
  75. package/lib/streaming.d.ts +1 -1
  76. package/lib/testing.d.ts +3 -3
  77. package/lib/testing.js +1 -1
  78. package/lib/timeout.d.ts +1 -1
  79. package/lib/timing.d.ts +1 -1
  80. package/lib/tiny.d.ts +1 -1
  81. package/lib/trailing-slash.d.ts +1 -1
  82. package/lib/{types-nEL8VPDC.d.ts → types-Dv_nV2cv.d.ts} +1 -1
  83. package/lib/{types-CQiGmsWW.d.ts → types-zNxyEpG2.d.ts} +2 -2
  84. package/lib/types.d.ts +1 -1
  85. package/lib/utils/body.d.ts +1 -1
  86. package/lib/utils/ipaddr.d.ts +1 -1
  87. package/lib/utils-Cx61GFMC.js +125 -0
  88. package/lib/validator.d.ts +1 -1
  89. package/lib/ws.d.ts +1 -1
  90. package/package.json +9 -1
  91. package/plugins/acryl-inspect/upstream.json +1 -1
  92. package/plugins/acryl-node-server/upstream.json +1 -1
  93. package/plugins/acryl-router/src/core/helper/ssg/index.ts +14 -0
  94. package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
  95. package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
  96. package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
  97. package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
  98. package/plugins/acryl-router/src/core/jsx/dom/render.ts +6 -6
  99. package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
  100. package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
  101. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  102. package/plugins/acryl-router/src/middleware/index.ts +1 -0
  103. package/plugins/acryl-router/upstream.json +2 -2
  104. package/plugins/acryl-router-http/upstream.json +1 -1
  105. package/plugins/acryl-state/upstream.json +1 -1
  106. package/plugins/desktop/upstream.json +1 -1
  107. package/plugins/desktop-web/upstream.json +1 -1
  108. package/plugins/extensions/resolve.ts +4 -0
  109. package/shared/AGENTS.core.md +4 -0
  110. package/templates/agent/tsconfig.json +7 -1
  111. package/templates/todo/upstream.json +1 -1
  112. package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
  113. package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
  114. package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
  115. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
  116. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  117. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
  118. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
  119. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
  120. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
  121. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
  122. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  123. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
  124. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
  125. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
  126. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
  127. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
  128. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
  129. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
  130. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
  131. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
  132. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
  133. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
  134. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
  135. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
  136. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
  137. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
  138. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
  139. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
  140. package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
  141. package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
  142. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
  143. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
  144. package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
  145. package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
  146. package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
  147. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
  148. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
  149. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
  150. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
  151. package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
  152. package/plugins/acryl-router/src/core/app.test.ts +0 -3833
  153. package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
  154. package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
  155. package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
  156. package/plugins/acryl-router/src/core/compose.test.ts +0 -751
  157. package/plugins/acryl-router/src/core/context.test.ts +0 -629
  158. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
  159. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
  160. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
  161. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +0 -506
  162. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +0 -678
  163. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
  164. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
  165. package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
  166. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
  167. package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
  168. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +0 -557
  169. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
  170. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
  171. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
  172. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
  173. package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
  174. package/plugins/acryl-router/src/core/jsx/base.test.tsx +0 -97
  175. package/plugins/acryl-router/src/core/jsx/children.test.ts +0 -52
  176. package/plugins/acryl-router/src/core/jsx/components.test.tsx +0 -936
  177. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +0 -655
  178. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +0 -128
  179. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +0 -330
  180. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +0 -184
  181. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +0 -149
  182. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +0 -172
  183. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +0 -3681
  184. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +0 -1344
  185. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +0 -199
  186. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +0 -859
  187. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +0 -40
  188. package/plugins/acryl-router/src/core/jsx/index.test.tsx +0 -1744
  189. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +0 -666
  190. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +0 -85
  191. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +0 -1281
  192. package/plugins/acryl-router/src/core/jsx/utils.test.ts +0 -366
  193. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
  194. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
  195. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
  196. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
  197. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
  198. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
  199. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
  200. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
  201. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
  202. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
  203. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
  204. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
  205. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
  206. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
  207. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
  208. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
  209. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
  210. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
  211. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
  212. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
  213. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
  214. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
  215. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
  216. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
  217. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
  218. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
  219. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
  220. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
  221. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
  222. package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
  223. package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
  224. package/plugins/acryl-router/src/core/request.test.ts +0 -751
  225. package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
  226. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
  227. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
  228. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
  229. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
  230. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
  231. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
  232. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
  233. package/plugins/acryl-router/src/core/types.test.ts +0 -4134
  234. package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
  235. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
  236. package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
  237. package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
  238. package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
  239. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
  240. package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
  241. package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
  242. package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
  243. package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
  244. package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
  245. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
  246. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
  247. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
  249. package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
  250. package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
  251. package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
  252. package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
  253. package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
  254. package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
  255. package/plugins/acryl-router/tests/jsx.spec.tsx +0 -63
  256. package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
  257. package/plugins/acryl-router/tests/loader.spec.ts +0 -72
  258. package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
  259. package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
  260. package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
  261. package/plugins/acryl-router/tests/service.spec.ts +0 -426
  262. package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
  263. package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
  264. package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
  265. package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
  266. package/plugins/acryl-state/src/react.ts +0 -11
  267. package/plugins/acryl-state/tests/cell.spec.ts +0 -71
  268. package/plugins/acryl-state/tests/react.spec.ts +0 -39
  269. package/plugins/acryl-state/tests/service.spec.ts +0 -132
@@ -1,1281 +0,0 @@
1
- /** @jsxImportSource ./ */
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { JSDOM } from 'jsdom'
4
- import { raw } from '../helper/html/index.ts'
5
- import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html.ts'
6
- import type { HtmlEscapedString } from '../utils/html.ts'
7
- import { createContext, useContext } from './context.ts'
8
- import { buildDataStack } from './dom/render.ts'
9
- import { use } from './hooks/index.ts'
10
- import { Suspense, renderToReadableStream, StreamingContext } from './streaming.ts'
11
-
12
- const unsafeHtml = '<img src=x onerror=alert(1)>'
13
- const escapedUnsafeHtml = '&lt;img src=x onerror=alert(1)&gt;'
14
-
15
- function replacementResult(html: string) {
16
- const document = new JSDOM(html, { runScripts: 'dangerously' }).window.document
17
- document.querySelectorAll('template, script').forEach((e) => e.remove())
18
- return document.body.innerHTML
19
- }
20
-
21
- async function drainStream(stream: unknown): Promise<string> {
22
- const textDecoder = new TextDecoder()
23
- let html = ''
24
- for await (const chunk of stream as any) {
25
- html += textDecoder.decode(chunk)
26
- }
27
- return html
28
- }
29
-
30
- async function stringify(node: { toString(): string | Promise<string> }): Promise<string> {
31
- return String(
32
- await resolveCallback(await node.toString(), HtmlEscapedCallbackPhase.Stringify, false, {})
33
- )
34
- }
35
-
36
- async function readInitialSuspenseChunk(fallback: unknown): Promise<string> {
37
- let resolve!: () => void
38
- const promise = new Promise<void>((done) => (resolve = done))
39
- const Async = async () => {
40
- await promise
41
- return <span>done</span>
42
- }
43
- const reader = renderToReadableStream(
44
- <Suspense fallback={fallback}>
45
- <Async />
46
- </Suspense>
47
- ).getReader()
48
-
49
- const first = await reader.read()
50
- resolve()
51
- while (!(await reader.read()).done) {
52
- // Drain the retry so it cannot outlive the test.
53
- }
54
- return new TextDecoder().decode(first.value)
55
- }
56
-
57
- describe('Streaming', () => {
58
- let suspenseCounter = 0
59
- afterEach(() => {
60
- suspenseCounter++
61
- })
62
-
63
- it('Suspense / async component returning an array', async () => {
64
- const Content = async () => {
65
- await new Promise((resolve) => setTimeout(resolve, 10))
66
- return [<h1>Hello</h1>, <h2>World</h2>]
67
- }
68
-
69
- const stream = renderToReadableStream(
70
- <Suspense fallback={<p>Loading...</p>}>
71
- <Content />
72
- </Suspense>
73
- )
74
-
75
- const chunks = []
76
- const textDecoder = new TextDecoder()
77
- for await (const chunk of stream as any) {
78
- chunks.push(textDecoder.decode(chunk))
79
- }
80
-
81
- expect(chunks[0]).toBe(
82
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`
83
- )
84
- expect(chunks[1]).toContain(
85
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1><h2>World</h2></template>`
86
- )
87
- })
88
-
89
- it('preserves callbacks in a streamed async component array', async () => {
90
- const phases: number[] = []
91
- const Content = async () => [
92
- raw('Hello', [({ phase }) => void phases.push(phase)]),
93
- <span>World</span>,
94
- ]
95
-
96
- const html = await drainStream(renderToReadableStream(<Content />))
97
-
98
- expect(html).toBe('Hello<span>World</span>')
99
- expect(phases).toEqual([HtmlEscapedCallbackPhase.BeforeStream, HtmlEscapedCallbackPhase.Stream])
100
- suspenseCounter--
101
- })
102
-
103
- it('Suspense / renderToReadableStream', async () => {
104
- let contentEvaluatedCount = 0
105
- const Content = () => {
106
- contentEvaluatedCount++
107
- const content = new Promise<HtmlEscapedString>((resolve) =>
108
- setTimeout(() => resolve(<h1>Hello</h1>), 10)
109
- )
110
- return content
111
- }
112
-
113
- const stream = renderToReadableStream(
114
- <Suspense fallback={<p>Loading...</p>}>
115
- <Content />
116
- </Suspense>
117
- )
118
-
119
- const chunks = []
120
- const textDecoder = new TextDecoder()
121
- for await (const chunk of stream as any) {
122
- chunks.push(textDecoder.decode(chunk))
123
- }
124
-
125
- expect(chunks).toEqual([
126
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
127
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1></template><script>
128
- ((d,c,n) => {
129
- c=d.currentScript.previousSibling
130
- d=d.getElementById('H:${suspenseCounter}')
131
- if(!d)return
132
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
133
- d.replaceWith(c.content)
134
- })(document)
135
- </script>`,
136
- ])
137
-
138
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
139
- '<h1>Hello</h1>'
140
- )
141
-
142
- expect(contentEvaluatedCount).toEqual(1)
143
- })
144
-
145
- it('Suspense with StreamingContext', async () => {
146
- let contentEvaluatedCount = 0
147
- const Content = () => {
148
- contentEvaluatedCount++
149
- const content = new Promise<HtmlEscapedString>((resolve) =>
150
- setTimeout(() => resolve(<h1>Hello</h1>), 10)
151
- )
152
- return content
153
- }
154
-
155
- const stream = renderToReadableStream(
156
- <StreamingContext value={{ scriptNonce: 'test-nonce' }}>
157
- <Suspense fallback={<p>Loading...</p>}>
158
- <Content />
159
- </Suspense>
160
- </StreamingContext>
161
- )
162
-
163
- const chunks = []
164
- const textDecoder = new TextDecoder()
165
- for await (const chunk of stream as any) {
166
- chunks.push(textDecoder.decode(chunk))
167
- }
168
-
169
- expect(chunks).toEqual([
170
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
171
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1></template><script nonce="test-nonce">
172
- ((d,c,n) => {
173
- c=d.currentScript.previousSibling
174
- d=d.getElementById('H:${suspenseCounter}')
175
- if(!d)return
176
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
177
- d.replaceWith(c.content)
178
- })(document)
179
- </script>`,
180
- ])
181
-
182
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
183
- '<h1>Hello</h1>'
184
- )
185
-
186
- expect(contentEvaluatedCount).toEqual(1)
187
- })
188
-
189
- it('`throw promise` inside Suspense', async () => {
190
- let contentEvaluatedCount = 0
191
- let resolvedContent: HtmlEscapedString | undefined = undefined
192
- const Content = () => {
193
- contentEvaluatedCount++
194
- if (!resolvedContent) {
195
- throw new Promise<void>((resolve) =>
196
- setTimeout(() => {
197
- resolvedContent = (<p>thrown a promise then resolved</p>) as HtmlEscapedString
198
- resolve()
199
- }, 10)
200
- )
201
- }
202
- return resolvedContent
203
- }
204
-
205
- const stream = renderToReadableStream(
206
- <Suspense fallback={<p>Loading...</p>}>
207
- <Content />
208
- </Suspense>
209
- )
210
-
211
- const chunks = []
212
- const textDecoder = new TextDecoder()
213
- for await (const chunk of stream as any) {
214
- chunks.push(textDecoder.decode(chunk))
215
- }
216
-
217
- expect(chunks).toEqual([
218
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
219
- `<template data-acryl-target="H:${suspenseCounter}"><p>thrown a promise then resolved</p></template><script>
220
- ((d,c,n) => {
221
- c=d.currentScript.previousSibling
222
- d=d.getElementById('H:${suspenseCounter}')
223
- if(!d)return
224
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
225
- d.replaceWith(c.content)
226
- })(document)
227
- </script>`,
228
- ])
229
-
230
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
231
- '<p>thrown a promise then resolved</p>'
232
- )
233
-
234
- expect(contentEvaluatedCount).toEqual(2)
235
- })
236
-
237
- it('simple content inside Suspense', async () => {
238
- const Content = () => {
239
- return <h1>Hello</h1>
240
- }
241
-
242
- const stream = renderToReadableStream(
243
- <Suspense fallback={<p>Loading...</p>}>
244
- <Content />
245
- </Suspense>
246
- )
247
-
248
- const chunks = []
249
- const textDecoder = new TextDecoder()
250
- for await (const chunk of stream as any) {
251
- chunks.push(textDecoder.decode(chunk))
252
- }
253
-
254
- expect(chunks).toEqual(['<h1>Hello</h1>'])
255
-
256
- suspenseCounter -= 1 // fallback is not rendered
257
- })
258
-
259
- it('nullish children', async () => {
260
- const stream = renderToReadableStream(
261
- <div>
262
- <Suspense fallback={<p>Loading...</p>}>{[null, undefined]}</Suspense>
263
- </div>
264
- )
265
-
266
- const chunks = []
267
- const textDecoder = new TextDecoder()
268
- for await (const chunk of stream as any) {
269
- chunks.push(textDecoder.decode(chunk))
270
- }
271
-
272
- expect(chunks).toEqual(['<div></div>'])
273
-
274
- suspenseCounter -= 1 // fallback is not rendered
275
- })
276
-
277
- it('without children', async () => {
278
- const stream = renderToReadableStream(
279
- <div>
280
- <Suspense fallback={<p>Loading...</p>}></Suspense>
281
- </div>
282
- )
283
-
284
- const chunks = []
285
- const textDecoder = new TextDecoder()
286
- for await (const chunk of stream as any) {
287
- chunks.push(textDecoder.decode(chunk))
288
- }
289
-
290
- expect(chunks).toEqual(['<div></div>'])
291
-
292
- suspenseCounter -= 1 // fallback is not rendered
293
- })
294
-
295
- it('only a "false" child', async () => {
296
- const stream = renderToReadableStream(
297
- <div>
298
- <Suspense fallback={<p>Loading...</p>}>{false}</Suspense>
299
- </div>
300
- )
301
-
302
- const chunks = []
303
- const textDecoder = new TextDecoder()
304
- for await (const chunk of stream as any) {
305
- chunks.push(textDecoder.decode(chunk))
306
- }
307
-
308
- expect(chunks).toEqual(['<div></div>'])
309
-
310
- suspenseCounter -= 1 // fallback is not rendered
311
- })
312
-
313
- it('async nullish children', async () => {
314
- let resolved = false
315
- const Content = () => {
316
- if (!resolved) {
317
- resolved = true
318
- throw new Promise<void>((r) =>
319
- setTimeout(() => {
320
- resolved = true
321
- r()
322
- }, 10)
323
- )
324
- }
325
- return <h1>Hello</h1>
326
- }
327
-
328
- const stream = renderToReadableStream(
329
- <Suspense fallback={<p>Loading...</p>}>
330
- <Content />
331
- {[null, undefined]}
332
- </Suspense>
333
- )
334
-
335
- const chunks = []
336
- const textDecoder = new TextDecoder()
337
- for await (const chunk of stream as any) {
338
- chunks.push(textDecoder.decode(chunk))
339
- }
340
-
341
- expect(chunks).toEqual([
342
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
343
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1></template><script>
344
- ((d,c,n) => {
345
- c=d.currentScript.previousSibling
346
- d=d.getElementById('H:${suspenseCounter}')
347
- if(!d)return
348
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
349
- d.replaceWith(c.content)
350
- })(document)
351
- </script>`,
352
- ])
353
-
354
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
355
- '<h1>Hello</h1>'
356
- )
357
- })
358
-
359
- it('boolean children', async () => {
360
- const stream = renderToReadableStream(
361
- <div>
362
- <Suspense fallback={<p>Loading...</p>}>{[true, false]}</Suspense>
363
- </div>
364
- )
365
-
366
- const chunks = []
367
- const textDecoder = new TextDecoder()
368
- for await (const chunk of stream as any) {
369
- chunks.push(textDecoder.decode(chunk))
370
- }
371
-
372
- expect(chunks).toEqual(['<div></div>'])
373
-
374
- suspenseCounter -= 1 // fallback is not rendered
375
- })
376
-
377
- it('async boolean children', async () => {
378
- let resolved = false
379
- const Content = () => {
380
- if (!resolved) {
381
- resolved = true
382
- throw new Promise<void>((r) =>
383
- setTimeout(() => {
384
- resolved = true
385
- r()
386
- }, 10)
387
- )
388
- }
389
- return <h1>Hello</h1>
390
- }
391
-
392
- const stream = renderToReadableStream(
393
- <Suspense fallback={<p>Loading...</p>}>
394
- <Content />
395
- {[true, false]}
396
- </Suspense>
397
- )
398
-
399
- const chunks = []
400
- const textDecoder = new TextDecoder()
401
- for await (const chunk of stream as any) {
402
- chunks.push(textDecoder.decode(chunk))
403
- }
404
-
405
- expect(chunks).toEqual([
406
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
407
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1></template><script>
408
- ((d,c,n) => {
409
- c=d.currentScript.previousSibling
410
- d=d.getElementById('H:${suspenseCounter}')
411
- if(!d)return
412
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
413
- d.replaceWith(c.content)
414
- })(document)
415
- </script>`,
416
- ])
417
-
418
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
419
- '<h1>Hello</h1>'
420
- )
421
- })
422
-
423
- it('children Suspense', async () => {
424
- const Content1 = () =>
425
- new Promise<HtmlEscapedString>((resolve) => setTimeout(() => resolve(<h1>Hello</h1>), 10))
426
- const Content2 = () =>
427
- new Promise<HtmlEscapedString>((resolve) => setTimeout(() => resolve(<h2>Acryl</h2>), 10))
428
-
429
- const stream = renderToReadableStream(
430
- <Suspense fallback={<p>Loading...</p>}>
431
- <Suspense fallback={<p>Loading sub content1...</p>}>
432
- <Content1 />
433
- </Suspense>
434
- <Suspense fallback={<p>Loading sub content2...</p>}>
435
- <Content2 />
436
- </Suspense>
437
- </Suspense>
438
- )
439
-
440
- const chunks = []
441
- const textDecoder = new TextDecoder()
442
- for await (const chunk of stream as any) {
443
- chunks.push(textDecoder.decode(chunk))
444
- }
445
-
446
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
447
- '<h1>Hello</h1><h2>Acryl</h2>'
448
- )
449
-
450
- suspenseCounter += 2
451
- })
452
-
453
- it('children Suspense: Suspense and string', async () => {
454
- const Content1 = () =>
455
- new Promise<HtmlEscapedString>((resolve) => setTimeout(() => resolve(<h1>Hello</h1>), 10))
456
-
457
- const stream = renderToReadableStream(
458
- <Suspense fallback={<p>Loading...</p>}>
459
- <Suspense fallback={<p>Loading sub content1...</p>}>
460
- <Content1 />
461
- </Suspense>
462
- Acryl
463
- </Suspense>
464
- )
465
-
466
- const chunks = []
467
- const textDecoder = new TextDecoder()
468
- for await (const chunk of stream as any) {
469
- chunks.push(textDecoder.decode(chunk))
470
- }
471
-
472
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
473
- '<h1>Hello</h1>Acryl'
474
- )
475
-
476
- suspenseCounter += 1
477
- })
478
-
479
- it('resolve(undefined)', async () => {
480
- const Content = async () => {
481
- const content = await Promise.resolve(undefined)
482
- return <p>{content}</p>
483
- }
484
-
485
- const stream = renderToReadableStream(
486
- <Suspense fallback={<p>Loading...</p>}>
487
- <Content />
488
- </Suspense>
489
- )
490
-
491
- const chunks = []
492
- const textDecoder = new TextDecoder()
493
- for await (const chunk of stream as any) {
494
- chunks.push(textDecoder.decode(chunk))
495
- }
496
-
497
- expect(chunks).toEqual([
498
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
499
- `<template data-acryl-target="H:${suspenseCounter}"><p></p></template><script>
500
- ((d,c,n) => {
501
- c=d.currentScript.previousSibling
502
- d=d.getElementById('H:${suspenseCounter}')
503
- if(!d)return
504
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
505
- d.replaceWith(c.content)
506
- })(document)
507
- </script>`,
508
- ])
509
-
510
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual('<p></p>')
511
- })
512
-
513
- it('resolve(null)', async () => {
514
- const Content = async () => {
515
- const content = await Promise.resolve(null)
516
- return <p>{content}</p>
517
- }
518
-
519
- const stream = renderToReadableStream(
520
- <Suspense fallback={<p>Loading...</p>}>
521
- <Content />
522
- </Suspense>
523
- )
524
-
525
- const chunks = []
526
- const textDecoder = new TextDecoder()
527
- for await (const chunk of stream as any) {
528
- chunks.push(textDecoder.decode(chunk))
529
- }
530
-
531
- expect(chunks).toEqual([
532
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
533
- `<template data-acryl-target="H:${suspenseCounter}"><p></p></template><script>
534
- ((d,c,n) => {
535
- c=d.currentScript.previousSibling
536
- d=d.getElementById('H:${suspenseCounter}')
537
- if(!d)return
538
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
539
- d.replaceWith(c.content)
540
- })(document)
541
- </script>`,
542
- ])
543
-
544
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual('<p></p>')
545
- })
546
-
547
- it('reject()', async () => {
548
- const Content = async () => {
549
- const content = await Promise.reject()
550
- return <p>{content}</p>
551
- }
552
-
553
- const onError = vi.fn()
554
- const stream = renderToReadableStream(
555
- <Suspense fallback={<p>Loading...</p>}>
556
- <Content />
557
- </Suspense>,
558
- onError
559
- )
560
-
561
- const chunks = []
562
- const textDecoder = new TextDecoder()
563
- for await (const chunk of stream as any) {
564
- chunks.push(textDecoder.decode(chunk))
565
- }
566
-
567
- expect(onError).toBeCalledTimes(1)
568
-
569
- expect(chunks).toEqual([
570
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
571
- '',
572
- ])
573
-
574
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
575
- '<p>Loading...</p><!--/$-->'
576
- )
577
- })
578
-
579
- it('closed()', async () => {
580
- const Content = async () => {
581
- await new Promise<void>((resolve) =>
582
- setTimeout(() => {
583
- vi.spyOn(ReadableStreamDefaultController.prototype, 'enqueue').mockImplementation(() => {
584
- throw new Error('closed')
585
- })
586
- resolve()
587
- }, 10)
588
- )
589
- return <p>content</p>
590
- }
591
-
592
- const onError = vi.fn()
593
- const stream = renderToReadableStream(
594
- <>
595
- <Suspense fallback={<p>Loading...</p>}>
596
- <Content />
597
- </Suspense>
598
- <Suspense fallback={<p>Loading...</p>}>
599
- <Content />
600
- </Suspense>
601
- </>,
602
- onError
603
- )
604
-
605
- const chunks = []
606
- const textDecoder = new TextDecoder()
607
- for await (const chunk of stream as any) {
608
- chunks.push(textDecoder.decode(chunk))
609
- }
610
-
611
- expect(onError).toBeCalledTimes(1)
612
-
613
- expect(chunks).toEqual([
614
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$--><template id="H:${
615
- suspenseCounter + 1
616
- }"></template><p>Loading...</p><!--/$-->`,
617
- ])
618
-
619
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
620
- '<p>Loading...</p><!--/$--><p>Loading...</p><!--/$-->'
621
- )
622
-
623
- suspenseCounter++
624
- await new Promise((resolve) => setTimeout(resolve, 10))
625
- vi.restoreAllMocks()
626
- })
627
-
628
- it('Multiple "await" call', async () => {
629
- const delayedContent = new Promise<HtmlEscapedString>((resolve) =>
630
- setTimeout(() => resolve(<h1>Hello</h1>), 10)
631
- )
632
- const delayedContent2 = new Promise<HtmlEscapedString>((resolve) =>
633
- setTimeout(() => resolve(<h2>World</h2>), 10)
634
- )
635
- const Content = async () => {
636
- const content = await delayedContent
637
- const content2 = await delayedContent2
638
- return (
639
- <>
640
- {content}
641
- {content2}
642
- </>
643
- )
644
- }
645
-
646
- const stream = renderToReadableStream(
647
- <Suspense fallback={<p>Loading...</p>}>
648
- <Content />
649
- </Suspense>
650
- )
651
-
652
- const chunks = []
653
- const textDecoder = new TextDecoder()
654
- for await (const chunk of stream as any) {
655
- chunks.push(textDecoder.decode(chunk))
656
- }
657
-
658
- expect(chunks).toEqual([
659
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
660
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1><h2>World</h2></template><script>
661
- ((d,c,n) => {
662
- c=d.currentScript.previousSibling
663
- d=d.getElementById('H:${suspenseCounter}')
664
- if(!d)return
665
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
666
- d.replaceWith(c.content)
667
- })(document)
668
- </script>`,
669
- ])
670
-
671
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
672
- '<h1>Hello</h1><h2>World</h2>'
673
- )
674
- })
675
-
676
- it('Complex fallback content', async () => {
677
- const delayedContent = new Promise<HtmlEscapedString>((resolve) =>
678
- setTimeout(() => resolve(<h1>Hello</h1>), 10)
679
- )
680
-
681
- const Content = async () => {
682
- const content = await delayedContent
683
- return content
684
- }
685
-
686
- const stream = renderToReadableStream(
687
- <Suspense
688
- fallback={
689
- <>
690
- Loading<span>...</span>
691
- </>
692
- }
693
- >
694
- <Content />
695
- </Suspense>
696
- )
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(chunks).toEqual([
705
- `<template id="H:${suspenseCounter}"></template>Loading<span>...</span><!--/$-->`,
706
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1></template><script>
707
- ((d,c,n) => {
708
- c=d.currentScript.previousSibling
709
- d=d.getElementById('H:${suspenseCounter}')
710
- if(!d)return
711
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
712
- d.replaceWith(c.content)
713
- })(document)
714
- </script>`,
715
- ])
716
-
717
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
718
- '<h1>Hello</h1>'
719
- )
720
- })
721
-
722
- it('renders Suspense fallback with outer context values', async () => {
723
- const ThemeContext = createContext('default')
724
- const Fallback = () => <p>Loading {useContext(ThemeContext)}</p>
725
- const Content = () =>
726
- new Promise<HtmlEscapedString>((resolve) => setTimeout(() => resolve(<h1>Done</h1>), 10))
727
-
728
- const stream = renderToReadableStream(
729
- <ThemeContext.Provider value='outer'>
730
- <Suspense fallback={<Fallback />}>
731
- <Content />
732
- </Suspense>
733
- </ThemeContext.Provider>
734
- )
735
-
736
- const chunks = []
737
- const textDecoder = new TextDecoder()
738
- for await (const chunk of stream as any) {
739
- chunks.push(textDecoder.decode(chunk))
740
- }
741
-
742
- expect(chunks[0]).toBe(
743
- `<template id="H:${suspenseCounter}"></template><p>Loading outer</p><!--/$-->`
744
- )
745
- expect(chunks.join('')).toContain('<h1>Done</h1>')
746
- expect(chunks.join('')).not.toContain('default')
747
- })
748
-
749
- it('nested Suspense', async () => {
750
- const SubContent = () => {
751
- const content = new Promise<HtmlEscapedString>((resolve) =>
752
- setTimeout(() => resolve(<h2>World</h2>), 10)
753
- )
754
- return content
755
- }
756
-
757
- const Content = () => {
758
- const content = new Promise<HtmlEscapedString>((resolve) =>
759
- setTimeout(
760
- () =>
761
- resolve(
762
- <>
763
- <h1>Hello</h1>
764
- <Suspense fallback={<p>Loading sub content...</p>}>
765
- <SubContent />
766
- </Suspense>
767
- </>
768
- ),
769
- 10
770
- )
771
- )
772
- return content
773
- }
774
-
775
- const stream = renderToReadableStream(
776
- <Suspense fallback={<p>Loading...</p>}>
777
- <Content />
778
- </Suspense>
779
- )
780
-
781
- const chunks = []
782
- const textDecoder = new TextDecoder()
783
- for await (const chunk of stream as any) {
784
- chunks.push(textDecoder.decode(chunk))
785
- }
786
-
787
- expect(chunks).toEqual([
788
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
789
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello</h1><template id=\"H:${
790
- suspenseCounter + 1
791
- }\"></template><p>Loading sub content...</p><!--/$--></template><script>
792
- ((d,c,n) => {
793
- c=d.currentScript.previousSibling
794
- d=d.getElementById('H:${suspenseCounter}')
795
- if(!d)return
796
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
797
- d.replaceWith(c.content)
798
- })(document)
799
- </script>`,
800
- `<template data-acryl-target="H:${suspenseCounter + 1}"><h2>World</h2></template><script>
801
- ((d,c,n) => {
802
- c=d.currentScript.previousSibling
803
- d=d.getElementById('H:${suspenseCounter + 1}')
804
- if(!d)return
805
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
806
- d.replaceWith(c.content)
807
- })(document)
808
- </script>`,
809
- ])
810
-
811
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
812
- '<h1>Hello</h1><h2>World</h2>'
813
- )
814
- suspenseCounter++
815
- })
816
-
817
- it('In multiple Suspense, go ahead in the order of resolved', async () => {
818
- const SubContent2 = () => {
819
- const content = new Promise<HtmlEscapedString>((resolve) =>
820
- setTimeout(() => resolve(<p>first</p>), 20)
821
- )
822
- return content
823
- }
824
- const SubContent1 = () => {
825
- const content = new Promise<HtmlEscapedString>((resolve) =>
826
- setTimeout(
827
- () =>
828
- resolve(
829
- <Suspense fallback={<p>Loading content2...</p>}>
830
- <SubContent2 />
831
- </Suspense>
832
- ),
833
- 10
834
- )
835
- )
836
- return content
837
- }
838
- const SubContent3 = () => {
839
- const content = new Promise<HtmlEscapedString>((resolve) =>
840
- setTimeout(() => resolve(<p>last</p>), 40)
841
- )
842
- return content
843
- }
844
-
845
- const Content = () => {
846
- const content = new Promise<HtmlEscapedString>((resolve) =>
847
- setTimeout(
848
- () =>
849
- resolve(
850
- <>
851
- <Suspense fallback={<p>Loading content1...</p>}>
852
- <SubContent1 />
853
- </Suspense>
854
- <Suspense fallback={<p>Loading content3...</p>}>
855
- <SubContent3 />
856
- </Suspense>
857
- </>
858
- ),
859
- 10
860
- )
861
- )
862
- return content
863
- }
864
-
865
- const stream = renderToReadableStream(
866
- <Suspense fallback={<p>Loading...</p>}>
867
- <Content />
868
- </Suspense>
869
- )
870
-
871
- const chunks = []
872
- const textDecoder = new TextDecoder()
873
- for await (const chunk of stream as any) {
874
- chunks.push(textDecoder.decode(chunk))
875
- }
876
-
877
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
878
- '<p>first</p><p>last</p>'
879
- )
880
- })
881
-
882
- it('Suspense with resolveStream', async () => {
883
- let contentEvaluatedCount = 0
884
- const Content = () => {
885
- contentEvaluatedCount++
886
- const content = new Promise<HtmlEscapedString>((resolve) =>
887
- setTimeout(() => resolve(<h1>Hello</h1>), 10)
888
- )
889
- return content
890
- }
891
-
892
- const str = await resolveCallback(
893
- await (
894
- <Suspense fallback={<p>Loading...</p>}>
895
- <Content />
896
- </Suspense>
897
- ).toString(),
898
- HtmlEscapedCallbackPhase.Stream,
899
- false,
900
- {}
901
- )
902
-
903
- expect(str).toEqual('<h1>Hello</h1>')
904
- expect(contentEvaluatedCount).toEqual(1)
905
- })
906
-
907
- it('renderToReadableStream(str: string)', async () => {
908
- const str = '<h1>Hello</h1>'
909
- const stream = renderToReadableStream(raw(str))
910
-
911
- const chunks = []
912
- const textDecoder = new TextDecoder()
913
- for await (const chunk of stream as any) {
914
- chunks.push(textDecoder.decode(chunk))
915
- }
916
-
917
- expect(chunks).toEqual([str])
918
- })
919
-
920
- it('renderToReadableStream(promise: Promise<HtmlEscapedString>)', async () => {
921
- const stream = renderToReadableStream(Promise.resolve(raw('<h1>Hello</h1>')))
922
-
923
- const chunks = []
924
- const textDecoder = new TextDecoder()
925
- for await (const chunk of stream as any) {
926
- chunks.push(textDecoder.decode(chunk))
927
- }
928
-
929
- expect(chunks).toEqual(['<h1>Hello</h1>'])
930
-
931
- suspenseCounter++
932
- })
933
-
934
- describe('use()', async () => {
935
- it('render to string', async () => {
936
- const promise = new Promise((resolve) => setTimeout(() => resolve('Hello from use()'), 0))
937
- const Content = () => {
938
- const message = use(promise)
939
- return <h1>{message}</h1>
940
- }
941
-
942
- const str = await resolveCallback(
943
- await (
944
- <Suspense fallback={<p>Loading...</p>}>
945
- <Content />
946
- </Suspense>
947
- ).toString(),
948
- HtmlEscapedCallbackPhase.Stream,
949
- false,
950
- {}
951
- )
952
- expect(str).toEqual('<h1>Hello from use()</h1>')
953
- })
954
-
955
- it('render to stream', async () => {
956
- const promise = new Promise((resolve) => setTimeout(() => resolve('Hello from use()'), 0))
957
- const Content = () => {
958
- const message = use(promise)
959
- return <h1>{message}</h1>
960
- }
961
-
962
- const stream = renderToReadableStream(
963
- <Suspense fallback={<p>Loading...</p>}>
964
- <Content />
965
- </Suspense>
966
- )
967
-
968
- const chunks = []
969
- const textDecoder = new TextDecoder()
970
- for await (const chunk of stream as any) {
971
- chunks.push(textDecoder.decode(chunk))
972
- }
973
-
974
- expect(chunks).toEqual([
975
- `<template id="H:${suspenseCounter}"></template><p>Loading...</p><!--/$-->`,
976
- `<template data-acryl-target="H:${suspenseCounter}"><h1>Hello from use()</h1></template><script>
977
- ((d,c,n) => {
978
- c=d.currentScript.previousSibling
979
- d=d.getElementById('H:${suspenseCounter}')
980
- if(!d)return
981
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
982
- d.replaceWith(c.content)
983
- })(document)
984
- </script>`,
985
- ])
986
-
987
- expect(replacementResult(`<html><body>${chunks.join('')}</body></html>`)).toEqual(
988
- '<h1>Hello from use()</h1>'
989
- )
990
- })
991
- })
992
-
993
- it('should not throw ERR_INVALID_STATE when reader is cancelled during nested Suspense streaming', async () => {
994
- const unhandled: unknown[] = []
995
- const onRejection = (e: unknown) => unhandled.push(e)
996
- process.on('unhandledRejection', onRejection)
997
-
998
- const SubContent = async () => <h2>World</h2>
999
- const Content = async () => (
1000
- <>
1001
- <h1>Hello</h1>
1002
- <Suspense fallback={<p>Loading sub...</p>}>
1003
- <SubContent />
1004
- </Suspense>
1005
- </>
1006
- )
1007
-
1008
- const onError = vi.fn()
1009
- const stream = renderToReadableStream(
1010
- <Suspense fallback={<p>Loading...</p>}>
1011
- <Content />
1012
- </Suspense>,
1013
- onError
1014
- )
1015
-
1016
- const reader = stream.getReader()
1017
- const firstChunk = await reader.read()
1018
- expect(firstChunk.done).toBe(false)
1019
-
1020
- // Simulate client disconnect
1021
- await reader.cancel()
1022
-
1023
- // Wait for nested Suspense callbacks to fire against the closed controller
1024
- await new Promise((resolve) => setTimeout(resolve))
1025
-
1026
- expect(unhandled).toHaveLength(0)
1027
- expect(onError).not.toHaveBeenCalled()
1028
-
1029
- process.off('unhandledRejection', onRejection)
1030
- })
1031
-
1032
- it('should not call onError when reader is cancelled during a slow callback resolution', async () => {
1033
- const unhandled: unknown[] = []
1034
- const onRejection = (e: unknown) => unhandled.push(e)
1035
- process.on('unhandledRejection', onRejection)
1036
-
1037
- let signalCallbackStarted!: () => void
1038
- const callbackStarted = new Promise<void>((r) => {
1039
- signalCallbackStarted = r
1040
- })
1041
-
1042
- const Content = async () =>
1043
- raw('<p>content</p>', [
1044
- ((opts: any) => {
1045
- if (opts.phase === HtmlEscapedCallbackPhase.BeforeStream) {
1046
- signalCallbackStarted()
1047
- return new Promise<string>((r) => setTimeout(() => r('')))
1048
- }
1049
- return undefined
1050
- }) as any,
1051
- ])
1052
-
1053
- const onError = vi.fn()
1054
- const stream = renderToReadableStream(
1055
- <Suspense fallback={<p>Loading...</p>}>
1056
- <Content />
1057
- </Suspense>,
1058
- onError
1059
- )
1060
-
1061
- const reader = stream.getReader()
1062
- await reader.read()
1063
-
1064
- await callbackStarted
1065
- await reader.cancel()
1066
-
1067
- await new Promise((resolve) => setTimeout(resolve))
1068
-
1069
- expect(unhandled).toHaveLength(0)
1070
- expect(onError).not.toHaveBeenCalled()
1071
-
1072
- process.off('unhandledRejection', onRejection)
1073
- })
1074
-
1075
- it('should not throw when cancelled before initial content resolves', async () => {
1076
- const unhandled: unknown[] = []
1077
- const onRejection = (e: unknown) => unhandled.push(e)
1078
- process.on('unhandledRejection', onRejection)
1079
-
1080
- const onError = vi.fn()
1081
- const stream = renderToReadableStream(
1082
- Promise.resolve(raw('<p>slow content</p>') as HtmlEscapedString),
1083
- onError
1084
- )
1085
-
1086
- const reader = stream.getReader()
1087
- await reader.cancel()
1088
-
1089
- await new Promise((resolve) => setTimeout(resolve))
1090
-
1091
- expect(unhandled).toHaveLength(0)
1092
- expect(onError).not.toHaveBeenCalled()
1093
-
1094
- process.off('unhandledRejection', onRejection)
1095
- })
1096
-
1097
- it('pops buildDataStack when a deferred re-render rejects', async () => {
1098
- const baseLength = buildDataStack.length
1099
-
1100
- const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('x'), 10))
1101
- let first = true
1102
- const Content = () => {
1103
- if (first) {
1104
- // Suspend like use() does so Suspense defers the re-render.
1105
- first = false
1106
- throw deferred
1107
- }
1108
- throw new Error('boom')
1109
- }
1110
-
1111
- const onError = vi.fn(() => '')
1112
- const stream = renderToReadableStream(
1113
- <Suspense fallback={<p>Loading</p>}>
1114
- <Content />
1115
- </Suspense>,
1116
- onError
1117
- )
1118
- for await (const _ of stream as any) {
1119
- // drain
1120
- }
1121
-
1122
- expect(onError).toHaveBeenCalledTimes(1)
1123
- // The deferred re-render pushed a stack frame; rejecting must still pop it.
1124
- expect(buildDataStack.length).toBe(baseLength)
1125
- })
1126
-
1127
- it('isolates context between concurrent streaming renders', async () => {
1128
- // Default value is distinct from both requests so a cross-request leak
1129
- // (reading the other request's value) would be detectable.
1130
- const SessionContext = createContext('nobody')
1131
- const waits = new Map<string, Promise<void>>()
1132
- const entered = new Map<string, () => void>()
1133
-
1134
- const Dashboard = async ({ name }: { name: string }) => {
1135
- // Read context only after suspending, the pattern that previously leaked.
1136
- entered.get(name)?.()
1137
- await waits.get(name)
1138
- return <span>role:{useContext(SessionContext)}</span>
1139
- }
1140
-
1141
- const drain = async (value: string) => {
1142
- return drainStream(
1143
- renderToReadableStream(
1144
- <SessionContext.Provider value={value}>
1145
- <Dashboard name={value} />
1146
- </SessionContext.Provider>
1147
- )
1148
- )
1149
- }
1150
-
1151
- let resolveAdmin!: () => void
1152
- let resolveGuest!: () => void
1153
- waits.set(
1154
- 'admin',
1155
- new Promise<void>((resolve) => {
1156
- resolveAdmin = resolve
1157
- })
1158
- )
1159
- waits.set(
1160
- 'guest',
1161
- new Promise<void>((resolve) => {
1162
- resolveGuest = resolve
1163
- })
1164
- )
1165
- const adminEntered = new Promise<void>((resolve) => {
1166
- entered.set('admin', resolve)
1167
- })
1168
- const guestEntered = new Promise<void>((resolve) => {
1169
- entered.set('guest', resolve)
1170
- })
1171
-
1172
- const adminDrain = drain('admin')
1173
- const guestDrain = drain('guest')
1174
-
1175
- await Promise.all([adminEntered, guestEntered])
1176
- resolveGuest()
1177
- await Promise.resolve()
1178
- resolveAdmin()
1179
-
1180
- const [adminHtml, guestHtml] = await Promise.all([adminDrain, guestDrain])
1181
-
1182
- expect(adminHtml).toContain('role:admin')
1183
- expect(adminHtml).not.toContain('role:guest')
1184
- expect(guestHtml).toContain('role:guest')
1185
- expect(guestHtml).not.toContain('role:admin')
1186
- })
1187
-
1188
- describe('escaping', () => {
1189
- it('escapes string children with an asynchronous sibling', async () => {
1190
- const Async = async () => <span>done</span>
1191
- const node = (
1192
- <Suspense fallback='loading'>
1193
- {unsafeHtml}
1194
- <Async />
1195
- </Suspense>
1196
- )
1197
-
1198
- expect(await stringify(node)).toBe(`${escapedUnsafeHtml}<span>done</span>`)
1199
- const streamed = await drainStream(renderToReadableStream(node))
1200
- expect(streamed).toContain(escapedUnsafeHtml)
1201
- expect(streamed).not.toContain(unsafeHtml)
1202
- })
1203
-
1204
- it('escapes nested array strings', async () => {
1205
- expect(await stringify(<Suspense fallback='loading'>{[[[unsafeHtml]]]}</Suspense>)).toBe(
1206
- escapedUnsafeHtml
1207
- )
1208
- })
1209
-
1210
- it('escapes a primitive fallback in the initial stream chunk', async () => {
1211
- const initialChunk = await readInitialSuspenseChunk(unsafeHtml)
1212
-
1213
- expect(initialChunk).toContain(escapedUnsafeHtml)
1214
- expect(initialChunk).not.toContain(unsafeHtml)
1215
- })
1216
-
1217
- it('escapes untrusted object fallbacks', async () => {
1218
- const object = { toString: () => unsafeHtml }
1219
- const asyncObject = { toString: async () => unsafeHtml }
1220
- const rawObject = { toString: () => raw(unsafeHtml) }
1221
-
1222
- for (const fallback of [object, asyncObject, rawObject]) {
1223
- const initialChunk = await readInitialSuspenseChunk(fallback)
1224
- expect(initialChunk).toContain(escapedUnsafeHtml)
1225
- expect(initialChunk).not.toContain(unsafeHtml)
1226
- }
1227
- })
1228
-
1229
- it('preserves an explicitly trusted fallback', async () => {
1230
- expect(await readInitialSuspenseChunk(raw('<strong>trusted</strong>'))).toContain(
1231
- '<strong>trusted</strong>'
1232
- )
1233
- })
1234
-
1235
- it('does not interpret replacement patterns in content', async () => {
1236
- const content = "literal $& $` $'"
1237
- const Async = async () => <span>done</span>
1238
-
1239
- expect(
1240
- await stringify(
1241
- <Suspense fallback='loading'>
1242
- {content}
1243
- <Async />
1244
- </Suspense>
1245
- )
1246
- ).toBe('literal $&amp; $` $&#39;<span>done</span>')
1247
- })
1248
-
1249
- it('escapes untrusted object children', async () => {
1250
- const object = { toString: () => unsafeHtml }
1251
- const asyncObject = { toString: async () => unsafeHtml }
1252
- const rawObject = { toString: () => raw(unsafeHtml) }
1253
-
1254
- for (const child of [object, asyncObject, rawObject]) {
1255
- const node = <Suspense fallback='loading'>{child as never}</Suspense>
1256
- expect(await stringify(node)).toBe(escapedUnsafeHtml)
1257
- const streamed = await drainStream(renderToReadableStream(node))
1258
- expect(streamed).toContain(escapedUnsafeHtml)
1259
- expect(streamed).not.toContain(unsafeHtml)
1260
- }
1261
- })
1262
-
1263
- it('preserves explicitly trusted children', async () => {
1264
- expect(
1265
- await stringify(<Suspense fallback='loading'>{raw('<strong>trusted</strong>')}</Suspense>)
1266
- ).toBe('<strong>trusted</strong>')
1267
- })
1268
-
1269
- it('keeps benign object stringification visible', async () => {
1270
- expect(await stringify(<Suspense fallback='loading'>{{} as never}</Suspense>)).toBe(
1271
- '[object Object]'
1272
- )
1273
- })
1274
-
1275
- it('keeps a direct Promise child unchanged', async () => {
1276
- expect(
1277
- await stringify(<Suspense fallback='loading'>{Promise.resolve('resolved')}</Suspense>)
1278
- ).toBe('[object Promise]')
1279
- })
1280
- })
1281
- })