@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 +59 -2
- package/dist/index.js +109 -8
- package/package.json +14 -9
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 {
|
|
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 {
|
|
8
|
-
import {
|
|
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.
|
|
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/
|
|
23
|
-
"@ubean/islands": "0.
|
|
24
|
-
"@ubean/routes": "0.
|
|
25
|
-
"@ubean/scan": "0.
|
|
26
|
-
"@ubean/shared": "0.
|
|
27
|
-
"@ubean/server": "0.
|
|
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.
|
|
33
|
+
"@ubean/pages": "0.3.0",
|
|
34
|
+
"@ubean/seo": "0.3.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
|
-
"@ubean/pages": "0.
|
|
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": {
|