acrylblends 0.3.0 → 0.4.1

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 (341) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.js +84 -5
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/LICENSE +23 -0
  5. package/docs/framework/README.md +63 -0
  6. package/docs/framework/UPSTREAM.md +3 -0
  7. package/docs/framework/api-summary.md +138 -0
  8. package/docs/framework/concepts.md +105 -0
  9. package/docs/framework/context.md +129 -0
  10. package/docs/framework/getting-started.md +146 -0
  11. package/docs/framework/helpers.md +118 -0
  12. package/docs/framework/middleware.md +154 -0
  13. package/docs/framework/migration-from-0-3.md +108 -0
  14. package/docs/framework/migration-from-dsh-webserver.md +103 -0
  15. package/docs/framework/node-server.md +75 -0
  16. package/docs/framework/plugin-lifecycle.md +111 -0
  17. package/docs/framework/request.md +106 -0
  18. package/docs/framework/routing.md +188 -0
  19. package/docs/framework/surfaces.md +71 -0
  20. package/docs/framework/upstream.json +9 -0
  21. package/docs/framework/validation.md +135 -0
  22. package/package.json +4 -2
  23. package/plugins/acryl-inspect/plugin.json +2 -1
  24. package/plugins/acryl-inspect/src/report.ts +21 -13
  25. package/plugins/acryl-inspect/src/service.ts +9 -7
  26. package/plugins/acryl-inspect/tests/report.spec.ts +13 -8
  27. package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
  28. package/plugins/acryl-inspect/upstream.json +2 -2
  29. package/plugins/acryl-node-server/LICENSE +26 -0
  30. package/plugins/acryl-node-server/plugin.json +2 -1
  31. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  32. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  33. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  34. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  35. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  36. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  37. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  38. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  39. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  40. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  41. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  42. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  43. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  44. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  45. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  46. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  47. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  48. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  49. package/plugins/acryl-node-server/src/index.ts +19 -6
  50. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  51. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  52. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  53. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  54. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  55. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  56. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  57. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  58. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  59. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  60. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  61. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  62. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  63. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  64. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  65. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  66. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  67. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  68. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  69. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  70. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  71. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  72. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  73. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  74. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  75. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  76. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  77. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  78. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  79. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  80. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  81. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  82. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  83. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  84. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  85. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  86. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  87. package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
  88. package/plugins/acryl-node-server/upstream.json +3 -3
  89. package/plugins/acryl-router/LICENSE +26 -0
  90. package/plugins/acryl-router/plugin.json +2 -1
  91. package/plugins/acryl-router/src/client.ts +2 -0
  92. package/plugins/acryl-router/src/connection.ts +42 -0
  93. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  94. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  95. package/plugins/acryl-router/src/core/app.ts +34 -0
  96. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  97. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  98. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  99. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  100. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  101. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  102. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  103. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  104. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  105. package/plugins/acryl-router/src/core/compose.ts +73 -0
  106. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  107. package/plugins/acryl-router/src/core/context.ts +781 -0
  108. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  109. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  110. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  111. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  112. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  113. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  114. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  115. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  116. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  117. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  118. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  119. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  120. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  121. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  122. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  123. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  124. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  125. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  126. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  127. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  128. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  129. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  130. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  131. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  132. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  133. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  134. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  135. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  136. package/plugins/acryl-router/src/core/index.ts +53 -0
  137. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  138. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  139. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  140. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  141. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  142. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  143. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  144. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  145. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  146. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  147. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  148. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  149. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  150. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  151. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  152. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  153. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  154. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  155. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  156. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  157. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  158. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  159. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  160. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  161. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  162. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  163. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  164. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  165. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  166. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  167. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  168. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  169. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  170. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  171. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  172. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  173. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  174. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  175. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  176. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  177. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  178. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  179. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  180. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  181. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  182. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  183. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  184. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  185. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  186. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  187. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  188. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  189. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  190. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  191. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  192. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  193. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  194. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  195. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  196. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  197. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  198. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  199. package/plugins/acryl-router/src/core/request.ts +502 -0
  200. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  201. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  202. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  203. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  204. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  205. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  206. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  207. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  208. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  209. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  210. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  211. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  212. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  213. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  214. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  215. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  216. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  217. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  218. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  219. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  220. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  221. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  222. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  223. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  224. package/plugins/acryl-router/src/core/router.ts +103 -0
  225. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  226. package/plugins/acryl-router/src/core/types.ts +2782 -0
  227. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  228. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  229. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  230. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  231. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  232. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  233. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  234. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  235. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  236. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  237. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  238. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  239. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  240. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  241. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  242. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  243. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  244. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  245. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  246. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  247. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  248. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  249. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  250. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  251. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  252. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  253. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  254. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  255. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  256. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  257. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  258. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  259. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  260. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  261. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  262. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  263. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  264. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  265. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  266. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  267. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  268. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  269. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  270. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  271. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  272. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  273. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  274. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  275. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  276. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  277. package/plugins/acryl-router/src/helpers/index.ts +13 -3
  278. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  279. package/plugins/acryl-router/src/index.ts +6 -6
  280. package/plugins/acryl-router/src/middleware/index.ts +25 -6
  281. package/plugins/acryl-router/src/middleware/loopback-only.ts +53 -14
  282. package/plugins/acryl-router/src/service.ts +262 -64
  283. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  284. package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
  285. package/plugins/acryl-router/tests/loader.spec.ts +3 -2
  286. package/plugins/acryl-router/tests/loopback-only.spec.ts +80 -0
  287. package/plugins/acryl-router/tests/service.spec.ts +281 -66
  288. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  289. package/plugins/acryl-router/upstream.json +2 -2
  290. package/plugins/acryl-router-http/plugin.json +2 -1
  291. package/plugins/acryl-router-http/src/bridge.ts +3 -1
  292. package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
  293. package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
  294. package/plugins/acryl-router-http/upstream.json +3 -3
  295. package/plugins/acryl-state/NOTICE +30 -0
  296. package/plugins/acryl-state/plugin.json +1 -1
  297. package/plugins/acryl-state/src/cell.ts +1 -1
  298. package/plugins/acryl-state/src/service.ts +33 -15
  299. package/plugins/acryl-state/upstream.json +6 -4
  300. package/plugins/builder/script.ts +4 -7
  301. package/plugins/extensions/index.ts +6 -5
  302. package/plugins/extensions/resolve.ts +28 -3
  303. package/plugins/openai/prompt.ts +9 -7
  304. package/plugins/tsconfig.json +5 -4
  305. package/shared/AGENTS.core.md +54 -0
  306. package/templates/agent/AGENTS.md +6 -0
  307. package/templates/agent/README.md +2 -2
  308. package/templates/agent/blend.yaml +1 -1
  309. package/templates/agent/src/routes.ts +37 -27
  310. package/templates/agent/test/model.test.ts +2 -1
  311. package/templates/agent/tsconfig.json +8 -5
  312. package/templates/blank/AGENTS.md +5 -0
  313. package/templates/blank/README.md +1 -1
  314. package/templates/blank/src/routes.ts +6 -4
  315. package/templates/blank/tsconfig.json +2 -4
  316. package/templates/todo/AGENTS.md +5 -0
  317. package/templates/todo/src/page.ts +1 -1
  318. package/templates/todo/src/todo.ts +58 -29
  319. package/templates/todo/tsconfig.json +2 -4
  320. package/templates/todo/upstream.json +2 -2
  321. package/plugins/acryl-router/src/compose.ts +0 -21
  322. package/plugins/acryl-router/src/context.ts +0 -59
  323. package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
  324. package/plugins/acryl-router/src/helpers/sse.ts +0 -83
  325. package/plugins/acryl-router/src/helpers/validate.ts +0 -44
  326. package/plugins/acryl-router/src/http-error.ts +0 -21
  327. package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
  328. package/plugins/acryl-router/src/middleware/cors.ts +0 -46
  329. package/plugins/acryl-router/src/middleware/logger.ts +0 -21
  330. package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
  331. package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
  332. package/plugins/acryl-router/src/pattern.ts +0 -89
  333. package/plugins/acryl-router/src/registrar.ts +0 -90
  334. package/plugins/acryl-router/src/table.ts +0 -156
  335. package/plugins/acryl-router/src/types.ts +0 -73
  336. package/plugins/acryl-router/src/util.ts +0 -23
  337. package/plugins/acryl-router/tests/api.spec.ts +0 -223
  338. package/plugins/acryl-router/tests/conformance.ts +0 -170
  339. package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
  340. package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
  341. package/plugins/acryl-router/tests/table.spec.ts +0 -47
