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: + + + + ) +} + +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: + + + + ) +} + +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: + + + + ) +} + +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: [