@ubean/routes 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soybean
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,334 @@
1
+ import { ScannedApiRoute, ScannedLayout, ScannedMiddleware, ScannedPageRoute } from "@ubean/scan";
2
+ import { ComposedHandler, GenericSchema, Input, IsrRule, IsrRule as IsrRule$1, RouteMeta, RouteMeta as RouteMeta$1, RouteRule, RouteRule as RouteRule$1, Span, SpanAttributes, SpanContext, SpanEndOptions, SpanEvent, SpanOptions, SpanStatus, UbeanBindings, UbeanContext, UbeanEnv, UbeanEnv as UbeanEnv$1, UbeanHandler, UbeanMiddleware, UbeanMiddleware as UbeanMiddleware$1, UbeanVariables } from "@ubean/shared";
3
+ import { Context, Hono, MiddlewareHandler, Next } from "hono";
4
+ import { H, HandlerResponse, Input as Input$1 } from "hono/types";
5
+ import { FetchAdapter } from "@soybeanjs/fetch";
6
+ //#region src/handler.d.ts
7
+ interface MiddlewareChain extends Array<MiddlewareHandler> {
8
+ __routeMeta?: RouteMeta$1;
9
+ }
10
+ declare function defineHandler<I extends Input$1 = {}, R extends HandlerResponse<any> = any>(handler1: H<UbeanEnv$1, any, I, R>): [H<UbeanEnv$1, any, I, R>];
11
+ declare function defineHandler<I extends Input$1 = {}, I2 extends Input$1 = I, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any>(handler1: H<UbeanEnv$1, any, I, R>, handler2: H<UbeanEnv$1, any, I & I2, R2>): [H<UbeanEnv$1, any, I, R>, H<UbeanEnv$1, any, I & I2, R2>];
12
+ declare function defineHandler<I extends Input$1 = {}, I2 extends Input$1 = I, I3 extends Input$1 = I & I2, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any>(handler1: H<UbeanEnv$1, any, I, R>, handler2: H<UbeanEnv$1, any, I2, R2>, handler3: H<UbeanEnv$1, any, I3, R3>): [H<UbeanEnv$1, any, I, R>, H<UbeanEnv$1, any, I2, R2>, H<UbeanEnv$1, any, I3, R3>];
13
+ declare function defineHandler<I extends Input$1 = {}, I2 extends Input$1 = I, I3 extends Input$1 = I & I2, I4 extends Input$1 = I & I2 & I3, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any>(handler1: H<UbeanEnv$1, any, I, R>, handler2: H<UbeanEnv$1, any, I2, R2>, handler3: H<UbeanEnv$1, any, I3, R3>, handler4: H<UbeanEnv$1, any, I4, R4>): [H<UbeanEnv$1, any, I, R>, H<UbeanEnv$1, any, I2, R2>, H<UbeanEnv$1, any, I3, R3>, H<UbeanEnv$1, any, I4, R4>];
14
+ declare function defineHandler<I extends Input$1 = {}, I2 extends Input$1 = I, I3 extends Input$1 = I & I2, I4 extends Input$1 = I & I2 & I3, I5 extends Input$1 = I & I2 & I3 & I4, R extends HandlerResponse<any> = any, R2 extends HandlerResponse<any> = any, R3 extends HandlerResponse<any> = any, R4 extends HandlerResponse<any> = any, R5 extends HandlerResponse<any> = any>(handler1: H<UbeanEnv$1, any, I, R>, handler2: H<UbeanEnv$1, any, I2, R2>, handler3: H<UbeanEnv$1, any, I3, R3>, handler4: H<UbeanEnv$1, any, I4, R4>, handler5: H<UbeanEnv$1, any, I5, R5>): [H<UbeanEnv$1, any, I, R>, H<UbeanEnv$1, any, I2, R2>, H<UbeanEnv$1, any, I3, R3>, H<UbeanEnv$1, any, I4, R4>, H<UbeanEnv$1, any, I5, R5>];
15
+ declare function isHandlerChain(obj: unknown): obj is MiddlewareChain;
16
+ declare function extractRouteMeta(handlers: MiddlewareHandler[]): RouteMeta$1;
17
+ declare function defineHandlerMeta(meta: Partial<RouteMeta$1>): (_c: Context<UbeanEnv$1>, next: Next) => Promise<void>;
18
+ declare function defineMiddleware(handler: MiddlewareHandler<UbeanEnv$1>): MiddlewareHandler<UbeanEnv$1>;
19
+ //#endregion
20
+ //#region src/server-router.d.ts
21
+ interface CompiledRoute {
22
+ method: string;
23
+ path: string;
24
+ id: string;
25
+ filePath: string;
26
+ meta?: Record<string, unknown>;
27
+ }
28
+ interface CompiledMiddleware {
29
+ path: string;
30
+ filePath: string;
31
+ order: number;
32
+ global: boolean;
33
+ }
34
+ interface CompiledPage {
35
+ name: string;
36
+ path: string;
37
+ filePath: string;
38
+ layout?: string | string[] | false;
39
+ reuseTarget?: string;
40
+ }
41
+ interface CompiledLayout {
42
+ name: string;
43
+ filePath: string;
44
+ isDefault: boolean;
45
+ }
46
+ declare class UbeanRouter {
47
+ private apiContext;
48
+ private middlewares;
49
+ private pages;
50
+ private layouts;
51
+ constructor();
52
+ addApiRoute(route: ScannedApiRoute): void;
53
+ addMiddleware(mw: ScannedMiddleware): void;
54
+ addPage(page: ScannedPageRoute): void;
55
+ addLayout(layout: ScannedLayout): void;
56
+ matchApi(method: string, path: string): CompiledRoute | undefined;
57
+ getMiddlewares(): CompiledMiddleware[];
58
+ getPages(): CompiledPage[];
59
+ getPage(name: string): CompiledPage | undefined;
60
+ getLayout(name: string): CompiledLayout | undefined;
61
+ getDefaultLayout(): CompiledLayout | undefined;
62
+ getLayouts(): CompiledLayout[];
63
+ getPageRouteNames(): string[];
64
+ }
65
+ declare function useRouter(): UbeanRouter;
66
+ /**
67
+ * 创建服务端 rou3 router 实例(无参数)。
68
+ *
69
+ * 命名说明:与 `@ubean/client` 的 `createUbeanRouter`(Vue Router 工厂,
70
+ * 接受 options)曾是同名冲突,服务端版本改名 `createServerRouter` 彻底消歧。
71
+ */
72
+ declare function createServerRouter(): UbeanRouter;
73
+ //#endregion
74
+ //#region src/isr.d.ts
75
+ /**
76
+ * ISR 缓存条目(内部表示,与 @ubean/server 的 CacheEntry 结构兼容)。
77
+ */
78
+ interface IsrCacheEntryInternal {
79
+ body: ArrayBuffer;
80
+ headers: Record<string, string>;
81
+ status: number;
82
+ statusText: string;
83
+ createdAt: number;
84
+ expiresAt: number;
85
+ }
86
+ /**
87
+ * ISR 缓存存储接口。与 `@ubean/server` 的 `CacheStore` 结构兼容 ——
88
+ * 调用方注入任何满足此形状的实例即可(无需真正依赖 @ubean/server)。
89
+ */
90
+ interface IsrCacheStore {
91
+ get(key: string): Promise<IsrCacheEntryInternal | undefined>;
92
+ set(key: string, entry: Omit<IsrCacheEntryInternal, 'createdAt' | 'expiresAt'>, ttl: number): Promise<void>;
93
+ delete(key: string): Promise<boolean>;
94
+ clear(): Promise<void>;
95
+ /** 返回 entry(无论是否过期)。不实现时退化为 `get`(SWR 自动降级为同步重新生成) */
96
+ peek?(key: string): Promise<IsrCacheEntryInternal | undefined>;
97
+ }
98
+ interface IsrCacheEntry {
99
+ html: string;
100
+ status: number;
101
+ headers: Record<string, string>;
102
+ }
103
+ interface IsrServeOptions {
104
+ /** 当前请求的路径名(用于构建 cache key) */
105
+ pathname: string;
106
+ /** ISR 规则(已规范化) */
107
+ rule: IsrRule$1;
108
+ /** 渲染函数:同步重新生成时调用 */
109
+ render: () => Promise<{
110
+ html: string;
111
+ status?: number;
112
+ headers?: Record<string, string>;
113
+ }>;
114
+ /** cache store(必传 —— 由调用方通过 RegisterOptions.cacheStore 注入) */
115
+ store: IsrCacheStore;
116
+ }
117
+ declare function buildIsrCacheKey(pathname: string): string;
118
+ /**
119
+ * 读取缓存的 ISR entry(仅未过期)。
120
+ */
121
+ declare function getIsrCache(store: IsrCacheStore, pathname: string): Promise<IsrCacheEntry | undefined>;
122
+ /**
123
+ * 读取缓存的 ISR entry(即使已过期,用于 SWR)。
124
+ * 后端不支持 `peek` 时退化为 `get`(无 SWR)。
125
+ */
126
+ declare function getStaleIsrCache(store: IsrCacheStore, pathname: string): Promise<IsrCacheEntry | undefined>;
127
+ /**
128
+ * 写入 ISR 缓存。
129
+ */
130
+ declare function setIsrCache(store: IsrCacheStore, pathname: string, entry: IsrCacheEntry, ttl: number): Promise<void>;
131
+ /**
132
+ * 失效指定路径的 ISR 缓存。需要调用方提供 store(避免本包依赖全局单例)。
133
+ */
134
+ declare function invalidateIsrCache(store: IsrCacheStore, pathname: string): Promise<boolean>;
135
+ /**
136
+ * 失效所有匹配模式的 ISR 缓存。仅对内存存储有效(读取底层 Map)。
137
+ */
138
+ declare function invalidateIsrCachePattern(store: IsrCacheStore, pattern: RegExp): Promise<number>;
139
+ /**
140
+ * 判断 entry 是否已过期。配合 `getStaleIsrCache` 用于 SWR 判定。
141
+ * 仅内存存储可用 —— 其他后端在 `getStaleIsrCache` 已退化为 `get`(永远 fresh),
142
+ * 不会进入此分支。
143
+ */
144
+ declare function isIsrEntryStale(pathname: string, store: IsrCacheStore): boolean;
145
+ /**
146
+ * ISR 主入口:检查缓存 → 命中则返回,未命中/过期则调用 render 重新生成。
147
+ *
148
+ * 返回 `Response` 表示 ISR 处理完成(直接返回给客户端),
149
+ * 返回 `undefined` 表示无 ISR 规则或调用方应自行处理(由 `serveIsr` 内部逻辑决定)。
150
+ *
151
+ * 实现说明:使用 `peek`(若可用)而非 `get` 来读取缓存,避免 `get` 在
152
+ * 过期时删除 entry 导致 SWR 无法读取旧内容。后端不支持 `peek` 时退化为 `get`
153
+ * (SWR 自动降级为同步重新生成)。
154
+ */
155
+ declare function serveIsr(_c: Context<UbeanEnv$1>, options: IsrServeOptions): Promise<Response | undefined>;
156
+ /**
157
+ * 从 context 读取匹配到的 routeRule,提取 ISR 规则。
158
+ * 返回 `undefined` 表示该路由未启用 ISR。
159
+ */
160
+ declare function getIsrRuleFromContext(c: Context<UbeanEnv$1>): IsrRule$1 | undefined;
161
+ //#endregion
162
+ //#region src/router.d.ts
163
+ /**
164
+ * Structural type for the route registrar app.
165
+ *
166
+ * Avoids a hard dependency on `@ubean/app` (T2-6). The actual `UbeanApp`
167
+ * returned by `createUbeanApp()` satisfies this interface because it
168
+ * extends `Hono<UbeanEnv>` and adds a `hooks` property from `hookable`.
169
+ */
170
+ interface RouteRegistrar {
171
+ on(method: string, path: string, ...handlers: MiddlewareHandler[]): void;
172
+ on(methods: string[], path: string, ...handlers: MiddlewareHandler[]): void;
173
+ get(path: string, ...handlers: MiddlewareHandler[]): void;
174
+ post(path: string, ...handlers: MiddlewareHandler[]): void;
175
+ hooks: {
176
+ callHook(name: string, ...args: any[]): Promise<void>;
177
+ };
178
+ }
179
+ interface RegisterOptions {
180
+ routes: ScannedApiRoute[];
181
+ middleware: ScannedMiddleware[];
182
+ pages: ScannedPageRoute[];
183
+ layouts?: ScannedLayout[];
184
+ routeLoaders: Record<string, () => Promise<Record<string, unknown>>>;
185
+ middlewareLoaders: Record<string, () => Promise<{
186
+ default?: UbeanMiddleware$1;
187
+ }>>;
188
+ pageRenderer?: unknown;
189
+ pageAssetTags?: unknown;
190
+ pageLoaders?: Record<string, () => Promise<any>>;
191
+ i18nConfig?: {
192
+ strategy?: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
193
+ defaultLocale?: string;
194
+ locales?: string[];
195
+ };
196
+ /**
197
+ * 不进行 SSR 的路由模式列表(glob)。
198
+ * 匹配的页面将跳过服务端渲染,返回客户端渲染 shell。
199
+ */
200
+ ssrExclude?: string[];
201
+ /**
202
+ * 启用流式 SSR。当为 `true` 且 pageRenderer 提供 `renderToStream` 时,
203
+ * 页面响应将以 `ReadableStream` 形式分块输出(头部先发送,app HTML 边渲染边输出),
204
+ * 显著改善 TTFB/LCP。回退:renderer 不支持流式时自动降级为缓冲渲染。
205
+ */
206
+ streaming?: boolean;
207
+ /**
208
+ * 爬虫降级(P9-24 / Task 6 流式 metadata):当 `streaming` 启用且检测到
209
+ * 爬虫/社交预览 UA(Facebook OG、Twitter、Slack 等)时,自动降级为缓冲渲染,
210
+ * 保证动态 `<head>` metadata 出现在初始响应中(社交爬虫只解析初始 `<head>`,
211
+ * 不会执行流式尾部的 metadata 注入)。默认 `true`(仅在 streaming 启用时生效)。
212
+ */
213
+ botFallback?: boolean;
214
+ /**
215
+ * `pages/404.vue` 自动检测的 404 页面。
216
+ * 注册为 Vue Router catch-all 路由的兜底,同时注册 Hono 的 `GET *` 兜底处理器。
217
+ */
218
+ notFoundPage?: ScannedPageRoute;
219
+ /**
220
+ * ISR 缓存存储(P9-03)。与 `@ubean/server` 的 `CacheStore` 结构兼容。
221
+ * 启用 `routeRules[*].isr` 时必须传入,否则 ISR 规则被忽略。
222
+ */
223
+ cacheStore?: IsrCacheStore;
224
+ /**
225
+ * Pre-rendered no-FOUC color-mode script (from `getColorModeScript`).
226
+ * Injected as the first child of `<head>` in every SSR/prerendered HTML
227
+ * response so the correct theme class is set before first paint.
228
+ * `transformIndexHtml` only runs in dev — SSG/prerender bypasses it, so
229
+ * the script must be injected here to cover production HTML.
230
+ */
231
+ colorModeScript?: string;
232
+ }
233
+ /**
234
+ * Sort page routes so catch-all routes (`/**:slug`, converted to Hono `*`)
235
+ * are registered last. Hono's RegExpRouter matches in registration order,
236
+ * so a catch-all registered before a specific path (e.g. `/`) would swallow
237
+ * it. File-system sort order puts `[...slug].vue` before `index.vue`
238
+ * (`[` < `i`), which is wrong for Hono — this sort corrects it.
239
+ *
240
+ * Stable: non-catch-all pages keep their original relative order.
241
+ */
242
+ declare function sortPagesForRegistration<T extends {
243
+ route: string;
244
+ }>(pages: readonly T[]): T[];
245
+ declare function registerApiRoutes(app: RouteRegistrar, options: RegisterOptions): Promise<void>;
246
+ declare function registerPageRoutes(app: RouteRegistrar, options: RegisterOptions): Promise<void>;
247
+ declare function registerRoutes(app: RouteRegistrar, options: RegisterOptions): Promise<void>;
248
+ declare function createRouteLoader(importMetaGlob: Record<string, () => Promise<any>>): {
249
+ [k: string]: () => Promise<any>;
250
+ };
251
+ //#endregion
252
+ //#region src/route-rules.d.ts
253
+ interface CompiledRouteRule {
254
+ pattern: RegExp;
255
+ rule: RouteRule$1;
256
+ /** 原始路径模式(用于路径特异性计算) */
257
+ path: string;
258
+ }
259
+ declare function compileRouteRules(rules: Record<string, RouteRule$1>): CompiledRouteRule[];
260
+ /**
261
+ * 规范化 `isr` 字段为 `IsrRule` 对象形式。
262
+ * `number` → `{ ttl: n }`,`undefined` → `undefined`。
263
+ */
264
+ declare function normalizeIsrRule(isr: RouteRule$1['isr']): IsrRule$1 | undefined;
265
+ declare function matchRouteRules(path: string, compiledRules: CompiledRouteRule[]): RouteRule$1;
266
+ declare function createRouteRulesMiddleware(rules: Record<string, RouteRule$1>): (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<undefined, 301 | 302 | 303 | 307 | 308, "redirect">) | undefined>;
267
+ //#endregion
268
+ //#region src/internal-fetch.d.ts
269
+ interface InternalFetchOptions {
270
+ baseURL?: string;
271
+ headers?: Record<string, string>;
272
+ credentials?: 'include' | 'omit' | 'same-origin';
273
+ forwardHeaders?: string[];
274
+ }
275
+ type AppFetcher = (request: Request) => Response | Promise<Response>;
276
+ declare function setInternalFetcher(fetcher: AppFetcher): void;
277
+ declare function getInternalFetcher(): AppFetcher | null;
278
+ declare function clearInternalFetcher(): void;
279
+ /**
280
+ * 创建进程内调度适配器。
281
+ *
282
+ * 将注册的 Hono `app.fetch` 包装为 `@soybeanjs/fetch` 的 `FetchAdapter`,
283
+ * 使其可通过 `createRequest` / `createTypedClient` / `toFlatTypedClient` 等 API
284
+ * 进行进程内调度(不发起新的网络请求)。
285
+ *
286
+ * 消费者可直接组合使用:
287
+ * ```typescript
288
+ * import { createInternalAdapter } from '@ubean/routes';
289
+ * import { createRequest, createTypedClient } from '@soybeanjs/fetch';
290
+ *
291
+ * const adapter = createInternalAdapter(c);
292
+ * const request = createRequest({ adapter, retry: { retries: 0 } }, { isBackendSuccess: () => true });
293
+ * const api = createTypedClient<paths>(request);
294
+ * ```
295
+ *
296
+ * @param c - Hono Context(可选,用于转发 cookie/authorization 等请求头)
297
+ * @param options - 选项(`headers` 自定义额外 header,`forwardHeaders` 自定义转发 header 列表)
298
+ */
299
+ declare function createInternalAdapter(c?: Context<UbeanEnv$1>, options?: InternalFetchOptions): FetchAdapter;
300
+ //#endregion
301
+ //#region src/bot-detection.d.ts
302
+ /**
303
+ * Bot / crawler User-Agent 检测(P9-24 / Task 6 流式 metadata)。
304
+ *
305
+ * 流式 SSR 把动态 `<head>` metadata 注入到响应尾部(浏览器把它们
306
+ * 移动到 `<head>`),但社交爬虫(Facebook OG、Twitter、Slack 等)
307
+ * 只解析初始 `<head>`,会错过这些标签。检测到爬虫 UA 时,router
308
+ * 自动降级为缓冲渲染(`renderPage`),保证 metadata 出现在初始
309
+ * `<head>` 中。
310
+ *
311
+ * 检测基于 UA 子串匹配(大小写不敏感),覆盖主流搜索引擎与社交
312
+ * 预览爬虫。空 UA 视为非爬虫(部分正常请求也带空 UA)。
313
+ */
314
+ /**
315
+ * 判断 User-Agent 是否为爬虫/社交预览机器人。
316
+ *
317
+ * @param ua User-Agent 字符串(来自 `c.req.header('user-agent')`)
318
+ * @returns `true` 表示疑似爬虫,应降级为缓冲渲染以保证 metadata 完整
319
+ */
320
+ declare function isBotUserAgent(ua: string | undefined | null): boolean;
321
+ //#endregion
322
+ //#region src/openapi.d.ts
323
+ interface OpenAPIGenerationOptions {
324
+ title?: string;
325
+ version?: string;
326
+ description?: string;
327
+ baseURL?: string;
328
+ }
329
+ declare function registerOpenAPIRoutes(app: Hono<UbeanEnv$1>, options?: OpenAPIGenerationOptions & {
330
+ scalarPath?: string;
331
+ openAPIPath?: string;
332
+ }): void;
333
+ //#endregion
334
+ export { type CompiledLayout, type CompiledMiddleware, type CompiledPage, type CompiledRoute, type CompiledRouteRule, type ComposedHandler, type GenericSchema, type Input, type InternalFetchOptions, type IsrCacheEntry, type IsrCacheEntryInternal, type IsrCacheStore, type IsrRule, type IsrServeOptions, type OpenAPIGenerationOptions, type RegisterOptions, type RouteMeta, type RouteRegistrar, type RouteRule, type Span, type SpanAttributes, type SpanContext, type SpanEndOptions, type SpanEvent, type SpanOptions, type SpanStatus, type UbeanBindings, type UbeanContext, type UbeanEnv, type UbeanHandler, type UbeanMiddleware, UbeanRouter, type UbeanVariables, buildIsrCacheKey, clearInternalFetcher, compileRouteRules, createInternalAdapter, createRouteLoader, createRouteRulesMiddleware, createServerRouter, defineHandler, defineHandlerMeta, defineMiddleware, extractRouteMeta, getInternalFetcher, getIsrCache, getIsrRuleFromContext, getStaleIsrCache, invalidateIsrCache, invalidateIsrCachePattern, isBotUserAgent, isHandlerChain, isIsrEntryStale, matchRouteRules, normalizeIsrRule, registerApiRoutes, registerOpenAPIRoutes, registerPageRoutes, registerRoutes, serveIsr, setInternalFetcher, setIsrCache, sortPagesForRegistration, useRouter };