@ubean/app 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +202 -4
- package/dist/index.js +143 -8
- package/package.json +12 -15
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,168 @@
|
|
|
1
1
|
import { Context, Hono, MiddlewareHandler } from "hono";
|
|
2
|
-
import { RegisterOptions, RouteRegistrar } from "@ubean/
|
|
2
|
+
import { RegisterOptions, RouteRegistrar } from "@ubean/routes";
|
|
3
|
+
import { ComposedHandler, ComposedHandler as ComposedHandler$1, RouteMeta, RouteMeta as RouteMeta$1, RouteRule, RouteRule as RouteRule$1, UbeanEnv, UbeanEnv as UbeanEnv$1, UbeanMiddleware, UbeanMiddleware as UbeanMiddleware$1 } from "@ubean/shared";
|
|
3
4
|
import { Hookable } from "hookable";
|
|
4
|
-
import { ScannedApiRoute, ScannedApiRoute as ScannedApiRoute$1, ScannedCronTask, ScannedLayout, ScannedLayout as ScannedLayout$1, ScannedMiddleware, ScannedMiddleware as ScannedMiddleware$1, ScannedPageRoute, ScannedPageRoute as ScannedPageRoute$1 } from "@ubean/
|
|
5
|
-
|
|
5
|
+
import { ScannedApiRoute, ScannedApiRoute as ScannedApiRoute$1, ScannedCronTask, ScannedLayout, ScannedLayout as ScannedLayout$1, ScannedMiddleware, ScannedMiddleware as ScannedMiddleware$1, ScannedPageRoute, ScannedPageRoute as ScannedPageRoute$1 } from "@ubean/scan";
|
|
6
|
+
//#region src/hooks.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* 请求事件 —— 传递给 `handle` 和 `handleError` 的上下文对象。
|
|
9
|
+
*/
|
|
10
|
+
interface HandleEvent {
|
|
11
|
+
/** 原始 Request 对象。 */
|
|
12
|
+
request: Request;
|
|
13
|
+
/** Hono Context(含 route params、variables、env 等)。 */
|
|
14
|
+
context: Context<UbeanEnv$1>;
|
|
15
|
+
/** 客户端 IP(从 `x-forwarded-for` 或 `x-real-ip` 提取)。 */
|
|
16
|
+
clientAddress: string;
|
|
17
|
+
/** 请求 ID(从 `x-request-id` header 或自动生成)。 */
|
|
18
|
+
requestId: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `handle` hook 的输入参数。
|
|
22
|
+
*/
|
|
23
|
+
interface HandleInput {
|
|
24
|
+
/** 请求事件。 */
|
|
25
|
+
event: HandleEvent;
|
|
26
|
+
/** resolve 函数:执行正常的请求处理流程,返回 Response。 */
|
|
27
|
+
resolve: (event: HandleEvent) => Promise<Response>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 全局请求处理 hook。
|
|
31
|
+
*
|
|
32
|
+
* 类似 Hono 中间件,但作用于**所有**请求(含 404、静态资源、错误响应)。
|
|
33
|
+
* 可用于:统一添加 header、请求日志、A/B 测试、认证预处理等。
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* handle: async ({ event, resolve }) => {
|
|
38
|
+
* const start = Date.now();
|
|
39
|
+
* const response = await resolve(event);
|
|
40
|
+
* response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
|
|
41
|
+
* return response;
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
type Handle = (input: HandleInput) => Promise<Response>;
|
|
46
|
+
/**
|
|
47
|
+
* `handleFetch` hook 的输入参数。
|
|
48
|
+
*/
|
|
49
|
+
interface HandleFetchInput {
|
|
50
|
+
/** 原始 fetch Request。 */
|
|
51
|
+
request: Request;
|
|
52
|
+
/** 内部 fetch 函数:执行实际的 HTTP 请求(进程内调度或网络请求)。 */
|
|
53
|
+
fetch: (request: Request) => Promise<Response>;
|
|
54
|
+
/** 调用 fetch 的服务端上下文(可能为 undefined)。 */
|
|
55
|
+
serverContext?: Context<UbeanEnv$1>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 服务端 fetch 拦截 hook。
|
|
59
|
+
*
|
|
60
|
+
* 拦截服务端代码中的 `fetch()` 调用(含 ubean 的 `internalFetch`),
|
|
61
|
+
* 可修改请求头、URL、注入认证 token 等。
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* handleFetch: async ({ request, fetch }) => {
|
|
66
|
+
* // 为所有服务端 fetch 添加内部 API token
|
|
67
|
+
* request.headers.set('X-Internal-Auth', process.env.INTERNAL_TOKEN);
|
|
68
|
+
* return fetch(request);
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
type HandleFetch = (input: HandleFetchInput) => Promise<Response>;
|
|
73
|
+
/**
|
|
74
|
+
* `handleError` hook 的输入参数。
|
|
75
|
+
*/
|
|
76
|
+
interface HandleErrorInput {
|
|
77
|
+
/** 请求事件。 */
|
|
78
|
+
event: HandleEvent;
|
|
79
|
+
/** 捕获的错误。 */
|
|
80
|
+
error: unknown;
|
|
81
|
+
/** HTTP 状态码(500/404 等)。 */
|
|
82
|
+
status: number;
|
|
83
|
+
/** 错误消息(已脱敏,适合展示给用户)。 */
|
|
84
|
+
message: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 全局错误处理 hook。
|
|
88
|
+
*
|
|
89
|
+
* 在未捕获错误发生时调用,可用于:日志记录、错误上报(Sentry 等)、
|
|
90
|
+
* 自定义错误响应(需在 `handle` 中实现)。
|
|
91
|
+
*
|
|
92
|
+
* 注意:此 hook 仅用于**副作用**(日志/上报),不影响返回给客户端的
|
|
93
|
+
* 错误响应。要自定义错误响应,请使用 `handle` 拦截 5xx 响应。
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* handleError: async ({ event, error, status, message }) => {
|
|
98
|
+
* Sentry.captureException(error, {
|
|
99
|
+
* tags: { status, path: event.request.url },
|
|
100
|
+
* });
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
type HandleError = (input: HandleErrorInput) => void | Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* 全局 hooks 集合。
|
|
107
|
+
*/
|
|
108
|
+
interface GlobalHooks {
|
|
109
|
+
/** 请求处理 hook。 */
|
|
110
|
+
handle?: Handle;
|
|
111
|
+
/** 服务端 fetch 拦截 hook。 */
|
|
112
|
+
handleFetch?: HandleFetch;
|
|
113
|
+
/** 错误处理 hook。 */
|
|
114
|
+
handleError?: HandleError;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 设置全局 hooks。
|
|
118
|
+
*
|
|
119
|
+
* 由 `applyServerConfig` 在应用启动时调用。
|
|
120
|
+
*/
|
|
121
|
+
declare function setGlobalHooks(hooks: GlobalHooks): void;
|
|
122
|
+
/**
|
|
123
|
+
* 获取全局 hooks。
|
|
124
|
+
*/
|
|
125
|
+
declare function getGlobalHooks(): GlobalHooks;
|
|
126
|
+
/**
|
|
127
|
+
* 清空全局 hooks(测试用)。
|
|
128
|
+
*/
|
|
129
|
+
declare function clearGlobalHooks(): void;
|
|
130
|
+
/**
|
|
131
|
+
* 从 Hono Context 构建请求事件。
|
|
132
|
+
*/
|
|
133
|
+
declare function createHandleEvent(c: Context<UbeanEnv$1>): HandleEvent;
|
|
134
|
+
/**
|
|
135
|
+
* 从错误中提取安全消息(移除敏感信息)。
|
|
136
|
+
*
|
|
137
|
+
* - 404 → "Not Found"
|
|
138
|
+
* - 5xx → "Internal Server Error"(生产环境不暴露内部错误详情,
|
|
139
|
+
* 也不区分 Error / 非 Error 类型,避免信息泄漏)
|
|
140
|
+
* - 4xx(非 404)→ 返回 `error.message`(仅当 error 是 Error 实例),
|
|
141
|
+
* 否则返回 "Bad Request"
|
|
142
|
+
*/
|
|
143
|
+
declare function extractErrorMessage(error: unknown, status: number): string;
|
|
144
|
+
/**
|
|
145
|
+
* 包装 resolve 函数:将 Hono 的请求处理流程封装为 `resolve(event)` 调用。
|
|
146
|
+
*
|
|
147
|
+
* 返回一个函数,调用后会执行 `next()` 让 Hono 处理请求,然后返回 `c.res`。
|
|
148
|
+
*/
|
|
149
|
+
declare function wrapResolve(c: Context<UbeanEnv$1>, next: () => Promise<void>): (event: HandleEvent) => Promise<Response>;
|
|
150
|
+
/**
|
|
151
|
+
* 应用 `handle` hook:如果已注册,则用 hook 包裹请求处理流程。
|
|
152
|
+
*
|
|
153
|
+
* 返回 `true` 表示 hook 已处理请求(调用方应跳过后续中间件),
|
|
154
|
+
* `false` 表示无 hook 或 hook 已调用 resolve。
|
|
155
|
+
*/
|
|
156
|
+
declare function applyHandleHook(c: Context<UbeanEnv$1>, next: () => Promise<void>): Promise<boolean>;
|
|
157
|
+
/**
|
|
158
|
+
* 应用 `handleFetch` hook:如果已注册,则用 hook 包裹 fetch 调用。
|
|
159
|
+
*/
|
|
160
|
+
declare function applyHandleFetchHook(request: Request, defaultFetch: (request: Request) => Promise<Response>, serverContext?: Context<UbeanEnv$1>): Promise<Response>;
|
|
161
|
+
/**
|
|
162
|
+
* 应用 `handleError` hook:如果已注册,则调用它。
|
|
163
|
+
*/
|
|
164
|
+
declare function applyHandleErrorHook(c: Context<UbeanEnv$1>, error: unknown, status: number): Promise<void>;
|
|
165
|
+
//#endregion
|
|
6
166
|
//#region src/define-server.d.ts
|
|
7
167
|
/**
|
|
8
168
|
* Apply a resolved server config to a `UbeanApp` instance.
|
|
@@ -11,6 +171,7 @@ import { ComposedHandler, ComposedHandler as ComposedHandler$1, RouteMeta, Route
|
|
|
11
171
|
* - `plugins` are available when `init()` calls `setup` / `ready`
|
|
12
172
|
* - `hooks` are registered before `init()` fires `app:created` etc.
|
|
13
173
|
* - `onAppCreate` runs before route registration
|
|
174
|
+
* - `globalHooks` (handle/handleFetch/handleError) are set before any request
|
|
14
175
|
*
|
|
15
176
|
* `onServerReady` is NOT called here — the caller must invoke it after
|
|
16
177
|
* `app.init()` completes.
|
|
@@ -41,6 +202,14 @@ interface DefineServerOptions {
|
|
|
41
202
|
* `middleware:register`, `error`.
|
|
42
203
|
*/
|
|
43
204
|
hooks?: ServerHooks;
|
|
205
|
+
/**
|
|
206
|
+
* P9-09: Global hooks (SvelteKit-style `handle`/`handleFetch`/`handleError`).
|
|
207
|
+
*
|
|
208
|
+
* - `handle` wraps every request (including 404 and static assets)
|
|
209
|
+
* - `handleFetch` intercepts server-side `fetch()` calls
|
|
210
|
+
* - `handleError` is called on uncaught errors for logging/reporting
|
|
211
|
+
*/
|
|
212
|
+
globalHooks?: GlobalHooks;
|
|
44
213
|
/**
|
|
45
214
|
* Called after `UbeanApp` is created but before `app.init()`.
|
|
46
215
|
* Useful for initializing databases, external service connections, etc.
|
|
@@ -58,6 +227,7 @@ interface DefineServerOptions {
|
|
|
58
227
|
interface ResolvedServerConfig {
|
|
59
228
|
plugins: UbeanAppPlugin[];
|
|
60
229
|
hooks: ServerHooks;
|
|
230
|
+
globalHooks?: GlobalHooks;
|
|
61
231
|
onAppCreate?: (app: UbeanApp) => void | Promise<void>;
|
|
62
232
|
onServerReady?: (app: UbeanApp) => void | Promise<void>;
|
|
63
233
|
}
|
|
@@ -84,6 +254,16 @@ interface ResolvedServerConfig {
|
|
|
84
254
|
* hooks: {
|
|
85
255
|
* 'request:start': (c) => { console.log(c.req.method, c.req.path); }
|
|
86
256
|
* },
|
|
257
|
+
* globalHooks: {
|
|
258
|
+
* handle: async ({ event, resolve }) => {
|
|
259
|
+
* const response = await resolve(event);
|
|
260
|
+
* response.headers.set('X-Custom', 'ubean');
|
|
261
|
+
* return response;
|
|
262
|
+
* },
|
|
263
|
+
* handleError: async ({ error, status }) => {
|
|
264
|
+
* console.error(`[${status}]`, error);
|
|
265
|
+
* }
|
|
266
|
+
* },
|
|
87
267
|
* onAppCreate: async (app) => { /* init db *\/ },
|
|
88
268
|
* onServerReady: async (app) => { /* start workers *\/ }
|
|
89
269
|
* });
|
|
@@ -158,6 +338,18 @@ interface UbeanAppOptions {
|
|
|
158
338
|
pageAssetTags?: PageAssetTags;
|
|
159
339
|
/** 不进行 SSR 的路由模式列表(glob),匹配的页面走 CSR */
|
|
160
340
|
ssrExclude?: string[];
|
|
341
|
+
/**
|
|
342
|
+
* 启用流式 SSR。`true` 时页面响应以 `ReadableStream` 分块输出
|
|
343
|
+
* (头部先发送,app HTML 边渲染边输出),改善 TTFB/LCP。
|
|
344
|
+
* renderer 不支持流式时自动降级为缓冲渲染。
|
|
345
|
+
*/
|
|
346
|
+
streaming?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* 爬虫降级(P9-24):当 `streaming` 启用且检测到爬虫/社交预览 UA 时,
|
|
349
|
+
* 自动降级为缓冲渲染以保证 metadata 出现在初始 `<head>`。
|
|
350
|
+
* 默认 `true`。设为 `false` 可禁用爬虫检测(不推荐)。
|
|
351
|
+
*/
|
|
352
|
+
botFallback?: boolean;
|
|
161
353
|
publicDir?: string;
|
|
162
354
|
healthEndpoint?: boolean;
|
|
163
355
|
openAPI?: boolean | {
|
|
@@ -174,6 +366,12 @@ interface UbeanAppOptions {
|
|
|
174
366
|
};
|
|
175
367
|
/** `pages/404.vue` 自动检测的 404 页面,注册为 Hono 兜底处理器 */
|
|
176
368
|
notFoundPage?: ScannedPageRoute$1;
|
|
369
|
+
/**
|
|
370
|
+
* Pre-rendered no-FOUC color-mode script (from `getColorModeScript`).
|
|
371
|
+
* Injected into `<head>` of every SSR/prerendered HTML response. Covers
|
|
372
|
+
* the SSG/prerender path that bypasses Vite's `transformIndexHtml`.
|
|
373
|
+
*/
|
|
374
|
+
colorModeScript?: string;
|
|
177
375
|
}
|
|
178
376
|
interface UbeanAppPlugin {
|
|
179
377
|
name: string;
|
|
@@ -212,4 +410,4 @@ interface AppPlugin {
|
|
|
212
410
|
}
|
|
213
411
|
declare function createUbeanApp(options?: UbeanAppOptions): UbeanApp;
|
|
214
412
|
//#endregion
|
|
215
|
-
export { type AppPlugin, type ComposedHandler, type DefineServerOptions, type PageAssetTags, type PageRenderer, type RegisterOptions, type ResolvedServerConfig, type RouteMeta, type RouteRegistrar, type RouteRule, type ScannedApiRoute, type ScannedLayout, type ScannedMiddleware, type ScannedPageRoute, type ServerHooks, UbeanApp, type UbeanAppOptions, type UbeanAppPlugin, type UbeanEnv, type UbeanMiddleware, type UbeanRuntimeHooks, applyServerConfig, createDefaultServerConfig, createUbeanApp, defineServer, mergeServerConfigs };
|
|
413
|
+
export { type AppPlugin, type ComposedHandler, type DefineServerOptions, type GlobalHooks, type Handle, type HandleError, type HandleErrorInput, type HandleEvent, type HandleFetch, type HandleFetchInput, type HandleInput, type PageAssetTags, type PageRenderer, type RegisterOptions, type ResolvedServerConfig, type RouteMeta, type RouteRegistrar, type RouteRule, type ScannedApiRoute, type ScannedLayout, type ScannedMiddleware, type ScannedPageRoute, type ServerHooks, UbeanApp, type UbeanAppOptions, type UbeanAppPlugin, type UbeanEnv, type UbeanMiddleware, type UbeanRuntimeHooks, applyHandleErrorHook, applyHandleFetchHook, applyHandleHook, applyServerConfig, clearGlobalHooks, createDefaultServerConfig, createHandleEvent, createUbeanApp, defineServer, extractErrorMessage, getGlobalHooks, mergeServerConfigs, setGlobalHooks, wrapResolve };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,117 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { ACTIONS_ENDPOINT, createActionsMiddleware } from "@ubean/actions";
|
|
4
|
+
import { createRouteRulesMiddleware, registerOpenAPIRoutes, registerRoutes, setInternalFetcher } from "@ubean/routes";
|
|
5
|
+
import { UbeanError, errorToResponse, isUbeanError } from "@ubean/shared";
|
|
6
|
+
import { SERVER_COMPONENT_ENDPOINT, createServerComponentMiddleware } from "@ubean/islands/server";
|
|
7
|
+
import { createCacheMiddleware, createMemoryStore, resolveRouteCacheRules, useCacheStore } from "@ubean/server/cache";
|
|
8
|
+
import { serveStatic } from "@ubean/server/static";
|
|
9
|
+
import { createWebSocketMiddleware } from "@ubean/server/realtime";
|
|
6
10
|
import { requestId } from "hono/request-id";
|
|
7
11
|
import { createHooks } from "hookable";
|
|
8
12
|
import { isAbsolute, join } from "pathe";
|
|
13
|
+
//#region src/hooks.ts
|
|
14
|
+
let globalHooks = {};
|
|
15
|
+
/**
|
|
16
|
+
* 设置全局 hooks。
|
|
17
|
+
*
|
|
18
|
+
* 由 `applyServerConfig` 在应用启动时调用。
|
|
19
|
+
*/
|
|
20
|
+
function setGlobalHooks(hooks) {
|
|
21
|
+
globalHooks = { ...hooks };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 获取全局 hooks。
|
|
25
|
+
*/
|
|
26
|
+
function getGlobalHooks() {
|
|
27
|
+
return globalHooks;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 清空全局 hooks(测试用)。
|
|
31
|
+
*/
|
|
32
|
+
function clearGlobalHooks() {
|
|
33
|
+
globalHooks = {};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 从 Hono Context 构建请求事件。
|
|
37
|
+
*/
|
|
38
|
+
function createHandleEvent(c) {
|
|
39
|
+
const requestId = c.get("requestId") || c.req.header("x-request-id") || "";
|
|
40
|
+
const clientAddress = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
41
|
+
return {
|
|
42
|
+
request: c.req.raw,
|
|
43
|
+
context: c,
|
|
44
|
+
clientAddress,
|
|
45
|
+
requestId
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 从错误中提取安全消息(移除敏感信息)。
|
|
50
|
+
*
|
|
51
|
+
* - 404 → "Not Found"
|
|
52
|
+
* - 5xx → "Internal Server Error"(生产环境不暴露内部错误详情,
|
|
53
|
+
* 也不区分 Error / 非 Error 类型,避免信息泄漏)
|
|
54
|
+
* - 4xx(非 404)→ 返回 `error.message`(仅当 error 是 Error 实例),
|
|
55
|
+
* 否则返回 "Bad Request"
|
|
56
|
+
*/
|
|
57
|
+
function extractErrorMessage(error, status) {
|
|
58
|
+
if (status === 404) return "Not Found";
|
|
59
|
+
if (status >= 500) return "Internal Server Error";
|
|
60
|
+
if (error instanceof Error) return error.message;
|
|
61
|
+
return "Bad Request";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 包装 resolve 函数:将 Hono 的请求处理流程封装为 `resolve(event)` 调用。
|
|
65
|
+
*
|
|
66
|
+
* 返回一个函数,调用后会执行 `next()` 让 Hono 处理请求,然后返回 `c.res`。
|
|
67
|
+
*/
|
|
68
|
+
function wrapResolve(c, next) {
|
|
69
|
+
return async (_event) => {
|
|
70
|
+
await next();
|
|
71
|
+
return c.res;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 应用 `handle` hook:如果已注册,则用 hook 包裹请求处理流程。
|
|
76
|
+
*
|
|
77
|
+
* 返回 `true` 表示 hook 已处理请求(调用方应跳过后续中间件),
|
|
78
|
+
* `false` 表示无 hook 或 hook 已调用 resolve。
|
|
79
|
+
*/
|
|
80
|
+
async function applyHandleHook(c, next) {
|
|
81
|
+
const hook = globalHooks.handle;
|
|
82
|
+
if (!hook) return false;
|
|
83
|
+
c.res = await hook({
|
|
84
|
+
event: createHandleEvent(c),
|
|
85
|
+
resolve: wrapResolve(c, next)
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 应用 `handleFetch` hook:如果已注册,则用 hook 包裹 fetch 调用。
|
|
91
|
+
*/
|
|
92
|
+
async function applyHandleFetchHook(request, defaultFetch, serverContext) {
|
|
93
|
+
const hook = globalHooks.handleFetch;
|
|
94
|
+
if (!hook) return defaultFetch(request);
|
|
95
|
+
return hook({
|
|
96
|
+
request,
|
|
97
|
+
fetch: defaultFetch,
|
|
98
|
+
serverContext
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 应用 `handleError` hook:如果已注册,则调用它。
|
|
103
|
+
*/
|
|
104
|
+
async function applyHandleErrorHook(c, error, status) {
|
|
105
|
+
const hook = globalHooks.handleError;
|
|
106
|
+
if (!hook) return;
|
|
107
|
+
await hook({
|
|
108
|
+
event: createHandleEvent(c),
|
|
109
|
+
error,
|
|
110
|
+
status,
|
|
111
|
+
message: extractErrorMessage(error, status)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
9
115
|
//#region src/define-server.ts
|
|
10
116
|
/**
|
|
11
117
|
* Apply a resolved server config to a `UbeanApp` instance.
|
|
@@ -14,6 +120,7 @@ import { isAbsolute, join } from "pathe";
|
|
|
14
120
|
* - `plugins` are available when `init()` calls `setup` / `ready`
|
|
15
121
|
* - `hooks` are registered before `init()` fires `app:created` etc.
|
|
16
122
|
* - `onAppCreate` runs before route registration
|
|
123
|
+
* - `globalHooks` (handle/handleFetch/handleError) are set before any request
|
|
17
124
|
*
|
|
18
125
|
* `onServerReady` is NOT called here — the caller must invoke it after
|
|
19
126
|
* `app.init()` completes.
|
|
@@ -21,6 +128,7 @@ import { isAbsolute, join } from "pathe";
|
|
|
21
128
|
async function applyServerConfig(app, config) {
|
|
22
129
|
if (config.plugins.length > 0) app.plugins.push(...config.plugins);
|
|
23
130
|
for (const [name, handler] of Object.entries(config.hooks)) if (handler) app.hooks.hook(name, handler);
|
|
131
|
+
if (config.globalHooks) setGlobalHooks(config.globalHooks);
|
|
24
132
|
if (config.onAppCreate) await config.onAppCreate(app);
|
|
25
133
|
}
|
|
26
134
|
/**
|
|
@@ -46,6 +154,16 @@ async function applyServerConfig(app, config) {
|
|
|
46
154
|
* hooks: {
|
|
47
155
|
* 'request:start': (c) => { console.log(c.req.method, c.req.path); }
|
|
48
156
|
* },
|
|
157
|
+
* globalHooks: {
|
|
158
|
+
* handle: async ({ event, resolve }) => {
|
|
159
|
+
* const response = await resolve(event);
|
|
160
|
+
* response.headers.set('X-Custom', 'ubean');
|
|
161
|
+
* return response;
|
|
162
|
+
* },
|
|
163
|
+
* handleError: async ({ error, status }) => {
|
|
164
|
+
* console.error(`[${status}]`, error);
|
|
165
|
+
* }
|
|
166
|
+
* },
|
|
49
167
|
* onAppCreate: async (app) => { /* init db *\/ },
|
|
50
168
|
* onServerReady: async (app) => { /* start workers *\/ }
|
|
51
169
|
* });
|
|
@@ -55,6 +173,7 @@ function defineServer(options) {
|
|
|
55
173
|
return {
|
|
56
174
|
plugins: options.plugins || [],
|
|
57
175
|
hooks: options.hooks || {},
|
|
176
|
+
globalHooks: options.globalHooks,
|
|
58
177
|
onAppCreate: options.onAppCreate,
|
|
59
178
|
onServerReady: options.onServerReady
|
|
60
179
|
};
|
|
@@ -78,6 +197,7 @@ function mergeServerConfigs(base, ...configs) {
|
|
|
78
197
|
const result = {
|
|
79
198
|
plugins: [...base.plugins],
|
|
80
199
|
hooks: { ...base.hooks },
|
|
200
|
+
globalHooks: base.globalHooks ? { ...base.globalHooks } : void 0,
|
|
81
201
|
onAppCreate: base.onAppCreate,
|
|
82
202
|
onServerReady: base.onServerReady
|
|
83
203
|
};
|
|
@@ -85,6 +205,10 @@ function mergeServerConfigs(base, ...configs) {
|
|
|
85
205
|
if (!cfg) continue;
|
|
86
206
|
if (cfg.plugins) result.plugins.push(...cfg.plugins);
|
|
87
207
|
if (cfg.hooks) Object.assign(result.hooks, cfg.hooks);
|
|
208
|
+
if (cfg.globalHooks) result.globalHooks = {
|
|
209
|
+
...result.globalHooks,
|
|
210
|
+
...cfg.globalHooks
|
|
211
|
+
};
|
|
88
212
|
if (cfg.onAppCreate) result.onAppCreate = cfg.onAppCreate;
|
|
89
213
|
if (cfg.onServerReady) result.onServerReady = cfg.onServerReady;
|
|
90
214
|
}
|
|
@@ -107,13 +231,17 @@ var UbeanApp = class {
|
|
|
107
231
|
this._setupFallback();
|
|
108
232
|
}
|
|
109
233
|
_setupBaseMiddleware() {
|
|
234
|
+
this.hono.use("*", async (c, next) => {
|
|
235
|
+
if (!await applyHandleHook(c, next)) await next();
|
|
236
|
+
});
|
|
110
237
|
this.hono.use("*", requestId());
|
|
111
238
|
if (this.options.routeRules && Object.keys(this.options.routeRules).length > 0) {
|
|
112
239
|
this.hono.use("*", createRouteRulesMiddleware(this.options.routeRules));
|
|
113
240
|
const cacheRules = resolveRouteCacheRules(this.options.routeRules);
|
|
114
|
-
|
|
241
|
+
const hasIsrRules = Object.values(this.options.routeRules).some((r) => r?.isr !== void 0);
|
|
242
|
+
if (Object.keys(cacheRules).length > 0 || hasIsrRules) {
|
|
115
243
|
useCacheStore(createMemoryStore());
|
|
116
|
-
this.hono.use("*", createCacheMiddleware({ rules: cacheRules }));
|
|
244
|
+
if (Object.keys(cacheRules).length > 0) this.hono.use("*", createCacheMiddleware({ rules: cacheRules }));
|
|
117
245
|
}
|
|
118
246
|
}
|
|
119
247
|
this.hono.use("*", createWebSocketMiddleware());
|
|
@@ -159,16 +287,22 @@ var UbeanApp = class {
|
|
|
159
287
|
pageRenderer: this.options.pageRenderer ?? null,
|
|
160
288
|
pageAssetTags: this.options.pageAssetTags ?? {},
|
|
161
289
|
ssrExclude: this.options.ssrExclude,
|
|
290
|
+
streaming: this.options.streaming,
|
|
291
|
+
botFallback: this.options.botFallback,
|
|
162
292
|
i18nConfig: this.options.i18nConfig,
|
|
163
|
-
notFoundPage: this.options.notFoundPage
|
|
293
|
+
notFoundPage: this.options.notFoundPage,
|
|
294
|
+
colorModeScript: this.options.colorModeScript,
|
|
295
|
+
cacheStore: this.options.routeRules && Object.keys(this.options.routeRules).length > 0 ? useCacheStore() : void 0
|
|
164
296
|
};
|
|
165
297
|
await registerRoutes(this, registerOpts);
|
|
298
|
+
this.hono.on("POST", ACTIONS_ENDPOINT, createActionsMiddleware());
|
|
299
|
+
this.hono.on("POST", SERVER_COMPONENT_ENDPOINT, createServerComponentMiddleware());
|
|
166
300
|
if (this.options.openAPI) {
|
|
167
301
|
const openAPIOpts = typeof this.options.openAPI === "object" ? this.options.openAPI : {};
|
|
168
302
|
registerOpenAPIRoutes(this.hono, openAPIOpts);
|
|
169
303
|
}
|
|
170
304
|
await this.hooks.callHook("app:after:register", this.hono);
|
|
171
|
-
setInternalFetcher((req) => this.hono.fetch(
|
|
305
|
+
setInternalFetcher((req) => applyHandleFetchHook(req, (r) => Promise.resolve(this.hono.fetch(r))));
|
|
172
306
|
for (const plugin of this.plugins) if (plugin.ready) await plugin.ready(this);
|
|
173
307
|
this._ready = true;
|
|
174
308
|
return this;
|
|
@@ -193,6 +327,7 @@ var UbeanApp = class {
|
|
|
193
327
|
});
|
|
194
328
|
this.hono.onError((err, c) => {
|
|
195
329
|
this.hooks.callHook("error", err, c);
|
|
330
|
+
applyHandleErrorHook(c, err, isUbeanError(err) ? err.statusCode : 500);
|
|
196
331
|
if (isUbeanError(err)) return errorToResponse(c, err);
|
|
197
332
|
return errorToResponse(c, new UbeanError(500, err.message || "Internal Server Error"));
|
|
198
333
|
});
|
|
@@ -239,4 +374,4 @@ function createUbeanApp(options = {}) {
|
|
|
239
374
|
return new UbeanApp(options);
|
|
240
375
|
}
|
|
241
376
|
//#endregion
|
|
242
|
-
export { UbeanApp, applyServerConfig, createDefaultServerConfig, createUbeanApp, defineServer, mergeServerConfigs };
|
|
377
|
+
export { UbeanApp, applyHandleErrorHook, applyHandleFetchHook, applyHandleHook, applyServerConfig, clearGlobalHooks, createDefaultServerConfig, createHandleEvent, createUbeanApp, defineServer, extractErrorMessage, getGlobalHooks, mergeServerConfigs, setGlobalHooks, wrapResolve };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Hono app factory and server config for ubean (createUbeanApp, defineServer)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -16,29 +16,26 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"hono": "4.
|
|
19
|
+
"hono": "4.13.2",
|
|
20
20
|
"hookable": "^6.1.1",
|
|
21
21
|
"pathe": "^2.0.3",
|
|
22
|
-
"@ubean/
|
|
23
|
-
"@ubean/
|
|
24
|
-
"@ubean/
|
|
25
|
-
"@ubean/
|
|
22
|
+
"@ubean/actions": "0.2.0",
|
|
23
|
+
"@ubean/islands": "0.2.0",
|
|
24
|
+
"@ubean/scan": "0.2.0",
|
|
25
|
+
"@ubean/routes": "0.2.0",
|
|
26
|
+
"@ubean/server": "0.2.0",
|
|
27
|
+
"@ubean/shared": "0.2.0"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^26.
|
|
30
|
+
"@types/node": "^26.2.0",
|
|
29
31
|
"typescript": "7.0.2",
|
|
30
|
-
"vite-plus": "0.2.
|
|
31
|
-
"@ubean/
|
|
32
|
-
"@ubean/pages": "0.1.13"
|
|
32
|
+
"vite-plus": "0.2.9",
|
|
33
|
+
"@ubean/pages": "0.2.0"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
|
-
"@ubean/pages": "0.
|
|
36
|
-
"@ubean/routing": "0.1.13"
|
|
36
|
+
"@ubean/pages": "0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
|
-
"@ubean/routing": {
|
|
40
|
-
"optional": true
|
|
41
|
-
},
|
|
42
39
|
"@ubean/pages": {
|
|
43
40
|
"optional": true
|
|
44
41
|
}
|