@@ -1,29 +1,38 @@
1
1
  /**
2
- * The ACRYL router service (`ctx.router`): plugin-owned routes and middleware on Web-standard Request and Response.
2
+ * The ACRYL router service (`ctx.router`): plugin-owned `App` mounts on Web-standard Request and Response.
3
3
  *
4
- * A route is registered from the owning plugin's context, so it disappears when that plugin unloads. `fetch(request)`
5
- * runs a request in process: Web serves it behind HTTP (see spec 046, milestone M2), Desktop and the terminal UI call it
6
- * directly, so one handler serves every surface without opening a socket. Disposal aborts in-flight requests through their
7
- * `signal` and waits for them to settle.
4
+ * A plugin builds an ordinary `App` and mounts it with `ctx.router.route(path, app)` from its own context, so the mount disappears when that
5
+ * plugin unloads. `fetch(request)` runs a request in process: Web and Desktop serve it behind HTTP through an adapter, the terminal UI calls
6
+ * it directly, so one handler serves every surface without opening a socket. Disposal refuses new requests, aborts in-flight ones, ends any
7
+ * response body still streaming and waits for all of it to settle.
8
8
  */
9
9
 
10
10
  import { Context, Service } from '@deepseek-ai/cordis'
11
11
  import z from '@deepseek-ai/schemastery'
