@finesoft/front 0.1.13 → 0.1.15
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/app-MYBG3TGV.js +10 -0
- package/dist/app-MYBG3TGV.js.map +1 -0
- package/dist/browser.js +12 -10
- package/dist/chunk-2VETWTN5.js +88 -0
- package/dist/chunk-2VETWTN5.js.map +1 -0
- package/dist/chunk-FYP2ZYYV.js +25 -0
- package/dist/chunk-FYP2ZYYV.js.map +1 -0
- package/dist/{chunk-OVGQ4NUA.js → chunk-RVKDILGM.js} +2 -327
- package/dist/chunk-RVKDILGM.js.map +1 -0
- package/dist/chunk-T2AQHAYK.js +337 -0
- package/dist/chunk-T2AQHAYK.js.map +1 -0
- package/dist/chunk-Z4MHYAS3.js +108 -0
- package/dist/chunk-Z4MHYAS3.js.map +1 -0
- package/dist/index.cjs +1788 -839
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -1
- package/dist/index.d.ts +202 -1
- package/dist/index.js +693 -180
- package/dist/index.js.map +1 -1
- package/dist/locale-YK3THSI6.js +7 -0
- package/dist/locale-YK3THSI6.js.map +1 -0
- package/dist/src-7D236CLJ.js +19 -0
- package/dist/src-7D236CLJ.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-OVGQ4NUA.js.map +0 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTION_KINDS,
|
|
3
|
+
DEP_KEYS,
|
|
4
|
+
Framework,
|
|
5
|
+
LruMap,
|
|
6
|
+
PrefetchedIntents,
|
|
7
|
+
generateUuid
|
|
8
|
+
} from "./chunk-RVKDILGM.js";
|
|
9
|
+
|
|
10
|
+
// ../browser/src/action-handlers/external-url-action.ts
|
|
11
|
+
function registerExternalUrlHandler(deps) {
|
|
12
|
+
const { framework, log } = deps;
|
|
13
|
+
framework.onAction(
|
|
14
|
+
ACTION_KINDS.EXTERNAL_URL,
|
|
15
|
+
(action) => {
|
|
16
|
+
log.debug(`ExternalUrlAction \u2192 ${action.url}`);
|
|
17
|
+
window.open(action.url, "_blank", "noopener,noreferrer");
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../browser/src/utils/try-scroll.ts
|
|
23
|
+
var MAX_TRIES = 100;
|
|
24
|
+
var FUDGE = 16;
|
|
25
|
+
var pendingFrame = null;
|
|
26
|
+
function tryScroll(log, getScrollableElement, scrollY) {
|
|
27
|
+
if (pendingFrame !== null) {
|
|
28
|
+
cancelAnimationFrame(pendingFrame);
|
|
29
|
+
pendingFrame = null;
|
|
30
|
+
}
|
|
31
|
+
let tries = 0;
|
|
32
|
+
pendingFrame = requestAnimationFrame(function attempt() {
|
|
33
|
+
if (++tries >= MAX_TRIES) {
|
|
34
|
+
log.warn(
|
|
35
|
+
`tryScroll: gave up after ${MAX_TRIES} frames, target=${scrollY}`
|
|
36
|
+
);
|
|
37
|
+
pendingFrame = null;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const el = getScrollableElement();
|
|
41
|
+
if (!el) {
|
|
42
|
+
log.warn(
|
|
43
|
+
"could not restore scroll: the scrollable element is missing"
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const { scrollHeight, offsetHeight } = el;
|
|
48
|
+
const canScroll = scrollY + offsetHeight <= scrollHeight + FUDGE;
|
|
49
|
+
if (!canScroll) {
|
|
50
|
+
log.info("page is not tall enough for scroll yet", {
|
|
51
|
+
scrollHeight,
|
|
52
|
+
offsetHeight
|
|
53
|
+
});
|
|
54
|
+
pendingFrame = requestAnimationFrame(attempt);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
el.scrollTop = scrollY;
|
|
58
|
+
log.info("scroll restored to", scrollY);
|
|
59
|
+
pendingFrame = null;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ../browser/src/utils/history.ts
|
|
64
|
+
var HISTORY_SIZE_LIMIT = 10;
|
|
65
|
+
var History = class {
|
|
66
|
+
entries;
|
|
67
|
+
log;
|
|
68
|
+
getScrollablePageElement;
|
|
69
|
+
currentStateId;
|
|
70
|
+
constructor(log, options, sizeLimit = HISTORY_SIZE_LIMIT) {
|
|
71
|
+
this.entries = new LruMap(sizeLimit);
|
|
72
|
+
this.log = log;
|
|
73
|
+
this.getScrollablePageElement = options.getScrollablePageElement;
|
|
74
|
+
}
|
|
75
|
+
replaceState(state, url) {
|
|
76
|
+
const id = generateUuid();
|
|
77
|
+
window.history.replaceState({ id }, "", url);
|
|
78
|
+
this.currentStateId = id;
|
|
79
|
+
this.entries.set(id, { state, scrollY: 0 });
|
|
80
|
+
this.scrollTop = 0;
|
|
81
|
+
this.log.info("replaceState", state, url, id);
|
|
82
|
+
}
|
|
83
|
+
pushState(state, url) {
|
|
84
|
+
const id = generateUuid();
|
|
85
|
+
window.history.pushState({ id }, "", url);
|
|
86
|
+
this.currentStateId = id;
|
|
87
|
+
this.entries.set(id, { state, scrollY: 0 });
|
|
88
|
+
this.scrollTop = 0;
|
|
89
|
+
this.log.info("pushState", state, url, id);
|
|
90
|
+
}
|
|
91
|
+
beforeTransition() {
|
|
92
|
+
const { state } = window.history;
|
|
93
|
+
if (!state) return;
|
|
94
|
+
const oldEntry = this.entries.get(state.id);
|
|
95
|
+
if (!oldEntry) {
|
|
96
|
+
this.log.info(
|
|
97
|
+
"current history state evicted from LRU, not saving scroll position"
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { scrollTop } = this;
|
|
102
|
+
this.entries.set(state.id, { ...oldEntry, scrollY: scrollTop });
|
|
103
|
+
this.log.info("saving scroll position", scrollTop);
|
|
104
|
+
}
|
|
105
|
+
onPopState(listener) {
|
|
106
|
+
window.addEventListener("popstate", (event) => {
|
|
107
|
+
this.currentStateId = event.state?.id;
|
|
108
|
+
if (!this.currentStateId) {
|
|
109
|
+
this.log.warn(
|
|
110
|
+
"encountered a null event.state.id in onPopState event:",
|
|
111
|
+
window.location.href
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
this.log.info("popstate", this.entries, this.currentStateId);
|
|
115
|
+
const entry = this.currentStateId ? this.entries.get(this.currentStateId) : void 0;
|
|
116
|
+
listener(window.location.href, entry?.state);
|
|
117
|
+
if (!entry) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const { scrollY } = entry;
|
|
121
|
+
this.log.info("restoring scroll to", scrollY);
|
|
122
|
+
tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
updateState(update) {
|
|
126
|
+
if (!this.currentStateId) {
|
|
127
|
+
this.log.warn(
|
|
128
|
+
"failed: encountered a null currentStateId inside updateState"
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const currentState = this.entries.get(this.currentStateId);
|
|
133
|
+
const newState = update(currentState?.state);
|
|
134
|
+
this.log.info("updateState", newState, this.currentStateId);
|
|
135
|
+
this.entries.set(this.currentStateId, {
|
|
136
|
+
...currentState,
|
|
137
|
+
state: newState
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
get scrollTop() {
|
|
141
|
+
return this.getScrollablePageElement()?.scrollTop || 0;
|
|
142
|
+
}
|
|
143
|
+
set scrollTop(scrollTop) {
|
|
144
|
+
const element = this.getScrollablePageElement();
|
|
145
|
+
if (element) {
|
|
146
|
+
element.scrollTop = scrollTop;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ../browser/src/action-handlers/flow-action.ts
|
|
152
|
+
function registerFlowActionHandler(deps) {
|
|
153
|
+
const { framework, log, callbacks, updateApp } = deps;
|
|
154
|
+
let isFirstPage = true;
|
|
155
|
+
const history = new History(log, {
|
|
156
|
+
getScrollablePageElement: () => document.getElementById("scrollable-page-override") || document.getElementById("scrollable-page") || document.documentElement
|
|
157
|
+
});
|
|
158
|
+
framework.onAction(ACTION_KINDS.FLOW, async (action) => {
|
|
159
|
+
const flowAction = action;
|
|
160
|
+
const url = flowAction.url;
|
|
161
|
+
log.debug(`FlowAction \u2192 ${url}`);
|
|
162
|
+
if (flowAction.presentationContext === "modal") {
|
|
163
|
+
const match2 = framework.routeUrl(url);
|
|
164
|
+
if (match2) {
|
|
165
|
+
const page = await framework.dispatch(
|
|
166
|
+
match2.intent
|
|
167
|
+
);
|
|
168
|
+
callbacks.onModal(page);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const shouldReplace = isFirstPage;
|
|
173
|
+
const match = framework.routeUrl(url);
|
|
174
|
+
if (!match) {
|
|
175
|
+
log.warn(`FlowAction: no route for ${url}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const pagePromise = framework.dispatch(
|
|
179
|
+
match.intent
|
|
180
|
+
);
|
|
181
|
+
await Promise.race([
|
|
182
|
+
pagePromise,
|
|
183
|
+
new Promise((r) => setTimeout(r, 500))
|
|
184
|
+
]).catch(() => {
|
|
185
|
+
});
|
|
186
|
+
history.beforeTransition();
|
|
187
|
+
updateApp({
|
|
188
|
+
page: pagePromise.then((page) => {
|
|
189
|
+
const canonicalURL = url;
|
|
190
|
+
if (shouldReplace) {
|
|
191
|
+
history.replaceState({ page }, canonicalURL);
|
|
192
|
+
} else {
|
|
193
|
+
history.pushState({ page }, canonicalURL);
|
|
194
|
+
}
|
|
195
|
+
callbacks.onNavigate(
|
|
196
|
+
new URL(canonicalURL, window.location.origin).pathname
|
|
197
|
+
);
|
|
198
|
+
didEnterPage(page);
|
|
199
|
+
return page;
|
|
200
|
+
}),
|
|
201
|
+
isFirstPage
|
|
202
|
+
});
|
|
203
|
+
isFirstPage = false;
|
|
204
|
+
});
|
|
205
|
+
history.onPopState(async (url, cachedState) => {
|
|
206
|
+
log.debug(`popstate \u2192 ${url}, cached=${!!cachedState}`);
|
|
207
|
+
callbacks.onNavigate(new URL(url).pathname);
|
|
208
|
+
if (cachedState) {
|
|
209
|
+
const { page } = cachedState;
|
|
210
|
+
didEnterPage(page);
|
|
211
|
+
updateApp({ page, isFirstPage });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const parsed = new URL(url);
|
|
215
|
+
const routeMatch = framework.routeUrl(parsed.pathname + parsed.search);
|
|
216
|
+
if (!routeMatch) {
|
|
217
|
+
log.error(
|
|
218
|
+
"received popstate without data, but URL was unroutable:",
|
|
219
|
+
url
|
|
220
|
+
);
|
|
221
|
+
didEnterPage(null);
|
|
222
|
+
updateApp({
|
|
223
|
+
page: Promise.reject(new Error("404")),
|
|
224
|
+
isFirstPage
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const pagePromise = framework.dispatch(
|
|
229
|
+
routeMatch.intent
|
|
230
|
+
);
|
|
231
|
+
await Promise.race([
|
|
232
|
+
pagePromise,
|
|
233
|
+
new Promise((r) => setTimeout(r, 500))
|
|
234
|
+
]).catch(() => {
|
|
235
|
+
});
|
|
236
|
+
updateApp({
|
|
237
|
+
page: pagePromise.then((page) => {
|
|
238
|
+
didEnterPage(page);
|
|
239
|
+
return page;
|
|
240
|
+
}),
|
|
241
|
+
isFirstPage
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
function didEnterPage(page) {
|
|
245
|
+
(async () => {
|
|
246
|
+
try {
|
|
247
|
+
if (page) {
|
|
248
|
+
await framework.didEnterPage(page);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
log.error("didEnterPage error:", e);
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ../browser/src/action-handlers/register.ts
|
|
258
|
+
function registerActionHandlers(deps) {
|
|
259
|
+
const { framework, log, callbacks, updateApp } = deps;
|
|
260
|
+
registerFlowActionHandler({
|
|
261
|
+
framework,
|
|
262
|
+
log,
|
|
263
|
+
callbacks,
|
|
264
|
+
updateApp
|
|
265
|
+
});
|
|
266
|
+
registerExternalUrlHandler({ framework, log });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ../browser/src/server-data.ts
|
|
270
|
+
var SERVER_DATA_ID = "serialized-server-data";
|
|
271
|
+
function deserializeServerData() {
|
|
272
|
+
const script = document.getElementById(SERVER_DATA_ID);
|
|
273
|
+
if (!script?.textContent) return void 0;
|
|
274
|
+
script.parentNode?.removeChild(script);
|
|
275
|
+
try {
|
|
276
|
+
return JSON.parse(script.textContent);
|
|
277
|
+
} catch {
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function createPrefetchedIntentsFromDom() {
|
|
282
|
+
const data = deserializeServerData();
|
|
283
|
+
if (!data || !Array.isArray(data)) {
|
|
284
|
+
return PrefetchedIntents.empty();
|
|
285
|
+
}
|
|
286
|
+
return PrefetchedIntents.fromArray(data);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ../browser/src/start-app.ts
|
|
290
|
+
async function startBrowserApp(config) {
|
|
291
|
+
const {
|
|
292
|
+
bootstrap,
|
|
293
|
+
defaultLocale = "en",
|
|
294
|
+
mountId = "app",
|
|
295
|
+
mount,
|
|
296
|
+
callbacks
|
|
297
|
+
} = config;
|
|
298
|
+
const prefetchedIntents = createPrefetchedIntentsFromDom();
|
|
299
|
+
const framework = Framework.create({ prefetchedIntents });
|
|
300
|
+
bootstrap(framework);
|
|
301
|
+
const loggerFactory = framework.container.resolve(
|
|
302
|
+
DEP_KEYS.LOGGER_FACTORY
|
|
303
|
+
);
|
|
304
|
+
const log = loggerFactory.loggerFor("browser");
|
|
305
|
+
const initialAction = framework.routeUrl(
|
|
306
|
+
window.location.pathname + window.location.search
|
|
307
|
+
);
|
|
308
|
+
const locale = document.documentElement.lang || defaultLocale;
|
|
309
|
+
const target = document.getElementById(mountId);
|
|
310
|
+
const updateApp = mount(target, { framework, locale });
|
|
311
|
+
registerActionHandlers({
|
|
312
|
+
framework,
|
|
313
|
+
log,
|
|
314
|
+
callbacks,
|
|
315
|
+
updateApp
|
|
316
|
+
});
|
|
317
|
+
if (initialAction) {
|
|
318
|
+
await framework.perform(initialAction.action);
|
|
319
|
+
} else {
|
|
320
|
+
updateApp({
|
|
321
|
+
page: Promise.reject(new Error("404")),
|
|
322
|
+
isFirstPage: true
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export {
|
|
328
|
+
registerExternalUrlHandler,
|
|
329
|
+
tryScroll,
|
|
330
|
+
History,
|
|
331
|
+
registerFlowActionHandler,
|
|
332
|
+
registerActionHandlers,
|
|
333
|
+
deserializeServerData,
|
|
334
|
+
createPrefetchedIntentsFromDom,
|
|
335
|
+
startBrowserApp
|
|
336
|
+
};
|
|
337
|
+
//# sourceMappingURL=chunk-T2AQHAYK.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(listener: (url: string, state?: State) => void): void {\n\t\twindow.addEventListener(\"popstate\", (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\tlistener(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\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 { BasePage, FlowAction, Framework, Logger } from \"@finesoft/core\";\nimport { ACTION_KINDS } 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}\n\nexport function registerFlowActionHandler(deps: FlowActionDependencies): void {\n\tconst { framework, log, callbacks, updateApp } = deps;\n\tlet isFirstPage = true;\n\n\tconst history = new History<FlowState>(log, {\n\t\tgetScrollablePageElement: () =>\n\t\t\tdocument.getElementById(\"scrollable-page-override\") ||\n\t\t\tdocument.getElementById(\"scrollable-page\") ||\n\t\t\tdocument.documentElement,\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 shouldReplace = isFirstPage;\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\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\thistory.beforeTransition();\n\n\t\tupdateApp({\n\t\t\tpage: pagePromise.then((page: BasePage): BasePage => {\n\t\t\t\tconst canonicalURL = url;\n\n\t\t\t\tif (shouldReplace) {\n\t\t\t\t\thistory.replaceState({ page }, canonicalURL);\n\t\t\t\t} else {\n\t\t\t\t\thistory.pushState({ page }, canonicalURL);\n\t\t\t\t}\n\n\t\t\t\tcallbacks.onNavigate(\n\t\t\t\t\tnew URL(canonicalURL, window.location.origin).pathname,\n\t\t\t\t);\n\n\t\t\t\tdidEnterPage(page);\n\t\t\t\treturn page;\n\t\t\t}),\n\t\t\tisFirstPage,\n\t\t});\n\n\t\tisFirstPage = false;\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\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}\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});\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\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});\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":["match"]}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
injectSSRContent
|
|
3
|
+
} from "./chunk-2VETWTN5.js";
|
|
4
|
+
import {
|
|
5
|
+
parseAcceptLanguage
|
|
6
|
+
} from "./chunk-FYP2ZYYV.js";
|
|
7
|
+
|
|
8
|
+
// ../server/src/app.ts
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
function createSSRApp(options) {
|
|
11
|
+
const {
|
|
12
|
+
root,
|
|
13
|
+
vite,
|
|
14
|
+
isProduction,
|
|
15
|
+
ssrEntryPath = "/src/ssr.ts",
|
|
16
|
+
ssrProductionModule,
|
|
17
|
+
supportedLocales,
|
|
18
|
+
defaultLocale
|
|
19
|
+
} = options;
|
|
20
|
+
const app = new Hono();
|
|
21
|
+
async function readTemplate(url) {
|
|
22
|
+
if (!isProduction && vite) {
|
|
23
|
+
const { readFileSync: readFileSync2 } = await import(
|
|
24
|
+
/* @vite-ignore */
|
|
25
|
+
"fs"
|
|
26
|
+
);
|
|
27
|
+
const { resolve: resolve2 } = await import(
|
|
28
|
+
/* @vite-ignore */
|
|
29
|
+
"path"
|
|
30
|
+
);
|
|
31
|
+
const raw = readFileSync2(resolve2(root, "index.html"), "utf-8");
|
|
32
|
+
return vite.transformIndexHtml(url, raw);
|
|
33
|
+
}
|
|
34
|
+
const isDeno = typeof globalThis.Deno !== "undefined";
|
|
35
|
+
if (isDeno) {
|
|
36
|
+
return globalThis.Deno.readTextFileSync(
|
|
37
|
+
new URL("../dist/client/index.html", import.meta.url)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const { readFileSync } = await import(
|
|
41
|
+
/* @vite-ignore */
|
|
42
|
+
"fs"
|
|
43
|
+
);
|
|
44
|
+
const { resolve } = await import(
|
|
45
|
+
/* @vite-ignore */
|
|
46
|
+
"path"
|
|
47
|
+
);
|
|
48
|
+
return readFileSync(resolve(root, "dist/client/index.html"), "utf-8");
|
|
49
|
+
}
|
|
50
|
+
async function loadSSRModule() {
|
|
51
|
+
if (!isProduction && vite) {
|
|
52
|
+
return await vite.ssrLoadModule(ssrEntryPath);
|
|
53
|
+
}
|
|
54
|
+
if (ssrProductionModule) {
|
|
55
|
+
return import(ssrProductionModule);
|
|
56
|
+
}
|
|
57
|
+
const { resolve } = await import(
|
|
58
|
+
/* @vite-ignore */
|
|
59
|
+
"path"
|
|
60
|
+
);
|
|
61
|
+
const { pathToFileURL } = await import(
|
|
62
|
+
/* @vite-ignore */
|
|
63
|
+
"url"
|
|
64
|
+
);
|
|
65
|
+
const absPath = pathToFileURL(resolve(root, "dist/server/ssr.js")).href;
|
|
66
|
+
return import(absPath);
|
|
67
|
+
}
|
|
68
|
+
app.get("*", async (c) => {
|
|
69
|
+
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
70
|
+
try {
|
|
71
|
+
const template = await readTemplate(url);
|
|
72
|
+
const { render, serializeServerData } = await loadSSRModule();
|
|
73
|
+
const locale = parseAcceptLanguage(
|
|
74
|
+
c.req.header("accept-language"),
|
|
75
|
+
supportedLocales,
|
|
76
|
+
defaultLocale
|
|
77
|
+
);
|
|
78
|
+
const {
|
|
79
|
+
html: appHtml,
|
|
80
|
+
head,
|
|
81
|
+
css,
|
|
82
|
+
serverData
|
|
83
|
+
} = await render(url, locale);
|
|
84
|
+
const serializedData = serializeServerData(serverData);
|
|
85
|
+
const finalHtml = injectSSRContent({
|
|
86
|
+
template,
|
|
87
|
+
locale,
|
|
88
|
+
head,
|
|
89
|
+
css,
|
|
90
|
+
html: appHtml,
|
|
91
|
+
serializedData
|
|
92
|
+
});
|
|
93
|
+
return c.html(finalHtml);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (!isProduction && vite) {
|
|
96
|
+
vite.ssrFixStacktrace(e);
|
|
97
|
+
}
|
|
98
|
+
console.error("[SSR Error]", e);
|
|
99
|
+
return c.text("Internal Server Error", 500);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return app;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
createSSRApp
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=chunk-Z4MHYAS3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../server/src/app.ts"],"sourcesContent":["/**\n * createSSRApp — 创建 Hono SSR 应用\n *\n * 提供 SSR 通配路由,读取模板、加载 SSR 模块、渲染。\n * 应用层可在此之上追加自定义路由(API 代理等)。\n */\n\nimport { injectSSRContent } from \"@finesoft/ssr\";\nimport { Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport { parseAcceptLanguage } from \"./locale\";\n\nexport interface SSRModule {\n\trender: (\n\t\turl: string,\n\t\tlocale: string,\n\t) => Promise<{\n\t\thtml: string;\n\t\thead: string;\n\t\tcss: string;\n\t\tserverData: unknown;\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}\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} = options;\n\n\tconst app = new Hono();\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\tconst isDeno = typeof (globalThis as any).Deno !== \"undefined\";\n\t\tif (isDeno) {\n\t\t\treturn (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}\n\n\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\treturn readFileSync(resolve(root, \"dist/client/index.html\"), \"utf-8\");\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(ssrProductionModule) 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(absPath) as Promise<SSRModule>;\n\t}\n\n\tapp.get(\"*\", async (c) => {\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\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} = await render(url, locale);\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\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"],"mappings":";;;;;;;;AAQA,SAAS,YAAY;AAkCd,SAAS,aAAa,SAA8B;AAC1D,QAAM;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,EACD,IAAI;AAEJ,QAAM,MAAM,IAAI,KAAK;AAErB,iBAAe,aAAa,KAA8B;AACzD,QAAI,CAAC,gBAAgB,MAAM;AAC1B,YAAM,EAAE,cAAAA,cAAa,IAAI,MAAM;AAAA;AAAA,QAA0B;AAAA,MAAS;AAClE,YAAM,EAAE,SAAAC,SAAQ,IAAI,MAAM;AAAA;AAAA,QAA0B;AAAA,MAAW;AAC/D,YAAM,MAAMD,cAAaC,SAAQ,MAAM,YAAY,GAAG,OAAO;AAC7D,aAAO,KAAK,mBAAmB,KAAK,GAAG;AAAA,IACxC;AAEA,UAAM,SAAS,OAAQ,WAAmB,SAAS;AACnD,QAAI,QAAQ;AACX,aAAQ,WAAmB,KAAK;AAAA,QAC/B,IAAI,IAAI,6BAA6B,YAAY,GAAG;AAAA,MACrD;AAAA,IACD;AAEA,UAAM,EAAE,aAAa,IAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,IAAS;AAClE,UAAM,EAAE,QAAQ,IAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,IAAW;AAC/D,WAAO,aAAa,QAAQ,MAAM,wBAAwB,GAAG,OAAO;AAAA,EACrE;AAEA,iBAAe,gBAAoC;AAClD,QAAI,CAAC,gBAAgB,MAAM;AAC1B,aAAQ,MAAM,KAAK,cAAc,YAAY;AAAA,IAC9C;AACA,QAAI,qBAAqB;AACxB,aAAO,OAAO;AAAA,IACf;AACA,UAAM,EAAE,QAAQ,IAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,IAAW;AAC/D,UAAM,EAAE,cAAc,IAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,IAAU;AACpE,UAAM,UAAU,cAAc,QAAQ,MAAM,oBAAoB,CAAC,EAAE;AACnE,WAAO,OAAO;AAAA,EACf;AAEA,MAAI,IAAI,KAAK,OAAO,MAAM;AACzB,UAAM,MACL,EAAE,IAAI,QACL,EAAE,IAAI,IAAI,SAAS,GAAG,IAAI,MAAM,EAAE,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI;AAE5D,QAAI;AACH,YAAM,WAAW,MAAM,aAAa,GAAG;AACvC,YAAM,EAAE,QAAQ,oBAAoB,IAAI,MAAM,cAAc;AAE5D,YAAM,SAAS;AAAA,QACd,EAAE,IAAI,OAAO,iBAAiB;AAAA,QAC9B;AAAA,QACA;AAAA,MACD;AAEA,YAAM;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,MACD,IAAI,MAAM,OAAO,KAAK,MAAM;AAE5B,YAAM,iBAAiB,oBAAoB,UAAU;AAErD,YAAM,YAAY,iBAAiB;AAAA,QAClC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN;AAAA,MACD,CAAC;AAED,aAAO,EAAE,KAAK,SAAS;AAAA,IACxB,SAAS,GAAG;AACX,UAAI,CAAC,gBAAgB,MAAM;AAC1B,aAAK,iBAAiB,CAAU;AAAA,MACjC;AACA,cAAQ,MAAM,eAAe,CAAC;AAC9B,aAAO,EAAE,KAAK,yBAAyB,GAAG;AAAA,IAC3C;AAAA,EACD,CAAC;AAED,SAAO;AACR;","names":["readFileSync","resolve"]}
|