@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 +30 -4
- package/dist/index.mjs +151 -83
- package/package.json +4 -4
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 {
|
|
@@ -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 =
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
803
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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":
|
|
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(
|
|
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.
|
|
2293
|
-
if (body.
|
|
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
|
-
|
|
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([^>]*)
|
|
2444
|
-
|
|
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([^>]*)
|
|
2624
|
-
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}">`;
|
|
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
|
-
|
|
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")
|
|
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
|
|
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")
|
|
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.
|
|
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.
|
|
23
|
+
"@hono/node-server": "^1.19.13",
|
|
24
24
|
"@types/node": "^22",
|
|
25
25
|
"dotenv": "^17.3.1",
|
|
26
|
-
"hono": "^4.
|
|
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",
|