♻️ refactor(server): make Next a pure shell — (backend) routes forward to the Hono runtime (phase 2)

All 54 migrated (backend) route files become uniform fetchHonoRuntime
forwarders: dev proxies to the standalone Hono server, production loads
the vite-built apps/server dist in-process via runtime require — the
backend dependency graph no longer passes through next build. Verified:
after() semantics survive in-process dist loading (trace 201), SSE
streams through the chain, full classic-dev e2e green.

- devStartupSequence spawns the Hono dev server and sets
  LOBE_DEV_HONO_TARGET, so `bun run dev` = Hono + Next shell + Vite
- root build/build:raw run build:hono first so the dist pairs with the
  Next build
- hono-runtime client throws an actionable error when the dist is missing
- route tests move next to their api-runtime handlers
  (apps/server/src/api-runtime/__tests__/, 8 files)
- fix oidcProviderAPIHandler: restore the rejection path lost in the
  port — createNodeRequest failures surface as 500 again (regression
  test restored)
- type re-exports (version/check-user/resolve-username) stay on the
  route files for client consumers; erased at build time
This commit is contained in:
Innei
2026-06-10 17:34:01 +08:00
parent daca567947
commit c811b1fecc
67 changed files with 378 additions and 202 deletions
+17 -6
View File
@@ -26,16 +26,27 @@ apps/server/src/
└── workflows-hono/ # /api/workflows/* Hono sub-app (agent-signal, memory, task)
```
The package's exports resolve via the `@/server/*` alias (dual-path tsconfig: `apps/server/src/*` first, `src/server/*` fallback for the SSR-page helpers that still live there).
The package's exports resolve via the `~server/*` alias (`apps/server/src/*`); `@/server/*` now refers only to the Next SSR-page helpers remaining in `src/server/`.
## Next as a Shell
Every `src/app/(backend)/**/route.ts` is a thin forwarder into this package:
- In dev, `fetchHonoRuntime` proxies each request to the standalone Hono server (`LOBE_DEV_HONO_TARGET`, default `http://localhost:3011`).
- In production, it loads the vite-built `apps/server/dist/index.js` in-process via an opaque runtime `require` (override with `LOBE_HONO_DIST_ENTRY`), so the backend never passes through `next build`.
- Route file paths, HTTP method export names, and segment config (`maxDuration`, `dynamic`) are contractual — the cloud repo re-exports them by path. `src/app/(backend)/route-shell.guard.test.ts` enforces that route files stay logic-free.
- Exceptions: `webapi/revalidate` (Next ISR machinery) and `hono-runtime/[...path]` (the binding route itself).
The root `build` / `build:raw` scripts run `build:hono` first so the dist always pairs with the Next build.
## Dev Modes
| Mode | Command | Topology |
| ------------------- | ----------------------- | ---------------------------------------- |
| **Classic** | `bun run dev` | Next (`:3010`) + Vite (`:9876`) |
| **Hono-Lite** (POC) | `bun run dev:hono-lite` | Hono (`:3011`) + Vite (`:9876`), no Next |
| Mode | Command | Topology |
| ------------------- | ----------------------- | ------------------------------------------------------ |
| **Classic** | `bun run dev` | Hono (`:3011`) + Next shell (`:3010`) + Vite (`:9876`) |
| **Hono-Lite** (POC) | `bun run dev:hono-lite` | Hono (`:3011`) + Vite (`:9876`), no Next |
Both modes coexist on this branch — pick whichever fits the task.
Both modes coexist on this branch — pick whichever fits the task. Classic dev now spawns the Hono server automatically and points the Next API shells at it.
### Standalone Server (this package only)
@@ -2,7 +2,7 @@
import { NextRequest } from 'next/server';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { GET } from '../route';
import { agentStreamAPIHandler } from '../agentStream';
// Mock dependencies first
const mockStreamEventManager = {
@@ -14,7 +14,7 @@ vi.mock('~server/modules/AgentRuntime', () => ({
createStreamEventManager: vi.fn(() => mockStreamEventManager),
}));
describe('/api/agent/stream route', () => {
describe('agentStreamAPIHandler', () => {
const MOCK_TIMESTAMP = 1758203237000;
beforeEach(() => {
@@ -30,7 +30,7 @@ describe('/api/agent/stream route', () => {
describe('GET handler', () => {
it('should return 400 when operationId parameter is missing', async () => {
const request = new NextRequest('https://test.com/api/agent/stream');
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(400);
const data = await response.json();
@@ -41,7 +41,7 @@ describe('/api/agent/stream route', () => {
const request = new NextRequest(
'https://test.com/api/agent/stream?operationId=test-operation',
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Type')).toBe('text/event-stream');
@@ -62,7 +62,7 @@ describe('/api/agent/stream route', () => {
'https://test.com/api/agent/stream?operationId=test-operation&lastEventId=123',
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
const decoder = new TextDecoder();
const reader = response.body!.getReader();
@@ -133,7 +133,7 @@ describe('/api/agent/stream route', () => {
];
mockStreamEventManager.getStreamHistory.mockResolvedValue(mockEvents);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
const decoder = new TextDecoder();
const reader = response.body!.getReader();
@@ -215,7 +215,7 @@ describe('/api/agent/stream route', () => {
];
mockStreamEventManager.getStreamHistory.mockResolvedValue(mockEvents);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
const decoder = new TextDecoder();
const reader = response.body!.getReader();
@@ -285,7 +285,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
new Error('Redis connection failed'),
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
const decoder = new TextDecoder();
const reader = response.body!.getReader();
@@ -347,7 +347,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
mockStreamEventManager.subscribeStreamEvents.mockResolvedValue(undefined);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
@@ -373,7 +373,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
mockStreamEventManager.subscribeStreamEvents.mockResolvedValue(undefined);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
@@ -394,7 +394,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
'https://test.com/api/agent/stream?operationId=test-operation',
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
const decoder = new TextDecoder();
const reader = response.body!.getReader();
@@ -456,7 +456,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
// Verify the subscription was set up correctly
expect(mockStreamEventManager.subscribeStreamEvents).toHaveBeenCalledWith(
@@ -491,7 +491,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
// Verify we captured the callback
expect(capturedCallback).toBeDefined();
@@ -527,7 +527,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
// Verify we captured the callback
expect(capturedCallback).toBeDefined();
@@ -563,7 +563,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(capturedCallback).toBeDefined();
expect(response.status).toBe(200);
@@ -624,7 +624,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(capturedCallback).toBeDefined();
expect(capturedSignal).toBeDefined();
@@ -661,7 +661,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
expect(capturedCallback).toBeDefined();
@@ -695,7 +695,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
expect(capturedCallback).toBeDefined();
@@ -730,7 +730,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
expect(capturedCallback).toBeDefined();
@@ -777,7 +777,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
},
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
expect(capturedCallback).toBeDefined();
@@ -818,7 +818,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
`https://test.com/api/agent/stream?operationId=${operationId}`,
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
});
@@ -828,7 +828,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
'https://test.com/api/agent/stream?operationId=test&lastEventId=12345',
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
});
@@ -838,7 +838,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
'https://test.com/api/agent/stream?operationId=test&includeHistory=false',
);
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(200);
expect(mockStreamEventManager.getStreamHistory).not.toHaveBeenCalled();
@@ -847,7 +847,7 @@ data: {"type":"stream_end","timestamp":300,"operationId":"test-operation","data"
it('should handle invalid URL gracefully', async () => {
const request = new NextRequest('https://test.com/api/agent/stream?operationId=');
const response = await GET(request);
const response = await agentStreamAPIHandler(request);
expect(response.status).toBe(400);
const data = await response.json();
@@ -2,7 +2,7 @@
import type { NextRequest } from 'next/server';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GET, POST } from './route';
import { betterAuthAPIHandler } from '../betterAuth';
type RouteHandler = (request: Request) => Promise<Response>;
@@ -29,7 +29,7 @@ const createPostRequest = (body: string, contentType = 'application/json') =>
method: 'POST',
}) as NextRequest;
describe('/api/auth/[...all] route', () => {
describe('betterAuthAPIHandler', () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.get.mockResolvedValue(Response.json({ ok: true }));
@@ -37,7 +37,7 @@ describe('/api/auth/[...all] route', () => {
});
it('returns 400 for malformed JSON auth requests before Better Auth handles them', async () => {
const response = await POST(
const response = await betterAuthAPIHandler(
createPostRequest('{"email":"user@example.com","password":"secret",}'),
);
@@ -54,7 +54,7 @@ describe('/api/auth/[...all] route', () => {
Response.json(await request.json()),
);
const response = await POST(
const response = await betterAuthAPIHandler(
createPostRequest(JSON.stringify({ email: 'user@example.com', password: 'secret' })),
);
@@ -66,7 +66,7 @@ describe('/api/auth/[...all] route', () => {
});
it('delegates non-JSON auth requests to Better Auth', async () => {
const response = await POST(
const response = await betterAuthAPIHandler(
createPostRequest(
'email=user%40example.com&password=secret',
'application/x-www-form-urlencoded',
@@ -80,7 +80,7 @@ describe('/api/auth/[...all] route', () => {
it('delegates GET requests to Better Auth', async () => {
const request = new Request('https://localhost/api/auth/get-session') as NextRequest;
const response = await GET(request);
const response = await betterAuthAPIHandler(request);
expect(response.status).toBe(200);
expect(mocks.get).toHaveBeenCalledWith(request);
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { auth } from '@/auth';
import { initModelRuntimeFromDB } from '~server/modules/ModelRuntime';
import { POST } from './route';
import { chatAPIHandler } from '../chat';
vi.mock('@/app/(backend)/middleware/auth/utils', () => ({
checkAuthMethod: vi.fn(),
@@ -45,11 +45,9 @@ afterEach(() => {
vi.clearAllMocks();
});
describe('POST handler', () => {
describe('chatAPIHandler', () => {
describe('init chat model', () => {
it('should initialize ModelRuntime correctly with valid session', async () => {
const mockParams = Promise.resolve({ provider: 'test-provider' });
const mockChatResponse = new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
@@ -60,7 +58,7 @@ describe('POST handler', () => {
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
await POST(request as unknown as Request, { params: mockParams });
await chatAPIHandler(request as unknown as Request, { provider: 'test-provider' });
expect(initModelRuntimeFromDB).toHaveBeenCalledWith(
expect.anything(),
@@ -73,9 +71,7 @@ describe('POST handler', () => {
it('should return Unauthorized error when no session exists', async () => {
vi.mocked(auth.api.getSession).mockResolvedValue(null);
const mockParams = Promise.resolve({ provider: 'test-provider' });
const response = await POST(request, { params: mockParams });
const response = await chatAPIHandler(request, { provider: 'test-provider' });
expect(response.status).toBe(401);
});
@@ -83,7 +79,6 @@ describe('POST handler', () => {
describe('chat', () => {
it('should correctly handle chat completion with valid payload', async () => {
const mockParams = Promise.resolve({ provider: 'test-provider' });
const mockChatPayload = { message: 'Hello, world!' };
request = new Request(new URL('https://test.com'), {
method: 'POST',
@@ -98,7 +93,9 @@ describe('POST handler', () => {
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await POST(request as unknown as Request, { params: mockParams });
const response = await chatAPIHandler(request as unknown as Request, {
provider: 'test-provider',
});
expect(response).toEqual(mockChatResponse);
expect(mockRuntime.chat).toHaveBeenCalledWith(mockChatPayload, {
@@ -108,7 +105,6 @@ describe('POST handler', () => {
});
it('should return an error response when chat completion fails', async () => {
const mockParams = Promise.resolve({ provider: 'test-provider' });
const mockChatPayload = { message: 'Hello, world!' };
request = new Request(new URL('https://test.com'), {
method: 'POST',
@@ -128,7 +124,7 @@ describe('POST handler', () => {
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await POST(request, { params: mockParams });
const response = await chatAPIHandler(request, { provider: 'test-provider' });
expect(response.status).toBe(500);
expect(await response.json()).toEqual({
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GET } from './route';
import { connectorOAuthCallbackAPIHandler } from '../connectorOAuthCallback';
const { mockConsume, mockFindById, mockSync, mockUpdate } = vi.hoisted(() => ({
mockConsume: vi.fn(),
@@ -41,7 +41,7 @@ vi.mock('@/database/models/connectorTool', () => ({
vi.mock('~server/services/connector/sync', () => ({ syncConnectorToolsById: mockSync }));
const makeReq = () =>
({ nextUrl: { searchParams: new URLSearchParams('code=abc&state=xyz') } }) as any;
new Request('https://app.example.com/oauth/connector/callback?code=abc&state=xyz');
beforeEach(() => {
vi.clearAllMocks();
@@ -62,11 +62,11 @@ beforeEach(() => {
mockUpdate.mockResolvedValue(undefined);
});
describe('connector OAuth callback', () => {
describe('connectorOAuthCallbackAPIHandler', () => {
it('reports synced:false when auth succeeds but tool sync fails', async () => {
mockSync.mockRejectedValue(new Error('mcp down'));
const body = await (await GET(makeReq())).text();
const body = await (await connectorOAuthCallbackAPIHandler(makeReq())).text();
expect(body).toContain('"success":true');
expect(body).toContain('"synced":false');
@@ -75,7 +75,7 @@ describe('connector OAuth callback', () => {
it('reports synced:true when auth and tool sync both succeed', async () => {
mockSync.mockResolvedValue({ toolCount: 5 });
const body = await (await GET(makeReq())).text();
const body = await (await connectorOAuthCallbackAPIHandler(makeReq())).text();
expect(body).toContain('"success":true');
expect(body).toContain('"synced":true');
@@ -7,7 +7,7 @@ import type { FileItem } from '@/database/schemas';
import { getServerDB } from '@/database/server';
import { FileService } from '~server/services/file';
import { GET } from './route';
import { fileProxyAPIHandler } from '../fileProxy';
const fileServiceMocks = vi.hoisted(() => {
const instance = {
@@ -35,7 +35,7 @@ vi.mock('~server/services/file', () => ({
FileService: fileServiceMocks.FileService,
}));
describe('file proxy route', () => {
describe('fileProxyAPIHandler', () => {
const db = {} as LobeChatDatabase;
beforeEach(() => {
@@ -53,8 +53,8 @@ describe('file proxy route', () => {
});
it('should redirect to a cached presigned preview URL instead of a public full file URL', async () => {
const response = await GET(new Request('https://lobehub.com/f/file-id'), {
params: Promise.resolve({ id: 'file-id' }),
const response = await fileProxyAPIHandler(new Request('https://lobehub.com/f/file-id'), {
id: 'file-id',
});
expect(response.status).toBe(302);
@@ -1,5 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { memoryExtractionBenchmarkLoCoMoWebhookAPIHandler } from '../memoryExtractionBenchmark';
const replaceParts = vi.fn();
const upsertSource = vi.fn();
const extractBenchmarkSource = vi.fn();
@@ -27,7 +29,7 @@ vi.mock('@lobechat/memory-user-memory', () => ({
BenchmarkLocomoContextProvider: vi.fn().mockImplementation((params) => params),
}));
describe('benchmark LoCoMo memory extraction webhook', () => {
describe('memoryExtractionBenchmarkLoCoMoWebhookAPIHandler', () => {
beforeEach(() => {
replaceParts.mockReset();
upsertSource.mockReset();
@@ -42,8 +44,7 @@ describe('benchmark LoCoMo memory extraction webhook', () => {
});
it('returns per-session ingestion results and inserted part count', async () => {
const { POST } = await import('../route');
const response = await POST(
const response = await memoryExtractionBenchmarkLoCoMoWebhookAPIHandler(
new Request('http://localhost/api/webhooks/memory-extraction/benchmark-locomo', {
body: JSON.stringify({
sampleId: 'conv-26',
@@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { auth } from '@/auth';
import { initModelRuntimeFromDB } from '~server/modules/ModelRuntime';
import { GET } from './route';
import { modelsAPIHandler } from '../models';
vi.mock('@/app/(backend)/middleware/auth/utils', () => ({
checkAuthMethod: vi.fn(),
@@ -43,11 +43,9 @@ afterEach(() => {
vi.clearAllMocks();
});
describe('GET handler', () => {
describe('modelsAPIHandler', () => {
describe('error handling', () => {
it('should not expose stack trace when an Error is thrown', async () => {
const mockParams = Promise.resolve({ provider: 'google' });
const errorWithStack = new Error('Something went wrong');
errorWithStack.stack =
'Error: Something went wrong\n at Object.<anonymous> (/path/to/file.ts:10:15)';
@@ -59,7 +57,7 @@ describe('GET handler', () => {
};
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await GET(request, { params: mockParams });
const response = await modelsAPIHandler(request, { provider: 'google' });
const responseBody = await response.json();
expect(responseBody.body.error.name).toBe('Error');
@@ -72,8 +70,6 @@ describe('GET handler', () => {
});
it('should preserve error name for custom error types', async () => {
const mockParams = Promise.resolve({ provider: 'google' });
class CustomError extends Error {
constructor(message: string) {
super(message);
@@ -91,7 +87,7 @@ describe('GET handler', () => {
};
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await GET(request, { params: mockParams });
const response = await modelsAPIHandler(request, { provider: 'google' });
const responseBody = await response.json();
expect(responseBody.body.error.name).toBe('CustomError');
@@ -100,8 +96,6 @@ describe('GET handler', () => {
});
it('should pass through structured error objects as-is', async () => {
const mockParams = Promise.resolve({ provider: 'google' });
const structuredError = {
errorType: ChatErrorType.InternalServerError,
error: { code: 'PROVIDER_ERROR', details: 'API limit exceeded' },
@@ -114,7 +108,7 @@ describe('GET handler', () => {
};
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await GET(request, { params: mockParams });
const response = await modelsAPIHandler(request, { provider: 'google' });
const responseBody = await response.json();
expect(responseBody.body.error.code).toBe('PROVIDER_ERROR');
@@ -122,8 +116,6 @@ describe('GET handler', () => {
});
it('should return correct status code for errors', async () => {
const mockParams = Promise.resolve({ provider: 'google' });
const mockRuntime: LobeRuntimeAI = {
baseURL: 'abc',
chat: vi.fn(),
@@ -131,14 +123,12 @@ describe('GET handler', () => {
};
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await GET(request, { params: mockParams });
const response = await modelsAPIHandler(request, { provider: 'google' });
expect(response.status).toBe(500);
});
it('should include provider in error response', async () => {
const mockParams = Promise.resolve({ provider: 'openai' });
const mockRuntime: LobeRuntimeAI = {
baseURL: 'abc',
chat: vi.fn(),
@@ -146,7 +136,7 @@ describe('GET handler', () => {
};
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await GET(request, { params: mockParams });
const response = await modelsAPIHandler(request, { provider: 'openai' });
const responseBody = await response.json();
expect(responseBody.body.provider).toBe('openai');
@@ -155,8 +145,6 @@ describe('GET handler', () => {
describe('success cases', () => {
it('should return model list on success', async () => {
const mockParams = Promise.resolve({ provider: 'openai' });
const mockModelList = [
{ id: 'gpt-4', name: 'GPT-4' },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' },
@@ -169,7 +157,7 @@ describe('GET handler', () => {
};
vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime));
const response = await GET(request, { params: mockParams });
const response = await modelsAPIHandler(request, { provider: 'openai' });
const responseBody = await response.json();
expect(response.status).toBe(200);
@@ -32,7 +32,22 @@ vi.mock('~server/services/oidc/oidcProvider', () => ({
})),
}));
describe('OIDC route', () => {
const makeRequest = () =>
new Request('https://example.com/oidc/token', {
body: 'grant_type=refresh_token',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
method: 'POST',
}) as unknown as NextRequest;
const callWithTimeout = (handler: (req: NextRequest) => Promise<Response>, request: NextRequest) =>
Promise.race([
handler(request),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('OIDC route timed out')), 50),
),
]);
describe('oidcProviderAPIHandler', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -48,22 +63,24 @@ describe('OIDC route', () => {
it('returns a 500 response when creating the Node request fails', async () => {
mocks.createNodeRequest.mockRejectedValueOnce(new Error('body stream aborted'));
const { POST } = await import('./route');
const request = new Request('https://example.com/oidc/token', {
body: 'grant_type=refresh_token',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
method: 'POST',
}) as unknown as NextRequest;
const response = await Promise.race([
POST(request),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('OIDC route timed out')), 50),
),
]);
const { oidcProviderAPIHandler } = await import('../oidc');
const response = await callWithTimeout(oidcProviderAPIHandler, makeRequest());
expect(response.status).toBe(500);
await expect(response.text()).resolves.toContain('body stream aborted');
expect(mocks.middleware).not.toHaveBeenCalled();
});
it('returns a 500 response when the OIDC provider flow fails', async () => {
mocks.providerCallback.mockImplementationOnce(() => {
throw new Error('callback exploded');
});
const { oidcProviderAPIHandler } = await import('../oidc');
const response = await callWithTimeout(oidcProviderAPIHandler, makeRequest());
expect(response.status).toBe(500);
await expect(response.text()).resolves.toContain('callback exploded');
expect(mocks.middleware).not.toHaveBeenCalled();
});
});
+1 -1
View File
@@ -339,7 +339,7 @@ export const oidcProviderAPIHandler = async (request: Request) => {
}
});
providerLog('Middleware call initiated, waiting for its callback OR nodeResponse.end()...');
});
}, reject);
});
providerLog('Promise surrounding middleware call resolved.');
+2 -2
View File
@@ -39,13 +39,13 @@
"apps/desktop/src/main"
],
"scripts": {
"build": "bun run build:spa && bun run build:spa:auth && bun run build:spa:copy && bun run build:next",
"build": "bun run build:hono && bun run build:spa && bun run build:spa:auth && bun run build:spa:copy && bun run build:next",
"build:analyze": "cross-env NODE_OPTIONS=--max-old-space-size=81920 next experimental-analyze",
"build:docker": "pnpm run build:spa && pnpm run build:spa:mobile && pnpm run build:spa:auth && pnpm run build:spa:copy && cross-env NODE_OPTIONS=--max-old-space-size=8192 DOCKER=true next build",
"build:hono": "pnpm --filter @lobechat/server build",
"build:next": "cross-env NODE_OPTIONS=--max-old-space-size=7168 bun run build:next:raw",
"build:next:raw": "next build",
"build:raw": "bun run build:spa:raw && bun run build:spa:auth && bun run build:spa:copy && bun run build:next:raw",
"build:raw": "bun run build:hono && bun run build:spa:raw && bun run build:spa:auth && bun run build:spa:copy && bun run build:next:raw",
"build:spa": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm run build:spa:raw",
"build:spa:auth": "rm -rf public/_spa-auth && cross-env NODE_OPTIONS=--max-old-space-size=8192 AUTH=true vite build",
"build:spa:copy": "tsx scripts/copySpaBuild.mts && tsx scripts/generateSpaTemplates.mts",
+17 -2
View File
@@ -42,8 +42,10 @@ let nextPort = 3010;
let nextRootUrl = `http://${NEXT_HOST}:${nextPort}/`;
let nextProcess: ChildProcess | undefined;
let viteProcess: ChildProcess | undefined;
let honoProcess: ChildProcess | undefined;
let nextHandle: DevProcessHandle | undefined;
let viteHandle: DevProcessHandle | undefined;
let honoHandle: DevProcessHandle | undefined;
let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
let shuttingDown = false;
@@ -171,11 +173,13 @@ const runNextBackgroundTasks = () => {
const terminateChildren = () => {
sendSignalToDevProcess(viteHandle, 'SIGTERM');
sendSignalToDevProcess(nextHandle, 'SIGTERM');
sendSignalToDevProcess(honoHandle, 'SIGTERM');
};
const forceKillChildren = () => {
sendSignalToDevProcess(viteHandle, 'SIGKILL');
sendSignalToDevProcess(nextHandle, 'SIGKILL');
sendSignalToDevProcess(honoHandle, 'SIGKILL');
};
const clearForceKillTimer = () => {
@@ -189,7 +193,8 @@ const hasChildSettled = (child?: ChildProcess) =>
const clearForceKillTimerWhenChildrenSettle = () => {
if (!shuttingDown) return;
if (hasChildSettled(nextProcess) && hasChildSettled(viteProcess)) clearForceKillTimer();
if (hasChildSettled(nextProcess) && hasChildSettled(viteProcess) && hasChildSettled(honoProcess))
clearForceKillTimer();
};
const shutdownAll = (signal: NodeJS.Signals) => {
@@ -209,7 +214,7 @@ const shutdownAll = (signal: NodeJS.Signals) => {
}, FORCE_KILL_TIMEOUT_MS);
};
const watchChildExit = (child: ChildProcess, name: 'next' | 'vite') => {
const watchChildExit = (child: ChildProcess, name: 'hono' | 'next' | 'vite') => {
child.once('exit', (code, signal) => {
if (shuttingDown) {
clearForceKillTimerWhenChildrenSettle();
@@ -247,6 +252,15 @@ const main = async () => {
forceKillChildren();
});
// The (backend) routes are thin shells forwarding into the standalone Hono
// runtime; point them at the dev Hono server and spawn it alongside Next.
const honoPort = Number.parseInt(process.env.HONO_PORT || '', 10) || 3011;
process.env.LOBE_DEV_HONO_TARGET ||= `http://localhost:${honoPort}`;
honoProcess = runNpmScript('dev:hono:server');
honoHandle = createDevProcessHandle({ isWindows, pid: honoProcess.pid });
watchChildExit(honoProcess, 'hono');
nextProcess = spawn('npx', ['next', 'dev', '-p', String(nextPort)], {
detached: !isWindows,
env: process.env,
@@ -264,6 +278,7 @@ const main = async () => {
await Promise.race([
new Promise((resolve) => nextProcess?.once('exit', resolve)),
new Promise((resolve) => viteProcess?.once('exit', resolve)),
new Promise((resolve) => honoProcess?.once('exit', resolve)),
]);
};
@@ -1,6 +1,6 @@
import app from '~server/agent-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (request: Request) => app.fetch(request);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
+4 -2
View File
@@ -1,3 +1,5 @@
import { agentStreamAPIHandler } from '~server/api-runtime/agentStream';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const GET = (req: Request) => agentStreamAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
+2 -2
View File
@@ -1,6 +1,6 @@
import { betterAuthAPIHandler } from '~server/api-runtime/betterAuth';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => betterAuthAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1,2 +1,7 @@
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
export type { CheckUserResponseData } from '~server/api-runtime/auth';
export { checkUserAPIHandler as POST } from '~server/api-runtime/auth';
@@ -1,2 +1,7 @@
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
export type { ResolveUsernameResponseData } from '~server/api-runtime/auth';
export { resolveUsernameAPIHandler as POST } from '~server/api-runtime/auth';
@@ -1 +1,5 @@
export { agentTracingAPIHandler as GET } from '~server/api-runtime/dev';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
@@ -1 +1,5 @@
export { memoryUserMemoryBenchmarkLoCoMoDevAPIHandler as POST } from '~server/api-runtime/memoryBenchmarkDev';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+5 -1
View File
@@ -1 +1,5 @@
export { testPushAPIHandler as POST } from '~server/api-runtime/dev';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,6 +1,6 @@
import { openAPIHandler } from '~server/api-runtime/openapi';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => openAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
+6 -1
View File
@@ -1,2 +1,7 @@
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export type { VersionResponseData } from '~server/api-runtime/version';
export { versionAPIHandler as GET } from '~server/api-runtime/version';
@@ -1 +1,5 @@
export { casdoorWebhookAPIHandler as POST } from '~server/api-runtime/webhooks';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { logtoWebhookAPIHandler as POST } from '~server/api-runtime/webhooks';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { memoryExtractionBenchmarkLoCoMoWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtractionBenchmark';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { memoryExtractionWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtraction';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { memoryUserPersonaUpdateWritingWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtraction';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { memoryExtractChatTopicCancelWebhookAPIHandler as POST } from '~server/api-runtime/memoryExtraction';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,4 +1,5 @@
import { videoWebhookAPIHandler } from '~server/api-runtime/videoWebhook';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = async (req: Request, ctx: { params: Promise<{ provider: string }> }) =>
videoWebhookAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import app from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (request: Request) => app.fetch(request);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { agentEvalRunOnThreadCompleteAPIHandler as POST } from '~server/api-runtime/agentEvalRunWorkflow';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { agentEvalRunOnTrajectoryCompleteAPIHandler as POST } from '~server/api-runtime/agentEvalRunWorkflow';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,3 +1,5 @@
import workflowsApp from '~server/workflows-hono';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => workflowsApp.fetch(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+4 -3
View File
@@ -1,4 +1,5 @@
import { fileProxyAPIHandler } from '~server/api-runtime/fileProxy';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const GET = async (req: Request, ctx: { params: Promise<{ id: string }> }) =>
fileProxyAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
@@ -1,9 +1,6 @@
import { marketAgentAPIHandler } from '~server/api-runtime/market';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
type RouteContext = { params: Promise<{ segments?: string[] }> };
const handler = async (req: Request, ctx: RouteContext) =>
marketAgentAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1,9 +1,6 @@
import { marketOIDCAPIHandler } from '~server/api-runtime/market';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
type RouteContext = { params: Promise<{ segments?: string[] }> };
const handler = async (req: Request, ctx: RouteContext) =>
marketOIDCAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1,9 +1,6 @@
import { marketSocialAPIHandler } from '~server/api-runtime/market';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
type RouteContext = { params: Promise<{ segments?: string[] }> };
const handler = async (req: Request, ctx: RouteContext) =>
marketSocialAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1,6 +1,7 @@
import { marketUserProfileAPIHandler } from '~server/api-runtime/market';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const GET = async (req: Request, ctx: { params: Promise<{ username: string }> }) =>
marketUserProfileAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const dynamic = 'force-dynamic';
+4 -2
View File
@@ -1,5 +1,7 @@
import { marketUserMeAPIHandler } from '~server/api-runtime/market';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const PUT = (req: Request) => marketUserMeAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const PUT = handler;
export const dynamic = 'force-dynamic';
@@ -1 +1,5 @@
export { connectorOAuthCallbackAPIHandler as GET } from '~server/api-runtime/connectorOAuthCallback';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
+2 -2
View File
@@ -1,6 +1,6 @@
import { oidcProviderAPIHandler } from '~server/api-runtime/oidc';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => oidcProviderAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1,3 +1,5 @@
import { oidcCallbackDesktopAPIHandler } from '~server/api-runtime/oidc';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const GET = (req: Request) => oidcCallbackDesktopAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
@@ -1 +1,5 @@
export { oidcClearSessionAPIHandler as POST } from '~server/api-runtime/oidc';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+5 -1
View File
@@ -1 +1,5 @@
export { oidcConsentAPIHandler as POST } from '~server/api-runtime/oidc';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+5 -1
View File
@@ -1 +1,5 @@
export { oidcHandoffAPIHandler as GET } from '~server/api-runtime/oidc';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
+6 -1
View File
@@ -1 +1,6 @@
export { asyncTRPCHandler as GET, asyncTRPCHandler as POST } from '~server/trpc-runtime/async';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1 +1,6 @@
export { lambdaTRPCHandler as GET, lambdaTRPCHandler as POST } from '~server/trpc-runtime/lambda';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1 +1,6 @@
export { mobileTRPCHandler as GET, mobileTRPCHandler as POST } from '~server/trpc-runtime/mobile';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
+6 -1
View File
@@ -1 +1,6 @@
export { toolsTRPCHandler as GET, toolsTRPCHandler as POST } from '~server/trpc-runtime/tools';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
export const POST = handler;
@@ -1,8 +1,7 @@
import { chatAPIHandler } from '~server/api-runtime/chat';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
// If user don't use fluid compute, will build failed
// this enforce user to enable fluid compute
export const maxDuration = 300;
export const POST = async (req: Request, ctx: { params: Promise<{ provider: string }> }) =>
chatAPIHandler(req, await ctx.params);
@@ -1,5 +1,7 @@
import { comfyUICreateImageAPIHandler } from '~server/api-runtime/createImage';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
export const maxDuration = 300;
export const POST = (req: Request) => comfyUICreateImageAPIHandler(req);
@@ -1,4 +1,5 @@
import { pullModelsAPIHandler } from '~server/api-runtime/models';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = async (req: Request, ctx: { params: Promise<{ provider: string }> }) =>
pullModelsAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,4 +1,5 @@
import { modelsAPIHandler } from '~server/api-runtime/models';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const GET = async (req: Request, ctx: { params: Promise<{ provider: string }> }) =>
modelsAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
+5 -1
View File
@@ -1 +1,5 @@
export { openAISTTAPIHandler as POST } from '~server/api-runtime/speech';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+4 -2
View File
@@ -1,3 +1,5 @@
import { traceAPIHandler } from '~server/api-runtime/trace';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const POST = (req: Request) => traceAPIHandler(req);
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+5 -1
View File
@@ -1 +1,5 @@
export { edgeTTSAPIHandler as POST } from '~server/api-runtime/speech';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1 +1,5 @@
export { microsoftTTSAPIHandler as POST } from '~server/api-runtime/speech';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
+5 -1
View File
@@ -1 +1,5 @@
export { openAITTSAPIHandler as POST } from '~server/api-runtime/speech';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
const handler = (req: Request) => fetchHonoRuntime(req);
export const POST = handler;
@@ -1,4 +1,5 @@
import { userAvatarAPIHandler } from '~server/api-runtime/userAvatar';
import { fetchHonoRuntime } from '@/server/hono-runtime/client';
export const GET = async (req: Request, ctx: { params: Promise<{ id: string; image: string }> }) =>
userAvatarAPIHandler(req, await ctx.params);
const handler = (req: Request) => fetchHonoRuntime(req);
export const GET = handler;
+14 -1
View File
@@ -63,7 +63,20 @@ const loadProductionHonoApp = () => {
const entry =
process.env.LOBE_HONO_DIST_ENTRY || path.join(process.cwd(), 'apps/server/dist/index.js');
const module = loadExternalModule(entry) as HonoDistModule | HonoFetchApp;
let module: HonoDistModule | HonoFetchApp;
try {
module = loadExternalModule(entry) as HonoDistModule | HonoFetchApp;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') {
throw new Error(
`Hono dist entry not found at ${entry}. Build it with \`pnpm --filter @lobechat/server build\`, ` +
'or in dev run `bun run dev` / set LOBE_DEV_HONO_TARGET to a running Hono server.',
{ cause: error },
);
}
throw error;
}
const app = isHonoFetchApp(module)
? module
: isHonoFetchApp(module.default)