@ubean/routes 0.2.1 → 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.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { C as registerAction, E as isValidActionId, S as listActions, T as createActionId, _ as parseActionInput, a as ACTION_RESPONSE_HEADER, b as getAction, c as unwrapServerFnResult, d as getActionContext, f as runWithActionContext, g as normalizeActionResult, h as defineServerFn, i as ACTIONS_ENDPOINT, l as bindActionContextStorage, m as defineAction, n as hasFormAction, o as invokeServerFn, p as buildActionContext, r as parseFormActionName, s as unwrapActionResult, t as buildFormActionUrl, u as createDetachedActionContext, v as validateActionInput, w as registerActions, x as hasAction, y as clearActions } from "./form-action-CZA0dSMO.js";
1
2
  import { addRoute, createRouter, findRoute } from "rou3";
3
+ import { compileLocalePaths } from "@ubean/i18n";
2
4
  import { validateParams } from "@ubean/scan";
3
- import { isServerAction, matchAnyGlob } from "@ubean/shared";
4
- import { parseFormActionName, runAction } from "@ubean/actions";
5
- import { openAPIRouteHandler } from "hono-openapi";
5
+ import { ACTION_BRAND, ActionError, fail, isActionFailure, isServerAction, isServerAction as isServerAction$1, matchAnyGlob } from "@ubean/shared";
6
+ import { generateSpecs } from "hono-openapi";
6
7
  //#region src/handler.ts
