acrylblends 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +22 -16
- package/dist/index.js +65 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/LICENSE +21 -0
- package/plugins/acryl-inspect/plugin.json +12 -0
- package/plugins/acryl-inspect/src/index.ts +3 -0
- package/plugins/acryl-inspect/src/report.ts +126 -0
- package/plugins/acryl-inspect/src/service.ts +68 -0
- package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
- package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
- package/plugins/acryl-inspect/upstream.json +18 -0
- package/plugins/acryl-node-server/LICENSE +47 -0
- package/plugins/acryl-node-server/plugin.json +13 -0
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
- package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
- package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
- package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
- package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
- package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
- package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
- package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
- package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
- package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
- package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
- package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
- package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
- package/plugins/acryl-node-server/src/index.ts +107 -0
- package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
- package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +141 -0
- package/plugins/acryl-node-server/upstream.json +18 -0
- package/plugins/acryl-router/LICENSE +47 -0
- package/plugins/acryl-router/plugin.json +13 -0
- package/plugins/acryl-router/src/client.ts +2 -0
- package/plugins/acryl-router/src/connection.ts +42 -0
- package/plugins/acryl-router/src/core/app-base.ts +544 -0
- package/plugins/acryl-router/src/core/app.test.ts +3833 -0
- package/plugins/acryl-router/src/core/app.ts +34 -0
- package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
- package/plugins/acryl-router/src/core/client/client.ts +249 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
- package/plugins/acryl-router/src/core/client/index.ts +17 -0
- package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
- package/plugins/acryl-router/src/core/client/types.ts +393 -0
- package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
- package/plugins/acryl-router/src/core/client/utils.ts +117 -0
- package/plugins/acryl-router/src/core/compose.test.ts +751 -0
- package/plugins/acryl-router/src/core/compose.ts +73 -0
- package/plugins/acryl-router/src/core/context.test.ts +629 -0
- package/plugins/acryl-router/src/core/context.ts +781 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
- package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
- package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
- package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
- package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
- package/plugins/acryl-router/src/core/http-exception.ts +77 -0
- package/plugins/acryl-router/src/core/index.ts +53 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
- package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
- package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
- package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
- package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
- package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
- package/plugins/acryl-router/src/core/request/constants.ts +1 -0
- package/plugins/acryl-router/src/core/request.test.ts +751 -0
- package/plugins/acryl-router/src/core/request.ts +502 -0
- package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
- package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
- package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
- package/plugins/acryl-router/src/core/router/utils.ts +1 -0
- package/plugins/acryl-router/src/core/router.ts +103 -0
- package/plugins/acryl-router/src/core/types.test.ts +4134 -0
- package/plugins/acryl-router/src/core/types.ts +2782 -0
- package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
- package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
- package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
- package/plugins/acryl-router/src/core/utils/body.ts +263 -0
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
- package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
- package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
- package/plugins/acryl-router/src/core/utils/color.ts +56 -0
- package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
- package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
- package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
- package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
- package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
- package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
- package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
- package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
- package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
- package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
- package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
- package/plugins/acryl-router/src/core/utils/html.ts +182 -0
- package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
- package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
- package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
- package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
- package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
- package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
- package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
- package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
- package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
- package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
- package/plugins/acryl-router/src/core/utils/types.ts +123 -0
- package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
- package/plugins/acryl-router/src/core/utils/url.ts +337 -0
- package/plugins/acryl-router/src/core/validator/index.ts +8 -0
- package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
- package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
- package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
- package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
- package/plugins/acryl-router/src/helpers/index.ts +13 -0
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/index.ts +25 -0
- package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
- package/plugins/acryl-router/src/service.ts +356 -0
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
- package/plugins/acryl-router/tests/loader.spec.ts +72 -0
- package/plugins/acryl-router/tests/service.spec.ts +409 -0
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +20 -0
- package/plugins/acryl-router-http/LICENSE +21 -0
- package/plugins/acryl-router-http/plugin.json +12 -0
- package/plugins/acryl-router-http/src/bridge.ts +54 -0
- package/plugins/acryl-router-http/src/index.ts +63 -0
- package/plugins/acryl-router-http/src/serve.ts +24 -0
- package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
- package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
- package/plugins/acryl-router-http/upstream.json +22 -0
- package/plugins/acryl-state/LICENSE +21 -0
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +12 -0
- package/plugins/acryl-state/src/cell.ts +67 -0
- package/plugins/acryl-state/src/index.ts +3 -0
- package/plugins/acryl-state/src/react.ts +11 -0
- package/plugins/acryl-state/src/service.ts +119 -0
- package/plugins/acryl-state/tests/cell.spec.ts +71 -0
- package/plugins/acryl-state/tests/react.spec.ts +39 -0
- package/plugins/acryl-state/tests/service.spec.ts +132 -0
- package/plugins/acryl-state/upstream.json +22 -0
- package/plugins/builder/script.ts +6 -10
- package/plugins/extensions/index.ts +20 -3
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +12 -6
- package/plugins/tasks/index.ts +14 -0
- package/plugins/tsconfig.json +14 -5
- package/templates/agent/blend.yaml +5 -7
- package/templates/agent/src/app.ts +8 -8
- package/templates/agent/src/routes.ts +39 -38
- package/templates/agent/template.json +3 -3
- package/templates/agent/test/builder.test.ts +22 -14
- package/templates/agent/test/model.test.ts +3 -3
- package/templates/agent/tsconfig.json +8 -5
- package/templates/{app → blank}/blend.yaml +3 -3
- package/templates/{app → blank}/src/app.ts +5 -5
- package/templates/blank/src/routes.ts +16 -0
- package/templates/blank/template.json +1 -0
- package/templates/{app → blank}/test/app.test.ts +1 -1
- package/templates/{app → blank}/tsconfig.json +2 -4
- package/templates/todo/README.md +27 -0
- package/templates/todo/blend.yaml +25 -0
- package/templates/todo/gitignore +2 -0
- package/templates/todo/package.json +23 -0
- package/templates/todo/src/app.ts +71 -0
- package/templates/todo/src/main.ts +19 -0
- package/templates/todo/src/page.ts +28 -0
- package/templates/todo/src/todo.ts +123 -0
- package/templates/todo/template.json +1 -0
- package/templates/todo/test/todo.test.ts +145 -0
- package/templates/todo/tsconfig.json +17 -0
- package/templates/todo/upstream.json +12 -0
- package/plugins/events/index.ts +0 -42
- package/plugins/events/plugin.json +0 -9
- package/plugins/http/index.ts +0 -64
- package/plugins/http/plugin.json +0 -10
- package/plugins/server/index.ts +0 -138
- package/plugins/server/plugin.json +0 -10
- package/templates/app/src/routes.ts +0 -14
- package/templates/app/template.json +0 -1
- /package/templates/{app → blank}/README.md +0 -0
- /package/templates/{app → blank}/gitignore +0 -0
- /package/templates/{app → blank}/package.json +0 -0
- /package/templates/{app → blank}/src/main.ts +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ACRYL router service (`ctx.router`): plugin-owned `App` mounts on Web-standard Request and Response.
|
|
3
|
+
*
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
11
|
+
import z from '@deepseek-ai/schemastery'
|
|
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'
|
|
20
|
+
|
|
21
|
+
const DEFAULT_DISPOSE_GRACE_MS = 2000
|
|
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
|
+
|
|
26
|
+
/** Validated plugin configuration. */
|
|
27
|
+
export interface Config {
|
|
28
|
+
trailingSlash: TrailingSlash
|
|
29
|
+
disposeGraceMs: number
|
|
30
|
+
}
|
|
31
|
+
|
|
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({
|
|
34
|
+
trailingSlash: z.union(['strict', 'ignore']).default('strict').description('Whether `/a` and `/a/` are different paths.'),
|
|
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.'),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The Web adapter's service (`ctx.routerHttp`, provided by `acryl-router-http`). Declared here, beside `router`, so a plugin can ask for its routes to be
|
|
40
|
+
* served over HTTP without depending on the adapter package.
|
|
41
|
+
*/
|
|
42
|
+
export interface RouterHttpService {
|
|
43
|
+
/** Serve `prefix` and everything below it through the router; returns a remover. The mount also goes away when the calling plugin unloads. */
|
|
44
|
+
mount(prefix: string): () => void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare module '@deepseek-ai/cordis' {
|
|
48
|
+
interface Context {
|
|
49
|
+
router: Router
|
|
50
|
+
routerHttp: RouterHttpService
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
55
|
+
export const name = 'acryl-router'
|
|
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
|
+
|
|
75
|
+
interface Flight {
|
|
76
|
+
readonly controller: AbortController
|
|
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. */
|
|
84
|
+
readonly settled: Promise<unknown>
|
|
85
|
+
}
|
|
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
|
+
|
|
97
|
+
export class Router extends Service {
|
|
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 }
|
|
100
|
+
private readonly flights = new Set<Flight>()
|
|
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
|
|
104
|
+
private readonly graceMs: number
|
|
105
|
+
private readonly logError: ErrorHandler
|
|
106
|
+
|
|
107
|
+
constructor(ctx: Context, config: Partial<Config> = {}) {
|
|
108
|
+
super(ctx, 'router')
|
|
109
|
+
// The schema supplies these defaults when the Loader mounts the package; a plugin object mounted by hand skips the schema.
|
|
110
|
+
const { trailingSlash = 'strict', disposeGraceMs = DEFAULT_DISPOSE_GRACE_MS } = config
|
|
111
|
+
this.strict = trailingSlash === 'strict'
|
|
112
|
+
this.graceMs = disposeGraceMs
|
|
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
|
+
}
|
|
120
|
+
ctx.effect(() => async () => {
|
|
121
|
+
this.state.closing = true
|
|
122
|
+
await this.end([...this.flights], 'disposal')
|
|
123
|
+
}, 'acryl-router: abort and settle in-flight requests')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Registrations currently held (mounts, and the router-wide error and not-found handlers); a leak check for reload tests. */
|
|
127
|
+
get size(): number {
|
|
128
|
+
return this.state.mounts.size + (this.state.errorHandler === undefined ? 0 : 1) + (this.state.notFoundHandler === undefined ? 0 : 1)
|
|
129
|
+
}
|
|
130
|
+
|
|
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
|
+
// The label lists what the mount serves, so the inspector can report routes without asking the router (`ALL` is middleware or `all`).
|
|
148
|
+
const served = app.routes.map(route => `${route.method} ${mergePath(path, route.path)}`).join(', ')
|
|
149
|
+
this.ctx.effect(() => remove, `acryl-router: route ${path} (${served})`)
|
|
150
|
+
return () => { void remove() }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 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
|
|
155
|
+
* 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.
|
|
156
|
+
*/
|
|
157
|
+
private snapshot(path: string, app: MountableApp): Mount {
|
|
158
|
+
const copy = new App().route('/', app)
|
|
159
|
+
const tagged = new App()
|
|
160
|
+
const mount: Mount = { path, app: tagged }
|
|
161
|
+
for (const route of copy.routes) {
|
|
162
|
+
const inner = route.handler
|
|
163
|
+
const handler: H = async (c, next) => {
|
|
164
|
+
const flight = this.flightOf.get(c.req.raw)
|
|
165
|
+
if (flight === undefined) return inner(c, next)
|
|
166
|
+
let calledNext = false
|
|
167
|
+
flight.running.set(mount, (flight.running.get(mount) ?? 0) + 1)
|
|
168
|
+
try {
|
|
169
|
+
const result = await inner(c, () => { calledNext = true; return next() })
|
|
170
|
+
if (!calledNext && result instanceof Response) flight.producer = mount
|
|
171
|
+
return result
|
|
172
|
+
} finally {
|
|
173
|
+
flight.running.set(mount, (flight.running.get(mount) ?? 1) - 1)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
;(handler as unknown as Record<string, unknown>)[COMPOSED_HANDLER] = inner
|
|
177
|
+
tagged.on(route.method, route.path, handler)
|
|
178
|
+
}
|
|
179
|
+
return mount
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Abort `flights`, cut their bodies short and wait for them to settle, at most `disposeGraceMs`. */
|
|
183
|
+
private async end(flights: readonly Flight[], why: string): Promise<void> {
|
|
184
|
+
if (flights.length === 0) return
|
|
185
|
+
for (const flight of flights) {
|
|
186
|
+
flight.controller.abort()
|
|
187
|
+
flight.endBody()
|
|
188
|
+
}
|
|
189
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
190
|
+
const gaveUp = new Promise<'gave-up'>(resolve => { timer = setTimeout(() => { resolve('gave-up') }, this.graceMs) })
|
|
191
|
+
const outcome = await Promise.race([Promise.allSettled(flights.map(flight => flight.settled)).then(() => 'settled' as const), gaveUp])
|
|
192
|
+
clearTimeout(timer)
|
|
193
|
+
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`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Turn errors no mounted app handled into responses. One handler per router; the calling plugin owns it. */
|
|
197
|
+
onError(handler: ErrorHandler): () => void {
|
|
198
|
+
if (this.state.errorHandler !== undefined) throw new Error('router: an error handler is already registered')
|
|
199
|
+
this.state.errorHandler = handler
|
|
200
|
+
this.state.composed = undefined
|
|
201
|
+
const remove = (): void => {
|
|
202
|
+
if (this.state.errorHandler !== handler) return
|
|
203
|
+
this.state.errorHandler = undefined
|
|
204
|
+
this.state.composed = undefined
|
|
205
|
+
}
|
|
206
|
+
this.ctx.effect(() => remove, 'acryl-router: onError')
|
|
207
|
+
return remove
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Answer requests no route claimed. One handler per router; the calling plugin owns it. */
|
|
211
|
+
notFound(handler: NotFoundHandler): () => void {
|
|
212
|
+
if (this.state.notFoundHandler !== undefined) throw new Error('router: a not-found handler is already registered')
|
|
213
|
+
this.state.notFoundHandler = handler
|
|
214
|
+
this.state.composed = undefined
|
|
215
|
+
const remove = (): void => {
|
|
216
|
+
if (this.state.notFoundHandler !== handler) return
|
|
217
|
+
this.state.notFoundHandler = undefined
|
|
218
|
+
this.state.composed = undefined
|
|
219
|
+
}
|
|
220
|
+
this.ctx.effect(() => remove, 'acryl-router: notFound')
|
|
221
|
+
return remove
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Call the router the way a test calls an app: a path or URL string, a `URL` or a `Request`, with the usual init.
|
|
226
|
+
* A relative path is resolved against `http://localhost`.
|
|
227
|
+
*/
|
|
228
|
+
request(input: string | URL | Request, init?: RequestInit, connection?: ConnectionInfo): Promise<Response> {
|
|
229
|
+
const request = input instanceof Request ? input : new Request(new URL(String(input), 'http://localhost'), init)
|
|
230
|
+
return this.fetch(request, connection)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes the transport
|
|
235
|
+
* (an HTTP adapter passes the socket's peer address and local port); an in-process caller omits it. Handlers read it with
|
|
236
|
+
* `connectionOf(c)`, abort with `c.req.raw.signal` and keep the request in flight with `c.executionCtx.waitUntil(task)`.
|
|
237
|
+
*/
|
|
238
|
+
fetch(request: Request, connection: ConnectionInfo = {}): Promise<Response> {
|
|
239
|
+
if (this.state.closing) return Promise.resolve(new Response('Service Unavailable', { status: 503 }))
|
|
240
|
+
const controller = new AbortController()
|
|
241
|
+
const forward = (): void => controller.abort()
|
|
242
|
+
if (request.signal.aborted) controller.abort()
|
|
243
|
+
else request.signal.addEventListener('abort', forward, { once: true })
|
|
244
|
+
const tasks: Promise<unknown>[] = []
|
|
245
|
+
const executionCtx: ExecutionContext = {
|
|
246
|
+
waitUntil: task => { tasks.push(task) },
|
|
247
|
+
passThroughOnException: () => {},
|
|
248
|
+
props: {},
|
|
249
|
+
}
|
|
250
|
+
const env: RouterBindings = { connection }
|
|
251
|
+
let bodyDone: Promise<void> = Promise.resolve()
|
|
252
|
+
let endBody = (): void => {}
|
|
253
|
+
let resolveSettled!: (value: Promise<unknown>) => void
|
|
254
|
+
const flight: Flight = { controller, running: new Map(), producer: undefined, endBody: () => { endBody() }, settled: new Promise(resolve => { resolveSettled = resolve }) }
|
|
255
|
+
const inner = new Request(request, { signal: controller.signal })
|
|
256
|
+
this.flightOf.set(inner, flight)
|
|
257
|
+
// The caller gets the Response the moment the handler returns it; the flight lasts until its body ends and every waitUntil task settles.
|
|
258
|
+
const response = this.dispatch(inner, env, executionCtx).then(raw => {
|
|
259
|
+
const tracked = this.track(raw)
|
|
260
|
+
bodyDone = tracked.done
|
|
261
|
+
endBody = tracked.end
|
|
262
|
+
// Aborted before the handler answered (disposal, or the caller left): nobody is going to read this body, so end it now.
|
|
263
|
+
if (controller.signal.aborted) tracked.end()
|
|
264
|
+
return tracked.response
|
|
265
|
+
})
|
|
266
|
+
const settled = response.then(async () => {
|
|
267
|
+
await bodyDone
|
|
268
|
+
await drain(tasks)
|
|
269
|
+
})
|
|
270
|
+
resolveSettled(settled)
|
|
271
|
+
this.flights.add(flight)
|
|
272
|
+
void settled.finally(() => {
|
|
273
|
+
request.signal.removeEventListener('abort', forward)
|
|
274
|
+
this.flights.delete(flight)
|
|
275
|
+
})
|
|
276
|
+
return response
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async dispatch(request: Request, env: RouterBindings, executionCtx: ExecutionContext): Promise<Response> {
|
|
280
|
+
try {
|
|
281
|
+
return await this.app().fetch(request, env, executionCtx)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
this.ctx.logger.error(`acryl-router: request failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
284
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private app(): App {
|
|
289
|
+
if (this.state.composed !== undefined) return this.state.composed
|
|
290
|
+
// The trie router, because the composed app is rebuilt after every mount change: the default matcher compiles one regular expression
|
|
291
|
+
// over all routes on its first request, which grows faster than the route count (measured: 22 ms at 1000 routes, 163 ms at 2000),
|
|
292
|
+
// while the trie rebuilds in about 1 ms per 1000 routes and matches as fast (scripts/bench-remount.mjs).
|
|
293
|
+
const root = new App({ strict: this.strict, router: new TrieRouter() })
|
|
294
|
+
for (const mount of this.state.mounts) root.route(mount.path, mount.app)
|
|
295
|
+
root.onError(this.state.errorHandler ?? this.logError)
|
|
296
|
+
if (this.state.notFoundHandler !== undefined) root.notFound(this.state.notFoundHandler)
|
|
297
|
+
this.state.composed = root
|
|
298
|
+
return root
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 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
|
|
303
|
+
* the stream helpers' own abort cleanup) and closes the caller's side, so a stream never outlives the plugin that served it.
|
|
304
|
+
*/
|
|
305
|
+
private track(response: Response): { response: Response, done: Promise<void>, end: () => void } {
|
|
306
|
+
const source = response.body
|
|
307
|
+
if (source === null) return { response, done: Promise.resolve(), end: () => {} }
|
|
308
|
+
const reader = source.getReader()
|
|
309
|
+
let finish!: () => void
|
|
310
|
+
const done = new Promise<void>(resolve => { finish = resolve })
|
|
311
|
+
let ended = false
|
|
312
|
+
let sink: ReadableStreamDefaultController<Uint8Array> | undefined
|
|
313
|
+
const end = (): void => {
|
|
314
|
+
if (ended) return
|
|
315
|
+
ended = true
|
|
316
|
+
finish()
|
|
317
|
+
}
|
|
318
|
+
const endEarly = (): void => {
|
|
319
|
+
if (ended) return
|
|
320
|
+
reader.cancel(new Error('acryl-router: disposed')).catch(() => {})
|
|
321
|
+
try { sink?.close() } catch { /* already closed */ }
|
|
322
|
+
end()
|
|
323
|
+
}
|
|
324
|
+
const body = new ReadableStream<Uint8Array>({
|
|
325
|
+
start: controller => { sink = controller },
|
|
326
|
+
pull: async controller => {
|
|
327
|
+
try {
|
|
328
|
+
const chunk = await reader.read()
|
|
329
|
+
if (ended) return
|
|
330
|
+
if (chunk.done) {
|
|
331
|
+
controller.close()
|
|
332
|
+
end()
|
|
333
|
+
} else {
|
|
334
|
+
controller.enqueue(chunk.value)
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!ended) controller.error(error)
|
|
338
|
+
end()
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
cancel: async reason => {
|
|
342
|
+
end()
|
|
343
|
+
await reader.cancel(reason)
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
return {
|
|
347
|
+
response: new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }),
|
|
348
|
+
done,
|
|
349
|
+
end: endEarly,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function apply(ctx: Context, config: Partial<Config> = {}): void {
|
|
355
|
+
new Router(ctx, config)
|
|
356
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounting a plugin after startup from inside a Service method (what an Extensions service does). Inside a service method `this.ctx` is Cordis's
|
|
3
|
+
* "shadow" context, which resolves services as the SERVICE'S OWN fiber; a child plugin mounted with it inherits that shadow, so its `ctx.router`
|
|
4
|
+
* is looked up in the installer's fiber (which never injected it) and throws "cannot get property router without inject", even though the child's
|
|
5
|
+
* own `fiber.inject` and `ctx.get('router')` are fine. The context captured in the constructor is not shadowed and mounts a plugin that behaves
|
|
6
|
+
* exactly like one mounted at startup.
|
|
7
|
+
*/
|
|
8
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
9
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
10
|
+
import { App, apply, name } from '../src/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
|
+
class Installer extends Service {
|
|
19
|
+
private readonly owner: Context
|
|
20
|
+
constructor(ctx: Context) {
|
|
21
|
+
super(ctx, 'installer')
|
|
22
|
+
this.owner = ctx
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Right: mount from the context captured in the constructor. */
|
|
26
|
+
async install(module: object): Promise<void> {
|
|
27
|
+
await this.owner.plugin(module as { apply(ctx: Context): void })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Wrong: `this.ctx` inside a method is the shadow context. */
|
|
31
|
+
async installFromShadow(module: object): Promise<void> {
|
|
32
|
+
await this.ctx.plugin(module as { apply(ctx: Context): void })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function system(): Promise<Context & { installer: Installer }> {
|
|
37
|
+
const root = new Context()
|
|
38
|
+
contexts.push(root)
|
|
39
|
+
root.plugin({ name, apply })
|
|
40
|
+
root.plugin({ name: 'installer', apply: (ctx: Context) => { new Installer(ctx) } })
|
|
41
|
+
await settle()
|
|
42
|
+
return root as Context & { installer: Installer }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const extension = (): object => ({
|
|
46
|
+
name: 'live-ext',
|
|
47
|
+
inject: ['router'],
|
|
48
|
+
apply(ctx: Context): void {
|
|
49
|
+
ctx.router.route('/', new App().get('/live', c => c.json({ ok: true })))
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('live install from a service method', () => {
|
|
54
|
+
it('works from the constructor context: the plugin reads ctx.router like one mounted at startup', async () => {
|
|
55
|
+
const root = await system()
|
|
56
|
+
await root.installer.install(extension())
|
|
57
|
+
expect(await (await root.router.request('/live')).json()).toEqual({ ok: true })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('fails from this.ctx, which is Cordis behavior the framework docs warn about', async () => {
|
|
61
|
+
const root = await system()
|
|
62
|
+
await expect(root.installer.installFromShadow(extension())).rejects.toThrow(/cannot get property "router" without inject/u)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The router mounted the way production mounts every plugin: a real `@deepseek-ai/cordis-plugin-loader` Loader row
|
|
3
|
+
* pointing at the built `lib/index.js`. Covers what a hand-built `ctx.plugin` cannot: the row id equals the package
|
|
4
|
+
* name, the `cordis.patch.yml` insert is loadable, and disabling and enabling the row ten times leaves nothing behind.
|
|
5
|
+
* @throws if the package has not been built: run `pnpm run build` first.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { tmpdir } from 'node:os'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { Context, type FiberState } from '@deepseek-ai/cordis'
|
|
12
|
+
import { Loader } from '@deepseek-ai/cordis-plugin-loader'
|
|
13
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
14
|
+
import { App } from '../src/index.ts'
|
|
15
|
+
|
|
16
|
+
const ACTIVE = 2 as FiberState.ACTIVE
|
|
17
|
+
const DISPOSED = 4 as FiberState.DISPOSED
|
|
18
|
+
const BUILT_ENTRY = new URL('../lib/index.js', import.meta.url).href
|
|
19
|
+
|
|
20
|
+
const roots: string[] = []
|
|
21
|
+
const contexts: Context[] = []
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
|
|
24
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
async function mount(): Promise<{ ctx: Context, entryId: string }> {
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), 'acryl-router-loader-'))
|
|
29
|
+
roots.push(root)
|
|
30
|
+
writeFileSync(join(root, 'row.mjs'), `export * from ${JSON.stringify(BUILT_ENTRY)}\n`)
|
|
31
|
+
const ctx = new Context()
|
|
32
|
+
contexts.push(ctx)
|
|
33
|
+
await ctx.plugin(Loader, { baseUrl: `file://${root}/` })
|
|
34
|
+
const entryId = await ctx.loader.create({ name: './row.mjs' })
|
|
35
|
+
await ctx.loader.await()
|
|
36
|
+
return { ctx, entryId }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function entryOf(ctx: Context, entryId: string) {
|
|
40
|
+
const found = [...ctx.loader.entries()].find(candidate => candidate.id === entryId)
|
|
41
|
+
if (found === undefined) throw new Error('entry not found')
|
|
42
|
+
return found
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('acryl-router through a real Loader', () => {
|
|
46
|
+
it('activates from the built entry and serves a request in process', async () => {
|
|
47
|
+
const { ctx, entryId } = await mount()
|
|
48
|
+
expect(entryOf(ctx, entryId).fiber?.state).toBe(ACTIVE)
|
|
49
|
+
ctx.router.route('/', new App().get('/ping', c => c.json({ ok: true })))
|
|
50
|
+
const response = await ctx.router.fetch(new Request('http://acryl.test/ping'))
|
|
51
|
+
expect(await response.json()).toEqual({ ok: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('answers 503 and drops its routes when the row is disabled, and serves again when re-enabled, ten times', async () => {
|
|
55
|
+
const { ctx, entryId } = await mount()
|
|
56
|
+
const first = ctx.router
|
|
57
|
+
for (let i = 0; i < 10; i += 1) {
|
|
58
|
+
await entryOf(ctx, entryId).update({ disabled: true })
|
|
59
|
+
await ctx.loader.await()
|
|
60
|
+
const fiber = entryOf(ctx, entryId).fiber
|
|
61
|
+
expect(fiber === undefined || fiber.state === DISPOSED).toBe(true)
|
|
62
|
+
await entryOf(ctx, entryId).update({ disabled: false })
|
|
63
|
+
await ctx.loader.await()
|
|
64
|
+
expect(entryOf(ctx, entryId).fiber?.state).toBe(ACTIVE)
|
|
65
|
+
}
|
|
66
|
+
expect(ctx.router).not.toBe(first)
|
|
67
|
+
expect(ctx.router.size).toBe(0)
|
|
68
|
+
expect((await first.fetch(new Request('http://acryl.test/x'))).status).toBe(503)
|
|
69
|
+
ctx.router.route('/', new App().get('/again', c => c.text('ok')))
|
|
70
|
+
expect(await (await ctx.router.fetch(new Request('http://acryl.test/again'))).text()).toBe('ok')
|
|
71
|
+
})
|
|
72
|
+
})
|