@finesoft/front 0.1.71 → 0.1.74

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.d.mts CHANGED
@@ -46,8 +46,16 @@ declare function makeExternalUrlAction(url: string): ExternalUrlAction;
46
46
  type ActionHandler<A extends Action = Action> = (action: A) => Promise<void> | void;
47
47
  declare class ActionDispatcher {
48
48
  private handlers;
49
- /** 注册指定 kind 的 handler(防止重复注册) */
49
+ /**
50
+ * 注册指定 kind 的 handler。
51
+ *
52
+ * 重复 kind 时保留第一个注册者并发出警告——这是有意设计:
53
+ * framework 内部 handler 先注册,应用层意外覆盖会被记录而非静默生效。
54
+ * 如需显式替换,先调用 removeAction(kind)。
55
+ */
50
56
  onAction<A extends Action>(kind: string, handler: ActionHandler<A>): void;
57
+ /** 移除指定 kind 的 handler(用于显式覆盖场景) */
58
+ removeAction(kind: string): boolean;
51
59
  /** 执行一个 Action(CompoundAction 递归展开,有深度限制) */
52
60
  perform(action: Action, _depth?: number): Promise<void>;
53
61
  }
@@ -61,6 +69,7 @@ declare class Container {
61
69
  private registrations;
62
70
  private resolutionStack;
63
71
  private parent?;
72
+ private children;
64
73
  /** 注册依赖(默认单例) */
65
74
  register<T>(key: string, factory: Factory<T>, singleton?: boolean): this;
66
75
  /** 解析依赖 — 当前容器未注册时回退到 parent */
@@ -71,10 +80,17 @@ declare class Container {
71
80
  * 创建子容器(请求级 scope)
72
81
  *
73
82
  * 子容器可覆写父容器的依赖(如每请求的 locale、user),
74
- * 未覆写的 key 自动回退到父容器解析。
83
+ * 未覆写的 key 自动回退到父容器解析。子容器会被父容器跟踪,
84
+ * 父容器 dispose 时一并销毁所有未独立 dispose 的子容器。
75
85
  */
76
86
  createScope(): Container;
77
- /** 销毁容器,清除所有缓存 */
87
+ /**
88
+ * 销毁容器,清除所有缓存。
89
+ *
90
+ * - 递归 dispose 所有 createScope() 创建的未 dispose 子容器
91
+ * - 自身被 dispose 后从父容器移除引用,允许 GC
92
+ * - 重复 dispose 安全(幂等)
93
+ */
78
94
  dispose(): void;
79
95
  }
80
96
  //#endregion
@@ -604,7 +620,7 @@ interface BaseItem {
604
620
  *
605
621
  * 用作缓存 key:相同内容的对象始终产生相同字符串。
606
622
  */
607
- declare function stableStringify(obj: unknown, _seen?: Set<object>, _depth?: number): string;
623
+ declare function stableStringify(obj: unknown): string;
608
624
  //#endregion
609
625
  //#region ../core/src/http/client.d.ts
610
626
  /**
@@ -1288,6 +1304,14 @@ interface SSRRenderResult {
1288
1304
  lang: string;
1289
1305
  dir: string;
1290
1306
  };
1307
+ /** 中间件 deny 时的 HTTP 状态码(服务端应据此设置 response status) */
1308
+ status?: number;
1309
+ /**
1310
+ * afterLoad rewrite 产生的内部 URL。
1311
+ * 此时数据按原 URL 已加载,页面正常渲染——rewriteUrl 仅供 server 层
1312
+ * 用于 canonical link / response header 等场景,**不会触发 HTTP 跳转**。
1313
+ */
1314
+ rewriteUrl?: string;
1291
1315
  }
1292
1316
  declare function ssrRender(options: SSRRenderOptions): Promise<SSRRenderResult>;
1293
1317
  //#endregion
@@ -1543,6 +1567,8 @@ interface SSRModule {
1543
1567
  lang: string;
1544
1568
  dir: string;
1545
1569
  };
1570
+ status?: number; /** afterLoad rewrite 产生的内部 URL,仅供 server 用作 canonical link */
1571
+ rewriteUrl?: string;
1546
1572
  }>;
1547
1573
  serializeServerData: (data: unknown) => string;
1548
1574
  }
