@finesoft/front 0.1.46 → 0.1.48

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
  /**
@@ -1545,18 +1585,24 @@ function registerProxyRoutes(app, configs) {
1545
1585
  const subPath = sanitizeProxyPath(c.req.path.replace(config.prefix, ""));
1546
1586
  if (!subPath) return c.text("Invalid path", 400);
1547
1587
  const targetUrl = new URL(subPath, config.target);
1588
+ const expectedOrigin = new URL(config.target).origin;
1589
+ if (targetUrl.origin !== expectedOrigin) return c.text("Invalid proxy target", 400);
1548
1590
  new URL(c.req.url).searchParams.forEach((v, k) => targetUrl.searchParams.set(k, v));
1549
1591
  const headers = { ...config.headers };
1550
1592
  if (config.auth) {
1551
- const token = process.env[config.auth.envKey] ?? "";
1552
- if (token) headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
1593
+ const token = process.env[config.auth.envKey];
1594
+ if (!token) console.warn(`[Proxy ${config.prefix}] Auth env var "${config.auth.envKey}" is not set`);
1595
+ else headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
1553
1596
  }
1554
1597
  try {
1555
1598
  const resp = await fetch(targetUrl.toString(), {
1556
1599
  headers,
1557
1600
  redirect: config.followRedirects ? "follow" : "manual"
1558
1601
  });
1602
+ const contentLength = resp.headers.get("Content-Length");
1603
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
1559
1604
  const body = await resp.text();
1605
+ if (body.length > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
1560
1606
  const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
1561
1607
  if (config.cache) respHeaders["Cache-Control"] = config.cache;
1562
1608
  return c.newResponse(body, resp.status, respHeaders);
@@ -1578,7 +1624,10 @@ function generateProxyCode(configs) {
1578
1624
  blocks.push(`
1579
1625
  // ─── 框架声明式代理路由 ───
1580
1626
  function _sanitizeProxyPath(raw) {
1627
+ if (raw.length > 2048) return null;
1628
+ try { if (decodeURIComponent(raw) !== raw) return null; } catch { return null; }
1581
1629
  if (raw.startsWith("//")) return null;
1630
+ if (!/^[/\\w.\\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
1582
1631
  return raw.startsWith("/") ? raw : "/" + raw;
1583
1632
  }
1584
1633
  `);
@@ -1656,14 +1705,12 @@ const NODE_BUILTINS = [
1656
1705
  /**
1657
1706
  * 生成 SSR serverless/edge 入口源码
1658
1707
  *
1659
- * 内联 parseAcceptLanguage / injectSSR 以避免
1708
+ * 内联 injectSSR 以避免
1660
1709
  * @finesoft/front → @finesoft/server → vite-plugin → import("vite") 依赖链。
1661
1710
  */
