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,1744 +0,0 @@
1
- import DefaultExport, {
2
- ErrorBoundary,
3
- Fragment,
4
- StrictMode,
5
- createContext,
6
- createElement,
7
- jsx,
8
- memo,
9
- useContext,
10
- version,
11
- } from './index.ts'
12
- import type { Context, FC, PropsWithChildren } from './index.ts'
13
- /** @jsxImportSource ./ */
14
- /* eslint-disable @typescript-eslint/no-explicit-any */
15
- import { html, raw } from '../helper/html/index.ts'
16
- import { Acryl } from '../app.ts'
17
- import { DOM_MEMO } from './constants.ts'
18
- import { captureRenderContext } from './context.ts'
19
- import { Suspense, renderToReadableStream } from './streaming.ts'
20
-
21
- interface SiteData {
22
- title: string
23
- children?: any
24
- }
25
-
26
- describe('JSX middleware', () => {
27
- let app: Acryl
28
-
29
- beforeEach(() => {
30
- app = new Acryl()
31
- })
32
-
33
- it('Should render HTML strings', async () => {
34
- app.get('/', (c) => {
35
- return c.html(<h1>Hello</h1>)
36
- })
37
- const res = await app.request('http://localhost/')
38
- expect(res.status).toBe(200)
39
- expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
40
- expect(await res.text()).toBe('<h1>Hello</h1>')
41
- })
42
-
43
- it('Should be able to be used with html middleware', async () => {
44
- const Layout = (props: SiteData) =>
45
- html`<!DOCTYPE html>
46
- <html>
47
- <head>
48
- <title>${props.title}</title>
49
- </head>
50
- <body>
51
- ${props.children}
52
- </body>
53
- </html>`
54
-
55
- const Content = (props: { siteData: SiteData; name: string }) => (
56
- <Layout {...props.siteData}>
57
- <h1>{props.name}</h1>
58
- </Layout>
59
- )
60
-
61
- app.get('/', (c) => {
62
- const props = {
63
- name: 'JSX',
64
- siteData: {
65
- title: 'with html middleware',
66
- },
67
- }
68
- return c.html(<Content {...props} />)
69
- })
70
- const res = await app.request('http://localhost/')
71
- expect(res.status).toBe(200)
72
- expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
73
- expect(await res.text()).toBe(`<!DOCTYPE html>
74
- <html>
75
- <head>
76
- <title>with html middleware</title>
77
- </head>
78
- <body>
79
- <h1>JSX</h1>
80
- </body>
81
- </html>`)
82
- })
83
-
84
- it('Should render async component', async () => {
85
- const ChildAsyncComponent = async () => {
86
- await new Promise((resolve) => setTimeout(resolve, 10))
87
- return <span>child async component</span>
88
- }
89
-
90
- const AsyncComponent = async () => {
91
- await new Promise((resolve) => setTimeout(resolve, 10))
92
- return (
93
- <h1>
94
- Hello from async component
95
- <ChildAsyncComponent />
96
- </h1>
97
- )
98
- }
99
-
100
- app.get('/', (c) => {
101
- return c.html(<AsyncComponent />)
102
- })
103
- const res = await app.request('http://localhost/')
104
- expect(res.status).toBe(200)
105
- expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
106
- expect(await res.text()).toBe(
107
- '<h1>Hello from async component<span>child async component</span></h1>'
108
- )
109
- })
110
-
111
- it('Should render async component with "html" tagged template strings', async () => {
112
- const AsyncComponent = async () => {
113
- await new Promise((resolve) => setTimeout(resolve, 10))
114
- return <h1>Hello from async component</h1>
115
- }
116
-
117
- app.get('/', (c) => {
118
- // prettier-ignore
119
- return c.html(
120
- html`<html><body>${(<AsyncComponent />)}</body></html>`
121
- )
122
- })
123
- const res = await app.request('http://localhost/')
124
- expect(res.status).toBe(200)
125
- expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
126
- expect(await res.text()).toBe('<html><body><h1>Hello from async component</h1></body></html>')
127
- })
128
-
129
- it('Should handle async component error', async () => {
130
- const componentError = new Error('Error from async error component')
131
-
132
- const AsyncComponent = async () => {
133
- await new Promise((resolve) => setTimeout(resolve, 10))
134
- return <h1>Hello from async component</h1>
135
- }
136
- const AsyncErrorComponent = async () => {
137
- await new Promise((resolve) => setTimeout(resolve, 0))
138
- throw componentError
139
- }
140
-
141
- let raisedError: any
142
- app.onError((e, c) => {
143
- raisedError = e
144
- return c.html('<html><body><h1>Error from onError</h1></body></html>', 500)
145
- })
146
- app.get('/', (c) => {
147
- return c.html(
148
- <>
149
- <AsyncComponent />
150
- <AsyncErrorComponent />
151
- </>
152
- )
153
- })
154
-
155
- const res = await app.request('http://localhost/')
156
- expect(res.status).toBe(500)
157
- expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8')
158
- expect(await res.text()).toBe('<html><body><h1>Error from onError</h1></body></html>')
159
- expect(raisedError).toBe(componentError)
160
- })
161
- })
162
-
163
- describe('render to string', () => {
164
- it('Nested array', () => {
165
- const template = (
166
- <p>
167
- {[[['a']], [['b']]].map((item1) =>
168
- item1.map((item2) => item2.map((item3) => <span>{item3}</span>))
169
- )}
170
- </p>
171
- )
172
- expect(template.toString()).toBe('<p><span>a</span><span>b</span></p>')
173
- })
174
-
175
- it('Component returning an array', () => {
176
- const Item = ({ x }: { x: number }) => <span>{x}</span>
177
- const Items = () => [0, 1].map((x) => <Item key={x} x={x} />)
178
- const template = <Items />
179
- expect(template.toString()).toBe('<span>0</span><span>1</span>')
180
- })
181
-
182
- it('Component returning a nested array', () => {
183
- const Items = () => [['a', 'b'], [<span>c</span>], null]
184
- const template = <Items />
185
- expect(template.toString()).toBe('ab<span>c</span>')
186
- })
187
-
188
- it('Component returning an array preserves escaped string callbacks', () => {
189
- const Items = () => [
190
- raw('a', [
191
- ({ buffer }) => {
192
- if (buffer) {
193
- buffer[0] += 'b'
194
- }
195
- },
196
- ]),
197
- ]
198
- expect((<Items />).toString()).toBe('ab')
199
- })
200
-
201
- it('Component returning an array escapes strings', async () => {
202
- const SyncItems = () => ['<script>alert(1)</script>']
203
- const AsyncItems = async () => ['<script>alert(1)</script>']
204
- const expected = '&lt;script&gt;alert(1)&lt;/script&gt;'
205
-
206
- expect((<SyncItems />).toString()).toBe(expected)
207
- expect((await (<AsyncItems />).toString()).toString()).toBe(expected)
208
- })
209
-
210
- it('Component returning an array including async components', async () => {
211
- const AsyncItem = async ({ x }: { x: number }) => <span>{x}</span>
212
- const Items = () => [0, 1].map((x) => <AsyncItem key={x} x={x} />)
213
- const template = <Items />
214
- expect((await template.toString()).toString()).toBe('<span>0</span><span>1</span>')
215
- })
216
-
217
- it('Component typed as FC returning an array', () => {
218
- const Item: FC<{ x: number }> = ({ x }) => <span>{x}</span>
219
- const Items: FC = () => [0, 1].map((x) => <Item key={x} x={x} />)
220
- const template = <Items />
221
- expect(template.toString()).toBe('<span>0</span><span>1</span>')
222
- })
223
-
224
- it('Async component returning an array', async () => {
225
- const AsyncItems = async () => [<span>a</span>, <span>b</span>]
226
- const template = <AsyncItems />
227
- expect((await template.toString()).toString()).toBe('<span>a</span><span>b</span>')
228
- })
229
-
230
- it('Async component returning an array of async components', async () => {
231
- const AsyncItem = async ({ x }: { x: number }) => <span>{x}</span>
232
- const AsyncItems = async () => [<AsyncItem key={0} x={0} />, <AsyncItem key={1} x={1} />]
233
- const template = <AsyncItems />
234
- expect((await template.toString()).toString()).toBe('<span>0</span><span>1</span>')
235
- })
236
-
237
- it('Empty elements are rended without closing tag', () => {
238
- const template = <input />
239
- expect(template.toString()).toBe('<input/>')
240
- })
241
-
242
- it('Empty elements with children are rended with children and closing tag', () => {
243
- const template = <link>https://example.com</link>
244
- expect(template.toString()).toBe('<link>https://example.com</link>')
245
- })
246
-
247
- describe('programmatic tag names', () => {
248
- it('Should throw when jsx() receives a tag name that can break out of the HTML context', () => {
249
- const payloads = [
250
- '><script>alert(1)</script><x',
251
- 'div onmouseover="alert(1)" x=',
252
- 'div></div><img src=x onerror=alert(1)><div',
253
- '!--',
254
- '!DOCTYPE',
255
- '?xml',
256
- ]
257
-
258
- for (const tag of payloads) {
259
- expect(() => jsx(tag, {}, 'hello')).toThrow(`Invalid JSX tag name: ${tag}`)
260
- }
261
- })
262
-
263
- it('Should throw when createElement() receives a tag name that can break out of the HTML context', () => {
264
- expect(() => createElement('div onmouseover="alert(1)" x=', {}, 'hello')).toThrow(
265
- 'Invalid JSX tag name: div onmouseover="alert(1)" x='
266
- )
267
- })
268
-
269
- it('Should throw when jsx() receives an invalid non-primitive tag name', () => {
270
- expect(() => jsx(new String('div') as never, {}, 'hello')).toThrow(
271
- 'Invalid JSX tag name: div'
272
- )
273
- expect(() =>
274
- jsx({ toString: () => 'div onmouseover="alert(1)" x=' } as never, {}, 'hello')
275
- ).toThrow('Invalid JSX tag name: div onmouseover="alert(1)" x=')
276
- })
277
-
278
- it('Should allow compatible custom and namespaced tag names', () => {
279
- expect(jsx('custom-element', {}, 'hello').toString()).toBe(
280
- '<custom-element>hello</custom-element>'
281
- )
282
- expect(jsx('foo:bar', {}, 'hello').toString()).toBe('<foo:bar>hello</foo:bar>')
283
- expect(jsx('x.foo', {}, 'hello').toString()).toBe('<x.foo>hello</x.foo>')
284
- expect(jsx('x_bar', {}, 'hello').toString()).toBe('<x_bar>hello</x_bar>')
285
- expect(jsx('é', {}, 'hello').toString()).toBe('<é>hello</é>')
286
- })
287
- })
288
-
289
- it('Props value is null', () => {
290
- const template = <span data-hello={null}>Hello</span>
291
- expect(template.toString()).toBe('<span>Hello</span>')
292
- })
293
-
294
- it('Props value is undefined', () => {
295
- const template = <span data-hello={undefined}>Hello</span>
296
- expect(template.toString()).toBe('<span>Hello</span>')
297
- })
298
-
299
- describe('dangerouslySetInnerHTML', () => {
300
- it('Should render dangerouslySetInnerHTML', () => {
301
- const template = <span dangerouslySetInnerHTML={{ __html: '" is allowed here' }}></span>
302
- expect(template.toString()).toBe('<span>" is allowed here</span>')
303
- })
304
-
305
- it('Should get an error if both dangerouslySetInnerHTML and children are specified', () => {
306
- expect(() =>
307
- (<span dangerouslySetInnerHTML={{ __html: '" is allowed here' }}>Hello</span>).toString()
308
- ).toThrow(Error)
309
- })
310
- })
311
-
312
- // https://en.reactjs.org/docs/jsx-in-depth.html#booleans-null-and-undefined-are-ignored
313
- describe('Booleans, Null, and Undefined Are Ignored', () => {
314
- it.each([true, false, undefined, null])('%s', (item) => {
315
- expect((<span>{item}</span>).toString()).toBe('<span></span>')
316
- })
317
-
318
- it('falsy value', () => {
319
- const template = <span>{0}</span>
320
- expect(template.toString()).toBe('<span>0</span>')
321
- })
322
- })
323
-
324
- // https://en.reactjs.org/docs/jsx-in-depth.html#props-default-to-true
325
- describe('Props Default to “True”', () => {
326
- it('default prop value', () => {
327
- const template = <span data-hello>Hello</span>
328
- expect(template.toString()).toBe('<span data-hello="true">Hello</span>')
329
- })
330
- })
331
-
332
- // https://html.spec.whatwg.org/#attributes-3
333
- describe('Boolean attribute', () => {
334
- it('default prop value for checked', () => {
335
- const template = <input type='checkbox' checked />
336
- expect(template.toString()).toBe('<input type="checkbox" checked=""/>')
337
- })
338
-
339
- it('default prop value for checked={true}', () => {
340
- const template = <input type='checkbox' checked={true} />
341
- expect(template.toString()).toBe('<input type="checkbox" checked=""/>')
342
- })
343
-
344
- it('no prop for checked={false}', () => {
345
- const template = <input type='checkbox' checked={false} />
346
- expect(template.toString()).toBe('<input type="checkbox"/>')
347
- })
348
-
349
- it('default prop value for disabled', () => {
350
- const template = <input type='checkbox' disabled />
351
- expect(template.toString()).toBe('<input type="checkbox" disabled=""/>')
352
- })
353
-
354
- it('default prop value for disabled={true}', () => {
355
- const template = <input type='checkbox' disabled={true} />
356
- expect(template.toString()).toBe('<input type="checkbox" disabled=""/>')
357
- })
358
-
359
- it('no prop for disabled={false}', () => {
360
- const template = <input type='checkbox' disabled={false} />
361
- expect(template.toString()).toBe('<input type="checkbox"/>')
362
- })
363
-
364
- it('default prop value for readonly', () => {
365
- const template = <input type='checkbox' readonly />
366
- expect(template.toString()).toBe('<input type="checkbox" readonly=""/>')
367
- })
368
-
369
- it('default prop value for readonly={true}', () => {
370
- const template = <input type='checkbox' readonly={true} />
371
- expect(template.toString()).toBe('<input type="checkbox" readonly=""/>')
372
- })
373
-
374
- it('no prop for readonly={false}', () => {
375
- const template = <input type='checkbox' readonly={false} />
376
- expect(template.toString()).toBe('<input type="checkbox"/>')
377
- })
378
-
379
- it('default prop value for selected', () => {
380
- const template = (
381
- <option value='test' selected>
382
- Test
383
- </option>
384
- )
385
- expect(template.toString()).toBe('<option value="test" selected="">Test</option>')
386
- })
387
-
388
- it('default prop value for selected={true}', () => {
389
- const template = (
390
- <option value='test' selected={true}>
391
- Test
392
- </option>
393
- )
394
- expect(template.toString()).toBe('<option value="test" selected="">Test</option>')
395
- })
396
-
397
- it('no prop for selected={false}', () => {
398
- const template = (
399
- <option value='test' selected={false}>
400
- Test
401
- </option>
402
- )
403
- expect(template.toString()).toBe('<option value="test">Test</option>')
404
- })
405
-
406
- it('default prop value for multiple select', () => {
407
- const template = (
408
- <select multiple>
409
- <option>test</option>
410
- </select>
411
- )
412
- expect(template.toString()).toBe('<select multiple=""><option>test</option></select>')
413
- })
414
-
415
- it('default prop value for select multiple={true}', () => {
416
- const template = (
417
- <select multiple={true}>
418
- <option>test</option>
419
- </select>
420
- )
421
- expect(template.toString()).toBe('<select multiple=""><option>test</option></select>')
422
- })
423
-
424
- it('no prop for select multiple={false}', () => {
425
- const template = (
426
- <select multiple={false}>
427
- <option>test</option>
428
- </select>
429
- )
430
- expect(template.toString()).toBe('<select><option>test</option></select>')
431
- })
432
-
433
- it('should render "false" value properly for other non-defined keys', () => {
434
- const template = <input type='checkbox' testkey={false} />
435
- expect(template.toString()).toBe('<input type="checkbox" testkey="false"/>')
436
- })
437
-
438
- it('should support attributes for elements other than input', () => {
439
- const template = (
440
- <video controls autoplay>
441
- <source src='movie.mp4' type='video/mp4' />
442
- </video>
443
- )
444
- expect(template.toString()).toBe(
445
- '<video controls="" autoplay=""><source src="movie.mp4" type="video/mp4"/></video>'
446
- )
447
- })
448
- })
449
-
450
- describe('download attribute', () => {
451
- it('<a download={true}></a> should be rendered as <a download=""></a>', () => {
452
- const template = <a download={true}></a>
453
- expect(template.toString()).toBe('<a download=""></a>')
454
- })
455
-
456
- it('<a download={false}></a> should be rendered as <a></a>', () => {
457
- const template = <a download={false}></a>
458
- expect(template.toString()).toBe('<a></a>')
459
- })
460
-
461
- it('<a download></a> should be rendered as <a download=""></a>', () => {
462
- const template = <a download></a>
463
- expect(template.toString()).toBe('<a download=""></a>')
464
- })
465
-
466
- it('<a download="test"></a> should be rendered as <a download="test"></a>', () => {
467
- const template = <a download='test'></a>
468
- expect(template.toString()).toBe('<a download="test"></a>')
469
- })
470
- })
471
-
472
- describe('Function', () => {
473
- it('should be ignored used in on* props', () => {
474
- const onClick = () => {}
475
- const template = <button onClick={onClick}>Click</button>
476
- expect(template.toString()).toBe('<button>Click</button>')
477
- })
478
-
479
- it('should be ignored used in ref props', () => {
480
- const ref = () => {}
481
- const template = <div ref={ref}>Content</div>
482
- expect(template.toString()).toBe('<div>Content</div>')
483
- })
484
-
485
- it('should raise an error if used in other props', () => {
486
- const onClick = () => {}
487
- const template = <button data-handler={onClick}>Click</button>
488
- expect(() => template.toString()).toThrow(Error)
489
- })
490
- })
491
-
492
- // https://en.reactjs.org/docs/jsx-in-depth.html#functions-as-children
493
- describe('Functions as Children', () => {
494
- it('Function', () => {
495
- function Repeat(props: any) {
496
- const items = []
497
- for (let i = 0; i < props.numTimes; i++) {
498
- items.push((props.children as Function)(i))
499
- }
500
- return <div>{items}</div>
501
- }
502
-
503
- function ListOfTenThings() {
504
- return (
505
- <Repeat numTimes={10}>
506
- {(index: string) => <div key={index}>This is item {index} in the list</div>}
507
- </Repeat>
508
- )
509
- }
510
-
511
- const template = <ListOfTenThings />
512
- expect(template.toString()).toBe(
513
- '<div><div>This is item 0 in the list</div><div>This is item 1 in the list</div><div>This is item 2 in the list</div><div>This is item 3 in the list</div><div>This is item 4 in the list</div><div>This is item 5 in the list</div><div>This is item 6 in the list</div><div>This is item 7 in the list</div><div>This is item 8 in the list</div><div>This is item 9 in the list</div></div>'
514
- )
515
- })
516
- })
517
-
518
- describe('FC', () => {
519
- it('Should define the type correctly', () => {
520
- const Layout: FC<PropsWithChildren<{ title: string }>> = (props) => {
521
- return (
522
- <html>
523
- <head>
524
- <title>{props.title}</title>
525
- </head>
526
- <body>{props.children}</body>
527
- </html>
528
- )
529
- }
530
-
531
- const Top = (
532
- <Layout title='Home page'>
533
- <h1>Acryl</h1>
534
- <p>Acryl is great</p>
535
- </Layout>
536
- )
537
-
538
- expect(Top.toString()).toBe(
539
- '<html><head><title>Home page</title></head><body><h1>Acryl</h1><p>Acryl is great</p></body></html>'
540
- )
541
- })
542
-
543
- describe('Booleans, Null, and Undefined Are Ignored', () => {
544
- it.each([true, false, undefined, null])('%s', (item) => {
545
- const Component: FC = (() => {
546
- return item
547
- }) as FC
548
- const template = <Component />
549
- expect(template.toString()).toBe('')
550
- })
551
-
552
- it('falsy value', () => {
553
- const Component: FC = (() => {
554
- return 0
555
- }) as unknown as FC
556
- const template = <Component />
557
- expect(template.toString()).toBe('0')
558
- })
559
- })
560
- })
561
-
562
- describe('style attribute', () => {
563
- it('should convert the object to strings', () => {
564
- const template = (
565
- <h1
566
- style={{
567
- color: 'red',
568
- fontSize: 'small',
569
- fontFamily: 'Menlo, Consolas, "DejaVu Sans Mono", monospace',
570
- }}
571
- >
572
- Hello
573
- </h1>
574
- )
575
- expect(template.toString()).toBe(
576
- '<h1 style="color:red;font-size:small;font-family:Menlo, Consolas, &quot;DejaVu Sans Mono&quot;, monospace">Hello</h1>'
577
- )
578
- })
579
- it('should not convert the strings', () => {
580
- const template = <h1 style='color:red;font-size:small'>Hello</h1>
581
- expect(template.toString()).toBe('<h1 style="color:red;font-size:small">Hello</h1>')
582
- })
583
- it('should render variable without any name conversion', () => {
584
- const template = <h1 style={{ '--myVar': 1 }}>Hello</h1>
585
- expect(template.toString()).toBe('<h1 style="--myVar:1px">Hello</h1>')
586
- })
587
-
588
- describe('CSS injection prevention', () => {
589
- it('should drop style values containing ";" to prevent CSS injection', () => {
590
- const userInput =
591
- 'transparent;background:url(https://attacker.example/a.png);position:fixed;top:0'
592
- const template = <div style={{ color: userInput }} />
593
- const out = template.toString() as string
594
- expect(out).not.toContain('background:url')
595
- expect(out).not.toContain('position:fixed')
596
- expect(out).not.toContain('color:')
597
- expect(out).toBe('<div style=""></div>')
598
- })
599
-
600
- it('should drop only the unsafe property and keep other safe properties', () => {
601
- const template = <div style={{ color: 'red;background:blue', backgroundColor: 'white' }} />
602
- const out = template.toString() as string
603
- expect(out).toBe('<div style="background-color:white"></div>')
604
- expect(out).not.toContain('background:blue')
605
- })
606
-
607
- it('should drop style values that try to hide declaration separators in CSS comments', () => {
608
- const template = (
609
- <div
610
- style={{
611
- color: 'red/*(*/;background:blue;position:fixed;top:0',
612
- backgroundColor: 'white',
613
- }}
614
- />
615
- )
616
- const out = template.toString() as string
617
- expect(out).toBe('<div style="background-color:white"></div>')
618
- expect(out).not.toContain('position:fixed')
619
- })
620
-
621
- it('should drop style values that try to expose declarations through CSS curly blocks', () => {
622
- const template = (
623
- <div
624
- style={{
625
- color: 'red{;background:blue}',
626
- backgroundColor: 'white',
627
- }}
628
- />
629
- )
630
- const out = template.toString() as string
631
- expect(out).toBe('<div style="background-color:white"></div>')
632
- expect(out).not.toContain('background:blue')
633
- })
634
-
635
- it('should drop unterminated style values that can swallow following declarations', () => {
636
- const template = <div style={{ color: 'red/*', display: 'none' }} />
637
- const out = template.toString() as string
638
- expect(out).toBe('<div style="display:none"></div>')
639
- expect(out).not.toContain('color:')
640
- })
641
-
642
- it('should drop style property names that can inject declarations', () => {
643
- const template = (
644
- <div
645
- style={{
646
- 'color;background-image': 'url(https://attacker.example/a.png)',
647
- backgroundColor: 'white',
648
- }}
649
- />
650
- )
651
- const out = template.toString() as string
652
- expect(out).toBe('<div style="background-color:white"></div>')
653
- expect(out).not.toContain('background-image')
654
- })
655
-
656
- it('should still HTML-escape safe style values', () => {
657
- const template = <h1 style={{ fontFamily: '"DejaVu Sans Mono", monospace' }}>Hello</h1>
658
- expect(template.toString()).toBe(
659
- '<h1 style="font-family:&quot;DejaVu Sans Mono&quot;, monospace">Hello</h1>'
660
- )
661
- })
662
-
663
- it('should keep semicolons inside quoted style values', () => {
664
- const template = <h1 style={{ fontFamily: '"a;b", sans-serif' }}>Hello</h1>
665
- expect(template.toString()).toBe(
666
- '<h1 style="font-family:&quot;a;b&quot;, sans-serif">Hello</h1>'
667
- )
668
- })
669
-
670
- it('should drop quoted style values that use newlines to expose declaration separators', () => {
671
- const template = (
672
- <div
673
- style={{
674
- fontFamily: '"\n;background:url(https://attacker.example/a.png)',
675
- backgroundColor: 'white',
676
- }}
677
- />
678
- )
679
- expect(template.toString()).toBe('<div style="background-color:white"></div>')
680
- })
681
-
682
- it('should keep numeric style values working', () => {
683
- const template = <div style={{ fontSize: 10, lineHeight: 1.5 }} />
684
- expect(template.toString()).toBe('<div style="font-size:10px;line-height:1.5"></div>')
685
- })
686
-
687
- it('should not throw when style values contain ";"', () => {
688
- expect(() => (<div style={{ color: 'red;evil:1' }} />).toString()).not.toThrow()
689
- })
690
- })
691
- })
692
-
693
- describe('HtmlEscaped in props', () => {
694
- it('should not be double-escaped', () => {
695
- const escapedString = html`${'<html-escaped-string>'}`
696
- const template = <span data-text={escapedString}>Hello</span>
697
- expect(template.toString()).toBe('<span data-text="&lt;html-escaped-string&gt;">Hello</span>')
698
- })
699
- })
700
-
701
- describe('XSS prevention for attribute keys', () => {
702
- it('Should skip attribute keys containing double quotes', () => {
703
- const props: Record<string, string> = { ['" onfocus="alert(1)']: 'x' }
704
- const template = <div {...props}>Hello</div>
705
- expect(template.toString()).toBe('<div>Hello</div>')
706
- })
707
-
708
- it('Should skip attribute keys containing >', () => {
709
- const props: Record<string, string> = { ['"><script>alert(1)</script><x x="']: 'x' }
710
- const template = <div {...props}>Hello</div>
711
- expect(template.toString()).toBe('<div>Hello</div>')
712
- })
713
-
714
- it('Should skip attribute keys containing <', () => {
715
- const props: Record<string, string> = { ['foo<bar']: 'x' }
716
- const template = <div {...props}>Hello</div>
717
- expect(template.toString()).toBe('<div>Hello</div>')
718
- })
719
-
720
- it('Should skip attribute keys containing backslashes', () => {
721
- const props: Record<string, string> = { ['foo\\bar']: 'x' }
722
- const template = <div {...props}>Hello</div>
723
- expect(template.toString()).toBe('<div>Hello</div>')
724
- })
725
-
726
- it('Should skip attribute keys containing backticks', () => {
727
- const props: Record<string, string> = { ['foo`bar']: 'x' }
728
- const template = <div {...props}>Hello</div>
729
- expect(template.toString()).toBe('<div>Hello</div>')
730
- })
731
-
732
- it('Should skip attribute keys containing spaces', () => {
733
- const props: Record<string, string> = { ['foo bar']: 'x' }
734
- const template = <div {...props}>Hello</div>
735
- expect(template.toString()).toBe('<div>Hello</div>')
736
- })
737
-
738
- it('Should still render valid attributes alongside invalid ones', () => {
739
- const props: Record<string, string> = {
740
- id: 'safe',
741
- ['" onfocus="alert(1)']: 'x',
742
- class: 'test',
743
- }
744
- const template = <div {...props}>Hello</div>
745
- expect(template.toString()).toBe('<div id="safe" class="test">Hello</div>')
746
- })
747
-
748
- it('Should skip invalid attribute keys before style serialization', () => {
749
- const template = <div {...{ ['" onfocus="alert(1)']: { fontSize: 10 } }}>Hello</div>
750
- expect(template.toString()).toBe('<div>Hello</div>')
751
- })
752
-
753
- it('Should skip invalid attribute keys before function prop validation', () => {
754
- const template = <div {...{ ['" onfocus="alert(1)']: () => 'x' }}>Hello</div>
755
- expect(template.toString()).toBe('<div>Hello</div>')
756
- })
757
-
758
- it('Should skip invalid attribute keys before dangerouslySetInnerHTML handling', () => {
759
- const template = (
760
- <div {...{ ['" onfocus="alert(1)']: { __html: '<strong>Injected</strong>' } }}>Hello</div>
761
- )
762
- expect(template.toString()).toBe('<div>Hello</div>')
763
- })
764
-
765
- it('Should skip invalid attribute keys with Promise values', async () => {
766
- const template = <div {...{ ['" onfocus="alert(1)']: Promise.resolve('x') }}>Hello</div>
767
- expect(await template.toString()).toBe('<div>Hello</div>')
768
- })
769
- })
770
-
771
- describe('head', () => {
772
- it('Simple head elements should be rendered as is', () => {
773
- const template = (
774
- <head>
775
- <title>Acryl!</title>
776
- <meta name='description' content='A description' />
777
- <script src='script.js'></script>
778
- </head>
779
- )
780
- expect(template.toString()).toBe(
781
- '<head><title>Acryl!</title><meta name="description" content="A description"/><script src="script.js"></script></head>'
782
- )
783
- })
784
- })
785
- })
786
-
787
- describe('className', () => {
788
- it('should convert to class attribute for intrinsic elements', () => {
789
- const template = <h1 className='h1'>Hello</h1>
790
- expect(template.toString()).toBe('<h1 class="h1">Hello</h1>')
791
- })
792
-
793
- it('should convert to class attribute for custom elements', () => {
794
- const template = <custom-element className='h1'>Hello</custom-element>
795
- expect(template.toString()).toBe('<custom-element class="h1">Hello</custom-element>')
796
- })
797
-
798
- it('should not convert to class attribute for custom components', () => {
799
- const CustomComponent: FC<{ className: string }> = ({ className }) => (
800
- <div data-class-name={className}>Hello</div>
801
- )
802
- const template = <CustomComponent className='h1' />
803
- expect(template.toString()).toBe('<div data-class-name="h1">Hello</div>')
804
- })
805
- })
806
-
807
- describe('memo', () => {
808
- it('does not reuse the result of a previous render', () => {
809
- let counter = 0
810
- const Header = memo(() => <title>Test Site {counter}</title>)
811
- const Body = () => <span>{counter}</span>
812
-
813
- let template = (
814
- <html>
815
- <head>
816
- <Header />
817
- </head>
818
- <body>
819
- <Body />
820
- </body>
821
- </html>
822
- )
823
- expect(template.toString()).toBe(
824
- '<html><head><title>Test Site 0</title></head><body><span>0</span></body></html>'
825
- )
826
-
827
- counter++
828
- template = (
829
- <html>
830
- <head>
831
- <Header />
832
- </head>
833
- <body>
834
- <Body />
835
- </body>
836
- </html>
837
- )
838
- expect(template.toString()).toBe(
839
- '<html><head><title>Test Site 1</title></head><body><span>1</span></body></html>'
840
- )
841
- })
842
-
843
- it('does not carry a context value into a later render', () => {
844
- const NameContext = createContext('anonymous')
845
- const Panel = memo(() => <p>{useContext(NameContext)}</p>)
846
- const render = (name: string) =>
847
- (
848
- <NameContext.Provider value={name}>
849
- <Panel />
850
- </NameContext.Provider>
851
- ).toString()
852
-
853
- expect(render('alice')).toBe('<p>alice</p>')
854
- expect(render('bob')).toBe('<p>bob</p>')
855
- })
856
-
857
- it('props are updated', () => {
858
- const Body = memo(({ counter }: { counter: number }) => <span>{counter}</span>)
859
-
860
- let template = <Body counter={0} />
861
- expect(template.toString()).toBe('<span>0</span>')
862
-
863
- template = <Body counter={1} />
864
- expect(template.toString()).toBe('<span>1</span>')
865
- })
866
-
867
- it('custom propsAreEqual is handed to the DOM renderer', () => {
868
- const propsAreEqual = (_: { counter: number }, nextProps: { counter: number }) =>
869
- nextProps.counter === 0
870
- const Body = memo(({ counter }: { counter: number }) => <span>{counter}</span>, propsAreEqual)
871
-
872
- expect((Body as any)[DOM_MEMO]).toBe(propsAreEqual)
873
-
874
- expect((<Body counter={0} />).toString()).toBe('<span>0</span>')
875
- expect((<Body counter={1} />).toString()).toBe('<span>1</span>')
876
- })
877
- })
878
-
879
- describe('Fragment', () => {
880
- it('Should render children', () => {
881
- const template = (
882
- <>
883
- <p>1</p>
884
- <p>2</p>
885
- </>
886
- )
887
- expect(template.toString()).toBe('<p>1</p><p>2</p>')
888
- })
889
-
890
- it('Should render children - with `Fragment`', () => {
891
- const template = (
892
- <Fragment>
893
- <p>1</p>
894
- <p>2</p>
895
- </Fragment>
896
- )
897
- expect(template.toString()).toBe('<p>1</p><p>2</p>')
898
- })
899
-
900
- it('Should render a child', () => {
901
- const template = (
902
- <>
903
- <p>1</p>
904
- </>
905
- )
906
- expect(template.toString()).toBe('<p>1</p>')
907
- })
908
-
909
- it('Should render a child - with `Fragment`', () => {
910
- const template = (
911
- <Fragment>
912
- <p>1</p>
913
- </Fragment>
914
- )
915
- expect(template.toString()).toBe('<p>1</p>')
916
- })
917
-
918
- it('Should render nothing for empty Fragment', () => {
919
- const template = <></>
920
- expect(template.toString()).toBe('')
921
- })
922
-
923
- it('Should render nothing for undefined', () => {
924
- const template = <>{undefined}</>
925
- expect(template.toString()).toBe('')
926
- })
927
- })
928
-
929
- describe('StrictMode', () => {
930
- it('Should render children', () => {
931
- const template = (
932
- <StrictMode>
933
- <p>1</p>
934
- <p>2</p>
935
- </StrictMode>
936
- )
937
- expect(template.toString()).toBe('<p>1</p><p>2</p>')
938
- })
939
- })
940
-
941
- describe('SVG', () => {
942
- it('simple', () => {
943
- const template = (
944
- <svg>
945
- <circle cx='50' cy='50' r='40' stroke='black' stroke-width='3' fill='red' />
946
- </svg>
947
- )
948
- expect(template.toString()).toBe(
949
- '<svg><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"></circle></svg>'
950
- )
951
- })
952
-
953
- it('title element', () => {
954
- const template = (
955
- <>
956
- <head>
957
- <title>Document Title</title>
958
- </head>
959
- <svg>
960
- <title>SVG Title</title>
961
- </svg>
962
- </>
963
- )
964
- expect(template.toString()).toBe(
965
- '<head><title>Document Title</title></head><svg><title>SVG Title</title></svg>'
966
- )
967
- })
968
-
969
- it('should skip invalid attribute keys in SVG while preserving valid ones', () => {
970
- const template = (
971
- <svg>
972
- <g {...{ ['" onload="alert(1)']: 'x', viewBox: '0 0 10 10' }} />
973
- </svg>
974
- )
975
- expect(template.toString()).toBe('<svg><g viewBox="0 0 10 10"></g></svg>')
976
- })
977
-
978
- it('should normalize kebab-case attributes on the <svg> root element', () => {
979
- const template = <svg {...{ strokeWidth: '1.5', strokeLinecap: 'round' }} viewBox='0 0 16 16' />
980
- expect(template.toString()).toBe(
981
- '<svg stroke-width="1.5" stroke-linecap="round" viewBox="0 0 16 16"></svg>'
982
- )
983
- })
984
-
985
- describe('attribute', () => {
986
- describe('camelCase', () => {
987
- test.each`
988
- key
989
- ${'attributeName'}
990
- ${'baseFrequency'}
991
- ${'calcMode'}
992
- ${'clipPathUnits'}
993
- ${'diffuseConstant'}
994
- ${'edgeMode'}
995
- ${'filterUnits'}
996
- ${'gradientTransform'}
997
- ${'gradientUnits'}
998
- ${'kernelMatrix'}
999
- ${'kernelUnitLength'}
1000
- ${'keyPoints'}
1001
- ${'keySplines'}
1002
- ${'keyTimes'}
1003
- ${'lengthAdjust'}
1004
- ${'limitingConeAngle'}
1005
- ${'markerHeight'}
1006
- ${'markerUnits'}
1007
- ${'markerWidth'}
1008
- ${'maskContentUnits'}
1009
- ${'maskUnits'}
1010
- ${'numOctaves'}
1011
- ${'pathLength'}
1012
- ${'patternContentUnits'}
1013
- ${'patternTransform'}
1014
- ${'patternUnits'}
1015
- ${'pointsAtX'}
1016
- ${'pointsAtY'}
1017
- ${'pointsAtZ'}
1018
- ${'preserveAlpha'}
1019
- ${'preserveAspectRatio'}
1020
- ${'primitiveUnits'}
1021
- ${'refX'}
1022
- ${'refY'}
1023
- ${'repeatCount'}
1024
- ${'repeatDur'}
1025
- ${'specularConstant'}
1026
- ${'specularExponent'}
1027
- ${'spreadMethod'}
1028
- ${'startOffset'}
1029
- ${'stdDeviation'}
1030
- ${'stitchTiles'}
1031
- ${'surfaceScale'}
1032
- ${'crossorigin'}
1033
- ${'systemLanguage'}
1034
- ${'tableValues'}
1035
- ${'targetX'}
1036
- ${'targetY'}
1037
- ${'textLength'}
1038
- ${'viewBox'}
1039
- ${'xChannelSelector'}
1040
- ${'yChannelSelector'}
1041
- `('$key', ({ key }) => {
1042
- const template = (
1043
- <svg>
1044
- <g {...{ [key]: 'test' }} />
1045
- </svg>
1046
- )
1047
- expect(template.toString()).toBe(`<svg><g ${key}="test"></g></svg>`)
1048
- })
1049
- })
1050
-
1051
- describe('kebab-case', () => {
1052
- test.each`
1053
- key
1054
- ${'alignmentBaseline'}
1055
- ${'baselineShift'}
1056
- ${'clipPath'}
1057
- ${'clipRule'}
1058
- ${'colorInterpolation'}
1059
- ${'colorInterpolationFilters'}
1060
- ${'dominantBaseline'}
1061
- ${'fillOpacity'}
1062
- ${'fillRule'}
1063
- ${'floodColor'}
1064
- ${'floodOpacity'}
1065
- ${'fontFamily'}
1066
- ${'fontSize'}
1067
- ${'fontSizeAdjust'}
1068
- ${'fontStretch'}
1069
- ${'fontStyle'}
1070
- ${'fontVariant'}
1071
- ${'fontWeight'}
1072
- ${'imageRendering'}
1073
- ${'letterSpacing'}
1074
- ${'lightingColor'}
1075
- ${'markerEnd'}
1076
- ${'markerMid'}
1077
- ${'markerStart'}
1078
- ${'overlinePosition'}
1079
- ${'overlineThickness'}
1080
- ${'paintOrder'}
1081
- ${'pointerEvents'}
1082
- ${'shapeRendering'}
1083
- ${'stopColor'}
1084
- ${'stopOpacity'}
1085
- ${'strikethroughPosition'}
1086
- ${'strikethroughThickness'}
1087
- ${'strokeDasharray'}
1088
- ${'strokeDashoffset'}
1089
- ${'strokeLinecap'}
1090
- ${'strokeLinejoin'}
1091
- ${'strokeMiterlimit'}
1092
- ${'strokeOpacity'}
1093
- ${'strokeWidth'}
1094
- ${'textAnchor'}
1095
- ${'textDecoration'}
1096
- ${'textRendering'}
1097
- ${'transformOrigin'}
1098
- ${'underlinePosition'}
1099
- ${'underlineThickness'}
1100
- ${'unicodeBidi'}
1101
- ${'vectorEffect'}
1102
- ${'wordSpacing'}
1103
- ${'writingMode'}
1104
- `('$key', ({ key }) => {
1105
- const template = (
1106
- <svg>
1107
- <g {...{ [key]: 'test' }} />
1108
- </svg>
1109
- )
1110
- expect(template.toString()).toBe(
1111
- `<svg><g ${key.replace(/([A-Z])/g, '-$1').toLowerCase()}="test"></g></svg>`
1112
- )
1113
- })
1114
- })
1115
-
1116
- describe('data-*', () => {
1117
- test.each`
1118
- key
1119
- ${'data-foo'}
1120
- ${'data-foo-bar'}
1121
- ${'data-fooBar'}
1122
- `('$key', ({ key }) => {
1123
- const template = (
1124
- <svg>
1125
- <g {...{ [key]: 'test' }} />
1126
- </svg>
1127
- )
1128
- expect(template.toString()).toBe(`<svg><g ${key}="test"></g></svg>`)
1129
- })
1130
- })
1131
- })
1132
- })
1133
-
1134
- describe('Context', () => {
1135
- let ThemeContext: Context<string>
1136
- let Consumer: FC
1137
- let ErrorConsumer: FC
1138
- let AsyncConsumer: FC
1139
- let AsyncErrorConsumer: FC
1140
- beforeAll(() => {
1141
- ThemeContext = createContext('light')
1142
- Consumer = () => {
1143
- const theme = useContext(ThemeContext)
1144
- return <span>{theme}</span>
1145
- }
1146
- ErrorConsumer = () => {
1147
- throw new Error('ErrorConsumer')
1148
- }
1149
- AsyncConsumer = async () => {
1150
- const theme = useContext(ThemeContext)
1151
- return <span>{theme}</span>
1152
- }
1153
- AsyncErrorConsumer = async () => {
1154
- throw new Error('AsyncErrorConsumer')
1155
- }
1156
- })
1157
-
1158
- describe('with .Provider', () => {
1159
- it('has a child', () => {
1160
- const template = (
1161
- <ThemeContext.Provider value='dark'>
1162
- <Consumer />
1163
- </ThemeContext.Provider>
1164
- )
1165
- expect(template.toString()).toBe('<span>dark</span>')
1166
- })
1167
-
1168
- it('has children', () => {
1169
- const template = (
1170
- <ThemeContext.Provider value='dark'>
1171
- <div>
1172
- <Consumer />!
1173
- </div>
1174
- <div>
1175
- <Consumer />!
1176
- </div>
1177
- </ThemeContext.Provider>
1178
- )
1179
- expect(template.toString()).toBe('<div><span>dark</span>!</div><div><span>dark</span>!</div>')
1180
- })
1181
-
1182
- it('escapes a single string child', async () => {
1183
- const template = (
1184
- <ThemeContext.Provider value='dark'>{'<img src=x onerror=alert(1)>'}</ThemeContext.Provider>
1185
- )
1186
-
1187
- expect(String(await template.toString())).toBe('&lt;img src=x onerror=alert(1)&gt;')
1188
- })
1189
-
1190
- it('escapes a string returned by an asynchronous child', async () => {
1191
- const Async = async () => '<img src=x onerror=alert(1)>' as any
1192
- const template = (
1193
- <ThemeContext.Provider value='dark'>
1194
- <Async />
1195
- </ThemeContext.Provider>
1196
- )
1197
-
1198
- expect(String(await template.toString())).toBe('&lt;img src=x onerror=alert(1)&gt;')
1199
- })
1200
-
1201
- it('preserves explicitly trusted content', async () => {
1202
- const template = (
1203
- <ThemeContext.Provider value='dark'>
1204
- {raw('<strong>trusted</strong>')}
1205
- </ThemeContext.Provider>
1206
- )
1207
-
1208
- expect(await template.toString()).toBe('<strong>trusted</strong>')
1209
- })
1210
-
1211
- it('escapes untrusted object stringification', async () => {
1212
- const object = { toString: () => '<img src=x onerror=alert(1)>' }
1213
- const asyncObject = { toString: async () => '<img src=x onerror=alert(1)>' }
1214
- const rawObject = { toString: () => raw('<img src=x onerror=alert(1)>') }
1215
-
1216
- for (const child of [object, asyncObject, rawObject]) {
1217
- const template = (
1218
- <ThemeContext.Provider value='dark'>{child as never}</ThemeContext.Provider>
1219
- )
1220
- expect(String(await template.toString())).toBe('&lt;img src=x onerror=alert(1)&gt;')
1221
- }
1222
- })
1223
-
1224
- it('continues to omit unsupported objects in child arrays', async () => {
1225
- const object = { toString: () => '<img src=x onerror=alert(1)>' }
1226
- const template = (
1227
- <ThemeContext.Provider value='dark'>{[object as never, 'safe']}</ThemeContext.Provider>
1228
- )
1229
-
1230
- expect(await template.toString()).toBe('safe')
1231
- })
1232
-
1233
- it('keeps a direct Promise child unchanged', async () => {
1234
- const template = (
1235
- <ThemeContext.Provider value='dark'>{Promise.resolve('resolved')}</ThemeContext.Provider>
1236
- )
1237
-
1238
- expect(await template.toString()).toBe('[object Promise]')
1239
- })
1240
-
1241
- it('nested', () => {
1242
- const template = (
1243
- <ThemeContext.Provider value='dark'>
1244
- <Consumer />
1245
- <ThemeContext.Provider value='black'>
1246
- <Consumer />
1247
- </ThemeContext.Provider>
1248
- <Consumer />
1249
- </ThemeContext.Provider>
1250
- )
1251
- expect(template.toString()).toBe('<span>dark</span><span>black</span><span>dark</span>')
1252
- })
1253
-
1254
- it('should reset context by error', () => {
1255
- const template = (
1256
- <ThemeContext.Provider value='dark'>
1257
- <ErrorConsumer />
1258
- </ThemeContext.Provider>
1259
- )
1260
- expect(() => template.toString()).toThrow()
1261
-
1262
- const nextRequest = <Consumer />
1263
- expect(nextRequest.toString()).toBe('<span>light</span>')
1264
- })
1265
- })
1266
-
1267
- describe('<Context> as a provider ', () => {
1268
- it('has a child', () => {
1269
- const template = (
1270
- <ThemeContext value='dark'>
1271
- <Consumer />
1272
- </ThemeContext>
1273
- )
1274
- expect(template.toString()).toBe('<span>dark</span>')
1275
- })
1276
- })
1277
-
1278
- it('default value', () => {
1279
- const template = <Consumer />
1280
- expect(template.toString()).toBe('<span>light</span>')
1281
- })
1282
-
1283
- describe('with Suspence', () => {
1284
- const RedTheme = () => (
1285
- <ThemeContext.Provider value='red'>
1286
- <Consumer />
1287
- </ThemeContext.Provider>
1288
- )
1289
-
1290
- it('Should preserve context in sync component', async () => {
1291
- const template = (
1292
- <ThemeContext.Provider value='dark'>
1293
- <Suspense fallback={<RedTheme />}>
1294
- <Consumer />
1295
- <ThemeContext.Provider value='black'>
1296
- <Consumer />
1297
- </ThemeContext.Provider>
1298
- </Suspense>
1299
- </ThemeContext.Provider>
1300
- )
1301
- const stream = renderToReadableStream(template)
1302
-
1303
- const chunks = []
1304
- const textDecoder = new TextDecoder()
1305
- for await (const chunk of stream as any) {
1306
- chunks.push(textDecoder.decode(chunk))
1307
- }
1308
-
1309
- expect(chunks).toEqual(['<span>dark</span><span>black</span>'])
1310
- })
1311
-
1312
- it('Should preserve context in async component', async () => {
1313
- const template = (
1314
- <ThemeContext.Provider value='dark'>
1315
- <Suspense fallback={<RedTheme />}>
1316
- <Consumer />
1317
- <ThemeContext.Provider value='black'>
1318
- <AsyncConsumer />
1319
- </ThemeContext.Provider>
1320
- </Suspense>
1321
- </ThemeContext.Provider>
1322
- )
1323
- const stream = renderToReadableStream(template)
1324
-
1325
- const chunks = []
1326
- const textDecoder = new TextDecoder()
1327
- for await (const chunk of stream as any) {
1328
- chunks.push(textDecoder.decode(chunk))
1329
- }
1330
-
1331
- expect(chunks).toEqual([
1332
- '<template id="H:0"></template><span>red</span><!--/$-->',
1333
- `<template data-acryl-target="H:0"><span>dark</span><span>black</span></template><script>
1334
- ((d,c,n) => {
1335
- c=d.currentScript.previousSibling
1336
- d=d.getElementById('H:0')
1337
- if(!d)return
1338
- do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
1339
- d.replaceWith(c.content)
1340
- })(document)
1341
- </script>`,
1342
- ])
1343
- })
1344
- })
1345
-
1346
- describe('async component', () => {
1347
- const ParentAsyncConsumer = async () => {
1348
- const theme = useContext(ThemeContext)
1349
- return (
1350
- <div>
1351
- <span>{theme}</span>
1352
- <AsyncConsumer />
1353
- </div>
1354
- )
1355
- }
1356
-
1357
- const ParentAsyncErrorConsumer = async () => {
1358
- const theme = useContext(ThemeContext)
1359
- return (
1360
- <div>
1361
- <span>{theme}</span>
1362
- <AsyncErrorConsumer />
1363
- </div>
1364
- )
1365
- }
1366
-
1367
- it('simple', async () => {
1368
- const template = (
1369
- <ThemeContext.Provider value='dark'>
1370
- <AsyncConsumer />
1371
- </ThemeContext.Provider>
1372
- )
1373
- expect((await template.toString()).toString()).toBe('<span>dark</span>')
1374
- })
1375
-
1376
- it('returning an array', async () => {
1377
- const ArrayConsumer = async () => {
1378
- await new Promise((resolve) => setTimeout(resolve, 10))
1379
- return [<span>{useContext(ThemeContext)}</span>, <span>x</span>]
1380
- }
1381
- const template = (
1382
- <ThemeContext.Provider value='dark'>
1383
- <ArrayConsumer />
1384
- </ThemeContext.Provider>
1385
- )
1386
- expect((await template.toString()).toString()).toBe('<span>dark</span><span>x</span>')
1387
- })
1388
-
1389
- it('isolates a shared async result between providers', async () => {
1390
- const sharedResult = Promise.resolve(<Consumer />)
1391
- const SharedConsumer = () => sharedResult
1392
- const [dark, black] = await Promise.all([
1393
- (
1394
- <ThemeContext.Provider value='dark'>
1395
- <SharedConsumer />
1396
- </ThemeContext.Provider>
1397
- ).toString(),
1398
- (
1399
- <ThemeContext.Provider value='black'>
1400
- <SharedConsumer />
1401
- </ThemeContext.Provider>
1402
- ).toString(),
1403
- ])
1404
- expect(dark.toString()).toBe('<span>dark</span>')
1405
- expect(black.toString()).toBe('<span>black</span>')
1406
- })
1407
-
1408
- it('nested', async () => {
1409
- const template = (
1410
- <ThemeContext.Provider value='dark'>
1411
- <ParentAsyncConsumer />
1412
- </ThemeContext.Provider>
1413
- )
1414
- expect((await template.toString()).toString()).toBe(
1415
- '<div><span>dark</span><span>dark</span></div>'
1416
- )
1417
- })
1418
-
1419
- it('should reset context by error', async () => {
1420
- const template = (
1421
- <ThemeContext.Provider value='dark'>
1422
- <ParentAsyncErrorConsumer />
1423
- </ThemeContext.Provider>
1424
- )
1425
- await expect(async () => (await template.toString()).toString()).rejects.toThrow()
1426
-
1427
- const nextRequest = <Consumer />
1428
- expect(nextRequest.toString()).toBe('<span>light</span>')
1429
- })
1430
-
1431
- it('should keep captured context until a resumed async callback settles', async () => {
1432
- let resolveReader!: () => void
1433
- const readerWait = new Promise<void>((resolve) => {
1434
- resolveReader = resolve
1435
- })
1436
- let markReaderEntered!: () => void
1437
- const readerEntered = new Promise<void>((resolve) => {
1438
- markReaderEntered = resolve
1439
- })
1440
-
1441
- const ResumedReader = () => {
1442
- const resume = captureRenderContext()
1443
- return resume(async () => {
1444
- markReaderEntered()
1445
- await readerWait
1446
- return <>{useContext(ThemeContext)}</>
1447
- })
1448
- }
1449
-
1450
- const htmlPromise = (
1451
- <ThemeContext.Provider value='dark'>
1452
- <ResumedReader />
1453
- </ThemeContext.Provider>
1454
- ).toString()
1455
-
1456
- await readerEntered
1457
- resolveReader()
1458
-
1459
- expect(`${await htmlPromise}`).toBe('dark')
1460
- })
1461
-
1462
- it('should pop captured context when a resumed callback throws', async () => {
1463
- const ThrowThenRead = ({ resume }: { resume: ReturnType<typeof captureRenderContext> }) => {
1464
- try {
1465
- resume(() => {
1466
- throw new Error('boom')
1467
- })
1468
- } catch {}
1469
- return <>{useContext(ThemeContext)}</>
1470
- }
1471
- const CaptureOuter = () => {
1472
- const resume = captureRenderContext()
1473
- return (
1474
- <ThemeContext.Provider value='inner'>
1475
- <ThrowThenRead resume={resume} />
1476
- </ThemeContext.Provider>
1477
- )
1478
- }
1479
-
1480
- expect(
1481
- `${await (
1482
- <ThemeContext.Provider value='outer'>
1483
- <CaptureOuter />
1484
- </ThemeContext.Provider>
1485
- ).toString()}`
1486
- ).toBe('inner')
1487
- })
1488
-
1489
- it('should pop captured context when a resumed async callback rejects', async () => {
1490
- const RejectThenRead = async ({
1491
- resume,
1492
- }: {
1493
- resume: ReturnType<typeof captureRenderContext>
1494
- }) => {
1495
- try {
1496
- await resume(async () => {
1497
- await Promise.resolve()
1498
- throw new Error('boom')
1499
- })
1500
- } catch {}
1501
- return <>{useContext(ThemeContext)}</>
1502
- }
1503
- const CaptureOuter = () => {
1504
- const resume = captureRenderContext()
1505
- return (
1506
- <ThemeContext.Provider value='inner'>
1507
- <RejectThenRead resume={resume} />
1508
- </ThemeContext.Provider>
1509
- )
1510
- }
1511
-
1512
- expect(
1513
- `${await (
1514
- <ThemeContext.Provider value='outer'>
1515
- <CaptureOuter />
1516
- </ThemeContext.Provider>
1517
- ).toString()}`
1518
- ).toBe('inner')
1519
- })
1520
-
1521
- it('should isolate async component context between concurrent requests', async () => {
1522
- const app = new Acryl()
1523
- const SessionContext = createContext({ username: 'guest', role: 'guest' })
1524
- const waits = new Map<string, Promise<void>>()
1525
- const entered = new Map<string, () => void>()
1526
-
1527
- const AdminDashboard = async ({ wait }: { wait: Promise<void> }) => {
1528
- entered.get(useContext(SessionContext).username)?.()
1529
- await wait
1530
- const session = useContext(SessionContext)
1531
-
1532
- if (session.role !== 'admin') {
1533
- return <div>Access Denied for {session.username}</div>
1534
- }
1535
-
1536
- return <div>Welcome Admin {session.username}. Secret Data: 42</div>
1537
- }
1538
-
1539
- app.get('/', (c) => {
1540
- const session = {
1541
- username: c.req.query('user') || 'guest',
1542
- role: c.req.query('role') || 'guest',
1543
- }
1544
- const wait = waits.get(session.username) || Promise.resolve()
1545
-
1546
- return c.html(
1547
- <SessionContext.Provider value={session}>
1548
- <AdminDashboard wait={wait} />
1549
- </SessionContext.Provider>
1550
- )
1551
- })
1552
-
1553
- let resolveAdmin!: () => void
1554
- let resolveAttacker!: () => void
1555
- waits.set(
1556
- 'admin',
1557
- new Promise<void>((resolve) => {
1558
- resolveAdmin = resolve
1559
- })
1560
- )
1561
- waits.set(
1562
- 'attacker',
1563
- new Promise<void>((resolve) => {
1564
- resolveAttacker = resolve
1565
- })
1566
- )
1567
- const adminEntered = new Promise<void>((resolve) => {
1568
- entered.set('admin', resolve)
1569
- })
1570
- const attackerEntered = new Promise<void>((resolve) => {
1571
- entered.set('attacker', resolve)
1572
- })
1573
-
1574
- const adminReq = app.fetch(new Request('http://localhost/?user=admin&role=admin'))
1575
- const attackerReq = app.fetch(new Request('http://localhost/?user=attacker&role=guest'))
1576
-
1577
- await Promise.all([adminEntered, attackerEntered])
1578
- resolveAttacker()
1579
- await Promise.resolve()
1580
- resolveAdmin()
1581
-
1582
- const [adminRes, attackerRes] = await Promise.all([adminReq, attackerReq])
1583
- const adminHtml = await adminRes.text()
1584
- const attackerHtml = await attackerRes.text()
1585
-
1586
- // Each request must render with exactly its own provided value: no
1587
- // cross-request leak, and no degradation to the context default.
1588
- expect(adminHtml).toBe('<div>Welcome Admin admin. Secret Data: 42</div>')
1589
- expect(attackerHtml).toBe('<div>Access Denied for attacker</div>')
1590
- })
1591
- })
1592
-
1593
- describe('async with html helper', () => {
1594
- it('should preserve context when using await before html helper', async () => {
1595
- // Context was being popped before async children resolved
1596
- const AsyncParentWithHtml = async (props: { children?: any }) => {
1597
- await new Promise((r) => setTimeout(r, 10))
1598
- return html`<div>${props.children}</div>`
1599
- }
1600
-
1601
- const template = (
1602
- <ThemeContext.Provider value='dark'>
1603
- <AsyncParentWithHtml>
1604
- <Consumer />
1605
- </AsyncParentWithHtml>
1606
- </ThemeContext.Provider>
1607
- )
1608
- expect((await template.toString()).toString()).toBe('<div><span>dark</span></div>')
1609
- })
1610
-
1611
- it('should preserve nested context when using await before html helper', async () => {
1612
- const AsyncParentWithHtml = async (props: { children?: any }) => {
1613
- await new Promise((r) => setTimeout(r, 10))
1614
- return html`<div>${props.children}</div>`
1615
- }
1616
-
1617
- const template = (
1618
- <ThemeContext.Provider value='dark'>
1619
- <AsyncParentWithHtml>
1620
- <ThemeContext.Provider value='black'>
1621
- <Consumer />
1622
- </ThemeContext.Provider>
1623
- </AsyncParentWithHtml>
1624
- </ThemeContext.Provider>
1625
- )
1626
- expect((await template.toString()).toString()).toBe('<div><span>black</span></div>')
1627
- })
1628
- })
1629
- })
1630
-
1631
- describe('ErrorBoundary', () => {
1632
- it('awaits thenable fallback values', async () => {
1633
- const Broken = () => {
1634
- throw new Error('boom')
1635
- }
1636
- const fallback = {
1637
- then(resolve: (value: unknown) => void) {
1638
- resolve(<span>Recovered</span>)
1639
- },
1640
- }
1641
-
1642
- expect(
1643
- `${await (
1644
- <ErrorBoundary fallback={fallback as any}>
1645
- <Broken />
1646
- </ErrorBoundary>
1647
- ).toString()}`
1648
- ).toBe('<span>Recovered</span>')
1649
- })
1650
-
1651
- it('prefers an empty fallback over fallbackRender', async () => {
1652
- const Broken = () => {
1653
- throw new Error('boom')
1654
- }
1655
- let fallbackRenderCalled = false
1656
-
1657
- expect(
1658
- `${await (
1659
- <ErrorBoundary
1660
- fallback=''
1661
- fallbackRender={() => {
1662
- fallbackRenderCalled = true
1663
- return <span>fallbackRender</span>
1664
- }}
1665
- >
1666
- <Broken />
1667
- </ErrorBoundary>
1668
- ).toString()}`
1669
- ).toBe('')
1670
- expect(fallbackRenderCalled).toBe(false)
1671
- })
1672
-
1673
- it('renders async rejection fallback with the captured context while streaming', async () => {
1674
- const ThemeContext = createContext('default')
1675
- const Fallback = () => <span>{useContext(ThemeContext)}</span>
1676
- const Broken = async () => {
1677
- await Promise.resolve()
1678
- throw new Error('boom')
1679
- }
1680
-
1681
- const stream = renderToReadableStream(
1682
- <ThemeContext.Provider value='outer'>
1683
- <ErrorBoundary fallbackRender={() => <Fallback />}>
1684
- <Broken />
1685
- </ErrorBoundary>
1686
- </ThemeContext.Provider>
1687
- )
1688
- const decoder = new TextDecoder()
1689
- let html = ''
1690
- for await (const chunk of stream) {
1691
- html += decoder.decode(chunk)
1692
- }
1693
-
1694
- expect(html).toContain('<span>outer</span>')
1695
- expect(html).not.toContain('<span>default</span>')
1696
- })
1697
- })
1698
-
1699
- describe('version', () => {
1700
- it('should be defined with semantic versioning format', () => {
1701
- expect(version).toMatch(/^\d+\.\d+\.\d+-acryl-jsx$/)
1702
- })
1703
- })
1704
-
1705
- describe('default export', () => {
1706
- ;[
1707
- 'version',
1708
- 'memo',
1709
- 'Fragment',
1710
- 'isValidElement',
1711
- 'createElement',
1712
- 'cloneElement',
1713
- 'ErrorBoundary',
1714
- 'createContext',
1715
- 'useContext',
1716
- 'useState',
1717
- 'useEffect',
1718
- 'useRef',
1719
- 'useCallback',
1720
- 'useReducer',
1721
- 'useDebugValue',
1722
- 'createRef',
1723
- 'forwardRef',
1724
- 'useImperativeHandle',
1725
- 'useSyncExternalStore',
1726
- 'use',
1727
- 'startTransition',
1728
- 'useTransition',
1729
- 'useDeferredValue',
1730
- 'startViewTransition',
1731
- 'useViewTransition',
1732
- 'useMemo',
1733
- 'useLayoutEffect',
1734
- 'useInsertionEffect',
1735
- 'useActionState',
1736
- 'useOptimistic',
1737
- 'Suspense',
1738
- 'StrictMode',
1739
- ].forEach((key) => {
1740
- it(key, () => {
1741
- expect((DefaultExport as any)[key]).toBeDefined()
1742
- })
1743
- })
1744
- })