diff --git a/examples/hono-react-vercel-edge/.gitignore b/examples/hono-react-vercel-edge/.gitignore
new file mode 100644
index 0000000..b0a5c34
--- /dev/null
+++ b/examples/hono-react-vercel-edge/.gitignore
@@ -0,0 +1,2 @@
+/node_modules/
+/dist/
diff --git a/examples/hono-react-vercel-edge/api/index.js b/examples/hono-react-vercel-edge/api/index.js
new file mode 100644
index 0000000..e6cbb87
--- /dev/null
+++ b/examples/hono-react-vercel-edge/api/index.js
@@ -0,0 +1,8 @@
+export const runtime = 'edge'
+
+// Import the built server entry from dist, so import.meta.env and other Vite features
+// are available in the server entry (Vite already processed this file)
+import fetch from '../dist/server/index.mjs'
+
+export const GET = fetch
+export const POST = fetch
diff --git a/examples/hono-react-vercel-edge/package.json b/examples/hono-react-vercel-edge/package.json
new file mode 100644
index 0000000..d7aeb16
--- /dev/null
+++ b/examples/hono-react-vercel-edge/package.json
@@ -0,0 +1,20 @@
+{
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "prod": "cross-env NODE_ENV=production node dist/server/index.mjs"
+ },
+ "dependencies": {
+ "@vitejs/plugin-react": "^4.3.1",
+ "hono": "^4.5.5",
+ "@hono/node-server": "^1.12.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "vike": "^0.4.181",
+ "vike-node": "^0.1.9",
+ "vike-react": "^0.4.18",
+ "vite": "^5.3.5",
+ "cross-env": "^7.0.3"
+ },
+ "type": "module"
+}
diff --git a/examples/hono-react-vercel-edge/pages/+Layout.jsx b/examples/hono-react-vercel-edge/pages/+Layout.jsx
new file mode 100644
index 0000000..3f5e20e
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/+Layout.jsx
@@ -0,0 +1,70 @@
+export { Layout }
+
+import React from 'react'
+import './Layout.css'
+
+function Layout({ children }) {
+ return (
+
+
+
+ Pre-rendered
+
+
+ Dynamic
+
+
+ Static
+
+
+ {children}
+
+ )
+}
+
+function PageLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function Sidebar({ children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+function Content({ children }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/examples/hono-react-vercel-edge/pages/+config.js b/examples/hono-react-vercel-edge/pages/+config.js
new file mode 100644
index 0000000..50244c7
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/+config.js
@@ -0,0 +1,9 @@
+export { config }
+
+import vikeReact from 'vike-react/config'
+
+const config = {
+ // https://vike.dev/extends
+ extends: vikeReact,
+ prerender: false
+}
diff --git a/examples/hono-react-vercel-edge/pages/Layout.css b/examples/hono-react-vercel-edge/pages/Layout.css
new file mode 100644
index 0000000..8c53088
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/Layout.css
@@ -0,0 +1,14 @@
+body {
+ margin: 0;
+ font-family: sans-serif;
+}
+* {
+ box-sizing: border-box;
+}
+a {
+ text-decoration: none;
+}
+
+.navitem {
+ padding: 3px;
+}
diff --git a/examples/hono-react-vercel-edge/pages/dynamic/+Page.jsx b/examples/hono-react-vercel-edge/pages/dynamic/+Page.jsx
new file mode 100644
index 0000000..7f8d9f7
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/dynamic/+Page.jsx
@@ -0,0 +1,27 @@
+export default Page
+
+import React, { useState } from 'react'
+
+function Page() {
+ return (
+ <>
+ Welcome
+ This page is:
+
+ - Dynamic
+ - No static html generated
+ - Interactive
+
+
+ >
+ )
+}
+
+function Counter() {
+ const [count, setCount] = useState(0)
+ return (
+
+ )
+}
diff --git a/examples/hono-react-vercel-edge/pages/index/+Page.jsx b/examples/hono-react-vercel-edge/pages/index/+Page.jsx
new file mode 100644
index 0000000..40682a7
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/index/+Page.jsx
@@ -0,0 +1,27 @@
+export default Page
+
+import React, { useState } from 'react'
+
+function Page() {
+ return (
+ <>
+ Welcome
+ This page is:
+
+ - Pre-rendered
+ - Static html generated
+ - Interactive
+
+
+ >
+ )
+}
+
+function Counter() {
+ const [count, setCount] = useState(0)
+ return (
+
+ )
+}
diff --git a/examples/hono-react-vercel-edge/pages/index/+config.js b/examples/hono-react-vercel-edge/pages/index/+config.js
new file mode 100644
index 0000000..63de0fa
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/index/+config.js
@@ -0,0 +1,3 @@
+export default {
+ prerender: true
+}
diff --git a/examples/hono-react-vercel-edge/pages/static/+Page.jsx b/examples/hono-react-vercel-edge/pages/static/+Page.jsx
new file mode 100644
index 0000000..bf0a2e8
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/static/+Page.jsx
@@ -0,0 +1,27 @@
+export default Page
+
+import React, { useState } from 'react'
+
+function Page() {
+ return (
+ <>
+ Welcome
+ This page is:
+
+ - Pre-rendered
+ - Static html generated
+ - Not interactive (no javascript is downloaded for this page)
+
+
+ >
+ )
+}
+
+function Counter() {
+ const [count, setCount] = useState(0)
+ return (
+
+ )
+}
diff --git a/examples/hono-react-vercel-edge/pages/static/+config.js b/examples/hono-react-vercel-edge/pages/static/+config.js
new file mode 100644
index 0000000..a81d760
--- /dev/null
+++ b/examples/hono-react-vercel-edge/pages/static/+config.js
@@ -0,0 +1,10 @@
+// https://vike.dev/render-modes#html-only
+
+export default {
+ prerender: true,
+ meta: {
+ Page: {
+ env: { server: true, client: false }
+ }
+ }
+}
diff --git a/examples/hono-react-vercel-edge/readme.md b/examples/hono-react-vercel-edge/readme.md
new file mode 100644
index 0000000..e25deaf
--- /dev/null
+++ b/examples/hono-react-vercel-edge/readme.md
@@ -0,0 +1,14 @@
+Minimal example of using `vike-node` and `vike-react`.
+
+```bash
+git clone git@github.com:vikejs/vike-node
+cd vike-node/examples/hono-react-vercel-edge/
+npm install
+npm run dev
+```
+
+## One-Click Deploy
+
+Deploy the example using [Vercel](https://vercel.com):
+
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vikejs/vike-node/tree/main/examples/hono-react-vercel-edge&project-name=hono-react&repository-name=hono-react)
diff --git a/examples/hono-react-vercel-edge/server/index.js b/examples/hono-react-vercel-edge/server/index.js
new file mode 100644
index 0000000..4720d2d
--- /dev/null
+++ b/examples/hono-react-vercel-edge/server/index.js
@@ -0,0 +1,21 @@
+import { serve } from '@hono/node-server'
+import { Hono } from 'hono'
+import vike from 'vike-node/hono'
+
+export default startServer()
+
+function startServer() {
+ const app = new Hono()
+ app.use(vike())
+ const port = process.env.PORT || 3000
+ serve(
+ {
+ fetch: app.fetch,
+ port: +port,
+ // Needed for Bun
+ overrideGlobalObjects: false
+ },
+ () => console.log(`Server running at http://localhost:${port}`)
+ )
+ return app.fetch
+}
diff --git a/examples/hono-react-vercel-edge/vercel.json b/examples/hono-react-vercel-edge/vercel.json
new file mode 100644
index 0000000..5ddf271
--- /dev/null
+++ b/examples/hono-react-vercel-edge/vercel.json
@@ -0,0 +1,9 @@
+{
+ "outputDirectory": "dist/client",
+ "rewrites": [
+ {
+ "source": "/((?!assets/).*)",
+ "destination": "/api"
+ }
+ ]
+}
diff --git a/examples/hono-react-vercel-edge/vite.config.js b/examples/hono-react-vercel-edge/vite.config.js
new file mode 100644
index 0000000..04c0f28
--- /dev/null
+++ b/examples/hono-react-vercel-edge/vite.config.js
@@ -0,0 +1,7 @@
+import react from '@vitejs/plugin-react'
+import vike from 'vike/plugin'
+import vikeNode from 'vike-node/plugin'
+
+export default {
+ plugins: [react(), vike({ prerender: true }), vikeNode('server/index.js')]
+}
diff --git a/packages/vike-node/src/runtime/adapters/connectToWeb.ts b/packages/vike-node/src/runtime/adapters/connectToWeb.ts
index 3e1bb7b..d3d7de3 100644
--- a/packages/vike-node/src/runtime/adapters/connectToWeb.ts
+++ b/packages/vike-node/src/runtime/adapters/connectToWeb.ts
@@ -2,12 +2,11 @@ export { connectToWeb }
import type { IncomingMessage } from 'node:http'
import { Readable } from 'node:stream'
-import type { ConnectMiddleware } from '../types.js'
+import type { ConnectMiddleware, WebHandler } from '../types.js'
import { flattenHeaders } from '../utils/header-utils.js'
import { createServerResponse } from './createServerResponse.js'
/** Type definition for a web-compatible request handler */
-type WebHandler = (request: Request) => Response | undefined | Promise
const statusCodesWithoutBody = [
100, // Continue
diff --git a/packages/vike-node/src/runtime/frameworks/elysia.ts b/packages/vike-node/src/runtime/frameworks/elysia.ts
index b9878b4..a72e97b 100644
--- a/packages/vike-node/src/runtime/frameworks/elysia.ts
+++ b/packages/vike-node/src/runtime/frameworks/elysia.ts
@@ -1,14 +1,13 @@
export { vike }
-import { Elysia, NotFoundError } from 'elysia'
-import { connectToWeb } from '../adapters/connectToWeb.js'
-import { createHandler } from '../handler.js'
+import { Context, Elysia, NotFoundError } from 'elysia'
+import { createHandler } from '../handler-web.js'
import type { VikeOptions } from '../types.js'
/**
* Creates an Elysia plugin to handle Vike requests.
*
- * @param {VikeOptions} [options] - Configuration options for Vike.
+ * @param {VikeOptions} [options] - Configuration options for Vike.
*
* @returns {Elysia} An Elysia plugin that handles all GET requests and processes them with Vike.
*
@@ -29,19 +28,12 @@ import type { VikeOptions } from '../types.js'
*
* @throws {NotFoundError} Thrown when Vike doesn't handle the request, allowing Elysia to manage 404 responses.
*/
-function vike(options?: VikeOptions): Elysia {
+function vike(options?: VikeOptions): Elysia {
const handler = createHandler(options)
return new Elysia({
name: 'vike-node:elysia'
}).get('*', async (ctx) => {
- const response = await connectToWeb((req, res, next) =>
- handler({
- req,
- res,
- next,
- platformRequest: ctx.request
- })
- )(ctx.request)
+ const response = await handler({ request: ctx.request, platformRequest: ctx })
if (response) {
return response
diff --git a/packages/vike-node/src/runtime/frameworks/hono.ts b/packages/vike-node/src/runtime/frameworks/hono.ts
index cdbb1d6..3633544 100644
--- a/packages/vike-node/src/runtime/frameworks/hono.ts
+++ b/packages/vike-node/src/runtime/frameworks/hono.ts
@@ -1,10 +1,9 @@
export { vike }
-import type { HonoRequest, MiddlewareHandler } from 'hono'
-import type { IncomingMessage, ServerResponse } from 'http'
-import { connectToWeb } from '../adapters/connectToWeb.js'
+import type { Context, MiddlewareHandler } from 'hono'
+import type { IncomingMessage } from 'http'
import { globalStore } from '../globalStore.js'
-import { createHandler } from '../handler.js'
+import { createHandler } from '../handler-web.js'
import type { VikeOptions } from '../types.js'
/**
@@ -33,19 +32,18 @@ import type { VikeOptions } from '../types.js'
* ```
*
*/
-function vike(options?: VikeOptions): MiddlewareHandler {
+function vike(options?: VikeOptions): MiddlewareHandler {
const handler = createHandler(options)
return async function middleware(ctx, next) {
- const req = ctx.env.incoming as IncomingMessage
- globalStore.setupHMRProxy(req)
- const response = await connectToWeb((req, res, next) =>
- handler({
- req,
- res,
- next,
- platformRequest: ctx.req
- })
- )(ctx.req.raw)
+ if (ctx.env.incoming) {
+ const req = ctx.env.incoming as IncomingMessage
+ globalStore.setupHMRProxy(req)
+ }
+
+ const response = await handler({
+ request: ctx.req.raw,
+ platformRequest: ctx
+ })
if (response) {
return response
diff --git a/packages/vike-node/src/runtime/handler-web.ts b/packages/vike-node/src/runtime/handler-web.ts
new file mode 100644
index 0000000..5667dbb
--- /dev/null
+++ b/packages/vike-node/src/runtime/handler-web.ts
@@ -0,0 +1,34 @@
+import { assert } from '../utils/assert.js'
+import { isNodeLike } from '../utils/isNodeLike.js'
+import { globalStore } from './globalStore.js'
+import { VikeOptions, WebHandler } from './types.js'
+import { renderPage } from './vike-handler.js'
+
+export function createHandler(options: VikeOptions = {}) {
+ let nodeLike = undefined
+ let nodeHandler: WebHandler | undefined = undefined
+
+ return async function handler({ request, platformRequest }: { request: Request; platformRequest: PlatformRequest }) {
+ if (request.method !== 'GET') {
+ return undefined
+ }
+ nodeLike ??= await isNodeLike()
+ if (nodeLike) {
+ if (!nodeHandler) {
+ const connectToWeb = (await import('./adapters/connectToWeb.js')).connectToWeb
+ const handler = (await import('./handler.js')).createHandler(options)
+ nodeHandler = connectToWeb((req, res, next) => handler!({ req, res, platformRequest, next }))
+ }
+ return nodeHandler(request)
+ }
+
+ const httpResponse = await renderPage({
+ request,
+ platformRequest,
+ options
+ })
+ if (!httpResponse) return undefined
+ const { statusCode, headers, getReadableWebStream } = httpResponse
+ return new Response(getReadableWebStream(), { status: statusCode, headers })
+ }
+}
diff --git a/packages/vike-node/src/runtime/handler.ts b/packages/vike-node/src/runtime/handler.ts
index de19160..9650f52 100644
--- a/packages/vike-node/src/runtime/handler.ts
+++ b/packages/vike-node/src/runtime/handler.ts
@@ -1,11 +1,12 @@
import type { IncomingMessage, ServerResponse } from 'http'
import { dirname, isAbsolute, join } from 'path'
import { fileURLToPath } from 'url'
-import { renderPage } from 'vike/server'
+
import { assert } from '../utils/assert.js'
import { globalStore } from './globalStore.js'
import type { ConnectMiddleware, VikeOptions } from './types.js'
import { writeHttpResponse } from './utils/writeHttpResponse.js'
+import { renderPage } from './vike-handler.js'
export function createHandler(options: VikeOptions = {}) {
const staticConfig = resolveStaticConfig(options.static)
@@ -46,10 +47,17 @@ export function createHandler(options: VikeOptions(options: VikeOptions resolve(false))
})
}
-
- async function renderPageAndRespond(
- req: IncomingMessage,
- res: ServerResponse,
- platformRequest: PlatformRequest
- ): Promise {
- const pageContext = await renderPage({
- urlOriginal: req.url ?? '',
- headersOriginal: req.headers,
- ...(await getPageContext(platformRequest))
- })
-
- if (pageContext.errorWhileRendering) {
- options.onError?.(pageContext.errorWhileRendering)
- }
-
- if (!pageContext.httpResponse) {
- return false
- }
-
- await writeHttpResponse(pageContext.httpResponse, res)
- return true
- }
-
- function getPageContext(platformRequest: PlatformRequest) {
- return typeof options.pageContext === 'function' ? options.pageContext(platformRequest) : options.pageContext ?? {}
- }
}
function handleViteDevServer(req: IncomingMessage, res: ServerResponse): Promise {
diff --git a/packages/vike-node/src/runtime/types.ts b/packages/vike-node/src/runtime/types.ts
index f4f7425..957544c 100644
--- a/packages/vike-node/src/runtime/types.ts
+++ b/packages/vike-node/src/runtime/types.ts
@@ -13,3 +13,4 @@ export type ConnectMiddleware<
PlatformRequest extends IncomingMessage = IncomingMessage,
PlatformResponse extends ServerResponse = ServerResponse
> = (req: PlatformRequest, res: PlatformResponse, next: NextFunction) => void
+export type WebHandler = (request: Request) => Response | undefined | Promise
diff --git a/packages/vike-node/src/runtime/vike-handler.ts b/packages/vike-node/src/runtime/vike-handler.ts
new file mode 100644
index 0000000..b640e78
--- /dev/null
+++ b/packages/vike-node/src/runtime/vike-handler.ts
@@ -0,0 +1,34 @@
+export { renderPage }
+
+import { renderPage as _renderPage } from 'vike/server'
+import type { VikeHttpResponse, VikeOptions } from './types.js'
+
+async function renderPage({
+ request,
+ platformRequest,
+ options
+}: {
+ request: { url?: string; headers: Record }
+ platformRequest: PlatformRequest
+ options: VikeOptions
+}): Promise {
+ function getPageContext(platformRequest: PlatformRequest): Record {
+ return typeof options.pageContext === 'function' ? options.pageContext(platformRequest) : options.pageContext ?? {}
+ }
+
+ const pageContext = await _renderPage({
+ urlOriginal: request.url ?? '',
+ headersOriginal: request.headers,
+ ...(await getPageContext(platformRequest))
+ })
+
+ if (pageContext.errorWhileRendering) {
+ options.onError?.(pageContext.errorWhileRendering)
+ }
+
+ if (!pageContext.httpResponse) {
+ return null
+ }
+
+ return pageContext.httpResponse
+}
diff --git a/packages/vike-node/src/utils/isNodeLike.ts b/packages/vike-node/src/utils/isNodeLike.ts
new file mode 100644
index 0000000..b5d916b
--- /dev/null
+++ b/packages/vike-node/src/utils/isNodeLike.ts
@@ -0,0 +1,7 @@
+export async function isNodeLike() {
+ try {
+ await import('node:http')
+ return true
+ } catch (error) {}
+ return false
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 745d169..62b85b6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -87,6 +87,39 @@ importers:
specifier: ^5.3.5
version: 5.3.5(@types/node@20.14.12)
+ examples/hono-react-vercel-edge:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^1.12.0
+ version: 1.12.0
+ '@vitejs/plugin-react':
+ specifier: ^4.3.1
+ version: 4.3.1(vite@5.3.5(@types/node@20.14.12))
+ cross-env:
+ specifier: ^7.0.3
+ version: 7.0.3
+ hono:
+ specifier: ^4.5.5
+ version: 4.5.5
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ vike:
+ specifier: ^0.4.181
+ version: 0.4.181(react-streaming@0.3.42)(vite@5.3.5(@types/node@20.14.12))
+ vike-node:
+ specifier: link:../../packages/vike-node
+ version: link:../../packages/vike-node
+ vike-react:
+ specifier: ^0.4.18
+ version: 0.4.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vike@0.4.181(react-streaming@0.3.42(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@5.3.5(@types/node@20.14.12)))(vite@5.3.5(@types/node@20.14.12))
+ vite:
+ specifier: ^5.3.5
+ version: 5.3.5(@types/node@20.14.12)
+
packages/vike-node:
dependencies:
'@brillout/picocolors':
@@ -1871,6 +1904,10 @@ packages:
resolution: {integrity: sha512-6q8AugoWG5wlrjdGG8OFFiqEsPlPGjODjUik48sEJeko4Tae1UsLS2vUiYHLEJx1gJvOZa4BWkQC+urwDmkEvQ==}
engines: {node: '>=16.0.0'}
+ hono@4.5.5:
+ resolution: {integrity: sha512-fXBXHqaVfimWofbelLXci8pZyIwBMkDIwCa4OwZvK+xVbEyYLELVP4DfbGaj1aEM6ZY3hHgs4qLvCO2ChkhgQw==}
+ engines: {node: '>=16.0.0'}
+
hosted-git-info@7.0.2:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -4316,6 +4353,8 @@ snapshots:
hono@4.5.1: {}
+ hono@4.5.5: {}
+
hosted-git-info@7.0.2:
dependencies:
lru-cache: 10.4.3
diff --git a/test/vike-node/vite.config.ts b/test/vike-node/vite.config.ts
index 955a02f..0c82062 100644
--- a/test/vike-node/vite.config.ts
+++ b/test/vike-node/vite.config.ts
@@ -3,7 +3,7 @@ import { telefunc } from 'telefunc/vite'
import vike from 'vike/plugin'
import vikeNode from 'vike-node/plugin'
-const FRAMEWORK = process.env.VIKE_NODE_FRAMEWORK || 'fastify'
+const FRAMEWORK = process.env.VIKE_NODE_FRAMEWORK || 'hono'
export default {
plugins: [