@ubean/app 0.2.2 → 0.3.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 CHANGED
@@ -1,5 +1,8 @@
1
1
  import { Context, Hono, MiddlewareHandler } from "hono";
2
2
  import { RegisterOptions, RouteRegistrar } from "@ubean/routes";
3
+ import { CacheStore } from "@ubean/server/cache";
4
+ import { DataCacheMiddlewareOptions } from "@ubean/server/middleware";
5
+ import { CsrfOptions, SecurityHeadersOptions } from "@ubean/server/security";
3
6
  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";
4
7
  import { Hookable } from "hookable";
5
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";
@@ -300,7 +303,7 @@ interface UbeanRuntimeHooks {
300
303
  error: (err: Error, c: Context<UbeanEnv$1>) => void | Promise<void>;
301
304
  }
302
305
  /**
303
- * Page renderer shape (satisfied by `@ubean/ssr`'s `PageRenderer`).
306
+ * Page renderer shape (satisfied by `@ubean/client/ssr`'s `PageRenderer`).
304
307
  * Declared as a type alias to the actual `@ubean/pages`'s `PageRenderer`
305
308
  * interface so consumers don't need to install `@ubean/pages` separately
306
309
  * to type-check against `UbeanAppOptions.pageRenderer`.
@@ -360,9 +363,19 @@ interface UbeanAppOptions {
360
363
  openAPIPath?: string;
361
364
  };
362
365
  i18nConfig?: {
366
+ enabled?: boolean;
363
367
  strategy?: 'prefix' | 'prefix_except_default' | 'prefix_and_default' | 'no_prefix';
364
368
  defaultLocale?: string;
365
- locales?: 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;
366
379
  };
367
380
  /** `pages/404.vue` 自动检测的 404 页面,注册为 Hono 兜底处理器 */
368
381
  notFoundPage?: ScannedPageRoute$1;
@@ -372,6 +385,49 @@ interface UbeanAppOptions {
372
385
  * the SSG/prerender path that bypasses Vite's `transformIndexHtml`.
373
386
  */
374
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 `createFsCacheStore(dir)` for a Node fs backend.
405
+ */
406
+ cacheStore?: CacheStore;
407
+ /** Declarative cache backend. `store: 'fs'` uses `createFsCacheStore`. */
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>;
375
431
  }