1662
1711
  function generateSSREntry(ctx, opts) {
1663
1712
  const setupImport = ctx.setupPath ? `import _setupDefault from "./${ctx.setupPath}";` : ``;
1664
1713
  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
1714
  const renderModes = JSON.stringify(ctx.renderModes ?? {});
1668
1715
  const cacheImpl = opts.platformCache ? opts.platformCache : `
1669
1716
  const ISR_CACHE_MAX = 1000;
@@ -1685,38 +1732,23 @@ import { render, serializeServerData } from "./${ctx.ssrEntry}";
1685
1732
  ${setupImport}
1686
1733
 
1687
1734
  const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
1688
- const LOCALES = ${locales};
1689
- const DEFAULT_LOCALE = ${defaultLocale};
1690
1735
  const RENDER_MODES = ${renderModes};
1691
1736
  ${cacheImpl}
1692
1737
 
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) {
1738
+ function injectSSR(t, head, css, html, data) {
1707
1739
  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>");
1740
+ .replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_, name) => {
1741
+ const replacements = {
1742
+ head: head + "\\n<style>" + css + "</style>",
1743
+ body: html,
1744
+ data: '<script id="serialized-server-data" type="application/json">' + data + "<\/script>",
1745
+ };
1746
+ return replacements[name] ?? "";
1747
+ });
1712
1748
  }
1713
1749
 
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-->", "");
1750
+ function injectCSRShell(t) {
1751
+ return t.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
1720
1752
  }
1721
1753
 
1722
1754
  function matchRenderMode(url) {
@@ -1762,32 +1794,29 @@ app.get("*", async (c) => {
1762
1794
 
1763
1795
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1764
1796
  try {
1765
- const locale = parseAcceptLanguage(c.req.header("accept-language"));
1766
-
1767
1797
  // Vite 配置级别覆盖: CSR 直接返回空壳
1768
1798
  const overrideMode = matchRenderMode(url);
1769
1799
  if (overrideMode === "csr") {
1770
- return c.html(injectCSRShell(TEMPLATE, locale));
1800
+ return c.html(injectCSRShell(TEMPLATE));
1771
1801
  }
1772
1802
 
1773
- // ISR 缓存命中(key 含 locale,避免跨语言缓存污染)
1774
- const _cacheKey = locale + ":" + url;
1775
- const cached = await platformCacheGet(_cacheKey);
1803
+ // ISR 缓存命中
1804
+ const cached = await platformCacheGet(url);
1776
1805
  if (cached) return c.html(cached);
1777
1806
 
1778
- const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale, { fetch: _createInternalFetch(_ssrDepth + 1) });
1807
+ const { html: appHtml, head, css, serverData, renderMode } = await render(url, { fetch: _createInternalFetch(_ssrDepth + 1) });
1779
1808
 
1780
1809
  // 路由级 CSR
1781
1810
  if (renderMode === "csr") {
1782
- return c.html(injectCSRShell(TEMPLATE, locale));
1811
+ return c.html(injectCSRShell(TEMPLATE));
1783
1812
  }
1784
1813
 
1785
1814
  const serializedData = serializeServerData(serverData);
1786
- const finalHtml = injectSSR(TEMPLATE, locale, head, css, appHtml, serializedData);
1815
+ const finalHtml = injectSSR(TEMPLATE, head, css, appHtml, serializedData);
1787
1816
 
1788
1817
  // Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
1789
1818
  if (renderMode === "prerender" || overrideMode === "prerender") {
1790
- await platformCacheSet(_cacheKey, finalHtml);
1819
+ await platformCacheSet(url, finalHtml);
1791
1820
  ${opts.platformPrerenderResponseHook ?? ""}
1792
1821
  }
1793
1822
 
@@ -1863,21 +1892,24 @@ async function prerenderRoutes(ctx) {
1863
1892
  const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
1864
1893
  const ssrModule = await dynamicImport(ssrPath);
1865
1894
  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
- }
1895
+ for (const url of prerenderPaths) try {
1896
+ const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
1897
+ const serializedData = ssrModule.serializeServerData(serverData);
1898
+ const finalHtml = ctx.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_match, name) => {
1899
+ return {
1900
+ head: head + "\n<style>" + css + "</style>",
1901
+ body: appHtml,
1902
+ data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
1903
+ }[name] ?? "";
1904
+ });
1905
+ results.push({
1906
+ url,
1907
+ html: finalHtml
1908
+ });
1909
+ } catch (e) {
1910
+ console.warn(` [prerender] Failed to render ${url}:`, e);
1879
1911
  }
1880
- if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes × ${ctx.locales.length} locales)\n`);
1912
+ if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes)\n`);
1881
1913
  return results;
1882
1914
  }
1883
1915
  //#endregion
@@ -2111,22 +2143,15 @@ function staticAdapter(opts = {}) {
2111
2143
  const ssrModule = await dynamicImport(ssrPath);
2112
2144
  ctx.copyStaticAssets(outputDir, { excludeHtml: true });
2113
2145
  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);
2146
+ console.log(` Pre-rendering ${routePaths.length} pages...\n`);
2147
+ for (const url of routePaths) try {
2148
+ const mode = resolveRenderMode(url, routeDefs.find((r) => r.path === url)?.renderMode, ctx.renderModes);
2124
2149
  let finalHtml;
2125
- if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml, locale);
2150
+ if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml);
2126
2151
  else {
2127
- const { html: appHtml, head, css, serverData } = await ssrModule.render(url, locale);
2152
+ const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
2128
2153
  const serializedData = ssrModule.serializeServerData(serverData);
2129
- finalHtml = injectSSRForStatic(ctx.templateHtml, locale, head, css, appHtml, serializedData);
2154
+ finalHtml = injectSSRForStatic(ctx.templateHtml, head, css, appHtml, serializedData);
2130
2155
  }
2131
2156
  const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
2132
2157
  fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
@@ -2181,28 +2206,19 @@ async function extractRoutesWithModes(ctx, opts) {
2181
2206
  defs
2182
2207
  };
2183
2208
  }
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>");
2209
+ /** 内联 SSR 注入 */
2210
+ function injectSSRForStatic(template, head, css, html, serializedData) {
2211
+ const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
2212
+ const replacements = {
2213
+ head: head + "\n<style>" + css + "</style>",
2214
+ body: html,
2215
+ data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
2216
+ };
2217
+ return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
2193
2218
  }
2194
2219
  /** 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;
2220
+ function injectCSRShellForStatic(template) {
2221
+ return template.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
2206
2222
  }
2207
2223
  /** 解析最终渲染模式:Vite 配置覆盖 > 路由级 > 默认 "ssr" */
2208
2224
  function resolveRenderMode(routePath, routeRenderMode, renderModes) {
@@ -2402,7 +2418,7 @@ function matchRenderModeOverride(url, renderModes) {
2402
2418
  return null;
2403
2419
  }
2404
2420
  function createSSRApp(options) {
2405
- const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, supportedLocales, defaultLocale, parentFetch, renderModes } = options;
2421
+ const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes } = options;
2406
2422
  const app = new Hono();
2407
2423
  /** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
2408
2424
  const ISR_CACHE_MAX = 1e3;
@@ -2446,28 +2462,28 @@ function createSSRApp(options) {
2446
2462
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
2447
2463
  try {
2448
2464
  const template = await readTemplate(url);
2449
- const { render, serializeServerData } = await loadSSRModule();
2450
- const locale = parseAcceptLanguage(c.req.header("accept-language"), supportedLocales, defaultLocale);
2465
+ const ssrMod = await loadSSRModule();
2466
+ if (typeof ssrMod.render !== "function" || typeof ssrMod.serializeServerData !== "function") throw new Error("[SSR] Module missing required exports: render, serializeServerData");
2467
+ const { render, serializeServerData } = ssrMod;
2451
2468
  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);
2469
+ if (overrideMode === "csr") return c.html(injectCSRShell(template));
2470
+ const cached = isrCache.get(url);
2455
2471
  if (cached) return c.html(cached);
2456
2472
  const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
2457
2473
  const ssrContext = { request: c.req.raw };
2458
2474
  if (requestFetch) ssrContext.fetch = requestFetch;
2459
- const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect } = await render(url, locale, ssrContext);
2475
+ const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots } = await render(url, ssrContext);
2460
2476
  if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
2461
- if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
2477
+ if (renderMode === "csr") return c.html(injectCSRShell(template));
2462
2478
  const finalHtml = injectSSRContent({
2463
2479
  template,
2464
- locale,
2465
2480
  head,
2466
2481
  css,
2467
2482
  html: appHtml,
2468
- serializedData: serializeServerData(serverData)
2483
+ serializedData: serializeServerData(serverData),
2484
+ slots
2469
2485
  });
2470
- if (renderMode === "prerender" || overrideMode === "prerender") isrSet(cacheKey, finalHtml);
2486
+ if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
2471
2487
  return c.html(finalHtml);
2472
2488
  } catch (e) {
2473
2489
  if (!isProduction && vite) vite.ssrFixStacktrace(e);
@@ -2517,7 +2533,7 @@ async function resolveRoot(importMetaUrl, levelsUp = 0) {
2517
2533
  * 支持 Node.js (dev HMR + prod)、Deno、Bun、Vercel。
2518
2534
  */
2519
2535
  async function startServer(options) {
2520
- const { app, root, port = 3e3, isProduction, vite, routes, locales, ssrEntryPath } = options;
2536
+ const { app, root, port = 3e3, isProduction, vite, routes, ssrEntryPath } = options;
2521
2537
  const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
2522
2538
  function printStartupBanner() {
2523
2539
  const lines = [`\n Server running at http://localhost:${port}\n`];
