@finesoft/front 0.1.39 → 0.1.41

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.
Files changed (41) hide show
  1. package/dist/app-re7jUCuM.mjs +178 -0
  2. package/dist/app-re7jUCuM.mjs.map +1 -0
  3. package/dist/index.d.mts +1276 -0
  4. package/dist/index.d.mts.map +1 -0
  5. package/dist/index.mjs +2749 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/locale-D2Bu7w47.mjs +27 -0
  8. package/dist/locale-D2Bu7w47.mjs.map +1 -0
  9. package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
  10. package/dist/server-peer-modules-BSSsxBaF.d.mts +41 -0
  11. package/dist/server-peer-modules-BSSsxBaF.d.mts.map +1 -0
  12. package/package.json +21 -32
  13. package/README.md +0 -1136
  14. package/dist/app-XEMPAI6H.js +0 -10
  15. package/dist/app-XEMPAI6H.js.map +0 -1
  16. package/dist/browser.cjs +0 -1465
  17. package/dist/browser.cjs.map +0 -1
  18. package/dist/browser.d.cts +0 -904
  19. package/dist/browser.d.ts +0 -904
  20. package/dist/browser.js +0 -115
  21. package/dist/browser.js.map +0 -1
  22. package/dist/chunk-4PPCVAKZ.js +0 -463
  23. package/dist/chunk-4PPCVAKZ.js.map +0 -1
  24. package/dist/chunk-OYTIGVEG.js +0 -193
  25. package/dist/chunk-OYTIGVEG.js.map +0 -1
  26. package/dist/chunk-PSPVIVC2.js +0 -25
  27. package/dist/chunk-PSPVIVC2.js.map +0 -1
  28. package/dist/chunk-SDPWQT2T.js +0 -936
  29. package/dist/chunk-SDPWQT2T.js.map +0 -1
  30. package/dist/chunk-XQ3UWZOS.js +0 -187
  31. package/dist/chunk-XQ3UWZOS.js.map +0 -1
  32. package/dist/index.cjs +0 -3661
  33. package/dist/index.cjs.map +0 -1
  34. package/dist/index.d.cts +0 -582
  35. package/dist/index.d.ts +0 -582
  36. package/dist/index.js +0 -1648
  37. package/dist/index.js.map +0 -1
  38. package/dist/locale-CAZ4INCX.js +0 -7
  39. package/dist/locale-CAZ4INCX.js.map +0 -1
  40. package/dist/src-XNC7QNVI.js +0 -21
  41. package/dist/src-XNC7QNVI.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,2749 @@
