acrylblends 0.6.2 → 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 (316) hide show
  1. package/README.md +5 -1
  2. package/docs/framework/README.md +3 -2
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/cordis/acryl-in-a-cordis-app.md +6 -0
  5. package/docs/framework/getting-started/basic.md +1 -0
  6. package/docs/framework/getting-started/desktop.md +41 -0
  7. package/docs/framework/guides/jsx-dom.md +0 -3
  8. package/docs/framework/guides/jsx.md +24 -3
  9. package/docs/framework/helpers/css.md +0 -3
  10. package/docs/framework/helpers/ssg.md +3 -6
  11. package/docs/framework/middleware/builtin/jsx-renderer.md +0 -3
  12. package/docs/framework/upstream.json +3 -3
  13. package/lib/accepts.d.ts +1 -1
  14. package/lib/acryl-base.d.ts +1 -1
  15. package/lib/adapter.d.ts +1 -1
  16. package/lib/{app-BdrVHBlJ.d.ts → app-D8riatvv.d.ts} +1 -1
  17. package/lib/base-BpQy2Rk7.d.ts +783 -0
  18. package/lib/base-ddiVmiE7.js +721 -0
  19. package/lib/basic-auth.d.ts +1 -1
  20. package/lib/bearer-auth.d.ts +1 -1
  21. package/lib/body-limit.d.ts +1 -1
  22. package/lib/cache.d.ts +1 -1
  23. package/lib/cache.js +4 -4
  24. package/lib/{client-BHTr318g.js → client-DDLXwhBo.js} +2 -125
  25. package/lib/client.d.ts +2 -2
  26. package/lib/client.js +2 -1
  27. package/lib/combine.d.ts +1 -1
  28. package/lib/common-D8Z4JRCw.d.ts +46 -0
  29. package/lib/components-0o0CBYNv.js +268 -0
  30. package/lib/components-DdI7-ns7.js +24 -0
  31. package/lib/compress.d.ts +1 -1
  32. package/lib/conninfo.d.ts +2 -2
  33. package/lib/constants-BR9jM9BC.js +9 -0
  34. package/lib/{context-B5Y1lLha.d.ts → context-DS3fYyG-.d.ts} +2 -2
  35. package/lib/context-DxFdY_un.d.ts +250 -0
  36. package/lib/context-storage.d.ts +1 -1
  37. package/lib/{cookie-D9_nQbsM.d.ts → cookie-CCelEqky.d.ts} +1 -1
  38. package/lib/cookie.d.ts +3 -3
  39. package/lib/cors.d.ts +1 -1
  40. package/lib/csrf.d.ts +1 -1
  41. package/lib/css-D2jb7X8b.js +320 -0
  42. package/lib/css.d.ts +82 -0
  43. package/lib/css.js +134 -0
  44. package/lib/dev.d.ts +2 -2
  45. package/lib/etag.d.ts +1 -1
  46. package/lib/factory.d.ts +2 -2
  47. package/lib/hooks-BMw6BAyP.js +762 -0
  48. package/lib/html-SllAe0YT.d.ts +46 -0
  49. package/lib/html.d.ts +1 -1
  50. package/lib/{index-m12zcmph.d.ts → index-Bb7YJKCq.d.ts} +3 -3
  51. package/lib/{index-Kqou8Fjh.d.ts → index-BvFh1Gfx.d.ts} +2 -2
  52. package/lib/index-C850MeBA.d.ts +94 -0
  53. package/lib/{index-DtEoak1i.d.ts → index-CTy4N7ng.d.ts} +2 -2
  54. package/lib/index.d.ts +4 -4
  55. package/lib/ip-restriction.d.ts +2 -2
  56. package/lib/jsx/dom/client.d.ts +30 -0
  57. package/lib/jsx/dom/client.js +49 -0
  58. package/lib/jsx/dom/css.d.ts +84 -0
  59. package/lib/jsx/dom/css.js +2 -0
  60. package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
  61. package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
  62. package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
  63. package/lib/jsx/dom/jsx-runtime.js +2 -0
  64. package/lib/jsx/dom/server.d.ts +105 -0
  65. package/lib/jsx/dom/server.js +37 -0
  66. package/lib/jsx/dom.d.ts +5 -0
  67. package/lib/jsx/dom.js +74 -0
  68. package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
  69. package/lib/jsx/jsx-dev-runtime.js +14 -0
  70. package/lib/jsx/jsx-runtime.d.ts +10 -0
  71. package/lib/jsx/jsx-runtime.js +30 -0
  72. package/lib/jsx/streaming.d.ts +2 -0
  73. package/lib/jsx/streaming.js +2 -0
  74. package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
  75. package/lib/jsx-dev-runtime-D18Y35TI.js +354 -0
  76. package/lib/jsx-renderer.d.ts +75 -0
  77. package/lib/jsx-renderer.js +99 -0
  78. package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
  79. package/lib/jsx.d.ts +3 -0
  80. package/lib/jsx.js +47 -0
  81. package/lib/jwk.d.ts +4 -4
  82. package/lib/{jwt-BA65QhnJ.d.ts → jwt-BARhotDz.d.ts} +1 -1
  83. package/lib/jwt.d.ts +4 -4
  84. package/lib/language.d.ts +1 -1
  85. package/lib/logger.d.ts +1 -1
  86. package/lib/loopback-only.d.ts +1 -1
  87. package/lib/method-not-allowed.d.ts +2 -2
  88. package/lib/method-override.d.ts +2 -2
  89. package/lib/mount.d.ts +1 -1
  90. package/lib/node-server/conninfo.d.ts +1 -1
  91. package/lib/node-server/serve-static.d.ts +1 -1
  92. package/lib/node-server/serve-static.js +1 -1
  93. package/lib/node-server.d.ts +1 -1
  94. package/lib/node-server.js +1 -1
  95. package/lib/powered-by.d.ts +1 -1
  96. package/lib/pretty-json.d.ts +1 -1
  97. package/lib/proxy.d.ts +1 -1
  98. package/lib/quick.d.ts +1 -1
  99. package/lib/request-id.d.ts +1 -1
  100. package/lib/request.d.ts +1 -1
  101. package/lib/route.d.ts +1 -1
  102. package/lib/schema-validator.d.ts +1 -1
  103. package/lib/secure-headers.d.ts +1 -1
  104. package/lib/{serve-static-Dw_m6KxR.js → serve-static-B14MF1Va.js} +2 -0
  105. package/lib/serve-static.d.ts +1 -1
  106. package/lib/ssg-BwjcA-Ge.js +389 -0
  107. package/lib/ssg.d.ts +156 -0
  108. package/lib/ssg.js +2 -0
  109. package/lib/streaming.d.ts +1 -1
  110. package/lib/testing.d.ts +4 -4
  111. package/lib/testing.js +1 -1
  112. package/lib/timeout.d.ts +1 -1
  113. package/lib/timing.d.ts +1 -1
  114. package/lib/tiny.d.ts +1 -1
  115. package/lib/trailing-slash.d.ts +1 -1
  116. package/lib/{types-ZM9gIiKh.d.ts → types-Dv_nV2cv.d.ts} +1 -1
  117. package/lib/{types-D9DEXMjE.d.ts → types-Lqx5BxQF.d.ts} +8 -1
  118. package/lib/{types-Bd29gh2o.d.ts → types-zNxyEpG2.d.ts} +3 -3
  119. package/lib/types.d.ts +1 -1
  120. package/lib/utils/body.d.ts +1 -1
  121. package/lib/utils/cookie.d.ts +1 -1
  122. package/lib/utils/crypto.d.ts +1 -1
  123. package/lib/utils/html.d.ts +1 -45
  124. package/lib/utils/ipaddr.d.ts +1 -1
  125. package/lib/utils/jwt.d.ts +1 -1
  126. package/lib/utils/types.d.ts +2 -2
  127. package/lib/utils-Cx61GFMC.js +125 -0
  128. package/lib/validator.d.ts +2 -2
  129. package/lib/ws.d.ts +1 -1
  130. package/package.json +53 -1
  131. package/plugins/acryl-inspect/upstream.json +1 -1
  132. package/plugins/acryl-node-server/upstream.json +1 -1
  133. package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
  134. package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
  135. package/plugins/acryl-router/src/core/helper/ssg/index.ts +14 -0
  136. package/plugins/acryl-router/src/core/helper/ssg/middleware.ts +83 -0
  137. package/plugins/acryl-router/src/core/helper/ssg/plugins.ts +72 -0
  138. package/plugins/acryl-router/src/core/helper/ssg/ssg.ts +471 -0
  139. package/plugins/acryl-router/src/core/helper/ssg/utils.ts +118 -0
  140. package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
  141. package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
  142. package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
  143. package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
  144. package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
  145. package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
  146. package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
  147. package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
  148. package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
  149. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
  150. package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
  151. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
  152. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
  153. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
  154. package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
  155. package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
  156. package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
  157. package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
  158. package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
  159. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
  160. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
  161. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
  162. package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
  163. package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
  164. package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
  165. package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
  166. package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
  167. package/plugins/acryl-router/src/core/middleware/cache/index.ts +4 -4
  168. package/plugins/acryl-router/src/core/middleware/jsx-renderer/index.ts +162 -0
  169. package/plugins/acryl-router/src/core/utils/types.ts +6 -0
  170. package/plugins/acryl-router/src/helpers/index.ts +2 -0
  171. package/plugins/acryl-router/src/middleware/index.ts +1 -0
  172. package/plugins/acryl-router/upstream.json +3 -3
  173. package/plugins/acryl-router-http/upstream.json +1 -1
  174. package/plugins/acryl-state/upstream.json +1 -1
  175. package/plugins/desktop/upstream.json +1 -1
  176. package/plugins/desktop-web/upstream.json +1 -1
  177. package/plugins/extensions/resolve.ts +4 -0
  178. package/plugins/save/plugin.json +1 -1
  179. package/shared/AGENTS.core.md +4 -0
  180. package/templates/agent/tsconfig.json +7 -1
  181. package/templates/todo/upstream.json +1 -1
  182. package/plugins/acryl-inspect/tests/report.spec.ts +0 -69
  183. package/plugins/acryl-inspect/tests/service.spec.ts +0 -121
  184. package/plugins/acryl-node-server/tests/adapter/app.ts +0 -76
  185. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +0 -1
  186. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  187. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +0 -1
  188. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +0 -1
  189. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +0 -1
  190. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +0 -5
  191. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +0 -1
  192. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  193. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +0 -1
  194. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +0 -1
  195. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +0 -1
  196. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +0 -1
  197. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +0 -1
  198. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +0 -1
  199. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +0 -1
  200. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +0 -1
  201. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +0 -34
  202. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +0 -248
  203. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +0 -457
  204. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +0 -23
  205. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +0 -27
  206. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +0 -252
  207. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +0 -190
  208. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +0 -727
  209. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +0 -189
  210. package/plugins/acryl-node-server/tests/adapter/request.test.ts +0 -1230
  211. package/plugins/acryl-node-server/tests/adapter/response.test.ts +0 -285
  212. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +0 -849
  213. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +0 -790
  214. package/plugins/acryl-node-server/tests/adapter/server.test.ts +0 -1312
  215. package/plugins/acryl-node-server/tests/adapter/setup.ts +0 -16
  216. package/plugins/acryl-node-server/tests/adapter/url.test.ts +0 -76
  217. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +0 -23
  218. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +0 -75
  219. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +0 -235
  220. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +0 -260
  221. package/plugins/acryl-node-server/tests/server.spec.ts +0 -141
  222. package/plugins/acryl-router/src/core/app.test.ts +0 -3833
  223. package/plugins/acryl-router/src/core/client/client.test.ts +0 -2313
  224. package/plugins/acryl-router/src/core/client/types.test.ts +0 -149
  225. package/plugins/acryl-router/src/core/client/utils.test.ts +0 -405
  226. package/plugins/acryl-router/src/core/compose.test.ts +0 -751
  227. package/plugins/acryl-router/src/core/context.test.ts +0 -629
  228. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +0 -344
  229. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +0 -55
  230. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +0 -510
  231. package/plugins/acryl-router/src/core/helper/dev/index.test.ts +0 -181
  232. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +0 -434
  233. package/plugins/acryl-router/src/core/helper/html/index.test.ts +0 -78
  234. package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +0 -356
  235. package/plugins/acryl-router/src/core/helper/route/index.test.ts +0 -259
  236. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +0 -128
  237. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +0 -34
  238. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +0 -42
  239. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +0 -124
  240. package/plugins/acryl-router/src/core/http-exception.test.ts +0 -45
  241. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +0 -434
  242. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +0 -1018
  243. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +0 -273
  244. package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +0 -1211
  245. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +0 -355
  246. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +0 -535
  247. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +0 -43
  248. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +0 -532
  249. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +0 -534
  250. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +0 -21
  251. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +0 -503
  252. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +0 -217
  253. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +0 -1135
  254. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +0 -48
  255. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +0 -802
  256. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +0 -552
  257. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +0 -204
  258. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +0 -292
  259. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +0 -221
  260. package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +0 -334
  261. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +0 -37
  262. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +0 -218
  263. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +0 -168
  264. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +0 -671
  265. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +0 -450
  266. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +0 -50
  267. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +0 -70
  268. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +0 -310
  269. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +0 -381
  270. package/plugins/acryl-router/src/core/preset/quick.test.ts +0 -42
  271. package/plugins/acryl-router/src/core/preset/tiny.test.ts +0 -42
  272. package/plugins/acryl-router/src/core/request.test.ts +0 -751
  273. package/plugins/acryl-router/src/core/router/common.case.test.ts +0 -1132
  274. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +0 -80
  275. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +0 -46
  276. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +0 -154
  277. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +0 -265
  278. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +0 -13
  279. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +0 -896
  280. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +0 -8
  281. package/plugins/acryl-router/src/core/types.test.ts +0 -4134
  282. package/plugins/acryl-router/src/core/utils/accept.test.ts +0 -299
  283. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +0 -56
  284. package/plugins/acryl-router/src/core/utils/body.test.ts +0 -496
  285. package/plugins/acryl-router/src/core/utils/buffer.test.ts +0 -147
  286. package/plugins/acryl-router/src/core/utils/color.test.ts +0 -23
  287. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +0 -69
  288. package/plugins/acryl-router/src/core/utils/cookie.test.ts +0 -460
  289. package/plugins/acryl-router/src/core/utils/crypto.test.ts +0 -43
  290. package/plugins/acryl-router/src/core/utils/encode.test.ts +0 -61
  291. package/plugins/acryl-router/src/core/utils/filepath.test.ts +0 -78
  292. package/plugins/acryl-router/src/core/utils/html.test.ts +0 -16
  293. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +0 -149
  294. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +0 -86
  295. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +0 -1788
  296. package/plugins/acryl-router/src/core/utils/mime.test.ts +0 -64
  297. package/plugins/acryl-router/src/core/utils/stream.test.ts +0 -212
  298. package/plugins/acryl-router/src/core/utils/types.test.ts +0 -295
  299. package/plugins/acryl-router/src/core/utils/url.test.ts +0 -384
  300. package/plugins/acryl-router/src/core/validator/utils.test.ts +0 -82
  301. package/plugins/acryl-router/src/core/validator/validator.test.ts +0 -1488
  302. package/plugins/acryl-router/tests/docs-examples.spec.ts +0 -152
  303. package/plugins/acryl-router/tests/live-install.spec.ts +0 -64
  304. package/plugins/acryl-router/tests/loader.spec.ts +0 -72
  305. package/plugins/acryl-router/tests/loopback-only.spec.ts +0 -80
  306. package/plugins/acryl-router/tests/no-dom/consumer.ts +0 -11
  307. package/plugins/acryl-router/tests/no-dom/tsconfig.json +0 -13
  308. package/plugins/acryl-router/tests/service.spec.ts +0 -426
  309. package/plugins/acryl-router/tests/shadow-state.spec.ts +0 -79
  310. package/plugins/acryl-router-http/tests/adapter.spec.ts +0 -174
  311. package/plugins/acryl-router-http/tests/stream.spec.ts +0 -133
  312. package/plugins/acryl-router-http/tests/surfaces.spec.ts +0 -101
  313. package/plugins/acryl-state/src/react.ts +0 -11
  314. package/plugins/acryl-state/tests/cell.spec.ts +0 -71
  315. package/plugins/acryl-state/tests/react.spec.ts +0 -39
  316. package/plugins/acryl-state/tests/service.spec.ts +0 -132
