@finesoft/front 0.1.32 → 0.1.34
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-OOLQDVXA.js → app-D4K35MX3.js} +2 -2
- package/dist/{chunk-UHQBKSHL.js → chunk-SFGR32K6.js} +21 -3
- package/dist/chunk-SFGR32K6.js.map +1 -0
- package/dist/index.cjs +229 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -6
- package/dist/index.d.ts +80 -6
- package/dist/index.js +209 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-UHQBKSHL.js.map +0 -1
- /package/dist/{app-OOLQDVXA.js.map → app-D4K35MX3.js.map} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSSRApp
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SFGR32K6.js";
|
|
4
4
|
import "./chunk-AYO3UUQC.js";
|
|
5
5
|
import "./chunk-OXKFPW4U.js";
|
|
6
6
|
import "./chunk-PSPVIVC2.js";
|
|
7
7
|
export {
|
|
8
8
|
createSSRApp
|
|
9
9
|
};
|
|
10
|
-
//# sourceMappingURL=app-
|
|
10
|
+
//# sourceMappingURL=app-D4K35MX3.js.map
|
|
@@ -24,6 +24,19 @@ function createInternalFetch(appFetch, depth = 1) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// ../server/src/app.ts
|
|
27
|
+
function matchRenderModeOverride(url, renderModes) {
|
|
28
|
+
if (!renderModes) return null;
|
|
29
|
+
const path = url.split("?")[0];
|
|
30
|
+
if (renderModes[path]) return renderModes[path];
|
|
31
|
+
for (const [pattern, mode] of Object.entries(renderModes)) {
|
|
32
|
+
if (pattern.includes("*")) {
|
|
33
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
34
|
+
const re = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
|
|
35
|
+
if (re.test(path)) return mode;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
27
40
|
function createSSRApp(options) {
|
|
28
41
|
const {
|
|
29
42
|
root,
|
|
@@ -33,7 +46,8 @@ function createSSRApp(options) {
|
|
|
33
46
|
ssrProductionModule,
|
|
34
47
|
supportedLocales,
|
|
35
48
|
defaultLocale,
|
|
36
|
-
parentFetch
|
|
49
|
+
parentFetch,
|
|
50
|
+
renderModes
|
|
37
51
|
} = options;
|
|
38
52
|
const app = new Hono();
|
|
39
53
|
const ISR_CACHE_MAX = 1e3;
|
|
@@ -119,6 +133,10 @@ function createSSRApp(options) {
|
|
|
119
133
|
supportedLocales,
|
|
120
134
|
defaultLocale
|
|
121
135
|
);
|
|
136
|
+
const overrideMode = matchRenderModeOverride(url, renderModes);
|
|
137
|
+
if (overrideMode === "csr") {
|
|
138
|
+
return c.html(injectCSRShell(template, locale));
|
|
139
|
+
}
|
|
122
140
|
const cacheKey = `${locale}:${url}`;
|
|
123
141
|
const cached = isrCache.get(cacheKey);
|
|
124
142
|
if (cached) return c.html(cached);
|
|
@@ -146,7 +164,7 @@ function createSSRApp(options) {
|
|
|
146
164
|
html: appHtml,
|
|
147
165
|
serializedData
|
|
148
166
|
});
|
|
149
|
-
if (renderMode === "prerender") {
|
|
167
|
+
if (renderMode === "prerender" || overrideMode === "prerender") {
|
|
150
168
|
isrSet(cacheKey, finalHtml);
|
|
151
169
|
}
|
|
152
170
|
return c.html(finalHtml);
|
|
@@ -167,4 +185,4 @@ export {
|
|
|
167
185
|
createInternalFetch,
|
|
168
186
|
createSSRApp
|
|
169
187
|
};
|
|
170
|
-
//# sourceMappingURL=chunk-
|
|
188
|
+
//# sourceMappingURL=chunk-SFGR32K6.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\n/**\n * 匹配 Vite 配置级别的 renderMode 覆盖。\n * 精确路径优先,然后 glob 模式。\n */\nfunction matchRenderModeOverride(\n\turl: string,\n\trenderModes?: Record<string, string>,\n): string | null {\n\tif (!renderModes) return null;\n\tconst path = url.split(\"?\")[0];\n\tif (renderModes[path]) return renderModes[path];\n\tfor (const [pattern, mode] of Object.entries(renderModes)) {\n\t\tif (pattern.includes(\"*\")) {\n\t\t\tconst escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst re = new RegExp(\"^\" + escaped.replace(/\\*/g, \".*\") + \"$\");\n\t\t\tif (re.test(path)) return mode;\n\t\t}\n\t}\n\treturn null;\n}\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\t/**\n\t * 按路由覆盖渲染模式(精确路径或 glob 模式)。\n\t * 优先级高于路由级 renderMode。\n\t */\n\trenderModes?: Record<string, string>;\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\trenderModes,\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// Vite 配置级别覆盖:CSR 直接返回空壳\n\t\t\tconst overrideMode = matchRenderModeOverride(url, renderModes);\n\t\t\tif (overrideMode === \"csr\") {\n\t\t\t\treturn c.html(injectCSRShell(template, locale));\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 缓存(包括 Vite 配置覆盖和路由级)\n\t\t\tif (renderMode === \"prerender\" || overrideMode === \"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"]}
|
package/dist/index.cjs
CHANGED
|
@@ -1177,6 +1177,19 @@ var app_exports = {};
|
|
|
1177
1177
|
__export(app_exports, {
|
|
1178
1178
|
createSSRApp: () => createSSRApp
|
|
1179
1179
|
});
|
|
1180
|
+
function matchRenderModeOverride(url, renderModes) {
|
|
1181
|
+
if (!renderModes) return null;
|
|
1182
|
+
const path = url.split("?")[0];
|
|
1183
|
+
if (renderModes[path]) return renderModes[path];
|
|
1184
|
+
for (const [pattern, mode] of Object.entries(renderModes)) {
|
|
1185
|
+
if (pattern.includes("*")) {
|
|
1186
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1187
|
+
const re = new RegExp("^" + escaped.replace(/\*/g, ".*") + "$");
|
|
1188
|
+
if (re.test(path)) return mode;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1180
1193
|
function createSSRApp(options) {
|
|
1181
1194
|
const {
|
|
1182
1195
|
root,
|
|
@@ -1186,7 +1199,8 @@ function createSSRApp(options) {
|
|
|
1186
1199
|
ssrProductionModule,
|
|
1187
1200
|
supportedLocales,
|
|
1188
1201
|
defaultLocale,
|
|
1189
|
-
parentFetch
|
|
1202
|
+
parentFetch,
|
|
1203
|
+
renderModes
|
|
1190
1204
|
} = options;
|
|
1191
1205
|
const app = new import_hono.Hono();
|
|
1192
1206
|
const ISR_CACHE_MAX = 1e3;
|
|
@@ -1272,6 +1286,10 @@ function createSSRApp(options) {
|
|
|
1272
1286
|
supportedLocales,
|
|
1273
1287
|
defaultLocale
|
|
1274
1288
|
);
|
|
1289
|
+
const overrideMode = matchRenderModeOverride(url, renderModes);
|
|
1290
|
+
if (overrideMode === "csr") {
|
|
1291
|
+
return c.html(injectCSRShell(template, locale));
|
|
1292
|
+
}
|
|
1275
1293
|
const cacheKey = `${locale}:${url}`;
|
|
1276
1294
|
const cached = isrCache.get(cacheKey);
|
|
1277
1295
|
if (cached) return c.html(cached);
|
|
@@ -1299,7 +1317,7 @@ function createSSRApp(options) {
|
|
|
1299
1317
|
html: appHtml,
|
|
1300
1318
|
serializedData
|
|
1301
1319
|
});
|
|
1302
|
-
if (renderMode === "prerender") {
|
|
1320
|
+
if (renderMode === "prerender" || overrideMode === "prerender") {
|
|
1303
1321
|
isrSet(cacheKey, finalHtml);
|
|
1304
1322
|
}
|
|
1305
1323
|
return c.html(finalHtml);
|
|
@@ -1358,6 +1376,7 @@ __export(index_exports, {
|
|
|
1358
1376
|
deserializeServerData: () => deserializeServerData,
|
|
1359
1377
|
detectRuntime: () => detectRuntime,
|
|
1360
1378
|
finesoftFrontViteConfig: () => finesoftFrontViteConfig,
|
|
1379
|
+
generateProxyCode: () => generateProxyCode,
|
|
1361
1380
|
generateUuid: () => generateUuid,
|
|
1362
1381
|
getBaseUrl: () => getBaseUrl,
|
|
1363
1382
|
injectCSRShell: () => injectCSRShell,
|
|
@@ -1379,6 +1398,7 @@ __export(index_exports, {
|
|
|
1379
1398
|
registerActionHandlers: () => registerActionHandlers,
|
|
1380
1399
|
registerExternalUrlHandler: () => registerExternalUrlHandler,
|
|
1381
1400
|
registerFlowActionHandler: () => registerFlowActionHandler,
|
|
1401
|
+
registerProxyRoutes: () => registerProxyRoutes,
|
|
1382
1402
|
removeHost: () => removeHost,
|
|
1383
1403
|
removeQueryParams: () => removeQueryParams,
|
|
1384
1404
|
removeScheme: () => removeScheme,
|
|
@@ -1732,6 +1752,120 @@ init_src();
|
|
|
1732
1752
|
// src/index.ts
|
|
1733
1753
|
init_src2();
|
|
1734
1754
|
|
|
1755
|
+
// ../server/src/proxy.ts
|
|
1756
|
+
function sanitizeProxyPath(raw) {
|
|
1757
|
+
if (raw.startsWith("//")) return null;
|
|
1758
|
+
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
1759
|
+
}
|
|
1760
|
+
function validateConfig(config) {
|
|
1761
|
+
if (!config.prefix.startsWith("/")) {
|
|
1762
|
+
throw new Error(
|
|
1763
|
+
`[proxy] prefix must start with "/": "${config.prefix}"`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
if (!config.target.startsWith("https://")) {
|
|
1767
|
+
throw new Error(`[proxy] target must use HTTPS: "${config.target}"`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function registerProxyRoutes(app, configs) {
|
|
1771
|
+
for (const config of configs) {
|
|
1772
|
+
validateConfig(config);
|
|
1773
|
+
const methods = config.methods ?? ["all"];
|
|
1774
|
+
const pattern = `${config.prefix}/*`;
|
|
1775
|
+
const handler = async (c) => {
|
|
1776
|
+
const subPath = sanitizeProxyPath(
|
|
1777
|
+
c.req.path.replace(config.prefix, "")
|
|
1778
|
+
);
|
|
1779
|
+
if (!subPath) return c.text("Invalid path", 400);
|
|
1780
|
+
const targetUrl = new URL(subPath, config.target);
|
|
1781
|
+
const reqUrl = new URL(c.req.url);
|
|
1782
|
+
reqUrl.searchParams.forEach(
|
|
1783
|
+
(v, k) => targetUrl.searchParams.set(k, v)
|
|
1784
|
+
);
|
|
1785
|
+
const headers = { ...config.headers };
|
|
1786
|
+
if (config.auth) {
|
|
1787
|
+
const token = process.env[config.auth.envKey] ?? "";
|
|
1788
|
+
if (token) {
|
|
1789
|
+
headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
try {
|
|
1793
|
+
const resp = await fetch(targetUrl.toString(), {
|
|
1794
|
+
headers,
|
|
1795
|
+
redirect: config.followRedirects ? "follow" : "manual"
|
|
1796
|
+
});
|
|
1797
|
+
const body = await resp.text();
|
|
1798
|
+
const respHeaders = {
|
|
1799
|
+
"Content-Type": resp.headers.get("Content-Type") ?? "application/json"
|
|
1800
|
+
};
|
|
1801
|
+
if (config.cache) {
|
|
1802
|
+
respHeaders["Cache-Control"] = config.cache;
|
|
1803
|
+
}
|
|
1804
|
+
return c.newResponse(body, resp.status, respHeaders);
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
console.error(`[Proxy ${config.prefix}]`, e);
|
|
1807
|
+
return c.json({ error: "Proxy request failed" }, 502);
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
for (const method of methods) {
|
|
1811
|
+
app[method](pattern, handler);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function generateProxyCode(configs) {
|
|
1816
|
+
if (!configs || configs.length === 0) return "";
|
|
1817
|
+
for (const config of configs) {
|
|
1818
|
+
validateConfig(config);
|
|
1819
|
+
}
|
|
1820
|
+
const blocks = [];
|
|
1821
|
+
blocks.push(`
|
|
1822
|
+
// \u2500\u2500\u2500 \u6846\u67B6\u58F0\u660E\u5F0F\u4EE3\u7406\u8DEF\u7531 \u2500\u2500\u2500
|
|
1823
|
+
function _sanitizeProxyPath(raw) {
|
|
1824
|
+
if (raw.startsWith("//")) return null;
|
|
1825
|
+
return raw.startsWith("/") ? raw : "/" + raw;
|
|
1826
|
+
}
|
|
1827
|
+
`);
|
|
1828
|
+
for (const config of configs) {
|
|
1829
|
+
const methods = config.methods ?? ["all"];
|
|
1830
|
+
const pattern = `"${config.prefix}/*"`;
|
|
1831
|
+
const headersJson = JSON.stringify(config.headers ?? {});
|
|
1832
|
+
const cacheStr = config.cache ? JSON.stringify(config.cache) : "null";
|
|
1833
|
+
const redirect = config.followRedirects ? '"follow"' : '"manual"';
|
|
1834
|
+
let authCode = "";
|
|
1835
|
+
if (config.auth) {
|
|
1836
|
+
const envKey = JSON.stringify(config.auth.envKey);
|
|
1837
|
+
const prefix = config.auth.type === "bearer" ? "Bearer " : "Basic ";
|
|
1838
|
+
authCode = `
|
|
1839
|
+
const _token = (typeof process !== "undefined" && process.env && process.env[${envKey}]) || "";
|
|
1840
|
+
if (_token) _headers.Authorization = "${prefix}" + _token;`;
|
|
1841
|
+
}
|
|
1842
|
+
const handlerCode = `async (c) => {
|
|
1843
|
+
const _sub = _sanitizeProxyPath(c.req.path.replace(${JSON.stringify(
|
|
1844
|
+
config.prefix
|
|
1845
|
+
)}, ""));
|
|
1846
|
+
if (!_sub) return c.text("Invalid path", 400);
|
|
1847
|
+
const _target = new URL(_sub, ${JSON.stringify(config.target)});
|
|
1848
|
+
const _reqUrl = new URL(c.req.url);
|
|
1849
|
+
_reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
|
|
1850
|
+
const _headers = ${headersJson};${authCode}
|
|
1851
|
+
try {
|
|
1852
|
+
const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect} });
|
|
1853
|
+
const _body = await _resp.text();
|
|
1854
|
+
const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
|
|
1855
|
+
if (${cacheStr}) _rh["Cache-Control"] = ${cacheStr};
|
|
1856
|
+
return c.newResponse(_body, _resp.status, _rh);
|
|
1857
|
+
} catch (_e) {
|
|
1858
|
+
console.error("[Proxy ${config.prefix}]", _e);
|
|
1859
|
+
return c.json({ error: "Proxy request failed" }, 502);
|
|
1860
|
+
}
|
|
1861
|
+
}`;
|
|
1862
|
+
for (const method of methods) {
|
|
1863
|
+
blocks.push(`app.${method}(${pattern}, ${handlerCode});`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return blocks.join("\n");
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1735
1869
|
// ../server/src/adapters/shared.ts
|
|
1736
1870
|
var BUILD_TOOL_EXTERNALS = [
|
|
1737
1871
|
"vite",
|
|
@@ -1814,6 +1948,7 @@ function matchRenderMode(url) {
|
|
|
1814
1948
|
}
|
|
1815
1949
|
|
|
1816
1950
|
const app = new Hono();
|
|
1951
|
+
${generateProxyCode(ctx.proxies ?? [])}
|
|
1817
1952
|
${setupCall}
|
|
1818
1953
|
${opts.platformMiddleware ?? ""}
|
|
1819
1954
|
|
|
@@ -2673,6 +2808,7 @@ async function createServer(config = {}) {
|
|
|
2673
2808
|
defaultLocale,
|
|
2674
2809
|
port = Number(process.env.PORT) || 3e3,
|
|
2675
2810
|
setup,
|
|
2811
|
+
proxies,
|
|
2676
2812
|
ssr
|
|
2677
2813
|
} = config;
|
|
2678
2814
|
const root = rootOverride ?? process.cwd();
|
|
@@ -2709,6 +2845,9 @@ async function createServer(config = {}) {
|
|
|
2709
2845
|
});
|
|
2710
2846
|
}
|
|
2711
2847
|
const app = new import_hono3.Hono();
|
|
2848
|
+
if (proxies?.length) {
|
|
2849
|
+
registerProxyRoutes(app, proxies);
|
|
2850
|
+
}
|
|
2712
2851
|
if (setup) {
|
|
2713
2852
|
await setup(app);
|
|
2714
2853
|
}
|
|
@@ -2765,6 +2904,7 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2765
2904
|
let resolvedCommand;
|
|
2766
2905
|
let resolvedResolve;
|
|
2767
2906
|
let resolvedCss;
|
|
2907
|
+
const CSS_EXTENSIONS = /\.(css|scss|less|sass|styl|stylus|pcss|postcss)($|\?)/;
|
|
2768
2908
|
return {
|
|
2769
2909
|
name: "finesoft-front",
|
|
2770
2910
|
config(userConfig, env) {
|
|
@@ -2784,6 +2924,82 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2784
2924
|
resolvedCss = config.css;
|
|
2785
2925
|
root = config.root;
|
|
2786
2926
|
},
|
|
2927
|
+
/**
|
|
2928
|
+
* Dev 模式 CSS 内联 — 消除 SSR 首屏布局抖动
|
|
2929
|
+
*
|
|
2930
|
+
* Vite dev 模式下,global.scss 等非组件 CSS 通过 JS 模块系统异步加载,
|
|
2931
|
+
* 导致 SSR HTML 初次渲染缺少布局关键样式(box-sizing、flex 布局、padding-top 等)。
|
|
2932
|
+
*
|
|
2933
|
+
* 此 hook 在 HTML 模板变换阶段(SSR 渲染之前):
|
|
2934
|
+
* 1. 找到浏览器入口脚本(排除 /@vite/client 等内部脚本)
|
|
2935
|
+
* 2. 编译入口脚本,填充 Vite 模块图
|
|
2936
|
+
* 3. 遍历模块图收集所有 CSS 依赖(排除 .svelte 组件 CSS,由 SSR 渲染自行处理)
|
|
2937
|
+
* 4. 通过 ssrLoadModule 获取编译后 CSS(SCSS→CSS)
|
|
2938
|
+
* 5. 注入 <style data-vite-dev-id> 标签到 <head>
|
|
2939
|
+
*
|
|
2940
|
+
* data-vite-dev-id 确保 Vite HMR 客户端复用已有标签,避免重复注入。
|
|
2941
|
+
*/
|
|
2942
|
+
transformIndexHtml: {
|
|
2943
|
+
order: "pre",
|
|
2944
|
+
async handler(html, ctx) {
|
|
2945
|
+
const server = ctx.server;
|
|
2946
|
+
if (!server) return;
|
|
2947
|
+
const urlPath = (ctx.originalUrl || ctx.path || "").split(
|
|
2948
|
+
"?"
|
|
2949
|
+
)[0];
|
|
2950
|
+
if (/\.\w+$/.test(urlPath) && !urlPath.endsWith(".html")) {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
const scripts = [
|
|
2954
|
+
...html.matchAll(
|
|
2955
|
+
/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g
|
|
2956
|
+
)
|
|
2957
|
+
];
|
|
2958
|
+
const appEntry = scripts.find((m) => !m[1].startsWith("/@"));
|
|
2959
|
+
if (!appEntry) return;
|
|
2960
|
+
const browserEntry = appEntry[1];
|
|
2961
|
+
try {
|
|
2962
|
+
await server.transformRequest(browserEntry);
|
|
2963
|
+
} catch {
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
const cssUrls = [];
|
|
2967
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2968
|
+
function walk(mod) {
|
|
2969
|
+
if (!mod?.url || visited.has(mod.url)) return;
|
|
2970
|
+
visited.add(mod.url);
|
|
2971
|
+
if (CSS_EXTENSIONS.test(mod.url) && !mod.url.includes(".svelte")) {
|
|
2972
|
+
cssUrls.push(mod.url);
|
|
2973
|
+
}
|
|
2974
|
+
if (mod.importedModules) {
|
|
2975
|
+
for (const imported of mod.importedModules) {
|
|
2976
|
+
walk(imported);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const mg = server.moduleGraph;
|
|
2981
|
+
const browserMod = await mg.getModuleByUrl(browserEntry);
|
|
2982
|
+
if (browserMod) walk(browserMod);
|
|
2983
|
+
if (cssUrls.length === 0) return;
|
|
2984
|
+
const tags = [];
|
|
2985
|
+
for (const url of cssUrls) {
|
|
2986
|
+
try {
|
|
2987
|
+
const mod = await server.ssrLoadModule(url);
|
|
2988
|
+
const css = mod?.default;
|
|
2989
|
+
if (typeof css === "string" && css.length > 0) {
|
|
2990
|
+
tags.push({
|
|
2991
|
+
tag: "style",
|
|
2992
|
+
attrs: { "data-vite-dev-id": url },
|
|
2993
|
+
children: css,
|
|
2994
|
+
injectTo: "head"
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
} catch {
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return tags;
|
|
3001
|
+
}
|
|
3002
|
+
},
|
|
2787
3003
|
// ─── Dev ───────────────────────────────────────────────
|
|
2788
3004
|
configureServer(server) {
|
|
2789
3005
|
return async () => {
|
|
@@ -2797,6 +3013,9 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2797
3013
|
"@hono/node-server"
|
|
2798
3014
|
);
|
|
2799
3015
|
const app = new HonoClass();
|
|
3016
|
+
if (options.proxies?.length) {
|
|
3017
|
+
registerProxyRoutes(app, options.proxies);
|
|
3018
|
+
}
|
|
2800
3019
|
if (typeof options.setup === "function") {
|
|
2801
3020
|
await options.setup(app);
|
|
2802
3021
|
} else if (typeof options.setup === "string") {
|
|
@@ -2811,7 +3030,8 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2811
3030
|
ssrEntryPath: "/" + ssrEntry,
|
|
2812
3031
|
supportedLocales: options.locales,
|
|
2813
3032
|
defaultLocale: options.defaultLocale,
|
|
2814
|
-
parentFetch: app.fetch.bind(app)
|
|
3033
|
+
parentFetch: app.fetch.bind(app),
|
|
3034
|
+
renderModes: options.renderModes
|
|
2815
3035
|
});
|
|
2816
3036
|
app.route("/", ssrApp);
|
|
2817
3037
|
const listener = getRequestListener(app.fetch);
|
|
@@ -2847,6 +3067,9 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2847
3067
|
);
|
|
2848
3068
|
const app = new HonoClass();
|
|
2849
3069
|
const isrCache = /* @__PURE__ */ new Map();
|
|
3070
|
+
if (options.proxies?.length) {
|
|
3071
|
+
registerProxyRoutes(app, options.proxies);
|
|
3072
|
+
}
|
|
2850
3073
|
if (typeof options.setup === "function") {
|
|
2851
3074
|
await options.setup(app);
|
|
2852
3075
|
} else if (typeof options.setup === "string") {
|
|
@@ -3001,6 +3224,7 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3001
3224
|
defaultLocale,
|
|
3002
3225
|
templateHtml,
|
|
3003
3226
|
renderModes: options.renderModes,
|
|
3227
|
+
proxies: options.proxies,
|
|
3004
3228
|
resolvedResolve,
|
|
3005
3229
|
resolvedCss,
|
|
3006
3230
|
vite,
|
|
@@ -3058,6 +3282,7 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3058
3282
|
deserializeServerData,
|
|
3059
3283
|
detectRuntime,
|
|
3060
3284
|
finesoftFrontViteConfig,
|
|
3285
|
+
generateProxyCode,
|
|
3061
3286
|
generateUuid,
|
|
3062
3287
|
getBaseUrl,
|
|
3063
3288
|
injectCSRShell,
|
|
@@ -3079,6 +3304,7 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3079
3304
|
registerActionHandlers,
|
|
3080
3305
|
registerExternalUrlHandler,
|
|
3081
3306
|
registerFlowActionHandler,
|
|
3307
|
+
registerProxyRoutes,
|
|
3082
3308
|
removeHost,
|
|
3083
3309
|
removeQueryParams,
|
|
3084
3310
|
removeScheme,
|