acrylblends 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +11 -6
  2. package/docs/framework/README.md +3 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/api/presets.md +0 -3
  5. package/docs/framework/cordis/acryl-in-a-cordis-app.md +9 -0
  6. package/docs/framework/getting-started/basic.md +5 -4
  7. package/docs/framework/getting-started/desktop.md +41 -0
  8. package/docs/framework/guides/jsx-dom.md +0 -3
  9. package/docs/framework/guides/jsx.md +1 -4
  10. package/docs/framework/guides/middleware.md +1 -1
  11. package/docs/framework/helpers/cookie.md +1 -1
  12. package/docs/framework/helpers/css.md +0 -3
  13. package/docs/framework/helpers/dev.md +0 -3
  14. package/docs/framework/helpers/jwt.md +1 -1
  15. package/docs/framework/helpers/proxy.md +0 -3
  16. package/docs/framework/middleware/builtin/basic-auth.md +6 -6
  17. package/docs/framework/middleware/builtin/cache.md +0 -3
  18. package/docs/framework/middleware/builtin/combine.md +1 -1
  19. package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
  20. package/docs/framework/middleware/builtin/logger.md +1 -1
  21. package/docs/framework/middleware/builtin/mount.md +0 -3
  22. package/docs/framework/upstream.json +3 -3
  23. package/lib/accepts.d.ts +1 -1
  24. package/lib/acryl-base.d.ts +1 -1
  25. package/lib/adapter.d.ts +1 -1
  26. package/lib/{app-CmAYW1Al.d.ts → app-wFwpg-bt.d.ts} +1 -1
  27. package/lib/base-BpQy2Rk7.d.ts +783 -0
  28. package/lib/base-ddiVmiE7.js +721 -0
  29. package/lib/basic-auth.d.ts +1 -1
  30. package/lib/bearer-auth.d.ts +1 -1
  31. package/lib/body-limit.d.ts +1 -1
  32. package/lib/cache.d.ts +43 -0
  33. package/lib/cache.js +169 -0
  34. package/lib/client.d.ts +2 -2
  35. package/lib/combine.d.ts +1 -1
  36. package/lib/common-D8Z4JRCw.d.ts +46 -0
  37. package/lib/components-Ce5FRyHU.js +268 -0
  38. package/lib/components-DYZdxJIJ.js +24 -0
  39. package/lib/compress.d.ts +1 -1
  40. package/lib/conninfo.d.ts +2 -2
  41. package/lib/constants-BR9jM9BC.js +9 -0
  42. package/lib/{context-Bm7rI-oK.d.ts → context-DVQBjuns.d.ts} +1 -1
  43. package/lib/context-DxFdY_un.d.ts +250 -0
  44. package/lib/context-storage.d.ts +1 -1
  45. package/lib/{cookie-ClMaKDQf.d.ts → cookie-CCelEqky.d.ts} +1 -1
  46. package/lib/cookie.d.ts +3 -3
  47. package/lib/cors.d.ts +1 -1
  48. package/lib/csrf.d.ts +1 -1
  49. package/lib/css-D2jb7X8b.js +320 -0
  50. package/lib/css.d.ts +82 -0
  51. package/lib/css.js +134 -0
  52. package/lib/dev.d.ts +19 -0
  53. package/lib/dev.js +50 -0
  54. package/lib/etag.d.ts +1 -1
  55. package/lib/factory.d.ts +2 -2
  56. package/lib/hooks-Bq5q9IMl.js +762 -0
  57. package/lib/html-SllAe0YT.d.ts +46 -0
  58. package/lib/html.d.ts +1 -1
  59. package/lib/index-C850MeBA.d.ts +94 -0
  60. package/lib/{index-oOClm_Xk.d.ts → index-CUTfU69R.d.ts} +3 -3
  61. package/lib/{index-B-3F4wt2.d.ts → index-Cqcdluyn.d.ts} +2 -2
  62. package/lib/{index-UTBmM3GX.d.ts → index-CsvB-F9j.d.ts} +2 -2
  63. package/lib/index.d.ts +4 -4
  64. package/lib/ip-restriction.d.ts +2 -2
  65. package/lib/jsx/dom/client.d.ts +30 -0
  66. package/lib/jsx/dom/client.js +49 -0
  67. package/lib/jsx/dom/css.d.ts +84 -0
  68. package/lib/jsx/dom/css.js +2 -0
  69. package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
  70. package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
  71. package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
  72. package/lib/jsx/dom/jsx-runtime.js +2 -0
  73. package/lib/jsx/dom/server.d.ts +105 -0
  74. package/lib/jsx/dom/server.js +37 -0
  75. package/lib/jsx/dom.d.ts +5 -0
  76. package/lib/jsx/dom.js +74 -0
  77. package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
  78. package/lib/jsx/jsx-dev-runtime.js +14 -0
  79. package/lib/jsx/jsx-runtime.d.ts +10 -0
  80. package/lib/jsx/jsx-runtime.js +30 -0
  81. package/lib/jsx/streaming.d.ts +2 -0
  82. package/lib/jsx/streaming.js +2 -0
  83. package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
  84. package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
  85. package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
  86. package/lib/jsx.d.ts +3 -0
  87. package/lib/jsx.js +47 -0
  88. package/lib/jwk.d.ts +4 -4
  89. package/lib/{jwt-Dg493Tai.d.ts → jwt-BARhotDz.d.ts} +1 -1
  90. package/lib/jwt.d.ts +4 -4
  91. package/lib/language.d.ts +1 -1
  92. package/lib/linear-router-C3FPib6h.js +96 -0
  93. package/lib/logger.d.ts +1 -1
  94. package/lib/loopback-only.d.ts +1 -1
  95. package/lib/method-not-allowed.d.ts +2 -2
  96. package/lib/method-override.d.ts +2 -2
  97. package/lib/mount.d.ts +44 -0
  98. package/lib/mount.js +67 -0
  99. package/lib/node-server/conninfo.d.ts +1 -1
  100. package/lib/node-server/serve-static.d.ts +1 -1
  101. package/lib/node-server/serve-static.js +1 -1
  102. package/lib/node-server.d.ts +1 -1
  103. package/lib/node-server.js +1 -1
  104. package/lib/pattern-router-HZxjDGEQ.js +41 -0
  105. package/lib/powered-by.d.ts +1 -1
  106. package/lib/pretty-json.d.ts +1 -1
  107. package/lib/proxy.d.ts +72 -0
  108. package/lib/proxy.js +121 -0
  109. package/lib/quick.d.ts +8 -0
  110. package/lib/quick.js +17 -0
  111. package/lib/request-id.d.ts +1 -1
  112. package/lib/request.d.ts +1 -1
  113. package/lib/route.d.ts +1 -1
  114. package/lib/router/linear-router.js +1 -95
  115. package/lib/router/pattern-router.js +1 -40
  116. package/lib/schema-validator.d.ts +1 -1
  117. package/lib/secure-headers.d.ts +1 -1
  118. package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
  119. package/lib/serve-static.d.ts +1 -1
  120. package/lib/streaming.d.ts +1 -1
  121. package/lib/testing.d.ts +4 -4
  122. package/lib/timeout.d.ts +1 -1
  123. package/lib/timing.d.ts +1 -1
  124. package/lib/tiny.d.ts +8 -0
  125. package/lib/tiny.js +15 -0
  126. package/lib/trailing-slash.d.ts +1 -1
  127. package/lib/{types-CHesD1KG.d.ts → types-CQiGmsWW.d.ts} +3 -3
  128. package/lib/{types-CwrQjlaM.d.ts → types-Lqx5BxQF.d.ts} +10 -1
  129. package/lib/{types-CDO6I3B0.d.ts → types-nEL8VPDC.d.ts} +1 -1
  130. package/lib/types.d.ts +1 -1
  131. package/lib/utils/body.d.ts +1 -1
  132. package/lib/utils/cookie.d.ts +1 -1
  133. package/lib/utils/crypto.d.ts +1 -1
  134. package/lib/utils/html.d.ts +1 -45
  135. package/lib/utils/ipaddr.d.ts +1 -1
  136. package/lib/utils/jwt.d.ts +1 -1
  137. package/lib/utils/types.d.ts +2 -2
  138. package/lib/validator.d.ts +2 -2
  139. package/lib/ws.d.ts +1 -1
  140. package/package.json +69 -1
  141. package/plugins/acryl-inspect/upstream.json +1 -1
  142. package/plugins/acryl-node-server/upstream.json +1 -1
  143. package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
  144. package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
  145. package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
  146. package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
  147. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +181 -0
  148. package/plugins/acryl-router/src/core/helper/dev/index.ts +79 -0
  149. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +356 -0
  150. package/plugins/acryl-router/src/core/helper/proxy/index.ts +203 -0
  151. package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
  152. package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
  153. package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
  154. package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
  155. package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
  156. package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
  157. package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
  158. package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
  159. package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
  160. package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
  161. package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
  162. package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
  163. package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
  164. package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
  165. package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
  166. package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
  167. package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
  168. package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
  169. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
  170. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
  171. package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
  172. package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
  173. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
  174. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
  175. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
  176. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
  177. package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
  178. package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
  179. package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
  180. package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
  181. package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
  182. package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
  183. package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
  184. package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
  185. package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
  186. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
  187. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
  188. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
  189. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
  190. package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
  191. package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
  192. package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
  193. package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
  194. package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
  195. package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
  196. package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
  197. package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
  198. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +1211 -0
  199. package/plugins/acryl-router/src/core/middleware/cache/index.ts +323 -0
  200. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +334 -0
  201. package/plugins/acryl-router/src/core/middleware/mount/index.ts +108 -0
  202. package/plugins/acryl-router/src/core/preset/quick.test.ts +42 -0
  203. package/plugins/acryl-router/src/core/preset/quick.ts +24 -0
  204. package/plugins/acryl-router/src/core/preset/tiny.test.ts +42 -0
  205. package/plugins/acryl-router/src/core/preset/tiny.ts +20 -0
  206. package/plugins/acryl-router/src/core/utils/types.ts +9 -0
  207. package/plugins/acryl-router/src/helpers/index.ts +3 -0
  208. package/plugins/acryl-router/src/middleware/index.ts +2 -0
  209. package/plugins/acryl-router/src/service.ts +22 -5
  210. package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
  211. package/plugins/acryl-router/upstream.json +3 -3
  212. package/plugins/acryl-router-http/upstream.json +1 -1
  213. package/plugins/acryl-state/upstream.json +1 -1
  214. package/plugins/desktop/upstream.json +1 -1
  215. package/plugins/desktop-web/upstream.json +1 -1
  216. package/plugins/save/plugin.json +1 -1
  217. package/templates/todo/upstream.json +1 -1
