mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
✨ feat(server): mount Hono runtime via Next catch-all + dist-loading client
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { GET } from './route';
|
||||
|
||||
describe('Next Hono binding route', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('forwards the rewritten request path into the configured Hono dev runtime', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'development');
|
||||
vi.stubEnv('LOBE_DEV_HONO_TARGET', 'http://localhost:3011');
|
||||
const fetchSpy = vi.fn(async (request: Request) => {
|
||||
expect(request.url).toBe('http://localhost:3011/api/version');
|
||||
|
||||
return Response.json({ version: '2.1.56' });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const response = await GET(new Request('http://localhost:3010/hono-runtime/api/version'));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('x-lobe-dev-hono-binding')).toBe('next-catch-all');
|
||||
await expect(response.json()).resolves.toEqual({ version: '2.1.56' });
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
|
||||
|
||||
const HONO_BINDING_PREFIX = '/hono-runtime';
|
||||
const HONO_BINDING_HEADER = 'x-lobe-dev-hono-binding';
|
||||
|
||||
const rewriteHonoBindingRequest = (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (!url.pathname.startsWith(HONO_BINDING_PREFIX)) return request;
|
||||
|
||||
const pathname = url.pathname.slice(HONO_BINDING_PREFIX.length);
|
||||
url.pathname = pathname || '/';
|
||||
|
||||
const init: RequestInit & { duplex?: 'half' } = {
|
||||
headers: request.headers,
|
||||
method: request.method,
|
||||
redirect: request.redirect,
|
||||
signal: request.signal,
|
||||
};
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
init.body = request.body;
|
||||
init.duplex = 'half';
|
||||
}
|
||||
|
||||
return new Request(url, init);
|
||||
};
|
||||
|
||||
const handler = async (request: Request) => {
|
||||
const response = await fetchHonoRuntime(rewriteHonoBindingRequest(request));
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set(HONO_BINDING_HEADER, 'next-catch-all');
|
||||
|
||||
return new Response(response.body, {
|
||||
headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
};
|
||||
|
||||
export const DELETE = handler;
|
||||
export const GET = handler;
|
||||
export const HEAD = handler;
|
||||
export const OPTIONS = handler;
|
||||
export const PATCH = handler;
|
||||
export const POST = handler;
|
||||
export const PUT = handler;
|
||||
@@ -0,0 +1,95 @@
|
||||
import path from 'node:path';
|
||||
|
||||
interface HonoFetchApp {
|
||||
fetch: (request: Request) => Promise<Response> | Response;
|
||||
}
|
||||
|
||||
interface HonoDistModule {
|
||||
default?: unknown;
|
||||
}
|
||||
|
||||
let productionHonoApp: HonoFetchApp | undefined;
|
||||
|
||||
const isHonoFetchApp = (value: unknown): value is HonoFetchApp =>
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'fetch' in value &&
|
||||
typeof value.fetch === 'function';
|
||||
|
||||
const createForwardRequest = (request: Request, url: URL) => {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.delete('host');
|
||||
|
||||
const init: RequestInit & { duplex?: 'half' } = {
|
||||
headers,
|
||||
method: request.method,
|
||||
redirect: request.redirect,
|
||||
signal: request.signal,
|
||||
};
|
||||
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
init.body = request.body;
|
||||
init.duplex = 'half';
|
||||
}
|
||||
|
||||
return new Request(url, init);
|
||||
};
|
||||
|
||||
interface ModuleLoader {
|
||||
createRequire?: (filename: string) => (id: string) => unknown;
|
||||
}
|
||||
|
||||
interface ProcessWithBuiltinModule {
|
||||
getBuiltinModule?: (id: string) => unknown;
|
||||
}
|
||||
|
||||
const loadExternalModule = (entry: string) => {
|
||||
// Resolve the require() factory through process.getBuiltinModule at runtime so the
|
||||
// separately built Hono dist stays opaque to the Next bundler and is never compiled in.
|
||||
const moduleLoader = (process as ProcessWithBuiltinModule).getBuiltinModule?.('node:module') as
|
||||
| ModuleLoader
|
||||
| undefined;
|
||||
const runtimeRequire = moduleLoader?.createRequire?.(path.join(process.cwd(), 'package.json'));
|
||||
|
||||
if (!runtimeRequire) {
|
||||
throw new TypeError('Runtime require is not available for the Hono dist entry');
|
||||
}
|
||||
|
||||
return runtimeRequire(entry);
|
||||
};
|
||||
|
||||
const loadProductionHonoApp = () => {
|
||||
if (productionHonoApp) return productionHonoApp;
|
||||
|
||||
const entry =
|
||||
process.env.LOBE_HONO_DIST_ENTRY || path.join(process.cwd(), 'apps/server/dist/index.js');
|
||||
const module = loadExternalModule(entry) as HonoDistModule | HonoFetchApp;
|
||||
const app = isHonoFetchApp(module)
|
||||
? module
|
||||
: isHonoFetchApp(module.default)
|
||||
? module.default
|
||||
: undefined;
|
||||
|
||||
if (!app) {
|
||||
throw new TypeError(`Hono dist entry does not export a fetch-compatible app: ${entry}`);
|
||||
}
|
||||
|
||||
productionHonoApp = app;
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
export const fetchHonoRuntime = async (request: Request) => {
|
||||
const devTarget = process.env.LOBE_DEV_HONO_TARGET;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && devTarget) {
|
||||
const sourceUrl = new URL(request.url);
|
||||
const targetUrl = new URL(devTarget);
|
||||
targetUrl.pathname = sourceUrl.pathname;
|
||||
targetUrl.search = sourceUrl.search;
|
||||
|
||||
return fetch(createForwardRequest(request, targetUrl));
|
||||
}
|
||||
|
||||
return loadProductionHonoApp().fetch(request);
|
||||
};
|
||||
Reference in New Issue
Block a user