From 0b7e8a3eda591645291752fdc86c136c068cb9a2 Mon Sep 17 00:00:00 2001 From: Thibaut LEBRETON Date: Tue, 30 Jul 2024 12:58:38 +0200 Subject: [PATCH] feat: h3 --- packages/vike-node/README.md | 33 ++++++ packages/vike-node/package.json | 5 + packages/vike-node/src/h3.ts | 1 + .../vike-node/src/runtime/frameworks/h3.ts | 45 ++++++++ pnpm-lock.yaml | 108 ++++++++++++++++++ test/vike-node/.dev-h3.test.ts | 4 + test/vike-node/.prod-h3.test.ts | 4 + test/vike-node/package.json | 1 + test/vike-node/server/index-h3.ts | 53 +++++++++ 9 files changed, 254 insertions(+) create mode 100644 packages/vike-node/src/h3.ts create mode 100644 packages/vike-node/src/runtime/frameworks/h3.ts create mode 100644 test/vike-node/.dev-h3.test.ts create mode 100644 test/vike-node/.prod-h3.test.ts create mode 100644 test/vike-node/server/index-h3.ts diff --git a/packages/vike-node/README.md b/packages/vike-node/README.md index ae433a1..bdf7222 100644 --- a/packages/vike-node/README.md +++ b/packages/vike-node/README.md @@ -149,6 +149,7 @@ app.use( - Express - Fastify - Hono +- H3 - Elysia (Bun) Express: @@ -212,6 +213,38 @@ function startServer() { } ``` +H3: + +```js +// server/index.js + +import vike from 'vike-node/h3' +import { + createApp, + createRouter, + eventHandler, + toNodeListener, + toWebRequest, +} from "h3" + +startServer() + +function startServer() { + const app = createApp(); + const port = +(process.env.PORT || 3000) + const router = createRouter(); + router.use("/**", eventHandler(vike())); + + app.use(router); + + const server = createServer(toNodeListener(app)).listen(port); + + server.on("listening", () => { + console.log(`Server running at http://localhost:${port}`); + }); +} +``` + Elysia (Bun): ```js diff --git a/packages/vike-node/package.json b/packages/vike-node/package.json index 962fd75..be5aaba 100644 --- a/packages/vike-node/package.json +++ b/packages/vike-node/package.json @@ -5,6 +5,7 @@ "exports": { "./connect": "./dist/connect.js", "./fastify": "./dist/fastify.js", + "./h3": "./dist/h3.js", "./hono": "./dist/hono.js", "./elysia": "./dist/elysia.js", "./plugin": "./dist/plugin/index.js" @@ -31,6 +32,7 @@ "@brillout/release-me": "^0.4.0", "@types/node": "^20.14.12", "fastify": "^4.28.1", + "h3": "^1.12.0", "hono": "^4.5.1", "elysia": "^1.1.4", "typescript": "^5.5.4", @@ -45,6 +47,9 @@ "fastify": [ "./dist/fastify.d.ts" ], + "h3": [ + "./dist/h3.d.ts" + ], "hono": [ "./dist/hono.d.ts" ], diff --git a/packages/vike-node/src/h3.ts b/packages/vike-node/src/h3.ts new file mode 100644 index 0000000..3223773 --- /dev/null +++ b/packages/vike-node/src/h3.ts @@ -0,0 +1 @@ +export { vike, vike as default } from './runtime/frameworks/h3.js' \ No newline at end of file diff --git a/packages/vike-node/src/runtime/frameworks/h3.ts b/packages/vike-node/src/runtime/frameworks/h3.ts new file mode 100644 index 0000000..c16c3e6 --- /dev/null +++ b/packages/vike-node/src/runtime/frameworks/h3.ts @@ -0,0 +1,45 @@ +export { vike } + +import { EventHandlerRequest, H3Event, toWebRequest } from 'h3' +import { connectToWeb } from '../adapters/connectToWeb.js' +import { globalStore } from '../globalStore.js' +import { createHandler } from '../handler.js' +import type { VikeOptions } from '../types.js' + +/** + * Creates a H3 middleware to handle Vike requests and HMR (Hot Module Replacement). + * + * @param {VikeOptions} [options] - Configuration options for Vike. + * + * @returns {MiddlewareHandler} A H3 middleware function that processes requests with Vike. + * + * @description + * This function creates a H3 middleware that integrates Vike's server-side rendering capabilities + * and handles Hot Module Replacement (HMR) for development environments. The middleware: + * + * 1. Checks for and handles HMR WebSocket upgrade requests. + * 2. Processes regular requests using Vike's handler. + * 3. Adapts Node.js-style request handling to work with Web standard Response objects. + */ +function vike(options?: VikeOptions) { + const handler = createHandler(options); + + return async function middleware(event: H3Event) { + const request = toWebRequest(event); + + globalStore.setupHMRProxy(event.node.req); + + const response = await connectToWeb((req, res, next) => + handler({ + req, + res, + next, + platformRequest: request + }) + )(request) + + if (response) { + return response + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9215c92..6d9109c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: fastify: specifier: ^4.28.1 version: 4.28.1 + h3: + specifier: ^1.12.0 + version: 1.12.0 hono: specifier: ^4.5.1 version: 4.5.1 @@ -138,6 +141,9 @@ importers: fastify: specifier: ^4.28.1 version: 4.28.1 + h3: + specifier: ^1.12.0 + version: 1.12.0 hono: specifier: ^4.5.1 version: 4.5.1 @@ -1410,6 +1416,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -1486,6 +1496,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -1505,6 +1518,14 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crossws@0.2.4: + resolution: {integrity: sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==} + peerDependencies: + uWebSockets.js: '*' + peerDependenciesMeta: + uWebSockets.js: + optional: true + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1537,6 +1558,9 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -1544,6 +1568,9 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1780,6 +1807,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + h3@1.12.0: + resolution: {integrity: sha512-Zi/CcNeWBXDrFNlV0hUBJQR9F7a96RjMeAZweW/ZWkR9fuXrMcvKnSA63f/zZ9l0GgQOZDVHGvXivNN9PWOwhA==} + handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} @@ -1845,6 +1875,9 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1994,6 +2027,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2058,6 +2096,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2099,6 +2140,9 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} + ohash@1.1.3: + resolution: {integrity: sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -2152,6 +2196,9 @@ packages: path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} @@ -2236,6 +2283,9 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2541,14 +2591,23 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + uglify-js@3.19.0: resolution: {integrity: sha512-wNKHUY2hYYkf6oSFfhwwiHo4WCHzHmzcXsqXYTN9ja3iApYIFbb2U6ics9hBcYLHcYGQoAlwnZlTrf3oF+BL/Q==} engines: {node: '>=0.8.0'} hasBin: true + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + unenv@1.10.0: + resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -3709,6 +3768,8 @@ snapshots: concat-map@0.0.1: {} + consola@3.2.3: {} + console-control-strings@1.1.0: {} content-disposition@0.5.4: @@ -3790,6 +3851,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@1.2.2: {} + cookie-signature@1.0.6: {} cookie@0.6.0: {} @@ -3806,6 +3869,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.2.4: {} + csstype@3.1.3: {} dargs@8.1.0: {} @@ -3828,10 +3893,14 @@ snapshots: es-errors: 1.3.0 gopd: 1.0.1 + defu@6.1.4: {} + delegates@1.0.0: {} depd@2.0.0: {} + destr@2.0.3: {} + destroy@1.2.0: {} detect-libc@2.0.3: {} @@ -4175,6 +4244,21 @@ snapshots: graceful-fs@4.2.11: {} + h3@1.12.0: + dependencies: + cookie-es: 1.2.2 + crossws: 0.2.4 + defu: 6.1.4 + destr: 2.0.3 + iron-webcrypto: 1.2.1 + ohash: 1.1.3 + radix3: 1.1.2 + ufo: 1.5.4 + uncrypto: 0.1.3 + unenv: 1.10.0 + transitivePeerDependencies: + - uWebSockets.js + handlebars@4.7.8: dependencies: minimist: 1.2.8 @@ -4238,6 +4322,8 @@ snapshots: ipaddr.js@1.9.1: {} + iron-webcrypto@1.2.1: {} + is-arrayish@0.2.1: {} is-arrayish@0.3.2: {} @@ -4343,6 +4429,8 @@ snapshots: mime@1.6.0: {} + mime@3.0.0: {} + mimic-fn@2.1.0: {} minimatch@3.1.2: @@ -4389,6 +4477,8 @@ snapshots: neo-async@2.6.2: {} + node-fetch-native@1.6.4: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -4422,6 +4512,8 @@ snapshots: object-inspect@1.13.2: {} + ohash@1.1.3: {} + on-exit-leak-free@2.1.2: {} on-finished@2.4.1: @@ -4466,6 +4558,8 @@ snapshots: path-to-regexp@0.1.7: {} + pathe@1.1.2: {} + pathval@1.1.1: {} picocolors@1.0.1: {} @@ -4544,6 +4638,8 @@ snapshots: quick-format-unescaped@4.0.4: {} + radix3@1.1.2: {} + range-parser@1.2.1: {} raw-body@2.5.2: @@ -4893,11 +4989,23 @@ snapshots: typescript@5.5.4: {} + ufo@1.5.4: {} + uglify-js@3.19.0: optional: true + uncrypto@0.1.3: {} + undici-types@5.26.5: {} + unenv@1.10.0: + dependencies: + consola: 3.2.3 + defu: 6.1.4 + mime: 3.0.0 + node-fetch-native: 1.6.4 + pathe: 1.1.2 + unpipe@1.0.0: {} update-browserslist-db@1.1.0(browserslist@4.23.2): diff --git a/test/vike-node/.dev-h3.test.ts b/test/vike-node/.dev-h3.test.ts new file mode 100644 index 0000000..fbf210f --- /dev/null +++ b/test/vike-node/.dev-h3.test.ts @@ -0,0 +1,4 @@ +process.env.VIKE_NODE_FRAMEWORK = 'h3' + +import { testRun } from './.testRun' +testRun('npm run dev') diff --git a/test/vike-node/.prod-h3.test.ts b/test/vike-node/.prod-h3.test.ts new file mode 100644 index 0000000..ab2e95d --- /dev/null +++ b/test/vike-node/.prod-h3.test.ts @@ -0,0 +1,4 @@ +process.env.VIKE_NODE_FRAMEWORK = 'h3' + +import { testRun } from './.testRun' +testRun('npm run prod') diff --git a/test/vike-node/package.json b/test/vike-node/package.json index 0eacf58..3d6abee 100644 --- a/test/vike-node/package.json +++ b/test/vike-node/package.json @@ -17,6 +17,7 @@ "cross-env": "^7.0.3", "express": "^4.19.2", "fastify": "^4.28.1", + "h3": "^1.12.0", "hono": "^4.5.1", "elysia": "^1.1.4", "prisma": "^5.17.0", diff --git a/test/vike-node/server/index-h3.ts b/test/vike-node/server/index-h3.ts new file mode 100644 index 0000000..6dea0a5 --- /dev/null +++ b/test/vike-node/server/index-h3.ts @@ -0,0 +1,53 @@ +import { createServer } from 'http' +import { init } from '../database/todoItems' +import { telefunc } from 'telefunc' +import vike from 'vike-node/h3' +import { + createApp, + createRouter, + eventHandler, + toNodeListener, + toWebRequest, +} from "h3" + +startServer(); + +async function startServer() { + await init() + const app = createApp(); + const port = process.env.PORT || 3000; + + const router = createRouter(); + + router.post("/_telefunc", eventHandler(async (event) => { + const request = toWebRequest(event); + + const httpResponse = await telefunc({ + url: request.url.toString(), + method: request.method, + body: await request.text(), + context: event.context + }); + const { body, statusCode, contentType } = httpResponse; + return new Response(body, { + status: statusCode, + headers: { + "content-type": contentType, + }, + }); + })); + + app.use(eventHandler((event) => { + event.node.res.setHeader('x-test', 'test'); + })) + + router.use("/**", eventHandler(vike())); + + app.use(router); + + const server = createServer(toNodeListener(app)).listen(port); + + server.on("listening", () => { + console.log(`Server running at http://localhost:${port}`); + }); +}