diff --git a/.prettierignore b/.prettierignore index 7aa07b00..85d11979 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ lib .nuxt .output docs/**/*.md +src/types/_headers.ts diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 00000000..91327faa --- /dev/null +++ b/build.config.ts @@ -0,0 +1,19 @@ +import { defineBuildConfig } from "unbuild"; + +export default defineBuildConfig({ + declaration: true, + rollup: { + emitCJS: false, + output: { + chunkFileNames: "_shared.js", + }, + esbuild: { + target: "ES2020", + tsconfigRaw: { + compilerOptions: { + useDefineForClassFields: false, + }, + }, + }, + }, +}); diff --git a/docs/2.utils/1.request.md b/docs/2.utils/1.request.md index 6dca6488..88b744e6 100644 --- a/docs/2.utils/1.request.md +++ b/docs/2.utils/1.request.md @@ -25,32 +25,6 @@ export default defineEventHandler((event) => { }); ``` -### `getHeader(event, name)` - -Get a request header by name. - -**Example:** - -```ts -export default defineEventHandler((event) => { - const contentType = getRequestHeader(event, "content-type"); // "application/json" -}); -``` - -### `getHeaders(event)` - -Get the request headers object. - -Array headers are joined with a comma. - -**Example:** - -```ts -export default defineEventHandler((event) => { - const headers = getRequestHeaders(event); // { "content-type": "application/json", "x-custom-header": "value" } -}); -``` - ### `getQuery(event)` Get query the params object from the request URL parsed with [unjs/ufo](https://ufo.unjs.io). @@ -261,12 +235,6 @@ export default defineEventHandler((event) => { }); ``` -### `toWebRequest(event)` - -Convert the H3Event to a WebRequest object. - -**NOTE:** This function is not stable and might have edge cases that are not handled properly. - @@ -281,74 +249,63 @@ Get a unique fingerprint for the incoming request. -### `getRequestWebStream(event)` +### `getBodyStream(event)` Captures a stream from a request. -### `readBody(event, options: { strict? })` +### `readFormDataBody(event)` -Reads request body and tries to safely parse using [destr](https://github.com/unjs/destr). +Constructs a FormData object from an event, after converting it to a a web request. **Example:** ```ts export default defineEventHandler(async (event) => { - const body = await readBody(event); + const formData = await readFormDataBody(event); + const email = formData.get("email"); + const password = formData.get("password"); }); ``` -### `readFormData(event)` +### `readJSONBody(event)` -Constructs a FormData object from an event, after converting it to a a web request. +Reads request body and tries to parse using JSON.parse or URLSearchParams. **Example:** ```ts export default defineEventHandler(async (event) => { - const formData = await readFormData(event); - const email = formData.get("email"); - const password = formData.get("password"); + const body = await readAndParseBody(event); }); ``` -### `readMultipartFormData(event)` +### `readRawBody(event)` -Tries to read and parse the body of a an H3Event as multipart form. +Reads body of the request and returns an Uint8Array of the raw body. **Example:** ```ts export default defineEventHandler(async (event) => { - const formData = await readMultipartFormData(event); - // The result could look like: - // [ - // { - // "data": "other", - // "name": "baz", - // }, - // { - // "data": "something", - // "name": "some-other-data", - // }, - // ]; + const body = await readRawBody(event); }); ``` -### `readRawBody(event, encoding)` +### `readTextBody(event)` -Reads body of the request and returns encoded raw string (default), or `Buffer` if encoding is falsy. +Reads body of the request and returns an string (utf-8) of the raw body. **Example:** ```ts export default defineEventHandler(async (event) => { - const body = await readRawBody(event, "utf-8"); + const body = await readTextBody(event); }); ``` -### `readValidatedBody(event, validate)` +### `readValidatedJSONBody(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. +Tries to read the request body via `readJSONBody`, then uses the provided validation function and either throws a validation error or returns the result. You can use a simple function to validate the body or use a library like `zod` to define a schema. @@ -356,7 +313,7 @@ You can use a simple function to validate the body or use a library like `zod` t ```ts export default defineEventHandler(async (event) => { - const body = await readValidatedBody(event, (body) => { + const body = await readValidatedJSONBody(event, (body) => { return typeof body === "object" && body !== null; }); }); @@ -368,7 +325,7 @@ export default defineEventHandler(async (event) => { import { z } from "zod"; export default defineEventHandler(async (event) => { const objectSchema = z.object(); - const body = await readValidatedBody(event, objectSchema.safeParse); + const body = await readValidatedJSONBody(event, objectSchema.safeParse); }); ``` diff --git a/docs/2.utils/2.response.md b/docs/2.utils/2.response.md index 6f971a26..d2f187f5 100644 --- a/docs/2.utils/2.response.md +++ b/docs/2.utils/2.response.md @@ -8,33 +8,6 @@ icon: material-symbols-light:output -### `appendHeader(event, name, value)` - -Append a response header by name. - -**Example:** - -```ts -export default defineEventHandler((event) => { - appendResponseHeader(event, "content-type", "text/html"); -}); -``` - -### `appendHeaders(event, headers)` - -Append the response headers. - -**Example:** - -```ts -export default defineEventHandler((event) => { - appendResponseHeaders(event, { - "content-type": "text/html", - "cache-control": "no-cache", - }); -}); -``` - ### `appendResponseHeader(event, name, value)` Append a response header by name. @@ -80,16 +53,6 @@ Set the response status code and message. ### `getResponseHeader(event, name)` -Alias for `getResponseHeaders`. - -**Example:** - -```ts -export default defineEventHandler((event) => { - const contentType = getResponseHeader(event, "content-type"); // Get the response content-type header -}); -``` - ### `getResponseHeaders(event)` Get the response headers object. @@ -128,14 +91,6 @@ export default defineEventHandler((event) => { }); ``` -### `isStream(data)` - -Checks if the data is a stream. (Node.js Readable Stream, React Pipeable Stream, or Web Stream) - -### `isWebResponse(data)` - -Checks if the data is a Response object. - ### `removeResponseHeader(event, name)` Remove a response header by name. @@ -148,12 +103,6 @@ export default defineEventHandler((event) => { }); ``` -### `send(event, data?, type?)` - -Directly send a response to the client. - -**Note:** This function should be used only when you want to send a response directly without using the `h3` event. Normally you can directly `return` a value inside event handlers. - ### `sendIterable(event, iterable)` Iterate a source of chunks and send back each chunk in order. Supports mixing async work together with emitting chunks. @@ -232,42 +181,9 @@ export default defineEventHandler((event) => { }); ``` -### `sendStream(event, stream)` - -Send a stream response to the client. - -Note: You can directly `return` a stream value inside event handlers alternatively which is recommended. - ### `sendWebResponse(event, response)` -Send a Response object to the client. - -### `setHeader(event, name, value)` - -Set a response header by name. - -**Example:** - -```ts -export default defineEventHandler((event) => { - setResponseHeader(event, "content-type", "text/html"); -}); -``` - -### `setHeaders(event, headers)` - -Set the response headers. - -**Example:** - -```ts -export default defineEventHandler((event) => { - setResponseHeaders(event, { - "content-type": "text/html", - "cache-control": "no-cache", - }); -}); -``` +Send a Web besponse object to the client. ### `setResponseHeader(event, name, value)` @@ -309,7 +225,7 @@ export default defineEventHandler((event) => { }); ``` -### `writeEarlyHints(event, hints, cb)` +### `writeEarlyHints(event, hints)` Write `HTTP/1.1 103 Early Hints` to the client. diff --git a/docs/2.utils/98.advanced.md b/docs/2.utils/98.advanced.md index 7db3d840..7d42b051 100644 --- a/docs/2.utils/98.advanced.md +++ b/docs/2.utils/98.advanced.md @@ -52,12 +52,36 @@ Get a cookie value by name. Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs. -### `setCookie(event, name, value, serializeOptions?)` +### `setCookie(event, name, value, options?)` Set a cookie value by name. +## Fingerprint utils + + + +### `getRequestFingerprint(event, opts)` + +Get a unique fingerprint for the incoming request. + + + +## WebSocket utils + + + +### `defineWebSocket(hooks)` + +Define WebSocket hooks. + +### `defineWebSocketHandler(hooks)` + +Define WebSocket event handler. + + + ## Sanitize @@ -74,9 +98,9 @@ Allowed characters: horizontal tabs, spaces or visible ascii characters: https:/ -## Route +## Base - + ### `useBase(base, handler)` @@ -133,7 +157,7 @@ Make a proxy request to a target URL and send the response back to the client. ## CORS - + ### `appendCorsHeaders(event, options)` @@ -185,7 +209,7 @@ Check if the incoming request is a CORS preflight request. ## Server Sent Events (SSE) - + ### `createEventStream(event, opts?)` @@ -196,7 +220,7 @@ Initialize an EventStream instance for creating [server sent events](https://dev ```ts import { createEventStream, sendEventStream } from "h3"; -eventHandler((event) => { +defineEventHandler((event) => { const eventStream = createEventStream(event); // Send a message every second diff --git a/eslint.config.mjs b/eslint.config.mjs index e8bd76c4..be09ce0b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,9 @@ export default unjs( "unicorn/no-null": "off", "unicorn/number-literal-case": "off", "@typescript-eslint/no-non-null-assertion": "off", - "unicorn/expiring-todo-comments": "off" + "unicorn/expiring-todo-comments": "off", + "@typescript-eslint/ban-types": "off", + "unicorn/prefer-export-from": "off", } } ); diff --git a/package.json b/package.json index 256cd4dc..7e5d0017 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,9 @@ "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" + "import": "./dist/index.mjs" } }, - "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "files": [ @@ -27,34 +25,44 @@ "play": "listhen -w ./playground/app.ts", "profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs", "release": "pnpm test && pnpm build && changelogen --publish --publishTag 2x --prerelease && git push --follow-tags", - "test": "pnpm lint && vitest --run --coverage" + "test:types": "tsc --noEmit --skipLibCheck", + "test": "pnpm lint && pnpm test:types && vitest --run --coverage" }, "dependencies": { "cookie-es": "^1.1.0", - "crossws": "^0.2.4", - "defu": "^6.1.4", - "destr": "^2.0.3", "iron-webcrypto": "^1.1.1", "ohash": "^1.1.3", "radix3": "^1.1.2", "ufo": "^1.5.3", - "uncrypto": "^0.1.3", - "unenv": "^1.9.0" + "uncrypto": "^0.1.3" + }, + "peerDependencies": { + "crossws": "^0.2.4" + }, + "peerDependenciesMeta": { + "crossws": { + "optional": true + } }, "devDependencies": { "0x": "^5.7.0", + "@types/connect": "^3.4.38", "@types/express": "^4.17.21", - "@types/node": "^20.12.7", + "@types/node": "^20.14.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@types/supertest": "^6.0.2", "@vitest/coverage-v8": "^1.5.2", "autocannon": "^7.15.0", "automd": "^0.3.7", "changelogen": "^0.5.5", "connect": "^3.7.0", + "esbuild": "^0.21.5", "eslint": "^9.1.1", "eslint-config-unjs": "^0.3.0-rc.6", "express": "^4.19.2", "get-port": "^7.1.0", + "get-port-please": "^3.1.2", "jiti": "^1.21.0", "listhen": "^1.7.2", "node-fetch-native": "^1.6.4", @@ -62,8 +70,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "supertest": "^7.0.0", - "typescript": "^5.4.5", - "unbuild": "^2.0.0", + "typescript": "^5.5.2", + "unbuild": "^3.0.0-rc.2", "undici": "^6.19.2", "vitest": "^1.5.2", "zod": "^3.23.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d55a34f..8c250e16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,6 @@ importers: crossws: specifier: ^0.2.4 version: 0.2.4 - defu: - specifier: ^6.1.4 - version: 6.1.4 - destr: - specifier: ^2.0.3 - version: 2.0.3 iron-webcrypto: specifier: ^1.1.1 version: 1.2.1 @@ -35,25 +29,31 @@ importers: uncrypto: specifier: ^0.1.3 version: 0.1.3 - unenv: - specifier: ^1.9.0 - version: 1.9.0 devDependencies: 0x: specifier: ^5.7.0 version: 5.7.0 + '@types/connect': + specifier: ^3.4.38 + version: 3.4.38 '@types/express': specifier: ^4.17.21 version: 4.17.21 '@types/node': - specifier: ^20.12.7 - version: 20.14.2 + specifier: ^20.14.7 + version: 20.14.7 + '@types/react': + specifier: ^18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 '@types/supertest': specifier: ^6.0.2 version: 6.0.2 '@vitest/coverage-v8': specifier: ^1.5.2 - version: 1.6.0(vitest@1.6.0(@types/node@20.14.2)) + version: 1.6.0(vitest@1.6.0(@types/node@20.14.7)) autocannon: specifier: ^7.15.0 version: 7.15.0 @@ -66,18 +66,24 @@ importers: connect: specifier: ^3.7.0 version: 3.7.0 + esbuild: + specifier: ^0.21.5 + version: 0.21.5 eslint: specifier: ^9.1.1 version: 9.5.0 eslint-config-unjs: specifier: ^0.3.0-rc.6 - version: 0.3.2(eslint@9.5.0)(typescript@5.4.5) + version: 0.3.2(eslint@9.5.0)(typescript@5.5.2) express: specifier: ^4.19.2 version: 4.19.2 get-port: specifier: ^7.1.0 version: 7.1.0 + get-port-please: + specifier: ^3.1.2 + version: 3.1.2 jiti: specifier: ^1.21.0 version: 1.21.6 @@ -100,17 +106,17 @@ importers: specifier: ^7.0.0 version: 7.0.0 typescript: - specifier: ^5.4.5 - version: 5.4.5 + specifier: ^5.5.2 + version: 5.5.2 unbuild: - specifier: ^2.0.0 - version: 2.0.0(typescript@5.4.5) + specifier: ^3.0.0-rc.2 + version: 3.0.0-rc.2(typescript@5.5.2) undici: specifier: ^6.19.2 version: 6.19.2 vitest: specifier: ^1.5.2 - version: 1.6.0(@types/node@20.14.2) + version: 1.6.0(@types/node@20.14.7) zod: specifier: ^3.23.4 version: 3.23.8 @@ -122,7 +128,7 @@ importers: version: 3.2.3 h3: specifier: latest - version: 1.11.1 + version: 1.12.0 listhen: specifier: latest version: 1.7.2 @@ -131,7 +137,7 @@ importers: dependencies: h3: specifier: latest - version: 1.11.1 + version: 1.12.0 listhen: specifier: latest version: 1.7.2 @@ -252,414 +258,138 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -977,8 +707,8 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/express-serve-static-core@4.19.3': - resolution: {integrity: sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==} + '@types/express-serve-static-core@4.19.5': + resolution: {integrity: sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==} '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} @@ -995,18 +725,27 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node@20.14.2': - resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} + '@types/node@20.14.7': + resolution: {integrity: sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/prop-types@15.7.12': + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/qs@6.9.15': resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@18.3.0': + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + + '@types/react@18.3.3': + resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -1025,8 +764,8 @@ packages: '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} - '@typescript-eslint/eslint-plugin@7.13.0': - resolution: {integrity: sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==} + '@typescript-eslint/eslint-plugin@7.13.1': + resolution: {integrity: sha512-kZqi+WZQaZfPKnsflLJQCz6Ze9FFSMfXrrIOcyargekQxG37ES7DJNpJUE9Q/X5n3yTIP/WPutVNzgknQ7biLg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -1036,8 +775,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@7.13.0': - resolution: {integrity: sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==} + '@typescript-eslint/parser@7.13.1': + resolution: {integrity: sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -1046,12 +785,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@7.13.0': - resolution: {integrity: sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==} + '@typescript-eslint/scope-manager@7.13.1': + resolution: {integrity: sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/type-utils@7.13.0': - resolution: {integrity: sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==} + '@typescript-eslint/type-utils@7.13.1': + resolution: {integrity: sha512-aWDbLu1s9bmgPGXSzNCxELu+0+HQOapV/y+60gPXafR8e2g1Bifxzevaa+4L2ytCWm+CHqpELq4CSoN9ELiwCg==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -1060,12 +799,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@7.13.0': - resolution: {integrity: sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==} + '@typescript-eslint/types@7.13.1': + resolution: {integrity: sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/typescript-estree@7.13.0': - resolution: {integrity: sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==} + '@typescript-eslint/typescript-estree@7.13.1': + resolution: {integrity: sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' @@ -1073,14 +812,14 @@ packages: typescript: optional: true - '@typescript-eslint/utils@7.13.0': - resolution: {integrity: sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==} + '@typescript-eslint/utils@7.13.1': + resolution: {integrity: sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/visitor-keys@7.13.0': - resolution: {integrity: sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==} + '@typescript-eslint/visitor-keys@7.13.1': + resolution: {integrity: sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==} engines: {node: ^18.18.0 || >=20.0.0} '@vitest/coverage-v8@1.6.0': @@ -1590,8 +1329,8 @@ packages: engines: {node: '>=4'} hasBin: true - cssnano-preset-default@7.0.2: - resolution: {integrity: sha512-z95kGKZx8VWHfERj7LFzuiTxylbvEp07ZEYaFu+t6bFyNOXLd/+3oPyNaY7ISwcrfHFCkt8OfRo4IZxVRJZ7dg==} + cssnano-preset-default@7.0.3: + resolution: {integrity: sha512-dQ3Ba1p/oewICp/szF1XjFFgql8OlOBrI2YNBUUwhHQnJNoMOcQTa+Bi7jSJN8r/eM1egW0Ud1se/S7qlduWKA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -1602,8 +1341,8 @@ packages: peerDependencies: postcss: ^8.4.31 - cssnano@7.0.2: - resolution: {integrity: sha512-LXm/Xx6TNLzfHM2lBaIQHfvtdW5QfdbyLzfJAWZrclCAb47yVa0/yJG69+amcw3Lq0YZ+kyU40rbsMPLcMt9aw==} + cssnano@7.0.3: + resolution: {integrity: sha512-lsekJctOTqdCn4cNrtrSwsuMR/fHC+oiVMHkp/OugBWtwjH8XJag1/OtGaYJGtz0un1fQcRy4ryfYTQsfh+KSQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -1612,6 +1351,9 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d3-array@2.12.1: resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} @@ -1806,8 +1548,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.4.803: - resolution: {integrity: sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==} + electron-to-chromium@1.4.808: + resolution: {integrity: sha512-0ItWyhPYnww2VOuCGF4s1LTfbrdAV2ajy/TN+ZTuhR23AHI6rWHCrBXJ/uxoXOvRRqw8qjYVrG81HFI7x/2wdQ==} elliptic@6.5.5: resolution: {integrity: sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==} @@ -1840,16 +1582,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1905,8 +1637,8 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true - espree@10.0.1: - resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} + espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.5.0: @@ -2128,10 +1860,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - globby@14.0.1: resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==} engines: {node: '>=18'} @@ -2145,8 +1873,8 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - h3@1.11.1: - resolution: {integrity: sha512-AbaH6IDnZN6nmbnJOH72y3c5Wwh9P97soSVdGSBbcDACRdkC0FEWf25pzx4f/NuOCK6quHmW18yF2Wx+G4Zi1A==} + h3@1.12.0: + resolution: {integrity: sha512-Zi/CcNeWBXDrFNlV0hUBJQR9F7a96RjMeAZweW/ZWkR9fuXrMcvKnSA63f/zZ9l0GgQOZDVHGvXivNN9PWOwhA==} has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} @@ -2350,8 +2078,9 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} is-decimal@1.0.4: resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} @@ -2736,13 +2465,13 @@ packages: engines: {node: '>=10'} hasBin: true - mkdist@1.5.1: - resolution: {integrity: sha512-lCu1spNiA52o7IaKgZnOjg28nNHwYqUDjBfXePXyUtzD7Xhe6rRTkGTalQ/ALfrZC/SrPw2+A/0qkeJ+fPDZtQ==} + mkdist@1.5.2: + resolution: {integrity: sha512-Xa6+CSzw6N338+vfWZcM5B5GEkZRmtWd2zFdoegNGnoF6p5o0je5lBfCKKCIo8jSQ9yG3hVbFOoz3G0ZmLfAjg==} hasBin: true peerDependencies: sass: ^1.75.0 typescript: '>=5.4.5' - vue-tsc: ^1.8.27 || ^2.0.14 + vue-tsc: ^1.8.27 || ^2.0.21 peerDependenciesMeta: sass: optional: true @@ -2759,8 +2488,8 @@ packages: engines: {node: '>= 0.8.0'} hasBin: true - morphdom@2.7.2: - resolution: {integrity: sha512-Dqb/lHFyTi7SZpY0a5R4I/0Edo+iPMbaUexsHHsLAByyixCDiLHPHyVoKVmrpL0THcT7V9Cgev9y21TQYq6wQg==} + morphdom@2.7.3: + resolution: {integrity: sha512-rvGK92GxSuPEZLY8D/JH07cG3BxyA+/F0Bxg32OoGAEFFhGWA3OqVpqPZlOgZTCR52clXrmz+z2pYSJ6gOig1w==} mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} @@ -3030,20 +2759,20 @@ packages: peerDependencies: postcss: ^8.4.38 - postcss-colormin@7.0.0: - resolution: {integrity: sha512-5CN6fqtsEtEtwf3mFV3B4UaZnlYljPpzmGeDB4yCK067PnAtfLe9uX2aFZaEwxHE7HopG5rUkW8gyHrNAesHEg==} + postcss-colormin@7.0.1: + resolution: {integrity: sha512-uszdT0dULt3FQs47G5UHCduYK+FnkLYlpu1HpWu061eGsKZ7setoG7kA+WC9NQLsOJf69D5TxGHgnAdRgylnFQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 - postcss-convert-values@7.0.0: - resolution: {integrity: sha512-bMuzDgXBbFbByPgj+/r6va8zNuIDUaIIbvAFgdO1t3zdgJZ77BZvu6dfWyd6gHEJnYzmeVr9ayUsAQL3/qLJ0w==} + postcss-convert-values@7.0.1: + resolution: {integrity: sha512-9x2ofb+hYPwHWMlWAzyWys2yMDZYGfkX9LodbaVTmLdlupmtH2AGvj8Up95wzzNPRDEzPIxQIkUaPJew3bT6xA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 - postcss-discard-comments@7.0.0: - resolution: {integrity: sha512-xpSdzRqYmy4YIVmjfGyYXKaI1SRnK6CTr+4Zmvyof8ANwvgfZgGdVtmgAvzh59gJm808mJCWQC9tFN0KF5dEXA==} + postcss-discard-comments@7.0.1: + resolution: {integrity: sha512-GVrQxUOhmle1W6jX2SvNLt4kmN+JYhV7mzI6BMnkAWR9DtVvg8e67rrV0NfdWhn7x1zxvzdWkMBPdBDCls+uwQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -3066,14 +2795,14 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-merge-longhand@7.0.1: - resolution: {integrity: sha512-qZlD26hnqSTMxSSOMS8+QCeRWtqOdMKeQHvHcBhjL3mJxKUs47cvO1Y1x3iTdYIk3ioMcRHTiy229TT0mEMH/A==} + postcss-merge-longhand@7.0.2: + resolution: {integrity: sha512-06vrW6ZWi9qeP7KMS9fsa9QW56+tIMW55KYqF7X3Ccn+NI2pIgPV6gFfvXTMQ05H90Y5DvnCDPZ2IuHa30PMUg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 - postcss-merge-rules@7.0.1: - resolution: {integrity: sha512-bb8McYQbo2etgs0uVt6AfngajACK3FHSVP3sGLhprrjbtHJWgG03JZ4KKBlJ8/5Fb8/Rr+mMKaybMYeoYrAg0A==} + postcss-merge-rules@7.0.2: + resolution: {integrity: sha512-VAR47UNvRsdrTHLe7TV1CeEtF9SJYR5ukIB9U4GZyZOptgtsS20xSxy+k5wMrI3udST6O1XuIn7cjQkg7sDAAw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -3090,14 +2819,14 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-minify-params@7.0.0: - resolution: {integrity: sha512-XOJAuX8Q/9GT1sGxlUvaFEe2H9n50bniLZblXXsAT/BwSfFYvzSZeFG7uupwc0KbKpTnflnQ7aMwGzX6JUWliQ==} + postcss-minify-params@7.0.1: + resolution: {integrity: sha512-e+Xt8xErSRPgSRFxHeBCSxMiO8B8xng7lh8E0A5ep1VfwYhY8FXhu4Q3APMjgx9YDDbSp53IBGENrzygbUvgUQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 - postcss-minify-selectors@7.0.1: - resolution: {integrity: sha512-YfIbGtcgMFquPxV2L/ASs36ZS4DsgfcDX9tQ8cTEIvBTv+0GXFKtcvvpi9tCKto/+DWGWYKMCESFG3Pnan0Feg==} + postcss-minify-selectors@7.0.2: + resolution: {integrity: sha512-dCzm04wqW1uqLmDZ41XYNBJfjgps3ZugDpogAmJXoCb5oCiTzIX4oPXXKxDpTvWOnKxQKR4EbV4ZawJBLcdXXA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -3144,8 +2873,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-normalize-unicode@7.0.0: - resolution: {integrity: sha512-OnKV52/VFFDAim4n0pdI+JAhsolLBdnCKxE6VV5lW5Q/JeVGFN8UM8ur6/A3EAMLsT1ZRm3fDHh/rBoBQpqi2w==} + postcss-normalize-unicode@7.0.1: + resolution: {integrity: sha512-PTPGdY9xAkTw+8ZZ71DUePb7M/Vtgkbbq+EoI33EuyQEzbKemEQMhe5QSr0VP5UfZlreANDPxSfcdSprENcbsg==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -3162,14 +2891,14 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-ordered-values@7.0.0: - resolution: {integrity: sha512-KROvC63A8UQW1eYDljQe1dtwc1E/M+mMwDT6z7khV/weHYLWTghaLRLunU7x1xw85lWFwVZOAGakxekYvKV+0w==} + postcss-ordered-values@7.0.1: + resolution: {integrity: sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 - postcss-reduce-initial@7.0.0: - resolution: {integrity: sha512-iqGgmBxY9LrblZ0BKLjmrA1mC/cf9A/wYCCqSmD6tMi+xAyVl0+DfixZIHSVDMbCPRPjNmVF0DFGth/IDGelFQ==} + postcss-reduce-initial@7.0.1: + resolution: {integrity: sha512-0JDUSV4bGB5FGM5g8MkS+rvqKukJZ7OTHw/lcKn7xPNqeaqJyQbUO8/dJpvyTpaVwPsd3Uc33+CfNzdVowp2WA==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -3374,11 +3103,6 @@ packages: rollup: ^3.29.4 || ^4 typescript: ^4.5 || ^5.0 - rollup@3.29.4: - resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.18.0: resolution: {integrity: sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3582,8 +3306,8 @@ packages: strip-literal@2.1.0: resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} - stylehacks@7.0.1: - resolution: {integrity: sha512-PnrT4HzajnxbjfChpeBKLSpSykilnGBlD+pIffCoT5KbLur9fcL8uKRQJJap85byR2wCYZl/4Otk5eq76qeZxQ==} + stylehacks@7.0.2: + resolution: {integrity: sha512-HdkWZS9b4gbgYTdMg4gJLmm7biAUug1qTqXjS+u8X+/pUd+9Px1E+520GnOW3rST9MNsVOVpsJG+mPHNosxjOQ==} engines: {node: ^18.12.0 || ^20.9.0 || >=22.0} peerDependencies: postcss: ^8.4.31 @@ -3726,8 +3450,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@7.13.0: - resolution: {integrity: sha512-upO0AXxyBwJ4BbiC6CRgAJKtGYha2zw4m1g7TIVPSonwYEuf7vCicw3syjS1OxdDMTz96sZIXl3Jx3vWJLLKFw==} + typescript-eslint@7.13.1: + resolution: {integrity: sha512-pvLEuRs8iS9s3Cnp/Wt//hpK8nKc8hVa3cLljHqzaJJQYP8oys8GUyIFqtlev+2lT/fqMPcyQko+HJ6iYK3nFA==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -3736,8 +3460,8 @@ packages: typescript: optional: true - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + typescript@5.5.2: + resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==} engines: {node: '>=14.17'} hasBin: true @@ -3748,11 +3472,11 @@ packages: resolution: {integrity: sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==} hasBin: true - unbuild@2.0.0: - resolution: {integrity: sha512-JWCUYx3Oxdzvw2J9kTAp+DKE8df/BnH/JTSj6JyA4SH40ECdFu7FoJJcrm8G92B7TjofQ6GZGjJs50TRxoH6Wg==} + unbuild@3.0.0-rc.2: + resolution: {integrity: sha512-XyGXoxbqQhzVrjizSrj8rXibpi1SeK+gL41amd29qNU3SSxAtuB1hKtFZTgNbkX80tKdEE9lFeB5jDTUkpSl+Q==} hasBin: true peerDependencies: - typescript: ^5.1.6 + typescript: ^5.4.5 peerDependenciesMeta: typescript: optional: true @@ -3977,7 +3701,7 @@ snapshots: jsonstream2: 3.0.0 make-dir: 3.1.0 minimist: 1.2.8 - morphdom: 2.7.2 + morphdom: 2.7.3 nanohtml: 1.10.0 on-net-listen: 1.1.2 opn: 5.5.0 @@ -4143,210 +3867,72 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@esbuild/aix-ppc64@0.19.12': - optional: true - - '@esbuild/aix-ppc64@0.20.2': - optional: true - '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/android-arm64@0.19.12': - optional: true - - '@esbuild/android-arm64@0.20.2': - optional: true - '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm@0.19.12': - optional: true - - '@esbuild/android-arm@0.20.2': - optional: true - '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-x64@0.19.12': - optional: true - - '@esbuild/android-x64@0.20.2': - optional: true - '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.19.12': - optional: true - - '@esbuild/darwin-arm64@0.20.2': - optional: true - '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-x64@0.19.12': - optional: true - - '@esbuild/darwin-x64@0.20.2': - optional: true - '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.19.12': - optional: true - - '@esbuild/freebsd-arm64@0.20.2': - optional: true - '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.19.12': - optional: true - - '@esbuild/freebsd-x64@0.20.2': - optional: true - '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/linux-arm64@0.19.12': - optional: true - - '@esbuild/linux-arm64@0.20.2': - optional: true - '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm@0.19.12': - optional: true - - '@esbuild/linux-arm@0.20.2': - optional: true - '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-ia32@0.19.12': - optional: true - - '@esbuild/linux-ia32@0.20.2': - optional: true - '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-loong64@0.19.12': - optional: true - - '@esbuild/linux-loong64@0.20.2': - optional: true - '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-mips64el@0.19.12': - optional: true - - '@esbuild/linux-mips64el@0.20.2': - optional: true - '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-ppc64@0.19.12': - optional: true - - '@esbuild/linux-ppc64@0.20.2': - optional: true - '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.19.12': - optional: true - - '@esbuild/linux-riscv64@0.20.2': - optional: true - '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-s390x@0.19.12': - optional: true - - '@esbuild/linux-s390x@0.20.2': - optional: true - '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-x64@0.19.12': - optional: true - - '@esbuild/linux-x64@0.20.2': - optional: true - '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.19.12': - optional: true - - '@esbuild/netbsd-x64@0.20.2': - optional: true - '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.19.12': - optional: true - - '@esbuild/openbsd-x64@0.20.2': - optional: true - '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.19.12': - optional: true - - '@esbuild/sunos-x64@0.20.2': - optional: true - '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/win32-arm64@0.19.12': - optional: true - - '@esbuild/win32-arm64@0.20.2': - optional: true - '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-ia32@0.19.12': - optional: true - - '@esbuild/win32-ia32@0.20.2': - optional: true - '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-x64@0.19.12': - optional: true - - '@esbuild/win32-x64@0.20.2': - optional: true - '@esbuild/win32-x64@0.21.5': optional: true @@ -4369,7 +3955,7 @@ snapshots: dependencies: ajv: 6.12.6 debug: 4.3.5 - espree: 10.0.1 + espree: 10.1.0 globals: 14.0.0 ignore: 5.3.1 import-fresh: 3.3.0 @@ -4483,54 +4069,54 @@ snapshots: '@parcel/watcher-win32-ia32': 2.4.1 '@parcel/watcher-win32-x64': 2.4.1 - '@rollup/plugin-alias@5.1.0(rollup@3.29.4)': + '@rollup/plugin-alias@5.1.0(rollup@4.18.0)': dependencies: slash: 4.0.0 optionalDependencies: - rollup: 3.29.4 + rollup: 4.18.0 - '@rollup/plugin-commonjs@25.0.8(rollup@3.29.4)': + '@rollup/plugin-commonjs@25.0.8(rollup@4.18.0)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) commondir: 1.0.1 estree-walker: 2.0.2 glob: 8.1.0 is-reference: 1.2.1 magic-string: 0.30.10 optionalDependencies: - rollup: 3.29.4 + rollup: 4.18.0 - '@rollup/plugin-json@6.1.0(rollup@3.29.4)': + '@rollup/plugin-json@6.1.0(rollup@4.18.0)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) optionalDependencies: - rollup: 3.29.4 + rollup: 4.18.0 - '@rollup/plugin-node-resolve@15.2.3(rollup@3.29.4)': + '@rollup/plugin-node-resolve@15.2.3(rollup@4.18.0)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 optionalDependencies: - rollup: 3.29.4 + rollup: 4.18.0 - '@rollup/plugin-replace@5.0.7(rollup@3.29.4)': + '@rollup/plugin-replace@5.0.7(rollup@4.18.0)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) magic-string: 0.30.10 optionalDependencies: - rollup: 3.29.4 + rollup: 4.18.0 - '@rollup/pluginutils@5.1.0(rollup@3.29.4)': + '@rollup/pluginutils@5.1.0(rollup@4.18.0)': dependencies: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 3.29.4 + rollup: 4.18.0 '@rollup/rollup-android-arm-eabi@4.18.0': optional: true @@ -4589,19 +4175,19 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.14.2 + '@types/node': 20.14.7 '@types/connect@3.4.38': dependencies: - '@types/node': 20.14.2 + '@types/node': 20.14.7 '@types/cookiejar@2.1.5': {} '@types/estree@1.0.5': {} - '@types/express-serve-static-core@4.19.3': + '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 20.14.2 + '@types/node': 20.14.7 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -4609,7 +4195,7 @@ snapshots: '@types/express@4.17.21': dependencies: '@types/body-parser': 1.19.5 - '@types/express-serve-static-core': 4.19.3 + '@types/express-serve-static-core': 4.19.5 '@types/qs': 6.9.15 '@types/serve-static': 1.15.7 @@ -4623,34 +4209,45 @@ snapshots: '@types/mime@1.3.5': {} - '@types/node@20.14.2': + '@types/node@20.14.7': dependencies: undici-types: 5.26.5 '@types/normalize-package-data@2.4.4': {} + '@types/prop-types@15.7.12': {} + '@types/qs@6.9.15': {} '@types/range-parser@1.2.7': {} + '@types/react-dom@18.3.0': + dependencies: + '@types/react': 18.3.3 + + '@types/react@18.3.3': + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + '@types/resolve@1.20.2': {} '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.14.2 + '@types/node': 20.14.7 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.14.2 + '@types/node': 20.14.7 '@types/send': 0.17.4 '@types/superagent@8.1.7': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 20.14.2 + '@types/node': 20.14.7 '@types/supertest@6.0.2': dependencies: @@ -4659,88 +4256,88 @@ snapshots: '@types/unist@2.0.10': {} - '@typescript-eslint/eslint-plugin@7.13.0(@typescript-eslint/parser@7.13.0(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(typescript@5.5.2)': dependencies: '@eslint-community/regexpp': 4.10.1 - '@typescript-eslint/parser': 7.13.0(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.13.0 - '@typescript-eslint/type-utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.13.0 + '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.5.2) + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/type-utils': 7.13.1(eslint@9.5.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.5.2) + '@typescript-eslint/visitor-keys': 7.13.1 eslint: 9.5.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.4.5) + ts-api-utils: 1.3.0(typescript@5.5.2) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.13.0(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2)': dependencies: - '@typescript-eslint/scope-manager': 7.13.0 - '@typescript-eslint/types': 7.13.0 - '@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.13.0 + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.5.2) + '@typescript-eslint/visitor-keys': 7.13.1 debug: 4.3.5 eslint: 9.5.0 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.13.0': + '@typescript-eslint/scope-manager@7.13.1': dependencies: - '@typescript-eslint/types': 7.13.0 - '@typescript-eslint/visitor-keys': 7.13.0 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/visitor-keys': 7.13.1 - '@typescript-eslint/type-utils@7.13.0(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/type-utils@7.13.1(eslint@9.5.0)(typescript@5.5.2)': dependencies: - '@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.5.2) + '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.5.2) debug: 4.3.5 eslint: 9.5.0 - ts-api-utils: 1.3.0(typescript@5.4.5) + ts-api-utils: 1.3.0(typescript@5.5.2) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.13.0': {} + '@typescript-eslint/types@7.13.1': {} - '@typescript-eslint/typescript-estree@7.13.0(typescript@5.4.5)': + '@typescript-eslint/typescript-estree@7.13.1(typescript@5.5.2)': dependencies: - '@typescript-eslint/types': 7.13.0 - '@typescript-eslint/visitor-keys': 7.13.0 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/visitor-keys': 7.13.1 debug: 4.3.5 globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.4 semver: 7.6.2 - ts-api-utils: 1.3.0(typescript@5.4.5) + ts-api-utils: 1.3.0(typescript@5.5.2) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.13.0(eslint@9.5.0)(typescript@5.4.5)': + '@typescript-eslint/utils@7.13.1(eslint@9.5.0)(typescript@5.5.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0) - '@typescript-eslint/scope-manager': 7.13.0 - '@typescript-eslint/types': 7.13.0 - '@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.5) + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.5.2) eslint: 9.5.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@7.13.0': + '@typescript-eslint/visitor-keys@7.13.1': dependencies: - '@typescript-eslint/types': 7.13.0 + '@typescript-eslint/types': 7.13.1 eslint-visitor-keys: 3.4.3 - '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.2))': + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.7))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -4755,7 +4352,7 @@ snapshots: std-env: 3.7.0 strip-literal: 2.1.0 test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@20.14.2) + vitest: 1.6.0(@types/node@20.14.7) transitivePeerDependencies: - supports-color @@ -5102,7 +4699,7 @@ snapshots: browserslist@4.23.1: dependencies: caniuse-lite: 1.0.30001636 - electron-to-chromium: 1.4.803 + electron-to-chromium: 1.4.808 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) @@ -5449,36 +5046,36 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.2(postcss@8.4.38): + cssnano-preset-default@7.0.3(postcss@8.4.38): dependencies: browserslist: 4.23.1 css-declaration-sorter: 7.2.0(postcss@8.4.38) cssnano-utils: 5.0.0(postcss@8.4.38) postcss: 8.4.38 postcss-calc: 10.0.0(postcss@8.4.38) - postcss-colormin: 7.0.0(postcss@8.4.38) - postcss-convert-values: 7.0.0(postcss@8.4.38) - postcss-discard-comments: 7.0.0(postcss@8.4.38) + postcss-colormin: 7.0.1(postcss@8.4.38) + postcss-convert-values: 7.0.1(postcss@8.4.38) + postcss-discard-comments: 7.0.1(postcss@8.4.38) postcss-discard-duplicates: 7.0.0(postcss@8.4.38) postcss-discard-empty: 7.0.0(postcss@8.4.38) postcss-discard-overridden: 7.0.0(postcss@8.4.38) - postcss-merge-longhand: 7.0.1(postcss@8.4.38) - postcss-merge-rules: 7.0.1(postcss@8.4.38) + postcss-merge-longhand: 7.0.2(postcss@8.4.38) + postcss-merge-rules: 7.0.2(postcss@8.4.38) postcss-minify-font-values: 7.0.0(postcss@8.4.38) postcss-minify-gradients: 7.0.0(postcss@8.4.38) - postcss-minify-params: 7.0.0(postcss@8.4.38) - postcss-minify-selectors: 7.0.1(postcss@8.4.38) + postcss-minify-params: 7.0.1(postcss@8.4.38) + postcss-minify-selectors: 7.0.2(postcss@8.4.38) postcss-normalize-charset: 7.0.0(postcss@8.4.38) postcss-normalize-display-values: 7.0.0(postcss@8.4.38) postcss-normalize-positions: 7.0.0(postcss@8.4.38) postcss-normalize-repeat-style: 7.0.0(postcss@8.4.38) postcss-normalize-string: 7.0.0(postcss@8.4.38) postcss-normalize-timing-functions: 7.0.0(postcss@8.4.38) - postcss-normalize-unicode: 7.0.0(postcss@8.4.38) + postcss-normalize-unicode: 7.0.1(postcss@8.4.38) postcss-normalize-url: 7.0.0(postcss@8.4.38) postcss-normalize-whitespace: 7.0.0(postcss@8.4.38) - postcss-ordered-values: 7.0.0(postcss@8.4.38) - postcss-reduce-initial: 7.0.0(postcss@8.4.38) + postcss-ordered-values: 7.0.1(postcss@8.4.38) + postcss-reduce-initial: 7.0.1(postcss@8.4.38) postcss-reduce-transforms: 7.0.0(postcss@8.4.38) postcss-svgo: 7.0.1(postcss@8.4.38) postcss-unique-selectors: 7.0.1(postcss@8.4.38) @@ -5487,9 +5084,9 @@ snapshots: dependencies: postcss: 8.4.38 - cssnano@7.0.2(postcss@8.4.38): + cssnano@7.0.3(postcss@8.4.38): dependencies: - cssnano-preset-default: 7.0.2(postcss@8.4.38) + cssnano-preset-default: 7.0.3(postcss@8.4.38) lilconfig: 3.1.2 postcss: 8.4.38 @@ -5497,6 +5094,8 @@ snapshots: dependencies: css-tree: 2.2.1 + csstype@3.1.3: {} + d3-array@2.12.1: dependencies: internmap: 1.0.1 @@ -5711,7 +5310,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.4.803: {} + electron-to-chromium@1.4.808: {} elliptic@6.5.5: dependencies: @@ -5745,58 +5344,6 @@ snapshots: es-errors@1.3.0: {} - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - - esbuild@0.20.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -5831,15 +5378,15 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-unjs@0.3.2(eslint@9.5.0)(typescript@5.4.5): + eslint-config-unjs@0.3.2(eslint@9.5.0)(typescript@5.5.2): dependencies: '@eslint/js': 9.5.0 eslint: 9.5.0 eslint-plugin-markdown: 5.0.0(eslint@9.5.0) eslint-plugin-unicorn: 53.0.0(eslint@9.5.0) globals: 15.6.0 - typescript: 5.4.5 - typescript-eslint: 7.13.0(eslint@9.5.0)(typescript@5.4.5) + typescript: 5.5.2 + typescript-eslint: 7.13.1(eslint@9.5.0)(typescript@5.5.2) transitivePeerDependencies: - supports-color @@ -5898,7 +5445,7 @@ snapshots: escape-string-regexp: 4.0.0 eslint-scope: 8.0.1 eslint-visitor-keys: 4.0.0 - espree: 10.0.1 + espree: 10.1.0 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -5920,7 +5467,7 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.0.1: + espree@10.1.0: dependencies: acorn: 8.12.0 acorn-jsx: 5.3.2(acorn@8.12.0) @@ -6220,14 +5767,6 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@13.2.2: - dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 4.0.0 - globby@14.0.1: dependencies: '@sindresorhus/merge-streams': 2.3.0 @@ -6245,7 +5784,7 @@ snapshots: graphemer@1.4.0: {} - h3@1.11.1: + h3@1.12.0: dependencies: cookie-es: 1.1.0 crossws: 0.2.4 @@ -6443,7 +5982,7 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.13.1: + is-core-module@2.14.0: dependencies: hasown: 2.0.2 @@ -6602,7 +6141,7 @@ snapshots: crossws: 0.2.4 defu: 6.1.4 get-port-please: 3.1.2 - h3: 1.11.1 + h3: 1.12.0 http-shutdown: 1.2.2 jiti: 1.21.6 mlly: 1.7.1 @@ -6792,13 +6331,13 @@ snapshots: mkdirp@1.0.4: {} - mkdist@1.5.1(typescript@5.4.5): + mkdist@1.5.2(typescript@5.5.2): dependencies: autoprefixer: 10.4.19(postcss@8.4.38) citty: 0.1.6 - cssnano: 7.0.2(postcss@8.4.38) + cssnano: 7.0.3(postcss@8.4.38) defu: 6.1.4 - esbuild: 0.20.2 + esbuild: 0.21.5 fs-extra: 11.2.0 globby: 14.0.1 jiti: 1.21.6 @@ -6810,7 +6349,7 @@ snapshots: postcss-nested: 6.0.1(postcss@8.4.38) semver: 7.6.2 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 mlly@1.7.1: dependencies: @@ -6837,7 +6376,7 @@ snapshots: through2: 2.0.5 xtend: 4.0.2 - morphdom@2.7.2: {} + morphdom@2.7.3: {} mri@1.2.0: {} @@ -7103,7 +6642,7 @@ snapshots: postcss-selector-parser: 6.1.0 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.0(postcss@8.4.38): + postcss-colormin@7.0.1(postcss@8.4.38): dependencies: browserslist: 4.23.1 caniuse-api: 3.0.0 @@ -7111,15 +6650,16 @@ snapshots: postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.0(postcss@8.4.38): + postcss-convert-values@7.0.1(postcss@8.4.38): dependencies: browserslist: 4.23.1 postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.0(postcss@8.4.38): + postcss-discard-comments@7.0.1(postcss@8.4.38): dependencies: postcss: 8.4.38 + postcss-selector-parser: 6.1.0 postcss-discard-duplicates@7.0.0(postcss@8.4.38): dependencies: @@ -7133,13 +6673,13 @@ snapshots: dependencies: postcss: 8.4.38 - postcss-merge-longhand@7.0.1(postcss@8.4.38): + postcss-merge-longhand@7.0.2(postcss@8.4.38): dependencies: postcss: 8.4.38 postcss-value-parser: 4.2.0 - stylehacks: 7.0.1(postcss@8.4.38) + stylehacks: 7.0.2(postcss@8.4.38) - postcss-merge-rules@7.0.1(postcss@8.4.38): + postcss-merge-rules@7.0.2(postcss@8.4.38): dependencies: browserslist: 4.23.1 caniuse-api: 3.0.0 @@ -7159,15 +6699,16 @@ snapshots: postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.0(postcss@8.4.38): + postcss-minify-params@7.0.1(postcss@8.4.38): dependencies: browserslist: 4.23.1 cssnano-utils: 5.0.0(postcss@8.4.38) postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.1(postcss@8.4.38): + postcss-minify-selectors@7.0.2(postcss@8.4.38): dependencies: + cssesc: 3.0.0 postcss: 8.4.38 postcss-selector-parser: 6.1.0 @@ -7205,7 +6746,7 @@ snapshots: postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.0(postcss@8.4.38): + postcss-normalize-unicode@7.0.1(postcss@8.4.38): dependencies: browserslist: 4.23.1 postcss: 8.4.38 @@ -7221,13 +6762,13 @@ snapshots: postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.0(postcss@8.4.38): + postcss-ordered-values@7.0.1(postcss@8.4.38): dependencies: cssnano-utils: 5.0.0(postcss@8.4.38) postcss: 8.4.38 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.0(postcss@8.4.38): + postcss-reduce-initial@7.0.1(postcss@8.4.38): dependencies: browserslist: 4.23.1 caniuse-api: 3.0.0 @@ -7417,7 +6958,7 @@ snapshots: resolve@1.22.8: dependencies: - is-core-module: 2.13.1 + is-core-module: 2.14.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -7430,18 +6971,14 @@ snapshots: hash-base: 3.1.0 inherits: 2.0.4 - rollup-plugin-dts@6.1.1(rollup@3.29.4)(typescript@5.4.5): + rollup-plugin-dts@6.1.1(rollup@4.18.0)(typescript@5.5.2): dependencies: magic-string: 0.30.10 - rollup: 3.29.4 - typescript: 5.4.5 + rollup: 4.18.0 + typescript: 5.5.2 optionalDependencies: '@babel/code-frame': 7.24.7 - rollup@3.29.4: - optionalDependencies: - fsevents: 2.3.3 - rollup@4.18.0: dependencies: '@types/estree': 1.0.5 @@ -7666,7 +7203,7 @@ snapshots: dependencies: js-tokens: 9.0.0 - stylehacks@7.0.1(postcss@8.4.38): + stylehacks@7.0.2(postcss@8.4.38): dependencies: browserslist: 4.23.1 postcss: 8.4.38 @@ -7792,9 +7329,9 @@ snapshots: merge-source-map: 1.0.4 nanobench: 2.1.1 - ts-api-utils@1.3.0(typescript@5.4.5): + ts-api-utils@1.3.0(typescript@5.5.2): dependencies: - typescript: 5.4.5 + typescript: 5.5.2 tty-browserify@0.0.1: {} @@ -7817,51 +7354,52 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@7.13.0(eslint@9.5.0)(typescript@5.4.5): + typescript-eslint@7.13.1(eslint@9.5.0)(typescript@5.5.2): dependencies: - '@typescript-eslint/eslint-plugin': 7.13.0(@typescript-eslint/parser@7.13.0(eslint@9.5.0)(typescript@5.4.5))(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/parser': 7.13.0(eslint@9.5.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.13.0(eslint@9.5.0)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(typescript@5.5.2) + '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.5.2) eslint: 9.5.0 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 transitivePeerDependencies: - supports-color - typescript@5.4.5: {} + typescript@5.5.2: {} ufo@1.5.3: {} umd@3.0.3: {} - unbuild@2.0.0(typescript@5.4.5): + unbuild@3.0.0-rc.2(typescript@5.5.2): dependencies: - '@rollup/plugin-alias': 5.1.0(rollup@3.29.4) - '@rollup/plugin-commonjs': 25.0.8(rollup@3.29.4) - '@rollup/plugin-json': 6.1.0(rollup@3.29.4) - '@rollup/plugin-node-resolve': 15.2.3(rollup@3.29.4) - '@rollup/plugin-replace': 5.0.7(rollup@3.29.4) - '@rollup/pluginutils': 5.1.0(rollup@3.29.4) + '@rollup/plugin-alias': 5.1.0(rollup@4.18.0) + '@rollup/plugin-commonjs': 25.0.8(rollup@4.18.0) + '@rollup/plugin-json': 6.1.0(rollup@4.18.0) + '@rollup/plugin-node-resolve': 15.2.3(rollup@4.18.0) + '@rollup/plugin-replace': 5.0.7(rollup@4.18.0) + '@rollup/pluginutils': 5.1.0(rollup@4.18.0) chalk: 5.3.0 citty: 0.1.6 consola: 3.2.3 defu: 6.1.4 - esbuild: 0.19.12 - globby: 13.2.2 + esbuild: 0.21.5 + globby: 14.0.1 hookable: 5.5.3 jiti: 1.21.6 magic-string: 0.30.10 - mkdist: 1.5.1(typescript@5.4.5) + mkdist: 1.5.2(typescript@5.5.2) mlly: 1.7.1 pathe: 1.1.2 pkg-types: 1.1.1 pretty-bytes: 6.1.1 - rollup: 3.29.4 - rollup-plugin-dts: 6.1.1(rollup@3.29.4)(typescript@5.4.5) + rollup: 4.18.0 + rollup-plugin-dts: 6.1.1(rollup@4.18.0)(typescript@5.5.2) scule: 1.3.0 + ufo: 1.5.3 untyped: 1.4.2 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.2 transitivePeerDependencies: - sass - supports-color @@ -7967,13 +7505,13 @@ snapshots: vary@1.1.2: {} - vite-node@1.6.0(@types/node@20.14.2): + vite-node@1.6.0(@types/node@20.14.7): dependencies: cac: 6.7.14 debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.3.1(@types/node@20.14.2) + vite: 5.3.1(@types/node@20.14.7) transitivePeerDependencies: - '@types/node' - less @@ -7984,16 +7522,16 @@ snapshots: - supports-color - terser - vite@5.3.1(@types/node@20.14.2): + vite@5.3.1(@types/node@20.14.7): dependencies: esbuild: 0.21.5 postcss: 8.4.38 rollup: 4.18.0 optionalDependencies: - '@types/node': 20.14.2 + '@types/node': 20.14.7 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.14.2): + vitest@1.6.0(@types/node@20.14.7): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -8012,11 +7550,11 @@ snapshots: strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.3.1(@types/node@20.14.2) - vite-node: 1.6.0(@types/node@20.14.2) + vite: 5.3.1(@types/node@20.14.7) + vite-node: 1.6.0(@types/node@20.14.7) why-is-node-running: 2.2.2 optionalDependencies: - '@types/node': 20.14.2 + '@types/node': 20.14.7 transitivePeerDependencies: - less - lightningcss diff --git a/src/adapters/index.ts b/src/adapters/index.ts deleted file mode 100644 index 17d425db..00000000 --- a/src/adapters/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from "./node"; - -export { type WebHandler, toWebHandler, fromWebHandler } from "./web"; - -export { - type PlainHandler, - type PlainRequest, - type PlainResponse, - toPlainHandler, - fromPlainHandler, -} from "./plain"; diff --git a/src/adapters/node.ts b/src/adapters/node.ts deleted file mode 100644 index 1d91d736..00000000 --- a/src/adapters/node.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { - IncomingMessage as NodeIncomingMessage, - ServerResponse as NodeServerResponse, -} from "node:http"; -import { App } from "../app"; -import { createError, isError, sendError } from "../error"; -import { createEvent, eventHandler, isEventHandler } from "../event"; -import { EventHandler, EventHandlerResponse } from "../types"; -import { setResponseStatus } from "../utils"; - -// Node.js -export type { - IncomingMessage as NodeIncomingMessage, - ServerResponse as NodeServerResponse, -} from "node:http"; -export type NodeListener = ( - req: NodeIncomingMessage, - res: NodeServerResponse, -) => void; -export type NodePromisifiedHandler = ( - req: NodeIncomingMessage, - res: NodeServerResponse, -) => Promise; -export type NodeMiddleware = ( - req: NodeIncomingMessage, - res: NodeServerResponse, - next: (err?: Error) => any, -) => any; - -export const defineNodeListener = (handler: NodeListener) => handler; - -export const defineNodeMiddleware = (middleware: NodeMiddleware) => middleware; - -export function fromNodeMiddleware( - handler: NodeListener | NodeMiddleware, -): EventHandler { - if (isEventHandler(handler)) { - return handler; - } - if (typeof handler !== "function") { - throw new (TypeError as any)( - "Invalid handler. It should be a function:", - handler, - ); - } - return eventHandler((event) => { - return callNodeListener( - handler, - event.node.req as NodeIncomingMessage, - event.node.res, - ) as EventHandlerResponse; - }); -} - -export function toNodeListener(app: App): NodeListener { - const toNodeHandle: NodeListener = async function (req, res) { - const event = createEvent(req, res); - try { - await app.handler(event); - } catch (_error: any) { - const error = createError(_error); - if (!isError(_error)) { - error.unhandled = true; - } - - // #754 Make sure hooks see correct status code and message - setResponseStatus(event, error.statusCode, error.statusMessage); - - if (app.options.onError) { - await app.options.onError(error, event); - } - if (event.handled) { - return; - } - if (error.unhandled || error.fatal) { - console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); - } - - if (app.options.onBeforeResponse && !event._onBeforeResponseCalled) { - await app.options.onBeforeResponse(event, { body: error }); - } - await sendError(event, error, !!app.options.debug); - if (app.options.onAfterResponse && !event._onAfterResponseCalled) { - await app.options.onAfterResponse(event, { body: error }); - } - } - }; - return toNodeHandle; -} - -export function promisifyNodeListener( - handler: NodeListener | NodeMiddleware, -): NodePromisifiedHandler { - return function (req: NodeIncomingMessage, res: NodeServerResponse) { - return callNodeListener(handler, req, res); - }; -} - -export function callNodeListener( - handler: NodeMiddleware, - req: NodeIncomingMessage, - res: NodeServerResponse, -) { - const isMiddleware = handler.length > 2; - return new Promise((resolve, reject) => { - const next = (err?: Error) => { - if (isMiddleware) { - res.off("close", next); - res.off("error", next); - } - return err ? reject(createError(err)) : resolve(undefined); - }; - try { - const returned = handler(req, res, next); - if (isMiddleware && returned === undefined) { - res.once("close", next); - res.once("error", next); - } else { - resolve(returned); - } - } catch (error) { - next(error as Error); - } - }); -} diff --git a/src/adapters/node/_headers.ts b/src/adapters/node/_headers.ts new file mode 100644 index 00000000..ffd2ead5 --- /dev/null +++ b/src/adapters/node/_headers.ts @@ -0,0 +1,96 @@ +import type { OutgoingHttpHeaders, IncomingHttpHeaders } from "node:http"; +import { splitCookiesString } from "../../utils/cookie"; + +type NodeHeaders = OutgoingHttpHeaders | IncomingHttpHeaders; + +export class NodeHeadersProxy implements Headers { + getHeaders: () => NodeHeaders; + + constructor(getHeaders: () => NodeHeaders) { + this.getHeaders = getHeaders; + } + + append(name: string, value: string): void { + const _headers = this.getHeaders(); + const _current = _headers[name]; + if (_current) { + if (Array.isArray(_current)) { + _current.push(value); + } else { + _headers[name] = [_current as string, value]; + } + } else { + _headers[name] = value; + } + } + + delete(name: string): void { + this.getHeaders()[name] = undefined; + } + + get(name: string): string | null { + return _normalizeValue(this.getHeaders()[name]); + } + + getSetCookie(): string[] { + const setCookie = this.getHeaders()["set-cookie"]; + if (!setCookie || setCookie.length === 0) { + return []; + } + return splitCookiesString(setCookie); + } + + has(name: string): boolean { + return !!this.getHeaders()[name]; + } + + set(name: string, value: string): void { + this.getHeaders()[name] = value; + } + + forEach( + cb: (value: string, key: string, parent: Headers) => void, + thisArg?: any, + ): void { + const _headers = this.getHeaders(); + for (const key in _headers) { + if (_headers[key]) { + cb.call(thisArg, _normalizeValue(_headers[key]), key, this); + } + } + } + + *entries(): IterableIterator<[string, string]> { + const _headers = this.getHeaders(); + for (const key in _headers) { + yield [key, _normalizeValue(_headers[key])]; + } + } + + *keys(): IterableIterator { + const keys = Object.keys(this.getHeaders()); + for (const key of keys) { + yield key; + } + } + + *values(): IterableIterator { + const values = Object.values(this.getHeaders()); + for (const value of values) { + yield _normalizeValue(value); + } + } + + [Symbol.iterator](): IterableIterator<[string, string]> { + return this.entries()[Symbol.iterator](); + } +} + +function _normalizeValue( + value: string | string[] | number | undefined, +): string { + if (Array.isArray(value)) { + return value.join(", "); + } + return (value as string) || ""; +} diff --git a/src/adapters/node/_internal.ts b/src/adapters/node/_internal.ts new file mode 100644 index 00000000..9085f2c6 --- /dev/null +++ b/src/adapters/node/_internal.ts @@ -0,0 +1,125 @@ +import type { Readable as NodeReadableStream } from "node:stream"; +import type { RawResponse } from "../../types/event"; +import type { NodeIncomingMessage, NodeServerResponse } from "../../types/node"; +import { _kRaw } from "../../event"; + +export function _getBodyStream( + req: NodeIncomingMessage, +): ReadableStream { + return new ReadableStream({ + start(controller) { + req.on("data", (chunk) => { + controller.enqueue(chunk); + }); + req.once("end", () => { + controller.close(); + }); + req.once("error", (err) => { + controller.error(err); + }); + }, + }); +} + +export function _sendResponse( + res: NodeServerResponse, + data: RawResponse, +): Promise { + // Native Web Streams + // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream + if (typeof (data as ReadableStream)?.pipeTo === "function") { + return (data as ReadableStream) + .pipeTo( + new WritableStream({ + write: (chunk) => { + res.write(chunk); + }, + }), + ) + .then(() => _endResponse(res)); + } + + // Node.js Readable Streams + // https://nodejs.org/api/stream.html#readable-streams + if (typeof (data as NodeReadableStream)?.pipe === "function") { + return new Promise((resolve, reject) => { + // Pipe stream to response + (data as NodeReadableStream).pipe(res); + + // Handle stream events (if supported) + if ((data as NodeReadableStream).on) { + (data as NodeReadableStream).on("end", resolve); + (data as NodeReadableStream).on("error", reject); + } + + // Handle request aborts + res.once("close", () => { + (data as NodeReadableStream).destroy?.(); + // https://react.dev/reference/react-dom/server/renderToPipeableStream + (data as any).abort?.(); + }); + }).then(() => _endResponse(res)); + } + + // Send as string or buffer + return _endResponse(res, data); +} + +export function _endResponse( + res: NodeServerResponse, + chunk?: any, +): Promise { + return new Promise((resolve) => { + res.end(chunk, resolve); + }); +} + +export function _normalizeHeaders( + headers: Record, +): Record { + const normalized: Record = Object.create(null); + for (const [key, value] of Object.entries(headers)) { + normalized[key] = Array.isArray(value) + ? value.join(", ") + : (value as string); + } + return normalized; +} + +const payloadMethods = ["PATCH", "POST", "PUT", "DELETE"] as string[]; + +export function _readBody( + req: NodeIncomingMessage, +): undefined | Promise { + // Check if request method requires a payload + if (!req.method || !payloadMethods.includes(req.method?.toUpperCase())) { + return; + } + + // Make sure either content-length or transfer-encoding/chunked is set + if (!Number.parseInt(req.headers["content-length"] || "")) { + const isChunked = (req.headers["transfer-encoding"] || "") + .split(",") + .map((e) => e.trim()) + .filter(Boolean) + .includes("chunked"); + if (!isChunked) { + return; + } + } + + // Read body + return new Promise((resolve, reject) => { + const bodyData: any[] = []; + req + .on("data", (chunk) => { + bodyData.push(chunk); + }) + .once("error", (err) => { + reject(err); + }) + .once("end", () => { + resolve(Buffer.concat(bodyData)); + }); + }); +} diff --git a/src/adapters/node/event.ts b/src/adapters/node/event.ts new file mode 100644 index 00000000..7751647f --- /dev/null +++ b/src/adapters/node/event.ts @@ -0,0 +1,207 @@ +import type { HTTPMethod } from "../../types"; +import { RawEvent, type RawResponse } from "../../types/event"; +import { splitCookiesString } from "../../utils/cookie"; +import { NodeHeadersProxy } from "./_headers"; +import { + _normalizeHeaders, + _readBody, + _getBodyStream, + _sendResponse, +} from "./_internal"; + +import type { NodeIncomingMessage, NodeServerResponse } from "../../types/node"; + +export class NodeEvent implements RawEvent { + static isNode = true; + + _req: NodeIncomingMessage; + _res: NodeServerResponse; + + _handled?: boolean; + + _originalPath?: string | undefined; + + _rawBody?: Promise; + _textBody?: Promise; + _formDataBody?: Promise; + _bodyStream?: undefined | ReadableStream; + + _headers?: NodeHeadersProxy; + _responseHeaders?: NodeHeadersProxy; + + constructor(req: NodeIncomingMessage, res: NodeServerResponse) { + this._req = req; + this._res = res; + } + + getContext() { + return { + req: this._req, + res: this._res, + }; + } + + // -- request -- + + get method() { + return this._req.method as HTTPMethod; + } + + get path() { + return this._req.url || "/"; + } + + set path(path: string) { + if (!this.originalPath) { + this._originalPath = this.path; + } + this._req.url = path; + } + + get originalPath() { + return this._originalPath || this.path; + } + + getHeader(key: string) { + const value = this._req.headers[key]; + if (Array.isArray(value)) { + return value.join(", "); + } + return value; + } + + getHeaders() { + if (!this._headers) { + this._headers = new NodeHeadersProxy(() => this._req.headers); + } + return this._headers; + } + + get remoteAddress() { + return this._req.socket.remoteAddress; + } + + get isSecure() { + return (this._req.connection as any).encrypted; + } + + readRawBody() { + if (!this._rawBody) { + this._rawBody = _readBody(this._req); + } + return this._rawBody; + } + + readTextBody() { + if (this._textBody) { + return this._textBody; + } + this._textBody = Promise.resolve(this.readRawBody()).then((body) => + body ? new TextDecoder().decode(body) : undefined, + ); + return this._textBody; + } + + async readFormDataBody() { + if (!this._formDataBody) { + this._formDataBody = Promise.resolve(this.readRawBody()).then((body) => + body + ? new Response(body, { + headers: this.getHeaders(), + }).formData() + : undefined, + ); + } + return this._formDataBody; + } + + getBodyStream() { + if (!this._bodyStream) { + this._bodyStream = _getBodyStream(this._req); + } + return this._bodyStream; + } + + // -- response -- + + get handled() { + return this._handled || this._res.writableEnded || this._res.headersSent; + } + + get responseCode() { + return this._res.statusCode || 200; + } + + set responseCode(code: number) { + this._res.statusCode = code; + } + + get responseMessage() { + return this._res.statusMessage; + } + + set responseMessage(message: string) { + this._res.statusMessage = message; + } + + setResponseHeader(key: string, value: string) { + this._res.setHeader(key, value); + } + + appendResponseHeader(key: string, value: string) { + this._res.appendHeader(key, value); + } + + getResponseHeader(key: string) { + const value = this._res.getHeader(key); + if (!value) { + return undefined; + } + if (Array.isArray(value)) { + return value.join(", "); + } + return value as string; + } + + getResponseHeaders() { + if (!this._responseHeaders) { + this._responseHeaders = new NodeHeadersProxy(() => + this._res.getHeaders(), + ); + } + return this._responseHeaders; + } + + getResponseSetCookie() { + const value = this._res.getHeader("set-cookie"); + if (!value) { + return []; + } + return splitCookiesString(value as string | string[]); + } + + removeResponseHeader(key: string) { + this._res.removeHeader(key); + } + + writeHead(code: number, message?: string) { + this._res.writeHead(code, message); + } + + writeEarlyHints(hints: Record) { + if (this._res.writeEarlyHints) { + return new Promise((resolve) => { + return this._res.writeEarlyHints(hints, resolve); + }); + } + } + + sendResponse(data: RawResponse) { + this._handled = true; + return _sendResponse(this._res, data).catch((error) => { + // TODO: better way? + this._handled = false; + throw error; + }); + } +} diff --git a/src/adapters/node/index.ts b/src/adapters/node/index.ts new file mode 100644 index 00000000..577455eb --- /dev/null +++ b/src/adapters/node/index.ts @@ -0,0 +1,19 @@ +import type { NodeHandler, NodeMiddleware } from "../../types/node"; + +export { + fromNodeHandler, + toNodeHandler, + fromNodeRequest, + getNodeContext, + callNodeHandler, +} from "./utils"; + +export type { + NodeHandler, + NodeMiddleware, + NodeIncomingMessage, + NodeServerResponse, +} from "../../types/node"; + +export const defineNodeHandler = (handler: NodeHandler) => handler; +export const defineNodeMiddleware = (handler: NodeMiddleware) => handler; diff --git a/src/adapters/node/utils.ts b/src/adapters/node/utils.ts new file mode 100644 index 00000000..0e88a827 --- /dev/null +++ b/src/adapters/node/utils.ts @@ -0,0 +1,139 @@ +import type { + App, + EventHandler, + EventHandlerResponse, + H3Event, +} from "../../types"; +import type { + NodeHandler, + NodeIncomingMessage, + NodeMiddleware, + NodeServerResponse, +} from "../../types/node"; +import { _kRaw } from "../../event"; +import { createError, isError, sendError } from "../../error"; +import { defineEventHandler, isEventHandler } from "../../handler"; +import { setResponseStatus } from "../../utils/response"; +import { EventWrapper } from "../../event"; +import { NodeEvent } from "./event"; + +/** + * Convert H3 app instance to a NodeHandler with (IncomingMessage, ServerResponse) => void signature. + */ +export function toNodeHandler(app: App): NodeHandler { + const nodeHandler: NodeHandler = async function (req, res) { + const rawEvent = new NodeEvent(req, res); + const event = new EventWrapper(rawEvent); + try { + await app.handler(event); + } catch (_error: any) { + const error = createError(_error); + if (!isError(_error)) { + error.unhandled = true; + } + + // #754 Make sure hooks see correct status code and message + setResponseStatus(event, error.statusCode, error.statusMessage); + + if (app.options.onError) { + await app.options.onError(error, event); + } + if (event[_kRaw].handled) { + return; + } + if (error.unhandled || error.fatal) { + console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); + } + + if (app.options.onBeforeResponse && !event._onBeforeResponseCalled) { + await app.options.onBeforeResponse(event, { body: error }); + } + await sendError(event, error, !!app.options.debug); + if (app.options.onAfterResponse && !event._onAfterResponseCalled) { + await app.options.onAfterResponse(event, { body: error }); + } + } + }; + return nodeHandler; +} + +/** + * Convert a Node.js handler function (req, res, next?) to an EventHandler. + * + * **Note:** The returned event handler requires to be executed with h3 Node.js handler. + */ +export function fromNodeHandler(handler: NodeMiddleware): EventHandler; +export function fromNodeHandler(handler: NodeHandler): EventHandler; +export function fromNodeHandler( + handler: NodeHandler | NodeMiddleware, +): EventHandler { + if (isEventHandler(handler)) { + return handler; + } + if (typeof handler !== "function") { + throw new TypeError(`Invalid handler. It should be a function: ${handler}`); + } + return defineEventHandler((event) => { + const nodeCtx = getNodeContext(event); + if (!nodeCtx) { + throw new Error( + "[h3] Executing Node.js middleware is not supported in this server!", + ); + } + return callNodeHandler( + handler, + nodeCtx.req, + nodeCtx.res, + ) as EventHandlerResponse; + }); +} + +/*** + * Create a H3Event object from a Node.js request and response. + */ +export function fromNodeRequest( + req: NodeIncomingMessage, + res: NodeServerResponse, +): H3Event { + const rawEvent = new NodeEvent(req, res); + const event = new EventWrapper(rawEvent); + return event; +} + +export function callNodeHandler( + handler: NodeHandler | NodeMiddleware, + req: NodeIncomingMessage, + res: NodeServerResponse, +) { + const isMiddleware = handler.length > 2; + return new Promise((resolve, reject) => { + const next = (err?: Error) => { + if (isMiddleware) { + res.off("close", next); + res.off("error", next); + } + return err ? reject(createError(err)) : resolve(undefined); + }; + try { + const returned = handler(req, res, next); + if (isMiddleware && returned === undefined) { + res.once("close", next); + res.once("error", next); + } else { + resolve(returned); + } + } catch (error) { + next(error as Error); + } + }); +} + +export function getNodeContext( + event: H3Event, +): undefined | ReturnType { + const raw = event[_kRaw] as NodeEvent; + if (!(raw?.constructor as any)?.isNode) { + return undefined; + } + return raw.getContext(); +} diff --git a/src/adapters/plain.ts b/src/adapters/plain.ts deleted file mode 100644 index 8b1f0261..00000000 --- a/src/adapters/plain.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { IncomingMessage } from "node:http"; -import { - IncomingMessage as NodeIncomingMessage, - ServerResponse as NodeServerResponse, -} from "unenv/runtime/node/http/index"; -import type { App } from "../app"; -import type { HTTPMethod } from "../types"; -import { createError, isError, sendError } from "../error"; -import { H3Event, createEvent, eventHandler } from "../event"; -import { - getRequestWebStream, - setResponseHeader, - setResponseStatus, - splitCookiesString, -} from "../utils"; - -export interface PlainRequest { - _eventOverrides?: Partial; - context?: Record; - - method: string; - path: string; - headers: HeadersInit; - body?: null | BodyInit; -} - -export interface PlainResponse { - status: number; - statusText: string; - headers: [string, string][]; - body?: unknown; -} - -export type PlainHandler = (request: PlainRequest) => Promise; - -/** @experimental */ -export function toPlainHandler(app: App) { - const handler: PlainHandler = (request) => { - return _handlePlainRequest(app, request); - }; - return handler; -} - -/** @experimental */ -export function fromPlainHandler(handler: PlainHandler) { - return eventHandler(async (event) => { - const res = await handler({ - method: event.method, - path: event.path, - headers: Object.fromEntries(event.headers.entries()), - body: getRequestWebStream(event), - context: event.context, - }); - setResponseStatus(event, res.status, res.statusText); - for (const [key, value] of res.headers) { - setResponseHeader(event, key, value); - } - return res.body; - }); -} - -// --- Internal --- - -export async function _handlePlainRequest(app: App, request: PlainRequest) { - // Normalize request - const path = request.path; - const method = (request.method || "GET").toUpperCase() as HTTPMethod; - const headers = new Headers(request.headers); - - // Shim for Node.js request and response objects - // TODO: Remove in next major version - const nodeReq = - new NodeIncomingMessage() as unknown /* unenv */ as IncomingMessage; - const nodeRes = new NodeServerResponse(nodeReq); - - // Fill node request properties - nodeReq.method = method; - nodeReq.url = path; - // TODO: Normalize with array merge and lazy getter - nodeReq.headers = Object.fromEntries(headers.entries()); - - // Create new event - const event = createEvent(nodeReq, nodeRes); - - // Fill internal event properties - event._method = method; - event._path = path; - event._headers = headers; - if (request.body) { - event._requestBody = request.body; - } - if (request._eventOverrides) { - Object.assign(event, request._eventOverrides); - } - if (request.context) { - Object.assign(event.context, request.context); - } - - // Run app handler logic - try { - await app.handler(event); - } catch (_error: any) { - const error = createError(_error); - if (!isError(_error)) { - error.unhandled = true; - } - if (app.options.onError) { - await app.options.onError(error, event); - } - if (!event.handled) { - if (error.unhandled || error.fatal) { - console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); - } - await sendError(event, error, !!app.options.debug); - } - } - - return { - status: nodeRes.statusCode, - statusText: nodeRes.statusMessage, - headers: _normalizeUnenvHeaders(nodeRes._headers), - body: (nodeRes as any)._data, - }; -} - -function _normalizeUnenvHeaders( - input: Record, -) { - const headers: [string, string][] = []; - const cookies: string[] = []; - - for (const _key in input) { - const key = _key.toLowerCase(); - - if (key === "set-cookie") { - cookies.push( - ...splitCookiesString(input["set-cookie"] as string | string[]), - ); - continue; - } - - const value = input[key]; - if (Array.isArray(value)) { - for (const _value of value) { - headers.push([key, _value]); - } - } else if (value !== undefined) { - headers.push([key, String(value)]); - } - } - - if (cookies.length > 0) { - for (const cookie of cookies) { - headers.push(["set-cookie", cookie]); - } - } - - return headers; -} diff --git a/src/adapters/web.ts b/src/adapters/web.ts deleted file mode 100644 index 07b23236..00000000 --- a/src/adapters/web.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { App } from "../app"; -import { eventHandler } from "../event"; -import { toWebRequest } from "../utils"; -import { _handlePlainRequest } from "./plain"; - -/** @experimental */ -export type WebHandler = ( - request: Request, - context?: Record, -) => Promise; - -/** @experimental */ -export function toWebHandler(app: App) { - const webHandler: WebHandler = (request, context) => { - return _handleWebRequest(app, request, context); - }; - - return webHandler; -} - -/** @experimental */ -export function fromWebHandler(handler: WebHandler) { - return eventHandler((event) => handler(toWebRequest(event), event.context)); -} - -// --- Internal --- - -const nullBodyResponses = new Set([101, 204, 205, 304]); - -async function _handleWebRequest( - app: App, - request: Request, - context?: Record, -) { - const url = new URL(request.url); - const res = await _handlePlainRequest(app, { - _eventOverrides: { - web: { request, url }, - }, - context, - method: request.method, - path: url.pathname + url.search, - headers: request.headers, - body: request.body, - }); - - // https://developer.mozilla.org/en-US/docs/Web/API/Response/body - const body = - nullBodyResponses.has(res.status) || request.method === "HEAD" - ? null - : (res.body as BodyInit); - - return new Response(body, { - status: res.status, - statusText: res.statusText, - headers: res.headers, - }); -} diff --git a/src/adapters/web/_internal.ts b/src/adapters/web/_internal.ts new file mode 100644 index 00000000..b35ad47b --- /dev/null +++ b/src/adapters/web/_internal.ts @@ -0,0 +1,81 @@ +import type { Readable as NodeReadableStream } from "node:stream"; +import type { App, H3EventContext } from "../../types"; +import type { RawResponse } from "../../types/event"; +import { createError, isError, sendError } from "../../error"; +import { EventWrapper, _kRaw } from "../../event"; +import { WebEvent } from "./event"; + +export function _normalizeResponse(data: RawResponse) { + // Node.js Readable Streams + // https://nodejs.org/api/stream.html#readable-streams + if (typeof (data as NodeReadableStream)?.pipe === "function") { + // Convert to a ReadableStream + return new ReadableStream({ + start(controller) { + (data as NodeReadableStream).on("data", (chunk) => { + controller.enqueue(chunk); + }); + (data as NodeReadableStream).on("end", () => { + controller.close(); + }); + }, + }); + } + return data as Exclude; +} + +export function _pathToRequestURL(path: string, headers?: HeadersInit): string { + if (path.startsWith("http://") || path.startsWith("https://")) { + return path; + } + const h = headers instanceof Headers ? headers : new Headers(headers); + const host = h.get("x-forwarded-host") || h.get("host") || "localhost"; + const protocol = h.get("x-forwarded-proto") === "https" ? "https" : "http"; + return `${protocol}://${host}${path}`; +} + +const nullBodyResponses = new Set([101, 204, 205, 304]); + +export async function _handleWebRequest( + app: App, + request: Request, + context?: H3EventContext, +) { + const rawEvent = new WebEvent(request); + const event = new EventWrapper(rawEvent); + + if (context) { + Object.assign(event.context, context); + } + + try { + await app.handler(event); + } catch (_error: any) { + const error = createError(_error); + if (!isError(_error)) { + error.unhandled = true; + } + if (app.options.onError) { + await app.options.onError(error, event); + } + if (!event[_kRaw].handled) { + if (error.unhandled || error.fatal) { + console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); + } + await sendError(event, error, !!app.options.debug); + } + } + + // https://developer.mozilla.org/en-US/docs/Web/API/Response/body + const body = + nullBodyResponses.has(rawEvent.responseCode!) || request.method === "HEAD" + ? null + : _normalizeResponse(rawEvent._responseBody); + + return { + body, + status: rawEvent.responseCode, + statusText: rawEvent.responseMessage, + headers: rawEvent._responseHeaders, + }; +} diff --git a/src/adapters/web/event.ts b/src/adapters/web/event.ts new file mode 100644 index 00000000..11770183 --- /dev/null +++ b/src/adapters/web/event.ts @@ -0,0 +1,176 @@ +import { RawEvent, type RawResponse } from "../../types/event"; +import { HTTPMethod } from "../../types"; + +export class WebEvent implements RawEvent { + static isWeb = true; + + _req: Request; + + _handled?: boolean; + + _path?: string; + _originalPath?: string | undefined; + + _responseBody?: RawResponse; + _responseCode?: number; + _responseMessage?: string; + _responseHeaders: Headers = new Headers(); + + _rawBody?: Promise; + _textBody?: Promise; + _formDataBody?: Promise; + + constructor(request: Request) { + this._req = request; + } + + getContext() { + return { + request: this._req, + }; + } + + // -- request -- + + get method() { + return this._req.method as HTTPMethod; + } + + get path() { + if (!this._path) { + this._path = this._originalPath = new URL(this._req.url).pathname; + } + return this._path; + } + + set path(path: string) { + if (!this._originalPath) { + this._originalPath = this._path; + } + this._path = path; + } + + get originalPath() { + return this._originalPath || this.path; + } + + getHeader(key: string) { + return this._req.headers.get(key) ?? undefined; + } + + getHeaders() { + return this._req.headers; + } + + get remoteAddress() { + return undefined; + } + + get isSecure() { + return undefined; + } + + async readRawBody() { + if (!this._rawBody) { + this._rawBody = this._req + .arrayBuffer() + .then((buffer) => new Uint8Array(buffer)); + } + return this._rawBody; + } + + readTextBody() { + if (this._textBody) { + return this._textBody; + } + if (this._rawBody) { + this._textBody = Promise.resolve(this._rawBody).then((body) => + body ? new TextDecoder().decode(body) : undefined, + ); + return this._textBody; + } + this._textBody = this._req.text(); + return this._textBody; + } + + readFormDataBody() { + if (this._formDataBody) { + return this._formDataBody; + } + this._formDataBody = this._req.formData(); + return this._formDataBody; + } + + getBodyStream() { + return this._req.body || undefined; + } + + // -- response -- + + get handled() { + return this._handled; + } + + get responseCode() { + return this._responseCode || 200; + } + + set responseCode(code: number) { + this._responseCode = code; + } + + get responseMessage() { + return this._responseMessage || ""; + } + + set responseMessage(message: string) { + this._responseMessage = message; + } + + setResponseHeader(key: string, value: string) { + this._responseHeaders.set(key, value); + } + + appendResponseHeader(key: string, value: string) { + this._responseHeaders.append(key, value); + } + + getResponseHeader(key: string) { + return this._responseHeaders.get(key) ?? undefined; + } + + getResponseHeaders() { + return this._responseHeaders; + } + + getResponseSetCookie() { + if (this._responseHeaders.getSetCookie) { + return this._responseHeaders.getSetCookie(); + } + const setCookie = this._responseHeaders.get("set-cookie"); + return setCookie ? [setCookie] : []; + } + + removeResponseHeader(key: string) { + this._responseHeaders.delete(key); + } + + writeHead(code: number, message?: string) { + this._handled = true; + if (code) { + this.responseCode = code; + } + if (message) { + this.responseMessage = message; + } + } + + writeEarlyHints(_hints: Record) { + // noop + } + + sendResponse(data: RawResponse) { + this._handled = true; + this._responseBody = data; + } +} diff --git a/src/adapters/web/index.ts b/src/adapters/web/index.ts new file mode 100644 index 00000000..cdab8cdd --- /dev/null +++ b/src/adapters/web/index.ts @@ -0,0 +1,10 @@ +export { + fromWebHandler, + toWebHandler, + fromWebRequest, + toWebRequest, + getWebContext, + fromPlainHandler, + toPlainHandler, + fromPlainRequest, +} from "./utils"; diff --git a/src/adapters/web/utils.ts b/src/adapters/web/utils.ts new file mode 100644 index 00000000..72b3c86e --- /dev/null +++ b/src/adapters/web/utils.ts @@ -0,0 +1,168 @@ +import type { App, EventHandler, H3Event, H3EventContext } from "../../types"; +import type { WebHandler, PlainHandler, PlainRequest } from "../../types/web"; +import { defineEventHandler } from "../../handler"; +import { EventWrapper, _kRaw } from "../../event"; +import { WebEvent } from "./event"; +import { _handleWebRequest, _pathToRequestURL } from "./_internal"; + +/** + * Convert H3 app instance to a WebHandler with (Request, H3EventContext) => Promise signature. + */ +export function toWebHandler(app: App): WebHandler { + const webHandler: WebHandler = async (request, context) => { + const response = await _handleWebRequest(app, request, context); + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + }; + + return webHandler; +} + +export function fromWebHandler(handler: WebHandler): EventHandler { + return defineEventHandler((event) => + handler(toWebRequest(event), event.context), + ); +} + +/** + * Convert an H3Event object to a web Request object. + * + */ +export function toWebRequest(event: H3Event): Request { + return ( + (event[_kRaw] as WebEvent)._req || + new Request( + _pathToRequestURL(event[_kRaw].path, event[_kRaw].getHeaders()), + { + // @ts-ignore Undici option + duplex: "half", + method: event[_kRaw].method, + headers: event[_kRaw].getHeaders(), + body: event[_kRaw].getBodyStream(), + }, + ) + ); +} + +/** + * Create an H3Event object from a web Request object. + */ +export function fromWebRequest( + request: Request, + context?: H3EventContext, +): H3Event { + const rawEvent = new WebEvent(request); + const event = new EventWrapper(rawEvent); + if (context) { + Object.assign(event.context, context); + } + return event; +} + +export function getWebContext( + event: H3Event, +): undefined | ReturnType { + const raw = event[_kRaw] as WebEvent; + if (!(raw?.constructor as any)?.isWeb) { + return undefined; + } + return raw.getContext(); +} + +// ---------------------------- +// Plain +// ---------------------------- + +/** + * Convert H3 app instance to a PlainHandler with (PlainRequest, H3EventContext) => Promise signature. + */ +export function toPlainHandler(app: App) { + const handler: PlainHandler = async (request, context) => { + const res = await _handleWebRequest( + app, + new Request(_pathToRequestURL(request.path, request.headers), { + method: request.method, + headers: request.headers, + body: request.body, + }), + context, + ); + + const setCookie = res.headers.getSetCookie(); + const headersObject = Object.fromEntries(res.headers.entries()); + if (setCookie.length > 0) { + headersObject["set-cookie"] = setCookie.join(", "); + } + + return { + status: res.status, + statusText: res.statusText, + headers: headersObject, + setCookie: setCookie, + body: res.body, + }; + }; + return handler; +} + +/** + * Convert a PlainHandler to an EventHandler. + */ +export function fromPlainHandler(handler: PlainHandler) { + return defineEventHandler(async (event) => { + const res = await handler( + { + get method() { + return event.method; + }, + get path() { + return event.path; + }, + get headers() { + return event[_kRaw].getHeaders(); + }, + get body() { + return event[_kRaw].getBodyStream(); + }, + }, + event.context, + ); + event[_kRaw].responseCode = res.status; + event[_kRaw].responseMessage = res.statusText; + + const hasSetCookie = res.setCookie?.length > 0; + for (const [key, value] of Object.entries(res.headers)) { + if (key === "set-cookie" && hasSetCookie) { + continue; + } + event[_kRaw].setResponseHeader(key, value); + } + if (res.setCookie?.length > 0) { + for (const cookie of res.setCookie) { + event[_kRaw].appendResponseHeader("set-cookie", cookie); + } + } + + return res.body; + }); +} + +/** + * Create an H3Event object from a plain request object. + */ +export function fromPlainRequest( + request: PlainRequest, + context?: H3EventContext, +): H3Event { + return fromWebRequest( + new Request(_pathToRequestURL(request.path, request.headers), { + method: request.method, + headers: request.headers, + body: request.body, + }), + context, + ); +} diff --git a/src/app.ts b/src/app.ts index 1ea5774d..b6a56af5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,82 +1,35 @@ -import { joinURL, parseURL, withoutTrailingSlash } from "ufo"; -import type { AdapterOptions as WSOptions } from "crossws"; +import type { + App, + Stack, + H3Event, + EventHandler, + EventHandlerResolver, + LazyEventHandler, + AppOptions, + InputLayer, + WebSocketOptions, + Layer, +} from "./types"; +import { _kRaw } from "./event"; import { - lazyEventHandler, + defineLazyEventHandler, toEventHandler, isEventHandler, - eventHandler, - H3Event, -} from "./event"; -import { H3Error, createError } from "./error"; + defineEventHandler, +} from "./handler"; +import { createError } from "./error"; import { - send, - sendStream, - isStream, - MIMES, sendWebResponse, - isWebResponse, sendNoContent, -} from "./utils"; -import type { - EventHandler, - EventHandlerResolver, - LazyEventHandler, -} from "./types"; - -export interface Layer { - route: string; - match?: Matcher; - handler: EventHandler; -} - -export type Stack = Layer[]; - -export interface InputLayer { - route?: string; - match?: Matcher; - handler: EventHandler; - lazy?: boolean; -} - -export type InputStack = InputLayer[]; - -export type Matcher = (url: string, event?: H3Event) => boolean; - -export interface AppUse { - ( - route: string | string[], - handler: EventHandler | EventHandler[], - options?: Partial, - ): App; - (handler: EventHandler | EventHandler[], options?: Partial): App; - (options: InputLayer): App; -} - -export type WebSocketOptions = WSOptions; - -export interface AppOptions { - debug?: boolean; - onError?: (error: H3Error, event: H3Event) => any; - onRequest?: (event: H3Event) => void | Promise; - onBeforeResponse?: ( - event: H3Event, - response: { body?: unknown }, - ) => void | Promise; - onAfterResponse?: ( - event: H3Event, - response?: { body?: unknown }, - ) => void | Promise; - websocket?: WebSocketOptions; -} - -export interface App { - stack: Stack; - handler: EventHandler; - options: AppOptions; - use: AppUse; - resolve: EventHandlerResolver; - readonly websocket: WebSocketOptions; -} + defaultContentType, +} from "./utils/response"; +import { isJSONSerializable } from "./utils/internal/object"; +import { + joinURL, + getPathname, + withoutTrailingSlash, +} from "./utils/internal/path"; +import { MIMES } from "./utils/internal/consts"; /** * Create a new H3 app instance. @@ -135,13 +88,9 @@ export function use( export function createAppEventHandler(stack: Stack, options: AppOptions) { const spacing = options.debug ? 2 : undefined; - return eventHandler(async (event) => { - // Keep original incoming url accessible - event.node.req.originalUrl = - event.node.req.originalUrl || event.node.req.url || "/"; - + return defineEventHandler(async (event) => { // Keep a copy of incoming url - const _reqPath = event._path || event.node.req.url || "/"; + const _reqPath = event[_kRaw].path || "/"; // Layer path is the path without the prefix let _layerPath: string; @@ -168,8 +117,7 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) { } // 3. Update event path with layer path - event._path = _layerPath; - event.node.req.url = _layerPath; + event[_kRaw].path = _layerPath; // 4. Handle request const val = await layer.handler(event); @@ -191,7 +139,7 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) { } // Already handled - if (event.handled) { + if (event[_kRaw].handled) { if (options.onAfterResponse) { event._onAfterResponseCalled = true; await options.onAfterResponse(event, undefined); @@ -200,7 +148,7 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) { } } - if (!event.handled) { + if (!event[_kRaw].handled) { throw createError({ statusCode: 404, statusMessage: `Cannot find any path matching ${event.path || "/"}.`, @@ -254,7 +202,7 @@ function normalizeLayer(input: InputLayer) { } if (input.lazy) { - handler = lazyEventHandler(handler as LazyEventHandler); + handler = defineLazyEventHandler(handler as LazyEventHandler); } else if (!isEventHandler(handler)) { handler = toEventHandler(handler, undefined, input.route); } @@ -272,62 +220,64 @@ function handleHandlerResponse(event: H3Event, val: any, jsonSpace?: number) { return sendNoContent(event); } - if (val) { - // Web Response - if (isWebResponse(val)) { - return sendWebResponse(event, val); - } + const valType = typeof val; - // Stream - if (isStream(val)) { - return sendStream(event, val); - } + // Undefined + if (valType === "undefined") { + return sendNoContent(event); + } - // Buffer - if (val.buffer) { - return send(event, val); - } + // Text + if (valType === "string") { + defaultContentType(event, MIMES.html); + return event[_kRaw].sendResponse(val); + } - // Blob - if (val.arrayBuffer && typeof val.arrayBuffer === "function") { - return (val as Blob).arrayBuffer().then((arrayBuffer) => { - return send(event, Buffer.from(arrayBuffer), val.type); - }); - } + // Buffer (should be before JSON) + if (val.buffer) { + return event[_kRaw].sendResponse(val); + } - // Error - if (val instanceof Error) { - throw createError(val); - } + // Error (should be before JSON) + if (val instanceof Error) { + throw createError(val); + } - // Node.js Server Response (already handled with res.end()) - if (typeof val.end === "function") { - return true; - } + // JSON + if (isJSONSerializable(val, valType)) { + defaultContentType(event, MIMES.json); + return event[_kRaw].sendResponse(JSON.stringify(val, undefined, jsonSpace)); } - const valType = typeof val; + // BigInt + if (valType === "bigint") { + defaultContentType(event, MIMES.json); + return event[_kRaw].sendResponse(val.toString()); + } - // HTML String - if (valType === "string") { - return send(event, val, MIMES.html); + // Web Response + if (val instanceof Response) { + return sendWebResponse(event, val); } - // JSON Response - if (valType === "object" || valType === "boolean" || valType === "number") { - return send(event, JSON.stringify(val, undefined, jsonSpace), MIMES.json); + // Blob + if (val.arrayBuffer && typeof val.arrayBuffer === "function") { + return (val as Blob).arrayBuffer().then((arrayBuffer) => { + defaultContentType(event, val.type); + return event[_kRaw].sendResponse(Buffer.from(arrayBuffer)); + }); } - // BigInt - if (valType === "bigint") { - return send(event, val.toString(), MIMES.json); + // Symbol or Function is not supported + if (valType === "symbol" || valType === "function") { + throw createError({ + statusCode: 500, + statusMessage: `[h3] Cannot send ${valType} as response.`, + }); } - // Symbol or Function (undefined is already handled by consumer) - throw createError({ - statusCode: 500, - statusMessage: `[h3] Cannot send ${valType} as response.`, - }); + // Other values: direct send + return event[_kRaw].sendResponse(val); } function cachedFn(fn: () => T): () => T { @@ -343,11 +293,11 @@ function cachedFn(fn: () => T): () => T { function websocketOptions( evResolver: EventHandlerResolver, appOptions: AppOptions, -): WSOptions { +): WebSocketOptions { return { ...appOptions.websocket, async resolve(info) { - const { pathname } = parseURL(info.url || "/"); + const pathname = getPathname(info.url || "/"); const resolved = await evResolver(pathname); return resolved?.handler?.__websocket__ || {}; }, diff --git a/src/deprecated.ts b/src/deprecated.ts new file mode 100644 index 00000000..a866ed17 --- /dev/null +++ b/src/deprecated.ts @@ -0,0 +1,112 @@ +import type { + H3Event, + NodeHandler, + RequestHeaderName, + RequestHeaders, + RequestMiddleware, + ResponseHeaderName, + ResponseHeaders, + ResponseMiddleware, +} from "./types"; +import type { Readable as NodeReadableStream } from "node:stream"; +import { _kRaw } from "./event"; +import { getRequestHeader, getRequestHeaders } from "./utils/request"; +import { getBodyStream } from "./utils/body"; +import { + appendResponseHeader, + appendResponseHeaders, + setResponseHeader, + setResponseHeaders, +} from "./utils/response"; +import { + callNodeHandler, + defineNodeHandler, + fromNodeHandler, + fromNodeRequest, + toNodeHandler, +} from "./adapters/node"; +import { + readFormDataBody, + readJSONBody, + readValidatedJSONBody, +} from "./utils/body"; +import { defineEventHandler, defineLazyEventHandler } from "./handler"; + +/** @deprecated Please use `getRequestHeader` */ +export const getHeader = getRequestHeader; + +/** @deprecated Please use `getRequestHeaders` */ +export const getHeaders = getRequestHeaders; + +/** @deprecated Directly return stream */ +export function sendStream( + event: H3Event, + value: ReadableStream | NodeReadableStream, +) { + return event[_kRaw].sendResponse(value); +} + +/** Please use `defineEventHandler` */ +export const eventHandler = defineEventHandler; + +/** Please use `defineLazyEventHandler` */ +export const lazyEventHandler = defineLazyEventHandler; + +/** @deprecated Please use `appendResponseHeader` */ +export const appendHeader = appendResponseHeader; + +/** @deprecated Please use `appendResponseHeaders` */ +export const appendHeaders = appendResponseHeaders; + +/** @deprecated please use `setResponseHeader` */ +export const setHeader = setResponseHeader; + +/** @deprecated Please use `setResponseHeaders` */ +export const setHeaders = setResponseHeaders; + +/** @deprecated Please use `defineNodeHandler` */ +export const defineNodeListener = defineNodeHandler; + +/** @deprecated Please use `defineNodeHandler` */ +export const fromNodeMiddleware = fromNodeHandler; + +/** @deprecated Please use `fromNodeRequest` */ +export const createEvent = fromNodeRequest; + +/** @deprecated Please use `toNodeHandler` */ +export const toNodeListener = toNodeHandler; + +/** @deprecated Please use `callNodeHandler` */ +export const callNodeListener = callNodeHandler; + +/** @deprecated Please use `readJSONBody` */ +export const readBody = readJSONBody; + +/** @deprecated Please use `readFormDataBody` */ +export const readFormData = readFormDataBody; + +/** @deprecated Please use `readValidatedJSONBody` */ +export const readValidatedBody = readValidatedJSONBody; + +/** @deprecated Please use `getBodyStream` */ +export const getRequestWebStream = getBodyStream; + +/** @deprecated Please use `event.path` instead */ +export const getRequestPath = (event: H3Event) => event.path; + +// --- Types --- + +/** @deprecated Please use `RequestMiddleware` */ +export type _RequestMiddleware = RequestMiddleware; + +/** @deprecated Please use `ResponseMiddleware` */ +export type _ResponseMiddleware = ResponseMiddleware; + +/** @deprecated Please use `NodeHandler` */ +export type NodeListener = NodeHandler; + +/** @deprecated Please use `RequestHeaders` or `ResponseHeaders` */ +export type TypedHeaders = RequestHeaders & ResponseHeaders; + +/** @deprecated Please use `RequestHeaderName` or `ResponseHeaderName` */ +export type HTTPHeaderName = RequestHeaderName | ResponseHeaderName; diff --git a/src/error.ts b/src/error.ts index 640fa095..db0f6133 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,11 +1,9 @@ -import type { H3Event } from "./event"; -import { - MIMES, - setResponseStatus, - sanitizeStatusMessage, - sanitizeStatusCode, -} from "./utils"; +import type { H3Event } from "./types"; +import { _kRaw } from "./event"; import { hasProp } from "./utils/internal/object"; +import { setResponseStatus } from "./utils/response"; +import { sanitizeStatusMessage, sanitizeStatusCode } from "./utils/sanitize"; +import { MIMES } from "./utils/internal/consts"; /** * H3 Runtime Error @@ -133,14 +131,7 @@ export function createError( err.statusMessage = input.statusText as string; } if (err.statusMessage) { - // TODO: Always sanitize the status message in the next major releases - const originalMessage = err.statusMessage; - const sanitizedMessage = sanitizeStatusMessage(err.statusMessage); - if (sanitizedMessage !== originalMessage) { - console.warn( - "[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default.", - ); - } + err.statusMessage = sanitizeStatusMessage(err.statusMessage); } if (input.fatal !== undefined) { @@ -168,7 +159,7 @@ export function sendError( error: Error | H3Error, debug?: boolean, ) { - if (event.handled) { + if (event[_kRaw].handled) { return; } @@ -185,13 +176,13 @@ export function sendError( responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim()); } - if (event.handled) { + if (event[_kRaw].handled) { return; } const _code = Number.parseInt(h3Error.statusCode as unknown as string); setResponseStatus(event, _code, h3Error.statusMessage); - event.node.res.setHeader("content-type", MIMES.json); - event.node.res.end(JSON.stringify(responseBody, undefined, 2)); + event[_kRaw].setResponseHeader("content-type", MIMES.json); + event[_kRaw].sendResponse(JSON.stringify(responseBody, undefined, 2)); } /** diff --git a/src/event.ts b/src/event.ts new file mode 100644 index 00000000..2fc47608 --- /dev/null +++ b/src/event.ts @@ -0,0 +1,52 @@ +import type { H3Event, RawEvent } from "./types/event"; + +export const _kRaw: unique symbol = Symbol.for("h3.internal.raw"); + +export class EventWrapper implements H3Event { + static "__is_event__" = true; + + context = Object.create(null); + + [_kRaw]: RawEvent; + + _onBeforeResponseCalled: boolean | undefined; + _onAfterResponseCalled: boolean | undefined; + + constructor(raw: RawEvent) { + this[_kRaw] = raw; + } + + get method() { + return this[_kRaw].method || "GET"; + } + + get path() { + return this[_kRaw].path; + } + + get headers(): Headers { + return this[_kRaw].getHeaders(); + } + + toString() { + return `[${this.method}] ${this.path}`; + } + + toJSON() { + return this.toString(); + } +} + +/** + * Checks if the input is an H3Event object. + * @param input - The input to check. + * @returns True if the input is an H3Event object, false otherwise. + * @see H3Event + */ +export function isEvent(input: any): input is H3Event { + const ctor = input?.constructor; + return ( + ctor.__is_event__ || + input.__is_event__ /* Backward compatibility with h3 v1 */ + ); +} diff --git a/src/event/_polyfills.ts b/src/event/_polyfills.ts deleted file mode 100644 index 2526ad54..00000000 --- a/src/event/_polyfills.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @deprecated Please use native web Headers - * https://developer.mozilla.org/en-US/docs/Web/API/Headers - */ -export const H3Headers = globalThis.Headers; - -/** - * @deprecated Please use native web Response - * https://developer.mozilla.org/en-US/docs/Web/API/Response - */ -export const H3Response = globalThis.Response; diff --git a/src/event/event.ts b/src/event/event.ts deleted file mode 100644 index 18526346..00000000 --- a/src/event/event.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { IncomingHttpHeaders } from "node:http"; -import type { H3EventContext, HTTPMethod, EventHandlerRequest } from "../types"; -import type { NodeIncomingMessage, NodeServerResponse } from "../adapters/node"; -import { sendWebResponse } from "../utils"; -import { hasProp } from "../utils/internal/object"; - -export interface NodeEventContext { - req: NodeIncomingMessage & { originalUrl?: string }; - res: NodeServerResponse; -} - -export interface WebEventContext { - request?: Request; - url?: URL; -} - -export class H3Event< - _RequestT extends EventHandlerRequest = EventHandlerRequest, -> implements Pick -{ - "__is_event__" = true; - - // Context - node: NodeEventContext; // Node - web?: WebEventContext; // Web - context: H3EventContext = {}; // Shared - - // Request - _method?: HTTPMethod; - _path?: string; - _headers?: Headers; - _requestBody?: BodyInit; - - // Response - _handled = false; - - // Hooks - _onBeforeResponseCalled: boolean | undefined; - _onAfterResponseCalled: boolean | undefined; - - constructor(req: NodeIncomingMessage, res: NodeServerResponse) { - this.node = { req, res }; - } - - // --- Request --- - - get method(): HTTPMethod { - if (!this._method) { - this._method = ( - this.node.req.method || "GET" - ).toUpperCase() as HTTPMethod; - } - return this._method; - } - - get path() { - return this._path || this.node.req.url || "/"; - } - - get headers(): Headers { - if (!this._headers) { - this._headers = _normalizeNodeHeaders(this.node.req.headers); - } - return this._headers; - } - - // --- Respoonse --- - - get handled(): boolean { - return ( - this._handled || this.node.res.writableEnded || this.node.res.headersSent - ); - } - - respondWith(response: Response | PromiseLike): Promise { - return Promise.resolve(response).then((_response) => - sendWebResponse(this, _response), - ); - } - - // --- Utils --- - - toString() { - return `[${this.method}] ${this.path}`; - } - - toJSON() { - return this.toString(); - } - - // --- Deprecated --- - - /** @deprecated Please use `event.node.req` instead. */ - get req() { - return this.node.req; - } - - /** @deprecated Please use `event.node.res` instead. */ - get res() { - return this.node.res; - } -} - -/** - * Checks if the input is an H3Event object. - * @param input - The input to check. - * @returns True if the input is an H3Event object, false otherwise. - * @see H3Event - */ -export function isEvent(input: any): input is H3Event { - return hasProp(input, "__is_event__"); -} - -/** - * Creates a new H3Event instance from the given Node.js request and response objects. - * @param req - The NodeIncomingMessage object. - * @param res - The NodeServerResponse object. - * @returns A new H3Event instance. - * @see H3Event - */ -export function createEvent( - req: NodeIncomingMessage, - res: NodeServerResponse, -): H3Event { - return new H3Event(req, res); -} - -// --- Internal --- - -function _normalizeNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers { - const headers = new Headers(); - - for (const [name, value] of Object.entries(nodeHeaders)) { - if (Array.isArray(value)) { - for (const item of value) { - headers.append(name, item); - } - } else if (value) { - headers.set(name, value); - } - } - - return headers; -} diff --git a/src/event/index.ts b/src/event/index.ts deleted file mode 100644 index 68571eb1..00000000 --- a/src/event/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./event"; -export * from "./utils"; -export * from "./_polyfills"; diff --git a/src/event/utils.ts b/src/handler.ts similarity index 86% rename from src/event/utils.ts rename to src/handler.ts index 4ec7fb57..bcb29825 100644 --- a/src/event/utils.ts +++ b/src/handler.ts @@ -1,18 +1,22 @@ +import type { + DynamicEventHandler, + H3Event, + RequestMiddleware, + ResponseMiddleware, +} from "./types"; import type { EventHandler, LazyEventHandler, EventHandlerRequest, EventHandlerResponse, EventHandlerObject, - _RequestMiddleware, - _ResponseMiddleware, -} from "../types"; -import { hasProp } from "../utils/internal/object"; -import type { H3Event } from "./event"; +} from "./types"; +import { _kRaw } from "./event"; +import { hasProp } from "./utils/internal/object"; type _EventHandlerHooks = { - onRequest?: _RequestMiddleware[]; - onBeforeResponse?: _ResponseMiddleware[]; + onRequest?: RequestMiddleware[]; + onBeforeResponse?: ResponseMiddleware[]; }; export function defineEventHandler< @@ -76,7 +80,7 @@ async function _callHandler( if (hooks.onRequest) { for (const hook of hooks.onRequest) { await hook(event); - if (event.handled) { + if (event[_kRaw].handled) { return; } } @@ -91,17 +95,15 @@ async function _callHandler( return response.body; } -export const eventHandler = defineEventHandler; - export function defineRequestMiddleware< Request extends EventHandlerRequest = EventHandlerRequest, ->(fn: _RequestMiddleware): _RequestMiddleware { +>(fn: RequestMiddleware): RequestMiddleware { return fn; } export function defineResponseMiddleware< Request extends EventHandlerRequest = EventHandlerRequest, ->(fn: _ResponseMiddleware): _ResponseMiddleware { +>(fn: ResponseMiddleware): ResponseMiddleware { return fn; } @@ -129,15 +131,11 @@ export function toEventHandler( return input; } -export interface DynamicEventHandler extends EventHandler { - set: (handler: EventHandler) => void; -} - export function dynamicEventHandler( initial?: EventHandler, ): DynamicEventHandler { let current: EventHandler | undefined = initial; - const wrapper = eventHandler((event) => { + const wrapper = defineEventHandler((event) => { if (current) { return current(event); } @@ -174,7 +172,7 @@ export function defineLazyEventHandler( return _promise; }; - const handler = eventHandler((event) => { + const handler = defineEventHandler((event) => { if (_resolved) { return _resolved.handler(event); } @@ -185,4 +183,3 @@ export function defineLazyEventHandler( return handler; } -export const lazyEventHandler = defineLazyEventHandler; diff --git a/src/index.ts b/src/index.ts index 6cf3dce4..349b12bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,158 @@ -export * from "./app"; -export * from "./error"; -export * from "./event"; -export * from "./utils"; -export * from "./router"; -export * from "./types"; -export * from "./adapters"; +// App +export { createApp, use, createAppEventHandler } from "./app"; + +// Event +export { isEvent } from "./event"; + +// Handler +export { + isEventHandler, + defineEventHandler, + defineLazyEventHandler, + dynamicEventHandler, + toEventHandler, + defineRequestMiddleware, + defineResponseMiddleware, +} from "./handler"; + +// Error +export { createError, isError, sendError } from "./error"; + +// Router +export { createRouter } from "./router"; + +// ---- Adapters ---- + +// Node +export { + defineNodeMiddleware, + callNodeHandler, + defineNodeHandler, + fromNodeHandler, + fromNodeRequest, + getNodeContext, + toNodeHandler, +} from "./adapters/node"; + +// Web +export { + fromPlainHandler, + fromWebHandler, + toPlainHandler, + toWebHandler, + toWebRequest, + fromPlainRequest, + getWebContext, +} from "./adapters/web"; + +// ------ Utils ------ + +// Request +export { + getRequestHeader, + getRequestHeaders, + getRequestHost, + getRequestIP, + getRequestProtocol, + getRequestURL, + isMethod, + getQuery, + getMethod, + getValidatedQuery, + assertMethod, + getRouterParam, + getRouterParams, + getValidatedRouterParams, +} from "./utils/request"; + +// Response +export { + appendResponseHeader, + appendResponseHeaders, + sendIterable, + sendNoContent, + sendRedirect, + sendWebResponse, + setResponseHeader, + setResponseHeaders, + setResponseStatus, + writeEarlyHints, + removeResponseHeader, + clearResponseHeaders, + defaultContentType, + getResponseHeader, + getResponseHeaders, + getResponseStatus, + getResponseStatusText, +} from "./utils/response"; + +// Proxy +export { + sendProxy, + getProxyRequestHeaders, + proxyRequest, + fetchWithEvent, +} from "./utils/proxy"; + +// Body +export { + readRawBody, + getBodyStream, + readFormDataBody, + readJSONBody, + readTextBody, + readValidatedJSONBody, +} from "./utils/body"; + +// Cookie +export { + getCookie, + deleteCookie, + parseCookies, + setCookie, + splitCookiesString, +} from "./utils/cookie"; + +// SSE +export { createEventStream } from "./utils/event-stream"; + +// Sanitize +export { sanitizeStatusCode, sanitizeStatusMessage } from "./utils/sanitize"; + +// Cache +export { handleCacheHeaders } from "./utils/cache"; + +// Static +export { serveStatic } from "./utils/static"; + +// Base +export { useBase } from "./utils/base"; + +// Session +export { + clearSession, + getSession, + sealSession, + unsealSession, + updateSession, + useSession, +} from "./utils/session"; + +// Cors +export { + appendCorsHeaders, + appendCorsPreflightHeaders, + handleCors, + isCorsOriginAllowed, + isPreflightRequest, +} from "./utils/cors"; + +// Fingerprint +export { getRequestFingerprint } from "./utils/fingerprint"; + +// WebSocket +export { defineWebSocketHandler, defineWebSocket } from "./utils/ws"; + +// ---- Deprecated ---- + +export * from "./deprecated"; diff --git a/src/router.ts b/src/router.ts index 3a88d5a0..acea7d0e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,14 +1,16 @@ -import { - createRouter as _createRouter, - toRouteMatcher, - RouteMatcher, -} from "radix3"; -import { withLeadingSlash } from "ufo"; -import type { HTTPMethod, EventHandler } from "./types"; +import { createRouter as _createRouter, toRouteMatcher } from "radix3"; +import type { RouteMatcher } from "radix3"; +import type { + CreateRouterOptions, + EventHandler, + RouteNode, + Router, + RouterMethod, +} from "./types"; import { createError } from "./error"; -import { eventHandler, toEventHandler } from "./event"; +import { defineEventHandler, toEventHandler } from "./handler"; +import { withLeadingSlash } from "./utils/internal/path"; -export type RouterMethod = Lowercase; const RouterMethods: RouterMethod[] = [ "connect", "delete", @@ -21,30 +23,6 @@ const RouterMethods: RouterMethod[] = [ "patch", ]; -export type RouterUse = ( - path: string, - handler: EventHandler, - method?: RouterMethod | RouterMethod[], -) => Router; -export type AddRouteShortcuts = Record; - -export interface Router extends AddRouteShortcuts { - add: RouterUse; - use: RouterUse; - handler: EventHandler; -} - -export interface RouteNode { - handlers: Partial>; - path: string; -} - -export interface CreateRouterOptions { - /** @deprecated Please use `preemptive` instead. */ - preemtive?: boolean; - preemptive?: boolean; -} - /** * Create a new h3 router instance. */ @@ -143,7 +121,7 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { // Main handle const isPreemptive = opts.preemptive || opts.preemtive; - router.handler = eventHandler((event) => { + router.handler = defineEventHandler((event) => { // Match handler const match = matchHandler( event.path, diff --git a/src/types/_headers.ts b/src/types/_headers.ts deleted file mode 100644 index e21e0b06..00000000 --- a/src/types/_headers.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { HTTPMethod } from "."; -import type { MimeType } from "./_mimes"; -import type { AnyString, AnyNumber } from "./_utils"; - -export type RequestHeaders = Partial< - Record ->; - -// prettier-ignore -type _HTTPHeaderName = "WWW-Authenticate" | "Authorization" | "Proxy-Authenticate" | "Proxy-Authorization" | "Age" | "Cache-Control" | "Clear-Site-Data" | "Expires" | "Pragma" | "Accept-CH" | "Critical-CH" | "Sec-CH-UA" | "Sec-CH-UA-Arch" | "Sec-CH-UA-Bitness" | "Sec-CH-UA-Full-Version-List" | "Sec-CH-UA-Mobile" | "Sec-CH-UA-Model" | "Sec-CH-UA-Platform" | "Sec-CH-UA-Platform-Version" | "Sec-CH-UA-Prefers-Color-Scheme" | "Sec-CH-UA-Prefers-Reduced-Motion" | "Downlink" | "ECT" | "RTT" | "Save-Data" | "Last-Modified" | "ETag" | "If-Match" | "If-None-Match" | "If-Modified-Since" | "If-Unmodified-Since" | "Vary" | "Connection" | "Keep-Alive" | "Accept" | "Accept-Encoding" | "Accept-Language" | "Expect" | "Max-Forwards" | "Cookie" | "Set-Cookie" | "Access-Control-Allow-Origin" | "Access-Control-Allow-Credentials" | "Access-Control-Allow-Headers" | "Access-Control-Allow-Methods" | "Access-Control-Expose-Headers" | "Access-Control-Max-Age" | "Access-Control-Request-Headers" | "Access-Control-Request-Method" | "Origin" | "Timing-Allow-Origin" | "Content-Disposition" | "Content-Length" | "Content-Type" | "Content-Encoding" | "Content-Language" | "Content-Location" | "Forwarded" | "X-Forwarded-For" | "X-Forwarded-Host" | "X-Forwarded-Proto" | "Via" | "Location" | "Refresh" | "From" | "Host" | "Referer" | "Referrer-Policy" | "User-Agent" | "Allow" | "Server" | "Accept-Ranges" | "Range" | "If-Range" | "Content-Range" | "Cross-Origin-Embedder-Policy" | "Cross-Origin-Opener-Policy" | "Cross-Origin-Resource-Policy" | "Content-Security-Policy" | "Content-Security-Policy-Report-Only" | "Expect-CT" | "Origin-Isolation" | "Permissions-Policy" | "Strict-Transport-Security" | "Upgrade-Insecure-Requests" | "X-Content-Type-Options" | "X-Frame-Options" | "X-Permitted-Cross-Domain-Policies" | "X-Powered-By" | "X-XSS-Protection" | "Sec-Fetch-Site" | "Sec-Fetch-Mode" | "Sec-Fetch-User" | "Sec-Fetch-Dest" | "Sec-Purpose" | "Service-Worker-Navigation-Preload" | "Last-Event-ID" | "NEL" | "Ping-From" | "Ping-To" | "Report-To" | "Transfer-Encoding" | "TE" | "Trailer" | "Sec-WebSocket-Key" | "Sec-WebSocket-Extensions" | "Sec-WebSocket-Accept" | "Sec-WebSocket-Protocol" | "Sec-WebSocket-Version" | "Accept-Push-Policy" | "Accept-Signature" | "Alt-Svc" | "Alt-Used" | "Date" | "Early-Data" | "Link" | "Push-Policy" | "Retry-After" | "Signature" | "Signed-Headers" | "Server-Timing" | "Service-Worker-Allowed" | "SourceMap" | "Upgrade" | "X-DNS-Prefetch-Control" | "X-Pingback" | "X-Requested-With" | "X-Robots-Tag"; - -export type HTTPHeaderName = - | _HTTPHeaderName - | Lowercase<_HTTPHeaderName> - | (string & {}); // eslint-disable-line @typescript-eslint/ban-types - -// prettier-ignore -type ClientHint = "Sec-CH-UA" | "Sec-CH-UA-Arch" | "Sec-CH-UA-Bitness" | "Sec-CH-UA-Full-Version-List" | "Sec-CH-UA-Full-Version" | "Sec-CH-UA-Mobile" | "Sec-CH-UA-Model" | "Sec-CH-UA-Platform" | "Sec-CH-UA-Platform-Version" | "Sec-CH-Prefers-Reduced-Motion" | "Sec-CH-Prefers-Color-Scheme" | "Device-Memory" | "Width" | "Viewport-Width" | "Save-Data" | "Downlink" | "ECT" | "RTT" | AnyString; - -export type TypedHeaders = Partial> & - Partial<{ - host: string; - - location: string; - - referrer: string; - - origin: "null" | AnyString; - - from: string; - - "alt-used": string; - - "content-location": string; - - sourcemap: string; - - "content-length": number; - - "access-control-max-age": number; - - "retry-after": number; - - rtt: number; - - age: number; - - "max-forwards": number; - - downlink: number; - - "device-memory": 0.25 | 0.5 | 1 | 2 | 4 | 8 | AnyNumber; - - accept: MimeType | MimeType[] | `${MimeType};q=${number}`[]; - - "content-type": MimeType; - - "accept-ch": ClientHint | ClientHint[]; - - "keep-alive": `timeout=${number}, max=${number}` | AnyString; - - "access-control-allow-credentials": "true" | AnyString; - - "access-control-allow-headers": "*" | HTTPHeaderName[] | AnyString; - - "access-control-allow-methods": "*" | HTTPMethod[] | AnyString; - - "access-control-allow-origin": "*" | "null" | AnyString; - - "access-control-expose-headers": "*" | HTTPHeaderName[] | AnyString; - - "access-control-request-headers": HTTPHeaderName[] | AnyString; - - "access-control-request-method": HTTPMethod | AnyString; - - "early-data": 1; - - "upgrade-insecure-requests": 1; - - // prettier-ignore - "accept-ranges": "bytes" | "none" | AnyString; - - // prettier-ignore - connection: "keep-alive" | "close" | "upgrade" | AnyString; - - // prettier-ignore - ect: "slow-2g" | "2g" | "3g" | "4g" | AnyString; - - // prettier-ignore - expect: "100-continue" | AnyString; - - // prettier-ignore - "save-data": `on` | `off` | AnyString; - - // prettier-ignore - "sec-ch-prefers-reduced-motion": "no-preference" | "reduce" | AnyString; - - // prettier-ignore - "sec-ch-prefers-reduced-transparency": "no-preference" | "reduce" | AnyString; - - // prettier-ignore - "sec-ch-ua-mobile": `?1` | `?0` | AnyString; - - // prettier-ignore - "origin-agent-cluster": `?1` | `?0` | AnyString; - - // prettier-ignore - "sec-fetch-user": "?1" | AnyString; - - // prettier-ignore - "sec-purpose": "prefetch" | AnyString; - - // prettier-ignore - "x-content-type-options": "nosniff" | AnyString; - - // prettier-ignore - "x-dns-prefetch-control": "on" | "off" | AnyString; - - // prettier-ignore - "x-frame-options": "DENY" | "SAMEORIGIN" | AnyString; - - // prettier-ignore - "sec-ch-ua-arch": "x86" | "ARM" | "[arm64-v8a, armeabi-v7a, armeabi]" | AnyString; - - // prettier-ignore - "sec-fetch-site": "cross-site" | "same-origin" | "same-site" | "none" | AnyString; - - // prettier-ignore - "sec-ch-prefers-color-scheme": "dark" | "light" | AnyString; - - // prettier-ignore - "sec-ch-ua-bitness": "64" | "32" | AnyString; - - // prettier-ignore - "sec-fetch-mode": "cors" | "navigate" | "no-cors" | "same-origin" | "websocket" | AnyString; - - // prettier-ignore - "cross-origin-embedder-policy": "unsafe-none" | "require-corp" | "credentialless" | AnyString; - - // prettier-ignore - "cross-origin-opener-policy": "unsafe-none" | "same-origin-allow-popups" | "same-origin" | AnyString; - - // prettier-ignore - "cross-origin-resource-policy": "same-site" | "same-origin" | "cross-origin" | AnyString; - - // prettier-ignore - "sec-ch-ua-platform": "Android" | "Chrome OS" | "Chromium OS" | "iOS" | "Linux" | "macOS" | "Windows" | "Unknown" | AnyString; - - // prettier-ignore - "referrer-policy": "no-referrer" | "no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "same-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url" | AnyString; - - // prettier-ignore - "sec-fetch-dest": "audio" | "audioworklet" | "document" | "embed" | "empty" | "font" | "frame" | "iframe" | "image" | "manifest" | "object" | "paintworklet" | "report" | "script" | "serviceworker" | "sharedworker" | "style" | "track" | "video" | "worker" | "xslt" | AnyString; - }>; diff --git a/src/types/_mimes.ts b/src/types/_mimes.ts deleted file mode 100644 index 74f73b80..00000000 --- a/src/types/_mimes.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { AnyString } from "./_utils"; - -// prettier-ignore -export type MimeType = 'application/1d-interleaved-parityfec'|'application/3gpdash-qoe-report+xml'|'application/3gppHal+json'|'application/3gppHalForms+json'|'application/3gpp-ims+xml'|'application/A2L'|'application/ace+cbor'|'application/ace+json'|'application/activemessage'|'application/activity+json'|'application/aif+cbor'|'application/aif+json'|'application/alto-cdni+json'|'application/alto-cdnifilter+json'|'application/alto-costmap+json'|'application/alto-costmapfilter+json'|'application/alto-directory+json'|'application/alto-endpointprop+json'|'application/alto-endpointpropparams+json'|'application/alto-endpointcost+json'|'application/alto-endpointcostparams+json'|'application/alto-error+json'|'application/alto-networkmapfilter+json'|'application/alto-networkmap+json'|'application/alto-propmap+json'|'application/alto-propmapparams+json'|'application/alto-tips+json'|'application/alto-tipsparams+json'|'application/alto-updatestreamcontrol+json'|'application/alto-updatestreamparams+json'|'application/AML'|'application/andrew-inset'|'application/applefile'|'application/at+jwt'|'application/ATF'|'application/ATFX'|'application/atom+xml'|'application/atomcat+xml'|'application/atomdeleted+xml'|'application/atomicmail'|'application/atomsvc+xml'|'application/atsc-dwd+xml'|'application/atsc-dynamic-event-message'|'application/atsc-held+xml'|'application/atsc-rdt+json'|'application/atsc-rsat+xml'|'application/ATXML'|'application/auth-policy+xml'|'application/automationml-aml+xml'|'application/automationml-amlx+zip'|'application/bacnet-xdd+zip'|'application/batch-SMTP'|'application/beep+xml'|'application/c2pa'|'application/calendar+json'|'application/calendar+xml'|'application/call-completion'|'application/CALS-1840'|'application/captive+json'|'application/cbor'|'application/cbor-seq'|'application/cccex'|'application/ccmp+xml'|'application/ccxml+xml'|'application/cda+xml'|'application/CDFX+XML'|'application/cdmi-capability'|'application/cdmi-container'|'application/cdmi-domain'|'application/cdmi-object'|'application/cdmi-queue'|'application/cdni'|'application/CEA'|'application/cea-2018+xml'|'application/cellml+xml'|'application/cfw'|'application/cid-edhoc+cbor-seq'|'application/city+json'|'application/clr'|'application/clue_info+xml'|'application/clue+xml'|'application/cms'|'application/cnrp+xml'|'application/coap-group+json'|'application/coap-payload'|'application/commonground'|'application/concise-problem-details+cbor'|'application/conference-info+xml'|'application/cpl+xml'|'application/cose'|'application/cose-key'|'application/cose-key-set'|'application/cose-x509'|'application/csrattrs'|'application/csta+xml'|'application/CSTAdata+xml'|'application/csvm+json'|'application/cwl'|'application/cwl+json'|'application/cwt'|'application/cybercash'|'application/dash+xml'|'application/dash-patch+xml'|'application/dashdelta'|'application/davmount+xml'|'application/dca-rft'|'application/DCD'|'application/dec-dx'|'application/dialog-info+xml'|'application/dicom'|'application/dicom+json'|'application/dicom+xml'|'application/DII'|'application/DIT'|'application/dns'|'application/dns+json'|'application/dns-message'|'application/dots+cbor'|'application/dpop+jwt'|'application/dskpp+xml'|'application/dssc+der'|'application/dssc+xml'|'application/dvcs'|'application/ecmascript'|'application/edhoc+cbor-seq'|'application/EDI-consent'|'application/EDIFACT'|'application/EDI-X12'|'application/efi'|'application/elm+json'|'application/elm+xml'|'application/EmergencyCallData.cap+xml'|'application/EmergencyCallData.Comment+xml'|'application/EmergencyCallData.Control+xml'|'application/EmergencyCallData.DeviceInfo+xml'|'application/EmergencyCallData.eCall.MSD'|'application/EmergencyCallData.LegacyESN+json'|'application/EmergencyCallData.ProviderInfo+xml'|'application/EmergencyCallData.ServiceInfo+xml'|'application/EmergencyCallData.SubscriberInfo+xml'|'application/EmergencyCallData.VEDS+xml'|'application/emma+xml'|'application/emotionml+xml'|'application/encaprtp'|'application/epp+xml'|'application/epub+zip'|'application/eshop'|'application/example'|'application/exi'|'application/expect-ct-report+json'|'application/express'|'application/fastinfoset'|'application/fastsoap'|'application/fdf'|'application/fdt+xml'|'application/fhir+json'|'application/fhir+xml'|'application/fits'|'application/flexfec'|'application/font-sfnt'|'application/font-tdpfr'|'application/font-woff'|'application/framework-attributes+xml'|'application/geo+json'|'application/geo+json-seq'|'application/geopackage+sqlite3'|'application/geoxacml+json'|'application/geoxacml+xml'|'application/gltf-buffer'|'application/gml+xml'|'application/gzip'|'application/H224'|'application/held+xml'|'application/hl7v2+xml'|'application/http'|'application/hyperstudio'|'application/ibe-key-request+xml'|'application/ibe-pkg-reply+xml'|'application/ibe-pp-data'|'application/iges'|'application/im-iscomposing+xml'|'application/index'|'application/index.cmd'|'application/index.obj'|'application/index.response'|'application/index.vnd'|'application/inkml+xml'|'application/IOTP'|'application/ipfix'|'application/ipp'|'application/ISUP'|'application/its+xml'|'application/java-archive'|'application/javascript'|'application/jf2feed+json'|'application/jose'|'application/jose+json'|'application/jrd+json'|'application/jscalendar+json'|'application/jscontact+json'|'application/json'|'application/json-patch+json'|'application/json-seq'|'application/jsonpath'|'application/jwk+json'|'application/jwk-set+json'|'application/jwt'|'application/kpml-request+xml'|'application/kpml-response+xml'|'application/ld+json'|'application/lgr+xml'|'application/link-format'|'application/linkset'|'application/linkset+json'|'application/load-control+xml'|'application/logout+jwt'|'application/lost+xml'|'application/lostsync+xml'|'application/lpf+zip'|'application/LXF'|'application/mac-binhex40'|'application/macwriteii'|'application/mads+xml'|'application/manifest+json'|'application/marc'|'application/marcxml+xml'|'application/mathematica'|'application/mathml+xml'|'application/mathml-content+xml'|'application/mathml-presentation+xml'|'application/mbms-associated-procedure-description+xml'|'application/mbms-deregister+xml'|'application/mbms-envelope+xml'|'application/mbms-msk-response+xml'|'application/mbms-msk+xml'|'application/mbms-protection-description+xml'|'application/mbms-reception-report+xml'|'application/mbms-register-response+xml'|'application/mbms-register+xml'|'application/mbms-schedule+xml'|'application/mbms-user-service-description+xml'|'application/mbox'|'application/media_control+xml'|'application/media-policy-dataset+xml'|'application/mediaservercontrol+xml'|'application/merge-patch+json'|'application/metalink4+xml'|'application/mets+xml'|'application/MF4'|'application/mikey'|'application/mipc'|'application/missing-blocks+cbor-seq'|'application/mmt-aei+xml'|'application/mmt-usd+xml'|'application/mods+xml'|'application/moss-keys'|'application/moss-signature'|'application/mosskey-data'|'application/mosskey-request'|'application/mp21'|'application/mp4'|'application/mpeg4-generic'|'application/mpeg4-iod'|'application/mpeg4-iod-xmt'|'application/mrb-consumer+xml'|'application/mrb-publish+xml'|'application/msc-ivr+xml'|'application/msc-mixer+xml'|'application/msword'|'application/mud+json'|'application/multipart-core'|'application/mxf'|'application/n-quads'|'application/n-triples'|'application/nasdata'|'application/news-checkgroups'|'application/news-groupinfo'|'application/news-transmission'|'application/nlsml+xml'|'application/node'|'application/nss'|'application/oauth-authz-req+jwt'|'application/oblivious-dns-message'|'application/ocsp-request'|'application/ocsp-response'|'application/octet-stream'|'application/ODA'|'application/odm+xml'|'application/ODX'|'application/oebps-package+xml'|'application/ogg'|'application/ohttp-keys'|'application/opc-nodeset+xml'|'application/oscore'|'application/oxps'|'application/p21'|'application/p21+zip'|'application/p2p-overlay+xml'|'application/parityfec'|'application/passport'|'application/patch-ops-error+xml'|'application/pdf'|'application/PDX'|'application/pem-certificate-chain'|'application/pgp-encrypted'|'application/pgp-keys'|'application/pgp-signature'|'application/pidf-diff+xml'|'application/pidf+xml'|'application/pkcs10'|'application/pkcs7-mime'|'application/pkcs7-signature'|'application/pkcs8'|'application/pkcs8-encrypted'|'application/pkcs12'|'application/pkix-attr-cert'|'application/pkix-cert'|'application/pkix-crl'|'application/pkix-pkipath'|'application/pkixcmp'|'application/pls+xml'|'application/poc-settings+xml'|'application/postscript'|'application/ppsp-tracker+json'|'application/private-token-issuer-directory'|'application/private-token-request'|'application/private-token-response'|'application/problem+json'|'application/problem+xml'|'application/provenance+xml'|'application/prs.alvestrand.titrax-sheet'|'application/prs.cww'|'application/prs.cyn'|'application/prs.hpub+zip'|'application/prs.implied-document+xml'|'application/prs.implied-executable'|'application/prs.implied-object+json'|'application/prs.implied-object+json-seq'|'application/prs.implied-object+yaml'|'application/prs.implied-structure'|'application/prs.nprend'|'application/prs.plucker'|'application/prs.rdf-xml-crypt'|'application/prs.vcfbzip2'|'application/prs.xsf+xml'|'application/pskc+xml'|'application/pvd+json'|'application/rdf+xml'|'application/route-apd+xml'|'application/route-s-tsid+xml'|'application/route-usd+xml'|'application/QSIG'|'application/raptorfec'|'application/rdap+json'|'application/reginfo+xml'|'application/relax-ng-compact-syntax'|'application/remote-printing'|'application/reputon+json'|'application/resource-lists-diff+xml'|'application/resource-lists+xml'|'application/rfc+xml'|'application/riscos'|'application/rlmi+xml'|'application/rls-services+xml'|'application/rpki-checklist'|'application/rpki-ghostbusters'|'application/rpki-manifest'|'application/rpki-publication'|'application/rpki-roa'|'application/rpki-updown'|'application/rtf'|'application/rtploopback'|'application/rtx'|'application/samlassertion+xml'|'application/samlmetadata+xml'|'application/sarif-external-properties+json'|'application/sarif+json'|'application/sbe'|'application/sbml+xml'|'application/scaip+xml'|'application/scim+json'|'application/scvp-cv-request'|'application/scvp-cv-response'|'application/scvp-vp-request'|'application/scvp-vp-response'|'application/sdp'|'application/secevent+jwt'|'application/senml-etch+cbor'|'application/senml-etch+json'|'application/senml-exi'|'application/senml+cbor'|'application/senml+json'|'application/senml+xml'|'application/sensml-exi'|'application/sensml+cbor'|'application/sensml+json'|'application/sensml+xml'|'application/sep-exi'|'application/sep+xml'|'application/session-info'|'application/set-payment'|'application/set-payment-initiation'|'application/set-registration'|'application/set-registration-initiation'|'application/SGML'|'application/sgml-open-catalog'|'application/shf+xml'|'application/sieve'|'application/simple-filter+xml'|'application/simple-message-summary'|'application/simpleSymbolContainer'|'application/sipc'|'application/slate'|'application/smil'|'application/smil+xml'|'application/smpte336m'|'application/soap+fastinfoset'|'application/soap+xml'|'application/sparql-query'|'application/spdx+json'|'application/sparql-results+xml'|'application/spirits-event+xml'|'application/sql'|'application/srgs'|'application/srgs+xml'|'application/sru+xml'|'application/ssml+xml'|'application/stix+json'|'application/swid+cbor'|'application/swid+xml'|'application/tamp-apex-update'|'application/tamp-apex-update-confirm'|'application/tamp-community-update'|'application/tamp-community-update-confirm'|'application/tamp-error'|'application/tamp-sequence-adjust'|'application/tamp-sequence-adjust-confirm'|'application/tamp-status-query'|'application/tamp-status-response'|'application/tamp-update'|'application/tamp-update-confirm'|'application/taxii+json'|'application/td+json'|'application/tei+xml'|'application/TETRA_ISI'|'application/thraud+xml'|'application/timestamp-query'|'application/timestamp-reply'|'application/timestamped-data'|'application/tlsrpt+gzip'|'application/tlsrpt+json'|'application/tm+json'|'application/tnauthlist'|'application/token-introspection+jwt'|'application/trickle-ice-sdpfrag'|'application/trig'|'application/ttml+xml'|'application/tve-trigger'|'application/tzif'|'application/tzif-leap'|'application/ulpfec'|'application/urc-grpsheet+xml'|'application/urc-ressheet+xml'|'application/urc-targetdesc+xml'|'application/urc-uisocketdesc+xml'|'application/vcard+json'|'application/vcard+xml'|'application/vemmi'|'application/vnd.1000minds.decision-model+xml'|'application/vnd.1ob'|'application/vnd.3gpp.5gnas'|'application/vnd.3gpp.access-transfer-events+xml'|'application/vnd.3gpp.bsf+xml'|'application/vnd.3gpp.crs+xml'|'application/vnd.3gpp.current-location-discovery+xml'|'application/vnd.3gpp.GMOP+xml'|'application/vnd.3gpp.gtpc'|'application/vnd.3gpp.interworking-data'|'application/vnd.3gpp.lpp'|'application/vnd.3gpp.mc-signalling-ear'|'application/vnd.3gpp.mcdata-affiliation-command+xml'|'application/vnd.3gpp.mcdata-info+xml'|'application/vnd.3gpp.mcdata-msgstore-ctrl-request+xml'|'application/vnd.3gpp.mcdata-payload'|'application/vnd.3gpp.mcdata-regroup+xml'|'application/vnd.3gpp.mcdata-service-config+xml'|'application/vnd.3gpp.mcdata-signalling'|'application/vnd.3gpp.mcdata-ue-config+xml'|'application/vnd.3gpp.mcdata-user-profile+xml'|'application/vnd.3gpp.mcptt-affiliation-command+xml'|'application/vnd.3gpp.mcptt-floor-request+xml'|'application/vnd.3gpp.mcptt-info+xml'|'application/vnd.3gpp.mcptt-location-info+xml'|'application/vnd.3gpp.mcptt-mbms-usage-info+xml'|'application/vnd.3gpp.mcptt-regroup+xml'|'application/vnd.3gpp.mcptt-service-config+xml'|'application/vnd.3gpp.mcptt-signed+xml'|'application/vnd.3gpp.mcptt-ue-config+xml'|'application/vnd.3gpp.mcptt-ue-init-config+xml'|'application/vnd.3gpp.mcptt-user-profile+xml'|'application/vnd.3gpp.mcvideo-affiliation-command+xml'|'application/vnd.3gpp.mcvideo-affiliation-info+xml'|'application/vnd.3gpp.mcvideo-info+xml'|'application/vnd.3gpp.mcvideo-location-info+xml'|'application/vnd.3gpp.mcvideo-mbms-usage-info+xml'|'application/vnd.3gpp.mcvideo-regroup+xml'|'application/vnd.3gpp.mcvideo-service-config+xml'|'application/vnd.3gpp.mcvideo-transmission-request+xml'|'application/vnd.3gpp.mcvideo-ue-config+xml'|'application/vnd.3gpp.mcvideo-user-profile+xml'|'application/vnd.3gpp.mid-call+xml'|'application/vnd.3gpp.ngap'|'application/vnd.3gpp.pfcp'|'application/vnd.3gpp.pic-bw-large'|'application/vnd.3gpp.pic-bw-small'|'application/vnd.3gpp.pic-bw-var'|'application/vnd.3gpp-prose-pc3a+xml'|'application/vnd.3gpp-prose-pc3ach+xml'|'application/vnd.3gpp-prose-pc3ch+xml'|'application/vnd.3gpp-prose-pc8+xml'|'application/vnd.3gpp-prose+xml'|'application/vnd.3gpp.s1ap'|'application/vnd.3gpp.seal-group-doc+xml'|'application/vnd.3gpp.seal-info+xml'|'application/vnd.3gpp.seal-location-info+xml'|'application/vnd.3gpp.seal-mbms-usage-info+xml'|'application/vnd.3gpp.seal-network-QoS-management-info+xml'|'application/vnd.3gpp.seal-ue-config-info+xml'|'application/vnd.3gpp.seal-unicast-info+xml'|'application/vnd.3gpp.seal-user-profile-info+xml'|'application/vnd.3gpp.sms'|'application/vnd.3gpp.sms+xml'|'application/vnd.3gpp.srvcc-ext+xml'|'application/vnd.3gpp.SRVCC-info+xml'|'application/vnd.3gpp.state-and-event-info+xml'|'application/vnd.3gpp.ussd+xml'|'application/vnd.3gpp.vae-info+xml'|'application/vnd.3gpp-v2x-local-service-information'|'application/vnd.3gpp2.bcmcsinfo+xml'|'application/vnd.3gpp2.sms'|'application/vnd.3gpp2.tcap'|'application/vnd.3gpp.v2x'|'application/vnd.3lightssoftware.imagescal'|'application/vnd.3M.Post-it-Notes'|'application/vnd.accpac.simply.aso'|'application/vnd.accpac.simply.imp'|'application/vnd.acm.addressxfer+json'|'application/vnd.acm.chatbot+json'|'application/vnd.acucobol'|'application/vnd.acucorp'|'application/vnd.adobe.flash.movie'|'application/vnd.adobe.formscentral.fcdt'|'application/vnd.adobe.fxp'|'application/vnd.adobe.partial-upload'|'application/vnd.adobe.xdp+xml'|'application/vnd.aether.imp'|'application/vnd.afpc.afplinedata'|'application/vnd.afpc.afplinedata-pagedef'|'application/vnd.afpc.cmoca-cmresource'|'application/vnd.afpc.foca-charset'|'application/vnd.afpc.foca-codedfont'|'application/vnd.afpc.foca-codepage'|'application/vnd.afpc.modca'|'application/vnd.afpc.modca-cmtable'|'application/vnd.afpc.modca-formdef'|'application/vnd.afpc.modca-mediummap'|'application/vnd.afpc.modca-objectcontainer'|'application/vnd.afpc.modca-overlay'|'application/vnd.afpc.modca-pagesegment'|'application/vnd.age'|'application/vnd.ah-barcode'|'application/vnd.ahead.space'|'application/vnd.airzip.filesecure.azf'|'application/vnd.airzip.filesecure.azs'|'application/vnd.amadeus+json'|'application/vnd.amazon.mobi8-ebook'|'application/vnd.americandynamics.acc'|'application/vnd.amiga.ami'|'application/vnd.amundsen.maze+xml'|'application/vnd.android.ota'|'application/vnd.anki'|'application/vnd.anser-web-certificate-issue-initiation'|'application/vnd.antix.game-component'|'application/vnd.apache.arrow.file'|'application/vnd.apache.arrow.stream'|'application/vnd.apache.thrift.binary'|'application/vnd.apache.thrift.compact'|'application/vnd.apache.thrift.json'|'application/vnd.apexlang'|'application/vnd.api+json'|'application/vnd.aplextor.warrp+json'|'application/vnd.apothekende.reservation+json'|'application/vnd.apple.installer+xml'|'application/vnd.apple.keynote'|'application/vnd.apple.mpegurl'|'application/vnd.apple.numbers'|'application/vnd.apple.pages'|'application/vnd.arastra.swi'|'application/vnd.aristanetworks.swi'|'application/vnd.artisan+json'|'application/vnd.artsquare'|'application/vnd.astraea-software.iota'|'application/vnd.audiograph'|'application/vnd.autopackage'|'application/vnd.avalon+json'|'application/vnd.avistar+xml'|'application/vnd.balsamiq.bmml+xml'|'application/vnd.banana-accounting'|'application/vnd.bbf.usp.error'|'application/vnd.bbf.usp.msg'|'application/vnd.bbf.usp.msg+json'|'application/vnd.balsamiq.bmpr'|'application/vnd.bekitzur-stech+json'|'application/vnd.belightsoft.lhzd+zip'|'application/vnd.belightsoft.lhzl+zip'|'application/vnd.bint.med-content'|'application/vnd.biopax.rdf+xml'|'application/vnd.blink-idb-value-wrapper'|'application/vnd.blueice.multipass'|'application/vnd.bluetooth.ep.oob'|'application/vnd.bluetooth.le.oob'|'application/vnd.bmi'|'application/vnd.bpf'|'application/vnd.bpf3'|'application/vnd.businessobjects'|'application/vnd.byu.uapi+json'|'application/vnd.bzip3'|'application/vnd.cab-jscript'|'application/vnd.canon-cpdl'|'application/vnd.canon-lips'|'application/vnd.capasystems-pg+json'|'application/vnd.cendio.thinlinc.clientconf'|'application/vnd.century-systems.tcp_stream'|'application/vnd.chemdraw+xml'|'application/vnd.chess-pgn'|'application/vnd.chipnuts.karaoke-mmd'|'application/vnd.ciedi'|'application/vnd.cinderella'|'application/vnd.cirpack.isdn-ext'|'application/vnd.citationstyles.style+xml'|'application/vnd.claymore'|'application/vnd.cloanto.rp9'|'application/vnd.clonk.c4group'|'application/vnd.cluetrust.cartomobile-config'|'application/vnd.cluetrust.cartomobile-config-pkg'|'application/vnd.cncf.helm.chart.content.v1.tar+gzip'|'application/vnd.cncf.helm.chart.provenance.v1.prov'|'application/vnd.cncf.helm.config.v1+json'|'application/vnd.coffeescript'|'application/vnd.collabio.xodocuments.document'|'application/vnd.collabio.xodocuments.document-template'|'application/vnd.collabio.xodocuments.presentation'|'application/vnd.collabio.xodocuments.presentation-template'|'application/vnd.collabio.xodocuments.spreadsheet'|'application/vnd.collabio.xodocuments.spreadsheet-template'|'application/vnd.collection.doc+json'|'application/vnd.collection+json'|'application/vnd.collection.next+json'|'application/vnd.comicbook-rar'|'application/vnd.comicbook+zip'|'application/vnd.commerce-battelle'|'application/vnd.commonspace'|'application/vnd.coreos.ignition+json'|'application/vnd.cosmocaller'|'application/vnd.contact.cmsg'|'application/vnd.crick.clicker'|'application/vnd.crick.clicker.keyboard'|'application/vnd.crick.clicker.palette'|'application/vnd.crick.clicker.template'|'application/vnd.crick.clicker.wordbank'|'application/vnd.criticaltools.wbs+xml'|'application/vnd.cryptii.pipe+json'|'application/vnd.crypto-shade-file'|'application/vnd.cryptomator.encrypted'|'application/vnd.cryptomator.vault'|'application/vnd.ctc-posml'|'application/vnd.ctct.ws+xml'|'application/vnd.cups-pdf'|'application/vnd.cups-postscript'|'application/vnd.cups-ppd'|'application/vnd.cups-raster'|'application/vnd.cups-raw'|'application/vnd.curl'|'application/vnd.cyan.dean.root+xml'|'application/vnd.cybank'|'application/vnd.cyclonedx+json'|'application/vnd.cyclonedx+xml'|'application/vnd.d2l.coursepackage1p0+zip'|'application/vnd.d3m-dataset'|'application/vnd.d3m-problem'|'application/vnd.dart'|'application/vnd.data-vision.rdz'|'application/vnd.datalog'|'application/vnd.datapackage+json'|'application/vnd.dataresource+json'|'application/vnd.dbf'|'application/vnd.debian.binary-package'|'application/vnd.dece.data'|'application/vnd.dece.ttml+xml'|'application/vnd.dece.unspecified'|'application/vnd.dece.zip'|'application/vnd.denovo.fcselayout-link'|'application/vnd.desmume.movie'|'application/vnd.dir-bi.plate-dl-nosuffix'|'application/vnd.dm.delegation+xml'|'application/vnd.dna'|'application/vnd.document+json'|'application/vnd.dolby.mobile.1'|'application/vnd.dolby.mobile.2'|'application/vnd.doremir.scorecloud-binary-document'|'application/vnd.dpgraph'|'application/vnd.dreamfactory'|'application/vnd.drive+json'|'application/vnd.dtg.local'|'application/vnd.dtg.local.flash'|'application/vnd.dtg.local.html'|'application/vnd.dvb.ait'|'application/vnd.dvb.dvbisl+xml'|'application/vnd.dvb.dvbj'|'application/vnd.dvb.esgcontainer'|'application/vnd.dvb.ipdcdftnotifaccess'|'application/vnd.dvb.ipdcesgaccess'|'application/vnd.dvb.ipdcesgaccess2'|'application/vnd.dvb.ipdcesgpdd'|'application/vnd.dvb.ipdcroaming'|'application/vnd.dvb.iptv.alfec-base'|'application/vnd.dvb.iptv.alfec-enhancement'|'application/vnd.dvb.notif-aggregate-root+xml'|'application/vnd.dvb.notif-container+xml'|'application/vnd.dvb.notif-generic+xml'|'application/vnd.dvb.notif-ia-msglist+xml'|'application/vnd.dvb.notif-ia-registration-request+xml'|'application/vnd.dvb.notif-ia-registration-response+xml'|'application/vnd.dvb.notif-init+xml'|'application/vnd.dvb.pfr'|'application/vnd.dvb.service'|'application/vnd.dxr'|'application/vnd.dynageo'|'application/vnd.dzr'|'application/vnd.easykaraoke.cdgdownload'|'application/vnd.ecip.rlp'|'application/vnd.ecdis-update'|'application/vnd.eclipse.ditto+json'|'application/vnd.ecowin.chart'|'application/vnd.ecowin.filerequest'|'application/vnd.ecowin.fileupdate'|'application/vnd.ecowin.series'|'application/vnd.ecowin.seriesrequest'|'application/vnd.ecowin.seriesupdate'|'application/vnd.efi.img'|'application/vnd.efi.iso'|'application/vnd.eln+zip'|'application/vnd.emclient.accessrequest+xml'|'application/vnd.enliven'|'application/vnd.enphase.envoy'|'application/vnd.eprints.data+xml'|'application/vnd.epson.esf'|'application/vnd.epson.msf'|'application/vnd.epson.quickanime'|'application/vnd.epson.salt'|'application/vnd.epson.ssf'|'application/vnd.ericsson.quickcall'|'application/vnd.erofs'|'application/vnd.espass-espass+zip'|'application/vnd.eszigno3+xml'|'application/vnd.etsi.aoc+xml'|'application/vnd.etsi.asic-s+zip'|'application/vnd.etsi.asic-e+zip'|'application/vnd.etsi.cug+xml'|'application/vnd.etsi.iptvcommand+xml'|'application/vnd.etsi.iptvdiscovery+xml'|'application/vnd.etsi.iptvprofile+xml'|'application/vnd.etsi.iptvsad-bc+xml'|'application/vnd.etsi.iptvsad-cod+xml'|'application/vnd.etsi.iptvsad-npvr+xml'|'application/vnd.etsi.iptvservice+xml'|'application/vnd.etsi.iptvsync+xml'|'application/vnd.etsi.iptvueprofile+xml'|'application/vnd.etsi.mcid+xml'|'application/vnd.etsi.mheg5'|'application/vnd.etsi.overload-control-policy-dataset+xml'|'application/vnd.etsi.pstn+xml'|'application/vnd.etsi.sci+xml'|'application/vnd.etsi.simservs+xml'|'application/vnd.etsi.timestamp-token'|'application/vnd.etsi.tsl+xml'|'application/vnd.etsi.tsl.der'|'application/vnd.eu.kasparian.car+json'|'application/vnd.eudora.data'|'application/vnd.evolv.ecig.profile'|'application/vnd.evolv.ecig.settings'|'application/vnd.evolv.ecig.theme'|'application/vnd.exstream-empower+zip'|'application/vnd.exstream-package'|'application/vnd.ezpix-album'|'application/vnd.ezpix-package'|'application/vnd.f-secure.mobile'|'application/vnd.fastcopy-disk-image'|'application/vnd.familysearch.gedcom+zip'|'application/vnd.fdsn.mseed'|'application/vnd.fdsn.seed'|'application/vnd.ffsns'|'application/vnd.ficlab.flb+zip'|'application/vnd.filmit.zfc'|'application/vnd.fints'|'application/vnd.firemonkeys.cloudcell'|'application/vnd.FloGraphIt'|'application/vnd.fluxtime.clip'|'application/vnd.font-fontforge-sfd'|'application/vnd.framemaker'|'application/vnd.freelog.comic'|'application/vnd.frogans.fnc'|'application/vnd.frogans.ltf'|'application/vnd.fsc.weblaunch'|'application/vnd.fujifilm.fb.docuworks'|'application/vnd.fujifilm.fb.docuworks.binder'|'application/vnd.fujifilm.fb.docuworks.container'|'application/vnd.fujifilm.fb.jfi+xml'|'application/vnd.fujitsu.oasys'|'application/vnd.fujitsu.oasys2'|'application/vnd.fujitsu.oasys3'|'application/vnd.fujitsu.oasysgp'|'application/vnd.fujitsu.oasysprs'|'application/vnd.fujixerox.ART4'|'application/vnd.fujixerox.ART-EX'|'application/vnd.fujixerox.ddd'|'application/vnd.fujixerox.docuworks'|'application/vnd.fujixerox.docuworks.binder'|'application/vnd.fujixerox.docuworks.container'|'application/vnd.fujixerox.HBPL'|'application/vnd.fut-misnet'|'application/vnd.futoin+cbor'|'application/vnd.futoin+json'|'application/vnd.fuzzysheet'|'application/vnd.genomatix.tuxedo'|'application/vnd.genozip'|'application/vnd.gentics.grd+json'|'application/vnd.gentoo.catmetadata+xml'|'application/vnd.gentoo.ebuild'|'application/vnd.gentoo.eclass'|'application/vnd.gentoo.gpkg'|'application/vnd.gentoo.manifest'|'application/vnd.gentoo.xpak'|'application/vnd.gentoo.pkgmetadata+xml'|'application/vnd.geo+json'|'application/vnd.geocube+xml'|'application/vnd.geogebra.file'|'application/vnd.geogebra.slides'|'application/vnd.geogebra.tool'|'application/vnd.geometry-explorer'|'application/vnd.geonext'|'application/vnd.geoplan'|'application/vnd.geospace'|'application/vnd.gerber'|'application/vnd.globalplatform.card-content-mgt'|'application/vnd.globalplatform.card-content-mgt-response'|'application/vnd.gmx'|'application/vnd.gnu.taler.exchange+json'|'application/vnd.gnu.taler.merchant+json'|'application/vnd.google-earth.kml+xml'|'application/vnd.google-earth.kmz'|'application/vnd.gov.sk.e-form+xml'|'application/vnd.gov.sk.e-form+zip'|'application/vnd.gov.sk.xmldatacontainer+xml'|'application/vnd.gpxsee.map+xml'|'application/vnd.grafeq'|'application/vnd.gridmp'|'application/vnd.groove-account'|'application/vnd.groove-help'|'application/vnd.groove-identity-message'|'application/vnd.groove-injector'|'application/vnd.groove-tool-message'|'application/vnd.groove-tool-template'|'application/vnd.groove-vcard'|'application/vnd.hal+json'|'application/vnd.hal+xml'|'application/vnd.HandHeld-Entertainment+xml'|'application/vnd.hbci'|'application/vnd.hc+json'|'application/vnd.hcl-bireports'|'application/vnd.hdt'|'application/vnd.heroku+json'|'application/vnd.hhe.lesson-player'|'application/vnd.hp-HPGL'|'application/vnd.hp-hpid'|'application/vnd.hp-hps'|'application/vnd.hp-jlyt'|'application/vnd.hp-PCL'|'application/vnd.hp-PCLXL'|'application/vnd.hsl'|'application/vnd.httphone'|'application/vnd.hydrostatix.sof-data'|'application/vnd.hyper-item+json'|'application/vnd.hyper+json'|'application/vnd.hyperdrive+json'|'application/vnd.hzn-3d-crossword'|'application/vnd.ibm.afplinedata'|'application/vnd.ibm.electronic-media'|'application/vnd.ibm.MiniPay'|'application/vnd.ibm.modcap'|'application/vnd.ibm.rights-management'|'application/vnd.ibm.secure-container'|'application/vnd.iccprofile'|'application/vnd.ieee.1905'|'application/vnd.igloader'|'application/vnd.imagemeter.folder+zip'|'application/vnd.imagemeter.image+zip'|'application/vnd.immervision-ivp'|'application/vnd.immervision-ivu'|'application/vnd.ims.imsccv1p1'|'application/vnd.ims.imsccv1p2'|'application/vnd.ims.imsccv1p3'|'application/vnd.ims.lis.v2.result+json'|'application/vnd.ims.lti.v2.toolconsumerprofile+json'|'application/vnd.ims.lti.v2.toolproxy.id+json'|'application/vnd.ims.lti.v2.toolproxy+json'|'application/vnd.ims.lti.v2.toolsettings+json'|'application/vnd.ims.lti.v2.toolsettings.simple+json'|'application/vnd.informedcontrol.rms+xml'|'application/vnd.infotech.project'|'application/vnd.infotech.project+xml'|'application/vnd.informix-visionary'|'application/vnd.innopath.wamp.notification'|'application/vnd.insors.igm'|'application/vnd.intercon.formnet'|'application/vnd.intergeo'|'application/vnd.intertrust.digibox'|'application/vnd.intertrust.nncp'|'application/vnd.intu.qbo'|'application/vnd.intu.qfx'|'application/vnd.ipfs.ipns-record'|'application/vnd.ipld.car'|'application/vnd.ipld.dag-cbor'|'application/vnd.ipld.dag-json'|'application/vnd.ipld.raw'|'application/vnd.iptc.g2.catalogitem+xml'|'application/vnd.iptc.g2.conceptitem+xml'|'application/vnd.iptc.g2.knowledgeitem+xml'|'application/vnd.iptc.g2.newsitem+xml'|'application/vnd.iptc.g2.newsmessage+xml'|'application/vnd.iptc.g2.packageitem+xml'|'application/vnd.iptc.g2.planningitem+xml'|'application/vnd.ipunplugged.rcprofile'|'application/vnd.irepository.package+xml'|'application/vnd.is-xpr'|'application/vnd.isac.fcs'|'application/vnd.jam'|'application/vnd.iso11783-10+zip'|'application/vnd.japannet-directory-service'|'application/vnd.japannet-jpnstore-wakeup'|'application/vnd.japannet-payment-wakeup'|'application/vnd.japannet-registration'|'application/vnd.japannet-registration-wakeup'|'application/vnd.japannet-setstore-wakeup'|'application/vnd.japannet-verification'|'application/vnd.japannet-verification-wakeup'|'application/vnd.jcp.javame.midlet-rms'|'application/vnd.jisp'|'application/vnd.joost.joda-archive'|'application/vnd.jsk.isdn-ngn'|'application/vnd.kahootz'|'application/vnd.kde.karbon'|'application/vnd.kde.kchart'|'application/vnd.kde.kformula'|'application/vnd.kde.kivio'|'application/vnd.kde.kontour'|'application/vnd.kde.kpresenter'|'application/vnd.kde.kspread'|'application/vnd.kde.kword'|'application/vnd.kenameaapp'|'application/vnd.kidspiration'|'application/vnd.Kinar'|'application/vnd.koan'|'application/vnd.kodak-descriptor'|'application/vnd.las'|'application/vnd.las.las+json'|'application/vnd.las.las+xml'|'application/vnd.laszip'|'application/vnd.ldev.productlicensing'|'application/vnd.leap+json'|'application/vnd.liberty-request+xml'|'application/vnd.llamagraphics.life-balance.desktop'|'application/vnd.llamagraphics.life-balance.exchange+xml'|'application/vnd.logipipe.circuit+zip'|'application/vnd.loom'|'application/vnd.lotus-1-2-3'|'application/vnd.lotus-approach'|'application/vnd.lotus-freelance'|'application/vnd.lotus-notes'|'application/vnd.lotus-organizer'|'application/vnd.lotus-screencam'|'application/vnd.lotus-wordpro'|'application/vnd.macports.portpkg'|'application/vnd.mapbox-vector-tile'|'application/vnd.marlin.drm.actiontoken+xml'|'application/vnd.marlin.drm.conftoken+xml'|'application/vnd.marlin.drm.license+xml'|'application/vnd.marlin.drm.mdcf'|'application/vnd.mason+json'|'application/vnd.maxar.archive.3tz+zip'|'application/vnd.maxmind.maxmind-db'|'application/vnd.mcd'|'application/vnd.mdl'|'application/vnd.mdl-mbsdf'|'application/vnd.medcalcdata'|'application/vnd.mediastation.cdkey'|'application/vnd.medicalholodeck.recordxr'|'application/vnd.meridian-slingshot'|'application/vnd.mermaid'|'application/vnd.MFER'|'application/vnd.mfmp'|'application/vnd.micro+json'|'application/vnd.micrografx.flo'|'application/vnd.micrografx.igx'|'application/vnd.microsoft.portable-executable'|'application/vnd.microsoft.windows.thumbnail-cache'|'application/vnd.miele+json'|'application/vnd.mif'|'application/vnd.minisoft-hp3000-save'|'application/vnd.mitsubishi.misty-guard.trustweb'|'application/vnd.Mobius.DAF'|'application/vnd.Mobius.DIS'|'application/vnd.Mobius.MBK'|'application/vnd.Mobius.MQY'|'application/vnd.Mobius.MSL'|'application/vnd.Mobius.PLC'|'application/vnd.Mobius.TXF'|'application/vnd.modl'|'application/vnd.mophun.application'|'application/vnd.mophun.certificate'|'application/vnd.motorola.flexsuite'|'application/vnd.motorola.flexsuite.adsi'|'application/vnd.motorola.flexsuite.fis'|'application/vnd.motorola.flexsuite.gotap'|'application/vnd.motorola.flexsuite.kmr'|'application/vnd.motorola.flexsuite.ttc'|'application/vnd.motorola.flexsuite.wem'|'application/vnd.motorola.iprm'|'application/vnd.mozilla.xul+xml'|'application/vnd.ms-artgalry'|'application/vnd.ms-asf'|'application/vnd.ms-cab-compressed'|'application/vnd.ms-3mfdocument'|'application/vnd.ms-excel'|'application/vnd.ms-excel.addin.macroEnabled.12'|'application/vnd.ms-excel.sheet.binary.macroEnabled.12'|'application/vnd.ms-excel.sheet.macroEnabled.12'|'application/vnd.ms-excel.template.macroEnabled.12'|'application/vnd.ms-fontobject'|'application/vnd.ms-htmlhelp'|'application/vnd.ms-ims'|'application/vnd.ms-lrm'|'application/vnd.ms-office.activeX+xml'|'application/vnd.ms-officetheme'|'application/vnd.ms-playready.initiator+xml'|'application/vnd.ms-powerpoint'|'application/vnd.ms-powerpoint.addin.macroEnabled.12'|'application/vnd.ms-powerpoint.presentation.macroEnabled.12'|'application/vnd.ms-powerpoint.slide.macroEnabled.12'|'application/vnd.ms-powerpoint.slideshow.macroEnabled.12'|'application/vnd.ms-powerpoint.template.macroEnabled.12'|'application/vnd.ms-PrintDeviceCapabilities+xml'|'application/vnd.ms-PrintSchemaTicket+xml'|'application/vnd.ms-project'|'application/vnd.ms-tnef'|'application/vnd.ms-windows.devicepairing'|'application/vnd.ms-windows.nwprinting.oob'|'application/vnd.ms-windows.printerpairing'|'application/vnd.ms-windows.wsd.oob'|'application/vnd.ms-wmdrm.lic-chlg-req'|'application/vnd.ms-wmdrm.lic-resp'|'application/vnd.ms-wmdrm.meter-chlg-req'|'application/vnd.ms-wmdrm.meter-resp'|'application/vnd.ms-word.document.macroEnabled.12'|'application/vnd.ms-word.template.macroEnabled.12'|'application/vnd.ms-works'|'application/vnd.ms-wpl'|'application/vnd.ms-xpsdocument'|'application/vnd.msa-disk-image'|'application/vnd.mseq'|'application/vnd.msign'|'application/vnd.multiad.creator'|'application/vnd.multiad.creator.cif'|'application/vnd.musician'|'application/vnd.music-niff'|'application/vnd.muvee.style'|'application/vnd.mynfc'|'application/vnd.nacamar.ybrid+json'|'application/vnd.nato.bindingdataobject+cbor'|'application/vnd.nato.bindingdataobject+json'|'application/vnd.nato.bindingdataobject+xml'|'application/vnd.nato.openxmlformats-package.iepd+zip'|'application/vnd.ncd.control'|'application/vnd.ncd.reference'|'application/vnd.nearst.inv+json'|'application/vnd.nebumind.line'|'application/vnd.nervana'|'application/vnd.netfpx'|'application/vnd.neurolanguage.nlu'|'application/vnd.nimn'|'application/vnd.nintendo.snes.rom'|'application/vnd.nintendo.nitro.rom'|'application/vnd.nitf'|'application/vnd.noblenet-directory'|'application/vnd.noblenet-sealer'|'application/vnd.noblenet-web'|'application/vnd.nokia.catalogs'|'application/vnd.nokia.conml+wbxml'|'application/vnd.nokia.conml+xml'|'application/vnd.nokia.iptv.config+xml'|'application/vnd.nokia.iSDS-radio-presets'|'application/vnd.nokia.landmark+wbxml'|'application/vnd.nokia.landmark+xml'|'application/vnd.nokia.landmarkcollection+xml'|'application/vnd.nokia.ncd'|'application/vnd.nokia.n-gage.ac+xml'|'application/vnd.nokia.n-gage.data'|'application/vnd.nokia.n-gage.symbian.install'|'application/vnd.nokia.pcd+wbxml'|'application/vnd.nokia.pcd+xml'|'application/vnd.nokia.radio-preset'|'application/vnd.nokia.radio-presets'|'application/vnd.novadigm.EDM'|'application/vnd.novadigm.EDX'|'application/vnd.novadigm.EXT'|'application/vnd.ntt-local.content-share'|'application/vnd.ntt-local.file-transfer'|'application/vnd.ntt-local.ogw_remote-access'|'application/vnd.ntt-local.sip-ta_remote'|'application/vnd.ntt-local.sip-ta_tcp_stream'|'application/vnd.oai.workflows'|'application/vnd.oai.workflows+json'|'application/vnd.oai.workflows+yaml'|'application/vnd.oasis.opendocument.base'|'application/vnd.oasis.opendocument.chart'|'application/vnd.oasis.opendocument.chart-template'|'application/vnd.oasis.opendocument.database'|'application/vnd.oasis.opendocument.formula'|'application/vnd.oasis.opendocument.formula-template'|'application/vnd.oasis.opendocument.graphics'|'application/vnd.oasis.opendocument.graphics-template'|'application/vnd.oasis.opendocument.image'|'application/vnd.oasis.opendocument.image-template'|'application/vnd.oasis.opendocument.presentation'|'application/vnd.oasis.opendocument.presentation-template'|'application/vnd.oasis.opendocument.spreadsheet'|'application/vnd.oasis.opendocument.spreadsheet-template'|'application/vnd.oasis.opendocument.text'|'application/vnd.oasis.opendocument.text-master'|'application/vnd.oasis.opendocument.text-master-template'|'application/vnd.oasis.opendocument.text-template'|'application/vnd.oasis.opendocument.text-web'|'application/vnd.obn'|'application/vnd.ocf+cbor'|'application/vnd.oci.image.manifest.v1+json'|'application/vnd.oftn.l10n+json'|'application/vnd.oipf.contentaccessdownload+xml'|'application/vnd.oipf.contentaccessstreaming+xml'|'application/vnd.oipf.cspg-hexbinary'|'application/vnd.oipf.dae.svg+xml'|'application/vnd.oipf.dae.xhtml+xml'|'application/vnd.oipf.mippvcontrolmessage+xml'|'application/vnd.oipf.pae.gem'|'application/vnd.oipf.spdiscovery+xml'|'application/vnd.oipf.spdlist+xml'|'application/vnd.oipf.ueprofile+xml'|'application/vnd.oipf.userprofile+xml'|'application/vnd.olpc-sugar'|'application/vnd.oma.bcast.associated-procedure-parameter+xml'|'application/vnd.oma.bcast.drm-trigger+xml'|'application/vnd.oma.bcast.imd+xml'|'application/vnd.oma.bcast.ltkm'|'application/vnd.oma.bcast.notification+xml'|'application/vnd.oma.bcast.provisioningtrigger'|'application/vnd.oma.bcast.sgboot'|'application/vnd.oma.bcast.sgdd+xml'|'application/vnd.oma.bcast.sgdu'|'application/vnd.oma.bcast.simple-symbol-container'|'application/vnd.oma.bcast.smartcard-trigger+xml'|'application/vnd.oma.bcast.sprov+xml'|'application/vnd.oma.bcast.stkm'|'application/vnd.oma.cab-address-book+xml'|'application/vnd.oma.cab-feature-handler+xml'|'application/vnd.oma.cab-pcc+xml'|'application/vnd.oma.cab-subs-invite+xml'|'application/vnd.oma.cab-user-prefs+xml'|'application/vnd.oma.dcd'|'application/vnd.oma.dcdc'|'application/vnd.oma.dd2+xml'|'application/vnd.oma.drm.risd+xml'|'application/vnd.oma.group-usage-list+xml'|'application/vnd.oma.lwm2m+cbor'|'application/vnd.oma.lwm2m+json'|'application/vnd.oma.lwm2m+tlv'|'application/vnd.oma.pal+xml'|'application/vnd.oma.poc.detailed-progress-report+xml'|'application/vnd.oma.poc.final-report+xml'|'application/vnd.oma.poc.groups+xml'|'application/vnd.oma.poc.invocation-descriptor+xml'|'application/vnd.oma.poc.optimized-progress-report+xml'|'application/vnd.oma.push'|'application/vnd.oma.scidm.messages+xml'|'application/vnd.oma.xcap-directory+xml'|'application/vnd.omads-email+xml'|'application/vnd.omads-file+xml'|'application/vnd.omads-folder+xml'|'application/vnd.omaloc-supl-init'|'application/vnd.oma-scws-config'|'application/vnd.oma-scws-http-request'|'application/vnd.oma-scws-http-response'|'application/vnd.onepager'|'application/vnd.onepagertamp'|'application/vnd.onepagertamx'|'application/vnd.onepagertat'|'application/vnd.onepagertatp'|'application/vnd.onepagertatx'|'application/vnd.onvif.metadata'|'application/vnd.openblox.game-binary'|'application/vnd.openblox.game+xml'|'application/vnd.openeye.oeb'|'application/vnd.openstreetmap.data+xml'|'application/vnd.opentimestamps.ots'|'application/vnd.openxmlformats-officedocument.custom-properties+xml'|'application/vnd.openxmlformats-officedocument.customXmlProperties+xml'|'application/vnd.openxmlformats-officedocument.drawing+xml'|'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'|'application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml'|'application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml'|'application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml'|'application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml'|'application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml'|'application/vnd.openxmlformats-officedocument.extended-properties+xml'|'application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml'|'application/vnd.openxmlformats-officedocument.presentationml.comments+xml'|'application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml'|'application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml'|'application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml'|'application/vnd.openxmlformats-officedocument.presentationml.presentation'|'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml'|'application/vnd.openxmlformats-officedocument.presentationml.presProps+xml'|'application/vnd.openxmlformats-officedocument.presentationml.slide'|'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'|'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml'|'application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml'|'application/vnd.openxmlformats-officedocument.presentationml.slideshow'|'application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml'|'application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml'|'application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml'|'application/vnd.openxmlformats-officedocument.presentationml.tags+xml'|'application/vnd.openxmlformats-officedocument.presentationml.template'|'application/vnd.openxmlformats-officedocument.presentationml.template.main+xml'|'application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'|'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.template'|'application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml'|'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'|'application/vnd.openxmlformats-officedocument.theme+xml'|'application/vnd.openxmlformats-officedocument.themeOverride+xml'|'application/vnd.openxmlformats-officedocument.vmlDrawing'|'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.document'|'application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.template'|'application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml'|'application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml'|'application/vnd.openxmlformats-package.core-properties+xml'|'application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml'|'application/vnd.openxmlformats-package.relationships+xml'|'application/vnd.oracle.resource+json'|'application/vnd.orange.indata'|'application/vnd.osa.netdeploy'|'application/vnd.osgeo.mapguide.package'|'application/vnd.osgi.bundle'|'application/vnd.osgi.dp'|'application/vnd.osgi.subsystem'|'application/vnd.otps.ct-kip+xml'|'application/vnd.oxli.countgraph'|'application/vnd.pagerduty+json'|'application/vnd.palm'|'application/vnd.panoply'|'application/vnd.paos.xml'|'application/vnd.patentdive'|'application/vnd.patientecommsdoc'|'application/vnd.pawaafile'|'application/vnd.pcos'|'application/vnd.pg.format'|'application/vnd.pg.osasli'|'application/vnd.piaccess.application-licence'|'application/vnd.picsel'|'application/vnd.pmi.widget'|'application/vnd.poc.group-advertisement+xml'|'application/vnd.pocketlearn'|'application/vnd.powerbuilder6'|'application/vnd.powerbuilder6-s'|'application/vnd.powerbuilder7'|'application/vnd.powerbuilder75'|'application/vnd.powerbuilder75-s'|'application/vnd.powerbuilder7-s'|'application/vnd.preminet'|'application/vnd.previewsystems.box'|'application/vnd.proteus.magazine'|'application/vnd.psfs'|'application/vnd.pt.mundusmundi'|'application/vnd.publishare-delta-tree'|'application/vnd.pvi.ptid1'|'application/vnd.pwg-multiplexed'|'application/vnd.pwg-xhtml-print+xml'|'application/vnd.qualcomm.brew-app-res'|'application/vnd.quarantainenet'|'application/vnd.Quark.QuarkXPress'|'application/vnd.quobject-quoxdocument'|'application/vnd.radisys.moml+xml'|'application/vnd.radisys.msml-audit-conf+xml'|'application/vnd.radisys.msml-audit-conn+xml'|'application/vnd.radisys.msml-audit-dialog+xml'|'application/vnd.radisys.msml-audit-stream+xml'|'application/vnd.radisys.msml-audit+xml'|'application/vnd.radisys.msml-conf+xml'|'application/vnd.radisys.msml-dialog-base+xml'|'application/vnd.radisys.msml-dialog-fax-detect+xml'|'application/vnd.radisys.msml-dialog-fax-sendrecv+xml'|'application/vnd.radisys.msml-dialog-group+xml'|'application/vnd.radisys.msml-dialog-speech+xml'|'application/vnd.radisys.msml-dialog-transform+xml'|'application/vnd.radisys.msml-dialog+xml'|'application/vnd.radisys.msml+xml'|'application/vnd.rainstor.data'|'application/vnd.rapid'|'application/vnd.rar'|'application/vnd.realvnc.bed'|'application/vnd.recordare.musicxml'|'application/vnd.recordare.musicxml+xml'|'application/vnd.relpipe'|'application/vnd.RenLearn.rlprint'|'application/vnd.resilient.logic'|'application/vnd.restful+json'|'application/vnd.rig.cryptonote'|'application/vnd.route66.link66+xml'|'application/vnd.rs-274x'|'application/vnd.ruckus.download'|'application/vnd.s3sms'|'application/vnd.sailingtracker.track'|'application/vnd.sar'|'application/vnd.sbm.cid'|'application/vnd.sbm.mid2'|'application/vnd.scribus'|'application/vnd.sealed.3df'|'application/vnd.sealed.csf'|'application/vnd.sealed.doc'|'application/vnd.sealed.eml'|'application/vnd.sealed.mht'|'application/vnd.sealed.net'|'application/vnd.sealed.ppt'|'application/vnd.sealed.tiff'|'application/vnd.sealed.xls'|'application/vnd.sealedmedia.softseal.html'|'application/vnd.sealedmedia.softseal.pdf'|'application/vnd.seemail'|'application/vnd.seis+json'|'application/vnd.sema'|'application/vnd.semd'|'application/vnd.semf'|'application/vnd.shade-save-file'|'application/vnd.shana.informed.formdata'|'application/vnd.shana.informed.formtemplate'|'application/vnd.shana.informed.interchange'|'application/vnd.shana.informed.package'|'application/vnd.shootproof+json'|'application/vnd.shopkick+json'|'application/vnd.shp'|'application/vnd.shx'|'application/vnd.sigrok.session'|'application/vnd.SimTech-MindMapper'|'application/vnd.siren+json'|'application/vnd.smaf'|'application/vnd.smart.notebook'|'application/vnd.smart.teacher'|'application/vnd.smintio.portals.archive'|'application/vnd.snesdev-page-table'|'application/vnd.software602.filler.form+xml'|'application/vnd.software602.filler.form-xml-zip'|'application/vnd.solent.sdkm+xml'|'application/vnd.spotfire.dxp'|'application/vnd.spotfire.sfs'|'application/vnd.sqlite3'|'application/vnd.sss-cod'|'application/vnd.sss-dtf'|'application/vnd.sss-ntf'|'application/vnd.stepmania.package'|'application/vnd.stepmania.stepchart'|'application/vnd.street-stream'|'application/vnd.sun.wadl+xml'|'application/vnd.sus-calendar'|'application/vnd.svd'|'application/vnd.swiftview-ics'|'application/vnd.sybyl.mol2'|'application/vnd.sycle+xml'|'application/vnd.syft+json'|'application/vnd.syncml.dm.notification'|'application/vnd.syncml.dmddf+xml'|'application/vnd.syncml.dmtnds+wbxml'|'application/vnd.syncml.dmtnds+xml'|'application/vnd.syncml.dmddf+wbxml'|'application/vnd.syncml.dm+wbxml'|'application/vnd.syncml.dm+xml'|'application/vnd.syncml.ds.notification'|'application/vnd.syncml+xml'|'application/vnd.tableschema+json'|'application/vnd.tao.intent-module-archive'|'application/vnd.tcpdump.pcap'|'application/vnd.think-cell.ppttc+json'|'application/vnd.tml'|'application/vnd.tmd.mediaflex.api+xml'|'application/vnd.tmobile-livetv'|'application/vnd.tri.onesource'|'application/vnd.trid.tpt'|'application/vnd.triscape.mxs'|'application/vnd.trueapp'|'application/vnd.truedoc'|'application/vnd.ubisoft.webplayer'|'application/vnd.ufdl'|'application/vnd.uiq.theme'|'application/vnd.umajin'|'application/vnd.unity'|'application/vnd.uoml+xml'|'application/vnd.uplanet.alert'|'application/vnd.uplanet.alert-wbxml'|'application/vnd.uplanet.bearer-choice'|'application/vnd.uplanet.bearer-choice-wbxml'|'application/vnd.uplanet.cacheop'|'application/vnd.uplanet.cacheop-wbxml'|'application/vnd.uplanet.channel'|'application/vnd.uplanet.channel-wbxml'|'application/vnd.uplanet.list'|'application/vnd.uplanet.listcmd'|'application/vnd.uplanet.listcmd-wbxml'|'application/vnd.uplanet.list-wbxml'|'application/vnd.uri-map'|'application/vnd.uplanet.signal'|'application/vnd.valve.source.material'|'application/vnd.vcx'|'application/vnd.vd-study'|'application/vnd.vectorworks'|'application/vnd.vel+json'|'application/vnd.verimatrix.vcas'|'application/vnd.veritone.aion+json'|'application/vnd.veryant.thin'|'application/vnd.ves.encrypted'|'application/vnd.vidsoft.vidconference'|'application/vnd.visio'|'application/vnd.visionary'|'application/vnd.vividence.scriptfile'|'application/vnd.vsf'|'application/vnd.wap.sic'|'application/vnd.wap.slc'|'application/vnd.wap.wbxml'|'application/vnd.wap.wmlc'|'application/vnd.wap.wmlscriptc'|'application/vnd.wasmflow.wafl'|'application/vnd.webturbo'|'application/vnd.wfa.dpp'|'application/vnd.wfa.p2p'|'application/vnd.wfa.wsc'|'application/vnd.windows.devicepairing'|'application/vnd.wmc'|'application/vnd.wmf.bootstrap'|'application/vnd.wolfram.mathematica'|'application/vnd.wolfram.mathematica.package'|'application/vnd.wolfram.player'|'application/vnd.wordlift'|'application/vnd.wordperfect'|'application/vnd.wqd'|'application/vnd.wrq-hp3000-labelled'|'application/vnd.wt.stf'|'application/vnd.wv.csp+xml'|'application/vnd.wv.csp+wbxml'|'application/vnd.wv.ssp+xml'|'application/vnd.xacml+json'|'application/vnd.xara'|'application/vnd.xecrets-encrypted'|'application/vnd.xfdl'|'application/vnd.xfdl.webform'|'application/vnd.xmi+xml'|'application/vnd.xmpie.cpkg'|'application/vnd.xmpie.dpkg'|'application/vnd.xmpie.plan'|'application/vnd.xmpie.ppkg'|'application/vnd.xmpie.xlim'|'application/vnd.yamaha.hv-dic'|'application/vnd.yamaha.hv-script'|'application/vnd.yamaha.hv-voice'|'application/vnd.yamaha.openscoreformat.osfpvg+xml'|'application/vnd.yamaha.openscoreformat'|'application/vnd.yamaha.remote-setup'|'application/vnd.yamaha.smaf-audio'|'application/vnd.yamaha.smaf-phrase'|'application/vnd.yamaha.through-ngn'|'application/vnd.yamaha.tunnel-udpencap'|'application/vnd.yaoweme'|'application/vnd.yellowriver-custom-menu'|'application/vnd.youtube.yt'|'application/vnd.zul'|'application/vnd.zzazz.deck+xml'|'application/voicexml+xml'|'application/voucher-cms+json'|'application/vq-rtcpxr'|'application/wasm'|'application/watcherinfo+xml'|'application/webpush-options+json'|'application/whoispp-query'|'application/whoispp-response'|'application/widget'|'application/wita'|'application/wordperfect5.1'|'application/wsdl+xml'|'application/wspolicy+xml'|'application/x-pki-message'|'application/x-www-form-urlencoded'|'application/x-x509-ca-cert'|'application/x-x509-ca-ra-cert'|'application/x-x509-next-ca-cert'|'application/x400-bp'|'application/xacml+xml'|'application/xcap-att+xml'|'application/xcap-caps+xml'|'application/xcap-diff+xml'|'application/xcap-el+xml'|'application/xcap-error+xml'|'application/xcap-ns+xml'|'application/xcon-conference-info-diff+xml'|'application/xcon-conference-info+xml'|'application/xenc+xml'|'application/xfdf'|'application/xhtml+xml'|'application/xliff+xml'|'application/xml'|'application/xml-dtd'|'application/xml-external-parsed-entity'|'application/xml-patch+xml'|'application/xmpp+xml'|'application/xop+xml'|'application/xslt+xml'|'application/xv+xml'|'application/yaml'|'application/yang'|'application/yang-data+cbor'|'application/yang-data+json'|'application/yang-data+xml'|'application/yang-patch+json'|'application/yang-patch+xml'|'application/yin+xml'|'application/zip'|'application/zlib'|'application/zstd'|'audio/1d-interleaved-parityfec'|'audio/32kadpcm'|'audio/3gpp'|'audio/3gpp2'|'audio/aac'|'audio/ac3'|'audio/AMR'|'audio/AMR-WB'|'audio/amr-wb+'|'audio/aptx'|'audio/asc'|'audio/ATRAC-ADVANCED-LOSSLESS'|'audio/ATRAC-X'|'audio/ATRAC3'|'audio/basic'|'audio/BV16'|'audio/BV32'|'audio/clearmode'|'audio/CN'|'audio/DAT12'|'audio/dls'|'audio/dsr-es201108'|'audio/dsr-es202050'|'audio/dsr-es202211'|'audio/dsr-es202212'|'audio/DV'|'audio/DVI4'|'audio/eac3'|'audio/encaprtp'|'audio/EVRC'|'audio/EVRC-QCP'|'audio/EVRC0'|'audio/EVRC1'|'audio/EVRCB'|'audio/EVRCB0'|'audio/EVRCB1'|'audio/EVRCNW'|'audio/EVRCNW0'|'audio/EVRCNW1'|'audio/EVRCWB'|'audio/EVRCWB0'|'audio/EVRCWB1'|'audio/EVS'|'audio/example'|'audio/flexfec'|'audio/fwdred'|'audio/G711-0'|'audio/G719'|'audio/G7221'|'audio/G722'|'audio/G723'|'audio/G726-16'|'audio/G726-24'|'audio/G726-32'|'audio/G726-40'|'audio/G728'|'audio/G729'|'audio/G7291'|'audio/G729D'|'audio/G729E'|'audio/GSM'|'audio/GSM-EFR'|'audio/GSM-HR-08'|'audio/iLBC'|'audio/ip-mr_v2.5'|'audio/L8'|'audio/L16'|'audio/L20'|'audio/L24'|'audio/LPC'|'audio/matroska'|'audio/MELP'|'audio/MELP600'|'audio/MELP1200'|'audio/MELP2400'|'audio/mhas'|'audio/mobile-xmf'|'audio/MPA'|'audio/mp4'|'audio/MP4A-LATM'|'audio/mpa-robust'|'audio/mpeg'|'audio/mpeg4-generic'|'audio/ogg'|'audio/opus'|'audio/parityfec'|'audio/PCMA'|'audio/PCMA-WB'|'audio/PCMU'|'audio/PCMU-WB'|'audio/prs.sid'|'audio/QCELP'|'audio/raptorfec'|'audio/RED'|'audio/rtp-enc-aescm128'|'audio/rtploopback'|'audio/rtp-midi'|'audio/rtx'|'audio/scip'|'audio/SMV'|'audio/SMV0'|'audio/SMV-QCP'|'audio/sofa'|'audio/sp-midi'|'audio/speex'|'audio/t140c'|'audio/t38'|'audio/telephone-event'|'audio/TETRA_ACELP'|'audio/TETRA_ACELP_BB'|'audio/tone'|'audio/TSVCIS'|'audio/UEMCLIP'|'audio/ulpfec'|'audio/usac'|'audio/VDVI'|'audio/VMR-WB'|'audio/vnd.3gpp.iufp'|'audio/vnd.4SB'|'audio/vnd.audiokoz'|'audio/vnd.CELP'|'audio/vnd.cisco.nse'|'audio/vnd.cmles.radio-events'|'audio/vnd.cns.anp1'|'audio/vnd.cns.inf1'|'audio/vnd.dece.audio'|'audio/vnd.digital-winds'|'audio/vnd.dlna.adts'|'audio/vnd.dolby.heaac.1'|'audio/vnd.dolby.heaac.2'|'audio/vnd.dolby.mlp'|'audio/vnd.dolby.mps'|'audio/vnd.dolby.pl2'|'audio/vnd.dolby.pl2x'|'audio/vnd.dolby.pl2z'|'audio/vnd.dolby.pulse.1'|'audio/vnd.dra'|'audio/vnd.dts'|'audio/vnd.dts.hd'|'audio/vnd.dts.uhd'|'audio/vnd.dvb.file'|'audio/vnd.everad.plj'|'audio/vnd.hns.audio'|'audio/vnd.lucent.voice'|'audio/vnd.ms-playready.media.pya'|'audio/vnd.nokia.mobile-xmf'|'audio/vnd.nortel.vbk'|'audio/vnd.nuera.ecelp4800'|'audio/vnd.nuera.ecelp7470'|'audio/vnd.nuera.ecelp9600'|'audio/vnd.octel.sbc'|'audio/vnd.presonus.multitrack'|'audio/vnd.qcelp'|'audio/vnd.rhetorex.32kadpcm'|'audio/vnd.rip'|'audio/vnd.sealedmedia.softseal.mpeg'|'audio/vnd.vmx.cvsd'|'audio/vorbis'|'audio/vorbis-config'|'font/collection'|'font/otf'|'font/sfnt'|'font/ttf'|'font/woff'|'font/woff2'|'image/aces'|'image/apng'|'image/avci'|'image/avcs'|'image/avif'|'image/bmp'|'image/cgm'|'image/dicom-rle'|'image/dpx'|'image/emf'|'image/example'|'image/fits'|'image/g3fax'|'image/heic'|'image/heic-sequence'|'image/heif'|'image/heif-sequence'|'image/hej2k'|'image/hsj2'|'image/j2c'|'image/jls'|'image/jp2'|'image/jph'|'image/jphc'|'image/jpm'|'image/jpx'|'image/jxr'|'image/jxrA'|'image/jxrS'|'image/jxs'|'image/jxsc'|'image/jxsi'|'image/jxss'|'image/ktx'|'image/ktx2'|'image/naplps'|'image/png'|'image/prs.btif'|'image/prs.pti'|'image/pwg-raster'|'image/svg+xml'|'image/t38'|'image/tiff'|'image/tiff-fx'|'image/vnd.adobe.photoshop'|'image/vnd.airzip.accelerator.azv'|'image/vnd.cns.inf2'|'image/vnd.dece.graphic'|'image/vnd.djvu'|'image/vnd.dwg'|'image/vnd.dxf'|'image/vnd.dvb.subtitle'|'image/vnd.fastbidsheet'|'image/vnd.fpx'|'image/vnd.fst'|'image/vnd.fujixerox.edmics-mmr'|'image/vnd.fujixerox.edmics-rlc'|'image/vnd.globalgraphics.pgb'|'image/vnd.microsoft.icon'|'image/vnd.mix'|'image/vnd.ms-modi'|'image/vnd.mozilla.apng'|'image/vnd.net-fpx'|'image/vnd.pco.b16'|'image/vnd.radiance'|'image/vnd.sealed.png'|'image/vnd.sealedmedia.softseal.gif'|'image/vnd.sealedmedia.softseal.jpg'|'image/vnd.svf'|'image/vnd.tencent.tap'|'image/vnd.valve.source.texture'|'image/vnd.wap.wbmp'|'image/vnd.xiff'|'image/vnd.zbrush.pcx'|'image/webp'|'image/wmf'|'image/emf'|'image/wmf'|'message/bhttp'|'message/CPIM'|'message/delivery-status'|'message/disposition-notification'|'message/example'|'message/feedback-report'|'message/global'|'message/global-delivery-status'|'message/global-disposition-notification'|'message/global-headers'|'message/http'|'message/imdn+xml'|'message/mls'|'message/news'|'message/ohttp-req'|'message/ohttp-res'|'message/s-http'|'message/sip'|'message/sipfrag'|'message/tracking-status'|'message/vnd.si.simp'|'message/vnd.wfa.wsc'|'model/3mf'|'model/e57'|'model/example'|'model/gltf-binary'|'model/gltf+json'|'model/JT'|'model/iges'|'model/mtl'|'model/obj'|'model/prc'|'model/step'|'model/step+xml'|'model/step+zip'|'model/step-xml+zip'|'model/stl'|'model/u3d'|'model/vnd.bary'|'model/vnd.cld'|'model/vnd.collada+xml'|'model/vnd.dwf'|'model/vnd.flatland.3dml'|'model/vnd.gdl'|'model/vnd.gs-gdl'|'model/vnd.gtw'|'model/vnd.moml+xml'|'model/vnd.mts'|'model/vnd.opengex'|'model/vnd.parasolid.transmit.binary'|'model/vnd.parasolid.transmit.text'|'model/vnd.pytha.pyox'|'model/vnd.rosette.annotated-data-model'|'model/vnd.sap.vds'|'model/vnd.usda'|'model/vnd.usdz+zip'|'model/vnd.valve.source.compiled-map'|'model/vnd.vtu'|'model/x3d-vrml'|'model/x3d+fastinfoset'|'model/x3d+xml'|'multipart/appledouble'|'multipart/byteranges'|'multipart/encrypted'|'multipart/example'|'multipart/form-data'|'multipart/header-set'|'multipart/multilingual'|'multipart/related'|'multipart/report'|'multipart/signed'|'multipart/vnd.bint.med-plus'|'multipart/voice-message'|'multipart/x-mixed-replace'|'text/1d-interleaved-parityfec'|'text/cache-manifest'|'text/calendar'|'text/cql'|'text/cql-expression'|'text/cql-identifier'|'text/css'|'text/csv'|'text/csv-schema'|'text/directory'|'text/dns'|'text/ecmascript'|'text/encaprtp'|'text/example'|'text/fhirpath'|'text/flexfec'|'text/fwdred'|'text/gff3'|'text/grammar-ref-list'|'text/hl7v2'|'text/html'|'text/javascript'|'text/jcr-cnd'|'text/markdown'|'text/mizar'|'text/n3'|'text/parameters'|'text/parityfec'|'text/provenance-notation'|'text/prs.fallenstein.rst'|'text/prs.lines.tag'|'text/prs.prop.logic'|'text/prs.texi'|'text/raptorfec'|'text/RED'|'text/rfc822-headers'|'text/rtf'|'text/rtp-enc-aescm128'|'text/rtploopback'|'text/rtx'|'text/SGML'|'text/shaclc'|'text/shex'|'text/spdx'|'text/strings'|'text/t140'|'text/tab-separated-values'|'text/troff'|'text/turtle'|'text/ulpfec'|'text/uri-list'|'text/vcard'|'text/vnd.a'|'text/vnd.abc'|'text/vnd.ascii-art'|'text/vnd.curl'|'text/vnd.debian.copyright'|'text/vnd.DMClientScript'|'text/vnd.dvb.subtitle'|'text/vnd.esmertec.theme-descriptor'|'text/vnd.exchangeable'|'text/vnd.familysearch.gedcom'|'text/vnd.ficlab.flt'|'text/vnd.fly'|'text/vnd.fmi.flexstor'|'text/vnd.gml'|'text/vnd.graphviz'|'text/vnd.hans'|'text/vnd.hgl'|'text/vnd.in3d.3dml'|'text/vnd.in3d.spot'|'text/vnd.IPTC.NewsML'|'text/vnd.IPTC.NITF'|'text/vnd.latex-z'|'text/vnd.motorola.reflex'|'text/vnd.ms-mediapackage'|'text/vnd.net2phone.commcenter.command'|'text/vnd.radisys.msml-basic-layout'|'text/vnd.senx.warpscript'|'text/vnd.si.uricatalogue'|'text/vnd.sun.j2me.app-descriptor'|'text/vnd.sosi'|'text/vnd.trolltech.linguist'|'text/vnd.wap.si'|'text/vnd.wap.sl'|'text/vnd.wap.wml'|'text/vnd.wap.wmlscript'|'text/vtt'|'text/wgsl'|'text/xml'|'text/xml-external-parsed-entity'|'video/1d-interleaved-parityfec'|'video/3gpp'|'video/3gpp2'|'video/3gpp-tt'|'video/AV1'|'video/BMPEG'|'video/BT656'|'video/CelB'|'video/DV'|'video/encaprtp'|'video/example'|'video/FFV1'|'video/flexfec'|'video/H261'|'video/H263'|'video/H263-1998'|'video/H263-2000'|'video/H264'|'video/H264-RCDO'|'video/H264-SVC'|'video/H265'|'video/H266'|'video/iso.segment'|'video/JPEG'|'video/jpeg2000'|'video/jxsv'|'video/matroska'|'video/matroska-3d'|'video/mj2'|'video/MP1S'|'video/MP2P'|'video/MP2T'|'video/mp4'|'video/MP4V-ES'|'video/MPV'|'video/mpeg4-generic'|'video/nv'|'video/ogg'|'video/parityfec'|'video/pointer'|'video/quicktime'|'video/raptorfec'|'video/raw'|'video/rtp-enc-aescm128'|'video/rtploopback'|'video/rtx'|'video/scip'|'video/smpte291'|'video/SMPTE292M'|'video/ulpfec'|'video/vc1'|'video/vc2'|'video/vnd.CCTV'|'video/vnd.dece.hd'|'video/vnd.dece.mobile'|'video/vnd.dece.mp4'|'video/vnd.dece.pd'|'video/vnd.dece.sd'|'video/vnd.dece.video'|'video/vnd.directv.mpeg'|'video/vnd.directv.mpeg-tts'|'video/vnd.dlna.mpeg-tts'|'video/vnd.dvb.file'|'video/vnd.fvt'|'video/vnd.hns.video'|'video/vnd.iptvforum.1dparityfec-1010'|'video/vnd.iptvforum.1dparityfec-2005'|'video/vnd.iptvforum.2dparityfec-1010'|'video/vnd.iptvforum.2dparityfec-2005'|'video/vnd.iptvforum.ttsavc'|'video/vnd.iptvforum.ttsmpeg2'|'video/vnd.motorola.video'|'video/vnd.motorola.videop'|'video/vnd.mpegurl'|'video/vnd.ms-playready.media.pyv'|'video/vnd.nokia.interleaved-multimedia'|'video/vnd.nokia.mp4vr'|'video/vnd.nokia.videovoip'|'video/vnd.objectvideo'|'video/vnd.radgamettools.bink'|'video/vnd.radgamettools.smacker'|'video/vnd.sealed.mpeg1'|'video/vnd.sealed.mpeg4'|'video/vnd.sealed.swf'|'video/vnd.sealedmedia.softseal.mov'|'video/vnd.uvvu.mp4'|'video/vnd.youtube.yt'|'video/vnd.vivo'|'video/VP8'|'video/VP9'| AnyString; diff --git a/src/types/_utils.ts b/src/types/_utils.ts deleted file mode 100644 index 1046bc55..00000000 --- a/src/types/_utils.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type AnyString = string & {}; // eslint-disable-line @typescript-eslint/ban-types -export type AnyNumber = number & {}; // eslint-disable-line @typescript-eslint/ban-types diff --git a/src/types/app.ts b/src/types/app.ts new file mode 100644 index 00000000..e8a7aca3 --- /dev/null +++ b/src/types/app.ts @@ -0,0 +1,61 @@ +import type { AdapterOptions as WSOptions } from "crossws"; +import type { H3Event } from "./event"; +import type { EventHandler, EventHandlerResolver } from "./handler"; +import type { H3Error } from "../error"; + +export type { H3Error } from "../error"; + +export interface Layer { + route: string; + match?: Matcher; + handler: EventHandler; +} + +export type Stack = Layer[]; + +export interface InputLayer { + route?: string; + match?: Matcher; + handler: EventHandler; + lazy?: boolean; +} + +export type InputStack = InputLayer[]; + +export type Matcher = (url: string, event?: H3Event) => boolean; + +export interface AppUse { + ( + route: string | string[], + handler: EventHandler | EventHandler[], + options?: Partial, + ): App; + (handler: EventHandler | EventHandler[], options?: Partial): App; + (options: InputLayer): App; +} + +export type WebSocketOptions = WSOptions; + +export interface AppOptions { + debug?: boolean; + onError?: (error: H3Error, event: H3Event) => any; + onRequest?: (event: H3Event) => void | Promise; + onBeforeResponse?: ( + event: H3Event, + response: { body?: unknown }, + ) => void | Promise; + onAfterResponse?: ( + event: H3Event, + response?: { body?: unknown }, + ) => void | Promise; + websocket?: WebSocketOptions; +} + +export interface App { + stack: Stack; + handler: EventHandler; + options: AppOptions; + use: AppUse; + resolve: EventHandlerResolver; + readonly websocket: WebSocketOptions; +} diff --git a/src/types/context.ts b/src/types/context.ts new file mode 100644 index 00000000..757e4b1b --- /dev/null +++ b/src/types/context.ts @@ -0,0 +1,17 @@ +import type { Session } from "./utils/session"; +import type { RouteNode } from "./router"; + +export interface H3EventContext extends Record { + /* Matched router parameters */ + params?: Record; + /** + * Matched router Node + * + * @experimental The object structure may change in non-major version. + */ + matchedRoute?: RouteNode; + /* Cached session data */ + sessions?: Record; + /* Trusted IP Address of client */ + clientAddress?: string; +} diff --git a/src/types/event.ts b/src/types/event.ts new file mode 100644 index 00000000..4e986d2b --- /dev/null +++ b/src/types/event.ts @@ -0,0 +1,74 @@ +import type { Readable as NodeReadableStream } from "node:stream"; +import type { EventHandlerRequest, H3EventContext, HTTPMethod } from "."; +import type { _kRaw } from "../event"; + +type MaybePromise = T | Promise; + +export interface H3Event< + _RequestT extends EventHandlerRequest = EventHandlerRequest, +> { + // Internal raw context + [_kRaw]: RawEvent; + + // Context + context: H3EventContext; + + // Request + readonly method: HTTPMethod; + readonly path: string; + readonly headers: Headers; + + toString(): string; + toJSON(): string; + + // ...State + _onBeforeResponseCalled: boolean | undefined; + _onAfterResponseCalled: boolean | undefined; +} + +export type RawResponse = + | undefined + | null + | Uint8Array + | string + | ReadableStream + | NodeReadableStream; + +export interface RawEvent { + // -- Context -- + getContext: () => Record; + + // -- Request -- + + path: string; + readonly originalPath: string; + readonly method: HTTPMethod; + + readonly remoteAddress?: string | undefined; + readonly isSecure?: boolean | undefined; + + getHeader: (key: string) => string | undefined; + getHeaders: () => Headers; + + readRawBody: () => MaybePromise; + readTextBody: () => MaybePromise; + readFormDataBody: () => MaybePromise; + getBodyStream: () => ReadableStream | undefined; + + // -- Response -- + + readonly handled: boolean | undefined; + + responseCode: number | undefined; + responseMessage: string | undefined; + + setResponseHeader: (key: string, value: string) => void; + appendResponseHeader: (key: string, value: string) => void; + getResponseHeader: (key: string) => string | undefined; + getResponseHeaders: () => Headers; + getResponseSetCookie: () => string[]; + removeResponseHeader: (key: string) => void; + writeHead: (code: number, message?: string) => void; + sendResponse: (data?: RawResponse) => void | Promise; + writeEarlyHints: (hints: Record) => void | Promise; +} diff --git a/src/types/handler.ts b/src/types/handler.ts new file mode 100644 index 00000000..4a0ded5e --- /dev/null +++ b/src/types/handler.ts @@ -0,0 +1,64 @@ +import type { QueryObject } from "ufo"; +import type { H3Event } from "./event"; +import type { Hooks as WSHooks } from "crossws"; + +export type EventHandlerResponse = T | Promise; + +export interface EventHandlerRequest { + body?: any; // TODO: Default to unknown in next major version + query?: QueryObject; + routerParams?: Record; +} + +export type InferEventInput< + Key extends keyof EventHandlerRequest, + Event extends H3Event, + T, +> = void extends T ? (Event extends H3Event ? E[Key] : never) : T; + +type MaybePromise = T | Promise; + +export type EventHandlerResolver = ( + path: string, +) => MaybePromise; + +export interface EventHandler< + Request extends EventHandlerRequest = EventHandlerRequest, + Response extends EventHandlerResponse = EventHandlerResponse, +> { + __is_handler__?: true; + __resolve__?: EventHandlerResolver; + __websocket__?: Partial; + (event: H3Event): Response; +} + +export type RequestMiddleware< + Request extends EventHandlerRequest = EventHandlerRequest, +> = (event: H3Event) => void | Promise; + +export type ResponseMiddleware< + Request extends EventHandlerRequest = EventHandlerRequest, + Response extends EventHandlerResponse = EventHandlerResponse, +> = ( + event: H3Event, + response: { body?: Awaited }, +) => void | Promise; + +export type EventHandlerObject< + Request extends EventHandlerRequest = EventHandlerRequest, + Response extends EventHandlerResponse = EventHandlerResponse, +> = { + onRequest?: RequestMiddleware | RequestMiddleware[]; + onBeforeResponse?: + | ResponseMiddleware + | ResponseMiddleware[]; + /** @experimental */ + websocket?: Partial; + handler: EventHandler; +}; + +export type LazyEventHandler = () => EventHandler | Promise; + +export interface DynamicEventHandler extends EventHandler { + set: (handler: EventHandler) => void; +} diff --git a/src/types/http/headers.ts b/src/types/http/headers.ts new file mode 100644 index 00000000..af0adfee --- /dev/null +++ b/src/types/http/headers.ts @@ -0,0 +1,349 @@ +import type { HTTPMethod } from ".."; +import type { MimeType } from "./mimes"; + +// --- Request Headers --- + +export type RequestHeaderName = keyof RequestHeaders | AnyString; + +export type RequestHeaders = HeadersMap<{ + Accept: MimeType | AnyString; + "Accept-Charset": AnyString; + "Accept-Encoding": + | "gzip" + | "compress" + | "deflate" + | "br" + | "identity" + | AnyString; + "Accept-Language": AnyString; + "Accept-Ch": + | "Sec-CH-UA" + | "Sec-CH-UA-Arch" + | "Sec-CH-UA-Bitness" + | "Sec-CH-UA-Full-Version-List" + | "Sec-CH-UA-Full-Version" + | "Sec-CH-UA-Mobile" + | "Sec-CH-UA-Model" + | "Sec-CH-UA-Platform" + | "Sec-CH-UA-Platform-Version" + | "Sec-CH-Prefers-Reduced-Motion" + | "Sec-CH-Prefers-Color-Scheme" + | "Device-Memory" + | "Width" + | "Viewport-Width" + | "Save-Data" + | "Downlink" + | "ECT" + | "RTT" + | AnyString; + "Access-Control-Allow-Credentials": "true" | "false" | AnyString; + "Access-Control-Allow-Headers": RequestHeaderName | AnyString; + "Access-Control-Allow-Methods": HTTPMethod | AnyString; + "Access-Control-Allow-Origin": "*" | AnyString; + "Access-Control-Expose-Headers": RequestHeaderName | AnyString; + "Access-Control-Max-Age": AnyString; + "Access-Control-Request-Headers": RequestHeaderName | AnyString; + "Access-Control-Request-Method": HTTPMethod | AnyString; + Age: AnyString; + Allow: HTTPMethod | AnyString; + Authorization: AnyString; + "Cache-Control": + | "no-cache" + | "no-store" + | "max-age" + | "must-revalidate" + | "public" + | "private" + | "proxy-revalidate" + | "s-maxage" + | "stale-while-revalidate" + | "stale-if-error" + | AnyString; + Connection: "keep-alive" | "close" | "upgrade" | AnyString; + "Content-Disposition": AnyString; + "Content-Encoding": + | "gzip" + | "compress" + | "deflate" + | "br" + | "identity" + | AnyString; + "Content-Language": AnyString; + "Content-Length": AnyString; + "Content-Location": AnyString; + "Content-Range": AnyString; + "Content-Security-Policy": AnyString; + "Content-Type": MimeType | AnyString; + Cookie: AnyString; + "Critical-CH": AnyString; + Date: AnyString; + "Device-Memory": "0.25" | "0.5" | "1" | "2" | "4" | "8" | AnyString; + Digest: AnyString; + ETag: AnyString; + Expect: "100-continue" | AnyString; + Expires: AnyString; + Forwarded: AnyString; + From: AnyString; + Host: AnyString; + "If-Match": AnyString; + "If-Modified-Since": AnyString; + "If-None-Match": AnyString; + "If-Range": AnyString; + "If-Unmodified-Since": AnyString; + "Keep-Alive": `timeout=${string}, max=${string}` | AnyString; + "Last-Modified": AnyString; + Link: AnyString; + Location: AnyString; + "Max-Forwards": AnyString; + Origin: AnyString; + "Origin-Agent-Cluster": `?1` | `?0` | AnyString; + "Ping-From": AnyString; + "Ping-To": AnyString; + Pragma: AnyString; + "Proxy-Authenticate": AnyString; + "Proxy-Authorization": AnyString; + Range: AnyString; + Referer: AnyString; + "Referrer-Policy": + | "no-referrer" + | "no-referrer-when-downgrade" + | "origin" + | "origin-when-cross-origin" + | "same-origin" + | "strict-origin" + | "strict-origin-when-cross-origin" + | "unsafe-url" + | AnyString; + "Retry-After": AnyString; + "Save-Data": `on` | `off` | AnyString; + "Sec-CH-UA": AnyString; + "Sec-CH-UA-Arch": + | "x86" + | "ARM" + | "[arm64-v8a, armeabi-v7a, armeabi]" + | AnyString; + "Sec-CH-UA-Bitness": "64" | "32" | AnyString; + "Sec-CH-UA-Full-Version-List": AnyString; + "Sec-CH-UA-Mobile": `?1` | `?0` | AnyString; + "Sec-CH-UA-Model": AnyString; + "Sec-CH-UA-Platform": + | "Android" + | "Chrome OS" + | "Chromium OS" + | "iOS" + | "Linux" + | "macOS" + | "Windows" + | "Unknown" + | AnyString; + "Sec-CH-UA-Platform-Version": AnyString; + "Sec-CH-UA-Prefers-Color-Scheme": "dark" | "light" | AnyString; + "Sec-CH-UA-Prefers-Reduced-Motion": "no-preference" | "reduce" | AnyString; + "Sec-Fetch-Dest": + | "audio" + | "audioworklet" + | "document" + | "embed" + | "empty" + | "font" + | "frame" + | "iframe" + | "image" + | "manifest" + | "object" + | "paintworklet" + | "report" + | "script" + | "serviceworker" + | "sharedworker" + | "style" + | "track" + | "video" + | "worker" + | "xslt" + | AnyString; + "Sec-Fetch-Mode": + | "cors" + | "navigate" + | "no-cors" + | "same-origin" + | "websocket" + | AnyString; + "Sec-Fetch-Site": + | "cross-site" + | "same-origin" + | "same-site" + | "none" + | AnyString; + "Sec-Fetch-User": "?1" | AnyString; + "Sec-Purpose": "prefetch" | AnyString; + "Sec-WebSocket-Accept": AnyString; + "Sec-WebSocket-Extensions": AnyString; + "Sec-WebSocket-Key": AnyString; + "Sec-WebSocket-Protocol": AnyString; + "Sec-WebSocket-Version": AnyString; + Server: AnyString; + "Service-Worker-Allowed": AnyString; + "Set-Cookie": AnyString; + "Strict-Transport-Security": AnyString; + TE: "trailers" | AnyString; + Trailer: AnyString; + "Transfer-Encoding": + | "chunked" + | "compress" + | "deflate" + | "gzip" + | "identity" + | AnyString; + Upgrade: AnyString; + "Upgrade-Insecure-Requests": "1" | AnyString; + "User-Agent": AnyString; + Vary: AnyString; + Via: AnyString; + Warning: AnyString; + "WWW-Authenticate": AnyString; + "X-Content-Type-Options": "nosniff" | AnyString; + "X-DNS-Prefetch-Control": "on" | "off" | AnyString; + "X-Forwarded-For": AnyString; + "X-Forwarded-Host": AnyString; + "X-Forwarded-Proto": AnyString; + "X-Frame-Options": "deny" | "sameorigin" | AnyString; + "X-Permitted-Cross-Domain-Policies": + | "none" + | "master-only" + | "by-content-type" + | "all" + | AnyString; + "X-Pingback": AnyString; + "X-Requested-With": AnyString; + "X-XSS-Protection": "0" | "1" | "1; mode=block" | AnyString; +}>; + +// --- Response Headers --- + +export type ResponseHeaderName = keyof ResponseHeaders | AnyString; + +export type ResponseHeaders = HeadersMap<{ + "Accept-Patch": AnyString; + "Accept-Ranges": "bytes" | "none" | AnyString; + "Access-Control-Allow-Credentials": "true" | AnyString; + "Access-Control-Allow-Headers": "*" | ResponseHeaderName | AnyString; + "Access-Control-Allow-Methods": "*" | HTTPMethod | AnyString; + "Access-Control-Allow-Origin": "*" | "null" | AnyString; + "Access-Control-Expose-Headers": "*" | ResponseHeaderName | AnyString; + "Access-Control-Max-Age": AnyString; + Age: AnyString; + Allow: HTTPMethod | AnyString; + "Alt-Svc": AnyString; + "Alt-Used": AnyString; + "Cache-Control": + | "no-cache" + | "no-store" + | "max-age" + | "must-revalidate" + | "public" + | "private" + | "proxy-revalidate" + | "s-maxage" + | "stale-while-revalidate" + | "stale-if-error" + | AnyString; + "Clear-Site-Data": AnyString; + Connection: "keep-alive" | "close" | AnyString; + "Content-Disposition": AnyString; + "Content-DPR": AnyString; + "Content-Encoding": + | "gzip" + | "compress" + | "deflate" + | "br" + | "identity" + | AnyString; + "Content-Language": AnyString; + "Content-Length": AnyString; + "Content-Location": AnyString; + "Content-Range": AnyString; + "Content-Security-Policy": AnyString; + "Content-Security-Policy-Report-Only": AnyString; + "Content-Type": MimeType | AnyString; + "Cross-Origin-Embedder-Policy": + | "unsafe-none" + | "require-corp" + | "credentialless" + | AnyString; + "Cross-Origin-Opener-Policy": + | "unsafe-none" + | "same-origin-allow-popups" + | "same-origin" + | AnyString; + "Cross-Origin-Resource-Policy": + | "same-site" + | "same-origin" + | "cross-origin" + | AnyString; + Date: AnyString; + "Device-Memory": AnyString; + Digest: AnyString; + Downlink: AnyString; + ECT: "slow-2g" | "2g" | "3g" | "4g" | AnyString; + ETag: AnyString; + "Early-Data": "1" | AnyString; + "Expect-CT": AnyString; + Expires: AnyString; + "Feature-Policy": AnyString; + "Last-Event-ID": AnyString; + "Last-Modified": AnyString; + Link: AnyString; + Location: AnyString; + NEL: AnyString; + "Origin-Agent-Cluster": AnyString; + "Origin-Isolation": AnyString; + "Proxy-Authenticate": AnyString; + "Public-Key-Pins": AnyString; + "Public-Key-Pins-Report-Only": AnyString; + Refresh: AnyString; + "Report-To": AnyString; + "Retry-After": AnyString; + "Save-Data": AnyString; + "Sec-WebSocket-Accept": AnyString; + "Sec-WebSocket-Extensions": AnyString; + "Sec-WebSocket-Protocol": AnyString; + "Sec-WebSocket-Version": AnyString; + Server: AnyString; + "Server-Timing": AnyString; + "Service-Worker-Allowed": AnyString; + "Service-Worker-Navigation-Preload": AnyString; + "Set-Cookie": AnyString; + Signature: AnyString; + "Signed-Headers": AnyString; + Sourcemap: AnyString; + "Strict-Transport-Security": AnyString; + "Timing-Allow-Origin": AnyString; + Tk: AnyString; + Vary: AnyString; + Via: AnyString; + "WWW-Authenticate": AnyString; + "X-Content-Type-Options": "nosniff" | AnyString; + "X-DNS-Prefetch-Control": "on" | "off" | AnyString; + "X-Frame-Options": "DENY" | "SAMEORIGIN" | AnyString; + "X-Permitted-Cross-Domain-Policies": + | "none" + | "master-only" + | "by-content-type" + | "all" + | AnyString; + "X-Powered-By": AnyString; + "X-Robots-Tag": AnyString; + "X-UA-Compatible": "IE=edge" | AnyString; + "X-XSS-Protection": "0" | "1" | "1; mode=block" | AnyString; +}>; + +// --- Type Utils --- + +type AnyString = string & {}; + +type HeadersMap> = Partial< + T & { + [K in keyof T as K extends string ? Lowercase : never]: T[K]; + } & Record +>; diff --git a/src/types/http/index.ts b/src/types/http/index.ts new file mode 100644 index 00000000..37e76eaa --- /dev/null +++ b/src/types/http/index.ts @@ -0,0 +1,19 @@ +// https://www.rfc-editor.org/rfc/rfc7231#section-4.1 +// prettier-ignore +export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"; + +// prettier-ignore +// eslint-disable-next-line unicorn/text-encoding-identifier-case +export type Encoding = false | "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; + +// prettier-ignore +export type StatusCode = 100 | 101 | 102 | 103 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 506 | 507 | 508 | 509 | 510 | 511 | 521 | 522 | 523 | 525 | 530 | 599 | (number & { __is_status_code__?: never }); + +export type { + RequestHeaderName, + RequestHeaders, + ResponseHeaderName, + ResponseHeaders, +} from "./headers"; + +export type { MimeType } from "./mimes"; diff --git a/src/types/http/mimes.ts b/src/types/http/mimes.ts new file mode 100644 index 00000000..86cd8075 --- /dev/null +++ b/src/types/http/mimes.ts @@ -0,0 +1,2084 @@ +type AnyString = string & {}; + +export type MimeType = + | "application/1d-interleaved-parityfec" + | "application/3gpdash-qoe-report+xml" + | "application/3gppHal+json" + | "application/3gppHalForms+json" + | "application/3gpp-ims+xml" + | "application/A2L" + | "application/ace+cbor" + | "application/ace+json" + | "application/activemessage" + | "application/activity+json" + | "application/aif+cbor" + | "application/aif+json" + | "application/alto-cdni+json" + | "application/alto-cdnifilter+json" + | "application/alto-costmap+json" + | "application/alto-costmapfilter+json" + | "application/alto-directory+json" + | "application/alto-endpointprop+json" + | "application/alto-endpointpropparams+json" + | "application/alto-endpointcost+json" + | "application/alto-endpointcostparams+json" + | "application/alto-error+json" + | "application/alto-networkmapfilter+json" + | "application/alto-networkmap+json" + | "application/alto-propmap+json" + | "application/alto-propmapparams+json" + | "application/alto-tips+json" + | "application/alto-tipsparams+json" + | "application/alto-updatestreamcontrol+json" + | "application/alto-updatestreamparams+json" + | "application/AML" + | "application/andrew-inset" + | "application/applefile" + | "application/at+jwt" + | "application/ATF" + | "application/ATFX" + | "application/atom+xml" + | "application/atomcat+xml" + | "application/atomdeleted+xml" + | "application/atomicmail" + | "application/atomsvc+xml" + | "application/atsc-dwd+xml" + | "application/atsc-dynamic-event-message" + | "application/atsc-held+xml" + | "application/atsc-rdt+json" + | "application/atsc-rsat+xml" + | "application/ATXML" + | "application/auth-policy+xml" + | "application/automationml-aml+xml" + | "application/automationml-amlx+zip" + | "application/bacnet-xdd+zip" + | "application/batch-SMTP" + | "application/beep+xml" + | "application/c2pa" + | "application/calendar+json" + | "application/calendar+xml" + | "application/call-completion" + | "application/CALS-1840" + | "application/captive+json" + | "application/cbor" + | "application/cbor-seq" + | "application/cccex" + | "application/ccmp+xml" + | "application/ccxml+xml" + | "application/cda+xml" + | "application/CDFX+XML" + | "application/cdmi-capability" + | "application/cdmi-container" + | "application/cdmi-domain" + | "application/cdmi-object" + | "application/cdmi-queue" + | "application/cdni" + | "application/CEA" + | "application/cea-2018+xml" + | "application/cellml+xml" + | "application/cfw" + | "application/cid-edhoc+cbor-seq" + | "application/city+json" + | "application/clr" + | "application/clue_info+xml" + | "application/clue+xml" + | "application/cms" + | "application/cnrp+xml" + | "application/coap-group+json" + | "application/coap-payload" + | "application/commonground" + | "application/concise-problem-details+cbor" + | "application/conference-info+xml" + | "application/cpl+xml" + | "application/cose" + | "application/cose-key" + | "application/cose-key-set" + | "application/cose-x509" + | "application/csrattrs" + | "application/csta+xml" + | "application/CSTAdata+xml" + | "application/csvm+json" + | "application/cwl" + | "application/cwl+json" + | "application/cwt" + | "application/cybercash" + | "application/dash+xml" + | "application/dash-patch+xml" + | "application/dashdelta" + | "application/davmount+xml" + | "application/dca-rft" + | "application/DCD" + | "application/dec-dx" + | "application/dialog-info+xml" + | "application/dicom" + | "application/dicom+json" + | "application/dicom+xml" + | "application/DII" + | "application/DIT" + | "application/dns" + | "application/dns+json" + | "application/dns-message" + | "application/dots+cbor" + | "application/dpop+jwt" + | "application/dskpp+xml" + | "application/dssc+der" + | "application/dssc+xml" + | "application/dvcs" + | "application/ecmascript" + | "application/edhoc+cbor-seq" + | "application/EDI-consent" + | "application/EDIFACT" + | "application/EDI-X12" + | "application/efi" + | "application/elm+json" + | "application/elm+xml" + | "application/EmergencyCallData.cap+xml" + | "application/EmergencyCallData.Comment+xml" + | "application/EmergencyCallData.Control+xml" + | "application/EmergencyCallData.DeviceInfo+xml" + | "application/EmergencyCallData.eCall.MSD" + | "application/EmergencyCallData.LegacyESN+json" + | "application/EmergencyCallData.ProviderInfo+xml" + | "application/EmergencyCallData.ServiceInfo+xml" + | "application/EmergencyCallData.SubscriberInfo+xml" + | "application/EmergencyCallData.VEDS+xml" + | "application/emma+xml" + | "application/emotionml+xml" + | "application/encaprtp" + | "application/epp+xml" + | "application/epub+zip" + | "application/eshop" + | "application/example" + | "application/exi" + | "application/expect-ct-report+json" + | "application/express" + | "application/fastinfoset" + | "application/fastsoap" + | "application/fdf" + | "application/fdt+xml" + | "application/fhir+json" + | "application/fhir+xml" + | "application/fits" + | "application/flexfec" + | "application/font-sfnt" + | "application/font-tdpfr" + | "application/font-woff" + | "application/framework-attributes+xml" + | "application/geo+json" + | "application/geo+json-seq" + | "application/geopackage+sqlite3" + | "application/geoxacml+json" + | "application/geoxacml+xml" + | "application/gltf-buffer" + | "application/gml+xml" + | "application/gzip" + | "application/H224" + | "application/held+xml" + | "application/hl7v2+xml" + | "application/http" + | "application/hyperstudio" + | "application/ibe-key-request+xml" + | "application/ibe-pkg-reply+xml" + | "application/ibe-pp-data" + | "application/iges" + | "application/im-iscomposing+xml" + | "application/index" + | "application/index.cmd" + | "application/index.obj" + | "application/index.response" + | "application/index.vnd" + | "application/inkml+xml" + | "application/IOTP" + | "application/ipfix" + | "application/ipp" + | "application/ISUP" + | "application/its+xml" + | "application/java-archive" + | "application/javascript" + | "application/jf2feed+json" + | "application/jose" + | "application/jose+json" + | "application/jrd+json" + | "application/jscalendar+json" + | "application/jscontact+json" + | "application/json" + | "application/json-patch+json" + | "application/json-seq" + | "application/jsonpath" + | "application/jwk+json" + | "application/jwk-set+json" + | "application/jwt" + | "application/kpml-request+xml" + | "application/kpml-response+xml" + | "application/ld+json" + | "application/lgr+xml" + | "application/link-format" + | "application/linkset" + | "application/linkset+json" + | "application/load-control+xml" + | "application/logout+jwt" + | "application/lost+xml" + | "application/lostsync+xml" + | "application/lpf+zip" + | "application/LXF" + | "application/mac-binhex40" + | "application/macwriteii" + | "application/mads+xml" + | "application/manifest+json" + | "application/marc" + | "application/marcxml+xml" + | "application/mathematica" + | "application/mathml+xml" + | "application/mathml-content+xml" + | "application/mathml-presentation+xml" + | "application/mbms-associated-procedure-description+xml" + | "application/mbms-deregister+xml" + | "application/mbms-envelope+xml" + | "application/mbms-msk-response+xml" + | "application/mbms-msk+xml" + | "application/mbms-protection-description+xml" + | "application/mbms-reception-report+xml" + | "application/mbms-register-response+xml" + | "application/mbms-register+xml" + | "application/mbms-schedule+xml" + | "application/mbms-user-service-description+xml" + | "application/mbox" + | "application/media_control+xml" + | "application/media-policy-dataset+xml" + | "application/mediaservercontrol+xml" + | "application/merge-patch+json" + | "application/metalink4+xml" + | "application/mets+xml" + | "application/MF4" + | "application/mikey" + | "application/mipc" + | "application/missing-blocks+cbor-seq" + | "application/mmt-aei+xml" + | "application/mmt-usd+xml" + | "application/mods+xml" + | "application/moss-keys" + | "application/moss-signature" + | "application/mosskey-data" + | "application/mosskey-request" + | "application/mp21" + | "application/mp4" + | "application/mpeg4-generic" + | "application/mpeg4-iod" + | "application/mpeg4-iod-xmt" + | "application/mrb-consumer+xml" + | "application/mrb-publish+xml" + | "application/msc-ivr+xml" + | "application/msc-mixer+xml" + | "application/msword" + | "application/mud+json" + | "application/multipart-core" + | "application/mxf" + | "application/n-quads" + | "application/n-triples" + | "application/nasdata" + | "application/news-checkgroups" + | "application/news-groupinfo" + | "application/news-transmission" + | "application/nlsml+xml" + | "application/node" + | "application/nss" + | "application/oauth-authz-req+jwt" + | "application/oblivious-dns-message" + | "application/ocsp-request" + | "application/ocsp-response" + | "application/octet-stream" + | "application/ODA" + | "application/odm+xml" + | "application/ODX" + | "application/oebps-package+xml" + | "application/ogg" + | "application/ohttp-keys" + | "application/opc-nodeset+xml" + | "application/oscore" + | "application/oxps" + | "application/p21" + | "application/p21+zip" + | "application/p2p-overlay+xml" + | "application/parityfec" + | "application/passport" + | "application/patch-ops-error+xml" + | "application/pdf" + | "application/PDX" + | "application/pem-certificate-chain" + | "application/pgp-encrypted" + | "application/pgp-keys" + | "application/pgp-signature" + | "application/pidf-diff+xml" + | "application/pidf+xml" + | "application/pkcs10" + | "application/pkcs7-mime" + | "application/pkcs7-signature" + | "application/pkcs8" + | "application/pkcs8-encrypted" + | "application/pkcs12" + | "application/pkix-attr-cert" + | "application/pkix-cert" + | "application/pkix-crl" + | "application/pkix-pkipath" + | "application/pkixcmp" + | "application/pls+xml" + | "application/poc-settings+xml" + | "application/postscript" + | "application/ppsp-tracker+json" + | "application/private-token-issuer-directory" + | "application/private-token-request" + | "application/private-token-response" + | "application/problem+json" + | "application/problem+xml" + | "application/provenance+xml" + | "application/prs.alvestrand.titrax-sheet" + | "application/prs.cww" + | "application/prs.cyn" + | "application/prs.hpub+zip" + | "application/prs.implied-document+xml" + | "application/prs.implied-executable" + | "application/prs.implied-object+json" + | "application/prs.implied-object+json-seq" + | "application/prs.implied-object+yaml" + | "application/prs.implied-structure" + | "application/prs.nprend" + | "application/prs.plucker" + | "application/prs.rdf-xml-crypt" + | "application/prs.vcfbzip2" + | "application/prs.xsf+xml" + | "application/pskc+xml" + | "application/pvd+json" + | "application/rdf+xml" + | "application/route-apd+xml" + | "application/route-s-tsid+xml" + | "application/route-usd+xml" + | "application/QSIG" + | "application/raptorfec" + | "application/rdap+json" + | "application/reginfo+xml" + | "application/relax-ng-compact-syntax" + | "application/remote-printing" + | "application/reputon+json" + | "application/resource-lists-diff+xml" + | "application/resource-lists+xml" + | "application/rfc+xml" + | "application/riscos" + | "application/rlmi+xml" + | "application/rls-services+xml" + | "application/rpki-checklist" + | "application/rpki-ghostbusters" + | "application/rpki-manifest" + | "application/rpki-publication" + | "application/rpki-roa" + | "application/rpki-updown" + | "application/rtf" + | "application/rtploopback" + | "application/rtx" + | "application/samlassertion+xml" + | "application/samlmetadata+xml" + | "application/sarif-external-properties+json" + | "application/sarif+json" + | "application/sbe" + | "application/sbml+xml" + | "application/scaip+xml" + | "application/scim+json" + | "application/scvp-cv-request" + | "application/scvp-cv-response" + | "application/scvp-vp-request" + | "application/scvp-vp-response" + | "application/sdp" + | "application/secevent+jwt" + | "application/senml-etch+cbor" + | "application/senml-etch+json" + | "application/senml-exi" + | "application/senml+cbor" + | "application/senml+json" + | "application/senml+xml" + | "application/sensml-exi" + | "application/sensml+cbor" + | "application/sensml+json" + | "application/sensml+xml" + | "application/sep-exi" + | "application/sep+xml" + | "application/session-info" + | "application/set-payment" + | "application/set-payment-initiation" + | "application/set-registration" + | "application/set-registration-initiation" + | "application/SGML" + | "application/sgml-open-catalog" + | "application/shf+xml" + | "application/sieve" + | "application/simple-filter+xml" + | "application/simple-message-summary" + | "application/simpleSymbolContainer" + | "application/sipc" + | "application/slate" + | "application/smil" + | "application/smil+xml" + | "application/smpte336m" + | "application/soap+fastinfoset" + | "application/soap+xml" + | "application/sparql-query" + | "application/spdx+json" + | "application/sparql-results+xml" + | "application/spirits-event+xml" + | "application/sql" + | "application/srgs" + | "application/srgs+xml" + | "application/sru+xml" + | "application/ssml+xml" + | "application/stix+json" + | "application/swid+cbor" + | "application/swid+xml" + | "application/tamp-apex-update" + | "application/tamp-apex-update-confirm" + | "application/tamp-community-update" + | "application/tamp-community-update-confirm" + | "application/tamp-error" + | "application/tamp-sequence-adjust" + | "application/tamp-sequence-adjust-confirm" + | "application/tamp-status-query" + | "application/tamp-status-response" + | "application/tamp-update" + | "application/tamp-update-confirm" + | "application/taxii+json" + | "application/td+json" + | "application/tei+xml" + | "application/TETRA_ISI" + | "application/thraud+xml" + | "application/timestamp-query" + | "application/timestamp-reply" + | "application/timestamped-data" + | "application/tlsrpt+gzip" + | "application/tlsrpt+json" + | "application/tm+json" + | "application/tnauthlist" + | "application/token-introspection+jwt" + | "application/trickle-ice-sdpfrag" + | "application/trig" + | "application/ttml+xml" + | "application/tve-trigger" + | "application/tzif" + | "application/tzif-leap" + | "application/ulpfec" + | "application/urc-grpsheet+xml" + | "application/urc-ressheet+xml" + | "application/urc-targetdesc+xml" + | "application/urc-uisocketdesc+xml" + | "application/vcard+json" + | "application/vcard+xml" + | "application/vemmi" + | "application/vnd.1000minds.decision-model+xml" + | "application/vnd.1ob" + | "application/vnd.3gpp.5gnas" + | "application/vnd.3gpp.access-transfer-events+xml" + | "application/vnd.3gpp.bsf+xml" + | "application/vnd.3gpp.crs+xml" + | "application/vnd.3gpp.current-location-discovery+xml" + | "application/vnd.3gpp.GMOP+xml" + | "application/vnd.3gpp.gtpc" + | "application/vnd.3gpp.interworking-data" + | "application/vnd.3gpp.lpp" + | "application/vnd.3gpp.mc-signalling-ear" + | "application/vnd.3gpp.mcdata-affiliation-command+xml" + | "application/vnd.3gpp.mcdata-info+xml" + | "application/vnd.3gpp.mcdata-msgstore-ctrl-request+xml" + | "application/vnd.3gpp.mcdata-payload" + | "application/vnd.3gpp.mcdata-regroup+xml" + | "application/vnd.3gpp.mcdata-service-config+xml" + | "application/vnd.3gpp.mcdata-signalling" + | "application/vnd.3gpp.mcdata-ue-config+xml" + | "application/vnd.3gpp.mcdata-user-profile+xml" + | "application/vnd.3gpp.mcptt-affiliation-command+xml" + | "application/vnd.3gpp.mcptt-floor-request+xml" + | "application/vnd.3gpp.mcptt-info+xml" + | "application/vnd.3gpp.mcptt-location-info+xml" + | "application/vnd.3gpp.mcptt-mbms-usage-info+xml" + | "application/vnd.3gpp.mcptt-regroup+xml" + | "application/vnd.3gpp.mcptt-service-config+xml" + | "application/vnd.3gpp.mcptt-signed+xml" + | "application/vnd.3gpp.mcptt-ue-config+xml" + | "application/vnd.3gpp.mcptt-ue-init-config+xml" + | "application/vnd.3gpp.mcptt-user-profile+xml" + | "application/vnd.3gpp.mcvideo-affiliation-command+xml" + | "application/vnd.3gpp.mcvideo-affiliation-info+xml" + | "application/vnd.3gpp.mcvideo-info+xml" + | "application/vnd.3gpp.mcvideo-location-info+xml" + | "application/vnd.3gpp.mcvideo-mbms-usage-info+xml" + | "application/vnd.3gpp.mcvideo-regroup+xml" + | "application/vnd.3gpp.mcvideo-service-config+xml" + | "application/vnd.3gpp.mcvideo-transmission-request+xml" + | "application/vnd.3gpp.mcvideo-ue-config+xml" + | "application/vnd.3gpp.mcvideo-user-profile+xml" + | "application/vnd.3gpp.mid-call+xml" + | "application/vnd.3gpp.ngap" + | "application/vnd.3gpp.pfcp" + | "application/vnd.3gpp.pic-bw-large" + | "application/vnd.3gpp.pic-bw-small" + | "application/vnd.3gpp.pic-bw-var" + | "application/vnd.3gpp-prose-pc3a+xml" + | "application/vnd.3gpp-prose-pc3ach+xml" + | "application/vnd.3gpp-prose-pc3ch+xml" + | "application/vnd.3gpp-prose-pc8+xml" + | "application/vnd.3gpp-prose+xml" + | "application/vnd.3gpp.s1ap" + | "application/vnd.3gpp.seal-group-doc+xml" + | "application/vnd.3gpp.seal-info+xml" + | "application/vnd.3gpp.seal-location-info+xml" + | "application/vnd.3gpp.seal-mbms-usage-info+xml" + | "application/vnd.3gpp.seal-network-QoS-management-info+xml" + | "application/vnd.3gpp.seal-ue-config-info+xml" + | "application/vnd.3gpp.seal-unicast-info+xml" + | "application/vnd.3gpp.seal-user-profile-info+xml" + | "application/vnd.3gpp.sms" + | "application/vnd.3gpp.sms+xml" + | "application/vnd.3gpp.srvcc-ext+xml" + | "application/vnd.3gpp.SRVCC-info+xml" + | "application/vnd.3gpp.state-and-event-info+xml" + | "application/vnd.3gpp.ussd+xml" + | "application/vnd.3gpp.vae-info+xml" + | "application/vnd.3gpp-v2x-local-service-information" + | "application/vnd.3gpp2.bcmcsinfo+xml" + | "application/vnd.3gpp2.sms" + | "application/vnd.3gpp2.tcap" + | "application/vnd.3gpp.v2x" + | "application/vnd.3lightssoftware.imagescal" + | "application/vnd.3M.Post-it-Notes" + | "application/vnd.accpac.simply.aso" + | "application/vnd.accpac.simply.imp" + | "application/vnd.acm.addressxfer+json" + | "application/vnd.acm.chatbot+json" + | "application/vnd.acucobol" + | "application/vnd.acucorp" + | "application/vnd.adobe.flash.movie" + | "application/vnd.adobe.formscentral.fcdt" + | "application/vnd.adobe.fxp" + | "application/vnd.adobe.partial-upload" + | "application/vnd.adobe.xdp+xml" + | "application/vnd.aether.imp" + | "application/vnd.afpc.afplinedata" + | "application/vnd.afpc.afplinedata-pagedef" + | "application/vnd.afpc.cmoca-cmresource" + | "application/vnd.afpc.foca-charset" + | "application/vnd.afpc.foca-codedfont" + | "application/vnd.afpc.foca-codepage" + | "application/vnd.afpc.modca" + | "application/vnd.afpc.modca-cmtable" + | "application/vnd.afpc.modca-formdef" + | "application/vnd.afpc.modca-mediummap" + | "application/vnd.afpc.modca-objectcontainer" + | "application/vnd.afpc.modca-overlay" + | "application/vnd.afpc.modca-pagesegment" + | "application/vnd.age" + | "application/vnd.ah-barcode" + | "application/vnd.ahead.space" + | "application/vnd.airzip.filesecure.azf" + | "application/vnd.airzip.filesecure.azs" + | "application/vnd.amadeus+json" + | "application/vnd.amazon.mobi8-ebook" + | "application/vnd.americandynamics.acc" + | "application/vnd.amiga.ami" + | "application/vnd.amundsen.maze+xml" + | "application/vnd.android.ota" + | "application/vnd.anki" + | "application/vnd.anser-web-certificate-issue-initiation" + | "application/vnd.antix.game-component" + | "application/vnd.apache.arrow.file" + | "application/vnd.apache.arrow.stream" + | "application/vnd.apache.thrift.binary" + | "application/vnd.apache.thrift.compact" + | "application/vnd.apache.thrift.json" + | "application/vnd.apexlang" + | "application/vnd.api+json" + | "application/vnd.aplextor.warrp+json" + | "application/vnd.apothekende.reservation+json" + | "application/vnd.apple.installer+xml" + | "application/vnd.apple.keynote" + | "application/vnd.apple.mpegurl" + | "application/vnd.apple.numbers" + | "application/vnd.apple.pages" + | "application/vnd.arastra.swi" + | "application/vnd.aristanetworks.swi" + | "application/vnd.artisan+json" + | "application/vnd.artsquare" + | "application/vnd.astraea-software.iota" + | "application/vnd.audiograph" + | "application/vnd.autopackage" + | "application/vnd.avalon+json" + | "application/vnd.avistar+xml" + | "application/vnd.balsamiq.bmml+xml" + | "application/vnd.banana-accounting" + | "application/vnd.bbf.usp.error" + | "application/vnd.bbf.usp.msg" + | "application/vnd.bbf.usp.msg+json" + | "application/vnd.balsamiq.bmpr" + | "application/vnd.bekitzur-stech+json" + | "application/vnd.belightsoft.lhzd+zip" + | "application/vnd.belightsoft.lhzl+zip" + | "application/vnd.bint.med-content" + | "application/vnd.biopax.rdf+xml" + | "application/vnd.blink-idb-value-wrapper" + | "application/vnd.blueice.multipass" + | "application/vnd.bluetooth.ep.oob" + | "application/vnd.bluetooth.le.oob" + | "application/vnd.bmi" + | "application/vnd.bpf" + | "application/vnd.bpf3" + | "application/vnd.businessobjects" + | "application/vnd.byu.uapi+json" + | "application/vnd.bzip3" + | "application/vnd.cab-jscript" + | "application/vnd.canon-cpdl" + | "application/vnd.canon-lips" + | "application/vnd.capasystems-pg+json" + | "application/vnd.cendio.thinlinc.clientconf" + | "application/vnd.century-systems.tcp_stream" + | "application/vnd.chemdraw+xml" + | "application/vnd.chess-pgn" + | "application/vnd.chipnuts.karaoke-mmd" + | "application/vnd.ciedi" + | "application/vnd.cinderella" + | "application/vnd.cirpack.isdn-ext" + | "application/vnd.citationstyles.style+xml" + | "application/vnd.claymore" + | "application/vnd.cloanto.rp9" + | "application/vnd.clonk.c4group" + | "application/vnd.cluetrust.cartomobile-config" + | "application/vnd.cluetrust.cartomobile-config-pkg" + | "application/vnd.cncf.helm.chart.content.v1.tar+gzip" + | "application/vnd.cncf.helm.chart.provenance.v1.prov" + | "application/vnd.cncf.helm.config.v1+json" + | "application/vnd.coffeescript" + | "application/vnd.collabio.xodocuments.document" + | "application/vnd.collabio.xodocuments.document-template" + | "application/vnd.collabio.xodocuments.presentation" + | "application/vnd.collabio.xodocuments.presentation-template" + | "application/vnd.collabio.xodocuments.spreadsheet" + | "application/vnd.collabio.xodocuments.spreadsheet-template" + | "application/vnd.collection.doc+json" + | "application/vnd.collection+json" + | "application/vnd.collection.next+json" + | "application/vnd.comicbook-rar" + | "application/vnd.comicbook+zip" + | "application/vnd.commerce-battelle" + | "application/vnd.commonspace" + | "application/vnd.coreos.ignition+json" + | "application/vnd.cosmocaller" + | "application/vnd.contact.cmsg" + | "application/vnd.crick.clicker" + | "application/vnd.crick.clicker.keyboard" + | "application/vnd.crick.clicker.palette" + | "application/vnd.crick.clicker.template" + | "application/vnd.crick.clicker.wordbank" + | "application/vnd.criticaltools.wbs+xml" + | "application/vnd.cryptii.pipe+json" + | "application/vnd.crypto-shade-file" + | "application/vnd.cryptomator.encrypted" + | "application/vnd.cryptomator.vault" + | "application/vnd.ctc-posml" + | "application/vnd.ctct.ws+xml" + | "application/vnd.cups-pdf" + | "application/vnd.cups-postscript" + | "application/vnd.cups-ppd" + | "application/vnd.cups-raster" + | "application/vnd.cups-raw" + | "application/vnd.curl" + | "application/vnd.cyan.dean.root+xml" + | "application/vnd.cybank" + | "application/vnd.cyclonedx+json" + | "application/vnd.cyclonedx+xml" + | "application/vnd.d2l.coursepackage1p0+zip" + | "application/vnd.d3m-dataset" + | "application/vnd.d3m-problem" + | "application/vnd.dart" + | "application/vnd.data-vision.rdz" + | "application/vnd.datalog" + | "application/vnd.datapackage+json" + | "application/vnd.dataresource+json" + | "application/vnd.dbf" + | "application/vnd.debian.binary-package" + | "application/vnd.dece.data" + | "application/vnd.dece.ttml+xml" + | "application/vnd.dece.unspecified" + | "application/vnd.dece.zip" + | "application/vnd.denovo.fcselayout-link" + | "application/vnd.desmume.movie" + | "application/vnd.dir-bi.plate-dl-nosuffix" + | "application/vnd.dm.delegation+xml" + | "application/vnd.dna" + | "application/vnd.document+json" + | "application/vnd.dolby.mobile.1" + | "application/vnd.dolby.mobile.2" + | "application/vnd.doremir.scorecloud-binary-document" + | "application/vnd.dpgraph" + | "application/vnd.dreamfactory" + | "application/vnd.drive+json" + | "application/vnd.dtg.local" + | "application/vnd.dtg.local.flash" + | "application/vnd.dtg.local.html" + | "application/vnd.dvb.ait" + | "application/vnd.dvb.dvbisl+xml" + | "application/vnd.dvb.dvbj" + | "application/vnd.dvb.esgcontainer" + | "application/vnd.dvb.ipdcdftnotifaccess" + | "application/vnd.dvb.ipdcesgaccess" + | "application/vnd.dvb.ipdcesgaccess2" + | "application/vnd.dvb.ipdcesgpdd" + | "application/vnd.dvb.ipdcroaming" + | "application/vnd.dvb.iptv.alfec-base" + | "application/vnd.dvb.iptv.alfec-enhancement" + | "application/vnd.dvb.notif-aggregate-root+xml" + | "application/vnd.dvb.notif-container+xml" + | "application/vnd.dvb.notif-generic+xml" + | "application/vnd.dvb.notif-ia-msglist+xml" + | "application/vnd.dvb.notif-ia-registration-request+xml" + | "application/vnd.dvb.notif-ia-registration-response+xml" + | "application/vnd.dvb.notif-init+xml" + | "application/vnd.dvb.pfr" + | "application/vnd.dvb.service" + | "application/vnd.dxr" + | "application/vnd.dynageo" + | "application/vnd.dzr" + | "application/vnd.easykaraoke.cdgdownload" + | "application/vnd.ecip.rlp" + | "application/vnd.ecdis-update" + | "application/vnd.eclipse.ditto+json" + | "application/vnd.ecowin.chart" + | "application/vnd.ecowin.filerequest" + | "application/vnd.ecowin.fileupdate" + | "application/vnd.ecowin.series" + | "application/vnd.ecowin.seriesrequest" + | "application/vnd.ecowin.seriesupdate" + | "application/vnd.efi.img" + | "application/vnd.efi.iso" + | "application/vnd.eln+zip" + | "application/vnd.emclient.accessrequest+xml" + | "application/vnd.enliven" + | "application/vnd.enphase.envoy" + | "application/vnd.eprints.data+xml" + | "application/vnd.epson.esf" + | "application/vnd.epson.msf" + | "application/vnd.epson.quickanime" + | "application/vnd.epson.salt" + | "application/vnd.epson.ssf" + | "application/vnd.ericsson.quickcall" + | "application/vnd.erofs" + | "application/vnd.espass-espass+zip" + | "application/vnd.eszigno3+xml" + | "application/vnd.etsi.aoc+xml" + | "application/vnd.etsi.asic-s+zip" + | "application/vnd.etsi.asic-e+zip" + | "application/vnd.etsi.cug+xml" + | "application/vnd.etsi.iptvcommand+xml" + | "application/vnd.etsi.iptvdiscovery+xml" + | "application/vnd.etsi.iptvprofile+xml" + | "application/vnd.etsi.iptvsad-bc+xml" + | "application/vnd.etsi.iptvsad-cod+xml" + | "application/vnd.etsi.iptvsad-npvr+xml" + | "application/vnd.etsi.iptvservice+xml" + | "application/vnd.etsi.iptvsync+xml" + | "application/vnd.etsi.iptvueprofile+xml" + | "application/vnd.etsi.mcid+xml" + | "application/vnd.etsi.mheg5" + | "application/vnd.etsi.overload-control-policy-dataset+xml" + | "application/vnd.etsi.pstn+xml" + | "application/vnd.etsi.sci+xml" + | "application/vnd.etsi.simservs+xml" + | "application/vnd.etsi.timestamp-token" + | "application/vnd.etsi.tsl+xml" + | "application/vnd.etsi.tsl.der" + | "application/vnd.eu.kasparian.car+json" + | "application/vnd.eudora.data" + | "application/vnd.evolv.ecig.profile" + | "application/vnd.evolv.ecig.settings" + | "application/vnd.evolv.ecig.theme" + | "application/vnd.exstream-empower+zip" + | "application/vnd.exstream-package" + | "application/vnd.ezpix-album" + | "application/vnd.ezpix-package" + | "application/vnd.f-secure.mobile" + | "application/vnd.fastcopy-disk-image" + | "application/vnd.familysearch.gedcom+zip" + | "application/vnd.fdsn.mseed" + | "application/vnd.fdsn.seed" + | "application/vnd.ffsns" + | "application/vnd.ficlab.flb+zip" + | "application/vnd.filmit.zfc" + | "application/vnd.fints" + | "application/vnd.firemonkeys.cloudcell" + | "application/vnd.FloGraphIt" + | "application/vnd.fluxtime.clip" + | "application/vnd.font-fontforge-sfd" + | "application/vnd.framemaker" + | "application/vnd.freelog.comic" + | "application/vnd.frogans.fnc" + | "application/vnd.frogans.ltf" + | "application/vnd.fsc.weblaunch" + | "application/vnd.fujifilm.fb.docuworks" + | "application/vnd.fujifilm.fb.docuworks.binder" + | "application/vnd.fujifilm.fb.docuworks.container" + | "application/vnd.fujifilm.fb.jfi+xml" + | "application/vnd.fujitsu.oasys" + | "application/vnd.fujitsu.oasys2" + | "application/vnd.fujitsu.oasys3" + | "application/vnd.fujitsu.oasysgp" + | "application/vnd.fujitsu.oasysprs" + | "application/vnd.fujixerox.ART4" + | "application/vnd.fujixerox.ART-EX" + | "application/vnd.fujixerox.ddd" + | "application/vnd.fujixerox.docuworks" + | "application/vnd.fujixerox.docuworks.binder" + | "application/vnd.fujixerox.docuworks.container" + | "application/vnd.fujixerox.HBPL" + | "application/vnd.fut-misnet" + | "application/vnd.futoin+cbor" + | "application/vnd.futoin+json" + | "application/vnd.fuzzysheet" + | "application/vnd.genomatix.tuxedo" + | "application/vnd.genozip" + | "application/vnd.gentics.grd+json" + | "application/vnd.gentoo.catmetadata+xml" + | "application/vnd.gentoo.ebuild" + | "application/vnd.gentoo.eclass" + | "application/vnd.gentoo.gpkg" + | "application/vnd.gentoo.manifest" + | "application/vnd.gentoo.xpak" + | "application/vnd.gentoo.pkgmetadata+xml" + | "application/vnd.geo+json" + | "application/vnd.geocube+xml" + | "application/vnd.geogebra.file" + | "application/vnd.geogebra.slides" + | "application/vnd.geogebra.tool" + | "application/vnd.geometry-explorer" + | "application/vnd.geonext" + | "application/vnd.geoplan" + | "application/vnd.geospace" + | "application/vnd.gerber" + | "application/vnd.globalplatform.card-content-mgt" + | "application/vnd.globalplatform.card-content-mgt-response" + | "application/vnd.gmx" + | "application/vnd.gnu.taler.exchange+json" + | "application/vnd.gnu.taler.merchant+json" + | "application/vnd.google-earth.kml+xml" + | "application/vnd.google-earth.kmz" + | "application/vnd.gov.sk.e-form+xml" + | "application/vnd.gov.sk.e-form+zip" + | "application/vnd.gov.sk.xmldatacontainer+xml" + | "application/vnd.gpxsee.map+xml" + | "application/vnd.grafeq" + | "application/vnd.gridmp" + | "application/vnd.groove-account" + | "application/vnd.groove-help" + | "application/vnd.groove-identity-message" + | "application/vnd.groove-injector" + | "application/vnd.groove-tool-message" + | "application/vnd.groove-tool-template" + | "application/vnd.groove-vcard" + | "application/vnd.hal+json" + | "application/vnd.hal+xml" + | "application/vnd.HandHeld-Entertainment+xml" + | "application/vnd.hbci" + | "application/vnd.hc+json" + | "application/vnd.hcl-bireports" + | "application/vnd.hdt" + | "application/vnd.heroku+json" + | "application/vnd.hhe.lesson-player" + | "application/vnd.hp-HPGL" + | "application/vnd.hp-hpid" + | "application/vnd.hp-hps" + | "application/vnd.hp-jlyt" + | "application/vnd.hp-PCL" + | "application/vnd.hp-PCLXL" + | "application/vnd.hsl" + | "application/vnd.httphone" + | "application/vnd.hydrostatix.sof-data" + | "application/vnd.hyper-item+json" + | "application/vnd.hyper+json" + | "application/vnd.hyperdrive+json" + | "application/vnd.hzn-3d-crossword" + | "application/vnd.ibm.afplinedata" + | "application/vnd.ibm.electronic-media" + | "application/vnd.ibm.MiniPay" + | "application/vnd.ibm.modcap" + | "application/vnd.ibm.rights-management" + | "application/vnd.ibm.secure-container" + | "application/vnd.iccprofile" + | "application/vnd.ieee.1905" + | "application/vnd.igloader" + | "application/vnd.imagemeter.folder+zip" + | "application/vnd.imagemeter.image+zip" + | "application/vnd.immervision-ivp" + | "application/vnd.immervision-ivu" + | "application/vnd.ims.imsccv1p1" + | "application/vnd.ims.imsccv1p2" + | "application/vnd.ims.imsccv1p3" + | "application/vnd.ims.lis.v2.result+json" + | "application/vnd.ims.lti.v2.toolconsumerprofile+json" + | "application/vnd.ims.lti.v2.toolproxy.id+json" + | "application/vnd.ims.lti.v2.toolproxy+json" + | "application/vnd.ims.lti.v2.toolsettings+json" + | "application/vnd.ims.lti.v2.toolsettings.simple+json" + | "application/vnd.informedcontrol.rms+xml" + | "application/vnd.infotech.project" + | "application/vnd.infotech.project+xml" + | "application/vnd.informix-visionary" + | "application/vnd.innopath.wamp.notification" + | "application/vnd.insors.igm" + | "application/vnd.intercon.formnet" + | "application/vnd.intergeo" + | "application/vnd.intertrust.digibox" + | "application/vnd.intertrust.nncp" + | "application/vnd.intu.qbo" + | "application/vnd.intu.qfx" + | "application/vnd.ipfs.ipns-record" + | "application/vnd.ipld.car" + | "application/vnd.ipld.dag-cbor" + | "application/vnd.ipld.dag-json" + | "application/vnd.ipld.raw" + | "application/vnd.iptc.g2.catalogitem+xml" + | "application/vnd.iptc.g2.conceptitem+xml" + | "application/vnd.iptc.g2.knowledgeitem+xml" + | "application/vnd.iptc.g2.newsitem+xml" + | "application/vnd.iptc.g2.newsmessage+xml" + | "application/vnd.iptc.g2.packageitem+xml" + | "application/vnd.iptc.g2.planningitem+xml" + | "application/vnd.ipunplugged.rcprofile" + | "application/vnd.irepository.package+xml" + | "application/vnd.is-xpr" + | "application/vnd.isac.fcs" + | "application/vnd.jam" + | "application/vnd.iso11783-10+zip" + | "application/vnd.japannet-directory-service" + | "application/vnd.japannet-jpnstore-wakeup" + | "application/vnd.japannet-payment-wakeup" + | "application/vnd.japannet-registration" + | "application/vnd.japannet-registration-wakeup" + | "application/vnd.japannet-setstore-wakeup" + | "application/vnd.japannet-verification" + | "application/vnd.japannet-verification-wakeup" + | "application/vnd.jcp.javame.midlet-rms" + | "application/vnd.jisp" + | "application/vnd.joost.joda-archive" + | "application/vnd.jsk.isdn-ngn" + | "application/vnd.kahootz" + | "application/vnd.kde.karbon" + | "application/vnd.kde.kchart" + | "application/vnd.kde.kformula" + | "application/vnd.kde.kivio" + | "application/vnd.kde.kontour" + | "application/vnd.kde.kpresenter" + | "application/vnd.kde.kspread" + | "application/vnd.kde.kword" + | "application/vnd.kenameaapp" + | "application/vnd.kidspiration" + | "application/vnd.Kinar" + | "application/vnd.koan" + | "application/vnd.kodak-descriptor" + | "application/vnd.las" + | "application/vnd.las.las+json" + | "application/vnd.las.las+xml" + | "application/vnd.laszip" + | "application/vnd.ldev.productlicensing" + | "application/vnd.leap+json" + | "application/vnd.liberty-request+xml" + | "application/vnd.llamagraphics.life-balance.desktop" + | "application/vnd.llamagraphics.life-balance.exchange+xml" + | "application/vnd.logipipe.circuit+zip" + | "application/vnd.loom" + | "application/vnd.lotus-1-2-3" + | "application/vnd.lotus-approach" + | "application/vnd.lotus-freelance" + | "application/vnd.lotus-notes" + | "application/vnd.lotus-organizer" + | "application/vnd.lotus-screencam" + | "application/vnd.lotus-wordpro" + | "application/vnd.macports.portpkg" + | "application/vnd.mapbox-vector-tile" + | "application/vnd.marlin.drm.actiontoken+xml" + | "application/vnd.marlin.drm.conftoken+xml" + | "application/vnd.marlin.drm.license+xml" + | "application/vnd.marlin.drm.mdcf" + | "application/vnd.mason+json" + | "application/vnd.maxar.archive.3tz+zip" + | "application/vnd.maxmind.maxmind-db" + | "application/vnd.mcd" + | "application/vnd.mdl" + | "application/vnd.mdl-mbsdf" + | "application/vnd.medcalcdata" + | "application/vnd.mediastation.cdkey" + | "application/vnd.medicalholodeck.recordxr" + | "application/vnd.meridian-slingshot" + | "application/vnd.mermaid" + | "application/vnd.MFER" + | "application/vnd.mfmp" + | "application/vnd.micro+json" + | "application/vnd.micrografx.flo" + | "application/vnd.micrografx.igx" + | "application/vnd.microsoft.portable-executable" + | "application/vnd.microsoft.windows.thumbnail-cache" + | "application/vnd.miele+json" + | "application/vnd.mif" + | "application/vnd.minisoft-hp3000-save" + | "application/vnd.mitsubishi.misty-guard.trustweb" + | "application/vnd.Mobius.DAF" + | "application/vnd.Mobius.DIS" + | "application/vnd.Mobius.MBK" + | "application/vnd.Mobius.MQY" + | "application/vnd.Mobius.MSL" + | "application/vnd.Mobius.PLC" + | "application/vnd.Mobius.TXF" + | "application/vnd.modl" + | "application/vnd.mophun.application" + | "application/vnd.mophun.certificate" + | "application/vnd.motorola.flexsuite" + | "application/vnd.motorola.flexsuite.adsi" + | "application/vnd.motorola.flexsuite.fis" + | "application/vnd.motorola.flexsuite.gotap" + | "application/vnd.motorola.flexsuite.kmr" + | "application/vnd.motorola.flexsuite.ttc" + | "application/vnd.motorola.flexsuite.wem" + | "application/vnd.motorola.iprm" + | "application/vnd.mozilla.xul+xml" + | "application/vnd.ms-artgalry" + | "application/vnd.ms-asf" + | "application/vnd.ms-cab-compressed" + | "application/vnd.ms-3mfdocument" + | "application/vnd.ms-excel" + | "application/vnd.ms-excel.addin.macroEnabled.12" + | "application/vnd.ms-excel.sheet.binary.macroEnabled.12" + | "application/vnd.ms-excel.sheet.macroEnabled.12" + | "application/vnd.ms-excel.template.macroEnabled.12" + | "application/vnd.ms-fontobject" + | "application/vnd.ms-htmlhelp" + | "application/vnd.ms-ims" + | "application/vnd.ms-lrm" + | "application/vnd.ms-office.activeX+xml" + | "application/vnd.ms-officetheme" + | "application/vnd.ms-playready.initiator+xml" + | "application/vnd.ms-powerpoint" + | "application/vnd.ms-powerpoint.addin.macroEnabled.12" + | "application/vnd.ms-powerpoint.presentation.macroEnabled.12" + | "application/vnd.ms-powerpoint.slide.macroEnabled.12" + | "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" + | "application/vnd.ms-powerpoint.template.macroEnabled.12" + | "application/vnd.ms-PrintDeviceCapabilities+xml" + | "application/vnd.ms-PrintSchemaTicket+xml" + | "application/vnd.ms-project" + | "application/vnd.ms-tnef" + | "application/vnd.ms-windows.devicepairing" + | "application/vnd.ms-windows.nwprinting.oob" + | "application/vnd.ms-windows.printerpairing" + | "application/vnd.ms-windows.wsd.oob" + | "application/vnd.ms-wmdrm.lic-chlg-req" + | "application/vnd.ms-wmdrm.lic-resp" + | "application/vnd.ms-wmdrm.meter-chlg-req" + | "application/vnd.ms-wmdrm.meter-resp" + | "application/vnd.ms-word.document.macroEnabled.12" + | "application/vnd.ms-word.template.macroEnabled.12" + | "application/vnd.ms-works" + | "application/vnd.ms-wpl" + | "application/vnd.ms-xpsdocument" + | "application/vnd.msa-disk-image" + | "application/vnd.mseq" + | "application/vnd.msign" + | "application/vnd.multiad.creator" + | "application/vnd.multiad.creator.cif" + | "application/vnd.musician" + | "application/vnd.music-niff" + | "application/vnd.muvee.style" + | "application/vnd.mynfc" + | "application/vnd.nacamar.ybrid+json" + | "application/vnd.nato.bindingdataobject+cbor" + | "application/vnd.nato.bindingdataobject+json" + | "application/vnd.nato.bindingdataobject+xml" + | "application/vnd.nato.openxmlformats-package.iepd+zip" + | "application/vnd.ncd.control" + | "application/vnd.ncd.reference" + | "application/vnd.nearst.inv+json" + | "application/vnd.nebumind.line" + | "application/vnd.nervana" + | "application/vnd.netfpx" + | "application/vnd.neurolanguage.nlu" + | "application/vnd.nimn" + | "application/vnd.nintendo.snes.rom" + | "application/vnd.nintendo.nitro.rom" + | "application/vnd.nitf" + | "application/vnd.noblenet-directory" + | "application/vnd.noblenet-sealer" + | "application/vnd.noblenet-web" + | "application/vnd.nokia.catalogs" + | "application/vnd.nokia.conml+wbxml" + | "application/vnd.nokia.conml+xml" + | "application/vnd.nokia.iptv.config+xml" + | "application/vnd.nokia.iSDS-radio-presets" + | "application/vnd.nokia.landmark+wbxml" + | "application/vnd.nokia.landmark+xml" + | "application/vnd.nokia.landmarkcollection+xml" + | "application/vnd.nokia.ncd" + | "application/vnd.nokia.n-gage.ac+xml" + | "application/vnd.nokia.n-gage.data" + | "application/vnd.nokia.n-gage.symbian.install" + | "application/vnd.nokia.pcd+wbxml" + | "application/vnd.nokia.pcd+xml" + | "application/vnd.nokia.radio-preset" + | "application/vnd.nokia.radio-presets" + | "application/vnd.novadigm.EDM" + | "application/vnd.novadigm.EDX" + | "application/vnd.novadigm.EXT" + | "application/vnd.ntt-local.content-share" + | "application/vnd.ntt-local.file-transfer" + | "application/vnd.ntt-local.ogw_remote-access" + | "application/vnd.ntt-local.sip-ta_remote" + | "application/vnd.ntt-local.sip-ta_tcp_stream" + | "application/vnd.oai.workflows" + | "application/vnd.oai.workflows+json" + | "application/vnd.oai.workflows+yaml" + | "application/vnd.oasis.opendocument.base" + | "application/vnd.oasis.opendocument.chart" + | "application/vnd.oasis.opendocument.chart-template" + | "application/vnd.oasis.opendocument.database" + | "application/vnd.oasis.opendocument.formula" + | "application/vnd.oasis.opendocument.formula-template" + | "application/vnd.oasis.opendocument.graphics" + | "application/vnd.oasis.opendocument.graphics-template" + | "application/vnd.oasis.opendocument.image" + | "application/vnd.oasis.opendocument.image-template" + | "application/vnd.oasis.opendocument.presentation" + | "application/vnd.oasis.opendocument.presentation-template" + | "application/vnd.oasis.opendocument.spreadsheet" + | "application/vnd.oasis.opendocument.spreadsheet-template" + | "application/vnd.oasis.opendocument.text" + | "application/vnd.oasis.opendocument.text-master" + | "application/vnd.oasis.opendocument.text-master-template" + | "application/vnd.oasis.opendocument.text-template" + | "application/vnd.oasis.opendocument.text-web" + | "application/vnd.obn" + | "application/vnd.ocf+cbor" + | "application/vnd.oci.image.manifest.v1+json" + | "application/vnd.oftn.l10n+json" + | "application/vnd.oipf.contentaccessdownload+xml" + | "application/vnd.oipf.contentaccessstreaming+xml" + | "application/vnd.oipf.cspg-hexbinary" + | "application/vnd.oipf.dae.svg+xml" + | "application/vnd.oipf.dae.xhtml+xml" + | "application/vnd.oipf.mippvcontrolmessage+xml" + | "application/vnd.oipf.pae.gem" + | "application/vnd.oipf.spdiscovery+xml" + | "application/vnd.oipf.spdlist+xml" + | "application/vnd.oipf.ueprofile+xml" + | "application/vnd.oipf.userprofile+xml" + | "application/vnd.olpc-sugar" + | "application/vnd.oma.bcast.associated-procedure-parameter+xml" + | "application/vnd.oma.bcast.drm-trigger+xml" + | "application/vnd.oma.bcast.imd+xml" + | "application/vnd.oma.bcast.ltkm" + | "application/vnd.oma.bcast.notification+xml" + | "application/vnd.oma.bcast.provisioningtrigger" + | "application/vnd.oma.bcast.sgboot" + | "application/vnd.oma.bcast.sgdd+xml" + | "application/vnd.oma.bcast.sgdu" + | "application/vnd.oma.bcast.simple-symbol-container" + | "application/vnd.oma.bcast.smartcard-trigger+xml" + | "application/vnd.oma.bcast.sprov+xml" + | "application/vnd.oma.bcast.stkm" + | "application/vnd.oma.cab-address-book+xml" + | "application/vnd.oma.cab-feature-handler+xml" + | "application/vnd.oma.cab-pcc+xml" + | "application/vnd.oma.cab-subs-invite+xml" + | "application/vnd.oma.cab-user-prefs+xml" + | "application/vnd.oma.dcd" + | "application/vnd.oma.dcdc" + | "application/vnd.oma.dd2+xml" + | "application/vnd.oma.drm.risd+xml" + | "application/vnd.oma.group-usage-list+xml" + | "application/vnd.oma.lwm2m+cbor" + | "application/vnd.oma.lwm2m+json" + | "application/vnd.oma.lwm2m+tlv" + | "application/vnd.oma.pal+xml" + | "application/vnd.oma.poc.detailed-progress-report+xml" + | "application/vnd.oma.poc.final-report+xml" + | "application/vnd.oma.poc.groups+xml" + | "application/vnd.oma.poc.invocation-descriptor+xml" + | "application/vnd.oma.poc.optimized-progress-report+xml" + | "application/vnd.oma.push" + | "application/vnd.oma.scidm.messages+xml" + | "application/vnd.oma.xcap-directory+xml" + | "application/vnd.omads-email+xml" + | "application/vnd.omads-file+xml" + | "application/vnd.omads-folder+xml" + | "application/vnd.omaloc-supl-init" + | "application/vnd.oma-scws-config" + | "application/vnd.oma-scws-http-request" + | "application/vnd.oma-scws-http-response" + | "application/vnd.onepager" + | "application/vnd.onepagertamp" + | "application/vnd.onepagertamx" + | "application/vnd.onepagertat" + | "application/vnd.onepagertatp" + | "application/vnd.onepagertatx" + | "application/vnd.onvif.metadata" + | "application/vnd.openblox.game-binary" + | "application/vnd.openblox.game+xml" + | "application/vnd.openeye.oeb" + | "application/vnd.openstreetmap.data+xml" + | "application/vnd.opentimestamps.ots" + | "application/vnd.openxmlformats-officedocument.custom-properties+xml" + | "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" + | "application/vnd.openxmlformats-officedocument.drawing+xml" + | "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + | "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml" + | "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml" + | "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml" + | "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" + | "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" + | "application/vnd.openxmlformats-officedocument.extended-properties+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.presentation" + | "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.slide" + | "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.slideshow" + | "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.tags+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.template" + | "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml" + | "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.template" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml" + | "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + | "application/vnd.openxmlformats-officedocument.theme+xml" + | "application/vnd.openxmlformats-officedocument.themeOverride+xml" + | "application/vnd.openxmlformats-officedocument.vmlDrawing" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.template" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml" + | "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml" + | "application/vnd.openxmlformats-package.core-properties+xml" + | "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" + | "application/vnd.openxmlformats-package.relationships+xml" + | "application/vnd.oracle.resource+json" + | "application/vnd.orange.indata" + | "application/vnd.osa.netdeploy" + | "application/vnd.osgeo.mapguide.package" + | "application/vnd.osgi.bundle" + | "application/vnd.osgi.dp" + | "application/vnd.osgi.subsystem" + | "application/vnd.otps.ct-kip+xml" + | "application/vnd.oxli.countgraph" + | "application/vnd.pagerduty+json" + | "application/vnd.palm" + | "application/vnd.panoply" + | "application/vnd.paos.xml" + | "application/vnd.patentdive" + | "application/vnd.patientecommsdoc" + | "application/vnd.pawaafile" + | "application/vnd.pcos" + | "application/vnd.pg.format" + | "application/vnd.pg.osasli" + | "application/vnd.piaccess.application-licence" + | "application/vnd.picsel" + | "application/vnd.pmi.widget" + | "application/vnd.poc.group-advertisement+xml" + | "application/vnd.pocketlearn" + | "application/vnd.powerbuilder6" + | "application/vnd.powerbuilder6-s" + | "application/vnd.powerbuilder7" + | "application/vnd.powerbuilder75" + | "application/vnd.powerbuilder75-s" + | "application/vnd.powerbuilder7-s" + | "application/vnd.preminet" + | "application/vnd.previewsystems.box" + | "application/vnd.proteus.magazine" + | "application/vnd.psfs" + | "application/vnd.pt.mundusmundi" + | "application/vnd.publishare-delta-tree" + | "application/vnd.pvi.ptid1" + | "application/vnd.pwg-multiplexed" + | "application/vnd.pwg-xhtml-print+xml" + | "application/vnd.qualcomm.brew-app-res" + | "application/vnd.quarantainenet" + | "application/vnd.Quark.QuarkXPress" + | "application/vnd.quobject-quoxdocument" + | "application/vnd.radisys.moml+xml" + | "application/vnd.radisys.msml-audit-conf+xml" + | "application/vnd.radisys.msml-audit-conn+xml" + | "application/vnd.radisys.msml-audit-dialog+xml" + | "application/vnd.radisys.msml-audit-stream+xml" + | "application/vnd.radisys.msml-audit+xml" + | "application/vnd.radisys.msml-conf+xml" + | "application/vnd.radisys.msml-dialog-base+xml" + | "application/vnd.radisys.msml-dialog-fax-detect+xml" + | "application/vnd.radisys.msml-dialog-fax-sendrecv+xml" + | "application/vnd.radisys.msml-dialog-group+xml" + | "application/vnd.radisys.msml-dialog-speech+xml" + | "application/vnd.radisys.msml-dialog-transform+xml" + | "application/vnd.radisys.msml-dialog+xml" + | "application/vnd.radisys.msml+xml" + | "application/vnd.rainstor.data" + | "application/vnd.rapid" + | "application/vnd.rar" + | "application/vnd.realvnc.bed" + | "application/vnd.recordare.musicxml" + | "application/vnd.recordare.musicxml+xml" + | "application/vnd.relpipe" + | "application/vnd.RenLearn.rlprint" + | "application/vnd.resilient.logic" + | "application/vnd.restful+json" + | "application/vnd.rig.cryptonote" + | "application/vnd.route66.link66+xml" + | "application/vnd.rs-274x" + | "application/vnd.ruckus.download" + | "application/vnd.s3sms" + | "application/vnd.sailingtracker.track" + | "application/vnd.sar" + | "application/vnd.sbm.cid" + | "application/vnd.sbm.mid2" + | "application/vnd.scribus" + | "application/vnd.sealed.3df" + | "application/vnd.sealed.csf" + | "application/vnd.sealed.doc" + | "application/vnd.sealed.eml" + | "application/vnd.sealed.mht" + | "application/vnd.sealed.net" + | "application/vnd.sealed.ppt" + | "application/vnd.sealed.tiff" + | "application/vnd.sealed.xls" + | "application/vnd.sealedmedia.softseal.html" + | "application/vnd.sealedmedia.softseal.pdf" + | "application/vnd.seemail" + | "application/vnd.seis+json" + | "application/vnd.sema" + | "application/vnd.semd" + | "application/vnd.semf" + | "application/vnd.shade-save-file" + | "application/vnd.shana.informed.formdata" + | "application/vnd.shana.informed.formtemplate" + | "application/vnd.shana.informed.interchange" + | "application/vnd.shana.informed.package" + | "application/vnd.shootproof+json" + | "application/vnd.shopkick+json" + | "application/vnd.shp" + | "application/vnd.shx" + | "application/vnd.sigrok.session" + | "application/vnd.SimTech-MindMapper" + | "application/vnd.siren+json" + | "application/vnd.smaf" + | "application/vnd.smart.notebook" + | "application/vnd.smart.teacher" + | "application/vnd.smintio.portals.archive" + | "application/vnd.snesdev-page-table" + | "application/vnd.software602.filler.form+xml" + | "application/vnd.software602.filler.form-xml-zip" + | "application/vnd.solent.sdkm+xml" + | "application/vnd.spotfire.dxp" + | "application/vnd.spotfire.sfs" + | "application/vnd.sqlite3" + | "application/vnd.sss-cod" + | "application/vnd.sss-dtf" + | "application/vnd.sss-ntf" + | "application/vnd.stepmania.package" + | "application/vnd.stepmania.stepchart" + | "application/vnd.street-stream" + | "application/vnd.sun.wadl+xml" + | "application/vnd.sus-calendar" + | "application/vnd.svd" + | "application/vnd.swiftview-ics" + | "application/vnd.sybyl.mol2" + | "application/vnd.sycle+xml" + | "application/vnd.syft+json" + | "application/vnd.syncml.dm.notification" + | "application/vnd.syncml.dmddf+xml" + | "application/vnd.syncml.dmtnds+wbxml" + | "application/vnd.syncml.dmtnds+xml" + | "application/vnd.syncml.dmddf+wbxml" + | "application/vnd.syncml.dm+wbxml" + | "application/vnd.syncml.dm+xml" + | "application/vnd.syncml.ds.notification" + | "application/vnd.syncml+xml" + | "application/vnd.tableschema+json" + | "application/vnd.tao.intent-module-archive" + | "application/vnd.tcpdump.pcap" + | "application/vnd.think-cell.ppttc+json" + | "application/vnd.tml" + | "application/vnd.tmd.mediaflex.api+xml" + | "application/vnd.tmobile-livetv" + | "application/vnd.tri.onesource" + | "application/vnd.trid.tpt" + | "application/vnd.triscape.mxs" + | "application/vnd.trueapp" + | "application/vnd.truedoc" + | "application/vnd.ubisoft.webplayer" + | "application/vnd.ufdl" + | "application/vnd.uiq.theme" + | "application/vnd.umajin" + | "application/vnd.unity" + | "application/vnd.uoml+xml" + | "application/vnd.uplanet.alert" + | "application/vnd.uplanet.alert-wbxml" + | "application/vnd.uplanet.bearer-choice" + | "application/vnd.uplanet.bearer-choice-wbxml" + | "application/vnd.uplanet.cacheop" + | "application/vnd.uplanet.cacheop-wbxml" + | "application/vnd.uplanet.channel" + | "application/vnd.uplanet.channel-wbxml" + | "application/vnd.uplanet.list" + | "application/vnd.uplanet.listcmd" + | "application/vnd.uplanet.listcmd-wbxml" + | "application/vnd.uplanet.list-wbxml" + | "application/vnd.uri-map" + | "application/vnd.uplanet.signal" + | "application/vnd.valve.source.material" + | "application/vnd.vcx" + | "application/vnd.vd-study" + | "application/vnd.vectorworks" + | "application/vnd.vel+json" + | "application/vnd.verimatrix.vcas" + | "application/vnd.veritone.aion+json" + | "application/vnd.veryant.thin" + | "application/vnd.ves.encrypted" + | "application/vnd.vidsoft.vidconference" + | "application/vnd.visio" + | "application/vnd.visionary" + | "application/vnd.vividence.scriptfile" + | "application/vnd.vsf" + | "application/vnd.wap.sic" + | "application/vnd.wap.slc" + | "application/vnd.wap.wbxml" + | "application/vnd.wap.wmlc" + | "application/vnd.wap.wmlscriptc" + | "application/vnd.wasmflow.wafl" + | "application/vnd.webturbo" + | "application/vnd.wfa.dpp" + | "application/vnd.wfa.p2p" + | "application/vnd.wfa.wsc" + | "application/vnd.windows.devicepairing" + | "application/vnd.wmc" + | "application/vnd.wmf.bootstrap" + | "application/vnd.wolfram.mathematica" + | "application/vnd.wolfram.mathematica.package" + | "application/vnd.wolfram.player" + | "application/vnd.wordlift" + | "application/vnd.wordperfect" + | "application/vnd.wqd" + | "application/vnd.wrq-hp3000-labelled" + | "application/vnd.wt.stf" + | "application/vnd.wv.csp+xml" + | "application/vnd.wv.csp+wbxml" + | "application/vnd.wv.ssp+xml" + | "application/vnd.xacml+json" + | "application/vnd.xara" + | "application/vnd.xecrets-encrypted" + | "application/vnd.xfdl" + | "application/vnd.xfdl.webform" + | "application/vnd.xmi+xml" + | "application/vnd.xmpie.cpkg" + | "application/vnd.xmpie.dpkg" + | "application/vnd.xmpie.plan" + | "application/vnd.xmpie.ppkg" + | "application/vnd.xmpie.xlim" + | "application/vnd.yamaha.hv-dic" + | "application/vnd.yamaha.hv-script" + | "application/vnd.yamaha.hv-voice" + | "application/vnd.yamaha.openscoreformat.osfpvg+xml" + | "application/vnd.yamaha.openscoreformat" + | "application/vnd.yamaha.remote-setup" + | "application/vnd.yamaha.smaf-audio" + | "application/vnd.yamaha.smaf-phrase" + | "application/vnd.yamaha.through-ngn" + | "application/vnd.yamaha.tunnel-udpencap" + | "application/vnd.yaoweme" + | "application/vnd.yellowriver-custom-menu" + | "application/vnd.youtube.yt" + | "application/vnd.zul" + | "application/vnd.zzazz.deck+xml" + | "application/voicexml+xml" + | "application/voucher-cms+json" + | "application/vq-rtcpxr" + | "application/wasm" + | "application/watcherinfo+xml" + | "application/webpush-options+json" + | "application/whoispp-query" + | "application/whoispp-response" + | "application/widget" + | "application/wita" + | "application/wordperfect5.1" + | "application/wsdl+xml" + | "application/wspolicy+xml" + | "application/x-pki-message" + | "application/x-www-form-urlencoded" + | "application/x-x509-ca-cert" + | "application/x-x509-ca-ra-cert" + | "application/x-x509-next-ca-cert" + | "application/x400-bp" + | "application/xacml+xml" + | "application/xcap-att+xml" + | "application/xcap-caps+xml" + | "application/xcap-diff+xml" + | "application/xcap-el+xml" + | "application/xcap-error+xml" + | "application/xcap-ns+xml" + | "application/xcon-conference-info-diff+xml" + | "application/xcon-conference-info+xml" + | "application/xenc+xml" + | "application/xfdf" + | "application/xhtml+xml" + | "application/xliff+xml" + | "application/xml" + | "application/xml-dtd" + | "application/xml-external-parsed-entity" + | "application/xml-patch+xml" + | "application/xmpp+xml" + | "application/xop+xml" + | "application/xslt+xml" + | "application/xv+xml" + | "application/yaml" + | "application/yang" + | "application/yang-data+cbor" + | "application/yang-data+json" + | "application/yang-data+xml" + | "application/yang-patch+json" + | "application/yang-patch+xml" + | "application/yin+xml" + | "application/zip" + | "application/zlib" + | "application/zstd" + | "audio/1d-interleaved-parityfec" + | "audio/32kadpcm" + | "audio/3gpp" + | "audio/3gpp2" + | "audio/aac" + | "audio/ac3" + | "audio/AMR" + | "audio/AMR-WB" + | "audio/amr-wb+" + | "audio/aptx" + | "audio/asc" + | "audio/ATRAC-ADVANCED-LOSSLESS" + | "audio/ATRAC-X" + | "audio/ATRAC3" + | "audio/basic" + | "audio/BV16" + | "audio/BV32" + | "audio/clearmode" + | "audio/CN" + | "audio/DAT12" + | "audio/dls" + | "audio/dsr-es201108" + | "audio/dsr-es202050" + | "audio/dsr-es202211" + | "audio/dsr-es202212" + | "audio/DV" + | "audio/DVI4" + | "audio/eac3" + | "audio/encaprtp" + | "audio/EVRC" + | "audio/EVRC-QCP" + | "audio/EVRC0" + | "audio/EVRC1" + | "audio/EVRCB" + | "audio/EVRCB0" + | "audio/EVRCB1" + | "audio/EVRCNW" + | "audio/EVRCNW0" + | "audio/EVRCNW1" + | "audio/EVRCWB" + | "audio/EVRCWB0" + | "audio/EVRCWB1" + | "audio/EVS" + | "audio/example" + | "audio/flexfec" + | "audio/fwdred" + | "audio/G711-0" + | "audio/G719" + | "audio/G7221" + | "audio/G722" + | "audio/G723" + | "audio/G726-16" + | "audio/G726-24" + | "audio/G726-32" + | "audio/G726-40" + | "audio/G728" + | "audio/G729" + | "audio/G7291" + | "audio/G729D" + | "audio/G729E" + | "audio/GSM" + | "audio/GSM-EFR" + | "audio/GSM-HR-08" + | "audio/iLBC" + | "audio/ip-mr_v2.5" + | "audio/L8" + | "audio/L16" + | "audio/L20" + | "audio/L24" + | "audio/LPC" + | "audio/matroska" + | "audio/MELP" + | "audio/MELP600" + | "audio/MELP1200" + | "audio/MELP2400" + | "audio/mhas" + | "audio/mobile-xmf" + | "audio/MPA" + | "audio/mp4" + | "audio/MP4A-LATM" + | "audio/mpa-robust" + | "audio/mpeg" + | "audio/mpeg4-generic" + | "audio/ogg" + | "audio/opus" + | "audio/parityfec" + | "audio/PCMA" + | "audio/PCMA-WB" + | "audio/PCMU" + | "audio/PCMU-WB" + | "audio/prs.sid" + | "audio/QCELP" + | "audio/raptorfec" + | "audio/RED" + | "audio/rtp-enc-aescm128" + | "audio/rtploopback" + | "audio/rtp-midi" + | "audio/rtx" + | "audio/scip" + | "audio/SMV" + | "audio/SMV0" + | "audio/SMV-QCP" + | "audio/sofa" + | "audio/sp-midi" + | "audio/speex" + | "audio/t140c" + | "audio/t38" + | "audio/telephone-event" + | "audio/TETRA_ACELP" + | "audio/TETRA_ACELP_BB" + | "audio/tone" + | "audio/TSVCIS" + | "audio/UEMCLIP" + | "audio/ulpfec" + | "audio/usac" + | "audio/VDVI" + | "audio/VMR-WB" + | "audio/vnd.3gpp.iufp" + | "audio/vnd.4SB" + | "audio/vnd.audiokoz" + | "audio/vnd.CELP" + | "audio/vnd.cisco.nse" + | "audio/vnd.cmles.radio-events" + | "audio/vnd.cns.anp1" + | "audio/vnd.cns.inf1" + | "audio/vnd.dece.audio" + | "audio/vnd.digital-winds" + | "audio/vnd.dlna.adts" + | "audio/vnd.dolby.heaac.1" + | "audio/vnd.dolby.heaac.2" + | "audio/vnd.dolby.mlp" + | "audio/vnd.dolby.mps" + | "audio/vnd.dolby.pl2" + | "audio/vnd.dolby.pl2x" + | "audio/vnd.dolby.pl2z" + | "audio/vnd.dolby.pulse.1" + | "audio/vnd.dra" + | "audio/vnd.dts" + | "audio/vnd.dts.hd" + | "audio/vnd.dts.uhd" + | "audio/vnd.dvb.file" + | "audio/vnd.everad.plj" + | "audio/vnd.hns.audio" + | "audio/vnd.lucent.voice" + | "audio/vnd.ms-playready.media.pya" + | "audio/vnd.nokia.mobile-xmf" + | "audio/vnd.nortel.vbk" + | "audio/vnd.nuera.ecelp4800" + | "audio/vnd.nuera.ecelp7470" + | "audio/vnd.nuera.ecelp9600" + | "audio/vnd.octel.sbc" + | "audio/vnd.presonus.multitrack" + | "audio/vnd.qcelp" + | "audio/vnd.rhetorex.32kadpcm" + | "audio/vnd.rip" + | "audio/vnd.sealedmedia.softseal.mpeg" + | "audio/vnd.vmx.cvsd" + | "audio/vorbis" + | "audio/vorbis-config" + | "font/collection" + | "font/otf" + | "font/sfnt" + | "font/ttf" + | "font/woff" + | "font/woff2" + | "image/aces" + | "image/apng" + | "image/avci" + | "image/avcs" + | "image/avif" + | "image/bmp" + | "image/cgm" + | "image/dicom-rle" + | "image/dpx" + | "image/emf" + | "image/example" + | "image/fits" + | "image/g3fax" + | "image/heic" + | "image/heic-sequence" + | "image/heif" + | "image/heif-sequence" + | "image/hej2k" + | "image/hsj2" + | "image/j2c" + | "image/jls" + | "image/jp2" + | "image/jph" + | "image/jphc" + | "image/jpm" + | "image/jpx" + | "image/jxr" + | "image/jxrA" + | "image/jxrS" + | "image/jxs" + | "image/jxsc" + | "image/jxsi" + | "image/jxss" + | "image/ktx" + | "image/ktx2" + | "image/naplps" + | "image/png" + | "image/prs.btif" + | "image/prs.pti" + | "image/pwg-raster" + | "image/svg+xml" + | "image/t38" + | "image/tiff" + | "image/tiff-fx" + | "image/vnd.adobe.photoshop" + | "image/vnd.airzip.accelerator.azv" + | "image/vnd.cns.inf2" + | "image/vnd.dece.graphic" + | "image/vnd.djvu" + | "image/vnd.dwg" + | "image/vnd.dxf" + | "image/vnd.dvb.subtitle" + | "image/vnd.fastbidsheet" + | "image/vnd.fpx" + | "image/vnd.fst" + | "image/vnd.fujixerox.edmics-mmr" + | "image/vnd.fujixerox.edmics-rlc" + | "image/vnd.globalgraphics.pgb" + | "image/vnd.microsoft.icon" + | "image/vnd.mix" + | "image/vnd.ms-modi" + | "image/vnd.mozilla.apng" + | "image/vnd.net-fpx" + | "image/vnd.pco.b16" + | "image/vnd.radiance" + | "image/vnd.sealed.png" + | "image/vnd.sealedmedia.softseal.gif" + | "image/vnd.sealedmedia.softseal.jpg" + | "image/vnd.svf" + | "image/vnd.tencent.tap" + | "image/vnd.valve.source.texture" + | "image/vnd.wap.wbmp" + | "image/vnd.xiff" + | "image/vnd.zbrush.pcx" + | "image/webp" + | "image/wmf" + | "image/emf" + | "image/wmf" + | "message/bhttp" + | "message/CPIM" + | "message/delivery-status" + | "message/disposition-notification" + | "message/example" + | "message/feedback-report" + | "message/global" + | "message/global-delivery-status" + | "message/global-disposition-notification" + | "message/global-headers" + | "message/http" + | "message/imdn+xml" + | "message/mls" + | "message/news" + | "message/ohttp-req" + | "message/ohttp-res" + | "message/s-http" + | "message/sip" + | "message/sipfrag" + | "message/tracking-status" + | "message/vnd.si.simp" + | "message/vnd.wfa.wsc" + | "model/3mf" + | "model/e57" + | "model/example" + | "model/gltf-binary" + | "model/gltf+json" + | "model/JT" + | "model/iges" + | "model/mtl" + | "model/obj" + | "model/prc" + | "model/step" + | "model/step+xml" + | "model/step+zip" + | "model/step-xml+zip" + | "model/stl" + | "model/u3d" + | "model/vnd.bary" + | "model/vnd.cld" + | "model/vnd.collada+xml" + | "model/vnd.dwf" + | "model/vnd.flatland.3dml" + | "model/vnd.gdl" + | "model/vnd.gs-gdl" + | "model/vnd.gtw" + | "model/vnd.moml+xml" + | "model/vnd.mts" + | "model/vnd.opengex" + | "model/vnd.parasolid.transmit.binary" + | "model/vnd.parasolid.transmit.text" + | "model/vnd.pytha.pyox" + | "model/vnd.rosette.annotated-data-model" + | "model/vnd.sap.vds" + | "model/vnd.usda" + | "model/vnd.usdz+zip" + | "model/vnd.valve.source.compiled-map" + | "model/vnd.vtu" + | "model/x3d-vrml" + | "model/x3d+fastinfoset" + | "model/x3d+xml" + | "multipart/appledouble" + | "multipart/byteranges" + | "multipart/encrypted" + | "multipart/example" + | "multipart/form-data" + | "multipart/header-set" + | "multipart/multilingual" + | "multipart/related" + | "multipart/report" + | "multipart/signed" + | "multipart/vnd.bint.med-plus" + | "multipart/voice-message" + | "multipart/x-mixed-replace" + | "text/1d-interleaved-parityfec" + | "text/cache-manifest" + | "text/calendar" + | "text/cql" + | "text/cql-expression" + | "text/cql-identifier" + | "text/css" + | "text/csv" + | "text/csv-schema" + | "text/directory" + | "text/dns" + | "text/ecmascript" + | "text/encaprtp" + | "text/example" + | "text/fhirpath" + | "text/flexfec" + | "text/fwdred" + | "text/gff3" + | "text/grammar-ref-list" + | "text/hl7v2" + | "text/html" + | "text/javascript" + | "text/jcr-cnd" + | "text/markdown" + | "text/mizar" + | "text/n3" + | "text/parameters" + | "text/parityfec" + | "text/provenance-notation" + | "text/prs.fallenstein.rst" + | "text/prs.lines.tag" + | "text/prs.prop.logic" + | "text/prs.texi" + | "text/raptorfec" + | "text/RED" + | "text/rfc822-headers" + | "text/rtf" + | "text/rtp-enc-aescm128" + | "text/rtploopback" + | "text/rtx" + | "text/SGML" + | "text/shaclc" + | "text/shex" + | "text/spdx" + | "text/strings" + | "text/t140" + | "text/tab-separated-values" + | "text/troff" + | "text/turtle" + | "text/ulpfec" + | "text/uri-list" + | "text/vcard" + | "text/vnd.a" + | "text/vnd.abc" + | "text/vnd.ascii-art" + | "text/vnd.curl" + | "text/vnd.debian.copyright" + | "text/vnd.DMClientScript" + | "text/vnd.dvb.subtitle" + | "text/vnd.esmertec.theme-descriptor" + | "text/vnd.exchangeable" + | "text/vnd.familysearch.gedcom" + | "text/vnd.ficlab.flt" + | "text/vnd.fly" + | "text/vnd.fmi.flexstor" + | "text/vnd.gml" + | "text/vnd.graphviz" + | "text/vnd.hans" + | "text/vnd.hgl" + | "text/vnd.in3d.3dml" + | "text/vnd.in3d.spot" + | "text/vnd.IPTC.NewsML" + | "text/vnd.IPTC.NITF" + | "text/vnd.latex-z" + | "text/vnd.motorola.reflex" + | "text/vnd.ms-mediapackage" + | "text/vnd.net2phone.commcenter.command" + | "text/vnd.radisys.msml-basic-layout" + | "text/vnd.senx.warpscript" + | "text/vnd.si.uricatalogue" + | "text/vnd.sun.j2me.app-descriptor" + | "text/vnd.sosi" + | "text/vnd.trolltech.linguist" + | "text/vnd.wap.si" + | "text/vnd.wap.sl" + | "text/vnd.wap.wml" + | "text/vnd.wap.wmlscript" + | "text/vtt" + | "text/wgsl" + | "text/xml" + | "text/xml-external-parsed-entity" + | "video/1d-interleaved-parityfec" + | "video/3gpp" + | "video/3gpp2" + | "video/3gpp-tt" + | "video/AV1" + | "video/BMPEG" + | "video/BT656" + | "video/CelB" + | "video/DV" + | "video/encaprtp" + | "video/example" + | "video/FFV1" + | "video/flexfec" + | "video/H261" + | "video/H263" + | "video/H263-1998" + | "video/H263-2000" + | "video/H264" + | "video/H264-RCDO" + | "video/H264-SVC" + | "video/H265" + | "video/H266" + | "video/iso.segment" + | "video/JPEG" + | "video/jpeg2000" + | "video/jxsv" + | "video/matroska" + | "video/matroska-3d" + | "video/mj2" + | "video/MP1S" + | "video/MP2P" + | "video/MP2T" + | "video/mp4" + | "video/MP4V-ES" + | "video/MPV" + | "video/mpeg4-generic" + | "video/nv" + | "video/ogg" + | "video/parityfec" + | "video/pointer" + | "video/quicktime" + | "video/raptorfec" + | "video/raw" + | "video/rtp-enc-aescm128" + | "video/rtploopback" + | "video/rtx" + | "video/scip" + | "video/smpte291" + | "video/SMPTE292M" + | "video/ulpfec" + | "video/vc1" + | "video/vc2" + | "video/vnd.CCTV" + | "video/vnd.dece.hd" + | "video/vnd.dece.mobile" + | "video/vnd.dece.mp4" + | "video/vnd.dece.pd" + | "video/vnd.dece.sd" + | "video/vnd.dece.video" + | "video/vnd.directv.mpeg" + | "video/vnd.directv.mpeg-tts" + | "video/vnd.dlna.mpeg-tts" + | "video/vnd.dvb.file" + | "video/vnd.fvt" + | "video/vnd.hns.video" + | "video/vnd.iptvforum.1dparityfec-1010" + | "video/vnd.iptvforum.1dparityfec-2005" + | "video/vnd.iptvforum.2dparityfec-1010" + | "video/vnd.iptvforum.2dparityfec-2005" + | "video/vnd.iptvforum.ttsavc" + | "video/vnd.iptvforum.ttsmpeg2" + | "video/vnd.motorola.video" + | "video/vnd.motorola.videop" + | "video/vnd.mpegurl" + | "video/vnd.ms-playready.media.pyv" + | "video/vnd.nokia.interleaved-multimedia" + | "video/vnd.nokia.mp4vr" + | "video/vnd.nokia.videovoip" + | "video/vnd.objectvideo" + | "video/vnd.radgamettools.bink" + | "video/vnd.radgamettools.smacker" + | "video/vnd.sealed.mpeg1" + | "video/vnd.sealed.mpeg4" + | "video/vnd.sealed.swf" + | "video/vnd.sealedmedia.softseal.mov" + | "video/vnd.uvvu.mp4" + | "video/vnd.youtube.yt" + | "video/vnd.vivo" + | "video/VP8" + | "video/VP9" + | AnyString; diff --git a/src/types/index.ts b/src/types/index.ts index cda2d2b1..711afb53 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,97 +1,93 @@ -import type { QueryObject } from "ufo"; -import type { Hooks as WSHooks } from "crossws"; -import type { H3Event } from "../event"; -import type { Session } from "../utils/session"; -import type { RouteNode } from "../router"; -import type { AnyNumber } from "./_utils"; +// App +export type { + App, + AppOptions, + AppUse, + Stack, + InputLayer, + InputStack, + Layer, + WebSocketOptions, + Matcher, + H3Error, +} from "./app"; + +// Event +export type { H3Event } from "./event"; + +// Handler +export type { + EventHandler, + EventHandlerObject, + EventHandlerRequest, + EventHandlerResolver, + EventHandlerResponse, + DynamicEventHandler, + LazyEventHandler, + InferEventInput, + RequestMiddleware, + ResponseMiddleware, +} from "./handler"; + +// Web +export type { + PlainHandler, + PlainRequest, + PlainResponse, + WebHandler, +} from "./web"; + +// Router +export type { + RouteNode, + Router, + RouterMethod, + RouterUse, + AddRouteShortcuts, + CreateRouterOptions, +} from "./router"; + +// Context +export type { H3EventContext } from "./context"; + +// SSE +export type { EventStreamMessage, EventStreamOptions } from "./utils/sse"; +export type { EventStream } from "../utils/internal/event-stream"; + +// Node +export type { NodeMiddleware, NodeHandler } from "./node"; +// HTTP export type { - ValidateFunction, - ValidateResult, -} from "../utils/internal/validate"; - -// https://www.rfc-editor.org/rfc/rfc7231#section-4.1 -// prettier-ignore -export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"; - -// prettier-ignore -// eslint-disable-next-line unicorn/text-encoding-identifier-case -export type Encoding = false | "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex"; - -// prettier-ignore -export type StatusCode = 100 | 101 | 102 | 103 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 444 | 450 | 451 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 506 | 507 | 508 | 509 | 510 | 511 | 521 | 522 | 523 | 525 | 530 | 599 | AnyNumber; - -export interface H3EventContext extends Record { - /* Matched router parameters */ - params?: Record; - /** - * Matched router Node - * - * @experimental The object structure may change in non-major version. - */ - matchedRoute?: RouteNode; - /* Cached session data */ - sessions?: Record; - /* Trusted IP Address of client */ - clientAddress?: string; -} - -export type EventHandlerResponse = T | Promise; - -export interface EventHandlerRequest { - body?: any; // TODO: Default to unknown in next major version - query?: QueryObject; - routerParams?: Record; -} - -export type InferEventInput< - Key extends keyof EventHandlerRequest, - Event extends H3Event, - T, -> = void extends T ? (Event extends H3Event ? E[Key] : never) : T; - -type MaybePromise = T | Promise; - -export type EventHandlerResolver = ( - path: string, -) => MaybePromise; - -export interface EventHandler< - Request extends EventHandlerRequest = EventHandlerRequest, - Response extends EventHandlerResponse = EventHandlerResponse, -> { - __is_handler__?: true; - __resolve__?: EventHandlerResolver; - __websocket__?: Partial; - (event: H3Event): Response; -} - -export type _RequestMiddleware< - Request extends EventHandlerRequest = EventHandlerRequest, -> = (event: H3Event) => void | Promise; - -export type _ResponseMiddleware< - Request extends EventHandlerRequest = EventHandlerRequest, - Response extends EventHandlerResponse = EventHandlerResponse, -> = ( - event: H3Event, - response: { body?: Awaited }, -) => void | Promise; - -export type EventHandlerObject< - Request extends EventHandlerRequest = EventHandlerRequest, - Response extends EventHandlerResponse = EventHandlerResponse, -> = { - onRequest?: _RequestMiddleware | _RequestMiddleware[]; - onBeforeResponse?: - | _ResponseMiddleware - | _ResponseMiddleware[]; - /** @experimental */ - websocket?: Partial; - handler: EventHandler; -}; - -export type LazyEventHandler = () => EventHandler | Promise; - -export type { MimeType } from "./_mimes"; -export type { TypedHeaders, RequestHeaders, HTTPHeaderName } from "./_headers"; + StatusCode, + HTTPMethod, + Encoding, + MimeType, + RequestHeaders, + RequestHeaderName, + ResponseHeaders, + ResponseHeaderName, +} from "./http"; + +// --- Utils --- + +// Cache +export type { CacheConditions } from "./utils/cache"; + +// Session +export type { Session, SessionConfig, SessionData } from "./utils/session"; + +// Proxy +export type { ProxyOptions, Duplex } from "./utils/proxy"; + +// Cors +export type { H3CorsOptions } from "./utils/cors"; + +// Fingerprint +export type { RequestFingerprintOptions } from "./utils/fingerprint"; + +// Static +export type { ServeStaticOptions, StaticAssetMeta } from "./utils/static"; + +// Validate +export type { ValidateFunction, ValidateResult } from "./utils/validate"; diff --git a/src/types/node.ts b/src/types/node.ts new file mode 100644 index 00000000..0ec4836f --- /dev/null +++ b/src/types/node.ts @@ -0,0 +1,22 @@ +import type { + IncomingMessage as NodeIncomingMessage, + ServerResponse as NodeServerResponse, +} from "node:http"; + +export type { + IncomingMessage as NodeIncomingMessage, + ServerResponse as NodeServerResponse, +} from "node:http"; + +export type NodeHandler = ( + req: NodeIncomingMessage, + res: NodeServerResponse, +) => unknown | Promise; + +export type NodeMiddleware = ( + req: NodeIncomingMessage, + res: NodeServerResponse, + next: (error?: Error) => void, +) => unknown | Promise; + +export type { NodeEvent } from "../adapters/node/event"; diff --git a/src/types/router.ts b/src/types/router.ts new file mode 100644 index 00000000..9e001af3 --- /dev/null +++ b/src/types/router.ts @@ -0,0 +1,28 @@ +import type { EventHandler } from "./handler"; +import type { HTTPMethod } from "./http"; + +export type RouterMethod = Lowercase; + +export type RouterUse = ( + path: string, + handler: EventHandler, + method?: RouterMethod | RouterMethod[], +) => Router; +export type AddRouteShortcuts = Record; + +export interface Router extends AddRouteShortcuts { + add: RouterUse; + use: RouterUse; + handler: EventHandler; +} + +export interface RouteNode { + handlers: Partial>; + path: string; +} + +export interface CreateRouterOptions { + /** @deprecated Please use `preemptive` instead. */ + preemtive?: boolean; + preemptive?: boolean; +} diff --git a/src/types/utils/cache.ts b/src/types/utils/cache.ts new file mode 100644 index 00000000..32956044 --- /dev/null +++ b/src/types/utils/cache.ts @@ -0,0 +1,6 @@ +export interface CacheConditions { + modifiedTime?: string | Date; + maxAge?: number; + etag?: string; + cacheControls?: string[]; +} diff --git a/src/utils/cors/types.ts b/src/types/utils/cors.ts similarity index 97% rename from src/utils/cors/types.ts rename to src/types/utils/cors.ts index 46c5e45b..d8aba97a 100644 --- a/src/utils/cors/types.ts +++ b/src/types/utils/cors.ts @@ -1,4 +1,4 @@ -import { HTTPMethod } from "../../types"; +import type { HTTPMethod } from ".."; export interface H3CorsOptions { origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); diff --git a/src/types/utils/fingerprint.ts b/src/types/utils/fingerprint.ts new file mode 100644 index 00000000..c30efdca --- /dev/null +++ b/src/types/utils/fingerprint.ts @@ -0,0 +1,19 @@ +export interface RequestFingerprintOptions { + /** @default SHA-1 */ + hash?: false | "SHA-1"; + + /** @default `true` */ + ip?: boolean; + + /** @default `false` */ + xForwardedFor?: boolean; + + /** @default `false` */ + method?: boolean; + + /** @default `false` */ + path?: boolean; + + /** @default `false` */ + userAgent?: boolean; +} diff --git a/src/types/utils/proxy.ts b/src/types/utils/proxy.ts new file mode 100644 index 00000000..c341ccfe --- /dev/null +++ b/src/types/utils/proxy.ts @@ -0,0 +1,17 @@ +import type { H3Event } from "../event"; +import type { RequestHeaders } from "../http"; + +export type Duplex = "half" | "full"; + +export interface ProxyOptions { + headers?: RequestHeaders | HeadersInit; + fetchOptions?: RequestInit & { duplex?: Duplex } & { + ignoreResponseError?: boolean; + }; + fetch?: typeof fetch; + sendStream?: boolean; + streamRequest?: boolean; + cookieDomainRewrite?: string | Record; + cookiePathRewrite?: string | Record; + onResponse?: (event: H3Event, response: Response) => void; +} diff --git a/src/types/utils/session.ts b/src/types/utils/session.ts new file mode 100644 index 00000000..d016aa1f --- /dev/null +++ b/src/types/utils/session.ts @@ -0,0 +1,31 @@ +import type { CookieSerializeOptions } from "cookie-es"; +import type { SealOptions } from "iron-webcrypto"; +import type { _kGetSession } from "../../utils/internal/session"; + +type SessionDataT = Record; + +export type SessionData = T; + +export interface Session { + id: string; + createdAt: number; + data: SessionData; + [_kGetSession]?: Promise>; +} + +export interface SessionConfig { + /** Private key used to encrypt session tokens */ + password: string; + /** Session expiration time in seconds */ + maxAge?: number; + /** default is h3 */ + name?: string; + /** Default is secure, httpOnly, / */ + cookie?: false | CookieSerializeOptions; + /** Default is x-h3-session / x-{name}-session */ + sessionHeader?: false | string; + seal?: SealOptions; + crypto?: Crypto; + /** Default is Crypto.randomUUID */ + generateId?: () => string; +} diff --git a/src/utils/sse/types.ts b/src/types/utils/sse.ts similarity index 100% rename from src/utils/sse/types.ts rename to src/types/utils/sse.ts diff --git a/src/types/utils/static.ts b/src/types/utils/static.ts new file mode 100644 index 00000000..c4b19f4f --- /dev/null +++ b/src/types/utils/static.ts @@ -0,0 +1,43 @@ +export interface StaticAssetMeta { + type?: string; + etag?: string; + mtime?: number | string | Date; + path?: string; + size?: number; + encoding?: string; +} + +export interface ServeStaticOptions { + /** + * This function should resolve asset meta + */ + getMeta: ( + id: string, + ) => StaticAssetMeta | undefined | Promise; + + /** + * This function should resolve asset content + */ + getContents: (id: string) => any | Promise; + + /** + * Map of supported encodings (compressions) and their file extensions. + * + * Each extension will be appended to the asset path to find the compressed version of the asset. + * + * @example { gzip: ".gz", br: ".br" } + */ + encodings?: Record; + + /** + * Default index file to serve when the path is a directory + * + * @default ["/index.html"] + */ + indexNames?: string[]; + + /** + * When set to true, the function will not throw 404 error when the asset meta is not found or meta validation failed + */ + fallthrough?: boolean; +} diff --git a/src/types/utils/validate.ts b/src/types/utils/validate.ts new file mode 100644 index 00000000..55e54db1 --- /dev/null +++ b/src/types/utils/validate.ts @@ -0,0 +1,5 @@ +export type ValidateResult = T | true | false | void; + +export type ValidateFunction = ( + data: unknown, +) => ValidateResult | Promise>; diff --git a/src/types/web.ts b/src/types/web.ts new file mode 100644 index 00000000..628eb87e --- /dev/null +++ b/src/types/web.ts @@ -0,0 +1,26 @@ +import type { H3EventContext } from "./context"; + +export type WebHandler = ( + request: Request, + context?: H3EventContext, +) => Promise; + +export type PlainHandler = ( + request: PlainRequest, + context?: H3EventContext, +) => Promise; + +export interface PlainRequest { + path: string; + method: string; + headers: HeadersInit; + body?: BodyInit; +} + +export interface PlainResponse { + status: number; + statusText: string; + headers: Record; + setCookie: string[]; + body?: unknown; +} diff --git a/src/utils/route.ts b/src/utils/base.ts similarity index 60% rename from src/utils/route.ts rename to src/utils/base.ts index cba359f9..9602cdf6 100644 --- a/src/utils/route.ts +++ b/src/utils/base.ts @@ -1,6 +1,7 @@ -import { withoutTrailingSlash, withoutBase } from "ufo"; -import { EventHandler } from "../types"; -import { eventHandler } from "../event"; +import type { EventHandler } from "../types"; +import { _kRaw } from "../event"; +import { defineEventHandler } from "../handler"; +import { withoutTrailingSlash, withoutBase } from "./internal/path"; /** * Prefixes and executes a handler with a base path. @@ -30,20 +31,13 @@ export function useBase(base: string, handler: EventHandler): EventHandler { return handler; } - return eventHandler(async (event) => { - // Keep original incoming url accessible - event.node.req.originalUrl = - event.node.req.originalUrl || event.node.req.url || "/"; - - const _path = event._path || event.node.req.url || "/"; - - event._path = withoutBase(event.path || "/", base); - event.node.req.url = event._path; - + return defineEventHandler(async (event) => { + const _pathBefore = event[_kRaw].path || "/"; + event[_kRaw].path = withoutBase(event.path || "/", base); try { return await handler(event); } finally { - event._path = event.node.req.url = _path; + event[_kRaw].path = _pathBefore; } }); } diff --git a/src/utils/body.ts b/src/utils/body.ts index c89aacb4..a3a8fd32 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -1,143 +1,51 @@ -import type { IncomingMessage } from "node:http"; -import destr from "destr"; -import type { Encoding, HTTPMethod, InferEventInput } from "../types"; -import type { H3Event } from "../event"; +import type { InferEventInput, ValidateFunction, H3Event } from "../types"; +import { _kRaw } from "../event"; import { createError } from "../error"; -import { - type MultiPartData, - parse as parseMultipartData, -} from "./internal/multipart"; -import { assertMethod, getRequestHeader, toWebRequest } from "./request"; -import { ValidateFunction, validateData } from "./internal/validate"; -import { hasProp } from "./internal/object"; - -export type { MultiPartData } from "./internal/multipart"; - -const RawBodySymbol = Symbol.for("h3RawBody"); -const ParsedBodySymbol = Symbol.for("h3ParsedBody"); -type InternalRequest = IncomingMessage & { - [RawBodySymbol]?: Promise; - [ParsedBodySymbol]?: T; - body?: string | undefined; -}; - -const PayloadMethods: HTTPMethod[] = ["PATCH", "POST", "PUT", "DELETE"]; +import { validateData } from "./internal/validate"; +import { parseURLEncodedBody } from "./internal/body"; /** - * Reads body of the request and returns encoded raw string (default), or `Buffer` if encoding is falsy. + * Reads body of the request and returns an Uint8Array of the raw body. * * @example * export default defineEventHandler(async (event) => { - * const body = await readRawBody(event, "utf-8"); + * const body = await readRawBody(event); * }); * * @param event {H3Event} H3 event or req passed by h3 handler - * @param encoding {Encoding} encoding="utf-8" - The character encoding to use. * - * @return {String|Buffer} Encoded raw string or raw Buffer of the body + * @return {Uint8Array} Raw body */ -export function readRawBody( +export async function readRawBody( event: H3Event, - encoding = "utf8" as E, -): E extends false ? Promise : Promise { - // Ensure using correct HTTP method before attempt to read payload - assertMethod(event, PayloadMethods); - - // Reuse body if already read - const _rawBody = - event._requestBody || - event.web?.request?.body || - (event.node.req as any)[RawBodySymbol] || - (event.node.req as any).rawBody /* firebase */ || - (event.node.req as any).body; /* unjs/unenv #8 */ - if (_rawBody) { - const promise = Promise.resolve(_rawBody).then((_resolved) => { - if (Buffer.isBuffer(_resolved)) { - return _resolved; - } - if (typeof _resolved.pipeTo === "function") { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - _resolved - .pipeTo( - new WritableStream({ - write(chunk) { - chunks.push(chunk); - }, - close() { - resolve(Buffer.concat(chunks)); - }, - abort(reason) { - reject(reason); - }, - }), - ) - .catch(reject); - }); - } else if (typeof _resolved.pipe === "function") { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - _resolved - .on("data", (chunk: any) => { - chunks.push(chunk); - }) - .on("end", () => { - resolve(Buffer.concat(chunks)); - }) - .on("error", reject); - }); - } - if (_resolved.constructor === Object) { - return Buffer.from(JSON.stringify(_resolved)); - } - return Buffer.from(_resolved); - }); - return encoding - ? promise.then((buff) => buff.toString(encoding)) - : (promise as Promise); - } - - if ( - !Number.parseInt(event.node.req.headers["content-length"] || "") && - !String(event.node.req.headers["transfer-encoding"] ?? "") - .split(",") - .map((e) => e.trim()) - .filter(Boolean) - .includes("chunked") - ) { - return Promise.resolve(undefined); - } - - const promise = ((event.node.req as any)[RawBodySymbol] = new Promise( - (resolve, reject) => { - const bodyData: any[] = []; - event.node.req - .on("error", (err) => { - reject(err); - }) - .on("data", (chunk) => { - bodyData.push(chunk); - }) - .on("end", () => { - resolve(Buffer.concat(bodyData)); - }); - }, - )); +): Promise { + return await event[_kRaw].readRawBody(); +} - const result = encoding - ? promise.then((buff) => buff.toString(encoding)) - : promise; - return result as E extends false - ? Promise - : Promise; +/** + * Reads body of the request and returns an string (utf-8) of the raw body. + * + * @example + * export default defineEventHandler(async (event) => { + * const body = await readTextBody(event); + * }); + * + * @param event {H3Event} H3 event or req passed by h3 handler + * + * @return {string} Text body + */ +export async function readTextBody( + event: H3Event, +): Promise { + return await event[_kRaw].readTextBody(); } /** - * Reads request body and tries to safely parse using [destr](https://github.com/unjs/destr). + * Reads request body and tries to parse using JSON.parse or URLSearchParams. * * @example * export default defineEventHandler(async (event) => { - * const body = await readBody(event); + * const body = await readAndParseBody(event); * }); * * @param event H3 event passed by h3 handler @@ -145,45 +53,40 @@ export function readRawBody( * * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body */ - -export async function readBody< +export async function readJSONBody< T, - Event extends H3Event = H3Event, - _T = InferEventInput<"body", Event, T>, ->(event: Event, options: { strict?: boolean } = {}): Promise<_T> { - const request = event.node.req as InternalRequest; - if (hasProp(request, ParsedBodySymbol)) { - return request[ParsedBodySymbol] as _T; + _Event extends H3Event = H3Event, + _T = InferEventInput<"body", _Event, T>, +>(event: _Event): Promise { + const text = await event[_kRaw].readTextBody(); + if (!text) { + return undefined; } - const contentType = request.headers["content-type"] || ""; - const body = await readRawBody(event); - - let parsed: T; - - if (contentType === "application/json") { - parsed = _parseJSON(body, options.strict ?? true) as T; - } else if (contentType.startsWith("application/x-www-form-urlencoded")) { - // TODO: Extract and pass charset as option (; charset=utf-8) - parsed = _parseURLEncodedBody(body!) as T; - } else if (contentType.startsWith("text/")) { - parsed = body as T; - } else { - parsed = _parseJSON(body, options.strict ?? false) as T; + const contentType = event[_kRaw].getHeader("content-type") || ""; + if (contentType.startsWith("application/x-www-form-urlencoded")) { + return parseURLEncodedBody(text) as _T; } - request[ParsedBodySymbol] = parsed; - return parsed as unknown as _T; + try { + return JSON.parse(text) as _T; + } catch { + throw createError({ + statusCode: 400, + statusMessage: "Bad Request", + message: "Invalid JSON body", + }); + } } /** - * Tries to read the request body via `readBody`, then uses the provided validation function and either throws a validation error or returns the result. + * Tries to read the request body via `readJSONBody`, then uses the provided validation function and either throws a validation error or returns the result. * * You can use a simple function to validate the body or use a library like `zod` to define a schema. * * @example * export default defineEventHandler(async (event) => { - * const body = await readValidatedBody(event, (body) => { + * const body = await readValidatedJSONBody(event, (body) => { * return typeof body === "object" && body !== null; * }); * }); @@ -192,7 +95,7 @@ export async function readBody< * * export default defineEventHandler(async (event) => { * const objectSchema = z.object(); - * const body = await readValidatedBody(event, objectSchema.safeParse); + * const body = await readValidatedJSONBody(event, objectSchema.safeParse); * }); * * @param event The H3Event passed by the handler. @@ -201,70 +104,31 @@ export async function readBody< * @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body. * @see {readBody} */ -export async function readValidatedBody< +export async function readValidatedJSONBody< T, Event extends H3Event = H3Event, _T = InferEventInput<"body", Event, T>, >(event: Event, validate: ValidateFunction<_T>): Promise<_T> { - const _body = await readBody(event, { strict: true }); + const _body = await readJSONBody(event); return validateData(_body, validate); } -/** - * Tries to read and parse the body of a an H3Event as multipart form. - * - * @example - * export default defineEventHandler(async (event) => { - * const formData = await readMultipartFormData(event); - * // The result could look like: - * // [ - * // { - * // "data": "other", - * // "name": "baz", - * // }, - * // { - * // "data": "something", - * // "name": "some-other-data", - * // }, - * // ]; - * }); - * - * @param event The H3Event object to read multipart form from. - * - * @return The parsed form data. If no form could be detected because the content type is not multipart/form-data or no boundary could be found. - */ -export async function readMultipartFormData( - event: H3Event, -): Promise { - const contentType = getRequestHeader(event, "content-type"); - if (!contentType || !contentType.startsWith("multipart/form-data")) { - return; - } - const boundary = contentType.match(/boundary=([^;]*)(;|$)/i)?.[1]; - if (!boundary) { - return; - } - const body = await readRawBody(event, false); - if (!body) { - return; - } - return parseMultipartData(body, boundary); -} - /** * Constructs a FormData object from an event, after converting it to a a web request. * * @example * export default defineEventHandler(async (event) => { - * const formData = await readFormData(event); + * const formData = await readFormDataBody(event); * const email = formData.get("email"); * const password = formData.get("password"); * }); * * @param event The H3Event object to read the form data from. */ -export async function readFormData(event: H3Event): Promise { - return await toWebRequest(event).formData(); +export async function readFormDataBody( + event: H3Event, +): Promise { + return await event[_kRaw].readFormDataBody(); } /** @@ -272,80 +136,8 @@ export async function readFormData(event: H3Event): Promise { * @param event The H3Event object containing the request information. * @returns Undefined if the request can't transport a payload, otherwise a ReadableStream of the request body. */ -export function getRequestWebStream( +export function getBodyStream( event: H3Event, -): undefined | ReadableStream { - if (!PayloadMethods.includes(event.method)) { - return; - } - - const bodyStream = event.web?.request?.body || event._requestBody; - if (bodyStream) { - return bodyStream as ReadableStream; - } - - // Use provided body (same as readBody) - const _hasRawBody = - RawBodySymbol in event.node.req || - "rawBody" in event.node.req /* firebase */ || - "body" in event.node.req /* unenv */ || - "__unenv__" in event.node.req; - if (_hasRawBody) { - return new ReadableStream({ - async start(controller) { - const _rawBody = await readRawBody(event, false); - if (_rawBody) { - controller.enqueue(_rawBody); - } - controller.close(); - }, - }); - } - - return new ReadableStream({ - start: (controller) => { - event.node.req.on("data", (chunk) => { - controller.enqueue(chunk); - }); - event.node.req.on("end", () => { - controller.close(); - }); - event.node.req.on("error", (err) => { - controller.error(err); - }); - }, - }); -} - -// --- Internal --- - -function _parseJSON(body = "", strict: boolean) { - if (!body) { - return undefined; - } - try { - return destr(body, { strict }); - } catch { - throw createError({ - statusCode: 400, - statusMessage: "Bad Request", - message: "Invalid JSON body", - }); - } -} - -function _parseURLEncodedBody(body: string) { - const form = new URLSearchParams(body); - const parsedForm: Record = Object.create(null); - for (const [key, value] of form.entries()) { - if (hasProp(parsedForm, key)) { - if (!Array.isArray(parsedForm[key])) { - parsedForm[key] = [parsedForm[key]]; - } - parsedForm[key].push(value); - } else { - parsedForm[key] = value; - } - } - return parsedForm as unknown; +): undefined | ReadableStream { + return event[_kRaw].getBodyStream(); } diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 5039fae6..924b2c86 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,11 +1,5 @@ -import type { H3Event } from "../event"; - -export interface CacheConditions { - modifiedTime?: string | Date; - maxAge?: number; - etag?: string; - cacheControls?: string[]; -} +import type { CacheConditions, H3Event } from "../types"; +import { _kRaw } from "../event"; /** * Check request caching headers (`If-Modified-Since`) and add caching headers (Last-Modified, Cache-Control) @@ -25,27 +19,27 @@ export function handleCacheHeaders( if (opts.modifiedTime) { const modifiedTime = new Date(opts.modifiedTime); - const ifModifiedSince = event.node.req.headers["if-modified-since"]; - event.node.res.setHeader("last-modified", modifiedTime.toUTCString()); + const ifModifiedSince = event[_kRaw].getHeader("if-modified-since"); + event[_kRaw].setResponseHeader("last-modified", modifiedTime.toUTCString()); if (ifModifiedSince && new Date(ifModifiedSince) >= opts.modifiedTime) { cacheMatched = true; } } if (opts.etag) { - event.node.res.setHeader("etag", opts.etag); - const ifNonMatch = event.node.req.headers["if-none-match"]; + event[_kRaw].setResponseHeader("etag", opts.etag); + const ifNonMatch = event[_kRaw].getHeader("if-none-match"); if (ifNonMatch === opts.etag) { cacheMatched = true; } } - event.node.res.setHeader("cache-control", cacheControls.join(", ")); + event[_kRaw].setResponseHeader("cache-control", cacheControls.join(", ")); if (cacheMatched) { - event.node.res.statusCode = 304; - if (!event.handled) { - event.node.res.end(); + event[_kRaw].responseCode = 304; + if (!event[_kRaw].handled) { + event[_kRaw].sendResponse(); } return true; } diff --git a/src/utils/cookie.ts b/src/utils/cookie.ts index 1dc48c30..8edc6379 100644 --- a/src/utils/cookie.ts +++ b/src/utils/cookie.ts @@ -1,7 +1,7 @@ -import { parse, serialize } from "cookie-es"; -import { objectHash } from "ohash"; import type { CookieSerializeOptions } from "cookie-es"; -import type { H3Event } from "../event"; +import type { H3Event } from "../types"; +import { parse as parseCookie, serialize as serializeCookie } from "cookie-es"; +import { _kRaw } from "../event"; /** * Parse the request to get HTTP Cookie header string and returning an object of all cookie name-value pairs. @@ -12,7 +12,7 @@ import type { H3Event } from "../event"; * ``` */ export function parseCookies(event: H3Event): Record { - return parse(event.node.req.headers.cookie || ""); + return parseCookie(event[_kRaw].getHeader("cookie") || ""); } /** @@ -33,7 +33,7 @@ export function getCookie(event: H3Event, name: string): string | undefined { * @param event {H3Event} H3 event or res passed by h3 handler * @param name Name of the cookie to set * @param value Value of the cookie to set - * @param serializeOptions {CookieSerializeOptions} Options for serializing the cookie + * @param options {CookieSerializeOptions} Options for serializing the cookie * ```ts * setCookie(res, 'Authorization', '1234567') * ``` @@ -42,20 +42,40 @@ export function setCookie( event: H3Event, name: string, value: string, - serializeOptions?: CookieSerializeOptions, + options?: CookieSerializeOptions, ) { - serializeOptions = { path: "/", ...serializeOptions }; - const cookieStr = serialize(name, value, serializeOptions); - let setCookies = event.node.res.getHeader("set-cookie"); - if (!Array.isArray(setCookies)) { - setCookies = [setCookies as any]; + // Serialize cookie + const newCookie = serializeCookie(name, value, { path: "/", ...options }); + + // Check and add only not any other set-cookie headers already set + const currentCookies = event[_kRaw].getResponseSetCookie(); + if (currentCookies.length === 0) { + event[_kRaw].setResponseHeader("set-cookie", newCookie); + return; } - const _optionsHash = objectHash(serializeOptions); - setCookies = setCookies.filter((cookieValue: string) => { - return cookieValue && _optionsHash !== objectHash(parse(cookieValue)); - }); - event.node.res.setHeader("set-cookie", [...setCookies, cookieStr]); + // Merge and deduplicate unique set-cookie headers + const newCookieKey = _getDistinctCookieKey(name, options || {}); + event[_kRaw].removeResponseHeader("set-cookie"); + for (const cookie of currentCookies) { + const _key = _getDistinctCookieKey( + cookie.split("=")?.[0], + parseCookie(cookie), + ); + if (_key === newCookieKey) { + console.log( + "Overwriting cookie:", + setCookie, + "to", + newCookie, + "key", + _key, + ); + continue; + } + event[_kRaw].appendResponseHeader("set-cookie", cookie); + } + event[_kRaw].appendResponseHeader("set-cookie", newCookie); } /** @@ -163,3 +183,14 @@ export function splitCookiesString(cookiesString: string | string[]): string[] { return cookiesStrings; } + +function _getDistinctCookieKey(name: string, options: CookieSerializeOptions) { + return [ + name, + options.domain || "", + options.path || "", + options.secure || "", + options.httpOnly || "", + options.sameSite || "", + ].join(";"); +} diff --git a/src/utils/cors.ts b/src/utils/cors.ts new file mode 100644 index 00000000..5ba2c45b --- /dev/null +++ b/src/utils/cors.ts @@ -0,0 +1,86 @@ +import type { H3Event } from "../types"; +import type { H3CorsOptions } from "../types/utils/cors"; +import { _kRaw } from "../event"; +import { sendNoContent, appendResponseHeaders } from "./response"; +import { + createAllowHeaderHeaders, + createCredentialsHeaders, + createExposeHeaders, + createMethodsHeaders, + createOriginHeaders, + resolveCorsOptions, +} from "./internal/cors"; + +export { isCorsOriginAllowed } from "./internal/cors"; + +/** + * Check if the incoming request is a CORS preflight request. + */ +export function isPreflightRequest(event: H3Event): boolean { + const origin = event[_kRaw].getHeader("origin"); + const accessControlRequestMethod = event[_kRaw].getHeader( + "access-control-request-method", + ); + + return event.method === "OPTIONS" && !!origin && !!accessControlRequestMethod; +} + +/** + * Append CORS preflight headers to the response. + */ +export function appendCorsPreflightHeaders( + event: H3Event, + options: H3CorsOptions, +) { + appendResponseHeaders(event, createOriginHeaders(event, options)); + appendResponseHeaders(event, createCredentialsHeaders(options)); + appendResponseHeaders(event, createExposeHeaders(options)); + appendResponseHeaders(event, createMethodsHeaders(options)); + appendResponseHeaders(event, createAllowHeaderHeaders(event, options)); +} + +/** + * Append CORS headers to the response. + */ +export function appendCorsHeaders(event: H3Event, options: H3CorsOptions) { + appendResponseHeaders(event, createOriginHeaders(event, options)); + appendResponseHeaders(event, createCredentialsHeaders(options)); + appendResponseHeaders(event, createExposeHeaders(options)); +} + +/** + * Handle CORS for the incoming request. + * + * If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response. + * + * If return value is `true`, the request is handled and no further action is needed. + * + * @example + * const app = createApp(); + * const router = createRouter(); + * router.use('/', + * defineEventHandler(async (event) => { + * const didHandleCors = handleCors(event, { + * origin: '*', + * preflight: { + * statusCode: 204, + * }, + * methods: '*', + * }); + * if (didHandleCors) { + * return; + * } + * // Your code here + * }) + * ); + */ +export function handleCors(event: H3Event, options: H3CorsOptions): boolean { + const _options = resolveCorsOptions(options); + if (isPreflightRequest(event)) { + appendCorsPreflightHeaders(event, options); + sendNoContent(event, _options.preflight.statusCode); + return true; + } + appendCorsHeaders(event, options); + return false; +} diff --git a/src/utils/cors/handler.ts b/src/utils/cors/handler.ts deleted file mode 100644 index 08e1151f..00000000 --- a/src/utils/cors/handler.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { H3Event } from "../../event"; -import { sendNoContent } from "../response"; -import { - resolveCorsOptions, - appendCorsPreflightHeaders, - appendCorsHeaders, - isPreflightRequest, -} from "./utils"; -import type { H3CorsOptions } from "./types"; - -/** - * Handle CORS for the incoming request. - * - * If the incoming request is a CORS preflight request, it will append the CORS preflight headers and send a 204 response. - * - * If return value is `true`, the request is handled and no further action is needed. - * - * @example - * const app = createApp(); - * const router = createRouter(); - * router.use('/', - * defineEventHandler(async (event) => { - * const didHandleCors = handleCors(event, { - * origin: '*', - * preflight: { - * statusCode: 204, - * }, - * methods: '*', - * }); - * if (didHandleCors) { - * return; - * } - * // Your code here - * }) - * ); - */ -export function handleCors(event: H3Event, options: H3CorsOptions): boolean { - const _options = resolveCorsOptions(options); - if (isPreflightRequest(event)) { - appendCorsPreflightHeaders(event, options); - sendNoContent(event, _options.preflight.statusCode); - return true; - } - appendCorsHeaders(event, options); - return false; -} diff --git a/src/utils/cors/index.ts b/src/utils/cors/index.ts deleted file mode 100644 index 400578ea..00000000 --- a/src/utils/cors/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { handleCors } from "./handler"; -export { - isPreflightRequest, - isCorsOriginAllowed, - appendCorsHeaders, - appendCorsPreflightHeaders, -} from "./utils"; - -export type { H3CorsOptions } from "./types"; diff --git a/src/utils/sse/index.ts b/src/utils/event-stream.ts similarity index 71% rename from src/utils/sse/index.ts rename to src/utils/event-stream.ts index 3e634ff6..3529bed2 100644 --- a/src/utils/sse/index.ts +++ b/src/utils/event-stream.ts @@ -1,6 +1,7 @@ -import type { H3Event } from "../../event"; -import type { EventStreamOptions } from "./types"; -import { EventStream as _EventStream } from "./event-stream"; +import type { H3Event } from "../types"; +import type { EventStreamOptions } from "../types/utils/sse"; +import { _kRaw } from "../event"; +import { EventStream } from "./internal/event-stream"; /** * Initialize an EventStream instance for creating [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) @@ -12,7 +13,7 @@ import { EventStream as _EventStream } from "./event-stream"; * ```ts * import { createEventStream, sendEventStream } from "h3"; * - * eventHandler((event) => { + * defineEventHandler((event) => { * const eventStream = createEventStream(event); * * // Send a message every second @@ -34,9 +35,6 @@ import { EventStream as _EventStream } from "./event-stream"; export function createEventStream( event: H3Event, opts?: EventStreamOptions, -): _EventStream { - return new _EventStream(event, opts); +): EventStream { + return new EventStream(event, opts); } - -export type EventStream = ReturnType; -export type { EventStreamOptions, EventStreamMessage } from "./types"; diff --git a/src/utils/fingerprint.ts b/src/utils/fingerprint.ts index 2b729e02..7ffeeb40 100644 --- a/src/utils/fingerprint.ts +++ b/src/utils/fingerprint.ts @@ -1,26 +1,7 @@ +import type { H3Event, RequestFingerprintOptions } from "../types"; import crypto from "uncrypto"; -import type { H3Event } from "../event"; -import { getRequestIP, getRequestHeader } from "./request"; - -export interface RequestFingerprintOptions { - /** @default SHA-1 */ - hash?: false | "SHA-1"; - - /** @default `true` */ - ip?: boolean; - - /** @default `false` */ - xForwardedFor?: boolean; - - /** @default `false` */ - method?: boolean; - - /** @default `false` */ - path?: boolean; - - /** @default `false` */ - userAgent?: boolean; -} +import { _kRaw } from "../event"; +import { getRequestIP } from "./request"; /** * @@ -49,7 +30,7 @@ export async function getRequestFingerprint( } if (opts.userAgent === true) { - fingerprint.push(getRequestHeader(event, "user-agent")); + fingerprint.push(event[_kRaw].getHeader("user-agent")); } const fingerprintString = fingerprint.filter(Boolean).join("|"); diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 9e788463..00000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export * from "./route"; -export * from "./body"; -export * from "./cache"; -export * from "./consts"; -export * from "./cors"; -export * from "./cookie"; -export * from "./fingerprint"; -export * from "./proxy"; -export * from "./request"; -export * from "./response"; -export * from "./sanitize"; -export * from "./session"; -export * from "./sse"; -export * from "./static"; -export * from "./ws"; diff --git a/src/utils/internal/body.ts b/src/utils/internal/body.ts new file mode 100644 index 00000000..b47e2fec --- /dev/null +++ b/src/utils/internal/body.ts @@ -0,0 +1,17 @@ +import { hasProp } from "./object"; + +export function parseURLEncodedBody(body: string) { + const form = new URLSearchParams(body); + const parsedForm: Record = Object.create(null); + for (const [key, value] of form.entries()) { + if (hasProp(parsedForm, key)) { + if (!Array.isArray(parsedForm[key])) { + parsedForm[key] = [parsedForm[key]]; + } + parsedForm[key].push(value); + } else { + parsedForm[key] = value; + } + } + return parsedForm as unknown; +} diff --git a/src/utils/consts.ts b/src/utils/internal/consts.ts similarity index 100% rename from src/utils/consts.ts rename to src/utils/internal/consts.ts diff --git a/src/utils/cors/utils.ts b/src/utils/internal/cors.ts similarity index 70% rename from src/utils/cors/utils.ts rename to src/utils/internal/cors.ts index b4900c63..84c26408 100644 --- a/src/utils/cors/utils.ts +++ b/src/utils/internal/cors.ts @@ -1,7 +1,4 @@ -import { defu } from "defu"; -import { appendHeaders } from "../response"; -import { getRequestHeaders, getRequestHeader } from "../request"; -import type { H3Event } from "../../event"; +import type { H3Event } from "../../types"; import type { H3CorsOptions, H3ResolvedCorsOptions, @@ -11,7 +8,8 @@ import type { H3AccessControlAllowHeadersHeader, H3AccessControlExposeHeadersHeader, H3AccessControlMaxAgeHeader, -} from "./types"; +} from "../../types/utils/cors"; +import { _kRaw } from "../../event"; /** * Resolve CORS options. @@ -31,27 +29,21 @@ export function resolveCorsOptions( }, }; - return defu(options, defaultOptions); -} - -/** - * Check if the incoming request is a CORS preflight request. - */ -export function isPreflightRequest(event: H3Event): boolean { - const origin = getRequestHeader(event, "origin"); - const accessControlRequestMethod = getRequestHeader( - event, - "access-control-request-method", - ); - - return event.method === "OPTIONS" && !!origin && !!accessControlRequestMethod; + return { + ...defaultOptions, + ...options, + preflight: { + ...defaultOptions.preflight, + ...options.preflight, + }, + }; } /** * Check if the incoming request is a CORS request. */ export function isCorsOriginAllowed( - origin: ReturnType["origin"], + origin: string | undefined, options: H3CorsOptions, ): boolean { const { origin: originOption } = options; @@ -86,7 +78,7 @@ export function createOriginHeaders( options: H3CorsOptions, ): H3AccessControlAllowOriginHeader { const { origin: originOption } = options; - const origin = getRequestHeader(event, "origin"); + const origin = event[_kRaw].getHeader("origin"); if (!origin || !originOption || originOption === "*") { return { "access-control-allow-origin": "*" }; @@ -147,7 +139,7 @@ export function createAllowHeaderHeaders( const { allowHeaders } = options; if (!allowHeaders || allowHeaders === "*" || allowHeaders.length === 0) { - const header = getRequestHeader(event, "access-control-request-headers"); + const header = event[_kRaw].getHeader("access-control-request-headers"); return header ? { @@ -196,26 +188,3 @@ export function createMaxAgeHeader( return {}; } - -/** - * Append CORS preflight headers to the response. - */ -export function appendCorsPreflightHeaders( - event: H3Event, - options: H3CorsOptions, -) { - appendHeaders(event, createOriginHeaders(event, options)); - appendHeaders(event, createCredentialsHeaders(options)); - appendHeaders(event, createExposeHeaders(options)); - appendHeaders(event, createMethodsHeaders(options)); - appendHeaders(event, createAllowHeaderHeaders(event, options)); -} - -/** - * Append CORS headers to the response. - */ -export function appendCorsHeaders(event: H3Event, options: H3CorsOptions) { - appendHeaders(event, createOriginHeaders(event, options)); - appendHeaders(event, createCredentialsHeaders(options)); - appendHeaders(event, createExposeHeaders(options)); -} diff --git a/src/utils/sse/event-stream.ts b/src/utils/internal/event-stream.ts similarity index 65% rename from src/utils/sse/event-stream.ts rename to src/utils/internal/event-stream.ts index d278958e..2e2363a3 100644 --- a/src/utils/sse/event-stream.ts +++ b/src/utils/internal/event-stream.ts @@ -1,17 +1,20 @@ -import type { H3Event } from "../../event"; -import { sendStream, setResponseStatus } from "../response"; -import { - formatEventStreamMessage, - formatEventStreamMessages, - setEventStreamHeaders, -} from "./utils"; -import { EventStreamMessage, EventStreamOptions } from "./types"; +import type { H3Event } from "../../types"; +import type { ResponseHeaders } from "../../types/http"; +import type { NodeEvent } from "../../types/node"; +import type { + EventStreamMessage, + EventStreamOptions, +} from "../../types/utils/sse"; +import { getRequestHeader } from "../request"; +import { setResponseHeaders } from "../response"; +import { _kRaw } from "../../event"; +import { setResponseStatus } from "../response"; /** * A helper class for [server sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) */ export class EventStream { - private readonly _h3Event: H3Event; + private readonly _event: H3Event; private readonly _transformStream = new TransformStream(); private readonly _writer: WritableStreamDefaultWriter; private readonly _encoder: TextEncoder = new TextEncoder(); @@ -23,13 +26,13 @@ export class EventStream { private _handled = false; constructor(event: H3Event, opts: EventStreamOptions = {}) { - this._h3Event = event; + this._event = event; this._writer = this._transformStream.writable.getWriter(); this._writer.closed.then(() => { this._writerIsClosed = true; }); if (opts.autoclose !== false) { - this._h3Event.node.req.on("close", () => this.close()); + (this._event[_kRaw] as NodeEvent)._res?.once("close", () => this.close()); } } @@ -137,12 +140,8 @@ export class EventStream { } } // check if the stream has been given to the client before closing the connection - if ( - this._h3Event._handled && - this._handled && - !this._h3Event.node.res.closed - ) { - this._h3Event.node.res.end(); + if (this._event[_kRaw].handled && this._handled) { + this._event[_kRaw].sendResponse(); } this._disposed = true; } @@ -156,11 +155,10 @@ export class EventStream { } async send() { - setEventStreamHeaders(this._h3Event); - setResponseStatus(this._h3Event, 200); - this._h3Event._handled = true; + setEventStreamHeaders(this._event); + setResponseStatus(this._event, 200); this._handled = true; - await sendStream(this._h3Event, this._transformStream.readable); + this._event[_kRaw].sendResponse(this._transformStream.readable); } } @@ -170,3 +168,50 @@ export function isEventStream(input: unknown): input is EventStream { } return input instanceof EventStream; } + +export function formatEventStreamMessage(message: EventStreamMessage): string { + let result = ""; + if (message.id) { + result += `id: ${message.id}\n`; + } + if (message.event) { + result += `event: ${message.event}\n`; + } + if (typeof message.retry === "number" && Number.isInteger(message.retry)) { + result += `retry: ${message.retry}\n`; + } + result += `data: ${message.data}\n\n`; + return result; +} + +export function formatEventStreamMessages( + messages: EventStreamMessage[], +): string { + let result = ""; + for (const msg of messages) { + result += formatEventStreamMessage(msg); + } + return result; +} + +export function setEventStreamHeaders(event: H3Event) { + const headers: ResponseHeaders = { + "Content-Type": "text/event-stream", + "Cache-Control": + "private, no-cache, no-store, no-transform, must-revalidate, max-age=0", + "X-Accel-Buffering": "no", // prevent nginx from buffering the response + }; + + if (!isHttp2Request(event)) { + headers.Connection = "keep-alive"; + } + + setResponseHeaders(event, headers); +} + +export function isHttp2Request(event: H3Event) { + return ( + getRequestHeader(event, ":path") !== undefined && + getRequestHeader(event, ":method") !== undefined + ); +} diff --git a/src/utils/internal/multipart.ts b/src/utils/internal/multipart.ts deleted file mode 100644 index 826003e8..00000000 --- a/src/utils/internal/multipart.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* -Implementation based on: - - https://github.com/nachomazzara/parse-multipart-data - Ref: a44c95319d09fd7d7ba51e01512567c444b90e14 - - https://github.com/freesoftwarefactory/parse-multipart - -By: - - Cristian Salazar (christiansalazarh@gmail.com) www.chileshift.cl - Twitter: @AmazonAwsChile - - Ignacio Mazzara - https://imazzara.com - Twitter: @nachomazzara - ---- - -MIT License - -Copyright (c) 2021 The Free Software Factory - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -**/ - -export interface MultiPartData { - data: Buffer; - name?: string; - filename?: string; - type?: string; -} - -enum ParsingState { - INIT, - READING_HEADERS, - READING_DATA, - READING_PART_SEPARATOR, -} - -export function parse( - multipartBodyBuffer: Buffer, - boundary: string, -): MultiPartData[] { - let lastline = ""; - let state: ParsingState = ParsingState.INIT; - let buffer: number[] = []; - const allParts: MultiPartData[] = []; - - let currentPartHeaders: [string, string][] = []; - - for (let i = 0; i < multipartBodyBuffer.length; i++) { - const prevByte: number | null = i > 0 ? multipartBodyBuffer[i - 1] : null; - const currByte: number = multipartBodyBuffer[i]; - - // 0x0a => \n | 0x0d => \r - const newLineChar: boolean = currByte === 0x0a || currByte === 0x0d; - if (!newLineChar) { - lastline += String.fromCodePoint(currByte); - } - - const newLineDetected: boolean = currByte === 0x0a && prevByte === 0x0d; - if (ParsingState.INIT === state && newLineDetected) { - // Searching for boundary - if ("--" + boundary === lastline) { - state = ParsingState.READING_HEADERS; // Found boundary. start reading headers - } - lastline = ""; - } else if (ParsingState.READING_HEADERS === state && newLineDetected) { - // Parsing headers. - // Headers are separated by an empty line from the content. - // Stop reading headers when the line is empty - if (lastline.length > 0) { - const i = lastline.indexOf(":"); - if (i > 0) { - const name = lastline.slice(0, i).toLowerCase(); - const value = lastline.slice(i + 1).trim(); - currentPartHeaders.push([name, value]); - } - } else { - // Found empty line. - // Reading headers is finished. - state = ParsingState.READING_DATA; - buffer = []; - } - lastline = ""; - } else if (ParsingState.READING_DATA === state) { - // Parsing data - if (lastline.length > boundary.length + 4) { - lastline = ""; // Free memory - } - if ("--" + boundary === lastline) { - const j = buffer.length - lastline.length; - const part = buffer.slice(0, j - 1); - allParts.push(process(part, currentPartHeaders)); - buffer = []; - currentPartHeaders = []; - lastline = ""; - state = ParsingState.READING_PART_SEPARATOR; - } else { - buffer.push(currByte); - } - if (newLineDetected) { - lastline = ""; - } - } else if ( - ParsingState.READING_PART_SEPARATOR === state && - newLineDetected - ) { - state = ParsingState.READING_HEADERS; - } - } - return allParts; -} - -function process(data: number[], headers: [string, string][]): MultiPartData { - const dataObj: Partial = {}; - - // Meta - const contentDispositionHeader = - headers.find((h) => h[0] === "content-disposition")?.[1] || ""; - for (const i of contentDispositionHeader.split(";")) { - const s = i.split("="); - if (s.length !== 2) { - continue; - } - const key = (s[0] || "").trim(); - if (key === "name" || key === "filename") { - const _value = (s[1] || "").trim().replace(/"/g, ""); - dataObj[key] = Buffer.from(_value, "latin1").toString("utf8"); - } - } - - // Type - const contentType = headers.find((h) => h[0] === "content-type")?.[1] || ""; - if (contentType) { - dataObj.type = contentType; - } - - // Data - dataObj.data = Buffer.from(data); - - return dataObj as MultiPartData; -} diff --git a/src/utils/internal/object.ts b/src/utils/internal/object.ts index a174e954..27ca2f79 100644 --- a/src/utils/internal/object.ts +++ b/src/utils/internal/object.ts @@ -13,3 +13,32 @@ export function hasProp(obj: any, prop: string | symbol) { return false; } } + +export function isJSONSerializable(value: any, _type: string): boolean { + // Primitive values are JSON serializable + if (value === null || value === undefined) { + return true; + } + if (_type !== "object") { + return _type === "boolean" || _type === "number" || _type === "string"; + } + + // Objects with `toJSON` are JSON serializable + if (typeof value.toJSON === "function") { + return true; + } + + // Arrays are JSON serializable (we assume items are safe too!) + if (Array.isArray(value)) { + return true; + } + + // Pipable streams are not JSON serializable (react pipe result is pure object :() + if (typeof value.pipe === "function" || typeof value.pipeTo === "function") { + return false; + } + + // Pure object + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} diff --git a/src/utils/internal/path.ts b/src/utils/internal/path.ts new file mode 100644 index 00000000..cf748e56 --- /dev/null +++ b/src/utils/internal/path.ts @@ -0,0 +1,62 @@ +export function withLeadingSlash(path: string | undefined): string { + if (!path || path === "/") { + return "/"; + } + return path[0] === "/" ? path : `/${path}`; +} + +export function withoutTrailingSlash(path: string | undefined): string { + if (!path || path === "/") { + return "/"; + } + // eslint-disable-next-line unicorn/prefer-at + return path[path.length - 1] === "/" ? path.slice(0, -1) : path; +} + +export function withTrailingSlash(path: string | undefined): string { + if (!path || path === "/") { + return "/"; + } + // eslint-disable-next-line unicorn/prefer-at + return path[path.length - 1] === "/" ? path : `${path}/`; +} + +export function joinURL( + base: string | undefined, + path: string | undefined, +): string { + if (!base || base === "/") { + return path || "/"; + } + if (!path || path === "/") { + return base || "/"; + } + // eslint-disable-next-line unicorn/prefer-at + const baseHasTrailing = base[base.length - 1] === "/"; + const pathHasLeading = path[0] === "/"; + if (baseHasTrailing && pathHasLeading) { + return base + path.slice(1); + } + if (!baseHasTrailing && !pathHasLeading) { + return base + "/" + path; + } + return base + path; +} + +export function withoutBase(input: string = "", base: string = "") { + if (!base || base === "/") { + return input; + } + const _base = withoutTrailingSlash(base); + if (!input.startsWith(_base)) { + return input; + } + const trimmed = input.slice(_base.length); + return trimmed[0] === "/" ? trimmed : "/" + trimmed; +} + +export function getPathname(path: string = "/") { + return path.startsWith("/") + ? path.split("?")[0] + : new URL(path, "http://localhost").pathname; +} diff --git a/src/utils/internal/proxy.ts b/src/utils/internal/proxy.ts new file mode 100644 index 00000000..0ba530a3 --- /dev/null +++ b/src/utils/internal/proxy.ts @@ -0,0 +1,66 @@ +import { RequestHeaders } from "../../types"; + +export const PayloadMethods = new Set(["PATCH", "POST", "PUT", "DELETE"]); + +export const ignoredHeaders = new Set([ + "transfer-encoding", + "connection", + "keep-alive", + "upgrade", + "expect", + "host", + "accept", +]); + +export function getFetch(_fetch?: T) { + if (_fetch) { + return _fetch; + } + if (globalThis.fetch) { + return globalThis.fetch; + } + throw new Error( + "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js.", + ); +} + +export function rewriteCookieProperty( + header: string, + map: string | Record, + property: string, +) { + const _map = typeof map === "string" ? { "*": map } : map; + return header.replace( + new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"), + (match, prefix, previousValue) => { + let newValue; + if (previousValue in _map) { + newValue = _map[previousValue]; + } else if ("*" in _map) { + newValue = _map["*"]; + } else { + return match; + } + return newValue ? prefix + newValue : ""; + }, + ); +} + +export function mergeHeaders( + defaults: HeadersInit, + ...inputs: (HeadersInit | RequestHeaders | undefined)[] +) { + const _inputs = inputs.filter(Boolean) as HeadersInit[]; + if (_inputs.length === 0) { + return defaults; + } + const merged = new Headers(defaults); + for (const input of _inputs) { + for (const [key, value] of Object.entries(input!)) { + if (value !== undefined) { + merged.set(key, value); + } + } + } + return merged; +} diff --git a/src/utils/internal/session.ts b/src/utils/internal/session.ts new file mode 100644 index 00000000..41ddb00d --- /dev/null +++ b/src/utils/internal/session.ts @@ -0,0 +1,13 @@ +import type { SessionConfig } from "../../types"; + +export const _kGetSession: unique symbol = Symbol.for( + "h3.internal.session.promise", +); + +export const DEFAULT_SESSION_NAME = "h3"; + +export const DEFAULT_SESSION_COOKIE: SessionConfig["cookie"] = { + path: "/", + secure: true, + httpOnly: true, +}; diff --git a/src/utils/internal/validate.ts b/src/utils/internal/validate.ts index 13417bfe..011754e0 100644 --- a/src/utils/internal/validate.ts +++ b/src/utils/internal/validate.ts @@ -1,14 +1,9 @@ +import type { ValidateFunction } from "../../types"; import { createError } from "../../error"; // TODO: Consider using similar method of typeschema for external library compatibility // https://github.com/decs/typeschema/blob/v0.1.3/src/assert.ts -export type ValidateResult = T | true | false | void; - -export type ValidateFunction = ( - data: unknown, -) => ValidateResult | Promise>; - /** * Validates the given data using the provided validation function. * @template T The expected type of the validated data. diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 0216e2dd..ecddc125 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -1,36 +1,15 @@ -import type { H3Event } from "../event"; -import type { H3EventContext, RequestHeaders } from "../types"; -import { getRequestHeaders } from "./request"; +import type { H3EventContext, H3Event, ProxyOptions, Duplex } from "../types"; import { splitCookiesString } from "./cookie"; import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize"; -import { getRequestWebStream, readRawBody } from "./body"; +import { _kRaw } from "../event"; import { createError } from "../error"; - -export type Duplex = "half" | "full"; - -export interface ProxyOptions { - headers?: RequestHeaders | HeadersInit; - fetchOptions?: RequestInit & { duplex?: Duplex } & { - ignoreResponseError?: boolean; - }; - fetch?: typeof fetch; - sendStream?: boolean; - streamRequest?: boolean; - cookieDomainRewrite?: string | Record; - cookiePathRewrite?: string | Record; - onResponse?: (event: H3Event, response: Response) => void; -} - -const PayloadMethods = new Set(["PATCH", "POST", "PUT", "DELETE"]); -const ignoredHeaders = new Set([ - "transfer-encoding", - "connection", - "keep-alive", - "upgrade", - "expect", - "host", - "accept", -]); +import { + PayloadMethods, + getFetch, + ignoredHeaders, + mergeHeaders, + rewriteCookieProperty, +} from "./internal/proxy"; /** * Proxy the incoming request to a target URL. @@ -45,10 +24,10 @@ export async function proxyRequest( let duplex: Duplex | undefined; if (PayloadMethods.has(event.method)) { if (opts.streamRequest) { - body = getRequestWebStream(event); + body = event[_kRaw].getBodyStream(); duplex = "half"; } else { - body = await readRawBody(event, false).catch(() => undefined); + body = await event[_kRaw].readRawBody(); } } @@ -84,7 +63,7 @@ export async function sendProxy( ) { let response: Response | undefined; try { - response = await _getFetch(opts.fetch)(target, { + response = await getFetch(opts.fetch)(target, { headers: opts.headers as HeadersInit, ignoreResponseError: true, // make $ofetch.raw transparent ...opts.fetchOptions, @@ -96,11 +75,11 @@ export async function sendProxy( cause: error, }); } - event.node.res.statusCode = sanitizeStatusCode( + event[_kRaw].responseCode = sanitizeStatusCode( response.status, - event.node.res.statusCode, + event[_kRaw].responseCode, ); - event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); + event[_kRaw].responseMessage = sanitizeStatusMessage(response.statusText); const cookies: string[] = []; @@ -115,30 +94,26 @@ export async function sendProxy( cookies.push(...splitCookiesString(value)); continue; } - event.node.res.setHeader(key, value); + event[_kRaw].setResponseHeader(key, value); } if (cookies.length > 0) { - event.node.res.setHeader( - "set-cookie", - cookies.map((cookie) => { - if (opts.cookieDomainRewrite) { - cookie = rewriteCookieProperty( - cookie, - opts.cookieDomainRewrite, - "domain", - ); - } - if (opts.cookiePathRewrite) { - cookie = rewriteCookieProperty( - cookie, - opts.cookiePathRewrite, - "path", - ); - } - return cookie; - }), - ); + const _cookies = cookies.map((cookie) => { + if (opts.cookieDomainRewrite) { + cookie = rewriteCookieProperty( + cookie, + opts.cookieDomainRewrite, + "domain", + ); + } + if (opts.cookiePathRewrite) { + cookie = rewriteCookieProperty(cookie, opts.cookiePathRewrite, "path"); + } + return cookie; + }); + for (const cookie of _cookies) { + event[_kRaw].appendResponseHeader("set-cookie", cookie); + } } if (opts.onResponse) { @@ -151,23 +126,18 @@ export async function sendProxy( } // Ensure event is not handled - if (event.handled) { + if (event[_kRaw].handled) { return; } // Send at once if (opts.sendStream === false) { const data = new Uint8Array(await response.arrayBuffer()); - return event.node.res.end(data); + return event[_kRaw].sendResponse(data); } // Send as stream - if (response.body) { - for await (const chunk of response.body as any as AsyncIterable) { - event.node.res.write(chunk); - } - } - return event.node.res.end(); + return event[_kRaw].sendResponse(response.body); } /** @@ -175,10 +145,9 @@ export async function sendProxy( */ export function getProxyRequestHeaders(event: H3Event) { const headers = Object.create(null); - const reqHeaders = getRequestHeaders(event); - for (const name in reqHeaders) { + for (const [name, value] of event[_kRaw].getHeaders()) { if (!ignoredHeaders.has(name)) { - headers[name] = reqHeaders[name]; + headers[name] = value; } } return headers; @@ -197,7 +166,7 @@ export function fetchWithEvent< init?: RequestInit & { context?: H3EventContext }, options?: { fetch: F }, ): unknown extends T ? ReturnType : T { - return _getFetch(options?.fetch)(req, { + return getFetch(options?.fetch)(req, { ...init, context: init?.context || event.context, headers: { @@ -206,58 +175,3 @@ export function fetchWithEvent< }, }); } - -// -- internal utils -- - -function _getFetch(_fetch?: T) { - if (_fetch) { - return _fetch; - } - if (globalThis.fetch) { - return globalThis.fetch; - } - throw new Error( - "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js.", - ); -} - -function rewriteCookieProperty( - header: string, - map: string | Record, - property: string, -) { - const _map = typeof map === "string" ? { "*": map } : map; - return header.replace( - new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"), - (match, prefix, previousValue) => { - let newValue; - if (previousValue in _map) { - newValue = _map[previousValue]; - } else if ("*" in _map) { - newValue = _map["*"]; - } else { - return match; - } - return newValue ? prefix + newValue : ""; - }, - ); -} - -function mergeHeaders( - defaults: HeadersInit, - ...inputs: (HeadersInit | RequestHeaders | undefined)[] -) { - const _inputs = inputs.filter(Boolean) as HeadersInit[]; - if (_inputs.length === 0) { - return defaults; - } - const merged = new Headers(defaults); - for (const input of _inputs) { - for (const [key, value] of Object.entries(input!)) { - if (value !== undefined) { - merged.set(key, value); - } - } - } - return merged; -} diff --git a/src/utils/request.ts b/src/utils/request.ts index aa96aec1..db8b24b6 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,14 +1,14 @@ import { getQuery as _getQuery, decode as decodeURI } from "ufo"; import { createError } from "../error"; import type { - HTTPHeaderName, HTTPMethod, InferEventInput, RequestHeaders, + ValidateFunction, + H3Event, } from "../types"; -import type { H3Event } from "../event"; -import { validateData, ValidateFunction } from "./internal/validate"; -import { getRequestWebStream } from "./body"; +import { _kRaw } from "../event"; +import { validateData } from "./internal/validate"; /** * Get query the params object from the request URL parsed with [unjs/ufo](https://ufo.unjs.io). @@ -150,7 +150,7 @@ export function getMethod( event: H3Event, defaultMethod: HTTPMethod = "GET", ): HTTPMethod { - return (event.node.req.method || defaultMethod).toUpperCase() as HTTPMethod; + return (event.method || defaultMethod).toUpperCase() as HTTPMethod; } /** @@ -225,19 +225,9 @@ export function assertMethod( * }); */ export function getRequestHeaders(event: H3Event): RequestHeaders { - const _headers: RequestHeaders = {}; - for (const key in event.node.req.headers) { - const val = event.node.req.headers[key]; - _headers[key] = Array.isArray(val) ? val.filter(Boolean).join(", ") : val; - } - return _headers; + return Object.fromEntries(event[_kRaw].getHeaders().entries()); } -/** - * Alias for `getRequestHeaders`. - */ -export const getHeaders = getRequestHeaders; - /** * Get a request header by name. * @@ -248,18 +238,12 @@ export const getHeaders = getRequestHeaders; */ export function getRequestHeader( event: H3Event, - name: HTTPHeaderName, -): RequestHeaders[string] { - const headers = getRequestHeaders(event); - const value = headers[name.toLowerCase()]; - return value; + name: keyof RequestHeaders, +): RequestHeaders[typeof name] | undefined { + const value = event[_kRaw].getHeader(name.toLowerCase()); + return value || undefined; } -/** - * Alias for `getRequestHeader`. - */ -export const getHeader = getRequestHeader; - /** * Get the request hostname. * @@ -277,12 +261,12 @@ export function getRequestHost( opts: { xForwardedHost?: boolean } = {}, ) { if (opts.xForwardedHost) { - const xForwardedHost = event.node.req.headers["x-forwarded-host"] as string; + const xForwardedHost = event[_kRaw].getHeader("x-forwarded-host"); if (xForwardedHost) { return xForwardedHost; } } - return event.node.req.headers.host || "localhost"; + return event[_kRaw].getHeader("host") || "localhost"; } /** @@ -303,19 +287,11 @@ export function getRequestProtocol( ) { if ( opts.xForwardedProto !== false && - event.node.req.headers["x-forwarded-proto"] === "https" + event[_kRaw].getHeader("x-forwarded-proto") === "https" ) { return "https"; } - return (event.node.req.connection as any)?.encrypted ? "https" : "http"; -} - -const DOUBLE_SLASH_RE = /[/\\]{2,}/g; - -/** @deprecated Use `event.path` instead */ -export function getRequestPath(event: H3Event): string { - const path = (event.node.req.url || "/").replace(DOUBLE_SLASH_RE, "/"); - return path; + return event[_kRaw].isSecure ? "https" : "http"; } /** @@ -336,31 +312,13 @@ export function getRequestURL( ) { const host = getRequestHost(event, opts); const protocol = getRequestProtocol(event, opts); - const path = (event.node.req.originalUrl || event.path).replace( + const path = (event[_kRaw].originalPath || event[_kRaw].path).replace( /^[/\\]+/g, "/", ); return new URL(path, `${protocol}://${host}`); } -/** - * Convert the H3Event to a WebRequest object. - * - * **NOTE:** This function is not stable and might have edge cases that are not handled properly. - */ -export function toWebRequest(event: H3Event) { - return ( - event.web?.request || - new Request(getRequestURL(event), { - // @ts-ignore Undici option - duplex: "half", - method: event.method, - headers: event.headers, - body: getRequestWebStream(event), - }) - ); -} - /** * Try to get the client IP address from the incoming request. * @@ -390,7 +348,8 @@ export function getRequestIP( if (opts.xForwardedFor) { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#syntax - const xForwardedFor = getRequestHeader(event, "x-forwarded-for") + const _header = event[_kRaw].getHeader("x-forwarded-for"); + const xForwardedFor = (Array.isArray(_header) ? _header[0] : _header) ?.split(",") .shift() ?.trim(); @@ -399,7 +358,5 @@ export function getRequestIP( } } - if (event.node.req.socket.remoteAddress) { - return event.node.req.socket.remoteAddress; - } + return event[_kRaw].remoteAddress; } diff --git a/src/utils/response.ts b/src/utils/response.ts index d0bf0b00..7acf63b5 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -1,16 +1,10 @@ -import type { Readable } from "node:stream"; -import type { Socket } from "node:net"; -import type { H3Event } from "../event"; -import type { - HTTPHeaderName, - MimeType, - TypedHeaders, - StatusCode, -} from "../types"; -import { MIMES } from "./consts"; +import type { H3Event } from "../types"; +import type { ResponseHeaders, ResponseHeaderName } from "../types/http"; +import type { MimeType, StatusCode } from "../types"; +import { _kRaw } from "../event"; +import { MIMES } from "./internal/consts"; import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize"; import { splitCookiesString } from "./cookie"; -import { hasProp } from "./internal/object"; import { serializeIterableValue, coerceIterable, @@ -18,33 +12,6 @@ import { IteratorSerializer, } from "./internal/iterable"; -const defer = - typeof setImmediate === "undefined" ? (fn: () => any) => fn() : setImmediate; - -/** - * Directly send a response to the client. - * - * **Note:** This function should be used only when you want to send a response directly without using the `h3` event. - * Normally you can directly `return` a value inside event handlers. - */ -export function send( - event: H3Event, - data?: any, - type?: MimeType, -): Promise { - if (type) { - defaultContentType(event, type); - } - return new Promise((resolve) => { - defer(() => { - if (!event.handled) { - event.node.res.end(data); - } - resolve(); - }); - }); -} - /** * Respond with an empty payload.
* @@ -64,22 +31,22 @@ export function send( * @param code status code to be send. By default, it is `204 No Content`. */ export function sendNoContent(event: H3Event, code?: StatusCode) { - if (event.handled) { + if (event[_kRaw].handled) { return; } - if (!code && event.node.res.statusCode !== 200) { + if (!code && event[_kRaw].responseCode !== 200) { // status code was set with setResponseStatus - code = event.node.res.statusCode; + code = event[_kRaw].responseCode; } const _code = sanitizeStatusCode(code, 204); // 204 responses MUST NOT have a Content-Length header field // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 if (_code === 204) { - event.node.res.removeHeader("content-length"); + event[_kRaw].removeResponseHeader("content-length"); } - event.node.res.writeHead(_code); - event.node.res.end(); + event[_kRaw].writeHead(_code); + event[_kRaw].sendResponse(); } /** @@ -97,13 +64,13 @@ export function setResponseStatus( text?: string, ): void { if (code) { - event.node.res.statusCode = sanitizeStatusCode( + event[_kRaw].responseCode = sanitizeStatusCode( code, - event.node.res.statusCode, + event[_kRaw].responseCode, ); } if (text) { - event.node.res.statusMessage = sanitizeStatusMessage(text); + event[_kRaw].responseMessage = sanitizeStatusMessage(text); } } @@ -117,7 +84,7 @@ export function setResponseStatus( * }); */ export function getResponseStatus(event: H3Event): number { - return event.node.res.statusCode; + return event[_kRaw].responseCode || 200; } /** @@ -130,7 +97,7 @@ export function getResponseStatus(event: H3Event): number { * }); */ export function getResponseStatusText(event: H3Event): string { - return event.node.res.statusMessage; + return event[_kRaw].responseMessage || ""; } /** @@ -139,10 +106,10 @@ export function getResponseStatusText(event: H3Event): string { export function defaultContentType(event: H3Event, type?: MimeType) { if ( type && - event.node.res.statusCode !== 304 /* unjs/h3#603 */ && - !event.node.res.getHeader("content-type") + event[_kRaw].responseCode !== 304 /* unjs/h3#603 */ && + !event[_kRaw].getResponseHeader("content-type") ) { - event.node.res.setHeader("content-type", type); + event[_kRaw].setResponseHeader("content-type", type); } } @@ -168,14 +135,15 @@ export function sendRedirect( location: string, code: StatusCode = 302, ) { - event.node.res.statusCode = sanitizeStatusCode( + event[_kRaw].responseCode = sanitizeStatusCode( code, - event.node.res.statusCode, + event[_kRaw].responseCode, ); - event.node.res.setHeader("location", location); + event[_kRaw].setResponseHeader("location", location); const encodedLoc = location.replace(/"/g, "%22"); const html = ``; - return send(event, html, MIMES.html); + defaultContentType(event, MIMES.html); + return event[_kRaw].sendResponse(html); } /** @@ -186,25 +154,12 @@ export function sendRedirect( * const headers = getResponseHeaders(event); * }); */ -export function getResponseHeaders( - event: H3Event, -): ReturnType { - return event.node.res.getHeaders(); +export function getResponseHeaders(event: H3Event) { + return event[_kRaw].getResponseHeaders(); } -/** - * Alias for `getResponseHeaders`. - * - * @example - * export default defineEventHandler((event) => { - * const contentType = getResponseHeader(event, "content-type"); // Get the response content-type header - * }); - */ -export function getResponseHeader( - event: H3Event, - name: HTTPHeaderName, -): ReturnType { - return event.node.res.getHeader(name); +export function getResponseHeader(event: H3Event, name: string) { + return event[_kRaw].getResponseHeader(name); } /** @@ -220,21 +175,13 @@ export function getResponseHeader( */ export function setResponseHeaders( event: H3Event, - headers: TypedHeaders, + headers: ResponseHeaders, ): void { for (const [name, value] of Object.entries(headers)) { - event.node.res.setHeader( - name, - value! as unknown as number | string | readonly string[], - ); + event[_kRaw].setResponseHeader(name, value!); } } -/** - * Alias for `setResponseHeaders`. - */ -export const setHeaders = setResponseHeaders; - /** * Set a response header by name. * @@ -243,19 +190,21 @@ export const setHeaders = setResponseHeaders; * setResponseHeader(event, "content-type", "text/html"); * }); */ -export function setResponseHeader( +export function setResponseHeader( event: H3Event, name: T, - value: TypedHeaders[Lowercase], + value: ResponseHeaders[T] | ResponseHeaders[T][], ): void { - event.node.res.setHeader(name, value as string); + if (Array.isArray(value)) { + event[_kRaw].removeResponseHeader(name); + for (const valueItem of value) { + event[_kRaw].appendResponseHeader(name, valueItem!); + } + } else { + event[_kRaw].setResponseHeader(name, value!); + } } -/** - * Alias for `setResponseHeader`. - */ -export const setHeader = setResponseHeader; - /** * Append the response headers. * @@ -269,18 +218,13 @@ export const setHeader = setResponseHeader; */ export function appendResponseHeaders( event: H3Event, - headers: TypedHeaders, + headers: ResponseHeaders, ): void { for (const [name, value] of Object.entries(headers)) { - appendResponseHeader(event, name, value); + appendResponseHeader(event, name, value!); } } -/** - * Alias for `appendResponseHeaders`. - */ -export const appendHeaders = appendResponseHeaders; - /** * Append a response header by name. * @@ -289,30 +233,20 @@ export const appendHeaders = appendResponseHeaders; * appendResponseHeader(event, "content-type", "text/html"); * }); */ -export function appendResponseHeader( +export function appendResponseHeader( event: H3Event, name: T, - value: TypedHeaders[Lowercase], + value: ResponseHeaders[T] | ResponseHeaders[T][], ): void { - let current = event.node.res.getHeader(name); - - if (!current) { - event.node.res.setHeader(name, value as string); - return; - } - - if (!Array.isArray(current)) { - current = [current.toString()]; + if (Array.isArray(value)) { + for (const valueItem of value) { + event[_kRaw].appendResponseHeader(name, valueItem!); + } + } else { + event[_kRaw].appendResponseHeader(name, value!); } - - event.node.res.setHeader(name, [...current, value as string]); } -/** - * Alias for `appendResponseHeader`. - */ -export const appendHeader = appendResponseHeader; - /** * Remove all response headers, or only those specified in the headerNames array. * @@ -326,15 +260,15 @@ export const appendHeader = appendResponseHeader; */ export function clearResponseHeaders( event: H3Event, - headerNames?: HTTPHeaderName[], + headerNames?: ResponseHeaderName[], ): void { if (headerNames && headerNames.length > 0) { for (const name of headerNames) { - removeResponseHeader(event, name); + event[_kRaw].removeResponseHeader(name); } } else { - for (const [name] of Object.entries(getResponseHeaders(event))) { - removeResponseHeader(event, name); + for (const name of event[_kRaw].getResponseHeaders().keys()) { + event[_kRaw].removeResponseHeader(name); } } } @@ -349,181 +283,23 @@ export function clearResponseHeaders( */ export function removeResponseHeader( event: H3Event, - name: HTTPHeaderName, + name: ResponseHeaderName, ): void { - return event.node.res.removeHeader(name); -} - -/** - * Checks if the data is a stream. (Node.js Readable Stream, React Pipeable Stream, or Web Stream) - */ -export function isStream(data: any): data is Readable | ReadableStream { - if (!data || typeof data !== "object") { - return false; - } - if (typeof data.pipe === "function") { - // Node.js Readable Streams - if (typeof data._read === "function") { - return true; - } - // React Pipeable Streams - if (typeof data.abort === "function") { - return true; - } - } - // Web Streams - if (typeof data.pipeTo === "function") { - return true; - } - return false; -} - -/** - * Checks if the data is a Response object. - */ -export function isWebResponse(data: any): data is Response { - return typeof Response !== "undefined" && data instanceof Response; -} - -/** - * Send a stream response to the client. - * - * Note: You can directly `return` a stream value inside event handlers alternatively which is recommended. - */ -export function sendStream( - event: H3Event, - stream: Readable | ReadableStream, -): Promise { - // Validate input - if (!stream || typeof stream !== "object") { - throw new Error("[h3] Invalid stream provided."); - } - - // Directly expose stream for worker environments (unjs/unenv) - (event.node.res as unknown as { _data: BodyInit })._data = stream as BodyInit; - - // Early return if response Socket is not available for worker environments (unjs/nitro) - if (!event.node.res.socket) { - event._handled = true; - // TODO: Hook and handle stream errors - return Promise.resolve(); - } - - // Native Web Streams - if ( - hasProp(stream, "pipeTo") && - typeof (stream as ReadableStream).pipeTo === "function" - ) { - return (stream as ReadableStream) - .pipeTo( - new WritableStream({ - write(chunk) { - event.node.res.write(chunk); - }, - }), - ) - .then(() => { - event.node.res.end(); - }); - } - - // Node.js Readable Streams - // https://nodejs.org/api/stream.html#readable-streams - if ( - hasProp(stream, "pipe") && - typeof (stream as Readable).pipe === "function" - ) { - return new Promise((resolve, reject) => { - // Pipe stream to response - (stream as Readable).pipe(event.node.res); - - // Handle stream events (if supported) - if ((stream as Readable).on) { - (stream as Readable).on("end", () => { - event.node.res.end(); - resolve(); - }); - (stream as Readable).on("error", (error: Error) => { - reject(error); - }); - } - - // Handle request aborts - event.node.res.on("close", () => { - // https://react.dev/reference/react-dom/server/renderToPipeableStream - if ((stream as any).abort) { - (stream as any).abort(); - } - }); - }); - } - - throw new Error("[h3] Invalid or incompatible stream provided."); + return event[_kRaw].removeResponseHeader(name); } -const noop = () => {}; - /** * Write `HTTP/1.1 103 Early Hints` to the client. */ export function writeEarlyHints( event: H3Event, - hints: string | string[] | Record, - cb: () => void = noop, -) { - if (!event.node.res.socket /* && !('writeEarlyHints' in event.node.res) */) { - cb(); - return; - } - - // Normalize if string or string[] is provided - if (typeof hints === "string" || Array.isArray(hints)) { - hints = { link: hints }; - } - - if (hints.link) { - hints.link = Array.isArray(hints.link) ? hints.link : hints.link.split(","); - // TODO: remove when https://github.com/nodejs/node/pull/44874 is released - // hints.link = hints.link.map(l => l.trim().replace(/; crossorigin/g, '')) - } - - // TODO: Enable when node 18 api is stable - // if ('writeEarlyHints' in event.node.res) { - // return event.node.res.writeEarlyHints(hints, cb) - // } - - const headers: [string, string | string[]][] = Object.entries(hints).map( - (e) => [e[0].toLowerCase(), e[1]], - ); - if (headers.length === 0) { - cb(); - return; - } - - let hint = "HTTP/1.1 103 Early Hints"; - if (hints.link) { - hint += `\r\nLink: ${(hints.link as string[]).join(", ")}`; - } - - for (const [header, value] of headers) { - if (header === "link") { - continue; - } - hint += `\r\n${header}: ${value}`; - } - if (event.node.res.socket) { - (event.node.res as { socket: Socket }).socket.write( - `${hint}\r\n\r\n`, - "utf8", - cb, - ); - } else { - cb(); - } + hints: Record, +): void | Promise { + return event[_kRaw].writeEarlyHints(hints); } /** - * Send a Response object to the client. + * Send a Web besponse object to the client. */ export function sendWebResponse( event: H3Event, @@ -531,29 +307,27 @@ export function sendWebResponse( ): void | Promise { for (const [key, value] of response.headers) { if (key === "set-cookie") { - event.node.res.appendHeader(key, splitCookiesString(value)); + for (const setCookie of splitCookiesString(value)) { + event[_kRaw].appendResponseHeader(key, setCookie); + } } else { - event.node.res.setHeader(key, value); + event[_kRaw].setResponseHeader(key, value); } } if (response.status) { - event.node.res.statusCode = sanitizeStatusCode( + event[_kRaw].responseCode = sanitizeStatusCode( response.status, - event.node.res.statusCode, + event[_kRaw].responseCode, ); } if (response.statusText) { - event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); + event[_kRaw].responseMessage = sanitizeStatusMessage(response.statusText); } if (response.redirected) { - event.node.res.setHeader("location", response.url); + event[_kRaw].setResponseHeader("location", response.url); } - if (!response.body) { - event.node.res.end(); - return; - } - return sendStream(event, response.body); + return event[_kRaw].sendResponse(response.body); } /** @@ -595,11 +369,10 @@ export function sendIterable( options?: { serializer: IteratorSerializer; }, -): Promise { +): void | Promise { const serializer = options?.serializer ?? serializeIterableValue; const iterator = coerceIterable(iterable); - return sendStream( - event, + event[_kRaw].sendResponse( new ReadableStream({ async pull(controller) { const { value, done } = await iterator.next(); diff --git a/src/utils/session.ts b/src/utils/session.ts index 0362910a..276c8fcb 100644 --- a/src/utils/session.ts +++ b/src/utils/session.ts @@ -1,56 +1,24 @@ -import type { CookieSerializeOptions } from "cookie-es"; +import type { H3Event, Session, SessionConfig, SessionData } from "../types"; import crypto from "uncrypto"; import { seal, unseal, defaults as sealDefaults } from "iron-webcrypto"; -import type { SealOptions } from "iron-webcrypto"; -import type { H3Event } from "../event"; +import { _kRaw } from "../event"; import { getCookie, setCookie } from "./cookie"; - -type SessionDataT = Record; -export type SessionData = T; - -const getSessionPromise = Symbol("getSession"); - -export interface Session { - id: string; - createdAt: number; - data: SessionData; - [getSessionPromise]?: Promise>; -} - -export interface SessionConfig { - /** Private key used to encrypt session tokens */ - password: string; - /** Session expiration time in seconds */ - maxAge?: number; - /** default is h3 */ - name?: string; - /** Default is secure, httpOnly, / */ - cookie?: false | CookieSerializeOptions; - /** Default is x-h3-session / x-{name}-session */ - sessionHeader?: false | string; - seal?: SealOptions; - crypto?: Crypto; - /** Default is Crypto.randomUUID */ - generateId?: () => string; -} - -const DEFAULT_NAME = "h3"; -const DEFAULT_COOKIE: SessionConfig["cookie"] = { - path: "/", - secure: true, - httpOnly: true, -}; +import { + DEFAULT_SESSION_NAME, + DEFAULT_SESSION_COOKIE, + _kGetSession, +} from "./internal/session"; /** * Create a session manager for the current request. * */ -export async function useSession( +export async function useSession( event: H3Event, config: SessionConfig, ) { // Create a synced wrapper around the session - const sessionName = config.name || DEFAULT_NAME; + const sessionName = config.name || DEFAULT_SESSION_NAME; await getSession(event, config); // Force init const sessionManager = { get id() { @@ -74,11 +42,11 @@ export async function useSession( /** * Get the session for the current request. */ -export async function getSession( +export async function getSession( event: H3Event, config: SessionConfig, ): Promise> { - const sessionName = config.name || DEFAULT_NAME; + const sessionName = config.name || DEFAULT_SESSION_NAME; // Return existing session if available if (!event.context.sessions) { @@ -87,7 +55,7 @@ export async function getSession( // Wait for existing session to load const existingSession = event.context.sessions![sessionName] as Session; if (existingSession) { - return existingSession[getSessionPromise] || existingSession; + return existingSession[_kGetSession] || existingSession; } // Prepare an empty session object and store in context @@ -106,7 +74,7 @@ export async function getSession( typeof config.sessionHeader === "string" ? config.sessionHeader.toLowerCase() : `x-${sessionName.toLowerCase()}-session`; - const headerValue = event.node.req.headers[headerName]; + const headerValue = event[_kRaw].getHeader(headerName); if (typeof headerValue === "string") { sealedSession = headerValue; } @@ -121,10 +89,10 @@ export async function getSession( .catch(() => {}) .then((unsealed) => { Object.assign(session, unsealed); - delete event.context.sessions![sessionName][getSessionPromise]; + delete event.context.sessions![sessionName][_kGetSession]; return session as Session; }); - event.context.sessions![sessionName][getSessionPromise] = promise; + event.context.sessions![sessionName][_kGetSession] = promise; await promise; } @@ -139,19 +107,19 @@ export async function getSession( return session; } -type SessionUpdate = +type SessionUpdate = | Partial> | ((oldData: SessionData) => Partial> | undefined); /** * Update the session data for the current request. */ -export async function updateSession( +export async function updateSession( event: H3Event, config: SessionConfig, update?: SessionUpdate, ): Promise> { - const sessionName = config.name || DEFAULT_NAME; + const sessionName = config.name || DEFAULT_SESSION_NAME; // Access current session const session: Session = @@ -170,7 +138,7 @@ export async function updateSession( if (config.cookie !== false) { const sealed = await sealSession(event, config); setCookie(event, sessionName, sealed, { - ...DEFAULT_COOKIE, + ...DEFAULT_SESSION_COOKIE, expires: config.maxAge ? new Date(session.createdAt + config.maxAge * 1000) : undefined, @@ -184,11 +152,11 @@ export async function updateSession( /** * Encrypt and sign the session data for the current request. */ -export async function sealSession( +export async function sealSession( event: H3Event, config: SessionConfig, ) { - const sessionName = config.name || DEFAULT_NAME; + const sessionName = config.name || DEFAULT_SESSION_NAME; // Access current session const session: Session = @@ -238,12 +206,12 @@ export function clearSession( event: H3Event, config: Partial, ): Promise { - const sessionName = config.name || DEFAULT_NAME; + const sessionName = config.name || DEFAULT_SESSION_NAME; if (event.context.sessions?.[sessionName]) { delete event.context.sessions![sessionName]; } setCookie(event, sessionName, "", { - ...DEFAULT_COOKIE, + ...DEFAULT_SESSION_COOKIE, ...config.cookie, }); return Promise.resolve(); diff --git a/src/utils/sse/utils.ts b/src/utils/sse/utils.ts deleted file mode 100644 index 9e0019a9..00000000 --- a/src/utils/sse/utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { TypedHeaders } from "../../types"; -import { H3Event } from "../../event"; -import { getHeader } from "../request"; -import { setResponseHeaders } from "../response"; -import { EventStreamMessage } from "./types"; - -export function formatEventStreamMessage(message: EventStreamMessage): string { - let result = ""; - if (message.id) { - result += `id: ${message.id}\n`; - } - if (message.event) { - result += `event: ${message.event}\n`; - } - if (typeof message.retry === "number" && Number.isInteger(message.retry)) { - result += `retry: ${message.retry}\n`; - } - result += `data: ${message.data}\n\n`; - return result; -} - -export function formatEventStreamMessages( - messages: EventStreamMessage[], -): string { - let result = ""; - for (const msg of messages) { - result += formatEventStreamMessage(msg); - } - return result; -} - -export function setEventStreamHeaders(event: H3Event) { - const headers: TypedHeaders = { - "Content-Type": "text/event-stream", - "Cache-Control": - "private, no-cache, no-store, no-transform, must-revalidate, max-age=0", - "X-Accel-Buffering": "no", // prevent nginx from buffering the response - }; - - if (!isHttp2Request(event)) { - headers.Connection = "keep-alive"; - } - - setResponseHeaders(event, headers); -} - -export function isHttp2Request(event: H3Event) { - return ( - getHeader(event, ":path") !== undefined && - getHeader(event, ":method") !== undefined - ); -} diff --git a/src/utils/static.ts b/src/utils/static.ts index aec9c7b6..2e79a01f 100644 --- a/src/utils/static.ts +++ b/src/utils/static.ts @@ -1,64 +1,12 @@ +import type { H3Event, StaticAssetMeta, ServeStaticOptions } from "../types"; +import { decodePath } from "ufo"; +import { _kRaw } from "../event"; +import { createError } from "../error"; import { - decodePath, - parseURL, withLeadingSlash, withoutTrailingSlash, -} from "ufo"; -import { H3Event } from "../event"; -import { createError } from "../error"; -import { getRequestHeader } from "./request"; -import { - getResponseHeader, - setResponseHeader, - setResponseStatus, - send, - isStream, - sendStream, -} from "./response"; - -export interface StaticAssetMeta { - type?: string; - etag?: string; - mtime?: number | string | Date; - path?: string; - size?: number; - encoding?: string; -} - -export interface ServeStaticOptions { - /** - * This function should resolve asset meta - */ - getMeta: ( - id: string, - ) => StaticAssetMeta | undefined | Promise; - - /** - * This function should resolve asset content - */ - getContents: (id: string) => unknown | Promise; - - /** - * Map of supported encodings (compressions) and their file extensions. - * - * Each extension will be appended to the asset path to find the compressed version of the asset. - * - * @example { gzip: ".gz", br: ".br" } - */ - encodings?: Record; - - /** - * Default index file to serve when the path is a directory - * - * @default ["/index.html"] - */ - indexNames?: string[]; - - /** - * When set to true, the function will not throw 404 error when the asset meta is not found or meta validation failed - */ - fallthrough?: boolean; -} + getPathname, +} from "./internal/path"; /** * Dynamically serve static assets based on the request path. @@ -78,16 +26,16 @@ export async function serveStatic( } const originalId = decodePath( - withLeadingSlash(withoutTrailingSlash(parseURL(event.path).pathname)), + withLeadingSlash(withoutTrailingSlash(getPathname(event.path))), ); const acceptEncodings = parseAcceptEncoding( - getRequestHeader(event, "accept-encoding"), + event[_kRaw].getHeader("accept-encoding") || "", options.encodings, ); if (acceptEncodings.length > 1) { - setResponseHeader(event, "vary", "accept-encoding"); + event[_kRaw].setResponseHeader("vary", "accept-encoding"); } let id = originalId; @@ -118,55 +66,55 @@ export async function serveStatic( return false; } - if (meta.etag && !getResponseHeader(event, "etag")) { - setResponseHeader(event, "etag", meta.etag); + if (meta.etag && !event[_kRaw].getResponseHeader("etag")) { + event[_kRaw].setResponseHeader("etag", meta.etag); } const ifNotMatch = - meta.etag && getRequestHeader(event, "if-none-match") === meta.etag; + meta.etag && event[_kRaw].getHeader("if-none-match") === meta.etag; if (ifNotMatch) { - setResponseStatus(event, 304, "Not Modified"); - return send(event, ""); + event[_kRaw].responseCode = 304; + event[_kRaw].responseMessage = "Not Modified"; + return event?.[_kRaw]?.sendResponse(""); } if (meta.mtime) { const mtimeDate = new Date(meta.mtime); - const ifModifiedSinceH = getRequestHeader(event, "if-modified-since"); + const ifModifiedSinceH = event[_kRaw].getHeader("if-modified-since"); if (ifModifiedSinceH && new Date(ifModifiedSinceH) >= mtimeDate) { - setResponseStatus(event, 304, "Not Modified"); - return send(event, null); + event[_kRaw].responseCode = 304; + event[_kRaw].responseMessage = "Not Modified"; + return event?.[_kRaw]?.sendResponse(""); } - if (!getResponseHeader(event, "last-modified")) { - setResponseHeader(event, "last-modified", mtimeDate.toUTCString()); + if (!event[_kRaw].getResponseHeader("last-modified")) { + event[_kRaw].setResponseHeader("last-modified", mtimeDate.toUTCString()); } } - if (meta.type && !getResponseHeader(event, "content-type")) { - setResponseHeader(event, "content-type", meta.type); + if (meta.type && !event[_kRaw].getResponseHeader("content-type")) { + event[_kRaw].setResponseHeader("content-type", meta.type); } - if (meta.encoding && !getResponseHeader(event, "content-encoding")) { - setResponseHeader(event, "content-encoding", meta.encoding); + if (meta.encoding && !event[_kRaw].getResponseHeader("content-encoding")) { + event[_kRaw].setResponseHeader("content-encoding", meta.encoding); } if ( meta.size !== undefined && meta.size > 0 && - !getResponseHeader(event, "content-length") + !event[_kRaw].getHeader("content-length") ) { - setResponseHeader(event, "content-length", meta.size); + event[_kRaw].setResponseHeader("content-length", meta.size + ""); } if (event.method === "HEAD") { - return send(event, null); + return event?.[_kRaw]?.sendResponse(); } const contents = await options.getContents(id); - return isStream(contents) - ? sendStream(event, contents) - : send(event, contents); + return event?.[_kRaw]?.sendResponse(contents); } // --- Internal Utils --- diff --git a/src/utils/ws.ts b/src/utils/ws.ts index 74b51958..488238c5 100644 --- a/src/utils/ws.ts +++ b/src/utils/ws.ts @@ -1,7 +1,6 @@ import type { Hooks as WSHooks } from "crossws"; - import { createError } from "../error"; -import { defineEventHandler } from "../event"; +import { defineEventHandler } from "../handler"; /** * Define WebSocket hooks. diff --git a/test/_utils.ts b/test/_utils.ts new file mode 100644 index 00000000..6c9e3edd --- /dev/null +++ b/test/_utils.ts @@ -0,0 +1,104 @@ +import type { Mock } from "vitest"; +import type { App, AppOptions, H3Error, H3Event } from "../src/types"; + +import { beforeEach, afterEach, vi } from "vitest"; +import supertest from "supertest"; +import { Server as NodeServer } from "node:http"; +import { Client as UndiciClient } from "undici"; +import { getRandomPort } from "get-port-please"; +import { createApp } from "../src"; +import { NodeHandler, toNodeHandler } from "../src/adapters/node"; +import { toPlainHandler, toWebHandler } from "../src/adapters/web"; +import type { PlainHandler, WebHandler } from "../src/types"; + +interface TestContext { + request: ReturnType; + webHandler: WebHandler; + nodeHandler: NodeHandler; + plainHandler: PlainHandler; + server: NodeServer; + client: UndiciClient; + url: string; + app: App; + + errors: H3Error[]; + + onRequest: Mock>>; + onError: Mock>>; + onBeforeResponse: Mock< + Parameters> + >; + onAfterResponse: Mock< + Parameters> + >; +} + +export function setupTest(opts: { allowUnhandledErrors?: boolean } = {}) { + const ctx: TestContext = {} as TestContext; + + beforeEach(async () => { + ctx.onRequest = vi.fn(); + ctx.onBeforeResponse = vi.fn(); + ctx.onAfterResponse = vi.fn(); + ctx.errors = []; + ctx.onError = vi.fn((error, _event: H3Event) => { + ctx.errors.push(error); + }); + + ctx.app = createApp({ + debug: true, + onError: ctx.onError, + onRequest: ctx.onRequest, + onBeforeResponse: ctx.onBeforeResponse, + onAfterResponse: ctx.onAfterResponse, + }); + + ctx.webHandler = toWebHandler(ctx.app); + ctx.nodeHandler = toNodeHandler(ctx.app); + ctx.plainHandler = toPlainHandler(ctx.app); + + ctx.request = supertest(ctx.nodeHandler); + + ctx.server = new NodeServer(ctx.nodeHandler); + const port = await getRandomPort(); + await new Promise((resolve) => ctx.server.listen(port, resolve)); + ctx.url = `http://localhost:${port}`; + ctx.client = new UndiciClient(ctx.url); + }); + + afterEach(async () => { + vi.resetAllMocks(); + + await Promise.all([ + ctx.client.close(), + new Promise((resolve, reject) => + ctx.server.close((error) => (error ? reject(error) : resolve())), + ), + ]).catch(console.error); + + if (opts.allowUnhandledErrors) { + return; + } + const unhandledErrors = ctx.errors.filter( + (error) => error.unhandled !== false, + ); + if (unhandledErrors.length > 0) { + throw _mergeErrors(ctx.errors); + } + }); + + return ctx; +} + +function _mergeErrors(err: Error | Error[]) { + if (Array.isArray(err)) { + if (err.length === 1) { + return _mergeErrors(err[0]); + } + return new Error( + "[tests] H3 global errors: \n" + + err.map((error) => " - " + (error.stack || "")).join("\n"), + ); + } + return new Error("[tests] H3 global error: " + (err.stack || "")); +} diff --git a/test/app.test.ts b/test/app.test.ts index a8d722c4..4983c986 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -1,80 +1,53 @@ import { Readable, Transform } from "node:stream"; -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; -import { - createApp, - toNodeListener, - App, - eventHandler, - fromNodeMiddleware, - createError, -} from "../src"; +import { describe, it, expect } from "vitest"; +import { fromNodeHandler } from "../src/adapters/node"; +import { eventHandler, createError, setResponseHeader } from "../src"; +import { setupTest } from "./_utils"; describe("app", () => { - let app: App; - let request: SuperTest; - - const onRequest = vi.fn(); - const onBeforeResponse = vi.fn(); - const onAfterResponse = vi.fn(); - const onError = vi.fn(); - - beforeEach(() => { - app = createApp({ - debug: true, - onError, - onRequest, - onBeforeResponse, - onAfterResponse, - }); - request = supertest(toNodeListener(app)); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); + const ctx = setupTest(); it("can return JSON directly", async () => { - app.use( + ctx.app.use( "/api", eventHandler((event) => ({ url: event.path })), ); - const res = await request.get("/api"); + const res = await ctx.request.get("/api"); expect(res.body).toEqual({ url: "/" }); }); it("can return bigint directly", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => BigInt(9_007_199_254_740_991)), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.text).toBe("9007199254740991"); }); it("throws error when returning symbol or function", async () => { - app.use( + ctx.app.use( "/fn", eventHandler(() => { return function foo() {}; }), ); - app.use( + ctx.app.use( "/symbol", eventHandler(() => { return Symbol.for("foo"); }), ); - const resFn = await request.get("/fn"); + const resFn = await ctx.request.get("/fn"); expect(resFn.status).toBe(500); expect(resFn.body.statusMessage).toBe( "[h3] Cannot send function as response.", ); - const resSymbol = await request.get("/symbol"); + const resSymbol = await ctx.request.get("/symbol"); expect(resSymbol.status).toBe(500); expect(resSymbol.body.statusMessage).toBe( "[h3] Cannot send symbol as response.", @@ -82,7 +55,7 @@ describe("app", () => { }); it("can return Response directly", async () => { - app.use( + ctx.app.use( "/", eventHandler( () => @@ -93,17 +66,17 @@ describe("app", () => { ), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.statusCode).toBe(201); expect(res.text).toBe("Hello World!"); }); it("can return a 204 response", async () => { - app.use( + ctx.app.use( "/api", eventHandler(() => null), ); - const res = await request.get("/api"); + const res = await ctx.request.get("/api"); expect(res.statusCode).toBe(204); expect(res.text).toEqual(""); @@ -113,16 +86,18 @@ describe("app", () => { it("can return primitive values", async () => { const values = [true, false, 42, 0, 1]; for (const value of values) { - app.use( + ctx.app.use( `/${value}`, eventHandler(() => value), ); - expect(await request.get(`/${value}`).then((r) => r.body)).toEqual(value); + expect(await ctx.request.get(`/${value}`).then((r) => r.body)).toEqual( + value, + ); } }); it("can return Blob directly", async () => { - app.use( + ctx.app.use( eventHandler( () => new Blob(["

Hello World

"], { @@ -130,21 +105,23 @@ describe("app", () => { }), ), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.headers["content-type"]).toBe("text/html"); expect(res.text).toBe("

Hello World

"); }); it("can return Buffer directly", async () => { - app.use(eventHandler(() => Buffer.from("

Hello world!

", "utf8"))); - const res = await request.get("/"); + ctx.app.use( + eventHandler(() => Buffer.from("

Hello world!

", "utf8")), + ); + const res = await ctx.request.get("/"); expect(res.text).toBe("

Hello world!

"); }); it("Node.js Readable Stream", async () => { - app.use( + ctx.app.use( eventHandler(() => { return new Readable({ read() { @@ -154,14 +131,14 @@ describe("app", () => { }); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.text).toBe("

Hello world!

"); expect(res.header["transfer-encoding"]).toBe("chunked"); }); it("Node.js Readable Stream with Error", async () => { - app.use( + ctx.app.use( eventHandler(() => { return new Readable({ read() { @@ -181,13 +158,13 @@ describe("app", () => { ); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.statusCode).toBe(500); expect(JSON.parse(res.text).statusMessage).toBe("test"); }); it("Web Stream", async () => { - app.use( + ctx.app.use( eventHandler(() => { return new ReadableStream({ start(controller) { @@ -198,14 +175,14 @@ describe("app", () => { }); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.text).toBe("

Hello world!

"); expect(res.header["transfer-encoding"]).toBe("chunked"); }); it("Web Stream with Error", async () => { - app.use( + ctx.app.use( eventHandler(() => { return new ReadableStream({ start() { @@ -217,48 +194,48 @@ describe("app", () => { }); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.statusCode).toBe(500); expect(JSON.parse(res.text).statusMessage).toBe("test"); }); it("can return HTML directly", async () => { - app.use(eventHandler(() => "

Hello world!

")); - const res = await request.get("/"); + ctx.app.use(eventHandler(() => "

Hello world!

")); + const res = await ctx.request.get("/"); expect(res.text).toBe("

Hello world!

"); expect(res.header["content-type"]).toBe("text/html"); }); it("allows overriding Content-Type", async () => { - app.use( + ctx.app.use( eventHandler((event) => { - event.node.res.setHeader("content-type", "text/xhtml"); + setResponseHeader(event, "content-type", "text/xhtml"); return "

Hello world!

"; }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.header["content-type"]).toBe("text/xhtml"); }); it("can match simple prefixes", async () => { - app.use( + ctx.app.use( "/1", eventHandler(() => "prefix1"), ); - app.use( + ctx.app.use( "/2", eventHandler(() => "prefix2"), ); - const res = await request.get("/2"); + const res = await ctx.request.get("/2"); expect(res.text).toBe("prefix2"); }); it("can chain .use calls", async () => { - app + ctx.app .use( "/1", eventHandler(() => "prefix1"), @@ -267,116 +244,117 @@ describe("app", () => { "/2", eventHandler(() => "prefix2"), ); - const res = await request.get("/2"); + const res = await ctx.request.get("/2"); expect(res.text).toBe("prefix2"); }); it("can use async routes", async () => { - app.use( + ctx.app.use( "/promise", eventHandler(async () => { return await Promise.resolve("42"); }), ); - app.use(eventHandler(async () => {})); + ctx.app.use(eventHandler(async () => {})); - const res = await request.get("/promise"); + const res = await ctx.request.get("/promise"); expect(res.text).toBe("42"); }); it("can use route arrays", async () => { - app.use( + ctx.app.use( ["/1", "/2"], eventHandler(() => "valid"), ); - const responses = [await request.get("/1"), await request.get("/2")].map( - (r) => r.text, - ); + const responses = [ + await ctx.request.get("/1"), + await ctx.request.get("/2"), + ].map((r) => r.text); expect(responses).toEqual(["valid", "valid"]); }); it("can use handler arrays", async () => { - app.use("/", [ + ctx.app.use("/", [ eventHandler(() => {}), eventHandler(() => {}), eventHandler(() => {}), eventHandler(eventHandler(() => "valid")), ]); - const response = await request.get("/"); + const response = await ctx.request.get("/"); expect(response.text).toEqual("valid"); }); it("prohibits use of next() in non-promisified handlers", () => { - app.use( + ctx.app.use( "/", eventHandler(() => {}), ); }); it("handles next() call with no routes matching", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => {}), ); - app.use( + ctx.app.use( "/", eventHandler(() => {}), ); - const response = await request.get("/"); + const response = await ctx.request.get("/"); expect(response.status).toEqual(404); }); it("can take an object", async () => { - app.use({ route: "/", handler: eventHandler(() => "valid") }); + ctx.app.use({ route: "/", handler: eventHandler(() => "valid") }); - const response = await request.get("/"); + const response = await ctx.request.get("/"); expect(response.text).toEqual("valid"); }); it("can short-circuit route matching", async () => { - app.use( - eventHandler((event) => { - event.node.res.end("done"); + ctx.app.use( + eventHandler((_event) => { + return "done"; }), ); - app.use(eventHandler(() => "valid")); + ctx.app.use(eventHandler(() => "valid")); - const response = await request.get("/"); + const response = await ctx.request.get("/"); expect(response.text).toEqual("done"); }); it("can use a custom matcher", async () => { - app.use( + ctx.app.use( "/odd", eventHandler(() => "Is odd!"), { match: (url) => Boolean(Number(url.slice(1)) % 2) }, ); - const res = await request.get("/odd/41"); + const res = await ctx.request.get("/odd/41"); expect(res.text).toBe("Is odd!"); - const notFound = await request.get("/odd/2"); + const notFound = await ctx.request.get("/odd/2"); expect(notFound.status).toBe(404); }); it("can normalise route definitions", async () => { - app.use( + ctx.app.use( "/test/", eventHandler(() => "valid"), ); - const res = await request.get("/test"); + const res = await ctx.request.get("/test"); expect(res.text).toBe("valid"); }); it("wait for middleware (req, res, next)", async () => { - app.use( + ctx.app.use( "/", - fromNodeMiddleware((_req, res, next) => { + fromNodeHandler((_req, res, next) => { setTimeout(() => { res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ works: 1 })); @@ -384,93 +362,7 @@ describe("app", () => { }, 10); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.body).toEqual({ works: 1 }); }); - - it("calls onRequest and onResponse", async () => { - app.use(eventHandler(() => Promise.resolve("Hello World!"))); - await request.get("/foo"); - - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest.mock.calls[0][0].path).toBe("/foo"); - - expect(onError).toHaveBeenCalledTimes(0); - - expect(onBeforeResponse).toHaveBeenCalledTimes(1); - expect(onBeforeResponse.mock.calls[0][1].body).toBe("Hello World!"); - - expect(onAfterResponse).toHaveBeenCalledTimes(1); - expect(onAfterResponse.mock.calls[0][1].body).toBe("Hello World!"); - }); - - it("сalls onRequest and onResponse when an exception is thrown", async () => { - app.use( - eventHandler(() => { - throw createError({ - statusCode: 503, - }); - }), - ); - await request.get("/foo"); - - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest.mock.calls[0][0].path).toBe("/foo"); - - expect(onError).toHaveBeenCalledTimes(1); - expect(onError.mock.calls[0][0].statusCode).toBe(503); - expect(onError.mock.calls[0][1].path).toBe("/foo"); - - expect(onBeforeResponse).toHaveBeenCalledTimes(1); - expect(onBeforeResponse.mock.calls[0][0].node.res.statusCode).toBe(503); - - expect(onAfterResponse).toHaveBeenCalledTimes(1); - expect(onAfterResponse.mock.calls[0][0].node.res.statusCode).toBe(503); - }); - - it("calls onRequest and onResponse when an error is returned", async () => { - app.use( - eventHandler(() => { - return createError({ - statusCode: 404, - }); - }), - ); - await request.get("/foo"); - - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest.mock.calls[0][0].path).toBe("/foo"); - - expect(onError).toHaveBeenCalledTimes(1); - - expect(onBeforeResponse).toHaveBeenCalledTimes(1); - expect(onBeforeResponse.mock.calls[0][0].node.res.statusCode).toBe(404); - - expect(onAfterResponse).toHaveBeenCalledTimes(1); - expect(onAfterResponse.mock.calls[0][0].node.res.statusCode).toBe(404); - }); - - it("calls onRequest and onResponse when an unhandled error occurs", async () => { - app.use( - eventHandler((event) => { - // @ts-expect-error - return event.unknown.property; - }), - ); - await request.get("/foo"); - - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest.mock.calls[0][0].path).toBe("/foo"); - - expect(onError).toHaveBeenCalledTimes(1); - expect(onError.mock.calls[0][0].statusCode).toBe(500); - expect(onError.mock.calls[0][0].cause).toBeInstanceOf(TypeError); - expect(onError.mock.calls[0][1].path).toBe("/foo"); - - expect(onBeforeResponse).toHaveBeenCalledTimes(1); - expect(onBeforeResponse.mock.calls[0][0].node.res.statusCode).toBe(500); - - expect(onAfterResponse).toHaveBeenCalledTimes(1); - expect(onAfterResponse.mock.calls[0][0].node.res.statusCode).toBe(500); - }); }); diff --git a/test/body.test.ts b/test/body.test.ts index 66d1b547..ea2e7654 100644 --- a/test/body.test.ts +++ b/test/body.test.ts @@ -1,332 +1,315 @@ -import { Server } from "node:http"; import { createReadStream } from "node:fs"; import { readFile } from "node:fs/promises"; -import getPort from "get-port"; -import { Client } from "undici"; -import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { - createApp, - toNodeListener, - App, readRawBody, - readBody, + readTextBody, + readJSONBody, eventHandler, - readMultipartFormData, + readFormDataBody, } from "../src"; +import { setupTest } from "./_utils"; describe("body", () => { - let app: App; - let server: Server; - let client: Client; - - beforeEach(async () => { - app = createApp({ debug: true }); - server = new Server(toNodeListener(app)); - const port = await getPort(); - server.listen(port); - client = new Client(`http://localhost:${port}`); - }); + const ctx = setupTest(); + + it("can read simple string", async () => { + ctx.app.use( + "/", + eventHandler(async (request) => { + const body = await readTextBody(request); + expect(body).toEqual('{"bool":true,"name":"string","number":1}'); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + body: JSON.stringify({ + bool: true, + name: "string", + number: 1, + }), + }); - afterEach(() => { - client.close(); - server.close(); + expect(await result.body.text()).toBe("200"); }); - describe("readRawBody", () => { - it("can handle raw string", async () => { - app.use( - "/", - eventHandler(async (request) => { - const body = await readRawBody(request); - expect(body).toEqual('{"bool":true,"name":"string","number":1}'); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - body: JSON.stringify({ - bool: true, - name: "string", - number: 1, - }), - }); - - expect(await result.body.text()).toBe("200"); + it("can read chunked string", async () => { + const requestJsonUrl = new URL("assets/sample.json", import.meta.url); + ctx.app.use( + "/", + eventHandler(async (request) => { + const body = await readTextBody(request); + const json = (await readFile(requestJsonUrl)).toString("utf8"); + + expect(body).toEqual(json); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + body: createReadStream(requestJsonUrl), }); - it("can handle chunked string", async () => { - const requestJsonUrl = new URL("assets/sample.json", import.meta.url); - app.use( - "/", - eventHandler(async (request) => { - const body = await readRawBody(request); - const json = (await readFile(requestJsonUrl)).toString("utf8"); - - expect(body).toEqual(json); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - body: createReadStream(requestJsonUrl), - }); - - expect(await result.body.text()).toBe("200"); - }); + expect(await result.body.text()).toBe("200"); + }); - it("returns undefined if body is not present", async () => { - let _body: string | undefined = "initial"; - app.use( - "/", - eventHandler(async (request) => { - _body = await readRawBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - }); - - expect(_body).toBeUndefined(); - expect(await result.body.text()).toBe("200"); + it("returns undefined if body is not present", async () => { + let _body: string | undefined = "initial"; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readTextBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", }); - it("returns an empty string if body is empty", async () => { - let _body: string | undefined = "initial"; - app.use( - "/", - eventHandler(async (request) => { - _body = await readRawBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - body: '""', - }); - - expect(_body).toBe('""'); - expect(await result.body.text()).toBe("200"); + expect(_body).toBeUndefined(); + expect(await result.body.text()).toBe("200"); + }); + + it("returns an empty string if body is string", async () => { + let _body: string | undefined = "initial"; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readJSONBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + body: '""', }); - it("returns an empty object string if body is empty object", async () => { - let _body: string | undefined = "initial"; - app.use( - "/", - eventHandler(async (request) => { - _body = await readRawBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - body: "{}", - }); - - expect(_body).toBe("{}"); - expect(await result.body.text()).toBe("200"); + expect(_body).toBe(""); + expect(await result.body.text()).toBe("200"); + }); + + it("returns an empty object string if body is empty object", async () => { + let _body: string | undefined = "initial"; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readTextBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + body: "{}", }); + + expect(_body).toBe("{}"); + expect(await result.body.text()).toBe("200"); }); - describe("readBody", () => { - it("can parse json payload", async () => { - app.use( - "/", - eventHandler(async (request) => { - const body = await readBody(request); - expect(body).toMatchObject({ - bool: true, - name: "string", - number: 1, - }); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - body: JSON.stringify({ + it("can parse json payload", async () => { + ctx.app.use( + "/", + eventHandler(async (request) => { + const body = await readJSONBody(request); + expect(body).toMatchObject({ bool: true, name: "string", number: 1, - }), - }); + }); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + body: JSON.stringify({ + bool: true, + name: "string", + number: 1, + }), + }); - expect(await result.body.text()).toBe("200"); + expect(await result.body.text()).toBe("200"); + }); + + it("handles non-present body", async () => { + let _body: string | undefined; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readJSONBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", }); + expect(_body).toBeUndefined(); + expect(await result.body.text()).toBe("200"); + }); - it("handles non-present body", async () => { - let _body: string | undefined; - app.use( - "/", - eventHandler(async (request) => { - _body = await readBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - }); - expect(_body).toBeUndefined(); - expect(await result.body.text()).toBe("200"); + it("handles empty body", async () => { + let _body: string | undefined = "initial"; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readTextBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: '""', }); + expect(_body).toStrictEqual('""'); + expect(await result.body.text()).toBe("200"); + }); - it("handles empty body", async () => { - let _body: string | undefined = "initial"; - app.use( - "/", - eventHandler(async (request) => { - _body = await readBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "text/plain", - }, - body: '""', - }); - expect(_body).toStrictEqual('""'); - expect(await result.body.text()).toBe("200"); + it("handles empty object as body", async () => { + let _body: string | undefined = "initial"; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readJSONBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + body: "{}", }); + expect(_body).toStrictEqual({}); + expect(await result.body.text()).toBe("200"); + }); - it("handles empty object as body", async () => { - let _body: string | undefined = "initial"; - app.use( - "/", - eventHandler(async (request) => { - _body = await readBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - body: "{}", - }); - expect(_body).toStrictEqual({}); - expect(await result.body.text()).toBe("200"); + it("parse the form encoded into an object", async () => { + ctx.app.use( + "/", + eventHandler(async (request) => { + const body = await readJSONBody(request); + expect(body).toMatchObject({ + field: "value", + another: "true", + number: ["20", "30", "40"], + }); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + }, + body: "field=value&another=true&number=20&number=30&number=40", }); - it("parse the form encoded into an object", async () => { - app.use( - "/", - eventHandler(async (request) => { - const body = await readBody(request); - expect(body).toMatchObject({ - field: "value", - another: "true", - number: ["20", "30", "40"], - }); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", - }, - body: "field=value&another=true&number=20&number=30&number=40", - }); - - expect(await result.body.text()).toBe("200"); + expect(await result.body.text()).toBe("200"); + }); + + it.skip("handle readBody with buffer type (unenv)", async () => { + ctx.app.use( + "/", + eventHandler(async (event) => { + // Emulate unenv + // @ts-ignore + event.node.req.body = Buffer.from("test"); + + const body = await readJSONBody(event); + expect(body).toMatchObject("test"); + + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", }); - it("handle readBody with buffer type (unenv)", async () => { - app.use( - "/", - eventHandler(async (event) => { - // Emulate unenv - // @ts-ignore - event.node.req.body = Buffer.from("test"); - - const body = await readBody(event); - expect(body).toMatchObject("test"); - - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - }); - - expect(await result.body.text()).toBe("200"); + expect(await result.body.text()).toBe("200"); + }); + + it.skip("handle readBody with Object type (unenv)", async () => { + ctx.app.use( + "/", + eventHandler(async (event) => { + // Emulate unenv + // @ts-ignore + event.node.req.body = { test: 1 }; + + const body = await readJSONBody(event); + expect(body).toMatchObject({ test: 1 }); + + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", }); - it("handle readBody with Object type (unenv)", async () => { - app.use( - "/", - eventHandler(async (event) => { - // Emulate unenv - // @ts-ignore - event.node.req.body = { test: 1 }; - - const body = await readBody(event); - expect(body).toMatchObject({ test: 1 }); - - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - }); - - expect(await result.body.text()).toBe("200"); + expect(await result.body.text()).toBe("200"); + }); + + it.skip("handle readRawBody with array buffer type (unenv)", async () => { + ctx.app.use( + "/", + eventHandler(async (event) => { + // Emulate unenv + // @ts-ignore + event.node.req.body = new Uint8Array([1, 2, 3]); + const body = await readRawBody(event); + expect(body).toBeInstanceOf(Buffer); + expect(body).toMatchObject(Buffer.from([1, 2, 3])); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", }); + expect(await result.body.text()).toBe("200"); + }); - it("handle readRawBody with array buffer type (unenv)", async () => { - app.use( - "/", - eventHandler(async (event) => { - // Emulate unenv - // @ts-ignore - event.node.req.body = new Uint8Array([1, 2, 3]); - const body = await readRawBody(event, false); - expect(body).toBeInstanceOf(Buffer); - expect(body).toMatchObject(Buffer.from([1, 2, 3])); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - }); - expect(await result.body.text()).toBe("200"); + it("parses multipart form data", async () => { + ctx.app.use( + "/", + eventHandler(async (request) => { + const formData = await readFormDataBody(request); + return [...formData!.entries()].map(([name, value]) => ({ + name, + data: value, + })); + }), + ); + + const formData = new FormData(); + formData.append("baz", "other"); + formData.append("号楼电表数据模版.xlsx", "something"); + + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "content-type": + "multipart/form-data; boundary=---------------------------12537827810750053901680552518", + }, + body: '-----------------------------12537827810750053901680552518\r\nContent-Disposition: form-data; name="baz"\r\n\r\nother\r\n-----------------------------12537827810750053901680552518\r\nContent-Disposition: form-data; name="号楼电表数据模版.xlsx"\r\n\r\nsomething\r\n-----------------------------12537827810750053901680552518--\r\n', }); - it("parses multipart form data", async () => { - app.use( - "/", - eventHandler(async (request) => { - const parts = (await readMultipartFormData(request)) || []; - return parts.map((part) => ({ - ...part, - data: part.data.toString("utf8"), - })); - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "content-type": - "multipart/form-data; boundary=---------------------------12537827810750053901680552518", - }, - body: '-----------------------------12537827810750053901680552518\r\nContent-Disposition: form-data; name="baz"\r\n\r\nother\r\n-----------------------------12537827810750053901680552518\r\nContent-Disposition: form-data; name="号楼电表数据模版.xlsx"\r\n\r\nsomething\r\n-----------------------------12537827810750053901680552518--\r\n', - }); - - expect(await result.body.json()).toMatchInlineSnapshot(` + expect(await result.body.json()).toMatchInlineSnapshot(` [ { "data": "other", @@ -338,114 +321,113 @@ describe("body", () => { }, ] `); - }); + }); - it("returns undefined if body is not present with text/plain", async () => { - let _body: string | undefined; - app.use( - "/", - eventHandler(async (request) => { - _body = await readBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "text/plain", - }, - }); - - expect(_body).toBeUndefined(); - expect(await result.body.text()).toBe("200"); + it("returns undefined if body is not present with text/plain", async () => { + let _body: string | undefined; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readTextBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "text/plain", + }, }); - it("returns undefined if body is not present with json", async () => { - let _body: string | undefined; - app.use( - "/", - eventHandler(async (request) => { - _body = await readBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - expect(_body).toBeUndefined(); - expect(await result.body.text()).toBe("200"); + expect(_body).toBeUndefined(); + expect(await result.body.text()).toBe("200"); + }); + + it("returns undefined if body is not present with json", async () => { + let _body: string | undefined; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readTextBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "application/json", + }, }); - it("returns the string if content type is text/*", async () => { - let _body: string | undefined; - app.use( - "/", - eventHandler(async (request) => { - _body = await readBody(request); - return "200"; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "text/*", - }, - body: '{ "hello": true }', - }); - - expect(_body).toBe('{ "hello": true }'); - expect(await result.body.text()).toBe("200"); + expect(_body).toBeUndefined(); + expect(await result.body.text()).toBe("200"); + }); + + it("returns the string if content type is text/*", async () => { + let _body: string | undefined; + ctx.app.use( + "/", + eventHandler(async (request) => { + _body = await readTextBody(request); + return "200"; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "text/*", + }, + body: '{ "hello": true }', }); - it("returns string as is if cannot parse with unknown content type", async () => { - app.use( - "/", - eventHandler(async (request) => { - const _body = await readBody(request); - return _body; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "application/foobar", - }, - body: "{ test: 123 }", - }); - - expect(result.statusCode).toBe(200); - expect(await result.body.text()).toBe("{ test: 123 }"); + expect(_body).toBe('{ "hello": true }'); + expect(await result.body.text()).toBe("200"); + }); + + it("returns string as is if cannot parse with unknown content type", async () => { + ctx.app.use( + "/", + eventHandler(async (request) => { + const _body = await readTextBody(request); + return _body; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "application/foobar", + }, + body: "{ test: 123 }", }); - it("fails if json is invalid", async () => { - app.use( - "/", - eventHandler(async (request) => { - const _body = await readBody(request); - return _body; - }), - ); - const result = await client.request({ - path: "/api/test", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: '{ "hello": true', - }); - const resultJson = (await result.body.json()) as any; - - expect(result.statusCode).toBe(400); - expect(resultJson.statusMessage).toBe("Bad Request"); - expect(resultJson.stack[0]).toBe("Error: Invalid JSON body"); + expect(result.statusCode).toBe(200); + expect(await result.body.text()).toBe("{ test: 123 }"); + }); + + it("fails if json is invalid", async () => { + ctx.app.use( + "/", + eventHandler(async (request) => { + const _body = await readJSONBody(request); + return _body; + }), + ); + const result = await ctx.client.request({ + path: "/api/test", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: '{ "hello": true', }); + const resultJson = (await result.body.json()) as any; + + expect(result.statusCode).toBe(400); + expect(resultJson.statusMessage).toBe("Bad Request"); + expect(resultJson.stack[0]).toBe("Error: Invalid JSON body"); }); }); diff --git a/test/cookie.test.ts b/test/cookie.test.ts index 3b569555..ea24f40a 100644 --- a/test/cookie.test.ts +++ b/test/cookie.test.ts @@ -1,20 +1,14 @@ -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach } from "vitest"; -import { createApp, toNodeListener, App, eventHandler } from "../src"; +import { describe, it, expect } from "vitest"; +import { eventHandler } from "../src"; import { getCookie, parseCookies, setCookie } from "../src/utils/cookie"; +import { setupTest } from "./_utils"; describe("", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("parseCookies", () => { it("can parse cookies", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { const cookies = parseCookies(event); @@ -23,7 +17,7 @@ describe("", () => { }), ); - const result = await request + const result = await ctx.request .get("/") .set("Cookie", ["Authorization=1234567"]); @@ -31,7 +25,7 @@ describe("", () => { }); it("can parse empty cookies", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { const cookies = parseCookies(event); @@ -40,7 +34,7 @@ describe("", () => { }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.text).toBe("200"); }); @@ -48,7 +42,7 @@ describe("", () => { describe("getCookie", () => { it("can parse cookie with name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { const authorization = getCookie(event, "Authorization"); @@ -57,7 +51,7 @@ describe("", () => { }), ); - const result = await request + const result = await ctx.request .get("/") .set("Cookie", ["Authorization=1234567"]); @@ -67,14 +61,14 @@ describe("", () => { describe("setCookie", () => { it("can set-cookie with setCookie", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { setCookie(event, "Authorization", "1234567", {}); return "200"; }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.headers["set-cookie"]).toEqual([ "Authorization=1234567; Path=/", ]); @@ -82,7 +76,7 @@ describe("", () => { }); it("can set cookies with the same name but different serializeOptions", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { setCookie(event, "Authorization", "1234567", { @@ -94,7 +88,7 @@ describe("", () => { return "200"; }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.headers["set-cookie"]).toEqual([ "Authorization=1234567; Domain=example1.test; Path=/", "Authorization=7654321; Domain=example2.test; Path=/", diff --git a/test/cors.test.ts b/test/cors.test.ts index cf1dba3c..d5a4edab 100644 --- a/test/cors.test.ts +++ b/test/cors.test.ts @@ -1,17 +1,16 @@ +import type { H3CorsOptions } from "../src/types"; import { expect, it, describe } from "vitest"; +import { fromPlainRequest } from "../src/adapters/web"; +import { isPreflightRequest, isCorsOriginAllowed } from "../src"; import { resolveCorsOptions, - isPreflightRequest, - isCorsOriginAllowed, createOriginHeaders, createMethodsHeaders, createCredentialsHeaders, createAllowHeaderHeaders, createExposeHeaders, createMaxAgeHeader, -} from "../src/utils/cors/utils"; -import type { H3Event } from "../src"; -import type { H3CorsOptions } from "../src/utils/cors"; +} from "../src/utils/internal/cors"; describe("resolveCorsOptions", () => { it("can merge default options and user options", () => { @@ -54,63 +53,51 @@ describe("resolveCorsOptions", () => { describe("isPreflightRequest", () => { it("can detect preflight request", () => { - const eventMock = { + const eventMock = fromPlainRequest({ + path: "/", method: "OPTIONS", - node: { - req: { - headers: { - origin: "http://example.com", - "access-control-request-method": "GET", - }, - }, + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", }, - } as H3Event; + }); expect(isPreflightRequest(eventMock)).toEqual(true); }); it("can detect request of non-OPTIONS method)", () => { - const eventMock = { - node: { - req: { - method: "GET", - headers: { - origin: "http://example.com", - "access-control-request-method": "GET", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "GET", + headers: { + origin: "https://example.com", + "access-control-request-method": "GET", }, - } as H3Event; + }); expect(isPreflightRequest(eventMock)).toEqual(false); }); it("can detect request without origin header", () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - "access-control-request-method": "GET", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + "access-control-request-method": "GET", }, - } as H3Event; + }); expect(isPreflightRequest(eventMock)).toEqual(false); }); it("can detect request without AccessControlRequestMethod header", () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - origin: "http://example.com", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + origin: "https://example.com", }, - } as H3Event; + }); expect(isPreflightRequest(eventMock)).toEqual(false); }); @@ -191,16 +178,13 @@ describe("isCorsOriginAllowed", () => { describe("createOriginHeaders", () => { it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` option is not defined, or `"*"`', () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - origin: "http://example.com", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + origin: "https://example.com", }, - } as H3Event; + }); const options1: H3CorsOptions = {}; const options2: H3CorsOptions = { origin: "*", @@ -215,14 +199,11 @@ describe("createOriginHeaders", () => { }); it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` header is not defined', () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: {}, - }, - }, - } as H3Event; + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: {}, + }); const options: H3CorsOptions = {}; expect(createOriginHeaders(eventMock, options)).toEqual({ @@ -231,16 +212,13 @@ describe("createOriginHeaders", () => { }); it('returns an object with `access-control-allow-origin` and `vary` keys if `origin` option is `"null"`', () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - origin: "http://example.com", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + origin: "https://example.com", }, - } as H3Event; + }); const options: H3CorsOptions = { origin: "null", }; @@ -252,16 +230,13 @@ describe("createOriginHeaders", () => { }); it("returns an object with `access-control-allow-origin` and `vary` keys if `origin` option and `origin` header matches", () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - origin: "http://example.com", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + origin: "http://example.com", }, - } as H3Event; + }); const options1: H3CorsOptions = { origin: ["http://example.com"], }; @@ -280,16 +255,13 @@ describe("createOriginHeaders", () => { }); it("returns an empty object if `origin` option is one that is not allowed", () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - origin: "http://example.com", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + origin: "https://example.com", }, - } as H3Event; + }); const options1: H3CorsOptions = { origin: ["http://example2.com"], }; @@ -354,16 +326,13 @@ describe("createCredentialsHeaders", () => { describe("createAllowHeaderHeaders", () => { it('returns an object with `access-control-allow-headers` and `vary` keys according to `access-control-request-headers` header if `allowHeaders` option is not defined, `"*"`, or an empty array', () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: { - "access-control-request-headers": "CUSTOM-HEADER", - }, - }, + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: { + "access-control-request-headers": "CUSTOM-HEADER", }, - } as H3Event; + }); const options1: H3CorsOptions = {}; const options2: H3CorsOptions = { allowHeaders: "*", @@ -387,14 +356,11 @@ describe("createAllowHeaderHeaders", () => { }); it("returns an object with `access-control-allow-headers` and `vary` keys according to `allowHeaders` option if `access-control-request-headers` header is not defined", () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: {}, - }, - }, - } as H3Event; + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: {}, + }); const options: H3CorsOptions = { allowHeaders: ["CUSTOM-HEADER"], }; @@ -406,14 +372,11 @@ describe("createAllowHeaderHeaders", () => { }); it('returns an empty object if `allowHeaders` option is not defined, `"*"`, or an empty array, and `access-control-request-headers` is not defined', () => { - const eventMock = { - node: { - req: { - method: "OPTIONS", - headers: {}, - }, - }, - } as H3Event; + const eventMock = fromPlainRequest({ + path: "/", + method: "OPTIONS", + headers: {}, + }); const options1: H3CorsOptions = {}; const options2: H3CorsOptions = { allowHeaders: "*", diff --git a/test/e2e.test.ts b/test/e2e.test.ts index cc3452fe..724215c1 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -1,46 +1,28 @@ -import { Server } from "node:http"; -import supertest, { SuperTest, Test } from "supertest"; -import getPort from "get-port"; -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; - -import { createApp, toNodeListener, App, eventHandler } from "../src"; -(global.console.error as any) = vi.fn(); +import { describe, it, expect } from "vitest"; +import { createError, eventHandler } from "../src"; +import { setupTest } from "./_utils"; describe("server", () => { - let app: App; - let request: SuperTest; - let server: Server; - - beforeEach(async () => { - app = createApp({ debug: false }); - server = new Server(toNodeListener(app)); - const port = await getPort(); - server.listen(port); - request = supertest(`http://localhost:${port}`); - }); - - afterEach(() => { - server.close(); - }); + const ctx = setupTest(); it("can serve requests", async () => { - app.use(eventHandler(() => "sample")); - const result = await request.get("/"); + ctx.app.use(eventHandler(() => "sample")); + const result = await ctx.request.get("/"); expect(result.text).toBe("sample"); }); it("can return 404s", async () => { - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.status).toBe(404); }); it("can return 500s", async () => { - app.use( + ctx.app.use( eventHandler(() => { - throw new Error("Unknown"); + throw createError("Unknown"); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.status).toBe(500); }); }); diff --git a/test/error.test.ts b/test/error.test.ts index 6406cc5e..4cc17edb 100644 --- a/test/error.test.ts +++ b/test/error.test.ts @@ -1,66 +1,42 @@ -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { - createApp, - App, - createError, - toNodeListener, - eventHandler, - H3Error, -} from "../src"; +import { describe, it, expect, vi } from "vitest"; +import { createError, eventHandler } from "../src"; +import { setupTest } from "./_utils"; const consoleMock = ((global.console.error as any) = vi.fn()); describe("error", () => { - let app: App; - let request: SuperTest; - - const capturedErrors: H3Error[] = []; - - beforeEach(() => { - app = createApp({ - debug: false, - onError(error) { - capturedErrors.push(error); - }, - }); - request = supertest(toNodeListener(app)); - }); - - afterEach(() => { - capturedErrors.length = 0; - }); + const ctx = setupTest({ allowUnhandledErrors: true }); it("logs errors", async () => { - app.use( + ctx.app.use( eventHandler(() => { throw createError({ statusMessage: "Unprocessable", statusCode: 422 }); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.status).toBe(422); }); it("returns errors", async () => { - app.use( + ctx.app.use( eventHandler(() => { throw createError({ statusMessage: "Unprocessable", statusCode: 422 }); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.status).toBe(422); }); it("can send internal error", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => { throw new Error("Booo"); }), ); - const result = await request.get("/api/test"); + const result = await ctx.request.get("/api/test"); expect(result.status).toBe(500); expect(JSON.parse(result.text)).toMatchObject({ @@ -71,7 +47,7 @@ describe("error", () => { it("can send runtime error", async () => { consoleMock.mockReset(); - app.use( + ctx.app.use( "/", eventHandler(() => { throw createError({ @@ -84,7 +60,7 @@ describe("error", () => { }), ); - const result = await request.get("/api/test"); + const result = await ctx.request.get("/api/test"); expect(result.status).toBe(400); expect(result.type).toMatch("application/json"); @@ -101,52 +77,52 @@ describe("error", () => { }); it("can handle errors in promises", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => { throw new Error("failed"); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.status).toBe(500); }); it("can handle returned Error", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => new Error("failed")), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.status).toBe(500); }); it("can handle returned H3Error", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => createError({ statusCode: 501 })), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.status).toBe(501); }); it("can access original error", async () => { class CustomError extends Error { - customError: true; + customError = true; } - app.use( + ctx.app.use( "/", eventHandler(() => { throw createError(new CustomError()); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.status).toBe(500); - expect(capturedErrors[0].cause).toBeInstanceOf(CustomError); + expect(ctx.errors[0].cause).toBeInstanceOf(CustomError); }); }); diff --git a/test/event.test.ts b/test/event.test.ts index 998662a7..94ba5386 100644 --- a/test/event.test.ts +++ b/test/event.test.ts @@ -1,26 +1,17 @@ -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { - createApp, - App, - toNodeListener, eventHandler, - readBody, - getRequestWebStream, + readJSONBody, + getBodyStream, getRequestURL, } from "../src"; +import { setupTest } from "./_utils"; describe("Event", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); it("can read the method", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { expect(event.method).toBe(event.method); @@ -28,12 +19,12 @@ describe("Event", () => { return "200"; }), ); - const result = await request.post("/hello"); + const result = await ctx.request.post("/hello"); expect(result.text).toBe("200"); }); it("can read the headers", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { return { @@ -41,34 +32,36 @@ describe("Event", () => { }; }), ); - const result = await request + const result = await ctx.request .post("/hello") .set("X-Test", "works") .set("Cookie", ["a", "b"]); - const { headers } = JSON.parse(result.text); - expect(headers.find(([key]) => key === "x-test")[1]).toBe("works"); - expect(headers.find(([key]) => key === "cookie")[1]).toBe("a; b"); + const { headers } = JSON.parse(result.text) as { + headers: [string, string][]; + }; + expect(headers.find(([key]) => key === "x-test")?.[1]).toBe("works"); + expect(headers.find(([key]) => key === "cookie")?.[1]).toBe("a; b"); }); it("can get request url", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { return getRequestURL(event); }), ); - const result = await request.get("/hello"); + const result = await ctx.request.get("/hello"); expect(result.text).toMatch(/http:\/\/127.0.0.1:\d+\/hello/); }); it("can read request body", async () => { - app.use( + ctx.app.use( "/", eventHandler(async (event) => { - const bodyStream = getRequestWebStream(event); + const bodyStream = getBodyStream(event); let bytes = 0; - // @ts-expect-error TODO: ReadableStream type is not async iterable! - for await (const chunk of bodyStream) { + // @ts-expect-error iterator + for await (const chunk of bodyStream!) { bytes += chunk.length; } return { @@ -77,26 +70,28 @@ describe("Event", () => { }), ); - const result = await request.post("/hello").send(Buffer.from([1, 2, 3])); + const result = await ctx.request + .post("/hello") + .send(Buffer.from([1, 2, 3])); expect(result.body).toMatchObject({ bytes: 3 }); }); it("can convert to a web request", async () => { - app.use( + ctx.app.use( "/", eventHandler(async (event) => { expect(event.method).toBe("POST"); expect(event.headers.get("x-test")).toBe("123"); // TODO: Find a workaround for Node.js 16 if (!process.versions.node.startsWith("16")) { - expect(await readBody(event)).toMatchObject({ hello: "world" }); + expect(await readJSONBody(event)).toMatchObject({ hello: "world" }); } return "200"; }), ); - const result = await request - .post("/hello") + const result = await ctx.request + .post("/") .set("x-test", "123") .set("content-type", "application/json") .send(JSON.stringify({ hello: "world" })); @@ -105,7 +100,7 @@ describe("Event", () => { }); it("can read path with URL", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { expect(event.path).toBe("/?url=https://example.com"); @@ -113,7 +108,7 @@ describe("Event", () => { }), ); - const result = await request.get("/?url=https://example.com"); + const result = await ctx.request.get("/?url=https://example.com"); expect(result.text).toBe("200"); }); diff --git a/test/header.test.ts b/test/header.test.ts index b99224c0..6d5f9634 100644 --- a/test/header.test.ts +++ b/test/header.test.ts @@ -1,64 +1,51 @@ -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { - createApp, - App, getRequestHeaders, - getHeaders, getRequestHeader, - getHeader, setResponseHeaders, - setHeaders, setResponseHeader, - setHeader, appendResponseHeaders, - appendHeaders, appendResponseHeader, - appendHeader, - toNodeListener, eventHandler, removeResponseHeader, clearResponseHeaders, } from "../src"; +import { setupTest } from "./_utils"; describe("", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("getRequestHeaders", () => { it("can return request headers", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { const headers = getRequestHeaders(event); - expect(headers).toEqual(event.node.req.headers); + expect(headers).toMatchObject({ + accept: "application/json", + }); }), ); - await request.get("/").set("Accept", "application/json"); + await ctx.request.get("/").set("Accept", "application/json"); }); }); - describe("getHeaders", () => { + describe("getRequestHeaders", () => { it("can return request headers", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - const headers = getHeaders(event); - expect(headers).toEqual(event.node.req.headers); + const headers = getRequestHeaders(event); + expect(headers).toMatchObject({ accept: "application/json" }); }), ); - await request.get("/").set("Accept", "application/json"); + await ctx.request.get("/").set("Accept", "application/json"); }); }); describe("getRequestHeader", () => { it("can return a value of request header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { expect(getRequestHeader(event, "accept")).toEqual("application/json"); @@ -66,177 +53,181 @@ describe("", () => { expect(getRequestHeader(event, "cookie")).toEqual("a; b; c"); }), ); - await request + await ctx.request .get("/") .set("Accept", "application/json") .set("Cookie", ["a", "b", "c"]); }); }); - describe("getHeader", () => { + describe("getRequestHeader", () => { it("can return a value of request header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - expect(getHeader(event, "accept")).toEqual("application/json"); - expect(getHeader(event, "Accept")).toEqual("application/json"); + expect(getRequestHeader(event, "accept")).toEqual("application/json"); + expect(getRequestHeader(event, "Accept")).toEqual("application/json"); }), ); - await request.get("/").set("Accept", "application/json"); + await ctx.request.get("/").set("Accept", "application/json"); }); }); describe("setResponseHeaders", () => { it("can set multiple values to multiple response headers corresponding to the given object", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { setResponseHeaders(event, { - "Nuxt-HTTP-Header-1": "string-value-1", - "Nuxt-HTTP-Header-2": "string-value-2", + "X-HTTP-Header-1": "string-value-1", + "X-HTTP-Header-2": "string-value-2", }); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header-1"]).toEqual("string-value-1"); - expect(result.headers["nuxt-http-header-2"]).toEqual("string-value-2"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header-1"]).toEqual("string-value-1"); + expect(result.headers["x-http-header-2"]).toEqual("string-value-2"); }); }); - describe("setHeaders", () => { + describe("setResponseHeaders", () => { it("can set multiple values to multiple response headers corresponding to the given object", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setHeaders(event, { - "Nuxt-HTTP-Header-1": "string-value-1", - "Nuxt-HTTP-Header-2": "string-value-2", + setResponseHeaders(event, { + "X-HTTP-Header-1": "string-value-1", + "X-HTTP-Header-2": "string-value-2", }); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header-1"]).toEqual("string-value-1"); - expect(result.headers["nuxt-http-header-2"]).toEqual("string-value-2"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header-1"]).toEqual("string-value-1"); + expect(result.headers["x-http-header-2"]).toEqual("string-value-2"); }); }); describe("setResponseHeader", () => { it("can set a string value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setResponseHeader(event, "Nuxt-HTTP-Header", "string-value"); + setResponseHeader(event, "X-HTTP-Header", "string-value"); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("string-value"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("string-value"); }); it("can set a number value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setResponseHeader(event, "Nuxt-HTTP-Header", 12_345); + setResponseHeader( + event, + "X-HTTP-Header", + 12_345 as unknown as string, + ); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("12345"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("12345"); }); it("can set an array value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setResponseHeader(event, "Nuxt-HTTP-Header", ["value 1", "value 2"]); - setResponseHeader(event, "Nuxt-HTTP-Header", ["value 3", "value 4"]); + setResponseHeader(event, "X-HTTP-Header", ["value 1", "value 2"]); + setResponseHeader(event, "X-HTTP-Header", ["value 3", "value 4"]); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("value 3, value 4"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("value 3, value 4"); }); }); - describe("setHeader", () => { + describe("setResponseHeader", () => { it("can set a string value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setHeader(event, "Nuxt-HTTP-Header", "string-value"); + setResponseHeader(event, "X-HTTP-Header", "string-value"); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("string-value"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("string-value"); }); it("can set a number value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setHeader(event, "Nuxt-HTTP-Header", 12_345); + setResponseHeader(event, "X-HTTP-Header", "12345"); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("12345"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("12345"); }); it("can set an array value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - setHeader(event, "Nuxt-HTTP-Header", ["value 1", "value 2"]); - setHeader(event, "Nuxt-HTTP-Header", ["value 3", "value 4"]); + setResponseHeader(event, "X-HTTP-Header", ["value 1", "value 2"]); + setResponseHeader(event, "X-HTTP-Header", ["value 3", "value 4"]); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("value 3, value 4"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("value 3, value 4"); }); }); describe("appendResponseHeaders", () => { it("can append multiple string values to multiple response header corresponding to the given object", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { appendResponseHeaders(event, { - "Nuxt-HTTP-Header-1": "string-value-1-1", - "Nuxt-HTTP-Header-2": "string-value-2-1", + "X-HTTP-Header-1": "string-value-1-1", + "X-HTTP-Header-2": "string-value-2-1", }); appendResponseHeaders(event, { - "Nuxt-HTTP-Header-1": "string-value-1-2", - "Nuxt-HTTP-Header-2": "string-value-2-2", + "X-HTTP-Header-1": "string-value-1-2", + "X-HTTP-Header-2": "string-value-2-2", }); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header-1"]).toEqual( + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header-1"]).toEqual( "string-value-1-1, string-value-1-2", ); - expect(result.headers["nuxt-http-header-2"]).toEqual( + expect(result.headers["x-http-header-2"]).toEqual( "string-value-2-1, string-value-2-2", ); }); }); - describe("appendHeaders", () => { + describe("appendResponseHeaders", () => { it("can append multiple string values to multiple response header corresponding to the given object", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - appendHeaders(event, { - "Nuxt-HTTP-Header-1": "string-value-1-1", - "Nuxt-HTTP-Header-2": "string-value-2-1", + appendResponseHeaders(event, { + "X-HTTP-Header-1": "string-value-1-1", + "X-HTTP-Header-2": "string-value-2-1", }); - appendHeaders(event, { - "Nuxt-HTTP-Header-1": "string-value-1-2", - "Nuxt-HTTP-Header-2": "string-value-2-2", + appendResponseHeaders(event, { + "X-HTTP-Header-1": "string-value-1-2", + "X-HTTP-Header-2": "string-value-2-2", }); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header-1"]).toEqual( + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header-1"]).toEqual( "string-value-1-1, string-value-1-2", ); - expect(result.headers["nuxt-http-header-2"]).toEqual( + expect(result.headers["x-http-header-2"]).toEqual( "string-value-2-1, string-value-2-2", ); }); @@ -244,73 +235,73 @@ describe("", () => { describe("appendResponseHeader", () => { it("can append a value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - appendResponseHeader(event, "Nuxt-HTTP-Header", "value 1"); - appendResponseHeader(event, "Nuxt-HTTP-Header", "value 2"); + appendResponseHeader(event, "X-HTTP-Header", "value 1"); + appendResponseHeader(event, "X-HTTP-Header", "value 2"); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("value 1, value 2"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("value 1, value 2"); }); }); describe("appendHeader", () => { it("can append a value to response header corresponding to the given name", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - appendHeader(event, "Nuxt-HTTP-Header", "value 1"); - appendHeader(event, "Nuxt-HTTP-Header", "value 2"); + appendResponseHeader(event, "X-HTTP-Header", "value 1"); + appendResponseHeader(event, "X-HTTP-Header", "value 2"); }), ); - const result = await request.get("/"); - expect(result.headers["nuxt-http-header"]).toEqual("value 1, value 2"); + const result = await ctx.request.get("/"); + expect(result.headers["x-http-header"]).toEqual("value 1, value 2"); }); }); describe("clearResponseHeaders", () => { it("can remove all response headers", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - appendHeader(event, "header-1", "1"); - appendHeader(event, "header-2", "2"); + appendResponseHeader(event, "header-1", "1"); + appendResponseHeader(event, "header-2", "2"); clearResponseHeaders(event); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.headers["header-1"]).toBeUndefined(); expect(result.headers["header-2"]).toBeUndefined(); }); it("can remove multiple response headers", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - appendHeader(event, "header-3", "3"); - appendHeader(event, "header-4", "4"); - appendHeader(event, "header-5", "5"); + appendResponseHeader(event, "header-3", "3"); + appendResponseHeader(event, "header-4", "4"); + appendResponseHeader(event, "header-5", "5"); clearResponseHeaders(event, ["header-3", "header-5"]); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.headers["header-3"]).toBeUndefined(); expect(result.headers["header-4"]).toBe("4"); expect(result.headers["header-5"]).toBeUndefined(); }); it("can remove a single response header", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - appendHeader(event, "header-6", "6"); - appendHeader(event, "header-7", "7"); + appendResponseHeader(event, "header-6", "6"); + appendResponseHeader(event, "header-7", "7"); removeResponseHeader(event, "header-6"); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.headers["header-6"]).toBeUndefined(); expect(result.headers["header-7"]).toBe("7"); }); diff --git a/test/hooks.test.ts b/test/hooks.test.ts new file mode 100644 index 00000000..631df502 --- /dev/null +++ b/test/hooks.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi } from "vitest"; +import { eventHandler, createError } from "../src"; +import { setupTest } from "./_utils"; + +describe("app", () => { + const ctx = setupTest(); + + it("calls onRequest and onResponse", async () => { + ctx.app.use(eventHandler(() => Promise.resolve("Hello World!"))); + await ctx.request.get("/foo"); + + expect(ctx.onRequest).toHaveBeenCalledTimes(1); + expect(ctx.onRequest.mock.calls[0]![0]!.path).toBe("/foo"); + + expect(ctx.onError).toHaveBeenCalledTimes(0); + + expect(ctx.onBeforeResponse).toHaveBeenCalledTimes(1); + expect(ctx.onBeforeResponse.mock.calls[0]![1]!.body).toBe("Hello World!"); + + expect(ctx.onAfterResponse).toHaveBeenCalledTimes(1); + expect(ctx.onAfterResponse.mock.calls[0]![1]!.body).toBe("Hello World!"); + }); + + it("сalls onRequest and onResponse when an exception is thrown", async () => { + ctx.app.use( + eventHandler(() => { + throw createError({ + statusCode: 503, + }); + }), + ); + await ctx.request.get("/foo"); + + expect(ctx.onRequest).toHaveBeenCalledTimes(1); + expect(ctx.onRequest.mock.calls[0]![0]!.path).toBe("/foo"); + + expect(ctx.onError).toHaveBeenCalledTimes(1); + expect(ctx.onError.mock.calls[0]![0]!.statusCode).toBe(503); + expect(ctx.onError.mock.calls[0]![1]!.path).toBe("/foo"); + + expect(ctx.onBeforeResponse).toHaveBeenCalledTimes(1); + expect(ctx.onAfterResponse).toHaveBeenCalledTimes(1); + }); + + it("calls onRequest and onResponse when an error is returned", async () => { + ctx.app.use( + eventHandler(() => { + return createError({ + statusCode: 404, + }); + }), + ); + await ctx.request.get("/foo"); + + expect(ctx.onRequest).toHaveBeenCalledTimes(1); + expect(ctx.onRequest.mock.calls[0]![0]!.path).toBe("/foo"); + + expect(ctx.onError).toHaveBeenCalledTimes(1); + expect(ctx.onError.mock.calls[0]![0]!.statusCode).toBe(404); + expect(ctx.onError.mock.calls[0]![1]!.path).toBe("/foo"); + + expect(ctx.onBeforeResponse).toHaveBeenCalledTimes(1); + expect(ctx.onAfterResponse).toHaveBeenCalledTimes(1); + }); + + it("calls onRequest and onResponse when an unhandled error occurs", async () => { + ctx.app.use( + eventHandler((event) => { + // @ts-expect-error + return event.unknown.property; + }), + ); + + vi.spyOn(console, "error").mockImplementation(() => {}); + await ctx.request.get("/foo"); + + expect(ctx.errors.length).toBe(1); + expect(ctx.errors[0].statusCode).toBe(500); + ctx.errors = []; + + expect(ctx.onRequest).toHaveBeenCalledTimes(1); + expect(ctx.onRequest.mock.calls[0][0].path).toBe("/foo"); + + expect(ctx.onError).toHaveBeenCalledTimes(1); + expect(ctx.onError.mock.calls[0]![0]!.statusCode).toBe(500); + expect(ctx.onError.mock.calls[0]![0]!.cause).toBeInstanceOf(TypeError); + expect(ctx.onError.mock.calls[0]![1]!.path).toBe("/foo"); + + expect(ctx.onBeforeResponse).toHaveBeenCalledTimes(1); + expect(ctx.onAfterResponse).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/integrations.test.ts b/test/integrations.test.ts index c5b211bd..2bd01b48 100644 --- a/test/integrations.test.ts +++ b/test/integrations.test.ts @@ -1,90 +1,75 @@ import express from "express"; import createConnectApp from "connect"; -import { describe, it, expect, beforeEach } from "vitest"; -import supertest, { SuperTest, Test } from "supertest"; +import { describe, it, expect } from "vitest"; import { createElement } from "react"; import { renderToString, renderToPipeableStream } from "react-dom/server"; -import { - createApp, - App, - toNodeListener, - fromNodeMiddleware, - eventHandler, -} from "../src"; +import { fromNodeHandler, defineNodeHandler } from "../src/adapters/node"; +import { eventHandler } from "../src"; +import { setupTest } from "./_utils"; describe("integration with react", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: true }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); it("renderToString", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => { const el = createElement("h1", null, `Hello`); return renderToString(el); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.text).toBe("

Hello

"); }); it("renderToPipeableStream", async () => { - app.use( + ctx.app.use( "/", eventHandler(() => { const el = createElement("h1", null, `Hello`); return renderToPipeableStream(el); }), ); - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.text).toBe("

Hello

"); }); }); describe("integration with express", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); it("can wrap an express instance", async () => { const expressApp = express(); expressApp.use("/", (_req, res) => { res.json({ express: "works" }); }); - app.use("/api/express", fromNodeMiddleware(expressApp)); - const res = await request.get("/api/express"); + ctx.app.use("/api/express", fromNodeHandler(expressApp)); + const res = await ctx.request.get("/api/express"); expect(res.body).toEqual({ express: "works" }); }); it("can be used as express middleware", async () => { const expressApp = express(); - app.use( + ctx.app.use( "/api/hello", - fromNodeMiddleware((_req, res, next) => { + fromNodeHandler((_req, res, next) => { (res as any).prop = "42"; next(); }), ); - app.use( + ctx.app.use( "/api/hello", - fromNodeMiddleware((req, res) => ({ - url: req.url, - prop: (res as any).prop, - })), + fromNodeHandler( + defineNodeHandler((req, res) => ({ + url: req.url, + prop: (res as any).prop, + })), + ), ); - expressApp.use("/api", toNodeListener(app)); + expressApp.use("/api", ctx.nodeHandler); - const res = await request.get("/api/hello"); + const res = await ctx.request.get("/api/hello"); expect(res.body).toEqual({ url: "/", prop: "42" }); }); @@ -95,31 +80,31 @@ describe("integration with express", () => { res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ connect: "works" })); }); - app.use("/", fromNodeMiddleware(connectApp)); - const res = await request.get("/api/connect"); + ctx.app.use("/", fromNodeHandler(connectApp)); + const res = await ctx.request.get("/api/connect"); expect(res.body).toEqual({ connect: "works" }); }); it("can be used as connect middleware", async () => { const connectApp = createConnectApp(); - app.use( + ctx.app.use( "/api/hello", - fromNodeMiddleware((_req, res, next) => { + fromNodeHandler((_req, res, next) => { (res as any).prop = "42"; - next(); + next?.(); }), ); - app.use( + ctx.app.use( "/api/hello", - fromNodeMiddleware((req, res) => ({ + fromNodeHandler((req, res) => ({ url: req.url, prop: (res as any).prop, })), ); - connectApp.use("/api", app); + connectApp.use("/api", ctx.nodeHandler); - const res = await request.get("/api/hello"); + const res = await ctx.request.get("/api/hello"); expect(res.body).toEqual({ url: "/", prop: "42" }); }); diff --git a/test/iterable.test.ts b/test/iterable.test.ts index b3d998a3..954460d4 100644 --- a/test/iterable.test.ts +++ b/test/iterable.test.ts @@ -1,23 +1,11 @@ import { ReadableStream } from "node:stream/web"; -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { - createApp, - App, - toNodeListener, - eventHandler, - sendIterable, -} from "../src"; +import { describe, it, expect, vi } from "vitest"; +import { eventHandler, sendIterable } from "../src"; import { serializeIterableValue } from "../src/utils/internal/iterable"; +import { setupTest } from "./_utils"; describe("iterable", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("serializeIterableValue", () => { const exampleDate: Date = new Date(Date.UTC(2015, 6, 21, 3, 24, 54, 888)); @@ -46,15 +34,17 @@ describe("iterable", () => { describe("sendIterable", () => { it("sends empty body for an empty iterator", async () => { - app.use(eventHandler((event) => sendIterable(event, []))); - const result = await request.get("/"); + ctx.app.use(eventHandler((event) => sendIterable(event, []))); + const result = await ctx.request.get("/"); expect(result.header["content-length"]).toBe("0"); expect(result.text).toBe(""); }); it("concatenates iterated values", async () => { - app.use(eventHandler((event) => sendIterable(event, ["a", "b", "c"]))); - const result = await request.get("/"); + ctx.app.use( + eventHandler((event) => sendIterable(event, ["a", "b", "c"])), + ); + const result = await ctx.request.get("/"); expect(result.text).toBe("abc"); }); @@ -126,8 +116,8 @@ describe("iterable", () => { }), }, ])("$type", async ({ iterable }) => { - app.use(eventHandler((event) => sendIterable(event, iterable))); - const response = await request.get("/"); + ctx.app.use(eventHandler((event) => sendIterable(event, iterable))); + const response = await ctx.request.get("/"); expect(response.text).toBe("the-value"); }); }); @@ -137,12 +127,12 @@ describe("iterable", () => { const iterable = [1, "2", { field: 3 }, null]; const serializer = vi.fn(() => "x"); - app.use( + ctx.app.use( eventHandler((event) => sendIterable(event, iterable, { serializer }), ), ); - const response = await request.get("/"); + const response = await ctx.request.get("/"); expect(response.text).toBe("x".repeat(iterable.length)); expect(serializer).toBeCalledTimes(4); for (const [i, obj] of iterable.entries()) { diff --git a/test/lazy.test.ts b/test/lazy.test.ts index 9963377a..15af636e 100644 --- a/test/lazy.test.ts +++ b/test/lazy.test.ts @@ -1,21 +1,15 @@ -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { createApp, App, toNodeListener, eventHandler } from "../src"; +import { describe, it, expect, vi } from "vitest"; +import { setupTest } from "./_utils"; +import { defineEventHandler } from "../src"; (global.console.error as any) = vi.fn(); describe("lazy loading", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); const handlers = [ - ["sync", eventHandler(() => "lazy")], - ["async", eventHandler(() => Promise.resolve("lazy"))], + ["sync", defineEventHandler(() => "lazy")], + ["async", defineEventHandler(() => Promise.resolve("lazy"))], ] as const; const kinds = [ ["default export", (handler: any) => ({ default: handler })], @@ -25,17 +19,17 @@ describe("lazy loading", () => { for (const [type, handler] of handlers) { for (const [kind, resolution] of kinds) { it(`can load ${type} handlers lazily from a ${kind}`, async () => { - app.use("/big", () => Promise.resolve(resolution(handler)), { + ctx.app.use("/big", () => Promise.resolve(resolution(handler)), { lazy: true, }); - const result = await request.get("/big"); + const result = await ctx.request.get("/big"); expect(result.text).toBe("lazy"); }); it(`can handle ${type} functions that don't return promises from a ${kind}`, async () => { - app.use("/big", () => resolution(handler), { lazy: true }); - const result = await request.get("/big"); + ctx.app.use("/big", () => resolution(handler), { lazy: true }); + const result = await ctx.request.get("/big"); expect(result.text).toBe("lazy"); }); diff --git a/test/plain.test.ts b/test/plain.test.ts index a58c9987..4c147da4 100644 --- a/test/plain.test.ts +++ b/test/plain.test.ts @@ -1,64 +1,61 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { - createApp, - App, - toPlainHandler, - PlainHandler, eventHandler, - readBody, + readTextBody, + appendResponseHeader, + setResponseStatus, + getRequestHeaders, } from "../src"; +import { setupTest } from "./_utils"; describe("Plain handler", () => { - let app: App; - let handler: PlainHandler; - - beforeEach(() => { - app = createApp({ debug: true }); - handler = toPlainHandler(app); - }); + const ctx = setupTest(); it("works", async () => { - app.use( + ctx.app.use( "/test", eventHandler(async (event) => { - const body = - event.method === "POST" ? await readBody(event) : undefined; - event.node.res.statusCode = 201; - event.node.res.statusMessage = "Created"; - event.node.res.setHeader("content-type", "application/json"); - event.node.res.appendHeader("set-cookie", "a=123, b=123"); - event.node.res.appendHeader("set-Cookie", ["c=123"]); - event.node.res.appendHeader("set-cookie", "d=123"); + const body = await readTextBody(event); + setResponseStatus(event, 201, "Created"); + appendResponseHeader( + event, + "content-type", + "application/json;charset=UTF-8", + ); + appendResponseHeader(event, "set-cookie", "a=123"); + appendResponseHeader(event, "set-cookie", "b=123"); + appendResponseHeader(event, "set-cookie", "c=123"); + appendResponseHeader(event, "set-cookie", "d=123"); return { method: event.method, path: event.path, - headers: [...event.headers.entries()], + headers: getRequestHeaders(event), body, contextKeys: Object.keys(event.context), }; }), ); - const res = await handler({ - method: "POST", - path: "/test/foo/bar", - headers: [["x-test", "true"]], - body: "request body", - context: { + const res = await ctx.plainHandler( + { + method: "POST", + path: "/test/foo/bar", + headers: [["x-test", "true"]], + body: "request body", + }, + { test: true, }, - }); + ); expect(res).toMatchObject({ status: 201, statusText: "Created", - headers: [ - ["content-type", "application/json"], - ["set-cookie", "a=123"], - ["set-cookie", "b=123"], - ["set-cookie", "c=123"], - ["set-cookie", "d=123"], - ], + headers: { + "content-type": "application/json;charset=UTF-8", + "set-cookie": "a=123, b=123, c=123, d=123", + }, + setCookie: ["a=123", "b=123", "c=123", "d=123"], }); expect(typeof res.body).toBe("string"); @@ -66,7 +63,10 @@ describe("Plain handler", () => { method: "POST", path: "/foo/bar", body: "request body", - headers: [["x-test", "true"]], + headers: { + "content-type": "text/plain;charset=UTF-8", + "x-test": "true", + }, contextKeys: ["test"], }); }); diff --git a/test/proxy.test.ts b/test/proxy.test.ts index fa11e461..bc16b14d 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -1,69 +1,37 @@ -import { Server } from "node:http"; import { readFile } from "node:fs/promises"; -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { fetch } from "node-fetch-native"; import { - createApp, - toNodeListener, - App, eventHandler, - getHeaders, - setHeader, - readRawBody, + getRequestHeaders, setCookie, setResponseHeader, + readTextBody, + readRawBody, + appendResponseHeader, } from "../src"; import { sendProxy, proxyRequest } from "../src/utils/proxy"; +import { setupTest } from "./_utils"; const spy = vi.spyOn(console, "error"); describe("proxy", () => { - let app: App; - let request: SuperTest; - - let server: Server; - let url: string; - - beforeEach(async () => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - server = new Server( - { - keepAlive: false, - keepAliveTimeout: 1, - }, - toNodeListener(app), - ); - server.on("error", (error) => { - console.log("[server error]", error); - }); - await new Promise((resolve) => { - server.listen(0, () => resolve(undefined)); - }); - url = "http://localhost:" + (server.address() as any).port; - }); - - afterEach(async () => { - await new Promise((resolve) => { - server.close(() => resolve(undefined)); - }); - }); + const ctx = setupTest(); describe("sendProxy", () => { it("works", async () => { - app.use( + ctx.app.use( "/hello", eventHandler(() => "hello"), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return sendProxy(event, url + "/hello", { fetch }); + return sendProxy(event, ctx.url + "/hello", { fetch }); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.text).toBe("hello"); }); @@ -71,17 +39,11 @@ describe("proxy", () => { describe("proxyRequest", () => { it("can proxy request", async () => { - app.use( + ctx.app.use( "/debug", eventHandler(async (event) => { - const headers = getHeaders(event); - delete headers.host; - let body; - try { - body = await readRawBody(event); - } catch { - // Ignore - } + const headers = getRequestHeaders(event); + const body = await readTextBody(event); return { method: event.method, headers, @@ -90,10 +52,10 @@ describe("proxy", () => { }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, headers: { "x-custom1": "overridden" }, fetchOptions: { @@ -103,7 +65,7 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/", { + const result = await fetch(ctx.url + "/", { method: "POST", body: "hello", headers: { @@ -130,22 +92,22 @@ describe("proxy", () => { }); it("can proxy binary request", async () => { - app.use( + ctx.app.use( "/debug", eventHandler(async (event) => { - const body = await readRawBody(event, false); + const body = await readRawBody(event); return { - headers: getHeaders(event), + headers: getRequestHeaders(event), bytes: body!.length, }; }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { setResponseHeader(event, "x-res-header", "works"); - return proxyRequest(event, url + "/debug", { fetch }); + return proxyRequest(event, ctx.url + "/debug", { fetch }); }), ); @@ -153,7 +115,7 @@ describe("proxy", () => { new URL("assets/dummy.pdf", import.meta.url), ); - const res = await fetch(url + "/", { + const res = await fetch(ctx.url + "/", { method: "POST", body: dummyFile, headers: { @@ -168,20 +130,20 @@ describe("proxy", () => { }); it("can proxy stream request", async () => { - app.use( + ctx.app.use( "/debug", eventHandler(async (event) => { return { - body: await readRawBody(event), - headers: getHeaders(event), + body: await readTextBody(event), + headers: getRequestHeaders(event), }; }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { fetch }); + return proxyRequest(event, ctx.url + "/debug", { fetch }); }), ); @@ -199,7 +161,7 @@ describe("proxy", () => { }, }).pipeThrough(new TextEncoderStream()); - const res = await fetch(url + "/", { + const res = await fetch(ctx.url + "/", { method: "POST", // @ts-ignore duplex: "half", @@ -224,22 +186,22 @@ describe("proxy", () => { it("can proxy json transparently", async () => { const message = '{"hello":"world"}'; - app.use( + ctx.app.use( "/debug", eventHandler((event) => { - setHeader(event, "content-type", "application/json"); + setResponseHeader(event, "content-type", "application/json"); return message; }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { fetch }); + return proxyRequest(event, ctx.url + "/debug", { fetch }); }), ); - const res = await fetch(url + "/", { + const res = await fetch(ctx.url + "/", { method: "GET", }); @@ -252,7 +214,7 @@ describe("proxy", () => { "can handle failed proxy requests gracefully", async () => { spy.mockReset(); - app.use( + ctx.app.use( "/", eventHandler((event) => { return proxyRequest( @@ -262,7 +224,7 @@ describe("proxy", () => { }), ); - await fetch(`${url}/`, { + await fetch(`${ctx.url}/`, { method: "GET", }); @@ -274,7 +236,7 @@ describe("proxy", () => { describe("multipleCookies", () => { it("can split multiple cookies", async () => { - app.use( + ctx.app.use( "/setcookies", eventHandler((event) => { setCookie(event, "user", "alice", { @@ -286,14 +248,14 @@ describe("proxy", () => { }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return sendProxy(event, url + "/setcookies", { fetch }); + return sendProxy(event, ctx.url + "/setcookies", { fetch }); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); const cookies = result.header["set-cookie"]; expect(cookies).toEqual([ "user=alice; Path=/; Expires=Thu, 01 Jun 2023 10:00:00 GMT; HttpOnly", @@ -304,10 +266,10 @@ describe("proxy", () => { describe("cookieDomainRewrite", () => { beforeEach(() => { - app.use( + ctx.app.use( "/debug", eventHandler((event) => { - setHeader( + setResponseHeader( event, "set-cookie", "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -318,17 +280,17 @@ describe("proxy", () => { }); it("can rewrite cookie domain by string", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, cookieDomainRewrite: "new.domain", }); }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Domain=new.domain; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -336,10 +298,10 @@ describe("proxy", () => { }); it("can rewrite cookie domain by mapper object", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, cookieDomainRewrite: { "somecompany.co.uk": "new.domain", @@ -348,7 +310,7 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Domain=new.domain; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -356,21 +318,27 @@ describe("proxy", () => { }); it("can rewrite domains of multiple cookies", async () => { - app.use( + ctx.app.use( "/multiple/debug", eventHandler((event) => { - setHeader(event, "set-cookie", [ + appendResponseHeader( + event, + "set-cookie", "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", + ); + appendResponseHeader( + event, + "set-cookie", "bar=38afes7a8; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", - ]); + ); return {}; }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/multiple/debug", { + return proxyRequest(event, ctx.url + "/multiple/debug", { fetch, cookieDomainRewrite: { "somecompany.co.uk": "new.domain", @@ -379,7 +347,7 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Domain=new.domain; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT, bar=38afes7a8; Domain=new.domain; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -387,10 +355,10 @@ describe("proxy", () => { }); it("can remove cookie domain", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, cookieDomainRewrite: { "somecompany.co.uk": "", @@ -399,7 +367,7 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -409,10 +377,10 @@ describe("proxy", () => { describe("cookiePathRewrite", () => { beforeEach(() => { - app.use( + ctx.app.use( "/debug", eventHandler((event) => { - setHeader( + setResponseHeader( event, "set-cookie", "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -423,17 +391,17 @@ describe("proxy", () => { }); it("can rewrite cookie path by string", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, cookiePathRewrite: "/api", }); }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/api; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -441,10 +409,10 @@ describe("proxy", () => { }); it("can rewrite cookie path by mapper object", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, cookiePathRewrite: { "/": "/api", @@ -453,7 +421,7 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/api; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -461,10 +429,10 @@ describe("proxy", () => { }); it("can rewrite paths of multiple cookies", async () => { - app.use( + ctx.app.use( "/multiple/debug", eventHandler((event) => { - setHeader(event, "set-cookie", [ + appendResponseHeader(event, "set-cookie", [ "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", "bar=38afes7a8; Domain=somecompany.co.uk; Path=/; Expires=Wed, 30 Aug 2022 00:00:00 GMT", ]); @@ -472,10 +440,10 @@ describe("proxy", () => { }), ); - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/multiple/debug", { + return proxyRequest(event, ctx.url + "/multiple/debug", { fetch, cookiePathRewrite: { "/": "/api", @@ -484,18 +452,19 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); - expect(result.headers.get("set-cookie")).toEqual( - "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/api; Expires=Wed, 30 Aug 2022 00:00:00 GMT, bar=38afes7a8; Domain=somecompany.co.uk; Path=/api; Expires=Wed, 30 Aug 2022 00:00:00 GMT", - ); + expect(result.headers.getSetCookie()).toEqual([ + "foo=219ffwef9w0f; Domain=somecompany.co.uk; Path=/api; Expires=Wed, 30 Aug 2022 00:00:00 GMT", + "bar=38afes7a8; Domain=somecompany.co.uk; Path=/api; Expires=Wed, 30 Aug 2022 00:00:00 GMT", + ]); }); it("can remove cookie path", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, cookiePathRewrite: { "/": "", @@ -504,7 +473,7 @@ describe("proxy", () => { }), ); - const result = await fetch(url + "/"); + const result = await fetch(ctx.url + "/"); expect(result.headers.get("set-cookie")).toEqual( "foo=219ffwef9w0f; Domain=somecompany.co.uk; Expires=Wed, 30 Aug 2022 00:00:00 GMT", @@ -514,7 +483,7 @@ describe("proxy", () => { describe("onResponse", () => { beforeEach(() => { - app.use( + ctx.app.use( "/debug", eventHandler(() => { return { @@ -525,39 +494,39 @@ describe("proxy", () => { }); it("allows modifying response event", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, onResponse(_event) { - setHeader(_event, "x-custom", "hello"); + setResponseHeader(_event, "x-custom", "hello"); }, }); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.header["x-custom"]).toEqual("hello"); }); it("allows modifying response event async", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, onResponse(_event) { return new Promise((resolve) => { - resolve(setHeader(_event, "x-custom", "hello")); + resolve(setResponseHeader(_event, "x-custom", "hello")); }); }, }); }), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.header["x-custom"]).toEqual("hello"); }); @@ -565,10 +534,10 @@ describe("proxy", () => { it("allows to get the actual response", async () => { let headers; - app.use( + ctx.app.use( "/", eventHandler((event) => { - return proxyRequest(event, url + "/debug", { + return proxyRequest(event, ctx.url + "/debug", { fetch, onResponse(_event, response) { headers = Object.fromEntries(response.headers.entries()); @@ -577,9 +546,9 @@ describe("proxy", () => { }), ); - await request.get("/"); + await ctx.request.get("/"); - expect(headers["content-type"]).toEqual("application/json"); + expect(headers?.["content-type"]).toEqual("application/json"); }); }); }); diff --git a/test/resolve.test.ts b/test/resolve.test.ts index 8340bd12..871538e1 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from "vitest"; import { createApp, createRouter, + defineLazyEventHandler, eventHandler, - lazyEventHandler, } from "../src"; describe("Event handler resolver", () => { @@ -35,7 +35,7 @@ describe("Event handler resolver", () => { router.get("/router/:id", testHandlers[7]); router.get( "/router/lazy", - lazyEventHandler(() => testHandlers[8]), + defineLazyEventHandler(() => testHandlers[8]), ); describe("middleware", () => { diff --git a/test/router.test.ts b/test/router.test.ts index f748b90e..55a80d79 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -1,23 +1,19 @@ -import supertest, { SuperTest, Test } from "supertest"; +import type { Router } from "../src/types"; import { describe, it, expect, beforeEach } from "vitest"; import { - createApp, createRouter, - App, - Router, getRouterParams, getRouterParam, - toNodeListener, eventHandler, } from "../src"; +import { setupTest } from "./_utils"; describe("router", () => { - let app: App; + const ctx = setupTest(); + let router: Router; - let request: SuperTest; beforeEach(() => { - app = createApp({ debug: false }); router = createRouter() .add( "/", @@ -41,12 +37,11 @@ describe("router", () => { eventHandler(() => "Test (POST)"), ); - app.use(router); - request = supertest(toNodeListener(app)); + ctx.app.use(router); }); it("Handle route", async () => { - const res = await request.get("/"); + const res = await ctx.request.get("/"); expect(res.text).toEqual("Hello"); }); @@ -56,45 +51,45 @@ describe("router", () => { eventHandler(() => "router2"), ); - app.use(secondRouter); + ctx.app.use(secondRouter); - const res1 = await request.get("/"); + const res1 = await ctx.request.get("/"); expect(res1.text).toEqual("Hello"); - const res2 = await request.get("/router2"); + const res2 = await ctx.request.get("/router2"); expect(res2.text).toEqual("router2"); }); it("Handle different methods", async () => { - const res1 = await request.get("/test"); + const res1 = await ctx.request.get("/test"); expect(res1.text).toEqual("Test (GET)"); - const res2 = await request.post("/test"); + const res2 = await ctx.request.post("/test"); expect(res2.text).toEqual("Test (POST)"); }); it("Handle url with query parameters", async () => { - const res = await request.get("/test?title=test"); + const res = await ctx.request.get("/test?title=test"); expect(res.status).toEqual(200); }); it('Handle url with query parameters, include "?" in url path', async () => { - const res = await request.get( + const res = await ctx.request.get( "/test/?/a?title=test&returnTo=/path?foo=bar", ); expect(res.status).toEqual(200); }); it("Handle many methods (get)", async () => { - const res = await request.get("/many/routes"); + const res = await ctx.request.get("/many/routes"); expect(res.status).toEqual(200); }); it("Handle many methods (post)", async () => { - const res = await request.post("/many/routes"); + const res = await ctx.request.post("/many/routes"); expect(res.status).toEqual(200); }); it("Not matching route", async () => { - const res = await request.get("/404"); + const res = await ctx.request.get("/404"); expect(res.status).toEqual(404); }); @@ -111,11 +106,11 @@ describe("router", () => { // Loop to validate cached behavior for (let i = 0; i < 5; i++) { - const postRed = await request.post("/test/123"); + const postRed = await ctx.request.post("/test/123"); expect(postRed.status).toEqual(200); expect(postRed.text).toEqual("[POST] /test/123"); - const getRes = await request.get("/test/123"); + const getRes = await ctx.request.get("/test/123"); expect(getRes.status).toEqual(200); expect(getRes.text).toEqual("[GET] /test/123"); } @@ -123,12 +118,11 @@ describe("router", () => { }); describe("router (preemptive)", () => { - let app: App; + const ctx = setupTest(); + let router: Router; - let request: SuperTest; beforeEach(() => { - app = createApp({ debug: false }); router = createRouter({ preemptive: true }) .get( "/test", @@ -138,17 +132,16 @@ describe("router (preemptive)", () => { "/undefined", eventHandler(() => undefined), ); - app.use(router); - request = supertest(toNodeListener(app)); + ctx.app.use(router); }); it("Handle /test", async () => { - const res = await request.get("/test"); + const res = await ctx.request.get("/test"); expect(res.text).toEqual("Test"); }); it("Handle /404", async () => { - const res = await request.get("/404"); + const res = await ctx.request.get("/404"); expect(JSON.parse(res.text)).toMatchObject({ statusCode: 404, statusMessage: "Cannot find any route matching /404.", @@ -156,24 +149,18 @@ describe("router (preemptive)", () => { }); it("Not matching route method", async () => { - const res = await request.head("/test"); + const res = await ctx.request.head("/test"); expect(res.status).toEqual(405); }); it("Handle /undefined", async () => { - const res = await request.get("/undefined"); + const res = await ctx.request.get("/undefined"); expect(res.text).toEqual(""); }); }); describe("getRouterParams", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("with router", () => { it("can return router params", async () => { @@ -184,8 +171,8 @@ describe("getRouterParams", () => { return "200"; }), ); - app.use(router); - const result = await request.get("/test/params/string"); + ctx.app.use(router); + const result = await ctx.request.get("/test/params/string"); expect(result.text).toBe("200"); }); @@ -200,8 +187,8 @@ describe("getRouterParams", () => { return "200"; }), ); - app.use(router); - const result = await request.get("/test/params/string with space"); + ctx.app.use(router); + const result = await ctx.request.get("/test/params/string with space"); expect(result.text).toBe("200"); }); @@ -209,14 +196,14 @@ describe("getRouterParams", () => { describe("without router", () => { it("can return an empty object if router is not used", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { expect(getRouterParams(event)).toMatchObject({}); return "200"; }), ); - const result = await request.get("/test/empty/params"); + const result = await ctx.request.get("/test/empty/params"); expect(result.text).toBe("200"); }); @@ -224,13 +211,7 @@ describe("getRouterParams", () => { }); describe("getRouterParam", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("with router", () => { it("can return a value of router params corresponding to the given name", async () => { @@ -241,8 +222,8 @@ describe("getRouterParam", () => { return "200"; }), ); - app.use(router); - const result = await request.get("/test/params/string"); + ctx.app.use(router); + const result = await ctx.request.get("/test/params/string"); expect(result.text).toBe("200"); }); @@ -257,8 +238,8 @@ describe("getRouterParam", () => { return "200"; }), ); - app.use(router); - const result = await request.get("/test/params/string with space"); + ctx.app.use(router); + const result = await ctx.request.get("/test/params/string with space"); expect(result.text).toBe("200"); }); @@ -266,14 +247,14 @@ describe("getRouterParam", () => { describe("without router", () => { it("can return `undefined` for any keys", async () => { - app.use( + ctx.app.use( "/", eventHandler((request) => { expect(getRouterParam(request, "name")).toEqual(undefined); return "200"; }), ); - const result = await request.get("/test/empty/params"); + const result = await ctx.request.get("/test/empty/params"); expect(result.text).toBe("200"); }); @@ -281,13 +262,7 @@ describe("getRouterParam", () => { }); describe("event.context.matchedRoute", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("with router", () => { it("can return the matched path", async () => { @@ -300,8 +275,8 @@ describe("event.context.matchedRoute", () => { return "200"; }), ); - app.use(router); - const result = await request.get("/test/path"); + ctx.app.use(router); + const result = await ctx.request.get("/test/path"); expect(result.text).toBe("200"); }); @@ -309,14 +284,14 @@ describe("event.context.matchedRoute", () => { describe("without router", () => { it("can return `undefined` for matched path", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { expect(event.context.matchedRoute).toEqual(undefined); return "200"; }), ); - const result = await request.get("/test/path"); + const result = await ctx.request.get("/test/path"); expect(result.text).toBe("200"); }); diff --git a/test/session.test.ts b/test/session.test.ts index b4c19fa8..df0e8324 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1,20 +1,13 @@ -import supertest from "supertest"; +import type { SessionConfig } from "../src/types"; import { describe, it, expect, beforeEach } from "vitest"; -import { - createApp, - createRouter, - toNodeListener, - App, - eventHandler, - useSession, - readBody, - SessionConfig, -} from "../src"; +import { createRouter, eventHandler, useSession, readJSONBody } from "../src"; +import { setupTest } from "./_utils"; describe("session", () => { - let app: App; + const ctx = setupTest(); + let router: ReturnType; - let request: ReturnType; + let cookie = ""; let sessionIdCtr = 0; @@ -26,23 +19,21 @@ describe("session", () => { beforeEach(() => { router = createRouter({ preemptive: true }); - app = createApp({ debug: true }).use(router); - request = supertest(toNodeListener(app)); - router.use( "/", eventHandler(async (event) => { const session = await useSession(event, sessionConfig); if (event.method === "POST") { - await session.update(await readBody(event)); + await session.update((await readJSONBody(event)) as any); } return { session }; }), ); + ctx.app.use(router); }); it("initiates session", async () => { - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.headers["set-cookie"]).toHaveLength(1); cookie = result.headers["set-cookie"][0]; expect(result.body).toMatchObject({ @@ -51,14 +42,14 @@ describe("session", () => { }); it("gets same session back", async () => { - const result = await request.get("/").set("Cookie", cookie); + const result = await ctx.request.get("/").set("Cookie", cookie); expect(result.body).toMatchObject({ session: { id: "1", data: {} }, }); }); it("set session data", async () => { - const result = await request + const result = await ctx.request .post("/") .set("Cookie", cookie) .send({ foo: "bar" }); @@ -67,7 +58,7 @@ describe("session", () => { session: { id: "1", data: { foo: "bar" } }, }); - const result2 = await request.get("/").set("Cookie", cookie); + const result2 = await ctx.request.get("/").set("Cookie", cookie); expect(result2.body).toMatchObject({ session: { id: "1", data: { foo: "bar" } }, }); @@ -90,7 +81,7 @@ describe("session", () => { }; }), ); - const result = await request.get("/concurrent").set("Cookie", cookie); + const result = await ctx.request.get("/concurrent").set("Cookie", cookie); expect(result.body).toMatchObject({ sessions: [1, 2, 3].map(() => ({ id: "1", data: { foo: "bar" } })), }); diff --git a/test/sse.test.ts b/test/sse.test.ts index 2c1b5bd5..db59a7d6 100644 --- a/test/sse.test.ts +++ b/test/sse.test.ts @@ -1,24 +1,16 @@ -import supertest, { SuperTest, Test } from "supertest"; import { describe, it, beforeEach, expect } from "vitest"; -import { - App, - createApp, - createEventStream, - eventHandler, - getQuery, - toNodeListener, -} from "../src"; +import { createEventStream, eventHandler, getQuery } from "../src"; import { formatEventStreamMessage, formatEventStreamMessages, -} from "../src/utils/sse/utils"; +} from "../src/utils/internal/event-stream"; +import { setupTest } from "./_utils"; describe("Server Sent Events (SSE)", () => { - let app: App; - let request: SuperTest; + const ctx = setupTest(); + beforeEach(() => { - app = createApp({ debug: true }); - app.use( + ctx.app.use( "/sse", eventHandler((event) => { const includeMeta = getQuery(event).includeMeta !== undefined; @@ -40,11 +32,11 @@ describe("Server Sent Events (SSE)", () => { return eventStream.send(); }), ); - request = supertest(toNodeListener(app)) as any; }); + it("streams events", async () => { let messageCount = 0; - request + ctx.request .get("/sse") .expect(200) .expect("Content-Type", "text/event-stream") @@ -70,7 +62,7 @@ describe("Server Sent Events (SSE)", () => { }); it("streams events with metadata", async () => { let messageCount = 0; - request + ctx.request .get("/sse?includeMeta=true") .expect(200) .expect("Content-Type", "text/event-stream") diff --git a/test/static.test.ts b/test/static.test.ts index e47f66f3..5bbe5903 100644 --- a/test/static.test.ts +++ b/test/static.test.ts @@ -1,50 +1,37 @@ -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { - App, - createApp, - toNodeListener, - eventHandler, - serveStatic, -} from "../src"; +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { eventHandler, serveStatic } from "../src"; +import { setupTest } from "./_utils"; describe("Serve Static", () => { - let app: App; - let request: SuperTest; - - const serveStaticOptions = { - getContents: vi.fn((id) => - id.includes("404") ? undefined : `asset:${id}`, - ), - getMeta: vi.fn((id) => - id.includes("404") - ? undefined - : { - type: "text/plain", - encoding: "utf8", - etag: "w/123", - mtime: 1_700_000_000_000, - path: id, - size: `asset:${id}`.length, - }, - ), - indexNames: ["/index.html"], - encodings: { gzip: ".gz", br: ".br" }, - }; + const ctx = setupTest(); beforeEach(() => { - app = createApp({ debug: true }); - app.use( + const serveStaticOptions = { + getContents: vi.fn((id) => + id.includes("404") ? undefined : `asset:${id}`, + ), + getMeta: vi.fn((id) => + id.includes("404") + ? undefined + : { + type: "text/plain", + encoding: "utf8", + etag: "w/123", + mtime: 1_700_000_000_000, + path: id, + size: `asset:${id}`.length, + }, + ), + indexNames: ["/index.html"], + encodings: { gzip: ".gz", br: ".br" }, + }; + + ctx.app.use( "/", eventHandler((event) => { return serveStatic(event, serveStaticOptions); }), ); - request = supertest(toNodeListener(app)); - }); - - afterEach(() => { - vi.clearAllMocks(); }); const expectedHeaders = { @@ -56,7 +43,7 @@ describe("Serve Static", () => { }; it("Can serve asset (GET)", async () => { - const res = await request + const res = await ctx.request .get("/test.png") .set("if-none-match", "w/456") .set("if-modified-since", new Date(1_700_000_000_000 - 1).toUTCString()) @@ -69,7 +56,7 @@ describe("Serve Static", () => { }); it("Can serve asset (HEAD)", async () => { - const headRes = await request + const headRes = await ctx.request .head("/test.png") .set("if-none-match", "w/456") .set("if-modified-since", new Date(1_700_000_000_000 - 1).toUTCString()) @@ -82,14 +69,16 @@ describe("Serve Static", () => { }); it("Handles cache (if-none-match)", async () => { - const res = await request.get("/test.png").set("if-none-match", "w/123"); + const res = await ctx.request + .get("/test.png") + .set("if-none-match", "w/123"); expect(res.headers.etag).toBe(expectedHeaders.etag); expect(res.status).toEqual(304); expect(res.text).toBe(""); }); it("Handles cache (if-modified-since)", async () => { - const res = await request + const res = await ctx.request .get("/test.png") .set("if-modified-since", new Date(1_700_000_000_001).toUTCString()); expect(res.status).toEqual(304); @@ -97,15 +86,15 @@ describe("Serve Static", () => { }); it("Returns 404 if not found", async () => { - const res = await request.get("/404/test.png"); + const res = await ctx.request.get("/404/test.png"); expect(res.status).toEqual(404); - const headRes = await request.head("/404/test.png"); + const headRes = await ctx.request.head("/404/test.png"); expect(headRes.status).toEqual(404); }); it("Returns 405 if other methods used", async () => { - const res = await request.post("/test.png"); + const res = await ctx.request.post("/test.png"); expect(res.status).toEqual(405); }); }); diff --git a/test/status.test.ts b/test/status.test.ts index ea1f02df..b20174e2 100644 --- a/test/status.test.ts +++ b/test/status.test.ts @@ -1,25 +1,19 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { - createApp, - App, - toPlainHandler, - PlainHandler, - eventHandler, - setResponseStatus, -} from "../src"; +import { toPlainHandler } from "../src/adapters/web"; +import { eventHandler, setResponseStatus } from "../src"; +import { setupTest } from "./_utils"; describe("setResponseStatus", () => { - let app: App; - let handler: PlainHandler; + const ctx = setupTest(); + let handler: ReturnType; beforeEach(() => { - app = createApp({ debug: true }); - handler = toPlainHandler(app); + handler = toPlainHandler(ctx.app); }); describe("content response", () => { it("sets status 200 as default", async () => { - app.use( + ctx.app.use( "/test", eventHandler(() => { return "text"; @@ -36,11 +30,13 @@ describe("setResponseStatus", () => { status: 200, statusText: "", body: "text", - headers: [["content-type", "text/html"]], + headers: { + "content-type": "text/html", + }, }); }); it("override status and statusText with setResponseStatus method", async () => { - app.use( + ctx.app.use( "/test", eventHandler((event) => { setResponseStatus(event, 418, "status-text"); @@ -59,14 +55,16 @@ describe("setResponseStatus", () => { status: 418, statusText: "status-text", body: "text", - headers: [["content-type", "text/html"]], + headers: { + "content-type": "text/html", + }, }); }); }); describe("no content response", () => { it("sets status 204 as default", async () => { - app.use( + ctx.app.use( "/test", eventHandler(() => { return null; @@ -82,12 +80,12 @@ describe("setResponseStatus", () => { expect(res).toMatchObject({ status: 204, statusText: "", - body: undefined, - headers: [], + body: null, + headers: {}, }); }); it("override status and statusText with setResponseStatus method", async () => { - app.use( + ctx.app.use( "/test", eventHandler((event) => { setResponseStatus(event, 418, "status-text"); @@ -106,12 +104,12 @@ describe("setResponseStatus", () => { status: 418, statusText: "status-text", body: undefined, - headers: [], + headers: {}, }); }); it("does not sets content-type for 304", async () => { - app.use( + ctx.app.use( "/test", eventHandler((event) => { setResponseStatus(event, 304, "Not Modified"); @@ -123,7 +121,6 @@ describe("setResponseStatus", () => { method: "GET", path: "/test", headers: [], - body: "", }); // console.log(res.headers); @@ -131,8 +128,8 @@ describe("setResponseStatus", () => { expect(res).toMatchObject({ status: 304, statusText: "Not Modified", - body: undefined, - headers: [], + body: null, + headers: {}, }); }); }); diff --git a/test/types.test-d.ts b/test/types.test-d.ts index 9160b0ef..211d47ac 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -1,11 +1,11 @@ -import { describe, it, expectTypeOf } from "vitest"; import type { QueryObject } from "ufo"; +import type { H3Event } from "../src/types"; +import { describe, it, expectTypeOf } from "vitest"; import { eventHandler, - H3Event, getQuery, - readBody, - readValidatedBody, + readJSONBody, + readValidatedJSONBody, getValidatedQuery, } from "../src"; @@ -21,7 +21,7 @@ describe("types", () => { async handler(event) { expectTypeOf(event).toEqualTypeOf(); - const body = await readBody(event); + const body = await readJSONBody(event); // TODO: Default to unknown in next major version expectTypeOf(body).toBeAny(); @@ -53,10 +53,10 @@ describe("types", () => { }); }); - describe("readBody", () => { + describe("readJSONBody", () => { it("untyped", () => { eventHandler(async (event) => { - const body = await readBody(event); + const body = await readJSONBody(event); // TODO: Default to unknown in next major version expectTypeOf(body).toBeAny(); }); @@ -64,16 +64,16 @@ describe("types", () => { it("typed via generic", () => { eventHandler(async (event) => { - const body = await readBody(event); + const body = await readJSONBody(event); expectTypeOf(body).not.toBeAny(); - expectTypeOf(body).toBeString(); + expectTypeOf(body!).toBeString(); }); }); it("typed via validator", () => { eventHandler(async (event) => { const validator = (body: unknown) => body as { id: string }; - const body = await readValidatedBody(event, validator); + const body = await readValidatedJSONBody(event, validator); expectTypeOf(body).not.toBeAny(); expectTypeOf(body).toEqualTypeOf<{ id: string }>(); }); @@ -81,9 +81,9 @@ describe("types", () => { it("typed via event handler", () => { eventHandler<{ body: { id: string } }>(async (event) => { - const body = await readBody(event); + const body = await readJSONBody(event); expectTypeOf(body).not.toBeAny(); - expectTypeOf(body).toEqualTypeOf<{ id: string }>(); + expectTypeOf(body).toEqualTypeOf<{ id: string } | undefined>(); }); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index 0c1b072b..312964bc 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,38 +1,30 @@ import { ReadableStream } from "node:stream/web"; -import supertest, { SuperTest, Test } from "supertest"; -import { describe, it, expect, beforeEach, vi } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { - createApp, - App, sendRedirect, useBase, assertMethod, - toNodeListener, eventHandler, getQuery, getRequestURL, - readFormData, + readFormDataBody, getRequestIP, getRequestFingerprint, sendIterable, } from "../src"; +import { getNodeContext } from "../src/adapters/node"; import { serializeIterableValue } from "../src/utils/internal/iterable"; +import { setupTest } from "./_utils"; describe("", () => { - let app: App; - let request: SuperTest; - - beforeEach(() => { - app = createApp({ debug: false }); - request = supertest(toNodeListener(app)); - }); + const ctx = setupTest(); describe("sendRedirect", () => { it("can redirect URLs", async () => { - app.use( + ctx.app.use( eventHandler((event) => sendRedirect(event, "https://google.com")), ); - const result = await request.get("/"); + const result = await ctx.request.get("/"); expect(result.header.location).toBe("https://google.com"); expect(result.header["content-type"]).toBe("text/html"); @@ -66,15 +58,17 @@ describe("", () => { describe("sendIterable", () => { it("sends empty body for an empty iterator", async () => { - app.use(eventHandler((event) => sendIterable(event, []))); - const result = await request.get("/"); + ctx.app.use(eventHandler((event) => sendIterable(event, []))); + const result = await ctx.request.get("/"); expect(result.header["content-length"]).toBe("0"); expect(result.text).toBe(""); }); it("concatenates iterated values", async () => { - app.use(eventHandler((event) => sendIterable(event, ["a", "b", "c"]))); - const result = await request.get("/"); + ctx.app.use( + eventHandler((event) => sendIterable(event, ["a", "b", "c"])), + ); + const result = await ctx.request.get("/"); expect(result.text).toBe("abc"); }); @@ -146,8 +140,8 @@ describe("", () => { }), }, ])("$type", async ({ iterable }) => { - app.use(eventHandler((event) => sendIterable(event, iterable))); - const response = await request.get("/"); + ctx.app.use(eventHandler((event) => sendIterable(event, iterable))); + const response = await ctx.request.get("/"); expect(response.text).toBe("the-value"); }); }); @@ -157,12 +151,12 @@ describe("", () => { const iterable = [1, "2", { field: 3 }, null]; const serializer = vi.fn(() => "x"); - app.use( + ctx.app.use( eventHandler((event) => sendIterable(event, iterable, { serializer }), ), ); - const response = await request.get("/"); + const response = await ctx.request.get("/"); expect(response.text).toBe("x".repeat(iterable.length)); expect(serializer).toBeCalledTimes(4); for (const [i, obj] of iterable.entries()) { @@ -174,26 +168,26 @@ describe("", () => { describe("useBase", () => { it("can prefix routes", async () => { - app.use( + ctx.app.use( "/", useBase( "/api", eventHandler((event) => Promise.resolve(event.path)), ), ); - const result = await request.get("/api/test"); + const result = await ctx.request.get("/api/test"); expect(result.text).toBe("/test"); }); it("does nothing when not provided a base", async () => { - app.use( + ctx.app.use( "/", useBase( "", eventHandler((event) => Promise.resolve(event.path)), ), ); - const result = await request.get("/api/test"); + const result = await ctx.request.get("/api/test"); expect(result.text).toBe("/api/test"); }); @@ -201,7 +195,7 @@ describe("", () => { describe("getQuery", () => { it("can parse query params", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { const query = getQuery(event); @@ -213,7 +207,7 @@ describe("", () => { return "200"; }), ); - const result = await request.get( + const result = await ctx.request.get( "/api/test?bool=true&name=string&number=1", ); @@ -223,12 +217,12 @@ describe("", () => { describe("getMethod", () => { it("can get method", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => event.method), ); - expect((await request.get("/api")).text).toBe("GET"); - expect((await request.post("/api")).text).toBe("POST"); + expect((await ctx.request.get("/api")).text).toBe("GET"); + expect((await ctx.request.post("/api")).text).toBe("POST"); }); }); @@ -256,7 +250,7 @@ describe("", () => { ]; for (const test of tests) { it("getRequestURL: " + JSON.stringify(test), async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { const url = getRequestURL(event, { @@ -268,7 +262,7 @@ describe("", () => { return url; }), ); - const req = request.get(test.path); + const req = ctx.request.get(test.path); if (test.host) { req.set("Host", test.host); } @@ -284,7 +278,7 @@ describe("", () => { describe("getRequestIP", () => { it("x-forwarded-for", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { return getRequestIP(event, { @@ -292,12 +286,12 @@ describe("", () => { }); }), ); - const req = request.get("/"); + const req = ctx.request.get("/"); req.set("x-forwarded-for", "127.0.0.1"); expect((await req).text).toBe("127.0.0.1"); }); it("ports", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { return getRequestIP(event, { @@ -305,12 +299,12 @@ describe("", () => { }); }), ); - const req = request.get("/"); + const req = ctx.request.get("/"); req.set("x-forwarded-for", "127.0.0.1:1234"); expect((await req).text).toBe("127.0.0.1:1234"); }); it("ipv6", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { return getRequestIP(event, { @@ -318,12 +312,12 @@ describe("", () => { }); }), ); - const req = request.get("/"); + const req = ctx.request.get("/"); req.set("x-forwarded-for", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"); expect((await req).text).toBe("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); }); it("multiple ips", async () => { - app.use( + ctx.app.use( "/", eventHandler((event) => { return getRequestIP(event, { @@ -331,7 +325,7 @@ describe("", () => { }); }), ); - const req = request.get("/"); + const req = ctx.request.get("/"); req.set("x-forwarded-for", "client , proxy1, proxy2"); expect((await req).text).toBe("client"); }); @@ -339,9 +333,9 @@ describe("", () => { describe("getRequestFingerprint", () => { it("returns an hash", async () => { - app.use(eventHandler((event) => getRequestFingerprint(event))); + ctx.app.use(eventHandler((event) => getRequestFingerprint(event))); - const req = request.get("/"); + const req = ctx.request.get("/"); // sha1 is 40 chars long expect((await req).text).toHaveLength(40); @@ -351,27 +345,27 @@ describe("", () => { }); it("returns the same hash every time for same request", async () => { - app.use( + ctx.app.use( eventHandler((event) => getRequestFingerprint(event, { hash: false })), ); - const req = request.get("/"); + const req = ctx.request.get("/"); expect((await req).text).toMatchInlineSnapshot('"::ffff:127.0.0.1"'); expect((await req).text).toMatchInlineSnapshot('"::ffff:127.0.0.1"'); }); it("returns null when all detections impossible", async () => { - app.use( + ctx.app.use( eventHandler((event) => getRequestFingerprint(event, { hash: false, ip: false }), ), ); - const f1 = (await request.get("/")).text; + const f1 = (await ctx.request.get("/")).text; expect(f1).toBe(""); }); it("can use path/method", async () => { - app.use( + ctx.app.use( eventHandler((event) => getRequestFingerprint(event, { hash: false, @@ -382,19 +376,19 @@ describe("", () => { ), ); - const req = request.post("/foo"); + const req = ctx.request.post("/foo"); expect((await req).text).toMatchInlineSnapshot('"POST|/foo"'); }); it("uses user agent when available", async () => { - app.use( + ctx.app.use( eventHandler((event) => getRequestFingerprint(event, { hash: false, userAgent: true }), ), ); - const req = request.get("/"); + const req = ctx.request.get("/"); req.set("user-agent", "test-user-agent"); expect((await req).text).toMatchInlineSnapshot( @@ -403,32 +397,33 @@ describe("", () => { }); it("uses x-forwarded-for ip when header set", async () => { - app.use( + ctx.app.use( eventHandler((event) => getRequestFingerprint(event, { hash: false, xForwardedFor: true }), ), ); - const req = request.get("/"); + const req = ctx.request.get("/"); req.set("x-forwarded-for", "x-forwarded-for"); expect((await req).text).toMatchInlineSnapshot('"x-forwarded-for"'); }); it("uses the request ip when no x-forwarded-for header set", async () => { - app.use( + ctx.app.use( eventHandler((event) => getRequestFingerprint(event, { hash: false })), ); - app.options.onRequest = (e) => { - Object.defineProperty(e.node.req.socket, "remoteAddress", { + ctx.app.options.onRequest = (event) => { + const { socket } = getNodeContext(event)?.req || {}; + Object.defineProperty(socket, "remoteAddress", { get(): any { return "0.0.0.0"; }, }); }; - const req = request.get("/"); + const req = ctx.request.get("/"); expect((await req).text).toMatchInlineSnapshot('"0.0.0.0"'); }); @@ -436,34 +431,34 @@ describe("", () => { describe("assertMethod", () => { it("only allow head and post", async () => { - app.use( + ctx.app.use( "/post", eventHandler((event) => { assertMethod(event, "POST", true); return "ok"; }), ); - expect((await request.get("/post")).status).toBe(405); - expect((await request.post("/post")).status).toBe(200); - expect((await request.head("/post")).status).toBe(200); + expect((await ctx.request.get("/post")).status).toBe(405); + expect((await ctx.request.post("/post")).status).toBe(200); + expect((await ctx.request.head("/post")).status).toBe(200); }); }); const below18 = Number.parseInt(process.version.slice(1).split(".")[0]) < 18; describe.skipIf(below18)("readFormData", () => { it("can handle form as FormData in event handler", async () => { - app.use( + ctx.app.use( "/", eventHandler(async (event) => { - const formData = await readFormData(event); - const user = formData.get("user"); + const formData = await readFormDataBody(event); + const user = formData!.get("user"); expect(formData instanceof FormData).toBe(true); expect(user).toBe("john"); return { user }; }), ); - const result = await request + const result = await ctx.request .post("/api/test") .set("content-type", "application/x-www-form-urlencoded; charset=utf-8") .field("user", "john"); diff --git a/test/validate.test.ts b/test/validate.test.ts index 6afbc84e..863f0880 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -1,15 +1,8 @@ -import supertest, { SuperTest, Test } from "supertest"; +import type { ValidateFunction } from "../src/types"; import { describe, it, expect, beforeEach } from "vitest"; import { z } from "zod"; -import { - createApp, - toNodeListener, - App, - eventHandler, - readValidatedBody, - getValidatedQuery, - ValidateFunction, -} from "../src"; +import { eventHandler, readValidatedJSONBody, getValidatedQuery } from "../src"; +import { setupTest } from "./_utils"; // Custom validator const customValidate: ValidateFunction<{ @@ -32,28 +25,22 @@ const zodValidate = z.object({ }).parse; describe("Validate", () => { - let app: App; - let request: SuperTest; + const ctx = setupTest(); - beforeEach(() => { - app = createApp({ debug: true }); - request = supertest(toNodeListener(app)); - }); - - describe("readValidatedBody", () => { + describe("readValidatedJSONBody", () => { beforeEach(() => { - app.use( + ctx.app.use( "/custom", eventHandler(async (event) => { - const data = await readValidatedBody(event, customValidate); + const data = await readValidatedJSONBody(event, customValidate); return data; }), ); - app.use( + ctx.app.use( "/zod", eventHandler(async (event) => { - const data = await readValidatedBody(event, zodValidate); + const data = await readValidatedJSONBody(event, zodValidate); return data; }), ); @@ -61,13 +48,13 @@ describe("Validate", () => { describe("custom validator", () => { it("Valid JSON", async () => { - const res = await request.post("/custom").send({ field: "value" }); + const res = await ctx.request.post("/custom").send({ field: "value" }); expect(res.body).toEqual({ field: "value", default: "default" }); expect(res.status).toEqual(200); }); - it("Valid x-www-form-urlencoded", async () => { - const res = await request + it("Validate x-www-form-urlencoded", async () => { + const res = await ctx.request .post("/custom") .set("Content-Type", "application/x-www-form-urlencoded") .send("field=value"); @@ -76,7 +63,7 @@ describe("Validate", () => { }); it("Invalid JSON", async () => { - const res = await request.post("/custom").send({ invalid: true }); + const res = await ctx.request.post("/custom").send({ invalid: true }); expect(res.text).include("Invalid key"); expect(res.status).toEqual(400); }); @@ -84,13 +71,13 @@ describe("Validate", () => { describe("zod validator", () => { it("Valid", async () => { - const res = await request.post("/zod").send({ field: "value" }); + const res = await ctx.request.post("/zod").send({ field: "value" }); expect(res.body).toEqual({ field: "value", default: "default" }); expect(res.status).toEqual(200); }); it("Invalid", async () => { - const res = await request.post("/zod").send({ invalid: true }); + const res = await ctx.request.post("/zod").send({ invalid: true }); expect(res.status).toEqual(400); expect(res.body.data?.issues?.[0]?.code).toEqual("invalid_type"); }); @@ -99,7 +86,7 @@ describe("Validate", () => { describe("getQuery", () => { beforeEach(() => { - app.use( + ctx.app.use( "/custom", eventHandler(async (event) => { const data = await getValidatedQuery(event, customValidate); @@ -107,7 +94,7 @@ describe("Validate", () => { }), ); - app.use( + ctx.app.use( "/zod", eventHandler(async (event) => { const data = await getValidatedQuery(event, zodValidate); @@ -118,13 +105,13 @@ describe("Validate", () => { describe("custom validator", () => { it("Valid", async () => { - const res = await request.get("/custom?field=value"); + const res = await ctx.request.get("/custom?field=value"); expect(res.body).toEqual({ field: "value", default: "default" }); expect(res.status).toEqual(200); }); it("Invalid", async () => { - const res = await request.get("/custom?invalid=true"); + const res = await ctx.request.get("/custom?invalid=true"); expect(res.text).include("Invalid key"); expect(res.status).toEqual(400); }); @@ -132,13 +119,13 @@ describe("Validate", () => { describe("zod validator", () => { it("Valid", async () => { - const res = await request.get("/zod?field=value"); + const res = await ctx.request.get("/zod?field=value"); expect(res.body).toEqual({ field: "value", default: "default" }); expect(res.status).toEqual(200); }); it("Invalid", async () => { - const res = await request.get("/zod?invalid=true"); + const res = await ctx.request.get("/zod?invalid=true"); expect(res.status).toEqual(400); }); }); diff --git a/test/web.test.ts b/test/web.test.ts index 24850219..0e0d830b 100644 --- a/test/web.test.ts +++ b/test/web.test.ts @@ -1,41 +1,32 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { - createApp, - App, eventHandler, - WebHandler, - toWebHandler, - readBody, + readTextBody, + setResponseStatus, + getRequestHeaders, } from "../src"; +import { setupTest } from "./_utils"; describe("Web handler", () => { - let app: App; - let handler: WebHandler; - - beforeEach(() => { - app = createApp({ debug: true }); - handler = toWebHandler(app); - }); + const ctx = setupTest(); it("works", async () => { - app.use( + ctx.app.use( "/test", eventHandler(async (event) => { - const body = - event.method === "POST" ? await readBody(event) : undefined; - event.node.res.statusCode = 201; - event.node.res.statusMessage = "Created"; + const body = await readTextBody(event); + setResponseStatus(event, 201, "Created"); return { method: event.method, path: event.path, - headers: [...event.headers.entries()], + headers: getRequestHeaders(event), body, contextKeys: Object.keys(event.context), }; }), ); - const res = await handler( + const res = await ctx.webHandler( new Request(new URL("/test/foo/bar", "http://localhost"), { method: "POST", headers: { @@ -58,10 +49,10 @@ describe("Web handler", () => { method: "POST", path: "/foo/bar", body: "request body", - headers: [ - ["content-type", "text/plain;charset=UTF-8"], - ["x-test", "true"], - ], + headers: { + "content-type": "text/plain;charset=UTF-8", + "x-test": "true", + }, contextKeys: ["test"], }); }); diff --git a/tsconfig.json b/tsconfig.json index 22fe6c96..2338ce93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "baseUrl": ".", "target": "ESNext", "module": "ESNext", "skipLibCheck": true, @@ -8,8 +7,8 @@ "moduleResolution": "Node", "lib": ["WebWorker", "DOM", "DOM.Iterable"], "strict": true, - "declaration": true, + "noEmit": true, "types": ["node"] }, - "include": ["src", "test/types.test-d.ts"] + "include": ["src", "test"] } diff --git a/typos.toml b/typos.toml new file mode 100644 index 00000000..d6be7ff5 --- /dev/null +++ b/typos.toml @@ -0,0 +1,8 @@ +# https://github.com/crate-ci/typos +# https://github.com/crate-ci/typos/blob/master/docs/reference.md + +[files] +extend-exclude = ["CHANGELOG.md"] + +[default.extend-words] +ECT = "ECT" diff --git a/vitest.config.mjs b/vitest.config.mjs index 358db8d7..b1169cbd 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -5,7 +5,7 @@ export default defineConfig({ setupFiles: ["./test/_setup"], typecheck: { enabled: true }, coverage: { - reporter: ["text", "clover", "json"], + include: ["src/**/*.ts"] }, }, });