@ubean/app 0.6.0 → 0.6.1-beta.3

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +191 -191
  2. package/dist/index.js +158 -124
  3. package/package.json +13 -13
package/dist/index.d.ts CHANGED
@@ -2,10 +2,199 @@ import { RegisterOptions, RouteRegistrar } from "@ubean/routes";
2
2
  import { CacheStore } from "@ubean/server/cache";
3
3
  import { DataCacheMiddlewareOptions } from "@ubean/server/middleware";
4
4
  import { CsrfOptions, SecurityHeadersOptions } from "@ubean/server/security";
5
- 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";
5
+ 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, UbeanMiddlewareStep } from "@ubean/shared";
6
6
  import { Context, Hono, MiddlewareHandler } from "hono";
7
7
  import { Hookable } from "hookable";
8
8
  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";
9
+ //#region src/app.d.ts
10
+ interface UbeanRuntimeHooks {
11
+ 'app:created': (app: Hono<UbeanEnv$1>) => void | Promise<void>;
12
+ 'app:before:register': (app: Hono<UbeanEnv$1>) => void | Promise<void>;
13
+ 'app:after:register': (app: Hono<UbeanEnv$1>) => void | Promise<void>;
14
+ 'request:start': (c: Context<UbeanEnv$1>) => void | Promise<void>;
15
+ 'request:end': (c: Context<UbeanEnv$1>, res: Response) => void | Promise<void>;
16
+ 'request:error': (c: Context<UbeanEnv$1>, err: Error) => void | Promise<void>;
17
+ 'route:register': (route: {
18
+ method: string;
19
+ path: string;
20
+ handler: MiddlewareHandler[] | ComposedHandler$1 | Function;
21
+ meta?: RouteMeta$1;
22
+ }) => void | Promise<void>;
23
+ 'middleware:register': (mw: ScannedMiddleware$1) => void | Promise<void>;
24
+ error: (err: Error, c: Context<UbeanEnv$1>) => void | Promise<void>;
25
+ }
26
+ /**
27
+ * Page renderer shape (satisfied by `@ubean/client/ssr`'s `PageRenderer`).
28
+ * Declared as a type alias to the actual `@ubean/pages`'s `PageRenderer`
29
+ * interface so consumers don't need to install `@ubean/pages` separately
30
+ * to type-check against `UbeanAppOptions.pageRenderer`.
31
+ *
32
+ * Type-only import works even when `@ubean/pages` is an optional peerDep —
33
+ * only runtime imports would fail. Consumers without `@ubean/pages` will
34
+ * see this type fall back to `any` (TS' default for missing modules under
35
+ * `skipLibCheck`).
36
+ */
37
+ type PageRenderer = import('@ubean/pages').PageRenderer;
38
+ interface PageAssetTags {
39
+ head?: string;
40
+ bodyAttrs?: string;
41
+ htmlAttrs?: string;
42
+ /** Favicon HREF (如 `/favicon.ico`),自动注入 `<link rel="icon">` 到 `<head>` */
43
+ favicon?: string;
44
+ }
45
+ interface UbeanAppOptions {
46
+ rootDir?: string;
47
+ routes?: ScannedApiRoute$1[];
48
+ middleware?: ScannedMiddleware$1[];
49
+ pages?: ScannedPageRoute$1[];
50
+ layouts?: ScannedLayout$1[];
51
+ crons?: ScannedCronTask[];
52
+ routeRules?: Record<string, RouteRule$1>;
53
+ plugins?: UbeanAppPlugin[];
54
+ routeLoaders?: Record<string, () => Promise<{
55
+ default?: ComposedHandler$1 | MiddlewareHandler[];
56
+ } | Record<string, ComposedHandler$1 | MiddlewareHandler[]>>>;
57
+ middlewareLoaders?: Record<string, () => Promise<{
58
+ default: UbeanMiddleware$1;
59
+ }>>;
60
+ pageLoaders?: Record<string, () => Promise<unknown>>;
61
+ pageRenderer?: PageRenderer | null;
62
+ pageAssetTags?: PageAssetTags;
63
+ /** 不进行 SSR 的路由模式列表(glob),匹配的页面走 CSR */
64
+ ssrExclude?: string[];
65
+ /**
66
+ * 启用流式 SSR。`true` 时页面响应以 `ReadableStream` 分块输出
67
+ * (头部先发送,app HTML 边渲染边输出),改善 TTFB/LCP。
68
+ * renderer 不支持流式时自动降级为缓冲渲染。
69
+ */
70
+ streaming?: boolean;
71
+ /**
72
+ * 爬虫降级(P9-24):当 `streaming` 启用且检测到爬虫/社交预览 UA 时,
73
+ * 自动降级为缓冲渲染以保证 metadata 出现在初始 `<head>`。
74
+ * 默认 `true`。设为 `false` 可禁用爬虫检测(不推荐)。
75
+ */
76
+ botFallback?: boolean;
77
+ publicDir?: string;
78
+ healthEndpoint?: boolean;
79
+ openAPI?: boolean | {
80
+ title?: string;
81
+ version?: string;
82
+ description?: string;
83
+ scalarPath?: string;
84
+ openAPIPath?: string;
85
+ };
86
+ i18nConfig?: {
87
+ enabled?: boolean;
88
+ strategy?: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
89
+ defaultLocale?: string;
90
+ locales?: string[] | Array<{
91
+ code: string;
92
+ }>;
93
+ detectBrowserLanguage?: false | {
94
+ cookieName?: string;
95
+ redirectOn?: 'root' | 'all';
96
+ alwaysRedirect?: boolean;
97
+ };
98
+ fallbackLocale?: string;
99
+ baseUrl?: string;
100
+ };
101
+ /** `pages/404.vue` 自动检测的 404 页面,注册为 Hono 兜底处理器 */
102
+ notFoundPage?: ScannedPageRoute$1;
103
+ /**
104
+ * Pre-rendered no-FOUC color-mode script (from `getColorModeScript`).
105
+ * Injected into `<head>` of every SSR/prerendered HTML response. Covers
106
+ * the SSG/prerender path that bypasses Vite's `transformIndexHtml`.
107
+ */
108
+ colorModeScript?: string;
109
+ /**
110
+ * CSRF protection. Default `true` (origin check). `false` disables.
111
+ * Pass `CsrfOptions` to override (e.g. token mode).
112
+ */
113
+ csrf?: boolean | CsrfOptions;
114
+ /**
115
+ * Security response headers. Default `true`. `false` disables.
116
+ */
117
+ securityHeaders?: boolean | SecurityHeadersOptions;
118
+ /**
119
+ * Cross-request fetch Data Cache (`next: { revalidate, tags }`).
120
+ * Default `true`. Dev still skips cache unless `forceDevCache`.
121
+ */
122
+ dataCache?: boolean | DataCacheMiddlewareOptions;
123
+ /**
124
+ * HTTP / ISR cache store. Default is in-process memory (not shared
125
+ * across instances). Pass `loadFsCacheStore(dir)`(Node fs 后端,动态加载)for a Node backend.
126
+ */
127
+ cacheStore?: CacheStore;
128
+ /** Declarative cache backend. `store: 'fs'` 走 `loadFsCacheStore()`(Node-only,动态加载)。 */
129
+ cache?: {
130
+ store?: 'memory' | 'fs';
131
+ dir?: string;
132
+ };
133
+ /**
134
+ * File-convention SEO (`src/sitemap.ts`, `robots.ts`, …). Default: on when
135
+ * `rootDir` or `seoConventions.srcDir` is set. `false` disables.
136
+ */
137
+ seoConventions?: boolean | {
138
+ srcDir?: string;
139
+ };
140
+ /**
141
+ * Preloaded SEO convention modules (production `import.meta.glob`). When
142
+ * set, disk discovery is skipped — required on serverless.
143
+ */
144
+ seoConventionModules?: Record<string, {
145
+ default?: unknown;
146
+ }>;
147
+ /**
148
+ * Production `/_ipx` handler (from `@ubean/image` when `image` is enabled).
149
+ * Dev still uses the Vite middleware.
150
+ */
151
+ ipxHandler?: MiddlewareHandler<UbeanEnv$1>;
152
+ }
153
+ interface UbeanAppPlugin {
154
+ name: string;
155
+ /** Called after app is created but before routes are registered */
156
+ setup?: (app: UbeanApp) => void | Promise<void>;
157
+ /** Called after all routes are registered */
158
+ ready?: (app: UbeanApp) => void | Promise<void>;
159
+ }
160
+ export declare class UbeanApp {
161
+ readonly hono: Hono<UbeanEnv$1>;
162
+ readonly hooks: Hookable<UbeanRuntimeHooks>;
163
+ readonly plugins: UbeanAppPlugin[];
164
+ readonly options: UbeanAppOptions;
165
+ private _ready;
166
+ /**
167
+ * Scalar 文档页(`/_scalar`)专用的 CSP:生效 CSP + `{@link SCALAR_SCRIPT_ORIGIN}`。
168
+ *
169
+ * 该页面从 CDN 取脚本,而 CSP 是全局的一份 —— 只在这一条响应上追加它需要的来源,不放宽应用
170
+ * 其余部分的策略。CSP 被关闭时为 `undefined`(不重加头)。构造期算好,`init()` 注册路由时用。
171
+ */
172
+ private _scalarCsp;
173
+ constructor(options?: UbeanAppOptions);
174
+ private _setupBaseMiddleware;
175
+ init(): Promise<this>;
176
+ private _registerSeoConventions;
177
+ private _lazyInitPromise;
178
+ lazyInit(): Promise<this>;
179
+ resetInit(): void;
180
+ private _setupFallback;
181
+ use(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
182
+ use(...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
183
+ on(method: string | string[], path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
184
+ get(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
185
+ post(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
186
+ put(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
187
+ patch(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
188
+ delete(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
189
+ fetch: Hono<UbeanEnv$1>['fetch'];
190
+ request: Hono<UbeanEnv$1>['request'];
191
+ }
192
+ interface AppPlugin {
193
+ name: string;
194
+ setup?: (app: Hono<UbeanEnv$1>) => void | Promise<void>;
195
+ }
196
+ export declare function createUbeanApp(options?: UbeanAppOptions): UbeanApp;
197
+ //#endregion
9
198
  //#region src/hooks.d.ts
10
199
  /**
11
200
  * 请求事件 —— 传递给 `handle` 和 `handleError` 的上下文对象。
@@ -285,193 +474,4 @@ export declare function createDefaultServerConfig(): ResolvedServerConfig;
285
474
  */
286
475
  export declare function mergeServerConfigs(base: ResolvedServerConfig, ...configs: (ResolvedServerConfig | null | undefined)[]): ResolvedServerConfig;
287
476
  //#endregion
288
- //#region src/app.d.ts
289
- interface UbeanRuntimeHooks {
290
- 'app:created': (app: Hono<UbeanEnv$1>) => void | Promise<void>;
291
- 'app:before:register': (app: Hono<UbeanEnv$1>) => void | Promise<void>;
292
- 'app:after:register': (app: Hono<UbeanEnv$1>) => void | Promise<void>;
293
- 'request:start': (c: Context<UbeanEnv$1>) => void | Promise<void>;
294
- 'request:end': (c: Context<UbeanEnv$1>, res: Response) => void | Promise<void>;
295
- 'request:error': (c: Context<UbeanEnv$1>, err: Error) => void | Promise<void>;
296
- 'route:register': (route: {
297
- method: string;
298
- path: string;
299
- handler: MiddlewareHandler[] | ComposedHandler$1 | Function;
300
- meta?: RouteMeta$1;
301
- }) => void | Promise<void>;
302
- 'middleware:register': (mw: ScannedMiddleware$1) => void | Promise<void>;
303
- error: (err: Error, c: Context<UbeanEnv$1>) => void | Promise<void>;
304
- }
305
- /**
306
- * Page renderer shape (satisfied by `@ubean/client/ssr`'s `PageRenderer`).
307
- * Declared as a type alias to the actual `@ubean/pages`'s `PageRenderer`
308
- * interface so consumers don't need to install `@ubean/pages` separately
309
- * to type-check against `UbeanAppOptions.pageRenderer`.
310
- *
311
- * Type-only import works even when `@ubean/pages` is an optional peerDep —
312
- * only runtime imports would fail. Consumers without `@ubean/pages` will
313
- * see this type fall back to `any` (TS' default for missing modules under
314
- * `skipLibCheck`).
315
- */
316
- type PageRenderer = import('@ubean/pages').PageRenderer;
317
- interface PageAssetTags {
318
- head?: string;
319
- bodyAttrs?: string;
320
- htmlAttrs?: string;
321
- /** Favicon HREF (如 `/favicon.ico`),自动注入 `<link rel="icon">` 到 `<head>` */
322
- favicon?: string;
323
- }
324
- interface UbeanAppOptions {
325
- rootDir?: string;
326
- routes?: ScannedApiRoute$1[];
327
- middleware?: ScannedMiddleware$1[];
328
- pages?: ScannedPageRoute$1[];
329
- layouts?: ScannedLayout$1[];
330
- crons?: ScannedCronTask[];
331
- routeRules?: Record<string, RouteRule$1>;
332
- plugins?: UbeanAppPlugin[];
333
- routeLoaders?: Record<string, () => Promise<{
334
- default?: ComposedHandler$1 | MiddlewareHandler[];
335
- } | Record<string, ComposedHandler$1 | MiddlewareHandler[]>>>;
336
- middlewareLoaders?: Record<string, () => Promise<{
337
- default: UbeanMiddleware$1;
338
- }>>;
339
- pageLoaders?: Record<string, () => Promise<unknown>>;
340
- pageRenderer?: PageRenderer | null;
341
- pageAssetTags?: PageAssetTags;
342
- /** 不进行 SSR 的路由模式列表(glob),匹配的页面走 CSR */
343
- ssrExclude?: string[];
344
- /**
345
- * 启用流式 SSR。`true` 时页面响应以 `ReadableStream` 分块输出
346
- * (头部先发送,app HTML 边渲染边输出),改善 TTFB/LCP。
347
- * renderer 不支持流式时自动降级为缓冲渲染。
348
- */
349
- streaming?: boolean;
350
- /**
351
- * 爬虫降级(P9-24):当 `streaming` 启用且检测到爬虫/社交预览 UA 时,
352
- * 自动降级为缓冲渲染以保证 metadata 出现在初始 `<head>`。
353
- * 默认 `true`。设为 `false` 可禁用爬虫检测(不推荐)。
354
- */
355
- botFallback?: boolean;
356
- publicDir?: string;
357
- healthEndpoint?: boolean;
358
- openAPI?: boolean | {
359
- title?: string;
360
- version?: string;
361
- description?: string;
362
- scalarPath?: string;
363
- openAPIPath?: string;
364
- };
365
- i18nConfig?: {
366
- enabled?: boolean;
367
- strategy?: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
368
- defaultLocale?: string;
369
- locales?: string[] | Array<{
370
- code: string;
371
- }>;
372
- detectBrowserLanguage?: false | {
373
- cookieName?: string;
374
- redirectOn?: 'root' | 'all';
375
- alwaysRedirect?: boolean;
376
- };
377
- fallbackLocale?: string;
378
- baseUrl?: string;
379
- };
380
- /** `pages/404.vue` 自动检测的 404 页面,注册为 Hono 兜底处理器 */
381
- notFoundPage?: ScannedPageRoute$1;
382
- /**
383
- * Pre-rendered no-FOUC color-mode script (from `getColorModeScript`).
384
- * Injected into `<head>` of every SSR/prerendered HTML response. Covers
385
- * the SSG/prerender path that bypasses Vite's `transformIndexHtml`.
386
- */
387
- colorModeScript?: string;
388
- /**
389
- * CSRF protection. Default `true` (origin check). `false` disables.
390
- * Pass `CsrfOptions` to override (e.g. token mode).
391
- */
392
- csrf?: boolean | CsrfOptions;
393
- /**
394
- * Security response headers. Default `true`. `false` disables.
395
- */
396
- securityHeaders?: boolean | SecurityHeadersOptions;
397
- /**
398
- * Cross-request fetch Data Cache (`next: { revalidate, tags }`).
399
- * Default `true`. Dev still skips cache unless `forceDevCache`.
400
- */
401
- dataCache?: boolean | DataCacheMiddlewareOptions;
402
- /**
403
- * HTTP / ISR cache store. Default is in-process memory (not shared
404
- * across instances). Pass `loadFsCacheStore(dir)`(Node fs 后端,动态加载)for a Node backend.
405
- */
406
- cacheStore?: CacheStore;
407
- /** Declarative cache backend. `store: 'fs'` 走 `loadFsCacheStore()`(Node-only,动态加载)。 */
408
- cache?: {
409
- store?: 'memory' | 'fs';
410
- dir?: string;
411
- };
412
- /**
413
- * File-convention SEO (`src/sitemap.ts`, `robots.ts`, …). Default: on when
414
- * `rootDir` or `seoConventions.srcDir` is set. `false` disables.
415
- */
416
- seoConventions?: boolean | {
417
- srcDir?: string;
418
- };
419
- /**
420
- * Preloaded SEO convention modules (production `import.meta.glob`). When
421
- * set, disk discovery is skipped — required on serverless.
422
- */
423
- seoConventionModules?: Record<string, {
424
- default?: unknown;
425
- }>;
426
- /**
427
- * Production `/_ipx` handler (from `@ubean/image` when `image` is enabled).
428
- * Dev still uses the Vite middleware.
429
- */
430
- ipxHandler?: MiddlewareHandler<UbeanEnv$1>;
431
- }
432
- interface UbeanAppPlugin {
433
- name: string;
434
- /** Called after app is created but before routes are registered */
435
- setup?: (app: UbeanApp) => void | Promise<void>;
436
- /** Called after all routes are registered */
437
- ready?: (app: UbeanApp) => void | Promise<void>;
438
- }
439
- export declare class UbeanApp {
440
- readonly hono: Hono<UbeanEnv$1>;
441
- readonly hooks: Hookable<UbeanRuntimeHooks>;
442
- readonly plugins: UbeanAppPlugin[];
443
- readonly options: UbeanAppOptions;
444
- private _ready;
445
- /**
446
- * Scalar 文档页(`/_scalar`)专用的 CSP:生效 CSP + `{@link SCALAR_SCRIPT_ORIGIN}`。
447
- *
448
- * 该页面从 CDN 取脚本,而 CSP 是全局的一份 —— 只在这一条响应上追加它需要的来源,不放宽应用
449
- * 其余部分的策略。CSP 被关闭时为 `undefined`(不重加头)。构造期算好,`init()` 注册路由时用。
450
- */
451
- private _scalarCsp;
452
- constructor(options?: UbeanAppOptions);
453
- private _setupBaseMiddleware;
454
- init(): Promise<this>;
455
- private _registerSeoConventions;
456
- private _lazyInitPromise;
457
- lazyInit(): Promise<this>;
458
- resetInit(): void;
459
- private _setupFallback;
460
- use(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
461
- use(...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
462
- on(method: string | string[], path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
463
- get(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
464
- post(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
465
- put(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
466
- patch(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
467
- delete(path: string, ...handlers: MiddlewareHandler<UbeanEnv$1>[]): this;
468
- fetch: Hono<UbeanEnv$1>['fetch'];
469
- request: Hono<UbeanEnv$1>['request'];
470
- }
471
- interface AppPlugin {
472
- name: string;
473
- setup?: (app: Hono<UbeanEnv$1>) => void | Promise<void>;
474
- }
475
- export declare function createUbeanApp(options?: UbeanAppOptions): UbeanApp;
476
- //#endregion
477
- export type { AppPlugin, ComposedHandler, DefineServerOptions, GlobalHooks, Handle, HandleError, HandleErrorInput, HandleEvent, HandleFetch, HandleFetchInput, HandleInput, PageAssetTags, PageRenderer, RegisterOptions, ResolvedServerConfig, RouteMeta, RouteRegistrar, RouteRule, ScannedApiRoute, ScannedLayout, ScannedMiddleware, ScannedPageRoute, ServerHooks, UbeanAppOptions, UbeanAppPlugin, UbeanEnv, UbeanMiddleware, UbeanRuntimeHooks };
477
+ export type { AppPlugin, ComposedHandler, DefineServerOptions, GlobalHooks, Handle, HandleError, HandleErrorInput, HandleEvent, HandleFetch, HandleFetchInput, HandleInput, PageAssetTags, PageRenderer, RegisterOptions, ResolvedServerConfig, RouteMeta, RouteRegistrar, RouteRule, ScannedApiRoute, ScannedLayout, ScannedMiddleware, ScannedPageRoute, ServerHooks, UbeanAppOptions, UbeanAppPlugin, UbeanEnv, UbeanMiddleware, UbeanMiddlewareStep, UbeanRuntimeHooks };
package/dist/index.js CHANGED
@@ -113,109 +113,6 @@ async function applyHandleErrorHook(c, error, status) {
113
113
  });
114
114
  }
115
115
  //#endregion
116
- //#region src/define-server.ts
117
- /**
118
- * Apply a resolved server config to a `UbeanApp` instance.
119
- *
120
- * This must be called BEFORE `app.init()` so that:
121
- * - `plugins` are available when `init()` calls `setup` / `ready`
122
- * - `hooks` are registered before `init()` fires `app:created` etc.
123
- * - `onAppCreate` runs before route registration
124
- * - `globalHooks` (handle/handleFetch/handleError) are set before any request
125
- *
126
- * `onServerReady` is NOT called here — the caller must invoke it after
127
- * `app.init()` completes.
128
- */
129
- async function applyServerConfig(app, config) {
130
- if (config.plugins.length > 0) app.plugins.push(...config.plugins);
131
- for (const [name, handler] of Object.entries(config.hooks)) if (handler) app.hooks.hook(name, handler);
132
- if (config.globalHooks) setGlobalHooks(config.globalHooks);
133
- if (config.onAppCreate) await config.onAppCreate(app);
134
- }
135
- /**
136
- * Define the backend server configuration.
137
- *
138
- * This is the server-side counterpart of `defineApp`. It allows users to
139
- * inject plugins, register runtime hooks, and run startup/ready logic
140
- * from a simple entry file (`src/server.ts`).
141
- *
142
- * @example
143
- * ```ts
144
- * // src/server.ts
145
- * import { defineServer } from '@ubean/app';
146
- *
147
- * export default defineServer({
148
- * plugins: [
149
- * {
150
- * name: 'my-plugin',
151
- * setup(app) { app.use('/api/custom', handler); },
152
- * ready(app) { /* routes registered *\/ }
153
- * }
154
- * ],
155
- * hooks: {
156
- * 'request:start': (c) => { console.log(c.req.method, c.req.path); }
157
- * },
158
- * globalHooks: {
159
- * handle: async ({ event, resolve }) => {
160
- * const response = await resolve(event);
161
- * response.headers.set('X-Custom', 'ubean');
162
- * return response;
163
- * },
164
- * handleError: async ({ error, status }) => {
165
- * console.error(`[${status}]`, error);
166
- * }
167
- * },
168
- * onAppCreate: async (app) => { /* init db *\/ },
169
- * onServerReady: async (app) => { /* start workers *\/ }
170
- * });
171
- * ```
172
- */
173
- function defineServer(options) {
174
- return {
175
- plugins: options.plugins || [],
176
- hooks: options.hooks || {},
177
- globalHooks: options.globalHooks,
178
- onAppCreate: options.onAppCreate,
179
- onServerReady: options.onServerReady
180
- };
181
- }
182
- /**
183
- * Create an empty server config — used as the fallback when no
184
- * `src/server.ts` file exists.
185
- */
186
- function createDefaultServerConfig() {
187
- return {
188
- plugins: [],
189
- hooks: {}
190
- };
191
- }
192
- /**
193
- * Merge multiple server configs (shared + mode-specific) into one.
194
- * Plugins are concatenated; hooks are merged; mode-specific callbacks
195
- * override shared ones.
196
- */
197
- function mergeServerConfigs(base, ...configs) {
198
- const result = {
199
- plugins: [...base.plugins],
200
- hooks: { ...base.hooks },
201
- globalHooks: base.globalHooks ? { ...base.globalHooks } : void 0,
202
- onAppCreate: base.onAppCreate,
203
- onServerReady: base.onServerReady
204
- };
205
- for (const cfg of configs) {
206
- if (!cfg) continue;
207
- if (cfg.plugins) result.plugins.push(...cfg.plugins);
208
- if (cfg.hooks) Object.assign(result.hooks, cfg.hooks);
209
- if (cfg.globalHooks) result.globalHooks = {
210
- ...result.globalHooks,
211
- ...cfg.globalHooks
212
- };
213
- if (cfg.onAppCreate) result.onAppCreate = cfg.onAppCreate;
214
- if (cfg.onServerReady) result.onServerReady = cfg.onServerReady;
215
- }
216
- return result;
217
- }
218
- //#endregion
219
116
  //#region src/app.ts
220
117
  const actionContextAls = new AsyncLocalStorage();
221
118
  bindActionContextStorage(actionContextAls);
@@ -261,6 +158,35 @@ function resolveToggle(value, defaultOn) {
261
158
  if (value === true) return {};
262
159
  return value;
263
160
  }
161
+ /**
162
+ * TS-07:把中间件链的每一步按**注册/执行顺序**记进请求级变量。
163
+ *
164
+ * 顺序是结构性事实 —— 13 步表(见 `_setupBaseMiddleware()` 与本包 middleware-order 测试)把它
165
+ * 固定下来,改动注册顺序即破坏契约。记录值只用于可观测性断言(`GET /_health` 回读),不参与任何请求处理逻辑。
166
+ */
167
+ function markMiddlewareStep(c, step) {
168
+ const recorded = c.get("__ubean_mw_order__");
169
+ c.set("__ubean_mw_order__", recorded ? [...recorded, step] : [step]);
170
+ }
171
+ /** 给中间件套一层「记录步骤名」外壳;注册顺序与不套壳时完全一致。 */
172
+ function withStep(step, handler) {
173
+ return async (c, next) => {
174
+ markMiddlewareStep(c, step);
175
+ return handler(c, next);
176
+ };
177
+ }
178
+ /**
179
+ * TS-07:链上存在「不是中间件、但有固定位置」的步骤(如 ⑦ cacheStore 初始化)。
180
+ *
181
+ * 为了让 13 步全序可观测,这里在该位置插一个**纯记录、纯透传**的空壳:它不读不写请求、
182
+ * 不改响应,只把步骤名按注册位置记进请求级序列。
183
+ */
184
+ function recordOnlyStep(step) {
185
+ return async (c, next) => {
186
+ markMiddlewareStep(c, step);
187
+ return next();
188
+ };
189
+ }
264
190
  var UbeanApp = class {
265
191
  hono;
266
192
  hooks;
@@ -283,55 +209,56 @@ var UbeanApp = class {
283
209
  this._setupFallback();
284
210
  }
285
211
  _setupBaseMiddleware() {
286
- this.hono.use("*", async (c, next) => {
212
+ this.hono.use("*", withStep("handle", async (c, next) => {
287
213
  if (!await applyHandleHook(c, next)) await next();
288
- });
289
- this.hono.use("*", requestId());
290
- this.hono.use("*", async (c, next) => {
214
+ }));
215
+ this.hono.use("*", withStep("requestId", requestId()));
216
+ this.hono.use("*", withStep("actionContext", async (c, next) => {
291
217
  await actionContextAls.run(buildActionContext(c), () => next());
292
- });
218
+ }));
293
219
  const securityHeaders = resolveToggle(this.options.securityHeaders, true);
294
220
  if (securityHeaders !== false) {
295
221
  const mergedSecurity = mergeSecurityHeadersOptions(DEFAULT_SECURITY_HEADERS, securityHeaders);
296
- this.hono.use("*", createSecurityHeadersMiddleware(mergedSecurity));
222
+ this.hono.use("*", withStep("securityHeaders", createSecurityHeadersMiddleware(mergedSecurity)));
297
223
  const effectiveCsp = mergedSecurity.contentSecurityPolicy;
298
224
  if (effectiveCsp && typeof effectiveCsp === "object") this._scalarCsp = serializeCsp(extendCspScriptSrc(effectiveCsp, [SCALAR_SCRIPT_ORIGIN]));
299
225
  }
300
226
  const csrf = resolveToggle(this.options.csrf, true);
301
- if (csrf !== false) this.hono.use("*", createCsrfMiddleware({
227
+ if (csrf !== false) this.hono.use("*", withStep("csrf", createCsrfMiddleware({
302
228
  mode: "origin",
303
229
  ...csrf,
304
230
  exclude: [...DEFAULT_CSRF_EXCLUDE, ...csrf.exclude ?? []]
305
- }));
231
+ })));
306
232
  const dataCache = resolveToggle(this.options.dataCache, true);
307
- if (dataCache !== false) this.hono.use("*", createDataCacheMiddleware(dataCache));
233
+ if (dataCache !== false) this.hono.use("*", withStep("dataCache", createDataCacheMiddleware(dataCache)));
308
234
  if (this.options.cacheStore) useCacheStore(this.options.cacheStore);
309
235
  else if (this.options.cache?.store === "fs") {
310
236
  const fsDir = this.options.cache.dir || ".ubean/cache";
311
237
  useCacheStore(createLazyCacheStore(() => loadFsCacheStore(fsDir)));
312
238
  }
239
+ if (this.options.cacheStore || this.options.cache?.store === "fs") this.hono.use("*", recordOnlyStep("cacheStore"));
313
240
  const i18nCfg = this.options.i18nConfig;
314
241
  if (i18nCfg?.enabled !== false && (i18nCfg?.locales?.length ?? 0) > 0 && i18nCfg) {
315
242
  const locales = (i18nCfg.locales || []).map((l) => typeof l === "string" ? l : l.code);
316
- this.hono.use("*", createI18nMiddleware({
243
+ this.hono.use("*", withStep("i18n", createI18nMiddleware({
317
244
  defaultLocale: i18nCfg.defaultLocale || "en",
318
245
  locales,
319
246
  strategy: i18nCfg.strategy || "prefix_except_default",
320
247
  detectBrowserLanguage: i18nCfg.detectBrowserLanguage,
321
248
  loadMessages: (locale, fallback) => ensureLocaleMessages(locale, fallback)
322
- }));
249
+ })));
323
250
  }
324
251
  if (this.options.routeRules && Object.keys(this.options.routeRules).length > 0) {
325
- this.hono.use("*", createRouteRulesMiddleware(this.options.routeRules, { dispatch: (req) => Promise.resolve(this.hono.fetch(req)) }));
252
+ this.hono.use("*", withStep("routeRules", createRouteRulesMiddleware(this.options.routeRules, { dispatch: (req) => Promise.resolve(this.hono.fetch(req)) })));
326
253
  const cacheRules = resolveRouteCacheRules(this.options.routeRules);
327
254
  const hasIsrRules = Object.values(this.options.routeRules).some((r) => r?.isr !== void 0);
328
255
  if (Object.keys(cacheRules).length > 0 || hasIsrRules) {
329
256
  if (!this.options.cacheStore && this.options.cache?.store !== "fs") useCacheStore(createMemoryStore());
330
- if (Object.keys(cacheRules).length > 0) this.hono.use("*", createCacheMiddleware({ rules: cacheRules }));
257
+ if (Object.keys(cacheRules).length > 0) this.hono.use("*", withStep("routeCache", createCacheMiddleware({ rules: cacheRules })));
331
258
  }
332
259
  }
333
- this.hono.use("*", createWebSocketMiddleware());
334
- this.hono.use("*", async (c, next) => {
260
+ this.hono.use("*", withStep("websocket", createWebSocketMiddleware()));
261
+ this.hono.use("*", withStep("lifecycle", async (c, next) => {
335
262
  c.set("route", {
336
263
  meta: { requiresAuth: true },
337
264
  path: c.req.path,
@@ -345,13 +272,17 @@ var UbeanApp = class {
345
272
  await this.hooks.callHook("request:error", c, err);
346
273
  throw err;
347
274
  }
348
- });
349
- if (this.options.healthEndpoint !== false) this.hono.get("/_health", (c) => {
350
- return c.json({
351
- status: "ok",
352
- timestamp: Date.now()
275
+ }));
276
+ if (this.options.healthEndpoint !== false) {
277
+ this.hono.use("*", recordOnlyStep("healthEndpoint"));
278
+ this.hono.get("/_health", (c) => {
279
+ return c.json({
280
+ status: "ok",
281
+ timestamp: Date.now(),
282
+ mwOrder: c.get("__ubean_mw_order__") ?? []
283
+ });
353
284
  });
354
- });
285
+ }
355
286
  }
356
287
  async init() {
357
288
  if (this._ready) return this;
@@ -491,4 +422,107 @@ function createUbeanApp(options = {}) {
491
422
  return new UbeanApp(options);
492
423
  }
493
424
  //#endregion
425
+ //#region src/define-server.ts
426
+ /**
427
+ * Apply a resolved server config to a `UbeanApp` instance.
428
+ *
429
+ * This must be called BEFORE `app.init()` so that:
430
+ * - `plugins` are available when `init()` calls `setup` / `ready`
431
+ * - `hooks` are registered before `init()` fires `app:created` etc.
432
+ * - `onAppCreate` runs before route registration
433
+ * - `globalHooks` (handle/handleFetch/handleError) are set before any request
434
+ *
435
+ * `onServerReady` is NOT called here — the caller must invoke it after
436
+ * `app.init()` completes.
437
+ */
438
+ async function applyServerConfig(app, config) {
439
+ if (config.plugins.length > 0) app.plugins.push(...config.plugins);
440
+ for (const [name, handler] of Object.entries(config.hooks)) if (handler) app.hooks.hook(name, handler);
441
+ if (config.globalHooks) setGlobalHooks(config.globalHooks);
442
+ if (config.onAppCreate) await config.onAppCreate(app);
443
+ }
444
+ /**
445
+ * Define the backend server configuration.
446
+ *
447
+ * This is the server-side counterpart of `defineApp`. It allows users to
448
+ * inject plugins, register runtime hooks, and run startup/ready logic
449
+ * from a simple entry file (`src/server.ts`).
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * // src/server.ts
454
+ * import { defineServer } from '@ubean/app';
455
+ *
456
+ * export default defineServer({
457
+ * plugins: [
458
+ * {
459
+ * name: 'my-plugin',
460
+ * setup(app) { app.use('/api/custom', handler); },
461
+ * ready(app) { /* routes registered *\/ }
462
+ * }
463
+ * ],
464
+ * hooks: {
465
+ * 'request:start': (c) => { console.log(c.req.method, c.req.path); }
466
+ * },
467
+ * globalHooks: {
468
+ * handle: async ({ event, resolve }) => {
469
+ * const response = await resolve(event);
470
+ * response.headers.set('X-Custom', 'ubean');
471
+ * return response;
472
+ * },
473
+ * handleError: async ({ error, status }) => {
474
+ * console.error(`[${status}]`, error);
475
+ * }
476
+ * },
477
+ * onAppCreate: async (app) => { /* init db *\/ },
478
+ * onServerReady: async (app) => { /* start workers *\/ }
479
+ * });
480
+ * ```
481
+ */
482
+ function defineServer(options) {
483
+ return {
484
+ plugins: options.plugins || [],
485
+ hooks: options.hooks || {},
486
+ globalHooks: options.globalHooks,
487
+ onAppCreate: options.onAppCreate,
488
+ onServerReady: options.onServerReady
489
+ };
490
+ }
491
+ /**
492
+ * Create an empty server config — used as the fallback when no
493
+ * `src/server.ts` file exists.
494
+ */
495
+ function createDefaultServerConfig() {
496
+ return {
497
+ plugins: [],
498
+ hooks: {}
499
+ };
500
+ }
501
+ /**
502
+ * Merge multiple server configs (shared + mode-specific) into one.
503
+ * Plugins are concatenated; hooks are merged; mode-specific callbacks
504
+ * override shared ones.
505
+ */
506
+ function mergeServerConfigs(base, ...configs) {
507
+ const result = {
508
+ plugins: [...base.plugins],
509
+ hooks: { ...base.hooks },
510
+ globalHooks: base.globalHooks ? { ...base.globalHooks } : void 0,
511
+ onAppCreate: base.onAppCreate,
512
+ onServerReady: base.onServerReady
513
+ };
514
+ for (const cfg of configs) {
515
+ if (!cfg) continue;
516
+ if (cfg.plugins) result.plugins.push(...cfg.plugins);
517
+ if (cfg.hooks) Object.assign(result.hooks, cfg.hooks);
518
+ if (cfg.globalHooks) result.globalHooks = {
519
+ ...result.globalHooks,
520
+ ...cfg.globalHooks
521
+ };
522
+ if (cfg.onAppCreate) result.onAppCreate = cfg.onAppCreate;
523
+ if (cfg.onServerReady) result.onServerReady = cfg.onServerReady;
524
+ }
525
+ return result;
526
+ }
527
+ //#endregion
494
528
  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.6.0",
3
+ "version": "0.6.1-beta.3",
4
4
  "description": "Hono app factory and server config for ubean (createUbeanApp, defineServer)",
5
5
  "files": [
6
6
  "dist"
@@ -16,26 +16,26 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@ubean/i18n": "0.6.0",
20
- "@ubean/islands": "0.6.0",
21
- "@ubean/routes": "0.6.0",
22
- "@ubean/scan": "0.6.0",
23
- "@ubean/server": "0.6.0",
24
- "@ubean/shared": "0.6.0",
25
- "hono": "4.13.12",
19
+ "@ubean/i18n": "0.6.1-beta.3",
20
+ "@ubean/islands": "0.6.1-beta.3",
21
+ "@ubean/routes": "0.6.1-beta.3",
22
+ "@ubean/scan": "0.6.1-beta.3",
23
+ "@ubean/server": "0.6.1-beta.3",
24
+ "@ubean/shared": "0.6.1-beta.3",
25
+ "hono": "4.13.13",
26
26
  "hookable": "^6.1.2",
27
27
  "pathe": "^2.0.3"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.6.4",
31
- "@ubean/pages": "0.6.0",
32
- "@ubean/seo": "0.6.0",
31
+ "@ubean/pages": "0.6.1-beta.3",
32
+ "@ubean/seo": "0.6.1-beta.3",
33
33
  "typescript": "npm:typescript-native-bridge@latest",
34
- "vite-plus": "1.0.0"
34
+ "vite-plus": "1.1.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@ubean/pages": "0.6.0",
38
- "@ubean/seo": "0.6.0"
37
+ "@ubean/pages": "0.6.1-beta.3",
38
+ "@ubean/seo": "0.6.1-beta.3"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@ubean/pages": {