@finesoft/front 0.1.47 → 0.1.49

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/index.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { n as parseAcceptLanguage } from "./locale-CvU-U6aP.mjs";
2
1
  import { Hono } from "hono";
3
2
  //#region ../core/src/actions/types.ts
4
3
  /**
@@ -38,22 +37,23 @@ function makeExternalUrlAction(url) {
38
37
  }
39
38
  //#endregion
40
39
  //#region ../core/src/actions/dispatcher.ts
40
+ /** CompoundAction 最大递归展开深度 */
41
+ const MAX_COMPOUND_DEPTH = 32;
41
42
  var ActionDispatcher = class {
42
43
  handlers = /* @__PURE__ */ new Map();
43
- wiredActions = /* @__PURE__ */ new Set();
44
44
  /** 注册指定 kind 的 handler(防止重复注册) */
45
45
  onAction(kind, handler) {
46
- if (this.wiredActions.has(kind)) {
46
+ if (this.handlers.has(kind)) {
47
47
  console.warn(`[ActionDispatcher] kind="${kind}" already registered, skipping`);
48
48
  return;
49
49
  }
50
- this.wiredActions.add(kind);
51
50
  this.handlers.set(kind, handler);
52
51
  }
53
- /** 执行一个 Action(CompoundAction 递归展开) */
54
- async perform(action) {
52
+ /** 执行一个 Action(CompoundAction 递归展开,有深度限制) */
53
+ async perform(action, _depth = 0) {
55
54
  if (isCompoundAction(action)) {
56
- for (const subAction of action.actions) await this.perform(subAction);
55
+ if (_depth >= MAX_COMPOUND_DEPTH) throw new Error(`[ActionDispatcher] CompoundAction recursion depth exceeded (max ${MAX_COMPOUND_DEPTH})`);
56
+ for (const subAction of action.actions) await this.perform(subAction, _depth + 1);
57
57
  return;
58
58
  }
59
59
  const handler = this.handlers.get(action.kind);
@@ -87,6 +87,7 @@ var IntentDispatcher = class {
87
87
  //#region ../core/src/dependencies/container.ts
88
88
  var Container = class {
89
89
  registrations = /* @__PURE__ */ new Map();
90
+ resolutionStack = /* @__PURE__ */ new Set();
90
91
  /** 注册依赖(默认单例) */
91
92
  register(key, factory, singleton = true) {
92
93
  this.registrations.set(key, {
@@ -100,7 +101,15 @@ var Container = class {
100
101
  const reg = this.registrations.get(key);
101
102
  if (!reg) throw new Error(`[Container] No registration for key: "${key}"`);
102
103
  if (reg.singleton) {
103
- if (reg.instance === void 0) reg.instance = reg.factory();
104
+ if (reg.instance === void 0) {
105
+ if (this.resolutionStack.has(key)) throw new Error(`[Container] Circular dependency detected: ${[...this.resolutionStack, key].join(" → ")}`);
106
+ this.resolutionStack.add(key);
107
+ try {
108
+ reg.instance = reg.factory();
109
+ } finally {
110
+ this.resolutionStack.delete(key);
111
+ }
112
+ }
104
113
  return reg.instance;
105
114
  }
106
115
  return reg.factory();
@@ -212,20 +221,11 @@ const DEP_KEYS = {
212
221
  LOGGER: "logger",
213
222
  LOGGER_FACTORY: "loggerFactory",
214
223
  NET: "net",
215
- LOCALE: "locale",
216
224
  STORAGE: "storage",
217
225
  FEATURE_FLAGS: "featureFlags",
218
226
  METRICS: "metrics",
219
227
  FETCH: "fetch"
220
228
  };
221
- var DefaultLocale = class {
222
- language = "en";
223
- storefront = "us";
224
- setActiveLocale(language, storefront) {
225
- this.language = language;
226
- this.storefront = storefront;
227
- }
228
- };
229
229
  var MemoryStorage = class {
230
230
  store = /* @__PURE__ */ new Map();
231
231
  get(key) {
@@ -264,16 +264,11 @@ var ConsoleMetrics = class {
264
264
  }
265
265
  };
266
266
  function makeDependencies(container, options = {}) {
267
- const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), language = "en", storefront = "us", featureFlags = {} } = options;
267
+ const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {} } = options;
268
268
  const loggerFactory = new ConsoleLoggerFactory();
269
269
  container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
270
270
  container.register(DEP_KEYS.LOGGER, () => loggerFactory.loggerFor("framework"));
271
271
  container.register(DEP_KEYS.NET, () => ({ fetch: (url, opts) => fetchFn(url, opts) }));
272
- container.register(DEP_KEYS.LOCALE, () => {
273
- const locale = new DefaultLocale();
274
- locale.setActiveLocale(language, storefront);
275
- return locale;
276
- });
277
272
  container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
278
273
  container.register(DEP_KEYS.FEATURE_FLAGS, () => new DefaultFeatureFlags(featureFlags));
279
274
  container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
@@ -293,6 +288,7 @@ var Router = class {
293
288
  const regexStr = pattern.split(/(\/:[\w]+\??)/).map((segment) => {
294
289
  const paramMatch = segment.match(/^\/:(\w+)(\?)?$/);
295
290
  if (paramMatch) {
291
+ if (paramNames.includes(paramMatch[1])) throw new Error(`[Router] Duplicate parameter name ":${paramMatch[1]}" in pattern "${pattern}"`);
296
292
  paramNames.push(paramMatch[1]);
297
293
  return paramMatch[2] ? "(?:/([^/]+))?" : "/([^/]+)";
298
294
  }
@@ -311,8 +307,7 @@ var Router = class {
311
307
  }
312
308
  /** 解析 URL → RouteMatch */
313
309
  resolve(urlOrPath) {
314
- const path = this.extractPath(urlOrPath);
315
- const queryParams = this.extractQueryParams(urlOrPath);
310
+ const { path, queryParams } = this.parseUrl(urlOrPath);
316
311
  for (const route of this.routes) {
317
312
  const match = path.match(route.regex);
318
313
  if (match) {
@@ -340,23 +335,22 @@ var Router = class {
340
335
  getRoutes() {
341
336
  return this.routes.map((r) => `${r.pattern} → ${r.intentId}`);
342
337
  }
343
- extractPath(url) {
344
- try {
345
- return new URL(url, "http://localhost").pathname;
346
- } catch {
347
- return url.split("?")[0].split("#")[0];
348
- }
349
- }
350
- extractQueryParams(url) {
338
+ parseUrl(url) {
351
339
  try {
352
340
  const parsed = new URL(url, "http://localhost");
353
341
  const params = {};
354
342
  parsed.searchParams.forEach((v, k) => {
355
343
  params[k] = v;
356
344
  });
357
- return params;
345
+ return {
346
+ path: parsed.pathname,
347
+ queryParams: params
348
+ };
358
349
  } catch {
359
- return {};
350
+ return {
351
+ path: url.split("?")[0].split("#")[0],
352
+ queryParams: {}
353
+ };
360
354
  }
361
355
  }
362
356
  };
@@ -416,14 +410,33 @@ async function runAfterLoadGuards(guards, ctx) {
416
410
  *
417
411
  * 用作缓存 key:相同内容的对象始终产生相同字符串。
418
412
  */
419
- function stableStringify(obj, _seen) {
413
+ /** WeakMap 缓存已序列化的对象,避免重复计算 */
414
+ const stringifyCache = /* @__PURE__ */ new WeakMap();
415
+ /** 最大递归深度 */
416
+ const MAX_DEPTH = 50;
417
+ function stableStringify(obj, _seen, _depth) {
420
418
  if (obj === null || obj === void 0) return String(obj);
421
419
  if (typeof obj !== "object") return JSON.stringify(obj);
420
+ const cached = stringifyCache.get(obj);
421
+ if (cached !== void 0) return cached;
422
+ const depth = _depth ?? 0;
423
+ if (depth > MAX_DEPTH) return "\"[Max Depth]\"";
422
424
  const seen = _seen ?? /* @__PURE__ */ new Set();
423
425
  if (seen.has(obj)) return "\"[Circular]\"";
424
426
  seen.add(obj);
425
- if (Array.isArray(obj)) return "[" + obj.map((v) => stableStringify(v, seen)).join(",") + "]";
426
- return "{" + Object.keys(obj).sort().filter((k) => obj[k] !== void 0).map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k], seen)).join(",") + "}";
427
+ let result;
428
+ if (Array.isArray(obj)) result = "[" + obj.map((v) => stableStringify(v, seen, depth + 1)).join(",") + "]";
429
+ else {
430
+ const keys = Object.keys(obj).sort();
431
+ const parts = [];
432
+ for (const k of keys) {
433
+ const v = obj[k];
434
+ if (v !== void 0) parts.push(JSON.stringify(k) + ":" + stableStringify(v, seen, depth + 1));
435
+ }
436
+ result = "{" + parts.join(",") + "}";
437
+ }
438
+ stringifyCache.set(obj, result);
439
+ return result;
427
440
  }
428
441
  //#endregion
429
442
  //#region ../core/src/prefetched-intents/prefetched-intents.ts
@@ -482,6 +495,7 @@ var Framework = class Framework {
482
495
  prefetchedIntents;
483
496
  beforeGuards = [];
484
497
  afterGuards = [];
498
+ _logger;
485
499
  constructor(container, prefetchedIntents) {
486
500
  this.container = container;
487
501
  this.intentDispatcher = new IntentDispatcher();
@@ -497,9 +511,12 @@ var Framework = class Framework {
497
511
  config.setupRoutes?.(fw.router);
498
512
  return fw;
499
513
  }
514
+ getLogger() {
515
+ return this._logger ??= this.container.resolve(DEP_KEYS.LOGGER);
516
+ }
500
517
  /** 分发 Intent — 获取页面数据 */
501
518
  async dispatch(intent) {
502
- const logger = this.container.resolve(DEP_KEYS.LOGGER);
519
+ const logger = this.getLogger();
503
520
  const cached = this.prefetchedIntents.get(intent);
504
521
  if (cached !== void 0) {
505
522
  logger.debug(`[Framework] re-using prefetched intent response for: ${intent.id}`, intent.params);
@@ -510,7 +527,7 @@ var Framework = class Framework {
510
527
  }
511
528
  /** 执行 Action — 处理用户交互 */
512
529
  async perform(action) {
513
- this.container.resolve(DEP_KEYS.LOGGER).debug(`[Framework] perform action: ${action.kind}`);
530
+ this.getLogger().debug(`[Framework] perform action: ${action.kind}`);
514
531
  return this.actionDispatcher.perform(action);
515
532
  }
516
533
  /** 路由 URL — 将 URL 解析为 Intent + Action */
@@ -645,7 +662,12 @@ var HttpClient = class {
645
662
  const body = await response.text().catch(() => void 0);
646
663
  throw new HttpError(response.status, response.statusText, body);
647
664
  }
648
- return response.json();
665
+ try {
666
+ return await response.json();
667
+ } catch (e) {
668
+ if (e instanceof SyntaxError) throw new HttpError(response.status, "Invalid JSON response", await response.text().catch(() => void 0));
669
+ throw e;
670
+ }
649
671
  }
650
672
  /** 构建完整 URL — 子类可覆写以自定义 URL 拼接逻辑 */
651
673
  buildUrl(path, params) {
@@ -771,6 +793,7 @@ var LruMap = class {
771
793
  map = /* @__PURE__ */ new Map();
772
794
  capacity;
773
795
  constructor(capacity) {
796
+ if (capacity < 1) throw new Error(`[LruMap] capacity must be >= 1, got ${capacity}`);
774
797
  this.capacity = capacity;
775
798
  }
776
799
  get(key) {
@@ -1300,17 +1323,15 @@ function createPrefetchedIntentsFromDom() {
1300
1323
  * 自动执行 hydration 全流程。
1301
1324
  */
1302
1325
  async function startBrowserApp(config) {
1303
- const { bootstrap, defaultLocale = "en", mountId = "app", mount, callbacks } = config;
1326
+ const { bootstrap, mountId = "app", mount, callbacks } = config;
1304
1327
  const prefetchedIntents = createPrefetchedIntentsFromDom();
1305
1328
  const framework = Framework.create({ prefetchedIntents });
1306
1329
  bootstrap(framework);
1307
1330
  const log = framework.container.resolve(DEP_KEYS.LOGGER_FACTORY).loggerFor("browser");
1308
1331
  const initialAction = framework.routeUrl(window.location.pathname + window.location.search);
1309
- const locale = document.documentElement.lang || defaultLocale;
1310
- const updateApp = mount(document.getElementById(mountId), {
1311
- framework,
1312
- locale
1313
- });
1332
+ const target = document.getElementById(mountId);
1333
+ if (!target) throw new Error(`[startBrowserApp] Mount target not found: #${mountId}. Ensure your HTML has <div id="${mountId}"></div>.`);
1334
+ const updateApp = mount(target, { framework });
1314
1335
  registerActionHandlers({
1315
1336
  framework,
1316
1337
  log,
@@ -1393,7 +1414,8 @@ async function ssrRender(options) {
1393
1414
  head: result.head,
1394
1415
  css: result.css,
1395
1416
  serverData,
1396
- renderMode: match?.renderMode
1417
+ renderMode: match?.renderMode,
1418
+ slots: result.slots
1397
1419
  };
1398
1420
  } finally {
1399
1421
  framework.dispose();
@@ -1432,7 +1454,8 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
1432
1454
  html: rendered.html,
1433
1455
  head: rendered.head,
1434
1456
  css: rendered.css,
1435
- serverData: []
1457
+ serverData: [],
1458
+ slots: rendered.slots
1436
1459
  };
1437
1460
  }
1438
1461
  }
@@ -1442,16 +1465,16 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
1442
1465
  /**
1443
1466
  * 创建 render 函数
1444
1467
  *
1445
- * @returns `render(url, locale, ssrContext?)` — 供 @finesoft/server SSRModule 使用
1468
+ * @returns `render(url, ssrContext?)` — 供 @finesoft/server SSRModule 使用
1446
1469
  */
1447
1470
  function createSSRRender(config) {
1448
1471
  const { bootstrap, getErrorPage, renderApp, frameworkConfig } = config;
1449
- return (url, locale, ssrContext) => ssrRender({
1472
+ return (url, ssrContext) => ssrRender({
1450
1473
  url,
1451
1474
  frameworkConfig: frameworkConfig ?? {},
1452
1475
  bootstrap,
1453
1476
  getErrorPage,
1454
- renderApp: (page) => renderApp(page, locale),
1477
+ renderApp: (page) => renderApp(page),
1455
1478
  ssrContext
1456
1479
  });
1457
1480
  }
@@ -1462,22 +1485,28 @@ function createSSRRender(config) {
1462
1485
  */
1463
1486
  /** SSR HTML 模板占位符常量 */
1464
1487
  const SSR_PLACEHOLDERS = {
1465
- LANG: "<!--ssr-lang-->",
1466
1488
  HEAD: "<!--ssr-head-->",
1467
1489
  BODY: "<!--ssr-body-->",
1468
1490
  DATA: "<!--ssr-data-->"
1469
1491
  };
1492
+ /** 匹配所有 <!--ssr-xxx--> 占位符(含内置与自定义) */
1493
+ const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
1470
1494
  function injectSSRContent(options) {
1471
- const { template, locale, head, css, html, serializedData } = options;
1472
- const cssTag = css ? `<style>${css}</style>` : "";
1473
- return template.replace(SSR_PLACEHOLDERS.LANG, locale).replace(SSR_PLACEHOLDERS.HEAD, `${head}\n${cssTag}`).replace(SSR_PLACEHOLDERS.BODY, html).replace(SSR_PLACEHOLDERS.DATA, `<script id="serialized-server-data" type="application/json">${serializedData}<\/script>`);
1495
+ const { template, head, css, html, serializedData, slots } = options;
1496
+ const replacements = {
1497
+ head: `${head}\n${css ? `<style>${css}</style>` : ""}`,
1498
+ body: html,
1499
+ data: `<script id="serialized-server-data" type="application/json">${serializedData}<\/script>`,
1500
+ ...slots
1501
+ };
1502
+ return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
1474
1503
  }
1475
1504
  /**
1476
- * CSR 空壳注入 — 只替换 lang,清空 body/head/data 占位符
1505
+ * CSR 空壳注入 — 清空所有占位符
1477
1506
  * 用于 renderMode === "csr" 的路由
1478
1507
  */
1479
- function injectCSRShell(template, locale) {
1480
- return template.replace(SSR_PLACEHOLDERS.LANG, locale).replace(SSR_PLACEHOLDERS.HEAD, "").replace(SSR_PLACEHOLDERS.BODY, "").replace(SSR_PLACEHOLDERS.DATA, "");
1508
+ function injectCSRShell(template) {
1509
+ return template.replace(PLACEHOLDER_REGEX, () => "");
1481
1510
  }
1482
1511
  //#endregion
1483
1512
  //#region ../ssr/src/server-data.ts
@@ -1517,12 +1546,23 @@ async function dynamicImport(specifier) {
1517
1546
  }
1518
1547
  //#endregion
1519
1548
  //#region ../server/src/proxy.ts
1549
+ /** 代理路径最大长度 */
1550
+ const MAX_PROXY_PATH_LENGTH = 2048;
1551
+ /** 代理响应最大体积(10 MB) */
1552
+ const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
1520
1553
  /**
1521
- * 校验代理路径,防止 SSRF(协议相对 URL 绕过)。
1554
+ * 校验代理路径,防止 SSRF(协议相对 URL 绕过、编码绕过)。
1522
1555
  * 返回规范化的路径,或 null 表示非法。
1523
1556
  */
1524
1557
  function sanitizeProxyPath(raw) {
1558
+ if (raw.length > MAX_PROXY_PATH_LENGTH) return null;
1559
+ try {
1560
+ if (decodeURIComponent(raw) !== raw) return null;
1561
+ } catch {
1562
+ return null;
1563
+ }
1525
1564
  if (raw.startsWith("//")) return null;
1565
+ if (!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
1526
1566
  return raw.startsWith("/") ? raw : `/${raw}`;
1527
1567
  }
1528
1568
  /**
@@ -1531,7 +1571,10 @@ function sanitizeProxyPath(raw) {
1531
1571
  */
1532
1572
  function validateConfig(config) {
1533
1573
  if (!config.prefix.startsWith("/")) throw new Error(`[proxy] prefix must start with "/": "${config.prefix}"`);
1534
- if (!config.target.startsWith("https://")) throw new Error(`[proxy] target must use HTTPS: "${config.target}"`);
1574
+ const isHttps = config.target.startsWith("https://");
1575
+ const isHttp = config.target.startsWith("http://");
1576
+ if (!isHttps && !isHttp) throw new Error(`[proxy] target must start with "https://" or "http://": "${config.target}"`);
1577
+ if (isHttp) console.warn(`[proxy] ⚠ target "${config.target}" uses plain HTTP — traffic will not be encrypted. Use HTTPS in production to prevent data interception.`);
1535
1578
  }
1536
1579
  /**
1537
1580
  * 注册声明式代理路由到 Hono app(运行时使用:dev / preview / createServer)
@@ -1545,18 +1588,24 @@ function registerProxyRoutes(app, configs) {
1545
1588
  const subPath = sanitizeProxyPath(c.req.path.replace(config.prefix, ""));
1546
1589
  if (!subPath) return c.text("Invalid path", 400);
1547
1590
  const targetUrl = new URL(subPath, config.target);
1591
+ const expectedOrigin = new URL(config.target).origin;
1592
+ if (targetUrl.origin !== expectedOrigin) return c.text("Invalid proxy target", 400);
1548
1593
  new URL(c.req.url).searchParams.forEach((v, k) => targetUrl.searchParams.set(k, v));
1549
1594
  const headers = { ...config.headers };
1550
1595
  if (config.auth) {
1551
- const token = process.env[config.auth.envKey] ?? "";
1552
- if (token) headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
1596
+ const token = process.env[config.auth.envKey];
1597
+ if (!token) console.warn(`[Proxy ${config.prefix}] Auth env var "${config.auth.envKey}" is not set`);
1598
+ else headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
1553
1599
  }
1554
1600
  try {
1555
1601
  const resp = await fetch(targetUrl.toString(), {
1556
1602
  headers,
1557
1603
  redirect: config.followRedirects ? "follow" : "manual"
1558
1604
  });
1605
+ const contentLength = resp.headers.get("Content-Length");
1606
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
1559
1607
  const body = await resp.text();
1608
+ if (body.length > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
1560
1609
  const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
1561
1610
  if (config.cache) respHeaders["Cache-Control"] = config.cache;
1562
1611
  return c.newResponse(body, resp.status, respHeaders);
@@ -1578,7 +1627,10 @@ function generateProxyCode(configs) {
1578
1627
  blocks.push(`
1579
1628
  // ─── 框架声明式代理路由 ───
1580
1629
  function _sanitizeProxyPath(raw) {
1630
+ if (raw.length > 2048) return null;
1631
+ try { if (decodeURIComponent(raw) !== raw) return null; } catch { return null; }
1581
1632
  if (raw.startsWith("//")) return null;
1633
+ if (!/^[/\\w.\\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
1582
1634
  return raw.startsWith("/") ? raw : "/" + raw;
1583
1635
  }
1584
1636
  `);
@@ -1596,6 +1648,7 @@ function _sanitizeProxyPath(raw) {
1596
1648
  const _sub = _sanitizeProxyPath(c.req.path.replace(${JSON.stringify(config.prefix)}, ""));
1597
1649
  if (!_sub) return c.text("Invalid path", 400);
1598
1650
  const _target = new URL(_sub, ${JSON.stringify(config.target)});
1651
+ if (_target.origin !== ${JSON.stringify(new URL(config.target).origin)}) return c.text("Invalid proxy target", 400);
1599
1652
  const _reqUrl = new URL(c.req.url);
1600
1653
  _reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
1601
1654
  const _headers = ${headersJson};${authCode}
@@ -1656,14 +1709,12 @@ const NODE_BUILTINS = [
1656
1709
  /**
1657
1710
  * 生成 SSR serverless/edge 入口源码
1658
1711
  *
1659
- * 内联 parseAcceptLanguage / injectSSR 以避免
1712
+ * 内联 injectSSR 以避免
1660
1713
  * @finesoft/front → @finesoft/server → vite-plugin → import("vite") 依赖链。
1661
1714
  */
1662
1715
  function generateSSREntry(ctx, opts) {
1663
1716
  const setupImport = ctx.setupPath ? `import _setupDefault from "./${ctx.setupPath}";` : ``;
1664
1717
  const setupCall = ctx.setupPath ? `if (typeof _setupDefault === "function") await _setupDefault(app);` : ``;
1665
- const locales = JSON.stringify(ctx.locales);
1666
- const defaultLocale = JSON.stringify(ctx.defaultLocale);
1667
1718
  const renderModes = JSON.stringify(ctx.renderModes ?? {});
1668
1719
  const cacheImpl = opts.platformCache ? opts.platformCache : `
1669
1720
  const ISR_CACHE_MAX = 1000;
@@ -1685,38 +1736,23 @@ import { render, serializeServerData } from "./${ctx.ssrEntry}";
1685
1736
  ${setupImport}
1686
1737
 
1687
1738
  const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
1688
- const LOCALES = ${locales};
1689
- const DEFAULT_LOCALE = ${defaultLocale};
1690
1739
  const RENDER_MODES = ${renderModes};
1691
1740
  ${cacheImpl}
1692
1741
 
1693
- function parseAcceptLanguage(header) {
1694
- if (!header) return DEFAULT_LOCALE;
1695
- const langs = header.split(",").map(p => {
1696
- const [l, q] = p.trim().split(";q=");
1697
- return { l: l.trim().toLowerCase(), q: q ? (+q || 0) : 1 };
1698
- }).sort((a, b) => b.q - a.q);
1699
- for (const { l } of langs) {
1700
- const prefix = l.split("-")[0];
1701
- if (LOCALES.includes(prefix)) return prefix;
1702
- }
1703
- return DEFAULT_LOCALE;
1704
- }
1705
-
1706
- function injectSSR(t, locale, head, css, html, data) {
1742
+ function injectSSR(t, head, css, html, data) {
1707
1743
  return t
1708
- .replace("<!--ssr-lang-->", locale)
1709
- .replace("<!--ssr-head-->", head + "\\n<style>" + css + "</style>")
1710
- .replace("<!--ssr-body-->", html)
1711
- .replace("<!--ssr-data-->", '<script id="serialized-server-data" type="application/json">' + data + "<\/script>");
1744
+ .replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_, name) => {
1745
+ const replacements = {
1746
+ head: head + "\\n<style>" + css + "</style>",
1747
+ body: html,
1748
+ data: '<script id="serialized-server-data" type="application/json">' + data + "<\/script>",
1749
+ };
1750
+ return replacements[name] ?? "";
1751
+ });
1712
1752
  }
1713
1753
 
1714
- function injectCSRShell(t, locale) {
1715
- return t
1716
- .replace("<!--ssr-lang-->", locale)
1717
- .replace("<!--ssr-head-->", "")
1718
- .replace("<!--ssr-body-->", "")
1719
- .replace("<!--ssr-data-->", "");
1754
+ function injectCSRShell(t) {
1755
+ return t.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
1720
1756
  }
1721
1757
 
1722
1758
  function matchRenderMode(url) {
@@ -1762,32 +1798,29 @@ app.get("*", async (c) => {
1762
1798
 
1763
1799
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1764
1800
  try {
1765
- const locale = parseAcceptLanguage(c.req.header("accept-language"));
1766
-
1767
1801
  // Vite 配置级别覆盖: CSR 直接返回空壳
1768
1802
  const overrideMode = matchRenderMode(url);
1769
1803
  if (overrideMode === "csr") {
1770
- return c.html(injectCSRShell(TEMPLATE, locale));
1804
+ return c.html(injectCSRShell(TEMPLATE));
1771
1805
  }
1772
1806
 
1773
- // ISR 缓存命中(key 含 locale,避免跨语言缓存污染)
1774
- const _cacheKey = locale + ":" + url;
1775
- const cached = await platformCacheGet(_cacheKey);
1807
+ // ISR 缓存命中
1808
+ const cached = await platformCacheGet(url);
1776
1809
  if (cached) return c.html(cached);
1777
1810
 
1778
- const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale, { fetch: _createInternalFetch(_ssrDepth + 1) });
1811
+ const { html: appHtml, head, css, serverData, renderMode } = await render(url, { fetch: _createInternalFetch(_ssrDepth + 1) });
1779
1812
 
1780
1813
  // 路由级 CSR
1781
1814
  if (renderMode === "csr") {
1782
- return c.html(injectCSRShell(TEMPLATE, locale));
1815
+ return c.html(injectCSRShell(TEMPLATE));
1783
1816
  }
1784
1817
 
1785
1818
  const serializedData = serializeServerData(serverData);
1786
- const finalHtml = injectSSR(TEMPLATE, locale, head, css, appHtml, serializedData);
1819
+ const finalHtml = injectSSR(TEMPLATE, head, css, appHtml, serializedData);
1787
1820
 
1788
1821
  // Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
1789
1822
  if (renderMode === "prerender" || overrideMode === "prerender") {
1790
- await platformCacheSet(_cacheKey, finalHtml);
1823
+ await platformCacheSet(url, finalHtml);
1791
1824
  ${opts.platformPrerenderResponseHook ?? ""}
1792
1825
  }
1793
1826
 
@@ -1863,21 +1896,24 @@ async function prerenderRoutes(ctx) {
1863
1896
  const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
1864
1897
  const ssrModule = await dynamicImport(ssrPath);
1865
1898
  const results = [];
1866
- for (const routePath of prerenderPaths) for (const locale of ctx.locales) {
1867
- const url = locale === ctx.defaultLocale ? routePath : `/${locale}${routePath === "/" ? "" : routePath}`;
1868
- try {
1869
- const { html: appHtml, head, css, serverData } = await ssrModule.render(url, locale);
1870
- const serializedData = ssrModule.serializeServerData(serverData);
1871
- const finalHtml = ctx.templateHtml.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", head + "\n<style>" + css + "</style>").replace("<!--ssr-body-->", appHtml).replace("<!--ssr-data-->", "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>");
1872
- results.push({
1873
- url,
1874
- html: finalHtml
1875
- });
1876
- } catch (e) {
1877
- console.warn(` [prerender] Failed to render ${url}:`, e);
1878
- }
1899
+ for (const url of prerenderPaths) try {
1900
+ const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
1901
+ const serializedData = ssrModule.serializeServerData(serverData);
1902
+ const finalHtml = ctx.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_match, name) => {
1903
+ return {
1904
+ head: head + "\n<style>" + css + "</style>",
1905
+ body: appHtml,
1906
+ data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
1907
+ }[name] ?? "";
1908
+ });
1909
+ results.push({
1910
+ url,
1911
+ html: finalHtml
1912
+ });
1913
+ } catch (e) {
1914
+ console.warn(` [prerender] Failed to render ${url}:`, e);
1879
1915
  }
1880
- if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes × ${ctx.locales.length} locales)\n`);
1916
+ if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes)\n`);
1881
1917
  return results;
1882
1918
  }
1883
1919
  //#endregion
@@ -2111,22 +2147,15 @@ function staticAdapter(opts = {}) {
2111
2147
  const ssrModule = await dynamicImport(ssrPath);
2112
2148
  ctx.copyStaticAssets(outputDir, { excludeHtml: true });
2113
2149
  const { paths: routePaths, defs: routeDefs } = await extractRoutesWithModes(ctx, opts);
2114
- const allUrls = [];
2115
- for (const routePath of routePaths) for (const locale of ctx.locales) {
2116
- const url = locale === ctx.defaultLocale ? routePath : `/${locale}${routePath === "/" ? "" : routePath}`;
2117
- allUrls.push(url);
2118
- }
2119
- console.log(` Pre-rendering ${allUrls.length} pages (${routePaths.length} routes × ${ctx.locales.length} locales)...\n`);
2120
- for (const url of allUrls) try {
2121
- const locale = inferLocale(url, ctx.locales, ctx.defaultLocale);
2122
- const routeDef = routeDefs.find((r) => r.path === stripLocalePrefix(url, ctx.locales));
2123
- const mode = resolveRenderMode(stripLocalePrefix(url, ctx.locales), routeDef?.renderMode, ctx.renderModes);
2150
+ console.log(` Pre-rendering ${routePaths.length} pages...\n`);
2151
+ for (const url of routePaths) try {
2152
+ const mode = resolveRenderMode(url, routeDefs.find((r) => r.path === url)?.renderMode, ctx.renderModes);
2124
2153
  let finalHtml;
2125
- if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml, locale);
2154
+ if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml);
2126
2155
  else {
2127
- const { html: appHtml, head, css, serverData } = await ssrModule.render(url, locale);
2156
+ const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
2128
2157
  const serializedData = ssrModule.serializeServerData(serverData);
2129
- finalHtml = injectSSRForStatic(ctx.templateHtml, locale, head, css, appHtml, serializedData);
2158
+ finalHtml = injectSSRForStatic(ctx.templateHtml, head, css, appHtml, serializedData);
2130
2159
  }
2131
2160
  const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
2132
2161
  fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
@@ -2181,28 +2210,19 @@ async function extractRoutesWithModes(ctx, opts) {
2181
2210
  defs
2182
2211
  };
2183
2212
  }
2184
- /** URL 推断 locale */
2185
- function inferLocale(url, locales, defaultLocale) {
2186
- const segments = url.split("/").filter(Boolean);
2187
- if (segments.length > 0 && locales.includes(segments[0])) return segments[0];
2188
- return defaultLocale;
2189
- }
2190
- /** 内联 SSR 注入(同 shared 中的逻辑) */
2191
- function injectSSRForStatic(template, locale, head, css, html, serializedData) {
2192
- return template.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", head + "\n<style>" + css + "</style>").replace("<!--ssr-body-->", html).replace("<!--ssr-data-->", "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>");
2213
+ /** 内联 SSR 注入 */
2214
+ function injectSSRForStatic(template, head, css, html, serializedData) {
2215
+ const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
2216
+ const replacements = {
2217
+ head: head + "\n<style>" + css + "</style>",
2218
+ body: html,
2219
+ data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
2220
+ };
2221
+ return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
2193
2222
  }
2194
2223
  /** CSR 空壳注入 */
2195
- function injectCSRShellForStatic(template, locale) {
2196
- return template.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", "").replace("<!--ssr-body-->", "").replace("<!--ssr-data-->", "");
2197
- }
2198
- /** 从 URL 去除 locale 前缀,还原路由路径 */
2199
- function stripLocalePrefix(url, locales) {
2200
- const segments = url.split("/").filter(Boolean);
2201
- if (segments.length > 0 && locales.includes(segments[0])) {
2202
- const rest = segments.slice(1).join("/");
2203
- return rest ? `/${rest}` : "/";
2204
- }
2205
- return url;
2224
+ function injectCSRShellForStatic(template) {
2225
+ return template.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
2206
2226
  }
2207
2227
  /** 解析最终渲染模式:Vite 配置覆盖 > 路由级 > 默认 "ssr" */
2208
2228
  function resolveRenderMode(routePath, routeRenderMode, renderModes) {
@@ -2402,7 +2422,7 @@ function matchRenderModeOverride(url, renderModes) {
2402
2422
  return null;
2403
2423
  }
2404
2424
  function createSSRApp(options) {
2405
- const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, supportedLocales, defaultLocale, parentFetch, renderModes } = options;
2425
+ const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes } = options;
2406
2426
  const app = new Hono();
2407
2427
  /** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
2408
2428
  const ISR_CACHE_MAX = 1e3;
@@ -2446,28 +2466,28 @@ function createSSRApp(options) {
2446
2466
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
2447
2467
  try {
2448
2468
  const template = await readTemplate(url);
2449
- const { render, serializeServerData } = await loadSSRModule();
2450
- const locale = parseAcceptLanguage(c.req.header("accept-language"), supportedLocales, defaultLocale);
2469
+ const ssrMod = await loadSSRModule();
2470
+ if (typeof ssrMod.render !== "function" || typeof ssrMod.serializeServerData !== "function") throw new Error("[SSR] Module missing required exports: render, serializeServerData");
2471
+ const { render, serializeServerData } = ssrMod;
2451
2472
  const overrideMode = matchRenderModeOverride(url, renderModes);
2452
- if (overrideMode === "csr") return c.html(injectCSRShell(template, locale));
2453
- const cacheKey = `${locale}:${url}`;
2454
- const cached = isrCache.get(cacheKey);
2473
+ if (overrideMode === "csr") return c.html(injectCSRShell(template));
2474
+ const cached = isrCache.get(url);
2455
2475
  if (cached) return c.html(cached);
2456
2476
  const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
2457
2477
  const ssrContext = { request: c.req.raw };
2458
2478
  if (requestFetch) ssrContext.fetch = requestFetch;
2459
- const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect } = await render(url, locale, ssrContext);
2479
+ const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots } = await render(url, ssrContext);
2460
2480
  if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
2461
- if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
2481
+ if (renderMode === "csr") return c.html(injectCSRShell(template));
2462
2482
  const finalHtml = injectSSRContent({
2463
2483
  template,
2464
- locale,
2465
2484
  head,
2466
2485
  css,
2467
2486
  html: appHtml,
2468
- serializedData: serializeServerData(serverData)
2487
+ serializedData: serializeServerData(serverData),
2488
+ slots
2469
2489
  });
2470
- if (renderMode === "prerender" || overrideMode === "prerender") isrSet(cacheKey, finalHtml);
2490
+ if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
2471
2491
  return c.html(finalHtml);
2472
2492
  } catch (e) {
2473
2493
  if (!isProduction && vite) vite.ssrFixStacktrace(e);
@@ -2517,7 +2537,7 @@ async function resolveRoot(importMetaUrl, levelsUp = 0) {
2517
2537
  * 支持 Node.js (dev HMR + prod)、Deno、Bun、Vercel。
2518
2538
  */
2519
2539
  async function startServer(options) {
2520
- const { app, root, port = 3e3, isProduction, vite, routes, locales, ssrEntryPath } = options;
2540
+ const { app, root, port = 3e3, isProduction, vite, routes, ssrEntryPath } = options;
2521
2541
  const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
2522
2542
  function printStartupBanner() {
2523
2543
  const lines = [`\n Server running at http://localhost:${port}\n`];
@@ -2526,9 +2546,8 @@ async function startServer(options) {
2526
2546
  for (const r of routes) lines.push(` ${r}`);
2527
2547
  lines.push("");
2528
2548
  }
2529
- if (locales && locales.length > 0) lines.push(` Locales: ${locales.join(", ")}`);
2530
2549
  if (ssrEntryPath) lines.push(` SSR Entry: ${ssrEntryPath}`);
2531
- if (locales?.length || ssrEntryPath) lines.push("");
2550
+ if (ssrEntryPath) lines.push("");
2532
2551
  console.log(lines.join("\n"));
2533
2552
  }
2534
2553
  if (isVercel) return { vite };
@@ -2587,21 +2606,22 @@ async function startServer(options) {
2587
2606
  * @example
2588
2607
  * ```ts
2589
2608
  * const { app } = await createServer({
2590
- * locales: ["zh", "en"],
2591
2609
  * setup: (app) => registerProxies(app),
2592
2610
  * });
2593
2611
  * export { app };
2594
2612
  * ```
2595
2613
  */
2596
2614
  async function createServer(config = {}) {
2597
- const { root: rootOverride, locales, defaultLocale, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
2615
+ const { root: rootOverride, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
2598
2616
  const root = rootOverride ?? process.cwd();
2599
2617
  const { existsSync } = await dynamicImport("node:fs");
2600
2618
  const envPath = (await dynamicImport("node:path")).resolve(root, ".env");
2601
2619
  if (existsSync(envPath)) try {
2602
2620
  const { config: dotenvConfig } = await dynamicImport("dotenv");
2603
2621
  dotenvConfig({ path: envPath });
2604
- } catch {}
2622
+ } catch (e) {
2623
+ console.warn(`[Server] Failed to load .env: ${e.message}`);
2624
+ }
2605
2625
  const runtime = detectRuntime();
2606
2626
  let vite;
2607
2627
  if (!runtime.isProduction && !runtime.isVercel) {
@@ -2619,8 +2639,6 @@ async function createServer(config = {}) {
2619
2639
  root,
2620
2640
  vite,
2621
2641
  isProduction: runtime.isProduction,
2622
- supportedLocales: locales,
2623
- defaultLocale,
2624
2642
  parentFetch: app.fetch.bind(app),
2625
2643
  ...ssr
2626
2644
  });
@@ -2632,7 +2650,6 @@ async function createServer(config = {}) {
2632
2650
  isProduction: runtime.isProduction,
2633
2651
  vite,
2634
2652
  runtime,
2635
- locales,
2636
2653
  ssrEntryPath: ssr?.ssrEntryPath
2637
2654
  });
2638
2655
  return {
@@ -2642,6 +2659,35 @@ async function createServer(config = {}) {
2642
2659
  };
2643
2660
  }
2644
2661
  //#endregion
2662
+ //#region ../server/src/locale.ts
2663
+ /**
2664
+ * Accept-Language 解析
2665
+ */
2666
+ /** Accept-Language 头最大长度 */
2667
+ const MAX_HEADER_LENGTH = 1024;
2668
+ /** 最大解析语言条目数 */
2669
+ const MAX_LANG_ENTRIES = 50;
2670
+ function parseAcceptLanguage(header, supported, fallback) {
2671
+ const effectiveSupported = supported ?? ["zh", "en"];
2672
+ const effectiveFallback = fallback ?? effectiveSupported[0] ?? "en";
2673
+ if (!header || header.length > MAX_HEADER_LENGTH) return effectiveFallback;
2674
+ const parts = header.split(",");
2675
+ if (parts.length > MAX_LANG_ENTRIES) return effectiveFallback;
2676
+ const langs = parts.map((part) => {
2677
+ const [lang, q] = part.trim().split(";q=");
2678
+ const qVal = q ? parseFloat(q) : 1;
2679
+ return {
2680
+ lang: lang.trim().toLowerCase(),
2681
+ q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
2682
+ };
2683
+ }).sort((a, b) => b.q - a.q);
2684
+ for (const { lang } of langs) {
2685
+ const prefix = lang.split("-")[0];
2686
+ if (effectiveSupported.includes(prefix)) return prefix;
2687
+ }
2688
+ return effectiveFallback;
2689
+ }
2690
+ //#endregion
2645
2691
  //#region ../server/src/vite-plugin.ts
2646
2692
  /**
2647
2693
  * finesoftFrontViteConfig — Vite 插件
@@ -2711,11 +2757,12 @@ function finesoftFrontViteConfig(options = {}) {
2711
2757
  }
2712
2758
  const cssUrls = [];
2713
2759
  const visited = /* @__PURE__ */ new Set();
2714
- function walk(mod) {
2760
+ function walk(mod, depth = 0) {
2761
+ if (depth > 100) return;
2715
2762
  if (!mod?.url || visited.has(mod.url)) return;
2716
2763
  visited.add(mod.url);
2717
2764
  if (CSS_EXTENSIONS.test(mod.url) && !mod.url.includes(".svelte")) cssUrls.push(mod.url);
2718
- if (mod.importedModules) for (const imported of mod.importedModules) walk(imported);
2765
+ if (mod.importedModules) for (const imported of mod.importedModules) walk(imported, depth + 1);
2719
2766
  }
2720
2767
  const browserMod = await server.moduleGraph.getModuleByUrl(browserEntry);
2721
2768
  if (browserMod) walk(browserMod);
@@ -2749,8 +2796,6 @@ function finesoftFrontViteConfig(options = {}) {
2749
2796
  vite: server,
2750
2797
  isProduction: false,
2751
2798
  ssrEntryPath: "/" + ssrEntry,
2752
- supportedLocales: options.locales,
2753
- defaultLocale: options.defaultLocale,
2754
2799
  parentFetch: app.fetch.bind(app),
2755
2800
  renderModes: options.renderModes
2756
2801
  });
@@ -2767,10 +2812,17 @@ function finesoftFrontViteConfig(options = {}) {
2767
2812
  const path = await dynamicImport("node:path");
2768
2813
  const { pathToFileURL } = await dynamicImport("node:url");
2769
2814
  const { Hono: HonoClass } = await dynamicImport("hono");
2770
- const { parseAcceptLanguage } = await import("./locale-CvU-U6aP.mjs").then((n) => n.t);
2771
2815
  const { getRequestListener } = await dynamicImport("@hono/node-server");
2772
2816
  const app = new HonoClass();
2817
+ const ISR_CACHE_MAX = 1e3;
2773
2818
  const isrCache = /* @__PURE__ */ new Map();
2819
+ function isrSet(key, val) {
2820
+ if (isrCache.size >= ISR_CACHE_MAX) {
2821
+ const first = isrCache.keys().next().value;
2822
+ if (first !== void 0) isrCache.delete(first);
2823
+ }
2824
+ isrCache.set(key, val);
2825
+ }
2774
2826
  if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
2775
2827
  if (typeof options.setup === "function") await options.setup(app);
2776
2828
  else if (typeof options.setup === "string") try {
@@ -2788,23 +2840,20 @@ function finesoftFrontViteConfig(options = {}) {
2788
2840
  if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
2789
2841
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
2790
2842
  try {
2791
- const locale = parseAcceptLanguage(c.req.header("accept-language"), options.locales, options.defaultLocale);
2792
2843
  const overrideMode = matchRenderModeConfig(url, options.renderModes);
2793
- if (overrideMode === "csr") return c.html(injectCSRShell(template, locale));
2794
- const cacheKey = `${locale}:${url}`;
2795
- const cached = isrCache.get(cacheKey);
2844
+ if (overrideMode === "csr") return c.html(injectCSRShell(template));
2845
+ const cached = isrCache.get(url);
2796
2846
  if (cached) return c.html(cached);
2797
- const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, locale, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
2798
- if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
2847
+ const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
2848
+ if (renderMode === "csr") return c.html(injectCSRShell(template));
2799
2849
  const finalHtml = injectSSRContent({
2800
2850
  template,
2801
- locale,
2802
2851
  head,
2803
2852
  css,
2804
2853
  html: appHtml,
2805
2854
  serializedData: ssrModule.serializeServerData(serverData)
2806
2855
  });
2807
- if (renderMode === "prerender" || overrideMode === "prerender") isrCache.set(cacheKey, finalHtml);
2856
+ if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
2808
2857
  return c.html(finalHtml);
2809
2858
  } catch (e) {
2810
2859
  console.error("[SSR Preview Error]", e);
@@ -2851,16 +2900,12 @@ function finesoftFrontViteConfig(options = {}) {
2851
2900
  }
2852
2901
  if (options.adapter) {
2853
2902
  const adapter = resolveAdapter(options.adapter);
2854
- const locales = options.locales ?? ["zh", "en"];
2855
- const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
2856
2903
  const templateHtml = fs.readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
2857
2904
  const ctx = {
2858
2905
  root,
2859
2906
  ssrEntry,
2860
2907
  setupPath: typeof options.setup === "string" ? options.setup : void 0,
2861
2908
  bootstrapEntry: options.bootstrapEntry,
2862
- locales,
2863
- defaultLocale,
2864
2909
  templateHtml,
2865
2910
  renderModes: options.renderModes,
2866
2911
  proxies: options.proxies,