@finesoft/front 0.1.30 → 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.
- package/dist/{app-O6QY6A43.js → app-IA6JQC7V.js} +3 -3
- package/dist/{chunk-I32JEYQR.js → chunk-M7VITIMR.js} +28 -25
- package/dist/chunk-M7VITIMR.js.map +1 -0
- package/dist/{chunk-N4AEOYUR.js → chunk-ZMOE42LB.js} +6 -2
- package/dist/chunk-ZMOE42LB.js.map +1 -0
- package/dist/index.cjs +67 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +43 -21
- package/dist/index.js.map +1 -1
- package/dist/{src-LYKZSLF7.js → src-B5YAJXIM.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-I32JEYQR.js.map +0 -1
- package/dist/chunk-N4AEOYUR.js.map +0 -1
- /package/dist/{app-O6QY6A43.js.map → app-IA6JQC7V.js.map} +0 -0
- /package/dist/{src-LYKZSLF7.js.map → src-B5YAJXIM.js.map} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSSRApp
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
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-
|
|
10
|
+
//# sourceMappingURL=app-IA6JQC7V.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
injectCSRShell,
|
|
3
3
|
injectSSRContent
|
|
4
|
-
} from "./chunk-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
status: 508
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
depth++;
|
|
26
|
-
return Promise.resolve(
|
|
27
|
-
appFetch(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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
status: 508
|
|
1124
|
-
})
|
|
1125
|
-
);
|
|
1126
|
-
}
|
|
1127
|
-
depth++;
|
|
1128
|
-
return Promise.resolve(
|
|
1129
|
-
appFetch(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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// \
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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
|
-
|
|
1817
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
2921
|
+
isrCache.set(cacheKey, finalHtml);
|
|
2897
2922
|
}
|
|
2898
2923
|
return c.html(finalHtml);
|
|
2899
2924
|
} catch (e) {
|