@finesoft/front 0.1.38 → 0.1.39

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../ssr/src/render.ts","../../ssr/src/create-render.ts","../../ssr/src/inject.ts","../../ssr/src/server-data.ts"],"sourcesContent":["/**\n * ssrRender — 通用 SSR 渲染管线\n *\n * 1. 创建 Framework + 注册 Controllers\n * 2. routeUrl → Intent\n * 3. dispatch → Page 数据\n * 4. 调用应用层提供的渲染函数\n */\n\nimport {\n\tFramework,\n\tcreateServerContext,\n\ttype BasePage,\n\ttype FrameworkConfig,\n\ttype MiddlewareResult,\n\ttype PostLoadContext,\n\ttype PrefetchedIntent,\n} from \"@finesoft/core\";\n\nexport interface SSRRenderOptions {\n\t/** 请求 URL */\n\turl: string;\n\t/** Framework 配置(含路由注册等) */\n\tframeworkConfig: FrameworkConfig;\n\t/** 注册 controllers 和路由的引导函数 */\n\tbootstrap: (framework: Framework) => void;\n\t/** 获取错误页面 */\n\tgetErrorPage: (status: number, message: string) => BasePage;\n\t/** 应用层渲染函数(如 Svelte SSR render) */\n\trenderApp: (page: BasePage, framework: Framework) => SSRAppResult;\n\t/** 可选的 SSR 请求上下文(如自定义 fetch) */\n\tssrContext?: SSRContext;\n}\n\n/** SSR 请求级上下文 */\nexport interface SSRContext {\n\t/** 自定义 fetch(如 Hono 内部路由回环) */\n\tfetch?: typeof globalThis.fetch;\n\t/** 原始 Request 对象(用于中间件读取 cookie/header) */\n\trequest?: Request;\n}\n\nexport interface SSRAppResult {\n\thtml: string;\n\thead: string;\n\tcss: string;\n}\n\nexport interface SSRRenderResult {\n\thtml: string;\n\thead: string;\n\tcss: string;\n\tserverData: PrefetchedIntent[];\n\t/** 该路由的渲染模式(由 Router 返回) */\n\trenderMode?: string;\n\t/** 中间件要求的重定向(服务端应返回 HTTP 301/302) */\n\tredirect?: { url: string; status: number };\n}\n\nexport async function ssrRender(\n\toptions: SSRRenderOptions,\n): Promise<SSRRenderResult> {\n\tconst {\n\t\turl,\n\t\tframeworkConfig,\n\t\tbootstrap,\n\t\tgetErrorPage,\n\t\trenderApp,\n\t\tssrContext,\n\t} = options;\n\n\t// 将 SSR 上下文中的 fetch 合并到 frameworkConfig,注入 DI 容器\n\tconst mergedConfig: FrameworkConfig = ssrContext?.fetch\n\t\t? { ...frameworkConfig, fetch: ssrContext.fetch }\n\t\t: frameworkConfig;\n\n\tconst framework = Framework.create(mergedConfig);\n\tbootstrap(framework);\n\n\ttry {\n\t\tconst parsed = new URL(url, \"http://localhost\");\n\t\tconst fullPath = parsed.pathname + parsed.search;\n\t\tconst match = framework.routeUrl(fullPath);\n\n\t\t// CSR 模式:跳过服务端渲染,返回空内容由客户端 JS 渲染\n\t\tif (match?.renderMode === \"csr\") {\n\t\t\treturn {\n\t\t\t\thtml: \"\",\n\t\t\t\thead: \"\",\n\t\t\t\tcss: \"\",\n\t\t\t\tserverData: [],\n\t\t\t\trenderMode: \"csr\",\n\t\t\t};\n\t\t}\n\n\t\tlet page: BasePage;\n\t\tlet serverData: PrefetchedIntent[] = [];\n\n\t\tif (match) {\n\t\t\t// ===== beforeLoad 中间件 =====\n\t\t\tconst navCtx = createServerContext({\n\t\t\t\turl: fullPath,\n\t\t\t\tintent: match.intent,\n\t\t\t\tcontainer: framework.container,\n\t\t\t\trequest: ssrContext?.request,\n\t\t\t});\n\t\t\tconst beforeResult = await framework.runBeforeLoad(\n\t\t\t\tnavCtx,\n\t\t\t\tmatch.beforeGuards,\n\t\t\t);\n\t\t\tif (beforeResult.kind !== \"next\") {\n\t\t\t\tconst earlyReturn = handleMiddlewareResult(\n\t\t\t\t\tbeforeResult,\n\t\t\t\t\tgetErrorPage,\n\t\t\t\t\trenderApp,\n\t\t\t\t\tframework,\n\t\t\t\t);\n\t\t\t\tif (earlyReturn) return earlyReturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tpage = (await framework.dispatch(match.intent)) as BasePage;\n\t\t\t\tserverData = [{ intent: match.intent, data: page }];\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[SSR] dispatch failed for intent \"${match.intent.id}\":`,\n\t\t\t\t\te,\n\t\t\t\t);\n\t\t\t\tpage = getErrorPage(500, \"Internal error\");\n\t\t\t}\n\n\t\t\t// ===== afterLoad 中间件 =====\n\t\t\tconst postCtx: PostLoadContext = {\n\t\t\t\t...navCtx,\n\t\t\t\tpage,\n\t\t\t};\n\t\t\tconst afterResult = await framework.runAfterLoad(\n\t\t\t\tpostCtx,\n\t\t\t\tmatch.afterGuards,\n\t\t\t);\n\t\t\tif (afterResult.kind !== \"next\") {\n\t\t\t\tconst lateReturn = handleMiddlewareResult(\n\t\t\t\t\tafterResult,\n\t\t\t\t\tgetErrorPage,\n\t\t\t\t\trenderApp,\n\t\t\t\t\tframework,\n\t\t\t\t);\n\t\t\t\tif (lateReturn) return lateReturn;\n\t\t\t}\n\t\t} else {\n\t\t\tpage = getErrorPage(404, \"Page not found\");\n\t\t}\n\n\t\tconst result = renderApp(page, framework);\n\n\t\treturn {\n\t\t\thtml: result.html,\n\t\t\thead: result.head,\n\t\t\tcss: result.css,\n\t\t\tserverData,\n\t\t\trenderMode: match?.renderMode,\n\t\t};\n\t} finally {\n\t\tframework.dispose();\n\t}\n}\n\n/**\n * 将中间件结果转换为 SSRRenderResult(如果需要短路返回)。\n * 返回 null 表示继续正常流程。\n */\nfunction handleMiddlewareResult(\n\tresult: MiddlewareResult,\n\tgetErrorPage: (status: number, message: string) => BasePage,\n\trenderApp: (page: BasePage, framework: Framework) => SSRAppResult,\n\tframework: Framework,\n): SSRRenderResult | null {\n\tswitch (result.kind) {\n\t\tcase \"next\":\n\t\t\treturn null;\n\t\tcase \"redirect\":\n\t\t\treturn {\n\t\t\t\thtml: \"\",\n\t\t\t\thead: \"\",\n\t\t\t\tcss: \"\",\n\t\t\t\tserverData: [],\n\t\t\t\tredirect: { url: result.url, status: result.status },\n\t\t\t};\n\t\tcase \"rewrite\":\n\t\t\treturn {\n\t\t\t\thtml: \"\",\n\t\t\t\thead: \"\",\n\t\t\t\tcss: \"\",\n\t\t\t\tserverData: [],\n\t\t\t\tredirect: { url: result.url, status: 301 },\n\t\t\t};\n\t\tcase \"deny\": {\n\t\t\tconst errorPage = getErrorPage(result.status, result.message);\n\t\t\tconst rendered = renderApp(errorPage, framework);\n\t\t\treturn {\n\t\t\t\thtml: rendered.html,\n\t\t\t\thead: rendered.head,\n\t\t\t\tcss: rendered.css,\n\t\t\t\tserverData: [],\n\t\t\t};\n\t\t}\n\t}\n}\n","/**\n * createSSRRender — 工厂函数,返回可直接被 SSR 服务器调用的 render 函数\n *\n * 将一次性配置(bootstrap / getErrorPage / renderApp)绑定后,\n * 返回 `(url, locale) => Promise<SSRRenderResult>` 签名,\n * 与 @finesoft/server 的 SSRModule 接口对齐。\n */\n\nimport type { BasePage, Framework, FrameworkConfig } from \"@finesoft/core\";\nimport {\n\tssrRender,\n\ttype SSRAppResult,\n\ttype SSRContext,\n\ttype SSRRenderResult,\n} from \"./render\";\n\nexport interface SSRRenderConfig {\n\t/** 注册 controllers 和路由的引导函数 */\n\tbootstrap: (framework: Framework) => void;\n\n\t/** 获取错误页面 */\n\tgetErrorPage: (status: number, message: string) => BasePage;\n\n\t/**\n\t * 应用层渲染函数\n\t *\n\t * @param page - 当前页面数据\n\t * @param locale - 当前语言\n\t * @returns SSR 渲染结果 { html, head, css }\n\t */\n\trenderApp: (page: BasePage, locale: string) => SSRAppResult;\n\n\t/** Framework 构造配置(可选) */\n\tframeworkConfig?: FrameworkConfig;\n}\n\n/**\n * 创建 render 函数\n *\n * @returns `render(url, locale, ssrContext?)` — 供 @finesoft/server SSRModule 使用\n */\nexport function createSSRRender(\n\tconfig: SSRRenderConfig,\n): (\n\turl: string,\n\tlocale: string,\n\tssrContext?: SSRContext,\n) => Promise<SSRRenderResult> {\n\tconst { bootstrap, getErrorPage, renderApp, frameworkConfig } = config;\n\n\treturn (url: string, locale: string, ssrContext?: SSRContext) =>\n\t\tssrRender({\n\t\t\turl,\n\t\t\tframeworkConfig: frameworkConfig ?? {},\n\t\t\tbootstrap,\n\t\t\tgetErrorPage,\n\t\t\trenderApp: (page) => renderApp(page, locale),\n\t\t\tssrContext,\n\t\t});\n}\n","/**\n * injectSSRContent — 将 SSR 渲染结果注入 HTML 模板\n */\n\n/** SSR HTML 模板占位符常量 */\nexport const SSR_PLACEHOLDERS = {\n\tLANG: \"<!--ssr-lang-->\",\n\tHEAD: \"<!--ssr-head-->\",\n\tBODY: \"<!--ssr-body-->\",\n\tDATA: \"<!--ssr-data-->\",\n} as const;\n\nexport interface InjectSSROptions {\n\ttemplate: string;\n\tlocale: string;\n\thead: string;\n\tcss: string;\n\thtml: string;\n\tserializedData: string;\n}\n\nexport function injectSSRContent(options: InjectSSROptions): string {\n\tconst { template, locale, head, css, html, serializedData } = options;\n\tconst cssTag = css ? `<style>${css}</style>` : \"\";\n\n\treturn template\n\t\t.replace(SSR_PLACEHOLDERS.LANG, locale)\n\t\t.replace(SSR_PLACEHOLDERS.HEAD, `${head}\\n${cssTag}`)\n\t\t.replace(SSR_PLACEHOLDERS.BODY, html)\n\t\t.replace(\n\t\t\tSSR_PLACEHOLDERS.DATA,\n\t\t\t`<script id=\"serialized-server-data\" type=\"application/json\">${serializedData}</script>`,\n\t\t);\n}\n\n/**\n * CSR 空壳注入 — 只替换 lang,清空 body/head/data 占位符\n * 用于 renderMode === \"csr\" 的路由\n */\nexport function injectCSRShell(template: string, locale: string): string {\n\treturn template\n\t\t.replace(SSR_PLACEHOLDERS.LANG, locale)\n\t\t.replace(SSR_PLACEHOLDERS.HEAD, \"\")\n\t\t.replace(SSR_PLACEHOLDERS.BODY, \"\")\n\t\t.replace(SSR_PLACEHOLDERS.DATA, \"\");\n}\n","/**\n * serializeServerData — 将 PrefetchedIntents 数据序列化为安全的 JSON\n *\n * 返回值可安全嵌入 <script> 标签。\n */\n\nimport type { PrefetchedIntent } from \"@finesoft/core\";\n\nconst HTML_REPLACEMENTS: Record<string, string> = {\n\t\"<\": \"\\\\u003C\",\n\t\">\": \"\\\\u003E\",\n\t\"/\": \"\\\\u002F\",\n\t\"\\u2028\": \"\\\\u2028\",\n\t\"\\u2029\": \"\\\\u2029\",\n};\n\nconst HTML_ESCAPE_PATTERN = /[<>/\\u2028\\u2029]/g;\n\nexport function serializeServerData(data: PrefetchedIntent[]): string {\n\tconst json = JSON.stringify(data);\n\treturn json.replace(\n\t\tHTML_ESCAPE_PATTERN,\n\t\t(match) => HTML_REPLACEMENTS[match] ?? match,\n\t);\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -395,7 +395,8 @@ var init_router = __esm({
395
395
  Router = class {
396
396
  routes = [];
397
397
  /** 添加路由规则 */
398
- add(pattern, intentId, renderMode) {
398
+ add(pattern, intentId, renderModeOrOptions) {
399
+ const opts = typeof renderModeOrOptions === "string" ? { renderMode: renderModeOrOptions } : renderModeOrOptions ?? {};
399
400
  const paramNames = [];
400
401
  const regexStr = pattern.split(/(\/:[\w]+\??)/).map((segment) => {
401
402
  const paramMatch = segment.match(/^\/:(\w+)(\?)?$/);
@@ -410,7 +411,9 @@ var init_router = __esm({
410
411
  intentId,
411
412
  regex: new RegExp(`^${regexStr}/?$`),
412
413
  paramNames,
413
- renderMode
414
+ renderMode: opts.renderMode,
415
+ beforeGuards: opts.beforeGuards,
416
+ afterGuards: opts.afterGuards
414
417
  });
415
418
  return this;
416
419
  }
@@ -432,7 +435,9 @@ var init_router = __esm({
432
435
  return {
433
436
  intent: { id: route.intentId, params },
434
437
  action: makeFlowAction(urlOrPath),
435
- renderMode: route.renderMode
438
+ renderMode: route.renderMode,
439
+ beforeGuards: route.beforeGuards,
440
+ afterGuards: route.afterGuards
436
441
  };
437
442
  }
438
443
  }
@@ -507,6 +512,27 @@ var init_composite = __esm({
507
512
  }
508
513
  });
509
514
 
515
+ // ../core/src/middleware/pipeline.ts
516
+ async function runBeforeLoadGuards(guards, ctx) {
517
+ for (const guard of guards) {
518
+ const result = await guard(ctx);
519
+ if (result.kind !== "next") return result;
520
+ }
521
+ return { kind: "next" };
522
+ }
523
+ async function runAfterLoadGuards(guards, ctx) {
524
+ for (const guard of guards) {
525
+ const result = await guard(ctx);
526
+ if (result.kind !== "next") return result;
527
+ }
528
+ return { kind: "next" };
529
+ }
530
+ var init_pipeline = __esm({
531
+ "../core/src/middleware/pipeline.ts"() {
532
+ "use strict";
533
+ }
534
+ });
535
+
510
536
  // ../core/src/prefetched-intents/stable-stringify.ts
511
537
  function stableStringify(obj, _seen) {
512
538
  if (obj === null || obj === void 0) return String(obj);
@@ -589,6 +615,7 @@ var init_framework = __esm({
589
615
  init_container();
590
616
  init_make_dependencies();
591
617
  init_dispatcher2();
618
+ init_pipeline();
592
619
  init_prefetched_intents();
593
620
  init_router();
594
621
  Framework = class _Framework {
@@ -597,6 +624,8 @@ var init_framework = __esm({
597
624
  actionDispatcher;
598
625
  router;
599
626
  prefetchedIntents;
627
+ beforeGuards = [];
628
+ afterGuards = [];
600
629
  constructor(container, prefetchedIntents) {
601
630
  this.container = container;
602
631
  this.intentDispatcher = new IntentDispatcher();
@@ -660,6 +689,25 @@ var init_framework = __esm({
660
689
  registerIntent(controller) {
661
690
  this.intentDispatcher.register(controller);
662
691
  }
692
+ // ===== Navigation Middleware =====
693
+ /** 注册 beforeLoad 守卫(路由匹配后、数据加载前) */
694
+ beforeLoad(guard) {
695
+ this.beforeGuards.push(guard);
696
+ }
697
+ /** 注册 afterLoad 守卫(数据加载后、渲染前) */
698
+ afterLoad(guard) {
699
+ this.afterGuards.push(guard);
700
+ }
701
+ /** 执行所有 beforeLoad 守卫(全局 → 路由级) */
702
+ runBeforeLoad(ctx, routeGuards) {
703
+ const guards = routeGuards?.length ? [...this.beforeGuards, ...routeGuards] : this.beforeGuards;
704
+ return runBeforeLoadGuards(guards, ctx);
705
+ }
706
+ /** 执行所有 afterLoad 守卫(全局 → 路由级) */
707
+ runAfterLoad(ctx, routeGuards) {
708
+ const guards = routeGuards?.length ? [...this.afterGuards, ...routeGuards] : this.afterGuards;
709
+ return runAfterLoadGuards(guards, ctx);
710
+ }
663
711
  /** 销毁 Framework 实例 */
664
712
  dispose() {
665
713
  this.container.dispose();
@@ -823,7 +871,11 @@ function defineRoutes(framework, definitions) {
823
871
  framework.registerIntent(def.controller);
824
872
  registeredIntents.add(def.intentId);
825
873
  }
826
- framework.router.add(def.path, def.intentId, def.renderMode);
874
+ framework.router.add(def.path, def.intentId, {
875
+ renderMode: def.renderMode,
876
+ beforeGuards: def.beforeLoad,
877
+ afterGuards: def.afterLoad
878
+ });
827
879
  }
828
880
  }
829
881
  var init_define_routes = __esm({
@@ -943,6 +995,74 @@ var init_uuid = __esm({
943
995
  }
944
996
  });
945
997
 
998
+ // ../core/src/middleware/context.ts
999
+ function parseCookieString(str) {
1000
+ const map = /* @__PURE__ */ new Map();
1001
+ if (!str) return map;
1002
+ for (const pair of str.split(";")) {
1003
+ const idx = pair.indexOf("=");
1004
+ if (idx === -1) continue;
1005
+ const key = pair.slice(0, idx).trim();
1006
+ const val = pair.slice(idx + 1).trim();
1007
+ if (key) map.set(key, val);
1008
+ }
1009
+ return map;
1010
+ }
1011
+ function createServerContext(options) {
1012
+ const { url, intent, container, request } = options;
1013
+ const parsed = new URL(url, "http://localhost");
1014
+ const cookieHeader = request?.headers.get("cookie") ?? "";
1015
+ const cookies = parseCookieString(cookieHeader);
1016
+ return {
1017
+ url,
1018
+ path: parsed.pathname,
1019
+ params: intent.params ?? {},
1020
+ intent,
1021
+ isServer: true,
1022
+ container,
1023
+ getCookie: (name) => cookies.get(name),
1024
+ getHeader: (name) => request?.headers.get(name) ?? void 0
1025
+ };
1026
+ }
1027
+ function createBrowserContext(options) {
1028
+ const { url, intent, container } = options;
1029
+ const parsed = new URL(url, window.location.origin);
1030
+ return {
1031
+ url,
1032
+ path: parsed.pathname,
1033
+ params: intent.params ?? {},
1034
+ intent,
1035
+ isServer: false,
1036
+ container,
1037
+ getCookie: (name) => parseCookieString(document.cookie).get(name),
1038
+ getHeader: () => void 0
1039
+ };
1040
+ }
1041
+ var init_context = __esm({
1042
+ "../core/src/middleware/context.ts"() {
1043
+ "use strict";
1044
+ }
1045
+ });
1046
+
1047
+ // ../core/src/middleware/types.ts
1048
+ function next() {
1049
+ return { kind: "next" };
1050
+ }
1051
+ function redirect(url, status = 302) {
1052
+ return { kind: "redirect", url, status };
1053
+ }
1054
+ function rewrite(url) {
1055
+ return { kind: "rewrite", url };
1056
+ }
1057
+ function deny(status = 403, message = "Forbidden") {
1058
+ return { kind: "deny", status, message };
1059
+ }
1060
+ var init_types2 = __esm({
1061
+ "../core/src/middleware/types.ts"() {
1062
+ "use strict";
1063
+ }
1064
+ });
1065
+
946
1066
  // ../core/src/index.ts
947
1067
  var init_src = __esm({
948
1068
  "../core/src/index.ts"() {
@@ -968,6 +1088,9 @@ var init_src = __esm({
968
1088
  init_optional();
969
1089
  init_url();
970
1090
  init_uuid();
1091
+ init_context();
1092
+ init_pipeline();
1093
+ init_types2();
971
1094
  }
972
1095
  });
973
1096
 
@@ -1000,6 +1123,25 @@ async function ssrRender(options) {
1000
1123
  let page;
1001
1124
  let serverData = [];
1002
1125
  if (match) {
1126
+ const navCtx = createServerContext({
1127
+ url: fullPath,
1128
+ intent: match.intent,
1129
+ container: framework.container,
1130
+ request: ssrContext?.request
1131
+ });
1132
+ const beforeResult = await framework.runBeforeLoad(
1133
+ navCtx,
1134
+ match.beforeGuards
1135
+ );
1136
+ if (beforeResult.kind !== "next") {
1137
+ const earlyReturn = handleMiddlewareResult(
1138
+ beforeResult,
1139
+ getErrorPage,
1140
+ renderApp,
1141
+ framework
1142
+ );
1143
+ if (earlyReturn) return earlyReturn;
1144
+ }
1003
1145
  try {
1004
1146
  page = await framework.dispatch(match.intent);
1005
1147
  serverData = [{ intent: match.intent, data: page }];
@@ -1010,6 +1152,23 @@ async function ssrRender(options) {
1010
1152
  );
1011
1153
  page = getErrorPage(500, "Internal error");
1012
1154
  }
1155
+ const postCtx = {
1156
+ ...navCtx,
1157
+ page
1158
+ };
1159
+ const afterResult = await framework.runAfterLoad(
1160
+ postCtx,
1161
+ match.afterGuards
1162
+ );
1163
+ if (afterResult.kind !== "next") {
1164
+ const lateReturn = handleMiddlewareResult(
1165
+ afterResult,
1166
+ getErrorPage,
1167
+ renderApp,
1168
+ framework
1169
+ );
1170
+ if (lateReturn) return lateReturn;
1171
+ }
1013
1172
  } else {
1014
1173
  page = getErrorPage(404, "Page not found");
1015
1174
  }
@@ -1025,6 +1184,38 @@ async function ssrRender(options) {
1025
1184
  framework.dispose();
1026
1185
  }
1027
1186
  }
1187
+ function handleMiddlewareResult(result, getErrorPage, renderApp, framework) {
1188
+ switch (result.kind) {
1189
+ case "next":
1190
+ return null;
1191
+ case "redirect":
1192
+ return {
1193
+ html: "",
1194
+ head: "",
1195
+ css: "",
1196
+ serverData: [],
1197
+ redirect: { url: result.url, status: result.status }
1198
+ };
1199
+ case "rewrite":
1200
+ return {
1201
+ html: "",
1202
+ head: "",
1203
+ css: "",
1204
+ serverData: [],
1205
+ redirect: { url: result.url, status: 301 }
1206
+ };
1207
+ case "deny": {
1208
+ const errorPage = getErrorPage(result.status, result.message);
1209
+ const rendered = renderApp(errorPage, framework);
1210
+ return {
1211
+ html: rendered.html,
1212
+ head: rendered.head,
1213
+ css: rendered.css,
1214
+ serverData: []
1215
+ };
1216
+ }
1217
+ }
1218
+ }
1028
1219
  var init_render = __esm({
1029
1220
  "../ssr/src/render.ts"() {
1030
1221
  "use strict";
@@ -1294,17 +1485,22 @@ function createSSRApp(options) {
1294
1485
  const cached = isrCache.get(cacheKey);
1295
1486
  if (cached) return c.html(cached);
1296
1487
  const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
1488
+ const ssrContext = { request: c.req.raw };
1489
+ if (requestFetch) ssrContext.fetch = requestFetch;
1297
1490
  const {
1298
1491
  html: appHtml,
1299
1492
  head,
1300
1493
  css,
1301
1494
  serverData,
1302
- renderMode
1303
- } = await render(
1304
- url,
1305
- locale,
1306
- requestFetch ? { fetch: requestFetch } : void 0
1307
- );
1495
+ renderMode,
1496
+ redirect: middlewareRedirect
1497
+ } = await render(url, locale, ssrContext);
1498
+ if (middlewareRedirect) {
1499
+ return c.redirect(
1500
+ middlewareRedirect.url,
1501
+ middlewareRedirect.status
1502
+ );
1503
+ }
1308
1504
  if (renderMode === "csr") {
1309
1505
  return c.html(injectCSRShell(template, locale));
1310
1506
  }
@@ -1368,11 +1564,14 @@ __export(index_exports, {
1368
1564
  autoAdapter: () => autoAdapter,
1369
1565
  buildUrl: () => buildUrl,
1370
1566
  cloudflareAdapter: () => cloudflareAdapter,
1567
+ createBrowserContext: () => createBrowserContext,
1371
1568
  createPrefetchedIntentsFromDom: () => createPrefetchedIntentsFromDom,
1372
1569
  createSSRApp: () => createSSRApp,
1373
1570
  createSSRRender: () => createSSRRender,
1374
1571
  createServer: () => createServer,
1572
+ createServerContext: () => createServerContext,
1375
1573
  defineRoutes: () => defineRoutes,
1574
+ deny: () => deny,
1376
1575
  deserializeServerData: () => deserializeServerData,
1377
1576
  detectRuntime: () => detectRuntime,
1378
1577
  finesoftFrontViteConfig: () => finesoftFrontViteConfig,
@@ -1391,10 +1590,12 @@ __export(index_exports, {
1391
1590
  makeFlowAction: () => makeFlowAction,
1392
1591
  mapEach: () => mapEach,
1393
1592
  netlifyAdapter: () => netlifyAdapter,
1593
+ next: () => next,
1394
1594
  nodeAdapter: () => nodeAdapter,
1395
1595
  parseAcceptLanguage: () => parseAcceptLanguage,
1396
1596
  pipe: () => pipe,
1397
1597
  pipeAsync: () => pipeAsync,
1598
+ redirect: () => redirect,
1398
1599
  registerActionHandlers: () => registerActionHandlers,
1399
1600
  registerExternalUrlHandler: () => registerExternalUrlHandler,
1400
1601
  registerFlowActionHandler: () => registerFlowActionHandler,
@@ -1405,6 +1606,9 @@ __export(index_exports, {
1405
1606
  resetFilterCache: () => resetFilterCache,
1406
1607
  resolveAdapter: () => resolveAdapter,
1407
1608
  resolveRoot: () => resolveRoot,
1609
+ rewrite: () => rewrite,
1610
+ runAfterLoadGuards: () => runAfterLoadGuards,
1611
+ runBeforeLoadGuards: () => runBeforeLoadGuards,
1408
1612
  serializeServerData: () => serializeServerData,
1409
1613
  shouldLog: () => shouldLog,
1410
1614
  ssrRender: () => ssrRender,
@@ -1587,31 +1791,49 @@ function registerFlowActionHandler(deps) {
1587
1791
  const { framework, log, callbacks, updateApp } = deps;
1588
1792
  let isFirstPage = true;
1589
1793
  let navigationId = 0;
1794
+ const MAX_REDIRECTS = 5;
1590
1795
  const defaultGetScrollable = () => document.getElementById("scrollable-page-override") || document.getElementById("scrollable-page") || document.documentElement;
1591
1796
  const history = new History(log, {
1592
1797
  getScrollablePageElement: deps.getScrollablePageElement ?? defaultGetScrollable
1593
1798
  });
1594
- framework.onAction(ACTION_KINDS.FLOW, async (action) => {
1595
- const flowAction = action;
1596
- const url = flowAction.url;
1597
- log.debug(`FlowAction \u2192 ${url}`);
1598
- if (flowAction.presentationContext === "modal") {
1599
- const match2 = framework.routeUrl(url);
1600
- if (match2) {
1601
- const page = await framework.dispatch(
1602
- match2.intent
1603
- );
1604
- callbacks.onModal(page);
1605
- }
1799
+ async function navigateTo(url, redirectCount, thisNav) {
1800
+ if (redirectCount >= MAX_REDIRECTS) {
1801
+ log.error(
1802
+ `Navigation redirect loop detected (${MAX_REDIRECTS} redirects), stopping at: ${url}`
1803
+ );
1606
1804
  return;
1607
1805
  }
1608
- const thisNav = ++navigationId;
1609
1806
  const shouldReplace = isFirstPage || url === window.location.pathname + window.location.search;
1610
1807
  const match = framework.routeUrl(url);
1611
1808
  if (!match) {
1612
1809
  log.warn(`FlowAction: no route for ${url}`);
1613
1810
  return;
1614
1811
  }
1812
+ const navCtx = createBrowserContext({
1813
+ url,
1814
+ intent: match.intent,
1815
+ container: framework.container
1816
+ });
1817
+ const beforeResult = await framework.runBeforeLoad(
1818
+ navCtx,
1819
+ match.beforeGuards
1820
+ );
1821
+ if (beforeResult.kind === "redirect") {
1822
+ log.debug(`beforeLoad \u2192 redirect to ${beforeResult.url}`);
1823
+ await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
1824
+ return;
1825
+ }
1826
+ if (beforeResult.kind === "deny") {
1827
+ log.warn(
1828
+ `beforeLoad \u2192 denied (${beforeResult.status}): ${beforeResult.message}`
1829
+ );
1830
+ return;
1831
+ }
1832
+ if (beforeResult.kind === "rewrite") {
1833
+ log.debug(`beforeLoad \u2192 rewrite to ${beforeResult.url}`);
1834
+ await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
1835
+ return;
1836
+ }
1615
1837
  const pagePromise = framework.dispatch(
1616
1838
  match.intent
1617
1839
  );
@@ -1627,12 +1849,33 @@ function registerFlowActionHandler(deps) {
1627
1849
  history.beforeTransition();
1628
1850
  updateApp({
1629
1851
  page: pagePromise.then(
1630
- (page) => {
1852
+ async (page) => {
1631
1853
  if (thisNav !== navigationId) {
1632
1854
  log.info("FlowAction commit superseded", url);
1633
1855
  return page;
1634
1856
  }
1635
- const canonicalURL = url;
1857
+ const postCtx = {
1858
+ ...navCtx,
1859
+ page
1860
+ };
1861
+ const afterResult = await framework.runAfterLoad(
1862
+ postCtx,
1863
+ match.afterGuards
1864
+ );
1865
+ if (afterResult.kind === "redirect") {
1866
+ log.debug(`afterLoad \u2192 redirect to ${afterResult.url}`);
1867
+ navigateTo(afterResult.url, redirectCount + 1, thisNav);
1868
+ return page;
1869
+ }
1870
+ let canonicalURL = url;
1871
+ if (afterResult.kind === "rewrite") {
1872
+ canonicalURL = afterResult.url;
1873
+ log.debug(`afterLoad \u2192 rewrite URL to ${canonicalURL}`);
1874
+ }
1875
+ if (afterResult.kind === "deny") {
1876
+ log.warn(`afterLoad \u2192 denied (${afterResult.status})`);
1877
+ return page;
1878
+ }
1636
1879
  if (shouldReplace) {
1637
1880
  history.replaceState({ page }, canonicalURL);
1638
1881
  } else {
@@ -1662,6 +1905,23 @@ function registerFlowActionHandler(deps) {
1662
1905
  isFirstPage
1663
1906
  });
1664
1907
  isFirstPage = false;
1908
+ }
1909
+ framework.onAction(ACTION_KINDS.FLOW, async (action) => {
1910
+ const flowAction = action;
1911
+ const url = flowAction.url;
1912
+ log.debug(`FlowAction \u2192 ${url}`);
1913
+ if (flowAction.presentationContext === "modal") {
1914
+ const match = framework.routeUrl(url);
1915
+ if (match) {
1916
+ const page = await framework.dispatch(
1917
+ match.intent
1918
+ );
1919
+ callbacks.onModal(page);
1920
+ }
1921
+ return;
1922
+ }
1923
+ const thisNav = ++navigationId;
1924
+ await navigateTo(url, 0, thisNav);
1665
1925
  });
1666
1926
  history.onPopState(async (url, cachedState) => {
1667
1927
  log.debug(`popstate \u2192 ${url}, cached=${!!cachedState}`);
@@ -1686,6 +1946,30 @@ function registerFlowActionHandler(deps) {
1686
1946
  });
1687
1947
  return;
1688
1948
  }
1949
+ const navCtx = createBrowserContext({
1950
+ url: parsed.pathname + parsed.search,
1951
+ intent: routeMatch.intent,
1952
+ container: framework.container
1953
+ });
1954
+ const beforeResult = await framework.runBeforeLoad(
1955
+ navCtx,
1956
+ routeMatch.beforeGuards
1957
+ );
1958
+ if (beforeResult.kind === "redirect") {
1959
+ log.debug(`popstate beforeLoad \u2192 redirect to ${beforeResult.url}`);
1960
+ const thisNav = ++navigationId;
1961
+ await navigateTo(beforeResult.url, 0, thisNav);
1962
+ return;
1963
+ }
1964
+ if (beforeResult.kind === "deny" || beforeResult.kind === "rewrite") {
1965
+ if (beforeResult.kind === "deny") {
1966
+ log.warn(`popstate beforeLoad \u2192 denied`);
1967
+ } else {
1968
+ const thisNav = ++navigationId;
1969
+ await navigateTo(beforeResult.url, 0, thisNav);
1970
+ }
1971
+ return;
1972
+ }
1689
1973
  const pagePromise = framework.dispatch(
1690
1974
  routeMatch.intent
1691
1975
  );
@@ -1875,7 +2159,7 @@ function _sanitizeProxyPath(raw) {
1875
2159
  const pattern = `"${config.prefix}/*"`;
1876
2160
  const headersJson = JSON.stringify(config.headers ?? {});
1877
2161
  const cacheStr = config.cache ? JSON.stringify(config.cache) : "null";
1878
- const redirect = config.followRedirects ? '"follow"' : '"manual"';
2162
+ const redirect2 = config.followRedirects ? '"follow"' : '"manual"';
1879
2163
  let authCode = "";
1880
2164
  if (config.auth) {
1881
2165
  const envKey = JSON.stringify(config.auth.envKey);
@@ -1894,7 +2178,7 @@ function _sanitizeProxyPath(raw) {
1894
2178
  _reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
1895
2179
  const _headers = ${headersJson};${authCode}
1896
2180
  try {
1897
- const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect} });
2181
+ const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect2} });
1898
2182
  const _body = await _resp.text();
1899
2183
  const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
1900
2184
  if (${cacheStr}) _rh["Cache-Control"] = ${cacheStr};
@@ -3319,11 +3603,14 @@ function finesoftFrontViteConfig(options = {}) {
3319
3603
  autoAdapter,
3320
3604
  buildUrl,
3321
3605
  cloudflareAdapter,
3606
+ createBrowserContext,
3322
3607
  createPrefetchedIntentsFromDom,
3323
3608
  createSSRApp,
3324
3609
  createSSRRender,
3325
3610
  createServer,
3611
+ createServerContext,
3326
3612
  defineRoutes,
3613
+ deny,
3327
3614
  deserializeServerData,
3328
3615
  detectRuntime,
3329
3616
  finesoftFrontViteConfig,
@@ -3342,10 +3629,12 @@ function finesoftFrontViteConfig(options = {}) {
3342
3629
  makeFlowAction,
3343
3630
  mapEach,
3344
3631
  netlifyAdapter,
3632
+ next,
3345
3633
  nodeAdapter,
3346
3634
  parseAcceptLanguage,
3347
3635
  pipe,
3348
3636
  pipeAsync,
3637
+ redirect,
3349
3638
  registerActionHandlers,
3350
3639
  registerExternalUrlHandler,
3351
3640
  registerFlowActionHandler,
@@ -3356,6 +3645,9 @@ function finesoftFrontViteConfig(options = {}) {
3356
3645
  resetFilterCache,
3357
3646
  resolveAdapter,
3358
3647
  resolveRoot,
3648
+ rewrite,
3649
+ runAfterLoadGuards,
3650
+ runBeforeLoadGuards,
3359
3651
  serializeServerData,
3360
3652
  shouldLog,
3361
3653
  ssrRender,