@@ -2526,9 +2542,8 @@ async function startServer(options) {
2526
2542
  for (const r of routes) lines.push(` ${r}`);
2527
2543
  lines.push("");
2528
2544
  }
2529
- if (locales && locales.length > 0) lines.push(` Locales: ${locales.join(", ")}`);
2530
2545
  if (ssrEntryPath) lines.push(` SSR Entry: ${ssrEntryPath}`);
2531
- if (locales?.length || ssrEntryPath) lines.push("");
2546
+ if (ssrEntryPath) lines.push("");
2532
2547
  console.log(lines.join("\n"));
2533
2548
  }
2534
2549
  if (isVercel) return { vite };
@@ -2587,21 +2602,22 @@ async function startServer(options) {
2587
2602
  * @example
2588
2603
  * ```ts
2589
2604
  * const { app } = await createServer({
2590
- * locales: ["zh", "en"],
2591
2605
  * setup: (app) => registerProxies(app),
2592
2606
  * });
2593
2607
  * export { app };
2594
2608
  * ```
2595
2609
  */
2596
2610
  async function createServer(config = {}) {
2597
- const { root: rootOverride, locales, defaultLocale, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
2611
+ const { root: rootOverride, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
2598
2612
  const root = rootOverride ?? process.cwd();
2599
2613
  const { existsSync } = await dynamicImport("node:fs");
2600
2614
  const envPath = (await dynamicImport("node:path")).resolve(root, ".env");
2601
2615
  if (existsSync(envPath)) try {
2602
2616
  const { config: dotenvConfig } = await dynamicImport("dotenv");
2603
2617
  dotenvConfig({ path: envPath });
2604
- } catch {}
2618
+ } catch (e) {
2619
+ console.warn(`[Server] Failed to load .env: ${e.message}`);
2620
+ }
2605
2621
  const runtime = detectRuntime();
2606
2622
  let vite;
2607
2623
  if (!runtime.isProduction && !runtime.isVercel) {
@@ -2619,8 +2635,6 @@ async function createServer(config = {}) {
2619
2635
  root,
2620
2636
  vite,
2621
2637
  isProduction: runtime.isProduction,
2622
- supportedLocales: locales,
2623
- defaultLocale,
2624
2638
  parentFetch: app.fetch.bind(app),
2625
2639
  ...ssr
2626
2640
  });
@@ -2632,7 +2646,6 @@ async function createServer(config = {}) {
2632
2646
  isProduction: runtime.isProduction,
2633
2647
  vite,
2634
2648
  runtime,
2635
- locales,
2636
2649
  ssrEntryPath: ssr?.ssrEntryPath
2637
2650
  });
2638
2651
  return {
@@ -2642,6 +2655,35 @@ async function createServer(config = {}) {
2642
2655
  };
2643
2656
  }
2644
2657
  //#endregion
2658
+ //#region ../server/src/locale.ts
2659
+ /**
2660
+ * Accept-Language 解析
2661
+ */
2662
+ /** Accept-Language 头最大长度 */
2663
+ const MAX_HEADER_LENGTH = 1024;
2664
+ /** 最大解析语言条目数 */
2665
+ const MAX_LANG_ENTRIES = 50;
2666
+ function parseAcceptLanguage(header, supported, fallback) {
2667
+ const effectiveSupported = supported ?? ["zh", "en"];
2668
+ const effectiveFallback = fallback ?? effectiveSupported[0] ?? "en";
2669
+ if (!header || header.length > MAX_HEADER_LENGTH) return effectiveFallback;
2670
+ const parts = header.split(",");
2671
+ if (parts.length > MAX_LANG_ENTRIES) return effectiveFallback;
2672
+ const langs = parts.map((part) => {
2673
+ const [lang, q] = part.trim().split(";q=");
2674
+ const qVal = q ? parseFloat(q) : 1;
2675
+ return {
2676
+ lang: lang.trim().toLowerCase(),
2677
+ q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
2678
+ };
2679
+ }).sort((a, b) => b.q - a.q);
2680
+ for (const { lang } of langs) {
2681
+ const prefix = lang.split("-")[0];
2682
+ if (effectiveSupported.includes(prefix)) return prefix;
2683
+ }
2684
+ return effectiveFallback;
2685
+ }
2686
+ //#endregion
2645
2687
  //#region ../server/src/vite-plugin.ts
2646
2688
  /**
2647
2689
  * finesoftFrontViteConfig — Vite 插件
@@ -2711,11 +2753,12 @@ function finesoftFrontViteConfig(options = {}) {
2711
2753
  }
2712
2754
  const cssUrls = [];
2713
2755
  const visited = /* @__PURE__ */ new Set();
2714
- function walk(mod) {
2756
+ function walk(mod, depth = 0) {
2757
+ if (depth > 100) return;
2715
2758
  if (!mod?.url || visited.has(mod.url)) return;
2716
2759
  visited.add(mod.url);
2717
2760
  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);
2761
+ if (mod.importedModules) for (const imported of mod.importedModules) walk(imported, depth + 1);
2719
2762
  }
2720
2763
  const browserMod = await server.moduleGraph.getModuleByUrl(browserEntry);
2721
2764
  if (browserMod) walk(browserMod);
@@ -2749,8 +2792,6 @@ function finesoftFrontViteConfig(options = {}) {
2749
2792
  vite: server,
2750
2793
  isProduction: false,
2751
2794
  ssrEntryPath: "/" + ssrEntry,
2752
- supportedLocales: options.locales,
2753
- defaultLocale: options.defaultLocale,
2754
2795
  parentFetch: app.fetch.bind(app),
2755
2796
  renderModes: options.renderModes
2756
2797
  });
@@ -2767,10 +2808,17 @@ function finesoftFrontViteConfig(options = {}) {
2767
2808
  const path = await dynamicImport("node:path");
2768
2809
  const { pathToFileURL } = await dynamicImport("node:url");
2769
2810
  const { Hono: HonoClass } = await dynamicImport("hono");
2770
- const { parseAcceptLanguage } = await import("./locale-CvU-U6aP.mjs").then((n) => n.t);
2771
2811
  const { getRequestListener } = await dynamicImport("@hono/node-server");
2772
2812
  const app = new HonoClass();
2813
+ const ISR_CACHE_MAX = 1e3;
2773
2814
  const isrCache = /* @__PURE__ */ new Map();
2815
+ function isrSet(key, val) {
2816
+ if (isrCache.size >= ISR_CACHE_MAX) {
2817
+ const first = isrCache.keys().next().value;
2818
+ if (first !== void 0) isrCache.delete(first);
2819
+ }
2820
+ isrCache.set(key, val);
2821
+ }
2774
2822
  if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
2775
2823
  if (typeof options.setup === "function") await options.setup(app);
2776
2824
  else if (typeof options.setup === "string") try {
@@ -2788,23 +2836,20 @@ function finesoftFrontViteConfig(options = {}) {
2788
2836
  if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
2789
2837
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
2790
2838
  try {
2791
- const locale = parseAcceptLanguage(c.req.header("accept-language"), options.locales, options.defaultLocale);
2792
2839
  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);
2840
+ if (overrideMode === "csr") return c.html(injectCSRShell(template));
2841
+ const cached = isrCache.get(url);
2796
2842
  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));
2843
+ const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
2844
+ if (renderMode === "csr") return c.html(injectCSRShell(template));
2799
2845
  const finalHtml = injectSSRContent({
2800
2846
  template,
2801
- locale,
2802
2847
  head,
2803
2848
  css,
2804
2849
  html: appHtml,
2805
2850
  serializedData: ssrModule.serializeServerData(serverData)
2806
2851
  });
2807
- if (renderMode === "prerender" || overrideMode === "prerender") isrCache.set(cacheKey, finalHtml);
2852
+ if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
2808
2853
  return c.html(finalHtml);
2809
2854
  } catch (e) {
2810
2855
  console.error("[SSR Preview Error]", e);
@@ -2851,16 +2896,12 @@ function finesoftFrontViteConfig(options = {}) {
2851
2896
  }
2852
2897
  if (options.adapter) {
2853
2898
  const adapter = resolveAdapter(options.adapter);
2854
- const locales = options.locales ?? ["zh", "en"];
2855
- const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
2856
2899
  const templateHtml = fs.readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
2857
2900
  const ctx = {
2858
2901
  root,
2859
2902
  ssrEntry,
2860
2903
  setupPath: typeof options.setup === "string" ? options.setup : void 0,
2861
2904
  bootstrapEntry: options.bootstrapEntry,
2862
- locales,
2863
- defaultLocale,
2864
2905
  templateHtml,
2865
2906
  renderModes: options.renderModes,
2866
2907
  proxies: options.proxies,