12
- import { createRegistrar, type Primitives, type Registrar, type RouteHandlers, type UseArguments } from './registrar.ts'
13
- import { RouteTable } from './table.ts'
14
- import type { ConnectionInfo, ErrorHandler, Method, Middleware, NotFoundHandler, RouteMethod, TrailingSlash } from './types.ts'
12
+ import type { ConnectionInfo, RouterBindings } from './connection.ts'
13
+ import { App } from './core/app.ts'
14
+ import type { ExecutionContext } from './core/context.ts'
15
+ import { HTTPException } from './core/http-exception.ts'
16
+ import { TrieRouter } from './core/router/trie-router/index.ts'
17
+ import { COMPOSED_HANDLER } from './core/utils/constants.ts'
18
+ import { mergePath } from './core/utils/url.ts'
19
+ import type { ErrorHandler, H, NotFoundHandler } from './core/types.ts'
15
20
 
16
21
  const DEFAULT_DISPOSE_GRACE_MS = 2000
17
22
 
23
+ /** How a trailing slash is read: `strict` keeps `/a` and `/a/` distinct, `ignore` treats them as one path. */
24
+ export type TrailingSlash = 'strict' | 'ignore'
25
+
18
26
  /** Validated plugin configuration. */
19
27
  export interface Config {
20
28
  trailingSlash: TrailingSlash
21
29
  disposeGraceMs: number
22
30
  }
23
31
 