@@ -0,0 +1,655 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const importIndex = () => import('./index')
4
+ const importStreaming = () => import('./streaming')
5
+ const importHooks = () => import('./hooks')
6
+ const importAsyncHooks = () => import('node:async_hooks')
7
+
8
+ /**
9
+ * These tests exercise the non-`AsyncLocalStorage` fallback path. They force
10
+ * every `AsyncLocalStorage` acquisition route to fail (and keep it disabled
11
+ * while the module lazily probes for it), then verify the React-style
12
+ * fail-safe: reading context after `await` returns the default value and never
13
+ * another concurrent request's value.
14
+ */
15
+ describe('JSX context isolation without AsyncLocalStorage', () => {
16
+ let originalGetBuiltinModule: unknown
17
+ let originalMainModule: unknown
18
+ let warnSpy: ReturnType<typeof vi.spyOn>
19
+
20
+ beforeEach(() => {
21
+ // Disable every route `loadAsyncLocalStorage()` tries. These stay in effect
22
+ // through the first render so the lazy probe caches "unavailable".
23
+ vi.resetModules()
24
+ originalGetBuiltinModule = (process as { getBuiltinModule?: unknown }).getBuiltinModule
25
+ ;(process as { getBuiltinModule?: unknown }).getBuiltinModule = undefined
26
+ originalMainModule = (process as { mainModule?: unknown }).mainModule
27
+ ;(process as { mainModule?: unknown }).mainModule = undefined
28
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
29
+ })
30
+
31
+ afterEach(() => {
32
+ ;(process as { getBuiltinModule?: unknown }).getBuiltinModule = originalGetBuiltinModule
33
+ ;(process as { mainModule?: unknown }).mainModule = originalMainModule
34
+ vi.unstubAllGlobals()
35
+ vi.resetModules()
36
+ warnSpy.mockRestore()
37
+ })
38
+
39
+ it('falls back to the default value after await and never leaks across concurrent renders', async () => {
40
+ const { createContext, useContext, jsx } = await importIndex()
41
+
42
+ const SessionContext = createContext<{ username: string; role: string }>({
43
+ username: 'nobody',
44
+ role: 'none',
45
+ })
46
+
47
+ const waits = new Map<string, Promise<void>>()
48
+ const entered = new Map<string, () => void>()
49
+
50
+ const AdminDashboard = async () => {
51
+ const initialSession = useContext(SessionContext)
52
+ entered.get(initialSession.username)?.()
53
+ await waits.get(initialSession.username)
54
+ const session = useContext(SessionContext)
55
+ if (session.role !== 'admin') {
56
+ return jsx('div', {}, `Access Denied for ${session.username}`)
57
+ }
58
+ return jsx('div', {}, `Welcome Admin ${session.username}. Secret Data: 42`)
59
+ }
60
+
61
+ const render = async (username: string, role: string): Promise<string> =>
62
+ `${await jsx(
63
+ SessionContext.Provider,
64
+ { value: { username, role } },
65
+ jsx(AdminDashboard, {})
66
+ ).toString()}`
67
+
68
+ let resolveAdmin!: () => void
69
+ let resolveAttacker!: () => void
70
+ waits.set(
71
+ 'admin',
72
+ new Promise<void>((resolve) => {
73
+ resolveAdmin = resolve
74
+ })
75
+ )
76
+ waits.set(
77
+ 'attacker',
78
+ new Promise<void>((resolve) => {
79
+ resolveAttacker = resolve
80
+ })
81
+ )
82
+ const adminEntered = new Promise<void>((resolve) => {
83
+ entered.set('admin', resolve)
84
+ })
85
+ const attackerEntered = new Promise<void>((resolve) => {
86
+ entered.set('attacker', resolve)
87
+ })
88
+
89
+ const adminReq = render('admin', 'admin')
90
+ const attackerReq = render('attacker', 'guest')
91
+
92
+ await Promise.all([adminEntered, attackerEntered])
93
+ resolveAttacker()
94
+ await Promise.resolve()
95
+ resolveAdmin()
96
+
97
+ const [adminHtml, attackerHtml] = await Promise.all([adminReq, attackerReq])
98
+
99
+ // Without AsyncLocalStorage the post-await read sees the default value —
100
+ // the same fail-safe React's request storage degrades to. The critical
101
+ // guarantee is that neither render observes the other request's value.
102
+ expect(adminHtml).toBe('<div>Access Denied for nobody</div>')
103
+ expect(attackerHtml).toBe('<div>Access Denied for nobody</div>')
104
+ })
105
+
106
+ it('warns once when a post-await read falls back to the default value', async () => {
107
+ const { createContext, useContext, jsx } = await importIndex()
108
+
109
+ const ThemeContext = createContext('default')
110
+ const Reader = async () => {
111
+ await new Promise((r) => setTimeout(r, 10))
112
+ return jsx('div', {}, useContext(ThemeContext))
113
+ }
114
+ const render = async () =>
115
+ `${await jsx(ThemeContext.Provider, { value: 'provided' }, jsx(Reader, {})).toString()}`
116
+
117
+ expect(await render()).toBe('<div>default</div>')
118
+ expect(await render()).toBe('<div>default</div>')
119
+
120
+ // Degrading to the default value is reported once, not per read.
121
+ expect(warnSpy).toHaveBeenCalledTimes(1)
122
+ expect(warnSpy.mock.calls[0][0]).toContain('AsyncLocalStorage')
123
+ })
124
+
125
+ it('clears the fallback warning window after an async render rejects', async () => {
126
+ const { createContext, useContext, jsx } = await importIndex()
127
+
128
+ const ThemeContext = createContext('default')
129
+ const Broken = async () => {
130
+ await Promise.resolve()
131
+ throw new Error('boom')
132
+ }
133
+
134
+ await expect(jsx(Broken, {}).toString()).rejects.toThrow('boom')
135
+
136
+ useContext(ThemeContext)
137
+ expect(warnSpy).not.toHaveBeenCalled()
138
+ })
139
+
140
+ it('finds AsyncLocalStorage via process.mainModule.require (Node < 20.16 CJS)', async () => {
141
+ // Re-enable only the legacy route: a CJS entrypoint exposes the main
142
+ // module's `require` even where `process.getBuiltinModule` doesn't exist.
143
+ // Context must then survive `await` with no warning.
144
+ const { AsyncLocalStorage } = await importAsyncHooks()
145
+ ;(process as { mainModule?: unknown }).mainModule = {
146
+ require: (specifier: string) =>
147
+ specifier === 'node:async_hooks' ? { AsyncLocalStorage } : {},
148
+ }
149
+
150
+ const { createContext, useContext, jsx } = await importIndex()
151
+
152
+ const ThemeContext = createContext('default')
153
+ const Reader = async () => {
154
+ await new Promise((r) => setTimeout(r, 10))
155
+ return jsx('div', {}, useContext(ThemeContext))
156
+ }
157
+
158
+ expect(
159
+ `${await jsx(ThemeContext.Provider, { value: 'provided' }, jsx(Reader, {})).toString()}`
160
+ ).toBe('<div>provided</div>')
161
+ expect(warnSpy).not.toHaveBeenCalled()
162
+ })
163
+
164
+ it('keeps a synchronous render synchronous and does not warn', async () => {
165
+ const { createContext, useContext, jsx } = await importIndex()
166
+
167
+ const ThemeContext = createContext('light')
168
+ const Consumer = () => jsx('span', {}, useContext(ThemeContext))
169
+
170
+ const result = jsx(ThemeContext.Provider, { value: 'dark' }, jsx(Consumer, {})).toString()
171
+
172
+ // A purely synchronous render must not be forced async by the serializer.
173
+ expect(result).toBe('<span>dark</span>')
174
+ expect(warnSpy).not.toHaveBeenCalled()
175
+ })
176
+
177
+ const drainStream = async (stream: unknown): Promise<string> => {
178
+ const textDecoder = new TextDecoder()
179
+ let html = ''
180
+ for await (const chunk of stream as AsyncIterable<Uint8Array>) {
181
+ html += textDecoder.decode(chunk)
182
+ }
183
+ return html
184
+ }
185
+
186
+ it('restores provider values for the subtree returned by a resumed async component', async () => {
187
+ const { createContext, useContext, jsx } = await importIndex()
188
+
189
+ const ThemeContext = createContext('default')
190
+ const Inner = () => jsx('span', {}, useContext(ThemeContext))
191
+ const Outer = async () => {
192
+ await new Promise((r) => setTimeout(r, 10))
193
+ // Unlike a useContext() call here in the async body (which degrades to
194
+ // the default), the returned subtree renders with the context values
195
+ // snapshotted at suspension and restored by the renderer.
196
+ return jsx(Inner, {})
197
+ }
198
+
199
+ expect(
200
+ `${await jsx(ThemeContext.Provider, { value: 'provided' }, jsx(Outer, {})).toString()}`
201
+ ).toBe('<span>provided</span>')
202
+ expect(warnSpy).not.toHaveBeenCalled()
203
+ })
204
+
205
+ it('resumes a Suspense-deferred subtree in the captured request store', async () => {
206
+ const { createContext, useContext, jsx } = await importIndex()
207
+ const { Suspense, renderToReadableStream } = await importStreaming()
208
+ const { use } = await importHooks()
209
+
210
+ // A `use()`-suspended child takes the deferred re-render path, which on the
211
+ // fallback path re-establishes the captured render store synchronously.
212
+ const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('done'), 10))
213
+ const ThemeContext = createContext('default')
214
+ const Inner = () => jsx('span', {}, useContext(ThemeContext))
215
+ const Content = () => {
216
+ const value = use(deferred)
217
+ return jsx(ThemeContext.Provider, { value }, jsx(Inner, {}))
218
+ }
219
+
220
+ const html = await drainStream(
221
+ renderToReadableStream(jsx(Suspense, { fallback: jsx('p', {}, 'Loading') }, jsx(Content, {})))
222
+ )
223
+
224
+ expect(html).toContain('Loading')
225
+ expect(html).toContain('<span>done</span>')
226
+ // Providers inside the resumed subtree render in the request store, not in
227
+ // the module-global fallback — so no degradation warning fires.
228
+ expect(warnSpy).not.toHaveBeenCalled()
229
+ })
230
+
231
+ it('resumes a Suspense-deferred subtree with outer provider values', async () => {
232
+ const { createContext, useContext, jsx } = await importIndex()
233
+ const { Suspense, renderToReadableStream } = await importStreaming()
234
+ const { use } = await importHooks()
235
+
236
+ const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('done'), 10))
237
+ const ThemeContext = createContext('default')
238
+ const Content = () => {
239
+ use(deferred)
240
+ return jsx('span', {}, useContext(ThemeContext))
241
+ }
242
+
243
+ const html = await drainStream(
244
+ renderToReadableStream(
245
+ jsx(
246
+ ThemeContext.Provider,
247
+ { value: 'outer' },
248
+ jsx(Suspense, { fallback: jsx('p', {}, 'Loading') }, jsx(Content, {}))
249
+ )
250
+ )
251
+ )
252
+
253
+ expect(html).toContain('Loading')
254
+ expect(html).toContain('<span>outer</span>')
255
+ expect(html).not.toContain('<span>default</span>')
256
+ expect(warnSpy).not.toHaveBeenCalled()
257
+ })
258
+
259
+ it('renders Suspense fallback with outer provider values', async () => {
260
+ const { createContext, useContext, jsx } = await importIndex()
261
+ const { Suspense, renderToReadableStream } = await importStreaming()
262
+ const { use } = await importHooks()
263
+
264
+ const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('done'), 10))
265
+ const ThemeContext = createContext('default')
266
+ const Fallback = () => jsx('p', {}, `Loading ${useContext(ThemeContext)}`)
267
+ const Content = () => {
268
+ const value = use(deferred)
269
+ return jsx('span', {}, value)
270
+ }
271
+
272
+ const html = await drainStream(
273
+ renderToReadableStream(
274
+ jsx(
275
+ ThemeContext.Provider,
276
+ { value: 'outer' },
277
+ jsx(Suspense, { fallback: jsx(Fallback, {}) }, jsx(Content, {}))
278
+ )
279
+ )
280
+ )
281
+
282
+ expect(html).toContain('<p>Loading outer</p>')
283
+ expect(html).toContain('<span>done</span>')
284
+ expect(html).not.toContain('default')
285
+ expect(warnSpy).not.toHaveBeenCalled()
286
+ })
287
+
288
+ it('restores nested provider values in a resumed Suspense subtree', async () => {
289
+ const { createContext, useContext, jsx, Fragment } = await importIndex()
290
+ const { Suspense, renderToReadableStream } = await importStreaming()
291
+ const { use } = await importHooks()
292
+
293
+ const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('done'), 10))
294
+ const ThemeContext = createContext('default')
295
+ const Inner = () => jsx('span', {}, useContext(ThemeContext))
296
+ const OuterSibling = () => jsx('em', {}, useContext(ThemeContext))
297
+ const Content = () => {
298
+ use(deferred)
299
+ return jsx(
300
+ Fragment,
301
+ {},
302
+ jsx(ThemeContext.Provider, { value: 'inner' }, jsx(Inner, {})),
303
+ jsx(OuterSibling, {})
304
+ )
305
+ }
306
+
307
+ const html = await drainStream(
308
+ renderToReadableStream(
309
+ jsx(
310
+ ThemeContext.Provider,
311
+ { value: 'outer' },
312
+ jsx(Suspense, { fallback: jsx('p', {}, 'Loading') }, jsx(Content, {}))
313
+ )
314
+ )
315
+ )
316
+
317
+ expect(html).toContain('<span>inner</span><em>outer</em>')
318
+ expect(html).not.toContain('default')
319
+ expect(warnSpy).not.toHaveBeenCalled()
320
+ })
321
+
322
+ it('keeps sibling Suspense resume contexts isolated when they resolve out of order', async () => {
323
+ const { createContext, useContext, jsx, Fragment } = await importIndex()
324
+ const { Suspense, renderToReadableStream } = await importStreaming()
325
+ const { use } = await importHooks()
326
+
327
+ const ThemeContext = createContext('default')
328
+ const waits = new Map<string, Promise<void>>()
329
+ const entered = new Map<string, () => void>()
330
+ let resolveFirst!: () => void
331
+ let resolveSecond!: () => void
332
+ waits.set(
333
+ 'first',
334
+ new Promise<void>((resolve) => {
335
+ resolveFirst = resolve
336
+ })
337
+ )
338
+ waits.set(
339
+ 'second',
340
+ new Promise<void>((resolve) => {
341
+ resolveSecond = resolve
342
+ })
343
+ )
344
+ const firstEntered = new Promise<void>((resolve) => {
345
+ entered.set('first', resolve)
346
+ })
347
+ const secondEntered = new Promise<void>((resolve) => {
348
+ entered.set('second', resolve)
349
+ })
350
+ const Content = ({ name }: { name: string }) => {
351
+ entered.get(name)?.()
352
+ use(waits.get(name)!)
353
+ return jsx('span', {}, `${name}:${useContext(ThemeContext)}`)
354
+ }
355
+
356
+ const htmlPromise = drainStream(
357
+ renderToReadableStream(
358
+ jsx(
359
+ Fragment,
360
+ {},
361
+ jsx(
362
+ ThemeContext.Provider,
363
+ { value: 'one' },
364
+ jsx(
365
+ Suspense,
366
+ { fallback: jsx('p', {}, 'Loading one') },
367
+ jsx(Content, { name: 'first' })
368
+ )
369
+ ),
370
+ jsx(
371
+ ThemeContext.Provider,
372
+ { value: 'two' },
373
+ jsx(
374
+ Suspense,
375
+ { fallback: jsx('p', {}, 'Loading two') },
376
+ jsx(Content, { name: 'second' })
377
+ )
378
+ )
379
+ )
380
+ )
381
+ )
382
+
383
+ await Promise.all([firstEntered, secondEntered])
384
+ resolveSecond()
385
+ await Promise.resolve()
386
+ resolveFirst()
387
+
388
+ const html = await htmlPromise
389
+
390
+ expect(html).toContain('<span>first:one</span>')
391
+ expect(html).toContain('<span>second:two</span>')
392
+ expect(html).not.toContain('default')
393
+ expect(warnSpy).not.toHaveBeenCalled()
394
+ })
395
+
396
+ it('resumes ErrorBoundary-deferred children in the captured request store', async () => {
397
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
398
+ const { renderToReadableStream } = await importStreaming()
399
+
400
+ const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('done'), 10))
401
+ let resolved: string | undefined
402
+ deferred.then((value) => {
403
+ resolved = value
404
+ })
405
+ const ThemeContext = createContext('default')
406
+ const Inner = () => jsx('span', {}, useContext(ThemeContext))
407
+ const Content = () => {
408
+ if (!resolved) {
409
+ // Suspend like use() does so ErrorBoundary defers the re-render.
410
+ throw deferred
411
+ }
412
+ return jsx(ThemeContext.Provider, { value: resolved }, jsx(Inner, {}))
413
+ }
414
+
415
+ const html = await drainStream(
416
+ renderToReadableStream(
417
+ jsx(ErrorBoundary, { fallback: jsx('p', {}, 'Failed') }, jsx(Content, {}))
418
+ )
419
+ )
420
+
421
+ expect(html).toContain('<span>done</span>')
422
+ expect(warnSpy).not.toHaveBeenCalled()
423
+ })
424
+
425
+ it('resumes ErrorBoundary-deferred children with outer provider values', async () => {
426
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
427
+ const { renderToReadableStream } = await importStreaming()
428
+
429
+ const deferred = new Promise<string>((resolve) => setTimeout(() => resolve('done'), 10))
430
+ let resolved: string | undefined
431
+ deferred.then((value) => {
432
+ resolved = value
433
+ })
434
+ const ThemeContext = createContext('default')
435
+ const Content = () => {
436
+ if (!resolved) {
437
+ // Suspend like use() does so ErrorBoundary defers the re-render.
438
+ throw deferred
439
+ }
440
+ return jsx('span', {}, useContext(ThemeContext))
441
+ }
442
+
443
+ const html = await drainStream(
444
+ renderToReadableStream(
445
+ jsx(
446
+ ThemeContext.Provider,
447
+ { value: 'outer' },
448
+ jsx(ErrorBoundary, { fallback: jsx('p', {}, 'Failed') }, jsx(Content, {}))
449
+ )
450
+ )
451
+ )
452
+
453
+ expect(html).toContain('<span>outer</span>')
454
+ expect(html).not.toContain('<span>default</span>')
455
+ expect(warnSpy).not.toHaveBeenCalled()
456
+ })
457
+
458
+ it('resumes ErrorBoundary fallback rendering with outer provider values', async () => {
459
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
460
+ const { renderToReadableStream } = await importStreaming()
461
+
462
+ const deferred = new Promise((_resolve, reject) =>
463
+ setTimeout(() => reject(new Error('boom')), 10)
464
+ )
465
+ const ThemeContext = createContext('default')
466
+ const Content = () => {
467
+ throw deferred
468
+ }
469
+
470
+ const html = await drainStream(
471
+ renderToReadableStream(
472
+ jsx(
473
+ ThemeContext.Provider,
474
+ { value: 'outer' },
475
+ jsx(
476
+ ErrorBoundary,
477
+ {
478
+ fallbackRender: (error: Error) =>
479
+ jsx('span', {}, `fallback:${useContext(ThemeContext)}:${error.message}`),
480
+ },
481
+ jsx(Content, {})
482
+ )
483
+ )
484
+ )
485
+ )
486
+
487
+ expect(html).toContain('<span>fallback:outer:boom</span>')
488
+ expect(html).not.toContain('fallback:default')
489
+ expect(warnSpy).not.toHaveBeenCalled()
490
+ })
491
+
492
+ it('resumes ErrorBoundary fallbackRender components with outer provider values', async () => {
493
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
494
+ const { renderToReadableStream } = await importStreaming()
495
+
496
+ const deferred = new Promise((_resolve, reject) =>
497
+ setTimeout(() => reject(new Error('boom')), 10)
498
+ )
499
+ const ThemeContext = createContext('default')
500
+ const Content = () => {
501
+ throw deferred
502
+ }
503
+ const Fallback = () => jsx('span', {}, `fallback:${useContext(ThemeContext)}`)
504
+
505
+ const html = await drainStream(
506
+ renderToReadableStream(
507
+ jsx(
508
+ ThemeContext.Provider,
509
+ { value: 'outer' },
510
+ jsx(
511
+ ErrorBoundary,
512
+ {
513
+ fallbackRender: () => jsx(Fallback, {}),
514
+ },
515
+ jsx(Content, {})
516
+ )
517
+ )
518
+ )
519
+ )
520
+
521
+ expect(html).toContain('<span>fallback:outer</span>')
522
+ expect(html).not.toContain('fallback:default')
523
+ expect(warnSpy).not.toHaveBeenCalled()
524
+ })
525
+
526
+ it('resumes ErrorBoundary fallback prop rendering with outer provider values', async () => {
527
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
528
+ const { renderToReadableStream } = await importStreaming()
529
+
530
+ const deferred = new Promise((_resolve, reject) =>
531
+ setTimeout(() => reject(new Error('boom')), 10)
532
+ )
533
+ const ThemeContext = createContext('default')
534
+ const Content = () => {
535
+ throw deferred
536
+ }
537
+ const Fallback = () => jsx('span', {}, `fallback:${useContext(ThemeContext)}`)
538
+
539
+ const html = await drainStream(
540
+ renderToReadableStream(
541
+ jsx(
542
+ ThemeContext.Provider,
543
+ { value: 'outer' },
544
+ jsx(ErrorBoundary, { fallback: jsx(Fallback, {}) }, jsx(Content, {}))
545
+ )
546
+ )
547
+ )
548
+
549
+ expect(html).toContain('<span>fallback:outer</span>')
550
+ expect(html).not.toContain('fallback:default')
551
+ expect(warnSpy).not.toHaveBeenCalled()
552
+ })
553
+
554
+ it('resumes thenable ErrorBoundary fallback prop rendering with outer provider values', async () => {
555
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
556
+ const { renderToReadableStream } = await importStreaming()
557
+
558
+ const deferred = new Promise((_resolve, reject) =>
559
+ setTimeout(() => reject(new Error('boom')), 10)
560
+ )
561
+ const ThemeContext = createContext('default')
562
+ const Content = () => {
563
+ throw deferred
564
+ }
565
+ const Fallback = () => jsx('span', {}, `fallback:${useContext(ThemeContext)}`)
566
+ const fallback = {
567
+ then(resolve: (value: unknown) => void) {
568
+ setTimeout(() => resolve(jsx(Fallback, {})), 0)
569
+ },
570
+ }
571
+
572
+ const html = await drainStream(
573
+ renderToReadableStream(
574
+ jsx(
575
+ ThemeContext.Provider,
576
+ { value: 'outer' },
577
+ jsx(ErrorBoundary, { fallback }, jsx(Content, {}))
578
+ )
579
+ )
580
+ )
581
+
582
+ expect(html).toContain('<span>fallback:outer</span>')
583
+ expect(html).not.toContain('fallback:default')
584
+ expect(warnSpy).not.toHaveBeenCalled()
585
+ })
586
+
587
+ it('resumes synchronous ErrorBoundary fallback rendering with outer provider values', async () => {
588
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
589
+
590
+ const ThemeContext = createContext('default')
591
+ const Content = () => {
592
+ throw new Error('boom')
593
+ }
594
+
595
+ expect(
596
+ `${await jsx(
597
+ ThemeContext.Provider,
598
+ { value: 'outer' },
599
+ jsx(
600
+ ErrorBoundary,
601
+ {
602
+ fallbackRender: (error: Error) =>
603
+ jsx('span', {}, `fallback:${useContext(ThemeContext)}:${error.message}`),
604
+ },
605
+ jsx(Content, {})
606
+ )
607
+ ).toString()}`
608
+ ).toBe('<span>fallback:outer:boom</span>')
609
+ expect(warnSpy).not.toHaveBeenCalled()
610
+ })
611
+
612
+ it('resumes synchronous ErrorBoundary fallbackRender components with outer provider values', async () => {
613
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
614
+
615
+ const ThemeContext = createContext('default')
616
+ const Content = () => {
617
+ throw new Error('boom')
618
+ }
619
+ const Fallback = () => jsx('span', {}, `fallback:${useContext(ThemeContext)}`)
620
+
621
+ expect(
622
+ `${await jsx(
623
+ ThemeContext.Provider,
624
+ { value: 'outer' },
625
+ jsx(
626
+ ErrorBoundary,
627
+ {
628
+ fallbackRender: () => jsx(Fallback, {}),
629
+ },
630
+ jsx(Content, {})
631
+ )
632
+ ).toString()}`
633
+ ).toBe('<span>fallback:outer</span>')
634
+ expect(warnSpy).not.toHaveBeenCalled()
635
+ })
636
+
637
+ it('resumes synchronous ErrorBoundary fallback prop rendering with outer provider values', async () => {
638
+ const { createContext, useContext, jsx, ErrorBoundary } = await importIndex()
639
+
640
+ const ThemeContext = createContext('default')
641
+ const Content = () => {
642
+ throw new Error('boom')
643
+ }
644
+ const Fallback = () => jsx('span', {}, `fallback:${useContext(ThemeContext)}`)
645
+
646
+ expect(
647
+ `${await jsx(
648
+ ThemeContext.Provider,
649
+ { value: 'outer' },
650
+ jsx(ErrorBoundary, { fallback: jsx(Fallback, {}) }, jsx(Content, {}))
651
+ ).toString()}`
652
+ ).toBe('<span>fallback:outer</span>')
653
+ expect(warnSpy).not.toHaveBeenCalled()
654
+ })
655
+ })