diff --git a/.prettierrc b/.prettierrc index 0967ef42..1ca87ab7 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1,3 @@ -{} +{ + "singleQuote": false +} diff --git a/README.md b/README.md index bbbc6b3c..db9bb3ec 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Rad ```js // Handle can directly return object or Promise for JSON response -app.use('/api', eventHandler((event) => ({ url: event.node.req.url }))) +app.use('/api', eventHandler((event) => ({ url: getUrlPath(event) }))) // We can have better matching other than quick prefix match app.use('/odd', eventHandler(() => 'Is odd!'), { match: url => url.substr(1) % 2 }) diff --git a/package.json b/package.json index 6904149b..2fc6a135 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "h3", - "version": "1.6.6", - "description": "Tiny JavaScript Server", - "repository": "unjs/h3", + "name": "@hebilicious/h3", + "version": "0.0.4", + "description": "Fork of unjs/h3", + "repository": "Hebilicious/h3", "license": "MIT", "sideEffects": false, "exports": { @@ -25,9 +25,10 @@ "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test playground", "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test playground -w", "play": "jiti ./playground/index.ts", + "script": "jiti ./playground/script.ts", "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs", "release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags", - "test": "pnpm lint && vitest run --coverage" + "test": "pnpm lint && NODE_OPTIONS=--experimental-vm-modules vitest --coverage" }, "dependencies": { "cookie-es": "^1.0.0", @@ -40,6 +41,7 @@ }, "devDependencies": { "0x": "^5.5.0", + "@cloudflare/workers-types": "^4.20230518.0", "@types/express": "^4.17.17", "@types/node": "^20.1.4", "@types/supertest": "^2.0.12", @@ -53,12 +55,14 @@ "get-port": "^7.0.0", "jiti": "^1.18.2", "listhen": "^1.0.4", + "miniflare": "^3.0.1", "node-fetch-native": "^1.1.1", "prettier": "^2.8.8", "supertest": "^6.3.3", "typescript": "^5.0.4", "unbuild": "^1.2.1", - "vitest": "^0.31.1" + "vitest": "^0.31.1", + "vitest-environment-miniflare": "^2.14.0" }, "packageManager": "pnpm@8.5.1" -} \ No newline at end of file +} diff --git a/playground/script.ts b/playground/script.ts new file mode 100644 index 00000000..76274312 --- /dev/null +++ b/playground/script.ts @@ -0,0 +1,21 @@ +import { createApp, createRouter, eventHandler, adapterFetch } from "../src"; + +const main = async () => { + const app = createApp(); + const router = createRouter(); + router.get( + "/hello", + eventHandler((event) => { + const request = event.request; // Request object, from the fetch API + return new Response(`hello ${request.method}`); + }) + ); + app.use(router); + const fetchResponse = adapterFetch(app); + const response = await fetchResponse( + new Request(new URL("http://localhost/hello")) + ); + console.log(await response.text()); // hello GET + return response; +}; +main(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9343be4..2ebbaa2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ devDependencies: 0x: specifier: ^5.5.0 version: 5.5.0 + '@cloudflare/workers-types': + specifier: ^4.20230518.0 + version: 4.20230518.0 '@types/express': specifier: ^4.17.17 version: 4.17.17 @@ -66,6 +69,9 @@ devDependencies: listhen: specifier: ^1.0.4 version: 1.0.4 + miniflare: + specifier: ^3.0.1 + version: 3.0.1 node-fetch-native: specifier: ^1.1.1 version: 1.1.1 @@ -84,6 +90,9 @@ devDependencies: vitest: specifier: ^0.31.1 version: 0.31.1 + vitest-environment-miniflare: + specifier: ^2.14.0 + version: 2.14.0(vitest@0.31.1) packages: @@ -340,6 +349,55 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@cloudflare/workerd-darwin-64@1.20230518.0: + resolution: {integrity: sha512-reApIf2/do6GjLlajU6LbRYh8gm/XcaRtzGbF8jo5IzyDSsdStmfNuvq7qssZXG92219Yp1kuTgR9+D1GGZGbg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-darwin-arm64@1.20230518.0: + resolution: {integrity: sha512-1l+xdbmPddqb2YIHd1YJ3YG/Fl1nhayzcxfL30xfNS89zJn9Xn3JomM0XMD4mk0d5GruBP3q8BQZ1Uo4rRLF3A==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-linux-64@1.20230518.0: + resolution: {integrity: sha512-/pfR+YBpMOPr2cAlwjtInil0hRZjD8KX9LqK9JkfkEiaBH8CYhnJQcOdNHZI+3OjcY09JnQtEVC5xC4nbW7Bvw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-linux-arm64@1.20230518.0: + resolution: {integrity: sha512-q3HQvn3J4uEkE0cfDAGG8zqzSZrD47cavB/Tzv4mNutqwg6B4wL3ifjtGeB55tnP2K2KL0GVmX4tObcvpUF4BA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workerd-windows-64@1.20230518.0: + resolution: {integrity: sha512-vNEHKS5gKKduNOBYtQjcBopAmFT1iScuPWMZa2nJboSjOB9I/5oiVsUpSyk5Y2ARyrohXNz0y8D7p87YzTASWw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@cloudflare/workers-types@4.20230518.0: + resolution: {integrity: sha512-A0w1V+5SUawGaaPRlhFhSC/SCDT9oQG8TMoWOKFLA4qbqagELqEAFD4KySBIkeVOvCBLT1DZSYBMCxbXddl0kw==} + dev: true + /@colors/colors@1.5.0: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -602,6 +660,10 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@iarna/toml@2.2.5: + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + dev: true + /@istanbuljs/schema@0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} @@ -641,6 +703,166 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@miniflare/cache@2.14.0: + resolution: {integrity: sha512-0mz0OCzTegiX75uMURLJpDo3DaOCSx9M0gv7NMFWDbK/XrvjoENiBZiKu98UBM5fts0qtK19a+MfB4aT0uBCFg==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/core': 2.14.0 + '@miniflare/shared': 2.14.0 + http-cache-semantics: 4.1.1 + undici: 5.20.0 + dev: true + + /@miniflare/core@2.14.0: + resolution: {integrity: sha512-BjmV/ZDwsKvXnJntYHt3AQgzVKp/5ZzWPpYWoOnUSNxq6nnRCQyvFvjvBZKnhubcmJCLSqegvz0yHejMA90CTA==} + engines: {node: '>=16.13'} + dependencies: + '@iarna/toml': 2.2.5 + '@miniflare/queues': 2.14.0 + '@miniflare/shared': 2.14.0 + '@miniflare/watcher': 2.14.0 + busboy: 1.6.0 + dotenv: 10.0.0 + kleur: 4.1.5 + set-cookie-parser: 2.6.0 + undici: 5.20.0 + urlpattern-polyfill: 4.0.3 + dev: true + + /@miniflare/d1@2.14.0: + resolution: {integrity: sha512-9YoeLAkZuWGAu9BMsoctHoMue0xHzJYZigAJWGvWrqSFT1gBaT+RlUefQCHXggi8P7sOJ1+BKlsWAhkB5wfMWQ==} + engines: {node: '>=16.7'} + dependencies: + '@miniflare/core': 2.14.0 + '@miniflare/shared': 2.14.0 + dev: true + + /@miniflare/durable-objects@2.14.0: + resolution: {integrity: sha512-P8eh1P62BPGpj+MCb1i1lj7Tlt/G3BMmnxHp9duyb0Wro/ILVGPQskZl+iq7DHq1w3C+n0+6/E1B44ff+qn0Mw==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/core': 2.14.0 + '@miniflare/shared': 2.14.0 + '@miniflare/storage-memory': 2.14.0 + undici: 5.20.0 + dev: true + + /@miniflare/html-rewriter@2.14.0: + resolution: {integrity: sha512-7CJZk3xZkxK8tGNofnhgWcChZ8YLx6MhAdN2nn6ONSXrK/TevzEKdL8bnVv1OJ6J8Y23OxvfinOhufr33tMS8g==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/core': 2.14.0 + '@miniflare/shared': 2.14.0 + html-rewriter-wasm: 0.4.1 + undici: 5.20.0 + dev: true + + /@miniflare/kv@2.14.0: + resolution: {integrity: sha512-FHAnVjmhV/VHxgjNf2whraz+k7kfMKlfM+5gO8WT6HrOsWxSdx8OueWVScnOuuDkSeUg5Ctrf5SuztTV8Uy1cg==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/shared': 2.14.0 + dev: true + + /@miniflare/queues@2.14.0: + resolution: {integrity: sha512-flS4MqlgBKyv6QBqKD0IofjmMDW9wP1prUNQy2wWPih9lA6bFKmml3VdFeDsPnWtE2J67K0vCTf5kj1Q0qdW1w==} + engines: {node: '>=16.7'} + dependencies: + '@miniflare/shared': 2.14.0 + dev: true + + /@miniflare/r2@2.14.0: + resolution: {integrity: sha512-+WJJP4J0QzY69HPrG6g5OyW23lJ02WHpHZirCxwPSz8CajooqZCJVx+qvUcNmU8MyKASbUZMWnH79LysuBh+jA==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/core': 2.14.0 + '@miniflare/shared': 2.14.0 + undici: 5.20.0 + dev: true + + /@miniflare/runner-vm@2.14.0: + resolution: {integrity: sha512-01CmNzv74u0RZgT/vjV/ggDzECXTG88ZJAKhXyhAx0s2DOLIXzsGHn6pUJIsfPCrtj8nfqtTCp1Vf0UMVWSpmw==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/shared': 2.14.0 + dev: true + + /@miniflare/shared-test-environment@2.14.0: + resolution: {integrity: sha512-Iarxqo9hR4Gi6i7iF/hXJbHuPCXTZbA4z91Gwhet8dhK6c0zWGU3xi7zjr5XTaawd/cuR1g6bi0cwt/10bAEsg==} + engines: {node: '>=16.13'} + dependencies: + '@cloudflare/workers-types': 4.20230518.0 + '@miniflare/cache': 2.14.0 + '@miniflare/core': 2.14.0 + '@miniflare/d1': 2.14.0 + '@miniflare/durable-objects': 2.14.0 + '@miniflare/html-rewriter': 2.14.0 + '@miniflare/kv': 2.14.0 + '@miniflare/queues': 2.14.0 + '@miniflare/r2': 2.14.0 + '@miniflare/shared': 2.14.0 + '@miniflare/sites': 2.14.0 + '@miniflare/storage-memory': 2.14.0 + '@miniflare/web-sockets': 2.14.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@miniflare/shared@2.14.0: + resolution: {integrity: sha512-O0jAEdMkp8BzrdFCfMWZu76h4Cq+tt3/oDtcTFgzum3fRW5vUhIi/5f6bfndu6rkGbSlzxwor8CJWpzityXGug==} + engines: {node: '>=16.13'} + dependencies: + '@types/better-sqlite3': 7.6.4 + kleur: 4.1.5 + npx-import: 1.1.4 + picomatch: 2.3.1 + dev: true + + /@miniflare/sites@2.14.0: + resolution: {integrity: sha512-qI8MFZpD1NV+g+HQ/qheDVwscKzwG58J+kAVTU/1fgub2lMLsxhE3Mmbi5AIpyIiJ7Q5Sezqga234CEkHkS7dA==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/kv': 2.14.0 + '@miniflare/shared': 2.14.0 + '@miniflare/storage-file': 2.14.0 + dev: true + + /@miniflare/storage-file@2.14.0: + resolution: {integrity: sha512-Ps0wHhTO+ie33a58efI0p/ppFXSjlbYmykQXfYtMeVLD60CKl+4Lxor0+gD6uYDFbhMWL5/GMDvyr4AM87FA+Q==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/shared': 2.14.0 + '@miniflare/storage-memory': 2.14.0 + dev: true + + /@miniflare/storage-memory@2.14.0: + resolution: {integrity: sha512-5aFjEiTSNrHJ+iAiGMCA/TVPnNMrnokG5r0vKrwj4knbf8pisgfP04x18zCgOlG7kaIWNmqdO/vtVT5BIioiSQ==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/shared': 2.14.0 + dev: true + + /@miniflare/watcher@2.14.0: + resolution: {integrity: sha512-O8Abg2eHpGmcZb8WyUaA6Av1Mqt5bSrorzz4CrWwsvJHBdekZPIX0GihC9vn327d/1pKRs81YTiSAfBoSZpVIw==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/shared': 2.14.0 + dev: true + + /@miniflare/web-sockets@2.14.0: + resolution: {integrity: sha512-lB1CB4rBq0mbCuh55WgIEH4L3c4/i4MNDBfrQL+6r+wGcr/BJUqF8BHpsfAt5yHWUJVtK5mlMeesS/xpg4Ao1w==} + engines: {node: '>=16.13'} + dependencies: + '@miniflare/core': 2.14.0 + '@miniflare/shared': 2.14.0 + undici: 5.20.0 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -765,6 +987,12 @@ packages: rollup: 3.21.7 dev: true + /@types/better-sqlite3@7.6.4: + resolution: {integrity: sha512-dzrRZCYPXIXfSR1/surNbJ/grU3scTaygS0OMzjlGf71i9sc2fGyHPXXiXmEvNIoE0cGwsanEFMVJxPXmco9Eg==} + dependencies: + '@types/node': 20.1.4 + dev: true + /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: @@ -1245,6 +1473,12 @@ packages: es-shim-unscopables: 1.0.0 dev: true + /as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + dependencies: + printable-characters: 1.0.42 + dev: true + /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} dev: true @@ -1315,6 +1549,14 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true + /better-sqlite3@8.4.0: + resolution: {integrity: sha512-NmsNW1CQvqMszu/CFAJ3pLct6NEFlNfuGM6vw72KHkjOD1UDnL96XNN1BMQc1hiHo8vE2GbOWQYIpZ+YM5wrZw==} + requiresBuild: true + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.1 + dev: true + /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -1325,6 +1567,20 @@ packages: engines: {node: '>=8'} dev: true + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + dependencies: + file-uri-to-path: 1.0.0 + dev: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + /blueimp-md5@2.19.0: resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} dev: true @@ -1546,6 +1802,13 @@ packages: ieee754: 1.2.1 dev: true + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -1568,6 +1831,13 @@ packages: run-applescript: 5.0.0 dev: true + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: true + /bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1642,6 +1912,15 @@ packages: resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==} dev: true + /capnp-ts@0.7.0: + resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} + dependencies: + debug: 4.3.4 + tslib: 2.5.0 + transitivePeerDependencies: + - supports-color + dev: true + /chai@4.3.7: resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} engines: {node: '>=4'} @@ -1733,6 +2012,10 @@ packages: fsevents: 2.3.2 dev: true + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true + /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -2124,6 +2407,10 @@ packages: resolution: {integrity: sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==} dev: true + /data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + dev: true + /date-time@3.1.0: resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} engines: {node: '>=6'} @@ -2169,6 +2456,13 @@ packages: ms: 2.1.2 dev: true + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -2176,6 +2470,11 @@ packages: type-detect: 4.0.8 dev: true + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -2258,6 +2557,11 @@ packages: engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dev: true + /detect-libc@2.0.1: + resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} + engines: {node: '>=8'} + dev: true + /detective@5.2.1: resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} engines: {node: '>=0.8.0'} @@ -2309,6 +2613,11 @@ packages: engines: {node: '>=0.4', npm: '>=1.2'} dev: true + /dotenv@10.0.0: + resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} + engines: {node: '>=10'} + dev: true + /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} @@ -2899,6 +3208,21 @@ packages: strip-final-newline: 2.0.0 dev: true + /execa@6.1.0: + resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 3.0.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + /execa@7.1.1: resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} @@ -2920,6 +3244,16 @@ packages: util-extend: 1.0.3 dev: true + /exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + dev: true + + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: true + /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -3003,6 +3337,10 @@ packages: flat-cache: 3.0.4 dev: true + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: true + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -3115,6 +3453,10 @@ packages: engines: {node: '>= 0.6'} dev: true + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: true + /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -3206,6 +3548,13 @@ packages: engines: {node: '>=16'} dev: true + /get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + dev: true + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -3238,6 +3587,10 @@ packages: - supports-color dev: true + /github-from-package@0.0.0: + resolution: {integrity: sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=} + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -3252,6 +3605,10 @@ packages: is-glob: 4.0.3 dev: true + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -3451,11 +3808,19 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /html-rewriter-wasm@0.4.1: + resolution: {integrity: sha512-lNovG8CMCCmcVB1Q7xggMSf7tqPCijZXaH4gL6iE8BFghdQCbaY5Met9i1x2Ex8m/cZHDUtXK9H6/znKamRP8Q==} + dev: true + /htmlescape@1.1.1: resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==} engines: {node: '>=0.10'} dev: true + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -3495,6 +3860,11 @@ packages: engines: {node: '>=10.17.0'} dev: true + /human-signals@3.0.1: + resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} + engines: {node: '>=12.20.0'} + dev: true + /human-signals@4.3.1: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} @@ -3566,6 +3936,10 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + /inline-source-map@0.6.2: resolution: {integrity: sha512-0mVWSSbNDvedDWIN4wxLsdPM4a7cIPcpyMxj3QZ406QRwQ6ePGB1YIHxVPjqpcUGbWQ5C+nHTwGNWAGvt7ggVA==} dependencies: @@ -3957,6 +4331,11 @@ packages: type-component: 0.0.1 dev: true + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + /labeled-stream-splicer@2.0.2: resolution: {integrity: sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==} dependencies: @@ -4180,11 +4559,41 @@ packages: engines: {node: '>=12'} dev: true + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} dev: true + /miniflare@3.0.1: + resolution: {integrity: sha512-aLOB8d26lOTn493GOv1LmpGHVLSxmeT4MixPG/k3Ze10j0wDKnMj8wsFgbZ6Q4cr1N4faf8O3IbNRJuQ+rLoJA==} + engines: {node: '>=16.13'} + dependencies: + acorn: 8.8.2 + acorn-walk: 8.2.0 + better-sqlite3: 8.4.0 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + http-cache-semantics: 4.1.1 + kleur: 4.1.5 + source-map-support: 0.5.21 + stoppable: 1.1.0 + undici: 5.20.0 + workerd: 1.20230518.0 + ws: 8.13.0 + youch: 3.2.3 + zod: 3.21.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} dev: true @@ -4315,6 +4724,11 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true + /mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + dev: true + /mutexify@1.4.0: resolution: {integrity: sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==} dependencies: @@ -4357,6 +4771,10 @@ packages: hasBin: true dev: true + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: true + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -4376,6 +4794,13 @@ packages: lower-case: 1.1.4 dev: true + /node-abi@3.40.0: + resolution: {integrity: sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.1 + dev: true + /node-fetch-native@1.1.1: resolution: {integrity: sha512-9VvspTSUp2Sxbl+9vbZTlFGq9lHwE8GDVVekxx6YsNd1YH59sb3Ba8v3Y3cD8PkLNcileGGcA21PFjVl0jzDaw==} dev: true @@ -4422,6 +4847,15 @@ packages: path-key: 4.0.0 dev: true + /npx-import@1.1.4: + resolution: {integrity: sha512-3ShymTWOgqGyNlh5lMJAejLuIv3W1K3fbI5Ewc6YErZU3Sp0PqsNs8UIU1O8z5+KVl/Du5ag56Gza9vdorGEoA==} + dependencies: + execa: 6.1.0 + parse-package-name: 1.0.0 + semver: 7.5.1 + validate-npm-package-name: 4.0.0 + dev: true + /number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} engines: {node: '>=0.10.0'} @@ -4621,6 +5055,10 @@ packages: lines-and-columns: 1.2.4 dev: true + /parse-package-name@1.0.0: + resolution: {integrity: sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg==} + dev: true + /parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4722,6 +5160,25 @@ packages: source-map-js: 1.0.2 dev: true + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.1 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.40.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4757,6 +5214,10 @@ packages: engines: {node: '>= 0.8'} dev: true + /printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + dev: true + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -4891,6 +5352,16 @@ packages: flat: 5.0.2 dev: true + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: true @@ -5134,6 +5605,10 @@ packages: - supports-color dev: true + /set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + dev: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: true @@ -5188,6 +5663,14 @@ packages: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} dev: true + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: true + /single-line-log@1.1.2: resolution: {integrity: sha512-awzaaIPtYFdexLr6TBpcZSGPB6D1RInNO/qNetgaJloPDF/D0GkVtLvGEp8InfmLV7CyLyQ5fIRP+tVN/JmWQA==} dependencies: @@ -5209,11 +5692,23 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + /source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} dev: true + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead @@ -5250,6 +5745,13 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + dev: true + /statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -5264,6 +5766,11 @@ packages: resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} dev: true + /stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + dev: true + /stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} dependencies: @@ -5298,6 +5805,11 @@ packages: readable-stream: 2.3.8 dev: true + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: true + /string-width@1.0.2: resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} engines: {node: '>=0.10.0'} @@ -5389,6 +5901,11 @@ packages: min-indent: 1.0.1 dev: true + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -5481,6 +5998,26 @@ packages: engines: {node: '>=6'} dev: true + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: true + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + /tar@6.1.14: resolution: {integrity: sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw==} engines: {node: '>=10'} @@ -5626,6 +6163,12 @@ packages: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} dev: true + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -5749,6 +6292,13 @@ packages: xtend: 4.0.2 dev: true + /undici@5.20.0: + resolution: {integrity: sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==} + engines: {node: '>=12.18'} + dependencies: + busboy: 1.6.0 + dev: true + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -5807,6 +6357,10 @@ packages: querystring: 0.2.0 dev: true + /urlpattern-polyfill@4.0.3: + resolution: {integrity: sha512-DOE84vZT2fEcl9gqCUTcnAw5ZY5Id55ikUcziSUntuEFL3pRvavg5kwDmTEUJkeCHInTlV/HexFomgYnzO5kdQ==} + dev: true + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true @@ -5861,6 +6415,13 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validate-npm-package-name@4.0.0: + resolution: {integrity: sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + builtins: 5.0.1 + dev: true + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -5920,6 +6481,23 @@ packages: fsevents: 2.3.2 dev: true + /vitest-environment-miniflare@2.14.0(vitest@0.31.1): + resolution: {integrity: sha512-VvIW693nkDWy9n6wxGazltWvpbreyBcG/3ntLbrf5yF0MkzaaaVKiaAfbLQ1Y6DieGDMazHKD50kW98zA5Ur8A==} + engines: {node: '>=16.13'} + peerDependencies: + vitest: '>=0.23.0' + dependencies: + '@miniflare/queues': 2.14.0 + '@miniflare/runner-vm': 2.14.0 + '@miniflare/shared': 2.14.0 + '@miniflare/shared-test-environment': 2.14.0 + undici: 5.20.0 + vitest: 0.31.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /vitest@0.31.1: resolution: {integrity: sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==} engines: {node: '>=v14.18.0'} @@ -6038,6 +6616,19 @@ packages: engines: {node: '>=0.10.0'} dev: true + /workerd@1.20230518.0: + resolution: {integrity: sha512-VNmK0zoNZXrwEEx77O/oQDVUzzyDjf5kKKK8bty+FmKCd5EQJCpqi8NlRKWLGMyyYrKm86MFz0kAsreTEs7HHA==} + engines: {node: '>=16'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20230518.0 + '@cloudflare/workerd-darwin-arm64': 1.20230518.0 + '@cloudflare/workerd-linux-64': 1.20230518.0 + '@cloudflare/workerd-linux-arm64': 1.20230518.0 + '@cloudflare/workerd-windows-64': 1.20230518.0 + dev: true + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -6051,6 +6642,19 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -6101,3 +6705,15 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /youch@3.2.3: + resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} + dependencies: + cookie: 0.5.0 + mustache: 4.2.0 + stacktracey: 2.1.8 + dev: true + + /zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: true diff --git a/src/adapters.ts b/src/adapters.ts new file mode 100644 index 00000000..0d46c521 --- /dev/null +++ b/src/adapters.ts @@ -0,0 +1,43 @@ +import { App, createEvent } from "./"; + +/** + * Adapter that helps return a Response from a Request. + * It can be used with something like Nitro. + * const localResponse = adapterFetch(h3app) + * const response = localResponse(request: Request, {context: { cf: request.cf, cloudflare: { request, env, context } }}) + * @param app + * @returns Promise + */ +export const adapterFetch = (app: App) => { + return async (request: Request, context?: Record) => { + try { + const event = createEvent(undefined, undefined, request); + if (context) { + event.context = context; + } + return (await app.handler(event)) as Response; + } catch (error: any) { + return new Response(error.toString(), { + status: Number.parseInt(error.statusCode || error.code) || 500, + statusText: error.statusText, + }); + } + }; +}; + +export const adapterCloudflareWorker = (app: App) => { + const worker = { + async fetch(request: Request, env?: any, context?: any) { + const fetchResponse = adapterFetch(app); + return await fetchResponse(request, { + // @ts-expect-error + cf: request.cf, + cloudflare: { env, context }, + }); + }, + fire: () => { + console.log("implement service worker syntax ..."); + }, + }; + return worker; +}; diff --git a/src/app.ts b/src/app.ts index 475f49aa..0c6c057a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,8 +7,21 @@ import { H3Event, } from "./event"; import { createError } from "./error"; -import { send, sendStream, isStream, MIMES } from "./utils"; +import { + send, + sendStream, + isStream, + MIMES, + sendResponse, + setResponseStatus, +} from "./utils"; import type { EventHandler, LazyEventHandler } from "./types"; +import { + getOriginalUrlPath, + getUrlPath, + setOriginalUrlPath, + setUrlPath, +} from "./utils/url"; export interface Layer { route: string; @@ -95,32 +108,37 @@ export function use( export function createAppEventHandler(stack: Stack, options: AppOptions) { const spacing = options.debug ? 2 : undefined; return eventHandler(async (event) => { - (event.node.req as any).originalUrl = - (event.node.req as any).originalUrl || event.node.req.url || "/"; - const reqUrl = event.node.req.url || "/"; + setOriginalUrlPath( + event, + getOriginalUrlPath(event) || getUrlPath(event) || "/" + ); + const reqUrl = getUrlPath(event) || "/"; for (const layer of stack) { if (layer.route.length > 1) { if (!reqUrl.startsWith(layer.route)) { continue; } - event.node.req.url = reqUrl.slice(layer.route.length) || "/"; + setUrlPath(event, reqUrl.slice(layer.route.length) || "/"); } else { - event.node.req.url = reqUrl; + setUrlPath(event, reqUrl); } - if (layer.match && !layer.match(event.node.req.url as string, event)) { + if (layer.match && !layer.match(getUrlPath(event), event)) { continue; } const val = await layer.handler(event); - if (event.node.res.writableEnded) { + if (event.node?.res?.writableEnded) { return; } const type = typeof val; + if (val instanceof Response) { + return sendResponse(event, val); + } if (type === "string") { return send(event, val, MIMES.html); } else if (isStream(val)) { return sendStream(event, val); } else if (val === null) { - event.node.res.statusCode = 204; + setResponseStatus(event, 204); return send(event); } else if ( type === "object" || @@ -140,11 +158,11 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) { } } } - if (!event.node.res.writableEnded) { + if (!event.node?.res?.writableEnded) { throw createError({ statusCode: 404, statusMessage: `Cannot find any route matching ${ - event.node.req.url || "/" + getUrlPath(event) || "/" }.`, }); } diff --git a/src/error.ts b/src/error.ts index 9c97d215..c7d6fa76 100644 --- a/src/error.ts +++ b/src/error.ts @@ -4,6 +4,9 @@ import { setResponseStatus, sanitizeStatusMessage, sanitizeStatusCode, + setResponseHeader, + sendResponse, + send, } from "./utils"; /** @@ -133,7 +136,7 @@ export function sendError( error: Error | H3Error, debug?: boolean ) { - if (event.node.res.writableEnded) { + if (event.node?.res?.writableEnded) { return; } @@ -150,13 +153,17 @@ export function sendError( responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim()); } - if (event.node.res.writableEnded) { + if (event.node?.res?.writableEnded) { return; } const _code = Number.parseInt(h3Error.statusCode as unknown as string); setResponseStatus(event, _code, h3Error.statusMessage); - event.node.res.setHeader("content-type", MIMES.json); - event.node.res.end(JSON.stringify(responseBody, undefined, 2)); + setResponseHeader(event, "content-type", MIMES.json); + const bodyToSend = JSON.stringify(responseBody, undefined, 2); + if (event.request) { + return sendResponse(event, new Response(bodyToSend)); + } + send(event, bodyToSend); } export function isError(input: any): input is H3Error { diff --git a/src/event/event.ts b/src/event/event.ts index e6974bce..11b88174 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -1,11 +1,7 @@ import type { H3EventContext } from "../types"; import type { NodeIncomingMessage, NodeServerResponse } from "../node"; -import { - MIMES, - sanitizeStatusCode, - sanitizeStatusMessage, - getRequestPath, -} from "../utils"; +import { MIMES, sanitizeStatusCode, sanitizeStatusMessage } from "../utils"; +import { getRequestPath } from "../utils/url"; import { H3Response } from "./response"; export interface NodeEventContext { @@ -13,13 +9,37 @@ export interface NodeEventContext { res: NodeServerResponse; } +interface InternalData { + headers: Map; + status: number; + statusMessage: string; + originalUrlPath: string | undefined; + currentUrlPath: string | undefined; +} export class H3Event implements Pick { "__is_event__" = true; - node: NodeEventContext; - context: H3EventContext = {}; + node!: NodeEventContext; + context: H3EventContext = { __sendRaw: false }; + request!: Request; + _internalData: InternalData = { + headers: new Map(), + status: 200, + statusMessage: "", + originalUrlPath: undefined, + currentUrlPath: undefined, + }; - constructor(req: NodeIncomingMessage, res: NodeServerResponse) { - this.node = { req, res }; + constructor( + req?: NodeIncomingMessage, + res?: NodeServerResponse, + request?: Request + ) { + if (req && res) { + this.node = { req, res }; + } + if (request) { + this.request = request; + } } get path() { @@ -84,8 +104,9 @@ export function isEvent(input: any): input is H3Event { } export function createEvent( - req: NodeIncomingMessage, - res: NodeServerResponse + req?: NodeIncomingMessage, + res?: NodeServerResponse, + request?: Request ): H3Event { - return new H3Event(req, res); + return new H3Event(req, res, request); } diff --git a/src/index.ts b/src/index.ts index 97dbae9d..5ff7706f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from "./app"; +export * from "./adapters"; export * from "./error"; export * from "./event"; export * from "./node"; diff --git a/src/router.ts b/src/router.ts index 063c7483..c0efce4e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -2,6 +2,8 @@ import { createRouter as _createRouter } from "radix3"; import type { HTTPMethod, EventHandler } from "./types"; import { createError } from "./error"; import { eventHandler, toEventHandler } from "./event"; +import { getUrlPath } from "./utils/url"; +import { getMethod } from "./utils"; export type RouterMethod = Lowercase; const RouterMethods: RouterMethod[] = [ @@ -75,7 +77,7 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { // Main handle router.handler = eventHandler((event) => { // Remove query parameters for matching - let path = event.node.req.url || "/"; + let path = getUrlPath(event); const qIndex = path.indexOf("?"); if (qIndex !== -1) { path = path.slice(0, Math.max(0, qIndex)); @@ -88,9 +90,7 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { throw createError({ statusCode: 404, name: "Not Found", - statusMessage: `Cannot find any route matching ${ - event.node.req.url || "/" - }.`, + statusMessage: `Cannot find any route matching ${getUrlPath(event)}.`, }); } else { return; // Let app match other handlers @@ -98,9 +98,7 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { } // Match method - const method = ( - event.node.req.method || "get" - ).toLowerCase() as RouterMethod; + const method = getMethod(event).toLowerCase() as RouterMethod; const handler = matched.handlers[method] || matched.handlers.all; if (!handler) { throw createError({ diff --git a/src/types.ts b/src/types.ts index c3fe7450..bd06f281 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,7 +33,11 @@ export interface H3EventContext extends Record { sessions?: Record; } -export type EventHandlerResponse = T | Promise; +export type EventHandlerResponse = + | T + | Promise + | Promise + | Response; export interface EventHandler { __is_handler__?: true; diff --git a/src/utils/body.ts b/src/utils/body.ts index 52cd9150..b1418fe4 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -2,7 +2,8 @@ import destr from "destr"; import type { Encoding, HTTPMethod } from "../types"; import type { H3Event } from "../event"; import { parse as parseMultipartData } from "./internal/multipart"; -import { assertMethod, getRequestHeader } from "./request"; +import { assertMethod } from "./request"; +import { getRequestHeader } from "./headers"; export type { MultiPartData } from "./internal/multipart"; @@ -13,6 +14,7 @@ const PayloadMethods: HTTPMethod[] = ["PATCH", "POST", "PUT", "DELETE"]; /** * Reads body of the request and returns encoded raw string (default) or `Buffer` if encoding if falsy. + * Node only. * @param event {H3Event} H3 event or req passed by h3 handler * @param encoding {Encoding} encoding="utf-8" - The character encoding to use. * @@ -25,6 +27,23 @@ export function readRawBody( // Ensure using correct HTTP method before attempt to read payload assertMethod(event, PayloadMethods); + if (event.request) { + if (!Number.parseInt(getRequestHeader(event, "content-length") || "")) { + return Promise.resolve(undefined); + } + // we clone the request so we can re-use readBody/ readBodyRaw later. + const request = event.request.clone(); + + const result = encoding + ? request.text().then((str) => str) + : request + .arrayBuffer() + .then((buffer) => Buffer.from(new Uint8Array(buffer))); + + return result as E extends false + ? Promise + : Promise; + } // Reuse body if already read const _rawBody = (event.node.req as any)[RawBodySymbol] || @@ -38,7 +57,7 @@ export function readRawBody( : (promise as Promise); } - if (!Number.parseInt(event.node.req.headers["content-length"] || "")) { + if (!Number.parseInt(getRequestHeader(event, "content-length") || "")) { return Promise.resolve(undefined); } @@ -78,29 +97,38 @@ export function readRawBody( * ``` */ export async function readBody(event: H3Event): Promise { + const contentType = + getRequestHeader(event, "content-type")?.toLowerCase() || ""; + if (event.request) { + const request = event.request.clone(); // Clone the request for re-use. + if (contentType === "application/json") { + return request.json(); + } + if (contentType === "application/octet-stream") { + return request.arrayBuffer() as T; + } + if (contentType === "multipart/form-data") { + return request.formData() as T; + } + if (contentType === "text") { + return request.text() as T; + } + if (contentType === "application/x-www-form-urlencoded") { + const text = await request.text(); + return parseUrlSearchParams(new URLSearchParams(text)) as T; + } + return request.blob() as T; // We return a blob if we don't know the type. + } + if (ParsedBodySymbol in event.node.req) { return (event.node.req as any)[ParsedBodySymbol]; } const body = await readRawBody(event, "utf8"); - if ( - event.node.req.headers["content-type"] === - "application/x-www-form-urlencoded" - ) { + if (contentType === "application/x-www-form-urlencoded") { const form = new URLSearchParams(body); - const parsedForm: Record = Object.create(null); - for (const [key, value] of form.entries()) { - if (key in parsedForm) { - if (!Array.isArray(parsedForm[key])) { - parsedForm[key] = [parsedForm[key]]; - } - parsedForm[key].push(value); - } else { - parsedForm[key] = value; - } - } - return parsedForm as unknown as T; + return parseUrlSearchParams(form) as T; } const json = destr(body) as T; @@ -117,9 +145,27 @@ export async function readMultipartFormData(event: H3Event) { if (!boundary) { return; } + if (event.request) { + return event.request.clone().formData(); + } const body = await readRawBody(event, false); if (!body) { return; } return parseMultipartData(body, boundary); } + +const parseUrlSearchParams = (form: URLSearchParams) => { + const parsedForm: Record = Object.create(null); + for (const [key, value] of form.entries()) { + if (key in parsedForm) { + if (!Array.isArray(parsedForm[key])) { + parsedForm[key] = [parsedForm[key]]; + } + parsedForm[key].push(value); + } else { + parsedForm[key] = value; + } + } + return parsedForm; +}; diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 28dda9eb..87c86e33 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,4 +1,6 @@ import type { H3Event } from "../event"; +import { getRequestRawHeader, setResponseHeader } from "./headers"; +import { sendResponse, setResponseStatus } from "./response"; export interface CacheConditions { modifiedTime?: string | Date; @@ -25,26 +27,30 @@ export function handleCacheHeaders( if (opts.modifiedTime) { const modifiedTime = new Date(opts.modifiedTime); - const ifModifiedSince = event.node.req.headers["if-modified-since"]; - event.node.res.setHeader("last-modified", modifiedTime.toUTCString()); - if (ifModifiedSince && new Date(ifModifiedSince) >= opts.modifiedTime) { + const ifModifiedSince = getRequestRawHeader(event, "if-modified-since"); + setResponseHeader(event, "last-modified", modifiedTime.toUTCString()); + if ( + ifModifiedSince && + !Array.isArray(ifModifiedSince) && + new Date(ifModifiedSince) >= opts.modifiedTime + ) { cacheMatched = true; } } if (opts.etag) { - event.node.res.setHeader("etag", opts.etag); - const ifNonMatch = event.node.req.headers["if-none-match"]; + setResponseHeader(event, "etag", opts.etag); + const ifNonMatch = getRequestRawHeader(event, "if-none-match"); if (ifNonMatch === opts.etag) { cacheMatched = true; } } - event.node.res.setHeader("cache-control", cacheControls.join(", ")); + setResponseHeader(event, "cache-control", cacheControls.join(", ")); if (cacheMatched) { - event.node.res.statusCode = 304; - event.node.res.end(); + setResponseStatus(event, 304); + sendResponse(event, new Response()); return true; } diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts index a8a25e60..6b7306c2 100644 --- a/src/utils/cookie.ts +++ b/src/utils/cookie.ts @@ -1,6 +1,7 @@ import { parse, serialize } from "cookie-es"; import type { CookieSerializeOptions } from "cookie-es"; import type { H3Event } from "../event"; +import { getResponseHeader, setResponseHeader } from "./headers"; /** * Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs. @@ -11,6 +12,9 @@ import type { H3Event } from "../event"; * ``` */ export function parseCookies(event: H3Event): Record { + if (event.request) { + return parse(event.request.headers.get("Cookie") || ""); + } return parse(event.node.req.headers.cookie || ""); } @@ -47,14 +51,14 @@ export function setCookie( path: "/", ...serializeOptions, }); - let setCookies = event.node.res.getHeader("set-cookie"); + let setCookies = getResponseHeader(event, "set-cookie"); if (!Array.isArray(setCookies)) { setCookies = [setCookies as any]; } setCookies = setCookies.filter((cookieValue: string) => { return cookieValue && !cookieValue.startsWith(name + "="); }); - event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]); + setResponseHeader(event, "set-cookie", [...setCookies, cookieStr]); } /** diff --git a/src/utils/cors/utils.ts b/src/utils/cors/utils.ts index a83872f1..38cc7114 100644 --- a/src/utils/cors/utils.ts +++ b/src/utils/cors/utils.ts @@ -1,6 +1,6 @@ import { defu } from "defu"; -import { appendHeaders } from "../response"; -import { getMethod, getRequestHeaders, getRequestHeader } from "../request"; +import { getMethod } from "../request"; +import { appendHeaders, getRequestHeaders, getRequestHeader } from "../headers"; import type { H3Event } from "../../event"; import type { H3CorsOptions, diff --git a/src/utils/headers.ts b/src/utils/headers.ts new file mode 100644 index 00000000..fc1c45b0 --- /dev/null +++ b/src/utils/headers.ts @@ -0,0 +1,136 @@ +import { OutgoingMessage } from "node:http"; +import { H3Event } from "src/event"; +import { RequestHeaders } from "src/types"; + +export function removeResponseHeaders(event: H3Event): void { + if (event.request) { + event._internalData.headers.clear(); + return; + } + for (const [name] of Object.entries(getHeaders(event))) { + removeResponseHeader(event, name); + } +} + +export function removeResponseHeader(event: H3Event, name: string): void { + if (event.request) { + event._internalData.headers.delete(name); + return; + } + return event.node.res.removeHeader(name); +} + +export function getRequestHeaders(event: H3Event): RequestHeaders { + const _headers: RequestHeaders = {}; + if (event.request) { + for (const [key] of event.request.headers.entries()) { + const val = event.request.headers.get(key); + if (val) { + _headers[key] = Array.isArray(val) + ? val.filter(Boolean).join(", ") + : val; + } + } + return _headers; + } + for (const key in event.node.req.headers) { + const val = event.node.req.headers[key]; + _headers[key] = Array.isArray(val) ? val.filter(Boolean).join(", ") : val; + } + return _headers; +} + +export function getRequestRawHeader(event: H3Event, name: string) { + if (event.request) { + return event.request.headers.get(name); + } + return event.node.req.headers[name]; +} + +export function getRequestHeader( + event: H3Event, + name: string +): RequestHeaders[string] { + const headers = getRequestHeaders(event); + const value = headers[name.toLowerCase()]; + return value; +} + +export function getResponseHeaders( + event: H3Event +): ReturnType { + if (event.request) { + return Object.fromEntries(event._internalData.headers.entries()); + } + return event.node.res.getHeaders() as Record; +} + +export function getResponseHeader( + event: H3Event, + name: string +): ReturnType { + if (event.request) { + return event._internalData.headers.get(name); + } + return event.node.res.getHeader(name); +} + +export function setResponseHeader( + event: H3Event, + name: string, + value: Parameters[1] +) { + if (event.request) { + return event._internalData.headers.set(name, value as any); + } + return event.node.res.setHeader(name, value); +} + +export function setResponseHeaders( + event: H3Event, + headers: Record[1]> +): void { + for (const [name, value] of Object.entries(headers)) { + setResponseHeader(event, name, value); + } +} + +export function appendResponseHeaders( + event: H3Event, + headers: Record +): void { + for (const [name, value] of Object.entries(headers)) { + appendResponseHeader(event, name, value); + } +} + +export function appendResponseHeader( + event: H3Event, + name: string, + value: string +): void { + let current = getResponseHeader(event, name); + + if (!current) { + setResponseHeader(event, name, value); + return; + } + + if (!Array.isArray(current)) { + current = [current.toString()]; + } + + setResponseHeader(event, name, [...current, value]); +} + +export const getHeaders = getRequestHeaders; + +export const getHeader = getRequestHeader; + +export const setHeaders = setResponseHeaders; + +export const setHeader = setResponseHeader; + +export const appendHeaders = appendResponseHeaders; + +export const appendHeader = appendResponseHeader; diff --git a/src/utils/index.ts b/src/utils/index.ts index 35d6efbc..38964b35 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,3 +9,5 @@ export * from "./response"; export * from "./session"; export * from "./cors"; export * from "./sanitize"; +export * from "./headers"; +export * from "./url"; diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 6e392850..5165b705 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -1,9 +1,14 @@ import type { H3Event } from "../event"; import type { H3EventContext, RequestHeaders } from "../types"; -import { getMethod, getRequestHeaders } from "./request"; +import { getMethod } from "./request"; import { readRawBody } from "./body"; import { splitCookiesString } from "./cookie"; -import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize"; +import { + appendResponseHeader, + getRequestHeaders, + setResponseHeader, +} from "./headers"; +import { sendResponse, setResponseStatus } from "./response"; export interface ProxyOptions { headers?: RequestHeaders | HeadersInit; @@ -67,11 +72,7 @@ export async function sendProxy( headers: opts.headers as HeadersInit, ...opts.fetchOptions, }); - event.node.res.statusCode = sanitizeStatusCode( - response.status, - event.node.res.statusCode - ); - event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); + setResponseStatus(event, response.status, response.statusText); for (const [key, value] of response.headers.entries()) { if (key === "content-encoding") { @@ -98,13 +99,18 @@ export async function sendProxy( } return cookie; }); - event.node.res.setHeader("set-cookie", cookies); + for (const cookie of cookies) { + appendResponseHeader(event, "set-cookie", cookie); + } continue; } - event.node.res.setHeader(key, value); + setResponseHeader(event, key, value); } + if (event.request) { + return sendResponse(event, response); + } // Directly send consumed _data if ((response as any)._data !== undefined) { return (response as any)._data; diff --git a/src/utils/request.ts b/src/utils/request.ts index 4e81b5e1..27b0406e 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,10 +1,21 @@ import { getQuery as _getQuery } from "ufo"; import { createError } from "../error"; -import type { HTTPMethod, RequestHeaders } from "../types"; +import type { HTTPMethod } from "../types"; import type { H3Event } from "../event"; +import { getUrlPath } from "./url"; + +export function getMethod( + event: H3Event, + defaultMethod: HTTPMethod = "GET" +): HTTPMethod { + if (event.request) { + return (event.request.method || defaultMethod).toUpperCase() as HTTPMethod; + } + return (event.node.req.method || defaultMethod).toUpperCase() as HTTPMethod; +} export function getQuery(event: H3Event) { - return _getQuery(event.node.req.url || ""); + return _getQuery(getUrlPath(event) || ""); } export function getRouterParams( @@ -23,13 +34,6 @@ export function getRouterParam( return params[name]; } -export function getMethod( - event: H3Event, - defaultMethod: HTTPMethod = "GET" -): HTTPMethod { - return (event.node.req.method || defaultMethod).toUpperCase() as HTTPMethod; -} - export function isMethod( event: H3Event, expected: HTTPMethod | HTTPMethod[], @@ -64,68 +68,3 @@ export function assertMethod( }); } } - -export function getRequestHeaders(event: H3Event): RequestHeaders { - const _headers: RequestHeaders = {}; - for (const key in event.node.req.headers) { - const val = event.node.req.headers[key]; - _headers[key] = Array.isArray(val) ? val.filter(Boolean).join(", ") : val; - } - return _headers; -} - -export const getHeaders = getRequestHeaders; - -export function getRequestHeader( - event: H3Event, - name: string -): RequestHeaders[string] { - const headers = getRequestHeaders(event); - const value = headers[name.toLowerCase()]; - return value; -} - -export const getHeader = getRequestHeader; - -export function getRequestHost( - event: H3Event, - opts: { xForwardedHost?: boolean } = {} -) { - if (opts.xForwardedHost) { - const xForwardedHost = event.node.req.headers["x-forwarded-host"] as string; - if (xForwardedHost) { - return xForwardedHost; - } - } - return event.node.req.headers.host || "localhost"; -} - -export function getRequestProtocol( - event: H3Event, - opts: { xForwardedProto?: boolean } = {} -) { - if ( - opts.xForwardedProto !== false && - event.node.req.headers["x-forwarded-proto"] === "https" - ) { - return "https"; - } - return (event.node.req.connection as any).encrypted ? "https" : "http"; -} - -const DOUBLE_SLASH_RE = /[/\\]{2,}/g; - -export function getRequestPath(event: H3Event): string { - const path = (event.node.req.url || "/").replace(DOUBLE_SLASH_RE, "/"); - return path; -} - -export function getRequestURL( - event: H3Event, - opts: { xForwardedHost?: boolean; xForwardedProto?: boolean } = {} -) { - const host = getRequestHost(event, opts); - const protocol = getRequestProtocol(event); - const path = getRequestPath(event); - return new URL(path, `${protocol}://${host}`); -} diff --git a/src/utils/response.ts b/src/utils/response.ts index f69b870f..5a8d0d4f 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -1,39 +1,31 @@ -import type { OutgoingMessage } from "node:http"; import type { Socket } from "node:net"; import { createError } from "../error"; import type { H3Event } from "../event"; import { MIMES } from "./consts"; import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize"; +import { + getResponseHeaders, + setResponseHeader, + removeResponseHeader, + getResponseHeader, + removeResponseHeaders, + setHeaders, + setHeader, + setResponseHeaders, +} from "./headers"; -const defer = - typeof setImmediate !== "undefined" ? setImmediate : (fn: () => any) => fn(); - -export function send(event: H3Event, data?: any, type?: string): Promise { - if (type) { - defaultContentType(event, type); +export function getResponseStatus(event: H3Event): number { + if (event.request) { + return event._internalData.status; } - return new Promise((resolve) => { - defer(() => { - event.node.res.end(data); - resolve(); - }); - }); + return event.node.res.statusCode; } -/** - * Respond with an empty payload.
- * Note that calling this function will close the connection and no other data can be sent to the client afterwards. - * - * @param event H3 event - * @param code status code to be send. By default, it is `204 No Content`. - */ -export function sendNoContent(event: H3Event, code = 204) { - event.node.res.statusCode = sanitizeStatusCode(code, 204); - // 204 responses MUST NOT have a Content-Length header field (https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2) - if (event.node.res.statusCode === 204) { - event.node.res.removeHeader("content-length"); +export function getResponseStatusText(event: H3Event): string { + if (event.request) { + return event._internalData.statusMessage; } - event.node.res.end(); + return event.node.res.statusMessage; } export function setResponseStatus( @@ -41,6 +33,15 @@ export function setResponseStatus( code?: number, text?: string ): void { + if (event.request) { + if (code) { + event._internalData.status = sanitizeStatusCode(code); + } + if (text) { + event._internalData.statusMessage = sanitizeStatusMessage(text); + } + return; + } if (code) { event.node.res.statusCode = sanitizeStatusCode( code, @@ -52,115 +53,128 @@ export function setResponseStatus( } } -export function getResponseStatus(event: H3Event): number { - return event.node.res.statusCode; -} +const defer = + typeof setImmediate !== "undefined" ? setImmediate : (fn: () => any) => fn(); -export function getResponseStatusText(event: H3Event): string { - return event.node.res.statusMessage; +function endNode(event: H3Event, data?: any) { + return new Promise((resolve) => { + defer(() => { + event.node.res.end(data); + resolve(); + }); + }); } -export function defaultContentType(event: H3Event, type?: string) { - if (type && !event.node.res.getHeader("content-type")) { - event.node.res.setHeader("content-type", type); +export function send(event: H3Event, data?: any, type?: string) { + if (type) { + defaultContentType(event, type); } -} - -export function sendRedirect(event: H3Event, location: string, code = 302) { - event.node.res.statusCode = sanitizeStatusCode( - code, - event.node.res.statusCode - ); - event.node.res.setHeader("location", location); - const encodedLoc = location.replace(/"/g, "%22"); - const html = ``; - return send(event, html, MIMES.html); -} - -export function getResponseHeaders( - event: H3Event -): ReturnType { - return event.node.res.getHeaders(); -} - -export function getResponseHeader( - event: H3Event, - name: string -): ReturnType { - return event.node.res.getHeader(name); -} - -export function setResponseHeaders( - event: H3Event, - headers: Record[1]> -): void { - for (const [name, value] of Object.entries(headers)) { - event.node.res.setHeader(name, value); + if (event.request) { + return sendResponse(event, new Response(data)); + } + return endNode(event, data); +} + +async function writeResponseWithNode(event: H3Event, response: Response) { + if (response.body) { + const contentType = response.headers.get("Content-Type") || ""; + if (contentType.includes("text") || contentType.includes("json")) { + for await (const chunk of response.body as unknown as AsyncIterable) { + const stringChunk = new TextDecoder().decode(chunk); + event.node.res.write(stringChunk); + } + } else { + // for binary data like images, videos, etc. + for await (const chunk of response.body as unknown as AsyncIterable) { + event.node.res.write(chunk); + } + } } + return endNode(event); } +/** + * Returns a response and respect the setters called before. + * @param event + * @param response + * @returns + */ +export function sendResponse(event: H3Event, response: Response) { + if (event.context.__sendRaw === true) { + console.log("Raw response detected"); + if (event.request) { + return response; + } + removeResponseHeaders(event); + setResponseStatus(event, response.status, response.statusText); + setResponseHeaders(event, Object.fromEntries(response.headers.entries())); + return writeResponseWithNode(event, response); + } + // Merge status and headers + const status = + getResponseStatus(event) !== 200 // 200 is the default. + ? getResponseStatus(event) + : response.status; + const statusText = getResponseStatusText(event) || response.statusText; + const storedHeaders = getResponseHeaders(event); + const mergedHeaders = { + ...Object.fromEntries(response.headers.entries()), + ...storedHeaders, + }; + const mergedResponse = new Response(response.body, { + ...response, + status, + statusText, + headers: mergedHeaders as HeadersInit, + }); -export const setHeaders = setResponseHeaders; - -export function setResponseHeader( - event: H3Event, - name: string, - value: Parameters[1] -): void { - event.node.res.setHeader(name, value); -} - -export const setHeader = setResponseHeader; - -export function appendResponseHeaders( - event: H3Event, - headers: Record -): void { - for (const [name, value] of Object.entries(headers)) { - appendResponseHeader(event, name, value); + if (event.request) { + return mergedResponse; } -} -export const appendHeaders = appendResponseHeaders; - -export function appendResponseHeader( - event: H3Event, - name: string, - value: string -): void { - let current = event.node.res.getHeader(name); - - if (!current) { - event.node.res.setHeader(name, value); - return; + // Set status and headers and write response + setHeaders(event, Object.fromEntries(mergedResponse.headers.entries())); + setResponseStatus(event, mergedResponse.status, mergedResponse.statusText); + if (mergedResponse.redirected) { + setHeader(event, "location", response.url); } + return writeResponseWithNode(event, mergedResponse); +} - if (!Array.isArray(current)) { - current = [current.toString()]; +/** + * Respond with an empty payload.
+ * Note that calling this function will close the connection and no other data can be sent to the client afterwards. + * + * @param event H3 event + * @param code status code to be send. By default, it is `204 No Content`. + */ +export function sendNoContent(event: H3Event, code = 204) { + setResponseStatus(event, code); + // 204 responses MUST NOT have a Content-Length header field (https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2) + if (code === 204) { + removeResponseHeader(event, "content-length"); } - - event.node.res.setHeader(name, [...current, value]); + return send(event, null); } -export const appendHeader = appendResponseHeader; - -export function isStream(data: any) { - return ( - data && - typeof data === "object" && - typeof data.pipe === "function" && - typeof data.on === "function" - ); +export function defaultContentType(event: H3Event, type?: string) { + if (type) { + const contentType = getResponseHeader(event, "content-type"); + if (!contentType) { + setResponseHeader(event, "content-type", type); + } + } } -export function sendStream(event: H3Event, data: any): Promise { - return new Promise((resolve, reject) => { - data.pipe(event.node.res); - data.on("end", () => resolve()); - data.on("error", (error: Error) => reject(createError(error))); - }); +export function sendRedirect(event: H3Event, location: string, code = 302) { + const encodedLoc = location.replace(/"/g, "%22"); + const html = ``; + setResponseStatus(event, code); + setResponseHeader(event, "location", location); + return send(event, html, MIMES.html); } const noop = () => {}; +// Node only export function writeEarlyHints( event: H3Event, hints: string | string[] | Record, @@ -207,6 +221,7 @@ export function writeEarlyHints( hint += `\r\n${header}: ${value}`; } if (event.node.res.socket) { + // eslint-disable-next-line @typescript-eslint/no-extra-semi (event.node.res as { socket: Socket }).socket.write( `${hint}\r\n\r\n`, "utf8", @@ -216,3 +231,22 @@ export function writeEarlyHints( cb(); } } + +// Node only +export function isStream(data: any) { + return ( + data && + typeof data === "object" && + typeof data.pipe === "function" && + typeof data.on === "function" + ); +} + +// Node only +export function sendStream(event: H3Event, data: any): Promise { + return new Promise((resolve, reject) => { + data.pipe(event.node.res); + data.on("end", () => resolve()); + data.on("error", (error: Error) => reject(createError(error))); + }); +} diff --git a/src/utils/route.ts b/src/utils/route.ts index c1ebc749..221765a2 100644 --- a/src/utils/route.ts +++ b/src/utils/route.ts @@ -1,6 +1,12 @@ import { withoutTrailingSlash, withoutBase } from "ufo"; import { EventHandler } from "../types"; import { eventHandler } from "../event"; +import { + getOriginalUrlPath, + getUrlPath, + setOriginalUrlPath, + setUrlPath, +} from "./url"; export function useBase(base: string, handler: EventHandler): EventHandler { base = withoutTrailingSlash(base); @@ -8,9 +14,11 @@ export function useBase(base: string, handler: EventHandler): EventHandler { return handler; } return eventHandler((event) => { - (event.node.req as any).originalUrl = - (event.node.req as any).originalUrl || event.node.req.url || "/"; - event.node.req.url = withoutBase(event.node.req.url || "/", base); + setOriginalUrlPath( + event, + getOriginalUrlPath(event) || getUrlPath(event) || "/" + ); + setUrlPath(event, withoutBase(getUrlPath(event) || "/", base)); return handler(event); }); } diff --git a/src/utils/session.ts b/src/utils/session.ts index 66a7ff5a..ea56501d 100644 --- a/src/utils/session.ts +++ b/src/utils/session.ts @@ -4,6 +4,7 @@ import { seal, unseal, defaults as sealDefaults } from "iron-webcrypto"; import type { SealOptions } from "iron-webcrypto"; import type { H3Event } from "../event"; import { getCookie, setCookie } from "./cookie"; +import { getRequestRawHeader } from "./headers"; type SessionDataT = Record; export type SessionData = T; @@ -92,7 +93,7 @@ export async function getSession( typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`; - const headerValue = event.node.req.headers[headerName]; + const headerValue = getRequestRawHeader(event, headerName); if (typeof headerValue === "string") { sealedSession = headerValue; } diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 00000000..a0d199c5 --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,83 @@ +import { getRequestRawHeader } from "./headers"; +import { H3Event } from "src/event"; + +export function setOriginalUrlPath(event: H3Event, url: string) { + if (event.request) { + event._internalData.originalUrlPath = url; + return; + } + // eslint-disable-next-line @typescript-eslint/no-extra-semi + (event.node.req as any).originalUrlPath = url; +} + +export function getOriginalUrlPath(event: H3Event) { + if (event.request) { + return event._internalData.originalUrlPath; + } + return (event.node.req as any).originalUrlPath as string; +} + +export function setUrlPath(event: H3Event, url: string) { + if (event.request) { + event._internalData.currentUrlPath = url; + return; + } + event.node.req.url = url; +} + +export function getUrlPath(event: H3Event) { + if (event.request) { + const url = new URL(event.request.url); + return event._internalData.currentUrlPath ?? url.pathname + url.search; + } + return event.node.req.url || "/"; +} + +export function getRequestURL( + event: H3Event, + opts: { xForwardedHost?: boolean; xForwardedProto?: boolean } = {} +) { + if (event.request) { + return new URL(event.request.url); + } + const host = getRequestHost(event, opts); + const protocol = getRequestProtocol(event); + const path = getRequestPath(event); + return new URL(path, `${protocol}://${host}`); +} + +export function getRequestHost( + event: H3Event, + opts: { xForwardedHost?: boolean } = {} +) { + if (opts.xForwardedHost) { + const xForwardedHost = getRequestRawHeader(event, "x-forwarded-host"); + if (xForwardedHost) { + return xForwardedHost as string; + } + } + return getRequestRawHeader(event, "host") || "localhost"; +} + +export function getRequestProtocol( + event: H3Event, + opts: { xForwardedProto?: boolean } = {} +) { + if ( + opts.xForwardedProto !== false && + getRequestRawHeader(event, "x-forwarded-proto") === "https" + ) { + return "https"; + } + if (event.request) { + return new URL(event.request.url).protocol; + } + return (event.node.req.connection as any).encrypted ? "https" : "http"; +} + +const DOUBLE_SLASH_RE = /[/\\]{2,}/g; + +export function getRequestPath(event: H3Event): string { + const path = (getUrlPath(event) || "/").replace(DOUBLE_SLASH_RE, "/"); + return path; +} diff --git a/test/app.test.ts b/test/app.test.ts index 3eea7bec..c2047a6a 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -7,6 +7,10 @@ import { App, eventHandler, fromNodeMiddleware, + getUrlPath, + setHeader, + send, + setResponseStatus, } from "../src"; describe("app", () => { @@ -21,7 +25,7 @@ describe("app", () => { it("can return JSON directly", async () => { app.use( "/api", - eventHandler((event) => ({ url: event.node.req.url })) + eventHandler((event) => ({ url: getUrlPath(event) })) ); const res = await request.get("/api"); @@ -102,7 +106,7 @@ describe("app", () => { it("allows overriding Content-Type", async () => { app.use( eventHandler((event) => { - event.node.res.setHeader("content-type", "text/xhtml"); + setHeader(event, "content-type", "text/xhtml"); return "

Hello world!

"; }) ); @@ -208,7 +212,7 @@ describe("app", () => { it("can short-circuit route matching", async () => { app.use( eventHandler((event) => { - event.node.res.end("done"); + return send(event, "done"); }) ); app.use(eventHandler(() => "valid")); @@ -255,4 +259,84 @@ describe("app", () => { const res = await request.get("/"); expect(res.body).toEqual({ works: 1 }); }); + + it("can return a Response instance", async () => { + app.use( + "/test/", + eventHandler(() => { + return new Response("valid", { + status: 207, + statusText: "hello-status", + headers: { hello: "world" }, + }); + }) + ); + + const res = await request.get("/test"); + expect(res.text).toBe("valid"); + expect(res.status).toBe(207); + // @ts-expect-error the type is wrong, it exists + expect(res.res.statusMessage).toBe("hello-status"); + expect(res.header.hello).toBe("world"); + }); + + it("can ignore setters with a Raw Response", async () => { + app.use( + "/test/", + eventHandler((event) => { + setHeader(event, "hello", "yes"); + setResponseStatus(event, 208, "bye-status"); + event.context.__sendRaw = true; + return new Response("valid", { + status: 207, + statusText: "hello-status", + headers: { hello: "world" }, + }); + }) + ); + + const res = await request.get("/test"); + expect(res.text).toBe("valid"); + expect(res.status).toBe(207); + // @ts-expect-error the type is wrong, it exists + expect(res.res.statusMessage).toBe("hello-status"); + expect(res.header.hello).toBe("world"); + }); + + it("can use setters to overwrite Response instance", async () => { + app.use( + "/test/", + eventHandler((event) => { + setHeader(event, "hello", "yes"); + setResponseStatus(event, 208, "hello-status"); + return new Response("valid", { + status: 207, + statusText: "bye-status", + headers: { hello: "no" }, + }); + }) + ); + + const res = await request.get("/test"); + expect(res.text).toBe("valid"); + expect(res.status).toBe(208); + // @ts-expect-error the type is wrong, it exists + expect(res.res.statusMessage).toBe("hello-status"); + expect(res.header.hello).toBe("yes"); + }); + + it("can use `TransformStream` to stream a response", async () => { + app.use( + "/test/", + eventHandler(() => { + const response = new Response(`Hello world !`); + const { readable, writable } = new TransformStream(); + response.body?.pipeTo(writable); + return new Response(readable, response); + }) + ); + const res = await request.get("/test"); + expect(res.text).toBe("Hello world !"); + expect(res.status).toBe(200); + }); }); diff --git a/test/cloudflare.test.ts b/test/cloudflare.test.ts new file mode 100644 index 00000000..eda2b01b --- /dev/null +++ b/test/cloudflare.test.ts @@ -0,0 +1,21 @@ +/// + +import { expect, test } from "vitest"; +import worker from "./h3-worker"; + +const env = getMiniflareBindings(); +const ctx = new ExecutionContext(); + +const baseUrl = "http://localhost"; + +test("responds with url", async () => { + const request = new Request(baseUrl); + const response = await worker.fetch(request, env, ctx); + expect(await response.text()).toBe(`Hello world ! ${baseUrl}/`); +}); + +test("Can use the router", async () => { + const request = new Request(`${baseUrl}/here`); + const response = await worker.fetch(request, env, ctx); + expect(await response.text()).toBe(`Routed there`); +}); diff --git a/test/h3-worker.ts b/test/h3-worker.ts new file mode 100644 index 00000000..56540232 --- /dev/null +++ b/test/h3-worker.ts @@ -0,0 +1,31 @@ +import { + adapterCloudflareWorker, + createApp, + createRouter, + eventHandler, +} from "../src"; + +const app = createApp({ debug: false }); +const router = createRouter(); + +router + .get( + "/", + eventHandler((event) => { + const response = new Response(`Hello world ! ${event.request.url}`); + const { readable, writable } = new TransformStream(); + response.body?.pipeTo(writable); + return new Response(readable, response); + }) + ) + .get( + "/here", + eventHandler(() => { + return new Response("Routed there"); + }) + ); + +app.use(router); + +const cloudflare = adapterCloudflareWorker(app); +export default cloudflare; diff --git a/test/header.test.ts b/test/header.test.ts index b78dd88e..c2269777 100644 --- a/test/header.test.ts +++ b/test/header.test.ts @@ -34,7 +34,7 @@ describe("", () => { "/", eventHandler((event) => { const headers = getRequestHeaders(event); - expect(headers).toEqual(event.node.req.headers); + expect(headers).toHaveProperty("accept", "application/json"); }) ); await request.get("/").set("Accept", "application/json"); @@ -47,7 +47,7 @@ describe("", () => { "/", eventHandler((event) => { const headers = getHeaders(event); - expect(headers).toEqual(event.node.req.headers); + expect(headers).toHaveProperty("accept", "application/json"); }) ); await request.get("/").set("Accept", "application/json"); diff --git a/test/proxy.test.ts b/test/proxy.test.ts index 3a5c458c..1b8a9615 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -47,12 +47,12 @@ describe("", () => { }) ); - const result = await request.get("/"); + const result = await request.get("/").retry(5); expect(result.text).toContain( 'a href="https://www.iana.org/domains/example">More information...' ); - }); + }, 5000); }); describe("proxyRequest", () => { diff --git a/test/router.test.ts b/test/router.test.ts index 651444d0..dec65548 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -99,7 +99,7 @@ describe("router", () => { }); it("Not matching route method", async () => { - const res = await request.head("/test"); + const res = await request.delete("/test"); expect(res.status).toEqual(405); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 660ae61f..1c59d24f 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -11,6 +11,8 @@ import { getMethod, getQuery, getRequestURL, + getUrlPath, + sendResponse, } from "../src"; describe("", () => { @@ -22,6 +24,27 @@ describe("", () => { request = supertest(toNodeListener(app)); }); + describe("sendResponse", () => { + it("can send a Response", async () => { + app.use( + eventHandler((event) => sendResponse(event, new Response("Response"))) + ); + const result = await request.get("/"); + expect(result.text).toBe("Response"); + }); + + it("can send a RawResponse", async () => { + app.use( + eventHandler((event) => { + event.context.__sendRaw = true; + return sendResponse(event, new Response("Raw")); + }) + ); + const result = await request.get("/"); + expect(result.text).toBe("Raw"); + }); + }); + describe("sendRedirect", () => { it("can redirect URLs", async () => { app.use( @@ -40,7 +63,7 @@ describe("", () => { "/", useBase( "/api", - eventHandler((event) => Promise.resolve(event.node.req.url || "none")) + eventHandler((event) => Promise.resolve(getUrlPath(event) || "none")) ) ); const result = await request.get("/api/test"); @@ -52,7 +75,7 @@ describe("", () => { "/", useBase( "", - eventHandler((event) => Promise.resolve(event.node.req.url || "none")) + eventHandler((event) => Promise.resolve(getUrlPath(event) || "none")) ) ); const result = await request.get("/api/test"); @@ -145,17 +168,17 @@ describe("", () => { }); describe("assertMethod", () => { - it("only allow head and post", async () => { + it("only allow delete and post", async () => { app.use( "/post", eventHandler((event) => { - assertMethod(event, "POST", true); + assertMethod(event, ["POST", "DELETE"], true); return "ok"; }) ); expect((await request.get("/post")).status).toBe(405); expect((await request.post("/post")).status).toBe(200); - expect((await request.head("/post")).status).toBe(200); + expect((await request.delete("/post")).status).toBe(200); }); }); }); diff --git a/tsconfig.json b/tsconfig.json index d778c0e2..d4135a0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,18 +4,14 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "Node", - "lib": [ - "WebWorker", - "DOM", - "DOM.Iterable" - ], + "lib": ["WebWorker", "DOM", "DOM.Iterable"], "strict": true, "declaration": true, "types": [ + "@cloudflare/workers-types", + "vitest-environment-miniflare/globals", "node" ] }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/vitest.config.ts b/vitest.config.ts index 7fa63ebb..fdf1c805 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,14 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vitest/config" export default defineConfig({ test: { coverage: { reporter: ["text", "clover", "json"] + }, + environment: "miniflare", + environmentOptions: { + bindings: { KEY: "value" }, + kvNamespaces: ["TEST_NAMESPACE"] } } -}); +})