@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.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createSSRApp
3
- } from "./chunk-UHQBKSHL.js";
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-OOLQDVXA.js.map
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-UHQBKSHL.js.map
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,