From a58d7c977b30ef1782bb390a38fed3b28f8afca9 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 19 Feb 2024 18:15:52 +0100 Subject: [PATCH 01/36] chore(release): v1.10.2 --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 224620e7..50d32b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## v1.10.2 + +[compare changes](https://github.com/unjs/h3/compare/v1.10.1...v1.10.2) + +### 🩹 Fixes + +- **proxy:** Ignore incoming `accept` header ([#646](https://github.com/unjs/h3/pull/646)) + +### ❤️ Contributors + +- Daniel Roe ([@danielroe](http://github.com/danielroe)) + ## v1.10.1 [compare changes](https://github.com/unjs/h3/compare/v1.10.0...v1.10.1) diff --git a/package.json b/package.json index fb21b352..372f8203 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "h3", - "version": "1.10.1", + "version": "1.10.2", "description": "Minimal H(TTP) framework built for high performance and portability.", "repository": "unjs/h3", "license": "MIT", From ebc8a8d39d10d8d414744068684d29a74b0d3814 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:10:49 +0100 Subject: [PATCH 02/36] chore(deps): update codecov/codecov-action action to v4 (#648) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c845e9f..3f58f046 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - run: pnpm lint - run: pnpm build - run: pnpm vitest --coverage && rm -rf coverage/tmp - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 - name: nightly release if: | github.event_name == 'push' && From 0b60530a8301044c4296a2b89d97fdf981bca1f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 11:10:55 +0100 Subject: [PATCH 03/36] chore(deps): update all non-major dependencies (#644) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 20 ++--- pnpm-lock.yaml | 222 ++++++++++++++++++++++++++++++------------------- 2 files changed, 145 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index 372f8203..4c36c141 100644 --- a/package.json +++ b/package.json @@ -33,21 +33,21 @@ "dependencies": { "cookie-es": "^1.0.0", "defu": "^6.1.4", - "destr": "^2.0.2", + "destr": "^2.0.3", "iron-webcrypto": "^1.0.0", "ohash": "^1.1.3", "radix3": "^1.1.0", - "ufo": "^1.3.2", + "ufo": "^1.4.0", "uncrypto": "^0.1.3", "unenv": "^1.9.0" }, "devDependencies": { "0x": "^5.7.0", "@types/express": "^4.17.21", - "@types/node": "^20.11.6", + "@types/node": "^20.11.19", "@types/supertest": "^6.0.2", - "@vitest/coverage-v8": "^1.2.1", - "autocannon": "^7.14.0", + "@vitest/coverage-v8": "^1.3.1", + "autocannon": "^7.15.0", "changelogen": "^0.5.5", "connect": "^3.7.0", "eslint": "^8.56.0", @@ -55,16 +55,16 @@ "express": "^4.18.2", "get-port": "^7.0.0", "jiti": "^1.21.0", - "listhen": "^1.5.6", - "node-fetch-native": "^1.6.1", - "prettier": "^3.2.4", + "listhen": "^1.6.0", + "node-fetch-native": "^1.6.2", + "prettier": "^3.2.5", "react": "^18.2.0", "react-dom": "^18.2.0", "supertest": "^6.3.4", "typescript": "^5.3.3", "unbuild": "^2.0.0", - "vitest": "^1.2.1", + "vitest": "^1.3.1", "zod": "^3.22.4" }, - "packageManager": "pnpm@8.14.3" + "packageManager": "pnpm@8.15.3" } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 908eb060..d1ce55ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^6.1.4 version: 6.1.4 destr: - specifier: ^2.0.2 - version: 2.0.2 + specifier: ^2.0.3 + version: 2.0.3 iron-webcrypto: specifier: ^1.0.0 version: 1.0.0 @@ -27,8 +27,8 @@ importers: specifier: ^1.1.0 version: 1.1.0 ufo: - specifier: ^1.3.2 - version: 1.3.2 + specifier: ^1.4.0 + version: 1.4.0 uncrypto: specifier: ^0.1.3 version: 0.1.3 @@ -43,17 +43,17 @@ importers: specifier: ^4.17.21 version: 4.17.21 '@types/node': - specifier: ^20.11.6 - version: 20.11.6 + specifier: ^20.11.19 + version: 20.11.19 '@types/supertest': specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': - specifier: ^1.2.1 - version: 1.2.1(vitest@1.2.1) + specifier: ^1.3.1 + version: 1.3.1(vitest@1.3.1) autocannon: - specifier: ^7.14.0 - version: 7.14.0 + specifier: ^7.15.0 + version: 7.15.0 changelogen: specifier: ^0.5.5 version: 0.5.5 @@ -76,14 +76,14 @@ importers: specifier: ^1.21.0 version: 1.21.0 listhen: - specifier: ^1.5.6 - version: 1.5.6 + specifier: ^1.6.0 + version: 1.6.0 node-fetch-native: - specifier: ^1.6.1 - version: 1.6.1 + specifier: ^1.6.2 + version: 1.6.2 prettier: - specifier: ^3.2.4 - version: 3.2.4 + specifier: ^3.2.5 + version: 3.2.5 react: specifier: ^18.2.0 version: 18.2.0 @@ -100,8 +100,8 @@ importers: specifier: ^2.0.0 version: 2.0.0(typescript@5.3.3) vitest: - specifier: ^1.2.1 - version: 1.2.1(@types/node@20.11.6) + specifier: ^1.3.1 + version: 1.3.1(@types/node@20.11.19) zod: specifier: ^3.22.4 version: 3.22.4 @@ -796,6 +796,17 @@ packages: dependencies: is-glob: 4.0.3 micromatch: 4.0.5 + dev: false + bundledDependencies: + - napi-wasm + + /@parcel/watcher-wasm@2.4.0: + resolution: {integrity: sha512-MNgQ4WCbBybqQ97KwR/hqJGYTg3+s8qHpgIyFWB2qJOBvoJWbXuJGmm4ZkPLq2bMaANqCZqrXwmKYagZTkMKZA==} + engines: {node: '>= 10.0.0'} + dependencies: + is-glob: 4.0.3 + micromatch: 4.0.5 + dev: true bundledDependencies: - napi-wasm @@ -1053,13 +1064,13 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.11.6 + '@types/node': 20.11.19 dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.11.6 + '@types/node': 20.11.19 dev: true /@types/cookiejar@2.1.5: @@ -1073,7 +1084,7 @@ packages: /@types/express-serve-static-core@4.17.41: resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} dependencies: - '@types/node': 20.11.6 + '@types/node': 20.11.19 '@types/qs': 6.9.11 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -1116,8 +1127,8 @@ packages: resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} dev: true - /@types/node@20.11.6: - resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} + /@types/node@20.11.19: + resolution: {integrity: sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==} dependencies: undici-types: 5.26.5 dev: true @@ -1146,7 +1157,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 20.11.6 + '@types/node': 20.11.19 dev: true /@types/serve-static@1.15.5: @@ -1154,7 +1165,7 @@ packages: dependencies: '@types/http-errors': 2.0.4 '@types/mime': 3.0.4 - '@types/node': 20.11.6 + '@types/node': 20.11.19 dev: true /@types/superagent@8.1.3: @@ -1162,7 +1173,7 @@ packages: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 20.11.6 + '@types/node': 20.11.19 dev: true /@types/supertest@6.0.2: @@ -1306,10 +1317,10 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitest/coverage-v8@1.2.1(vitest@1.2.1): - resolution: {integrity: sha512-fJEhKaDwGMZtJUX7BRcGxooGwg1Hl0qt53mVup/ZJeznhvL5EodteVnb/mcByhEcvVWbK83ZF31c7nPEDi4LOQ==} + /@vitest/coverage-v8@1.3.1(vitest@1.3.1): + resolution: {integrity: sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==} peerDependencies: - vitest: ^1.0.0 + vitest: 1.3.1 dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 @@ -1324,43 +1335,43 @@ packages: std-env: 3.7.0 test-exclude: 6.0.0 v8-to-istanbul: 9.2.0 - vitest: 1.2.1(@types/node@20.11.6) + vitest: 1.3.1(@types/node@20.11.19) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@1.2.1: - resolution: {integrity: sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ==} + /@vitest/expect@1.3.1: + resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} dependencies: - '@vitest/spy': 1.2.1 - '@vitest/utils': 1.2.1 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 chai: 4.4.1 dev: true - /@vitest/runner@1.2.1: - resolution: {integrity: sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ==} + /@vitest/runner@1.3.1: + resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==} dependencies: - '@vitest/utils': 1.2.1 + '@vitest/utils': 1.3.1 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.2.1: - resolution: {integrity: sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==} + /@vitest/snapshot@1.3.1: + resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} dependencies: magic-string: 0.30.5 pathe: 1.1.2 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.2.1: - resolution: {integrity: sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ==} + /@vitest/spy@1.3.1: + resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} dependencies: tinyspy: 2.2.0 dev: true - /@vitest/utils@1.2.1: - resolution: {integrity: sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg==} + /@vitest/utils@1.3.1: + resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -1585,8 +1596,8 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /autocannon@7.14.0: - resolution: {integrity: sha512-lusP43BAwrTwQhihLjKwy7LceyX01eKSvFJUsBktASGqcR1g1ySYSPxCoCGDX08uLEs9oaqEKBBUFMenK3B3lQ==} + /autocannon@7.15.0: + resolution: {integrity: sha512-NaP2rQyA+tcubOJMFv2+oeW9jv2pq/t+LM6BL3cfJic0HEfscEcnWgAyU5YovE/oTHUzAgTliGdLPR+RQAWUbg==} hasBin: true dependencies: chalk: 4.1.2 @@ -2023,7 +2034,7 @@ packages: convert-gitmoji: 0.1.5 execa: 8.0.1 mri: 1.2.0 - node-fetch-native: 1.6.1 + node-fetch-native: 1.6.2 ofetch: 1.3.3 open: 9.1.0 pathe: 1.1.2 @@ -2311,6 +2322,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crossws@0.1.1: + resolution: {integrity: sha512-c9c/o7bS3OjsdpSkvexpka0JNlesBF2JU9B2V1yNsYGwRbAafxhJQ7VI9b48D5bpONz/oxbPGMzBojy9sXoQIQ==} + dev: true + /crypto-browserify@3.12.0: resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} dependencies: @@ -2685,8 +2700,8 @@ packages: minimalistic-assert: 1.0.1 dev: true - /destr@2.0.2: - resolution: {integrity: sha512-65AlobnZMiCET00KaFFjUefxDX0khFA/E4myqZ7a6Sq1yZtR8+FVIvilVX66vF2uobSumxooYZChiRPCKNqhmg==} + /destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} @@ -3726,7 +3741,7 @@ packages: citty: 0.1.5 consola: 3.2.3 defu: 6.1.4 - node-fetch-native: 1.6.1 + node-fetch-native: 1.6.2 nypm: 0.3.6 ohash: 1.1.3 pathe: 1.1.2 @@ -3825,15 +3840,16 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /h3@1.10.0: - resolution: {integrity: sha512-Tw1kcIC+AeimwRmviiObaD5EB430Yt+lTgOxLJxNr96Vd/fGRu04EF7aKfOAcpwKCI+U2JlbxOLhycD86p3Ciw==} + /h3@1.10.2: + resolution: {integrity: sha512-r1iNNcFGL4G9pL3lgYxwX0O2ZmqdKqhILAJsnlw5icn5I1QHnADM4TgVdYRtHUqy+NntVpHIEFwnw/XCbebICg==} dependencies: cookie-es: 1.0.0 defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 iron-webcrypto: 1.0.0 + ohash: 1.1.3 radix3: 1.1.0 - ufo: 1.3.2 + ufo: 1.4.0 uncrypto: 0.1.3 unenv: 1.9.0 @@ -4395,6 +4411,10 @@ packages: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true + /js-tokens@8.0.3: + resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} + dev: true + /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -4519,7 +4539,7 @@ packages: consola: 3.2.3 defu: 6.1.4 get-port-please: 3.1.2 - h3: 1.10.0 + h3: 1.10.2 http-shutdown: 1.2.2 jiti: 1.21.0 mlly: 1.5.0 @@ -4529,6 +4549,31 @@ packages: ufo: 1.3.2 untun: 0.1.3 uqr: 0.1.2 + dev: false + + /listhen@1.6.0: + resolution: {integrity: sha512-z0RcEXVX5oTpY1bO02SKoTU/kmZSrFSngNNzHRM6KICR17PTq7ANush6AE6ztGJwJD4RLpBrVHd9GnV51J7s3w==} + hasBin: true + dependencies: + '@parcel/watcher': 2.4.0 + '@parcel/watcher-wasm': 2.4.0 + citty: 0.1.5 + clipboardy: 4.0.0 + consola: 3.2.3 + crossws: 0.1.1 + defu: 6.1.4 + get-port-please: 3.1.2 + h3: 1.10.2 + http-shutdown: 1.2.2 + jiti: 1.21.0 + mlly: 1.5.0 + node-forge: 1.3.1 + pathe: 1.1.2 + std-env: 3.7.0 + ufo: 1.4.0 + untun: 0.1.3 + uqr: 0.1.2 + dev: true /local-pkg@0.5.0: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} @@ -4844,7 +4889,7 @@ packages: acorn: 8.11.3 pathe: 1.1.2 pkg-types: 1.0.3 - ufo: 1.3.2 + ufo: 1.4.0 /module-deps@6.2.3: resolution: {integrity: sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==} @@ -4954,8 +4999,8 @@ packages: resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} engines: {node: ^16 || ^18 || >= 20} - /node-fetch-native@1.6.1: - resolution: {integrity: sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==} + /node-fetch-native@1.6.2: + resolution: {integrity: sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==} /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} @@ -5021,7 +5066,7 @@ packages: citty: 0.1.5 execa: 8.0.1 pathe: 1.1.2 - ufo: 1.3.2 + ufo: 1.4.0 dev: true /object-inspect@1.13.1: @@ -5073,9 +5118,9 @@ packages: /ofetch@1.3.3: resolution: {integrity: sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg==} dependencies: - destr: 2.0.2 - node-fetch-native: 1.6.1 - ufo: 1.3.2 + destr: 2.0.3 + node-fetch-native: 1.6.2 + ufo: 1.4.0 dev: true /ohash@1.1.3: @@ -5634,8 +5679,8 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /prettier@3.2.4: - resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} hasBin: true dev: true @@ -5783,7 +5828,7 @@ packages: resolution: {integrity: sha512-lNeOl38Ws0eNxpO3+wD1I9rkHGQyj1NU1jlzv4go2CtEnEQEUfqnIvZG7W+bC/aXdJ27n5x/yUjb6RoT9tko+Q==} dependencies: defu: 6.1.4 - destr: 2.0.2 + destr: 2.0.3 flat: 5.0.2 dev: true @@ -6378,10 +6423,10 @@ packages: engines: {node: '>=8'} dev: true - /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + /strip-literal@2.0.0: + resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} dependencies: - acorn: 8.11.3 + js-tokens: 8.0.3 dev: true /stylehacks@6.0.2(postcss@8.4.33): @@ -6711,6 +6756,10 @@ packages: /ufo@1.3.2: resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} + dev: false + + /ufo@1.4.0: + resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==} /umd@3.0.3: resolution: {integrity: sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==} @@ -6789,7 +6838,7 @@ packages: consola: 3.2.3 defu: 6.1.4 mime: 3.0.0 - node-fetch-native: 1.6.1 + node-fetch-native: 1.6.2 pathe: 1.1.2 /universalify@2.0.1: @@ -6920,8 +6969,8 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-node@1.2.1(@types/node@20.11.6): - resolution: {integrity: sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg==} + /vite-node@1.3.1(@types/node@20.11.19): + resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -6929,7 +6978,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.0.12(@types/node@20.11.6) + vite: 5.0.12(@types/node@20.11.19) transitivePeerDependencies: - '@types/node' - less @@ -6941,7 +6990,7 @@ packages: - terser dev: true - /vite@5.0.12(@types/node@20.11.6): + /vite@5.0.12(@types/node@20.11.19): resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -6969,7 +7018,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.6 + '@types/node': 20.11.19 esbuild: 0.19.12 postcss: 8.4.33 rollup: 4.9.6 @@ -6977,15 +7026,15 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.2.1(@types/node@20.11.6): - resolution: {integrity: sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA==} + /vitest@1.3.1(@types/node@20.11.19): + resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': ^1.0.0 - '@vitest/ui': ^1.0.0 + '@vitest/browser': 1.3.1 + '@vitest/ui': 1.3.1 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -7002,14 +7051,13 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.6 - '@vitest/expect': 1.2.1 - '@vitest/runner': 1.2.1 - '@vitest/snapshot': 1.2.1 - '@vitest/spy': 1.2.1 - '@vitest/utils': 1.2.1 + '@types/node': 20.11.19 + '@vitest/expect': 1.3.1 + '@vitest/runner': 1.3.1 + '@vitest/snapshot': 1.3.1 + '@vitest/spy': 1.3.1 + '@vitest/utils': 1.3.1 acorn-walk: 8.3.2 - cac: 6.7.14 chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 @@ -7018,11 +7066,11 @@ packages: pathe: 1.1.2 picocolors: 1.0.0 std-env: 3.7.0 - strip-literal: 1.3.0 + strip-literal: 2.0.0 tinybench: 2.6.0 tinypool: 0.8.2 - vite: 5.0.12(@types/node@20.11.6) - vite-node: 1.2.1(@types/node@20.11.6) + vite: 5.0.12(@types/node@20.11.19) + vite-node: 1.3.1(@types/node@20.11.19) why-is-node-running: 2.2.2 transitivePeerDependencies: - less From 71c633c81372ac86d326fa89f509ec97d94d9da3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 22 Feb 2024 23:00:32 +0100 Subject: [PATCH 04/36] initialize docs (#595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: barbapapazes Co-authored-by: Estéban --- .gitignore | 1 + README.md | 307 -------------------- docs/.config/docs.yaml | 74 +++++ docs/.docs/public/icon.svg | 5 + docs/.gitignore | 4 + docs/1.guide/1.index.md | 99 +++++++ docs/1.guide/2.app.md | 173 +++++++++++ docs/1.guide/2.event-handler.md | 237 +++++++++++++++ docs/1.guide/4.event.md | 118 ++++++++ docs/1.guide/5.router.md | 146 ++++++++++ docs/2.utils/0.index.md | 13 + docs/2.utils/1.request.md | 75 +++++ docs/2.utils/2.reponse.md | 59 ++++ docs/2.utils/98.advanced.md | 112 ++++++++ docs/2.utils/99.community.md | 25 ++ docs/3.adapters/0.index.md | 19 ++ docs/3.adapters/1.node.md | 43 +++ docs/3.adapters/2.web.md | 44 +++ docs/3.adapters/3.plain.md | 58 ++++ docs/3.adapters/bun.md | 38 +++ docs/3.adapters/cloudflare.md | 60 ++++ docs/3.adapters/deno.md | 54 ++++ docs/3.adapters/netlify.md | 64 +++++ docs/4.guides/from-expressjs-to-h3.md | 398 ++++++++++++++++++++++++++ docs/4.guides/handle-cookie.md | 79 +++++ docs/4.guides/handle-session.md | 146 ++++++++++ docs/4.guides/serve-static-assets.md | 115 ++++++++ docs/4.guides/stream-response.md | 103 +++++++ docs/4.guides/validate-data.md | 117 ++++++++ docs/99.furthor/nightly.md | 37 +++ docs/bun.lockb | Bin 0 -> 520656 bytes docs/package.json | 11 + examples/first-server.ts | 2 +- examples/nested-router.ts | 1 - package.json | 6 +- 35 files changed, 2531 insertions(+), 312 deletions(-) create mode 100644 docs/.config/docs.yaml create mode 100644 docs/.docs/public/icon.svg create mode 100644 docs/.gitignore create mode 100644 docs/1.guide/1.index.md create mode 100644 docs/1.guide/2.app.md create mode 100644 docs/1.guide/2.event-handler.md create mode 100644 docs/1.guide/4.event.md create mode 100644 docs/1.guide/5.router.md create mode 100644 docs/2.utils/0.index.md create mode 100644 docs/2.utils/1.request.md create mode 100644 docs/2.utils/2.reponse.md create mode 100644 docs/2.utils/98.advanced.md create mode 100644 docs/2.utils/99.community.md create mode 100644 docs/3.adapters/0.index.md create mode 100644 docs/3.adapters/1.node.md create mode 100644 docs/3.adapters/2.web.md create mode 100644 docs/3.adapters/3.plain.md create mode 100644 docs/3.adapters/bun.md create mode 100644 docs/3.adapters/cloudflare.md create mode 100644 docs/3.adapters/deno.md create mode 100644 docs/3.adapters/netlify.md create mode 100644 docs/4.guides/from-expressjs-to-h3.md create mode 100644 docs/4.guides/handle-cookie.md create mode 100644 docs/4.guides/handle-session.md create mode 100644 docs/4.guides/serve-static-assets.md create mode 100644 docs/4.guides/stream-response.md create mode 100644 docs/4.guides/validate-data.md create mode 100644 docs/99.furthor/nightly.md create mode 100755 docs/bun.lockb create mode 100644 docs/package.json diff --git a/.gitignore b/.gitignore index 08767158..74b98e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage .idea .eslintcache tsconfig.vitest-temp.json +.DS_Store diff --git a/README.md b/README.md index 1ebdefa0..2f3d0fd6 100644 --- a/README.md +++ b/README.md @@ -27,313 +27,6 @@ H3 (pronounced as /eɪtʃθriː/, like h-3) is a minimal h(ttp) framework built ✔️  **Compatible:** Compatibility layer with node/connect/express middleware -## Install - -```bash -# Using npm -npm install h3 - -# Using yarn -yarn add h3 - -# Using pnpm -pnpm add h3 -``` - -
- Using Nightly Releases - -If you are directly using `h3` as a dependency: - -```json -{ - "dependencies": { - "h3": "npm:h3-nightly@latest" - } -} -``` - -If you are using a framework ([Nuxt](https://nuxt.com/) or [Nitro](https://nitro.unjs.io/)) that is using `h3`: - -pnpm and yarn: - -```json -{ - "resolutions": { - "h3": "npm:h3-nightly@latest" - } -} -``` - -npm: - -```json -{ - "overrides": { - "h3": "npm:h3-nightly@latest" - } -} -``` - -**Note:** Make sure to recreate lockfile and `node_modules` after reinstall to avoid hoisting issues. - -
- -## Usage - -```ts -import { createServer } from "node:http"; -import { createApp, eventHandler, toNodeListener } from "h3"; - -const app = createApp(); -app.use( - "/", - eventHandler(() => "Hello world!"), -); - -createServer(toNodeListener(app)).listen(process.env.PORT || 3000); -``` - -Example using listhen for an elegant listener: - -```ts -import { createApp, eventHandler, toNodeListener } from "h3"; -import { listen } from "listhen"; - -const app = createApp(); -app.use( - "/", - eventHandler(() => "Hello world!"), -); - -listen(toNodeListener(app)); -``` - -## Router - -The `app` instance created by `h3` uses a middleware stack (see [how it works](./src/app.ts)) with the ability to match route prefix and apply matched middleware. - -To opt-in using a more advanced and convenient routing system, we can create a router instance and register it to app instance. - -```ts -import { createApp, eventHandler, createRouter } from "h3"; - -const app = createApp(); - -const router = createRouter() - .get( - "/", - eventHandler(() => "Hello World!"), - ) - .get( - "/hello/:name", - eventHandler((event) => `Hello ${event.context.params.name}!`), - ); - -app.use(router); -``` - -**Tip:** We can register the same route more than once with different methods. - -Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) and matched using [unjs/radix3](https://github.com/unjs/radix3). - -For using nested routers, see [this example](./examples/nested-router.ts) - -## More app usage examples - -```js -// Handle can directly return object or Promise for JSON response -app.use( - "/api", - eventHandler((event) => ({ url: event.node.req.url })), -); - -// We can have better matching other than quick prefix match -app.use( - "/odd", - eventHandler(() => "Is odd!"), - { match: (url) => url.substr(1) % 2 }, -); - -// Handle can directly return string for HTML response -app.use(eventHandler(() => "

Hello world!

")); - -// We can chain calls to .use() -app - .use( - "/1", - eventHandler(() => "

Hello world!

"), - ) - .use( - "/2", - eventHandler(() => "

Goodbye!

"), - ); - -// We can proxy requests and rewrite cookie's domain and path -app.use( - "/api", - eventHandler((event) => - proxyRequest(event, "https://example.com", { - // f.e. keep one domain unchanged, rewrite one domain and remove other domains - cookieDomainRewrite: { - "example.com": "example.com", - "example.com": "somecompany.co.uk", - "*": "", - }, - cookiePathRewrite: { - "/": "/api", - }, - }), - ), -); - -// Legacy middleware with 3rd argument are automatically promisified -app.use( - fromNodeMiddleware((req, res, next) => { - req.setHeader("x-foo", "bar"); - next(); - }), -); - -// Lazy loaded routes using { lazy: true } -app.use("/big", () => import("./big-handler"), { lazy: true }); -``` - -## Utilities - -H3 has a concept of composable utilities that accept `event` (from `eventHandler((event) => {})`) as their first argument. This has several performance benefits over injecting them to `event` or `app` instances in global middleware commonly used in Node.js frameworks, such as Express. This concept means only required code is evaluated and bundled, and the rest of the utilities can be tree-shaken when not used. - -👉 You can check list of exported built-in utils from [JSDocs Documentation](https://www.jsdocs.io/package/h3#package-functions). - -#### Body - -- `readRawBody(event, encoding?)` -- `readBody(event)` -- `readValidatedBody(event, validate)` -- `readMultipartFormData(event)` - -#### Request - -- `getQuery(event)` -- `getValidatedQuery(event, validate)` -- `getRouterParams(event, { decode? })` -- `getRouterParam(event, name, { decode? })` -- `getValidatedRouterParams(event, validate, { decode? })` -- `getMethod(event, default?)` -- `isMethod(event, expected, allowHead?)` -- `assertMethod(event, expected, allowHead?)` -- `getRequestHeaders(event)` (alias: `getHeaders`) -- `getRequestHeader(event, name)` (alias: `getHeader`) -- `getRequestURL(event)` -- `getRequestHost(event)` -- `getRequestProtocol(event)` -- `getRequestPath(event)` -- `getRequestIP(event, { xForwardedFor: boolean })` - -#### Response - -- `send(event, data, type?)` -- `sendNoContent(event, code = 204)` -- `setResponseStatus(event, status)` -- `getResponseStatus(event)` -- `getResponseStatusText(event)` -- `getResponseHeaders(event)` -- `getResponseHeader(event, name)` -- `setResponseHeaders(event, headers)` (alias: `setHeaders`) -- `setResponseHeader(event, name, value)` (alias: `setHeader`) -- `appendResponseHeaders(event, headers)` (alias: `appendHeaders`) -- `appendResponseHeader(event, name, value)` (alias: `appendHeader`) -- `defaultContentType(event, type)` -- `sendRedirect(event, location, code=302)` -- `isStream(data)` -- `sendStream(event, data)` -- `writeEarlyHints(event, links, callback)` - -#### Sanitize - -- `sanitizeStatusMessage(statusMessage)` -- `sanitizeStatusCode(statusCode, default = 200)` - -#### Error - -- `sendError(event, error, debug?)` -- `createError({ statusCode, statusMessage, data? })` - -#### Route - -- `useBase(base, handler)` - -#### Proxy - -- `sendProxy(event, { target, ...options })` -- `proxyRequest(event, { target, ...options })` -- `fetchWithEvent(event, req, init, { fetch? }?)` -- `getProxyRequestHeaders(event)` - -#### Cookie - -- `parseCookies(event)` -- `getCookie(event, name)` -- `setCookie(event, name, value, opts?)` -- `deleteCookie(event, name, opts?)` -- `splitCookiesString(cookiesString)` - -#### Session - -- `useSession(event, config = { password, maxAge?, name?, cookie?, seal?, crypto? })` -- `getSession(event, config)` -- `updateSession(event, config, update)` -- `sealSession(event, config)` -- `unsealSession(event, config, sealed)` -- `clearSession(event, config)` - -#### Cache - -- `handleCacheHeaders(event, opts)` - -#### Cors - -- `handleCors(options)` (see [h3-cors](https://github.com/NozomuIkuta/h3-cors) for more detail about options) -- `isPreflightRequest(event)` -- `isCorsOriginAllowed(event)` -- `appendCorsHeaders(event, options)` (see [h3-cors](https://github.com/NozomuIkuta/h3-cors) for more detail about options) -- `appendCorsPreflightHeaders(event, options)` (see [h3-cors](https://github.com/NozomuIkuta/h3-cors) for more detail about options) - -## Community Packages - -You can use more H3 event utilities made by the community. - -Please check their READMEs for more details. - -PRs are welcome to add your packages. - -- [h3-typebox](https://github.com/kevinmarrec/h3-typebox) - - `validateBody(event, schema)` - - `validateQuery(event, schema)` -- [h3-zod](https://github.com/wobsoriano/h3-zod) - - `useValidatedBody(event, schema)` - - `useValidatedQuery(event, schema)` -- [h3-valibot](https://github.com/intevel/h3-valibot) - - `useValidateBody(event, schema)` - - `useValidateParams(event, schema)` -- [h3-compression](https://github.com/CodeDredd/h3-compression) - - `useGZipCompression(event, response)` - - `useDeflateCompression(event, response)` - - `useBrotliCompression(event, response)` - - `useCompression(event, response)` - - `useGZipCompressionStream(event, response)` - - `useDeflateCompressionStream(event, response)` - - `useCompressionStream(event, response)` -- [@intlify/h3](https://github.com/intlify/h3) - - `defineI18nMiddleware(options)` - - `useTranslation(event)` - - `getHeaderLocale(event, options)` - - `getHeaderLocales(event, options)` - - `getCookieLocale(event, options)` - - `setCookieLocale(event, options)` - - `getPathLocale(event, options)` - - `getQueryLocale(event, options)` - ## License MIT diff --git a/docs/.config/docs.yaml b/docs/.config/docs.yaml new file mode 100644 index 00000000..4f75a443 --- /dev/null +++ b/docs/.config/docs.yaml @@ -0,0 +1,74 @@ +# yaml-language-server: $schema=https://unpkg.com/undocs/schema/config.json +name: h3 +shortDescription: The Web Framework for Modern JavaScript Era +description: + H(TTP) server framework built for high performance and portability running + in any JavaScript runtime. +github: unjs/h3 +url: https://h3.unjs.io +automd: true +redirects: {} +landing: + heroLinks: + playOnline: + label: "Play Online" + icon: "i-simple-icons-lightning" + to: "https://stackblitz.com/github/unjs/h3/tree/main/playground" + contributors: true + featuresTitle: "The Web Framework for Modern JavaScript Era" + features: + # Runtime Agnostic + - title: "Runtime Agnostic" + description: "Your code will work on any JavaScript runtime including Node.js, Bun, Deno and Workers." + icon: "i-material-symbols-lock-open-right-outline-rounded" + to: "" + target: "_blank" + + # Tree-shakable + - title: "Small and Tree-shakable" + description: "h3 core is super lightweight and tree-shakable, only what you use will be included in the the final bundle." + icon: "i-material-symbols-potted-plant-outline" + to: "" + target: "_blank" + + # Composable + - title: "Composable" + description: "Expand your server and add capabilities. Your codebase will scale with your project." + icon: "i-fa-puzzle-piece" + to: "" + target: "_blank" + + # Router + - title: "Fast Router" + description: "Super fast route matching using unjs/radix3" + icon: "i-fa-tree" + to: "https://github.com/unjs/radix3" + target: "_blank" + + # Ecosystem + - title: "UnJS ecosystem" + description: "Built on top of powerful UnJS ecosystem powering Nitro, Nuxt and more frameworks!" + icon: "i-mdi-umbrella-outline" + to: "https://unjs.io" + target: "_blank" + + # Made for Humans + - title: "Made for Humans" + description: "Elegant minimal API to implement HTTP handlers compatible with Web-Standards." + icon: "i-material-symbols-robot-2-sharp" + to: "" + target: "_blank" + + # Compatibility + - title: "Compatible" + description: "Compatibility layer with node/connect/express middleware." + icon: "i-simple-icons-express" + to: "" + target: "_blank" + + # Type Friendly + - title: "Type Friendly" + description: "Codebase fully written in TypeScript with strongly typed utils." + icon: "i-mdi-language-typescript" + to: "" + target: "_blank" diff --git a/docs/.docs/public/icon.svg b/docs/.docs/public/icon.svg new file mode 100644 index 00000000..f650727a --- /dev/null +++ b/docs/.docs/public/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..21fd3a93 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +node_modules +.nuxt +.output +dist diff --git a/docs/1.guide/1.index.md b/docs/1.guide/1.index.md new file mode 100644 index 00000000..af6d7362 --- /dev/null +++ b/docs/1.guide/1.index.md @@ -0,0 +1,99 @@ +# Getting Started + +> Getting started with h3 + +## Overview + +h3 (short for HTTP) is a lightweight and [composable](/utils) server framework for JavaScript that is designed to +work with various javascript runtimes through [adapters](/adapters). + +## Quick Start + +Create a new file `app.ts` (or `app.js`): + +```ts [app.ts] +// Import h3 as npm dependency +import { createApp, createRouter, eventHandler } from "h3"; + +// Create an app instance +export const app = createApp(); + +// Create a new router and register it in app +const router = createRouter(); +app.use(router); + +// Add a new route that matches GET requests to / path +router.get( + "/", + eventHandler((event) => { + return { message: "⚡️ Tadaa!" }; + }), +); +``` + +Now run the development server using [unjs/listhen](https://listhen.unjs.io): + +```sh +npx --yes listhen -w --open ./app.ts +``` + +> [!TIP] +> You don't need to install any additional dependency. Listhen has as preinstalled version of h3! + +And tadaa! We have a web server running locally. + +### What happend? + +Okay, let's now break down our hello world example: + +We first created an [app instance](/guide/app) using `createApp()`. `app` is a tiny server capable of matching requests, generating response and handling lifecycle hooks (such as errors): + +```ts +export const app = createApp(); +``` + +Then we create a [router instance](/guide/router) that can match route patterns and http methods using [unjs/radix3](https://radix3.unjs.io) and register it for app as main handler: + +```ts +const router = createRouter(); + +app.use(router); +``` + +Now it it time to add our first endpoint. In h3, request handlers can be defined using `defineEventHandler` or `eventHandler` helpers (they are aliases). Using wrappers, h3 can supercharge your code with better typehints and future compatibility. + +```ts +eventHandler((event) => {}); +``` + +What is beatiful in h3 is that all you have to do to make a reponse, is to simply return it! Responses can be simple string, JSON objects, data buffers, streams or standard [Web Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). + +```ts +return { message: "⚡️ Tadaa!" }; +``` + +Finally, we use [unjs/listhen](https://listhen.unjs.io) CLI using npx to auto install it. Listhen will automatically setup and start our webserver with zero configuration and adds on-the-fly TypeScript support to your experience! + +## Installing h3 + +You can import h3 either as an npm package or from CDN. + +### Install as npm package + +You can use this method for Node.js and Bun. + +::pm-install{name="h3"} + +:: + +### Import from CDN + +You can directly import h3 from CDN. This method can be used for Bun, Deno and other runtimes such as Cloudflare Workers (you need an adapter). + +```js +import { createApp, eventHandler, toWebhandler } from "https://esm.sh/h3"; + +export const app = createApp(); + +export const handler = toWebHandler(app); +``` diff --git a/docs/1.guide/2.app.md b/docs/1.guide/2.app.md new file mode 100644 index 00000000..bbd40aa5 --- /dev/null +++ b/docs/1.guide/2.app.md @@ -0,0 +1,173 @@ +# App Instance + +> App instance is the core of a h3 server. + +The core of a h3 server is an `app` instance. It is the core of the server that handles incoming requests. You can use app instance to register event handlers. + +## Initializing an app + +You can create a new h3 app instance using [`createApp`](/utilities/create-app) utility: + +```js [app.mjs] +import { createApp } from "h3"; + +const app = createApp(); +``` + +## Setting global options + +You can pass global app configuration when initializing an app. + +**Example:** Create an app with verbose logging enabled. + +```js +const app = createApp({ + debug: true, +}); +``` + +## Setting global hooks + +When initializing an h3 app, you can register global hooks: + +- `onError` +- `onRequest` +- `onBeforeResponse` +- `onAfterResponse` + +These hooks are called for every request and can be used to add global logic to your app such as logging, error handling, etc. + +```js +const app = createApp({ + onError: (error) => { + console.error(error); + }, + onRequest: (event) => { + console.log("Request:", event.path); + }, +}); +``` + +## Registering event handlers + +You can register [event handlers](/guide/event-handler) to app instance using the `app.use`: + +```js +app.use( + "/hello", + defineEventHandler((event) => { + return "Hello world!"; + }), +); +``` + +This will register the event handler to the app instance and will be called for every request starting with the prefix `/hello`. This means that the event handler will be called for `/hello`, `/hello/world`, `/hello/123`, etc. + +You can define multiple event handlers for the same route. h3 will try to to call them one by one in order of registration until one of them returns a response. This is called `stack runner`. + +```js +app.use( + "/", + eventHandler((event) => { + return "First"; + }), +); +app.use( + "/", + eventHandler((event) => { + return "Second"; + }), +); +``` + +In this example, the first event handler will be called for every request starting with `/hello` and the second one will never be called. + +However, if you do not return a response from the first event handler, the second one will be called. This is useful to have a _middleware_ pattern. + +```js +app.use( + "/", + eventHandler((event) => { + console.log("First"); + // No response returned + }), +); +app.use( + "/", + eventHandler((event) => { + return "Second"; + }), +); +``` + +If all handlers get called and no response is returned, h3 will end the request with 404 status response. + +> [!NOTE] +> Using an empty `return` or `return undefined` make a [`404 Not Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404) status response. +> Also using `return null` will make a [`204 No Content`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204) status reponse. + +> [!TIP] +> Use `return {}` or `return ""` or `return true` to make an explicit response. + +## Event handler options + +The method `use` accepts an optional `options` object as third argument: + +```js +app.use( + "/hello", + defineEventHandler((event) => { + return "Hello world!"; + }), + { + // Options + }, +); +``` + +### `matcher` + +You can define a custom matcher function to have more advanced logic for matching requests but simple than a router. + +For example, you can match only odd URLs, `/1`, `/3`, `/5`, etc.: + +```js +app.use( + "/", + defineEventHandler((event) => { + return "Odd URLs only"; + }), + { + match: (url) => { + return url.substr(1) % 2; + }, + }, +); +``` + +> [!WARNING] +> Do not use the custom matcher as a router. It is not designed for that purpose. Use a [router](/guide/router) instead. + +### `lazy` + +You can provide an async function that h3 will load on first time a request matching the route is received. It's useful for dynamic imports to reduce startup time. + +```js +app.use("/big", () => import("./big-handler"), { lazy: true }); +``` + +This reduce the startup time because the runtime have less code to load and parse when starting the server. + +You can use a [syntax sugar](#lazy-event-handlers) over this option using [`defineLazyEventHandler`](/concepts/utilities) or [`lazyEventHandler`](/concepts/utilities) utilities. + +## Internals + +> [!IMPORTANT] +> This details are mainly informational. **never** directly use internals for production applications! + +h3 app instance has some additional properties. However it is usually not recommended to directly access them unless you know what are you doing! + +- `app.stack`: An ordered array of currently registered event handlers. + - Each item has `route` and `handler` properties. +- `app.options`: Global options object provided when initializing the app. +- `app.handler`: Direct stack handler function (**unsafe to directly call**). diff --git a/docs/1.guide/2.event-handler.md b/docs/1.guide/2.event-handler.md new file mode 100644 index 00000000..5f2520f0 --- /dev/null +++ b/docs/1.guide/2.event-handler.md @@ -0,0 +1,237 @@ +# Event Handler + +> Event handler define application logic. + +After creating an [app instance](/guide/app), you can start defining your application logic using event handlers. + +An event handler is a function that receive an [`Event`](/concepts/event) instance and returns a response. You can compare it to controllers in other frameworks. + +## Defining event handlers + +You can define event handlers using [`defineEventHandler`](/concepts/event) or [`eventHandler`](/concepts/event) utilities: + +> [!NOTE] +> You can use [`eventHandler`](/concepts/event) and [`defineEventHandler`](/concepts/event) interchangeably. They are aliases. You can use the one you prefer but **stick to it** for consistency. + +```js +import { defineEventHandler } from "h3"; + +defineEventHandler((event) => { + return "Response"; +}); +``` + +The callback function can be sync or async: + +```js +import { defineEventHandler } from "h3"; + +defineEventHandler(async (event) => { + return "Response"; +}); +``` + +### Object Syntax + +You can use an object syntax in [`defineEventHandler`](/concepts/utilities) for more flexible options. + +```js +defineEventHandler({ + onRequest: [], + onBeforeResponse: [] + handler: (event) => { + return "Response"; + }, +}) +``` + +## Responses Types + +Values returned from event handlers are automatically converted to responses. It can be: + +- JSON serializable value. If returning a JSON object or serializable value, it will be stringified and sent with default `application/json` content-type. +- `string`: Sent as-is using default `application/html` content-type. +- `null`: h3 with end response with `204 - No Content` status code. +- [Web `ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) or [node `Readable`](https://nodejs.org/api/stream.html#readable-streams) +- [Web `ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) or [node `Buffer`](https://nodejs.org/api/buffer.html#buffer) +- [Web Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) +- [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) instance. It's supported but **recommended** to throw errors instead of returning them using [`createError`](/concepts/utilities) utility. + +Any of above values could also be wrapped in a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). This means that you can return a `Promise` from your event handler and h3 will wait for it to resolve before sending the response. + +**Example:** Send HTML response: + +```js +app.use(eventHandler(async (event) => "

Hello world!

")); +``` + +**Example:** Send JSON response: + +```js +app.use( + "/api", + eventHandler(async (event) => ({ url: event.node.req.url })), +); +``` + +**Example:** Send a promise: + +```js +app.use( + defineEventHandlers(async (event) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ url: event.node.req.url }); + }, 1000); + }); + }), +); +``` + +## Error Handling + +You can easily control the error returned by using the `createError` utility. + +```js +import { createError } from "h3"; + +app.use( + "/hello", + defineEventHandler((event) => { + throw createError({ + status: 400, + message: "An error occured", + }); + }), +); +``` + +This will end the request with `400 - Bad Request` status code and the following JSON response: + +```json +{ + "status": 400, + "message": "An error occured" +} +``` + +### Internal errors + +If during calling an event handler an error with `new Error()` will be thrown (without `create Error`), h3 will automatically catch as a [`500 - Internal Server Error`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) status response considering it an unhandled error. + +```js +app.use( + "/hello", + defineEventHandler((event) => { + // Do NOT do this and use createError()! + throw new Error("Something went wrong"); + }), +); +``` + +## Lazy event handlers + +> [!NOTE] +> This is a syntax sugar over the [`lazy`](#lazy-matcher) option. + +You can define lazy event handlers using `defineLazyEventHandler` or `lazyEventHandler` utilities. This allow you to define some one-time logic that will be executed only once when the first request matching the route is received. + +A lazy event handler must return an event handler: + +```js +import { defineLazyEventHandler } from "h3"; + +app.use( + defineLazyEventHandler(() => { + console.log("This will be executed only once"); + // This will be executed only once + return defineEventHandler((event) => { + // This will be executed on every request + return "Response"; + }); + }), +); +``` + +This is useful to define some one-time logic such as configuration, class initialization, heavy computation, etc. + +## Middleware + +Event handlers that don't return any value act as middleware. They can be used to add side effects to your application such as logging, caching, etc or to modify the request or response. + +> [!TIP] +> Middleware pattern is **not recommanded** for h3 in general. Side effects can affect global application performance and make tracing logic harder. +> Instead use h3 composables and object syntax hooks. + +Similar to normal event handlers, you can define middleware using `defineEventHandler` or `eventHandler` utilities: + +```js +import { defineEventHandler } from "h3"; + +defineEventHandler((event) => { + console.log(`Middleware. Path: ${event.path}`); +}); +``` + +> [!IMPORTANT] +> Middleware **must not** return any value or directly return a response for `event`. +> If you return a response, it will act as a normal event handler! + +### Registering middleware + +Then, you can register middleware to [app instance](/guide/app) using the `use` method: + +```js +app.use( + defineEventHandler((event) => { + console.log("Middleware 1"); + }), +); +app.use( + defineEventHandler((event) => { + console.log("Middleware 2"); + }), +); +app.use( + defineEventHandler((event) => { + return "Response"; + }), +); +``` + +You can define as much middleware as you need. They will be called in order of registration. + +## Converting to h3 handler + +There are situations that you might want to convert an event handler or utility made for Node.js or another framework to h3. +There are built-in utils to do this.! + +### Converting from Node.js handlers + +If you have a legacy request handler with `(req, res) => {}` syntax made for Node.js, you can use `fromNodeListener` to convert it to an h3 event handler. + +```js [app.mjs] +import { createApp, fromNodeMiddleware } from "h3"; + +import exampleMiddleware from "example-node-middleware"; + +export const app = createApp(); + +app.use(fromNodeListener(exampleMiddleware())); +``` + +> [!TIP] +> For example, this will help you to use [Vite Middleware mode](https://vitejs.dev/config/server-options.html#server-middlewaremode) with h3 apps. + +### Converting from Web handlers + +You can convert a fetch-like function (with `Request => Response` signuture) into an h3 event handler using `fromWebHandler` util. + +```js [app.mjs] +import { webHandler } from "web-handler"; // This package doesn't exist, it's just an example +import { createApp, fromWebHandler } from "h3"; + +export const app = createApp(); + +app.use(fromWebHandler(webHandler)); +``` diff --git a/docs/1.guide/4.event.md b/docs/1.guide/4.event.md new file mode 100644 index 00000000..d25b5c37 --- /dev/null +++ b/docs/1.guide/4.event.md @@ -0,0 +1,118 @@ +# Event Object + +> Event object carries an incoming request and context. + +Everytime a new HTTP requerst comes, h3 internally create an Event object and passes it though event handlers until sending response. + +An event is passed through all the lifecycle hooks and composable utils to use it as context. + +**Example:** + +```js +import { defineEventHandler, readBody, query } from "h3"; + +app.use( + defineEventHandler((event) => { + // Log event. `.toString()` stringifies to a simple string like `[GET] /` + console.log(`Request: ${event.toString()}`); + + // Parse query parms + const query = getQuery(event) + + // Try to read request body + const body = await readBody(event).catch(() => {}) + + // Echo back request as response + return { + path: event.path, + method: event.method, + query, + body, + } + }), +); +``` + +## Properties + +The main properties of an event are: + +### `event.node` + +The `event.node` allows to access the native Node.js request and response. In runtimes other than Node.js/Bun, h3 makes a compatible shim using [unjs/unenv](https://unenv.unjs.io). + +> [!IMPORTANT] +> Try to **avoid** depending on `event.node.*` context as much as you can and instead prefer h3 utils. + +```js +defineEventHandler((event) => { + event.node.req; // Node.js HTTP Request + event.node.res; // Node.js HTTP Response +}); +``` + +### `event.web?` + +Only if available, it is an object with [`request`](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) and [`url`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) propertiers to access native web request context. + +### `event.method` + +Access to the normalized (uppercase) request [method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods). + +### `event.path` + +Access to the request request path. (**Example:** `/test?test=123`) + +- `context` with some context information about the request. +- `headers` with a **normalized version** of the headers of the request. +- `handled` with a boolean that indicates if the request has terminated. + +### `event.headers` + +Access tp the normalized request [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers). + +> [!TIP] +> You can alternatively use `getHeaders(event)` or `getHeader(event, name)` for a simplified interface. + +### `event.context` + +The context is an object that contains arbitrary information abut the request. +You can store your custom properies inside `event.context` to share across composable utils. + +### `event.handled` + +Specifies if response is already handled or not. Initially for each request it is `false` and when a response is generated, it is set to `true`. + +**Advanced:** If you manually handle the response, set it to `true` to tell h3 stop sending any responses. + +## Methods + +Actually, h3 provide a function to help you to create a response before the end of the request. + +### `event.respondWith` + +The `respondWith` method is used to create a response without ending the request. + +You must craft a response using the [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) constructor. + +> [!TIP] +> Prefer explicit `return` over `respondWith` as best practice. + +> [!IMPORTANT] +> A `responseWith` will **always** take precedence over the returned value, from current and next event handlers. If there is no returned value, the request will continue until the end of the stack runner. + +**Example:** + +```js +import { defineEventHandler } from "h3"; + +app.use( + defineEventHandler((event) => { + await event.respondWith(new Response("Hello World")); + + return "..."; // DOES NOT WORKS + }), +); +``` + +With this example, the client will receive `Hello World`. diff --git a/docs/1.guide/5.router.md b/docs/1.guide/5.router.md new file mode 100644 index 00000000..7e271588 --- /dev/null +++ b/docs/1.guide/5.router.md @@ -0,0 +1,146 @@ +# Router + +> Split your application using routes. + +Using h3 router allows more advanced and convenient routing system such as parameters and HTTP methods while the [app instance](/guide/app) itself only allows static prefix matching. + +> [!NOTE] +> Internally h3 uses [unjs/radix3](https://radix3.unjs.io) for route matching. + +## Usage + +First, you need to create a router using `createRouter` utility and add it to app stack. + +```js +import { createApp, defineEventHandler, createRouter } from "h3"; + +const app = createApp(); +const router = createRouter(); +app.use(router;) +``` + +Then, you can register a route to the router using a method where the name is the HTTP method: + +```js +router.get( + "/hello", + defineEventHandler((event) => { + return "Hello world!"; + }), +); +``` + +In this example, we register a route for the `GET` method. This means that the event handler will be called only for `GET` requests for the `/hello` route. If you try to send a `POST` or a request to `/hello/world`, the event handler will not be called. + +> [!NOTE] +> You can still use `use` to register an event handler to the router. It will be called for every HTTP methods. + +This means that you can register multiple event handlers for the same route with different methods: + +```js +router + .get( + "/hello", + defineEventHandler((event) => { + return "GET Hello world!"; + }), + ) + .post( + "/hello", + defineEventHandler((event) => { + return "POST Hello world!"; + }), + ); +``` + +## Route params + +You can define parameters in your routes using `:` prefix: + +```js +router.get( + "/hello/:name", + defineEventHandler((event) => { + return `Hello ${event.context.params.name}!`; + }), +); +``` + +In this example, the `name` parameter will be available in the `event.context.params` object. + +If you send a request to `/hello/world`, the event handler will return `Hello world!`. + +> [!NOTE] +> You can use as many parameters as you want in your routes. + +## Wildcard matcher + +Instead of named params, you can use `*` for unnamed + +```js +router.get( + "/hello/*", + defineEventHandler((event) => { + return `Hello ${event.context.params._}!`; + }), +); +``` + +This will match both `/hello` and sub routes such as `/hello/world` or `/hello/123`. But it will only match one level of sub routes. + +You can access to the wildcard content using `event.context.params._` where `_` is a string containing the wildcard content. + +If you need to match multiple levels of sub routes, you can use `**` prefix: + +```js +router.get( + "/hello/**", + defineEventHandler((event) => { + return `Hello ${event.context.params._}!`; + }), +); +``` + +This will match `/hello`, `/hello/world`, `/hello/123`, `/hello/world/123`, etc. + +> [!NOTE] +> Param `_` will store the full wildcard content as a single string. + +## Nested Routers + +You can nest routers to create a tree of routers. This is useful to split your application into multiple parts like the API and the website. + +```js +import { createRouter, createApp } from "h3"; + +export const app = createApp(); + +const websiteRouter = createRouter().get( + "/", + defineEventHandler((event) => { + return "Hello world!"; + }), +); + +const apiRouter = createRouter().get( + "/hello", + defineEventHandler((event) => { + return "Hello API!"; + }), +); + +websiteRouter.use("/api/**", useBase("/api", apiRouter.handler)); + +app.use(websiteRouter); +``` + +We create two routers. The first one, called `websiteRouter` is the main one. The second one, we create a second router called `apiRouter`. + +Then, we connect the `apiRouter` to the `websiteRouter` using `use` and a wildcard to be sure that every routes starting and HTTP methods with `/api` will be handled by the `apiRouter`. + +> [!NOTE] +> Do not forget to use `.handler` to get the event handler from the router. + +`useBase` is used to add a prefix to each routes of the router. In this example, we add `/api` prefix to each routes of the `apiRouter`. So, the route `/hello` will be `/api/hello`. + +Finally, we register the `websiteRouter` to the `app` instance. diff --git a/docs/2.utils/0.index.md b/docs/2.utils/0.index.md new file mode 100644 index 00000000..12e3009e --- /dev/null +++ b/docs/2.utils/0.index.md @@ -0,0 +1,13 @@ +# Composable Utils + +> Keep your severs light and fast with composables. + +H3 is a composable framework. Instead of providing a big core, you start with a lightweight [app instance](/guide/app) and for every functionality, there is either a built-in utility or you can make yours. + +Composable utilities have huge advantages comparing to traditional plugin/middleware approaches: + +✅ Your server only includes and runs the code that is needed
+✅ You can extend your server functionality easily without adding global plugins
+✅ The usage is explicit and clean with less global middleware and plugins
+ +All utilities, have access to [event object](/guide/event). This way they can access incoming request and use a shared context with `event.context`. diff --git a/docs/2.utils/1.request.md b/docs/2.utils/1.request.md new file mode 100644 index 00000000..e281454e --- /dev/null +++ b/docs/2.utils/1.request.md @@ -0,0 +1,75 @@ +# Request + +> Utilities to access incoming request + + + +### `assertMethod(event, expected, allowHead?)` + +### `getHeader(event, name)` + +### `getHeaders(event)` + +### `getQuery(event)` + +### `getRequestHeader(event, name)` + +### `getRequestHeaders(event)` + +### `getRequestHost(event, opts: { xForwardedHost? })` + +### `getRequestIP(event)` + +### `getRequestProtocol(event, opts: { xForwardedProto? })` + +### `getRequestURL(event, opts: { xForwardedHost?, xForwardedProto? })` + +### `getRouterParam(event, name, opts: { decode? })` + +### `getRouterParams(event, opts: { decode? })` + +### `getValidatedQuery(event, validate)` + +### `getValidatedRouterParams(event, validate, opts: { decode? })` + +### `isMethod(event, expected, allowHead?)` + +### `toWebRequest(event)` + + + + + +### `getRequestFingerprint(event, opts)` + + + +## Body utils + + + +### `getRequestWebStream(event)` + +Captures a stream from a request. + +### `readBody(event, options: { strict? })` + +Reads request body and tries to safely parse using [destr](https://github.com/unjs/destr). + +### `readFormData(event)` + +Constructs a FormData object from an event, after converting it to a a web request. + +### `readMultipartFormData(event)` + +Tries to read and parse the body of a an H3Event as multipart form. + +### `readRawBody(event, encoding)` + +Reads body of the request and returns encoded raw string (default), or `Buffer` if encoding is falsy. + +### `readValidatedBody(event, validate)` + +Tries to read the request body via `readBody`, then uses the provided validation function and either throws a validation error or returns the result. + + diff --git a/docs/2.utils/2.reponse.md b/docs/2.utils/2.reponse.md new file mode 100644 index 00000000..a8010000 --- /dev/null +++ b/docs/2.utils/2.reponse.md @@ -0,0 +1,59 @@ +# Response + +> Utilities to send response headers and data + + + +### `appendHeader(event, name, value)` + +### `appendHeaders(event, headers)` + +### `appendResponseHeader(event, name, value)` + +### `appendResponseHeaders(event, headers)` + +### `clearResponseHeaders(event, headerNames?)` + +Remove all response headers, or only those specified in the headerNames array. + +### `defaultContentType(event, type?)` + +### `getResponseHeader(event, name)` + +### `getResponseHeaders(event)` + +### `getResponseStatus(event)` + +### `getResponseStatusText(event)` + +### `isStream(data)` + +### `isWebResponse(data)` + +### `removeResponseHeader(event, name)` + +### `send(event, data?, type?)` + +### `sendNoContent(event, code?)` + +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. + +### `sendRedirect(event, location, code)` + +### `sendStream(event, stream)` + +### `sendWebResponse(event, response)` + +### `setHeader(event, name, value)` + +### `setHeaders(event)` + +### `setResponseHeader(event, name, value)` + +### `setResponseHeaders(event)` + +### `setResponseStatus(event, code?, text?)` + +### `writeEarlyHints(event, hints, cb)` + + diff --git a/docs/2.utils/98.advanced.md b/docs/2.utils/98.advanced.md new file mode 100644 index 00000000..ceb72e1e --- /dev/null +++ b/docs/2.utils/98.advanced.md @@ -0,0 +1,112 @@ +# Advanced + +> More utilities + +## Session utils + + + +### `clearSession(event, config)` + +### `getSession(event, config)` + +### `sealSession(event, config)` + +### `unsealSession(_event, config, sealed)` + +### `updateSession(event, config, update?)` + +### `useSession(event, config)` + + + +## Cookie utils + + + +### `deleteCookie(event, name, serializeOptions?)` + +Remove a cookie by name. + +### `getCookie(event, name)` + +Get a cookie value by name. + +### `parseCookies(event)` + +Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs. + +### `setCookie(event, name, value, serializeOptions?)` + +Set a cookie value by name. + +### `splitCookiesString(cookiesString)` + +Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas that are within a single set-cookie field-value, such as in the Expires portion. This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2 Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128 Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25 Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation + + + +## Sanitize + + + +### `sanitizeStatusCode(statusCode?, defaultStatusCode)` + +### `sanitizeStatusMessage(statusMessage)` + + + +## Route + + + +### `useBase(base, handler)` + +Prefixes and executes a handler with a base path. + + + +## Cache + + + +### `handleCacheHeaders(event, opts)` + +Check request caching headers (`If-Modified-Since`) and add caching headers (Last-Modified, Cache-Control) Note: `public` cache control will be added by default + + + +## Proxy + + + +### `fetchWithEvent(event, req, init?, options?: { fetch: F })` + +### `getProxyRequestHeaders(event)` + +### `proxyRequest(event, target, opts)` + +### `sendProxy(event, target, opts)` + + + +## CORS + + + +### `appendCorsHeaders(event, options)` + +c8 ignore end +c8 ignore start + +### `appendCorsPreflightHeaders(event, options)` + +c8 ignore start + +### `handleCors(event, options)` + +### `isCorsOriginAllowed(origin, options)` + +### `isPreflightRequest(event)` + + diff --git a/docs/2.utils/99.community.md b/docs/2.utils/99.community.md new file mode 100644 index 00000000..d868be80 --- /dev/null +++ b/docs/2.utils/99.community.md @@ -0,0 +1,25 @@ +# Community + +> Utils from community 💛 + +You can use external h3 event utilities made by the community. + +## `h3-typebox` + +:read-more{to="https://github.com/kevinmarrec/h3-typebox"} + +## `h3-zod` + +:read-more{to="https://github.com/wobsoriano/h3-zod"} + +## `h3-valibot` + +:read-more{to="https://github.com/intevel/h3-valibot"} + +## `h3-compression` + +:read-more{to="https://github.com/CodeDredd/h3-compression"} + +## `@intlify/h3` + +:read-more{to="https://github.com/intlify/h3"} diff --git a/docs/3.adapters/0.index.md b/docs/3.adapters/0.index.md new file mode 100644 index 00000000..443c89bd --- /dev/null +++ b/docs/3.adapters/0.index.md @@ -0,0 +1,19 @@ +# Runtime Adapters + +> Run h3 everywhere using adapters. + +The [app instance](/guide/app) of h3 is lightweight without any logic about runtime it is going to run. +Using h3 adapters, we can easily integrate server with each runtime. + +There are 3 base adapters: + +- [Node.js](/adapters/node) +- [Web](/adapters/web) +- [Plain](/adapters/plain) + +Also see guides for specific runtimes: + +- [Bun](/adapters/bun) +- [Cloudflare](/adapters/cloudflare) +- [Deno](/adapters/deno) +- [Netlify](/adapters/netlify) diff --git a/docs/3.adapters/1.node.md b/docs/3.adapters/1.node.md new file mode 100644 index 00000000..ca67aff0 --- /dev/null +++ b/docs/3.adapters/1.node.md @@ -0,0 +1,43 @@ +# Node.js + +> Natively run h3 servers with Node.js. + +In order to start h3 apps in [Node.js](https://nodejs.org/), use `toNodeListener` adapter to convert h3 app into a [Node.js requestListener](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener). + +## Usage + +First, create an h3 app: + +```js [app.mjs] +import { createApp, defineEventHandler } from "h3"; + +export const app = createApp(); + +app.use(defineEventHandler(() => "Hello world!")); +``` + +Create Node.js server entry: + +```js [server.mjs] +import { createServer } from "node:http"; +import { toNodeListener } from "h3"; +import { app } from "./app.mjs"; + +createServer(toNodeListener(app)).listen(process.env.PORT || 3000); +``` + +Now, you can run you h3 app natively with Node.js: + +```bash [terminal] +node ./server.mjs +``` + +## Using listhen + +Alternatively, you can use [unjs/listhen](https://listhen.unjs.io). In this method, you only need to make `app.mjs` with a `default` or `app` export. + +Run this command to run your servers: + +```sh +npx --yes listhen ./app.ts +``` diff --git a/docs/3.adapters/2.web.md b/docs/3.adapters/2.web.md new file mode 100644 index 00000000..9b6b44ea --- /dev/null +++ b/docs/3.adapters/2.web.md @@ -0,0 +1,44 @@ +# Web + +> Run your h3 apps in edge runtimes with Web API compatibility. + +In order to run h3 apps in web compatible edge runtimes supporting [`fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), use `toWebHandler` adapter to convert h3 app into a fetch-like function. + +## Usage + +First, create app entry: + +```js [app.mjs] +import { createApp, eventHandler } from "h3"; + +export const app = createApp(); + +app.use(eventHandler(() => "Hello world!")); +``` + +Create web entry: + +```js [web.mjs] +import { toWebHandler } from "h3"; +import { app } from "./app.mjs"; + +// Create Web Adapter +export const handler = toWebHandler(app); + +// Integrate handler with your runtime. +// Input is a Request and response is Promise +``` + +### Local testing + +You can test adapter using any compatible JavaScript runtime by passing a Request object. + +```js [web.test.mjs] +import { handler } from "./web.mjs"; + +const response = await handler(new Request(new URL("/", "http://localhost"))); + +console.log(await response.text()); // Hello world! +``` + +Run with `node ./web.test.mjs`. diff --git a/docs/3.adapters/3.plain.md b/docs/3.adapters/3.plain.md new file mode 100644 index 00000000..c5e19da5 --- /dev/null +++ b/docs/3.adapters/3.plain.md @@ -0,0 +1,58 @@ +# Plain + +> Run h3 servers into any unknown runtime! + +There might be cases where your runtime is nither Node.js or Web compatible. Using plain adapter you can have an object input/output interface. + +> [!NOTE] +> This can be also be particulary useful for testing your server or running inside lambda-like environments. + +## Usage + +First, create app entry: + +```js [app.mjs] +import { createApp, eventHandler } from "h3"; + +export const app = createApp(); + +app.use(eventHandler(() => "Hello world!")); +``` + +Create plain entry: + +```js [plain.mjs] +import { toPlainHandler } from "h3"; +import { app } from "./app.mjs"; + +export const handler = toPlainHandler(app); +``` + +### Local testing + +You can test adapter using any JavaScript runtime. + +```js [plain.test.mjs] +import { handler } from "./plain.mjs"; + +const response = await handlerRequest({ + method: "GET", + path: "/", + headers: { + "x-test": "test", + }, + body: undefined, + context: {}, +}); +``` + +Example response: + +```js +{ + status: 200, + statusText: '', + headers: [ [ 'content-type', 'text/html' ] ], + body: 'Hello world!' +} +``` diff --git a/docs/3.adapters/bun.md b/docs/3.adapters/bun.md new file mode 100644 index 00000000..031cd1fb --- /dev/null +++ b/docs/3.adapters/bun.md @@ -0,0 +1,38 @@ +# Bun + +> Run your h3 apps with Bun + +In order to run h3 apps in [Bun](https://bun.sh/), use the [Web Adapter](/adapters/web). + +> [!NOTE] +> Alternatively you can use [Node.js adapter](/adapters/node) as Bun is fully compatible with Node.js API! + +## Usage + +Create app entry: + +```js [app.mjs] +import { createApp, defineEventHandler } from "h3"; + +export const app = createApp(); + +app.use(defineEventHandler(() => "Hello world!")); +``` + +Create Bun server entry: + +```js [server.mjs] +import { toWebHandler } from "h3"; +import { app } from "./app.mjs"; + +const server = Bun.serve({ + port: 3000, + fetch: toWebHandler(app), +}); +``` + +Now, your can run Bun server: + +```bash +bun --bun ./server.mjs +``` diff --git a/docs/3.adapters/cloudflare.md b/docs/3.adapters/cloudflare.md new file mode 100644 index 00000000..3c7be2de --- /dev/null +++ b/docs/3.adapters/cloudflare.md @@ -0,0 +1,60 @@ +# Cloudflare + +> Run your h3 apps in Cloudflare Workers + +You can directly host your h3 applications to [Cloudflare Workers](https://workers.cloudflare.com/) using [Web Adapter](/adapters/web). + +## Usage + +Create app entry: + +```js [app.mjs] +import { createApp, defineEventHandler } from "h3"; + +export const app = createApp(); + +app.use(defineEventHandler(() => "Hello world!")); +``` + +Create entry for a Cloudflare Worker: + +```js [cloudflare.mjs] +import { toWebHandler } from "h3"; +import { app } from "./app.mjs"; + +const handler = toWebHandler(app); + +export default { + async fetch(request, env, ctx) { + return handler(request, { + cloudflare: { env, ctx }, + }); + }, +}; +``` + +Then, create a simple `wrangler.toml`: + +```ini [wrangler.toml] +name = "h3-app" +main = "cloudflare.mjs" +compatibility_date = "2023-08-01" +``` + +Finally, use `wrangler dev` to locally preview: + +```bash +npx wrangler dev +``` + +To deploy, use `wrangler deploy`: + +```bash +npx wrangler deploy +``` + +--- + +::read-more +👉 See [pi0/h3-on-edge](https://github.com/pi0/h3-on-edge) demo for a fully working example ([deployment](https://h3-on-edge.pi0.workers.dev/)). +:: diff --git a/docs/3.adapters/deno.md b/docs/3.adapters/deno.md new file mode 100644 index 00000000..6e4256aa --- /dev/null +++ b/docs/3.adapters/deno.md @@ -0,0 +1,54 @@ +# Deno + +> Run your h3 apps in Deno Deploy + +You can directly host your h3 applications to [Deno Deploy](https://deno.com/deploy) using [Web Adapter](/adapters/web). + +## Usage + +Create app entry: + +```js [app.mjs] +import { createApp, defineEventHandler } from "h3"; + +export const app = createApp(); + +app.use(defineEventHandler(() => "Hello world!")); +``` + +Create entry for Deno Deploy: + +```js [deno.mjs] +import { toWebHandler } from "h3"; +import { app } from "./app.mjs"; + +Deno.serve(toWebHandler(app)); +``` + +Create an `import_map.json`: + +```json [import_map.json] +{ + "imports": { + "h3": "https://esm.sh/h3@latest" + } +} +``` + +Finally, use `deno run` to locally preview: + +```bash [terminal] +deno run --allow-net ./deno.mjs +``` + +To deploy, use `deployctl deploy`: + +```bash [terminal] +deployctl deploy --prod --exclude=node_modules --import-map=./import_map.json ./deno.mjs +``` + +--- + +::read-more +See [pi0/h3-on-edge](https://github.com/pi0/h3-on-edge) demo for a fully working example ([deployment](https://h3-on-edge.deno.dev/)). +:: diff --git a/docs/3.adapters/netlify.md b/docs/3.adapters/netlify.md new file mode 100644 index 00000000..1ffd60d0 --- /dev/null +++ b/docs/3.adapters/netlify.md @@ -0,0 +1,64 @@ +# Netlify + +> Run your h3 apps in Netlify Edge + +You can directly host your h3 applications to [Netlify Edge](https://www.netlify.com/platform/core/edge/) using [Web Adapter](/adapters/web). + +## Usage + +Create app entry: + +```js [app.mjs] +import { createApp, eventHandler } from "h3"; + +export const app = createApp(); + +app.use(eventHandler(() => "Hello world!")); +``` + +Create entry for netlify-edge: + +```js [netlify/index.mjs] +import { toWebHandler } from "h3"; +import { app } from "./app.mjs"; + +export const handler = toWebHandler(app); +``` + +Then, create `import_map.json`: + +```json [import_map.json] +{ + "imports": { + "h3": "https://esm.sh/h3@latest" + } +} +``` + +Create `netlify.toml`: + +```ini [netlify.toml] +[build] + edge_functions = "netlify" + +[functions] + deno_import_map = "./import_map.json" +``` + +Finally, use `netlify dev` to locally preview: + +```bash [terminal] +npx netlify dev +``` + +To deploy, use `netlify deploy`: + +```bash [terminal] +npx netlify deploy --prod +``` + +--- + +::read-more +See [pi0/h3-on-edge](https://github.com/pi0/h3-on-edge) demo for a fully working example. +:: diff --git a/docs/4.guides/from-expressjs-to-h3.md b/docs/4.guides/from-expressjs-to-h3.md new file mode 100644 index 00000000..8d6c90b5 --- /dev/null +++ b/docs/4.guides/from-expressjs-to-h3.md @@ -0,0 +1,398 @@ +# From Express.js to h3 + +> Through various examples, let's see how easy it is to use h3 if you are familiar with Express.js. + +During this guide, we will reproduce many examples from the [Express.js documentation](https://expressjs.com/en/starter/examples.html) to show you how to do the same thing with h3. + +> [!NOTE] +> If you are not familiar with Express.js, you can safely skip this guide. + +The idea is to show you how similar h3 is to Express.js. Once you understand the similarities, you will be able to use h3 without any problem if you are familiar with Express.js. + +> [!CAUTION] +> Even if h3 seems to be similar to Express.js, it does not mean that Express.js is still viable. Express.js is an old framework that has not evolved for a long time. It's not a good choice for new projects since it can easily lead to security issues and memory leaks. + +With h3, you also have reloading out-of-the-box without any configuration using [unjs/listhen](https://listhen.unjs.io). + +> [!NOTE] +> You can run every h3 examples using `npx --yes listhen -w ./app.ts`. + +## Hello World + +The first example from the Express.js documentation is the [Hello World](https://github.com/expressjs/express/blob/master/examples/hello-world/index.js). + +The code is pretty simple: + +```js [index.js] +/** + * Express.js example app. + */ +var express = require("express"); +var app = express(); + +app.get("/", function (req, res) { + res.send("Hello World"); +}); + +app.listen(3000); +console.log("Express started on port 3000"); +``` + +Let's see how to do the same thing with h3: + +```ts [app.ts] +/** + * h3 example app. + */ +import { defineEventHandler, createApp } from "h3"; + +export const app = createApp(); + +app.use( + "/", + defineEventHandler((event) => { + return "Hello World"; + }), +); +``` + +Then, you can use `npx --yes listhen -w ./app.ts` to start the server and go to http://localhost:3000 to see the result. + +:read-more{to="/concepts/app"} + +## Multi Router + +The second example is the [Multi Router](https://github.com/expressjs/express/blob/master/examples/multi-router/index.js). In this example, we create many routers to split the logic. + +```js [index.js] +/** + * Express.js example app. + */ +var express = require("express"); + +var app = express(); + +var apiv1 = express.Router(); + +apiv1.get("/", function (req, res) { + res.send("Hello from APIv1 root route."); +}); + +apiv1.get("/users", function (req, res) { + res.send("List of APIv1 users."); +}); + +var apiv2 = express.Router(); + +apiv2.get("/", function (req, res) { + res.send("Hello from APIv2 root route."); +}); + +apiv2.get("/users", function (req, res) { + res.send("List of APIv2 users."); +}); + +app.use("/api/v1", apiv1); +app.use("/api/v2", apiv2); + +app.get("/", function (req, res) { + res.send("Hello from root route."); +}); + +app.listen(3000); +console.log("Express started on port 3000"); +``` + +> [!NOTE] +> For some facilities, we group every files in the same one. + +Using h3, we can do the same thing: + +```ts [app.ts] +/** + * h3 example app. + */ +import { createApp, createRouter, defineEventHandler, useBase } from "h3"; + +export const app = createApp(); + +const apiv1 = createRouter() + .get( + "/", + defineEventHandler(() => { + return "Hello from APIv1 root route."; + }), + ) + .get( + "/users", + defineEventHandler(() => { + return "List of APIv1 users."; + }), + ); + +const apiv2 = createRouter() + .get( + "/", + defineEventHandler(() => { + return "Hello from APIv2 root route."; + }), + ) + .get( + "/users", + defineEventHandler(() => { + return "List of APIv2 users."; + }), + ); + +app.use("/api/v1/**", useBase("/api/v1", apiv1.handler)); +app.use("/api/v2/**", useBase("/api/v2", apiv2.handler)); +``` + +It's quite similar. The main difference is that we have to use `useBase` to define a base path for a router. + +:read-more{to="/concepts/router"} + +## Params + +The third example is the [Params](https://github.com/expressjs/express/tree/master/examples/params/index.js). In this example, we use parameters in the route. + +```js [index.js] +/** + * Express.js example app. + */ +var createError = require("http-errors"); +var express = require("express"); +var app = express(); + +var users = [ + { name: "tj" }, + { name: "tobi" }, + { name: "loki" }, + { name: "jane" }, + { name: "bandit" }, +]; + +app.param(["to", "from"], function (req, res, next, num, name) { + req.params[name] = parseInt(num, 10); + if (isNaN(req.params[name])) { + next(createError(400, "failed to parseInt " + num)); + } else { + next(); + } +}); + +app.param("user", function (req, res, next, id) { + if ((req.user = users[id])) { + next(); + } else { + next(createError(404, "failed to find user")); + } +}); + +app.get("/", function (req, res) { + res.send("Visit /user/0 or /users/0-2"); +}); + +app.get("/user/:user", function (req, res) { + res.send("user " + req.user.name); +}); + +app.get("/users/:from-:to", function (req, res) { + var from = req.params.from; + var to = req.params.to; + var names = users.map(function (user) { + return user.name; + }); + res.send("users " + names.slice(from, to + 1).join(", ")); +}); + +app.listen(3000); +console.log("Express started on port 3000"); +``` + +Using h3, we can do the same thing: + +```ts [app.ts] +/** + * h3 example app. + */ +import { + createApp, + createError, + createRouter, + defineEventHandler, + getRouterParam, + getValidatedRouterParams, +} from "h3"; +import { z } from "zod"; + +const users = [ + { name: "tj" }, + { name: "tobi" }, + { name: "loki" }, + { name: "jane" }, + { name: "bandit" }, +]; + +export const app = createApp(); +const router = createRouter(); + +router.get( + "/", + defineEventHandler(() => { + return "Visit /users/0 or /users/0/2"; + }), +); + +router.get( + "/user/:user", + defineEventHandler(async (event) => { + const { user } = await getValidatedRouterParams( + event, + z.object({ + user: z.number({ coerce: true }), + }).parse, + ); + + if (!users[user]) + throw createError({ + status: 404, + statusMessage: "User Not Found", + }); + + return `user ${user}`; + }), +); + +router.get( + "/users/:from/:to", + defineEventHandler(async (event) => { + const { from, to } = await getValidatedRouterParams( + event, + z.object({ + from: z.number({ coerce: true }), + to: z.number({ coerce: true }), + }).parse, + ); + + const names = users.map((user) => { + return user.name; + }); + + return `users ${names.slice(from, to).join(", ")}`; + }), +); + +app.use(router); +``` + +With h3, we do not have a `param` method. Instead, we use `getRouterParam` or `getValidatedRouterParams` to validate the params. It's more explicit and easier to use. In this example, we use `Zod` but you are free to use any other validation library. + +:read-more{to="/guides/valide-data"} + +## Cookies + +The fourth example is the [Cookies](https://github.com/expressjs/express/blob/master/examples/cookies/index.js). In this example, we use cookies. + +```js [index.js] +/** + * Express.js example app. + */ +var express = require("express"); +var app = express(); +var cookieParser = require("cookie-parser"); + +app.use(cookieParser("my secret here")); + +app.use(express.urlencoded({ extended: false })); + +app.get("/", function (req, res) { + if (req.cookies.remember) { + res.send('Remembered :). Click to forget!.'); + } else { + res.send( + '

Check to ' + + '.

', + ); + } +}); + +app.get("/forget", function (req, res) { + res.clearCookie("remember"); + res.redirect("back"); +}); + +app.post("/", function (req, res) { + var minute = 60000; + if (req.body.remember) res.cookie("remember", 1, { maxAge: minute }); + res.redirect("back"); +}); + +app.listen(3000); +console.log("Express started on port 3000"); +``` + +Using h3, we can do the same thing: + +```ts [app.ts] +import { + createApp, + createRouter, + defineEventHandler, + getCookie, + getHeader, + readBody, + sendRedirect, + setCookie, +} from "h3"; + +export const app = createApp(); +const router = createRouter(); + +router.get( + "/", + defineEventHandler((event) => { + const remember = getCookie(event, "remember"); + + console.log(remember); + + if (remember) { + return 'Remembered :). Click to forget!.'; + } else { + return `

Check to + .

`; + } + }), +); + +router.get( + "/forget", + defineEventHandler((event) => { + deleteCookie(event, "remember"); + + const back = getHeader(event, "referer") || "/"; + return sendRedirect(event, back); + }), +); + +router.post( + "/", + defineEventHandler(async (event) => { + const body = await readBody(event); + + if (body.remember) + setCookie(event, "remember", "1", { maxAge: 60 * 60 * 24 * 7 }); + + const back = getHeader(event, "referer") || "/"; + return sendRedirect(event, back); + }), +); + +app.use(router); +``` + +With h3, we do not have a `cookieParser` middleware. Instead, we use `getCookie` and `setCookie` to get and set cookies. It's more explicit and easier to use. + +:read-more{to="/guides/handle-cookies"} diff --git a/docs/4.guides/handle-cookie.md b/docs/4.guides/handle-cookie.md new file mode 100644 index 00000000..1882df51 --- /dev/null +++ b/docs/4.guides/handle-cookie.md @@ -0,0 +1,79 @@ +# Handle Cookie + +> Use cookies to store data on the client. + +Handling cookies with h3 is straightforward. There is three utilities to handle cookies: + +- [`setCookie`](/concepts/utilities) to attach a cookie to the response. +- [`getCookie`](/concepts/utilities) to get a cookie from the request. +- [`deleteCookie`](/concepts/utilities) to clear a cookie from the response. + +## Set a Cookie + +To set a cookie, you need to use [`setCookie`](/concepts/utilities) in an [event handler](/concepts/event-handler): + +```ts +import { defineEventHandler, setCookie } from "h3"; + +app.use( + defineEventHandler(async (event) => { + setCookie(event, "name", "value", { maxAge: 60 * 60 * 24 * 7 }); + + return; + }), +); +``` + +In the options, you can configure the [cookie flags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie): + +- `maxAge` to set the expiration date of the cookie in seconds. +- `expires` to set the expiration date of the cookie in a `Date` object. +- `path` to set the path of the cookie. +- `domain` to set the domain of the cookie. +- `secure` to set the `Secure` flag of the cookie. +- `httpOnly` to set the `HttpOnly` flag of the cookie. +- `sameSite` to set the `SameSite` flag of the cookie. + +:read-more{to="/concepts/utilities"} + +## Get a Cookie + +To get a cookie, you need to use [`getCookie`](/concepts/utilities) in an [event handler](/concepts/event-handler): + +```ts +import { defineEventHandler, getCookie } from "h3"; + +app.use( + defineEventHandler(async (event) => { + const name = getCookie(event, "name"); + + return; + }), +); +``` + +This will return the value of the cookie if it exists, or `undefined` otherwise. + +:read-more{to="/concepts/utilities"} + +## Delete a Cookie + +To delete a cookie, you need to use [`deleteCookie`](/concepts/utilities) in an [event handler](/concepts/event-handler): + +```ts +import { defineEventHandler, deleteCookie } from "h3"; + +app.use( + defineEventHandler(async (event) => { + deleteCookie(event, "name"); + + return; + }), +); +``` + +The utility `deleteCookie` is a wrapper around [`setCookie`](/concepts/utilities) with the value set to `""` and the `maxAge` set to `0`. + +This will erase the cookie from the client. + +:read-more{to="/concepts/utilities"} diff --git a/docs/4.guides/handle-session.md b/docs/4.guides/handle-session.md new file mode 100644 index 00000000..78bfac95 --- /dev/null +++ b/docs/4.guides/handle-session.md @@ -0,0 +1,146 @@ +# Handle Session + +> Remember your users using a session. + +A session is a way to remember users using cookies. It is a very common way to authenticate users or save data about them such as their language or their preferences on the web. + +h3 provide many [utilities](/concepts/utilities) to handle sessions: + +- [`useSession`](/concepts/utilities) to initializes a session and returns a wrapper to control it. +- [`getSession`](/concepts/utilities) to initializes or gets the current user session. +- [`updateSession`](/concepts/utilities) to updates data of the current session. +- [`clearSession`](/concepts/utilities) to clears the current session. + +Most of the time, you will use [`useSession`](/concepts/utilities) to manipulate the session. + +## Initialize a Session + +To initialize a session, you need to use [`useSession`](/concepts/utilities) in an [event handler](/concepts/event-handler): + +```js +import { defineEventHandler, useSession } from "h3"; + +app.use( + defineEventHandler(async (event) => { + const session = await useSession(event, { + password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", + }); + + return; + }), +); +``` + +> [!WARNING] +> You must provide a password to encrypt the session. + +This will initialize a session and return an header `Set-Cookie` with a cookie named `h3` and an encrypted content. + +If the request contains a cookie named `h3` or a header named `x-h3-session`, the session will be initialized with the content of the cookie or the header. + +> [!NOTE] +> The header take precedence over the cookie. + +## Get Data from a Session + +To get data from a session, we will still use [`useSession`](/concepts/utilities). Under the hood, it will use [`getSession`](/concepts/utilities) to get the session. + +```js +import { defineEventHandler, useSession } from "h3"; + +app.use( + defineEventHandler(async (event) => { + const session = await useSession(event, { + password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", + }); + + return session.data; + }), +); +``` + +Data are stored in the `data` property of the session. If there is no data, it will be an empty object. + +## Add Data to a Session + +To add data to a session, we will still use [`useSession`](/concepts/utilities). Under the hood, it will use [`updateSession`](/concepts/utilities) to update the session. + +```js +import { defineEventHandler, useSession } from "h3"; + +app.use( + defineEventHandler(async (event) => { + const session = await useSession(event, { + password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", + }); + + const count = (session.data.count || 0) + 1; + await session.update({ + count: count, + }); + + return count === 0 + ? "Hello world!" + : `Hello world! You have visited this page ${count} times.`; + }), +); +``` + +What is happening here? + +We try to get a session from the request. If there is no session, a new one will be created. Then, we increment the `count` property of the session and we update the session with the new value. Finally, we return a message with the number of times the user visited the page. + +Try to visit the page multiple times and you will see the number of times you visited the page. + +> [!NOTE] +> If you use a CLI tool like `curl` to test this example, you will not see the number of times you visited the page because the CLI tool does not save cookies. You must get the cookie from the response and send it back to the server. + +## Clear a Session + +To clear a session, we will still use [`useSession`](/concepts/utilities). Under the hood, it will use [`clearSession`](/concepts/utilities) to clear the session. + +```js +import { defineEventHandler, useSession } from "h3"; + +app.use( + "/clear", + defineEventHandler(async (event) => { + const session = await useSession(event, { + password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", + }); + + await session.clear(); + + return "Session cleared"; + }), +); +``` + +h3 will send a header `Set-Cookie` with an empty cookie named `h3` to clear the session. + +## Options + +When to use [`useSession`](/concepts/utilities), you can pass an object with options as the second argument to configure the session: + +```js +import { defineEventHandler, useSession } from "h3"; + +app.use( + defineEventHandler(async (event) => { + const session = await useSession(event, { + password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9", + cookie: { + name: "my-session", + httpOnly: true, + secure: true, + sameSite: "strict", + }, + maxAge: 60 * 60 * 24 * 7, // 7 days + }); + + return session.data; + }), +); +``` + +:read-more{to="/concepts/utilities"} diff --git a/docs/4.guides/serve-static-assets.md b/docs/4.guides/serve-static-assets.md new file mode 100644 index 00000000..a86478a7 --- /dev/null +++ b/docs/4.guides/serve-static-assets.md @@ -0,0 +1,115 @@ +# Serve Static Assets + +> Serve static assets such as HMTL, images, CSS, JavaScript, etc. + +h3 can serve static assets such as HTML, images, CSS, JavaScript, etc. + +> [!NOTE] +> If you use [`unjs/listhen`](https://listhen.unjs.io), you've just to create a `public` directory in your project root and put your static assets in it. They will be served automatically. + +## Usage + +To serve a static directory, you can use the [`serveStatic`](/concepts/utilities) utility. + +```ts +import { createApp, serveStatic } from "h3"; + +const app = createApp(); + +import { createApp, defineEventHandler, serveStatic } from "h3"; + +export const app = createApp(); + +app.use( + defineEventHandler((event) => { + return serveStatic(event, { + getContents: (id) => { + return undefined; + }, + getMeta: (id) => { + return undefined; + }, + }); + }), +); +``` + +This does not serve any files yet. You need to implement the `getContents` and `getMeta` methods. + +- `getContents` is used to read the contents of a file. It should return a `Promise` that resolves to the contents of the file or `undefined` if the file does not exist. +- `getMeta` is used to get the metadata of a file. It should return a `Promise` that resolves to the metadata of the file or `undefined` if the file does not exist. + +They are separated to allow h3 to respond to `HEAD` requests without reading the contents of the file and to use the `Last-Modified` header. + +:read-more{to="/concepts/utilities"} + +## Read files + +Now, create a `index.html` file in the `public` directory with a simple message and open your browser to http://localhost:3000. You should see the message. + +> [!NOTE] +> Usage of `public` is a convention but you can use any directory name you want. + +> [!NOTE] +> If you're are using [`unjs/listhen`](https://listhen.unjs.io) and want to try this example, create a directory with another name than `public` because it's the default directory used by `listhen`. + +Then, we can create the `getContents` and `getMeta` methods: + +```ts +import { createApp, defineEventHandler, serveStatic } from "h3"; +import { stat, readFile } from "node:fs/promises"; +import { join } from "pathe"; + +export const app = createApp(); + +const publicDir = "assets"; + +app.use( + defineEventHandler((event) => { + return serveStatic(event, { + getContents: (id) => readFile(join(publicDir, id)), + getMeta: async (id) => { + const stats = await stat(join(publicDir, id)).catch(() => {}); + + if (!stats || !stats.isFile()) { + return; + } + + return { + size: stats.size, + mtime: stats.mtimeMs, + }; + }, + }); + }), +); +``` + +The `getContents` read the file and returns its contents, pretty simple. The `getMeta` uses `fs.stat` to get the file metadata. If the file does not exist or is not a file, it returns `undefined`. Otherwise, it returns the file size and the last modification time. + +The file size and last modification time are used to create an etag to send a `304 Not Modified` response if the file has not been modified since the last request. This is useful to avoid sending the same file multiple times if it has not changed. + +:read-more{to="/concepts/utilities"} + +## Resolving Assets + +If the path does not match a file, h3 will try to add `index.html` to the path and try again. If it still does not match, it will return a 404 error. + +You can change this behavior by passing a `indexNames` option to `serveStatic`: + +```ts +import { createApp, serveStatic } from "h3"; + +const app = createApp(); + +app.use( + serveStatic({ + indexNames: ["/app.html", "/index.html"], + }), +); +``` + +With this option, h3 will try to match `/app.html` first, then `/index.html` and finally return a 404 error. + +> [!IMPORTANT] +> Do not forget `/` at the beginning of the h3 concatenates the path with the index name. For example, `/index.html` will be concatenated with `/hello` to form `hello/index.html`. diff --git a/docs/4.guides/stream-response.md b/docs/4.guides/stream-response.md new file mode 100644 index 00000000..3b717a6b --- /dev/null +++ b/docs/4.guides/stream-response.md @@ -0,0 +1,103 @@ +# Stream Response + +> Stream response to the client. + +Streaming is a powerful feature of h3. It allows you to send data to the client as soon as you have it. This is useful for large files or long running tasks. + +> [!WARNING] +> Steaming is complicated and can become an overhead if you don't need it. + +## Create a Stream + +To stream a response, you first need to create a stream using the [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) API: + +```ts +const steam = new ReadableStream(); +``` + +For the example, we will create a start function that will send a random number every 100 milliseconds. After 1000 milliseconds, it will close the stream: + +```ts +let interval: NodeJS.Timeout; +const stream = new ReadableStream({ + start(controller) { + controller.enqueue("