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

Hello world!

"; }) ); @@ -208,7 +211,7 @@ describe("app", () => { it("can short-circuit route matching", async () => { app.use( eventHandler((event) => { - event.node.res.end("done"); + return send(event, "done"); }) ); app.use(eventHandler(() => "valid")); diff --git a/cloudflare/cloudflare.test.ts b/test/cloudflare.test.ts similarity index 100% rename from cloudflare/cloudflare.test.ts rename to test/cloudflare.test.ts diff --git a/cloudflare/h3-worker.ts b/test/h3-worker.ts similarity index 94% rename from cloudflare/h3-worker.ts rename to test/h3-worker.ts index 0795f43a..56540232 100644 --- a/cloudflare/h3-worker.ts +++ b/test/h3-worker.ts @@ -12,7 +12,6 @@ router .get( "/", eventHandler((event) => { - console.log("In the handler ..."); const response = new Response(`Hello world ! ${event.request.url}`); const { readable, writable } = new TransformStream(); response.body?.pipeTo(writable); diff --git a/test/header.test.ts b/test/header.test.ts index b78dd88e..c2269777 100644 --- a/test/header.test.ts +++ b/test/header.test.ts @@ -34,7 +34,7 @@ describe("", () => { "/", eventHandler((event) => { const headers = getRequestHeaders(event); - expect(headers).toEqual(event.node.req.headers); + expect(headers).toHaveProperty("accept", "application/json"); }) ); await request.get("/").set("Accept", "application/json"); @@ -47,7 +47,7 @@ describe("", () => { "/", eventHandler((event) => { const headers = getHeaders(event); - expect(headers).toEqual(event.node.req.headers); + expect(headers).toHaveProperty("accept", "application/json"); }) ); await request.get("/").set("Accept", "application/json"); diff --git a/test/proxy.test.ts b/test/proxy.test.ts index 3a5c458c..1b8a9615 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -47,12 +47,12 @@ describe("", () => { }) ); - const result = await request.get("/"); + const result = await request.get("/").retry(5); expect(result.text).toContain( 'a href="https://www.iana.org/domains/example">More information...' ); - }); + }, 5000); }); describe("proxyRequest", () => { diff --git a/test/router.test.ts b/test/router.test.ts index 651444d0..dec65548 100644 --- a/test/router.test.ts +++ b/test/router.test.ts @@ -99,7 +99,7 @@ describe("router", () => { }); it("Not matching route method", async () => { - const res = await request.head("/test"); + const res = await request.delete("/test"); expect(res.status).toEqual(405); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index f65a437e..df210cd0 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -11,6 +11,7 @@ import { getMethod, getQuery, getRequestURL, + getUrlPath, } from "../src"; describe("", () => { @@ -40,7 +41,7 @@ describe("", () => { "/", useBase( "/api", - eventHandler((event) => Promise.resolve(event.node.req.url || "none")) + eventHandler((event) => Promise.resolve(getUrlPath(event) || "none")) ) ); const result = await request.get("/api/test"); @@ -52,7 +53,7 @@ describe("", () => { "/", useBase( "", - eventHandler((event) => Promise.resolve(event.node.req.url || "none")) + eventHandler((event) => Promise.resolve(getUrlPath(event) || "none")) ) ); const result = await request.get("/api/test"); @@ -145,19 +146,17 @@ describe("", () => { }); describe("assertMethod", () => { - it("only allow head and post", async () => { + it("only allow delete and post", async () => { app.use( "/post", eventHandler((event) => { - console.log("HANDLER"); - console.log(event.request); - assertMethod(event, "POST", true); + assertMethod(event, ["POST", "DELETE"], true); return "ok"; }) ); - // expect((await request.get('/post')).status).toBe(405) - // expect((await request.post('/post')).status).toBe(200) - expect((await request.head("/post")).status).toBe(200); + expect((await request.get("/post")).status).toBe(405); + expect((await request.post("/post")).status).toBe(200); + expect((await request.delete("/post")).status).toBe(200); }); }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 7fa63ebb..fdf1c805 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,14 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vitest/config" export default defineConfig({ test: { coverage: { reporter: ["text", "clover", "json"] + }, + environment: "miniflare", + environmentOptions: { + bindings: { KEY: "value" }, + kvNamespaces: ["TEST_NAMESPACE"] } } -}); +}) From 83ff2b83dcdd2ff24e2402c2fc6a1a82719cf6d2 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Jun 2023 01:27:02 +0700 Subject: [PATCH 06/14] feat: add universal support for native response --- src/utils/headers.ts | 12 +++- src/utils/response.ts | 128 +++++++++++++++++++----------------------- 2 files changed, 68 insertions(+), 72 deletions(-) diff --git a/src/utils/headers.ts b/src/utils/headers.ts index 616283a8..8caf6865 100644 --- a/src/utils/headers.ts +++ b/src/utils/headers.ts @@ -2,6 +2,14 @@ import { OutgoingMessage } from "node:http"; import { H3Event } from "src/event"; import { RequestHeaders } from "src/types"; +export function removeResponseHeader(event: H3Event, name: string): void { + if (event.request) { + event._internalData.headers.delete(name); + return; + } + return event.node.res.removeHeader(name); +} + export function getRequestHeaders(event: H3Event): RequestHeaders { const _headers: RequestHeaders = {}; if (event.request) { @@ -42,9 +50,9 @@ export function getResponseHeaders( event: H3Event ): ReturnType { if (event.request) { - return Object.fromEntries(event._internalData.headers); + return Object.fromEntries(event._internalData.headers.entries()); } - return event.node.res.getHeaders(); + return event.node.res.getHeaders() as Record; } export function getResponseHeader( diff --git a/src/utils/response.ts b/src/utils/response.ts index 2fd64d2d..7bae3c0e 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -3,6 +3,51 @@ import { createError } from "../error"; import type { H3Event } from "../event"; import { MIMES } from "./consts"; import { sanitizeStatusCode, sanitizeStatusMessage } from "./sanitize"; +import { + getResponseHeaders, + setResponseHeader, + removeResponseHeader, + getResponseHeader, +} from "./headers"; + +export function getResponseStatus(event: H3Event): number { + if (event.request) { + return event._internalData.status; + } + return event.node.res.statusCode; +} + +export function getResponseStatusText(event: H3Event): string { + if (event.request) { + return event._internalData.statusMessage; + } + return event.node.res.statusMessage; +} + +export function setResponseStatus( + event: H3Event, + code?: number, + text?: string +): void { + if (event.request) { + if (code) { + event._internalData.status = sanitizeStatusCode(code); + } + if (text) { + event._internalData.statusMessage = sanitizeStatusMessage(text); + } + return; + } + if (code) { + event.node.res.statusCode = sanitizeStatusCode( + code, + event.node.res.statusCode + ); + } + if (text) { + event.node.res.statusMessage = sanitizeStatusMessage(text); + } +} const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn: () => any) => fn(); @@ -21,18 +66,19 @@ export function send(event: H3Event, data?: any, type?: string) { }); }); } + export function sendResponseWithInternal(event: H3Event, response: Response) { const mergedHeaders = new Map(); for (const [key, value] of response.headers.entries()) { mergedHeaders.set(key, value); } - for (const [key, value] of event._internalData.headers.entries()) { + for (const [key, value] of Object.entries(getResponseHeaders(event))) { mergedHeaders.set(key, value); } const headers = Object.fromEntries(mergedHeaders); return new Response(response.body, { ...response, - status: event._internalData.status || response.status, + status: getResponseStatus(event) || response.status, headers, }); } @@ -45,86 +91,28 @@ export function sendResponseWithInternal(event: H3Event, response: Response) { * @param code status code to be send. By default, it is `204 No Content`. */ export function sendNoContent(event: H3Event, code = 204) { - if (event.request) { - event._internalData.status = sanitizeStatusCode(code); - if (code === 204) { - event._internalData.headers.delete("content-length"); - } - return sendResponseWithInternal(event, new Response(null)); - } - event.node.res.statusCode = sanitizeStatusCode(code, 204); + setResponseStatus(event, code); // 204 responses MUST NOT have a Content-Length header field (https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2) - if (event.node.res.statusCode === 204) { - event.node.res.removeHeader("content-length"); + if (code === 204) { + removeResponseHeader(event, "content-length"); } - event.node.res.end(); -} - -export function setResponseStatus( - event: H3Event, - code?: number, - text?: string -): void { - if (event.request) { - if (code) { - event._internalData.status = sanitizeStatusCode(code); - } - if (text) { - event._internalData.statusMessage = sanitizeStatusMessage(text); - } - return; - } - if (code) { - event.node.res.statusCode = sanitizeStatusCode( - code, - event.node.res.statusCode - ); - } - if (text) { - event.node.res.statusMessage = sanitizeStatusMessage(text); - } -} - -export function getResponseStatus(event: H3Event): number { - if (event.request) { - return event._internalData.status; - } - return event.node.res.statusCode; -} - -export function getResponseStatusText(event: H3Event): string { - if (event.request) { - return event._internalData.statusMessage; - } - return event.node.res.statusMessage; + return send(event, null); } export function defaultContentType(event: H3Event, type?: string) { - if (type && event.request) { - if (!event._internalData.headers.has("content-type")) { - event._internalData.headers.set("content-type", type); + if (type) { + const contentType = getResponseHeader(event, "content-type"); + if (!contentType) { + setResponseHeader(event, "content-type", type); } - return; - } - if (type && !event.node.res.getHeader("content-type")) { - event.node.res.setHeader("content-type", type); } } export function sendRedirect(event: H3Event, location: string, code = 302) { const encodedLoc = location.replace(/"/g, "%22"); const html = ``; - if (event.request) { - event._internalData.status = sanitizeStatusCode(code); - event._internalData.headers.set("location", location); - event._internalData.headers.set("content-type", MIMES.html); - return sendResponseWithInternal(event, new Response(html)); - } - event.node.res.statusCode = sanitizeStatusCode( - code, - event.node.res.statusCode - ); - event.node.res.setHeader("location", location); + setResponseStatus(event, code); + setResponseHeader(event, "location", location); return send(event, html, MIMES.html); } From b61f9283594f049f60817d744ace7722574b8185 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Jun 2023 18:12:16 +0700 Subject: [PATCH 07/14] feat: improve native body parsing --- src/event/event.ts | 4 +++ src/utils/body.ts | 67 +++++++++++++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/event/event.ts b/src/event/event.ts index 6f64caac..b7813fd1 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -15,6 +15,8 @@ interface InternalData { statusMessage: string; originalUrlPath: string | undefined; currentUrlPath: string | undefined; + parsedBody: unknown; + rawBody: unknown; } export class H3Event implements Pick { "__is_event__" = true; @@ -27,6 +29,8 @@ export class H3Event implements Pick { statusMessage: "", originalUrlPath: undefined, currentUrlPath: undefined, + parsedBody: null, + rawBody: null, }; constructor( diff --git a/src/utils/body.ts b/src/utils/body.ts index 9eb05ee8..c66cfdd2 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -28,9 +28,23 @@ export function readRawBody( assertMethod(event, PayloadMethods); if (event.request) { - return encoding - ? event.request.text() - : (event.request.arrayBuffer() as Promise); + // Reuse body if already read + if (event._internalData.rawBody) { + return event._internalData.rawBody as any; + } + if (!Number.parseInt(getRequestHeader(event, "content-length") || "")) { + return Promise.resolve(undefined); + } + + if (encoding) { + const textBody = event.request.text(); + event._internalData.rawBody = textBody; + return textBody.then((body) => body) as any; + } else { + const arrayBuffer = event.request.arrayBuffer(); + event._internalData.rawBody = arrayBuffer; + return arrayBuffer.then((body) => body) as any; + } } // Reuse body if already read const _rawBody = @@ -75,7 +89,6 @@ export function readRawBody( /** * Reads request body and try to safely parse using [destr](https://github.com/unjs/destr) - * Node only. * @param event {H3Event} H3 event or req passed by h3 handler * @param encoding {Encoding} encoding="utf-8" - The character encoding to use. * @@ -86,26 +99,41 @@ export function readRawBody( * ``` */ export async function readBody(event: H3Event): Promise { + const contentType = + getRequestHeader(event, "content-type")?.toLowerCase() || ""; if (event.request) { - const contentType = getRequestHeader(event, "content-type") || ""; - if (contentType.includes("application/json")) { - return event.request.json(); + if (event._internalData.parsedBody) { + return event._internalData.parsedBody as T; + } + if (contentType === "application/json") { + const body = event.request.json(); + event._internalData.parsedBody = body; + return body as T; } - if (contentType.includes("application/octet-stream")) { - return event.request.arrayBuffer() as T; + if (contentType === "application/octet-stream") { + const body = event.request.arrayBuffer(); + event._internalData.parsedBody = body; + return body as T; } - if (contentType.includes("multipart/form-data")) { - return event.request.formData() as T; + if (contentType === "multipart/form-data") { + const body = event.request.formData(); + event._internalData.parsedBody = body; + return body as T; } - if (contentType.includes("text")) { - return event.request.text() as T; + if (contentType === "text") { + const body = event.request.text(); + event._internalData.parsedBody = body; + return body as T; } - if (contentType.includes("application/x-www-form-urlencoded")) { + if (contentType === "application/x-www-form-urlencoded") { const text = await event.request.text(); - const form = new URLSearchParams(text); - return parseUrlSearchParams(form) as T; + const body = parseUrlSearchParams(new URLSearchParams(text)); + event._internalData.parsedBody = body; + return body as T; } - return event.request.blob() as T; + const body = event.request.blob(); + event._internalData.parsedBody = body; + return body as T; } if (ParsedBodySymbol in event.node.req) { @@ -114,10 +142,7 @@ export async function readBody(event: H3Event): Promise { const body = await readRawBody(event, "utf8"); - if ( - getRequestHeader(event, "content-type") === - "application/x-www-form-urlencoded" - ) { + if (contentType === "application/x-www-form-urlencoded") { const form = new URLSearchParams(body); return parseUrlSearchParams(form) as T; } From 43c1d4a555d2157242911be04055ad4066fe85b2 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Jun 2023 20:55:10 +0700 Subject: [PATCH 08/14] feat: improve response support --- src/app.ts | 9 +++-- src/error.ts | 4 +-- src/event/event.ts | 4 --- src/utils/body.ts | 59 +++++++++++++------------------- src/utils/cache.ts | 4 +-- src/utils/headers.ts | 46 +++++++++++++++---------- src/utils/proxy.ts | 5 ++- src/utils/request.ts | 23 ++++++------- src/utils/response.ts | 78 ++++++++++++++++++++++++++++++++----------- 9 files changed, 134 insertions(+), 98 deletions(-) diff --git a/src/app.ts b/src/app.ts index 3d227dec..f23f908b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,8 +12,10 @@ import { sendStream, isStream, MIMES, - sendResponseWithInternal, + sendResponse, setResponseStatus, + RawResponse, + sendResponseRaw, } from "./utils"; import type { EventHandler, LazyEventHandler } from "./types"; import { @@ -130,8 +132,11 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) { return; } const type = typeof val; + if (val instanceof RawResponse) { + return sendResponseRaw(event, val); + } if (val instanceof Response) { - return sendResponseWithInternal(event, val); + return sendResponse(event, val); } if (type === "string") { return send(event, val, MIMES.html); diff --git a/src/error.ts b/src/error.ts index 9ddca34e..c7d6fa76 100644 --- a/src/error.ts +++ b/src/error.ts @@ -5,7 +5,7 @@ import { sanitizeStatusMessage, sanitizeStatusCode, setResponseHeader, - sendResponseWithInternal, + sendResponse, send, } from "./utils"; @@ -161,7 +161,7 @@ export function sendError( setResponseHeader(event, "content-type", MIMES.json); const bodyToSend = JSON.stringify(responseBody, undefined, 2); if (event.request) { - return sendResponseWithInternal(event, new Response(bodyToSend)); + return sendResponse(event, new Response(bodyToSend)); } send(event, bodyToSend); } diff --git a/src/event/event.ts b/src/event/event.ts index b7813fd1..6f64caac 100644 --- a/src/event/event.ts +++ b/src/event/event.ts @@ -15,8 +15,6 @@ interface InternalData { statusMessage: string; originalUrlPath: string | undefined; currentUrlPath: string | undefined; - parsedBody: unknown; - rawBody: unknown; } export class H3Event implements Pick { "__is_event__" = true; @@ -29,8 +27,6 @@ export class H3Event implements Pick { statusMessage: "", originalUrlPath: undefined, currentUrlPath: undefined, - parsedBody: null, - rawBody: null, }; constructor( diff --git a/src/utils/body.ts b/src/utils/body.ts index c66cfdd2..b1418fe4 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -28,23 +28,21 @@ export function readRawBody( assertMethod(event, PayloadMethods); if (event.request) { - // Reuse body if already read - if (event._internalData.rawBody) { - return event._internalData.rawBody as any; - } if (!Number.parseInt(getRequestHeader(event, "content-length") || "")) { return Promise.resolve(undefined); } - - if (encoding) { - const textBody = event.request.text(); - event._internalData.rawBody = textBody; - return textBody.then((body) => body) as any; - } else { - const arrayBuffer = event.request.arrayBuffer(); - event._internalData.rawBody = arrayBuffer; - return arrayBuffer.then((body) => body) as any; - } + // we clone the request so we can re-use readBody/ readBodyRaw later. + const request = event.request.clone(); + + const result = encoding + ? request.text().then((str) => str) + : request + .arrayBuffer() + .then((buffer) => Buffer.from(new Uint8Array(buffer))); + + return result as E extends false + ? Promise + : Promise; } // Reuse body if already read const _rawBody = @@ -102,38 +100,24 @@ export async function readBody(event: H3Event): Promise { const contentType = getRequestHeader(event, "content-type")?.toLowerCase() || ""; if (event.request) { - if (event._internalData.parsedBody) { - return event._internalData.parsedBody as T; - } + const request = event.request.clone(); // Clone the request for re-use. if (contentType === "application/json") { - const body = event.request.json(); - event._internalData.parsedBody = body; - return body as T; + return request.json(); } if (contentType === "application/octet-stream") { - const body = event.request.arrayBuffer(); - event._internalData.parsedBody = body; - return body as T; + return request.arrayBuffer() as T; } if (contentType === "multipart/form-data") { - const body = event.request.formData(); - event._internalData.parsedBody = body; - return body as T; + return request.formData() as T; } if (contentType === "text") { - const body = event.request.text(); - event._internalData.parsedBody = body; - return body as T; + return request.text() as T; } if (contentType === "application/x-www-form-urlencoded") { - const text = await event.request.text(); - const body = parseUrlSearchParams(new URLSearchParams(text)); - event._internalData.parsedBody = body; - return body as T; + const text = await request.text(); + return parseUrlSearchParams(new URLSearchParams(text)) as T; } - const body = event.request.blob(); - event._internalData.parsedBody = body; - return body as T; + return request.blob() as T; // We return a blob if we don't know the type. } if (ParsedBodySymbol in event.node.req) { @@ -161,6 +145,9 @@ export async function readMultipartFormData(event: H3Event) { if (!boundary) { return; } + if (event.request) { + return event.request.clone().formData(); + } const body = await readRawBody(event, false); if (!body) { return; diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 8151312b..2064199d 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,6 +1,6 @@ import type { H3Event } from "../event"; import { getRequestRawHeader, setResponseHeader } from "./headers"; -import { sendResponseWithInternal, setResponseStatus } from "./response"; +import { sendResponse, setResponseStatus } from "./response"; export interface CacheConditions { modifiedTime?: string | Date; @@ -51,7 +51,7 @@ export function handleCacheHeaders( if (cacheMatched) { setResponseStatus(event, 304); if (!event.request) { - sendResponseWithInternal(event, new Response()); + sendResponse(event, new Response()); return true; } event.node.res.end(); diff --git a/src/utils/headers.ts b/src/utils/headers.ts index 8caf6865..a84b04fa 100644 --- a/src/utils/headers.ts +++ b/src/utils/headers.ts @@ -2,6 +2,16 @@ import { OutgoingMessage } from "node:http"; import { H3Event } from "src/event"; import { RequestHeaders } from "src/types"; +export function removeResponseHeaders(event: H3Event): void { + if (event.request) { + event._internalData.headers.clear(); + return; + } + for (const [name] of Object.entries(getHeaders(event))) { + removeResponseHeader(event, name); + } +} + export function removeResponseHeader(event: H3Event, name: string): void { if (event.request) { event._internalData.headers.delete(name); @@ -30,15 +40,6 @@ export function getRequestHeaders(event: H3Event): RequestHeaders { return _headers; } -export function getRequestHeader( - event: H3Event, - name: string -): RequestHeaders[string] { - const headers = getRequestHeaders(event); - const value = headers[name.toLowerCase()]; - return value; -} - export function getRequestRawHeader(event: H3Event, name: string) { if (event.request) { return event.request.headers.get(name); @@ -65,15 +66,6 @@ export function getResponseHeader( return event.node.res.getHeader(name); } -export function setResponseHeaders( - event: H3Event, - headers: Record[1]> -): void { - for (const [name, value] of Object.entries(headers)) { - setResponseHeader(event, name, value); - } -} - export function setResponseHeader( event: H3Event, name: string, @@ -85,6 +77,24 @@ export function setResponseHeader( return event.node.res.setHeader(name, value); } +export function getRequestHeader( + event: H3Event, + name: string +): RequestHeaders[string] { + const headers = getRequestHeaders(event); + const value = headers[name.toLowerCase()]; + return value; +} + +export function setResponseHeaders( + event: H3Event, + headers: Record[1]> +): void { + for (const [name, value] of Object.entries(headers)) { + setResponseHeader(event, name, value); + } +} + export function appendResponseHeaders( event: H3Event, headers: Record diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index a971cb9d..5165b705 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -3,13 +3,12 @@ import type { H3EventContext, RequestHeaders } from "../types"; import { getMethod } from "./request"; import { readRawBody } from "./body"; import { splitCookiesString } from "./cookie"; -import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize"; import { appendResponseHeader, getRequestHeaders, setResponseHeader, } from "./headers"; -import { sendResponseWithInternal, setResponseStatus } from "./response"; +import { sendResponse, setResponseStatus } from "./response"; export interface ProxyOptions { headers?: RequestHeaders | HeadersInit; @@ -110,7 +109,7 @@ export async function sendProxy( } if (event.request) { - return sendResponseWithInternal(event, response); + return sendResponse(event, response); } // Directly send consumed _data if ((response as any)._data !== undefined) { diff --git a/src/utils/request.ts b/src/utils/request.ts index 2fd93d61..27b0406e 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -4,6 +4,16 @@ import type { HTTPMethod } from "../types"; import type { H3Event } from "../event"; import { getUrlPath } from "./url"; +export function getMethod( + event: H3Event, + defaultMethod: HTTPMethod = "GET" +): HTTPMethod { + if (event.request) { + return (event.request.method || defaultMethod).toUpperCase() as HTTPMethod; + } + return (event.node.req.method || defaultMethod).toUpperCase() as HTTPMethod; +} + export function getQuery(event: H3Event) { return _getQuery(getUrlPath(event) || ""); } @@ -24,16 +34,6 @@ export function getRouterParam( return params[name]; } -export function getMethod( - event: H3Event, - defaultMethod: HTTPMethod = "GET" -): HTTPMethod { - if (event.request) { - return (event.request.method || defaultMethod).toUpperCase() as HTTPMethod; - } - return (event.node.req.method || defaultMethod).toUpperCase() as HTTPMethod; -} - export function isMethod( event: H3Event, expected: HTTPMethod | HTTPMethod[], @@ -62,10 +62,9 @@ export function assertMethod( allowHead?: boolean ) { if (!isMethod(event, expected, allowHead)) { - const error = createError({ + throw createError({ statusCode: 405, statusMessage: "HTTP method is not allowed.", }); - throw error; } } diff --git a/src/utils/response.ts b/src/utils/response.ts index 7bae3c0e..88b0b7e3 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -8,6 +8,8 @@ import { setResponseHeader, removeResponseHeader, getResponseHeader, + removeResponseHeaders, + setHeaders, } from "./headers"; export function getResponseStatus(event: H3Event): number { @@ -52,13 +54,7 @@ export function setResponseStatus( const defer = typeof setImmediate !== "undefined" ? setImmediate : (fn: () => any) => fn(); -export function send(event: H3Event, data?: any, type?: string) { - if (type) { - defaultContentType(event, type); - } - if (event.request) { - return sendResponseWithInternal(event, new Response(data)); - } +function endNode(event: H3Event, data?: any) { return new Promise((resolve) => { defer(() => { event.node.res.end(data); @@ -67,20 +63,64 @@ export function send(event: H3Event, data?: any, type?: string) { }); } -export function sendResponseWithInternal(event: H3Event, response: Response) { - const mergedHeaders = new Map(); - for (const [key, value] of response.headers.entries()) { - mergedHeaders.set(key, value); +export function send(event: H3Event, data?: any, type?: string) { + if (type) { + defaultContentType(event, type); } - for (const [key, value] of Object.entries(getResponseHeaders(event))) { - mergedHeaders.set(key, value); + if (event.request) { + return sendResponse(event, new Response(data)); } - const headers = Object.fromEntries(mergedHeaders); - return new Response(response.body, { - ...response, - status: getResponseStatus(event) || response.status, - headers, - }); + return endNode(event, data); +} + +export class RawResponse extends Response {} +/** + * Ignores all previously called setter methods, and send a raw Response. + * @param event + * @param response + * @returns + */ +export async function sendResponseRaw(event: H3Event, response: Response) { + if (event.request) { + return new RawResponse(response.body, { ...response }); + } + removeResponseHeaders(event); + setHeaders(event, Object.fromEntries(response.headers.entries())); + setResponseStatus(event, response.status, response.statusText); + if (response.body) { + // This handles response streaming. + for await (const chunk of response.body as unknown as AsyncIterable) { + event.node.res.write(chunk); + } + } + return endNode(event); +} +/** + * Returns a response and respect the setters called before. + * @param event + * @param response + * @returns + */ +export function sendResponse(event: H3Event, response: Response) { + const status = + getResponseStatus(event) !== 200 // 200 is the default. + ? getResponseStatus(event) + : response.status; + const statusText = getResponseStatusText(event) || response.statusText; + const storedHeaders = getResponseHeaders(event); + const mergedHeaders = { + ...Object.fromEntries(response.headers.entries()), + ...storedHeaders, + }; + return sendResponseRaw( + event, + new Response(response.body, { + ...response, + status, + statusText, + headers: mergedHeaders as HeadersInit, + }) + ); } /** From 776fd2b6752eb8feddbe11608ff70b42ebb7f06e Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Jun 2023 20:55:54 +0700 Subject: [PATCH 09/14] test: add response and raw response --- test/app.test.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++++ test/utils.test.ts | 20 ++++++++++++ 2 files changed, 101 insertions(+) diff --git a/test/app.test.ts b/test/app.test.ts index ed912b5c..41dfc582 100644 --- a/test/app.test.ts +++ b/test/app.test.ts @@ -10,6 +10,8 @@ import { getUrlPath, setHeader, send, + RawResponse, + setResponseStatus, } from "../src"; describe("app", () => { @@ -258,4 +260,83 @@ describe("app", () => { const res = await request.get("/"); expect(res.body).toEqual({ works: 1 }); }); + + it("can return a Response instance", async () => { + app.use( + "/test/", + eventHandler(() => { + return new Response("valid", { + status: 207, + statusText: "hello-status", + headers: { hello: "world" }, + }); + }) + ); + + const res = await request.get("/test"); + expect(res.text).toBe("valid"); + expect(res.status).toBe(207); + // @ts-expect-error the type is wrong, it exists + expect(res.res.statusMessage).toBe("hello-status"); + expect(res.header.hello).toBe("world"); + }); + + it("can ignore setters with a Raw Response instance", async () => { + app.use( + "/test/", + eventHandler((event) => { + setHeader(event, "hello", "yes"); + setResponseStatus(event, 208, "bye-status"); + return new RawResponse("valid", { + status: 207, + statusText: "hello-status", + headers: { hello: "world" }, + }); + }) + ); + + const res = await request.get("/test"); + expect(res.text).toBe("valid"); + expect(res.status).toBe(207); + // @ts-expect-error the type is wrong, it exists + expect(res.res.statusMessage).toBe("hello-status"); + expect(res.header.hello).toBe("world"); + }); + + it("can use setters to overwrite Response instance", async () => { + app.use( + "/test/", + eventHandler((event) => { + setHeader(event, "hello", "yes"); + setResponseStatus(event, 208, "hello-status"); + return new Response("valid", { + status: 207, + statusText: "bye-status", + headers: { hello: "no" }, + }); + }) + ); + + const res = await request.get("/test"); + expect(res.text).toBe("valid"); + expect(res.status).toBe(208); + // @ts-expect-error the type is wrong, it exists + expect(res.res.statusMessage).toBe("hello-status"); + expect(res.header.hello).toBe("yes"); + }); + + it("can use `TransformStream` to stream a response", async () => { + app.use( + "/test/", + eventHandler(() => { + const response = new Response(`Hello world !`); + const { readable, writable } = new TransformStream(); + response.body?.pipeTo(writable); + return new Response(readable, response); + }) + ); + const res = await request.get("/test"); + expect(res.text).toBe("Hello world !"); + expect(res.status).toBe(200); + }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts index df210cd0..c810b3b3 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -12,6 +12,8 @@ import { getQuery, getRequestURL, getUrlPath, + sendResponse, + sendResponseRaw, } from "../src"; describe("", () => { @@ -23,6 +25,24 @@ describe("", () => { request = supertest(toNodeListener(app)); }); + describe("sendResponse", () => { + it("can send a Response", async () => { + app.use( + eventHandler((event) => sendResponse(event, new Response("Response"))) + ); + const result = await request.get("/"); + expect(result.text).toBe("Response"); + }); + + it("can send a RawResponse", async () => { + app.use( + eventHandler((event) => sendResponseRaw(event, new Response("Raw"))) + ); + const result = await request.get("/"); + expect(result.text).toBe("Raw"); + }); + }); + describe("sendRedirect", () => { it("can redirect URLs", async () => { app.use( From a7809f337aa819c362cf563c0f60dbb14ec69ae7 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Thu, 8 Jun 2023 22:19:29 +0700 Subject: [PATCH 10/14] feat: support response.redirect --- src/router.ts | 3 +-- src/utils/cache.ts | 6 +----- src/utils/response.ts | 4 ++++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/router.ts b/src/router.ts index 9e5dba7c..c0efce4e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -101,12 +101,11 @@ export function createRouter(opts: CreateRouterOptions = {}): Router { const method = getMethod(event).toLowerCase() as RouterMethod; const handler = matched.handlers[method] || matched.handlers.all; if (!handler) { - const error = createError({ + throw createError({ statusCode: 405, name: "Method Not Allowed", statusMessage: `Method ${method} is not allowed on this route.`, }); - throw error; } // Add params diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 2064199d..87c86e33 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -50,11 +50,7 @@ export function handleCacheHeaders( if (cacheMatched) { setResponseStatus(event, 304); - if (!event.request) { - sendResponse(event, new Response()); - return true; - } - event.node.res.end(); + sendResponse(event, new Response()); return true; } diff --git a/src/utils/response.ts b/src/utils/response.ts index 88b0b7e3..511daeec 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -10,6 +10,7 @@ import { getResponseHeader, removeResponseHeaders, setHeaders, + setHeader, } from "./headers"; export function getResponseStatus(event: H3Event): number { @@ -87,6 +88,9 @@ export async function sendResponseRaw(event: H3Event, response: Response) { removeResponseHeaders(event); setHeaders(event, Object.fromEntries(response.headers.entries())); setResponseStatus(event, response.status, response.statusText); + if (response.redirected) { + setHeader(event, "location", response.url); + } if (response.body) { // This handles response streaming. for await (const chunk of response.body as unknown as AsyncIterable) { From 826ea81afa166203cd640d02c87b12b2c1740b11 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Mon, 12 Jun 2023 21:15:21 +0700 Subject: [PATCH 11/14] chore: re-order methods --- src/utils/headers.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/utils/headers.ts b/src/utils/headers.ts index a84b04fa..6977f0e1 100644 --- a/src/utils/headers.ts +++ b/src/utils/headers.ts @@ -47,6 +47,15 @@ export function getRequestRawHeader(event: H3Event, name: string) { return event.node.req.headers[name]; } +export function getRequestHeader( + event: H3Event, + name: string +): RequestHeaders[string] { + const headers = getRequestHeaders(event); + const value = headers[name.toLowerCase()]; + return value; +} + export function getResponseHeaders( event: H3Event ): ReturnType { @@ -77,15 +86,6 @@ export function setResponseHeader( return event.node.res.setHeader(name, value); } -export function getRequestHeader( - event: H3Event, - name: string -): RequestHeaders[string] { - const headers = getRequestHeaders(event); - const value = headers[name.toLowerCase()]; - return value; -} - export function setResponseHeaders( event: H3Event, headers: Record[1]> From c95f6cdc1c9d1ab5a43e5031fcd817ea34f6d228 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Mon, 12 Jun 2023 22:29:27 +0700 Subject: [PATCH 12/14] feat: process string bodies more efficiently --- src/utils/response.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/utils/response.ts b/src/utils/response.ts index 511daeec..56530938 100644 --- a/src/utils/response.ts +++ b/src/utils/response.ts @@ -92,9 +92,17 @@ export async function sendResponseRaw(event: H3Event, response: Response) { setHeader(event, "location", response.url); } if (response.body) { - // This handles response streaming. - for await (const chunk of response.body as unknown as AsyncIterable) { - event.node.res.write(chunk); + const contentType = response.headers.get("Content-Type") || ""; + if (contentType.includes("text") || contentType.includes("json")) { + for await (const chunk of response.body as unknown as AsyncIterable) { + const stringChunk = new TextDecoder().decode(chunk); + event.node.res.write(stringChunk); + } + } else { + // for binary data like images, videos, etc. + for await (const chunk of response.body as unknown as AsyncIterable) { + event.node.res.write(chunk); + } } } return endNode(event); From 3ac5cf51f757f85932b4c33c580e74d2e40cb639 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Fri, 16 Jun 2023 15:30:43 +0700 Subject: [PATCH 13/14] feat: add adapterFetch --- src/adapters.ts | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/adapters.ts b/src/adapters.ts index c3898d72..0d46c521 100644 --- a/src/adapters.ts +++ b/src/adapters.ts @@ -1,17 +1,39 @@ import { App, createEvent } from "./"; +/** + * Adapter that helps return a Response from a Request. + * It can be used with something like Nitro. + * const localResponse = adapterFetch(h3app) + * const response = localResponse(request: Request, {context: { cf: request.cf, cloudflare: { request, env, context } }}) + * @param app + * @returns Promise + */ +export const adapterFetch = (app: App) => { + return async (request: Request, context?: Record) => { + try { + const event = createEvent(undefined, undefined, request); + if (context) { + event.context = context; + } + return (await app.handler(event)) as Response; + } catch (error: any) { + return new Response(error.toString(), { + status: Number.parseInt(error.statusCode || error.code) || 500, + statusText: error.statusText, + }); + } + }; +}; + export const adapterCloudflareWorker = (app: App) => { const worker = { async fetch(request: Request, env?: any, context?: any) { - try { - const event = createEvent(undefined, undefined, request); - event.context = { cloudflare: { env, context } }; - const response = (await app.handler(event)) as Response; - return response; - } catch (error) { - console.error(error); - throw error; - } + const fetchResponse = adapterFetch(app); + return await fetchResponse(request, { + // @ts-expect-error + cf: request.cf, + cloudflare: { env, context }, + }); }, fire: () => { console.log("implement service worker syntax ..."); From 85ddc2905ca29bf850e90cbef790a3d9c75326e4 Mon Sep 17 00:00:00 2001 From: Hebilicious Date: Fri, 16 Jun 2023 16:58:10 +0700 Subject: [PATCH 14/14] chore: clean-up test --- test/cloudflare.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/cloudflare.test.ts b/test/cloudflare.test.ts index ad31180d..eda2b01b 100644 --- a/test/cloudflare.test.ts +++ b/test/cloudflare.test.ts @@ -19,19 +19,3 @@ test("Can use the router", async () => { const response = await worker.fetch(request, env, ctx); expect(await response.text()).toBe(`Routed there`); }); - -// Miniflare tests -// const mf = new Miniflare({ -// script: "./h3-worker.ts", -// }); -// const baseUrl = "http://localhost"; - -// test("responds with url", async () => { -// const response = await mf.dispatchFetch(baseUrl); -// expect(await response.text()).toBe(`Hello world ! ${baseUrl}/`); -// }); - -// test("Can use the router", async () => { -// const response = await mf.dispatchFetch(`${baseUrl}/here`); -// expect(await response.text()).toBe(`Routed there`); -// });