acrylblends 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +4 -3
  2. package/docs/framework/README.md +2 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/guides/jsx.md +51 -1
  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/{base-ddiVmiE7.js → base-NSN_SNv-.js} +1 -0
  13. package/lib/basic-auth.d.ts +1 -1
  14. package/lib/bearer-auth.d.ts +1 -1
  15. package/lib/body-limit.d.ts +1 -1
  16. package/lib/cache.d.ts +1 -1
  17. package/lib/cache.js +4 -4
  18. package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
  19. package/lib/client.d.ts +2 -2
  20. package/lib/client.js +2 -1
  21. package/lib/combine.d.ts +1 -1
  22. package/lib/{components-DYZdxJIJ.js → components-JXVm45kK.js} +1 -1
  23. package/lib/{components-Ce5FRyHU.js → components-cEW3ovK0.js} +3 -3
  24. package/lib/compress.d.ts +1 -1
  25. package/lib/conninfo.d.ts +2 -2
  26. package/lib/{context-DVQBjuns.d.ts → context-DS3fYyG-.d.ts} +1 -1
  27. package/lib/context-storage.d.ts +1 -1
  28. package/lib/cookie.d.ts +1 -1
  29. package/lib/cors.d.ts +1 -1
  30. package/lib/csrf.d.ts +1 -1
  31. package/lib/dev.d.ts +2 -2
  32. package/lib/etag.d.ts +1 -1
  33. package/lib/factory.d.ts +2 -2
  34. package/lib/{hooks-Bq5q9IMl.js → hooks-w72iLeFR.js} +6 -6
  35. package/lib/{index-CUTfU69R.d.ts → index-Bb7YJKCq.d.ts} +2 -2
  36. package/lib/{index-CsvB-F9j.d.ts → index-BvFh1Gfx.d.ts} +1 -1
  37. package/lib/{index-Cqcdluyn.d.ts → index-CTy4N7ng.d.ts} +2 -2
  38. package/lib/index.d.ts +4 -4
  39. package/lib/ip-restriction.d.ts +2 -2
  40. package/lib/jsx/dom/client.js +1 -1
  41. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  42. package/lib/jsx/dom/jsx-runtime.js +1 -1
  43. package/lib/jsx/dom/server.js +2 -2
  44. package/lib/jsx/dom.js +4 -4
  45. package/lib/jsx/jsx-dev-runtime.js +1 -1
  46. package/lib/jsx/jsx-runtime.js +1 -1
  47. package/lib/jsx/streaming.js +1 -1
  48. package/lib/{jsx-dev-runtime-kAhR_4C7.js → jsx-dev-runtime-CWLRA2F3.js} +2 -2
  49. package/lib/jsx-renderer.d.ts +75 -0
  50. package/lib/jsx-renderer.js +99 -0
  51. package/lib/jsx.js +4 -4
  52. package/lib/jwk.d.ts +1 -1
  53. package/lib/jwt.d.ts +1 -1
  54. package/lib/language.d.ts +1 -1
  55. package/lib/logger.d.ts +1 -1
  56. package/lib/loopback-only.d.ts +1 -1
  57. package/lib/method-not-allowed.d.ts +2 -2
  58. package/lib/method-override.d.ts +2 -2
  59. package/lib/mount.d.ts +1 -1
  60. package/lib/node-server/conninfo.d.ts +1 -1
  61. package/lib/node-server/register.d.ts +1 -0
  62. package/lib/node-server/register.js +73 -0
  63. package/lib/node-server/serve-static.d.ts +1 -1
  64. package/lib/node-server/serve-static.js +1 -1
  65. package/lib/node-server.d.ts +1 -1
  66. package/lib/node-server.js +1 -1
  67. package/lib/powered-by.d.ts +1 -1
  68. package/lib/pretty-json.d.ts +1 -1
  69. package/lib/quick.d.ts +1 -1
  70. package/lib/request-id.d.ts +1 -1
  71. package/lib/request.d.ts +1 -1
  72. package/lib/route.d.ts +1 -1
  73. package/lib/schema-validator.d.ts +1 -1
  74. package/lib/secure-headers.d.ts +1 -1
  75. package/lib/{serve-static-Cw23shXv.js → serve-static-B14MF1Va.js} +1 -0
  76. package/lib/serve-static.d.ts +1 -1
  77. package/lib/ssg-BwjcA-Ge.js +389 -0
  78. package/lib/ssg.d.ts +156 -0
  79. package/lib/ssg.js +2 -0
  80. package/lib/streaming.d.ts +1 -1
  81. package/lib/testing.d.ts +3 -3
  82. package/lib/testing.js +1 -1
  83. package/lib/timeout.d.ts +1 -1
  84. package/lib/timing.d.ts +1 -1
  85. package/lib/tiny.d.ts +1 -1
  86. package/lib/trailing-slash.d.ts +1 -1
  87. package/lib/{types-nEL8VPDC.d.ts → types-Dv_nV2cv.d.ts} +1 -1
  88. package/lib/{types-CQiGmsWW.d.ts → types-zNxyEpG2.d.ts} +2 -2
  89. package/lib/types.d.ts +1 -1
  90. package/lib/utils/body.d.ts +1 -1
  91. package/lib/utils/ipaddr.d.ts +1 -1
  92. package/lib/utils-Cx61GFMC.js +125 -0
  93. package/lib/validator.d.ts +1 -1
  94. package/lib/ws.d.ts +1 -1
  95. package/package.json +13 -1
  96. package/plugins/acryl-inspect/upstream.json +1 -1
  97. package/plugins/acryl-node-server/src/register.ts +86 -0
  98. package/plugins/acryl-node-server/upstream.json +5 -3
  99. package/plugins/acryl-router/src/core/helper/ssg/index.ts +14 -0
  100. package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
  101. package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
  102. package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
  103. package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
  104. package/plugins/acryl-router/src/core/jsx/dom/render.ts +6 -6
  105. package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
  106. package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
  107. package/plugins/acryl-router/src/helpers/index.ts +1 -0
  108. package/plugins/acryl-router/src/middleware/index.ts +1 -0
  109. package/plugins/acryl-router/upstream.json +2 -2
  110. package/plugins/acryl-router-http/upstream.json +1 -1
  111. package/plugins/acryl-state/upstream.json +1 -1
  112. package/plugins/desktop/main.ts +4 -1
  113. package/plugins/desktop/upstream.json +1 -1
  114. package/plugins/desktop-web/main.ts +3 -1
  115. package/plugins/desktop-web/upstream.json +1 -1
  116. package/plugins/extensions/resolve.ts +4 -0
  117. package/shared/AGENTS.core.md +3 -0
  118. package/templates/agent/package.json +7 -3
  119. package/templates/agent/tsconfig.json +10 -2
  120. package/templates/blank/package.json +7 -3
  121. package/templates/blank/src/about.tsx +14 -0
  122. package/templates/blank/src/routes.ts +2 -0
  123. package/templates/blank/test/app.test.ts +15 -0
  124. package/templates/blank/tsconfig.json +3 -1
  125. package/templates/todo/package.json +7 -3
  126. package/templates/todo/tsconfig.json +3 -1
  127. package/templates/todo/upstream.json +1 -1
  128. package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
  129. package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
  130. package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
  131. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
  132. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  133. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
  134. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
  135. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
  136. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
  137. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
  138. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  139. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
  140. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
  141. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
  142. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
  143. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
  144. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
  145. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
  146. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
  147. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
  148. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
  149. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
  150. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
  151. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
  152. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
  153. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
  154. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
  155. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
  156. package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
  157. package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
  158. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
  159. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
  160. package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
  161. package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
  162. package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
  163. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
  164. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
  165. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
  166. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
  167. package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
  168. package/plugins/acryl-router/src/core/app.test.ts +0 -3833
  169. package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
  170. package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
  171. package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
  172. package/plugins/acryl-router/src/core/compose.test.ts +0 -751
  173. package/plugins/acryl-router/src/core/context.test.ts +0 -629
  174. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
  175. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
  176. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
  177. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +0 -506
  178. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +0 -678
  179. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
  180. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
  181. package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
  182. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
  183. package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
  184. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +0 -557
  185. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
  186. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
  187. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
  188. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
  189. package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
  190. package/plugins/acryl-router/src/core/jsx/base.test.tsx +0 -97
  191. package/plugins/acryl-router/src/core/jsx/children.test.ts +0 -52
  192. package/plugins/acryl-router/src/core/jsx/components.test.tsx +0 -936
  193. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +0 -655
  194. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +0 -128
  195. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +0 -330
  196. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +0 -184
  197. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +0 -149
  198. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +0 -172
  199. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +0 -3681
  200. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +0 -1344
  201. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +0 -199
  202. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +0 -859
  203. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +0 -40
  204. package/plugins/acryl-router/src/core/jsx/index.test.tsx +0 -1744
  205. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +0 -666
  206. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +0 -85
  207. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +0 -1281
  208. package/plugins/acryl-router/src/core/jsx/utils.test.ts +0 -366
  209. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
  210. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
  211. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
  212. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
  213. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
  214. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
  215. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
  216. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
  217. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
  218. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
  219. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
  220. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
  221. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
  222. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
  223. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
  224. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
  225. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
  226. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
  227. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
  228. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
  229. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
  230. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
  231. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
  232. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
  233. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
  234. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
  235. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
  236. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
  237. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
  238. package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
  239. package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
  240. package/plugins/acryl-router/src/core/request.test.ts +0 -751
  241. package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
  242. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
  243. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
  244. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
  245. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
  246. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
  247. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
  248. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
  249. package/plugins/acryl-router/src/core/types.test.ts +0 -4134
  250. package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
  251. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
  252. package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
  253. package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
  254. package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
  255. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
  256. package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
  257. package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
  258. package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
  259. package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
  260. package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
  261. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
  262. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
  263. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
  264. package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
  265. package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
  266. package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
  267. package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
  268. package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
  269. package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
  270. package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
  271. package/plugins/acryl-router/tests/jsx.spec.tsx +0 -63
  272. package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
  273. package/plugins/acryl-router/tests/loader.spec.ts +0 -72
  274. package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
  275. package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
  276. package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
  277. package/plugins/acryl-router/tests/service.spec.ts +0 -426
  278. package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
  279. package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
  280. package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
  281. package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
  282. package/plugins/acryl-state/src/react.ts +0 -11
  283. package/plugins/acryl-state/tests/cell.spec.ts +0 -71
  284. package/plugins/acryl-state/tests/react.spec.ts +0 -39
  285. 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
- })