376
432
  interface UbeanAppPlugin {
377
433
  name: string;
@@ -389,6 +445,7 @@ declare class UbeanApp {
389
445
  constructor(options?: UbeanAppOptions);
390
446
  private _setupBaseMiddleware;
391
447
  init(): Promise<this>;
448
+ private _registerSeoConventions;
392
449
  private _lazyInitPromise;
393
450
  lazyInit(): Promise<this>;
394
451
  resetInit(): void;
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { existsSync } from "node:fs";
2
3
  import { Hono } from "hono";
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";
4
+ import { createI18nMiddleware, ensureLocaleMessages } from "@ubean/i18n";
6
5
  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";
6
+ import { ACTIONS_ENDPOINT, bindActionContextStorage, buildActionContext, createActionsMiddleware, createRouteRulesMiddleware, registerOpenAPIRoutes, registerRoutes, setInternalFetcher } from "@ubean/routes";
7
+ import { createCacheMiddleware, createFsCacheStore, createMemoryStore, resolveRouteCacheRules, useCacheStore } from "@ubean/server/cache";
8
+ import { createDataCacheMiddleware } from "@ubean/server/middleware";
9
9
  import { createWebSocketMiddleware } from "@ubean/server/realtime";
10
+ import { createCsrfMiddleware, createSecurityHeadersMiddleware } from "@ubean/server/security";
11
+ import { serveStatic } from "@ubean/server/static";
12
+ import { UbeanError, errorToResponse, isUbeanError } from "@ubean/shared";
10
13
  import { requestId } from "hono/request-id";
11
14
  import { createHooks } from "hookable";
12
15
  import { isAbsolute, join } from "pathe";
@@ -216,6 +219,50 @@ function mergeServerConfigs(base, ...configs) {
216
219
  }
217
220
  //#endregion
218
221
  //#region src/app.ts
222
+ const actionContextAls = new AsyncLocalStorage();
223
+ bindActionContextStorage(actionContextAls);
224
+ const DEFAULT_CSRF_EXCLUDE = [
225
+ "/_health",
226
+ "/_openapi.json",
227
+ "/_scalar",
228
+ "/_ipx/**",
229
+ "/_devtools/**",
230
+ "/_iconify/**"
231
+ ];
232
+ const DEFAULT_SECURITY_HEADERS = {
233
+ strictTransportSecurity: false,
234
+ contentSecurityPolicy: {
235
+ "default-src": ["'self'"],
236
+ "script-src": [
237
+ "'self'",
238
+ "'unsafe-inline'",
239
+ "'unsafe-eval'"
240
+ ],
241
+ "style-src": ["'self'", "'unsafe-inline'"],
242
+ "img-src": [
243
+ "'self'",
244
+ "data:",
245
+ "blob:",
246
+ "https:"
247
+ ],
248
+ "font-src": ["'self'", "data:"],
249
+ "connect-src": [
250
+ "'self'",
251
+ "ws:",
252
+ "wss:"
253
+ ],
254
+ "object-src": ["'none'"],
255
+ "base-uri": ["'self'"],
256
+ "form-action": ["'self'"],
257
+ "frame-ancestors": ["'self'"]
258
+ }
259
+ };
260
+ function resolveToggle(value, defaultOn) {
261
+ if (value === false) return false;
262
+ if (value === void 0) return defaultOn ? {} : false;
263
+ if (value === true) return {};
264
+ return value;
265
+ }
219
266
  var UbeanApp = class {
220
267
  hono;
221
268
  hooks;
@@ -235,12 +282,41 @@ var UbeanApp = class {
235
282
  if (!await applyHandleHook(c, next)) await next();
236
283
  });
237
284
  this.hono.use("*", requestId());
285
+ this.hono.use("*", async (c, next) => {
286
+ await actionContextAls.run(buildActionContext(c), () => next());
287
+ });
288
+ const securityHeaders = resolveToggle(this.options.securityHeaders, true);
289
+ if (securityHeaders !== false) this.hono.use("*", createSecurityHeadersMiddleware({
290
+ ...DEFAULT_SECURITY_HEADERS,
291
+ ...securityHeaders
292
+ }));
293
+ const csrf = resolveToggle(this.options.csrf, true);
294
+ if (csrf !== false) this.hono.use("*", createCsrfMiddleware({
295
+ mode: "origin",
296
+ ...csrf,
297
+ exclude: [...DEFAULT_CSRF_EXCLUDE, ...csrf.exclude ?? []]
298
+ }));
299
+ const dataCache = resolveToggle(this.options.dataCache, true);
300
+ if (dataCache !== false) this.hono.use("*", createDataCacheMiddleware(dataCache));
301
+ if (this.options.cacheStore) useCacheStore(this.options.cacheStore);
302
+ else if (this.options.cache?.store === "fs") useCacheStore(createFsCacheStore(this.options.cache.dir || ".ubean/cache"));
303
+ const i18nCfg = this.options.i18nConfig;
304
+ if (i18nCfg?.enabled !== false && (i18nCfg?.locales?.length ?? 0) > 0 && i18nCfg) {
305
+ const locales = (i18nCfg.locales || []).map((l) => typeof l === "string" ? l : l.code);
306
+ this.hono.use("*", createI18nMiddleware({
307
+ defaultLocale: i18nCfg.defaultLocale || "en",
308
+ locales,
309
+ strategy: i18nCfg.strategy || "prefix_except_default",
310
+ detectBrowserLanguage: i18nCfg.detectBrowserLanguage,
311
+ loadMessages: (locale, fallback) => ensureLocaleMessages(locale, fallback)
312
+ }));
313
+ }
238
314
  if (this.options.routeRules && Object.keys(this.options.routeRules).length > 0) {
239
- this.hono.use("*", createRouteRulesMiddleware(this.options.routeRules));
315
+ this.hono.use("*", createRouteRulesMiddleware(this.options.routeRules, { dispatch: (req) => Promise.resolve(this.hono.fetch(req)) }));
240
316
  const cacheRules = resolveRouteCacheRules(this.options.routeRules);
241
317
  const hasIsrRules = Object.values(this.options.routeRules).some((r) => r?.isr !== void 0);
242
318
  if (Object.keys(cacheRules).length > 0 || hasIsrRules) {
243
- useCacheStore(createMemoryStore());
319
+ if (!this.options.cacheStore && this.options.cache?.store !== "fs") useCacheStore(createMemoryStore());
244
320
  if (Object.keys(cacheRules).length > 0) this.hono.use("*", createCacheMiddleware({ rules: cacheRules }));
245
321
  }
246
322
  }
@@ -289,12 +365,20 @@ var UbeanApp = class {
289
365
  ssrExclude: this.options.ssrExclude,
290
366
  streaming: this.options.streaming,
291
367
  botFallback: this.options.botFallback,
292
- i18nConfig: this.options.i18nConfig,
368
+ i18nConfig: this.options.i18nConfig ? {
369
+ strategy: this.options.i18nConfig.strategy,
370
+ defaultLocale: this.options.i18nConfig.defaultLocale,
371
+ locales: (this.options.i18nConfig.locales || []).map((l) => typeof l === "string" ? l : l.code),
372
+ cookieName: this.options.i18nConfig.detectBrowserLanguage === false ? void 0 : this.options.i18nConfig.detectBrowserLanguage?.cookieName || "ubean_locale",
373
+ baseUrl: this.options.i18nConfig.baseUrl
374
+ } : void 0,
293
375
  notFoundPage: this.options.notFoundPage,
294
376
  colorModeScript: this.options.colorModeScript,
295
377
  cacheStore: this.options.routeRules && Object.keys(this.options.routeRules).length > 0 ? useCacheStore() : void 0
296
378
  };
297
379
  await registerRoutes(this, registerOpts);
380
+ if (this.options.ipxHandler) this.hono.get("/_ipx/*", this.options.ipxHandler);
381
+ await this._registerSeoConventions();
298
382
  this.hono.on("POST", ACTIONS_ENDPOINT, createActionsMiddleware());
299
383
  this.hono.on("POST", SERVER_COMPONENT_ENDPOINT, createServerComponentMiddleware());
300
384
  if (this.options.openAPI) {
@@ -307,6 +391,23 @@ var UbeanApp = class {
307
391
  this._ready = true;
308
392
  return this;
309
393
  }
394
+ async _registerSeoConventions() {
395
+ if (this.options.seoConventions === false) return;
396
+ const srcDir = (typeof this.options.seoConventions === "object" ? this.options.seoConventions : {}).srcDir ?? (this.options.rootDir ? join(this.options.rootDir, "src") : void 0);
397
+ try {
398
+ const mod = await import("@ubean/seo/conventions");
399
+ if (this.options.seoConventionModules) {
400
+ await mod.registerSeoConventionModules(this, this.options.seoConventionModules);
401
+ return;
402
+ }
403
+ if (!srcDir) return;
404
+ await mod.registerSeoConventions(this, { srcDir });
405
+ } catch (err) {
406
+ const message = err instanceof Error ? err.message : String(err);
407
+ if (message.includes("Cannot find module '@ubean/seo") || message.includes("Failed to resolve")) return;
408
+ throw err;
409
+ }
410
+ }
310
411
  _lazyInitPromise = null;
311
412
  lazyInit() {
312
413
  if (this._ready) return Promise.resolve(this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/app",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Hono app factory and server config for ubean (createUbeanApp, defineServer)",
5
5
  "files": [
6
6
  "dist"
@@ -19,25 +19,30 @@
19
19
  "hono": "4.13.3",
20
20
  "hookable": "^6.1.1",
21
21
  "pathe": "^2.0.3",
22
- "@ubean/actions": "0.2.2",
23
- "@ubean/islands": "0.2.2",
24
- "@ubean/routes": "0.2.2",
25
- "@ubean/scan": "0.2.2",
26
- "@ubean/shared": "0.2.2",
27
- "@ubean/server": "0.2.2"
22
+ "@ubean/i18n": "0.3.0",
23
+ "@ubean/islands": "0.3.0",
24
+ "@ubean/routes": "0.3.0",
25
+ "@ubean/scan": "0.3.0",
26
+ "@ubean/shared": "0.3.0",
27
+ "@ubean/server": "0.3.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.2.0",
31
31
  "typescript": "7.0.2",
32
32
  "vite-plus": "0.2.9",
33
- "@ubean/pages": "0.2.2"
33
+ "@ubean/pages": "0.3.0",
34
+ "@ubean/seo": "0.3.0"
34
35
  },
35
36
  "peerDependencies": {
36
- "@ubean/pages": "0.2.2"
37
+ "@ubean/pages": "0.3.0",
38
+ "@ubean/seo": "0.3.0"
37
39
  },
38
40
  "peerDependenciesMeta": {
39
41
  "@ubean/pages": {
40
42
  "optional": true
43
+ },
44
+ "@ubean/seo": {
45
+ "optional": true
41
46
  }
42
47
  },
43
48
  "scripts": {