24
- export const Config: z<Config> = z.object({
32
+ /** Every field has a default, so a caller passes only what it changes: `ctx.plugin(plugin, { ... })` takes the partial shape. */
33
+ export const Config: z<Partial<Config>, Config> = z.object({
25
34
  trailingSlash: z.union(['strict', 'ignore']).default('strict').description('Whether `/a` and `/a/` are different paths.'),
26
- disposeGraceMs: z.number().min(0).default(DEFAULT_DISPOSE_GRACE_MS).description('How long disposal waits for aborted requests (and their waitUntil tasks) to finish before it gives up on them.'),
35
+ disposeGraceMs: z.number().min(0).default(DEFAULT_DISPOSE_GRACE_MS).description('How long disposal waits for aborted requests, their response bodies and their waitUntil tasks before it gives up on them.'),
27
36
  })
28
37
 
29
38
  /**
@@ -45,84 +54,176 @@ declare module '@deepseek-ai/cordis' {
45
54
  /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
46
55
  export const name = 'acryl-router'
47
56
 
57
+ // Any `App`, whatever its Env and schema; the router only reads its routes.
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ export type MountableApp = App<any, any, any>
60
+
61
+ interface Mount {
62
+ readonly path: string
63
+ readonly app: MountableApp
64
+ }
65
+
66
+ interface RouterState {
67
+ readonly mounts: Set<Mount>
68
+ errorHandler: ErrorHandler | undefined
69
+ notFoundHandler: NotFoundHandler | undefined
70
+ /** The app composed from the live mounts; dropped on every change and composed again by the next request. */
71
+ composed: App | undefined
72
+ closing: boolean
73
+ }
74
+
48
75
  interface Flight {
49
76
  readonly controller: AbortController
50
- /** Settles when the handler is done and every `waitUntil` task it registered has settled. */
77
+ /** How many of each mount's handlers are running for this request right now (a middleware counts until its `next()` returns). */
78
+ readonly running: Map<Mount, number>
79
+ /** The mount whose handler produced the response; its body is that mount's code still running. */
80
+ producer: Mount | undefined
81
+ /** Ends the response body early (disposal); a no-op once the body is done or when there is none. */
82
+ readonly endBody: () => void
83
+ /** Settles when the handler is done, its body has been read or cancelled, and every `waitUntil` task it registered has settled. */
51
84
  readonly settled: Promise<unknown>
52
85
  }
53
86
 
87
+ /** Wait for `tasks`, including any added while waiting (a stream may register more after the handler returned). */
88
+ async function drain(tasks: readonly Promise<unknown>[]): Promise<void> {
89
+ let seen = 0
90
+ while (seen < tasks.length) {
91
+ const batch = tasks.slice(seen)
92
+ seen = tasks.length
93
+ await Promise.allSettled(batch)
94
+ }
95
+ }
96
+
54
97
  export class Router extends Service {
55
- private readonly table: RouteTable
98
+ /** Everything that changes after construction, kept in one object so the service's live state is in one place. */
99
+ private readonly state: RouterState = { mounts: new Set(), errorHandler: undefined, notFoundHandler: undefined, composed: undefined, closing: false }
56
100
  private readonly flights = new Set<Flight>()
57
- private closing = false
101
+ /** The flight of each request the router dispatched, keyed by the request its handlers see as `c.req.raw`. */
102
+ private readonly flightOf = new WeakMap<Request, Flight>()
103
+ private readonly strict: boolean
58
104
  private readonly graceMs: number
105
+ private readonly logError: ErrorHandler
59
106
 
60
107
  constructor(ctx: Context, config: Partial<Config> = {}) {
61
108
  super(ctx, 'router')
62
109
  // The schema supplies these defaults when the Loader mounts the package; a plugin object mounted by hand skips the schema.
63
110
  const { trailingSlash = 'strict', disposeGraceMs = DEFAULT_DISPOSE_GRACE_MS } = config
111
+ this.strict = trailingSlash === 'strict'
64
112
  this.graceMs = disposeGraceMs
65
- this.table = new RouteTable({
66
- trailingSlash,
67
- onError: error => ctx.logger.error(`acryl-router: handler failed: ${error instanceof Error ? error.message : String(error)}`),
68
- })
113
+ this.logError = (error, c) => {
114
+ if (error instanceof HTTPException) return error.getResponse()
115
+ // A handler that gave up because its caller left or the router is closing is not a failure worth logging.
116
+ if (c.req.raw.signal.aborted) return new Response('Client Closed Request', { status: 499 })
117
+ ctx.logger.error(`acryl-router: handler failed: ${error instanceof Error ? error.message : String(error)}`)
118
+ return c.text('Internal Server Error', 500)
119
+ }
69
120
  ctx.effect(() => async () => {
70
- this.closing = true
71
- const open = [...this.flights]
72
- for (const flight of open) flight.controller.abort()
73
- let timer: ReturnType<typeof setTimeout> | undefined
74
- const gaveUp = new Promise<'gave-up'>(resolve => { timer = setTimeout(() => { resolve('gave-up') }, this.graceMs) })
75
- const outcome = await Promise.race([Promise.allSettled(open.map(flight => flight.settled)).then(() => 'settled' as const), gaveUp])
76
- clearTimeout(timer)
77
- if (outcome === 'gave-up') ctx.logger.warn(`acryl-router: ${String(this.flights.size)} request(s) did not finish ${String(this.graceMs)} ms after abort; disposal continued without them`)
121
+ this.state.closing = true
122
+ await this.end([...this.flights], 'disposal')
78
123
  }, 'acryl-router: abort and settle in-flight requests')
79
124
  }
80
125
 
81
- /** Routes plus middleware currently registered; a leak check for reload tests. */
126
+ /** Registrations currently held (mounts, and the router-wide error and not-found handlers); a leak check for reload tests. */
82
127
  get size(): number {
83
- return this.table.size
128
+ return this.state.mounts.size + (this.state.errorHandler === undefined ? 0 : 1) + (this.state.notFoundHandler === undefined ? 0 : 1)
84
129
  }
85
130
 
86
- private readonly primitives: Primitives = {
87
- add: (owner, method, path, handler) => {
88
- const remove = this.table.add(method, path, handler)
89
- owner.effect(() => () => { remove() }, `acryl-router: ${method} ${path}`)
90
- return remove
91
- },
92
- use: (owner, path, middleware) => {
93
- const remove = this.table.use(path, middleware)
94
- owner.effect(() => () => { remove() }, `acryl-router: use ${path}`)
95
- return remove
96
- },
131
+ /**
132
+ * Serve `app` under `path` (`'/'` for its own paths as they are). The mount belongs to the calling plugin and goes away when it unloads;
133
+ * the returned function removes it sooner. Mounts are composed in the order they were made. Like the core's own `app.route`, the routes
134
+ * are copied now: one the app registers after this call is not served (mount again, or finish the app first).
135
+ */
136
+ route(path: string, app: MountableApp): () => void {
137
+ const mount = this.snapshot(path, app)
138
+ this.state.mounts.add(mount)
139
+ this.state.composed = undefined
140
+ // Removing a mount ends the requests its code is still serving: their signals abort, a body it produced is cancelled, and the
141
+ // plugin's unload waits for them (bounded by `disposeGraceMs`), so nothing a plugin started outlives it.
142
+ const remove = (): Promise<void> => {
143
+ if (!this.state.mounts.delete(mount)) return Promise.resolve()
144
+ this.state.composed = undefined
145
+ return this.end([...this.flights].filter(flight => (flight.running.get(mount) ?? 0) > 0 || flight.producer === mount), `removing ${path}`)
146
+ }
147
+ // Each route the mount serves is a child effect of it, labeled on its own, so the inspector can report routes without asking the
148
+ // router and no label grows with the size of the app (`ALL` is middleware or `all`). The children hold nothing: the mount does.
149
+ // (Cordis adopts an effect as a child only when the parent's body yields it.)
150
+ const served = new Set(app.routes.map(route => `${route.method} ${mergePath(path, route.path)}`))
151
+ const owner = this.ctx
152
+ owner.effect(function* () {
153
+ for (const route of served) yield owner.effect(() => () => {}, `acryl-router: serves ${route}`)
154
+ yield remove
155
+ }, `acryl-router: route ${path}`)
156
+ return () => { void remove() }
97
157
  }
98
158
 
99
- /** The scope the calling plugin registers into; `this.ctx` is the caller's context for the duration of a call made through `ctx.router`. */
100
- private scope(): Registrar {
101
- return createRegistrar(this.ctx, '', this.primitives)
159
+ /**
160
+ * The mount's own copy of `app`'s routes as they are now (like the core's `app.route`), each handler wrapped so the router knows which
161
+ * mount's code a request is in. The core's `app.route` runs first so a sub-app's own `onError` still applies to its routes.
162
+ */
163
+ private snapshot(path: string, app: MountableApp): Mount {
164
+ const copy = new App().route('/', app)
165
+ const tagged = new App()
166
+ const mount: Mount = { path, app: tagged }
167
+ for (const route of copy.routes) {
168
+ const inner = route.handler
169
+ const handler: H = async (c, next) => {
170
+ const flight = this.flightOf.get(c.req.raw)
171
+ if (flight === undefined) return inner(c, next)
172
+ let calledNext = false
173
+ flight.running.set(mount, (flight.running.get(mount) ?? 0) + 1)
174
+ try {
175
+ const result = await inner(c, () => { calledNext = true; return next() })
176
+ if (!calledNext && result instanceof Response) flight.producer = mount
177
+ return result
178
+ } finally {
179
+ flight.running.set(mount, (flight.running.get(mount) ?? 1) - 1)
180
+ }
181
+ }
182
+ ;(handler as unknown as Record<string, unknown>)[COMPOSED_HANDLER] = inner
183
+ tagged.on(route.method, route.path, handler)
184
+ }
185
+ return mount
102
186
  }
103
187
 
104
- route(method: RouteMethod, path: string, ...handlers: RouteHandlers): () => void { return this.scope().route(method, path, ...handlers) }
105
- get(path: string, ...handlers: RouteHandlers): () => void { return this.scope().get(path, ...handlers) }
106
- post(path: string, ...handlers: RouteHandlers): () => void { return this.scope().post(path, ...handlers) }
107
- put(path: string, ...handlers: RouteHandlers): () => void { return this.scope().put(path, ...handlers) }
108
- patch(path: string, ...handlers: RouteHandlers): () => void { return this.scope().patch(path, ...handlers) }
109
- delete(path: string, ...handlers: RouteHandlers): () => void { return this.scope().delete(path, ...handlers) }
110
- all(path: string, ...handlers: RouteHandlers): () => void { return this.scope().all(path, ...handlers) }
111
- on(methods: Method | readonly Method[], path: string, ...handlers: RouteHandlers): () => void { return this.scope().on(methods, path, ...handlers) }
112
- use(...args: UseArguments): () => void { return this.scope().use(...args) }
113
- group(prefix: string, ...middleware: Middleware[]): Registrar { return this.scope().group(prefix, ...middleware) }
114
-
115
- /** Turn errors into responses. One handler per router; the calling plugin owns it. */
188
+ /** Abort `flights`, cut their bodies short and wait for them to settle, at most `disposeGraceMs`. */
189
+ private async end(flights: readonly Flight[], why: string): Promise<void> {
190
+ if (flights.length === 0) return
191
+ for (const flight of flights) {
192
+ flight.controller.abort()
193
+ flight.endBody()
194
+ }
195
+ let timer: ReturnType<typeof setTimeout> | undefined
196
+ const gaveUp = new Promise<'gave-up'>(resolve => { timer = setTimeout(() => { resolve('gave-up') }, this.graceMs) })
197
+ const outcome = await Promise.race([Promise.allSettled(flights.map(flight => flight.settled)).then(() => 'settled' as const), gaveUp])
198
+ clearTimeout(timer)
199
+ if (outcome === 'gave-up') this.ctx.logger.warn(`acryl-router: ${String(flights.length)} request(s) did not finish ${String(this.graceMs)} ms after abort (${why}); continued without them`)
200
+ }
201
+
202
+ /** Turn errors no mounted app handled into responses. One handler per router; the calling plugin owns it. */
116
203
  onError(handler: ErrorHandler): () => void {
117
- const remove = this.table.setErrorHandler(handler)
118
- this.ctx.effect(() => () => { remove() }, 'acryl-router: onError')
204
+ if (this.state.errorHandler !== undefined) throw new Error('router: an error handler is already registered')
205
+ this.state.errorHandler = handler
206
+ this.state.composed = undefined
207
+ const remove = (): void => {
208
+ if (this.state.errorHandler !== handler) return
209
+ this.state.errorHandler = undefined
210
+ this.state.composed = undefined
211
+ }
212
+ this.ctx.effect(() => remove, 'acryl-router: onError')
119
213
  return remove
120
214
  }
121
215
 
122
216
  /** Answer requests no route claimed. One handler per router; the calling plugin owns it. */
123
217
  notFound(handler: NotFoundHandler): () => void {
124
- const remove = this.table.setNotFoundHandler(handler)
125
- this.ctx.effect(() => () => { remove() }, 'acryl-router: notFound')
218
+ if (this.state.notFoundHandler !== undefined) throw new Error('router: a not-found handler is already registered')
219
+ this.state.notFoundHandler = handler
220
+ this.state.composed = undefined
221
+ const remove = (): void => {
222
+ if (this.state.notFoundHandler !== handler) return
223
+ this.state.notFoundHandler = undefined
224
+ this.state.composed = undefined
225
+ }
226
+ this.ctx.effect(() => remove, 'acryl-router: notFound')
126
227
  return remove
127
228
  }
128
229
 
@@ -136,20 +237,43 @@ export class Router extends Service {
136
237
  }
137
238
 
138
239
  /**
139
- * Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes
140
- * the transport (an HTTP adapter passes the socket's peer address); an in-process caller omits it.
240
+ * Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes the transport
241
+ * (an HTTP adapter passes the socket's peer address and local port); an in-process caller omits it. Handlers read it with
242
+ * `connectionOf(c)`, abort with `c.req.raw.signal` and keep the request in flight with `c.executionCtx.waitUntil(task)`.
141
243
  */
142
244
  fetch(request: Request, connection: ConnectionInfo = {}): Promise<Response> {
143
- if (this.closing) return Promise.resolve(new Response('Service Unavailable', { status: 503 }))
245
+ if (this.state.closing) return Promise.resolve(new Response('Service Unavailable', { status: 503 }))
144
246
  const controller = new AbortController()
145
247
  const forward = (): void => controller.abort()
146
248
  if (request.signal.aborted) controller.abort()
147
249
  else request.signal.addEventListener('abort', forward, { once: true })
148
250
  const tasks: Promise<unknown>[] = []
149
- // The caller gets the Response the moment the handler returns it; the flight lasts until every waitUntil task settles.
150
- const response = this.table.dispatch(request, controller.signal, connection, task => { tasks.push(task) })
151
- const settled = response.then(() => Promise.allSettled(tasks))
152
- const flight: Flight = { controller, settled }
251
+ const executionCtx: ExecutionContext = {
252
+ waitUntil: task => { tasks.push(task) },
253
+ passThroughOnException: () => {},
254
+ props: {},
255
+ }
256
+ const env: RouterBindings = { connection }
257
+ let bodyDone: Promise<void> = Promise.resolve()
258
+ let endBody = (): void => {}
259
+ let resolveSettled!: (value: Promise<unknown>) => void
260
+ const flight: Flight = { controller, running: new Map(), producer: undefined, endBody: () => { endBody() }, settled: new Promise(resolve => { resolveSettled = resolve }) }
261
+ const inner = new Request(request, { signal: controller.signal })
262
+ this.flightOf.set(inner, flight)
263
+ // The caller gets the Response the moment the handler returns it; the flight lasts until its body ends and every waitUntil task settles.
264
+ const response = this.dispatch(inner, env, executionCtx).then(raw => {
265
+ const tracked = this.track(raw)
266
+ bodyDone = tracked.done
267
+ endBody = tracked.end
268
+ // Aborted before the handler answered (disposal, or the caller left): nobody is going to read this body, so end it now.
269
+ if (controller.signal.aborted) tracked.end()
270
+ return tracked.response
271
+ })
272
+ const settled = response.then(async () => {
273
+ await bodyDone
274
+ await drain(tasks)
275
+ })
276
+ resolveSettled(settled)
153
277
  this.flights.add(flight)
154
278
  void settled.finally(() => {
155
279
  request.signal.removeEventListener('abort', forward)
@@ -157,6 +281,80 @@ export class Router extends Service {
157
281
  })
158
282
  return response
159
283
  }
284
+
285
+ private async dispatch(request: Request, env: RouterBindings, executionCtx: ExecutionContext): Promise<Response> {
286
+ try {
287
+ return await this.app().fetch(request, env, executionCtx)
288
+ } catch (error) {
289
+ this.ctx.logger.error(`acryl-router: request failed: ${error instanceof Error ? error.message : String(error)}`)
290
+ return new Response('Internal Server Error', { status: 500 })
291
+ }
292
+ }
293
+
294
+ private app(): App {
295
+ if (this.state.composed !== undefined) return this.state.composed
296
+ // The trie router, because the composed app is rebuilt after every mount change: the default matcher compiles one regular expression
297
+ // over all routes on its first request, which grows faster than the route count (measured: 22 ms at 1000 routes, 163 ms at 2000),
298
+ // while the trie rebuilds in about 1 ms per 1000 routes and matches as fast (scripts/bench-remount.mjs).
299
+ const root = new App({ strict: this.strict, router: new TrieRouter() })
300
+ for (const mount of this.state.mounts) root.route(mount.path, mount.app)
301
+ root.onError(this.state.errorHandler ?? this.logError)
302
+ if (this.state.notFoundHandler !== undefined) root.notFound(this.state.notFoundHandler)
303
+ this.state.composed = root
304
+ return root
305
+ }
306
+
307
+ /**
308
+ * Hand the caller a body the router can end. Reading it passes the handler's chunks through; disposal cancels the handler's stream (which runs
309
+ * the stream helpers' own abort cleanup) and closes the caller's side, so a stream never outlives the plugin that served it.
310
+ */
311
+ private track(response: Response): { response: Response, done: Promise<void>, end: () => void } {
312
+ const source = response.body
313
+ if (source === null) return { response, done: Promise.resolve(), end: () => {} }
314
+ const reader = source.getReader()
315
+ let finish!: () => void
316
+ const done = new Promise<void>(resolve => { finish = resolve })
317
+ let ended = false
318
+ let sink: ReadableStreamDefaultController<Uint8Array> | undefined
319
+ const end = (): void => {
320
+ if (ended) return
321
+ ended = true
322
+ finish()
323
+ }
324
+ const endEarly = (): void => {
325
+ if (ended) return
326
+ reader.cancel(new Error('acryl-router: disposed')).catch(() => {})
327
+ try { sink?.close() } catch { /* already closed */ }
328
+ end()
329
+ }
330
+ const body = new ReadableStream<Uint8Array>({
331
+ start: controller => { sink = controller },
332
+ pull: async controller => {
333
+ try {
334
+ const chunk = await reader.read()
335
+ if (ended) return
336
+ if (chunk.done) {
337
+ controller.close()
338
+ end()
339
+ } else {
340
+ controller.enqueue(chunk.value)
341
+ }
342
+ } catch (error) {
343
+ if (!ended) controller.error(error)
344
+ end()
345
+ }
346
+ },
347
+ cancel: async reason => {
348
+ end()
349
+ await reader.cancel(reason)
350
+ },
351
+ })
352
+ return {
353
+ response: new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }),
354
+ done,
355
+ end: endEarly,
356
+ }
357
+ }
160
358
  }
161
359
 
162
360
  export function apply(ctx: Context, config: Partial<Config> = {}): void {
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The examples `docs/framework/` relies on, run as written: if one of these fails, the page that shows it is wrong.
3
+ */
4
+
5
+ import { Context } from '@deepseek-ai/cordis'
6
+ import z from '@deepseek-ai/schemastery'
7
+ import { afterEach, describe, expect, it } from 'vitest'
8
+ import { createMiddleware, schemaValidator, testClient } from '../src/helpers/index.ts'
9
+ import { App, HTTPException, apply, name } from '../src/index.ts'
10
+ import { methodNotAllowed } from '../src/middleware/index.ts'
11
+
12
+ const contexts: Context[] = []
13
+ const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
14
+ afterEach(async () => {
15
+ while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
16
+ })
17
+
18
+ async function withPlugin(plugin: { name: string, inject: string[], apply(ctx: Context): void }): Promise<Context> {
19
+ const ctx = new Context()
20
+ contexts.push(ctx)
21
+ ctx.plugin({ name, apply })
22
+ ctx.plugin(plugin)
23
+ await settle()
24
+ return ctx
25
+ }
26
+
27
+ describe('getting-started.md', () => {
28
+ it('serves the minimal plugin as the route table says', async () => {
29
+ const ctx = await withPlugin({
30
+ name: 'todo',
31
+ inject: ['router'],
32
+ apply(inner) {
33
+ const app = new App()
34
+ .get('/', c => c.json({ ok: true }))
35
+ .get('/todos/:id', c => c.json({ id: c.req.param('id') }))
36
+ inner.router.route('/todo', app)
37
+ inner.inject(['routerHttp'], deeper => { deeper.routerHttp.mount('/todo') })
38
+ },
39
+ })
40
+ expect(await (await ctx.router.request('/todo')).json()).toEqual({ ok: true })
41
+ expect(await (await ctx.router.request('/todo/todos/42')).json()).toEqual({ id: '42' })
42
+ expect((await ctx.router.request('/todo/')).status).toBe(404)
43
+ })
44
+ })
45
+
46
+ describe('routing.md', () => {
47
+ it('serves the routes an app had at the mount, not ones added later', async () => {
48
+ const app = new App().get('/a', c => c.text('a'))
49
+ const ctx = await withPlugin({ name: 'late', inject: ['router'], apply(inner) { inner.router.route('/x', app) } })
50
+ app.get('/b', c => c.text('b'))
51
+ expect((await ctx.router.request('/x/a')).status).toBe(200)
52
+ expect((await ctx.router.request('/x/b')).status).toBe(404)
53
+ })
54
+
55
+ it('answers a wrong method with 404, or 405 and Allow with methodNotAllowed', async () => {
56
+ const plain = new App().get('/item', c => c.text('item'))
57
+ expect((await plain.request('/item', { method: 'POST' })).status).toBe(404)
58
+ const strict = new App()
59
+ strict.use(methodNotAllowed({ app: strict })).get('/item', c => c.text('item'))
60
+ const response = await strict.request('/item', { method: 'POST' })
61
+ expect(response.status).toBe(405)
62
+ expect(response.headers.get('allow')).toContain('GET')
63
+
64
+ const ctx = await withPlugin({ name: 'items', inject: ['router'], apply(inner) { inner.router.route('/shop', strict) } })
65
+ const mounted = await ctx.router.request('/shop/item', { method: 'POST' })
66
+ expect(mounted.status).toBe(405)
67
+ expect(mounted.headers.get('allow')).toContain('GET')
68
+ expect((await ctx.router.request('/shop/item')).status).toBe(200)
69
+ })
70
+
71
+ it('uses a mounted app\'s onError for its routes', async () => {
72
+ const app = new App().get('/boom', () => { throw new Error('boom') })
73
+ app.onError((_error, c) => c.json({ error: 'internal_error' }, 500))
74
+ const ctx = await withPlugin({ name: 'errors', inject: ['router'], apply(inner) { inner.router.route('/', app) } })
75
+ expect(await (await ctx.router.request('/boom')).json()).toEqual({ error: 'internal_error' })
76
+ })
77
+ })
78
+
79
+ describe('context.md and middleware.md', () => {
80
+ it('passes typed variables from middleware to the handler', async () => {
81
+ const withUser = createMiddleware<{ Variables: { user: { name: string } } }>(async (c, next) => {
82
+ c.set('user', { name: 'ada' })
83
+ await next()
84
+ })
85
+ const app = new App().use(withUser).get('/me', c => c.json(c.var.user))
86
+ expect(await (await app.request('/me')).json()).toEqual({ name: 'ada' })
87
+ })
88
+
89
+ it('never throws from next(): the error is already a response and is left in c.error', async () => {
90
+ let caught = false
91
+ let seen: unknown
92
+ const app = new App()
93
+ .use(async (c, next) => {
94
+ try {
95
+ await next()
96
+ } catch {
97
+ caught = true
98
+ }
99
+ seen = c.error
100
+ })
101
+ .get('/', () => { throw new Error('boom') })
102
+ expect((await app.request('/')).status).toBe(500)
103
+ expect(caught).toBe(false)
104
+ expect(seen).toBeInstanceOf(Error)
105
+ })
106
+
107
+ it('turns an HTTPException into its own response', async () => {
108
+ const app = new App().get('/', () => { throw new HTTPException(403, { message: 'not yours' }) })
109
+ const response = await app.request('/')
110
+ expect(response.status).toBe(403)
111
+ expect(await response.text()).toBe('not yours')
112
+ })
113
+ })
114
+
115
+ describe('validation.md', () => {
116
+ const NewTodo = z.object({
117
+ title: z.string().required(),
118
+ done: z.boolean().default(false),
119
+ })
120
+ const app = new App().post('/todos', schemaValidator('json', NewTodo), c => c.json({ todo: c.req.valid('json') }, 201))
121
+ const post = (body: string, type?: string): Promise<Response> =>
122
+ Promise.resolve(app.request('/todos', { method: 'POST', body, headers: type === undefined ? {} : { 'content-type': type } }))
123
+
124
+ it('hands the handler the schema output', async () => {
125
+ const response = await post('{"title":"hello"}', 'application/json')
126
+ expect(response.status).toBe(201)
127
+ expect(await response.json()).toEqual({ todo: { title: 'hello', done: false } })
128
+ })
129
+
130
+ it('answers 400 with the issues the page shows', async () => {
131
+ const response = await post('{"title":1}', 'application/json')
132
+ expect(response.status).toBe(400)
133
+ expect(await response.json()).toEqual({ error: 'invalid json', issues: [{ message: '$.title expected string but got 1', path: 'title' }] })
134
+ })
135
+
136
+ it('validates {} when the request is not JSON, so a required field fails', async () => {
137
+ expect((await post('{"title":"hello"}')).status).toBe(400)
138
+ })
139
+
140
+ it('answers 400 for a body that is not JSON', async () => {
141
+ expect((await post('{', 'application/json')).status).toBe(400)
142
+ })
143
+ })
144
+
145
+ describe('helpers.md', () => {
146
+ it('calls a chained app through the typed test client', async () => {
147
+ const app = new App().get('/search', c => c.json({ query: c.req.query('q') }))
148
+ const client = testClient(app)
149
+ const response = await client.search.$get({ query: { q: 'cordis' } })
150
+ expect(await response.json()).toEqual({ query: 'cordis' })
151
+ })
152
+ })
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Context, Service } from '@deepseek-ai/cordis'
9
9
  import { afterEach, describe, expect, it } from 'vitest'
10
- import { apply, name } from '../src/index.ts'
10
+ import { App, apply, name } from '../src/index.ts'
11
11
 
12
12
  const contexts: Context[] = []
13
13
  const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
@@ -46,7 +46,7 @@ const extension = (): object => ({
46
46
  name: 'live-ext',
47
47
  inject: ['router'],
48
48
  apply(ctx: Context): void {
49
- ctx.router.get('/live', c => c.json({ ok: true }))
49
+ ctx.router.route('/', new App().get('/live', c => c.json({ ok: true })))
50
50
  },
51
51
  })
52
52
 
@@ -11,6 +11,7 @@ import { join } from 'node:path'
11
11
  import { Context, type FiberState } from '@deepseek-ai/cordis'
12
12
  import { Loader } from '@deepseek-ai/cordis-plugin-loader'
13
13
  import { afterEach, describe, expect, it } from 'vitest'
14
+ import { App } from '../src/index.ts'
14
15
 
15
16
  const ACTIVE = 2 as FiberState.ACTIVE
16
17
  const DISPOSED = 4 as FiberState.DISPOSED
@@ -45,7 +46,7 @@ describe('acryl-router through a real Loader', () => {
45
46
  it('activates from the built entry and serves a request in process', async () => {
46
47
  const { ctx, entryId } = await mount()
47
48
  expect(entryOf(ctx, entryId).fiber?.state).toBe(ACTIVE)
48
- ctx.router.get('/ping', c => c.json({ ok: true }))
49
+ ctx.router.route('/', new App().get('/ping', c => c.json({ ok: true })))
49
50
  const response = await ctx.router.fetch(new Request('http://acryl.test/ping'))
50
51
  expect(await response.json()).toEqual({ ok: true })
51
52
  })
@@ -65,7 +66,7 @@ describe('acryl-router through a real Loader', () => {
65
66
  expect(ctx.router).not.toBe(first)
66
67
  expect(ctx.router.size).toBe(0)
67
68
  expect((await first.fetch(new Request('http://acryl.test/x'))).status).toBe(503)
68
- ctx.router.get('/again', c => c.text('ok'))
69
+ ctx.router.route('/', new App().get('/again', c => c.text('ok')))
69
70
  expect(await (await ctx.router.fetch(new Request('http://acryl.test/again'))).text()).toBe('ok')
70
71
  })
71
72
  })