1
+ import "./server-peer-modules-BSSsxBaF.d.mts";
2
+ import { a as SSR_PLACEHOLDERS, i as createInternalFetch, n as createSSRApp, o as injectCSRShell, s as injectSSRContent } from "./app-re7jUCuM.mjs";
3
+ import { n as parseAcceptLanguage } from "./locale-D2Bu7w47.mjs";
4
+ import { Hono } from "hono";
5
+ //#region ../core/src/actions/types.ts
6
+ /**
7
+ * Action 类型定义
8
+ *
9
+ * FlowAction — SPA 内部导航
10
+ * ExternalUrlAction — 打开外部链接
11
+ * CompoundAction — 组合多个 Action
12
+ */
13
+ /** Action Kind 常量 */
14
+ const ACTION_KINDS = {
15
+ FLOW: "flow",
16
+ EXTERNAL_URL: "externalUrl",
17
+ COMPOUND: "compound"
18
+ };
19
+ function isFlowAction(action) {
20
+ return action.kind === ACTION_KINDS.FLOW;
21
+ }
22
+ function isExternalUrlAction(action) {
23
+ return action.kind === ACTION_KINDS.EXTERNAL_URL;
24
+ }
25
+ function isCompoundAction(action) {
26
+ return action.kind === ACTION_KINDS.COMPOUND;
27
+ }
28
+ function makeFlowAction(url, presentationContext) {
29
+ return {
30
+ kind: ACTION_KINDS.FLOW,
31
+ url,
32
+ presentationContext
33
+ };
34
+ }
35
+ function makeExternalUrlAction(url) {
36
+ return {
37
+ kind: ACTION_KINDS.EXTERNAL_URL,
38
+ url
39
+ };
40
+ }
41
+ //#endregion
42
+ //#region ../core/src/actions/dispatcher.ts
43
+ var ActionDispatcher = class {
44
+ handlers = /* @__PURE__ */ new Map();
45
+ wiredActions = /* @__PURE__ */ new Set();
46
+ /** 注册指定 kind 的 handler(防止重复注册) */
47
+ onAction(kind, handler) {
48
+ if (this.wiredActions.has(kind)) {
49
+ console.warn(`[ActionDispatcher] kind="${kind}" already registered, skipping`);
50
+ return;
51
+ }
52
+ this.wiredActions.add(kind);
53
+ this.handlers.set(kind, handler);
54
+ }
55
+ /** 执行一个 Action(CompoundAction 递归展开) */
56
+ async perform(action) {
57
+ if (isCompoundAction(action)) {
58
+ for (const subAction of action.actions) await this.perform(subAction);
59
+ return;
60
+ }
61
+ const handler = this.handlers.get(action.kind);
62
+ if (!handler) {
63
+ console.warn(`[ActionDispatcher] No handler for kind="${action.kind}"`);
64
+ return;
65
+ }
66
+ await handler(action);
67
+ }
68
+ };
69
+ //#endregion
70
+ //#region ../core/src/intents/dispatcher.ts
71
+ var IntentDispatcher = class {
72
+ controllers = /* @__PURE__ */ new Map();
73
+ /** 注册一个 IntentController */
74
+ register(controller) {
75
+ this.controllers.set(controller.intentId, controller);
76
+ }
77
+ /** 分发 Intent 到对应 Controller */
78
+ async dispatch(intent, container) {
79
+ const controller = this.controllers.get(intent.id);
80
+ if (!controller) throw new Error(`[IntentDispatcher] No controller for "${intent.id}". Registered: [${Array.from(this.controllers.keys()).join(", ")}]`);
81
+ return controller.perform(intent, container);
82
+ }
83
+ /** 检查是否已注册某个 Intent */
84
+ has(intentId) {
85
+ return this.controllers.has(intentId);
86
+ }
87
+ };
88
+ //#endregion
89
+ //#region ../core/src/dependencies/container.ts
90
+ var Container = class {
91
+ registrations = /* @__PURE__ */ new Map();
92
+ /** 注册依赖(默认单例) */
93
+ register(key, factory, singleton = true) {
94
+ this.registrations.set(key, {
95
+ factory,
96
+ singleton
97
+ });
98
+ return this;
99
+ }
100
+ /** 解析依赖 */
101
+ resolve(key) {
102
+ const reg = this.registrations.get(key);
103
+ if (!reg) throw new Error(`[Container] No registration for key: "${key}"`);
104
+ if (reg.singleton) {
105
+ if (reg.instance === void 0) reg.instance = reg.factory();
106
+ return reg.instance;
107
+ }
108
+ return reg.factory();
109
+ }
110
+ /** 检查是否已注册 */
111
+ has(key) {
112
+ return this.registrations.has(key);
113
+ }
114
+ /** 销毁容器,清除所有缓存 */
115
+ dispose() {
116
+ for (const reg of this.registrations.values()) reg.instance = void 0;
117
+ this.registrations.clear();
118
+ }
119
+ };
120
+ //#endregion
121
+ //#region ../core/src/logger/base.ts
122
+ var BaseLogger = class {
123
+ category;
124
+ constructor(category) {
125
+ this.category = category;
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region ../core/src/logger/local-storage-filter.ts
130
+ const LEVEL_TO_NUM = {
131
+ "*": 4,
132
+ debug: 4,
133
+ info: 3,
134
+ warn: 2,
135
+ error: 1,
136
+ off: 0,
137
+ "": 0
138
+ };
139
+ let cachedRules;
140
+ let cachedRaw;
141
+ function parseRules() {
142
+ if (typeof globalThis.localStorage === "undefined") return {};
143
+ let raw;
144
+ try {
145
+ raw = globalThis.localStorage.getItem("onyxLog");
146
+ } catch {
147
+ return {};
148
+ }
149
+ if (!raw) return {};
150
+ if (raw === cachedRaw && cachedRules) return cachedRules;
151
+ cachedRaw = raw;
152
+ const rules = {};
153
+ const parts = raw.split(",");
154
+ for (const part of parts) {
155
+ const [name, level] = part.trim().split("=");
156
+ if (!name || level === void 0) continue;
157
+ const num = LEVEL_TO_NUM[level.toLowerCase()] ?? void 0;
158
+ if (num === void 0) continue;
159
+ if (name === "*") rules.defaultLevel = num;
160
+ else {
161
+ rules.named ??= {};
162
+ rules.named[name] = num;
163
+ }
164
+ }
165
+ cachedRules = rules;
166
+ return rules;
167
+ }
168
+ function shouldLog(name, level) {
169
+ const rules = parseRules();
170
+ if (rules.defaultLevel === void 0 && !rules.named) return true;
171
+ const currentNum = LEVEL_TO_NUM[level] ?? 4;
172
+ if (rules.named?.[name] !== void 0) return currentNum <= rules.named[name];
173
+ if (rules.defaultLevel !== void 0) return currentNum <= rules.defaultLevel;
174
+ return true;
175
+ }
176
+ function resetFilterCache() {
177
+ cachedRules = void 0;
178
+ cachedRaw = void 0;
179
+ }
180
+ //#endregion
181
+ //#region ../core/src/logger/console.ts
182
+ /**
183
+ * ConsoleLogger — 基于 console 的日志实现
184
+ */
185
+ var ConsoleLogger = class extends BaseLogger {
186
+ debug(...args) {
187
+ if (shouldLog(this.category, "debug")) console.debug(`[${this.category}]`, ...args);
188
+ return "";
189
+ }
190
+ info(...args) {
191
+ if (shouldLog(this.category, "info")) console.info(`[${this.category}]`, ...args);
192
+ return "";
193
+ }
194
+ warn(...args) {
195
+ if (shouldLog(this.category, "warn")) console.warn(`[${this.category}]`, ...args);
196
+ return "";
197
+ }
198
+ error(...args) {
199
+ console.error(`[${this.category}]`, ...args);
200
+ return "";
201
+ }
202
+ };
203
+ var ConsoleLoggerFactory = class {
204
+ loggerFor(category) {
205
+ return new ConsoleLogger(category);
206
+ }
207
+ };
208
+ //#endregion
209
+ //#region ../core/src/dependencies/make-dependencies.ts
210
+ /**
211
+ * 依赖工厂 — 创建所有基础依赖
212
+ */
213
+ const DEP_KEYS = {
214
+ LOGGER: "logger",
215
+ LOGGER_FACTORY: "loggerFactory",
216
+ NET: "net",
217
+ LOCALE: "locale",
218
+ STORAGE: "storage",
219
+ FEATURE_FLAGS: "featureFlags",
220
+ METRICS: "metrics",
221
+ FETCH: "fetch"
222
+ };
223
+ var DefaultLocale = class {
224
+ language = "en";
225
+ storefront = "us";
226
+ setActiveLocale(language, storefront) {
227
+ this.language = language;
228
+ this.storefront = storefront;
229
+ }
230
+ };
231
+ var MemoryStorage = class {
232
+ store = /* @__PURE__ */ new Map();
233
+ get(key) {
234
+ return this.store.get(key);
235
+ }
236
+ set(key, value) {
237
+ this.store.set(key, value);
238
+ }
239
+ delete(key) {
240
+ this.store.delete(key);
241
+ }
242
+ };
243
+ var DefaultFeatureFlags = class {
244
+ flags;
245
+ constructor(flags = {}) {
246
+ this.flags = flags;
247
+ }
248
+ isEnabled(key) {
249
+ return this.flags[key] === true;
250
+ }
251
+ getString(key) {
252
+ const v = this.flags[key];
253
+ return typeof v === "string" ? v : void 0;
254
+ }
255
+ getNumber(key) {
256
+ const v = this.flags[key];
257
+ return typeof v === "number" ? v : void 0;
258
+ }
259
+ };
260
+ var ConsoleMetrics = class {
261
+ recordPageView(page, fields) {
262
+ console.info(`[Metrics:PageView] ${page}`, fields ?? "");
263
+ }
264
+ recordEvent(name, fields) {
265
+ console.info(`[Metrics:Event] ${name}`, fields ?? "");
266
+ }
267
+ };
268
+ function makeDependencies(container, options = {}) {
269
+ const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), language = "en", storefront = "us", featureFlags = {} } = options;
270
+ const loggerFactory = new ConsoleLoggerFactory();
271
+ container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
272
+ container.register(DEP_KEYS.LOGGER, () => loggerFactory.loggerFor("framework"));
273
+ container.register(DEP_KEYS.NET, () => ({ fetch: (url, opts) => fetchFn(url, opts) }));
274
+ container.register(DEP_KEYS.LOCALE, () => {
275
+ const locale = new DefaultLocale();
276
+ locale.setActiveLocale(language, storefront);
277
+ return locale;
278
+ });
279
+ container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
280
+ container.register(DEP_KEYS.FEATURE_FLAGS, () => new DefaultFeatureFlags(featureFlags));
281
+ container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
282
+ container.register(DEP_KEYS.FETCH, () => fetchFn);
283
+ }
284
+ //#endregion
285
+ //#region ../core/src/router/router.ts
286
+ /**
287
+ * URL 路由器 — URL pattern → Intent + FlowAction
288
+ */
289
+ var Router = class {
290
+ routes = [];
291
+ /** 添加路由规则 */
292
+ add(pattern, intentId, renderModeOrOptions) {
293
+ const opts = typeof renderModeOrOptions === "string" ? { renderMode: renderModeOrOptions } : renderModeOrOptions ?? {};
294
+ const paramNames = [];
295
+ const regexStr = pattern.split(/(\/:[\w]+\??)/).map((segment) => {
296
+ const paramMatch = segment.match(/^\/:(\w+)(\?)?$/);
297
+ if (paramMatch) {
298
+ paramNames.push(paramMatch[1]);
299
+ return paramMatch[2] ? "(?:/([^/]+))?" : "/([^/]+)";
300
+ }
301
+ return segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
302
+ }).join("");
303
+ this.routes.push({
304
+ pattern,
305
+ intentId,
306
+ regex: new RegExp(`^${regexStr}/?$`),
307
+ paramNames,
308
+ renderMode: opts.renderMode,
309
+ beforeGuards: opts.beforeGuards,
310
+ afterGuards: opts.afterGuards
311
+ });
312
+ return this;
313
+ }
314
+ /** 解析 URL → RouteMatch */
315
+ resolve(urlOrPath) {
316
+ const path = this.extractPath(urlOrPath);
317
+ const queryParams = this.extractQueryParams(urlOrPath);
318
+ for (const route of this.routes) {
319
+ const match = path.match(route.regex);
320
+ if (match) {
321
+ const params = {};
322
+ route.paramNames.forEach((name, index) => {
323
+ const value = match[index + 1];
324
+ if (value) params[name] = value;
325
+ });
326
+ for (const [k, v] of Object.entries(queryParams)) if (!(k in params)) params[k] = v;
327
+ return {
328
+ intent: {
329
+ id: route.intentId,
330
+ params
331
+ },
332
+ action: makeFlowAction(urlOrPath),
333
+ renderMode: route.renderMode,
334
+ beforeGuards: route.beforeGuards,
335
+ afterGuards: route.afterGuards
336
+ };
337
+ }
338
+ }
339
+ return null;
340
+ }
341
+ /** 获取所有已注册的路由 */
342
+ getRoutes() {
343
+ return this.routes.map((r) => `${r.pattern} → ${r.intentId}`);
344
+ }
345
+ extractPath(url) {
346
+ try {
347
+ return new URL(url, "http://localhost").pathname;
348
+ } catch {
349
+ return url.split("?")[0].split("#")[0];
350
+ }
351
+ }
352
+ extractQueryParams(url) {
353
+ try {
354
+ const parsed = new URL(url, "http://localhost");
355
+ const params = {};
356
+ parsed.searchParams.forEach((v, k) => {
357
+ params[k] = v;
358
+ });
359
+ return params;
360
+ } catch {
361
+ return {};
362
+ }
363
+ }
364
+ };
365
+ //#endregion
366
+ //#region ../core/src/logger/composite.ts
367
+ var CompositeLoggerFactory = class {
368
+ constructor(factories) {
369
+ this.factories = factories;
370
+ }
371
+ loggerFor(name) {
372
+ return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
373
+ }
374
+ };
375
+ var CompositeLogger = class {
376
+ constructor(loggers) {
377
+ this.loggers = loggers;
378
+ }
379
+ debug(...args) {
380
+ return this.callAll("debug", args);
381
+ }
382
+ info(...args) {
383
+ return this.callAll("info", args);
384
+ }
385
+ warn(...args) {
386
+ return this.callAll("warn", args);
387
+ }
388
+ error(...args) {
389
+ return this.callAll("error", args);
390
+ }
391
+ callAll(method, args) {
392
+ for (const logger of this.loggers) logger[method](...args);
393
+ return "";
394
+ }
395
+ };
396
+ //#endregion
397
+ //#region ../core/src/middleware/pipeline.ts
398
+ /** 执行 beforeLoad 守卫链 */
399
+ async function runBeforeLoadGuards(guards, ctx) {
400
+ for (const guard of guards) {
401
+ const result = await guard(ctx);
402
+ if (result.kind !== "next") return result;
403
+ }
404
+ return { kind: "next" };
405
+ }
406
+ /** 执行 afterLoad 守卫链 */
407
+ async function runAfterLoadGuards(guards, ctx) {
408
+ for (const guard of guards) {
409
+ const result = await guard(ctx);
410
+ if (result.kind !== "next") return result;
411
+ }
412
+ return { kind: "next" };
413
+ }
414
+ //#endregion
415
+ //#region ../core/src/prefetched-intents/stable-stringify.ts
416
+ /**
417
+ * stableStringify — 确定性 JSON 序列化(keys 按字母排序)
418
+ *
419
+ * 用作缓存 key:相同内容的对象始终产生相同字符串。
420
+ */
421
+ function stableStringify(obj, _seen) {
422
+ if (obj === null || obj === void 0) return String(obj);
423
+ if (typeof obj !== "object") return JSON.stringify(obj);
424
+ const seen = _seen ?? /* @__PURE__ */ new Set();
425
+ if (seen.has(obj)) return "\"[Circular]\"";
426
+ seen.add(obj);
427
+ if (Array.isArray(obj)) return "[" + obj.map((v) => stableStringify(v, seen)).join(",") + "]";
428
+ return "{" + Object.keys(obj).sort().filter((k) => obj[k] !== void 0).map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k], seen)).join(",") + "}";
429
+ }
430
+ //#endregion
431
+ //#region ../core/src/prefetched-intents/prefetched-intents.ts
432
+ var PrefetchedIntents = class PrefetchedIntents {
433
+ intents;
434
+ constructor(intents) {
435
+ this.intents = intents;
436
+ }
437
+ /** 从 PrefetchedIntent 数组创建缓存实例 */
438
+ static fromArray(items) {
439
+ const map = /* @__PURE__ */ new Map();
440
+ for (const item of items) if (item.intent && item.data !== void 0) {
441
+ const key = stableStringify(item.intent);
442
+ map.set(key, item.data);
443
+ }
444
+ return new PrefetchedIntents(map);
445
+ }
446
+ /** 创建空缓存实例 */
447
+ static empty() {
448
+ return new PrefetchedIntents(/* @__PURE__ */ new Map());
449
+ }
450
+ /**
451
+ * 获取缓存的 Intent 结果(一次性使用)。
452
+ * 命中后从缓存中删除。
453
+ */
454
+ get(intent) {
455
+ const key = stableStringify(intent);
456
+ const data = this.intents.get(key);
457
+ if (data !== void 0) {
458
+ this.intents.delete(key);
459
+ return data;
460
+ }
461
+ }
462
+ /** 检查缓存中是否有某个 Intent 的数据 */
463
+ has(intent) {
464
+ return this.intents.has(stableStringify(intent));
465
+ }
466
+ /** 缓存中的条目数 */
467
+ get size() {
468
+ return this.intents.size;
469
+ }
470
+ };
471
+ //#endregion
472
+ //#region ../core/src/framework.ts
473
+ /**
474
+ * Framework — 框架核心类
475
+ *
476
+ * 对应原版 Jet 类,统一管理: DI 容器、Intent 分发、Action 分发、路由、Metrics。
477
+ * 纯 TypeScript,不依赖任何 UI 框架。
478
+ */
479
+ var Framework = class Framework {
480
+ container;
481
+ intentDispatcher;
482
+ actionDispatcher;
483
+ router;
484
+ prefetchedIntents;
485
+ beforeGuards = [];
486
+ afterGuards = [];
487
+ constructor(container, prefetchedIntents) {
488
+ this.container = container;
489
+ this.intentDispatcher = new IntentDispatcher();
490
+ this.actionDispatcher = new ActionDispatcher();
491
+ this.router = new Router();
492
+ this.prefetchedIntents = prefetchedIntents;
493
+ }
494
+ /** 创建并初始化 Framework 实例 */
495
+ static create(config = {}) {
496
+ const container = new Container();
497
+ makeDependencies(container, config);
498
+ const fw = new Framework(container, config.prefetchedIntents ?? PrefetchedIntents.empty());
499
+ config.setupRoutes?.(fw.router);
500
+ return fw;
501
+ }
502
+ /** 分发 Intent — 获取页面数据 */
503
+ async dispatch(intent) {
504
+ const logger = this.container.resolve(DEP_KEYS.LOGGER);
505
+ const cached = this.prefetchedIntents.get(intent);
506
+ if (cached !== void 0) {
507
+ logger.debug(`[Framework] re-using prefetched intent response for: ${intent.id}`, intent.params);
508
+ return cached;
509
+ }
510
+ logger.debug(`[Framework] dispatch intent: ${intent.id}`, intent.params);
511
+ return this.intentDispatcher.dispatch(intent, this.container);
512
+ }
513
+ /** 执行 Action — 处理用户交互 */
514
+ async perform(action) {
515
+ this.container.resolve(DEP_KEYS.LOGGER).debug(`[Framework] perform action: ${action.kind}`);
516
+ return this.actionDispatcher.perform(action);
517
+ }
518
+ /** 路由 URL — 将 URL 解析为 Intent + Action */
519
+ routeUrl(url) {
520
+ return this.router.resolve(url);
521
+ }
522
+ /** 记录页面访问事件 */
523
+ didEnterPage(page) {
524
+ this.container.resolve(DEP_KEYS.METRICS).recordPageView(page.pageType, {
525
+ pageId: page.id,
526
+ title: page.title
527
+ });
528
+ }
529
+ /** 注册 Action 处理器 */
530
+ onAction(kind, handler) {
531
+ this.actionDispatcher.onAction(kind, handler);
532
+ }
533
+ /** 注册 Intent Controller */
534
+ registerIntent(controller) {
535
+ this.intentDispatcher.register(controller);
536
+ }
537
+ /** 注册 beforeLoad 守卫(路由匹配后、数据加载前) */
538
+ beforeLoad(guard) {
539
+ this.beforeGuards.push(guard);
540
+ }
541
+ /** 注册 afterLoad 守卫(数据加载后、渲染前) */
542
+ afterLoad(guard) {
543
+ this.afterGuards.push(guard);
544
+ }
545
+ /** 执行所有 beforeLoad 守卫(全局 → 路由级) */
546
+ runBeforeLoad(ctx, routeGuards) {
547
+ return runBeforeLoadGuards(routeGuards?.length ? [...this.beforeGuards, ...routeGuards] : this.beforeGuards, ctx);
548
+ }
549
+ /** 执行所有 afterLoad 守卫(全局 → 路由级) */
550
+ runAfterLoad(ctx, routeGuards) {
551
+ return runAfterLoadGuards(routeGuards?.length ? [...this.afterGuards, ...routeGuards] : this.afterGuards, ctx);
552
+ }
553
+ /** 销毁 Framework 实例 */
554
+ dispose() {
555
+ this.container.dispose();
556
+ }
557
+ };
558
+ //#endregion
559
+ //#region ../core/src/http/client.ts
560
+ /**
561
+ * HttpClient — 通用 HTTP 客户端基类
562
+ *
563
+ * 为 API Client 提供标准化的 HTTP 请求能力。
564
+ * 子类继承后只需关注业务端点定义,不需要重复实现 fetch / JSON 解析 / 错误处理。
565
+ */
566
+ /** HTTP 请求错误 */
567
+ var HttpError = class extends Error {
568
+ constructor(status, statusText, body) {
569
+ super(`HTTP ${status}: ${statusText}`);
570
+ this.status = status;
571
+ this.statusText = statusText;
572
+ this.body = body;
573
+ this.name = "HttpError";
574
+ }
575
+ };
576
+ /**
577
+ * 通用 HTTP 客户端基类
578
+ *
579
+ * 使用方式: 创建子类继承 HttpClient,定义业务方法调用 this.get() / this.post() 等。
580
+ *
581
+ * @example
582
+ * ```ts
583
+ * class MyApiClient extends HttpClient {
584
+ * async getUser(id: string) {
585
+ * return this.get<User>(`/users/${id}`);
586
+ * }
587
+ * }
588
+ * ```
589
+ */
590
+ var HttpClient = class {
591
+ baseUrl;
592
+ defaultHeaders;
593
+ fetchFn;
594
+ constructor(config) {
595
+ this.baseUrl = config.baseUrl;
596
+ this.defaultHeaders = config.defaultHeaders ?? {};
597
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
598
+ }
599
+ /** GET 请求,返回解析后的 JSON */
600
+ async get(path, params) {
601
+ return this.request("GET", path, { params });
602
+ }
603
+ /** POST 请求,自动序列化 body 为 JSON */
604
+ async post(path, body, params) {
605
+ return this.request("POST", path, {
606
+ body,
607
+ params
608
+ });
609
+ }
610
+ /** PUT 请求 */
611
+ async put(path, body, params) {
612
+ return this.request("PUT", path, {
613
+ body,
614
+ params
615
+ });
616
+ }
617
+ /** DELETE 请求 */
618
+ async del(path, params) {
619
+ return this.request("DELETE", path, { params });
620
+ }
621
+ /**
622
+ * 底层请求方法 — 子类可覆写以自定义行为
623
+ *
624
+ * 自动处理:
625
+ * - URL 拼接 (baseUrl + path + params)
626
+ * - 默认 headers 合并
627
+ * - JSON body 序列化
628
+ * - 响应 JSON 解析
629
+ * - 非 2xx 状态码抛出 HttpError
630
+ */
631
+ async request(method, path, options) {
632
+ const url = this.buildUrl(path, options?.params);
633
+ const headers = {
634
+ ...this.defaultHeaders,
635
+ ...options?.headers
636
+ };
637
+ const init = {
638
+ method,
639
+ headers
640
+ };
641
+ if (options?.body !== void 0) {
642
+ headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
643
+ init.body = JSON.stringify(options.body);
644
+ }
645
+ const response = await this.fetchFn(url, init);
646
+ if (!response.ok) {
647
+ const body = await response.text().catch(() => void 0);
648
+ throw new HttpError(response.status, response.statusText, body);
649
+ }
650
+ return response.json();
651
+ }
652
+ /** 构建完整 URL — 子类可覆写以自定义 URL 拼接逻辑 */
653
+ buildUrl(path, params) {
654
+ const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
655
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
656
+ const url = new URL(`${base}${normalizedPath}`, "http://placeholder");
657
+ if (params) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
658
+ if (this.baseUrl.startsWith("http")) return url.toString();
659
+ return `${url.pathname}${url.search}`;
660
+ }
661
+ };
662
+ //#endregion
663
+ //#region ../core/src/intents/base-controller.ts
664
+ /**
665
+ * 抽象 Controller 基类
666
+ *
667
+ * 统一处理:
668
+ * - 类型安全的参数提取 (TParams)
669
+ * - 返回类型约束 (TResult)
670
+ * - try/catch 错误处理 + 可选 fallback
671
+ *
672
+ * @example
673
+ * ```ts
674
+ * class ProductController extends BaseController<{ productId: string }, ProductPage> {
675
+ * readonly intentId = "product-page";
676
+ *
677
+ * async execute(params: { productId: string }, container: Container) {
678
+ * const api = container.resolve<ApiClient>("api");
679
+ * return api.getProduct(params.productId);
680
+ * }
681
+ *
682
+ * fallback(params: { productId: string }, error: Error) {
683
+ * return getMockProduct(params.productId);
684
+ * }
685
+ * }
686
+ * ```
687
+ */
688
+ var BaseController = class {
689
+ /**
690
+ * 错误回退 — 子类可选覆写
691
+ *
692
+ * 当 execute() 抛出异常时调用。
693
+ * 默认行为: 重新抛出原始错误。
694
+ *
695
+ * @param params - Intent 参数
696
+ * @param error - execute() 抛出的错误
697
+ * @returns 回退数据
698
+ */
699
+ fallback(params, error) {
700
+ throw error;
701
+ }
702
+ /**
703
+ * IntentController.perform() 实现
704
+ *
705
+ * 自动 try/catch → fallback 模式。
706
+ */
707
+ async perform(intent, container) {
708
+ const params = intent.params ?? {};
709
+ try {
710
+ return await this.execute(params, container);
711
+ } catch (e) {
712
+ return this.fallback(params, e instanceof Error ? e : new Error(String(e)));
713
+ }
714
+ }
715
+ };
716
+ //#endregion
717
+ //#region ../core/src/data/mapper.ts
718
+ function pipe(...mappers) {
719
+ return (input) => mappers.reduce((acc, mapper) => mapper(acc), input);
720
+ }
721
+ function pipeAsync(...mappers) {
722
+ return async (input) => {
723
+ let acc = input;
724
+ for (const mapper of mappers) acc = await mapper(acc);
725
+ return acc;
726
+ };
727
+ }
728
+ /**
729
+ * 将一个 Mapper 应用到数组的每个元素
730
+ */
731
+ function mapEach(mapper) {
732
+ return (items) => items.map(mapper);
733
+ }
734
+ //#endregion
735
+ //#region ../core/src/bootstrap/define-routes.ts
736
+ /**
737
+ * 声明式注册路由和 Controller
738
+ *
739
+ * - 自动去重: 同一 intentId 的 controller 只注册一次
740
+ * - 路由和 controller 在同一个配置数组中,方便检查一致性
741
+ *
742
+ * @example
743
+ * ```ts
744
+ * defineRoutes(framework, [
745
+ * { path: "/", intentId: "home", controller: new HomeController() },
746
+ * { path: "/product/:id", intentId: "product", controller: new ProductController() },
747
+ * { path: "/search", intentId: "search", controller: new SearchController() },
748
+ * { path: "/charts/:type", intentId: "charts", controller: new ChartsController() },
749
+ * { path: "/charts", intentId: "charts" }, // 同 intentId,不需要重复 controller
750
+ * ]);
751
+ * ```
752
+ */
753
+ function defineRoutes(framework, definitions) {
754
+ const registeredIntents = /* @__PURE__ */ new Set();
755
+ for (const def of definitions) {
756
+ if (def.controller && !registeredIntents.has(def.intentId)) {
757
+ framework.registerIntent(def.controller);
758
+ registeredIntents.add(def.intentId);
759
+ }
760
+ framework.router.add(def.path, def.intentId, {
761
+ renderMode: def.renderMode,
762
+ beforeGuards: def.beforeLoad,
763
+ afterGuards: def.afterLoad
764
+ });
765
+ }
766
+ }
767
+ //#endregion
768
+ //#region ../core/src/utils/lru-map.ts
769
+ /**
770
+ * LruMap — 固定容量的 LRU 缓存
771
+ */
772
+ var LruMap = class {
773
+ map = /* @__PURE__ */ new Map();
774
+ capacity;
775
+ constructor(capacity) {
776
+ this.capacity = capacity;
777
+ }
778
+ get(key) {
779
+ const value = this.map.get(key);
780
+ if (value !== void 0) {
781
+ this.map.delete(key);
782
+ this.map.set(key, value);
783
+ }
784
+ return value;
785
+ }
786
+ set(key, value) {
787
+ if (this.map.has(key)) this.map.delete(key);
788
+ else if (this.map.size >= this.capacity) {
789
+ const oldest = this.map.keys().next().value;
790
+ if (oldest !== void 0) this.map.delete(oldest);
791
+ }
792
+ this.map.set(key, value);
793
+ }
794
+ has(key) {
795
+ return this.map.has(key);
796
+ }
797
+ delete(key) {
798
+ return this.map.delete(key);
799
+ }
800
+ get size() {
801
+ return this.map.size;
802
+ }
803
+ clear() {
804
+ this.map.clear();
805
+ }
806
+ };
807
+ //#endregion
808
+ //#region ../core/src/utils/optional.ts
809
+ function isSome(value) {
810
+ return value !== null && value !== void 0;
811
+ }
812
+ function isNone(value) {
813
+ return value === null || value === void 0;
814
+ }
815
+ //#endregion
816
+ //#region ../core/src/utils/url.ts
817
+ /**
818
+ * URL 工具函数
819
+ */
820
+ /** 移除 URL scheme (https://, http://) */
821
+ function removeScheme(url) {
822
+ return url.replace(/^https?:\/\//, "");
823
+ }
824
+ /** 移除 URL host 部分,保留路径 */
825
+ function removeHost(url) {
826
+ try {
827
+ const parsed = new URL(url);
828
+ return parsed.pathname + parsed.search + parsed.hash;
829
+ } catch {
830
+ return url;
831
+ }
832
+ }
833
+ /** 移除 query 参数 */
834
+ function removeQueryParams(url) {
835
+ return url.split("?")[0];
836
+ }
837
+ /** 获取 URL 的基础路径(无 query、hash) */
838
+ function getBaseUrl(url) {
839
+ return url.split("?")[0].split("#")[0];
840
+ }
841
+ /** 构建 URL(路径 + query 参数) */
842
+ function buildUrl(path, params) {
843
+ if (!params) return path;
844
+ const searchParams = new URLSearchParams();
845
+ for (const [key, value] of Object.entries(params)) if (value !== void 0) searchParams.set(key, value);
846
+ const qs = searchParams.toString();
847
+ return qs ? `${path}?${qs}` : path;
848
+ }
849
+ //#endregion
850
+ //#region ../core/src/utils/uuid.ts
851
+ /**
852
+ * UUID v4 生成器
853
+ */
854
+ function generateUuid() {
855
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
856
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
857
+ const r = Math.random() * 16 | 0;
858
+ return (c === "x" ? r : r & 3 | 8).toString(16);
859
+ });
860
+ }
861
+ //#endregion
862
+ //#region ../core/src/middleware/context.ts
863
+ function parseCookieString(str) {
864
+ const map = /* @__PURE__ */ new Map();
865
+ if (!str) return map;
866
+ for (const pair of str.split(";")) {
867
+ const idx = pair.indexOf("=");
868
+ if (idx === -1) continue;
869
+ const key = pair.slice(0, idx).trim();
870
+ const val = pair.slice(idx + 1).trim();
871
+ if (key) map.set(key, val);
872
+ }
873
+ return map;
874
+ }
875
+ /** 从 Request 对象构建服务端上下文 */
876
+ function createServerContext(options) {
877
+ const { url, intent, container, request } = options;
878
+ const parsed = new URL(url, "http://localhost");
879
+ const cookies = parseCookieString(request?.headers.get("cookie") ?? "");
880
+ return {
881
+ url,
882
+ path: parsed.pathname,
883
+ params: intent.params ?? {},
884
+ intent,
885
+ isServer: true,
886
+ container,
887
+ getCookie: (name) => cookies.get(name),
888
+ getHeader: (name) => request?.headers.get(name) ?? void 0
889
+ };
890
+ }
891
+ /** 从 document.cookie 构建浏览器端上下文 */
892
+ function createBrowserContext(options) {
893
+ const { url, intent, container } = options;
894
+ return {
895
+ url,
896
+ path: new URL(url, window.location.origin).pathname,
897
+ params: intent.params ?? {},
898
+ intent,
899
+ isServer: false,
900
+ container,
901
+ getCookie: (name) => parseCookieString(document.cookie).get(name),
902
+ getHeader: () => void 0
903
+ };
904
+ }
905
+ //#endregion
906
+ //#region ../core/src/middleware/types.ts
907
+ /** 继续执行 */
908
+ function next() {
909
+ return { kind: "next" };
910
+ }
911
+ /** 重定向到新 URL */
912
+ function redirect(url, status = 302) {
913
+ return {
914
+ kind: "redirect",
915
+ url,
916
+ status
917
+ };
918
+ }
919
+ /** URL 重写(不重新加载数据) */
920
+ function rewrite(url) {
921
+ return {
922
+ kind: "rewrite",
923
+ url
924
+ };
925
+ }
926
+ /** 拒绝访问 */
927
+ function deny(status = 403, message = "Forbidden") {
928
+ return {
929
+ kind: "deny",
930
+ status,
931
+ message
932
+ };
933
+ }
934
+ //#endregion
935
+ //#region ../browser/src/action-handlers/external-url-action.ts
936
+ function registerExternalUrlHandler(deps) {
937
+ const { framework, log } = deps;
938
+ framework.onAction(ACTION_KINDS.EXTERNAL_URL, (action) => {
939
+ log.debug(`ExternalUrlAction → ${action.url}`);
940
+ window.open(action.url, "_blank", "noopener,noreferrer");
941
+ });
942
+ }
943
+ //#endregion
944
+ //#region ../browser/src/utils/try-scroll.ts
945
+ const MAX_TRIES = 100;
946
+ const FUDGE = 16;
947
+ let pendingFrame = null;
948
+ function tryScroll(log, getScrollableElement, scrollY) {
949
+ if (pendingFrame !== null) {
950
+ cancelAnimationFrame(pendingFrame);
951
+ pendingFrame = null;
952
+ }
953
+ let tries = 0;
954
+ pendingFrame = requestAnimationFrame(function attempt() {
955
+ if (++tries >= MAX_TRIES) {
956
+ log.warn(`tryScroll: gave up after ${MAX_TRIES} frames, target=${scrollY}`);
957
+ pendingFrame = null;
958
+ return;
959
+ }
960
+ const el = getScrollableElement();
961
+ if (!el) {
962
+ log.warn("could not restore scroll: the scrollable element is missing");
963
+ return;
964
+ }
965
+ const { scrollHeight, offsetHeight } = el;
966
+ if (!(scrollY + offsetHeight <= scrollHeight + FUDGE)) {
967
+ log.info("page is not tall enough for scroll yet", {
968
+ scrollHeight,
969
+ offsetHeight
970
+ });
971
+ pendingFrame = requestAnimationFrame(attempt);
972
+ return;
973
+ }
974
+ el.scrollTop = scrollY;
975
+ log.info("scroll restored to", scrollY);
976
+ pendingFrame = null;
977
+ });
978
+ }
979
+ //#endregion
980
+ //#region ../browser/src/utils/history.ts
981
+ const HISTORY_SIZE_LIMIT = 10;
982
+ var History = class {
983
+ entries;
984
+ log;
985
+ getScrollablePageElement;
986
+ currentStateId;
987
+ constructor(log, options, sizeLimit = HISTORY_SIZE_LIMIT) {
988
+ this.entries = new LruMap(sizeLimit);
989
+ this.log = log;
990
+ this.getScrollablePageElement = options.getScrollablePageElement;
991
+ }
992
+ replaceState(state, url) {
993
+ const id = generateUuid();
994
+ window.history.replaceState({ id }, "", url);
995
+ this.currentStateId = id;
996
+ this.entries.set(id, {
997
+ state,
998
+ scrollY: 0
999
+ });
1000
+ this.scrollTop = 0;
1001
+ this.log.info("replaceState", state, url, id);
1002
+ }
1003
+ pushState(state, url) {
1004
+ const id = generateUuid();
1005
+ window.history.pushState({ id }, "", url);
1006
+ this.currentStateId = id;
1007
+ this.entries.set(id, {
1008
+ state,
1009
+ scrollY: 0
1010
+ });
1011
+ this.scrollTop = 0;
1012
+ this.log.info("pushState", state, url, id);
1013
+ }
1014
+ beforeTransition() {
1015
+ const { state } = window.history;
1016
+ if (!state) return;
1017
+ const oldEntry = this.entries.get(state.id);
1018
+ if (!oldEntry) {
1019
+ this.log.info("current history state evicted from LRU, not saving scroll position");
1020
+ return;
1021
+ }
1022
+ const { scrollTop } = this;
1023
+ this.entries.set(state.id, {
1024
+ ...oldEntry,
1025
+ scrollY: scrollTop
1026
+ });
1027
+ this.log.info("saving scroll position", scrollTop);
1028
+ }
1029
+ onPopState(listener) {
1030
+ window.addEventListener("popstate", async (event) => {
1031
+ this.currentStateId = event.state?.id;
1032
+ if (!this.currentStateId) this.log.warn("encountered a null event.state.id in onPopState event:", window.location.href);
1033
+ this.log.info("popstate", this.entries, this.currentStateId);
1034
+ const entry = this.currentStateId ? this.entries.get(this.currentStateId) : void 0;
1035
+ await listener(window.location.href, entry?.state);
1036
+ if (!entry) return;
1037
+ const { scrollY } = entry;
1038
+ this.log.info("restoring scroll to", scrollY);
1039
+ tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
1040
+ });
1041
+ }
1042
+ /** 仅推入 URL,不缓存页面状态(用于页面加载失败场景) */
1043
+ pushUrl(url) {
1044
+ const id = generateUuid();
1045
+ window.history.pushState({ id }, "", url);
1046
+ this.currentStateId = id;
1047
+ this.scrollTop = 0;
1048
+ this.log.info("pushUrl (no state)", url, id);
1049
+ }
1050
+ /** 仅替换 URL,不缓存页面状态(用于页面加载失败场景) */
1051
+ replaceUrl(url) {
1052
+ const id = generateUuid();
1053
+ window.history.replaceState({ id }, "", url);
1054
+ this.currentStateId = id;
1055
+ this.scrollTop = 0;
1056
+ this.log.info("replaceUrl (no state)", url, id);
1057
+ }
1058
+ updateState(update) {
1059
+ if (!this.currentStateId) {
1060
+ this.log.warn("failed: encountered a null currentStateId inside updateState");
1061
+ return;
1062
+ }
1063
+ const currentState = this.entries.get(this.currentStateId);
1064
+ const newState = update(currentState?.state);
1065
+ this.log.info("updateState", newState, this.currentStateId);
1066
+ this.entries.set(this.currentStateId, {
1067
+ ...currentState,
1068
+ state: newState
1069
+ });
1070
+ }
1071
+ get scrollTop() {
1072
+ return this.getScrollablePageElement()?.scrollTop || 0;
1073
+ }
1074
+ set scrollTop(scrollTop) {
1075
+ const element = this.getScrollablePageElement();
1076
+ if (element) element.scrollTop = scrollTop;
1077
+ }
1078
+ };
1079
+ //#endregion
1080
+ //#region ../browser/src/action-handlers/flow-action.ts
1081
+ function registerFlowActionHandler(deps) {
1082
+ const { framework, log, callbacks, updateApp } = deps;
1083
+ let isFirstPage = true;
1084
+ let navigationId = 0;
1085
+ /** 重定向循环保护计数器 */
1086
+ const MAX_REDIRECTS = 5;
1087
+ const defaultGetScrollable = () => document.getElementById("scrollable-page-override") || document.getElementById("scrollable-page") || document.documentElement;
1088
+ const history = new History(log, { getScrollablePageElement: deps.getScrollablePageElement ?? defaultGetScrollable });
1089
+ /**
1090
+ * 核心导航逻辑(支持递归重定向)
1091
+ * @param redirectCount 当前重定向次数,用于循环保护
1092
+ */
1093
+ async function navigateTo(url, redirectCount, thisNav) {
1094
+ if (redirectCount >= MAX_REDIRECTS) {
1095
+ log.error(`Navigation redirect loop detected (${MAX_REDIRECTS} redirects), stopping at: ${url}`);
1096
+ return;
1097
+ }
1098
+ const shouldReplace = isFirstPage || url === window.location.pathname + window.location.search;
1099
+ const match = framework.routeUrl(url);
1100
+ if (!match) {
1101
+ log.warn(`FlowAction: no route for ${url}`);
1102
+ return;
1103
+ }
1104
+ const navCtx = createBrowserContext({
1105
+ url,
1106
+ intent: match.intent,
1107
+ container: framework.container
1108
+ });
1109
+ const beforeResult = await framework.runBeforeLoad(navCtx, match.beforeGuards);
1110
+ if (beforeResult.kind === "redirect") {
1111
+ log.debug(`beforeLoad → redirect to ${beforeResult.url}`);
1112
+ await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
1113
+ return;
1114
+ }
1115
+ if (beforeResult.kind === "deny") {
1116
+ log.warn(`beforeLoad → denied (${beforeResult.status}): ${beforeResult.message}`);
1117
+ return;
1118
+ }
1119
+ if (beforeResult.kind === "rewrite") {
1120
+ log.debug(`beforeLoad → rewrite to ${beforeResult.url}`);
1121
+ await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
1122
+ return;
1123
+ }
1124
+ const pagePromise = framework.dispatch(match.intent);
1125
+ await Promise.race([pagePromise, new Promise((r) => setTimeout(r, 500))]).catch(() => {});
1126
+ if (thisNav !== navigationId) {
1127
+ log.info("FlowAction superseded by newer navigation", url);
1128
+ return;
1129
+ }
1130
+ history.beforeTransition();
1131
+ updateApp({
1132
+ page: pagePromise.then(async (page) => {
1133
+ if (thisNav !== navigationId) {
1134
+ log.info("FlowAction commit superseded", url);
1135
+ return page;
1136
+ }
1137
+ const postCtx = {
1138
+ ...navCtx,
1139
+ page
1140
+ };
1141
+ const afterResult = await framework.runAfterLoad(postCtx, match.afterGuards);
1142
+ if (afterResult.kind === "redirect") {
1143
+ log.debug(`afterLoad → redirect to ${afterResult.url}`);
1144
+ navigateTo(afterResult.url, redirectCount + 1, thisNav);
1145
+ return page;
1146
+ }
1147
+ let canonicalURL = url;
1148
+ if (afterResult.kind === "rewrite") {
1149
+ canonicalURL = afterResult.url;
1150
+ log.debug(`afterLoad → rewrite URL to ${canonicalURL}`);
1151
+ }
1152
+ if (afterResult.kind === "deny") {
1153
+ log.warn(`afterLoad → denied (${afterResult.status})`);
1154
+ return page;
1155
+ }
1156
+ if (shouldReplace) history.replaceState({ page }, canonicalURL);
1157
+ else history.pushState({ page }, canonicalURL);
1158
+ callbacks.onNavigate(new URL(canonicalURL, window.location.origin).pathname);
1159
+ didEnterPage(page);
1160
+ return page;
1161
+ }, (error) => {
1162
+ if (thisNav === navigationId) {
1163
+ const canonicalURL = url;
1164
+ if (shouldReplace) history.replaceUrl(canonicalURL);
1165
+ else history.pushUrl(canonicalURL);
1166
+ callbacks.onNavigate(new URL(canonicalURL, window.location.origin).pathname);
1167
+ }
1168
+ throw error;
1169
+ }),
1170
+ isFirstPage
1171
+ });
1172
+ isFirstPage = false;
1173
+ }
1174
+ framework.onAction(ACTION_KINDS.FLOW, async (action) => {
1175
+ const flowAction = action;
1176
+ const url = flowAction.url;
1177
+ log.debug(`FlowAction → ${url}`);
1178
+ if (flowAction.presentationContext === "modal") {
1179
+ const match = framework.routeUrl(url);
1180
+ if (match) {
1181
+ const page = await framework.dispatch(match.intent);
1182
+ callbacks.onModal(page);
1183
+ }
1184
+ return;
1185
+ }
1186
+ await navigateTo(url, 0, ++navigationId);
1187
+ });
1188
+ history.onPopState(async (url, cachedState) => {
1189
+ log.debug(`popstate → ${url}, cached=${!!cachedState}`);
1190
+ callbacks.onNavigate(new URL(url).pathname);
1191
+ if (cachedState) {
1192
+ const { page } = cachedState;
1193
+ didEnterPage(page);
1194
+ updateApp({
1195
+ page,
1196
+ isFirstPage
1197
+ });
1198
+ return;
1199
+ }
1200
+ const parsed = new URL(url);
1201
+ const routeMatch = framework.routeUrl(parsed.pathname + parsed.search);
1202
+ if (!routeMatch) {
1203
+ log.error("received popstate without data, but URL was unroutable:", url);
1204
+ didEnterPage(null);
1205
+ updateApp({
1206
+ page: Promise.reject(/* @__PURE__ */ new Error("404")),
1207
+ isFirstPage
1208
+ });
1209
+ return;
1210
+ }
1211
+ const navCtx = createBrowserContext({
1212
+ url: parsed.pathname + parsed.search,
1213
+ intent: routeMatch.intent,
1214
+ container: framework.container
1215
+ });
1216
+ const beforeResult = await framework.runBeforeLoad(navCtx, routeMatch.beforeGuards);
1217
+ if (beforeResult.kind === "redirect") {
1218
+ log.debug(`popstate beforeLoad → redirect to ${beforeResult.url}`);
1219
+ const thisNav = ++navigationId;
1220
+ await navigateTo(beforeResult.url, 0, thisNav);
1221
+ return;
1222
+ }
1223
+ if (beforeResult.kind === "deny" || beforeResult.kind === "rewrite") {
1224
+ if (beforeResult.kind === "deny") log.warn(`popstate beforeLoad → denied`);
1225
+ else {
1226
+ const thisNav = ++navigationId;
1227
+ await navigateTo(beforeResult.url, 0, thisNav);
1228
+ }
1229
+ return;
1230
+ }
1231
+ const pagePromise = framework.dispatch(routeMatch.intent);
1232
+ await Promise.race([pagePromise, new Promise((r) => setTimeout(r, 500))]).catch(() => {});
1233
+ updateApp({
1234
+ page: pagePromise.then((page) => {
1235
+ didEnterPage(page);
1236
+ return page;
1237
+ }),
1238
+ isFirstPage
1239
+ });
1240
+ });
1241
+ function didEnterPage(page) {
1242
+ (async () => {
1243
+ try {
1244
+ if (page) framework.didEnterPage(page);
1245
+ } catch (e) {
1246
+ log.error("didEnterPage error:", e);
1247
+ }
1248
+ })();
1249
+ }
1250
+ }
1251
+ //#endregion
1252
+ //#region ../browser/src/action-handlers/register.ts
1253
+ function registerActionHandlers(deps) {
1254
+ const { framework, log, callbacks, updateApp } = deps;
1255
+ registerFlowActionHandler({
1256
+ framework,
1257
+ log,
1258
+ callbacks,
1259
+ updateApp,
1260
+ getScrollablePageElement: deps.getScrollablePageElement
1261
+ });
1262
+ registerExternalUrlHandler({
1263
+ framework,
1264
+ log
1265
+ });
1266
+ }
1267
+ //#endregion
1268
+ //#region ../browser/src/server-data.ts
1269
+ /**
1270
+ * Server Data (browser side) — 从 DOM 反序列化服务端嵌入数据
1271
+ */
1272
+ /** DOM 中嵌入数据的 script 标签 ID */
1273
+ const SERVER_DATA_ID = "serialized-server-data";
1274
+ /**
1275
+ * 从 DOM 反序列化服务端嵌入的数据。
1276
+ * 读取 `<script id="serialized-server-data">` 的内容并移除标签。
1277
+ */
1278
+ function deserializeServerData() {
1279
+ const script = document.getElementById(SERVER_DATA_ID);
1280
+ if (!script?.textContent) return void 0;
1281
+ script.parentNode?.removeChild(script);
1282
+ try {
1283
+ return JSON.parse(script.textContent);
1284
+ } catch {
1285
+ return;
1286
+ }
1287
+ }
1288
+ /**
1289
+ * 从 DOM 提取 SSR 数据并构建 PrefetchedIntents 实例。
1290
+ * 替代原来的 PrefetchedIntents.fromDom()。
1291
+ */
1292
+ function createPrefetchedIntentsFromDom() {
1293
+ const data = deserializeServerData();
1294
+ if (!data || !Array.isArray(data)) return PrefetchedIntents.empty();
1295
+ return PrefetchedIntents.fromArray(data);
1296
+ }
1297
+ //#endregion
1298
+ //#region ../browser/src/start-app.ts
1299
+ /**
1300
+ * 启动客户端应用
1301
+ *
1302
+ * 自动执行 hydration 全流程。
1303
+ */
1304
+ async function startBrowserApp(config) {
1305
+ const { bootstrap, defaultLocale = "en", mountId = "app", mount, callbacks } = config;
1306
+ const prefetchedIntents = createPrefetchedIntentsFromDom();
1307
+ const framework = Framework.create({ prefetchedIntents });
1308
+ bootstrap(framework);
1309
+ const log = framework.container.resolve(DEP_KEYS.LOGGER_FACTORY).loggerFor("browser");
1310
+ const initialAction = framework.routeUrl(window.location.pathname + window.location.search);
1311
+ const locale = document.documentElement.lang || defaultLocale;
1312
+ const updateApp = mount(document.getElementById(mountId), {
1313
+ framework,
1314
+ locale
1315
+ });
1316
+ registerActionHandlers({
1317
+ framework,
1318
+ log,
1319
+ callbacks,
1320
+ updateApp,
1321
+ getScrollablePageElement: config.getScrollablePageElement
1322
+ });
1323
+ if (initialAction) await framework.perform(initialAction.action);
1324
+ else updateApp({
1325
+ page: Promise.reject(/* @__PURE__ */ new Error("404")),
1326
+ isFirstPage: true
1327
+ });
1328
+ }
1329
+ //#endregion
1330
+ //#region ../ssr/src/render.ts
1331
+ /**
1332
+ * ssrRender — 通用 SSR 渲染管线
1333
+ *
1334
+ * 1. 创建 Framework + 注册 Controllers
1335
+ * 2. routeUrl → Intent
1336
+ * 3. dispatch → Page 数据
1337
+ * 4. 调用应用层提供的渲染函数
1338
+ */
1339
+ async function ssrRender(options) {
1340
+ const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext } = options;
1341
+ const mergedConfig = ssrContext?.fetch ? {
1342
+ ...frameworkConfig,
1343
+ fetch: ssrContext.fetch
1344
+ } : frameworkConfig;
1345
+ const framework = Framework.create(mergedConfig);
1346
+ bootstrap(framework);
1347
+ try {
1348
+ const parsed = new URL(url, "http://localhost");
1349
+ const fullPath = parsed.pathname + parsed.search;
1350
+ const match = framework.routeUrl(fullPath);
1351
+ if (match?.renderMode === "csr") return {
1352
+ html: "",
1353
+ head: "",
1354
+ css: "",
1355
+ serverData: [],
1356
+ renderMode: "csr"
1357
+ };
1358
+ let page;
1359
+ let serverData = [];
1360
+ if (match) {
1361
+ const navCtx = createServerContext({
1362
+ url: fullPath,
1363
+ intent: match.intent,
1364
+ container: framework.container,
1365
+ request: ssrContext?.request
1366
+ });
1367
+ const beforeResult = await framework.runBeforeLoad(navCtx, match.beforeGuards);
1368
+ if (beforeResult.kind !== "next") {
1369
+ const earlyReturn = handleMiddlewareResult(beforeResult, getErrorPage, renderApp, framework);
1370
+ if (earlyReturn) return earlyReturn;
1371
+ }
1372
+ try {
1373
+ page = await framework.dispatch(match.intent);
1374
+ serverData = [{
1375
+ intent: match.intent,
1376
+ data: page
1377
+ }];
1378
+ } catch (e) {
1379
+ console.error(`[SSR] dispatch failed for intent "${match.intent.id}":`, e);
1380
+ page = getErrorPage(500, "Internal error");
1381
+ }
1382
+ const postCtx = {
1383
+ ...navCtx,
1384
+ page
1385
+ };
1386
+ const afterResult = await framework.runAfterLoad(postCtx, match.afterGuards);
1387
+ if (afterResult.kind !== "next") {
1388
+ const lateReturn = handleMiddlewareResult(afterResult, getErrorPage, renderApp, framework);
1389
+ if (lateReturn) return lateReturn;
1390
+ }
1391
+ } else page = getErrorPage(404, "Page not found");
1392
+ const result = renderApp(page, framework);
1393
+ return {
1394
+ html: result.html,
1395
+ head: result.head,
1396
+ css: result.css,
1397
+ serverData,
1398
+ renderMode: match?.renderMode
1399
+ };
1400
+ } finally {
1401
+ framework.dispose();
1402
+ }
1403
+ }
1404
+ /**
1405
+ * 将中间件结果转换为 SSRRenderResult(如果需要短路返回)。
1406
+ * 返回 null 表示继续正常流程。
1407
+ */
1408
+ function handleMiddlewareResult(result, getErrorPage, renderApp, framework) {
1409
+ switch (result.kind) {
1410
+ case "next": return null;
1411
+ case "redirect": return {
1412
+ html: "",
1413
+ head: "",
1414
+ css: "",
1415
+ serverData: [],
1416
+ redirect: {
1417
+ url: result.url,
1418
+ status: result.status
1419
+ }
1420
+ };
1421
+ case "rewrite": return {
1422
+ html: "",
1423
+ head: "",
1424
+ css: "",
1425
+ serverData: [],
1426
+ redirect: {
1427
+ url: result.url,
1428
+ status: 301
1429
+ }
1430
+ };
1431
+ case "deny": {
1432
+ const rendered = renderApp(getErrorPage(result.status, result.message), framework);
1433
+ return {
1434
+ html: rendered.html,
1435
+ head: rendered.head,
1436
+ css: rendered.css,
1437
+ serverData: []
1438
+ };
1439
+ }
1440
+ }
1441
+ }
1442
+ //#endregion
1443
+ //#region ../ssr/src/create-render.ts
1444
+ /**
1445
+ * 创建 render 函数
1446
+ *
1447
+ * @returns `render(url, locale, ssrContext?)` — 供 @finesoft/server SSRModule 使用
1448
+ */
1449
+ function createSSRRender(config) {
1450
+ const { bootstrap, getErrorPage, renderApp, frameworkConfig } = config;
1451
+ return (url, locale, ssrContext) => ssrRender({
1452
+ url,
1453
+ frameworkConfig: frameworkConfig ?? {},
1454
+ bootstrap,
1455
+ getErrorPage,
1456
+ renderApp: (page) => renderApp(page, locale),
1457
+ ssrContext
1458
+ });
1459
+ }
1460
+ //#endregion
1461
+ //#region ../ssr/src/server-data.ts
1462
+ const HTML_REPLACEMENTS = {
1463
+ "<": "\\u003C",
1464
+ ">": "\\u003E",
1465
+ "/": "\\u002F",
1466
+ "\u2028": "\\u2028",
1467
+ "\u2029": "\\u2029"
1468
+ };
1469
+ const HTML_ESCAPE_PATTERN = /[<>/\u2028\u2029]/g;
1470
+ function serializeServerData(data) {
1471
+ return JSON.stringify(data).replace(HTML_ESCAPE_PATTERN, (match) => HTML_REPLACEMENTS[match] ?? match);
1472
+ }
1473
+ //#endregion
1474
+ //#region ../server/src/proxy.ts
1475
+ /**
1476
+ * 校验代理路径,防止 SSRF(协议相对 URL 绕过)。
1477
+ * 返回规范化的路径,或 null 表示非法。
1478
+ */
1479
+ function sanitizeProxyPath(raw) {
1480
+ if (raw.startsWith("//")) return null;
1481
+ return raw.startsWith("/") ? raw : `/${raw}`;
1482
+ }
1483
+ /**
1484
+ * 校验代理配置合法性。
1485
+ * 在注册时(启动阶段)调用,非法配置直接抛错阻止启动。
1486
+ */
1487
+ function validateConfig(config) {
1488
+ if (!config.prefix.startsWith("/")) throw new Error(`[proxy] prefix must start with "/": "${config.prefix}"`);
1489
+ if (!config.target.startsWith("https://")) throw new Error(`[proxy] target must use HTTPS: "${config.target}"`);
1490
+ }
1491
+ /**
1492
+ * 注册声明式代理路由到 Hono app(运行时使用:dev / preview / createServer)
1493
+ */
1494
+ function registerProxyRoutes(app, configs) {
1495
+ for (const config of configs) {
1496
+ validateConfig(config);
1497
+ const methods = config.methods ?? ["all"];
1498
+ const pattern = `${config.prefix}/*`;
1499
+ const handler = async (c) => {
1500
+ const subPath = sanitizeProxyPath(c.req.path.replace(config.prefix, ""));
1501
+ if (!subPath) return c.text("Invalid path", 400);
1502
+ const targetUrl = new URL(subPath, config.target);
1503
+ new URL(c.req.url).searchParams.forEach((v, k) => targetUrl.searchParams.set(k, v));
1504
+ const headers = { ...config.headers };
1505
+ if (config.auth) {
1506
+ const token = process.env[config.auth.envKey] ?? "";
1507
+ if (token) headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
1508
+ }
1509
+ try {
1510
+ const resp = await fetch(targetUrl.toString(), {
1511
+ headers,
1512
+ redirect: config.followRedirects ? "follow" : "manual"
1513
+ });
1514
+ const body = await resp.text();
1515
+ const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
1516
+ if (config.cache) respHeaders["Cache-Control"] = config.cache;
1517
+ return c.newResponse(body, resp.status, respHeaders);
1518
+ } catch (e) {
1519
+ console.error(`[Proxy ${config.prefix}]`, e);
1520
+ return c.json({ error: "Proxy request failed" }, 502);
1521
+ }
1522
+ };
1523
+ for (const method of methods) app[method](pattern, handler);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * 生成代理路由的内联代码(用于 serverless/edge 入口,避免运行时依赖)
1528
+ */
1529
+ function generateProxyCode(configs) {
1530
+ if (!configs || configs.length === 0) return "";
1531
+ for (const config of configs) validateConfig(config);
1532
+ const blocks = [];
1533
+ blocks.push(`
1534
+ // ─── 框架声明式代理路由 ───
1535
+ function _sanitizeProxyPath(raw) {
1536
+ if (raw.startsWith("//")) return null;
1537
+ return raw.startsWith("/") ? raw : "/" + raw;
1538
+ }
1539
+ `);
1540
+ for (const config of configs) {
1541
+ const methods = config.methods ?? ["all"];
1542
+ const pattern = `"${config.prefix}/*"`;
1543
+ const headersJson = JSON.stringify(config.headers ?? {});
1544
+ const cacheStr = config.cache ? JSON.stringify(config.cache) : "null";
1545
+ const redirect = config.followRedirects ? "\"follow\"" : "\"manual\"";
1546
+ let authCode = "";
1547
+ if (config.auth) authCode = `
1548
+ const _token = (typeof process !== "undefined" && process.env && process.env[${JSON.stringify(config.auth.envKey)}]) || "";
1549
+ if (_token) _headers.Authorization = "${config.auth.type === "bearer" ? "Bearer " : "Basic "}" + _token;`;
1550
+ const handlerCode = `async (c) => {
1551
+ const _sub = _sanitizeProxyPath(c.req.path.replace(${JSON.stringify(config.prefix)}, ""));
1552
+ if (!_sub) return c.text("Invalid path", 400);
1553
+ const _target = new URL(_sub, ${JSON.stringify(config.target)});
1554
+ const _reqUrl = new URL(c.req.url);
1555
+ _reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
1556
+ const _headers = ${headersJson};${authCode}
1557
+ try {
1558
+ const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect} });
1559
+ const _body = await _resp.text();
1560
+ const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
1561
+ if (${cacheStr}) _rh["Cache-Control"] = ${cacheStr};
1562
+ return c.newResponse(_body, _resp.status, _rh);
1563
+ } catch (_e) {
1564
+ console.error("[Proxy ${config.prefix}]", _e);
1565
+ return c.json({ error: "Proxy request failed" }, 502);
1566
+ }
1567
+ }`;
1568
+ for (const method of methods) blocks.push(`app.${method}(${pattern}, ${handlerCode});`);
1569
+ }
1570
+ return blocks.join("\n");
1571
+ }
1572
+ //#endregion
1573
+ //#region ../server/src/adapters/shared.ts
1574
+ /**
1575
+ * 适配器共享工具函数
1576
+ *
1577
+ * 提供 generateSSREntry / buildBundle / copyStaticAssets 三个方法,
1578
+ * 避免各适配器重复实现相同逻辑。
1579
+ */
1580
+ const BUILD_TOOL_EXTERNALS = [
1581
+ "vite",
1582
+ "esbuild",
1583
+ "rollup",
1584
+ "fsevents",
1585
+ "lightningcss"
1586
+ ];
1587
+ /**
1588
+ * 生成 SSR serverless/edge 入口源码
1589
+ *
1590
+ * 内联 parseAcceptLanguage / injectSSR 以避免
1591
+ * @finesoft/front → @finesoft/server → vite-plugin → import("vite") 依赖链。
1592
+ */
1593
+ function generateSSREntry(ctx, opts) {
1594
+ const setupImport = ctx.setupPath ? `import _setupDefault from "./${ctx.setupPath}";` : ``;
1595
+ const setupCall = ctx.setupPath ? `if (typeof _setupDefault === "function") await _setupDefault(app);` : ``;
1596
+ const locales = JSON.stringify(ctx.locales);
1597
+ const defaultLocale = JSON.stringify(ctx.defaultLocale);
1598
+ const renderModes = JSON.stringify(ctx.renderModes ?? {});
1599
+ const cacheImpl = opts.platformCache ? opts.platformCache : `
1600
+ const ISR_CACHE_MAX = 1000;
1601
+ const _isrMap = new Map();
1602
+ async function platformCacheGet(url) {
1603
+ return _isrMap.get(url) ?? null;
1604
+ }
1605
+ async function platformCacheSet(url, html) {
1606
+ if (_isrMap.size >= ISR_CACHE_MAX) {
1607
+ const first = _isrMap.keys().next().value;
1608
+ _isrMap.delete(first);
1609
+ }
1610
+ _isrMap.set(url, html);
1611
+ }`;
1612
+ return `
1613
+ import { Hono } from "hono";
1614
+ ${opts.platformImport}
1615
+ import { render, serializeServerData } from "./${ctx.ssrEntry}";
1616
+ ${setupImport}
1617
+
1618
+ const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
1619
+ const LOCALES = ${locales};
1620
+ const DEFAULT_LOCALE = ${defaultLocale};
1621
+ const RENDER_MODES = ${renderModes};
1622
+ ${cacheImpl}
1623
+
1624
+ function parseAcceptLanguage(header) {
1625
+ if (!header) return DEFAULT_LOCALE;
1626
+ const langs = header.split(",").map(p => {
1627
+ const [l, q] = p.trim().split(";q=");
1628
+ return { l: l.trim().toLowerCase(), q: q ? (+q || 0) : 1 };
1629
+ }).sort((a, b) => b.q - a.q);
1630
+ for (const { l } of langs) {
1631
+ const prefix = l.split("-")[0];
1632
+ if (LOCALES.includes(prefix)) return prefix;
1633
+ }
1634
+ return DEFAULT_LOCALE;
1635
+ }
1636
+
1637
+ function injectSSR(t, locale, head, css, html, data) {
1638
+ return t
1639
+ .replace("<!--ssr-lang-->", locale)
1640
+ .replace("<!--ssr-head-->", head + "\\n<style>" + css + "</style>")
1641
+ .replace("<!--ssr-body-->", html)
1642
+ .replace("<!--ssr-data-->", '<script id="serialized-server-data" type="application/json">' + data + "<\/script>");
1643
+ }
1644
+
1645
+ function injectCSRShell(t, locale) {
1646
+ return t
1647
+ .replace("<!--ssr-lang-->", locale)
1648
+ .replace("<!--ssr-head-->", "")
1649
+ .replace("<!--ssr-body-->", "")
1650
+ .replace("<!--ssr-data-->", "");
1651
+ }
1652
+
1653
+ function matchRenderMode(url) {
1654
+ const path = url.split("?")[0];
1655
+ if (RENDER_MODES[path]) return RENDER_MODES[path];
1656
+ for (const [pattern, mode] of Object.entries(RENDER_MODES)) {
1657
+ if (pattern.includes("*")) {
1658
+ const escaped = pattern.replace(/[.+?^\${}()|[\\]\\\\]/g, "\\\\$&");
1659
+ const re = new RegExp("^" + escaped.replace(/\\*/g, ".*") + "$");
1660
+ if (re.test(path)) return mode;
1661
+ }
1662
+ }
1663
+ return null;
1664
+ }
1665
+
1666
+ const app = new Hono();
1667
+ ${generateProxyCode(ctx.proxies ?? [])}
1668
+ ${setupCall}
1669
+ ${opts.platformMiddleware ?? ""}
1670
+
1671
+ // 内部 fetch 回环:SSR 控制器的 fetch 请求直接走 Hono 内存路由
1672
+ // 深度通过请求头传递,并发安全且能跨渲染正确追踪递归
1673
+ const _SSR_DEPTH_HEADER = "x-ssr-depth";
1674
+ const _MAX_SSR_DEPTH = 5;
1675
+
1676
+ function _createInternalFetch(depth) {
1677
+ return function(input, init) {
1678
+ if (typeof input === "string" && input.startsWith("/")) {
1679
+ const req = new Request("http://localhost" + input, init);
1680
+ req.headers.set(_SSR_DEPTH_HEADER, String(depth));
1681
+ return app.fetch(req);
1682
+ }
1683
+ return globalThis.fetch(input, init);
1684
+ };
1685
+ }
1686
+
1687
+ app.get("*", async (c) => {
1688
+ // 递归深度保护:从请求头读取 SSR 深度
1689
+ const _ssrDepth = parseInt(c.req.header(_SSR_DEPTH_HEADER) || "0", 10);
1690
+ if (_ssrDepth >= _MAX_SSR_DEPTH) {
1691
+ return c.text("SSR recursion loop detected", 508);
1692
+ }
1693
+
1694
+ const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1695
+ try {
1696
+ const locale = parseAcceptLanguage(c.req.header("accept-language"));
1697
+
1698
+ // Vite 配置级别覆盖: CSR 直接返回空壳
1699
+ const overrideMode = matchRenderMode(url);
1700
+ if (overrideMode === "csr") {
1701
+ return c.html(injectCSRShell(TEMPLATE, locale));
1702
+ }
1703
+
1704
+ // ISR 缓存命中(key 含 locale,避免跨语言缓存污染)
1705
+ const _cacheKey = locale + ":" + url;
1706
+ const cached = await platformCacheGet(_cacheKey);
1707
+ if (cached) return c.html(cached);
1708
+
1709
+ const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale, { fetch: _createInternalFetch(_ssrDepth + 1) });
1710
+
1711
+ // 路由级 CSR
1712
+ if (renderMode === "csr") {
1713
+ return c.html(injectCSRShell(TEMPLATE, locale));
1714
+ }
1715
+
1716
+ const serializedData = serializeServerData(serverData);
1717
+ const finalHtml = injectSSR(TEMPLATE, locale, head, css, appHtml, serializedData);
1718
+
1719
+ // Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
1720
+ if (renderMode === "prerender" || overrideMode === "prerender") {
1721
+ await platformCacheSet(_cacheKey, finalHtml);
1722
+ ${opts.platformPrerenderResponseHook ?? ""}
1723
+ }
1724
+
1725
+ return c.html(finalHtml);
1726
+ } catch (e) {
1727
+ console.error("[SSR Error]", e);
1728
+ return c.text("Internal Server Error", 500);
1729
+ }
1730
+ });
1731
+
1732
+ ${opts.platformExport}
1733
+ `;
1734
+ }
1735
+ /** 用 Vite SSR 模式构建 bundle */
1736
+ async function buildBundle(ctx, opts) {
1737
+ await ctx.vite.build({
1738
+ root: ctx.root,
1739
+ build: {
1740
+ ssr: opts.entry,
1741
+ outDir: opts.outDir,
1742
+ emptyOutDir: true,
1743
+ target: opts.target ?? "node18",
1744
+ rollupOptions: { output: { entryFileNames: opts.fileName ?? "index.mjs" } }
1745
+ },
1746
+ ssr: {
1747
+ noExternal: opts.noExternal !== false,
1748
+ external: opts.external ?? BUILD_TOOL_EXTERNALS
1749
+ },
1750
+ resolve: ctx.resolvedResolve,
1751
+ css: ctx.resolvedCss
1752
+ });
1753
+ }
1754
+ /** 复制 dist/client 静态资源到目标目录 */
1755
+ function copyStaticAssets(ctx, destDir, opts) {
1756
+ const { fs, path } = ctx;
1757
+ fs.cpSync(path.resolve(ctx.root, "dist/client"), destDir, { recursive: true });
1758
+ if (opts?.excludeHtml !== false) fs.rmSync(path.join(destDir, "index.html"), { force: true });
1759
+ }
1760
+ /**
1761
+ * 构建时预渲染 prerender 路由。
1762
+ *
1763
+ * 1. 加载路由定义文件,找出 renderMode === "prerender" 的路由
1764
+ * 2. 合并 ctx.renderModes 配置覆盖
1765
+ * 3. 渲染每个 URL × locale
1766
+ */
1767
+ async function prerenderRoutes(ctx) {
1768
+ const { fs, path, root, vite } = ctx;
1769
+ const { pathToFileURL } = await import(
1770
+ /* @vite-ignore */
1771
+ "node:url"
1772
+ );
1773
+ const routesExport = ctx.bootstrapEntry ?? "src/lib/bootstrap.ts";
1774
+ let routes = [];
1775
+ if (fs.existsSync(path.resolve(root, routesExport))) {
1776
+ await vite.build({
1777
+ root,
1778
+ build: {
1779
+ ssr: routesExport,
1780
+ outDir: path.resolve(root, "dist/server"),
1781
+ emptyOutDir: false,
1782
+ rollupOptions: { output: { entryFileNames: "_routes_prerender.mjs" } }
1783
+ },
1784
+ resolve: ctx.resolvedResolve
1785
+ });
1786
+ const routesMod = await import(pathToFileURL(path.resolve(root, "dist/server/_routes_prerender.mjs")).href);
1787
+ routes = routesMod.routes ?? routesMod.default ?? [];
1788
+ fs.rmSync(path.resolve(root, "dist/server/_routes_prerender.mjs"), { force: true });
1789
+ }
1790
+ const prerenderPaths = /* @__PURE__ */ new Set();
1791
+ for (const r of routes) if (r.renderMode === "prerender" && r.path && !r.path.includes(":")) prerenderPaths.add(r.path);
1792
+ if (ctx.renderModes) {
1793
+ for (const [pattern, mode] of Object.entries(ctx.renderModes)) if (mode === "prerender" && !pattern.includes("*") && !pattern.includes(":")) prerenderPaths.add(pattern);
1794
+ }
1795
+ if (prerenderPaths.size === 0) return [];
1796
+ const ssrModule = await import(pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href);
1797
+ const results = [];
1798
+ for (const routePath of prerenderPaths) for (const locale of ctx.locales) {
1799
+ const url = locale === ctx.defaultLocale ? routePath : `/${locale}${routePath === "/" ? "" : routePath}`;
1800
+ try {
1801
+ const { html: appHtml, head, css, serverData } = await ssrModule.render(url, locale);
1802
+ const serializedData = ssrModule.serializeServerData(serverData);
1803
+ const finalHtml = ctx.templateHtml.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", head + "\n<style>" + css + "</style>").replace("<!--ssr-body-->", appHtml).replace("<!--ssr-data-->", "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>");
1804
+ results.push({
1805
+ url,
1806
+ html: finalHtml
1807
+ });
1808
+ } catch (e) {
1809
+ console.warn(` [prerender] Failed to render ${url}:`, e);
1810
+ }
1811
+ }
1812
+ if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes × ${ctx.locales.length} locales)\n`);
1813
+ return results;
1814
+ }
1815
+ //#endregion
1816
+ //#region ../server/src/adapters/cloudflare.ts
1817
+ /**
1818
+ * Cloudflare Pages 适配器
1819
+ *
1820
+ * 生成 dist/cloudflare/ 目录:
1821
+ * - _worker.js — Workers 入口(Hono 原生支持 CF fetch 接口)
1822
+ * - assets/ — 静态资源
1823
+ *
1824
+ * 注意:Cloudflare Workers 不支持原生 Node.js API。
1825
+ * 若 setup 代理使用了 process.env,需在 wrangler.toml 启用 nodejs_compat。
1826
+ */
1827
+ function cloudflareAdapter() {
1828
+ return {
1829
+ name: "cloudflare",
1830
+ async build(ctx) {
1831
+ const { fs, path, root } = ctx;
1832
+ const outputDir = path.resolve(root, "dist/cloudflare");
1833
+ fs.rmSync(outputDir, {
1834
+ recursive: true,
1835
+ force: true
1836
+ });
1837
+ const entrySource = generateSSREntry(ctx, {
1838
+ platformImport: ``,
1839
+ platformExport: `export default app;`,
1840
+ platformCache: `
1841
+ const ISR_CACHE_TTL = 3600; // 1 hour
1842
+ async function platformCacheGet(url) {
1843
+ try {
1844
+ const cache = caches.default;
1845
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
1846
+ const resp = await cache.match(cacheKey);
1847
+ if (resp) return await resp.text();
1848
+ } catch {}
1849
+ return null;
1850
+ }
1851
+ async function platformCacheSet(url, html) {
1852
+ try {
1853
+ const cache = caches.default;
1854
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
1855
+ const resp = new Response(html, {
1856
+ headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=" + ISR_CACHE_TTL },
1857
+ });
1858
+ await cache.put(cacheKey, resp);
1859
+ } catch {}
1860
+ }`
1861
+ });
1862
+ const tempEntry = path.resolve(root, ".cf-entry.tmp.mjs");
1863
+ fs.writeFileSync(tempEntry, entrySource);
1864
+ try {
1865
+ await buildBundle(ctx, {
1866
+ entry: ".cf-entry.tmp.mjs",
1867
+ outDir: outputDir,
1868
+ target: "es2022",
1869
+ fileName: "_worker.js"
1870
+ });
1871
+ copyStaticAssets(ctx, path.resolve(outputDir, "assets"));
1872
+ const prerendered = await prerenderRoutes(ctx);
1873
+ for (const { url, html } of prerendered) {
1874
+ const filePath = url === "/" ? path.join(outputDir, "assets", "index.html") : path.join(outputDir, "assets", url, "index.html");
1875
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
1876
+ fs.writeFileSync(filePath, html);
1877
+ }
1878
+ } finally {
1879
+ fs.rmSync(tempEntry, { force: true });
1880
+ }
1881
+ console.log(" Cloudflare output → dist/cloudflare/\n");
1882
+ }
1883
+ };
1884
+ }
1885
+ //#endregion
1886
+ //#region ../server/src/adapters/netlify.ts
1887
+ /**
1888
+ * Netlify 适配器 — Netlify Functions v2
1889
+ *
1890
+ * 生成:
1891
+ * - .netlify/functions-internal/ssr/index.mjs — Serverless Function
1892
+ * - dist/client/_redirects — 路由重写规则
1893
+ */
1894
+ function netlifyAdapter() {
1895
+ return {
1896
+ name: "netlify",
1897
+ async build(ctx) {
1898
+ const { fs, path, root } = ctx;
1899
+ const funcDir = path.resolve(root, ".netlify/functions-internal/ssr");
1900
+ fs.rmSync(path.resolve(root, ".netlify"), {
1901
+ recursive: true,
1902
+ force: true
1903
+ });
1904
+ const entrySource = generateSSREntry(ctx, {
1905
+ platformImport: `import { handle } from "hono/netlify";`,
1906
+ platformExport: `export default handle(app);
1907
+ export const config = { path: "/*", preferStatic: true };`,
1908
+ platformCache: `
1909
+ const ISR_SWR_TTL = 3600;
1910
+ const ISR_CACHE_MAX = 1000;
1911
+ const _isrMap = new Map();
1912
+ async function platformCacheGet(url) {
1913
+ return _isrMap.get(url) ?? null;
1914
+ }
1915
+ async function platformCacheSet(url, html) {
1916
+ if (_isrMap.size >= ISR_CACHE_MAX) {
1917
+ const first = _isrMap.keys().next().value;
1918
+ _isrMap.delete(first);
1919
+ }
1920
+ _isrMap.set(url, html);
1921
+ }`,
1922
+ platformPrerenderResponseHook: `c.header("Cache-Control", "public, max-age=0, must-revalidate");
1923
+ c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`
1924
+ });
1925
+ const tempEntry = path.resolve(root, ".netlify-entry.tmp.mjs");
1926
+ fs.writeFileSync(tempEntry, entrySource);
1927
+ try {
1928
+ await buildBundle(ctx, {
1929
+ entry: ".netlify-entry.tmp.mjs",
1930
+ outDir: funcDir,
1931
+ target: "node18"
1932
+ });
1933
+ } finally {
1934
+ fs.rmSync(tempEntry, { force: true });
1935
+ }
1936
+ fs.writeFileSync(path.resolve(root, "dist/client/_redirects"), `/* /.netlify/functions/ssr 200\n`);
1937
+ const prerendered = await prerenderRoutes(ctx);
1938
+ const clientDir = path.resolve(root, "dist/client");
1939
+ for (const { url, html } of prerendered) {
1940
+ const filePath = url === "/" ? path.join(clientDir, "index.html") : path.join(clientDir, url, "index.html");
1941
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
1942
+ fs.writeFileSync(filePath, html);
1943
+ }
1944
+ console.log(" Netlify output → .netlify/functions-internal/ssr/\n Publish dir: dist/client/\n");
1945
+ }
1946
+ };
1947
+ }
1948
+ //#endregion
1949
+ //#region ../server/src/adapters/node.ts
1950
+ /**
1951
+ * Node 适配器 — 独立 HTTP 服务器
1952
+ *
1953
+ * 生成 dist/server/index.mjs,使用 @hono/node-server 监听端口。
1954
+ * 运行:node dist/server/index.mjs
1955
+ */
1956
+ function nodeAdapter() {
1957
+ return {
1958
+ name: "node",
1959
+ async build(ctx) {
1960
+ const { fs, path, root } = ctx;
1961
+ const entrySource = generateSSREntry(ctx, {
1962
+ platformImport: `import { serve } from "@hono/node-server";
1963
+ import { readFileSync, existsSync } from "node:fs";
1964
+ import { resolve, dirname } from "node:path";
1965
+ import { fileURLToPath } from "node:url";`,
1966
+ platformMiddleware: `
1967
+ // 预渲染文件中间件:检查 dist/prerender/ 下是否有对应的静态 HTML
1968
+ const __entry_dirname = dirname(fileURLToPath(import.meta.url));
1969
+ const prerenderDir = resolve(__entry_dirname, "../prerender");
1970
+
1971
+ app.use("*", async (c, next) => {
1972
+ const urlPath = c.req.path;
1973
+ const candidates = [
1974
+ resolve(prerenderDir, "." + urlPath, "index.html"),
1975
+ resolve(prerenderDir, "." + urlPath + ".html"),
1976
+ ];
1977
+ if (urlPath === "/") candidates.unshift(resolve(prerenderDir, "index.html"));
1978
+ for (const f of candidates) {
1979
+ if (existsSync(f)) {
1980
+ const html = readFileSync(f, "utf-8");
1981
+ return c.html(html);
1982
+ }
1983
+ }
1984
+ await next();
1985
+ });
1986
+ `,
1987
+ platformExport: `
1988
+ const port = +(process.env.PORT || 3000);
1989
+ serve({ fetch: app.fetch, port }, (info) => {
1990
+ console.log(\`Server running at http://localhost:\${info.port}\`);
1991
+ });
1992
+ `
1993
+ });
1994
+ const tempEntry = path.resolve(root, ".node-entry.tmp.mjs");
1995
+ fs.writeFileSync(tempEntry, entrySource);
1996
+ try {
1997
+ await buildBundle(ctx, {
1998
+ entry: ".node-entry.tmp.mjs",
1999
+ outDir: path.resolve(root, "dist/server"),
2000
+ target: "node18"
2001
+ });
2002
+ } finally {
2003
+ fs.rmSync(tempEntry, { force: true });
2004
+ }
2005
+ const prerendered = await prerenderRoutes(ctx);
2006
+ if (prerendered.length > 0) {
2007
+ const prerenderDir = path.resolve(root, "dist/prerender");
2008
+ fs.mkdirSync(prerenderDir, { recursive: true });
2009
+ for (const { url, html } of prerendered) {
2010
+ const filePath = url === "/" ? path.join(prerenderDir, "index.html") : path.join(prerenderDir, url, "index.html");
2011
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2012
+ fs.writeFileSync(filePath, html);
2013
+ }
2014
+ }
2015
+ console.log(" Node output → dist/server/index.mjs\n Run: node dist/server/index.mjs\n");
2016
+ }
2017
+ };
2018
+ }
2019
+ //#endregion
2020
+ //#region ../server/src/adapters/static.ts
2021
+ function staticAdapter(opts = {}) {
2022
+ return {
2023
+ name: "static",
2024
+ async build(ctx) {
2025
+ const { fs, path, root } = ctx;
2026
+ const outputDir = path.resolve(root, "dist/static");
2027
+ fs.rmSync(outputDir, {
2028
+ recursive: true,
2029
+ force: true
2030
+ });
2031
+ fs.mkdirSync(outputDir, { recursive: true });
2032
+ const { pathToFileURL } = await import(
2033
+ /* @vite-ignore */
2034
+ "node:url"
2035
+ );
2036
+ const ssrModule = await import(pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href);
2037
+ ctx.copyStaticAssets(outputDir, { excludeHtml: true });
2038
+ const { paths: routePaths, defs: routeDefs } = await extractRoutesWithModes(ctx, opts);
2039
+ const allUrls = [];
2040
+ for (const routePath of routePaths) for (const locale of ctx.locales) {
2041
+ const url = locale === ctx.defaultLocale ? routePath : `/${locale}${routePath === "/" ? "" : routePath}`;
2042
+ allUrls.push(url);
2043
+ }
2044
+ console.log(` Pre-rendering ${allUrls.length} pages (${routePaths.length} routes × ${ctx.locales.length} locales)...\n`);
2045
+ for (const url of allUrls) try {
2046
+ const locale = inferLocale(url, ctx.locales, ctx.defaultLocale);
2047
+ const routeDef = routeDefs.find((r) => r.path === stripLocalePrefix(url, ctx.locales));
2048
+ const mode = resolveRenderMode(stripLocalePrefix(url, ctx.locales), routeDef?.renderMode, ctx.renderModes);
2049
+ let finalHtml;
2050
+ if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml, locale);
2051
+ else {
2052
+ const { html: appHtml, head, css, serverData } = await ssrModule.render(url, locale);
2053
+ const serializedData = ssrModule.serializeServerData(serverData);
2054
+ finalHtml = injectSSRForStatic(ctx.templateHtml, locale, head, css, appHtml, serializedData);
2055
+ }
2056
+ const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
2057
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2058
+ fs.writeFileSync(filePath, finalHtml);
2059
+ } catch (e) {
2060
+ console.warn(` [static] Failed to render ${url}:`, e);
2061
+ }
2062
+ console.log(` Static output → dist/static/\n`);
2063
+ }
2064
+ };
2065
+ }
2066
+ /** 从路由文件提取无参数路由 + 合并 dynamicRoutes(含 renderMode) */
2067
+ async function extractRoutesWithModes(ctx, opts) {
2068
+ const routesFile = opts.routesExport ?? "src/lib/bootstrap.ts";
2069
+ const paths = [];
2070
+ const defs = [];
2071
+ try {
2072
+ const { pathToFileURL } = await import(
2073
+ /* @vite-ignore */
2074
+ "node:url"
2075
+ );
2076
+ await ctx.vite.build({
2077
+ root: ctx.root,
2078
+ build: {
2079
+ ssr: routesFile,
2080
+ outDir: ctx.path.resolve(ctx.root, "dist/server"),
2081
+ emptyOutDir: false,
2082
+ rollupOptions: { output: { entryFileNames: "_routes.mjs" } }
2083
+ },
2084
+ resolve: ctx.resolvedResolve
2085
+ });
2086
+ const routesMod = await import(pathToFileURL(ctx.path.resolve(ctx.root, "dist/server/_routes.mjs")).href);
2087
+ const routes = routesMod.routes ?? routesMod.default;
2088
+ if (Array.isArray(routes)) {
2089
+ for (const r of routes) if (r.path && !r.path.includes(":")) {
2090
+ paths.push(r.path);
2091
+ defs.push({
2092
+ path: r.path,
2093
+ renderMode: r.renderMode
2094
+ });
2095
+ }
2096
+ }
2097
+ ctx.fs.rmSync(ctx.path.resolve(ctx.root, "dist/server/_routes.mjs"), { force: true });
2098
+ } catch (e) {
2099
+ console.warn(` [static] Could not load routes from "${routesFile}". Using "/" only.`, e);
2100
+ if (paths.length === 0) paths.push("/");
2101
+ }
2102
+ if (opts.dynamicRoutes) {
2103
+ for (const r of opts.dynamicRoutes) if (!paths.includes(r)) paths.push(r);
2104
+ }
2105
+ if (paths.length === 0) paths.push("/");
2106
+ return {
2107
+ paths,
2108
+ defs
2109
+ };
2110
+ }
2111
+ /** 从 URL 推断 locale */
2112
+ function inferLocale(url, locales, defaultLocale) {
2113
+ const segments = url.split("/").filter(Boolean);
2114
+ if (segments.length > 0 && locales.includes(segments[0])) return segments[0];
2115
+ return defaultLocale;
2116
+ }
2117
+ /** 内联 SSR 注入(同 shared 中的逻辑) */
2118
+ function injectSSRForStatic(template, locale, head, css, html, serializedData) {
2119
+ return template.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", head + "\n<style>" + css + "</style>").replace("<!--ssr-body-->", html).replace("<!--ssr-data-->", "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>");
2120
+ }
2121
+ /** CSR 空壳注入 */
2122
+ function injectCSRShellForStatic(template, locale) {
2123
+ return template.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", "").replace("<!--ssr-body-->", "").replace("<!--ssr-data-->", "");
2124
+ }
2125
+ /** 从 URL 去除 locale 前缀,还原路由路径 */
2126
+ function stripLocalePrefix(url, locales) {
2127
+ const segments = url.split("/").filter(Boolean);
2128
+ if (segments.length > 0 && locales.includes(segments[0])) {
2129
+ const rest = segments.slice(1).join("/");
2130
+ return rest ? `/${rest}` : "/";
2131
+ }
2132
+ return url;
2133
+ }
2134
+ /** 解析最终渲染模式:Vite 配置覆盖 > 路由级 > 默认 "ssr" */
2135
+ function resolveRenderMode(routePath, routeRenderMode, renderModes) {
2136
+ if (renderModes) {
2137
+ if (renderModes[routePath]) return renderModes[routePath];
2138
+ for (const [pattern, mode] of Object.entries(renderModes)) if (pattern.includes("*")) {
2139
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2140
+ if (new RegExp("^" + escaped.replace(/\*/g, ".*") + "$").test(routePath)) return mode;
2141
+ }
2142
+ }
2143
+ return routeRenderMode ?? "ssr";
2144
+ }
2145
+ //#endregion
2146
+ //#region ../server/src/adapters/vercel.ts
2147
+ /**
2148
+ * Vercel 适配器 — Build Output API v3
2149
+ *
2150
+ * 生成 .vercel/output/ 目录:
2151
+ * - config.json — 路由规则
2152
+ * - static/ — 静态资源
2153
+ * - functions/ssr.func/ — Serverless Function
2154
+ */
2155
+ function vercelAdapter() {
2156
+ return {
2157
+ name: "vercel",
2158
+ async build(ctx) {
2159
+ const { fs, path, root } = ctx;
2160
+ const outputDir = path.resolve(root, ".vercel/output");
2161
+ fs.rmSync(outputDir, {
2162
+ recursive: true,
2163
+ force: true
2164
+ });
2165
+ const entrySource = generateSSREntry(ctx, {
2166
+ platformImport: `import { getRequestListener } from "@hono/node-server";`,
2167
+ platformExport: [
2168
+ `const _listener = getRequestListener(app.fetch);`,
2169
+ `export default (req, res) => {`,
2170
+ ` const m = req.headers["x-now-route-matches"];`,
2171
+ ` if (typeof m === "string") {`,
2172
+ ` try {`,
2173
+ ` const p = new URLSearchParams(m);`,
2174
+ ` const c = p.get("1");`,
2175
+ ` if (c != null) {`,
2176
+ ` const qi = (req.url || "").indexOf("?");`,
2177
+ ` const qs = qi !== -1 ? req.url.slice(qi) : "";`,
2178
+ ` req.url = "/" + decodeURIComponent(c) + qs;`,
2179
+ ` }`,
2180
+ ` } catch {}`,
2181
+ ` }`,
2182
+ ` return _listener(req, res);`,
2183
+ `};`
2184
+ ].join("\n")
2185
+ });
2186
+ const tempEntry = path.resolve(root, ".vercel-entry.tmp.mjs");
2187
+ fs.writeFileSync(tempEntry, entrySource);
2188
+ try {
2189
+ const funcDir = path.resolve(root, ".vercel/output/functions/ssr.func");
2190
+ await buildBundle(ctx, {
2191
+ entry: ".vercel-entry.tmp.mjs",
2192
+ outDir: funcDir,
2193
+ target: "node18"
2194
+ });
2195
+ fs.writeFileSync(path.resolve(funcDir, ".vc-config.json"), JSON.stringify({
2196
+ runtime: "nodejs20.x",
2197
+ handler: "index.mjs",
2198
+ launcherType: "Nodejs"
2199
+ }, null, 2));
2200
+ copyStaticAssets(ctx, path.resolve(root, ".vercel/output/static"));
2201
+ fs.writeFileSync(path.resolve(root, ".vercel/output/config.json"), JSON.stringify({
2202
+ version: 3,
2203
+ routes: [{ handle: "filesystem" }, {
2204
+ src: "/(.*)",
2205
+ dest: "/ssr"
2206
+ }]
2207
+ }, null, 2));
2208
+ } finally {
2209
+ fs.rmSync(tempEntry, { force: true });
2210
+ }
2211
+ const prerendered = await prerenderRoutes(ctx);
2212
+ const staticDir = path.resolve(root, ".vercel/output/static");
2213
+ for (const { url, html } of prerendered) {
2214
+ const filePath = url === "/" ? path.join(staticDir, "index.html") : path.join(staticDir, url, "index.html");
2215
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2216
+ fs.writeFileSync(filePath, html);
2217
+ }
2218
+ if (prerendered.length > 0) {
2219
+ const configPath = path.resolve(root, ".vercel/output/config.json");
2220
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2221
+ config.overrides = config.overrides ?? {};
2222
+ for (const { url } of prerendered) {
2223
+ const key = url === "/" ? "index.html" : `${url.replace(/^\//, "")}/index.html`;
2224
+ config.overrides[key] = {
2225
+ path: url === "/" ? "/" : url,
2226
+ contentType: "text/html; charset=utf-8"
2227
+ };
2228
+ }
2229
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
2230
+ }
2231
+ console.log(" Vercel output → .vercel/output/\n");
2232
+ }
2233
+ };
2234
+ }
2235
+ //#endregion
2236
+ //#region ../server/src/adapters/resolve.ts
2237
+ /**
2238
+ * resolveAdapter — 字符串 → Adapter 映射
2239
+ */
2240
+ function resolveAdapter(value) {
2241
+ if (typeof value !== "string") return value;
2242
+ switch (value) {
2243
+ case "vercel": return vercelAdapter();
2244
+ case "cloudflare": return cloudflareAdapter();
2245
+ case "netlify": return netlifyAdapter();
2246
+ case "node": return nodeAdapter();
2247
+ case "static": return staticAdapter();
2248
+ case "auto": return autoAdapter();
2249
+ default: throw new Error(`[finesoft] Unknown adapter: "${value}". Available: vercel, cloudflare, netlify, node, static, auto`);
2250
+ }
2251
+ }
2252
+ //#endregion
2253
+ //#region ../server/src/adapters/auto.ts
2254
+ /**
2255
+ * Auto 适配器 — 根据环境变量自动选择目标平台
2256
+ *
2257
+ * 检测顺序:
2258
+ * VERCEL → vercel
2259
+ * CF_PAGES → cloudflare
2260
+ * NETLIFY → netlify
2261
+ * (default) → node
2262
+ */
2263
+ function autoAdapter() {
2264
+ return {
2265
+ name: "auto",
2266
+ async build(ctx) {
2267
+ const detected = detectPlatform();
2268
+ console.log(` [auto] Detected platform: ${detected}\n`);
2269
+ return resolveAdapter(detected).build(ctx);
2270
+ }
2271
+ };
2272
+ }
2273
+ function detectPlatform() {
2274
+ if (process.env.VERCEL) return "vercel";
2275
+ if (process.env.CF_PAGES) return "cloudflare";
2276
+ if (process.env.NETLIFY) return "netlify";
2277
+ return "node";
2278
+ }
2279
+ //#endregion
2280
+ //#region ../server/src/runtime.ts
2281
+ /** 检测当前运行时环境 */
2282
+ function detectRuntime() {
2283
+ return {
2284
+ isDeno: typeof globalThis.Deno !== "undefined",
2285
+ isBun: typeof globalThis.Bun !== "undefined",
2286
+ isVercel: !!process.env.VERCEL,
2287
+ isProduction: process.env.NODE_ENV === "production"
2288
+ };
2289
+ }
2290
+ /**
2291
+ * 从 `import.meta.url` 推导项目根路径
2292
+ *
2293
+ * @param importMetaUrl - 调用方的 `import.meta.url`
2294
+ * @param levelsUp - 向上移动多少级(默认 0,即调用方所在目录就是项目根)
2295
+ */
2296
+ async function resolveRoot(importMetaUrl, levelsUp = 0) {
2297
+ if (typeof globalThis.Deno !== "undefined") {
2298
+ let url = new URL(importMetaUrl);
2299
+ for (let i = 0; i < levelsUp; i++) url = new URL("..", url);
2300
+ return url.pathname;
2301
+ }
2302
+ const path = await import(
2303
+ /* @vite-ignore */
2304
+ "node:path"
2305
+ );
2306
+ const { fileURLToPath } = await import(
2307
+ /* @vite-ignore */
2308
+ "node:url"
2309
+ );
2310
+ let dir = path.normalize(path.dirname(fileURLToPath(importMetaUrl)));
2311
+ for (let i = 0; i < levelsUp; i++) dir = path.resolve(dir, "..");
2312
+ return dir;
2313
+ }
2314
+ //#endregion
2315
+ //#region ../server/src/start.ts
2316
+ /**
2317
+ * startServer — 多运行时自动启动
2318
+ *
2319
+ * 支持 Node.js (dev HMR + prod)、Deno、Bun、Vercel。
2320
+ */
2321
+ async function startServer(options) {
2322
+ const { app, root, port = 3e3, isProduction, vite, routes, locales, ssrEntryPath } = options;
2323
+ const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
2324
+ function printStartupBanner() {
2325
+ const lines = [`\n Server running at http://localhost:${port}\n`];
2326
+ if (routes && routes.length > 0) {
2327
+ lines.push(" Routes:");
2328
+ for (const r of routes) lines.push(` ${r}`);
2329
+ lines.push("");
2330
+ }
2331
+ if (locales && locales.length > 0) lines.push(` Locales: ${locales.join(", ")}`);
2332
+ if (ssrEntryPath) lines.push(` SSR Entry: ${ssrEntryPath}`);
2333
+ if (locales?.length || ssrEntryPath) lines.push("");
2334
+ console.log(lines.join("\n"));
2335
+ }
2336
+ if (isVercel) return { vite };
2337
+ if (!isProduction) {
2338
+ let devVite = vite;
2339
+ if (!devVite) {
2340
+ const { createServer: createViteServer } = await import(
2341
+ /* @vite-ignore */
2342
+ "vite"
2343
+ );
2344
+ devVite = await createViteServer({
2345
+ root,
2346
+ server: { middlewareMode: true },
2347
+ appType: "custom"
2348
+ });
2349
+ }
2350
+ const { getRequestListener } = await import(
2351
+ /* @vite-ignore */
2352
+ "@hono/node-server"
2353
+ );
2354
+ const { createServer } = await import(
2355
+ /* @vite-ignore */
2356
+ "node:http"
2357
+ );
2358
+ const listener = getRequestListener(app.fetch);
2359
+ createServer((req, res) => {
2360
+ devVite.middlewares(req, res, () => listener(req, res));
2361
+ }).listen(port, () => {
2362
+ printStartupBanner();
2363
+ });
2364
+ return { vite: devVite };
2365
+ }
2366
+ if (isDeno) globalThis.Deno.serve({ port }, app.fetch);
2367
+ else if (isBun) {} else {
2368
+ const { serveStatic } = await import(
2369
+ /* @vite-ignore */
2370
+ "@hono/node-server/serve-static"
2371
+ );
2372
+ const path = await import(
2373
+ /* @vite-ignore */
2374
+ "node:path"
2375
+ );
2376
+ const prodApp = new Hono();
2377
+ const clientDir = path.resolve(root, "dist/client");
2378
+ prodApp.use("/*", serveStatic({
2379
+ root: clientDir,
2380
+ rewriteRequestPath: (path) => path.endsWith("/") ? "/__nosuchfile__" : path
2381
+ }));
2382
+ prodApp.route("/", app);
2383
+ const { serve } = await import(
2384
+ /* @vite-ignore */
2385
+ "@hono/node-server"
2386
+ );
2387
+ serve({
2388
+ fetch: prodApp.fetch,
2389
+ port
2390
+ }, () => {
2391
+ printStartupBanner();
2392
+ });
2393
+ }
2394
+ return { vite };
2395
+ }
2396
+ //#endregion
2397
+ //#region ../server/src/create-server.ts
2398
+ /**
2399
+ * createServer — 一站式服务器工厂
2400
+ *
2401
+ * 封装 env 加载、运行时检测、Vite 创建、Hono app、SSR、启动。
2402
+ * 保留 setup() 钩子用于注册业务路由。
2403
+ */
2404
+ /**
2405
+ * 创建并启动 SSR 服务器
2406
+ *
2407
+ * @example
2408
+ * ```ts
2409
+ * const { app } = await createServer({
2410
+ * locales: ["zh", "en"],
2411
+ * setup: (app) => registerProxies(app),
2412
+ * });
2413
+ * export { app };
2414
+ * ```
2415
+ */
2416
+ async function createServer(config = {}) {
2417
+ const { root: rootOverride, locales, defaultLocale, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
2418
+ const root = rootOverride ?? process.cwd();
2419
+ const { existsSync } = await import(
2420
+ /* @vite-ignore */
2421
+ "node:fs"
2422
+ );
2423
+ const envPath = (await import(
2424
+ /* @vite-ignore */
2425
+ "node:path"
2426
+ )).resolve(root, ".env");
2427
+ if (existsSync(envPath)) try {
2428
+ const { config: dotenvConfig } = await import(
2429
+ /* @vite-ignore */
2430
+ "dotenv"
2431
+ );
2432
+ dotenvConfig({ path: envPath });
2433
+ } catch {}
2434
+ const runtime = detectRuntime();
2435
+ let vite;
2436
+ if (!runtime.isProduction && !runtime.isVercel) {
2437
+ const { createServer: createViteServer } = await import(
2438
+ /* @vite-ignore */
2439
+ "vite"
2440
+ );
2441
+ vite = await createViteServer({
2442
+ root,
2443
+ server: { middlewareMode: true },
2444
+ appType: "custom"
2445
+ });
2446
+ }
2447
+ const app = new Hono();
2448
+ if (proxies?.length) registerProxyRoutes(app, proxies);
2449
+ if (setup) await setup(app);
2450
+ const ssrApp = createSSRApp({
2451
+ root,
2452
+ vite,
2453
+ isProduction: runtime.isProduction,
2454
+ supportedLocales: locales,
2455
+ defaultLocale,
2456
+ parentFetch: app.fetch.bind(app),
2457
+ ...ssr
2458
+ });
2459
+ app.route("/", ssrApp);
2460
+ await startServer({
2461
+ app,
2462
+ root,
2463
+ port,
2464
+ isProduction: runtime.isProduction,
2465
+ vite,
2466
+ runtime,
2467
+ locales,
2468
+ ssrEntryPath: ssr?.ssrEntryPath
2469
+ });
2470
+ return {
2471
+ app,
2472
+ vite,
2473
+ runtime
2474
+ };
2475
+ }
2476
+ //#endregion
2477
+ //#region ../server/src/vite-plugin.ts
2478
+ /**
2479
+ * 从 setup 模块中查找 setup 函数:优先 default,其次 setup 命名导出。
2480
+ */
2481
+ function resolveSetupFn(mod) {
2482
+ if (typeof mod.default === "function") return mod.default;
2483
+ if (typeof mod.setup === "function") return mod.setup;
2484
+ return Object.values(mod).find((v) => typeof v === "function") ?? null;
2485
+ }
2486
+ /**
2487
+ * 匹配 Vite 配置级别的 renderMode 覆盖。
2488
+ * 精确路径优先,然后 glob 模式。
2489
+ */
2490
+ function matchRenderModeConfig(url, renderModes) {
2491
+ if (!renderModes) return null;
2492
+ const path = url.split("?")[0];
2493
+ if (renderModes[path]) return renderModes[path];
2494
+ for (const [pattern, mode] of Object.entries(renderModes)) if (pattern.includes("*")) {
2495
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2496
+ if (new RegExp("^" + escaped.replace(/\*/g, ".*") + "$").test(path)) return mode;
2497
+ }
2498
+ return null;
2499
+ }
2500
+ function finesoftFrontViteConfig(options = {}) {
2501
+ const ssrEntry = options.ssr?.entry ?? "src/ssr.ts";
2502
+ let root = process.cwd();
2503
+ let resolvedCommand;
2504
+ let resolvedResolve;
2505
+ let resolvedCss;
2506
+ const CSS_EXTENSIONS = /\.(css|scss|less|sass|styl|stylus|pcss|postcss)($|\?)/;
2507
+ return {
2508
+ name: "finesoft-front",
2509
+ config(userConfig, env) {
2510
+ const overrides = { appType: "custom" };
2511
+ if (env.command === "build" && !process.env.__FINESOFT_SUB_BUILD__) overrides.build = { outDir: userConfig.build?.outDir ?? "dist/client" };
2512
+ return overrides;
2513
+ },
2514
+ configResolved(config) {
2515
+ resolvedCommand = config.command;
2516
+ resolvedResolve = config.resolve;
2517
+ resolvedCss = config.css;
2518
+ root = config.root;
2519
+ },
2520
+ transformIndexHtml: {
2521
+ order: "pre",
2522
+ async handler(html, ctx) {
2523
+ const server = ctx.server;
2524
+ if (!server) return;
2525
+ const urlPath = (ctx.originalUrl || ctx.path || "").split("?")[0];
2526
+ if (/\.\w+$/.test(urlPath) && !urlPath.endsWith(".html")) return;
2527
+ const appEntry = [...html.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g)].find((m) => !m[1].startsWith("/@"));
2528
+ if (!appEntry) return;
2529
+ const browserEntry = appEntry[1];
2530
+ try {
2531
+ await server.transformRequest(browserEntry);
2532
+ } catch {
2533
+ return;
2534
+ }
2535
+ const cssUrls = [];
2536
+ const visited = /* @__PURE__ */ new Set();
2537
+ function walk(mod) {
2538
+ if (!mod?.url || visited.has(mod.url)) return;
2539
+ visited.add(mod.url);
2540
+ if (CSS_EXTENSIONS.test(mod.url) && !mod.url.includes(".svelte")) cssUrls.push(mod.url);
2541
+ if (mod.importedModules) for (const imported of mod.importedModules) walk(imported);
2542
+ }
2543
+ const browserMod = await server.moduleGraph.getModuleByUrl(browserEntry);
2544
+ if (browserMod) walk(browserMod);
2545
+ if (cssUrls.length === 0) return;
2546
+ const tags = [];
2547
+ for (const url of cssUrls) try {
2548
+ const css = (await server.ssrLoadModule(url))?.default;
2549
+ if (typeof css === "string" && css.length > 0) tags.push({
2550
+ tag: "style",
2551
+ attrs: { "data-vite-dev-id": url },
2552
+ children: css,
2553
+ injectTo: "head"
2554
+ });
2555
+ } catch {}
2556
+ return tags;
2557
+ }
2558
+ },
2559
+ configureServer(server) {
2560
+ return async () => {
2561
+ const { Hono: HonoClass } = await import(
2562
+ /* @vite-ignore */
2563
+ "hono"
2564
+ );
2565
+ const { createSSRApp } = await import("./app-re7jUCuM.mjs").then((n) => n.t);
2566
+ const { getRequestListener } = await import(
2567
+ /* @vite-ignore */
2568
+ "@hono/node-server"
2569
+ );
2570
+ const app = new HonoClass();
2571
+ if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
2572
+ if (typeof options.setup === "function") await options.setup(app);
2573
+ else if (typeof options.setup === "string") {
2574
+ const fn = resolveSetupFn(await server.ssrLoadModule("/" + options.setup));
2575
+ if (fn) await fn(app);
2576
+ }
2577
+ const ssrApp = createSSRApp({
2578
+ root,
2579
+ vite: server,
2580
+ isProduction: false,
2581
+ ssrEntryPath: "/" + ssrEntry,
2582
+ supportedLocales: options.locales,
2583
+ defaultLocale: options.defaultLocale,
2584
+ parentFetch: app.fetch.bind(app),
2585
+ renderModes: options.renderModes
2586
+ });
2587
+ app.route("/", ssrApp);
2588
+ const listener = getRequestListener(app.fetch);
2589
+ server.middlewares.use((req, res) => {
2590
+ listener(req, res);
2591
+ });
2592
+ };
2593
+ },
2594
+ configurePreviewServer(server) {
2595
+ return async () => {
2596
+ const { readFileSync } = await import(
2597
+ /* @vite-ignore */
2598
+ "node:fs"
2599
+ );
2600
+ const path = await import(
2601
+ /* @vite-ignore */
2602
+ "node:path"
2603
+ );
2604
+ const { pathToFileURL } = await import(
2605
+ /* @vite-ignore */
2606
+ "node:url"
2607
+ );
2608
+ const { Hono: HonoClass } = await import(
2609
+ /* @vite-ignore */
2610
+ "hono"
2611
+ );
2612
+ const { injectSSRContent, injectCSRShell } = await import(
2613
+ /* @vite-ignore */
2614
+ "@finesoft/ssr"
2615
+ );
2616
+ const { parseAcceptLanguage } = await import("./locale-D2Bu7w47.mjs").then((n) => n.t);
2617
+ const { getRequestListener } = await import(
2618
+ /* @vite-ignore */
2619
+ "@hono/node-server"
2620
+ );
2621
+ const app = new HonoClass();
2622
+ const isrCache = /* @__PURE__ */ new Map();
2623
+ if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
2624
+ if (typeof options.setup === "function") await options.setup(app);
2625
+ else if (typeof options.setup === "string") try {
2626
+ const fn = resolveSetupFn(await import(pathToFileURL(path.resolve(root, "dist/server/setup.mjs")).href));
2627
+ if (fn) await fn(app);
2628
+ } catch {
2629
+ console.warn("[finesoft] Could not load setup module for preview. API routes disabled.");
2630
+ }
2631
+ const template = readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
2632
+ const ssrModule = await import(pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href);
2633
+ app.get("*", async (c) => {
2634
+ const ssrDepth = parseInt(c.req.header("x-ssr-depth") ?? "0", 10);
2635
+ if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
2636
+ const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
2637
+ try {
2638
+ const locale = parseAcceptLanguage(c.req.header("accept-language"), options.locales, options.defaultLocale);
2639
+ const overrideMode = matchRenderModeConfig(url, options.renderModes);
2640
+ if (overrideMode === "csr") return c.html(injectCSRShell(template, locale));
2641
+ const cacheKey = `${locale}:${url}`;
2642
+ const cached = isrCache.get(cacheKey);
2643
+ if (cached) return c.html(cached);
2644
+ const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, locale, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
2645
+ if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
2646
+ const finalHtml = injectSSRContent({
2647
+ template,
2648
+ locale,
2649
+ head,
2650
+ css,
2651
+ html: appHtml,
2652
+ serializedData: ssrModule.serializeServerData(serverData)
2653
+ });
2654
+ if (renderMode === "prerender" || overrideMode === "prerender") isrCache.set(cacheKey, finalHtml);
2655
+ return c.html(finalHtml);
2656
+ } catch (e) {
2657
+ console.error("[SSR Preview Error]", e);
2658
+ return c.text("Internal Server Error", 500);
2659
+ }
2660
+ });
2661
+ const listener = getRequestListener(app.fetch);
2662
+ server.middlewares.use((req, res) => {
2663
+ listener(req, res);
2664
+ });
2665
+ };
2666
+ },
2667
+ async closeBundle() {
2668
+ if (process.env.__FINESOFT_SUB_BUILD__) return;
2669
+ if (resolvedCommand !== "build") return;
2670
+ process.env.__FINESOFT_SUB_BUILD__ = "1";
2671
+ try {
2672
+ const vite = await import(
2673
+ /* @vite-ignore */
2674
+ "vite"
2675
+ );
2676
+ const fs = await import(
2677
+ /* @vite-ignore */
2678
+ "node:fs"
2679
+ );
2680
+ const path = await import(
2681
+ /* @vite-ignore */
2682
+ "node:path"
2683
+ );
2684
+ console.log("\n Building SSR bundle...\n");
2685
+ await vite.build({
2686
+ root,
2687
+ build: {
2688
+ ssr: ssrEntry,
2689
+ outDir: "dist/server"
2690
+ },
2691
+ resolve: resolvedResolve,
2692
+ css: resolvedCss
2693
+ });
2694
+ if (typeof options.setup === "string") {
2695
+ console.log(" Building setup module...\n");
2696
+ await vite.build({
2697
+ root,
2698
+ build: {
2699
+ ssr: options.setup,
2700
+ outDir: "dist/server",
2701
+ emptyOutDir: false,
2702
+ rollupOptions: { output: { entryFileNames: "setup.mjs" } }
2703
+ },
2704
+ resolve: resolvedResolve
2705
+ });
2706
+ }
2707
+ if (options.adapter) {
2708
+ const adapter = resolveAdapter(options.adapter);
2709
+ const locales = options.locales ?? ["zh", "en"];
2710
+ const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
2711
+ const templateHtml = fs.readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
2712
+ const ctx = {
2713
+ root,
2714
+ ssrEntry,
2715
+ setupPath: typeof options.setup === "string" ? options.setup : void 0,
2716
+ bootstrapEntry: options.bootstrapEntry,
2717
+ locales,
2718
+ defaultLocale,
2719
+ templateHtml,
2720
+ renderModes: options.renderModes,
2721
+ proxies: options.proxies,
2722
+ resolvedResolve,
2723
+ resolvedCss,
2724
+ vite,
2725
+ fs,
2726
+ path,
2727
+ generateSSREntry(opts) {
2728
+ return generateSSREntry(ctx, opts);
2729
+ },
2730
+ buildBundle(opts) {
2731
+ return buildBundle(ctx, opts);
2732
+ },
2733
+ copyStaticAssets(destDir, opts) {
2734
+ return copyStaticAssets(ctx, destDir, opts);
2735
+ }
2736
+ };
2737
+ console.log(` Running adapter: ${adapter.name}...\n`);
2738
+ await adapter.build(ctx);
2739
+ }
2740
+ } finally {
2741
+ delete process.env.__FINESOFT_SUB_BUILD__;
2742
+ }
2743
+ }
2744
+ };
2745
+ }
2746
+ //#endregion
2747
+ export { ACTION_KINDS, ActionDispatcher, BaseController, BaseLogger, CompositeLogger, CompositeLoggerFactory, ConsoleLogger, ConsoleLoggerFactory, Container, DEP_KEYS, Framework, History, HttpClient, HttpError, IntentDispatcher, LruMap, PrefetchedIntents, Router, SSR_PLACEHOLDERS, autoAdapter, buildUrl, cloudflareAdapter, createBrowserContext, createPrefetchedIntentsFromDom, createSSRApp, createSSRRender, createServer, createServerContext, defineRoutes, deny, deserializeServerData, detectRuntime, finesoftFrontViteConfig, generateProxyCode, generateUuid, getBaseUrl, injectCSRShell, injectSSRContent, isCompoundAction, isExternalUrlAction, isFlowAction, isNone, isSome, makeDependencies, makeExternalUrlAction, makeFlowAction, mapEach, netlifyAdapter, next, nodeAdapter, parseAcceptLanguage, pipe, pipeAsync, redirect, registerActionHandlers, registerExternalUrlHandler, registerFlowActionHandler, registerProxyRoutes, removeHost, removeQueryParams, removeScheme, resetFilterCache, resolveAdapter, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
2748
+
2749
+ //# sourceMappingURL=index.mjs.map