@finesoft/front 0.1.75 → 0.1.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,2383 +1,4 @@
1
- import { Hono } from "hono";
2
- //#region ../core/src/actions/types.ts
3
- /**
4
- * Action 类型定义
5
- *
6
- * FlowAction — SPA 内部导航
7
- * ExternalUrlAction — 打开外部链接
8
- * CompoundAction — 组合多个 Action
9
- */
10
- /** Action Kind 常量 */
11
- const ACTION_KINDS = {
12
- FLOW: "flow",
13
- EXTERNAL_URL: "externalUrl",
14
- COMPOUND: "compound"
15
- };
16
- function isFlowAction(action) {
17
- return action.kind === ACTION_KINDS.FLOW;
18
- }
19
- function isExternalUrlAction(action) {
20
- return action.kind === ACTION_KINDS.EXTERNAL_URL;
21
- }
22
- function isCompoundAction(action) {
23
- return action.kind === ACTION_KINDS.COMPOUND;
24
- }
25
- function makeFlowAction(url, presentationContext) {
26
- return {
27
- kind: ACTION_KINDS.FLOW,
28
- url,
29
- presentationContext
30
- };
31
- }
32
- function makeExternalUrlAction(url) {
33
- return {
34
- kind: ACTION_KINDS.EXTERNAL_URL,
35
- url
36
- };
37
- }
38
- //#endregion
39
- //#region ../core/src/actions/dispatcher.ts
40
- /** CompoundAction 最大递归展开深度 */
41
- const MAX_COMPOUND_DEPTH = 32;
42
- var ActionDispatcher = class {
43
- handlers = /* @__PURE__ */ new Map();
44
- /**
45
- * 注册指定 kind 的 handler。
46
- *
47
- * 重复 kind 时保留第一个注册者并发出警告——这是有意设计:
48
- * framework 内部 handler 先注册,应用层意外覆盖会被记录而非静默生效。
49
- * 如需显式替换,先调用 removeAction(kind)。
50
- */
51
- onAction(kind, handler) {
52
- if (this.handlers.has(kind)) {
53
- console.warn(`[ActionDispatcher] kind="${kind}" already registered, skipping`);
54
- return;
55
- }
56
- this.handlers.set(kind, handler);
57
- }
58
- /** 移除指定 kind 的 handler(用于显式覆盖场景) */
59
- removeAction(kind) {
60
- return this.handlers.delete(kind);
61
- }
62
- /** 执行一个 Action(CompoundAction 递归展开,有深度限制) */
63
- async perform(action, _depth = 0) {
64
- if (isCompoundAction(action)) {
65
- if (_depth >= MAX_COMPOUND_DEPTH) throw new Error(`[ActionDispatcher] CompoundAction recursion depth exceeded (max ${MAX_COMPOUND_DEPTH})`);
66
- for (const subAction of action.actions) await this.perform(subAction, _depth + 1);
67
- return;
68
- }
69
- const handler = this.handlers.get(action.kind);
70
- if (!handler) {
71
- console.warn(`[ActionDispatcher] No handler for kind="${action.kind}"`);
72
- return;
73
- }
74
- await handler(action);
75
- }
76
- };
77
- //#endregion
78
- //#region ../core/src/intents/dispatcher.ts
79
- var IntentDispatcher = class {
80
- controllers = /* @__PURE__ */ new Map();
81
- /** 注册一个 IntentController */
82
- register(controller) {
83
- this.controllers.set(controller.intentId, controller);
84
- }
85
- /** 分发 Intent 到对应 Controller */
86
- async dispatch(intent, container) {
87
- const controller = this.controllers.get(intent.id);
88
- if (!controller) throw new Error(`[IntentDispatcher] No controller for "${intent.id}". Registered: [${Array.from(this.controllers.keys()).join(", ")}]`);
89
- return controller.perform(intent, container);
90
- }
91
- /** 检查是否已注册某个 Intent */
92
- has(intentId) {
93
- return this.controllers.has(intentId);
94
- }
95
- };
96
- //#endregion
97
- //#region ../core/src/dependencies/container.ts
98
- var Container = class Container {
99
- registrations = /* @__PURE__ */ new Map();
100
- resolutionStack = /* @__PURE__ */ new Set();
101
- parent;
102
- children = /* @__PURE__ */ new Set();
103
- /** 注册依赖(默认单例) */
104
- register(key, factory, singleton = true) {
105
- this.registrations.set(key, {
106
- factory,
107
- singleton
108
- });
109
- return this;
110
- }
111
- /** 解析依赖 — 当前容器未注册时回退到 parent */
112
- resolve(key) {
113
- const reg = this.registrations.get(key);
114
- if (!reg) {
115
- if (this.parent) return this.parent.resolve(key);
116
- throw new Error(`[Container] No registration for key: "${key}"`);
117
- }
118
- if (reg.singleton) {
119
- if (reg.instance === void 0) {
120
- if (this.resolutionStack.has(key)) throw new Error(`[Container] Circular dependency detected: ${[...this.resolutionStack, key].join(" → ")}`);
121
- this.resolutionStack.add(key);
122
- try {
123
- reg.instance = reg.factory();
124
- } finally {
125
- this.resolutionStack.delete(key);
126
- }
127
- }
128
- return reg.instance;
129
- }
130
- return reg.factory();
131
- }
132
- /** 检查是否已注册(含 parent) */
133
- has(key) {
134
- return this.registrations.has(key) || (this.parent?.has(key) ?? false);
135
- }
136
- /**
137
- * 创建子容器(请求级 scope)
138
- *
139
- * 子容器可覆写父容器的依赖(如每请求的 locale、user),
140
- * 未覆写的 key 自动回退到父容器解析。子容器会被父容器跟踪,
141
- * 父容器 dispose 时一并销毁所有未独立 dispose 的子容器。
142
- */
143
- createScope() {
144
- const child = new Container();
145
- child.parent = this;
146
- this.children.add(child);
147
- return child;
148
- }
149
- /**
150
- * 销毁容器,清除所有缓存。
151
- *
152
- * - 递归 dispose 所有 createScope() 创建的未 dispose 子容器
153
- * - 自身被 dispose 后从父容器移除引用,允许 GC
154
- * - 重复 dispose 安全(幂等)
155
- */
156
- dispose() {
157
- const childSnapshot = Array.from(this.children);
158
- for (const child of childSnapshot) child.dispose();
159
- this.children.clear();
160
- for (const reg of this.registrations.values()) reg.instance = void 0;
161
- this.registrations.clear();
162
- if (this.parent) {
163
- this.parent.children.delete(this);
164
- this.parent = void 0;
165
- }
166
- }
167
- };
168
- //#endregion
169
- //#region ../core/src/i18n/locale.ts
170
- /** RTL 语言列表 */
171
- const RTL_LANGUAGES = new Set([
172
- "ar",
173
- "arc",
174
- "dv",
175
- "fa",
176
- "ha",
177
- "he",
178
- "khw",
179
- "ks",
180
- "ku",
181
- "ps",
182
- "ur",
183
- "yi"
184
- ]);
185
- /** 检测语言是否为 RTL */
186
- function isRtl(language) {
187
- const primary = language.split("-")[0].toLowerCase();
188
- return RTL_LANGUAGES.has(primary);
189
- }
190
- /** 获取文本方向 */
191
- function getTextDirection(language) {
192
- return isRtl(language) ? "rtl" : "ltr";
193
- }
194
- /**
195
- * 从语言代码生成 HTML lang/dir 属性
196
- *
197
- * @example
198
- * ```ts
199
- * getLocaleAttributes("ar-SA") // { lang: "ar-SA", dir: "rtl" }
200
- * getLocaleAttributes("en-US") // { lang: "en-US", dir: "ltr" }
201
- * ```
202
- */
203
- function getLocaleAttributes(language) {
204
- return {
205
- lang: language,
206
- dir: getTextDirection(language)
207
- };
208
- }
209
- /**
210
- * 构建 LocaleInfo
211
- *
212
- * @param language - 语言代码(如 "zh-Hans")
213
- * @param region - 地区代码(如 "CN"),可选
214
- */
215
- function makeLocaleInfo(language, region) {
216
- return {
217
- language,
218
- region,
219
- bcp47: region ? `${language}-${region}` : language,
220
- dir: getTextDirection(language)
221
- };
222
- }
223
- /**
224
- * 将 locale 属性应用到 `<html>` 元素
225
- *
226
- * 服务端渲染时可用于字符串拼接,浏览器端直接操作 DOM。
227
- */
228
- function setHtmlLocaleAttributes(attrs) {
229
- document.documentElement.lang = attrs.lang;
230
- document.documentElement.dir = attrs.dir;
231
- }
232
- /**
233
- * 从 URL 前缀中提取 locale
234
- *
235
- * @param url - 请求 URL(如 "/zh/about")
236
- * @param supportedLocales - 支持的 locale 列表(如 ["zh", "en", "ja"])
237
- * @returns 匹配时返回 `{ locale, strippedUrl }`,不匹配返回 null
238
- *
239
- * @example
240
- * ```ts
241
- * resolveLocaleFromUrl("/zh/about", ["zh", "en"])
242
- * // → { locale: "zh", strippedUrl: "/about" }
243
- *
244
- * resolveLocaleFromUrl("/about", ["zh", "en"])
245
- * // → null
246
- * ```
247
- */
248
- function resolveLocaleFromUrl(url, supportedLocales) {
249
- const match = url.split("?")[0].match(/^\/([^/]+)(\/.*)?$/);
250
- if (!match) return null;
251
- const candidate = match[1];
252
- const found = supportedLocales.find((l) => l.toLowerCase() === candidate.toLowerCase());
253
- if (!found) return null;
254
- return {
255
- locale: found,
256
- strippedUrl: match[2] || "/"
257
- };
258
- }
259
- //#endregion
260
- //#region ../core/src/i18n/generated-loader.ts
261
- async function resolveGeneratedMessages(locale, context) {
262
- if (!locale || !context) return;
263
- const loader = await getGeneratedMessagesLoader();
264
- if (!loader) return;
265
- return loader(locale, context);
266
- }
267
- async function getGeneratedMessagesLoader() {
268
- if (typeof globalThis.__FINESOFT_I18N_LOADER__ === "function") return globalThis.__FINESOFT_I18N_LOADER__;
269
- if (typeof __FINESOFT_I18N_LOADER_SPECIFIER__ !== "string") return;
270
- try {
271
- const imported = await import(__FINESOFT_I18N_LOADER_SPECIFIER__);
272
- const loader = typeof imported === "function" ? imported : typeof imported?.loadMessages === "function" ? imported.loadMessages : void 0;
273
- if (!loader) return;
274
- globalThis.__FINESOFT_I18N_LOADER__ = loader;
275
- return loader;
276
- } catch {
277
- return;
278
- }
279
- }
280
- //#endregion
281
- //#region ../core/src/i18n/messages.ts
282
- /**
283
- * i18n message helpers shared by SSR and browser startup.
284
- */
285
- /**
286
- * Resolve the effective translation source for a locale.
287
- */
288
- async function resolveConfiguredMessages(options) {
289
- const { locale, loadMessages, context } = options;
290
- if (!locale || !context) return;
291
- if (loadMessages) return loadMessages(locale, context);
292
- return resolveGeneratedMessages(locale, context);
293
- }
294
- /**
295
- * Resolve `TranslationMessages` into the flat map consumed by
296
- * `SimpleTranslator`.
297
- */
298
- function resolveMessages(messages, locale) {
299
- const entries = Object.entries(messages);
300
- if (entries.length === 0) return void 0;
301
- if (typeof entries[0][1] === "string") return messages;
302
- const localeMessages = messages[locale];
303
- if (!localeMessages) return void 0;
304
- const flat = {};
305
- for (const [key, value] of Object.entries(localeMessages)) {
306
- if (typeof value === "string") {
307
- flat[key] = value;
308
- continue;
309
- }
310
- for (const [suffix, text] of Object.entries(value)) flat[`${key}.${suffix}`] = text;
311
- }
312
- return flat;
313
- }
314
- //#endregion
315
- //#region ../core/src/i18n/interpolate.ts
316
- /**
317
- * ICU 消息格式插值
318
- *
319
- * 支持 `{name}` 占位符替换和基础复数规则。
320
- */
321
- /** 将 `{key}` 占位符替换为 values 中的对应值 */
322
- function interpolate(template, values) {
323
- if (!values) return template;
324
- return template.replace(/\{(\w+)\}/g, (_, key) => {
325
- const val = values[key];
326
- return val !== void 0 ? String(val) : `{${key}}`;
327
- });
328
- }
329
- /**
330
- * 英语复数规则(默认)
331
- * 0 → other, 1 → one, 2+ → other
332
- */
333
- function englishPlural(count) {
334
- return count === 1 ? "one" : "other";
335
- }
336
- /**
337
- * 解析带复数后缀的翻译 key
338
- *
339
- * 约定: `key.one`, `key.other`, `key.zero`, etc.
340
- */
341
- function resolvePluralKey(key, category) {
342
- return `${key}.${category}`;
343
- }
344
- //#endregion
345
- //#region ../core/src/i18n/translator.ts
346
- /**
347
- * SimpleTranslator — 默认翻译器实现
348
- *
349
- * 从扁平的 key→string 映射提供翻译,支持 ICU 插值和复数规则。
350
- */
351
- var SimpleTranslator = class {
352
- locale;
353
- messages;
354
- pluralRule;
355
- fallback;
356
- constructor(options) {
357
- this.locale = options.locale;
358
- this.messages = options.messages;
359
- this.pluralRule = options.pluralRule ?? englishPlural;
360
- this.fallback = options.fallback ?? ((key) => key);
361
- }
362
- t(key, values) {
363
- const template = this.messages[key];
364
- if (template === void 0) return this.fallback(key);
365
- return interpolate(template, values);
366
- }
367
- plural(key, count, values) {
368
- const pluralKey = resolvePluralKey(key, this.pluralRule(count));
369
- const mergedValues = {
370
- count,
371
- ...values
372
- };
373
- return this.t(pluralKey, mergedValues);
374
- }
375
- };
376
- //#endregion
377
- //#region ../core/src/logger/composite.ts
378
- var CompositeLoggerFactory = class {
379
- constructor(factories) {
380
- this.factories = factories;
381
- }
382
- loggerFor(name) {
383
- return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
384
- }
385
- };
386
- var CompositeLogger = class {
387
- constructor(loggers) {
388
- this.loggers = loggers;
389
- }
390
- debug(...args) {
391
- return this.callAll("debug", args);
392
- }
393
- info(...args) {
394
- return this.callAll("info", args);
395
- }
396
- warn(...args) {
397
- return this.callAll("warn", args);
398
- }
399
- error(...args) {
400
- return this.callAll("error", args);
401
- }
402
- callAll(method, args) {
403
- for (const logger of this.loggers) logger[method](...args);
404
- return "";
405
- }
406
- };
407
- //#endregion
408
- //#region ../core/src/logger/base.ts
409
- var BaseLogger = class {
410
- category;
411
- constructor(category) {
412
- this.category = category;
413
- }
414
- };
415
- //#endregion
416
- //#region ../core/src/logger/local-storage-filter.ts
417
- const LEVEL_TO_NUM = {
418
- "*": 4,
419
- debug: 4,
420
- info: 3,
421
- warn: 2,
422
- error: 1,
423
- off: 0,
424
- "": 0
425
- };
426
- let cachedRules;
427
- let cachedRaw;
428
- function parseRules() {
429
- if (typeof globalThis.localStorage === "undefined") return {};
430
- let raw;
431
- try {
432
- raw = globalThis.localStorage.getItem("onyxLog");
433
- } catch {
434
- return {};
435
- }
436
- if (!raw) return {};
437
- if (raw === cachedRaw && cachedRules) return cachedRules;
438
- cachedRaw = raw;
439
- const rules = {};
440
- const parts = raw.split(",");
441
- for (const part of parts) {
442
- const [name, level] = part.trim().split("=");
443
- if (!name || level === void 0) continue;
444
- const num = LEVEL_TO_NUM[level.toLowerCase()] ?? void 0;
445
- if (num === void 0) continue;
446
- if (name === "*") rules.defaultLevel = num;
447
- else {
448
- rules.named ??= {};
449
- rules.named[name] = num;
450
- }
451
- }
452
- cachedRules = rules;
453
- return rules;
454
- }
455
- function shouldLog(name, level) {
456
- const rules = parseRules();
457
- if (rules.defaultLevel === void 0 && !rules.named) return true;
458
- const currentNum = LEVEL_TO_NUM[level] ?? 4;
459
- if (rules.named?.[name] !== void 0) return currentNum <= rules.named[name];
460
- if (rules.defaultLevel !== void 0) return currentNum <= rules.defaultLevel;
461
- return true;
462
- }
463
- function resetFilterCache() {
464
- cachedRules = void 0;
465
- cachedRaw = void 0;
466
- }
467
- //#endregion
468
- //#region ../core/src/logger/console.ts
469
- /**
470
- * ConsoleLogger — 基于 console 的日志实现
471
- */
472
- var ConsoleLogger = class extends BaseLogger {
473
- debug(...args) {
474
- if (shouldLog(this.category, "debug")) console.debug(`[${this.category}]`, ...args);
475
- return "";
476
- }
477
- info(...args) {
478
- if (shouldLog(this.category, "info")) console.info(`[${this.category}]`, ...args);
479
- return "";
480
- }
481
- warn(...args) {
482
- if (shouldLog(this.category, "warn")) console.warn(`[${this.category}]`, ...args);
483
- return "";
484
- }
485
- error(...args) {
486
- console.error(`[${this.category}]`, ...args);
487
- return "";
488
- }
489
- };
490
- var ConsoleLoggerFactory = class {
491
- loggerFor(category) {
492
- return new ConsoleLogger(category);
493
- }
494
- };
495
- //#endregion
496
- //#region ../core/src/logger/reporting.ts
497
- /**
498
- * ReportingLogger — 上报型日志实现
499
- *
500
- * 将 warn/error 级别日志转发到外部监控服务(如 Sentry、Datadog)。
501
- * 用户通过 ReportCallback 注入上报逻辑,框架不直接依赖任何第三方 SDK。
502
- */
503
- const LEVEL_PRIORITY = {
504
- debug: 0,
505
- info: 1,
506
- warn: 2,
507
- error: 3
508
- };
509
- var ReportingLogger = class extends BaseLogger {
510
- minPriority;
511
- report;
512
- constructor(category, options) {
513
- super(category);
514
- this.minPriority = LEVEL_PRIORITY[options.minLevel ?? "warn"];
515
- this.report = options.report;
516
- }
517
- debug(...args) {
518
- this.maybeReport("debug", args);
519
- return "";
520
- }
521
- info(...args) {
522
- this.maybeReport("info", args);
523
- return "";
524
- }
525
- warn(...args) {
526
- this.maybeReport("warn", args);
527
- return "";
528
- }
529
- error(...args) {
530
- this.maybeReport("error", args);
531
- return "";
532
- }
533
- maybeReport(level, args) {
534
- if (LEVEL_PRIORITY[level] < this.minPriority) return;
535
- try {
536
- this.report(level, this.category, args);
537
- } catch (e) {
538
- console.error("[ReportingLogger] report callback threw:", e);
539
- }
540
- }
541
- };
542
- var ReportingLoggerFactory = class {
543
- options;
544
- constructor(options) {
545
- this.options = options;
546
- }
547
- loggerFor(category) {
548
- return new ReportingLogger(category, this.options);
549
- }
550
- };
551
- //#endregion
552
- //#region ../core/src/metrics/console-recorder.ts
553
- var ConsoleEventRecorder = class {
554
- prefix;
555
- constructor(prefix = "Metrics") {
556
- this.prefix = prefix;
557
- }
558
- record(type, fields) {
559
- console.info(`[${this.prefix}:${type}]`, fields ?? "");
560
- }
561
- async flush() {}
562
- destroy() {}
563
- };
564
- //#endregion
565
- //#region ../core/src/utils/platform.ts
566
- /**
567
- * 从 User-Agent 字符串解析平台信息
568
- *
569
- * @param ua - User-Agent 字符串(默认取 navigator.userAgent)
570
- */
571
- function detectPlatform(ua) {
572
- const agent = ua ?? (typeof navigator !== "undefined" ? navigator.userAgent : "");
573
- const lower = agent.toLowerCase();
574
- return {
575
- os: detectOS(lower),
576
- browser: detectBrowser(lower),
577
- engine: detectEngine(lower),
578
- isMobile: /mobile|android|iphone|ipad|ipod/i.test(agent),
579
- isTouch: typeof navigator !== "undefined" && "maxTouchPoints" in navigator ? navigator.maxTouchPoints > 0 : false
580
- };
581
- }
582
- function detectOS(ua) {
583
- if (/iphone|ipad|ipod/.test(ua)) return "ios";
584
- if (/android/.test(ua)) return "android";
585
- if (/macintosh|mac os x/.test(ua)) return "macos";
586
- if (/windows/.test(ua)) return "windows";
587
- if (/linux/.test(ua)) return "linux";
588
- return "unknown";
589
- }
590
- function detectBrowser(ua) {
591
- if (/edg\//.test(ua)) return "edge";
592
- if (/opr\/|opera/.test(ua)) return "opera";
593
- if (/samsungbrowser/.test(ua)) return "samsung";
594
- if (/chrome|crios/.test(ua) && !/edg\//.test(ua)) return "chrome";
595
- if (/firefox|fxios/.test(ua)) return "firefox";
596
- if (/safari/.test(ua) && !/chrome/.test(ua)) return "safari";
597
- return "unknown";
598
- }
599
- function detectEngine(ua) {
600
- if (/applewebkit/.test(ua) && !/chrome/.test(ua)) return "webkit";
601
- if (/applewebkit/.test(ua) && /chrome/.test(ua)) return "blink";
602
- if (/gecko\//.test(ua)) return "gecko";
603
- return "unknown";
604
- }
605
- //#endregion
606
- //#region ../core/src/dependencies/make-dependencies.ts
607
- /**
608
- * 依赖工厂 — 创建所有基础依赖
609
- */
610
- const DEP_KEYS = {
611
- LOGGER: "logger",
612
- LOGGER_FACTORY: "loggerFactory",
613
- NET: "net",
614
- STORAGE: "storage",
615
- FEATURE_FLAGS: "featureFlags",
616
- METRICS: "metrics",
617
- FETCH: "fetch",
618
- EVENT_RECORDER: "eventRecorder",
619
- LOCALE: "locale",
620
- PLATFORM: "platform",
621
- TRANSLATOR: "translator"
622
- };
623
- var MemoryStorage = class {
624
- store = /* @__PURE__ */ new Map();
625
- get(key) {
626
- return this.store.get(key);
627
- }
628
- set(key, value) {
629
- this.store.set(key, value);
630
- }
631
- delete(key) {
632
- this.store.delete(key);
633
- }
634
- };
635
- var DefaultFeatureFlags = class {
636
- flags;
637
- providers = [];
638
- constructor(flags = {}) {
639
- this.flags = flags;
640
- }
641
- /** 注册外部 provider(如远程配置、A/B 测试 SDK) */
642
- addProvider(provider) {
643
- this.providers.push(provider);
644
- }
645
- isEnabled(key) {
646
- for (let i = this.providers.length - 1; i >= 0; i--) if (this.providers[i].isEnabled(key)) return true;
647
- return this.flags[key] === true;
648
- }
649
- getString(key) {
650
- for (let i = this.providers.length - 1; i >= 0; i--) {
651
- const result = this.providers[i].getString?.(key);
652
- if (result !== void 0) return result;
653
- }
654
- const v = this.flags[key];
655
- return typeof v === "string" ? v : void 0;
656
- }
657
- getNumber(key) {
658
- for (let i = this.providers.length - 1; i >= 0; i--) {
659
- const result = this.providers[i].getNumber?.(key);
660
- if (result !== void 0) return result;
661
- }
662
- const v = this.flags[key];
663
- return typeof v === "number" ? v : void 0;
664
- }
665
- };
666
- var ConsoleMetrics = class {
667
- record(type, fields) {
668
- console.info(`[Metrics:${type}]`, fields ?? "");
669
- }
670
- recordPageView(page, fields) {
671
- this.record("PageView", {
672
- page,
673
- ...fields
674
- });
675
- }
676
- recordEvent(name, fields) {
677
- this.record("Event", {
678
- name,
679
- ...fields
680
- });
681
- }
682
- };
683
- function makeDependencies(container, options = {}) {
684
- const { _resolvedMessages: messages } = options;
685
- const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {}, featureFlagsProviders = [], reportCallback, eventRecorder, locale, platform } = options;
686
- const consoleFactory = new ConsoleLoggerFactory();
687
- const loggerFactory = reportCallback ? new CompositeLoggerFactory([consoleFactory, new ReportingLoggerFactory({ report: reportCallback })]) : consoleFactory;
688
- container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
689
- container.register(DEP_KEYS.LOGGER, () => loggerFactory.loggerFor("framework"));
690
- container.register(DEP_KEYS.NET, () => ({ fetch: (url, opts) => fetchFn(url, opts) }));
691
- container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
692
- const flags = new DefaultFeatureFlags(featureFlags);
693
- for (const provider of featureFlagsProviders) flags.addProvider(provider);
694
- container.register(DEP_KEYS.FEATURE_FLAGS, () => flags);
695
- container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
696
- container.register(DEP_KEYS.EVENT_RECORDER, () => eventRecorder ?? new ConsoleEventRecorder());
697
- if (locale) container.register(DEP_KEYS.LOCALE, () => getLocaleAttributes(locale));
698
- if (locale && messages) {
699
- const flat = resolveMessages(messages, locale);
700
- if (flat) container.register(DEP_KEYS.TRANSLATOR, () => new SimpleTranslator({
701
- locale,
702
- messages: flat
703
- }));
704
- }
705
- container.register(DEP_KEYS.PLATFORM, () => platform ?? detectPlatform(typeof navigator !== "undefined" ? navigator.userAgent : void 0));
706
- container.register(DEP_KEYS.FETCH, () => fetchFn);
707
- }
708
- //#endregion
709
- //#region ../core/src/router/router.ts
710
- /**
711
- * URL 路由器 — URL pattern → Intent + FlowAction
712
- */
713
- function createNullPrototypeRecord(source) {
714
- return Object.assign(Object.create(null), source);
715
- }
716
- var Router = class {
717
- routes = [];
718
- /** 添加路由规则 */
719
- add(pattern, intentId, renderModeOrOptions) {
720
- const opts = typeof renderModeOrOptions === "string" ? { renderMode: renderModeOrOptions } : renderModeOrOptions ?? {};
721
- const paramNames = [];
722
- const regexStr = pattern.split(/(\/:[\w]+\??)/).map((segment) => {
723
- const paramMatch = segment.match(/^\/:(\w+)(\?)?$/);
724
- if (paramMatch) {
725
- if (paramNames.includes(paramMatch[1])) throw new Error(`[Router] Duplicate parameter name ":${paramMatch[1]}" in pattern "${pattern}"`);
726
- paramNames.push(paramMatch[1]);
727
- return paramMatch[2] ? "(?:/([^/]+))?" : "/([^/]+)";
728
- }
729
- return segment.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
730
- }).join("");
731
- this.routes.push({
732
- pattern,
733
- intentId,
734
- regex: new RegExp(`^${regexStr}/?$`),
735
- paramNames,
736
- renderMode: opts.renderMode,
737
- beforeGuards: opts.beforeGuards,
738
- afterGuards: opts.afterGuards
739
- });
740
- return this;
741
- }
742
- /** 解析 URL → RouteMatch */
743
- resolve(urlOrPath) {
744
- const { path, queryParams } = this.parseUrl(urlOrPath);
745
- for (const route of this.routes) {
746
- const match = path.match(route.regex);
747
- if (match) {
748
- const params = createNullPrototypeRecord(queryParams);
749
- route.paramNames.forEach((name, index) => {
750
- const value = match[index + 1];
751
- if (value) params[name] = value;
752
- });
753
- return {
754
- intent: {
755
- id: route.intentId,
756
- params
757
- },
758
- action: makeFlowAction(urlOrPath),
759
- renderMode: route.renderMode,
760
- beforeGuards: route.beforeGuards,
761
- afterGuards: route.afterGuards
762
- };
763
- }
764
- }
765
- return null;
766
- }
767
- /** 获取所有已注册的路由 */
768
- getRoutes() {
769
- return this.routes.map((r) => `${r.pattern} → ${r.intentId}`);
770
- }
771
- parseUrl(url) {
772
- try {
773
- const parsed = new URL(url, "http://localhost");
774
- const params = createNullPrototypeRecord(Object.fromEntries(parsed.searchParams));
775
- return {
776
- path: parsed.pathname,
777
- queryParams: params
778
- };
779
- } catch {
780
- return {
781
- path: url.split("?")[0].split("#")[0],
782
- queryParams: createNullPrototypeRecord()
783
- };
784
- }
785
- }
786
- };
787
- //#endregion
788
- //#region ../core/src/middleware/pipeline.ts
789
- /** 执行 beforeLoad 守卫链 */
790
- async function runBeforeLoadGuards(guards, ctx) {
791
- for (const guard of guards) {
792
- const result = await guard(ctx);
793
- if (result.kind !== "next") return result;
794
- }
795
- return { kind: "next" };
796
- }
797
- /** 执行 afterLoad 守卫链 */
798
- async function runAfterLoadGuards(guards, ctx) {
799
- for (const guard of guards) {
800
- const result = await guard(ctx);
801
- if (result.kind !== "next") return result;
802
- }
803
- return { kind: "next" };
804
- }
805
- //#endregion
806
- //#region ../core/src/prefetched-intents/stable-stringify.ts
807
- /**
808
- * stableStringify — 确定性 JSON 序列化(keys 按字母排序)
809
- *
810
- * 用作缓存 key:相同内容的对象始终产生相同字符串。
811
- */
812
- /** WeakMap 缓存已序列化的对象,避免重复计算 */
813
- const stringifyCache = /* @__PURE__ */ new WeakMap();
814
- /** 最大递归深度 */
815
- const MAX_DEPTH = 50;
816
- function stableStringify(obj) {
817
- return stringifyWithContext(obj, /* @__PURE__ */ new Set(), 0);
818
- }
819
- function stringifyWithContext(obj, seen, depth) {
820
- if (obj === null || obj === void 0) return String(obj);
821
- if (typeof obj !== "object") return JSON.stringify(obj);
822
- const cached = stringifyCache.get(obj);
823
- if (cached !== void 0) return cached;
824
- if (depth > MAX_DEPTH) return "\"[Max Depth]\"";
825
- if (seen.has(obj)) return "\"[Circular]\"";
826
- seen.add(obj);
827
- try {
828
- let result;
829
- if (Array.isArray(obj)) result = "[" + obj.map((v) => stringifyWithContext(v, seen, depth + 1)).join(",") + "]";
830
- else {
831
- const keys = Object.keys(obj).sort();
832
- const parts = [];
833
- for (const k of keys) {
834
- const v = obj[k];
835
- if (v !== void 0) parts.push(JSON.stringify(k) + ":" + stringifyWithContext(v, seen, depth + 1));
836
- }
837
- result = "{" + parts.join(",") + "}";
838
- }
839
- stringifyCache.set(obj, result);
840
- return result;
841
- } finally {
842
- seen.delete(obj);
843
- }
844
- }
845
- //#endregion
846
- //#region ../core/src/prefetched-intents/prefetched-intents.ts
847
- var PrefetchedIntents = class PrefetchedIntents {
848
- intents;
849
- constructor(intents) {
850
- this.intents = intents;
851
- }
852
- /** 从 PrefetchedIntent 数组创建缓存实例 */
853
- static fromArray(items) {
854
- const map = /* @__PURE__ */ new Map();
855
- for (const item of items) if (item.intent && item.data !== void 0) {
856
- const key = stableStringify(item.intent);
857
- map.set(key, item.data);
858
- }
859
- return new PrefetchedIntents(map);
860
- }
861
- /** 创建空缓存实例 */
862
- static empty() {
863
- return new PrefetchedIntents(/* @__PURE__ */ new Map());
864
- }
865
- /**
866
- * 获取缓存的 Intent 结果(一次性使用)。
867
- * 命中后从缓存中删除。
868
- */
869
- get(intent) {
870
- const key = stableStringify(intent);
871
- const data = this.intents.get(key);
872
- if (data !== void 0) {
873
- this.intents.delete(key);
874
- return data;
875
- }
876
- }
877
- /** 检查缓存中是否有某个 Intent 的数据 */
878
- has(intent) {
879
- return this.intents.has(stableStringify(intent));
880
- }
881
- /** 缓存中的条目数 */
882
- get size() {
883
- return this.intents.size;
884
- }
885
- };
886
- //#endregion
887
- //#region ../core/src/framework.ts
888
- /**
889
- * Framework — 框架核心类
890
- *
891
- * 对应原版 Jet 类,统一管理: DI 容器、Intent 分发、Action 分发、路由、Metrics。
892
- * 纯 TypeScript,不依赖任何 UI 框架。
893
- */
894
- var Framework = class Framework {
895
- container;
896
- intentDispatcher;
897
- actionDispatcher;
898
- router;
899
- prefetchedIntents;
900
- beforeGuards = [];
901
- afterGuards = [];
902
- _logger;
903
- constructor(container, prefetchedIntents) {
904
- this.container = container;
905
- this.intentDispatcher = new IntentDispatcher();
906
- this.actionDispatcher = new ActionDispatcher();
907
- this.router = new Router();
908
- this.prefetchedIntents = prefetchedIntents;
909
- }
910
- /** 创建并初始化 Framework 实例 */
911
- static create(config = {}) {
912
- const container = new Container();
913
- makeDependencies(container, config);
914
- const fw = new Framework(container, config.prefetchedIntents ?? PrefetchedIntents.empty());
915
- config.setupRoutes?.(fw.router);
916
- return fw;
917
- }
918
- getLogger() {
919
- return this._logger ??= this.container.resolve(DEP_KEYS.LOGGER);
920
- }
921
- /** 分发 Intent — 获取页面数据 */
922
- async dispatch(intent) {
923
- const logger = this.getLogger();
924
- const cached = this.prefetchedIntents.get(intent);
925
- if (cached !== void 0) {
926
- logger.debug(`[Framework] re-using prefetched intent response for: ${intent.id}`, intent.params);
927
- return cached;
928
- }
929
- logger.debug(`[Framework] dispatch intent: ${intent.id}`, intent.params);
930
- return this.intentDispatcher.dispatch(intent, this.container);
931
- }
932
- /** 执行 Action — 处理用户交互 */
933
- async perform(action) {
934
- this.getLogger().debug(`[Framework] perform action: ${action.kind}`);
935
- return this.actionDispatcher.perform(action);
936
- }
937
- /** 路由 URL — 将 URL 解析为 Intent + Action */
938
- routeUrl(url) {
939
- return this.router.resolve(url);
940
- }
941
- /** 记录页面访问事件 */
942
- didEnterPage(page) {
943
- this.container.resolve(DEP_KEYS.METRICS).recordPageView(page.pageType, {
944
- pageId: page.id,
945
- title: page.title
946
- });
947
- }
948
- /** 获取 locale 信息(如果已配置) */
949
- getLocale() {
950
- return this.container.has(DEP_KEYS.LOCALE) ? this.container.resolve(DEP_KEYS.LOCALE) : void 0;
951
- }
952
- /** 获取翻译器(如果当前 locale 已经初始化了翻译字典) */
953
- getTranslator() {
954
- return this.container.has(DEP_KEYS.TRANSLATOR) ? this.container.resolve(DEP_KEYS.TRANSLATOR) : void 0;
955
- }
956
- /** 获取平台信息 */
957
- getPlatform() {
958
- return this.container.resolve(DEP_KEYS.PLATFORM);
959
- }
960
- /** 注册 Action 处理器 */
961
- onAction(kind, handler) {
962
- this.actionDispatcher.onAction(kind, handler);
963
- }
964
- /** 注册 Intent Controller */
965
- registerIntent(controller) {
966
- this.intentDispatcher.register(controller);
967
- }
968
- /** 注册 beforeLoad 守卫(路由匹配后、数据加载前) */
969
- beforeLoad(guard) {
970
- this.beforeGuards.push(guard);
971
- }
972
- /** 注册 afterLoad 守卫(数据加载后、渲染前) */
973
- afterLoad(guard) {
974
- this.afterGuards.push(guard);
975
- }
976
- /** 执行所有 beforeLoad 守卫(全局 → 路由级) */
977
- runBeforeLoad(ctx, routeGuards) {
978
- return runBeforeLoadGuards(routeGuards?.length ? [...this.beforeGuards, ...routeGuards] : this.beforeGuards, ctx);
979
- }
980
- /** 执行所有 afterLoad 守卫(全局 → 路由级) */
981
- runAfterLoad(ctx, routeGuards) {
982
- return runAfterLoadGuards(routeGuards?.length ? [...this.afterGuards, ...routeGuards] : this.afterGuards, ctx);
983
- }
984
- /** 销毁 Framework 实例 */
985
- dispose() {
986
- this.container.dispose();
987
- }
988
- };
989
- //#endregion
990
- //#region ../core/src/http/client.ts
991
- /**
992
- * HttpClient — 通用 HTTP 客户端基类
993
- *
994
- * 为 API Client 提供标准化的 HTTP 请求能力。
995
- * 子类继承后只需关注业务端点定义,不需要重复实现 fetch / JSON 解析 / 错误处理。
996
- */
997
- /** HTTP 请求错误 */
998
- var HttpError = class extends Error {
999
- constructor(status, statusText, body) {
1000
- super(`HTTP ${status}: ${statusText}`);
1001
- this.status = status;
1002
- this.statusText = statusText;
1003
- this.body = body;
1004
- this.name = "HttpError";
1005
- }
1006
- };
1007
- /**
1008
- * 通用 HTTP 客户端基类
1009
- *
1010
- * 使用方式: 创建子类继承 HttpClient,定义业务方法调用 this.get() / this.post() 等。
1011
- *
1012
- * @example
1013
- * ```ts
1014
- * class MyApiClient extends HttpClient {
1015
- * async getUser(id: string) {
1016
- * return this.get<User>(`/users/${id}`);
1017
- * }
1018
- * }
1019
- * ```
1020
- */
1021
- var HttpClient = class {
1022
- baseUrl;
1023
- defaultHeaders;
1024
- fetchFn;
1025
- requestInterceptors;
1026
- responseInterceptors;
1027
- constructor(config) {
1028
- this.baseUrl = config.baseUrl;
1029
- this.defaultHeaders = config.defaultHeaders ?? {};
1030
- this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
1031
- this.requestInterceptors = [...config.requestInterceptors ?? []];
1032
- this.responseInterceptors = [...config.responseInterceptors ?? []];
1033
- }
1034
- /** 动态添加请求拦截器 */
1035
- useRequestInterceptor(interceptor) {
1036
- this.requestInterceptors.push(interceptor);
1037
- return this;
1038
- }
1039
- /** 动态添加响应拦截器 */
1040
- useResponseInterceptor(interceptor) {
1041
- this.responseInterceptors.push(interceptor);
1042
- return this;
1043
- }
1044
- /** GET 请求,返回解析后的 JSON */
1045
- async get(path, params) {
1046
- return this.request("GET", path, { params });
1047
- }
1048
- /** POST 请求,自动序列化 body 为 JSON */
1049
- async post(path, body, params) {
1050
- return this.request("POST", path, {
1051
- body,
1052
- params
1053
- });
1054
- }
1055
- /** PUT 请求 */
1056
- async put(path, body, params) {
1057
- return this.request("PUT", path, {
1058
- body,
1059
- params
1060
- });
1061
- }
1062
- /** DELETE 请求 */
1063
- async del(path, params) {
1064
- return this.request("DELETE", path, { params });
1065
- }
1066
- /**
1067
- * 底层请求方法 — 子类可覆写以自定义行为
1068
- *
1069
- * 自动处理:
1070
- * - URL 拼接 (baseUrl + path + params)
1071
- * - 默认 headers 合并
1072
- * - JSON body 序列化
1073
- * - 响应 JSON 解析
1074
- * - 非 2xx 状态码抛出 HttpError
1075
- */
1076
- async request(method, path, options) {
1077
- const url = this.buildUrl(path, options?.params);
1078
- const headers = {
1079
- ...this.defaultHeaders,
1080
- ...options?.headers
1081
- };
1082
- let init = {
1083
- method,
1084
- headers
1085
- };
1086
- if (options?.body !== void 0) {
1087
- if (!Object.keys(headers).some((k) => k.toLowerCase() === "content-type")) headers["Content-Type"] = "application/json";
1088
- init.body = JSON.stringify(options.body);
1089
- }
1090
- for (const interceptor of this.requestInterceptors) init = await interceptor(url, init);
1091
- let response = await this.fetchFn(url, init);
1092
- for (const interceptor of this.responseInterceptors) response = await interceptor(response, url);
1093
- if (!response.ok) {
1094
- const body = await response.text().catch(() => void 0);
1095
- throw new HttpError(response.status, response.statusText, body);
1096
- }
1097
- try {
1098
- return await response.json();
1099
- } catch (e) {
1100
- if (e instanceof SyntaxError) throw new HttpError(response.status, "Invalid JSON response", await response.text().catch(() => void 0));
1101
- throw e;
1102
- }
1103
- }
1104
- /** 构建完整 URL — 子类可覆写以自定义 URL 拼接逻辑 */
1105
- buildUrl(path, params) {
1106
- const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
1107
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
1108
- const url = new URL(`${base}${normalizedPath}`, "http://placeholder");
1109
- if (params) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
1110
- if (this.baseUrl.startsWith("http")) return url.toString();
1111
- return `${url.pathname}${url.search}`;
1112
- }
1113
- };
1114
- //#endregion
1115
- //#region ../core/src/intents/base-controller.ts
1116
- /**
1117
- * 抽象 Controller 基类
1118
- *
1119
- * 统一处理:
1120
- * - 类型安全的参数提取 (TParams)
1121
- * - 返回类型约束 (TResult)
1122
- * - try/catch 错误处理 + 可选 fallback
1123
- *
1124
- * @example
1125
- * ```ts
1126
- * class ProductController extends BaseController<{ productId: string }, ProductPage> {
1127
- * readonly intentId = "product-page";
1128
- *
1129
- * async execute(params: { productId: string }, container: Container) {
1130
- * const api = container.resolve<ApiClient>("api");
1131
- * return api.getProduct(params.productId);
1132
- * }
1133
- *
1134
- * fallback(params: { productId: string }, error: Error) {
1135
- * return getMockProduct(params.productId);
1136
- * }
1137
- * }
1138
- * ```
1139
- */
1140
- var BaseController = class {
1141
- /**
1142
- * 错误回退 — 子类可选覆写
1143
- *
1144
- * 当 execute() 抛出异常时调用。
1145
- * 默认行为: 重新抛出原始错误。
1146
- *
1147
- * @param params - Intent 参数
1148
- * @param error - execute() 抛出的错误
1149
- * @returns 回退数据
1150
- */
1151
- fallback(params, error) {
1152
- throw error;
1153
- }
1154
- /**
1155
- * IntentController.perform() 实现
1156
- *
1157
- * 自动 try/catch → fallback 模式。
1158
- */
1159
- async perform(intent, container) {
1160
- const params = intent.params ?? {};
1161
- try {
1162
- return await this.execute(params, container);
1163
- } catch (e) {
1164
- return this.fallback(params, e instanceof Error ? e : new Error(String(e)));
1165
- }
1166
- }
1167
- };
1168
- //#endregion
1169
- //#region ../core/src/data/mapper.ts
1170
- function pipe(...mappers) {
1171
- return (input) => mappers.reduce((acc, mapper) => mapper(acc), input);
1172
- }
1173
- function pipeAsync(...mappers) {
1174
- return async (input) => {
1175
- let acc = input;
1176
- for (const mapper of mappers) acc = await mapper(acc);
1177
- return acc;
1178
- };
1179
- }
1180
- /**
1181
- * 将一个 Mapper 应用到数组的每个元素
1182
- */
1183
- function mapEach(mapper) {
1184
- return (items) => items.map(mapper);
1185
- }
1186
- //#endregion
1187
- //#region ../core/src/bootstrap/define-routes.ts
1188
- /**
1189
- * 声明式注册路由和 Controller
1190
- *
1191
- * - 自动去重: 同一 intentId 的 controller 只注册一次
1192
- * - 路由和 controller 在同一个配置数组中,方便检查一致性
1193
- *
1194
- * @example
1195
- * ```ts
1196
- * defineRoutes(framework, [
1197
- * { path: "/", intentId: "home", controller: new HomeController() },
1198
- * { path: "/product/:id", intentId: "product", controller: new ProductController() },
1199
- * { path: "/search", intentId: "search", controller: new SearchController() },
1200
- * { path: "/charts/:type", intentId: "charts", controller: new ChartsController() },
1201
- * { path: "/charts", intentId: "charts" }, // 同 intentId,不需要重复 controller
1202
- * ]);
1203
- * ```
1204
- */
1205
- function defineRoutes(framework, definitions, options) {
1206
- const registeredIntents = /* @__PURE__ */ new Set();
1207
- for (const def of definitions) {
1208
- if (def.controller && !registeredIntents.has(def.intentId)) {
1209
- framework.registerIntent(def.controller);
1210
- registeredIntents.add(def.intentId);
1211
- }
1212
- const routeOpts = {
1213
- renderMode: def.renderMode,
1214
- beforeGuards: def.beforeLoad,
1215
- afterGuards: def.afterLoad
1216
- };
1217
- framework.router.add(def.path, def.intentId, routeOpts);
1218
- if (options?.locales?.length) {
1219
- const localePath = def.path === "/" ? "/:locale" : `/:locale${def.path}`;
1220
- framework.router.add(localePath, def.intentId, routeOpts);
1221
- }
1222
- }
1223
- }
1224
- //#endregion
1225
- //#region ../core/src/utils/lru-map.ts
1226
- /**
1227
- * LruMap — 固定容量的 LRU 缓存
1228
- */
1229
- var LruMap = class {
1230
- map = /* @__PURE__ */ new Map();
1231
- capacity;
1232
- constructor(capacity) {
1233
- if (capacity < 1) throw new Error(`[LruMap] capacity must be >= 1, got ${capacity}`);
1234
- this.capacity = capacity;
1235
- }
1236
- get(key) {
1237
- if (!this.map.has(key)) return void 0;
1238
- const value = this.map.get(key);
1239
- this.map.delete(key);
1240
- this.map.set(key, value);
1241
- return value;
1242
- }
1243
- set(key, value) {
1244
- if (this.map.has(key)) this.map.delete(key);
1245
- else if (this.map.size >= this.capacity) {
1246
- const oldest = this.map.keys().next().value;
1247
- if (oldest !== void 0) this.map.delete(oldest);
1248
- }
1249
- this.map.set(key, value);
1250
- }
1251
- has(key) {
1252
- return this.map.has(key);
1253
- }
1254
- delete(key) {
1255
- return this.map.delete(key);
1256
- }
1257
- get size() {
1258
- return this.map.size;
1259
- }
1260
- clear() {
1261
- this.map.clear();
1262
- }
1263
- };
1264
- //#endregion
1265
- //#region ../core/src/utils/optional.ts
1266
- function isSome(value) {
1267
- return value !== null && value !== void 0;
1268
- }
1269
- function isNone(value) {
1270
- return value === null || value === void 0;
1271
- }
1272
- //#endregion
1273
- //#region ../core/src/utils/pwa.ts
1274
- /**
1275
- * 检测 PWA display mode
1276
- *
1277
- * - `standalone`: 已安装的 PWA(通过 Add to Home Screen)
1278
- * - `twa`: Trusted Web Activity(Android 原生壳)
1279
- * - `browser`: 普通浏览器标签页
1280
- */
1281
- function getPWADisplayMode() {
1282
- if (typeof window === "undefined") return "browser";
1283
- if (document.referrer.startsWith("android-app://")) return "twa";
1284
- if (window.matchMedia("(display-mode: standalone)").matches || "standalone" in window.navigator && window.navigator.standalone === true) return "standalone";
1285
- return "browser";
1286
- }
1287
- //#endregion
1288
- //#region ../core/src/utils/url.ts
1289
- /**
1290
- * URL 工具函数
1291
- */
1292
- /** 移除 URL scheme (https://, http://) */
1293
- function removeScheme(url) {
1294
- return url.replace(/^https?:\/\//, "");
1295
- }
1296
- /** 移除 URL host 部分,保留路径 */
1297
- function removeHost(url) {
1298
- try {
1299
- const parsed = new URL(url);
1300
- return parsed.pathname + parsed.search + parsed.hash;
1301
- } catch {
1302
- return url;
1303
- }
1304
- }
1305
- /** 移除 query 参数 */
1306
- function removeQueryParams(url) {
1307
- return url.split("?")[0];
1308
- }
1309
- /** 获取 URL 的基础路径(无 query、hash) */
1310
- function getBaseUrl(url) {
1311
- return url.split("?")[0].split("#")[0];
1312
- }
1313
- /** 构建 URL(路径 + query 参数) */
1314
- function buildUrl(path, params) {
1315
- if (!params) return path;
1316
- const searchParams = new URLSearchParams();
1317
- for (const [key, value] of Object.entries(params)) if (value !== void 0) searchParams.set(key, value);
1318
- const qs = searchParams.toString();
1319
- return qs ? `${path}?${qs}` : path;
1320
- }
1321
- //#endregion
1322
- //#region ../core/src/utils/uuid.ts
1323
- /**
1324
- * UUID v4 生成器
1325
- */
1326
- function generateUuid() {
1327
- if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
1328
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1329
- const r = Math.random() * 16 | 0;
1330
- return (c === "x" ? r : r & 3 | 8).toString(16);
1331
- });
1332
- }
1333
- //#endregion
1334
- //#region ../core/src/middleware/context.ts
1335
- function parseCookieString(str) {
1336
- const map = /* @__PURE__ */ new Map();
1337
- if (!str) return map;
1338
- for (const pair of str.split(";")) {
1339
- const idx = pair.indexOf("=");
1340
- if (idx === -1) continue;
1341
- const key = pair.slice(0, idx).trim();
1342
- const val = pair.slice(idx + 1).trim();
1343
- if (key) map.set(key, val);
1344
- }
1345
- return map;
1346
- }
1347
- /** 从 Request 对象构建服务端上下文 */
1348
- function createServerContext(options) {
1349
- const { url, intent, container, request } = options;
1350
- const parsed = new URL(url, "http://localhost");
1351
- const cookies = parseCookieString(request?.headers.get("cookie") ?? "");
1352
- return {
1353
- url,
1354
- path: parsed.pathname,
1355
- params: intent.params ?? {},
1356
- intent,
1357
- isServer: true,
1358
- container,
1359
- getCookie: (name) => cookies.get(name),
1360
- getHeader: (name) => request?.headers.get(name) ?? void 0
1361
- };
1362
- }
1363
- /** 从 document.cookie 构建浏览器端上下文 */
1364
- function createBrowserContext(options) {
1365
- const { url, intent, container } = options;
1366
- return {
1367
- url,
1368
- path: new URL(url, window.location.origin).pathname,
1369
- params: intent.params ?? {},
1370
- intent,
1371
- isServer: false,
1372
- container,
1373
- getCookie: (name) => parseCookieString(document.cookie).get(name),
1374
- getHeader: () => void 0
1375
- };
1376
- }
1377
- //#endregion
1378
- //#region ../core/src/middleware/types.ts
1379
- /** 继续执行 */
1380
- function next() {
1381
- return { kind: "next" };
1382
- }
1383
- /** 重定向到新 URL */
1384
- function redirect(url, status = 302) {
1385
- return {
1386
- kind: "redirect",
1387
- url,
1388
- status
1389
- };
1390
- }
1391
- /** URL 重写(不重新加载数据) */
1392
- function rewrite(url) {
1393
- return {
1394
- kind: "rewrite",
1395
- url
1396
- };
1397
- }
1398
- /** 拒绝访问 */
1399
- function deny(status = 403, message = "Forbidden") {
1400
- return {
1401
- kind: "deny",
1402
- status,
1403
- message
1404
- };
1405
- }
1406
- //#endregion
1407
- //#region ../core/src/metrics/composite-recorder.ts
1408
- var CompositeEventRecorder = class {
1409
- recorders;
1410
- constructor(recorders) {
1411
- this.recorders = recorders;
1412
- }
1413
- record(type, fields) {
1414
- for (const recorder of this.recorders) recorder.record(type, fields);
1415
- }
1416
- async flush() {
1417
- const pending = [];
1418
- for (const recorder of this.recorders) if (recorder.flush) pending.push(recorder.flush());
1419
- await Promise.all(pending);
1420
- }
1421
- destroy() {
1422
- for (const recorder of this.recorders) recorder.destroy?.();
1423
- }
1424
- };
1425
- //#endregion
1426
- //#region ../core/src/metrics/impression-observer.ts
1427
- var IntersectionImpressionObserver = class {
1428
- observer;
1429
- tracked = /* @__PURE__ */ new Map();
1430
- captured = [];
1431
- minDuration;
1432
- constructor(options = {}) {
1433
- this.minDuration = options.minVisibleDuration ?? 1e3;
1434
- this.observer = new IntersectionObserver((entries) => {
1435
- const now = Date.now();
1436
- for (const entry of entries) {
1437
- const tracked = this.tracked.get(entry.target);
1438
- if (!tracked) continue;
1439
- if (entry.isIntersecting) {
1440
- if (tracked.visibleSince === null) tracked.visibleSince = now;
1441
- } else if (tracked.visibleSince !== null) {
1442
- if (now - tracked.visibleSince >= this.minDuration) this.captured.push({
1443
- id: tracked.id,
1444
- timestamp: tracked.visibleSince,
1445
- metadata: tracked.metadata
1446
- });
1447
- tracked.visibleSince = null;
1448
- }
1449
- }
1450
- }, { threshold: options.threshold ?? .5 });
1451
- }
1452
- observe(element, id, metadata) {
1453
- this.tracked.set(element, {
1454
- id,
1455
- metadata,
1456
- visibleSince: null
1457
- });
1458
- this.observer.observe(element);
1459
- }
1460
- unobserve(element) {
1461
- this.observer.unobserve(element);
1462
- this.tracked.delete(element);
1463
- }
1464
- consume() {
1465
- const now = Date.now();
1466
- for (const [, tracked] of this.tracked) if (tracked.visibleSince !== null) {
1467
- if (now - tracked.visibleSince >= this.minDuration) {
1468
- this.captured.push({
1469
- id: tracked.id,
1470
- timestamp: tracked.visibleSince,
1471
- metadata: tracked.metadata
1472
- });
1473
- tracked.visibleSince = now;
1474
- }
1475
- }
1476
- return this.captured.splice(0);
1477
- }
1478
- destroy() {
1479
- this.observer.disconnect();
1480
- this.tracked.clear();
1481
- this.captured.length = 0;
1482
- }
1483
- };
1484
- //#endregion
1485
- //#region ../core/src/metrics/void-recorder.ts
1486
- var VoidEventRecorder = class {
1487
- record() {}
1488
- async flush() {}
1489
- destroy() {}
1490
- };
1491
- //#endregion
1492
- //#region ../core/src/metrics/with-fields-recorder.ts
1493
- var WithFieldsRecorder = class {
1494
- constructor(inner, providers) {
1495
- this.inner = inner;
1496
- this.providers = providers;
1497
- }
1498
- record(type, fields) {
1499
- let merged = {};
1500
- for (const provider of this.providers) Object.assign(merged, provider.getFields());
1501
- if (fields) Object.assign(merged, fields);
1502
- this.inner.record(type, merged);
1503
- }
1504
- async flush() {
1505
- return this.inner.flush?.();
1506
- }
1507
- destroy() {
1508
- this.inner.destroy?.();
1509
- }
1510
- };
1511
- //#endregion
1512
- //#region ../browser/src/action-handlers/external-url-action.ts
1513
- function registerExternalUrlHandler(deps) {
1514
- const { framework, log } = deps;
1515
- framework.onAction(ACTION_KINDS.EXTERNAL_URL, (action) => {
1516
- log.debug(`ExternalUrlAction → ${action.url}`);
1517
- window.open(action.url, "_blank", "noopener,noreferrer");
1518
- });
1519
- }
1520
- //#endregion
1521
- //#region ../browser/src/utils/try-scroll.ts
1522
- const MAX_WAIT_MS = 5e3;
1523
- const POLL_INTERVAL_MS = 100;
1524
- const SCROLL_TOLERANCE = 2;
1525
- let pendingCleanup = null;
1526
- function cancelTryScroll() {
1527
- pendingCleanup?.();
1528
- }
1529
- function tryScroll(log, getScrollableElement, scrollY) {
1530
- cancelTryScroll();
1531
- const target = Math.max(0, scrollY);
1532
- const startedAt = Date.now();
1533
- let disposed = false;
1534
- let pendingFrame = null;
1535
- let intervalId = null;
1536
- let timeoutId = null;
1537
- let mutationObserver = null;
1538
- pendingCleanup = cleanup;
1539
- observeDocumentActivity();
1540
- document.addEventListener("load", scheduleAttempt, true);
1541
- intervalId = setInterval(scheduleAttempt, POLL_INTERVAL_MS);
1542
- timeoutId = setTimeout(scheduleAttempt, MAX_WAIT_MS);
1543
- scheduleAttempt();
1544
- function scheduleAttempt() {
1545
- if (disposed || pendingFrame !== null) return;
1546
- pendingFrame = requestAnimationFrame(() => {
1547
- pendingFrame = null;
1548
- attemptRestore();
1549
- });
1550
- }
1551
- function attemptRestore() {
1552
- if (disposed) return;
1553
- const elapsedMs = Date.now() - startedAt;
1554
- const element = getScrollableElement();
1555
- if (!element) {
1556
- if (elapsedMs >= MAX_WAIT_MS) {
1557
- log.warn("tryScroll: timed out waiting for the scrollable element", {
1558
- target,
1559
- elapsedMs
1560
- });
1561
- cleanup();
1562
- }
1563
- return;
1564
- }
1565
- element.scrollTop = target;
1566
- const actual = element.scrollTop;
1567
- if (actual >= target - SCROLL_TOLERANCE) {
1568
- log.info("scroll restored", {
1569
- target,
1570
- actual,
1571
- elapsedMs
1572
- });
1573
- cleanup();
1574
- return;
1575
- }
1576
- if (elapsedMs >= MAX_WAIT_MS) {
1577
- log.warn("tryScroll: timed out before reaching the target", {
1578
- target,
1579
- actual,
1580
- elapsedMs,
1581
- scrollHeight: element.scrollHeight,
1582
- clientHeight: element.clientHeight
1583
- });
1584
- cleanup();
1585
- }
1586
- }
1587
- function observeDocumentActivity() {
1588
- if (typeof MutationObserver === "undefined") return;
1589
- const root = document.body ?? document.documentElement;
1590
- if (!root) return;
1591
- mutationObserver = new MutationObserver(() => {
1592
- scheduleAttempt();
1593
- });
1594
- mutationObserver.observe(root, {
1595
- childList: true,
1596
- subtree: true
1597
- });
1598
- }
1599
- function cleanup() {
1600
- if (disposed) return;
1601
- disposed = true;
1602
- if (pendingCleanup === cleanup) pendingCleanup = null;
1603
- if (pendingFrame !== null) {
1604
- cancelAnimationFrame(pendingFrame);
1605
- pendingFrame = null;
1606
- }
1607
- if (intervalId !== null) {
1608
- clearInterval(intervalId);
1609
- intervalId = null;
1610
- }
1611
- if (timeoutId !== null) {
1612
- clearTimeout(timeoutId);
1613
- timeoutId = null;
1614
- }
1615
- mutationObserver?.disconnect();
1616
- mutationObserver = null;
1617
- document.removeEventListener("load", scheduleAttempt, true);
1618
- }
1619
- }
1620
- //#endregion
1621
- //#region ../browser/src/utils/history.ts
1622
- const HISTORY_SIZE_LIMIT = 10;
1623
- var History = class {
1624
- entries;
1625
- log;
1626
- getScrollablePageElement;
1627
- currentStateId;
1628
- constructor(log, options, sizeLimit = HISTORY_SIZE_LIMIT) {
1629
- this.entries = new LruMap(sizeLimit);
1630
- this.log = log;
1631
- this.getScrollablePageElement = options.getScrollablePageElement;
1632
- }
1633
- replaceState(state, url) {
1634
- cancelTryScroll();
1635
- const id = generateUuid();
1636
- window.history.replaceState({ id }, "", url);
1637
- this.currentStateId = id;
1638
- this.entries.set(id, {
1639
- state,
1640
- scrollY: 0
1641
- });
1642
- this.scrollTop = 0;
1643
- this.log.info("replaceState", state, url, id);
1644
- }
1645
- pushState(state, url) {
1646
- cancelTryScroll();
1647
- const id = generateUuid();
1648
- window.history.pushState({ id }, "", url);
1649
- this.currentStateId = id;
1650
- this.entries.set(id, {
1651
- state,
1652
- scrollY: 0
1653
- });
1654
- this.scrollTop = 0;
1655
- this.log.info("pushState", state, url, id);
1656
- }
1657
- beforeTransition() {
1658
- cancelTryScroll();
1659
- const { state } = window.history;
1660
- if (!state) return;
1661
- const oldEntry = this.entries.get(state.id);
1662
- if (!oldEntry) {
1663
- this.log.info("current history state evicted from LRU, not saving scroll position");
1664
- return;
1665
- }
1666
- const { scrollTop } = this;
1667
- this.entries.set(state.id, {
1668
- ...oldEntry,
1669
- scrollY: scrollTop
1670
- });
1671
- this.log.info("saving scroll position", scrollTop);
1672
- }
1673
- onPopState(listener) {
1674
- window.addEventListener("popstate", (event) => {
1675
- cancelTryScroll();
1676
- this.currentStateId = event.state?.id;
1677
- if (!this.currentStateId) this.log.warn("encountered a null event.state.id in onPopState event:", window.location.href);
1678
- this.log.info("popstate", this.entries, this.currentStateId);
1679
- const entry = this.currentStateId ? this.entries.get(this.currentStateId) : void 0;
1680
- Promise.resolve(listener(window.location.href, entry?.state)).catch((error) => {
1681
- this.log.error("onPopState listener error:", error);
1682
- });
1683
- if (!entry) return;
1684
- const { scrollY } = entry;
1685
- this.log.info("restoring scroll to", scrollY);
1686
- tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
1687
- });
1688
- }
1689
- /** 仅推入 URL,不缓存页面状态(用于页面加载失败场景) */
1690
- pushUrl(url) {
1691
- cancelTryScroll();
1692
- const id = generateUuid();
1693
- window.history.pushState({ id }, "", url);
1694
- this.currentStateId = id;
1695
- this.scrollTop = 0;
1696
- this.log.info("pushUrl (no state)", url, id);
1697
- }
1698
- /** 仅替换 URL,不缓存页面状态(用于页面加载失败场景) */
1699
- replaceUrl(url) {
1700
- cancelTryScroll();
1701
- const id = generateUuid();
1702
- window.history.replaceState({ id }, "", url);
1703
- this.currentStateId = id;
1704
- this.scrollTop = 0;
1705
- this.log.info("replaceUrl (no state)", url, id);
1706
- }
1707
- updateState(update) {
1708
- if (!this.currentStateId) {
1709
- this.log.warn("failed: encountered a null currentStateId inside updateState");
1710
- return;
1711
- }
1712
- const currentState = this.entries.get(this.currentStateId);
1713
- const newState = update(currentState?.state);
1714
- this.log.info("updateState", newState, this.currentStateId);
1715
- this.entries.set(this.currentStateId, {
1716
- scrollY: currentState?.scrollY ?? 0,
1717
- state: newState
1718
- });
1719
- }
1720
- get scrollTop() {
1721
- return this.getScrollablePageElement()?.scrollTop || 0;
1722
- }
1723
- set scrollTop(scrollTop) {
1724
- const element = this.getScrollablePageElement();
1725
- if (element) element.scrollTop = scrollTop;
1726
- }
1727
- };
1728
- //#endregion
1729
- //#region ../browser/src/action-handlers/flow-action.ts
1730
- function registerFlowActionHandler(deps) {
1731
- const { framework, log, callbacks, updateApp } = deps;
1732
- let isFirstPage = true;
1733
- let navigationId = 0;
1734
- /** 重定向循环保护计数器 */
1735
- const MAX_REDIRECTS = 5;
1736
- const defaultGetScrollable = () => document.getElementById("scrollable-page-override") || document.getElementById("scrollable-page") || document.documentElement;
1737
- const history = new History(log, { getScrollablePageElement: deps.getScrollablePageElement ?? defaultGetScrollable });
1738
- /**
1739
- * 核心导航逻辑(支持递归重定向)
1740
- * @param redirectCount 当前重定向次数,用于循环保护
1741
- */
1742
- async function navigateTo(url, redirectCount, thisNav) {
1743
- if (redirectCount >= MAX_REDIRECTS) {
1744
- log.error(`Navigation redirect loop detected (${MAX_REDIRECTS} redirects), stopping at: ${url}`);
1745
- return;
1746
- }
1747
- const shouldReplace = isFirstPage || url === window.location.pathname + window.location.search;
1748
- const match = framework.routeUrl(url);
1749
- if (!match) {
1750
- log.warn(`FlowAction: no route for ${url}`);
1751
- return;
1752
- }
1753
- const navCtx = createBrowserContext({
1754
- url,
1755
- intent: match.intent,
1756
- container: framework.container
1757
- });
1758
- const beforeResult = await framework.runBeforeLoad(navCtx, match.beforeGuards);
1759
- if (beforeResult.kind === "redirect") {
1760
- log.debug(`beforeLoad → redirect to ${beforeResult.url}`);
1761
- await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
1762
- return;
1763
- }
1764
- if (beforeResult.kind === "deny") {
1765
- log.warn(`beforeLoad → denied (${beforeResult.status}): ${beforeResult.message}`);
1766
- return;
1767
- }
1768
- if (beforeResult.kind === "rewrite") {
1769
- log.debug(`beforeLoad → rewrite to ${beforeResult.url}`);
1770
- await navigateTo(beforeResult.url, redirectCount + 1, thisNav);
1771
- return;
1772
- }
1773
- const pagePromise = framework.dispatch(match.intent);
1774
- await Promise.race([pagePromise, new Promise((r) => setTimeout(r, 500))]).catch(() => {});
1775
- if (thisNav !== navigationId) {
1776
- log.info("FlowAction superseded by newer navigation", url);
1777
- return;
1778
- }
1779
- history.beforeTransition();
1780
- updateApp({
1781
- page: pagePromise.then(async (page) => {
1782
- if (thisNav !== navigationId) {
1783
- log.info("FlowAction commit superseded", url);
1784
- return page;
1785
- }
1786
- const postCtx = {
1787
- ...navCtx,
1788
- page
1789
- };
1790
- const afterResult = await framework.runAfterLoad(postCtx, match.afterGuards);
1791
- if (afterResult.kind === "redirect") {
1792
- log.debug(`afterLoad → redirect to ${afterResult.url}`);
1793
- navigateTo(afterResult.url, redirectCount + 1, thisNav);
1794
- return page;
1795
- }
1796
- let canonicalURL = url;
1797
- if (afterResult.kind === "rewrite") {
1798
- canonicalURL = afterResult.url;
1799
- log.debug(`afterLoad → rewrite URL to ${canonicalURL}`);
1800
- }
1801
- if (afterResult.kind === "deny") {
1802
- log.warn(`afterLoad → denied (${afterResult.status})`);
1803
- return page;
1804
- }
1805
- if (shouldReplace) history.replaceState({ page }, canonicalURL);
1806
- else history.pushState({ page }, canonicalURL);
1807
- callbacks.onNavigate(new URL(canonicalURL, window.location.origin).pathname);
1808
- didEnterPage(page);
1809
- return page;
1810
- }, (error) => {
1811
- if (thisNav === navigationId) {
1812
- const canonicalURL = url;
1813
- if (shouldReplace) history.replaceUrl(canonicalURL);
1814
- else history.pushUrl(canonicalURL);
1815
- callbacks.onNavigate(new URL(canonicalURL, window.location.origin).pathname);
1816
- }
1817
- throw error;
1818
- }),
1819
- isFirstPage
1820
- });
1821
- isFirstPage = false;
1822
- }
1823
- framework.onAction(ACTION_KINDS.FLOW, async (action) => {
1824
- const flowAction = action;
1825
- const url = flowAction.url;
1826
- log.debug(`FlowAction → ${url}`);
1827
- if (flowAction.presentationContext === "modal") {
1828
- const match = framework.routeUrl(url);
1829
- if (match) {
1830
- const page = await framework.dispatch(match.intent);
1831
- callbacks.onModal(page);
1832
- }
1833
- return;
1834
- }
1835
- await navigateTo(url, 0, ++navigationId);
1836
- });
1837
- history.onPopState(async (url, cachedState) => {
1838
- log.debug(`popstate → ${url}, cached=${!!cachedState}`);
1839
- callbacks.onNavigate(new URL(url).pathname);
1840
- if (cachedState) {
1841
- const { page } = cachedState;
1842
- didEnterPage(page);
1843
- updateApp({
1844
- page,
1845
- isFirstPage
1846
- });
1847
- return;
1848
- }
1849
- const parsed = new URL(url);
1850
- const routeMatch = framework.routeUrl(parsed.pathname + parsed.search);
1851
- if (!routeMatch) {
1852
- log.error("received popstate without data, but URL was unroutable:", url);
1853
- didEnterPage(null);
1854
- updateApp({
1855
- page: Promise.reject(/* @__PURE__ */ new Error("404")),
1856
- isFirstPage
1857
- });
1858
- return;
1859
- }
1860
- const navCtx = createBrowserContext({
1861
- url: parsed.pathname + parsed.search,
1862
- intent: routeMatch.intent,
1863
- container: framework.container
1864
- });
1865
- const beforeResult = await framework.runBeforeLoad(navCtx, routeMatch.beforeGuards);
1866
- if (beforeResult.kind === "redirect") {
1867
- log.debug(`popstate beforeLoad → redirect to ${beforeResult.url}`);
1868
- const thisNav = ++navigationId;
1869
- await navigateTo(beforeResult.url, 0, thisNav);
1870
- return;
1871
- }
1872
- if (beforeResult.kind === "deny" || beforeResult.kind === "rewrite") {
1873
- if (beforeResult.kind === "deny") log.warn(`popstate beforeLoad → denied`);
1874
- else {
1875
- const thisNav = ++navigationId;
1876
- await navigateTo(beforeResult.url, 0, thisNav);
1877
- }
1878
- return;
1879
- }
1880
- const pagePromise = framework.dispatch(routeMatch.intent);
1881
- await Promise.race([pagePromise, new Promise((r) => setTimeout(r, 500))]).catch(() => {});
1882
- updateApp({
1883
- page: pagePromise.then(async (page) => {
1884
- const postCtx = {
1885
- ...navCtx,
1886
- page
1887
- };
1888
- const afterResult = await framework.runAfterLoad(postCtx, routeMatch.afterGuards);
1889
- if (afterResult.kind === "redirect") {
1890
- log.debug(`popstate afterLoad → redirect to ${afterResult.url}`);
1891
- const newNav = ++navigationId;
1892
- navigateTo(afterResult.url, 0, newNav);
1893
- return page;
1894
- }
1895
- if (afterResult.kind === "rewrite") {
1896
- log.debug(`popstate afterLoad → rewrite URL to ${afterResult.url}`);
1897
- const stateId = window.history.state?.id;
1898
- window.history.replaceState({ id: stateId }, "", afterResult.url);
1899
- callbacks.onNavigate(new URL(afterResult.url, window.location.origin).pathname);
1900
- didEnterPage(page);
1901
- return page;
1902
- }
1903
- if (afterResult.kind === "deny") {
1904
- log.warn(`popstate afterLoad → denied (${afterResult.status})`);
1905
- return page;
1906
- }
1907
- didEnterPage(page);
1908
- return page;
1909
- }),
1910
- isFirstPage
1911
- });
1912
- });
1913
- function didEnterPage(page) {
1914
- (async () => {
1915
- try {
1916
- if (page) framework.didEnterPage(page);
1917
- } catch (e) {
1918
- log.error("didEnterPage error:", e);
1919
- }
1920
- })();
1921
- }
1922
- }
1923
- //#endregion
1924
- //#region ../browser/src/action-handlers/register.ts
1925
- function registerActionHandlers(deps) {
1926
- const { framework, log, callbacks, updateApp } = deps;
1927
- registerFlowActionHandler({
1928
- framework,
1929
- log,
1930
- callbacks,
1931
- updateApp,
1932
- getScrollablePageElement: deps.getScrollablePageElement
1933
- });
1934
- registerExternalUrlHandler({
1935
- framework,
1936
- log
1937
- });
1938
- }
1939
- //#endregion
1940
- //#region ../browser/src/server-data.ts
1941
- /**
1942
- * Server Data (browser side) — 从 DOM 反序列化服务端嵌入数据
1943
- */
1944
- /** DOM 中嵌入数据的 script 标签 ID */
1945
- const SERVER_DATA_ID = "serialized-server-data";
1946
- /**
1947
- * 从 DOM 反序列化服务端嵌入的数据。
1948
- * 读取 `<script id="serialized-server-data">` 的内容并移除标签。
1949
- */
1950
- function deserializeServerData() {
1951
- const script = document.getElementById(SERVER_DATA_ID);
1952
- if (!script?.textContent) return void 0;
1953
- script.parentNode?.removeChild(script);
1954
- try {
1955
- return JSON.parse(script.textContent);
1956
- } catch {
1957
- return;
1958
- }
1959
- }
1960
- /**
1961
- * 从 DOM 提取 SSR 数据并构建 PrefetchedIntents 实例。
1962
- * 替代原来的 PrefetchedIntents.fromDom()。
1963
- */
1964
- function createPrefetchedIntentsFromDom() {
1965
- const data = deserializeServerData();
1966
- if (!data || !Array.isArray(data)) return PrefetchedIntents.empty();
1967
- return PrefetchedIntents.fromArray(data);
1968
- }
1969
- //#endregion
1970
- //#region ../browser/src/start-app.ts
1971
- /**
1972
- * 启动客户端应用
1973
- *
1974
- * 自动执行 hydration 全流程。
1975
- */
1976
- async function startBrowserApp(config) {
1977
- const { bootstrap, mountId = "app", mount, callbacks, onBeforeStart, onAfterStart, frameworkConfig = {}, loadMessages } = config;
1978
- const prefetchedIntents = createPrefetchedIntentsFromDom();
1979
- const initialUrl = window.location.pathname + window.location.search;
1980
- const locale = resolveBrowserLocale(frameworkConfig.locale);
1981
- const resolvedMessages = await resolveConfiguredMessages({
1982
- locale,
1983
- loadMessages,
1984
- context: locale ? {
1985
- runtime: "browser",
1986
- fetch: getBrowserFetch(frameworkConfig.fetch),
1987
- url: initialUrl
1988
- } : void 0
1989
- });
1990
- const framework = Framework.create({
1991
- ...frameworkConfig,
1992
- locale,
1993
- _resolvedMessages: resolvedMessages,
1994
- prefetchedIntents
1995
- });
1996
- bootstrap(framework);
1997
- const log = framework.container.resolve(DEP_KEYS.LOGGER_FACTORY).loggerFor("browser");
1998
- const resolvedLocale = framework.getLocale();
1999
- if (resolvedLocale) {
2000
- setHtmlLocaleAttributes(resolvedLocale);
2001
- log.debug("[startBrowserApp] Applied locale attributes:", resolvedLocale);
2002
- }
2003
- await onBeforeStart?.(framework);
2004
- const initialAction = framework.routeUrl(initialUrl);
2005
- const target = document.getElementById(mountId);
2006
- if (!target) throw new Error(`[startBrowserApp] Mount target not found: #${mountId}. Ensure your HTML has <div id="${mountId}"></div>.`);
2007
- const updateApp = mount(target, { framework });
2008
- registerActionHandlers({
2009
- framework,
2010
- log,
2011
- callbacks,
2012
- updateApp,
2013
- getScrollablePageElement: config.getScrollablePageElement
2014
- });
2015
- if (initialAction) await framework.perform(initialAction.action);
2016
- else updateApp({
2017
- page: Promise.reject(/* @__PURE__ */ new Error("404")),
2018
- isFirstPage: true
2019
- });
2020
- await onAfterStart?.(framework);
2021
- }
2022
- function resolveBrowserLocale(locale) {
2023
- if (locale) return locale;
2024
- return document.documentElement.lang.trim() || void 0;
2025
- }
2026
- function getBrowserFetch(fetchFn) {
2027
- const resolvedFetch = fetchFn ?? globalThis.fetch?.bind(globalThis);
2028
- if (resolvedFetch) return resolvedFetch;
2029
- return (() => {
2030
- throw new Error("[startBrowserApp] loadMessages requires a fetch implementation.");
2031
- });
2032
- }
2033
- //#endregion
2034
- //#region ../ssr/src/render.ts
2035
- /**
2036
- * ssrRender — 通用 SSR 渲染管线
2037
- *
2038
- * 1. 创建 Framework + 注册 Controllers
2039
- * 2. routeUrl → Intent
2040
- * 3. dispatch → Page 数据
2041
- * 4. 调用应用层提供的渲染函数
2042
- */
2043
- /** SSR 内部 rewrite 最大递归深度,防止 guard 配置错导致无限重路由 */
2044
- const MAX_SSR_REWRITE_DEPTH = 5;
2045
- async function ssrRender(options) {
2046
- return ssrRenderInternal(options, 0);
2047
- }
2048
- async function ssrRenderInternal(options, rewriteDepth) {
2049
- if (rewriteDepth >= MAX_SSR_REWRITE_DEPTH) throw new Error(`[SSR] Rewrite recursion depth exceeded (max ${MAX_SSR_REWRITE_DEPTH}) at "${options.url}"`);
2050
- const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext, resolveLocale, loadMessages } = options;
2051
- const parsed = new URL(url, "http://localhost");
2052
- const fullPath = parsed.pathname + parsed.search;
2053
- const resolvedLocale = resolveLocale?.(url, ssrContext?.request);
2054
- const effectiveConfig = resolvedLocale ? {
2055
- ...frameworkConfig,
2056
- locale: resolvedLocale.lang
2057
- } : frameworkConfig;
2058
- const resolvedMessages = await resolveConfiguredMessages({
2059
- locale: effectiveConfig.locale,
2060
- loadMessages,
2061
- context: effectiveConfig.locale ? {
2062
- runtime: "server",
2063
- fetch: getSSRFetch(ssrContext?.fetch ?? effectiveConfig.fetch),
2064
- url: fullPath,
2065
- request: ssrContext?.request
2066
- } : void 0
2067
- });
2068
- const mergedConfig = {
2069
- ...effectiveConfig,
2070
- fetch: ssrContext?.fetch ?? effectiveConfig.fetch
2071
- };
2072
- const framework = Framework.create({
2073
- ...mergedConfig,
2074
- _resolvedMessages: resolvedMessages
2075
- });
2076
- bootstrap(framework);
2077
- try {
2078
- const match = framework.routeUrl(fullPath);
2079
- if (match?.renderMode === "csr") return {
2080
- html: "",
2081
- head: "",
2082
- css: "",
2083
- serverData: [],
2084
- renderMode: "csr"
2085
- };
2086
- let page;
2087
- let serverData = [];
2088
- let rewriteUrl;
2089
- if (match) {
2090
- const navCtx = createServerContext({
2091
- url: fullPath,
2092
- intent: match.intent,
2093
- container: framework.container,
2094
- request: ssrContext?.request
2095
- });
2096
- const beforeResult = await framework.runBeforeLoad(navCtx, match.beforeGuards);
2097
- if (beforeResult.kind === "rewrite") return ssrRenderInternal({
2098
- ...options,
2099
- url: beforeResult.url
2100
- }, rewriteDepth + 1);
2101
- if (beforeResult.kind !== "next") {
2102
- const earlyReturn = await handleMiddlewareResult(beforeResult, getErrorPage, renderApp, framework);
2103
- if (earlyReturn) return earlyReturn;
2104
- }
2105
- try {
2106
- page = await framework.dispatch(match.intent);
2107
- serverData = [{
2108
- intent: match.intent,
2109
- data: page
2110
- }];
2111
- } catch (e) {
2112
- framework.container.resolve(DEP_KEYS.LOGGER).error(`[SSR] dispatch failed for intent "${match.intent.id}":`, e);
2113
- page = getErrorPage(500, "Internal error");
2114
- }
2115
- const postCtx = {
2116
- ...navCtx,
2117
- page
2118
- };
2119
- const afterResult = await framework.runAfterLoad(postCtx, match.afterGuards);
2120
- if (afterResult.kind === "rewrite") rewriteUrl = afterResult.url;
2121
- else if (afterResult.kind !== "next") {
2122
- const lateReturn = await handleMiddlewareResult(afterResult, getErrorPage, renderApp, framework);
2123
- if (lateReturn) return lateReturn;
2124
- }
2125
- } else page = getErrorPage(404, "Page not found");
2126
- const result = await renderApp(page, framework);
2127
- const locale = resolvedLocale ?? framework.getLocale();
2128
- return {
2129
- html: result.html,
2130
- head: result.head,
2131
- css: result.css,
2132
- serverData,
2133
- renderMode: match?.renderMode,
2134
- slots: result.slots,
2135
- locale,
2136
- rewriteUrl
2137
- };
2138
- } finally {
2139
- framework.dispose();
2140
- }
2141
- }
2142
- function getSSRFetch(fetchFn) {
2143
- const resolvedFetch = fetchFn ?? globalThis.fetch?.bind(globalThis);
2144
- if (resolvedFetch) return resolvedFetch;
2145
- return (() => {
2146
- throw new Error("[ssrRender] loadMessages requires a fetch implementation.");
2147
- });
2148
- }
2149
- /**
2150
- * 将中间件结果转换为 SSRRenderResult(如果需要短路返回)。
2151
- * 返回 null 表示继续正常流程。
2152
- *
2153
- * 注意:`rewrite` 不在此处理 — 它由 ssrRenderInternal 直接处理(内部重路由或标记 rewriteUrl)。
2154
- */
2155
- async function handleMiddlewareResult(result, getErrorPage, renderApp, framework) {
2156
- switch (result.kind) {
2157
- case "next":
2158
- case "rewrite": return null;
2159
- case "redirect": return {
2160
- html: "",
2161
- head: "",
2162
- css: "",
2163
- serverData: [],
2164
- redirect: {
2165
- url: result.url,
2166
- status: result.status
2167
- }
2168
- };
2169
- case "deny": {
2170
- const rendered = await renderApp(getErrorPage(result.status, result.message), framework);
2171
- return {
2172
- html: rendered.html,
2173
- head: rendered.head,
2174
- css: rendered.css,
2175
- serverData: [],
2176
- slots: rendered.slots,
2177
- status: result.status
2178
- };
2179
- }
2180
- }
2181
- }
2182
- //#endregion
2183
- //#region ../ssr/src/create-render.ts
2184
- /**
2185
- * 创建 render 函数
2186
- *
2187
- * @returns `render(url, ssrContext?)` — 供 @finesoft/server SSRModule 使用
2188
- */
2189
- function createSSRRender(config) {
2190
- const { bootstrap, getErrorPage, renderApp, frameworkConfig, resolveLocale, loadMessages } = config;
2191
- return (url, ssrContext) => ssrRender({
2192
- url,
2193
- frameworkConfig: frameworkConfig ?? {},
2194
- bootstrap,
2195
- getErrorPage,
2196
- renderApp: (page, framework) => renderApp(page, framework),
2197
- ssrContext,
2198
- resolveLocale,
2199
- loadMessages
2200
- });
2201
- }
2202
- //#endregion
2203
- //#region ../ssr/src/inject.ts
2204
- /**
2205
- * injectSSRContent — 将 SSR 渲染结果注入 HTML 模板
2206
- */
2207
- /** SSR HTML 模板占位符常量 */
2208
- const SSR_PLACEHOLDERS = {
2209
- HEAD: "<!--ssr-head-->",
2210
- BODY: "<!--ssr-body-->",
2211
- DATA: "<!--ssr-data-->"
2212
- };
2213
- /** 匹配所有 <!--ssr-xxx--> 占位符(含内置与自定义) */
2214
- const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
2215
- function injectSSRContent(options) {
2216
- const { template, head, css, html, serializedData, slots, locale } = options;
2217
- const replacements = {
2218
- head: `${head}\n${css ? `<style>${css}</style>` : ""}`,
2219
- body: html,
2220
- data: `<script id="serialized-server-data" type="application/json">${serializedData}<\/script>`,
2221
- ...slots
2222
- };
2223
- let result = template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
2224
- if (locale) result = applyLocaleToHtml(result, locale);
2225
- return result;
2226
- }
2227
- /**
2228
- * CSR 空壳注入 — 清空所有占位符
2229
- * 用于 renderMode === "csr" 的路由
2230
- *
2231
- * @param locale - 可选的 locale 属性,注入到 `<html lang="" dir="">`
2232
- */
2233
- function injectCSRShell(template, locale) {
2234
- let result = template.replace(PLACEHOLDER_REGEX, () => "");
2235
- if (locale) result = applyLocaleToHtml(result, locale);
2236
- return result;
2237
- }
2238
- /** 将 lang/dir 注入到 <html> 标签(支持双引号 / 单引号 / 无引号属性值) */
2239
- const HTML_LANG_PATTERN = /\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi;
2240
- const HTML_DIR_PATTERN = /\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi;
2241
- function applyLocaleToHtml(html, locale) {
2242
- return html.replace(/(<html)([^>]*)(>)/i, (_match, open, attrs, close) => {
2243
- return `${open}${attrs.replace(HTML_LANG_PATTERN, "").replace(HTML_DIR_PATTERN, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
2244
- });
2245
- }
2246
- //#endregion
2247
- //#region ../ssr/src/server-data.ts
2248
- const HTML_REPLACEMENTS = {
2249
- "<": "\\u003C",
2250
- ">": "\\u003E",
2251
- "/": "\\u002F",
2252
- "\u2028": "\\u2028",
2253
- "\u2029": "\\u2029"
2254
- };
2255
- const HTML_ESCAPE_PATTERN = /[<>/\u2028\u2029]/g;
2256
- function serializeServerData(data) {
2257
- return JSON.stringify(data).replace(HTML_ESCAPE_PATTERN, (match) => HTML_REPLACEMENTS[match] ?? match);
2258
- }
2259
- //#endregion
2260
- //#region ../server/src/dynamic-import.ts
2261
- /** Cache for stable (non-file://) modules — avoids redundant resolution. */
2262
- const moduleCache = /* @__PURE__ */ new Map();
2263
- /**
2264
- * Opaque dynamic import wrapper.
2265
- *
2266
- * Uses native `import()` directly — compatible with all JS runtimes
2267
- * including Cloudflare Workers (which forbid `new Function`).
2268
- *
2269
- * This may produce "dynamic import cannot be analyzed" warnings in Vite dev
2270
- * mode when the `@vite-ignore` comment is stripped by tsdown during bundling.
2271
- * These warnings are harmless: all specifiers are either well-known Node.js
2272
- * built-ins (externalized) or absolute file:// URLs constructed by the framework.
2273
- */
2274
- const rawImport = (specifier) => import(
2275
- /* @vite-ignore */
2276
- specifier
2277
- );
2278
- const debugEnabled = typeof process !== "undefined" && process.env?.FINESOFT_DEBUG === "1";
2279
- function logDebug(msg) {
2280
- if (debugEnabled) console.debug(`[finesoft:dynamic-import] ${msg}`);
2281
- }
2282
- async function dynamicImport(specifier) {
2283
- const cacheable = !specifier.startsWith("file:") && !specifier.startsWith("/");
2284
- if (cacheable) {
2285
- const cached = moduleCache.get(specifier);
2286
- if (cached) {
2287
- logDebug(`cache hit → ${specifier}`);
2288
- return cached;
2289
- }
2290
- }
2291
- logDebug(`importing → ${specifier}`);
2292
- const mod = await rawImport(specifier);
2293
- if (cacheable) moduleCache.set(specifier, mod);
2294
- return mod;
2295
- }
2296
- //#endregion
2297
- //#region ../server/src/proxy.ts
2298
- /** 代理路径最大长度 */
2299
- const MAX_PROXY_PATH_LENGTH = 2048;
2300
- /** 代理响应最大体积(10 MB) */
2301
- const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
2302
- /**
2303
- * 校验代理路径,防止 SSRF(协议相对 URL 绕过、编码绕过)。
2304
- * 返回规范化的路径,或 null 表示非法。
2305
- *
2306
- * 策略保守:拒绝任何含编码字符的路径,避免上游对 %2F 等解码差异导致绕过。
2307
- * 副作用:合法的 %20、%E4%B8%AD(Unicode)也会被拒。
2308
- * 如需放宽,应在上层路由前自行 decode,或为该代理单独提供 sanitizer 选项。
2309
- */
2310
- function sanitizeProxyPath(raw) {
2311
- if (raw.length > MAX_PROXY_PATH_LENGTH) return null;
2312
- try {
2313
- if (decodeURIComponent(raw) !== raw) return null;
2314
- } catch {
2315
- return null;
2316
- }
2317
- if (raw.startsWith("//")) return null;
2318
- if (!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
2319
- return raw.startsWith("/") ? raw : `/${raw}`;
2320
- }
2321
- /**
2322
- * 校验代理配置合法性。
2323
- * 在注册时(启动阶段)调用,非法配置直接抛错阻止启动。
2324
- */
2325
- function validateConfig(config) {
2326
- if (!config.prefix.startsWith("/")) throw new Error(`[proxy] prefix must start with "/": "${config.prefix}"`);
2327
- const isHttps = config.target.startsWith("https://");
2328
- const isHttp = config.target.startsWith("http://");
2329
- if (!isHttps && !isHttp) throw new Error(`[proxy] target must start with "https://" or "http://": "${config.target}"`);
2330
- if (isHttp) console.warn(`[proxy] ⚠ target "${config.target}" uses plain HTTP — traffic will not be encrypted. Use HTTPS in production to prevent data interception.`);
2331
- }
2332
- /**
2333
- * 注册声明式代理路由到 Hono app(运行时使用:dev / preview / createServer)
2334
- */
2335
- function registerProxyRoutes(app, configs) {
2336
- for (const config of configs) {
2337
- validateConfig(config);
2338
- const methods = config.methods ?? ["all"];
2339
- const pattern = `${config.prefix}/*`;
2340
- const handler = async (c) => {
2341
- const subPath = sanitizeProxyPath(c.req.path.replace(config.prefix, ""));
2342
- if (!subPath) return c.text("Invalid path", 400);
2343
- const targetUrl = new URL(subPath, config.target);
2344
- const expectedOrigin = new URL(config.target).origin;
2345
- if (targetUrl.origin !== expectedOrigin) return c.text("Invalid proxy target", 400);
2346
- new URL(c.req.url).searchParams.forEach((v, k) => targetUrl.searchParams.set(k, v));
2347
- const headers = { ...config.headers };
2348
- if (config.auth) {
2349
- const token = process.env[config.auth.envKey];
2350
- if (!token) console.warn(`[Proxy ${config.prefix}] Auth env var "${config.auth.envKey}" is not set`);
2351
- else headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
2352
- }
2353
- try {
2354
- const resp = await fetch(targetUrl.toString(), {
2355
- headers,
2356
- redirect: config.followRedirects ? "follow" : "manual"
2357
- });
2358
- const contentLength = resp.headers.get("Content-Length");
2359
- if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
2360
- const body = await resp.arrayBuffer();
2361
- if (body.byteLength > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
2362
- const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
2363
- if (config.cache) respHeaders["Cache-Control"] = config.cache;
2364
- return c.newResponse(body, resp.status, respHeaders);
2365
- } catch (e) {
2366
- console.error(`[Proxy ${config.prefix}]`, e);
2367
- return c.json({ error: "Proxy request failed" }, 502);
2368
- }
2369
- };
2370
- for (const method of methods) app[method](pattern, handler);
2371
- }
2372
- }
2373
- /**
2374
- * 生成代理路由的内联代码(用于 serverless/edge 入口,避免运行时依赖)
2375
- */
2376
- function generateProxyCode(configs) {
2377
- if (!configs || configs.length === 0) return "";
2378
- for (const config of configs) validateConfig(config);
2379
- const blocks = [];
2380
- blocks.push(`
1
+ import{$ as e,A as t,B as n,C as r,D as i,E as a,F as o,G as s,H as c,I as l,J as u,K as d,L as f,M as p,N as m,O as h,P as g,Q as _,R as v,S as y,T as b,U as x,V as S,W as C,X as w,Y as T,Z as E,_ as D,_t as ee,a as te,at as ne,b as re,bt as ie,c as ae,ct as oe,d as se,dt as ce,et as le,f as ue,ft as de,g as fe,gt as pe,h as me,ht as he,i as ge,it as _e,j as ve,k as ye,l as be,lt as xe,m as Se,mt as Ce,n as we,nt as Te,o as Ee,ot as De,p as Oe,pt as ke,q as Ae,r as je,rt as Me,s as Ne,st as O,t as Pe,tt as Fe,u as Ie,ut as Le,v as k,vt as Re,w as ze,x as Be,xt as Ve,y as He,yt as Ue,z as We}from"./start-app-BdXBCcor.mjs";import{Hono as A}from"hono";async function Ge(e){return j(e,0)}async function j(e,t){if(t>=5)throw Error(`[SSR] Rewrite recursion depth exceeded (max 5) at "${e.url}"`);let{url:n,frameworkConfig:r,bootstrap:i,getErrorPage:a,renderApp:o,ssrContext:s,resolveLocale:u,loadMessages:d}=e,f=new URL(n,`http://localhost`),p=f.pathname+f.search,m=u?.(n,s?.request),h=m?{...r,locale:m.lang}:r,g=await ne({locale:h.locale,loadMessages:d,context:h.locale?{runtime:`server`,fetch:Ke(s?.fetch??h.fetch),url:p,request:s?.request}:void 0}),_={...h,fetch:s?.fetch??h.fetch},v=l.create({..._,_resolvedMessages:g});i(v);try{let n=v.routeUrl(p);if(n?.renderMode===`csr`)return{html:``,head:``,css:``,serverData:[],renderMode:`csr`};let r,i=[],l;if(n){let u=k({url:p,intent:n.intent,container:v.container,request:s?.request}),d=await v.runBeforeLoad(u,n.beforeGuards);if(d.kind===`rewrite`)return j({...e,url:d.url},t+1);if(d.kind!==`next`){let e=await M(d,a,o,v);if(e)return e}try{r=await v.dispatch(n.intent),i=[{intent:n.intent,data:r}]}catch(e){v.container.resolve(c.LOGGER).error(`[SSR] dispatch failed for intent "${n.intent.id}":`,e),r=a(500,`Internal error`)}let f={...u,page:r},m=await v.runAfterLoad(f,n.afterGuards);if(m.kind===`rewrite`)l=m.url;else if(m.kind!==`next`){let e=await M(m,a,o,v);if(e)return e}}else r=a(404,`Page not found`);let u=await o(r,v),d=m??v.getLocale();return{html:u.html,head:u.head,css:u.css,serverData:i,renderMode:n?.renderMode,slots:u.slots,locale:d,rewriteUrl:l}}finally{v.dispose()}}function Ke(e){return(e??globalThis.fetch?.bind(globalThis))||(()=>{throw Error(`[ssrRender] loadMessages requires a fetch implementation.`)})}async function M(e,t,n,r){switch(e.kind){case`next`:case`rewrite`:return null;case`redirect`:return{html:``,head:``,css:``,serverData:[],redirect:{url:e.url,status:e.status}};case`deny`:{let i=await n(t(e.status,e.message),r);return{html:i.html,head:i.head,css:i.css,serverData:[],slots:i.slots,status:e.status}}}}function qe(e){let{bootstrap:t,getErrorPage:n,renderApp:r,frameworkConfig:i,resolveLocale:a,loadMessages:o}=e;return(e,s)=>Ge({url:e,frameworkConfig:i??{},bootstrap:t,getErrorPage:n,renderApp:(e,t)=>r(e,t),ssrContext:s,resolveLocale:a,loadMessages:o})}const Je={HEAD:`<!--ssr-head-->`,BODY:`<!--ssr-body-->`,DATA:`<!--ssr-data-->`},N=/<!--ssr-([a-z][a-z0-9-]*)-->/g;function P(e){let{template:t,head:n,css:r,html:i,serializedData:a,slots:o,locale:s}=e,c={head:`${n}\n${r?`<style>${r}</style>`:``}`,body:i,data:`<script id="serialized-server-data" type="application/json">${a}<\/script>`,...o},l=t.replace(N,(e,t)=>c[t]??``);return s&&(l=Ze(l,s)),l}function F(e,t){let n=e.replace(N,()=>``);return t&&(n=Ze(n,t)),n}const Ye=/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi,Xe=/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi;function Ze(e,t){return e.replace(/(<html)([^>]*)(>)/i,(e,n,r,i)=>`${n}${r.replace(Ye,``).replace(Xe,``)} lang="${t.lang}" dir="${t.dir}"${i}`)}const Qe={"<":`\\u003C`,">":`\\u003E`,"/":`\\u002F`,"\u2028":`\\u2028`,"\u2029":`\\u2029`},$e=/[<>/\u2028\u2029]/g;function et(e){return JSON.stringify(e).replace($e,e=>Qe[e]??e)}const tt=new Map,nt=e=>import(e),rt=typeof process<`u`&&process.env?.FINESOFT_DEBUG===`1`;function it(e){rt&&console.debug(`[finesoft:dynamic-import] ${e}`)}async function I(e){let t=!e.startsWith(`file:`)&&!e.startsWith(`/`);if(t){let t=tt.get(e);if(t)return it(`cache hit → ${e}`),t}it(`importing → ${e}`);let n=await nt(e);return t&&tt.set(e,n),n}const L=10*1024*1024;function at(e){if(e.length>2048)return null;try{if(decodeURIComponent(e)!==e)return null}catch{return null}return e.startsWith(`//`)||!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(e)?null:e.startsWith(`/`)?e:`/${e}`}function ot(e){if(!e.prefix.startsWith(`/`))throw Error(`[proxy] prefix must start with "/": "${e.prefix}"`);let t=e.target.startsWith(`https://`),n=e.target.startsWith(`http://`);if(!t&&!n)throw Error(`[proxy] target must start with "https://" or "http://": "${e.target}"`);n&&console.warn(`[proxy] ⚠ target "${e.target}" uses plain HTTP — traffic will not be encrypted. Use HTTPS in production to prevent data interception.`)}function R(e,t){for(let n of t){ot(n);let t=n.methods??[`all`],r=`${n.prefix}/*`,i=async e=>{let t=at(e.req.path.replace(n.prefix,``));if(!t)return e.text(`Invalid path`,400);let r=new URL(t,n.target),i=new URL(n.target).origin;if(r.origin!==i)return e.text(`Invalid proxy target`,400);new URL(e.req.url).searchParams.forEach((e,t)=>r.searchParams.set(t,e));let a={...n.headers};if(n.auth){let e=process.env[n.auth.envKey];e?a.Authorization=n.auth.type===`bearer`?`Bearer ${e}`:`Basic ${e}`:console.warn(`[Proxy ${n.prefix}] Auth env var "${n.auth.envKey}" is not set`)}try{let t=await fetch(r.toString(),{headers:a,redirect:n.followRedirects?`follow`:`manual`}),i=t.headers.get(`Content-Length`);if(i&&parseInt(i,10)>L)return e.text(`Proxy response too large`,502);let o=await t.arrayBuffer();if(o.byteLength>L)return e.text(`Proxy response too large`,502);let s={"Content-Type":t.headers.get(`Content-Type`)??`application/json`};return n.cache&&(s[`Cache-Control`]=n.cache),e.newResponse(o,t.status,s)}catch(t){return console.error(`[Proxy ${n.prefix}]`,t),e.json({error:`Proxy request failed`},502)}};for(let n of t)e[n](r,i)}}function z(e){if(!e||e.length===0)return``;for(let t of e)ot(t);let t=[];t.push(`
2381
2
  // ─── 框架声明式代理路由 ───
2382
3
  function _sanitizeProxyPath(raw) {
2383
4
  if (raw.length > 2048) return null;
@@ -2386,99 +7,37 @@ function _sanitizeProxyPath(raw) {
2386
7
  if (!/^[/\\w.\\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
2387
8
  return raw.startsWith("/") ? raw : "/" + raw;
2388
9
  }
2389
- `);
2390
- for (const config of configs) {
2391
- const methods = config.methods ?? ["all"];
2392
- const pattern = `"${config.prefix}/*"`;
2393
- const headersJson = JSON.stringify(config.headers ?? {});
2394
- const cacheStr = config.cache ? JSON.stringify(config.cache) : "null";
2395
- const redirect = config.followRedirects ? "\"follow\"" : "\"manual\"";
2396
- let authCode = "";
2397
- if (config.auth) authCode = `
2398
- const _token = (typeof process !== "undefined" && process.env && process.env[${JSON.stringify(config.auth.envKey)}]) || "";
2399
- if (_token) _headers.Authorization = "${config.auth.type === "bearer" ? "Bearer " : "Basic "}" + _token;`;
2400
- const handlerCode = `async (c) => {
2401
- const _sub = _sanitizeProxyPath(c.req.path.replace(${JSON.stringify(config.prefix)}, ""));
10
+ `);for(let n of e){let e=n.methods??[`all`],r=`"${n.prefix}/*"`,i=JSON.stringify(n.headers??{}),a=n.cache?JSON.stringify(n.cache):`null`,o=n.followRedirects?`"follow"`:`"manual"`,s=``;n.auth&&(s=`
11
+ const _token = (typeof process !== "undefined" && process.env && process.env[${JSON.stringify(n.auth.envKey)}]) || "";
12
+ if (_token) _headers.Authorization = "${n.auth.type===`bearer`?`Bearer `:`Basic `}" + _token;`);let c=`async (c) => {
13
+ const _sub = _sanitizeProxyPath(c.req.path.replace(${JSON.stringify(n.prefix)}, ""));
2402
14
  if (!_sub) return c.text("Invalid path", 400);
2403
- const _target = new URL(_sub, ${JSON.stringify(config.target)});
2404
- if (_target.origin !== ${JSON.stringify(new URL(config.target).origin)}) return c.text("Invalid proxy target", 400);
15
+ const _target = new URL(_sub, ${JSON.stringify(n.target)});
16
+ if (_target.origin !== ${JSON.stringify(new URL(n.target).origin)}) return c.text("Invalid proxy target", 400);
2405
17
  const _reqUrl = new URL(c.req.url);
2406
18
  _reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
2407
- const _headers = ${headersJson};${authCode}
19
+ const _headers = ${i};${s}
2408
20
  try {
2409
- const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${redirect} });
21
+ const _resp = await fetch(_target.toString(), { headers: _headers, redirect: ${o} });
2410
22
  // Content-Length 快速拒绝,防止 serverless/edge 加载超大响应到内存
2411
23
  const _cl = _resp.headers.get("Content-Length");
2412
- if (_cl && parseInt(_cl, 10) > ${MAX_RESPONSE_SIZE}) {
24
+ if (_cl && parseInt(_cl, 10) > ${L}) {
2413
25
  return c.text("Proxy response too large", 502);
2414
26
  }
2415
27
  // arrayBuffer 保留二进制完整性
2416
28
  const _body = await _resp.arrayBuffer();
2417
- if (_body.byteLength > ${MAX_RESPONSE_SIZE}) {
29
+ if (_body.byteLength > ${L}) {
2418
30
  return c.text("Proxy response too large", 502);
2419
31
  }
2420
32
  const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
2421
- if (${cacheStr}) _rh["Cache-Control"] = ${cacheStr};
33
+ if (${a}) _rh["Cache-Control"] = ${a};
2422
34
  return c.newResponse(_body, _resp.status, _rh);
2423
35
  } catch (_e) {
2424
- console.error("[Proxy ${config.prefix}]", _e);
36
+ console.error("[Proxy ${n.prefix}]", _e);
2425
37
  return c.json({ error: "Proxy request failed" }, 502);
2426
38
  }
2427
- }`;
2428
- for (const method of methods) blocks.push(`app.${method}(${pattern}, ${handlerCode});`);
2429
- }
2430
- return blocks.join("\n");
2431
- }
2432
- //#endregion
2433
- //#region ../server/src/adapters/shared.ts
2434
- /**
2435
- * 适配器共享工具函数
2436
- *
2437
- * 提供 generateSSREntry / buildBundle / copyStaticAssets 三个方法,
2438
- * 避免各适配器重复实现相同逻辑。
2439
- */
2440
- const BUILD_TOOL_EXTERNALS = [
2441
- "vite",
2442
- "esbuild",
2443
- "rollup",
2444
- "fsevents",
2445
- "lightningcss"
2446
- ];
2447
- /**
2448
- * Common Node.js built-in modules. Listed explicitly so that Rolldown's
2449
- * vite-resolve plugin does not emit "Automatically externalized" warnings.
2450
- */
2451
- const NODE_BUILTINS = [
2452
- "node:async_hooks",
2453
- "node:buffer",
2454
- "node:crypto",
2455
- "node:fs",
2456
- "node:http",
2457
- "node:http2",
2458
- "node:module",
2459
- "node:net",
2460
- "node:os",
2461
- "node:path",
2462
- "node:stream",
2463
- "node:url",
2464
- "node:util",
2465
- "node:zlib",
2466
- "crypto",
2467
- "http",
2468
- "http2",
2469
- "stream"
2470
- ];
2471
- /**
2472
- * 生成 SSR serverless/edge 入口源码
2473
- *
2474
- * 内联 injectSSR 以避免
2475
- * @finesoft/front → @finesoft/server → vite-plugin → import("vite") 依赖链。
2476
- */
2477
- function generateSSREntry(ctx, opts) {
2478
- const setupImport = ctx.setupPath ? `import _setupDefault from "./${ctx.setupPath}";` : ``;
2479
- const setupCall = ctx.setupPath ? `if (typeof _setupDefault === "function") await _setupDefault(app);` : ``;
2480
- const renderModes = JSON.stringify(ctx.renderModes ?? {});
2481
- const cacheImpl = opts.platformCache ? opts.platformCache : `
39
+ }`;for(let n of e)t.push(`app.${n}(${r}, ${c});`)}return t.join(`
40
+ `)}const st=[`vite`,`esbuild`,`rollup`,`fsevents`,`lightningcss`],B=[`node:async_hooks`,`node:buffer`,`node:crypto`,`node:fs`,`node:http`,`node:http2`,`node:module`,`node:net`,`node:os`,`node:path`,`node:stream`,`node:url`,`node:util`,`node:zlib`,`crypto`,`http`,`http2`,`stream`];function V(e,t){let n=e.setupPath?`import _setupDefault from "./${e.setupPath}";`:``,r=e.setupPath?`if (typeof _setupDefault === "function") await _setupDefault(app);`:``,i=JSON.stringify(e.renderModes??{}),a=t.platformCache?t.platformCache:`
2482
41
  const ISR_CACHE_MAX = 1000;
2483
42
  const _isrMap = new Map();
2484
43
  async function platformCacheGet(url) {
@@ -2490,17 +49,16 @@ async function platformCacheSet(url, html) {
2490
49
  _isrMap.delete(first);
2491
50
  }
2492
51
  _isrMap.set(url, html);
2493
- }`;
2494
- return `
52
+ }`;return`
2495
53
  import { Hono } from "hono";
2496
- ${opts.platformImport}
2497
- import { render, serializeServerData } from "./${ctx.ssrEntry}";
2498
- ${setupImport}
54
+ ${t.platformImport}
55
+ import { render, serializeServerData } from "./${e.ssrEntry}";
56
+ ${n}
2499
57
 
2500
- const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
2501
- const RENDER_MODES = ${renderModes};
2502
- const DEFAULT_LOCALE = ${JSON.stringify(ctx.defaultLocale ?? null)};
2503
- ${cacheImpl}
58
+ const TEMPLATE = ${JSON.stringify(e.templateHtml)};
59
+ const RENDER_MODES = ${i};
60
+ const DEFAULT_LOCALE = ${JSON.stringify(e.defaultLocale??null)};
61
+ ${a}
2504
62
 
2505
63
  function injectSSR(t, head, css, html, data, locale) {
2506
64
  const injected = t
@@ -2551,9 +109,9 @@ function matchRenderMode(url) {
2551
109
  }
2552
110
 
2553
111
  const app = new Hono();
2554
- ${generateProxyCode(ctx.proxies ?? [])}
2555
- ${setupCall}
2556
- ${opts.platformMiddleware ?? ""}
112
+ ${z(e.proxies??[])}
113
+ ${r}
114
+ ${t.platformMiddleware??``}
2557
115
 
2558
116
  // 内部 fetch 回环:SSR 控制器的 fetch 请求直接走 Hono 内存路由
2559
117
  // 深度通过请求头传递,并发安全且能跨渲染正确追踪递归
@@ -2604,7 +162,7 @@ app.get("*", async (c) => {
2604
162
  // Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
2605
163
  if (renderMode === "prerender" || overrideMode === "prerender") {
2606
164
  await platformCacheSet(url, finalHtml);
2607
- ${opts.platformPrerenderResponseHook ?? ""}
165
+ ${t.platformPrerenderResponseHook??``}
2608
166
  }
2609
167
 
2610
168
  return c.html(finalHtml);
@@ -2614,131 +172,9 @@ app.get("*", async (c) => {
2614
172
  }
2615
173
  });
2616
174
 
2617
- ${opts.platformExport}
2618
- `;
2619
- }
2620
- /** 用 Vite SSR 模式构建 bundle */
2621
- async function buildBundle(ctx, opts) {
2622
- await ctx.vite.build({
2623
- root: ctx.root,
2624
- build: {
2625
- ssr: opts.entry,
2626
- outDir: opts.outDir,
2627
- emptyOutDir: opts.emptyOutDir ?? true,
2628
- target: opts.target ?? "node18",
2629
- rollupOptions: { output: { entryFileNames: opts.fileName ?? "index.mjs" } }
2630
- },
2631
- ssr: {
2632
- noExternal: opts.noExternal !== false,
2633
- external: [...opts.external ?? BUILD_TOOL_EXTERNALS, ...NODE_BUILTINS]
2634
- },
2635
- resolve: ctx.resolvedResolve,
2636
- css: ctx.resolvedCss
2637
- });
2638
- }
2639
- /** 复制 dist/client 静态资源到目标目录 */
2640
- function copyStaticAssets(ctx, destDir, opts) {
2641
- const { fs, path } = ctx;
2642
- fs.cpSync(path.resolve(ctx.root, "dist/client"), destDir, { recursive: true });
2643
- if (opts?.excludeHtml !== false) fs.rmSync(path.join(destDir, "index.html"), { force: true });
2644
- }
2645
- /**
2646
- * 构建时预渲染 prerender 路由。
2647
- *
2648
- * 1. 加载路由定义文件,找出 renderMode === "prerender" 的路由
2649
- * 2. 合并 ctx.renderModes 配置覆盖
2650
- * 3. 渲染每个 URL × locale
2651
- */
2652
- async function prerenderRoutes(ctx) {
2653
- const { fs, path, root, vite } = ctx;
2654
- const { pathToFileURL } = await dynamicImport("node:url");
2655
- const routesExport = ctx.bootstrapEntry ?? "src/lib/bootstrap.ts";
2656
- let routes = [];
2657
- if (fs.existsSync(path.resolve(root, routesExport))) {
2658
- await vite.build({
2659
- root,
2660
- build: {
2661
- ssr: routesExport,
2662
- outDir: path.resolve(root, "dist/server"),
2663
- emptyOutDir: false,
2664
- rollupOptions: { output: { entryFileNames: "_routes_prerender.mjs" } }
2665
- },
2666
- resolve: ctx.resolvedResolve
2667
- });
2668
- const routesPath = pathToFileURL(path.resolve(root, "dist/server/_routes_prerender.mjs")).href;
2669
- const routesMod = await dynamicImport(routesPath);
2670
- routes = routesMod.routes ?? routesMod.default ?? [];
2671
- fs.rmSync(path.resolve(root, "dist/server/_routes_prerender.mjs"), { force: true });
2672
- }
2673
- const prerenderPaths = /* @__PURE__ */ new Set();
2674
- for (const r of routes) if (r.renderMode === "prerender" && r.path && !r.path.includes(":")) prerenderPaths.add(r.path);
2675
- if (ctx.renderModes) {
2676
- for (const [pattern, mode] of Object.entries(ctx.renderModes)) if (mode === "prerender" && !pattern.includes("*") && !pattern.includes(":")) prerenderPaths.add(pattern);
2677
- }
2678
- if (ctx.locales?.length) {
2679
- const basePaths = [...prerenderPaths];
2680
- for (const locale of ctx.locales) for (const basePath of basePaths) {
2681
- const localePath = basePath === "/" ? `/${locale}` : `/${locale}${basePath}`;
2682
- prerenderPaths.add(localePath);
2683
- }
2684
- }
2685
- if (prerenderPaths.size === 0) return [];
2686
- const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
2687
- const ssrModule = await dynamicImport(ssrPath);
2688
- const results = [];
2689
- for (const url of prerenderPaths) try {
2690
- const { html: appHtml, head, css, serverData, locale } = await ssrModule.render(url);
2691
- const serializedData = ssrModule.serializeServerData(serverData);
2692
- let finalHtml = ctx.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_match, name) => {
2693
- return {
2694
- head: head + "\n<style>" + css + "</style>",
2695
- body: appHtml,
2696
- data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
2697
- }[name] ?? "";
2698
- });
2699
- if (locale) {
2700
- const { getLocaleAttributes } = await dynamicImport("@finesoft/core");
2701
- const attrs = getLocaleAttributes(locale);
2702
- finalHtml = finalHtml.replace(/<html([^>]*)>/i, (_m, a) => {
2703
- return `<html${a.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi, "")} lang="${attrs.lang}" dir="${attrs.dir}">`;
2704
- });
2705
- }
2706
- results.push({
2707
- url,
2708
- html: finalHtml
2709
- });
2710
- } catch (e) {
2711
- console.warn(` [prerender] Failed to render ${url}:`, e);
2712
- }
2713
- if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes)\n`);
2714
- return results;
2715
- }
2716
- //#endregion
2717
- //#region ../server/src/adapters/cloudflare.ts
2718
- /**
2719
- * Cloudflare Pages 适配器
2720
- *
2721
- * 生成 dist/cloudflare/ 目录:
2722
- * - _worker.js — Workers 入口(Hono 原生支持 CF fetch 接口)
2723
- * - assets/ — 静态资源
2724
- *
2725
- * 注意:Cloudflare Workers 不支持原生 Node.js API。
2726
- * 若 setup 代理使用了 process.env,需在 wrangler.toml 启用 nodejs_compat。
2727
- */
2728
- function cloudflareAdapter() {
2729
- return {
2730
- name: "cloudflare",
2731
- async build(ctx) {
2732
- const { fs, path, root } = ctx;
2733
- const outputDir = path.resolve(root, "dist/cloudflare");
2734
- fs.rmSync(outputDir, {
2735
- recursive: true,
2736
- force: true
2737
- });
2738
- const entrySource = generateSSREntry(ctx, {
2739
- platformImport: ``,
2740
- platformExport: `export default app;`,
2741
- platformCache: `
175
+ ${t.platformExport}
176
+ `}async function H(e,t){await e.vite.build({root:e.root,build:{ssr:t.entry,outDir:t.outDir,emptyOutDir:t.emptyOutDir??!0,target:t.target??`node18`,rollupOptions:{output:{entryFileNames:t.fileName??`index.mjs`}}},ssr:{noExternal:t.noExternal!==!1,external:[...t.external??st,...B]},resolve:e.resolvedResolve,css:e.resolvedCss})}function U(e,t,n){let{fs:r,path:i}=e;r.cpSync(i.resolve(e.root,`dist/client`),t,{recursive:!0}),n?.excludeHtml!==!1&&r.rmSync(i.join(t,`index.html`),{force:!0})}async function W(e){let{fs:t,path:n,root:r,vite:i}=e,{pathToFileURL:a}=await I(`node:url`),o=e.bootstrapEntry??`src/lib/bootstrap.ts`,s=[];if(t.existsSync(n.resolve(r,o))){await i.build({root:r,build:{ssr:o,outDir:n.resolve(r,`dist/server`),emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`_routes_prerender.mjs`}}},resolve:e.resolvedResolve});let c=a(n.resolve(r,`dist/server/_routes_prerender.mjs`)).href,l=await I(c);s=l.routes??l.default??[],t.rmSync(n.resolve(r,`dist/server/_routes_prerender.mjs`),{force:!0})}let c=new Set;for(let e of s)e.renderMode===`prerender`&&e.path&&!e.path.includes(`:`)&&c.add(e.path);if(e.renderModes)for(let[t,n]of Object.entries(e.renderModes))n===`prerender`&&!t.includes(`*`)&&!t.includes(`:`)&&c.add(t);if(e.locales?.length){let t=[...c];for(let n of e.locales)for(let e of t){let t=e===`/`?`/${n}`:`/${n}${e}`;c.add(t)}}if(c.size===0)return[];let l=a(n.resolve(r,`dist/server/ssr.js`)).href,u=await I(l),d=[];for(let t of c)try{let{html:n,head:r,css:i,serverData:a,locale:o}=await u.render(t),s=u.serializeServerData(a),c=e.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g,(e,t)=>({head:r+`
177
+ <style>`+i+`</style>`,body:n,data:`<script id="serialized-server-data" type="application/json">`+s+`<\/script>`})[t]??``);if(o){let{getLocaleAttributes:e}=await I(`@finesoft/core`),t=e(o);c=c.replace(/<html([^>]*)>/i,(e,n)=>`<html${n.replace(/\s+lang=("[^"]*"|'[^']*'|[^\s>]+)/gi,``).replace(/\s+dir=("[^"]*"|'[^']*'|[^\s>]+)/gi,``)} lang="${t.lang}" dir="${t.dir}">`)}d.push({url:t,html:c})}catch(e){console.warn(` [prerender] Failed to render ${t}:`,e)}return d.length>0&&console.log(` Pre-rendered ${d.length} pages (${c.size} routes)\n`),d}function G(){return{name:`cloudflare`,async build(e){let{fs:t,path:n,root:r}=e,i=n.resolve(r,`dist/cloudflare`);t.rmSync(i,{recursive:!0,force:!0});let a=V(e,{platformImport:``,platformExport:`export default app;`,platformCache:`
2742
178
  const ISR_CACHE_TTL = 3600; // 1 hour
2743
179
  async function platformCacheGet(url) {
2744
180
  try {
@@ -2758,55 +194,9 @@ async function platformCacheSet(url, html) {
2758
194
  });
2759
195
  await cache.put(cacheKey, resp);
2760
196
  } catch {}
2761
- }`
2762
- });
2763
- const tempEntry = path.resolve(root, ".cf-entry.tmp.mjs");
2764
- fs.writeFileSync(tempEntry, entrySource);
2765
- try {
2766
- await buildBundle(ctx, {
2767
- entry: ".cf-entry.tmp.mjs",
2768
- outDir: outputDir,
2769
- target: "es2022",
2770
- fileName: "_worker.js"
2771
- });
2772
- copyStaticAssets(ctx, path.resolve(outputDir, "assets"));
2773
- const prerendered = await prerenderRoutes(ctx);
2774
- for (const { url, html } of prerendered) {
2775
- const filePath = url === "/" ? path.join(outputDir, "assets", "index.html") : path.join(outputDir, "assets", url, "index.html");
2776
- fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2777
- fs.writeFileSync(filePath, html);
2778
- }
2779
- } finally {
2780
- fs.rmSync(tempEntry, { force: true });
2781
- }
2782
- console.log(" Cloudflare output → dist/cloudflare/\n");
2783
- }
2784
- };
2785
- }
2786
- //#endregion
2787
- //#region ../server/src/adapters/netlify.ts
2788
- /**
2789
- * Netlify 适配器 — Netlify Functions v2
2790
- *
2791
- * 生成:
2792
- * - .netlify/functions-internal/ssr/index.mjs — Serverless Function
2793
- * - dist/client/_redirects — 路由重写规则
2794
- */
2795
- function netlifyAdapter() {
2796
- return {
2797
- name: "netlify",
2798
- async build(ctx) {
2799
- const { fs, path, root } = ctx;
2800
- const funcDir = path.resolve(root, ".netlify/functions-internal/ssr");
2801
- fs.rmSync(path.resolve(root, ".netlify"), {
2802
- recursive: true,
2803
- force: true
2804
- });
2805
- const entrySource = generateSSREntry(ctx, {
2806
- platformImport: `import { handle } from "hono/netlify";`,
2807
- platformExport: `export default handle(app);
2808
- export const config = { path: "/*", preferStatic: true };`,
2809
- platformCache: `
197
+ }`}),o=n.resolve(r,`.cf-entry.tmp.mjs`);t.writeFileSync(o,a);try{await H(e,{entry:`.cf-entry.tmp.mjs`,outDir:i,target:`es2022`,fileName:`_worker.js`}),U(e,n.resolve(i,`assets`));let r=await W(e);for(let{url:e,html:a}of r){let r=e===`/`?n.join(i,`assets`,`index.html`):n.join(i,`assets`,e,`index.html`);t.mkdirSync(n.resolve(r,`..`),{recursive:!0}),t.writeFileSync(r,a)}}finally{t.rmSync(o,{force:!0})}console.log(` Cloudflare output → dist/cloudflare/
198
+ `)}}}function K(){return{name:`netlify`,async build(e){let{fs:t,path:n,root:r}=e,i=n.resolve(r,`.netlify/functions-internal/ssr`);t.rmSync(n.resolve(r,`.netlify`),{recursive:!0,force:!0});let a=V(e,{platformImport:`import { handle } from "hono/netlify";`,platformExport:`export default handle(app);
199
+ export const config = { path: "/*", preferStatic: true };`,platformCache:`
2810
200
  const ISR_SWR_TTL = 3600;
2811
201
  const ISR_CACHE_MAX = 1000;
2812
202
  const _isrMap = new Map();
@@ -2819,52 +209,14 @@ async function platformCacheSet(url, html) {
2819
209
  _isrMap.delete(first);
2820
210
  }
2821
211
  _isrMap.set(url, html);
2822
- }`,
2823
- platformPrerenderResponseHook: `c.header("Cache-Control", "public, max-age=0, must-revalidate");
2824
- c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`
2825
- });
2826
- const tempEntry = path.resolve(root, ".netlify-entry.tmp.mjs");
2827
- fs.writeFileSync(tempEntry, entrySource);
2828
- try {
2829
- await buildBundle(ctx, {
2830
- entry: ".netlify-entry.tmp.mjs",
2831
- outDir: funcDir,
2832
- target: "node18"
2833
- });
2834
- } finally {
2835
- fs.rmSync(tempEntry, { force: true });
2836
- }
2837
- fs.writeFileSync(path.resolve(root, "dist/client/_redirects"), `/* /.netlify/functions/ssr 200\n`);
2838
- const prerendered = await prerenderRoutes(ctx);
2839
- const clientDir = path.resolve(root, "dist/client");
2840
- for (const { url, html } of prerendered) {
2841
- const filePath = url === "/" ? path.join(clientDir, "index.html") : path.join(clientDir, url, "index.html");
2842
- fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2843
- fs.writeFileSync(filePath, html);
2844
- }
2845
- console.log(" Netlify output → .netlify/functions-internal/ssr/\n Publish dir: dist/client/\n");
2846
- }
2847
- };
2848
- }
2849
- //#endregion
2850
- //#region ../server/src/adapters/node.ts
2851
- /**
2852
- * Node 适配器 — 独立 HTTP 服务器
2853
- *
2854
- * 生成 dist/server/index.mjs,使用 @hono/node-server 监听端口。
2855
- * 运行:node dist/server/index.mjs
2856
- */
2857
- function nodeAdapter() {
2858
- return {
2859
- name: "node",
2860
- async build(ctx) {
2861
- const { fs, path, root } = ctx;
2862
- const entrySource = generateSSREntry(ctx, {
2863
- platformImport: `import { serve } from "@hono/node-server";
212
+ }`,platformPrerenderResponseHook:`c.header("Cache-Control", "public, max-age=0, must-revalidate");
213
+ c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`}),o=n.resolve(r,`.netlify-entry.tmp.mjs`);t.writeFileSync(o,a);try{await H(e,{entry:`.netlify-entry.tmp.mjs`,outDir:i,target:`node18`})}finally{t.rmSync(o,{force:!0})}t.writeFileSync(n.resolve(r,`dist/client/_redirects`),`/* /.netlify/functions/ssr 200
214
+ `);let s=await W(e),c=n.resolve(r,`dist/client`);for(let{url:e,html:r}of s){let i=e===`/`?n.join(c,`index.html`):n.join(c,e,`index.html`);t.mkdirSync(n.resolve(i,`..`),{recursive:!0}),t.writeFileSync(i,r)}console.log(` Netlify output .netlify/functions-internal/ssr/
215
+ Publish dir: dist/client/
216
+ `)}}}function q(){return{name:`node`,async build(e){let{fs:t,path:n,root:r}=e,i=V(e,{platformImport:`import { serve } from "@hono/node-server";
2864
217
  import { readFileSync, existsSync } from "node:fs";
2865
218
  import { resolve, dirname } from "node:path";
2866
- import { fileURLToPath } from "node:url";`,
2867
- platformMiddleware: `
219
+ import { fileURLToPath } from "node:url";`,platformMiddleware:`
2868
220
  // 预渲染文件中间件:检查 dist/prerender/ 下是否有对应的静态 HTML
2869
221
  const __entry_dirname = dirname(fileURLToPath(import.meta.url));
2870
222
  const prerenderDir = resolve(__entry_dirname, "../prerender");
@@ -2884,884 +236,29 @@ app.use("*", async (c, next) => {
2884
236
  }
2885
237
  await next();
2886
238
  });
2887
- `,
2888
- platformExport: `
239
+ `,platformExport:`
2889
240
  const port = +(process.env.PORT || 3000);
2890
241
  serve({ fetch: app.fetch, port }, (info) => {
2891
242
  console.log(\`Server running at http://localhost:\${info.port}\`);
2892
243
  });
2893
- `
2894
- });
2895
- const tempEntry = path.resolve(root, ".node-entry.tmp.mjs");
2896
- fs.writeFileSync(tempEntry, entrySource);
2897
- try {
2898
- await buildBundle(ctx, {
2899
- entry: ".node-entry.tmp.mjs",
2900
- outDir: path.resolve(root, "dist/server"),
2901
- target: "node18",
2902
- emptyOutDir: false
2903
- });
2904
- } finally {
2905
- fs.rmSync(tempEntry, { force: true });
2906
- }
2907
- const prerendered = await prerenderRoutes(ctx);
2908
- if (prerendered.length > 0) {
2909
- const prerenderDir = path.resolve(root, "dist/prerender");
2910
- fs.mkdirSync(prerenderDir, { recursive: true });
2911
- for (const { url, html } of prerendered) {
2912
- const filePath = url === "/" ? path.join(prerenderDir, "index.html") : path.join(prerenderDir, url, "index.html");
2913
- fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2914
- fs.writeFileSync(filePath, html);
2915
- }
2916
- }
2917
- console.log(" Node output → dist/server/index.mjs\n Run: node dist/server/index.mjs\n");
2918
- }
2919
- };
2920
- }
2921
- //#endregion
2922
- //#region ../server/src/adapters/static.ts
2923
- /**
2924
- * Static 适配器 — 构建时预渲染
2925
- *
2926
- * 加载 SSR 模块和路由定义,将无参数路由自动预渲染为静态 HTML。
2927
- * 动态参数路由通过 dynamicRoutes 选项补充具体 URL。
2928
- *
2929
- * 输出:dist/static/ — 纯静态站点,可部署到任何静态托管。
2930
- */
2931
- function staticAdapter(opts = {}) {
2932
- return {
2933
- name: "static",
2934
- async build(ctx) {
2935
- const { fs, path, root } = ctx;
2936
- const outputDir = path.resolve(root, "dist/static");
2937
- fs.rmSync(outputDir, {
2938
- recursive: true,
2939
- force: true
2940
- });
2941
- fs.mkdirSync(outputDir, { recursive: true });
2942
- const { pathToFileURL } = await dynamicImport("node:url");
2943
- const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
2944
- const ssrModule = await dynamicImport(ssrPath);
2945
- ctx.copyStaticAssets(outputDir, { excludeHtml: true });
2946
- const { paths: routePaths, defs: routeDefs } = await extractRoutesWithModes(ctx, opts);
2947
- console.log(` Pre-rendering ${routePaths.length} pages...\n`);
2948
- for (const url of routePaths) try {
2949
- const mode = resolveRenderMode(url, routeDefs.find((r) => r.path === url)?.renderMode, ctx.renderModes);
2950
- let finalHtml;
2951
- if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml);
2952
- else {
2953
- const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
2954
- const serializedData = ssrModule.serializeServerData(serverData);
2955
- finalHtml = injectSSRForStatic(ctx.templateHtml, head, css, appHtml, serializedData);
2956
- }
2957
- const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
2958
- fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2959
- fs.writeFileSync(filePath, finalHtml);
2960
- } catch (e) {
2961
- console.warn(` [static] Failed to render ${url}:`, e);
2962
- }
2963
- console.log(` Static output → dist/static/\n`);
2964
- }
2965
- };
2966
- }
2967
- /** 从路由文件提取无参数路由 + 合并 dynamicRoutes(含 renderMode) */
2968
- async function extractRoutesWithModes(ctx, opts) {
2969
- const routesFile = opts.routesExport ?? "src/lib/bootstrap.ts";
2970
- const paths = [];
2971
- const defs = [];
2972
- try {
2973
- const { pathToFileURL } = await dynamicImport("node:url");
2974
- await ctx.vite.build({
2975
- root: ctx.root,
2976
- build: {
2977
- ssr: routesFile,
2978
- outDir: ctx.path.resolve(ctx.root, "dist/server"),
2979
- emptyOutDir: false,
2980
- rollupOptions: { output: { entryFileNames: "_routes.mjs" } }
2981
- },
2982
- resolve: ctx.resolvedResolve
2983
- });
2984
- const routesPath = pathToFileURL(ctx.path.resolve(ctx.root, "dist/server/_routes.mjs")).href;
2985
- const routesMod = await dynamicImport(routesPath);
2986
- const routes = routesMod.routes ?? routesMod.default;
2987
- if (Array.isArray(routes)) {
2988
- for (const r of routes) if (r.path && !r.path.includes(":")) {
2989
- paths.push(r.path);
2990
- defs.push({
2991
- path: r.path,
2992
- renderMode: r.renderMode
2993
- });
2994
- }
2995
- }
2996
- ctx.fs.rmSync(ctx.path.resolve(ctx.root, "dist/server/_routes.mjs"), { force: true });
2997
- } catch (e) {
2998
- console.warn(` [static] Could not load routes from "${routesFile}". Using "/" only.`, e);
2999
- if (paths.length === 0) paths.push("/");
3000
- }
3001
- if (opts.dynamicRoutes) {
3002
- for (const r of opts.dynamicRoutes) if (!paths.includes(r)) paths.push(r);
3003
- }
3004
- if (paths.length === 0) paths.push("/");
3005
- return {
3006
- paths,
3007
- defs
3008
- };
3009
- }
3010
- /** 内联 SSR 注入 */
3011
- function injectSSRForStatic(template, head, css, html, serializedData) {
3012
- const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
3013
- const replacements = {
3014
- head: head + "\n<style>" + css + "</style>",
3015
- body: html,
3016
- data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
3017
- };
3018
- return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
3019
- }
3020
- /** CSR 空壳注入 */
3021
- function injectCSRShellForStatic(template) {
3022
- return template.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
3023
- }
3024
- /** 解析最终渲染模式:Vite 配置覆盖 > 路由级 > 默认 "ssr" */
3025
- function resolveRenderMode(routePath, routeRenderMode, renderModes) {
3026
- if (renderModes) {
3027
- if (renderModes[routePath]) return renderModes[routePath];
3028
- for (const [pattern, mode] of Object.entries(renderModes)) if (pattern.includes("*")) {
3029
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
3030
- if (new RegExp("^" + escaped.replace(/\*/g, ".*") + "$").test(routePath)) return mode;
3031
- }
3032
- }
3033
- return routeRenderMode ?? "ssr";
3034
- }
3035
- //#endregion
3036
- //#region ../server/src/adapters/vercel.ts
3037
- /**
3038
- * Vercel 适配器 — Build Output API v3
3039
- *
3040
- * 生成 .vercel/output/ 目录:
3041
- * - config.json — 路由规则
3042
- * - static/ — 静态资源
3043
- * - functions/ssr.func/ — Serverless Function
3044
- */
3045
- function vercelAdapter() {
3046
- return {
3047
- name: "vercel",
3048
- async build(ctx) {
3049
- const { fs, path, root } = ctx;
3050
- const outputDir = path.resolve(root, ".vercel/output");
3051
- fs.rmSync(outputDir, {
3052
- recursive: true,
3053
- force: true
3054
- });
3055
- const entrySource = generateSSREntry(ctx, {
3056
- platformImport: `import { getRequestListener } from "@hono/node-server";`,
3057
- platformExport: [
3058
- `const _listener = getRequestListener(app.fetch);`,
3059
- `export default (req, res) => {`,
3060
- ` const m = req.headers["x-now-route-matches"];`,
3061
- ` if (typeof m === "string") {`,
3062
- ` try {`,
3063
- ` const p = new URLSearchParams(m);`,
3064
- ` const c = p.get("1");`,
3065
- ` if (c != null) {`,
3066
- ` const qi = (req.url || "").indexOf("?");`,
3067
- ` const qs = qi !== -1 ? req.url.slice(qi) : "";`,
3068
- ` req.url = "/" + decodeURIComponent(c) + qs;`,
3069
- ` }`,
3070
- ` } catch {}`,
3071
- ` }`,
3072
- ` return _listener(req, res);`,
3073
- `};`
3074
- ].join("\n")
3075
- });
3076
- const tempEntry = path.resolve(root, ".vercel-entry.tmp.mjs");
3077
- fs.writeFileSync(tempEntry, entrySource);
3078
- try {
3079
- const funcDir = path.resolve(root, ".vercel/output/functions/ssr.func");
3080
- await buildBundle(ctx, {
3081
- entry: ".vercel-entry.tmp.mjs",
3082
- outDir: funcDir,
3083
- target: "node18"
3084
- });
3085
- fs.writeFileSync(path.resolve(funcDir, ".vc-config.json"), JSON.stringify({
3086
- runtime: "nodejs20.x",
3087
- handler: "index.mjs",
3088
- launcherType: "Nodejs"
3089
- }, null, 2));
3090
- copyStaticAssets(ctx, path.resolve(root, ".vercel/output/static"));
3091
- fs.writeFileSync(path.resolve(root, ".vercel/output/config.json"), JSON.stringify({
3092
- version: 3,
3093
- routes: [{ handle: "filesystem" }, {
3094
- src: "/(.*)",
3095
- dest: "/ssr"
3096
- }]
3097
- }, null, 2));
3098
- } finally {
3099
- fs.rmSync(tempEntry, { force: true });
3100
- }
3101
- const prerendered = await prerenderRoutes(ctx);
3102
- const staticDir = path.resolve(root, ".vercel/output/static");
3103
- for (const { url, html } of prerendered) {
3104
- const filePath = url === "/" ? path.join(staticDir, "index.html") : path.join(staticDir, url, "index.html");
3105
- fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
3106
- fs.writeFileSync(filePath, html);
3107
- }
3108
- if (prerendered.length > 0) {
3109
- const configPath = path.resolve(root, ".vercel/output/config.json");
3110
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
3111
- config.overrides = config.overrides ?? {};
3112
- for (const { url } of prerendered) {
3113
- const key = url === "/" ? "index.html" : `${url.replace(/^\//, "")}/index.html`;
3114
- config.overrides[key] = {
3115
- path: url === "/" ? "/" : url,
3116
- contentType: "text/html; charset=utf-8"
3117
- };
3118
- }
3119
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
3120
- }
3121
- console.log(" Vercel output → .vercel/output/\n");
3122
- }
3123
- };
3124
- }
3125
- //#endregion
3126
- //#region ../server/src/adapters/resolve.ts
3127
- /**
3128
- * resolveAdapter — 字符串 → Adapter 映射
3129
- */
3130
- function resolveAdapter(value) {
3131
- if (typeof value !== "string") return value;
3132
- switch (value) {
3133
- case "vercel": return vercelAdapter();
3134
- case "cloudflare": return cloudflareAdapter();
3135
- case "netlify": return netlifyAdapter();
3136
- case "node": return nodeAdapter();
3137
- case "static": return staticAdapter();
3138
- case "auto": return autoAdapter();
3139
- default: throw new Error(`[finesoft] Unknown adapter: "${value}". Available: vercel, cloudflare, netlify, node, static, auto`);
3140
- }
3141
- }
3142
- //#endregion
3143
- //#region ../server/src/adapters/auto.ts
3144
- /**
3145
- * Auto 适配器 — 根据环境变量自动选择目标平台
3146
- *
3147
- * 检测顺序:
3148
- * VERCEL → vercel
3149
- * CF_PAGES → cloudflare
3150
- * NETLIFY → netlify
3151
- * (default) → node
3152
- */
3153
- function autoAdapter() {
3154
- return {
3155
- name: "auto",
3156
- async build(ctx) {
3157
- const detected = detectPlatform$1();
3158
- console.log(` [auto] Detected platform: ${detected}\n`);
3159
- return resolveAdapter(detected).build(ctx);
3160
- }
3161
- };
3162
- }
3163
- function detectPlatform$1() {
3164
- if (process.env.VERCEL) return "vercel";
3165
- if (process.env.CF_PAGES) return "cloudflare";
3166
- if (process.env.NETLIFY) return "netlify";
3167
- return "node";
3168
- }
3169
- //#endregion
3170
- //#region ../server/src/internal-fetch.ts
3171
- /**
3172
- * createInternalFetch — SSR 内部路由回环 fetch 包装器
3173
- *
3174
- * 将相对路径(/api/…)请求转为 Hono app.fetch 内存调用,
3175
- * 绝对 URL 和非字符串 input 走 globalThis.fetch(真实网络)。
3176
- *
3177
- * 递归深度保护采用请求头传递:每次 SSR 回环在请求头中写入深度值,
3178
- * SSR catch-all 读取深度判断是否超限。对比闭包计数器方案:
3179
- * - 并发安全:无共享可变状态
3180
- * - 跨渲染准确:深度随请求在 Hono 路由链中传递
3181
- */
3182
- const SSR_DEPTH_HEADER = "x-ssr-depth";
3183
- /**
3184
- * 创建请求级 internal fetch
3185
- *
3186
- * @param appFetch - Hono app.fetch(父级路由)
3187
- * @param depth - 当前 SSR 深度(由 catch-all handler 从请求头读取后 +1 传入)
3188
- */
3189
- function createInternalFetch(appFetch, depth = 1) {
3190
- return ((input, init) => {
3191
- if (typeof input === "string" && input.startsWith("/")) {
3192
- const request = new Request(`http://localhost${input}`, init);
3193
- request.headers.set(SSR_DEPTH_HEADER, String(depth));
3194
- return Promise.resolve(appFetch(request));
3195
- }
3196
- return globalThis.fetch(input, init);
3197
- });
3198
- }
3199
- //#endregion
3200
- //#region ../server/src/app.ts
3201
- /**
3202
- * createSSRApp — 创建 Hono SSR 应用
3203
- *
3204
- * 提供 SSR 通配路由,读取模板、加载 SSR 模块、渲染。
3205
- * 应用层可在此之上追加自定义路由(API 代理等)。
3206
- */
3207
- /**
3208
- * 匹配 Vite 配置级别的 renderMode 覆盖。
3209
- * 精确路径优先,然后 glob 模式。
3210
- */
3211
- function matchRenderModeOverride(url, renderModes) {
3212
- if (!renderModes) return null;
3213
- const path = url.split("?")[0];
3214
- if (renderModes[path]) return renderModes[path];
3215
- for (const [pattern, mode] of Object.entries(renderModes)) if (pattern.includes("*")) {
3216
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
3217
- if (new RegExp("^" + escaped.replace(/\*/g, ".*") + "$").test(path)) return mode;
3218
- }
3219
- return null;
3220
- }
3221
- function createSSRApp(options) {
3222
- const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes, defaultLocale } = options;
3223
- const app = new Hono();
3224
- const isrCache = new LruMap(1e3);
3225
- /** 生产环境模板缓存(模板不变,避免每请求重复读盘) */
3226
- let templateCache;
3227
- async function readTemplate(url) {
3228
- if (!isProduction && vite) {
3229
- const { readFileSync } = await dynamicImport("node:fs");
3230
- const raw = readFileSync((await dynamicImport("node:path")).resolve(root, "index.html"), "utf-8");
3231
- return vite.transformIndexHtml(url, raw);
3232
- }
3233
- if (templateCache) return templateCache;
3234
- if (typeof globalThis.Deno !== "undefined") {
3235
- const base = import.meta.url;
3236
- templateCache = globalThis.Deno.readTextFileSync(new URL("../dist/client/index.html", base));
3237
- return templateCache;
3238
- }
3239
- const { readFileSync } = await dynamicImport("node:fs");
3240
- templateCache = readFileSync((await dynamicImport("node:path")).resolve(root, "dist/client/index.html"), "utf-8");
3241
- return templateCache;
3242
- }
3243
- async function loadSSRModule() {
3244
- if (!isProduction && vite) return await vite.ssrLoadModule(ssrEntryPath);
3245
- if (ssrProductionModule) return dynamicImport(ssrProductionModule);
3246
- const path = await dynamicImport("node:path");
3247
- const { pathToFileURL } = await dynamicImport("node:url");
3248
- const absPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
3249
- return dynamicImport(absPath);
3250
- }
3251
- app.get("*", async (c) => {
3252
- const ssrDepth = parseInt(c.req.header("x-ssr-depth") ?? "0", 10);
3253
- if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
3254
- const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
3255
- try {
3256
- const template = await readTemplate(url);
3257
- const ssrMod = await loadSSRModule();
3258
- if (typeof ssrMod.render !== "function" || typeof ssrMod.serializeServerData !== "function") throw new Error("[SSR] Module missing required exports: render, serializeServerData");
3259
- const { render, serializeServerData } = ssrMod;
3260
- const overrideMode = matchRenderModeOverride(url, renderModes);
3261
- if (overrideMode === "csr") return c.html(injectCSRShell(template, defaultLocale ? getLocaleAttributes(defaultLocale) : void 0));
3262
- const cached = isrCache.get(url);
3263
- if (cached) return c.html(cached);
3264
- const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
3265
- const ssrContext = { request: c.req.raw };
3266
- if (requestFetch) ssrContext.fetch = requestFetch;
3267
- const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots, locale, status, rewriteUrl } = await render(url, ssrContext);
3268
- if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
3269
- if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
3270
- const finalHtml = injectSSRContent({
3271
- template,
3272
- head,
3273
- css,
3274
- html: appHtml,
3275
- serializedData: serializeServerData(serverData),
3276
- slots,
3277
- locale
3278
- });
3279
- if ((renderMode === "prerender" || overrideMode === "prerender") && !status && !rewriteUrl) isrCache.set(url, finalHtml);
3280
- if (rewriteUrl) c.header("Content-Location", rewriteUrl);
3281
- if (status && status >= 400) return c.html(finalHtml, status);
3282
- return c.html(finalHtml);
3283
- } catch (e) {
3284
- if (!isProduction && vite) vite.ssrFixStacktrace(e);
3285
- console.error("[SSR Error]", e);
3286
- return c.text("Internal Server Error", 500);
3287
- }
3288
- });
3289
- return app;
3290
- }
3291
- //#endregion
3292
- //#region ../server/src/runtime.ts
3293
- /**
3294
- * runtime — 运行时检测 + 项目根路径推导
3295
- */
3296
- /** 检测当前运行时环境 */
3297
- function detectRuntime() {
3298
- return {
3299
- isDeno: typeof globalThis.Deno !== "undefined",
3300
- isBun: typeof globalThis.Bun !== "undefined",
3301
- isVercel: !!process.env.VERCEL,
3302
- isProduction: process.env.NODE_ENV === "production"
3303
- };
3304
- }
3305
- /**
3306
- * 从 `import.meta.url` 推导项目根路径
3307
- *
3308
- * @param importMetaUrl - 调用方的 `import.meta.url`
3309
- * @param levelsUp - 向上移动多少级(默认 0,即调用方所在目录就是项目根)
3310
- */
3311
- async function resolveRoot(importMetaUrl, levelsUp = 0) {
3312
- if (typeof globalThis.Deno !== "undefined") {
3313
- let url = new URL(importMetaUrl);
3314
- for (let i = 0; i < levelsUp; i++) url = new URL("..", url);
3315
- return url.pathname;
3316
- }
3317
- const path = await dynamicImport("node:path");
3318
- const { fileURLToPath } = await dynamicImport("node:url");
3319
- let dir = path.normalize(path.dirname(fileURLToPath(importMetaUrl)));
3320
- for (let i = 0; i < levelsUp; i++) dir = path.resolve(dir, "..");
3321
- return dir;
3322
- }
3323
- //#endregion
3324
- //#region ../server/src/start.ts
3325
- /**
3326
- * startServer — 多运行时自动启动
3327
- *
3328
- * 支持 Node.js (dev HMR + prod)、Deno、Bun、Vercel。
3329
- */
3330
- async function startServer(options) {
3331
- const { app, root, port = 3e3, isProduction, vite, routes, ssrEntryPath } = options;
3332
- const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
3333
- function printStartupBanner() {
3334
- const lines = [`\n Server running at http://localhost:${port}\n`];
3335
- if (routes && routes.length > 0) {
3336
- lines.push(" Routes:");
3337
- for (const r of routes) lines.push(` ${r}`);
3338
- lines.push("");
3339
- }
3340
- if (ssrEntryPath) lines.push(` SSR Entry: ${ssrEntryPath}`);
3341
- if (ssrEntryPath) lines.push("");
3342
- console.log(lines.join("\n"));
3343
- }
3344
- if (isVercel) return { vite };
3345
- if (!isProduction) {
3346
- let devVite = vite;
3347
- if (!devVite) {
3348
- const { createServer: createViteServer } = await dynamicImport("vite");
3349
- devVite = await createViteServer({
3350
- root,
3351
- server: { middlewareMode: true },
3352
- appType: "custom"
3353
- });
3354
- }
3355
- const { getRequestListener } = await dynamicImport("@hono/node-server");
3356
- const { createServer } = await dynamicImport("node:http");
3357
- const listener = getRequestListener(app.fetch);
3358
- createServer((req, res) => {
3359
- devVite.middlewares(req, res, () => listener(req, res));
3360
- }).listen(port, () => {
3361
- printStartupBanner();
3362
- });
3363
- return { vite: devVite };
3364
- }
3365
- if (isDeno) globalThis.Deno.serve({ port }, app.fetch);
3366
- else if (isBun) {} else {
3367
- const { serveStatic } = await dynamicImport("@hono/node-server/serve-static");
3368
- const path = await dynamicImport("node:path");
3369
- const prodApp = new Hono();
3370
- const clientDir = path.resolve(root, "dist/client");
3371
- prodApp.use("/*", serveStatic({
3372
- root: clientDir,
3373
- rewriteRequestPath: (path) => path.endsWith("/") ? "/__nosuchfile__" : path
3374
- }));
3375
- prodApp.route("/", app);
3376
- const { serve } = await dynamicImport("@hono/node-server");
3377
- serve({
3378
- fetch: prodApp.fetch,
3379
- port
3380
- }, () => {
3381
- printStartupBanner();
3382
- });
3383
- }
3384
- return { vite };
3385
- }
3386
- //#endregion
3387
- //#region ../server/src/create-server.ts
3388
- /**
3389
- * createServer — 一站式服务器工厂
3390
- *
3391
- * 封装 env 加载、运行时检测、Vite 创建、Hono app、SSR、启动。
3392
- * 保留 setup() 钩子用于注册业务路由。
3393
- */
3394
- /**
3395
- * 创建并启动 SSR 服务器
3396
- *
3397
- * @example
3398
- * ```ts
3399
- * const { app } = await createServer({
3400
- * setup: (app) => registerProxies(app),
3401
- * });
3402
- * export { app };
3403
- * ```
3404
- */
3405
- async function createServer(config = {}) {
3406
- const { root: rootOverride, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
3407
- const root = rootOverride ?? process.cwd();
3408
- const { existsSync } = await dynamicImport("node:fs");
3409
- const envPath = (await dynamicImport("node:path")).resolve(root, ".env");
3410
- if (existsSync(envPath)) try {
3411
- const { config: dotenvConfig } = await dynamicImport("dotenv");
3412
- dotenvConfig({ path: envPath });
3413
- } catch (e) {
3414
- console.warn(`[Server] Failed to load .env: ${e.message}`);
3415
- }
3416
- const runtime = detectRuntime();
3417
- let vite;
3418
- if (!runtime.isProduction && !runtime.isVercel) {
3419
- const { createServer: createViteServer } = await dynamicImport("vite");
3420
- vite = await createViteServer({
3421
- root,
3422
- server: { middlewareMode: true },
3423
- appType: "custom"
3424
- });
3425
- }
3426
- const app = new Hono();
3427
- if (proxies?.length) registerProxyRoutes(app, proxies);
3428
- if (setup) await setup(app);
3429
- const ssrApp = createSSRApp({
3430
- root,
3431
- vite,
3432
- isProduction: runtime.isProduction,
3433
- parentFetch: app.fetch.bind(app),
3434
- ...ssr
3435
- });
3436
- app.route("/", ssrApp);
3437
- await startServer({
3438
- app,
3439
- root,
3440
- port,
3441
- isProduction: runtime.isProduction,
3442
- vite,
3443
- runtime,
3444
- ssrEntryPath: ssr?.ssrEntryPath
3445
- });
3446
- return {
3447
- app,
3448
- vite,
3449
- runtime
3450
- };
3451
- }
3452
- //#endregion
3453
- //#region ../server/src/locale.ts
3454
- /**
3455
- * Accept-Language 解析
3456
- */
3457
- /** Accept-Language 头最大长度 */
3458
- const MAX_HEADER_LENGTH = 1024;
3459
- /** 最大解析语言条目数 */
3460
- const MAX_LANG_ENTRIES = 50;
3461
- function parseAcceptLanguage(header, supported, fallback) {
3462
- const effectiveSupported = supported ?? ["zh", "en"];
3463
- const effectiveFallback = fallback ?? effectiveSupported[0] ?? "en";
3464
- if (!header || header.length > MAX_HEADER_LENGTH) return effectiveFallback;
3465
- const parts = header.split(",");
3466
- if (parts.length > MAX_LANG_ENTRIES) return effectiveFallback;
3467
- const langs = parts.map((part) => {
3468
- const [lang, q] = part.trim().split(";q=");
3469
- const qVal = q ? parseFloat(q) : 1;
3470
- return {
3471
- lang: lang.trim().toLowerCase(),
3472
- q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
3473
- };
3474
- }).filter((entry) => entry.q > 0).sort((a, b) => b.q - a.q);
3475
- for (const { lang } of langs) {
3476
- const prefix = lang.split("-")[0];
3477
- if (effectiveSupported.includes(prefix)) return prefix;
3478
- }
3479
- return effectiveFallback;
3480
- }
3481
- //#endregion
3482
- //#region ../server/src/vite-plugin.ts
3483
- /**
3484
- * finesoftFrontViteConfig — Vite 插件
3485
- *
3486
- * 将 Hono SSR 服务器集成到 Vite 的 dev / build / preview 生命周期中,
3487
- * 使 template-project 只需 `vite` / `vite build` / `vite preview` 即可运行。
3488
- *
3489
- * 支持多平台 adapter: "vercel" | "cloudflare" | "netlify" | "node" | "static" | "auto"
3490
- * 或自定义 Adapter 对象。
3491
- */
3492
- const GENERATED_I18N_LOADER_ID = "virtual:finesoft-front/i18n-loader";
3493
- const RESOLVED_GENERATED_I18N_LOADER_ID = `\0${GENERATED_I18N_LOADER_ID}`;
3494
- /**
3495
- * 从 setup 模块中查找 setup 函数:优先 default,其次 setup 命名导出。
3496
- */
3497
- function resolveSetupFn(mod) {
3498
- if (typeof mod.default === "function") return mod.default;
3499
- if (typeof mod.setup === "function") return mod.setup;
3500
- return Object.values(mod).find((v) => typeof v === "function") ?? null;
3501
- }
3502
- /**
3503
- * 匹配 Vite 配置级别的 renderMode 覆盖。
3504
- * 精确路径优先,然后 glob 模式。
3505
- */
3506
- function matchRenderModeConfig(url, renderModes) {
3507
- if (!renderModes) return null;
3508
- const path = url.split("?")[0];
3509
- if (renderModes[path]) return renderModes[path];
3510
- for (const [pattern, mode] of Object.entries(renderModes)) if (pattern.includes("*")) {
3511
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
3512
- if (new RegExp("^" + escaped.replace(/\*/g, ".*") + "$").test(path)) return mode;
3513
- }
3514
- return null;
3515
- }
3516
- function normalizePathForGlob(pathname) {
3517
- return pathname.replace(/\\/g, "/");
3518
- }
3519
- async function resolveMessagesDir(root, messagesDir) {
3520
- const { existsSync } = await dynamicImport("node:fs");
3521
- const path = await dynamicImport("node:path");
3522
- const absoluteDir = path.isAbsolute(messagesDir) ? messagesDir : path.resolve(root, messagesDir);
3523
- if (!existsSync(absoluteDir)) throw new Error(`[finesoftFrontViteConfig] i18n.messagesDir not found: ${messagesDir}`);
3524
- const relativeDir = normalizePathForGlob(path.relative(root, absoluteDir));
3525
- if (!relativeDir || relativeDir.startsWith("..")) throw new Error(`[finesoftFrontViteConfig] i18n.messagesDir must stay inside the project root: ${messagesDir}`);
3526
- return relativeDir.replace(/^\.\/+/, "");
3527
- }
3528
- function finesoftFrontViteConfig(options = {}) {
3529
- const ssrEntry = options.ssr?.entry ?? "src/ssr.ts";
3530
- let root = process.cwd();
3531
- let resolvedCommand;
3532
- let resolvedResolve;
3533
- let resolvedCss;
3534
- const CSS_EXTENSIONS = /\.(css|scss|less|sass|styl|stylus|pcss|postcss)($|\?)/;
3535
- return {
3536
- name: "finesoft-front",
3537
- config(userConfig) {
3538
- const overrides = {
3539
- appType: "custom",
3540
- define: { __FINESOFT_I18N_LOADER_SPECIFIER__: options.i18n?.messagesDir ? JSON.stringify(GENERATED_I18N_LOADER_ID) : "undefined" }
3541
- };
3542
- if (!process.env.__FINESOFT_SUB_BUILD__) overrides.build = { outDir: userConfig.build?.outDir ?? "dist/client" };
3543
- return overrides;
3544
- },
3545
- configResolved(config) {
3546
- resolvedCommand = config.command;
3547
- resolvedResolve = config.resolve;
3548
- resolvedCss = config.css;
3549
- root = config.root;
3550
- },
3551
- resolveId(id) {
3552
- if (id === GENERATED_I18N_LOADER_ID && options.i18n?.messagesDir) return RESOLVED_GENERATED_I18N_LOADER_ID;
3553
- return null;
3554
- },
3555
- async load(id) {
3556
- if (id !== RESOLVED_GENERATED_I18N_LOADER_ID || !options.i18n?.messagesDir) return null;
3557
- const baseDir = `/${await resolveMessagesDir(root, options.i18n.messagesDir)}`;
3558
- const globPattern = `${baseDir}/*.json`;
3559
- const filePrefix = `${baseDir}/`;
3560
- return `
3561
- const localeModules = import.meta.glob(${JSON.stringify(globPattern)}, { import: "default" });
244
+ `}),a=n.resolve(r,`.node-entry.tmp.mjs`);t.writeFileSync(a,i);try{await H(e,{entry:`.node-entry.tmp.mjs`,outDir:n.resolve(r,`dist/server`),target:`node18`,emptyOutDir:!1})}finally{t.rmSync(a,{force:!0})}let o=await W(e);if(o.length>0){let e=n.resolve(r,`dist/prerender`);t.mkdirSync(e,{recursive:!0});for(let{url:r,html:i}of o){let a=r===`/`?n.join(e,`index.html`):n.join(e,r,`index.html`);t.mkdirSync(n.resolve(a,`..`),{recursive:!0}),t.writeFileSync(a,i)}}console.log(` Node output → dist/server/index.mjs
245
+ Run: node dist/server/index.mjs
246
+ `)}}}function J(e={}){return{name:`static`,async build(t){let{fs:n,path:r,root:i}=t,a=r.resolve(i,`dist/static`);n.rmSync(a,{recursive:!0,force:!0}),n.mkdirSync(a,{recursive:!0});let{pathToFileURL:o}=await I(`node:url`),s=o(r.resolve(i,`dist/server/ssr.js`)).href,c=await I(s);t.copyStaticAssets(a,{excludeHtml:!0});let{paths:l,defs:u}=await ct(t,e);console.log(` Pre-rendering ${l.length} pages...\n`);for(let e of l)try{let i=dt(e,u.find(t=>t.path===e)?.renderMode,t.renderModes),o;if(i===`csr`)o=ut(t.templateHtml);else{let{html:n,head:r,css:i,serverData:a}=await c.render(e),s=c.serializeServerData(a);o=lt(t.templateHtml,r,i,n,s)}let s=e===`/`?r.join(a,`index.html`):r.join(a,e,`index.html`);n.mkdirSync(r.resolve(s,`..`),{recursive:!0}),n.writeFileSync(s,o)}catch(t){console.warn(` [static] Failed to render ${e}:`,t)}console.log(` Static output → dist/static/
247
+ `)}}}async function ct(e,t){let n=t.routesExport??`src/lib/bootstrap.ts`,r=[],i=[];try{let{pathToFileURL:t}=await I(`node:url`);await e.vite.build({root:e.root,build:{ssr:n,outDir:e.path.resolve(e.root,`dist/server`),emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`_routes.mjs`}}},resolve:e.resolvedResolve});let a=t(e.path.resolve(e.root,`dist/server/_routes.mjs`)).href,o=await I(a),s=o.routes??o.default;if(Array.isArray(s))for(let e of s)e.path&&!e.path.includes(`:`)&&(r.push(e.path),i.push({path:e.path,renderMode:e.renderMode}));e.fs.rmSync(e.path.resolve(e.root,`dist/server/_routes.mjs`),{force:!0})}catch(e){console.warn(` [static] Could not load routes from "${n}". Using "/" only.`,e),r.length===0&&r.push(`/`)}if(t.dynamicRoutes)for(let e of t.dynamicRoutes)r.includes(e)||r.push(e);return r.length===0&&r.push(`/`),{paths:r,defs:i}}function lt(e,t,n,r,i){let a=/<!--ssr-([a-z][a-z0-9-]*)-->/g,o={head:t+`
248
+ <style>`+n+`</style>`,body:r,data:`<script id="serialized-server-data" type="application/json">`+i+`<\/script>`};return e.replace(a,(e,t)=>o[t]??``)}function ut(e){return e.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g,()=>``)}function dt(e,t,n){if(n){if(n[e])return n[e];for(let[t,r]of Object.entries(n))if(t.includes(`*`)){let n=t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);if(RegExp(`^`+n.replace(/\*/g,`.*`)+`$`).test(e))return r}}return t??`ssr`}function ft(){return{name:`vercel`,async build(e){let{fs:t,path:n,root:r}=e,i=n.resolve(r,`.vercel/output`);t.rmSync(i,{recursive:!0,force:!0});let a=V(e,{platformImport:`import { getRequestListener } from "@hono/node-server";`,platformExport:[`const _listener = getRequestListener(app.fetch);`,`export default (req, res) => {`,` const m = req.headers["x-now-route-matches"];`,` if (typeof m === "string") {`,` try {`,` const p = new URLSearchParams(m);`,` const c = p.get("1");`,` if (c != null) {`,` const qi = (req.url || "").indexOf("?");`,` const qs = qi !== -1 ? req.url.slice(qi) : "";`,` req.url = "/" + decodeURIComponent(c) + qs;`,` }`,` } catch {}`,` }`,` return _listener(req, res);`,`};`].join(`
249
+ `)}),o=n.resolve(r,`.vercel-entry.tmp.mjs`);t.writeFileSync(o,a);try{let i=n.resolve(r,`.vercel/output/functions/ssr.func`);await H(e,{entry:`.vercel-entry.tmp.mjs`,outDir:i,target:`node18`}),t.writeFileSync(n.resolve(i,`.vc-config.json`),JSON.stringify({runtime:`nodejs20.x`,handler:`index.mjs`,launcherType:`Nodejs`},null,2)),U(e,n.resolve(r,`.vercel/output/static`)),t.writeFileSync(n.resolve(r,`.vercel/output/config.json`),JSON.stringify({version:3,routes:[{handle:`filesystem`},{src:`/(.*)`,dest:`/ssr`}]},null,2))}finally{t.rmSync(o,{force:!0})}let s=await W(e),c=n.resolve(r,`.vercel/output/static`);for(let{url:e,html:r}of s){let i=e===`/`?n.join(c,`index.html`):n.join(c,e,`index.html`);t.mkdirSync(n.resolve(i,`..`),{recursive:!0}),t.writeFileSync(i,r)}if(s.length>0){let e=n.resolve(r,`.vercel/output/config.json`),i=JSON.parse(t.readFileSync(e,`utf-8`));i.overrides=i.overrides??{};for(let{url:e}of s){let t=e===`/`?`index.html`:`${e.replace(/^\//,``)}/index.html`;i.overrides[t]={path:e===`/`?`/`:e,contentType:`text/html; charset=utf-8`}}t.writeFileSync(e,JSON.stringify(i,null,2))}console.log(` Vercel output → .vercel/output/
250
+ `)}}}function Y(e){if(typeof e!=`string`)return e;switch(e){case`vercel`:return ft();case`cloudflare`:return G();case`netlify`:return K();case`node`:return q();case`static`:return J();case`auto`:return pt();default:throw Error(`[finesoft] Unknown adapter: "${e}". Available: vercel, cloudflare, netlify, node, static, auto`)}}function pt(){return{name:`auto`,async build(e){let t=mt();return console.log(` [auto] Detected platform: ${t}\n`),Y(t).build(e)}}}function mt(){return process.env.VERCEL?`vercel`:process.env.CF_PAGES?`cloudflare`:process.env.NETLIFY?`netlify`:`node`}function ht(e,t=1){return((n,r)=>{if(typeof n==`string`&&n.startsWith(`/`)){let i=new Request(`http://localhost${n}`,r);return i.headers.set(`x-ssr-depth`,String(t)),Promise.resolve(e(i))}return globalThis.fetch(n,r)})}function gt(e,t){if(!t)return null;let n=e.split(`?`)[0];if(t[n])return t[n];for(let[e,r]of Object.entries(t))if(e.includes(`*`)){let t=e.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);if(RegExp(`^`+t.replace(/\*/g,`.*`)+`$`).test(n))return r}return null}function X(e){let{root:t,vite:n,isProduction:r,ssrEntryPath:i=`/src/ssr.ts`,ssrProductionModule:a,parentFetch:o,renderModes:s,defaultLocale:c}=e,l=new A,u=new h(1e3),d;async function f(e){if(!r&&n){let{readFileSync:r}=await I(`node:fs`),i=r((await I(`node:path`)).resolve(t,`index.html`),`utf-8`);return n.transformIndexHtml(e,i)}if(d)return d;if(globalThis.Deno!==void 0){let e=import.meta.url;return d=globalThis.Deno.readTextFileSync(new URL(`../dist/client/index.html`,e)),d}let{readFileSync:i}=await I(`node:fs`);return d=i((await I(`node:path`)).resolve(t,`dist/client/index.html`),`utf-8`),d}async function p(){if(!r&&n)return await n.ssrLoadModule(i);if(a)return I(a);let e=await I(`node:path`),{pathToFileURL:o}=await I(`node:url`),s=o(e.resolve(t,`dist/server/ssr.js`)).href;return I(s)}return l.get(`*`,async e=>{let t=parseInt(e.req.header(`x-ssr-depth`)??`0`,10);if(t>=5)return e.text(`SSR recursion loop detected`,508);let i=e.req.path+(e.req.url.includes(`?`)?`?`+e.req.url.split(`?`)[1]:``);try{let n=await f(i),r=await p();if(typeof r.render!=`function`||typeof r.serializeServerData!=`function`)throw Error(`[SSR] Module missing required exports: render, serializeServerData`);let{render:a,serializeServerData:l}=r,d=gt(i,s);if(d===`csr`)return e.html(F(n,c?O(c):void 0));let m=u.get(i);if(m)return e.html(m);let h=o?ht(o,t+1):void 0,g={request:e.req.raw};h&&(g.fetch=h);let{html:_,head:v,css:y,serverData:b,renderMode:x,redirect:S,slots:C,locale:w,status:T,rewriteUrl:E}=await a(i,g);if(S)return e.redirect(S.url,S.status);if(x===`csr`)return e.html(F(n,w));let D=P({template:n,head:v,css:y,html:_,serializedData:l(b),slots:C,locale:w});return(x===`prerender`||d===`prerender`)&&!T&&!E&&u.set(i,D),E&&e.header(`Content-Location`,E),T&&T>=400?e.html(D,T):e.html(D)}catch(t){return!r&&n&&n.ssrFixStacktrace(t),console.error(`[SSR Error]`,t),e.text(`Internal Server Error`,500)}}),l}function Z(){return{isDeno:globalThis.Deno!==void 0,isBun:globalThis.Bun!==void 0,isVercel:!!process.env.VERCEL,isProduction:process.env.NODE_ENV===`production`}}async function _t(e,t=0){if(globalThis.Deno!==void 0){let n=new URL(e);for(let e=0;e<t;e++)n=new URL(`..`,n);return n.pathname}let n=await I(`node:path`),{fileURLToPath:r}=await I(`node:url`),i=n.normalize(n.dirname(r(e)));for(let e=0;e<t;e++)i=n.resolve(i,`..`);return i}async function vt(e){let{app:t,root:n,port:r=3e3,isProduction:i,vite:a,routes:o,ssrEntryPath:s}=e,{isDeno:c,isBun:l,isVercel:u}=e.runtime??Z();function d(){let e=[`\n Server running at http://localhost:${r}\n`];if(o&&o.length>0){e.push(` Routes:`);for(let t of o)e.push(` ${t}`);e.push(``)}s&&e.push(` SSR Entry: ${s}`),s&&e.push(``),console.log(e.join(`
251
+ `))}if(u)return{vite:a};if(!i){let e=a;if(!e){let{createServer:t}=await I(`vite`);e=await t({root:n,server:{middlewareMode:!0},appType:`custom`})}let{getRequestListener:i}=await I(`@hono/node-server`),{createServer:o}=await I(`node:http`),s=i(t.fetch);return o((t,n)=>{e.middlewares(t,n,()=>s(t,n))}).listen(r,()=>{d()}),{vite:e}}if(c)globalThis.Deno.serve({port:r},t.fetch);else if(!l){let{serveStatic:e}=await I(`@hono/node-server/serve-static`),i=await I(`node:path`),a=new A,o=i.resolve(n,`dist/client`);a.use(`/*`,e({root:o,rewriteRequestPath:e=>e.endsWith(`/`)?`/__nosuchfile__`:e})),a.route(`/`,t);let{serve:s}=await I(`@hono/node-server`);s({fetch:a.fetch,port:r},()=>{d()})}return{vite:a}}async function yt(e={}){let{root:t,port:n=Number(process.env.PORT)||3e3,setup:r,proxies:i,ssr:a}=e,o=t??process.cwd(),{existsSync:s}=await I(`node:fs`),c=(await I(`node:path`)).resolve(o,`.env`);if(s(c))try{let{config:e}=await I(`dotenv`);e({path:c})}catch(e){console.warn(`[Server] Failed to load .env: ${e.message}`)}let l=Z(),u;if(!l.isProduction&&!l.isVercel){let{createServer:e}=await I(`vite`);u=await e({root:o,server:{middlewareMode:!0},appType:`custom`})}let d=new A;i?.length&&R(d,i),r&&await r(d);let f=X({root:o,vite:u,isProduction:l.isProduction,parentFetch:d.fetch.bind(d),...a});return d.route(`/`,f),await vt({app:d,root:o,port:n,isProduction:l.isProduction,vite:u,runtime:l,ssrEntryPath:a?.ssrEntryPath}),{app:d,vite:u,runtime:l}}function bt(e,t,n){let r=t??[`zh`,`en`],i=n??r[0]??`en`;if(!e||e.length>1024)return i;let a=e.split(`,`);if(a.length>50)return i;let o=a.map(e=>{let[t,n]=e.trim().split(`;q=`),r=n?parseFloat(n):1;return{lang:t.trim().toLowerCase(),q:Number.isFinite(r)&&r>=0&&r<=1?r:0}}).filter(e=>e.q>0).sort((e,t)=>t.q-e.q);for(let{lang:e}of o){let t=e.split(`-`)[0];if(r.includes(t))return t}return i}const Q=`virtual:finesoft-front/i18n-loader`,$=`\0${Q}`;function xt(e){return typeof e.default==`function`?e.default:typeof e.setup==`function`?e.setup:Object.values(e).find(e=>typeof e==`function`)??null}function St(e,t){if(!t)return null;let n=e.split(`?`)[0];if(t[n])return t[n];for(let[e,r]of Object.entries(t))if(e.includes(`*`)){let t=e.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);if(RegExp(`^`+t.replace(/\*/g,`.*`)+`$`).test(n))return r}return null}function Ct(e){return e.replace(/\\/g,`/`)}async function wt(e,t){let{existsSync:n}=await I(`node:fs`),r=await I(`node:path`),i=r.isAbsolute(t)?t:r.resolve(e,t);if(!n(i))throw Error(`[finesoftFrontViteConfig] i18n.messagesDir not found: ${t}`);let a=Ct(r.relative(e,i));if(!a||a.startsWith(`..`))throw Error(`[finesoftFrontViteConfig] i18n.messagesDir must stay inside the project root: ${t}`);return a.replace(/^\.\/+/,``)}function Tt(e={}){let t=e.ssr?.entry??`src/ssr.ts`,n=process.cwd(),r,i,a,o=/\.(css|scss|less|sass|styl|stylus|pcss|postcss)($|\?)/;return{name:`finesoft-front`,config(t){let n={appType:`custom`,define:{__FINESOFT_I18N_LOADER_SPECIFIER__:e.i18n?.messagesDir?JSON.stringify(Q):`undefined`}};return process.env.__FINESOFT_SUB_BUILD__||(n.build={outDir:t.build?.outDir??`dist/client`}),n},configResolved(e){r=e.command,i=e.resolve,a=e.css,n=e.root},resolveId(t){return t===Q&&e.i18n?.messagesDir?$:null},async load(t){if(t!==$||!e.i18n?.messagesDir)return null;let r=`/${await wt(n,e.i18n.messagesDir)}`,i=`${r}/*.json`,a=`${r}/`;return`
252
+ const localeModules = import.meta.glob(${JSON.stringify(i)}, { import: "default" });
3562
253
 
3563
254
  export async function loadMessages(locale) {
3564
- const loader = localeModules[${JSON.stringify(filePrefix)} + locale + ".json"];
255
+ const loader = localeModules[${JSON.stringify(a)} + locale + ".json"];
3565
256
  if (!loader) return undefined;
3566
257
  const messages = await loader();
3567
258
  return messages ?? undefined;
3568
259
  }
3569
- `;
3570
- },
3571
- transformIndexHtml: {
3572
- order: "pre",
3573
- async handler(html, ctx) {
3574
- const server = ctx.server;
3575
- if (!server) return;
3576
- const urlPath = (ctx.originalUrl || ctx.path || "").split("?")[0];
3577
- if (/\.\w+$/.test(urlPath) && !urlPath.endsWith(".html")) return;
3578
- const appEntry = [...html.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g)].find((m) => !m[1].startsWith("/@"));
3579
- if (!appEntry) return;
3580
- const browserEntry = appEntry[1];
3581
- try {
3582
- await server.transformRequest(browserEntry);
3583
- } catch {
3584
- return;
3585
- }
3586
- const cssUrls = [];
3587
- const visited = /* @__PURE__ */ new Set();
3588
- function walk(mod, depth = 0) {
3589
- if (depth > 100) return;
3590
- if (!mod?.url || visited.has(mod.url)) return;
3591
- visited.add(mod.url);
3592
- if (CSS_EXTENSIONS.test(mod.url) && !mod.url.includes(".svelte")) cssUrls.push(mod.url);
3593
- if (mod.importedModules) for (const imported of mod.importedModules) walk(imported, depth + 1);
3594
- }
3595
- const browserMod = await server.moduleGraph.getModuleByUrl(browserEntry);
3596
- if (browserMod) walk(browserMod);
3597
- if (cssUrls.length === 0) return;
3598
- const tags = [];
3599
- for (const url of cssUrls) try {
3600
- const css = (await server.ssrLoadModule(url))?.default;
3601
- if (typeof css === "string" && css.length > 0) tags.push({
3602
- tag: "style",
3603
- attrs: { "data-vite-dev-id": url },
3604
- children: css,
3605
- injectTo: "head"
3606
- });
3607
- } catch {}
3608
- return tags;
3609
- }
3610
- },
3611
- configureServer(server) {
3612
- return async () => {
3613
- const { Hono: HonoClass } = await dynamicImport("hono");
3614
- const { getRequestListener } = await dynamicImport("@hono/node-server");
3615
- const app = new HonoClass();
3616
- if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
3617
- if (typeof options.setup === "function") await options.setup(app);
3618
- else if (typeof options.setup === "string") {
3619
- const fn = resolveSetupFn(await server.ssrLoadModule("/" + options.setup));
3620
- if (fn) await fn(app);
3621
- }
3622
- const ssrApp = createSSRApp({
3623
- root,
3624
- vite: server,
3625
- isProduction: false,
3626
- ssrEntryPath: "/" + ssrEntry,
3627
- parentFetch: app.fetch.bind(app),
3628
- renderModes: options.renderModes,
3629
- defaultLocale: options.defaultLocale
3630
- });
3631
- app.route("/", ssrApp);
3632
- const listener = getRequestListener(app.fetch);
3633
- server.middlewares.use((req, res) => {
3634
- listener(req, res);
3635
- });
3636
- };
3637
- },
3638
- configurePreviewServer(server) {
3639
- return async () => {
3640
- const { readFileSync } = await dynamicImport("node:fs");
3641
- const path = await dynamicImport("node:path");
3642
- const { pathToFileURL } = await dynamicImport("node:url");
3643
- const { Hono: HonoClass } = await dynamicImport("hono");
3644
- const { getRequestListener } = await dynamicImport("@hono/node-server");
3645
- const app = new HonoClass();
3646
- const isrCache = new LruMap(1e3);
3647
- if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
3648
- if (typeof options.setup === "function") await options.setup(app);
3649
- else if (typeof options.setup === "string") try {
3650
- const setupPath = pathToFileURL(path.resolve(root, "dist/server/setup.mjs")).href;
3651
- const fn = resolveSetupFn(await dynamicImport(setupPath));
3652
- if (fn) await fn(app);
3653
- } catch {
3654
- console.warn("[finesoft] Could not load setup module for preview. API routes disabled.");
3655
- }
3656
- const template = readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
3657
- const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
3658
- const ssrModule = await dynamicImport(ssrPath);
3659
- app.get("*", async (c) => {
3660
- const ssrDepth = parseInt(c.req.header("x-ssr-depth") ?? "0", 10);
3661
- if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
3662
- const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
3663
- try {
3664
- const overrideMode = matchRenderModeConfig(url, options.renderModes);
3665
- if (overrideMode === "csr") return c.html(injectCSRShell(template, options.defaultLocale ? getLocaleAttributes(options.defaultLocale) : void 0));
3666
- const cached = isrCache.get(url);
3667
- if (cached) return c.html(cached);
3668
- const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots, locale, status, rewriteUrl } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
3669
- if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
3670
- if (renderMode === "csr") return c.html(injectCSRShell(template, locale));
3671
- const finalHtml = injectSSRContent({
3672
- template,
3673
- head,
3674
- css,
3675
- html: appHtml,
3676
- serializedData: ssrModule.serializeServerData(serverData),
3677
- slots,
3678
- locale
3679
- });
3680
- if ((renderMode === "prerender" || overrideMode === "prerender") && !status && !rewriteUrl) isrCache.set(url, finalHtml);
3681
- if (rewriteUrl) c.header("Content-Location", rewriteUrl);
3682
- if (status && status >= 400) return c.html(finalHtml, status);
3683
- return c.html(finalHtml);
3684
- } catch (e) {
3685
- console.error("[SSR Preview Error]", e);
3686
- return c.text("Internal Server Error", 500);
3687
- }
3688
- });
3689
- const listener = getRequestListener(app.fetch);
3690
- server.middlewares.use((req, res) => {
3691
- listener(req, res);
3692
- });
3693
- };
3694
- },
3695
- async closeBundle() {
3696
- if (process.env.__FINESOFT_SUB_BUILD__) return;
3697
- if (resolvedCommand !== "build") return;
3698
- process.env.__FINESOFT_SUB_BUILD__ = "1";
3699
- try {
3700
- const vite = await dynamicImport("vite");
3701
- const fs = await dynamicImport("node:fs");
3702
- const path = await dynamicImport("node:path");
3703
- console.log("\n Building SSR bundle...\n");
3704
- await vite.build({
3705
- root,
3706
- build: {
3707
- ssr: ssrEntry,
3708
- outDir: "dist/server"
3709
- },
3710
- ssr: { external: NODE_BUILTINS },
3711
- resolve: resolvedResolve,
3712
- css: resolvedCss
3713
- });
3714
- if (typeof options.setup === "string") {
3715
- console.log(" Building setup module...\n");
3716
- await vite.build({
3717
- root,
3718
- build: {
3719
- ssr: options.setup,
3720
- outDir: "dist/server",
3721
- emptyOutDir: false,
3722
- rollupOptions: { output: { entryFileNames: "setup.mjs" } }
3723
- },
3724
- resolve: resolvedResolve
3725
- });
3726
- }
3727
- if (options.adapter) {
3728
- const adapter = resolveAdapter(options.adapter);
3729
- const templateHtml = fs.readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
3730
- const ctx = {
3731
- root,
3732
- ssrEntry,
3733
- setupPath: typeof options.setup === "string" ? options.setup : void 0,
3734
- bootstrapEntry: options.bootstrapEntry,
3735
- templateHtml,
3736
- renderModes: options.renderModes,
3737
- proxies: options.proxies,
3738
- locales: options.locales,
3739
- defaultLocale: options.defaultLocale,
3740
- resolvedResolve,
3741
- resolvedCss,
3742
- vite,
3743
- fs,
3744
- path,
3745
- generateSSREntry(opts) {
3746
- return generateSSREntry(ctx, opts);
3747
- },
3748
- buildBundle(opts) {
3749
- return buildBundle(ctx, opts);
3750
- },
3751
- copyStaticAssets(destDir, opts) {
3752
- return copyStaticAssets(ctx, destDir, opts);
3753
- }
3754
- };
3755
- console.log(` Running adapter: ${adapter.name}...\n`);
3756
- await adapter.build(ctx);
3757
- }
3758
- } finally {
3759
- delete process.env.__FINESOFT_SUB_BUILD__;
3760
- }
3761
- }
3762
- };
3763
- }
3764
- //#endregion
3765
- export { ACTION_KINDS, ActionDispatcher, BaseController, BaseLogger, CompositeEventRecorder, CompositeLogger, CompositeLoggerFactory, ConsoleEventRecorder, ConsoleLogger, ConsoleLoggerFactory, Container, DEP_KEYS, Framework, History, HttpClient, HttpError, IntentDispatcher, IntersectionImpressionObserver, LruMap, PrefetchedIntents, ReportingLogger, ReportingLoggerFactory, Router, SSR_PLACEHOLDERS, SimpleTranslator, VoidEventRecorder, WithFieldsRecorder, autoAdapter, buildUrl, cloudflareAdapter, createBrowserContext, createPrefetchedIntentsFromDom, createSSRApp, createSSRRender, createServer, createServerContext, defineRoutes, deny, deserializeServerData, detectPlatform, detectRuntime, englishPlural, finesoftFrontViteConfig, generateProxyCode, generateUuid, getBaseUrl, getLocaleAttributes, getPWADisplayMode, getTextDirection, injectCSRShell, injectSSRContent, interpolate, isCompoundAction, isExternalUrlAction, isFlowAction, isNone, isRtl, isSome, makeDependencies, makeExternalUrlAction, makeFlowAction, makeLocaleInfo, mapEach, netlifyAdapter, next, nodeAdapter, parseAcceptLanguage, pipe, pipeAsync, redirect, registerActionHandlers, registerExternalUrlHandler, registerFlowActionHandler, registerProxyRoutes, removeHost, removeQueryParams, removeScheme, resetFilterCache, resolveAdapter, resolveConfiguredMessages, resolveLocaleFromUrl, resolveMessages, resolvePluralKey, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, setHtmlLocaleAttributes, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
3766
-
260
+ `},transformIndexHtml:{order:`pre`,async handler(e,t){let n=t.server;if(!n)return;let r=(t.originalUrl||t.path||``).split(`?`)[0];if(/\.\w+$/.test(r)&&!r.endsWith(`.html`))return;let i=[...e.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g)].find(e=>!e[1].startsWith(`/@`));if(!i)return;let a=i[1];try{await n.transformRequest(a)}catch{return}let s=[],c=new Set;function l(e,t=0){if(!(t>100)&&!(!e?.url||c.has(e.url))&&(c.add(e.url),o.test(e.url)&&!e.url.includes(`.svelte`)&&s.push(e.url),e.importedModules))for(let n of e.importedModules)l(n,t+1)}let u=await n.moduleGraph.getModuleByUrl(a);if(u&&l(u),s.length===0)return;let d=[];for(let e of s)try{let t=(await n.ssrLoadModule(e))?.default;typeof t==`string`&&t.length>0&&d.push({tag:`style`,attrs:{"data-vite-dev-id":e},children:t,injectTo:`head`})}catch{}return d}},configureServer(r){return async()=>{let{Hono:i}=await I(`hono`),{getRequestListener:a}=await I(`@hono/node-server`),o=new i;if(e.proxies?.length&&R(o,e.proxies),typeof e.setup==`function`)await e.setup(o);else if(typeof e.setup==`string`){let t=xt(await r.ssrLoadModule(`/`+e.setup));t&&await t(o)}let s=X({root:n,vite:r,isProduction:!1,ssrEntryPath:`/`+t,parentFetch:o.fetch.bind(o),renderModes:e.renderModes,defaultLocale:e.defaultLocale});o.route(`/`,s);let c=a(o.fetch);r.middlewares.use((e,t)=>{c(e,t)})}},configurePreviewServer(t){return async()=>{let{readFileSync:r}=await I(`node:fs`),i=await I(`node:path`),{pathToFileURL:a}=await I(`node:url`),{Hono:o}=await I(`hono`),{getRequestListener:s}=await I(`@hono/node-server`),c=new o,l=new h(1e3);if(e.proxies?.length&&R(c,e.proxies),typeof e.setup==`function`)await e.setup(c);else if(typeof e.setup==`string`)try{let e=a(i.resolve(n,`dist/server/setup.mjs`)).href,t=xt(await I(e));t&&await t(c)}catch{console.warn(`[finesoft] Could not load setup module for preview. API routes disabled.`)}let u=r(i.resolve(n,`dist/client/index.html`),`utf-8`),d=a(i.resolve(n,`dist/server/ssr.js`)).href,f=await I(d);c.get(`*`,async t=>{let n=parseInt(t.req.header(`x-ssr-depth`)??`0`,10);if(n>=5)return t.text(`SSR recursion loop detected`,508);let r=t.req.path+(t.req.url.includes(`?`)?`?`+t.req.url.split(`?`)[1]:``);try{let i=St(r,e.renderModes);if(i===`csr`)return t.html(F(u,e.defaultLocale?O(e.defaultLocale):void 0));let a=l.get(r);if(a)return t.html(a);let{html:o,head:s,css:d,serverData:p,renderMode:m,redirect:h,slots:g,locale:_,status:v,rewriteUrl:y}=await f.render(r,{fetch:ht(c.fetch.bind(c),n+1)});if(h)return t.redirect(h.url,h.status);if(m===`csr`)return t.html(F(u,_));let b=P({template:u,head:s,css:d,html:o,serializedData:f.serializeServerData(p),slots:g,locale:_});return(m===`prerender`||i===`prerender`)&&!v&&!y&&l.set(r,b),y&&t.header(`Content-Location`,y),v&&v>=400?t.html(b,v):t.html(b)}catch(e){return console.error(`[SSR Preview Error]`,e),t.text(`Internal Server Error`,500)}});let p=s(c.fetch);t.middlewares.use((e,t)=>{p(e,t)})}},async closeBundle(){if(!process.env.__FINESOFT_SUB_BUILD__&&r===`build`){process.env.__FINESOFT_SUB_BUILD__=`1`;try{let r=await I(`vite`),o=await I(`node:fs`),s=await I(`node:path`);if(console.log(`
261
+ Building SSR bundle...
262
+ `),await r.build({root:n,build:{ssr:t,outDir:`dist/server`},ssr:{external:B},resolve:i,css:a}),typeof e.setup==`string`&&(console.log(` Building setup module...
263
+ `),await r.build({root:n,build:{ssr:e.setup,outDir:`dist/server`,emptyOutDir:!1,rollupOptions:{output:{entryFileNames:`setup.mjs`}}},resolve:i})),e.adapter){let c=Y(e.adapter),l=o.readFileSync(s.resolve(n,`dist/client/index.html`),`utf-8`),u={root:n,ssrEntry:t,setupPath:typeof e.setup==`string`?e.setup:void 0,bootstrapEntry:e.bootstrapEntry,templateHtml:l,renderModes:e.renderModes,proxies:e.proxies,locales:e.locales,defaultLocale:e.defaultLocale,resolvedResolve:i,resolvedCss:a,vite:r,fs:o,path:s,generateSSREntry(e){return V(u,e)},buildBundle(e){return H(u,e)},copyStaticAssets(e,t){return U(u,e,t)}};console.log(` Running adapter: ${c.name}...\n`),await c.build(u)}}finally{delete process.env.__FINESOFT_SUB_BUILD__}}}}}export{pe as ACTION_KINDS,he as ActionDispatcher,m as BaseController,_ as BaseLogger,ue as CompositeEventRecorder,e as CompositeLogger,le as CompositeLoggerFactory,s as ConsoleEventRecorder,u as ConsoleLogger,T as ConsoleLoggerFactory,ke as Container,c as DEP_KEYS,l as Framework,Ee as History,g as HttpClient,o as HttpError,Ce as IntentDispatcher,se as IntersectionImpressionObserver,h as LruMap,f as PrefetchedIntents,d as ReportingLogger,Ae as ReportingLoggerFactory,S as Router,Je as SSR_PLACEHOLDERS,Fe as SimpleTranslator,Ie as VoidEventRecorder,be as WithFieldsRecorder,pt as autoAdapter,re as buildUrl,G as cloudflareAdapter,D as createBrowserContext,we as createPrefetchedIntentsFromDom,X as createSSRApp,qe as createSSRRender,yt as createServer,k as createServerContext,ye as defineRoutes,Oe as deny,je as deserializeServerData,C as detectPlatform,Z as detectRuntime,Te as englishPlural,Tt as finesoftFrontViteConfig,z as generateProxyCode,He as generateUuid,Be as getBaseUrl,O as getLocaleAttributes,b as getPWADisplayMode,oe as getTextDirection,F as injectCSRShell,P as injectSSRContent,Me as interpolate,ee as isCompoundAction,Re as isExternalUrlAction,Ue as isFlowAction,a as isNone,xe as isRtl,i as isSome,x as makeDependencies,ie as makeExternalUrlAction,Ve as makeFlowAction,Le as makeLocaleInfo,t as mapEach,K as netlifyAdapter,Se as next,q as nodeAdapter,bt as parseAcceptLanguage,ve as pipe,p as pipeAsync,me as redirect,ge as registerActionHandlers,ae as registerExternalUrlHandler,te as registerFlowActionHandler,R as registerProxyRoutes,y as removeHost,r as removeQueryParams,ze as removeScheme,w as resetFilterCache,Y as resolveAdapter,ne as resolveConfiguredMessages,ce as resolveLocaleFromUrl,De as resolveMessages,_e as resolvePluralKey,_t as resolveRoot,fe as rewrite,We as runAfterLoadGuards,n as runBeforeLoadGuards,et as serializeServerData,de as setHtmlLocaleAttributes,E as shouldLog,Ge as ssrRender,v as stableStringify,Pe as startBrowserApp,vt as startServer,J as staticAdapter,Ne as tryScroll,ft as vercelAdapter};
3767
264
  //# sourceMappingURL=index.mjs.map