@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 +30 -4
- package/dist/index.mjs +145 -76
- package/package.json +2 -2
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
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
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]
|
|
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
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
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(
|
|
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.
|
|
2292
|
-
if (body.
|
|
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
|
-
|
|
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([^>]*)
|
|
2443
|
-
|
|
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([^>]*)
|
|
2623
|
-
return `<html${a.replace(/\s
|
|
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
|
-
|
|
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")
|
|
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
|
|
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")
|
|
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.
|
|
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.
|
|
28
|
+
"vite-plus": "0.1.17"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@hono/node-server": "^1.0.0",
|