7
8
  function isResponse(value) {
8
9
  return value instanceof Response;
@@ -195,47 +196,80 @@ function isBotUserAgent(ua) {
195
196
  return BOT_PATTERN.test(ua);
196
197
  }
197
198
  //#endregion
198
- //#region src/page-actions.ts
199
+ //#region src/path-transform.ts
199
200
  /**
200
- * Shared redirect/non-redirect Response handling for both the legacy
201
- * `mod.action(c)` path and the `mod.actions` map path.
201
+ * Apply a rewrite/proxy target to a request path (Nuxt-style wildcards).
202
202
  *
203
- * Returns a `Response` to return to the client, or `null` to fall through
204
- * (caller leaves `actionResult` as the Response for downstream handling).
203
+ * - `/old` + `/new` `/new`
204
+ * - rule `/blog/**`, request `/blog/a/b`, target `/news/**` `/news/a/b`
205
+ * - target containing `$1` receives the captured suffix without a leading slash
206
+ * - absolute `https://…` targets keep the origin and transform the pathname
205
207
  */
206
- function handleActionResponse(c, response, isPagesReq) {
207
- if (response.status >= 300 && response.status < 400) {
208
- const redirectUrl = response.headers.get("Location");
209
- if (redirectUrl) {
210
- if (isPagesReq(c)) return c.json({ redirect: redirectUrl }, {
211
- status: 200,
212
- headers: { "X-Ubean-Redirect": redirectUrl }
213
- });
214
- return response;
215
- }
208
+ function applyPathTransform(requestPath, rulePath, target) {
209
+ const suffix = captureWildcardSuffix(requestPath, rulePath);
210
+ const absolute = parseAbsoluteTarget(target);
211
+ if (absolute) {
212
+ const nextPath = substitutePath(absolute.pathname || "/", suffix);
213
+ return `${absolute.origin}${nextPath}${absolute.search}`;
216
214
  }
217
- if (isPagesReq(c)) return response;
218
- return null;
215
+ return substitutePath(target, suffix);
219
216
  }
220
- /**
221
- * Run a `ServerAction` (from a page module's `actions` map) against the
222
- * current Hono context via `runAction` from `@ubean/actions`.
223
- *
224
- * Returns one of:
225
- * - `{ response }` — handler returned/threw a `Response` (redirect)
226
- * - `{ errors }` — validation failure, `fail()` from the handler, or a
227
- * thrown error (mapped to `{ _error: message }`)
228
- * - `{ data }` — successful return value (merged into page props)
229
- */
230
- async function runServerAction(action, c) {
231
- const result = await runAction(action, c);
232
- if (result.response) return { response: result.response };
233
- if (result.errors) return { errors: result.errors ?? {} };
234
- if (result.error) return { errors: { _error: result.error.message } };
235
- return { data: result.data };
217
+ function parseAbsoluteTarget(target) {
218
+ if (!/^https?:\/\//.test(target)) return void 0;
219
+ try {
220
+ const url = new URL(target);
221
+ return {
222
+ origin: url.origin,
223
+ pathname: url.pathname,
224
+ search: url.search
225
+ };
226
+ } catch {
227
+ return;
228
+ }
229
+ }
230
+ function captureWildcardSuffix(requestPath, rulePath) {
231
+ const doubleIdx = rulePath.indexOf("/**");
232
+ if (doubleIdx !== -1) {
233
+ const prefix = rulePath.slice(0, doubleIdx);
234
+ if (requestPath === prefix || requestPath === `${prefix}/`) return "";
235
+ if (requestPath.startsWith(`${prefix}/`)) return requestPath.slice(prefix.length);
236
+ return "";
237
+ }
238
+ const singleIdx = rulePath.indexOf("/*");
239
+ if (singleIdx !== -1 && !rulePath.includes("**")) {
240
+ const prefix = rulePath.slice(0, singleIdx);
241
+ if (requestPath.startsWith(`${prefix}/`)) return `/${requestPath.slice(prefix.length + 1).split("/")[0] ?? ""}`;
242
+ }
243
+ return "";
244
+ }
245
+ function substitutePath(targetPath, suffix) {
246
+ const trimmedSuffix = suffix.replace(/^\//, "");
247
+ if (targetPath.includes("$1")) return targetPath.replaceAll("$1", trimmedSuffix);
248
+ if (targetPath.includes("/**")) return normalizePath(targetPath.replace("/**", suffix || ""));
249
+ if (targetPath.includes("/*")) {
250
+ const first = suffix.split("/").filter(Boolean)[0];
251
+ return normalizePath(targetPath.replace("/*", first ? `/${first}` : ""));
252
+ }
253
+ return normalizePath(targetPath);
254
+ }
255
+ function normalizePath(path) {
256
+ if (!path) return "/";
257
+ return (path.startsWith("/") ? path : `/${path}`).replace(/\/{2,}/g, "/");
236
258
  }
237
259
  //#endregion
238
260
  //#region src/route-rules.ts
261
+ const REWRITE_GUARD = "x-ubean-rewrite";
262
+ const HOP_BY_HOP = /* @__PURE__ */ new Set([
263
+ "connection",
264
+ "keep-alive",
265
+ "proxy-authenticate",
266
+ "proxy-authorization",
267
+ "te",
268
+ "trailers",
269
+ "transfer-encoding",
270
+ "upgrade",
271
+ "host"
272
+ ]);
239
273
  function compileRulePath(pattern) {
240
274
  const DOUBLE = "__DWC__";
241
275
  const SINGLE = "__SWC__";
@@ -256,7 +290,7 @@ function compileRulePath(pattern) {
256
290
  * P9-04 扩展:`ppr` 字段同样参与 specificity 计算。
257
291
  */
258
292
  function ruleSpecificity(rule) {
259
- return (rule.redirect ? 3 : 0) + (rule.rewrite ? 2 : 0) + (rule.headers ? 1 : 0) + (rule.ssr !== void 0 ? 1 : 0) + (rule.isr !== void 0 ? 1 : 0) + (rule.prerender !== void 0 ? 1 : 0) + (rule.ppr !== void 0 ? 1 : 0);
293
+ return (rule.redirect ? 3 : 0) + (rule.rewrite ? 2 : 0) + (rule.proxy ? 2 : 0) + (rule.headers ? 1 : 0) + (rule.ssr !== void 0 ? 1 : 0) + (rule.isr !== void 0 ? 1 : 0) + (rule.prerender !== void 0 ? 1 : 0) + (rule.ppr !== void 0 ? 1 : 0);
260
294
  }
261
295
  /**
262
296
  * 路径特异性计算:作为 rule specificity 的 tiebreaker。
@@ -303,6 +337,7 @@ function matchRouteRules(path, compiledRules) {
303
337
  };
304
338
  if (rule.redirect && !matched.redirect) matched.redirect = rule.redirect;
305
339
  if (rule.rewrite && !matched.rewrite) matched.rewrite = rule.rewrite;
340
+ if (rule.proxy && !matched.proxy) matched.proxy = rule.proxy;
306
341
  if (rule.cache) matched.cache = {
307
342
  ...matched.cache,
308
343
  ...rule.cache
@@ -314,6 +349,9 @@ function matchRouteRules(path, compiledRules) {
314
349
  }
315
350
  return matched;
316
351
  }
352
+ function findFirstRulePath(path, compiledRules, field) {
353
+ for (const { pattern, rule, path: rulePath } of compiledRules) if (pattern.test(path) && rule[field]) return rulePath;
354
+ }
317
355
  function resolveRedirectUrl(redirect, currentPath) {
318
356
  if (typeof redirect === "string") return {
319
357
  url: redirect,
@@ -338,7 +376,28 @@ function buildCacheControlHeader(cache) {
338
376
  }
339
377
  return parts.length > 0 ? parts.join(", ") : null;
340
378
  }
341
- function createRouteRulesMiddleware(rules) {
379
+ function cloneForwardHeaders(source) {
380
+ const headers = new Headers();
381
+ source.forEach((value, key) => {
382
+ if (!HOP_BY_HOP.has(key.toLowerCase())) headers.set(key, value);
383
+ });
384
+ return headers;
385
+ }
386
+ function buildForwardRequest(c, dest, extraHeaders) {
387
+ const headers = cloneForwardHeaders(c.req.raw.headers);
388
+ if (extraHeaders) new Headers(extraHeaders).forEach((value, key) => headers.set(key, value));
389
+ const init = {
390
+ method: c.req.method,
391
+ headers,
392
+ redirect: "manual"
393
+ };
394
+ if (c.req.method !== "GET" && c.req.method !== "HEAD" && c.req.raw.body) {
395
+ init.body = c.req.raw.body;
396
+ init.duplex = "half";
397
+ }
398
+ return new Request(dest, init);
399
+ }
400
+ function createRouteRulesMiddleware(rules, options = {}) {
342
401
  const compiled = compileRouteRules(rules);
343
402
  return async function routeRulesMiddleware(c, next) {
344
403
  const path = c.req.path;
@@ -355,6 +414,23 @@ function createRouteRulesMiddleware(rules) {
355
414
  if (matched.headers) for (const [key, value] of Object.entries(matched.headers)) c.header(key, value);
356
415
  const cacheHeader = buildCacheControlHeader(matched.cache);
357
416
  if (cacheHeader) c.header("Cache-Control", cacheHeader);
417
+ if (matched.proxy) {
418
+ const dest = applyPathTransform(path, findFirstRulePath(path, compiled, "proxy") ?? path, matched.proxy);
419
+ const upstream = await fetch(buildForwardRequest(c, dest));
420
+ return new Response(upstream.body, {
421
+ status: upstream.status,
422
+ statusText: upstream.statusText,
423
+ headers: upstream.headers
424
+ });
425
+ }
426
+ if (matched.rewrite && options.dispatch && !c.req.header(REWRITE_GUARD)) {
427
+ const dest = applyPathTransform(path, findFirstRulePath(path, compiled, "rewrite") ?? path, matched.rewrite);
428
+ if (dest !== path) {
429
+ const url = new URL(c.req.url);
430
+ url.pathname = dest;
431
+ return options.dispatch(buildForwardRequest(c, url.toString(), { [REWRITE_GUARD]: "1" }));
432
+ }
433
+ }
358
434
  await next();
359
435
  };
360
436
  }
@@ -439,7 +515,7 @@ function isIsrEntryStale(pathname, store) {
439
515
  if (!memStore.store) return false;
440
516
  const entry = memStore.store.get(buildIsrCacheKey(pathname));
441
517
  if (!entry?.expiresAt) return false;
442
- return Date.now() > entry.expiresAt;
518
+ return Date.now() >= entry.expiresAt;
443
519
  }
444
520
  /**
445
521
  * ISR 主入口:检查缓存 → 命中则返回,未命中/过期则调用 render 重新生成。
@@ -457,7 +533,7 @@ async function serveIsr(_c, options) {
457
533
  const key = buildIsrCacheKey(pathname);
458
534
  const rawEntry = store.peek ? await store.peek(key) : await store.get(key);
459
535
  if (rawEntry) {
460
- const isStale = Date.now() > rawEntry.expiresAt;
536
+ const isStale = Date.now() >= rawEntry.expiresAt;
461
537
  const html = new TextDecoder().decode(rawEntry.body);
462
538
  if (!isStale) {
463
539
  const headers = new Headers(rawEntry.headers);
@@ -514,6 +590,268 @@ function getIsrRuleFromContext(c) {
514
590
  return normalizeIsrRule(rule.isr);
515
591
  }
516
592
  //#endregion
593
+ //#region src/actions/openapi.ts
594
+ /**
595
+ * Optional OpenAPI fragment for the shared `POST /__actions` RPC.
596
+ *
597
+ * Per-action operations stay on the same endpoint (id in the body) so this
598
+ * does not create a second RPC surface.
599
+ */
600
+ function describeActionsOpenApi() {
601
+ const names = listActions().map((action) => `${action.name} (${action.id})`);
602
+ return { paths: { "/__actions": { post: {
603
+ summary: "Server Actions / server functions RPC",
604
+ operationId: "ubeanActionsRpc",
605
+ description: names.length > 0 ? `Registered functions: ${names.join(", ")}.` : "No server functions registered yet. Define them with defineAction / defineServerFn."
606
+ } } } };
607
+ }
608
+ //#endregion
609
+ //#region src/actions/dispatch.ts
610
+ /**
611
+ * Dispatch a registered server action by ID.
612
+ *
613
+ * Used by the `/__actions` POST endpoint (RPC-style invocation from the
614
+ * client). The action ID is looked up in the global registry; the request
615
+ * body is parsed as JSON or FormData depending on `Content-Type`.
616
+ *
617
+ * When `preParsedInput` is provided (RPC path — the middleware has already
618
+ * read the body to extract the `{ id, args }` envelope), body parsing is
619
+ * skipped and `preParsedInput` is used as the handler input directly.
620
+ *
621
+ * Returns an `ActionResult` (serializable). When the action ID is unknown
622
+ * or invalid, returns a 404 error result without throwing.
623
+ */
624
+ async function dispatchAction(actionId, c, preParsedInput) {
625
+ const action = getAction(actionId);
626
+ if (!action) return {
627
+ error: { message: `Action not found: ${actionId}` },
628
+ status: 404
629
+ };
630
+ return runAction(action, c, preParsedInput);
631
+ }
632
+ /**
633
+ * Run an arbitrary `ServerAction` against the given Hono context.
634
+ *
635
+ * Used internally by `dispatchAction` and by `handlePageRequest` for
636
+ * page-level form actions (which are not in the global registry but
637
+ * are exported from the page module's `actions` map).
638
+ *
639
+ * When `preParsedInput` is provided, body parsing is skipped (the caller
640
+ * has already read the request body — e.g. the RPC middleware extracting
641
+ * the `{ id, args }` envelope).
642
+ */
643
+ async function runAction(action, c, preParsedInput) {
644
+ const ctx = buildActionContext(c);
645
+ const rawInput = preParsedInput !== void 0 ? preParsedInput : await parseActionInput(c.req.raw);
646
+ let input = rawInput;
647
+ if (action.schema) {
648
+ const validation = validateActionInput(action.schema, rawInput);
649
+ if (!validation.success) return {
650
+ errors: validation.errors,
651
+ status: 400
652
+ };
653
+ input = validation.data;
654
+ }
655
+ let result;
656
+ let error;
657
+ try {
658
+ result = await action.handler(input, ctx);
659
+ } catch (err) {
660
+ if (err instanceof Response) return {
661
+ response: err,
662
+ status: err.status
663
+ };
664
+ error = err;
665
+ }
666
+ if (result instanceof Response) return {
667
+ response: result,
668
+ status: result.status
669
+ };
670
+ return normalizeActionResult(result, error);
671
+ }
672
+ /**
673
+ * Run a page-level form action by name (SvelteKit-style `?/name` convention).
674
+ *
675
+ * Page modules may export an `actions` map:
676
+ *
677
+ * ```ts
678
+ * export const actions = {
679
+ * default: defineAction(async (input, ctx) => { ... }),
680
+ * login: defineAction(async (input, ctx) => { ... }),
681
+ * register: defineAction(async (input, ctx) => { ... })
682
+ * };
683
+ * ```
684
+ *
685
+ * - `POST /page` (no `?/`) → `actions.default`
686
+ * - `POST /page?/login` → `actions.login`
687
+ * - `POST /page?/register` → `actions.register`
688
+ *
689
+ * Returns `null` when:
690
+ * - No `actions` export exists (caller should fall back to `mod.action`).
691
+ * - The named action doesn't exist (caller should return 404).
692
+ *
693
+ * The caller (`handlePageRequest`) is responsible for converting the
694
+ * `ActionResult` into an HTTP response (HTML for browser navigation,
695
+ * JSON for SPA `submit()` calls).
696
+ */
697
+ async function runPageAction(actions, actionName, c) {
698
+ const action = actions[actionName];
699
+ if (!action) return null;
700
+ return runAction(action, c);
701
+ }
702
+ //#endregion
703
+ //#region src/actions/middleware.ts
704
+ /**
705
+ * Create the actions middleware — a Hono handler that mounts the
706
+ * `/__actions` POST endpoint.
707
+ *
708
+ * Usage:
709
+ *
710
+ * ```ts
711
+ * import { createUbeanApp } from 'ubean/runtime/app';
712
+ * import { createActionsMiddleware } from '@ubean/routes';
713
+ *
714
+ * const app = createUbeanApp();
715
+ * app.on('POST', '/__actions', createActionsMiddleware());
716
+ * ```
717
+ *
718
+ * The middleware is auto-registered by ubean's dev server / production
719
+ * server setup — users don't need to mount it manually.
720
+ */
721
+ function createActionsMiddleware() {
722
+ return async (c) => {
723
+ let actionId;
724
+ let args;
725
+ if ((c.req.header("Content-Type") || "").includes("application/json")) try {
726
+ const body = await c.req.json();
727
+ actionId = body?.id;
728
+ args = body?.args;
729
+ } catch {}
730
+ if (!actionId) actionId = c.req.query("id");
731
+ if (!actionId) return _jsonResult(c, {
732
+ error: { message: "Missing action id" },
733
+ status: 400
734
+ }, 400);
735
+ const preParsedInput = Array.isArray(args) && args.length > 0 ? args[0] : void 0;
736
+ const result = await dispatchAction(actionId, c, preParsedInput);
737
+ if (result.response) return result.response;
738
+ return _jsonResult(c, result, result.status);
739
+ };
740
+ }
741
+ /**
742
+ * Serialize an `ActionResult` as a JSON Hono response with the action
743
+ * response header set.
744
+ */
745
+ function _jsonResult(c, result, status) {
746
+ return c.json(result, status, {
747
+ [ACTION_RESPONSE_HEADER]: "true",
748
+ "Cache-Control": "no-store"
749
+ });
750
+ }
751
+ /**
752
+ * Check whether a request targets the actions endpoint.
753
+ *
754
+ * Used by route registrars to skip registering regular API routes for
755
+ * the `/__actions` path.
756
+ */
757
+ function isActionsRequest(c) {
758
+ return c.req.method === "POST" && c.req.path === "/__actions";
759
+ }
760
+ /**
761
+ * Check whether a response came from the actions middleware.
762
+ *
763
+ * Used by the client `callAction()` to validate the response shape.
764
+ */
765
+ function isActionResponse(response) {
766
+ return response.headers.get(ACTION_RESPONSE_HEADER) === "true";
767
+ }
768
+ //#endregion
769
+ //#region src/page-actions.ts
770
+ /**
771
+ * Shared redirect/non-redirect Response handling for both the legacy
772
+ * `mod.action(c)` path and the `mod.actions` map path.
773
+ *
774
+ * Returns a `Response` to return to the client, or `null` to fall through
775
+ * (caller leaves `actionResult` as the Response for downstream handling).
776
+ */
777
+ function handleActionResponse(c, response, isPagesReq) {
778
+ if (response.status >= 300 && response.status < 400) {
779
+ const redirectUrl = response.headers.get("Location");
780
+ if (redirectUrl) {
781
+ if (isPagesReq(c)) return c.json({ redirect: redirectUrl }, {
782
+ status: 200,
783
+ headers: { "X-Ubean-Redirect": redirectUrl }
784
+ });
785
+ return response;
786
+ }
787
+ }
788
+ if (isPagesReq(c)) return response;
789
+ return null;
790
+ }
791
+ /**
792
+ * Run a `ServerAction` (from a page module's `actions` map) against the
793
+ * current Hono context via `runAction`.
794
+ *
795
+ * Returns one of:
796
+ * - `{ response }` — handler returned/threw a `Response` (redirect)
797
+ * - `{ errors }` — validation failure, `fail()` from the handler, or a
798
+ * thrown error (mapped to `{ _error: message }`)
799
+ * - `{ data }` — successful return value (merged into page props)
800
+ */
801
+ async function runServerAction(action, c) {
802
+ const result = await runAction(action, c);
803
+ if (result.response) return { response: result.response };
804
+ if (result.errors) return { errors: result.errors ?? {} };
805
+ if (result.error) return { errors: { _error: result.error.message } };
806
+ return { data: result.data };
807
+ }
808
+ //#endregion
809
+ //#region src/select-ssr.ts
810
+ /**
811
+ * TanStack-style select SSR: page meta wins over routeRules, which win over
812
+ * the global exclude list. Glob exclude stays backward-compatible (CSR shell
813
+ * still runs loader). Explicit `ssr: false` skips the loader.
814
+ */
815
+ function resolveSelectSsr(input) {
816
+ const ssr = input.pageSsr ?? input.routeRuleSsr;
817
+ if (ssr === false) return {
818
+ mode: "csr",
819
+ runLoader: false,
820
+ useRenderer: false
821
+ };
822
+ if (ssr === "data-only") return {
823
+ mode: "data-only",
824
+ runLoader: true,
825
+ useRenderer: false
826
+ };
827
+ if (ssr === "streaming" || input.ppr) return {
828
+ mode: "streaming",
829
+ runLoader: true,
830
+ useRenderer: true
831
+ };
832
+ if (ssr === true) return {
833
+ mode: "ssr",
834
+ runLoader: true,
835
+ useRenderer: true
836
+ };
837
+ if (input.excludedByGlob) return {
838
+ mode: "csr",
839
+ runLoader: true,
840
+ useRenderer: false
841
+ };
842
+ return {
843
+ mode: input.streaming ? "streaming" : "ssr",
844
+ runLoader: true,
845
+ useRenderer: true
846
+ };
847
+ }
848
+ function ssrModeHeader(mode) {
849
+ if (mode === "csr") return "csr";
850
+ if (mode === "data-only") return "data-only";
851
+ if (mode === "streaming") return "streaming";
852
+ return "ssr";
853
+ }
854
+ //#endregion
517
855
  //#region src/router.ts
518
856
  const HTTP_METHODS = [
519
857
  "GET",
@@ -753,20 +1091,16 @@ async function registerPageRoutes(app, options) {
753
1091
  function getMatchingMiddleware(routePath) {
754
1092
  return loadedUserMiddleware.filter((mw) => matchMiddlewarePath(mw.mountPath, routePath)).map((mw) => mw.handler);
755
1093
  }
756
- function getLocalePrefixedPaths(honoPath) {
757
- if (!i18nConfig) return [];
758
- const { strategy, defaultLocale, locales = [] } = i18nConfig;
759
- if (strategy === "no_prefix") return [];
760
- const result = [];
761
- for (const locale of locales) {
762
- if (strategy === "prefix_except_default" && locale === defaultLocale) continue;
763
- const cleanPath = honoPath === "/" ? "" : honoPath;
764
- result.push({
765
- locale,
766
- path: `/${locale}${cleanPath}`
767
- });
768
- }
769
- return result;
1094
+ function getHonoLocalePaths(honoPath) {
1095
+ if (!i18nConfig?.locales?.length || i18nConfig.strategy === "no_prefix" || honoPath === "*") return [{
1096
+ locale: i18nConfig?.defaultLocale || "en",
1097
+ path: honoPath
1098
+ }];
1099
+ return compileLocalePaths(honoPath === "/" ? "/" : honoPath, {
1100
+ defaultLocale: i18nConfig.defaultLocale || "en",
1101
+ locales: i18nConfig.locales,
1102
+ strategy: i18nConfig.strategy || "prefix_except_default"
1103
+ }).hono;
770
1104
  }
771
1105
  const hasDefaultLayout = layouts.some((l) => l.isDefault);
772
1106
  const { isPagesRequest, pageJsonResponse, renderPage, renderPageToStream } = pagesMod;
@@ -800,7 +1134,7 @@ async function registerPageRoutes(app, options) {
800
1134
  const actionsExport = mod?.actions;
801
1135
  if (actionsExport && typeof actionsExport === "object") {
802
1136
  const action = actionsExport[parseFormActionName(c.req.url)];
803
- if (action && isServerAction(action)) {
1137
+ if (action && isServerAction$1(action)) {
804
1138
  const res = await runServerAction(action, c);
805
1139
  if (res.response) {
806
1140
  const handled = handleActionResponse(c, res.response, isPagesRequest);
@@ -829,7 +1163,19 @@ async function registerPageRoutes(app, options) {
829
1163
  actionErrors = { _error: err instanceof Error ? err.message : String(err) };
830
1164
  }
831
1165
  }
832
- if (mod?.loader && typeof mod.loader === "function") try {
1166
+ const routeRule = c.get("routeRule");
1167
+ const isrRule = normalizeIsrRule(routeRule?.isr);
1168
+ const pprEnabled = routeRule?.ppr === true;
1169
+ const selected = resolveSelectSsr({
1170
+ pageSsr: page.pageMeta?.ssr,
1171
+ routeRuleSsr: routeRule?.ssr,
1172
+ ppr: pprEnabled,
1173
+ excludedByGlob: matchAnyGlob(page.route, ssrExclude),
1174
+ streaming
1175
+ });
1176
+ const routeStreaming = selected.mode === "streaming";
1177
+ const renderer = selected.useRenderer ? pageRenderer ?? null : null;
1178
+ if (selected.runLoader && mod?.loader && typeof mod.loader === "function") try {
833
1179
  loaderResult = await mod.loader(c);
834
1180
  if (loaderResult instanceof Response) return loaderResult;
835
1181
  } catch {}
@@ -851,28 +1197,26 @@ async function registerPageRoutes(app, options) {
851
1197
  locale: currentLocale,
852
1198
  localeDir: "ltr"
853
1199
  };
1200
+ if (i18nConfig) {
1201
+ renderContext.routing = {
1202
+ defaultLocale: i18nConfig.defaultLocale || "en",
1203
+ locales: i18nConfig.locales || [],
1204
+ strategy: i18nConfig.strategy || "prefix_except_default"
1205
+ };
1206
+ if (i18nConfig.cookieName) renderContext.cookieName = i18nConfig.cookieName;
1207
+ if (i18nConfig.baseUrl) renderContext.baseUrl = i18nConfig.baseUrl;
1208
+ }
854
1209
  const i18nMod = await loadI18n();
855
1210
  if (i18nMod && currentLocale) {
856
1211
  renderContext.localeDir = i18nMod.getLocaleDir(currentLocale);
857
1212
  renderContext.messages = i18nMod.getLocaleMessages(currentLocale);
1213
+ const fallback = i18nMod.getFallbackLocale();
1214
+ renderContext.fallbackLocale = fallback;
1215
+ if (fallback !== currentLocale) renderContext.fallbackMessages = i18nMod.getLocaleMessages(fallback);
858
1216
  renderContext.availableLocales = i18nMod.getRegisteredLocalesMeta();
859
1217
  }
860
- const routeRule = c.get("routeRule");
861
- const perRouteSsr = routeRule?.ssr;
862
- const isrRule = normalizeIsrRule(routeRule?.isr);
863
- const pprEnabled = routeRule?.ppr === true;
864
- let excluded = matchAnyGlob(page.route, ssrExclude);
865
- let routeStreaming = streaming;
866
- if (perRouteSsr === false) excluded = true;
867
- else if (perRouteSsr === true) excluded = false;
868
- else if (perRouteSsr === "streaming") {
869
- excluded = false;
870
- routeStreaming = true;
871
- } else if (pprEnabled) {
872
- excluded = false;
873
- routeStreaming = true;
874
- }
875
- const renderer = excluded ? null : pageRenderer ?? null;
1218
+ const htmlHeaders = { "X-SSR-Mode": ssrModeHeader(selected.mode) };
1219
+ if (pprEnabled) htmlHeaders["X-PPR"] = "streaming";
876
1220
  if (isrRule && cacheStore && method === "GET") {
877
1221
  const pathname = new URL(c.req.url).pathname;
878
1222
  const isrResponse = await serveIsr(c, {
@@ -883,7 +1227,10 @@ async function registerPageRoutes(app, options) {
883
1227
  return {
884
1228
  html: await renderPage(pageObj, pageAssetTags ?? {}, renderer, "app", renderContext),
885
1229
  status: 200,
886
- headers: { "Content-Type": "text/html; charset=utf-8" }
1230
+ headers: {
1231
+ "Content-Type": "text/html; charset=utf-8",
1232
+ ...htmlHeaders
1233
+ }
887
1234
  };
888
1235
  }
889
1236
  });
@@ -893,15 +1240,19 @@ async function registerPageRoutes(app, options) {
893
1240
  const isBot = botFallback && isBotUserAgent(userAgent);
894
1241
  if (routeStreaming && renderer && !isBot) {
895
1242
  const stream = renderPageToStream(pageObj, pageAssetTags ?? {}, renderer, "app", renderContext);
896
- const headers = { "Content-Type": "text/html; charset=utf-8" };
897
- if (pprEnabled) headers["X-PPR"] = "true";
898
1243
  return c.body(stream, {
899
- headers,
1244
+ headers: {
1245
+ "Content-Type": "text/html; charset=utf-8",
1246
+ ...htmlHeaders
1247
+ },
900
1248
  ...method === "POST" && actionErrors ? { status: 422 } : {}
901
1249
  });
902
1250
  }
903
1251
  const html = await renderPage(pageObj, pageAssetTags ?? {}, renderer, "app", renderContext);
904
- return c.html(injectColorMode(html), method === "POST" && actionErrors ? { status: 422 } : void 0);
1252
+ return c.html(injectColorMode(html), {
1253
+ headers: htmlHeaders,
1254
+ ...method === "POST" && actionErrors ? { status: 422 } : {}
1255
+ });
905
1256
  }
906
1257
  const pageByName = /* @__PURE__ */ new Map();
907
1258
  for (const p of pages) pageByName.set(p.name, p);
@@ -910,14 +1261,6 @@ async function registerPageRoutes(app, options) {
910
1261
  const honoPath = convertUbeanRoutePath(page.route);
911
1262
  const matchingMiddleware = getMatchingMiddleware(honoPath);
912
1263
  const pageMeta = { requiresAuth: page.pageMeta?.requiresAuth ?? true };
913
- const metaMiddleware = async (c, next) => {
914
- c.set("route", {
915
- meta: pageMeta,
916
- path: c.req.path,
917
- method: c.req.method
918
- });
919
- await next();
920
- };
921
1264
  const matcherMiddleware = async (c, next) => {
922
1265
  if (!page.matchers) {
923
1266
  await next();
@@ -933,9 +1276,7 @@ async function registerPageRoutes(app, options) {
933
1276
  };
934
1277
  const loaderKey = page.isReuse && page.reuseTarget ? pageByName.get(page.reuseTarget)?.relativePath ?? page.relativePath : page.relativePath;
935
1278
  const pageHandler = (method) => async (c) => handlePageRequest(c, page, method, loaderKey);
936
- app.on(["GET"], honoPath, metaMiddleware, matcherMiddleware, ...matchingMiddleware, pageHandler("GET"));
937
- app.on(["POST"], honoPath, metaMiddleware, matcherMiddleware, ...matchingMiddleware, pageHandler("POST"));
938
- for (const { path: localePath } of getLocalePrefixedPaths(honoPath)) {
1279
+ for (const { path: mountPath } of getHonoLocalePaths(honoPath)) {
939
1280
  const localeMetaMiddleware = async (c, next) => {
940
1281
  c.set("route", {
941
1282
  meta: pageMeta,
@@ -944,8 +1285,8 @@ async function registerPageRoutes(app, options) {
944
1285
  });
945
1286
  await next();
946
1287
  };
947
- app.on(["GET"], localePath, localeMetaMiddleware, matcherMiddleware, ...matchingMiddleware, pageHandler("GET"));
948
- app.on(["POST"], localePath, localeMetaMiddleware, matcherMiddleware, ...matchingMiddleware, pageHandler("POST"));
1288
+ app.on(["GET"], mountPath, localeMetaMiddleware, matcherMiddleware, ...matchingMiddleware, pageHandler("GET"));
1289
+ app.on(["POST"], mountPath, localeMetaMiddleware, matcherMiddleware, ...matchingMiddleware, pageHandler("POST"));
949
1290
  }
950
1291
  }
951
1292
  if (options.notFoundPage) {
@@ -978,6 +1319,9 @@ async function registerPageRoutes(app, options) {
978
1319
  if (i18nMod && currentLocale) {
979
1320
  renderContext.localeDir = i18nMod.getLocaleDir(currentLocale);
980
1321
  renderContext.messages = i18nMod.getLocaleMessages(currentLocale);
1322
+ const fallback = i18nMod.getFallbackLocale();
1323
+ renderContext.fallbackLocale = fallback;
1324
+ if (fallback !== currentLocale) renderContext.fallbackMessages = i18nMod.getLocaleMessages(fallback);
981
1325
  renderContext.availableLocales = i18nMod.getRegisteredLocalesMeta();
982
1326
  }
983
1327
  const pageObj = {
@@ -1085,15 +1429,25 @@ function createInternalAdapter(c, options) {
1085
1429
  //#region src/openapi.ts
1086
1430
  function registerOpenAPIRoutes(app, options = {}) {
1087
1431
  const { scalarPath = "/_scalar", openAPIPath = "/_openapi.json", title = "API Reference" } = options;
1088
- const openapiHandler = openAPIRouteHandler(app, { documentation: {
1432
+ const documentation = {
1089
1433
  info: {
1090
1434
  title: options.title || "UBEAN API",
1091
1435
  version: options.version || "1.0.0",
1092
1436
  description: options.description
1093
1437
  },
1094
1438
  servers: options.baseURL ? [{ url: options.baseURL }] : void 0
1095
- } });
1096
- app.get(openAPIPath, openapiHandler);
1439
+ };
1440
+ app.get(openAPIPath, async (c) => {
1441
+ const spec = await generateSpecs(app, { documentation });
1442
+ const { paths } = describeActionsOpenApi();
1443
+ return c.json({
1444
+ ...spec,
1445
+ paths: {
1446
+ ...spec.paths,
1447
+ ...paths
1448
+ }
1449
+ });
1450
+ });
1097
1451
  app.get(scalarPath, (c) => {
1098
1452
  const scalarHTML = `<!doctype html>
1099
1453
  <html>
@@ -1125,4 +1479,4 @@ function registerOpenAPIRoutes(app, options = {}) {
1125
1479
  });
1126
1480
  }
1127
1481
  //#endregion
1128
- export { UbeanRouter, 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 };
1482
+ export { ACTIONS_ENDPOINT, ACTION_BRAND, ACTION_RESPONSE_HEADER, ActionError, UbeanRouter, applyPathTransform, bindActionContextStorage, buildActionContext, buildFormActionUrl, buildIsrCacheKey, clearActions, clearInternalFetcher, compileRouteRules, createActionId, createActionsMiddleware, createDetachedActionContext, createInternalAdapter, createRouteLoader, createRouteRulesMiddleware, createServerRouter, defineAction, defineHandler, defineHandlerMeta, defineMiddleware, defineServerFn, describeActionsOpenApi, dispatchAction, extractRouteMeta, fail, getAction, getActionContext, getInternalFetcher, getIsrCache, getIsrRuleFromContext, getStaleIsrCache, hasAction, hasFormAction, invalidateIsrCache, invalidateIsrCachePattern, invokeServerFn, isActionFailure, isActionResponse, isActionsRequest, isBotUserAgent, isHandlerChain, isIsrEntryStale, isServerAction, isValidActionId, listActions, matchRouteRules, normalizeActionResult, normalizeIsrRule, parseActionInput, parseFormActionName, registerAction, registerActions, registerApiRoutes, registerOpenAPIRoutes, registerPageRoutes, registerRoutes, resolveSelectSsr, runAction, runPageAction, runWithActionContext, serveIsr, setInternalFetcher, setIsrCache, sortPagesForRegistration, ssrModeHeader, unwrapActionResult, unwrapServerFnResult, useRouter, validateActionInput };