@finesoft/front 0.1.37 → 0.1.39
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/README.md +70 -0
- package/dist/app-XEMPAI6H.js +10 -0
- package/dist/browser.cjs +265 -33
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +152 -15
- package/dist/browser.d.ts +152 -15
- package/dist/browser.js +18 -2
- package/dist/{chunk-AX7PXYXU.js → chunk-4PPCVAKZ.js} +144 -31
- package/dist/chunk-4PPCVAKZ.js.map +1 -0
- package/dist/{chunk-SFGR32K6.js → chunk-OYTIGVEG.js} +13 -8
- package/dist/chunk-OYTIGVEG.js.map +1 -0
- package/dist/{chunk-OXKFPW4U.js → chunk-SDPWQT2T.js} +118 -6
- package/dist/chunk-SDPWQT2T.js.map +1 -0
- package/dist/{chunk-AYO3UUQC.js → chunk-XQ3UWZOS.js} +72 -3
- package/dist/chunk-XQ3UWZOS.js.map +1 -0
- package/dist/index.cjs +365 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +24 -8
- package/dist/index.js.map +1 -1
- package/dist/{src-MVACJWBF.js → src-XNC7QNVI.js} +3 -3
- package/package.json +1 -1
- package/dist/app-D4K35MX3.js +0 -10
- package/dist/chunk-AX7PXYXU.js.map +0 -1
- package/dist/chunk-AYO3UUQC.js.map +0 -1
- package/dist/chunk-OXKFPW4U.js.map +0 -1
- package/dist/chunk-SFGR32K6.js.map +0 -1
- /package/dist/{app-D4K35MX3.js.map → app-XEMPAI6H.js.map} +0 -0
- /package/dist/{src-MVACJWBF.js.map → src-XNC7QNVI.js.map} +0 -0
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
Framework,
|
|
5
5
|
LruMap,
|
|
6
6
|
PrefetchedIntents,
|
|
7
|
+
createBrowserContext,
|
|
7
8
|
generateUuid
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-SDPWQT2T.js";
|
|
9
10
|
|
|
10
11
|
// ../browser/src/action-handlers/external-url-action.ts
|
|
11
12
|
function registerExternalUrlHandler(deps) {
|
|
@@ -122,6 +123,22 @@ var History = class {
|
|
|
122
123
|
tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
|
|
123
124
|
});
|
|
124
125
|
}
|
|
126
|
+
/** 仅推入 URL,不缓存页面状态(用于页面加载失败场景) */
|
|
127
|
+
pushUrl(url) {
|
|
128
|
+
const id = generateUuid();
|
|
129
|
+
window.history.pushState({ id }, "", url);
|
|
130
|
+
this.currentStateId = id;
|
|
131
|
+
this.scrollTop = 0;
|
|
132
|
+
this.log.info("pushUrl (no state)", url, id);
|
|
133
|
+
}
|
|
134
|
+
/** 仅替换 URL,不缓存页面状态(用于页面加载失败场景) */
|
|
135
|
+
replaceUrl(url) {
|
|
136
|
+
const id = generateUuid();
|
|
137
|
+
window.history.replaceState({ id }, "", url);
|
|
138
|
+
this.currentStateId = id;
|
|
139
|
+
this.scrollTop = 0;
|
|
140
|
+
this.log.info("replaceUrl (no state)", url, id);
|
|
141
|
+
}
|
|
125
142
|
updateState(update) {
|
|
126
143
|
if (!this.currentStateId) {
|
|
127
144
|
this.log.warn(
|
|
@@ -153,31 +170,49 @@ function registerFlowActionHandler(deps) {
|
|
|
153
170
|
const { framework, log, callbacks, updateApp } = deps;
|
|
154
171
|
let isFirstPage = true;
|
|
155
172
|
let navigationId = 0;
|
|
173
|
+
const MAX_REDIRECTS = 5;
|
|
156
174
|
const defaultGetScrollable = () => document.getElementById("scrollable-page-override") || document.getElementById("scrollable-page") || document.documentElement;
|
|
157
175
|
const history = new History(log, {
|
|
158
176
|
getScrollablePageElement: deps.getScrollablePageElement ?? defaultGetScrollable
|
|
159
177
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const match2 = framework.routeUrl(url);
|
|
166
|
-
if (match2) {
|
|
167
|
-
const page = await framework.dispatch(
|
|
168
|
-
match2.intent
|
|
169
|
-
);
|
|
170
|
-
callbacks.onModal(page);
|
|
171
|
-
}
|
|
178
|
+
async function navigateTo(url, redirectCount, thisNav) {
|
|
179
|
+
if (redirectCount >= MAX_REDIRECTS) {
|
|
180
|
+
log.error(
|
|
181
|
+
`Navigation redirect loop detected (${MAX_REDIRECTS} redirects), stopping at: ${url}`
|
|
182
|
+
);
|
|
172
183
|
return;
|
|
173
184
|
}
|
|
174
|
-
const
|
|
175
|
-
const shouldReplace = isFirstPage;
|
|
185
|
+
const shouldReplace = isFirstPage || url === window.location.pathname + window.location.search;
|
|
176
186
|
const match = framework.routeUrl(url);
|
|
177
187
|
if (!match) {
|
|
178
188
|
log.warn(`FlowAction: no route for ${url}`);
|
|
179
189
|
return;
|
|
180
190
|
}
|
|
191
|
+
const navCtx = createBrowserContext({
|
|
192
|
+
url,
|
|
193
|
+
intent: match.intent,
|
|
194
|
+
container: framework.container
|
|
195
|
+
});
|
|
196
|
+
const beforeResult = await framework.runBeforeLoad(
|
|
197
|
+
navCtx,
|
|
198
|
+
match.beforeGuards
|
|
199
|
+
);
|
|
200
|
+
if (beforeResult.kind === "redirect") {
|
|
201
|
+
log.debug(`beforeLoad \u2192 redirect to ${beforeResult.url}`);
|
|
202
|
+
await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (beforeResult.kind === "deny") {
|
|
206
|
+
log.warn(
|
|
207
|
+
`beforeLoad \u2192 denied (${beforeResult.status}): ${beforeResult.message}`
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (beforeResult.kind === "rewrite") {
|
|
212
|
+
log.debug(`beforeLoad \u2192 rewrite to ${beforeResult.url}`);
|
|
213
|
+
await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
181
216
|
const pagePromise = framework.dispatch(
|
|
182
217
|
match.intent
|
|
183
218
|
);
|
|
@@ -192,26 +227,80 @@ function registerFlowActionHandler(deps) {
|
|
|
192
227
|
}
|
|
193
228
|
history.beforeTransition();
|
|
194
229
|
updateApp({
|
|
195
|
-
page: pagePromise.then(
|
|
196
|
-
|
|
197
|
-
|
|
230
|
+
page: pagePromise.then(
|
|
231
|
+
async (page) => {
|
|
232
|
+
if (thisNav !== navigationId) {
|
|
233
|
+
log.info("FlowAction commit superseded", url);
|
|
234
|
+
return page;
|
|
235
|
+
}
|
|
236
|
+
const postCtx = {
|
|
237
|
+
...navCtx,
|
|
238
|
+
page
|
|
239
|
+
};
|
|
240
|
+
const afterResult = await framework.runAfterLoad(
|
|
241
|
+
postCtx,
|
|
242
|
+
match.afterGuards
|
|
243
|
+
);
|
|
244
|
+
if (afterResult.kind === "redirect") {
|
|
245
|
+
log.debug(`afterLoad \u2192 redirect to ${afterResult.url}`);
|
|
246
|
+
navigateTo(afterResult.url, redirectCount + 1, thisNav);
|
|
247
|
+
return page;
|
|
248
|
+
}
|
|
249
|
+
let canonicalURL = url;
|
|
250
|
+
if (afterResult.kind === "rewrite") {
|
|
251
|
+
canonicalURL = afterResult.url;
|
|
252
|
+
log.debug(`afterLoad \u2192 rewrite URL to ${canonicalURL}`);
|
|
253
|
+
}
|
|
254
|
+
if (afterResult.kind === "deny") {
|
|
255
|
+
log.warn(`afterLoad \u2192 denied (${afterResult.status})`);
|
|
256
|
+
return page;
|
|
257
|
+
}
|
|
258
|
+
if (shouldReplace) {
|
|
259
|
+
history.replaceState({ page }, canonicalURL);
|
|
260
|
+
} else {
|
|
261
|
+
history.pushState({ page }, canonicalURL);
|
|
262
|
+
}
|
|
263
|
+
callbacks.onNavigate(
|
|
264
|
+
new URL(canonicalURL, window.location.origin).pathname
|
|
265
|
+
);
|
|
266
|
+
didEnterPage(page);
|
|
198
267
|
return page;
|
|
268
|
+
},
|
|
269
|
+
(error) => {
|
|
270
|
+
if (thisNav === navigationId) {
|
|
271
|
+
const canonicalURL = url;
|
|
272
|
+
if (shouldReplace) {
|
|
273
|
+
history.replaceUrl(canonicalURL);
|
|
274
|
+
} else {
|
|
275
|
+
history.pushUrl(canonicalURL);
|
|
276
|
+
}
|
|
277
|
+
callbacks.onNavigate(
|
|
278
|
+
new URL(canonicalURL, window.location.origin).pathname
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
199
282
|
}
|
|
200
|
-
|
|
201
|
-
if (shouldReplace) {
|
|
202
|
-
history.replaceState({ page }, canonicalURL);
|
|
203
|
-
} else {
|
|
204
|
-
history.pushState({ page }, canonicalURL);
|
|
205
|
-
}
|
|
206
|
-
callbacks.onNavigate(
|
|
207
|
-
new URL(canonicalURL, window.location.origin).pathname
|
|
208
|
-
);
|
|
209
|
-
didEnterPage(page);
|
|
210
|
-
return page;
|
|
211
|
-
}),
|
|
283
|
+
),
|
|
212
284
|
isFirstPage
|
|
213
285
|
});
|
|
214
286
|
isFirstPage = false;
|
|
287
|
+
}
|
|
288
|
+
framework.onAction(ACTION_KINDS.FLOW, async (action) => {
|
|
289
|
+
const flowAction = action;
|
|
290
|
+
const url = flowAction.url;
|
|
291
|
+
log.debug(`FlowAction \u2192 ${url}`);
|
|
292
|
+
if (flowAction.presentationContext === "modal") {
|
|
293
|
+
const match = framework.routeUrl(url);
|
|
294
|
+
if (match) {
|
|
295
|
+
const page = await framework.dispatch(
|
|
296
|
+
match.intent
|
|
297
|
+
);
|
|
298
|
+
callbacks.onModal(page);
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const thisNav = ++navigationId;
|
|
303
|
+
await navigateTo(url, 0, thisNav);
|
|
215
304
|
});
|
|
216
305
|
history.onPopState(async (url, cachedState) => {
|
|
217
306
|
log.debug(`popstate \u2192 ${url}, cached=${!!cachedState}`);
|
|
@@ -236,6 +325,30 @@ function registerFlowActionHandler(deps) {
|
|
|
236
325
|
});
|
|
237
326
|
return;
|
|
238
327
|
}
|
|
328
|
+
const navCtx = createBrowserContext({
|
|
329
|
+
url: parsed.pathname + parsed.search,
|
|
330
|
+
intent: routeMatch.intent,
|
|
331
|
+
container: framework.container
|
|
332
|
+
});
|
|
333
|
+
const beforeResult = await framework.runBeforeLoad(
|
|
334
|
+
navCtx,
|
|
335
|
+
routeMatch.beforeGuards
|
|
336
|
+
);
|
|
337
|
+
if (beforeResult.kind === "redirect") {
|
|
338
|
+
log.debug(`popstate beforeLoad \u2192 redirect to ${beforeResult.url}`);
|
|
339
|
+
const thisNav = ++navigationId;
|
|
340
|
+
await navigateTo(beforeResult.url, 0, thisNav);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (beforeResult.kind === "deny" || beforeResult.kind === "rewrite") {
|
|
344
|
+
if (beforeResult.kind === "deny") {
|
|
345
|
+
log.warn(`popstate beforeLoad \u2192 denied`);
|
|
346
|
+
} else {
|
|
347
|
+
const thisNav = ++navigationId;
|
|
348
|
+
await navigateTo(beforeResult.url, 0, thisNav);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
239
352
|
const pagePromise = framework.dispatch(
|
|
240
353
|
routeMatch.intent
|
|
241
354
|
);
|
|
@@ -347,4 +460,4 @@ export {
|
|
|
347
460
|
createPrefetchedIntentsFromDom,
|
|
348
461
|
startBrowserApp
|
|
349
462
|
};
|
|
350
|
-
//# sourceMappingURL=chunk-
|
|
463
|
+
//# sourceMappingURL=chunk-4PPCVAKZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../browser/src/action-handlers/external-url-action.ts","../../browser/src/utils/try-scroll.ts","../../browser/src/utils/history.ts","../../browser/src/action-handlers/flow-action.ts","../../browser/src/action-handlers/register.ts","../../browser/src/server-data.ts","../../browser/src/start-app.ts"],"sourcesContent":["/**\n * ExternalUrl Action Handler — 外部链接处理器\n */\n\nimport type { ExternalUrlAction, Framework, Logger } from \"@finesoft/core\";\nimport { ACTION_KINDS } from \"@finesoft/core\";\n\nexport interface ExternalUrlDependencies {\n\tframework: Framework;\n\tlog: Logger;\n}\n\nexport function registerExternalUrlHandler(\n\tdeps: ExternalUrlDependencies,\n): void {\n\tconst { framework, log } = deps;\n\n\tframework.onAction(\n\t\tACTION_KINDS.EXTERNAL_URL,\n\t\t(action: ExternalUrlAction) => {\n\t\t\tlog.debug(`ExternalUrlAction → ${action.url}`);\n\t\t\twindow.open(action.url, \"_blank\", \"noopener,noreferrer\");\n\t\t},\n\t);\n}\n","/**\n * tryScroll — 渐进式滚动位置恢复\n *\n * 使用 rAF 循环等待页面高度足够后恢复滚动位置。\n */\n\nimport type { Logger } from \"@finesoft/core\";\n\nconst MAX_TRIES = 100;\nconst FUDGE = 16;\n\nlet pendingFrame: number | null = null;\n\nexport function tryScroll(\n\tlog: Logger,\n\tgetScrollableElement: () => HTMLElement | null,\n\tscrollY: number,\n): void {\n\tif (pendingFrame !== null) {\n\t\tcancelAnimationFrame(pendingFrame);\n\t\tpendingFrame = null;\n\t}\n\n\tlet tries = 0;\n\n\tpendingFrame = requestAnimationFrame(function attempt() {\n\t\tif (++tries >= MAX_TRIES) {\n\t\t\tlog.warn(\n\t\t\t\t`tryScroll: gave up after ${MAX_TRIES} frames, target=${scrollY}`,\n\t\t\t);\n\t\t\tpendingFrame = null;\n\t\t\treturn;\n\t\t}\n\n\t\tconst el = getScrollableElement();\n\t\tif (!el) {\n\t\t\tlog.warn(\n\t\t\t\t\"could not restore scroll: the scrollable element is missing\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tconst { scrollHeight, offsetHeight } = el;\n\t\tconst canScroll = scrollY + offsetHeight <= scrollHeight + FUDGE;\n\n\t\tif (!canScroll) {\n\t\t\tlog.info(\"page is not tall enough for scroll yet\", {\n\t\t\t\tscrollHeight,\n\t\t\t\toffsetHeight,\n\t\t\t});\n\t\t\tpendingFrame = requestAnimationFrame(attempt);\n\t\t\treturn;\n\t\t}\n\n\t\tel.scrollTop = scrollY;\n\t\tlog.info(\"scroll restored to\", scrollY);\n\t\tpendingFrame = null;\n\t});\n}\n","/**\n * History 管理器 — 浏览器历史状态 + 滚动位置\n */\n\nimport type { Logger } from \"@finesoft/core\";\nimport { LruMap, generateUuid } from \"@finesoft/core\";\nimport { tryScroll } from \"./try-scroll\";\n\nconst HISTORY_SIZE_LIMIT = 10;\n\ninterface HistoryEntry<State> {\n\tstate: State;\n\tscrollY: number;\n}\n\ninterface HistoryOptions {\n\tgetScrollablePageElement: () => HTMLElement | null;\n}\n\nexport class History<State> {\n\tprivate readonly entries: LruMap<string, HistoryEntry<State>>;\n\tprivate readonly log: Logger;\n\tprivate readonly getScrollablePageElement: () => HTMLElement | null;\n\tprivate currentStateId: string | undefined;\n\n\tconstructor(\n\t\tlog: Logger,\n\t\toptions: HistoryOptions,\n\t\tsizeLimit = HISTORY_SIZE_LIMIT,\n\t) {\n\t\tthis.entries = new LruMap(sizeLimit);\n\t\tthis.log = log;\n\t\tthis.getScrollablePageElement = options.getScrollablePageElement;\n\t}\n\n\treplaceState(state: State, url: string): void {\n\t\tconst id = generateUuid();\n\t\twindow.history.replaceState({ id }, \"\", url);\n\t\tthis.currentStateId = id;\n\t\tthis.entries.set(id, { state, scrollY: 0 });\n\t\tthis.scrollTop = 0;\n\t\tthis.log.info(\"replaceState\", state, url, id);\n\t}\n\n\tpushState(state: State, url: string): void {\n\t\tconst id = generateUuid();\n\t\twindow.history.pushState({ id }, \"\", url);\n\t\tthis.currentStateId = id;\n\t\tthis.entries.set(id, { state, scrollY: 0 });\n\t\tthis.scrollTop = 0;\n\t\tthis.log.info(\"pushState\", state, url, id);\n\t}\n\n\tbeforeTransition(): void {\n\t\tconst { state } = window.history;\n\t\tif (!state) return;\n\n\t\tconst oldEntry = this.entries.get(state.id);\n\t\tif (!oldEntry) {\n\t\t\tthis.log.info(\n\t\t\t\t\"current history state evicted from LRU, not saving scroll position\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tconst { scrollTop } = this;\n\t\tthis.entries.set(state.id, { ...oldEntry, scrollY: scrollTop });\n\t\tthis.log.info(\"saving scroll position\", scrollTop);\n\t}\n\n\tonPopState(\n\t\tlistener: (url: string, state?: State) => void | Promise<void>,\n\t): void {\n\t\twindow.addEventListener(\"popstate\", async (event: PopStateEvent) => {\n\t\t\tthis.currentStateId = event.state?.id;\n\n\t\t\tif (!this.currentStateId) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t\"encountered a null event.state.id in onPopState event:\",\n\t\t\t\t\twindow.location.href,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tthis.log.info(\"popstate\", this.entries, this.currentStateId);\n\n\t\t\tconst entry = this.currentStateId\n\t\t\t\t? this.entries.get(this.currentStateId)\n\t\t\t\t: undefined;\n\n\t\t\tawait listener(window.location.href, entry?.state);\n\n\t\t\tif (!entry) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { scrollY } = entry;\n\t\t\tthis.log.info(\"restoring scroll to\", scrollY);\n\t\t\ttryScroll(this.log, () => this.getScrollablePageElement(), scrollY);\n\t\t});\n\t}\n\n\t/** 仅推入 URL,不缓存页面状态(用于页面加载失败场景) */\n\tpushUrl(url: string): void {\n\t\tconst id = generateUuid();\n\t\twindow.history.pushState({ id }, \"\", url);\n\t\tthis.currentStateId = id;\n\t\tthis.scrollTop = 0;\n\t\tthis.log.info(\"pushUrl (no state)\", url, id);\n\t}\n\n\t/** 仅替换 URL,不缓存页面状态(用于页面加载失败场景) */\n\treplaceUrl(url: string): void {\n\t\tconst id = generateUuid();\n\t\twindow.history.replaceState({ id }, \"\", url);\n\t\tthis.currentStateId = id;\n\t\tthis.scrollTop = 0;\n\t\tthis.log.info(\"replaceUrl (no state)\", url, id);\n\t}\n\n\tupdateState(update: (current?: State) => State): void {\n\t\tif (!this.currentStateId) {\n\t\t\tthis.log.warn(\n\t\t\t\t\"failed: encountered a null currentStateId inside updateState\",\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentState = this.entries.get(this.currentStateId);\n\t\tconst newState = update(currentState?.state);\n\t\tthis.log.info(\"updateState\", newState, this.currentStateId);\n\t\tthis.entries.set(this.currentStateId, {\n\t\t\t...(currentState as HistoryEntry<State>),\n\t\t\tstate: newState,\n\t\t});\n\t}\n\n\tprivate get scrollTop(): number {\n\t\treturn this.getScrollablePageElement()?.scrollTop || 0;\n\t}\n\n\tprivate set scrollTop(scrollTop: number) {\n\t\tconst element = this.getScrollablePageElement();\n\t\tif (element) {\n\t\t\telement.scrollTop = scrollTop;\n\t\t}\n\t}\n}\n","/**\n * FlowAction Handler — 核心 SPA 导航处理器\n *\n * 通过 FlowActionCallbacks 注入 UI 更新回调,\n * 不直接依赖任何 UI 框架的 store。\n */\n\nimport type {\n\tBasePage,\n\tFlowAction,\n\tFramework,\n\tLogger,\n\tPostLoadContext,\n} from \"@finesoft/core\";\nimport { ACTION_KINDS, createBrowserContext } from \"@finesoft/core\";\nimport { History } from \"../utils/history\";\n\n/** FlowAction handler 的 History state */\ninterface FlowState {\n\tpage: BasePage;\n}\n\n/** UI 框架回调 — 解耦 Svelte store 等依赖 */\nexport interface FlowActionCallbacks {\n\t/** 导航后更新当前路径(替代 currentPath.set()) */\n\tonNavigate(pathname: string): void;\n\t/** 模态页面展示(替代 openModal()) */\n\tonModal(page: BasePage): void;\n}\n\n/** 注册 FlowAction handler 所需的依赖 */\nexport interface FlowActionDependencies {\n\tframework: Framework;\n\tlog: Logger;\n\tcallbacks: FlowActionCallbacks;\n\t/** 更新应用 UI 的回调,page 可以是 Promise */\n\tupdateApp: (props: {\n\t\tpage: Promise<BasePage> | BasePage;\n\t\tisFirstPage?: boolean;\n\t}) => void;\n\t/** 获取可滚动页面元素,用于滚动位置保存/恢复 */\n\tgetScrollablePageElement?: () => HTMLElement | null;\n}\n\nexport function registerFlowActionHandler(deps: FlowActionDependencies): void {\n\tconst { framework, log, callbacks, updateApp } = deps;\n\tlet isFirstPage = true;\n\tlet navigationId = 0;\n\n\t/** 重定向循环保护计数器 */\n\tconst MAX_REDIRECTS = 5;\n\n\tconst defaultGetScrollable = (): HTMLElement | null =>\n\t\tdocument.getElementById(\"scrollable-page-override\") ||\n\t\tdocument.getElementById(\"scrollable-page\") ||\n\t\tdocument.documentElement;\n\n\tconst history = new History<FlowState>(log, {\n\t\tgetScrollablePageElement:\n\t\t\tdeps.getScrollablePageElement ?? defaultGetScrollable,\n\t});\n\n\t/**\n\t * 核心导航逻辑(支持递归重定向)\n\t * @param redirectCount 当前重定向次数,用于循环保护\n\t */\n\tasync function navigateTo(\n\t\turl: string,\n\t\tredirectCount: number,\n\t\tthisNav: number,\n\t): Promise<void> {\n\t\tif (redirectCount >= MAX_REDIRECTS) {\n\t\t\tlog.error(\n\t\t\t\t`Navigation redirect loop detected (${MAX_REDIRECTS} redirects), stopping at: ${url}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tconst shouldReplace =\n\t\t\tisFirstPage ||\n\t\t\turl === window.location.pathname + window.location.search;\n\n\t\tconst match = framework.routeUrl(url);\n\t\tif (!match) {\n\t\t\tlog.warn(`FlowAction: no route for ${url}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// ===== beforeLoad 中间件 =====\n\t\tconst navCtx = createBrowserContext({\n\t\t\turl,\n\t\t\tintent: match.intent,\n\t\t\tcontainer: framework.container,\n\t\t});\n\t\tconst beforeResult = await framework.runBeforeLoad(\n\t\t\tnavCtx,\n\t\t\tmatch.beforeGuards,\n\t\t);\n\n\t\tif (beforeResult.kind === \"redirect\") {\n\t\t\tlog.debug(`beforeLoad → redirect to ${beforeResult.url}`);\n\t\t\tawait navigateTo(beforeResult.url, redirectCount + 1, thisNav);\n\t\t\treturn;\n\t\t}\n\t\tif (beforeResult.kind === \"deny\") {\n\t\t\tlog.warn(\n\t\t\t\t`beforeLoad → denied (${beforeResult.status}): ${beforeResult.message}`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\t\t// rewrite in beforeLoad: 无数据可言,当作 redirect 处理\n\t\tif (beforeResult.kind === \"rewrite\") {\n\t\t\tlog.debug(`beforeLoad → rewrite to ${beforeResult.url}`);\n\t\t\tawait navigateTo(beforeResult.url, redirectCount + 1, thisNav);\n\t\t\treturn;\n\t\t}\n\n\t\tconst pagePromise = framework.dispatch(\n\t\t\tmatch.intent,\n\t\t) as Promise<BasePage>;\n\n\t\tawait Promise.race([\n\t\t\tpagePromise,\n\t\t\tnew Promise((r) => setTimeout(r, 500)),\n\t\t]).catch(() => {});\n\n\t\tif (thisNav !== navigationId) {\n\t\t\tlog.info(\"FlowAction superseded by newer navigation\", url);\n\t\t\treturn;\n\t\t}\n\n\t\thistory.beforeTransition();\n\n\t\tupdateApp({\n\t\t\tpage: pagePromise.then(\n\t\t\t\tasync (page: BasePage): Promise<BasePage> => {\n\t\t\t\t\tif (thisNav !== navigationId) {\n\t\t\t\t\t\tlog.info(\"FlowAction commit superseded\", url);\n\t\t\t\t\t\treturn page;\n\t\t\t\t\t}\n\n\t\t\t\t\t// ===== afterLoad 中间件 =====\n\t\t\t\t\tconst postCtx: PostLoadContext = {\n\t\t\t\t\t\t...navCtx,\n\t\t\t\t\t\tpage,\n\t\t\t\t\t};\n\t\t\t\t\tconst afterResult = await framework.runAfterLoad(\n\t\t\t\t\t\tpostCtx,\n\t\t\t\t\t\tmatch.afterGuards,\n\t\t\t\t\t);\n\n\t\t\t\t\tif (afterResult.kind === \"redirect\") {\n\t\t\t\t\t\tlog.debug(`afterLoad → redirect to ${afterResult.url}`);\n\t\t\t\t\t\tnavigateTo(afterResult.url, redirectCount + 1, thisNav);\n\t\t\t\t\t\treturn page;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet canonicalURL = url;\n\n\t\t\t\t\tif (afterResult.kind === \"rewrite\") {\n\t\t\t\t\t\t// rewrite: 仅更新地址栏,保留已加载的 page 数据\n\t\t\t\t\t\tcanonicalURL = afterResult.url;\n\t\t\t\t\t\tlog.debug(`afterLoad → rewrite URL to ${canonicalURL}`);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (afterResult.kind === \"deny\") {\n\t\t\t\t\t\tlog.warn(`afterLoad → denied (${afterResult.status})`);\n\t\t\t\t\t\treturn page;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (shouldReplace) {\n\t\t\t\t\t\thistory.replaceState({ page }, canonicalURL);\n\t\t\t\t\t} else {\n\t\t\t\t\t\thistory.pushState({ page }, canonicalURL);\n\t\t\t\t\t}\n\n\t\t\t\t\tcallbacks.onNavigate(\n\t\t\t\t\t\tnew URL(canonicalURL, window.location.origin).pathname,\n\t\t\t\t\t);\n\n\t\t\t\t\tdidEnterPage(page);\n\t\t\t\t\treturn page;\n\t\t\t\t},\n\t\t\t\t(error: unknown) => {\n\t\t\t\t\t// 页面加载失败仍需推入历史记录,确保 back/forward 正常\n\t\t\t\t\tif (thisNav === navigationId) {\n\t\t\t\t\t\tconst canonicalURL = url;\n\t\t\t\t\t\tif (shouldReplace) {\n\t\t\t\t\t\t\thistory.replaceUrl(canonicalURL);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\thistory.pushUrl(canonicalURL);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcallbacks.onNavigate(\n\t\t\t\t\t\t\tnew URL(canonicalURL, window.location.origin)\n\t\t\t\t\t\t\t\t.pathname,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tthrow error;\n\t\t\t\t},\n\t\t\t),\n\t\t\tisFirstPage,\n\t\t});\n\n\t\tisFirstPage = false;\n\t}\n\n\t// ===== FlowAction handler =====\n\tframework.onAction(ACTION_KINDS.FLOW, async (action) => {\n\t\tconst flowAction = action as FlowAction;\n\t\tconst url = flowAction.url;\n\t\tlog.debug(`FlowAction → ${url}`);\n\n\t\t// 模态展示\n\t\tif (flowAction.presentationContext === \"modal\") {\n\t\t\tconst match = framework.routeUrl(url);\n\t\t\tif (match) {\n\t\t\t\tconst page = (await framework.dispatch(\n\t\t\t\t\tmatch.intent,\n\t\t\t\t)) as BasePage;\n\t\t\t\tcallbacks.onModal(page);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst thisNav = ++navigationId;\n\t\tawait navigateTo(url, 0, thisNav);\n\t});\n\n\t// ===== popstate handler =====\n\thistory.onPopState(async (url, cachedState) => {\n\t\tlog.debug(`popstate → ${url}, cached=${!!cachedState}`);\n\n\t\tcallbacks.onNavigate(new URL(url).pathname);\n\n\t\tif (cachedState) {\n\t\t\tconst { page } = cachedState;\n\t\t\tdidEnterPage(page);\n\t\t\tupdateApp({ page, isFirstPage });\n\t\t\treturn;\n\t\t}\n\n\t\tconst parsed = new URL(url);\n\t\tconst routeMatch = framework.routeUrl(parsed.pathname + parsed.search);\n\t\tif (!routeMatch) {\n\t\t\tlog.error(\n\t\t\t\t\"received popstate without data, but URL was unroutable:\",\n\t\t\t\turl,\n\t\t\t);\n\t\t\tdidEnterPage(null);\n\t\t\tupdateApp({\n\t\t\t\tpage: Promise.reject(new Error(\"404\")),\n\t\t\t\tisFirstPage,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// ===== beforeLoad 中间件 =====\n\t\tconst navCtx = createBrowserContext({\n\t\t\turl: parsed.pathname + parsed.search,\n\t\t\tintent: routeMatch.intent,\n\t\t\tcontainer: framework.container,\n\t\t});\n\t\tconst beforeResult = await framework.runBeforeLoad(\n\t\t\tnavCtx,\n\t\t\trouteMatch.beforeGuards,\n\t\t);\n\n\t\tif (beforeResult.kind === \"redirect\") {\n\t\t\tlog.debug(`popstate beforeLoad → redirect to ${beforeResult.url}`);\n\t\t\tconst thisNav = ++navigationId;\n\t\t\tawait navigateTo(beforeResult.url, 0, thisNav);\n\t\t\treturn;\n\t\t}\n\t\tif (beforeResult.kind === \"deny\" || beforeResult.kind === \"rewrite\") {\n\t\t\t// popstate 场景下 deny/rewrite 直接触发跳转\n\t\t\tif (beforeResult.kind === \"deny\") {\n\t\t\t\tlog.warn(`popstate beforeLoad → denied`);\n\t\t\t} else {\n\t\t\t\tconst thisNav = ++navigationId;\n\t\t\t\tawait navigateTo(beforeResult.url, 0, thisNav);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst pagePromise = framework.dispatch(\n\t\t\trouteMatch.intent,\n\t\t) as Promise<BasePage>;\n\n\t\tawait Promise.race([\n\t\t\tpagePromise,\n\t\t\tnew Promise((r) => setTimeout(r, 500)),\n\t\t]).catch(() => {});\n\n\t\tupdateApp({\n\t\t\tpage: pagePromise.then((page: BasePage): BasePage => {\n\t\t\t\tdidEnterPage(page);\n\t\t\t\treturn page;\n\t\t\t}),\n\t\t\tisFirstPage,\n\t\t});\n\t});\n\n\tfunction didEnterPage(page: BasePage | null): void {\n\t\t(async (): Promise<void> => {\n\t\t\ttry {\n\t\t\t\tif (page) {\n\t\t\t\t\tawait framework.didEnterPage(page);\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tlog.error(\"didEnterPage error:\", e);\n\t\t\t}\n\t\t})();\n\t}\n}\n","/**\n * Action Handler 统一注册入口\n */\n\nimport type { BasePage, Framework, Logger } from \"@finesoft/core\";\nimport { registerExternalUrlHandler } from \"./external-url-action\";\nimport {\n\tregisterFlowActionHandler,\n\ttype FlowActionCallbacks,\n} from \"./flow-action\";\n\nexport type { FlowActionCallbacks };\n\nexport interface ActionHandlerDependencies {\n\tframework: Framework;\n\tlog: Logger;\n\tcallbacks: FlowActionCallbacks;\n\tupdateApp: (props: {\n\t\tpage: Promise<BasePage> | BasePage;\n\t\tisFirstPage?: boolean;\n\t}) => void;\n\t/** 获取可滚动页面元素,用于滚动位置保存/恢复 */\n\tgetScrollablePageElement?: () => HTMLElement | null;\n}\n\nexport function registerActionHandlers(deps: ActionHandlerDependencies): void {\n\tconst { framework, log, callbacks, updateApp } = deps;\n\n\tregisterFlowActionHandler({\n\t\tframework,\n\t\tlog,\n\t\tcallbacks,\n\t\tupdateApp,\n\t\tgetScrollablePageElement: deps.getScrollablePageElement,\n\t});\n\n\tregisterExternalUrlHandler({ framework, log });\n}\n","/**\n * Server Data (browser side) — 从 DOM 反序列化服务端嵌入数据\n */\n\nimport { PrefetchedIntents, type PrefetchedIntent } from \"@finesoft/core\";\n\n/** DOM 中嵌入数据的 script 标签 ID */\nexport const SERVER_DATA_ID = \"serialized-server-data\";\n\n/**\n * 从 DOM 反序列化服务端嵌入的数据。\n * 读取 `<script id=\"serialized-server-data\">` 的内容并移除标签。\n */\nexport function deserializeServerData(): PrefetchedIntent[] | undefined {\n\tconst script = document.getElementById(SERVER_DATA_ID);\n\tif (!script?.textContent) return undefined;\n\n\tscript.parentNode?.removeChild(script);\n\n\ttry {\n\t\treturn JSON.parse(script.textContent);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * 从 DOM 提取 SSR 数据并构建 PrefetchedIntents 实例。\n * 替代原来的 PrefetchedIntents.fromDom()。\n */\nexport function createPrefetchedIntentsFromDom(): PrefetchedIntents {\n\tconst data = deserializeServerData();\n\tif (!data || !Array.isArray(data)) {\n\t\treturn PrefetchedIntents.empty();\n\t}\n\treturn PrefetchedIntents.fromArray(data);\n}\n","/**\n * startBrowserApp — 客户端 hydration 一站式启动\n *\n * 封装:\n * 1. PrefetchedIntents 从 DOM 提取\n * 2. Framework 创建 + 引导\n * 3. 应用层挂载(通过 mount 回调,框架无关)\n * 4. Action handlers 注册\n * 5. 初始页面触发\n */\n\nimport type { BasePage, Logger } from \"@finesoft/core\";\nimport { DEP_KEYS, Framework, type LoggerFactory } from \"@finesoft/core\";\nimport {\n\tregisterActionHandlers,\n\ttype FlowActionCallbacks,\n} from \"./action-handlers/register\";\nimport { createPrefetchedIntentsFromDom } from \"./server-data\";\n\nexport interface BrowserAppConfig {\n\t/** 注册 controllers 和路由的引导函数 */\n\tbootstrap: (framework: Framework) => void;\n\n\t/** 默认语言(回退值) */\n\tdefaultLocale?: string;\n\n\t/** DOM 挂载点 ID(默认 \"app\") */\n\tmountId?: string;\n\n\t/** 获取可滚动页面元素,用于滚动位置保存/恢复 */\n\tgetScrollablePageElement?: () => HTMLElement | null;\n\n\t/**\n\t * 挂载应用到 DOM\n\t *\n\t * 框架无关 — Svelte / React / Vue 均可通过此回调实现。\n\t *\n\t * @param target - DOM 挂载点\n\t * @param context - Framework 实例 + 语言\n\t * @returns 更新函数,用于后续页面切换\n\t */\n\tmount: (\n\t\ttarget: HTMLElement,\n\t\tcontext: { framework: Framework; locale: string },\n\t) => (props: {\n\t\tpage: Promise<BasePage> | BasePage;\n\t\tisFirstPage?: boolean;\n\t}) => void;\n\n\t/** FlowAction / ExternalUrl 回调 */\n\tcallbacks: FlowActionCallbacks;\n}\n\n/**\n * 启动客户端应用\n *\n * 自动执行 hydration 全流程。\n */\nexport async function startBrowserApp(config: BrowserAppConfig): Promise<void> {\n\tconst {\n\t\tbootstrap,\n\t\tdefaultLocale = \"en\",\n\t\tmountId = \"app\",\n\t\tmount,\n\t\tcallbacks,\n\t} = config;\n\n\t// 1. 从 DOM 提取 PrefetchedIntents 缓存\n\tconst prefetchedIntents = createPrefetchedIntentsFromDom();\n\n\t// 2. 初始化 Framework + 注册 Controllers\n\tconst framework = Framework.create({ prefetchedIntents });\n\tbootstrap(framework);\n\n\tconst loggerFactory = framework.container.resolve<LoggerFactory>(\n\t\tDEP_KEYS.LOGGER_FACTORY,\n\t);\n\tconst log: Logger = loggerFactory.loggerFor(\"browser\");\n\n\t// 3. 路由初始 URL\n\tconst initialAction = framework.routeUrl(\n\t\twindow.location.pathname + window.location.search,\n\t);\n\n\t// 4. 挂载应用(框架无关)\n\tconst locale = document.documentElement.lang || defaultLocale;\n\tconst target = document.getElementById(mountId)!;\n\tconst updateApp = mount(target, { framework, locale });\n\n\t// 5. 注册 Action Handlers\n\tregisterActionHandlers({\n\t\tframework,\n\t\tlog,\n\t\tcallbacks,\n\t\tupdateApp,\n\t\tgetScrollablePageElement: config.getScrollablePageElement,\n\t});\n\n\t// 6. 触发初始页面\n\tif (initialAction) {\n\t\tawait framework.perform(initialAction.action);\n\t} else {\n\t\tupdateApp({\n\t\t\tpage: Promise.reject(new Error(\"404\")),\n\t\t\tisFirstPage: true,\n\t\t});\n\t}\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
injectCSRShell,
|
|
3
3
|
injectSSRContent
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XQ3UWZOS.js";
|
|
5
5
|
import {
|
|
6
6
|
parseAcceptLanguage
|
|
7
7
|
} from "./chunk-PSPVIVC2.js";
|
|
@@ -141,17 +141,22 @@ function createSSRApp(options) {
|
|
|
141
141
|
const cached = isrCache.get(cacheKey);
|
|
142
142
|
if (cached) return c.html(cached);
|
|
143
143
|
const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
|
|
144
|
+
const ssrContext = { request: c.req.raw };
|
|
145
|
+
if (requestFetch) ssrContext.fetch = requestFetch;
|
|
144
146
|
const {
|
|
145
147
|
html: appHtml,
|
|
146
148
|
head,
|
|
147
149
|
css,
|
|
148
150
|
serverData,
|
|
149
|
-
renderMode
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
renderMode,
|
|
152
|
+
redirect: middlewareRedirect
|
|
153
|
+
} = await render(url, locale, ssrContext);
|
|
154
|
+
if (middlewareRedirect) {
|
|
155
|
+
return c.redirect(
|
|
156
|
+
middlewareRedirect.url,
|
|
157
|
+
middlewareRedirect.status
|
|
158
|
+
);
|
|
159
|
+
}
|
|
155
160
|
if (renderMode === "csr") {
|
|
156
161
|
return c.html(injectCSRShell(template, locale));
|
|
157
162
|
}
|
|
@@ -185,4 +190,4 @@ export {
|
|
|
185
190
|
createInternalFetch,
|
|
186
191
|
createSSRApp
|
|
187
192
|
};
|
|
188
|
-
//# sourceMappingURL=chunk-
|
|
193
|
+
//# sourceMappingURL=chunk-OYTIGVEG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../server/src/app.ts","../../server/src/internal-fetch.ts"],"sourcesContent":["/**\n * createSSRApp — 创建 Hono SSR 应用\n *\n * 提供 SSR 通配路由,读取模板、加载 SSR 模块、渲染。\n * 应用层可在此之上追加自定义路由(API 代理等)。\n */\n\nimport { injectCSRShell, injectSSRContent } from \"@finesoft/ssr\";\nimport { Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport {\n\tcreateInternalFetch,\n\tMAX_SSR_DEPTH,\n\tSSR_DEPTH_HEADER,\n} from \"./internal-fetch\";\nimport { parseAcceptLanguage } from \"./locale\";\n\n/**\n * 匹配 Vite 配置级别的 renderMode 覆盖。\n * 精确路径优先,然后 glob 模式。\n */\nfunction matchRenderModeOverride(\n\turl: string,\n\trenderModes?: Record<string, string>,\n): string | null {\n\tif (!renderModes) return null;\n\tconst path = url.split(\"?\")[0];\n\tif (renderModes[path]) return renderModes[path];\n\tfor (const [pattern, mode] of Object.entries(renderModes)) {\n\t\tif (pattern.includes(\"*\")) {\n\t\t\tconst escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\t\t\tconst re = new RegExp(\"^\" + escaped.replace(/\\*/g, \".*\") + \"$\");\n\t\t\tif (re.test(path)) return mode;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport interface SSRModule {\n\trender: (\n\t\turl: string,\n\t\tlocale: string,\n\t\tssrContext?: {\n\t\t\tfetch?: typeof globalThis.fetch;\n\t\t\trequest?: Request;\n\t\t},\n\t) => Promise<{\n\t\thtml: string;\n\t\thead: string;\n\t\tcss: string;\n\t\tserverData: unknown;\n\t\trenderMode?: string;\n\t\tredirect?: { url: string; status: number };\n\t}>;\n\tserializeServerData: (data: unknown) => string;\n}\n\nexport interface SSRAppOptions {\n\t/** 项目根路径 */\n\troot: string;\n\t/** Vite dev server(仅开发模式) */\n\tvite?: ViteDevServer;\n\t/** 是否生产环境 */\n\tisProduction: boolean;\n\t/** SSR 入口文件路径(开发用,如 \"/src/ssr.ts\") */\n\tssrEntryPath?: string;\n\t/** 生产环境 SSR 模块路径(如 \"../dist/server/ssr.js\") */\n\tssrProductionModule?: string;\n\t/** 支持的语言列表 */\n\tsupportedLocales?: string[];\n\t/** 默认语言 */\n\tdefaultLocale?: string;\n\t/**\n\t * 父级 Hono app 的 fetch 函数,用于 SSR 内部路由回环。\n\t * SSR 渲染时,控制器的 fetch 请求(如 /api/apple/*)会通过此函数\n\t * 直接在进程内路由到代理 handler,避免网络自请求死锁。\n\t */\n\tparentFetch?: (request: Request) => Response | Promise<Response>;\n\t/**\n\t * 按路由覆盖渲染模式(精确路径或 glob 模式)。\n\t * 优先级高于路由级 renderMode。\n\t */\n\trenderModes?: Record<string, string>;\n}\n\nexport function createSSRApp(options: SSRAppOptions): Hono {\n\tconst {\n\t\troot,\n\t\tvite,\n\t\tisProduction,\n\t\tssrEntryPath = \"/src/ssr.ts\",\n\t\tssrProductionModule,\n\t\tsupportedLocales,\n\t\tdefaultLocale,\n\t\tparentFetch,\n\t\trenderModes,\n\t} = options;\n\n\tconst app = new Hono();\n\n\t/** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */\n\tconst ISR_CACHE_MAX = 1000;\n\tconst isrCache = new Map<string, string>();\n\tfunction isrSet(key: string, val: string) {\n\t\tif (isrCache.size >= ISR_CACHE_MAX) {\n\t\t\tconst first = isrCache.keys().next().value;\n\t\t\tif (first !== undefined) isrCache.delete(first);\n\t\t}\n\t\tisrCache.set(key, val);\n\t}\n\n\t/** 生产环境模板缓存(模板不变,避免每请求重复读盘) */\n\tlet templateCache: string | undefined;\n\n\tasync function readTemplate(url: string): Promise<string> {\n\t\tif (!isProduction && vite) {\n\t\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\t\tconst raw = readFileSync(resolve(root, \"index.html\"), \"utf-8\");\n\t\t\treturn vite.transformIndexHtml(url, raw);\n\t\t}\n\n\t\tif (templateCache) return templateCache;\n\n\t\tconst isDeno = typeof (globalThis as any).Deno !== \"undefined\";\n\t\tif (isDeno) {\n\t\t\ttemplateCache = (globalThis as any).Deno.readTextFileSync(\n\t\t\t\tnew URL(\"../dist/client/index.html\", import.meta.url),\n\t\t\t);\n\t\t\treturn templateCache!;\n\t\t}\n\n\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\ttemplateCache = readFileSync(\n\t\t\tresolve(root, \"dist/client/index.html\"),\n\t\t\t\"utf-8\",\n\t\t);\n\t\treturn templateCache!;\n\t}\n\n\tasync function loadSSRModule(): Promise<SSRModule> {\n\t\tif (!isProduction && vite) {\n\t\t\treturn (await vite.ssrLoadModule(ssrEntryPath)) as SSRModule;\n\t\t}\n\t\tif (ssrProductionModule) {\n\t\t\treturn import(\n\t\t\t\t/* @vite-ignore */ ssrProductionModule\n\t\t\t) as Promise<SSRModule>;\n\t\t}\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\tconst { pathToFileURL } = await import(/* @vite-ignore */ \"node:url\");\n\t\tconst absPath = pathToFileURL(resolve(root, \"dist/server/ssr.js\")).href;\n\t\treturn import(/* @vite-ignore */ absPath) as Promise<SSRModule>;\n\t}\n\n\tapp.get(\"*\", async (c) => {\n\t\t// 递归深度保护:从请求头读取 SSR 深度\n\t\tconst ssrDepth = parseInt(c.req.header(SSR_DEPTH_HEADER) ?? \"0\", 10);\n\t\tif (ssrDepth >= MAX_SSR_DEPTH) {\n\t\t\treturn c.text(\"SSR recursion loop detected\", 508);\n\t\t}\n\n\t\tconst url =\n\t\t\tc.req.path +\n\t\t\t(c.req.url.includes(\"?\") ? \"?\" + c.req.url.split(\"?\")[1] : \"\");\n\n\t\ttry {\n\t\t\tconst template = await readTemplate(url);\n\t\t\tconst { render, serializeServerData } = await loadSSRModule();\n\n\t\t\tconst locale = parseAcceptLanguage(\n\t\t\t\tc.req.header(\"accept-language\"),\n\t\t\t\tsupportedLocales,\n\t\t\t\tdefaultLocale,\n\t\t\t);\n\n\t\t\t// Vite 配置级别覆盖:CSR 直接返回空壳\n\t\t\tconst overrideMode = matchRenderModeOverride(url, renderModes);\n\t\t\tif (overrideMode === \"csr\") {\n\t\t\t\treturn c.html(injectCSRShell(template, locale));\n\t\t\t}\n\n\t\t\t// ISR 缓存命中(key 含 locale,避免跨语言缓存污染)\n\t\t\tconst cacheKey = `${locale}:${url}`;\n\t\t\tconst cached = isrCache.get(cacheKey);\n\t\t\tif (cached) return c.html(cached);\n\n\t\t\t// 每请求创建 internalFetch,深度通过请求头传递\n\t\t\tconst requestFetch = parentFetch\n\t\t\t\t? createInternalFetch(parentFetch, ssrDepth + 1)\n\t\t\t\t: undefined;\n\n\t\t\tconst ssrContext: {\n\t\t\t\tfetch?: typeof globalThis.fetch;\n\t\t\t\trequest?: Request;\n\t\t\t} = { request: c.req.raw };\n\t\t\tif (requestFetch) ssrContext.fetch = requestFetch;\n\n\t\t\tconst {\n\t\t\t\thtml: appHtml,\n\t\t\t\thead,\n\t\t\t\tcss,\n\t\t\t\tserverData,\n\t\t\t\trenderMode,\n\t\t\t\tredirect: middlewareRedirect,\n\t\t\t} = await render(url, locale, ssrContext);\n\n\t\t\t// 中间件要求重定向\n\t\t\tif (middlewareRedirect) {\n\t\t\t\treturn c.redirect(\n\t\t\t\t\tmiddlewareRedirect.url,\n\t\t\t\t\tmiddlewareRedirect.status as 301 | 302,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// CSR 模式:返回空壳 HTML\n\t\t\tif (renderMode === \"csr\") {\n\t\t\t\treturn c.html(injectCSRShell(template, locale));\n\t\t\t}\n\n\t\t\tconst serializedData = serializeServerData(serverData);\n\n\t\t\tconst finalHtml = injectSSRContent({\n\t\t\t\ttemplate,\n\t\t\t\tlocale,\n\t\t\t\thead,\n\t\t\t\tcss,\n\t\t\t\thtml: appHtml,\n\t\t\t\tserializedData,\n\t\t\t});\n\n\t\t\t// Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)\n\t\t\tif (renderMode === \"prerender\" || overrideMode === \"prerender\") {\n\t\t\t\tisrSet(cacheKey, finalHtml);\n\t\t\t}\n\n\t\t\treturn c.html(finalHtml);\n\t\t} catch (e) {\n\t\t\tif (!isProduction && vite) {\n\t\t\t\tvite.ssrFixStacktrace(e as Error);\n\t\t\t}\n\t\t\tconsole.error(\"[SSR Error]\", e);\n\t\t\treturn c.text(\"Internal Server Error\", 500);\n\t\t}\n\t});\n\n\treturn app;\n}\n","/**\n * createInternalFetch — SSR 内部路由回环 fetch 包装器\n *\n * 将相对路径(/api/…)请求转为 Hono app.fetch 内存调用,\n * 绝对 URL 和非字符串 input 走 globalThis.fetch(真实网络)。\n *\n * 递归深度保护采用请求头传递:每次 SSR 回环在请求头中写入深度值,\n * SSR catch-all 读取深度判断是否超限。对比闭包计数器方案:\n * - 并发安全:无共享可变状态\n * - 跨渲染准确:深度随请求在 Hono 路由链中传递\n */\n\nexport const SSR_DEPTH_HEADER = \"x-ssr-depth\";\nexport const MAX_SSR_DEPTH = 5;\n\n/**\n * 创建请求级 internal fetch\n *\n * @param appFetch - Hono app.fetch(父级路由)\n * @param depth - 当前 SSR 深度(由 catch-all handler 从请求头读取后 +1 传入)\n */\nexport function createInternalFetch(\n\tappFetch: (request: Request) => Response | Promise<Response>,\n\tdepth: number = 1,\n): typeof globalThis.fetch {\n\treturn ((input: RequestInfo | URL, init?: RequestInit) => {\n\t\t// 只拦截相对路径(以 / 开头),其他一律走真实网络\n\t\tif (typeof input === \"string\" && input.startsWith(\"/\")) {\n\t\t\tconst request = new Request(`http://localhost${input}`, init);\n\t\t\trequest.headers.set(SSR_DEPTH_HEADER, String(depth));\n\t\t\treturn Promise.resolve(appFetch(request));\n\t\t}\n\n\t\t// 绝对 URL / URL 对象 / Request 对象 → 走正常网络\n\t\treturn globalThis.fetch(input, init);\n\t}) as typeof globalThis.fetch;\n}\n"],"mappings":"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","names":["readFileSync","resolve"]}
|
|
@@ -308,7 +308,8 @@ function makeDependencies(container, options = {}) {
|
|
|
308
308
|
var Router = class {
|
|
309
309
|
routes = [];
|
|
310
310
|
/** 添加路由规则 */
|
|
311
|
-
add(pattern, intentId,
|
|
311
|
+
add(pattern, intentId, renderModeOrOptions) {
|
|
312
|
+
const opts = typeof renderModeOrOptions === "string" ? { renderMode: renderModeOrOptions } : renderModeOrOptions ?? {};
|
|
312
313
|
const paramNames = [];
|
|
313
314
|
const regexStr = pattern.split(/(\/:[\w]+\??)/).map((segment) => {
|
|
314
315
|
const paramMatch = segment.match(/^\/:(\w+)(\?)?$/);
|
|
@@ -323,7 +324,9 @@ var Router = class {
|
|
|
323
324
|
intentId,
|
|
324
325
|
regex: new RegExp(`^${regexStr}/?$`),
|
|
325
326
|
paramNames,
|
|
326
|
-
renderMode
|
|
327
|
+
renderMode: opts.renderMode,
|
|
328
|
+
beforeGuards: opts.beforeGuards,
|
|
329
|
+
afterGuards: opts.afterGuards
|
|
327
330
|
});
|
|
328
331
|
return this;
|
|
329
332
|
}
|
|
@@ -345,7 +348,9 @@ var Router = class {
|
|
|
345
348
|
return {
|
|
346
349
|
intent: { id: route.intentId, params },
|
|
347
350
|
action: makeFlowAction(urlOrPath),
|
|
348
|
-
renderMode: route.renderMode
|
|
351
|
+
renderMode: route.renderMode,
|
|
352
|
+
beforeGuards: route.beforeGuards,
|
|
353
|
+
afterGuards: route.afterGuards
|
|
349
354
|
};
|
|
350
355
|
}
|
|
351
356
|
}
|
|
@@ -412,6 +417,22 @@ var CompositeLogger = class {
|
|
|
412
417
|
}
|
|
413
418
|
};
|
|
414
419
|
|
|
420
|
+
// ../core/src/middleware/pipeline.ts
|
|
421
|
+
async function runBeforeLoadGuards(guards, ctx) {
|
|
422
|
+
for (const guard of guards) {
|
|
423
|
+
const result = await guard(ctx);
|
|
424
|
+
if (result.kind !== "next") return result;
|
|
425
|
+
}
|
|
426
|
+
return { kind: "next" };
|
|
427
|
+
}
|
|
428
|
+
async function runAfterLoadGuards(guards, ctx) {
|
|
429
|
+
for (const guard of guards) {
|
|
430
|
+
const result = await guard(ctx);
|
|
431
|
+
if (result.kind !== "next") return result;
|
|
432
|
+
}
|
|
433
|
+
return { kind: "next" };
|
|
434
|
+
}
|
|
435
|
+
|
|
415
436
|
// ../core/src/prefetched-intents/stable-stringify.ts
|
|
416
437
|
function stableStringify(obj, _seen) {
|
|
417
438
|
if (obj === null || obj === void 0) return String(obj);
|
|
@@ -480,6 +501,8 @@ var Framework = class _Framework {
|
|
|
480
501
|
actionDispatcher;
|
|
481
502
|
router;
|
|
482
503
|
prefetchedIntents;
|
|
504
|
+
beforeGuards = [];
|
|
505
|
+
afterGuards = [];
|
|
483
506
|
constructor(container, prefetchedIntents) {
|
|
484
507
|
this.container = container;
|
|
485
508
|
this.intentDispatcher = new IntentDispatcher();
|
|
@@ -543,6 +566,25 @@ var Framework = class _Framework {
|
|
|
543
566
|
registerIntent(controller) {
|
|
544
567
|
this.intentDispatcher.register(controller);
|
|
545
568
|
}
|
|
569
|
+
// ===== Navigation Middleware =====
|
|
570
|
+
/** 注册 beforeLoad 守卫(路由匹配后、数据加载前) */
|
|
571
|
+
beforeLoad(guard) {
|
|
572
|
+
this.beforeGuards.push(guard);
|
|
573
|
+
}
|
|
574
|
+
/** 注册 afterLoad 守卫(数据加载后、渲染前) */
|
|
575
|
+
afterLoad(guard) {
|
|
576
|
+
this.afterGuards.push(guard);
|
|
577
|
+
}
|
|
578
|
+
/** 执行所有 beforeLoad 守卫(全局 → 路由级) */
|
|
579
|
+
runBeforeLoad(ctx, routeGuards) {
|
|
580
|
+
const guards = routeGuards?.length ? [...this.beforeGuards, ...routeGuards] : this.beforeGuards;
|
|
581
|
+
return runBeforeLoadGuards(guards, ctx);
|
|
582
|
+
}
|
|
583
|
+
/** 执行所有 afterLoad 守卫(全局 → 路由级) */
|
|
584
|
+
runAfterLoad(ctx, routeGuards) {
|
|
585
|
+
const guards = routeGuards?.length ? [...this.afterGuards, ...routeGuards] : this.afterGuards;
|
|
586
|
+
return runAfterLoadGuards(guards, ctx);
|
|
587
|
+
}
|
|
546
588
|
/** 销毁 Framework 实例 */
|
|
547
589
|
dispose() {
|
|
548
590
|
this.container.dispose();
|
|
@@ -687,7 +729,11 @@ function defineRoutes(framework, definitions) {
|
|
|
687
729
|
framework.registerIntent(def.controller);
|
|
688
730
|
registeredIntents.add(def.intentId);
|
|
689
731
|
}
|
|
690
|
-
framework.router.add(def.path, def.intentId,
|
|
732
|
+
framework.router.add(def.path, def.intentId, {
|
|
733
|
+
renderMode: def.renderMode,
|
|
734
|
+
beforeGuards: def.beforeLoad,
|
|
735
|
+
afterGuards: def.afterLoad
|
|
736
|
+
});
|
|
691
737
|
}
|
|
692
738
|
}
|
|
693
739
|
|
|
@@ -781,6 +827,64 @@ function generateUuid() {
|
|
|
781
827
|
});
|
|
782
828
|
}
|
|
783
829
|
|
|
830
|
+
// ../core/src/middleware/context.ts
|
|
831
|
+
function parseCookieString(str) {
|
|
832
|
+
const map = /* @__PURE__ */ new Map();
|
|
833
|
+
if (!str) return map;
|
|
834
|
+
for (const pair of str.split(";")) {
|
|
835
|
+
const idx = pair.indexOf("=");
|
|
836
|
+
if (idx === -1) continue;
|
|
837
|
+
const key = pair.slice(0, idx).trim();
|
|
838
|
+
const val = pair.slice(idx + 1).trim();
|
|
839
|
+
if (key) map.set(key, val);
|
|
840
|
+
}
|
|
841
|
+
return map;
|
|
842
|
+
}
|
|
843
|
+
function createServerContext(options) {
|
|
844
|
+
const { url, intent, container, request } = options;
|
|
845
|
+
const parsed = new URL(url, "http://localhost");
|
|
846
|
+
const cookieHeader = request?.headers.get("cookie") ?? "";
|
|
847
|
+
const cookies = parseCookieString(cookieHeader);
|
|
848
|
+
return {
|
|
849
|
+
url,
|
|
850
|
+
path: parsed.pathname,
|
|
851
|
+
params: intent.params ?? {},
|
|
852
|
+
intent,
|
|
853
|
+
isServer: true,
|
|
854
|
+
container,
|
|
855
|
+
getCookie: (name) => cookies.get(name),
|
|
856
|
+
getHeader: (name) => request?.headers.get(name) ?? void 0
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function createBrowserContext(options) {
|
|
860
|
+
const { url, intent, container } = options;
|
|
861
|
+
const parsed = new URL(url, window.location.origin);
|
|
862
|
+
return {
|
|
863
|
+
url,
|
|
864
|
+
path: parsed.pathname,
|
|
865
|
+
params: intent.params ?? {},
|
|
866
|
+
intent,
|
|
867
|
+
isServer: false,
|
|
868
|
+
container,
|
|
869
|
+
getCookie: (name) => parseCookieString(document.cookie).get(name),
|
|
870
|
+
getHeader: () => void 0
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ../core/src/middleware/types.ts
|
|
875
|
+
function next() {
|
|
876
|
+
return { kind: "next" };
|
|
877
|
+
}
|
|
878
|
+
function redirect(url, status = 302) {
|
|
879
|
+
return { kind: "redirect", url, status };
|
|
880
|
+
}
|
|
881
|
+
function rewrite(url) {
|
|
882
|
+
return { kind: "rewrite", url };
|
|
883
|
+
}
|
|
884
|
+
function deny(status = 403, message = "Forbidden") {
|
|
885
|
+
return { kind: "deny", status, message };
|
|
886
|
+
}
|
|
887
|
+
|
|
784
888
|
export {
|
|
785
889
|
ACTION_KINDS,
|
|
786
890
|
isFlowAction,
|
|
@@ -801,6 +905,8 @@ export {
|
|
|
801
905
|
Router,
|
|
802
906
|
CompositeLoggerFactory,
|
|
803
907
|
CompositeLogger,
|
|
908
|
+
runBeforeLoadGuards,
|
|
909
|
+
runAfterLoadGuards,
|
|
804
910
|
stableStringify,
|
|
805
911
|
PrefetchedIntents,
|
|
806
912
|
Framework,
|
|
@@ -819,6 +925,12 @@ export {
|
|
|
819
925
|
removeQueryParams,
|
|
820
926
|
getBaseUrl,
|
|
821
927
|
buildUrl,
|
|
822
|
-
generateUuid
|
|
928
|
+
generateUuid,
|
|
929
|
+
createServerContext,
|
|
930
|
+
createBrowserContext,
|
|
931
|
+
next,
|
|
932
|
+
redirect,
|
|
933
|
+
rewrite,
|
|
934
|
+
deny
|
|
823
935
|
};
|
|
824
|
-
//# sourceMappingURL=chunk-
|
|
936
|
+
//# sourceMappingURL=chunk-SDPWQT2T.js.map
|