@finesoft/front 0.1.70 → 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 {
@@ -679,6 +710,9 @@ function makeDependencies(container, options = {}) {
679
710
  /**
680
711
  * URL 路由器 — URL pattern → Intent + FlowAction
681
712
  */
713
+ function createNullPrototypeRecord(source) {
714
+ return Object.assign(Object.create(null), source);
715
+ }
682
716
  var Router = class {
683
717
  routes = [];
684
718
  /** 添加路由规则 */
@@ -711,12 +745,11 @@ var Router = class {
711
745
  for (const route of this.routes) {
712
746
  const match = path.match(route.regex);
713
747
  if (match) {
714
- const params = Object.create(null);
748
+ const params = createNullPrototypeRecord(queryParams);
715
749
  route.paramNames.forEach((name, index) => {
716
750
  const value = match[index + 1];
717
751
  if (value) params[name] = value;
718
752
  });
719
- for (const [k, v] of Object.entries(queryParams)) if (!(k in params)) params[k] = v;
720
753
  return {
721
754
  intent: {
722
755
  id: route.intentId,
@@ -738,10 +771,7 @@ var Router = class {
738
771
  parseUrl(url) {
739
772
  try {
740
773
  const parsed = new URL(url, "http://localhost");
741
- const params = Object.create(null);
742
- parsed.searchParams.forEach((v, k) => {
743
- params[k] = v;
744
- });
774
+ const params = createNullPrototypeRecord(Object.fromEntries(parsed.searchParams));
745
775
  return {
746
776
  path: parsed.pathname,
747
777
  queryParams: params
@@ -749,7 +779,7 @@ var Router = class {
749
779
  } catch {
750
780
  return {
751
781
  path: url.split("?")[0].split("#")[0],
752
- queryParams: Object.create(null)
782
+ queryParams: createNullPrototypeRecord()
753
783
  };
754
784
  }
755
785
  }
@@ -783,29 +813,34 @@ async function runAfterLoadGuards(guards, ctx) {
783
813
  const stringifyCache = /* @__PURE__ */ new WeakMap();
784
814
  /** 最大递归深度 */
785
815
  const MAX_DEPTH = 50;
786
- function stableStringify(obj, _seen, _depth) {
816
+ function stableStringify(obj) {
817
+ return stringifyWithContext(obj, /* @__PURE__ */ new Set(), 0);
818
+ }
819
+ function stringifyWithContext(obj, seen, depth) {
787
820
  if (obj === null || obj === void 0) return String(obj);
788
821
  if (typeof obj !== "object") return JSON.stringify(obj);
789
822
  const cached = stringifyCache.get(obj);
790
823
  if (cached !== void 0) return cached;
791
- const depth = _depth ?? 0;
792
824
  if (depth > MAX_DEPTH) return "\"[Max Depth]\"";
793
- const seen = _seen ?? /* @__PURE__ */ new Set();
794
825
  if (seen.has(obj)) return "\"[Circular]\"";
795
826
  seen.add(obj);
796
- let result;
797
- if (Array.isArray(obj)) result = "[" + obj.map((v) => stableStringify(v, seen, depth + 1)).join(",") + "]";
798
- else {
799
- const keys = Object.keys(obj).sort();
800
- const parts = [];
801
- for (const k of keys) {
802
- const v = obj[k];
803
- 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(",") + "}";
804
838
  }
805
- result = "{" + parts.join(",") + "}";
839
+ stringifyCache.set(obj, result);
840
+ return result;
841
+ } finally {
842
+ seen.delete(obj);
806
843
  }
807
- stringifyCache.set(obj, result);
808
- return result;
809
844
  }
810
845
  //#endregion
811
846
  //#region ../core/src/prefetched-intents/prefetched-intents.ts
@@ -1049,7 +1084,7 @@ var HttpClient = class {
1049
1084
  headers
1050
1085
  };
1051
1086
  if (options?.body !== void 0) {
1052
- headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
1087
+ if (!Object.keys(headers).some((k) => k.toLowerCase() === "content-type")) headers["Content-Type"] = "application/json";
1053
1088
  init.body = JSON.stringify(options.body);
1054
1089
  }
1055
1090
  for (const interceptor of this.requestInterceptors) init = await interceptor(url, init);
@@ -1199,11 +1234,10 @@ var LruMap = class {
1199
1234
  this.capacity = capacity;
1200
1235
  }
1201
1236
  get(key) {
1237
+ if (!this.map.has(key)) return void 0;
1202
1238
  const value = this.map.get(key);
1203
- if (value !== void 0) {
1204
- this.map.delete(key);
1205
- this.map.set(key, value);
1206
- }
1239
+ this.map.delete(key);
1240
+ this.map.set(key, value);
1207
1241
  return value;
1208
1242
  }
1209
1243
  set(key, value) {
@@ -1559,9 +1593,7 @@ function tryScroll(log, getScrollableElement, scrollY) {
1559
1593
  });
1560
1594
  mutationObserver.observe(root, {
1561
1595
  childList: true,
1562
- subtree: true,
1563
- characterData: true,
1564
- attributes: true
1596
+ subtree: true
1565
1597
  });
1566
1598
  }
1567
1599
  function cleanup() {
@@ -1681,7 +1713,7 @@ var History = class {
1681
1713
  const newState = update(currentState?.state);
1682
1714
  this.log.info("updateState", newState, this.currentStateId);
1683
1715
  this.entries.set(this.currentStateId, {
1684
- ...currentState,
1716
+ scrollY: currentState?.scrollY ?? 0,
1685
1717
  state: newState
1686
1718
  });
1687
1719
  }
@@ -1848,7 +1880,30 @@ function registerFlowActionHandler(deps) {
1848
1880
  const pagePromise = framework.dispatch(routeMatch.intent);
1849
1881
  await Promise.race([pagePromise, new Promise((r) => setTimeout(r, 500))]).catch(() => {});
1850
1882
  updateApp({
1851
- 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
+ }
1852
1907
  didEnterPage(page);
1853
1908
  return page;
1854
1909
  }),
@@ -1985,7 +2040,13 @@ function getBrowserFetch(fetchFn) {
1985
2040
  * 3. dispatch → Page 数据
1986
2041
  * 4. 调用应用层提供的渲染函数
1987
2042
  */
2043
+ /** SSR 内部 rewrite 最大递归深度,防止 guard 配置错导致无限重路由 */
2044
+ const MAX_SSR_REWRITE_DEPTH = 5;
1988
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}"`);
1989
2050
  const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext, resolveLocale, loadMessages } = options;
1990
2051
  const parsed = new URL(url, "http://localhost");
1991
2052
  const fullPath = parsed.pathname + parsed.search;
@@ -2024,6 +2085,7 @@ async function ssrRender(options) {
2024
2085
  };
2025
2086
  let page;
2026
2087
  let serverData = [];
2088
+ let rewriteUrl;
2027
2089
  if (match) {
2028
2090
  const navCtx = createServerContext({
2029
2091
  url: fullPath,
@@ -2032,6 +2094,10 @@ async function ssrRender(options) {
2032
2094
  request: ssrContext?.request
2033
2095
  });
2034
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);
2035
2101
  if (beforeResult.kind !== "next") {
2036
2102
  const earlyReturn = await handleMiddlewareResult(beforeResult, getErrorPage, renderApp, framework);
2037
2103
  if (earlyReturn) return earlyReturn;
@@ -2043,7 +2109,7 @@ async function ssrRender(options) {
2043
2109
  data: page
2044
2110
  }];
2045
2111
  } catch (e) {
2046
- 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);
2047
2113
  page = getErrorPage(500, "Internal error");
2048
2114
  }
2049
2115
  const postCtx = {
@@ -2051,7 +2117,8 @@ async function ssrRender(options) {
2051
2117
  page
2052
2118
  };
2053
2119
  const afterResult = await framework.runAfterLoad(postCtx, match.afterGuards);
2054
- if (afterResult.kind !== "next") {
2120
+ if (afterResult.kind === "rewrite") rewriteUrl = afterResult.url;
2121
+ else if (afterResult.kind !== "next") {
2055
2122
  const lateReturn = await handleMiddlewareResult(afterResult, getErrorPage, renderApp, framework);
2056
2123
  if (lateReturn) return lateReturn;
2057
2124
  }
@@ -2065,7 +2132,8 @@ async function ssrRender(options) {
2065
2132
  serverData,
2066
2133
  renderMode: match?.renderMode,
2067
2134
  slots: result.slots,
2068
- locale
2135
+ locale,
2136
+ rewriteUrl
2069
2137
  };
2070
2138
  } finally {
2071
2139
  framework.dispose();
@@ -2081,10 +2149,13 @@ function getSSRFetch(fetchFn) {
2081
2149
  /**
2082
2150
  * 将中间件结果转换为 SSRRenderResult(如果需要短路返回)。
2083
2151
  * 返回 null 表示继续正常流程。
2152
+ *
2153
+ * 注意:`rewrite` 不在此处理 — 它由 ssrRenderInternal 直接处理(内部重路由或标记 rewriteUrl)。
2084
2154
  */
2085
2155
  async function handleMiddlewareResult(result, getErrorPage, renderApp, framework) {
2086
2156
  switch (result.kind) {
2087
- case "next": return null;
2157
+ case "next":
2158
+ case "rewrite": return null;
2088
2159
  case "redirect": return {
2089
2160
  html: "",
2090
2161
  head: "",
@@ -2095,16 +2166,6 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
2095
2166
  status: result.status
2096
2167
  }
2097
2168
  };
2098
- case "rewrite": return {
2099
- html: "",
2100
- head: "",
2101
- css: "",
2102
- serverData: [],
2103
- redirect: {
2104
- url: result.url,
2105
- status: 301
2106
- }
2107
- };
2108
2169
  case "deny": {
2109
2170
  const rendered = await renderApp(getErrorPage(result.status, result.message), framework);
2110
2171
  return {
@@ -2112,7 +2173,8 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
2112
2173
  head: rendered.head,
2113
2174
  css: rendered.css,
2114
2175
  serverData: [],
2115
- slots: rendered.slots
2176
+ slots: rendered.slots,
2177
+ status: result.status
2116
2178
  };
2117
2179
  }
2118
2180
  }
@@ -2173,10 +2235,12 @@ function injectCSRShell(template, locale) {
2173
2235
  if (locale) result = applyLocaleToHtml(result, locale);
2174
2236
  return result;
2175
2237
  }
2176
- /** 将 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;
2177
2241
  function applyLocaleToHtml(html, locale) {
2178
2242
  return html.replace(/(<html)([^>]*)(>)/i, (_match, open, attrs, close) => {
2179
- 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}`;
2180
2244
  });
2181
2245
  }
2182
2246
  //#endregion
@@ -2238,6 +2302,10 @@ const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
2238
2302
  /**
2239
2303
  * 校验代理路径,防止 SSRF(协议相对 URL 绕过、编码绕过)。
2240
2304
  * 返回规范化的路径,或 null 表示非法。
2305
+ *
2306
+ * 策略保守:拒绝任何含编码字符的路径,避免上游对 %2F 等解码差异导致绕过。
2307
+ * 副作用:合法的 %20、%E4%B8%AD(Unicode)也会被拒。
2308
+ * 如需放宽,应在上层路由前自行 decode,或为该代理单独提供 sanitizer 选项。
2241
2309
  */
2242
2310
  function sanitizeProxyPath(raw) {
2243
2311
  if (raw.length > MAX_PROXY_PATH_LENGTH) return null;
@@ -2289,8 +2357,8 @@ function registerProxyRoutes(app, configs) {
2289
2357
  });
2290
2358
  const contentLength = resp.headers.get("Content-Length");
2291
2359
  if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
2292
- const body = await resp.text();
2293
- 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);
2294
2362
  const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
2295
2363
  if (config.cache) respHeaders["Cache-Control"] = config.cache;
2296
2364
  return c.newResponse(body, resp.status, respHeaders);
@@ -2339,7 +2407,16 @@ function _sanitizeProxyPath(raw) {
2339
2407
  const _headers = ${headersJson};${authCode}
2340
2408
  try {
2341
2409
  const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect} });
2342
- 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
+ }
2343
2420
  const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
2344
2421
  if (${cacheStr}) _rh["Cache-Control"] = ${cacheStr};
2345
2422
  return c.newResponse(_body, _resp.status, _rh);
@@ -2440,8 +2517,10 @@ function injectSSR(t, head, css, html, data, locale) {
2440
2517
 
2441
2518
  function applyLocaleToHtml(html, locale) {
2442
2519
  if (!locale) return html;
2443
- return html.replace(/<html([^>]*)>/, (_, attrs) => {
2444
- 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, "");
2445
2524
  return "<html" + a + ' lang="' + locale.lang + '" dir="' + locale.dir + '">';
2446
2525
  });
2447
2526
  }
@@ -2620,8 +2699,8 @@ async function prerenderRoutes(ctx) {
2620
2699
  if (locale) {
2621
2700
  const { getLocaleAttributes } = await dynamicImport("@finesoft/core");
2622
2701
  const attrs = getLocaleAttributes(locale);
2623
- finalHtml = finalHtml.replace(/<html([^>]*)>/, (_m, a) => {
2624
- 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}">`;
2625
2704
  });
2626
2705
  }
2627
2706
  results.push({
@@ -3142,16 +3221,7 @@ function matchRenderModeOverride(url, renderModes) {
3142
3221
  function createSSRApp(options) {
3143
3222
  const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes, defaultLocale } = options;
3144
3223
  const app = new Hono();
3145
- /** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
3146
- const ISR_CACHE_MAX = 1e3;
3147
- const isrCache = /* @__PURE__ */ new Map();
3148
- function isrSet(key, val) {
3149
- if (isrCache.size >= ISR_CACHE_MAX) {
3150
- const first = isrCache.keys().next().value;
3151
- if (first !== void 0) isrCache.delete(first);
3152
- }
3153
- isrCache.set(key, val);
3154
- }
3224
+ const isrCache = new LruMap(1e3);
3155
3225
  /** 生产环境模板缓存(模板不变,避免每请求重复读盘) */
3156
3226
  let templateCache;
3157
3227
  async function readTemplate(url) {
@@ -3194,7 +3264,7 @@ function createSSRApp(options) {
3194
3264
  const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
3195
3265
  const ssrContext = { request: c.req.raw };
3196
3266
  if (requestFetch) ssrContext.fetch = requestFetch;
3197
- 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);
3198
3268
  if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
3199
3269
  if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
3200
3270
  const finalHtml = injectSSRContent({
@@ -3206,7 +3276,9 @@ function createSSRApp(options) {
3206
3276
  slots,
3207
3277
  locale
3208
3278
  });
3209
- 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);
3210
3282
  return c.html(finalHtml);
3211
3283
  } catch (e) {
3212
3284
  if (!isProduction && vite) vite.ssrFixStacktrace(e);
@@ -3399,7 +3471,7 @@ function parseAcceptLanguage(header, supported, fallback) {
3399
3471
  lang: lang.trim().toLowerCase(),
3400
3472
  q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
3401
3473
  };
3402
- }).sort((a, b) => b.q - a.q);
3474
+ }).filter((entry) => entry.q > 0).sort((a, b) => b.q - a.q);
3403
3475
  for (const { lang } of langs) {
3404
3476
  const prefix = lang.split("-")[0];
3405
3477
  if (effectiveSupported.includes(prefix)) return prefix;
@@ -3571,15 +3643,7 @@ export async function loadMessages(locale) {
3571
3643
  const { Hono: HonoClass } = await dynamicImport("hono");
3572
3644
  const { getRequestListener } = await dynamicImport("@hono/node-server");
3573
3645
  const app = new HonoClass();
3574
- const ISR_CACHE_MAX = 1e3;
3575
- const isrCache = /* @__PURE__ */ new Map();
3576
- function isrSet(key, val) {
3577
- if (isrCache.size >= ISR_CACHE_MAX) {
3578
- const first = isrCache.keys().next().value;
3579
- if (first !== void 0) isrCache.delete(first);
3580
- }
3581
- isrCache.set(key, val);
3582
- }
3646
+ const isrCache = new LruMap(1e3);
3583
3647
  if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
3584
3648
  if (typeof options.setup === "function") await options.setup(app);
3585
3649
  else if (typeof options.setup === "string") try {
@@ -3601,7 +3665,8 @@ export async function loadMessages(locale) {
3601
3665
  if (overrideMode === "csr") return c.html(injectCSRShell(template, options.defaultLocale ? getLocaleAttributes(options.defaultLocale) : void 0));
3602
3666
  const cached = isrCache.get(url);
3603
3667
  if (cached) return c.html(cached);
3604
- 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);
3605
3670
  if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
3606
3671
  const finalHtml = injectSSRContent({
3607
3672
  template,
@@ -3609,9 +3674,12 @@ export async function loadMessages(locale) {
3609
3674
  css,
3610
3675
  html: appHtml,
3611
3676
  serializedData: ssrModule.serializeServerData(serverData),
3677
+ slots,
3612
3678
  locale
3613
3679
  });
3614
- 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);
3615
3683
  return c.html(finalHtml);
3616
3684
  } catch (e) {
3617
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.70",
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": [
@@ -20,12 +20,12 @@
20
20
  },
21
21
  "dependencies": {},
22
22
  "devDependencies": {
23
- "@hono/node-server": "^1.14.1",
23
+ "@hono/node-server": "^1.19.13",
24
24
  "@types/node": "^22",
25
25
  "dotenv": "^17.3.1",
26
- "hono": "^4.7.10",
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",