package/dist/index.mjs CHANGED
@@ -41,7 +41,13 @@ function makeExternalUrlAction(url) {
41
41
  const MAX_COMPOUND_DEPTH = 32;
42
42
  var ActionDispatcher = class {
43
43
  handlers = /* @__PURE__ */ new Map();
44
- /** 注册指定 kind 的 handler(防止重复注册) */
44
+ /**
45
+ * 注册指定 kind 的 handler。
46
+ *
47
+ * 重复 kind 时保留第一个注册者并发出警告——这是有意设计:
48
+ * framework 内部 handler 先注册,应用层意外覆盖会被记录而非静默生效。
49
+ * 如需显式替换,先调用 removeAction(kind)。
50
+ */
45
51
  onAction(kind, handler) {
46
52
  if (this.handlers.has(kind)) {
47
53
  console.warn(`[ActionDispatcher] kind="${kind}" already registered, skipping`);
@@ -49,6 +55,10 @@ var ActionDispatcher = class {
49
55
  }
50
56
  this.handlers.set(kind, handler);
51
57
  }
58
+ /** 移除指定 kind 的 handler(用于显式覆盖场景) */
59
+ removeAction(kind) {
60
+ return this.handlers.delete(kind);
61
+ }
52
62
  /** 执行一个 Action(CompoundAction 递归展开,有深度限制) */
53
63
  async perform(action, _depth = 0) {
54
64
  if (isCompoundAction(action)) {
@@ -89,6 +99,7 @@ var Container = class Container {
89
99
  registrations = /* @__PURE__ */ new Map();
90
100
  resolutionStack = /* @__PURE__ */ new Set();
91
101
  parent;
102
+ children = /* @__PURE__ */ new Set();
92
103
  /** 注册依赖(默认单例) */
93
104
  register(key, factory, singleton = true) {
94
105
  this.registrations.set(key, {
@@ -126,17 +137,32 @@ var Container = class Container {
126
137
  * 创建子容器(请求级 scope)
127
138
  *
128
139
  * 子容器可覆写父容器的依赖(如每请求的 locale、user),
129
- * 未覆写的 key 自动回退到父容器解析。
140
+ * 未覆写的 key 自动回退到父容器解析。子容器会被父容器跟踪,
141
+ * 父容器 dispose 时一并销毁所有未独立 dispose 的子容器。
130
142
  */
131
143
  createScope() {
132
144
  const child = new Container();
133
145
  child.parent = this;
146
+ this.children.add(child);
134
147
  return child;
135
148
  }
136
- /** 销毁容器,清除所有缓存 */
149
+ /**
150
+ * 销毁容器,清除所有缓存。
151
+ *
152
+ * - 递归 dispose 所有 createScope() 创建的未 dispose 子容器
153
+ * - 自身被 dispose 后从父容器移除引用,允许 GC
154
+ * - 重复 dispose 安全(幂等)
155
+ */
137
156
  dispose() {
157
+ const childSnapshot = Array.from(this.children);
158
+ for (const child of childSnapshot) child.dispose();
159
+ this.children.clear();
138
160
  for (const reg of this.registrations.values()) reg.instance = void 0;
139
161
  this.registrations.clear();
162
+ if (this.parent) {
163
+ this.parent.children.delete(this);
164
+ this.parent = void 0;
165
+ }
140
166
  }
141
167
  };
142
168
  //#endregion
@@ -505,7 +531,12 @@ var ReportingLogger = class extends BaseLogger {
505
531
  return "";
506
532
  }
507
533
  maybeReport(level, args) {
508
- if (LEVEL_PRIORITY[level] >= this.minPriority) this.report(level, this.category, args);
534
+ if (LEVEL_PRIORITY[level] < this.minPriority) return;
535
+ try {
536
+ this.report(level, this.category, args);
537
+ } catch (e) {
538
+ console.error("[ReportingLogger] report callback threw:", e);
539
+ }
509
540
  }
510
541
  };
511
542
  var ReportingLoggerFactory = class {
@@ -782,29 +813,34 @@ async function runAfterLoadGuards(guards, ctx) {
782
813
  const stringifyCache = /* @__PURE__ */ new WeakMap();
783
814
  /** 最大递归深度 */
784
815
  const MAX_DEPTH = 50;
785
- function stableStringify(obj, _seen, _depth) {
816
+ function stableStringify(obj) {
817
+ return stringifyWithContext(obj, /* @__PURE__ */ new Set(), 0);
818
+ }
819
+ function stringifyWithContext(obj, seen, depth) {
786
820
  if (obj === null || obj === void 0) return String(obj);
787
821
  if (typeof obj !== "object") return JSON.stringify(obj);
788
822
  const cached = stringifyCache.get(obj);
789
823
  if (cached !== void 0) return cached;
790
- const depth = _depth ?? 0;
791
824
  if (depth > MAX_DEPTH) return "\"[Max Depth]\"";
792
- const seen = _seen ?? /* @__PURE__ */ new Set();
793
825
  if (seen.has(obj)) return "\"[Circular]\"";
794
826
  seen.add(obj);
795
- let result;
796
- if (Array.isArray(obj)) result = "[" + obj.map((v) => stableStringify(v, seen, depth + 1)).join(",") + "]";
797
- else {
798
- const keys = Object.keys(obj).sort();
799
- const parts = [];
800
- for (const k of keys) {
801
- const v = obj[k];
802
- if (v !== void 0) parts.push(JSON.stringify(k) + ":" + stableStringify(v, seen, depth + 1));
827
+ try {
828
+ let result;
829
+ if (Array.isArray(obj)) result = "[" + obj.map((v) => stringifyWithContext(v, seen, depth + 1)).join(",") + "]";
830
+ else {
831
+ const keys = Object.keys(obj).sort();
832
+ const parts = [];
833
+ for (const k of keys) {
834
+ const v = obj[k];
835
+ if (v !== void 0) parts.push(JSON.stringify(k) + ":" + stringifyWithContext(v, seen, depth + 1));
836
+ }
837
+ result = "{" + parts.join(",") + "}";
803
838
  }
804
- result = "{" + parts.join(",") + "}";
839
+ stringifyCache.set(obj, result);
840
+ return result;
841
+ } finally {
842
+ seen.delete(obj);
805
843
  }
806
- stringifyCache.set(obj, result);
807
- return result;
808
844
  }
809
845
  //#endregion
810
846
  //#region ../core/src/prefetched-intents/prefetched-intents.ts
@@ -1048,7 +1084,7 @@ var HttpClient = class {
1048
1084
  headers
1049
1085
  };
1050
1086
  if (options?.body !== void 0) {
1051
- headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
1087
+ if (!Object.keys(headers).some((k) => k.toLowerCase() === "content-type")) headers["Content-Type"] = "application/json";
1052
1088
  init.body = JSON.stringify(options.body);
1053
1089
  }
1054
1090
  for (const interceptor of this.requestInterceptors) init = await interceptor(url, init);
@@ -1198,11 +1234,10 @@ var LruMap = class {
1198
1234
  this.capacity = capacity;
1199
1235
  }
1200
1236
  get(key) {
1237
+ if (!this.map.has(key)) return void 0;
1201
1238
  const value = this.map.get(key);
1202
- if (value !== void 0) {
1203
- this.map.delete(key);
1204
- this.map.set(key, value);
1205
- }
1239
+ this.map.delete(key);
1240
+ this.map.set(key, value);
1206
1241
  return value;
1207
1242
  }
1208
1243
  set(key, value) {
@@ -1558,9 +1593,7 @@ function tryScroll(log, getScrollableElement, scrollY) {
1558
1593
  });
1559
1594
  mutationObserver.observe(root, {
1560
1595
  childList: true,
1561
- subtree: true,
1562
- characterData: true,
1563
- attributes: true
1596
+ subtree: true
1564
1597
  });
1565
1598
  }
1566
1599
  function cleanup() {
@@ -1680,7 +1713,7 @@ var History = class {
1680
1713
  const newState = update(currentState?.state);
1681
1714
  this.log.info("updateState", newState, this.currentStateId);
1682
1715
  this.entries.set(this.currentStateId, {
1683
- ...currentState,
1716
+ scrollY: currentState?.scrollY ?? 0,
1684
1717
  state: newState
1685
1718
  });
1686
1719
  }
@@ -1847,7 +1880,30 @@ function registerFlowActionHandler(deps) {
1847
1880
  const pagePromise = framework.dispatch(routeMatch.intent);
1848
1881
  await Promise.race([pagePromise, new Promise((r) => setTimeout(r, 500))]).catch(() => {});
1849
1882
  updateApp({
1850
- page: pagePromise.then((page) => {
1883
+ page: pagePromise.then(async (page) => {
1884
+ const postCtx = {
1885
+ ...navCtx,
1886
+ page
1887
+ };
1888
+ const afterResult = await framework.runAfterLoad(postCtx, routeMatch.afterGuards);
1889
+ if (afterResult.kind === "redirect") {
1890
+ log.debug(`popstate afterLoad → redirect to ${afterResult.url}`);
1891
+ const newNav = ++navigationId;
1892
+ navigateTo(afterResult.url, 0, newNav);
1893
+ return page;
1894
+ }
1895
+ if (afterResult.kind === "rewrite") {
1896
+ log.debug(`popstate afterLoad → rewrite URL to ${afterResult.url}`);
1897
+ const stateId = window.history.state?.id;
1898
+ window.history.replaceState({ id: stateId }, "", afterResult.url);
1899
+ callbacks.onNavigate(new URL(afterResult.url, window.location.origin).pathname);
1900
+ didEnterPage(page);
1901
+ return page;
1902
+ }
1903
+ if (afterResult.kind === "deny") {
1904
+ log.warn(`popstate afterLoad → denied (${afterResult.status})`);
1905
+ return page;
1906
+ }
1851
1907
  didEnterPage(page);
1852
1908
  return page;
1853
1909
  }),
@@ -1984,7 +2040,13 @@ function getBrowserFetch(fetchFn) {
1984
2040
  * 3. dispatch → Page 数据
1985
2041
  * 4. 调用应用层提供的渲染函数
1986
2042
  */
2043
+ /** SSR 内部 rewrite 最大递归深度,防止 guard 配置错导致无限重路由 */
2044
+ const MAX_SSR_REWRITE_DEPTH = 5;
1987
2045
  async function ssrRender(options) {
2046
+ return ssrRenderInternal(options, 0);
2047
+ }
2048
+ async function ssrRenderInternal(options, rewriteDepth) {
2049
+ if (rewriteDepth >= MAX_SSR_REWRITE_DEPTH) throw new Error(`[SSR] Rewrite recursion depth exceeded (max ${MAX_SSR_REWRITE_DEPTH}) at "${options.url}"`);
1988
2050
  const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext, resolveLocale, loadMessages } = options;
1989
2051
  const parsed = new URL(url, "http://localhost");
1990
2052
  const fullPath = parsed.pathname + parsed.search;
@@ -2023,6 +2085,7 @@ async function ssrRender(options) {
2023
2085
  };
2024
2086
  let page;
2025
2087
  let serverData = [];
2088
+ let rewriteUrl;
2026
2089
  if (match) {
2027
2090
  const navCtx = createServerContext({
2028
2091
  url: fullPath,
@@ -2031,6 +2094,10 @@ async function ssrRender(options) {
2031
2094
  request: ssrContext?.request
2032
2095
  });
2033
2096
  const beforeResult = await framework.runBeforeLoad(navCtx, match.beforeGuards);
2097
+ if (beforeResult.kind === "rewrite") return ssrRenderInternal({
2098
+ ...options,
2099
+ url: beforeResult.url
2100
+ }, rewriteDepth + 1);
2034
2101
  if (beforeResult.kind !== "next") {
2035
2102
  const earlyReturn = await handleMiddlewareResult(beforeResult, getErrorPage, renderApp, framework);
2036
2103
  if (earlyReturn) return earlyReturn;
@@ -2042,7 +2109,7 @@ async function ssrRender(options) {
2042
2109
  data: page
2043
2110
  }];
2044
2111
  } catch (e) {
2045
- console.error(`[SSR] dispatch failed for intent "${match.intent.id}":`, e);
2112
+ framework.container.resolve(DEP_KEYS.LOGGER).error(`[SSR] dispatch failed for intent "${match.intent.id}":`, e);
2046
2113
  page = getErrorPage(500, "Internal error");
2047
2114
  }
2048
2115
  const postCtx = {
@@ -2050,7 +2117,8 @@ async function ssrRender(options) {
2050
2117
  page
2051
2118
  };
2052
2119
  const afterResult = await framework.runAfterLoad(postCtx, match.afterGuards);
2053
- if (afterResult.kind !== "next") {
2120
+ if (afterResult.kind === "rewrite") rewriteUrl = afterResult.url;
2121
+ else if (afterResult.kind !== "next") {
2054
2122
  const lateReturn = await handleMiddlewareResult(afterResult, getErrorPage, renderApp, framework);
2055
2123
  if (lateReturn) return lateReturn;
2056
2124
  }
@@ -2064,7 +2132,8 @@ async function ssrRender(options) {
2064
2132
  serverData,
2065
2133
  renderMode: match?.renderMode,
2066
2134
  slots: result.slots,
2067
- locale
2135
+ locale,
2136
+ rewriteUrl
2068
2137
  };
2069
2138
  } finally {
2070
2139
  framework.dispose();
@@ -2080,10 +2149,13 @@ function getSSRFetch(fetchFn) {
2080
2149
  /**
2081
2150
  * 将中间件结果转换为 SSRRenderResult(如果需要短路返回)。
2082
2151
  * 返回 null 表示继续正常流程。
2152
+ *
2153
+ * 注意:`rewrite` 不在此处理 — 它由 ssrRenderInternal 直接处理(内部重路由或标记 rewriteUrl)。
2083
2154
  */
2084
2155
  async function handleMiddlewareResult(result, getErrorPage, renderApp, framework) {
2085
2156
  switch (result.kind) {
2086
- case "next": return null;
2157
+ case "next":
2158
+ case "rewrite": return null;
2087
2159
  case "redirect": return {
2088
2160
  html: "",
2089
2161
  head: "",
@@ -2094,16 +2166,6 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
2094
2166
  status: result.status
2095
2167
  }
2096
2168
  };
2097
- case "rewrite": return {
2098
- html: "",
2099
- head: "",
2100
- css: "",
2101
- serverData: [],
2102
- redirect: {
2103
- url: result.url,
2104
- status: 301
2105
- }
2106
- };
2107
2169
  case "deny": {
2108
2170
  const rendered = await renderApp(getErrorPage(result.status, result.message), framework);
2109
2171
  return {
@@ -2111,7 +2173,8 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
2111
2173
  head: rendered.head,
2112
2174
  css: rendered.css,
2113
2175
  serverData: [],
2114
- slots: rendered.slots
2176
+ slots: rendered.slots,
2177
+ status: result.status
2115
2178
  };
2116
2179
  }
2117
2180
  }
@@ -2172,10 +2235,12 @@ function injectCSRShell(template, locale) {
2172
2235
  if (locale) result = applyLocaleToHtml(result, locale);
2173
2236
  return result;
2174
2237
  }
2175
- /** 将 lang/dir 注入到 <html> 标签 */
2238
+ /** 将 lang/dir 注入到 <html> 标签(支持双引号 / 单引号 / 无引号属性值) */
2239
+ const HTML_LANG_PATTERN = /\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi;
2240
+ const HTML_DIR_PATTERN = /\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi;
2176
2241
  function applyLocaleToHtml(html, locale) {
2177
2242
  return html.replace(/(<html)([^>]*)(>)/i, (_match, open, attrs, close) => {
2178
- return `${open}${attrs.replace(/\s+lang="[^"]*"/gi, "").replace(/\s+dir="[^"]*"/gi, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
2243
+ return `${open}${attrs.replace(HTML_LANG_PATTERN, "").replace(HTML_DIR_PATTERN, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
2179
2244
  });
2180
2245
  }
2181
2246
  //#endregion
@@ -2237,6 +2302,10 @@ const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
2237
2302
  /**
2238
2303
  * 校验代理路径,防止 SSRF(协议相对 URL 绕过、编码绕过)。
2239
2304
  * 返回规范化的路径,或 null 表示非法。
2305
+ *
2306
+ * 策略保守:拒绝任何含编码字符的路径,避免上游对 %2F 等解码差异导致绕过。
2307
+ * 副作用:合法的 %20、%E4%B8%AD(Unicode)也会被拒。
2308
+ * 如需放宽,应在上层路由前自行 decode,或为该代理单独提供 sanitizer 选项。
2240
2309
  */
2241
2310
  function sanitizeProxyPath(raw) {
2242
2311
  if (raw.length > MAX_PROXY_PATH_LENGTH) return null;
@@ -2288,8 +2357,8 @@ function registerProxyRoutes(app, configs) {
2288
2357
  });
2289
2358
  const contentLength = resp.headers.get("Content-Length");
2290
2359
  if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
2291
- const body = await resp.text();
2292
- if (body.length > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
2360
+ const body = await resp.arrayBuffer();
2361
+ if (body.byteLength > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
2293
2362
  const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
2294
2363
  if (config.cache) respHeaders["Cache-Control"] = config.cache;
2295
2364
  return c.newResponse(body, resp.status, respHeaders);
@@ -2338,7 +2407,16 @@ function _sanitizeProxyPath(raw) {
2338
2407
  const _headers = ${headersJson};${authCode}
2339
2408
  try {
2340
2409
  const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect} });
2341
- const _body = await _resp.text();
2410
+ // Content-Length 快速拒绝,防止 serverless/edge 加载超大响应到内存
2411
+ const _cl = _resp.headers.get("Content-Length");
2412
+ if (_cl && parseInt(_cl, 10) > ${MAX_RESPONSE_SIZE}) {
2413
+ return c.text("Proxy response too large", 502);
2414
+ }
2415
+ // arrayBuffer 保留二进制完整性
2416
+ const _body = await _resp.arrayBuffer();
2417
+ if (_body.byteLength > ${MAX_RESPONSE_SIZE}) {
2418
+ return c.text("Proxy response too large", 502);
2419
+ }
2342
2420
  const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
2343
2421
  if (${cacheStr}) _rh["Cache-Control"] = ${cacheStr};
2344
2422
  return c.newResponse(_body, _resp.status, _rh);
@@ -2439,8 +2517,10 @@ function injectSSR(t, head, css, html, data, locale) {
2439
2517
 
2440
2518
  function applyLocaleToHtml(html, locale) {
2441
2519
  if (!locale) return html;
2442
- return html.replace(/<html([^>]*)>/, (_, attrs) => {
2443
- let a = attrs.replace(/s*lang="[^"]*"/g, "").replace(/s*dir="[^"]*"/g, "");
2520
+ return html.replace(/<html([^>]*)>/i, (_, attrs) => {
2521
+ const a = attrs
2522
+ .replace(/\\s+lang=("[^"]*"|'[^']*'|[^\\s>]+)/gi, "")
2523
+ .replace(/\\s+dir=("[^"]*"|'[^']*'|[^\\s>]+)/gi, "");
2444
2524
  return "<html" + a + ' lang="' + locale.lang + '" dir="' + locale.dir + '">';
2445
2525
  });
2446
2526
  }
@@ -2619,8 +2699,8 @@ async function prerenderRoutes(ctx) {
2619
2699
  if (locale) {
2620
2700
  const { getLocaleAttributes } = await dynamicImport("@finesoft/core");
2621
2701
  const attrs = getLocaleAttributes(locale);
2622
- finalHtml = finalHtml.replace(/<html([^>]*)>/, (_m, a) => {
2623
- return `<html${a.replace(/\s*lang="[^"]*"/g, "").replace(/\s*dir="[^"]*"/g, "")} lang="${attrs.lang}" dir="${attrs.dir}">`;
2702
+ finalHtml = finalHtml.replace(/<html([^>]*)>/i, (_m, a) => {
2703
+ return `<html${a.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi, "")} lang="${attrs.lang}" dir="${attrs.dir}">`;
2624
2704
  });
2625
2705
  }
2626
2706
  results.push({
@@ -3141,16 +3221,7 @@ function matchRenderModeOverride(url, renderModes) {
3141
3221
  function createSSRApp(options) {
3142
3222
  const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes, defaultLocale } = options;
3143
3223
  const app = new Hono();
3144
- /** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
3145
- const ISR_CACHE_MAX = 1e3;
3146
- const isrCache = /* @__PURE__ */ new Map();
3147
- function isrSet(key, val) {
3148
- if (isrCache.size >= ISR_CACHE_MAX) {
3149
- const first = isrCache.keys().next().value;
3150
- if (first !== void 0) isrCache.delete(first);
3151
- }
3152
- isrCache.set(key, val);
3153
- }
3224
+ const isrCache = new LruMap(1e3);
3154
3225
  /** 生产环境模板缓存(模板不变,避免每请求重复读盘) */
3155
3226
  let templateCache;
3156
3227
  async function readTemplate(url) {
@@ -3193,7 +3264,7 @@ function createSSRApp(options) {
3193
3264
  const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
3194
3265
  const ssrContext = { request: c.req.raw };
3195
3266
  if (requestFetch) ssrContext.fetch = requestFetch;
3196
- const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots, locale } = await render(url, ssrContext);
3267
+ const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots, locale, status, rewriteUrl } = await render(url, ssrContext);
3197
3268
  if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
3198
3269
  if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
3199
3270
  const finalHtml = injectSSRContent({
@@ -3205,7 +3276,9 @@ function createSSRApp(options) {
3205
3276
  slots,
3206
3277
  locale
3207
3278
  });
3208
- if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
3279
+ if ((renderMode === "prerender" || overrideMode === "prerender") && !status && !rewriteUrl) isrCache.set(url, finalHtml);
3280
+ if (rewriteUrl) c.header("Content-Location", rewriteUrl);
3281
+ if (status && status >= 400) return c.html(finalHtml, status);
3209
3282
  return c.html(finalHtml);
3210
3283
  } catch (e) {
3211
3284
  if (!isProduction && vite) vite.ssrFixStacktrace(e);
@@ -3398,7 +3471,7 @@ function parseAcceptLanguage(header, supported, fallback) {
3398
3471
  lang: lang.trim().toLowerCase(),
3399
3472
  q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
3400
3473
  };
3401
- }).sort((a, b) => b.q - a.q);
3474
+ }).filter((entry) => entry.q > 0).sort((a, b) => b.q - a.q);
3402
3475
  for (const { lang } of langs) {
3403
3476
  const prefix = lang.split("-")[0];
3404
3477
  if (effectiveSupported.includes(prefix)) return prefix;
@@ -3570,15 +3643,7 @@ export async function loadMessages(locale) {
3570
3643
  const { Hono: HonoClass } = await dynamicImport("hono");
3571
3644
  const { getRequestListener } = await dynamicImport("@hono/node-server");
3572
3645
  const app = new HonoClass();
3573
- const ISR_CACHE_MAX = 1e3;
3574
- const isrCache = /* @__PURE__ */ new Map();
3575
- function isrSet(key, val) {
3576
- if (isrCache.size >= ISR_CACHE_MAX) {
3577
- const first = isrCache.keys().next().value;
3578
- if (first !== void 0) isrCache.delete(first);
3579
- }
3580
- isrCache.set(key, val);
3581
- }
3646
+ const isrCache = new LruMap(1e3);
3582
3647
  if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
3583
3648
  if (typeof options.setup === "function") await options.setup(app);
3584
3649
  else if (typeof options.setup === "string") try {
@@ -3600,7 +3665,8 @@ export async function loadMessages(locale) {
3600
3665
  if (overrideMode === "csr") return c.html(injectCSRShell(template, options.defaultLocale ? getLocaleAttributes(options.defaultLocale) : void 0));
3601
3666
  const cached = isrCache.get(url);
3602
3667
  if (cached) return c.html(cached);
3603
- const { html: appHtml, head, css, serverData, renderMode, locale } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
3668
+ const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots, locale, status, rewriteUrl } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
3669
+ if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
3604
3670
  if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
3605
3671
  const finalHtml = injectSSRContent({
3606
3672
  template,
@@ -3608,9 +3674,12 @@ export async function loadMessages(locale) {
3608
3674
  css,
3609
3675
  html: appHtml,
3610
3676
  serializedData: ssrModule.serializeServerData(serverData),
3677
+ slots,
3611
3678
  locale
3612
3679
  });
3613
- if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
3680
+ if ((renderMode === "prerender" || overrideMode === "prerender") && !status && !rewriteUrl) isrCache.set(url, finalHtml);
3681
+ if (rewriteUrl) c.header("Content-Location", rewriteUrl);
3682
+ if (status && status >= 400) return c.html(finalHtml, status);
3614
3683
  return c.html(finalHtml);
3615
3684
  } catch (e) {
3616
3685
  console.error("[SSR Preview Error]", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finesoft/front",
3
- "version": "0.1.71",
3
+ "version": "0.1.74",
4
4
  "description": "Full-stack framework: router, DI, actions, SSR, and server — all in one package",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -25,7 +25,7 @@
25
25
  "dotenv": "^17.3.1",
26
26
  "hono": "^4.12.12",
27
27
  "vite": "npm:@voidzero-dev/vite-plus-core@0.1.16",
28
- "vite-plus": "0.1.16"
28
+ "vite-plus": "0.1.17"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@hono/node-server": "^1.0.0",