@@ -1,426 +0,0 @@
1
- /**
2
- * The router as a Cordis service: mounts owned by the plugin that made them, and the lifecycle mechanics the repo protocol requires -
3
- * PENDING while the provider is missing, reactivation when it appears, provider replacement without duplicate registrations, disposal,
4
- * repeated mount and reload, in-flight abort, and streams that end with the router or with the plugin that serves them. Routing itself is covered by the core's own suite.
5
- */
6
-
7
- import { Context, type FiberState } from '@deepseek-ai/cordis'
8
- import { afterEach, describe, expect, it } from 'vitest'
9
- import { streamSSE } from '../src/helpers/index.ts'
10
- import { apply, Acryl, Config, connectionOf, HTTPException, name, type Router } from '../src/index.ts'
11
-
12
- const PENDING = 0 as FiberState.PENDING
13
- const ACTIVE = 2 as FiberState.ACTIVE
14
- const DISPOSED = 4 as FiberState.DISPOSED
15
-
16
- const contexts: Context[] = []
17
- const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
18
-
19
- function root(): Context {
20
- const ctx = new Context()
21
- contexts.push(ctx)
22
- return ctx
23
- }
24
-
25
- const provider = { name, apply }
26
- const config: Config = { trailingSlash: 'strict', disposeGraceMs: 2000 }
27
-
28
- async function mountRouter(): Promise<{ ctx: Context, router: Router }> {
29
- const ctx = root()
30
- ctx.plugin(provider, config)
31
- await settle()
32
- return { ctx, router: ctx.router }
33
- }
34
-
35
- afterEach(async () => {
36
- while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
37
- })
38
-
39
- /** A consumer plugin that needs `router` and mounts one route, the way the Market does. */
40
- function consumer(path: string, label: string) {
41
- return {
42
- name: `consumer${label}`,
43
- inject: ['router'],
44
- apply(ctx: Context): void {
45
- ctx.router.route('/', new Acryl().get(path, c => c.text(label)))
46
- },
47
- }
48
- }
49
-
50
- const get = (router: Router, path: string): Promise<Response> => router.fetch(new Request(`http://acryl.test${path}`))
51
-
52
- describe('acryl-router mounts', () => {
53
- it('serves a mounted app under its path, with the app\'s own routes, middleware and error handler', async () => {
54
- const { router } = await mountRouter()
55
- const api = new Acryl()
56
- .use(async (c, next) => { await next(); c.header('x-api', 'yes') })
57
- .get('/items/:id', c => c.json({ id: c.req.param('id') }))
58
- .get('/boom', () => { throw new Error('boom') })
59
- .onError((error, c) => c.json({ handled: error instanceof Error ? error.message : 'unknown' }, 418))
60
- router.route('/api', api)
61
-
62
- const item = await get(router, '/api/items/a%20b')
63
- expect(await item.json()).toEqual({ id: 'a b' })
64
- expect(item.headers.get('x-api')).toBe('yes')
65
- const boom = await get(router, '/api/boom')
66
- expect(boom.status).toBe(418)
67
- expect(await boom.json()).toEqual({ handled: 'boom' })
68
- expect((await get(router, '/items/a')).status).toBe(404)
69
- })
70
-
71
- it('removes a mount with its remover, idempotently, and keeps the others', async () => {
72
- const { router } = await mountRouter()
73
- const removeA = router.route('/', new Acryl().get('/a', c => c.text('a')))
74
- router.route('/', new Acryl().get('/b', c => c.text('b')))
75
- expect((await get(router, '/a')).status).toBe(200)
76
- removeA()
77
- removeA()
78
- expect((await get(router, '/a')).status).toBe(404)
79
- expect(await (await get(router, '/b')).text()).toBe('b')
80
- router.route('/', new Acryl().get('/a', c => c.text('a2')))
81
- expect(await (await get(router, '/a')).text()).toBe('a2')
82
- })
83
-
84
- it('runs mounts in the order they were made, so an earlier mount\'s middleware wraps a later mount\'s routes', async () => {
85
- const { router } = await mountRouter()
86
- router.route('/', new Acryl().use('/guarded/*', async c => c.text('denied', 401)))
87
- router.route('/', new Acryl().get('/guarded/x', c => c.text('secret')).get('/open', c => c.text('open')))
88
- expect((await get(router, '/guarded/x')).status).toBe(401)
89
- expect(await (await get(router, '/open')).text()).toBe('open')
90
- })
91
-
92
- it('takes one router-wide error handler and one not-found handler, owned by the caller', async () => {
93
- const { router } = await mountRouter()
94
- router.route('/', new Acryl().get('/boom', () => { throw new Error('boom') }).get('/teapot', () => { throw new HTTPException(418, { message: 'short' }) }))
95
- expect((await get(router, '/boom')).status).toBe(500)
96
- expect((await get(router, '/teapot')).status).toBe(418)
97
-
98
- const removeError = router.onError((_error, c) => c.text('custom', 503))
99
- const removeNotFound = router.notFound(c => c.text('nothing here', 404))
100
- expect(() => router.onError((_error, c) => c.text('second'))).toThrow(/already registered/)
101
- expect((await get(router, '/boom')).status).toBe(503)
102
- expect(await (await get(router, '/missing')).text()).toBe('nothing here')
103
- removeError()
104
- removeNotFound()
105
- expect((await get(router, '/boom')).status).toBe(500)
106
- })
107
-
108
- it('treats /a and /a/ as one path when trailingSlash is ignore', async () => {
109
- const ctx = root()
110
- ctx.plugin(provider, { trailingSlash: 'ignore', disposeGraceMs: 2000 })
111
- await settle()
112
- ctx.router.route('/', new Acryl().get('/a', c => c.text('a')))
113
- expect((await get(ctx.router, '/a/')).status).toBe(200)
114
- })
115
-
116
- it('hands the transport\'s connection info to the handler and gives an in-process call an empty one', async () => {
117
- const { router } = await mountRouter()
118
- router.route('/', new Acryl().get('/who', c => c.json({ remote: connectionOf(c).remoteAddress ?? null, port: connectionOf(c).localPort ?? null })))
119
- const over = await router.fetch(new Request('http://acryl.test/who'), { remoteAddress: '127.0.0.1', localPort: 4000 })
120
- expect(await over.json()).toEqual({ remote: '127.0.0.1', port: 4000 })
121
- const inProcess = await router.request('/who')
122
- expect(await inProcess.json()).toEqual({ remote: null, port: null })
123
- })
124
- })
125
-
126
- describe('acryl-router lifecycle through Cordis', () => {
127
- it('leaves a consumer PENDING while router is missing and activates it when router appears', async () => {
128
- const ctx = root()
129
- const fiber = ctx.plugin(consumer('/c', 'A'))
130
- await settle()
131
- expect(fiber.state).toBe(PENDING)
132
-
133
- ctx.plugin(provider, config)
134
- await settle()
135
- expect(fiber.state).toBe(ACTIVE)
136
- expect(await (await get(ctx.router, '/c')).text()).toBe('A')
137
- expect(ctx.router.size).toBe(1)
138
- })
139
-
140
- it('removes a consumer\'s mounts when the consumer unloads and leaves other consumers alone', async () => {
141
- const { ctx, router } = await mountRouter()
142
- const a = ctx.plugin(consumer('/a', 'A'))
143
- ctx.plugin(consumer('/b', 'B'))
144
- await settle()
145
- expect(router.size).toBe(2)
146
-
147
- await a.dispose()
148
- await settle()
149
- expect(a.state).toBe(DISPOSED)
150
- expect((await get(router, '/a')).status).toBe(404)
151
- expect(await (await get(router, '/b')).text()).toBe('B')
152
- expect(router.size).toBe(1)
153
- })
154
-
155
- it('labels a mount by its path with one child per route, so no label grows with the app (an inspector caps label length)', async () => {
156
- const { ctx, router } = await mountRouter()
157
- const big = new Acryl().use('/api/*', async (_c, next) => { await next() })
158
- for (let i = 0; i < 60; i += 1) big.get(`/api/a-fairly-long-resource-name-${String(i)}`, c => c.text('ok'))
159
- const fiber = ctx.plugin({ name: 'big', inject: ['router'], apply: (inner: Context) => { inner.router.route('/', big) } })
160
- await settle()
161
- const [mount, ...rest] = fiber.getEffects()
162
- expect(rest).toEqual([])
163
- expect(mount?.label).toBe('acryl-router: route /')
164
- expect(mount?.children).toHaveLength(61)
165
- expect(mount?.children[0]?.label).toBe('acryl-router: serves ALL /api/*')
166
- expect(mount?.children.every(child => child.label.length < 100 && child.children.length === 0)).toBe(true)
167
- expect(router.size).toBe(1)
168
- await fiber.dispose()
169
- expect(router.size).toBe(0)
170
- })
171
-
172
- it('replaces the provider without leaving doubled registrations: consumers re-register exactly once', async () => {
173
- const ctx = root()
174
- const first = ctx.plugin(provider, config)
175
- const fiber = ctx.plugin(consumer('/c', 'A'))
176
- await settle()
177
- const oldRouter = ctx.router
178
- expect(fiber.state).toBe(ACTIVE)
179
-
180
- await first.dispose()
181
- await settle()
182
- expect(fiber.state).toBe(PENDING)
183
- expect(oldRouter.size).toBe(0)
184
-
185
- ctx.plugin(provider, config)
186
- await settle()
187
- expect(fiber.state).toBe(ACTIVE)
188
- expect(ctx.router).not.toBe(oldRouter)
189
- expect(ctx.router.size).toBe(1)
190
- expect(await (await get(ctx.router, '/c')).text()).toBe('A')
191
- })
192
-
193
- it('serves the routes an app had when it was mounted, whatever else changes later', async () => {
194
- const { ctx, router } = await mountRouter()
195
- const app = new Acryl().get('/early', c => c.text('early'))
196
- router.route('/', app)
197
- app.get('/late', c => c.text('late'))
198
- expect((await get(router, '/early')).status).toBe(200)
199
- expect((await get(router, '/late')).status).toBe(404)
200
- // Another plugin's mount recomposes the router; the late route still is not served.
201
- ctx.plugin(consumer('/other', 'other'))
202
- await settle()
203
- expect((await get(router, '/other')).status).toBe(200)
204
- expect((await get(router, '/late')).status).toBe(404)
205
- })
206
-
207
- it('serves two plugins on the same path in mount order, and hands the path to the other when the first unloads', async () => {
208
- const { ctx, router } = await mountRouter()
209
- const first = ctx.plugin(consumer('/same', 'first'))
210
- await settle()
211
- ctx.plugin(consumer('/same', 'second'))
212
- await settle()
213
- expect(await (await get(router, '/same')).text()).toBe('first')
214
- await first.dispose()
215
- await settle()
216
- expect(await (await get(router, '/same')).text()).toBe('second')
217
- })
218
-
219
- it('runs a reloaded plugin\'s handler once per request: reloading leaves no duplicate route behind, ten times', async () => {
220
- const { ctx, router } = await mountRouter()
221
- let calls = 0
222
- const counting = {
223
- name: 'counting',
224
- inject: ['router'],
225
- apply(inner: Context): void {
226
- inner.router.route('/', new Acryl().use(async (_c, next) => { calls += 1; await next() }).get('/count', c => c.text(String(calls))))
227
- },
228
- }
229
- for (let round = 0; round < 10; round += 1) {
230
- const fiber = ctx.plugin(counting)
231
- await settle()
232
- expect(router.size).toBe(1)
233
- calls = 0
234
- expect(await (await get(router, '/count')).text()).toBe('1')
235
- await fiber.dispose()
236
- await settle()
237
- expect(router.size).toBe(0)
238
- }
239
- })
240
-
241
- it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
242
- const { ctx, router } = await mountRouter()
243
- ctx.plugin(consumer('/stay', 'S'))
244
- await settle()
245
- const baseline = router.size
246
- for (let i = 0; i < 50; i += 1) {
247
- const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
248
- await settle()
249
- expect(router.size).toBe(baseline + 1)
250
- expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
251
- await fiber.dispose()
252
- await settle()
253
- }
254
- expect(router.size).toBe(baseline)
255
- expect((await get(router, '/cycle/0')).status).toBe(404)
256
- })
257
-
258
- it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
259
- const ctx = root()
260
- const fiber = ctx.plugin(provider, config)
261
- await settle()
262
- const router = ctx.router
263
- let aborted = false
264
- router.route('/', new Acryl().get('/slow', async c => {
265
- await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
266
- return c.text('late')
267
- }))
268
- const pending = get(router, '/slow')
269
- await settle()
270
-
271
- await fiber.dispose()
272
- await settle()
273
- expect(aborted).toBe(true)
274
- expect((await pending).status).toBe(499)
275
- expect((await get(router, '/slow')).status).toBe(503)
276
- })
277
-
278
- it('forwards the caller\'s own abort to the handler signal', async () => {
279
- const { router } = await mountRouter()
280
- let seen = false
281
- router.route('/', new Acryl().get('/slow', async c => {
282
- await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
283
- return c.text('x')
284
- }))
285
- const controller = new AbortController()
286
- const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
287
- await settle()
288
- controller.abort()
289
- expect((await pending).status).toBe(499)
290
- expect(seen).toBe(true)
291
- })
292
-
293
- it('ends a server-sent-event stream when the router is disposed: the stream\'s abort cleanup runs and the reader sees the end', async () => {
294
- const ctx = root()
295
- const fiber = ctx.plugin(provider, config)
296
- await settle()
297
- let cleaned = false
298
- ctx.router.route('/', new Acryl().get('/events', c => streamSSE(c, async stream => {
299
- stream.onAbort(() => { cleaned = true })
300
- await stream.writeSSE({ data: 'first' })
301
- while (!stream.aborted) await stream.sleep(10)
302
- })))
303
- const response = await get(ctx.router, '/events')
304
- expect(response.headers.get('content-type')).toBe('text/event-stream')
305
- const reader = response.body!.getReader()
306
- expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
307
-
308
- await fiber.dispose()
309
- expect(cleaned).toBe(true)
310
- await expect(reader.read()).resolves.toMatchObject({ done: true })
311
- })
312
-
313
- it('ends the stream a plugin opened when that plugin unloads, and leaves another plugin\'s request running', async () => {
314
- const { ctx, router } = await mountRouter()
315
- let cleaned = false
316
- let otherAborted = false
317
- let release!: () => void
318
- const streaming = ctx.plugin({
319
- name: 'streaming',
320
- inject: ['router'],
321
- apply(inner: Context): void {
322
- inner.router.route('/', new Acryl().get('/events', c => streamSSE(c, async stream => {
323
- stream.onAbort(() => { cleaned = true })
324
- await stream.writeSSE({ data: 'first' })
325
- while (!stream.aborted) await stream.sleep(10)
326
- })))
327
- },
328
- })
329
- ctx.plugin({
330
- name: 'other',
331
- inject: ['router'],
332
- apply(inner: Context): void {
333
- inner.router.route('/', new Acryl().get('/slow', async c => {
334
- c.req.raw.signal.addEventListener('abort', () => { otherAborted = true }, { once: true })
335
- await new Promise<void>(resolve => { release = resolve })
336
- return c.text('done')
337
- }))
338
- },
339
- })
340
- await settle()
341
- const response = await get(router, '/events')
342
- const reader = response.body!.getReader()
343
- expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
344
- const slow = get(router, '/slow')
345
- await settle()
346
-
347
- await streaming.dispose()
348
- expect(cleaned).toBe(true)
349
- await expect(reader.read()).resolves.toMatchObject({ done: true })
350
- expect(otherAborted).toBe(false)
351
- release()
352
- expect(await (await slow).text()).toBe('done')
353
- })
354
-
355
- it('aborts a request still inside an unloading plugin\'s middleware, but not one that middleware already passed on', async () => {
356
- const { ctx, router } = await mountRouter()
357
- let guardAborted = false
358
- let handlerAborted = false
359
- let release!: () => void
360
- const guard = ctx.plugin({
361
- name: 'guard',
362
- inject: ['router'],
363
- apply(inner: Context): void {
364
- inner.router.route('/', new Acryl()
365
- .use('/held/*', async (c, next) => {
366
- c.req.raw.signal.addEventListener('abort', () => { guardAborted = true }, { once: true })
367
- await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) })
368
- await next()
369
- })
370
- .use('/passed/*', async (_c, next) => { await next() }))
371
- },
372
- })
373
- ctx.plugin({
374
- name: 'handler',
375
- inject: ['router'],
376
- apply(inner: Context): void {
377
- inner.router.route('/', new Acryl()
378
- .get('/held/x', c => c.text('held'))
379
- .get('/passed/x', c => streamSSE(c, async stream => {
380
- stream.onAbort(() => { handlerAborted = true })
381
- await stream.writeSSE({ data: 'open' })
382
- await new Promise<void>(resolve => { release = resolve })
383
- })))
384
- },
385
- })
386
- await settle()
387
- const held = get(router, '/held/x')
388
- const passed = await get(router, '/passed/x')
389
- const reader = passed.body!.getReader()
390
- await reader.read()
391
-
392
- await guard.dispose()
393
- expect(guardAborted).toBe(true)
394
- expect((await held).status).toBe(499)
395
- expect(handlerAborted).toBe(false)
396
- release()
397
- await expect(reader.read()).resolves.toMatchObject({ done: true })
398
- })
399
-
400
- it('keeps a request in flight through waitUntil until its task settles', async () => {
401
- const ctx = root()
402
- const fiber = ctx.plugin(provider, config)
403
- await settle()
404
- let finished = false
405
- ctx.router.route('/', new Acryl().get('/later', c => {
406
- c.executionCtx.waitUntil(new Promise<void>(resolve => { setTimeout(() => { finished = true; resolve() }, 30) }))
407
- return c.text('now')
408
- }))
409
- expect(await (await get(ctx.router, '/later')).text()).toBe('now')
410
- expect(finished).toBe(false)
411
- await fiber.dispose()
412
- expect(finished).toBe(true)
413
- })
414
-
415
- it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
416
- const ctx = root()
417
- const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
418
- await settle()
419
- ctx.router.route('/', new Acryl().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
420
- await get(ctx.router, '/stuck')
421
- const started = Date.now()
422
- await fiber.dispose()
423
- expect(Date.now() - started).toBeLessThan(1000)
424
- expect(fiber.state).toBe(DISPOSED)
425
- })
426
- })
@@ -1,79 +0,0 @@
1
- /**
2
- * What a Service's fields look like to the plugins that call it. A plugin reaches a service through its own shadow of the instance
3
- * (`ctx.counter` in plugin A and in plugin B are two different objects over one service), but reads and writes of fields pass through to
4
- * the one instance: a field assigned in a method is seen by every caller, as is state kept in an object that is only mutated. What the
5
- * shadow does change is `this.ctx` inside a method, which `live-install.spec.ts` covers.
6
- */
7
- import { Context, Service } from '@deepseek-ai/cordis'
8
- import { afterEach, describe, expect, it } from 'vitest'
9
-
10
- declare module '@deepseek-ai/cordis' {
11
- interface Context {
12
- counter: Counter
13
- }
14
- }
15
-
16
- const contexts: Context[] = []
17
- const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
18
- afterEach(async () => {
19
- while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
20
- })
21
-
22
- class Counter extends Service {
23
- /** Reassigned by `assign`. */
24
- assigned = 0
25
- /** Created once, only mutated. */
26
- private readonly state = { count: 0 }
27
-
28
- constructor(ctx: Context) {
29
- super(ctx, 'counter')
30
- }
31
-
32
- assign(): void {
33
- this.assigned += 1
34
- }
35
-
36
- mutate(): void {
37
- this.state.count += 1
38
- }
39
-
40
- get count(): number {
41
- return this.state.count
42
- }
43
- }
44
-
45
- async function twoCallers(): Promise<{ a: Context, b: Context }> {
46
- const root = new Context()
47
- contexts.push(root)
48
- root.plugin({ name: 'counter', apply: (ctx: Context) => { new Counter(ctx) } })
49
- const callers: Context[] = []
50
- for (const name of ['a', 'b']) root.plugin({ name, inject: ['counter'], apply: (ctx: Context) => { callers.push(ctx) } })
51
- await settle()
52
- const [a, b] = callers
53
- if (a === undefined || b === undefined) throw new Error('both callers should be active')
54
- return { a, b }
55
- }
56
-
57
- describe('service state and the shadow context', () => {
58
- it('shows a field assigned in a method through one caller to every caller', async () => {
59
- const { a, b } = await twoCallers()
60
- a.counter.assign()
61
- b.counter.assign()
62
- expect(a.counter.assigned).toBe(2)
63
- expect(b.counter.assigned).toBe(2)
64
- })
65
-
66
- it('shows a field assigned from outside through one caller to every caller', async () => {
67
- const { a, b } = await twoCallers()
68
- a.counter.assigned = 5
69
- expect(b.counter.assigned).toBe(5)
70
- })
71
-
72
- it('shares state kept in one object that is only mutated', async () => {
73
- const { a, b } = await twoCallers()
74
- a.counter.mutate()
75
- b.counter.mutate()
76
- expect(a.counter.count).toBe(2)
77
- expect(b.counter.count).toBe(2)
78
- })
79
- })
@@ -1,174 +0,0 @@
1
- /**
2
- * The Web adapter against the real DSH `webServer` listening on a loopback port: a plugin mounts a prefix, requests
3
- * travel over real HTTP into `router.fetch`, and the lifecycle (PENDING, unload, reload) is observed the way a browser
4
- * would see it. The router is the built `acryl-router` entry, as production loads it.
5
- */
6
-
7
- import { Context, type FiberState } from '@deepseek-ai/cordis'
8
- import WebServer from '@deepseek-ai/dsh-host-webserver'
9
- import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
10
- import { afterEach, describe, expect, it } from 'vitest'
11
- import { Acryl, connectionOf, type RouterHttpService } from '../../acryl-router/src/index.ts'
12
- import { methodNotAllowed } from '../../acryl-router/src/middleware/index.ts'
13
- import { apply, inject, name } from '../src/index.ts'
14
-
15
- const PENDING = 0 as FiberState.PENDING
16
- const ACTIVE = 2 as FiberState.ACTIVE
17
-
18
- const contexts: Context[] = []
19
- const settle = (ms = 0): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
20
- afterEach(async () => {
21
- while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
22
- })
23
-
24
- async function until(check: () => boolean, label: string): Promise<void> {
25
- for (let i = 0; i < 300; i += 1) { if (check()) return; await settle(10) }
26
- throw new Error(`timed out waiting for: ${label}`)
27
- }
28
-
29
- const adapter = { name, inject, apply }
30
- const router = { name: routerName, apply: routerApply }
31
-
32
- /** A whole stack on one root context: webServer on an OS-assigned port, the router, and the adapter. */
33
- async function stack(): Promise<{ ctx: Context, base: () => string }> {
34
- const ctx = new Context()
35
- contexts.push(ctx)
36
- ctx.plugin(WebServer, { host: '127.0.0.1', port: 0 } as never)
37
- ctx.plugin(router, { trailingSlash: 'strict' })
38
- ctx.plugin(adapter)
39
- await until(() => (ctx as unknown as { webServer?: { port: number } }).webServer?.port !== undefined && ctx.routerHttp !== undefined, 'the stack to start')
40
- return { ctx, base: () => `http://127.0.0.1:${String((ctx as unknown as { webServer: { port: number } }).webServer.port)}` }
41
- }
42
-
43
- function demo(prefix: string, extra?: (app: Acryl) => void) {
44
- return {
45
- name: `demo${prefix.replaceAll('/', '-')}`,
46
- inject: ['router', 'routerHttp'],
47
- apply(ctx: Context): void {
48
- ctx.routerHttp.mount(prefix)
49
- const app = new Acryl()
50
- app.use(methodNotAllowed({ app }))
51
- app.get('/hello/:who', c => c.json({ hello: c.req.param('who'), q: c.req.query('q') ?? null, host: c.req.header('host') !== undefined }))
52
- app.post('/echo', async c => c.json({ got: await c.req.text(), type: c.req.header('content-type') ?? null }, 201))
53
- app.get('/cookies', () => {
54
- const headers = new Headers()
55
- headers.append('set-cookie', 'a=1; Path=/')
56
- headers.append('set-cookie', 'b=2; Path=/')
57
- return new Response('ok', { headers })
58
- })
59
- app.get('/stream', () => new Response(new ReadableStream({
60
- start(controller) { controller.enqueue(new TextEncoder().encode('one,')); controller.enqueue(new TextEncoder().encode('two')); controller.close() },
61
- })))
62
- app.get('/peer', c => c.json({ remote: connectionOf(c).remoteAddress ?? null, localPort: connectionOf(c).localPort ?? null }))
63
- extra?.(app)
64
- ctx.router.route(prefix, app)
65
- },
66
- }
67
- }
68
-
69
- describe('acryl-router-http against a real webServer', () => {
70
- it('serves a mounted prefix over HTTP: params, query, JSON, status', async () => {
71
- const { ctx, base } = await stack()
72
- ctx.plugin(demo('/api/demo'))
73
- await settle(20)
74
- const response = await fetch(`${base()}/api/demo/hello/zed%20x?q=1`)
75
- expect(response.status).toBe(200)
76
- expect(response.headers.get('content-type')).toContain('application/json')
77
- expect(await response.json()).toEqual({ hello: 'zed x', q: '1', host: true })
78
- })
79
-
80
- it('passes the socket peer address as connection info, not as part of the request', async () => {
81
- const { ctx, base } = await stack()
82
- ctx.plugin(demo('/api/demo'))
83
- await settle(20)
84
- const body = await (await fetch(`${base()}/api/demo/peer`)).json() as { remote: string | null, localPort: number | null }
85
- expect(['127.0.0.1', '::ffff:127.0.0.1']).toContain(body.remote)
86
- expect(body.localPort).toBe(Number(new URL(base()).port))
87
- })
88
-
89
- it('streams a request body into the handler and keeps status and headers on the way out', async () => {
90
- const { ctx, base } = await stack()
91
- ctx.plugin(demo('/api/demo'))
92
- await settle(20)
93
- const response = await fetch(`${base()}/api/demo/echo`, { method: 'POST', body: 'payload', headers: { 'content-type': 'text/plain' } })
94
- expect(response.status).toBe(201)
95
- expect(await response.json()).toEqual({ got: 'payload', type: 'text/plain' })
96
- })
97
-
98
- it('keeps repeated set-cookie headers separate and streams a response body', async () => {
99
- const { ctx, base } = await stack()
100
- ctx.plugin(demo('/api/demo'))
101
- await settle(20)
102
- const cookies = await fetch(`${base()}/api/demo/cookies`)
103
- expect(cookies.headers.getSetCookie()).toEqual(['a=1; Path=/', 'b=2; Path=/'])
104
- expect(await (await fetch(`${base()}/api/demo/stream`)).text()).toBe('one,two')
105
- })
106
-
107
- it('answers the router\'s 404 and 405 below the mount, and answers HEAD without a body', async () => {
108
- const { ctx, base } = await stack()
109
- ctx.plugin(demo('/api/demo'))
110
- await settle(20)
111
- const missing = await fetch(`${base()}/api/demo/none`)
112
- expect(missing.status).toBe(404)
113
- expect(await missing.text()).toBe('404 Not Found')
114
- expect((await fetch(`${base()}/api/demo/echo`)).status).toBe(405)
115
- const head = await fetch(`${base()}/api/demo/hello/a`, { method: 'HEAD' })
116
- expect(head.status).toBe(200)
117
- expect(head.headers.get('content-type')).toContain('application/json')
118
- expect(await head.text()).toBe('')
119
- })
120
-
121
- it('aborts the handler signal when the HTTP client disconnects', async () => {
122
- const { ctx, base } = await stack()
123
- let aborted = false
124
- ctx.plugin(demo('/api/demo', app => {
125
- app.get('/slow', async r => {
126
- await new Promise<void>((_resolve, reject) => { r.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
127
- return r.text('never')
128
- })
129
- }))
130
- await settle(20)
131
- const controller = new AbortController()
132
- const pending = fetch(`${base()}/api/demo/slow`, { signal: controller.signal }).catch(() => undefined)
133
- await settle(50)
134
- controller.abort()
135
- await pending
136
- await until(() => aborted, 'the handler to see the disconnect')
137
- })
138
-
139
- it('stops serving the prefix when the owning plugin unloads, and serves it again on reload, fifty times', async () => {
140
- const { ctx, base } = await stack()
141
- for (let i = 0; i < 50; i += 1) {
142
- const fiber = ctx.plugin(demo('/api/demo'))
143
- await settle(5)
144
- expect((await fetch(`${base()}/api/demo/hello/a`)).status).toBe(200)
145
- await fiber.dispose()
146
- await settle(5)
147
- // Unmounted: the webServer has no fallback in this stack, so it answers its own empty 404, not the router's body.
148
- const gone = await fetch(`${base()}/api/demo/hello/a`)
149
- expect(gone.status).toBe(404)
150
- expect(await gone.text()).toBe('')
151
- }
152
- expect(ctx.router.size).toBe(0)
153
- })
154
-
155
- it('stays PENDING while webServer is missing and activates when it appears', async () => {
156
- const ctx = new Context()
157
- contexts.push(ctx)
158
- ctx.plugin(router, { trailingSlash: 'strict' })
159
- const fiber = ctx.plugin(adapter)
160
- await settle(20)
161
- expect(fiber.state).toBe(PENDING)
162
- ctx.plugin(WebServer, { host: '127.0.0.1', port: 0 } as never)
163
- await until(() => fiber.state === ACTIVE, 'the adapter to activate')
164
- })
165
-
166
- it('refuses a prefix that is not a clean path and a prefix another plugin already mounted', async () => {
167
- const { ctx } = await stack()
168
- const http: RouterHttpService = ctx.routerHttp
169
- expect(() => http.mount('no-slash')).toThrow(/mountable/)
170
- expect(() => http.mount('/trailing/')).toThrow(/mountable/)
171
- http.mount('/api/once')
172
- expect(() => http.mount('/api/once')).toThrow(/duplicate/)
173
- })
174
- })