@finesoft/front 0.1.29 → 0.1.31

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.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createSSRApp
3
- } from "./chunk-WD72JAMS.js";
4
- import "./chunk-N4AEOYUR.js";
3
+ } from "./chunk-M7VITIMR.js";
4
+ import "./chunk-ZMOE42LB.js";
5
5
  import "./chunk-BUYWNNNQ.js";
6
6
  import "./chunk-FYP2ZYYV.js";
7
7
  export {
8
8
  createSSRApp
9
9
  };
10
- //# sourceMappingURL=app-QERC5VUP.js.map
10
+ //# sourceMappingURL=app-IA6JQC7V.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  injectCSRShell,
3
3
  injectSSRContent
4
- } from "./chunk-N4AEOYUR.js";
4
+ } from "./chunk-ZMOE42LB.js";
5
5
  import {
6
6
  parseAcceptLanguage
7
7
  } from "./chunk-FYP2ZYYV.js";
@@ -10,24 +10,14 @@ import {
10
10
  import { Hono } from "hono";
11
11
 
12
12
  // ../server/src/internal-fetch.ts
13
- var MAX_INTERNAL_FETCH_DEPTH = 5;
14
- function createInternalFetch(appFetch) {
15
- let depth = 0;
13
+ var SSR_DEPTH_HEADER = "x-ssr-depth";
14
+ var MAX_SSR_DEPTH = 5;
15
+ function createInternalFetch(appFetch, depth = 1) {
16
16
  return ((input, init) => {
17
17
  if (typeof input === "string" && input.startsWith("/")) {
18
- if (depth >= MAX_INTERNAL_FETCH_DEPTH) {
19
- return Promise.resolve(
20
- new Response("Internal fetch loop detected", {
21
- status: 508
22
- })
23
- );
24
- }
25
- depth++;
26
- return appFetch(
27
- new Request(`http://localhost${input}`, init)
28
- ).finally(() => {
29
- depth--;
30
- });
18
+ const request = new Request(`http://localhost${input}`, init);
19
+ request.headers.set(SSR_DEPTH_HEADER, String(depth));
20
+ return Promise.resolve(appFetch(request));
31
21
  }
32
22
  return globalThis.fetch(input, init);
33
23
  });
@@ -46,7 +36,6 @@ function createSSRApp(options) {
46
36
  parentFetch
47
37
  } = options;
48
38
  const app = new Hono();
49
- const internalFetch = parentFetch ? createInternalFetch(parentFetch) : void 0;
50
39
  const ISR_CACHE_MAX = 1e3;
51
40
  const isrCache = /* @__PURE__ */ new Map();
52
41
  function isrSet(key, val) {
@@ -56,6 +45,7 @@ function createSSRApp(options) {
56
45
  }
57
46
  isrCache.set(key, val);
58
47
  }
48
+ let templateCache;
59
49
  async function readTemplate(url) {
60
50
  if (!isProduction && vite) {
61
51
  const { readFileSync: readFileSync2 } = await import(
@@ -69,11 +59,13 @@ function createSSRApp(options) {
69
59
  const raw = readFileSync2(resolve2(root, "index.html"), "utf-8");
70
60
  return vite.transformIndexHtml(url, raw);
71
61
  }
62
+ if (templateCache) return templateCache;
72
63
  const isDeno = typeof globalThis.Deno !== "undefined";
73
64
  if (isDeno) {
74
- return globalThis.Deno.readTextFileSync(
65
+ templateCache = globalThis.Deno.readTextFileSync(
75
66
  new URL("../dist/client/index.html", import.meta.url)
76
67
  );
68
+ return templateCache;
77
69
  }
78
70
  const { readFileSync } = await import(
79
71
  /* @vite-ignore */
@@ -83,7 +75,11 @@ function createSSRApp(options) {
83
75
  /* @vite-ignore */
84
76
  "path"
85
77
  );
86
- return readFileSync(resolve(root, "dist/client/index.html"), "utf-8");
78
+ templateCache = readFileSync(
79
+ resolve(root, "dist/client/index.html"),
80
+ "utf-8"
81
+ );
82
+ return templateCache;
87
83
  }
88
84
  async function loadSSRModule() {
89
85
  if (!isProduction && vite) {
@@ -110,11 +106,12 @@ function createSSRApp(options) {
110
106
  );
111
107
  }
112
108
  app.get("*", async (c) => {
109
+ const ssrDepth = parseInt(c.req.header(SSR_DEPTH_HEADER) ?? "0", 10);
110
+ if (ssrDepth >= MAX_SSR_DEPTH) {
111
+ return c.text("SSR recursion loop detected", 508);
112
+ }
113
113
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
114
114
  try {
115
- const cacheKey = url;
116
- const cached = isrCache.get(cacheKey);
117
- if (cached) return c.html(cached);
118
115
  const template = await readTemplate(url);
119
116
  const { render, serializeServerData } = await loadSSRModule();
120
117
  const locale = parseAcceptLanguage(
@@ -122,6 +119,10 @@ function createSSRApp(options) {
122
119
  supportedLocales,
123
120
  defaultLocale
124
121
  );
122
+ const cacheKey = `${locale}:${url}`;
123
+ const cached = isrCache.get(cacheKey);
124
+ if (cached) return c.html(cached);
125
+ const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
125
126
  const {
126
127
  html: appHtml,
127
128
  head,
@@ -131,7 +132,7 @@ function createSSRApp(options) {
131
132
  } = await render(
132
133
  url,
133
134
  locale,
134
- internalFetch ? { fetch: internalFetch } : void 0
135
+ requestFetch ? { fetch: requestFetch } : void 0
135
136
  );
136
137
  if (renderMode === "csr") {
137
138
  return c.html(injectCSRShell(template, locale));
@@ -161,7 +162,9 @@ function createSSRApp(options) {
161
162
  }
162
163
 
163
164
  export {
165
+ SSR_DEPTH_HEADER,
166
+ MAX_SSR_DEPTH,
164
167
  createInternalFetch,
165
168
  createSSRApp
166
169
  };
167
- //# sourceMappingURL=chunk-WD72JAMS.js.map
170
+ //# sourceMappingURL=chunk-M7VITIMR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../server/src/app.ts","../../server/src/internal-fetch.ts"],"sourcesContent":["/**\n * createSSRApp — 创建 Hono SSR 应用\n *\n * 提供 SSR 通配路由,读取模板、加载 SSR 模块、渲染。\n * 应用层可在此之上追加自定义路由(API 代理等)。\n */\n\nimport { injectCSRShell, injectSSRContent } from \"@finesoft/ssr\";\nimport { Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport {\n\tcreateInternalFetch,\n\tMAX_SSR_DEPTH,\n\tSSR_DEPTH_HEADER,\n} from \"./internal-fetch\";\nimport { parseAcceptLanguage } from \"./locale\";\n\nexport interface SSRModule {\n\trender: (\n\t\turl: string,\n\t\tlocale: string,\n\t\tssrContext?: { fetch?: typeof globalThis.fetch },\n\t) => Promise<{\n\t\thtml: string;\n\t\thead: string;\n\t\tcss: string;\n\t\tserverData: unknown;\n\t\trenderMode?: string;\n\t}>;\n\tserializeServerData: (data: unknown) => string;\n}\n\nexport interface SSRAppOptions {\n\t/** 项目根路径 */\n\troot: string;\n\t/** Vite dev server(仅开发模式) */\n\tvite?: ViteDevServer;\n\t/** 是否生产环境 */\n\tisProduction: boolean;\n\t/** SSR 入口文件路径(开发用,如 \"/src/ssr.ts\") */\n\tssrEntryPath?: string;\n\t/** 生产环境 SSR 模块路径(如 \"../dist/server/ssr.js\") */\n\tssrProductionModule?: string;\n\t/** 支持的语言列表 */\n\tsupportedLocales?: string[];\n\t/** 默认语言 */\n\tdefaultLocale?: string;\n\t/**\n\t * 父级 Hono app 的 fetch 函数,用于 SSR 内部路由回环。\n\t * SSR 渲染时,控制器的 fetch 请求(如 /api/apple/*)会通过此函数\n\t * 直接在进程内路由到代理 handler,避免网络自请求死锁。\n\t */\n\tparentFetch?: (request: Request) => Response | Promise<Response>;\n}\n\nexport function createSSRApp(options: SSRAppOptions): Hono {\n\tconst {\n\t\troot,\n\t\tvite,\n\t\tisProduction,\n\t\tssrEntryPath = \"/src/ssr.ts\",\n\t\tssrProductionModule,\n\t\tsupportedLocales,\n\t\tdefaultLocale,\n\t\tparentFetch,\n\t} = options;\n\n\tconst app = new Hono();\n\n\t/** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */\n\tconst ISR_CACHE_MAX = 1000;\n\tconst isrCache = new Map<string, string>();\n\tfunction isrSet(key: string, val: string) {\n\t\tif (isrCache.size >= ISR_CACHE_MAX) {\n\t\t\tconst first = isrCache.keys().next().value;\n\t\t\tif (first !== undefined) isrCache.delete(first);\n\t\t}\n\t\tisrCache.set(key, val);\n\t}\n\n\t/** 生产环境模板缓存(模板不变,避免每请求重复读盘) */\n\tlet templateCache: string | undefined;\n\n\tasync function readTemplate(url: string): Promise<string> {\n\t\tif (!isProduction && vite) {\n\t\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\t\tconst raw = readFileSync(resolve(root, \"index.html\"), \"utf-8\");\n\t\t\treturn vite.transformIndexHtml(url, raw);\n\t\t}\n\n\t\tif (templateCache) return templateCache;\n\n\t\tconst isDeno = typeof (globalThis as any).Deno !== \"undefined\";\n\t\tif (isDeno) {\n\t\t\ttemplateCache = (globalThis as any).Deno.readTextFileSync(\n\t\t\t\tnew URL(\"../dist/client/index.html\", import.meta.url),\n\t\t\t);\n\t\t\treturn templateCache!;\n\t\t}\n\n\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\ttemplateCache = readFileSync(\n\t\t\tresolve(root, \"dist/client/index.html\"),\n\t\t\t\"utf-8\",\n\t\t);\n\t\treturn templateCache!;\n\t}\n\n\tasync function loadSSRModule(): Promise<SSRModule> {\n\t\tif (!isProduction && vite) {\n\t\t\treturn (await vite.ssrLoadModule(ssrEntryPath)) as SSRModule;\n\t\t}\n\t\tif (ssrProductionModule) {\n\t\t\treturn import(\n\t\t\t\t/* @vite-ignore */ ssrProductionModule\n\t\t\t) as Promise<SSRModule>;\n\t\t}\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\tconst { pathToFileURL } = await import(/* @vite-ignore */ \"node:url\");\n\t\tconst absPath = pathToFileURL(resolve(root, \"dist/server/ssr.js\")).href;\n\t\treturn import(/* @vite-ignore */ absPath) as Promise<SSRModule>;\n\t}\n\n\tapp.get(\"*\", async (c) => {\n\t\t// 递归深度保护:从请求头读取 SSR 深度\n\t\tconst ssrDepth = parseInt(c.req.header(SSR_DEPTH_HEADER) ?? \"0\", 10);\n\t\tif (ssrDepth >= MAX_SSR_DEPTH) {\n\t\t\treturn c.text(\"SSR recursion loop detected\", 508);\n\t\t}\n\n\t\tconst url =\n\t\t\tc.req.path +\n\t\t\t(c.req.url.includes(\"?\") ? \"?\" + c.req.url.split(\"?\")[1] : \"\");\n\n\t\ttry {\n\t\t\tconst template = await readTemplate(url);\n\t\t\tconst { render, serializeServerData } = await loadSSRModule();\n\n\t\t\tconst locale = parseAcceptLanguage(\n\t\t\t\tc.req.header(\"accept-language\"),\n\t\t\t\tsupportedLocales,\n\t\t\t\tdefaultLocale,\n\t\t\t);\n\n\t\t\t// ISR 缓存命中(key 含 locale,避免跨语言缓存污染)\n\t\t\tconst cacheKey = `${locale}:${url}`;\n\t\t\tconst cached = isrCache.get(cacheKey);\n\t\t\tif (cached) return c.html(cached);\n\n\t\t\t// 每请求创建 internalFetch,深度通过请求头传递\n\t\t\tconst requestFetch = parentFetch\n\t\t\t\t? createInternalFetch(parentFetch, ssrDepth + 1)\n\t\t\t\t: undefined;\n\n\t\t\tconst {\n\t\t\t\thtml: appHtml,\n\t\t\t\thead,\n\t\t\t\tcss,\n\t\t\t\tserverData,\n\t\t\t\trenderMode,\n\t\t\t} = await render(\n\t\t\t\turl,\n\t\t\t\tlocale,\n\t\t\t\trequestFetch ? { fetch: requestFetch } : undefined,\n\t\t\t);\n\n\t\t\t// CSR 模式:返回空壳 HTML\n\t\t\tif (renderMode === \"csr\") {\n\t\t\t\treturn c.html(injectCSRShell(template, locale));\n\t\t\t}\n\n\t\t\tconst serializedData = serializeServerData(serverData);\n\n\t\t\tconst finalHtml = injectSSRContent({\n\t\t\t\ttemplate,\n\t\t\t\tlocale,\n\t\t\t\thead,\n\t\t\t\tcss,\n\t\t\t\thtml: appHtml,\n\t\t\t\tserializedData,\n\t\t\t});\n\n\t\t\t// Prerender ISR 缓存\n\t\t\tif (renderMode === \"prerender\") {\n\t\t\t\tisrSet(cacheKey, finalHtml);\n\t\t\t}\n\n\t\t\treturn c.html(finalHtml);\n\t\t} catch (e) {\n\t\t\tif (!isProduction && vite) {\n\t\t\t\tvite.ssrFixStacktrace(e as Error);\n\t\t\t}\n\t\t\tconsole.error(\"[SSR Error]\", e);\n\t\t\treturn c.text(\"Internal Server Error\", 500);\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * createInternalFetch — SSR 内部路由回环 fetch 包装器\n *\n * 将相对路径(/api/…)请求转为 Hono app.fetch 内存调用,\n * 绝对 URL 和非字符串 input 走 globalThis.fetch(真实网络)。\n *\n * 递归深度保护采用请求头传递:每次 SSR 回环在请求头中写入深度值,\n * SSR catch-all 读取深度判断是否超限。对比闭包计数器方案:\n * - 并发安全:无共享可变状态\n * - 跨渲染准确:深度随请求在 Hono 路由链中传递\n */\n\nexport const SSR_DEPTH_HEADER = \"x-ssr-depth\";\nexport const MAX_SSR_DEPTH = 5;\n\n/**\n * 创建请求级 internal fetch\n *\n * @param appFetch - Hono app.fetch(父级路由)\n * @param depth - 当前 SSR 深度(由 catch-all handler 从请求头读取后 +1 传入)\n */\nexport function createInternalFetch(\n\tappFetch: (request: Request) => Response | Promise<Response>,\n\tdepth: number = 1,\n): typeof globalThis.fetch {\n\treturn ((input: RequestInfo | URL, init?: RequestInit) => {\n\t\t// 只拦截相对路径(以 / 开头),其他一律走真实网络\n\t\tif (typeof input === \"string\" && input.startsWith(\"/\")) {\n\t\t\tconst request = new Request(`http://localhost${input}`, init);\n\t\t\trequest.headers.set(SSR_DEPTH_HEADER, String(depth));\n\t\t\treturn Promise.resolve(appFetch(request));\n\t\t}\n\n\t\t// 绝对 URL / URL 对象 / Request 对象 → 走正常网络\n\t\treturn globalThis.fetch(input, init);\n\t}) as typeof globalThis.fetch;\n}\n"],"mappings":"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","names":["readFileSync","resolve"]}
@@ -34,7 +34,11 @@ async function ssrRender(options) {
34
34
  try {
35
35
  page = await framework.dispatch(match.intent);
36
36
  serverData = [{ intent: match.intent, data: page }];
37
- } catch {
37
+ } catch (e) {
38
+ console.error(
39
+ `[SSR] dispatch failed for intent "${match.intent.id}":`,
40
+ e
41
+ );
38
42
  page = getErrorPage(500, "Internal error");
39
43
  }
40
44
  } else {
@@ -109,4 +113,4 @@ export {
109
113
  injectCSRShell,
110
114
  serializeServerData
111
115
  };
112
- //# sourceMappingURL=chunk-N4AEOYUR.js.map
116
+ //# sourceMappingURL=chunk-ZMOE42LB.js.map
@@ -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\ttype BasePage,\n\ttype FrameworkConfig,\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}\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}\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\tconst parsed = new URL(url, \"http://localhost\");\n\tconst fullPath = parsed.pathname + parsed.search;\n\tconst match = framework.routeUrl(fullPath);\n\n\t// CSR 模式:跳过服务端渲染,返回空内容由客户端 JS 渲染\n\tif (match?.renderMode === \"csr\") {\n\t\tframework.dispose();\n\t\treturn {\n\t\t\thtml: \"\",\n\t\t\thead: \"\",\n\t\t\tcss: \"\",\n\t\t\tserverData: [],\n\t\t\trenderMode: \"csr\",\n\t\t};\n\t}\n\n\tlet page: BasePage;\n\tlet serverData: PrefetchedIntent[] = [];\n\n\tif (match) {\n\t\ttry {\n\t\t\tpage = (await framework.dispatch(match.intent)) as BasePage;\n\t\t\tserverData = [{ intent: match.intent, data: page }];\n\t\t} catch (e) {\n\t\t\tconsole.error(\n\t\t\t\t`[SSR] dispatch failed for intent \"${match.intent.id}\":`,\n\t\t\t\te,\n\t\t\t);\n\t\t\tpage = getErrorPage(500, \"Internal error\");\n\t\t}\n\t} else {\n\t\tpage = getErrorPage(404, \"Page not found\");\n\t}\n\n\tconst result = renderApp(page, framework);\n\n\tframework.dispose();\n\n\treturn {\n\t\thtml: result.html,\n\t\thead: result.head,\n\t\tcss: result.css,\n\t\tserverData,\n\t\trenderMode: match?.renderMode,\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":";;;;;AAoDA,eAAsB,UACrB,SAC2B;AAC3B,QAAM;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD,IAAI;AAGJ,QAAM,eAAgC,YAAY,QAC/C,EAAE,GAAG,iBAAiB,OAAO,WAAW,MAAM,IAC9C;AAEH,QAAM,YAAY,UAAU,OAAO,YAAY;AAC/C,YAAU,SAAS;AAEnB,QAAM,SAAS,IAAI,IAAI,KAAK,kBAAkB;AAC9C,QAAM,WAAW,OAAO,WAAW,OAAO;AAC1C,QAAM,QAAQ,UAAU,SAAS,QAAQ;AAGzC,MAAI,OAAO,eAAe,OAAO;AAChC,cAAU,QAAQ;AAClB,WAAO;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,KAAK;AAAA,MACL,YAAY,CAAC;AAAA,MACb,YAAY;AAAA,IACb;AAAA,EACD;AAEA,MAAI;AACJ,MAAI,aAAiC,CAAC;AAEtC,MAAI,OAAO;AACV,QAAI;AACH,aAAQ,MAAM,UAAU,SAAS,MAAM,MAAM;AAC7C,mBAAa,CAAC,EAAE,QAAQ,MAAM,QAAQ,MAAM,KAAK,CAAC;AAAA,IACnD,SAAS,GAAG;AACX,cAAQ;AAAA,QACP,qCAAqC,MAAM,OAAO,EAAE;AAAA,QACpD;AAAA,MACD;AACA,aAAO,aAAa,KAAK,gBAAgB;AAAA,IAC1C;AAAA,EACD,OAAO;AACN,WAAO,aAAa,KAAK,gBAAgB;AAAA,EAC1C;AAEA,QAAM,SAAS,UAAU,MAAM,SAAS;AAExC,YAAU,QAAQ;AAElB,SAAO;AAAA,IACN,MAAM,OAAO;AAAA,IACb,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ;AAAA,IACA,YAAY,OAAO;AAAA,EACpB;AACD;;;AC5EO,SAAS,gBACf,QAK6B;AAC7B,QAAM,EAAE,WAAW,cAAc,WAAW,gBAAgB,IAAI;AAEhE,SAAO,CAAC,KAAa,QAAgB,eACpC,UAAU;AAAA,IACT;AAAA,IACA,iBAAiB,mBAAmB,CAAC;AAAA,IACrC;AAAA,IACA;AAAA,IACA,WAAW,CAAC,SAAS,UAAU,MAAM,MAAM;AAAA,IAC3C;AAAA,EACD,CAAC;AACH;;;ACtDO,IAAM,mBAAmB;AAAA,EAC/B,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACP;AAWO,SAAS,iBAAiB,SAAmC;AACnE,QAAM,EAAE,UAAU,QAAQ,MAAM,KAAK,MAAM,eAAe,IAAI;AAC9D,QAAM,SAAS,MAAM,UAAU,GAAG,aAAa;AAE/C,SAAO,SACL,QAAQ,iBAAiB,MAAM,MAAM,EACrC,QAAQ,iBAAiB,MAAM,GAAG,IAAI;AAAA,EAAK,MAAM,EAAE,EACnD,QAAQ,iBAAiB,MAAM,IAAI,EACnC;AAAA,IACA,iBAAiB;AAAA,IACjB,+DAA+D,cAAc;AAAA,EAC9E;AACF;AAMO,SAAS,eAAe,UAAkB,QAAwB;AACxE,SAAO,SACL,QAAQ,iBAAiB,MAAM,MAAM,EACrC,QAAQ,iBAAiB,MAAM,EAAE,EACjC,QAAQ,iBAAiB,MAAM,EAAE,EACjC,QAAQ,iBAAiB,MAAM,EAAE;AACpC;;;ACrCA,IAAM,oBAA4C;AAAA,EACjD,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACX;AAEA,IAAM,sBAAsB;AAErB,SAAS,oBAAoB,MAAkC;AACrE,QAAM,OAAO,KAAK,UAAU,IAAI;AAChC,SAAO,KAAK;AAAA,IACX;AAAA,IACA,CAAC,UAAU,kBAAkB,KAAK,KAAK;AAAA,EACxC;AACD;","names":[]}
package/dist/index.cjs CHANGED
@@ -999,7 +999,11 @@ async function ssrRender(options) {
999
999
  try {
1000
1000
  page = await framework.dispatch(match.intent);
1001
1001
  serverData = [{ intent: match.intent, data: page }];
1002
- } catch {
1002
+ } catch (e) {
1003
+ console.error(
1004
+ `[SSR] dispatch failed for intent "${match.intent.id}":`,
1005
+ e
1006
+ );
1003
1007
  page = getErrorPage(500, "Internal error");
1004
1008
  }
1005
1009
  } else {
@@ -1113,32 +1117,22 @@ var init_src2 = __esm({
1113
1117
  });
1114
1118
 
1115
1119
  // ../server/src/internal-fetch.ts
1116
- function createInternalFetch(appFetch) {
1117
- let depth = 0;
1120
+ function createInternalFetch(appFetch, depth = 1) {
1118
1121
  return ((input, init) => {
1119
1122
  if (typeof input === "string" && input.startsWith("/")) {
1120
- if (depth >= MAX_INTERNAL_FETCH_DEPTH) {
1121
- return Promise.resolve(
1122
- new Response("Internal fetch loop detected", {
1123
- status: 508
1124
- })
1125
- );
1126
- }
1127
- depth++;
1128
- return appFetch(
1129
- new Request(`http://localhost${input}`, init)
1130
- ).finally(() => {
1131
- depth--;
1132
- });
1123
+ const request = new Request(`http://localhost${input}`, init);
1124
+ request.headers.set(SSR_DEPTH_HEADER, String(depth));
1125
+ return Promise.resolve(appFetch(request));
1133
1126
  }
1134
1127
  return globalThis.fetch(input, init);
1135
1128
  });
1136
1129
  }
1137
- var MAX_INTERNAL_FETCH_DEPTH;
1130
+ var SSR_DEPTH_HEADER, MAX_SSR_DEPTH;
1138
1131
  var init_internal_fetch = __esm({
1139
1132
  "../server/src/internal-fetch.ts"() {
1140
1133
  "use strict";
1141
- MAX_INTERNAL_FETCH_DEPTH = 5;
1134
+ SSR_DEPTH_HEADER = "x-ssr-depth";
1135
+ MAX_SSR_DEPTH = 5;
1142
1136
  }
1143
1137
  });
1144
1138
 
@@ -1189,7 +1183,6 @@ function createSSRApp(options) {
1189
1183
  parentFetch
1190
1184
  } = options;
1191
1185
  const app = new import_hono.Hono();
1192
- const internalFetch = parentFetch ? createInternalFetch(parentFetch) : void 0;
1193
1186
  const ISR_CACHE_MAX = 1e3;
1194
1187
  const isrCache = /* @__PURE__ */ new Map();
1195
1188
  function isrSet(key, val) {
@@ -1199,6 +1192,7 @@ function createSSRApp(options) {
1199
1192
  }
1200
1193
  isrCache.set(key, val);
1201
1194
  }
1195
+ let templateCache;
1202
1196
  async function readTemplate(url) {
1203
1197
  if (!isProduction && vite) {
1204
1198
  const { readFileSync: readFileSync2 } = await import(
@@ -1212,11 +1206,13 @@ function createSSRApp(options) {
1212
1206
  const raw = readFileSync2(resolve2(root, "index.html"), "utf-8");
1213
1207
  return vite.transformIndexHtml(url, raw);
1214
1208
  }
1209
+ if (templateCache) return templateCache;
1215
1210
  const isDeno = typeof globalThis.Deno !== "undefined";
1216
1211
  if (isDeno) {
1217
- return globalThis.Deno.readTextFileSync(
1212
+ templateCache = globalThis.Deno.readTextFileSync(
1218
1213
  new URL("../dist/client/index.html", import_meta.url)
1219
1214
  );
1215
+ return templateCache;
1220
1216
  }
1221
1217
  const { readFileSync } = await import(
1222
1218
  /* @vite-ignore */
@@ -1226,7 +1222,11 @@ function createSSRApp(options) {
1226
1222
  /* @vite-ignore */
1227
1223
  "path"
1228
1224
  );
1229
- return readFileSync(resolve(root, "dist/client/index.html"), "utf-8");
1225
+ templateCache = readFileSync(
1226
+ resolve(root, "dist/client/index.html"),
1227
+ "utf-8"
1228
+ );
1229
+ return templateCache;
1230
1230
  }
1231
1231
  async function loadSSRModule() {
1232
1232
  if (!isProduction && vite) {
@@ -1253,11 +1253,12 @@ function createSSRApp(options) {
1253
1253
  );
1254
1254
  }
1255
1255
  app.get("*", async (c) => {
1256
+ const ssrDepth = parseInt(c.req.header(SSR_DEPTH_HEADER) ?? "0", 10);
1257
+ if (ssrDepth >= MAX_SSR_DEPTH) {
1258
+ return c.text("SSR recursion loop detected", 508);
1259
+ }
1256
1260
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1257
1261
  try {
1258
- const cacheKey = url;
1259
- const cached = isrCache.get(cacheKey);
1260
- if (cached) return c.html(cached);
1261
1262
  const template = await readTemplate(url);
1262
1263
  const { render, serializeServerData: serializeServerData2 } = await loadSSRModule();
1263
1264
  const locale = parseAcceptLanguage(
@@ -1265,6 +1266,10 @@ function createSSRApp(options) {
1265
1266
  supportedLocales,
1266
1267
  defaultLocale
1267
1268
  );
1269
+ const cacheKey = `${locale}:${url}`;
1270
+ const cached = isrCache.get(cacheKey);
1271
+ if (cached) return c.html(cached);
1272
+ const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
1268
1273
  const {
1269
1274
  html: appHtml,
1270
1275
  head,
@@ -1274,7 +1279,7 @@ function createSSRApp(options) {
1274
1279
  } = await render(
1275
1280
  url,
1276
1281
  locale,
1277
- internalFetch ? { fetch: internalFetch } : void 0
1282
+ requestFetch ? { fetch: requestFetch } : void 0
1278
1283
  );
1279
1284
  if (renderMode === "csr") {
1280
1285
  return c.html(injectCSRShell(template, locale));
@@ -1806,20 +1811,28 @@ ${setupCall}
1806
1811
  ${opts.platformMiddleware ?? ""}
1807
1812
 
1808
1813
  // \u5185\u90E8 fetch \u56DE\u73AF\uFF1ASSR \u63A7\u5236\u5668\u7684 fetch \u8BF7\u6C42\u76F4\u63A5\u8D70 Hono \u5185\u5B58\u8DEF\u7531
1809
- // \u53EA\u62E6\u622A\u76F8\u5BF9\u8DEF\u5F84\uFF08/api/* \u7B49\uFF09\uFF0C\u7EDD\u5BF9 URL \u8D70\u6B63\u5E38\u7F51\u7EDC\uFF1B\u542B\u9012\u5F52\u4FDD\u62A4
1810
- let _fetchDepth = 0;
1811
- function _internalFetch(input, init) {
1812
- if (typeof input === "string" && input.startsWith("/")) {
1813
- if (_fetchDepth >= 5) {
1814
- return Promise.resolve(new Response("Internal fetch loop detected", { status: 508 }));
1814
+ // \u6DF1\u5EA6\u901A\u8FC7\u8BF7\u6C42\u5934\u4F20\u9012\uFF0C\u5E76\u53D1\u5B89\u5168\u4E14\u80FD\u8DE8\u6E32\u67D3\u6B63\u786E\u8FFD\u8E2A\u9012\u5F52
1815
+ const _SSR_DEPTH_HEADER = "x-ssr-depth";
1816
+ const _MAX_SSR_DEPTH = 5;
1817
+
1818
+ function _createInternalFetch(depth) {
1819
+ return function(input, init) {
1820
+ if (typeof input === "string" && input.startsWith("/")) {
1821
+ const req = new Request("http://localhost" + input, init);
1822
+ req.headers.set(_SSR_DEPTH_HEADER, String(depth));
1823
+ return app.fetch(req);
1815
1824
  }
1816
- _fetchDepth++;
1817
- return app.fetch(new Request("http://localhost" + input, init)).finally(() => { _fetchDepth--; });
1818
- }
1819
- return globalThis.fetch(input, init);
1825
+ return globalThis.fetch(input, init);
1826
+ };
1820
1827
  }
1821
1828
 
1822
1829
  app.get("*", async (c) => {
1830
+ // \u9012\u5F52\u6DF1\u5EA6\u4FDD\u62A4\uFF1A\u4ECE\u8BF7\u6C42\u5934\u8BFB\u53D6 SSR \u6DF1\u5EA6
1831
+ const _ssrDepth = parseInt(c.req.header(_SSR_DEPTH_HEADER) || "0", 10);
1832
+ if (_ssrDepth >= _MAX_SSR_DEPTH) {
1833
+ return c.text("SSR recursion loop detected", 508);
1834
+ }
1835
+
1823
1836
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1824
1837
  try {
1825
1838
  const locale = parseAcceptLanguage(c.req.header("accept-language"));
@@ -1830,11 +1843,12 @@ app.get("*", async (c) => {
1830
1843
  return c.html(injectCSRShell(TEMPLATE, locale));
1831
1844
  }
1832
1845
 
1833
- // ISR \u7F13\u5B58\u547D\u4E2D
1834
- const cached = await platformCacheGet(url);
1846
+ // ISR \u7F13\u5B58\u547D\u4E2D\uFF08key \u542B locale\uFF0C\u907F\u514D\u8DE8\u8BED\u8A00\u7F13\u5B58\u6C61\u67D3\uFF09
1847
+ const _cacheKey = locale + ":" + url;
1848
+ const cached = await platformCacheGet(_cacheKey);
1835
1849
  if (cached) return c.html(cached);
1836
1850
 
1837
- const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale, { fetch: _internalFetch });
1851
+ const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale, { fetch: _createInternalFetch(_ssrDepth + 1) });
1838
1852
 
1839
1853
  // \u8DEF\u7531\u7EA7 CSR
1840
1854
  if (renderMode === "csr") {
@@ -1846,7 +1860,7 @@ app.get("*", async (c) => {
1846
1860
 
1847
1861
  // Prerender ISR \u7F13\u5B58\uFF08\u5305\u62EC Vite \u914D\u7F6E\u8986\u76D6\u548C\u8DEF\u7531\u7EA7\uFF09
1848
1862
  if (renderMode === "prerender" || overrideMode === "prerender") {
1849
- await platformCacheSet(url, finalHtml);
1863
+ await platformCacheSet(_cacheKey, finalHtml);
1850
1864
  ${opts.platformPrerenderResponseHook ?? ""}
1851
1865
  }
1852
1866
 
@@ -2855,6 +2869,13 @@ function finesoftFrontViteConfig(options = {}) {
2855
2869
  ssrPath
2856
2870
  );
2857
2871
  app.get("*", async (c) => {
2872
+ const ssrDepth = parseInt(
2873
+ c.req.header(SSR_DEPTH_HEADER) ?? "0",
2874
+ 10
2875
+ );
2876
+ if (ssrDepth >= MAX_SSR_DEPTH) {
2877
+ return c.text("SSR recursion loop detected", 508);
2878
+ }
2858
2879
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
2859
2880
  try {
2860
2881
  const locale = parseAcceptLanguage2(
@@ -2869,7 +2890,8 @@ function finesoftFrontViteConfig(options = {}) {
2869
2890
  if (overrideMode === "csr") {
2870
2891
  return c.html(injectCSRShell2(template, locale));
2871
2892
  }
2872
- const cached = isrCache.get(url);
2893
+ const cacheKey = `${locale}:${url}`;
2894
+ const cached = isrCache.get(cacheKey);
2873
2895
  if (cached) return c.html(cached);
2874
2896
  const {
2875
2897
  html: appHtml,
@@ -2878,7 +2900,10 @@ function finesoftFrontViteConfig(options = {}) {
2878
2900
  serverData,
2879
2901
  renderMode
2880
2902
  } = await ssrModule.render(url, locale, {
2881
- fetch: createInternalFetch(app.fetch.bind(app))
2903
+ fetch: createInternalFetch(
2904
+ app.fetch.bind(app),
2905
+ ssrDepth + 1
2906
+ )
2882
2907
  });
2883
2908
  if (renderMode === "csr") {
2884
2909
  return c.html(injectCSRShell2(template, locale));
@@ -2893,7 +2918,7 @@ function finesoftFrontViteConfig(options = {}) {
2893
2918
  serializedData
2894
2919
  });
2895
2920
  if (renderMode === "prerender" || overrideMode === "prerender") {
2896
- isrCache.set(url, finalHtml);
2921
+ isrCache.set(cacheKey, finalHtml);
2897
2922
  }
2898
2923
  return c.html(finalHtml);
2899
2924
  } catch (e) {