@finesoft/front 0.1.2 → 0.1.11

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.js CHANGED
@@ -1,1040 +1,53 @@
1
- // ../core/dist/index.js
2
- var ACTION_KINDS = {
3
- FLOW: "flow",
4
- EXTERNAL_URL: "externalUrl",
5
- COMPOUND: "compound"
6
- };
7
- function isFlowAction(action) {
8
- return action.kind === ACTION_KINDS.FLOW;
9
- }
10
- function isExternalUrlAction(action) {
11
- return action.kind === ACTION_KINDS.EXTERNAL_URL;
12
- }
13
- function isCompoundAction(action) {
14
- return action.kind === ACTION_KINDS.COMPOUND;
15
- }
16
- function makeFlowAction(url, presentationContext) {
17
- return { kind: ACTION_KINDS.FLOW, url, presentationContext };
18
- }
19
- function makeExternalUrlAction(url) {
20
- return { kind: ACTION_KINDS.EXTERNAL_URL, url };
21
- }
22
- var ActionDispatcher = class {
23
- handlers = /* @__PURE__ */ new Map();
24
- wiredActions = /* @__PURE__ */ new Set();
25
- /** 注册指定 kind 的 handler(防止重复注册) */
26
- onAction(kind, handler) {
27
- if (this.wiredActions.has(kind)) {
28
- console.warn(
29
- `[ActionDispatcher] kind="${kind}" already registered, skipping`
30
- );
31
- return;
32
- }
33
- this.wiredActions.add(kind);
34
- this.handlers.set(kind, handler);
35
- }
36
- /** 执行一个 Action(CompoundAction 递归展开) */
37
- async perform(action) {
38
- if (isCompoundAction(action)) {
39
- for (const subAction of action.actions) {
40
- await this.perform(subAction);
41
- }
42
- return;
43
- }
44
- const handler = this.handlers.get(action.kind);
45
- if (!handler) {
46
- console.warn(
47
- `[ActionDispatcher] No handler for kind="${action.kind}"`
48
- );
49
- return;
50
- }
51
- await handler(action);
52
- }
53
- };
54
- var IntentDispatcher = class {
55
- controllers = /* @__PURE__ */ new Map();
56
- /** 注册一个 IntentController */
57
- register(controller) {
58
- this.controllers.set(controller.intentId, controller);
59
- }
60
- /** 分发 Intent 到对应 Controller */
61
- async dispatch(intent, container) {
62
- const controller = this.controllers.get(intent.id);
63
- if (!controller) {
64
- throw new Error(
65
- `[IntentDispatcher] No controller for "${intent.id}". Registered: [${Array.from(this.controllers.keys()).join(", ")}]`
66
- );
67
- }
68
- return controller.perform(intent, container);
69
- }
70
- /** 检查是否已注册某个 Intent */
71
- has(intentId) {
72
- return this.controllers.has(intentId);
73
- }
74
- };
75
- var Container = class {
76
- registrations = /* @__PURE__ */ new Map();
77
- /** 注册依赖(默认单例) */
78
- register(key, factory, singleton = true) {
79
- this.registrations.set(key, { factory, singleton });
80
- return this;
81
- }
82
- /** 解析依赖 */
83
- resolve(key) {
84
- const reg = this.registrations.get(key);
85
- if (!reg) {
86
- throw new Error(`[Container] No registration for key: "${key}"`);
87
- }
88
- if (reg.singleton) {
89
- if (reg.instance === void 0) {
90
- reg.instance = reg.factory();
91
- }
92
- return reg.instance;
93
- }
94
- return reg.factory();
95
- }
96
- /** 检查是否已注册 */
97
- has(key) {
98
- return this.registrations.has(key);
99
- }
100
- /** 销毁容器,清除所有缓存 */
101
- dispose() {
102
- for (const reg of this.registrations.values()) {
103
- reg.instance = void 0;
104
- }
105
- this.registrations.clear();
106
- }
107
- };
108
- var BaseLogger = class {
109
- category;
110
- constructor(category) {
111
- this.category = category;
112
- }
113
- };
114
- var LEVEL_TO_NUM = {
115
- "*": 4,
116
- debug: 4,
117
- info: 3,
118
- warn: 2,
119
- error: 1,
120
- off: 0,
121
- "": 0
122
- };
123
- var cachedRules;
124
- var cachedRaw;
125
- function parseRules() {
126
- if (typeof globalThis.localStorage === "undefined") {
127
- return {};
128
- }
129
- let raw;
130
- try {
131
- raw = globalThis.localStorage.getItem("onyxLog");
132
- } catch {
133
- return {};
134
- }
135
- if (!raw) return {};
136
- if (raw === cachedRaw && cachedRules) return cachedRules;
137
- cachedRaw = raw;
138
- const rules = {};
139
- const parts = raw.split(",");
140
- for (const part of parts) {
141
- const [name, level] = part.trim().split("=");
142
- if (!name || level === void 0) continue;
143
- const num = LEVEL_TO_NUM[level.toLowerCase()] ?? void 0;
144
- if (num === void 0) continue;
145
- if (name === "*") {
146
- rules.defaultLevel = num;
147
- } else {
148
- rules.named ??= {};
149
- rules.named[name] = num;
150
- }
151
- }
152
- cachedRules = rules;
153
- return rules;
154
- }
155
- function shouldLog(name, level) {
156
- const rules = parseRules();
157
- if (rules.defaultLevel === void 0 && !rules.named) {
158
- return true;
159
- }
160
- const currentNum = LEVEL_TO_NUM[level] ?? 4;
161
- if (rules.named?.[name] !== void 0) {
162
- return currentNum <= rules.named[name];
163
- }
164
- if (rules.defaultLevel !== void 0) {
165
- return currentNum <= rules.defaultLevel;
166
- }
167
- return true;
168
- }
169
- function resetFilterCache() {
170
- cachedRules = void 0;
171
- cachedRaw = void 0;
172
- }
173
- var ConsoleLogger = class extends BaseLogger {
174
- debug(...args) {
175
- if (shouldLog(this.category, "debug")) {
176
- console.debug(`[${this.category}]`, ...args);
177
- }
178
- return "";
179
- }
180
- info(...args) {
181
- if (shouldLog(this.category, "info")) {
182
- console.info(`[${this.category}]`, ...args);
183
- }
184
- return "";
185
- }
186
- warn(...args) {
187
- if (shouldLog(this.category, "warn")) {
188
- console.warn(`[${this.category}]`, ...args);
189
- }
190
- return "";
191
- }
192
- error(...args) {
193
- console.error(`[${this.category}]`, ...args);
194
- return "";
195
- }
196
- };
197
- var ConsoleLoggerFactory = class {
198
- loggerFor(category) {
199
- return new ConsoleLogger(category);
200
- }
201
- };
202
- var DEP_KEYS = {
203
- LOGGER: "logger",
204
- LOGGER_FACTORY: "loggerFactory",
205
- NET: "net",
206
- LOCALE: "locale",
207
- STORAGE: "storage",
208
- FEATURE_FLAGS: "featureFlags",
209
- METRICS: "metrics",
210
- FETCH: "fetch"
211
- };
212
- var DefaultLocale = class {
213
- language = "en";
214
- storefront = "us";
215
- setActiveLocale(language, storefront) {
216
- this.language = language;
217
- this.storefront = storefront;
218
- }
219
- };
220
- var MemoryStorage = class {
221
- store = /* @__PURE__ */ new Map();
222
- get(key) {
223
- return this.store.get(key);
224
- }
225
- set(key, value) {
226
- this.store.set(key, value);
227
- }
228
- delete(key) {
229
- this.store.delete(key);
230
- }
231
- };
232
- var DefaultFeatureFlags = class {
233
- flags;
234
- constructor(flags = {}) {
235
- this.flags = flags;
236
- }
237
- isEnabled(key) {
238
- return this.flags[key] === true;
239
- }
240
- getString(key) {
241
- const v = this.flags[key];
242
- return typeof v === "string" ? v : void 0;
243
- }
244
- getNumber(key) {
245
- const v = this.flags[key];
246
- return typeof v === "number" ? v : void 0;
247
- }
248
- };
249
- var ConsoleMetrics = class {
250
- recordPageView(page, fields) {
251
- console.info(`[Metrics:PageView] ${page}`, fields ?? "");
252
- }
253
- recordEvent(name, fields) {
254
- console.info(`[Metrics:Event] ${name}`, fields ?? "");
255
- }
256
- };
257
- function makeDependencies(container, options = {}) {
258
- const {
259
- fetch: fetchFn = globalThis.fetch?.bind(globalThis),
260
- language = "en",
261
- storefront = "us",
262
- featureFlags = {}
263
- } = options;
264
- const loggerFactory = new ConsoleLoggerFactory();
265
- container.register(
266
- DEP_KEYS.LOGGER_FACTORY,
267
- () => loggerFactory
268
- );
269
- container.register(
270
- DEP_KEYS.LOGGER,
271
- () => loggerFactory.loggerFor("framework")
272
- );
273
- container.register(DEP_KEYS.NET, () => ({
274
- fetch: (url, opts) => fetchFn(url, opts)
275
- }));
276
- container.register(DEP_KEYS.LOCALE, () => {
277
- const locale = new DefaultLocale();
278
- locale.setActiveLocale(language, storefront);
279
- return locale;
280
- });
281
- container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
282
- container.register(
283
- DEP_KEYS.FEATURE_FLAGS,
284
- () => new DefaultFeatureFlags(featureFlags)
285
- );
286
- container.register(
287
- DEP_KEYS.METRICS,
288
- () => new ConsoleMetrics()
289
- );
290
- container.register(DEP_KEYS.FETCH, () => fetchFn);
291
- }
292
- var Router = class {
293
- routes = [];
294
- /** 添加路由规则 */
295
- add(pattern, intentId) {
296
- const paramNames = [];
297
- const regexStr = pattern.replace(
298
- /\/:(\w+)(\?)?/g,
299
- (_, name, optional) => {
300
- paramNames.push(name);
301
- return optional ? "(?:/([^/]+))?" : "/([^/]+)";
302
- }
303
- );
304
- this.routes.push({
305
- pattern,
306
- intentId,
307
- regex: new RegExp(`^${regexStr}/?$`),
308
- paramNames
309
- });
310
- return this;
311
- }
312
- /** 解析 URL → RouteMatch */
313
- resolve(urlOrPath) {
314
- const path = this.extractPath(urlOrPath);
315
- const queryParams = this.extractQueryParams(urlOrPath);
316
- for (const route of this.routes) {
317
- const match = path.match(route.regex);
318
- if (match) {
319
- const params = {};
320
- route.paramNames.forEach((name, index) => {
321
- const value = match[index + 1];
322
- if (value) params[name] = value;
323
- });
324
- for (const [k, v] of Object.entries(queryParams)) {
325
- if (!(k in params)) params[k] = v;
326
- }
327
- return {
328
- intent: { id: route.intentId, params },
329
- action: makeFlowAction(urlOrPath)
330
- };
331
- }
332
- }
333
- return null;
334
- }
335
- /** 获取所有已注册的路由 */
336
- getRoutes() {
337
- return this.routes.map((r) => `${r.pattern} \u2192 ${r.intentId}`);
338
- }
339
- extractPath(url) {
340
- try {
341
- const parsed = new URL(url, "http://localhost");
342
- return parsed.pathname;
343
- } catch {
344
- return url.split("?")[0].split("#")[0];
345
- }
346
- }
347
- extractQueryParams(url) {
348
- try {
349
- const parsed = new URL(url, "http://localhost");
350
- const params = {};
351
- parsed.searchParams.forEach((v, k) => {
352
- params[k] = v;
353
- });
354
- return params;
355
- } catch {
356
- return {};
357
- }
358
- }
359
- };
360
- var CompositeLoggerFactory = class {
361
- constructor(factories) {
362
- this.factories = factories;
363
- }
364
- loggerFor(name) {
365
- return new CompositeLogger(
366
- this.factories.map((f) => f.loggerFor(name))
367
- );
368
- }
369
- };
370
- var CompositeLogger = class {
371
- constructor(loggers) {
372
- this.loggers = loggers;
373
- }
374
- debug(...args) {
375
- return this.callAll("debug", args);
376
- }
377
- info(...args) {
378
- return this.callAll("info", args);
379
- }
380
- warn(...args) {
381
- return this.callAll("warn", args);
382
- }
383
- error(...args) {
384
- return this.callAll("error", args);
385
- }
386
- callAll(method, args) {
387
- for (const logger of this.loggers) {
388
- logger[method](...args);
389
- }
390
- return "";
391
- }
392
- };
393
- function stableStringify(obj) {
394
- if (obj === null || obj === void 0) return String(obj);
395
- if (typeof obj !== "object") return JSON.stringify(obj);
396
- if (Array.isArray(obj)) {
397
- return "[" + obj.map(stableStringify).join(",") + "]";
398
- }
399
- const keys = Object.keys(obj).sort();
400
- const parts = keys.filter((k) => obj[k] !== void 0).map(
401
- (k) => JSON.stringify(k) + ":" + stableStringify(obj[k])
402
- );
403
- return "{" + parts.join(",") + "}";
404
- }
405
- var PrefetchedIntents = class _PrefetchedIntents {
406
- intents;
407
- constructor(intents) {
408
- this.intents = intents;
409
- }
410
- /** 从 PrefetchedIntent 数组创建缓存实例 */
411
- static fromArray(items) {
412
- const map = /* @__PURE__ */ new Map();
413
- for (const item of items) {
414
- if (item.intent && item.data !== void 0) {
415
- const key = stableStringify(item.intent);
416
- map.set(key, item.data);
417
- }
418
- }
419
- return new _PrefetchedIntents(map);
420
- }
421
- /** 创建空缓存实例 */
422
- static empty() {
423
- return new _PrefetchedIntents(/* @__PURE__ */ new Map());
424
- }
425
- /**
426
- * 获取缓存的 Intent 结果(一次性使用)。
427
- * 命中后从缓存中删除。
428
- */
429
- get(intent) {
430
- const key = stableStringify(intent);
431
- const data = this.intents.get(key);
432
- if (data !== void 0) {
433
- this.intents.delete(key);
434
- return data;
435
- }
436
- return void 0;
437
- }
438
- /** 检查缓存中是否有某个 Intent 的数据 */
439
- has(intent) {
440
- return this.intents.has(stableStringify(intent));
441
- }
442
- /** 缓存中的条目数 */
443
- get size() {
444
- return this.intents.size;
445
- }
446
- };
447
- var Framework = class _Framework {
448
- container;
449
- intentDispatcher;
450
- actionDispatcher;
451
- router;
452
- prefetchedIntents;
453
- constructor(container, prefetchedIntents) {
454
- this.container = container;
455
- this.intentDispatcher = new IntentDispatcher();
456
- this.actionDispatcher = new ActionDispatcher();
457
- this.router = new Router();
458
- this.prefetchedIntents = prefetchedIntents;
459
- }
460
- /** 创建并初始化 Framework 实例 */
461
- static create(config = {}) {
462
- const container = new Container();
463
- makeDependencies(container, config);
464
- const fw = new _Framework(
465
- container,
466
- config.prefetchedIntents ?? PrefetchedIntents.empty()
467
- );
468
- config.setupRoutes?.(fw.router);
469
- return fw;
470
- }
471
- /** 分发 Intent — 获取页面数据 */
472
- async dispatch(intent) {
473
- const logger = this.container.resolve(DEP_KEYS.LOGGER);
474
- const cached = this.prefetchedIntents.get(intent);
475
- if (cached !== void 0) {
476
- logger.debug(
477
- `[Framework] re-using prefetched intent response for: ${intent.id}`,
478
- intent.params
479
- );
480
- return cached;
481
- }
482
- logger.debug(
483
- `[Framework] dispatch intent: ${intent.id}`,
484
- intent.params
485
- );
486
- return this.intentDispatcher.dispatch(intent, this.container);
487
- }
488
- /** 执行 Action — 处理用户交互 */
489
- async perform(action) {
490
- const logger = this.container.resolve(DEP_KEYS.LOGGER);
491
- logger.debug(`[Framework] perform action: ${action.kind}`);
492
- return this.actionDispatcher.perform(action);
493
- }
494
- /** 路由 URL — 将 URL 解析为 Intent + Action */
495
- routeUrl(url) {
496
- return this.router.resolve(url);
497
- }
498
- /** 记录页面访问事件 */
499
- didEnterPage(page) {
500
- const metrics = this.container.resolve(
501
- DEP_KEYS.METRICS
502
- );
503
- metrics.recordPageView(page.pageType, {
504
- pageId: page.id,
505
- title: page.title
506
- });
507
- }
508
- /** 注册 Action 处理器 */
509
- onAction(kind, handler) {
510
- this.actionDispatcher.onAction(kind, handler);
511
- }
512
- /** 注册 Intent Controller */
513
- registerIntent(controller) {
514
- this.intentDispatcher.register(controller);
515
- }
516
- /** 销毁 Framework 实例 */
517
- dispose() {
518
- this.container.dispose();
519
- }
520
- };
521
- var HttpError = class extends Error {
522
- constructor(status, statusText, body) {
523
- super(`HTTP ${status}: ${statusText}`);
524
- this.status = status;
525
- this.statusText = statusText;
526
- this.body = body;
527
- this.name = "HttpError";
528
- }
529
- };
530
- var HttpClient = class {
531
- baseUrl;
532
- defaultHeaders;
533
- fetchFn;
534
- constructor(config) {
535
- this.baseUrl = config.baseUrl;
536
- this.defaultHeaders = config.defaultHeaders ?? {};
537
- this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
538
- }
539
- /** GET 请求,返回解析后的 JSON */
540
- async get(path, params) {
541
- return this.request("GET", path, { params });
542
- }
543
- /** POST 请求,自动序列化 body 为 JSON */
544
- async post(path, body, params) {
545
- return this.request("POST", path, { body, params });
546
- }
547
- /** PUT 请求 */
548
- async put(path, body, params) {
549
- return this.request("PUT", path, { body, params });
550
- }
551
- /** DELETE 请求 */
552
- async del(path, params) {
553
- return this.request("DELETE", path, { params });
554
- }
555
- /**
556
- * 底层请求方法 — 子类可覆写以自定义行为
557
- *
558
- * 自动处理:
559
- * - URL 拼接 (baseUrl + path + params)
560
- * - 默认 headers 合并
561
- * - JSON body 序列化
562
- * - 响应 JSON 解析
563
- * - 非 2xx 状态码抛出 HttpError
564
- */
565
- async request(method, path, options) {
566
- const url = this.buildUrl(path, options?.params);
567
- const headers = {
568
- ...this.defaultHeaders,
569
- ...options?.headers
570
- };
571
- const init = { method, headers };
572
- if (options?.body !== void 0) {
573
- headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
574
- init.body = JSON.stringify(options.body);
575
- }
576
- const response = await this.fetchFn(url, init);
577
- if (!response.ok) {
578
- const body = await response.text().catch(() => void 0);
579
- throw new HttpError(response.status, response.statusText, body);
580
- }
581
- return response.json();
582
- }
583
- /** 构建完整 URL — 子类可覆写以自定义 URL 拼接逻辑 */
584
- buildUrl(path, params) {
585
- const base = this.baseUrl.endsWith("/") ? this.baseUrl.slice(0, -1) : this.baseUrl;
586
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
587
- const url = new URL(`${base}${normalizedPath}`, "http://placeholder");
588
- if (params) {
589
- for (const [k, v] of Object.entries(params)) {
590
- url.searchParams.set(k, v);
591
- }
592
- }
593
- if (this.baseUrl.startsWith("http")) {
594
- return url.toString();
595
- }
596
- return `${url.pathname}${url.search}`;
597
- }
598
- };
599
- var BaseController = class {
600
- /**
601
- * 错误回退 — 子类可选覆写
602
- *
603
- * 当 execute() 抛出异常时调用。
604
- * 默认行为: 重新抛出原始错误。
605
- *
606
- * @param params - Intent 参数
607
- * @param error - execute() 抛出的错误
608
- * @returns 回退数据
609
- */
610
- fallback(params, error) {
611
- throw error;
612
- }
613
- /**
614
- * IntentController.perform() 实现
615
- *
616
- * 自动 try/catch → fallback 模式。
617
- */
618
- async perform(intent, container) {
619
- const params = intent.params ?? {};
620
- try {
621
- return await this.execute(params, container);
622
- } catch (e) {
623
- return this.fallback(
624
- params,
625
- e instanceof Error ? e : new Error(String(e))
626
- );
627
- }
628
- }
629
- };
630
- function pipe(...mappers) {
631
- return (input) => mappers.reduce((acc, mapper) => mapper(acc), input);
632
- }
633
- function pipeAsync(...mappers) {
634
- return async (input) => {
635
- let acc = input;
636
- for (const mapper of mappers) {
637
- acc = await mapper(acc);
638
- }
639
- return acc;
640
- };
641
- }
642
- function mapEach(mapper) {
643
- return (items) => items.map(mapper);
644
- }
645
- function defineRoutes(framework, definitions) {
646
- const registeredIntents = /* @__PURE__ */ new Set();
647
- for (const def of definitions) {
648
- if (def.controller && !registeredIntents.has(def.intentId)) {
649
- framework.registerIntent(def.controller);
650
- registeredIntents.add(def.intentId);
651
- }
652
- framework.router.add(def.path, def.intentId);
653
- }
654
- }
655
- var LruMap = class {
656
- map = /* @__PURE__ */ new Map();
657
- capacity;
658
- constructor(capacity) {
659
- this.capacity = capacity;
660
- }
661
- get(key) {
662
- const value = this.map.get(key);
663
- if (value !== void 0) {
664
- this.map.delete(key);
665
- this.map.set(key, value);
666
- }
667
- return value;
668
- }
669
- set(key, value) {
670
- if (this.map.has(key)) {
671
- this.map.delete(key);
672
- } else if (this.map.size >= this.capacity) {
673
- const oldest = this.map.keys().next().value;
674
- if (oldest !== void 0) {
675
- this.map.delete(oldest);
676
- }
677
- }
678
- this.map.set(key, value);
679
- }
680
- has(key) {
681
- return this.map.has(key);
682
- }
683
- delete(key) {
684
- return this.map.delete(key);
685
- }
686
- get size() {
687
- return this.map.size;
688
- }
689
- clear() {
690
- this.map.clear();
691
- }
692
- };
693
- function isSome(value) {
694
- return value !== null && value !== void 0;
695
- }
696
- function isNone(value) {
697
- return value === null || value === void 0;
698
- }
699
- function removeScheme(url) {
700
- return url.replace(/^https?:\/\//, "");
701
- }
702
- function removeHost(url) {
703
- try {
704
- const parsed = new URL(url);
705
- return parsed.pathname + parsed.search + parsed.hash;
706
- } catch {
707
- return url;
708
- }
709
- }
710
- function removeQueryParams(url) {
711
- return url.split("?")[0];
712
- }
713
- function getBaseUrl(url) {
714
- return url.split("?")[0].split("#")[0];
715
- }
716
- function buildUrl(path, params) {
717
- if (!params) return path;
718
- const searchParams = new URLSearchParams();
719
- for (const [key, value] of Object.entries(params)) {
720
- if (value !== void 0) {
721
- searchParams.set(key, value);
722
- }
723
- }
724
- const qs = searchParams.toString();
725
- return qs ? `${path}?${qs}` : path;
726
- }
727
- function generateUuid() {
728
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
729
- return crypto.randomUUID();
730
- }
731
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
732
- const r = Math.random() * 16 | 0;
733
- const v = c === "x" ? r : r & 3 | 8;
734
- return v.toString(16);
735
- });
736
- }
737
-
738
- // ../browser/dist/index.js
739
- function registerExternalUrlHandler(deps) {
740
- const { framework, log } = deps;
741
- framework.onAction(
742
- ACTION_KINDS.EXTERNAL_URL,
743
- (action) => {
744
- log.debug(`ExternalUrlAction \u2192 ${action.url}`);
745
- window.open(action.url, "_blank", "noopener,noreferrer");
746
- }
747
- );
748
- }
749
- var MAX_TRIES = 100;
750
- var FUDGE = 16;
751
- var pendingFrame = null;
752
- function tryScroll(log, getScrollableElement, scrollY) {
753
- if (pendingFrame !== null) {
754
- cancelAnimationFrame(pendingFrame);
755
- pendingFrame = null;
756
- }
757
- let tries = 0;
758
- pendingFrame = requestAnimationFrame(function attempt() {
759
- if (++tries >= MAX_TRIES) {
760
- log.warn(
761
- `tryScroll: gave up after ${MAX_TRIES} frames, target=${scrollY}`
762
- );
763
- pendingFrame = null;
764
- return;
765
- }
766
- const el = getScrollableElement();
767
- if (!el) {
768
- log.warn(
769
- "could not restore scroll: the scrollable element is missing"
770
- );
771
- return;
772
- }
773
- const { scrollHeight, offsetHeight } = el;
774
- const canScroll = scrollY + offsetHeight <= scrollHeight + FUDGE;
775
- if (!canScroll) {
776
- log.info("page is not tall enough for scroll yet", {
777
- scrollHeight,
778
- offsetHeight
779
- });
780
- pendingFrame = requestAnimationFrame(attempt);
781
- return;
782
- }
783
- el.scrollTop = scrollY;
784
- log.info("scroll restored to", scrollY);
785
- pendingFrame = null;
786
- });
787
- }
788
- var HISTORY_SIZE_LIMIT = 10;
789
- var History = class {
790
- entries;
791
- log;
792
- getScrollablePageElement;
793
- currentStateId;
794
- constructor(log, options, sizeLimit = HISTORY_SIZE_LIMIT) {
795
- this.entries = new LruMap(sizeLimit);
796
- this.log = log;
797
- this.getScrollablePageElement = options.getScrollablePageElement;
798
- }
799
- replaceState(state, url) {
800
- const id = generateUuid();
801
- window.history.replaceState({ id }, "", url);
802
- this.currentStateId = id;
803
- this.entries.set(id, { state, scrollY: 0 });
804
- this.scrollTop = 0;
805
- this.log.info("replaceState", state, url, id);
806
- }
807
- pushState(state, url) {
808
- const id = generateUuid();
809
- window.history.pushState({ id }, "", url);
810
- this.currentStateId = id;
811
- this.entries.set(id, { state, scrollY: 0 });
812
- this.scrollTop = 0;
813
- this.log.info("pushState", state, url, id);
814
- }
815
- beforeTransition() {
816
- const { state } = window.history;
817
- if (!state) return;
818
- const oldEntry = this.entries.get(state.id);
819
- if (!oldEntry) {
820
- this.log.info(
821
- "current history state evicted from LRU, not saving scroll position"
822
- );
823
- return;
824
- }
825
- const { scrollTop } = this;
826
- this.entries.set(state.id, { ...oldEntry, scrollY: scrollTop });
827
- this.log.info("saving scroll position", scrollTop);
828
- }
829
- onPopState(listener) {
830
- window.addEventListener("popstate", (event) => {
831
- this.currentStateId = event.state?.id;
832
- if (!this.currentStateId) {
833
- this.log.warn(
834
- "encountered a null event.state.id in onPopState event:",
835
- window.location.href
836
- );
837
- }
838
- this.log.info("popstate", this.entries, this.currentStateId);
839
- const entry = this.currentStateId ? this.entries.get(this.currentStateId) : void 0;
840
- listener(window.location.href, entry?.state);
841
- if (!entry) {
842
- return;
843
- }
844
- const { scrollY } = entry;
845
- this.log.info("restoring scroll to", scrollY);
846
- tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
847
- });
848
- }
849
- updateState(update) {
850
- if (!this.currentStateId) {
851
- this.log.warn(
852
- "failed: encountered a null currentStateId inside updateState"
853
- );
854
- return;
855
- }
856
- const currentState = this.entries.get(this.currentStateId);
857
- const newState = update(currentState?.state);
858
- this.log.info("updateState", newState, this.currentStateId);
859
- this.entries.set(this.currentStateId, {
860
- ...currentState,
861
- state: newState
862
- });
863
- }
864
- get scrollTop() {
865
- return this.getScrollablePageElement()?.scrollTop || 0;
866
- }
867
- set scrollTop(scrollTop) {
868
- const element = this.getScrollablePageElement();
869
- if (element) {
870
- element.scrollTop = scrollTop;
871
- }
872
- }
873
- };
874
- function registerFlowActionHandler(deps) {
875
- const { framework, log, callbacks, updateApp } = deps;
876
- let isFirstPage = true;
877
- const history = new History(log, {
878
- getScrollablePageElement: () => document.getElementById("scrollable-page-override") || document.getElementById("scrollable-page") || document.documentElement
879
- });
880
- framework.onAction(ACTION_KINDS.FLOW, async (action) => {
881
- const flowAction = action;
882
- const url = flowAction.url;
883
- log.debug(`FlowAction \u2192 ${url}`);
884
- if (flowAction.presentationContext === "modal") {
885
- const match2 = framework.routeUrl(url);
886
- if (match2) {
887
- const page = await framework.dispatch(
888
- match2.intent
889
- );
890
- callbacks.onModal(page);
891
- }
892
- return;
893
- }
894
- const shouldReplace = isFirstPage;
895
- const match = framework.routeUrl(url);
896
- if (!match) {
897
- log.warn(`FlowAction: no route for ${url}`);
898
- return;
899
- }
900
- const pagePromise = framework.dispatch(
901
- match.intent
902
- );
903
- await Promise.race([
904
- pagePromise,
905
- new Promise((r) => setTimeout(r, 500))
906
- ]).catch(() => {
907
- });
908
- history.beforeTransition();
909
- updateApp({
910
- page: pagePromise.then((page) => {
911
- const canonicalURL = url;
912
- if (shouldReplace) {
913
- history.replaceState({ page }, canonicalURL);
914
- } else {
915
- history.pushState({ page }, canonicalURL);
916
- }
917
- callbacks.onNavigate(
918
- new URL(canonicalURL, window.location.origin).pathname
919
- );
920
- didEnterPage(page);
921
- return page;
922
- }),
923
- isFirstPage
924
- });
925
- isFirstPage = false;
926
- });
927
- history.onPopState(async (url, cachedState) => {
928
- log.debug(`popstate \u2192 ${url}, cached=${!!cachedState}`);
929
- callbacks.onNavigate(new URL(url).pathname);
930
- if (cachedState) {
931
- const { page } = cachedState;
932
- didEnterPage(page);
933
- updateApp({ page, isFirstPage });
934
- return;
935
- }
936
- const parsed = new URL(url);
937
- const routeMatch = framework.routeUrl(parsed.pathname + parsed.search);
938
- if (!routeMatch) {
939
- log.error(
940
- "received popstate without data, but URL was unroutable:",
941
- url
942
- );
943
- didEnterPage(null);
944
- updateApp({
945
- page: Promise.reject(new Error("404")),
946
- isFirstPage
947
- });
948
- return;
949
- }
950
- const pagePromise = framework.dispatch(
951
- routeMatch.intent
952
- );
953
- await Promise.race([
954
- pagePromise,
955
- new Promise((r) => setTimeout(r, 500))
956
- ]).catch(() => {
957
- });
958
- updateApp({
959
- page: pagePromise.then((page) => {
960
- didEnterPage(page);
961
- return page;
962
- }),
963
- isFirstPage
964
- });
965
- });
966
- function didEnterPage(page) {
967
- (async () => {
968
- try {
969
- if (page) {
970
- await framework.didEnterPage(page);
971
- }
972
- } catch (e) {
973
- log.error("didEnterPage error:", e);
974
- }
975
- })();
976
- }
977
- }
978
- function registerActionHandlers(deps) {
979
- const { framework, log, callbacks, updateApp } = deps;
980
- registerFlowActionHandler({
981
- framework,
982
- log,
983
- callbacks,
984
- updateApp
985
- });
986
- registerExternalUrlHandler({ framework, log });
987
- }
988
- var SERVER_DATA_ID = "serialized-server-data";
989
- function deserializeServerData() {
990
- const script = document.getElementById(SERVER_DATA_ID);
991
- if (!script?.textContent) return void 0;
992
- script.parentNode?.removeChild(script);
993
- try {
994
- return JSON.parse(script.textContent);
995
- } catch {
996
- return void 0;
997
- }
998
- }
999
- function createPrefetchedIntentsFromDom() {
1000
- const data = deserializeServerData();
1001
- if (!data || !Array.isArray(data)) {
1002
- return PrefetchedIntents.empty();
1003
- }
1004
- return PrefetchedIntents.fromArray(data);
1005
- }
1006
- async function startBrowserApp(config) {
1007
- const { bootstrap, defaultLocale = "en", mount, callbacks } = config;
1008
- const prefetchedIntents = createPrefetchedIntentsFromDom();
1009
- const framework = Framework.create({ prefetchedIntents });
1010
- bootstrap(framework);
1011
- const loggerFactory = framework.container.resolve(
1012
- DEP_KEYS.LOGGER_FACTORY
1013
- );
1014
- const log = loggerFactory.loggerFor("browser");
1015
- const initialAction = framework.routeUrl(
1016
- window.location.pathname + window.location.search
1017
- );
1018
- const locale = document.documentElement.lang || defaultLocale;
1019
- const target = document.getElementById("app");
1020
- const updateApp = mount(target, { framework, locale });
1021
- registerActionHandlers({
1022
- framework,
1023
- log,
1024
- callbacks,
1025
- updateApp
1026
- });
1027
- if (initialAction) {
1028
- await framework.perform(initialAction.action);
1029
- } else {
1030
- updateApp({
1031
- page: Promise.reject(new Error("404")),
1032
- isFirstPage: true
1033
- });
1034
- }
1035
- }
1
+ import {
2
+ ACTION_KINDS,
3
+ ActionDispatcher,
4
+ BaseController,
5
+ BaseLogger,
6
+ CompositeLogger,
7
+ CompositeLoggerFactory,
8
+ ConsoleLogger,
9
+ ConsoleLoggerFactory,
10
+ Container,
11
+ DEP_KEYS,
12
+ Framework,
13
+ History,
14
+ HttpClient,
15
+ HttpError,
16
+ IntentDispatcher,
17
+ LruMap,
18
+ PrefetchedIntents,
19
+ Router,
20
+ buildUrl,
21
+ createPrefetchedIntentsFromDom,
22
+ defineRoutes,
23
+ deserializeServerData,
24
+ generateUuid,
25
+ getBaseUrl,
26
+ isCompoundAction,
27
+ isExternalUrlAction,
28
+ isFlowAction,
29
+ isNone,
30
+ isSome,
31
+ makeDependencies,
32
+ makeExternalUrlAction,
33
+ makeFlowAction,
34
+ mapEach,
35
+ pipe,
36
+ pipeAsync,
37
+ registerActionHandlers,
38
+ registerExternalUrlHandler,
39
+ registerFlowActionHandler,
40
+ removeHost,
41
+ removeQueryParams,
42
+ removeScheme,
43
+ resetFilterCache,
44
+ shouldLog,
45
+ stableStringify,
46
+ startBrowserApp,
47
+ tryScroll
48
+ } from "./chunk-OVGQ4NUA.js";
1036
49
 
1037
- // ../ssr/dist/index.js
50
+ // ../ssr/src/render.ts
1038
51
  async function ssrRender(options) {
1039
52
  const { url, frameworkConfig, bootstrap, getErrorPage, renderApp } = options;
1040
53
  const framework = Framework.create(frameworkConfig);
@@ -1063,6 +76,8 @@ async function ssrRender(options) {
1063
76
  serverData
1064
77
  };
1065
78
  }
79
+
80
+ // ../ssr/src/create-render.ts
1066
81
  function createSSRRender(config) {
1067
82
  const { bootstrap, getErrorPage, renderApp, frameworkConfig } = config;
1068
83
  return (url, locale) => ssrRender({
@@ -1073,12 +88,25 @@ function createSSRRender(config) {
1073
88
  renderApp: (page) => renderApp(page, locale)
1074
89
  });
1075
90
  }
91
+
92
+ // ../ssr/src/inject.ts
93
+ var SSR_PLACEHOLDERS = {
94
+ LANG: "<!--ssr-lang-->",
95
+ HEAD: "<!--ssr-head-->",
96
+ BODY: "<!--ssr-body-->",
97
+ DATA: "<!--ssr-data-->"
98
+ };
1076
99
  function injectSSRContent(options) {
1077
100
  const { template, locale, head, css, html, serializedData } = options;
1078
101
  const cssTag = css ? `<style>${css}</style>` : "";
1079
- return template.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", `${head}
1080
- ${cssTag}`).replace("<!--ssr-body-->", html).replace("<!--ssr-data-->", serializedData);
102
+ return template.replace(SSR_PLACEHOLDERS.LANG, locale).replace(SSR_PLACEHOLDERS.HEAD, `${head}
103
+ ${cssTag}`).replace(SSR_PLACEHOLDERS.BODY, html).replace(
104
+ SSR_PLACEHOLDERS.DATA,
105
+ `<script id="serialized-server-data" type="application/json">${serializedData}</script>`
106
+ );
1081
107
  }
108
+
109
+ // ../ssr/src/server-data.ts
1082
110
  var HTML_REPLACEMENTS = {
1083
111
  "<": "\\u003C",
1084
112
  ">": "\\u003E",
@@ -1095,14 +123,14 @@ function serializeServerData(data) {
1095
123
  );
1096
124
  }
1097
125
 
1098
- // ../server/dist/index.js
126
+ // ../server/src/app.ts
1099
127
  import { Hono } from "hono";
1100
- import { Hono as Hono3 } from "hono";
1101
- import { Hono as Hono2 } from "hono";
1102
- var DEFAULT_SUPPORTED = ["zh", "en"];
1103
- var DEFAULT_LOCALE = "en";
1104
- function parseAcceptLanguage(header, supported = DEFAULT_SUPPORTED, fallback = DEFAULT_LOCALE) {
1105
- if (!header) return fallback;
128
+
129
+ // ../server/src/locale.ts
130
+ function parseAcceptLanguage(header, supported, fallback) {
131
+ const effectiveSupported = supported ?? ["zh", "en"];
132
+ const effectiveFallback = fallback ?? effectiveSupported[0] ?? "en";
133
+ if (!header) return effectiveFallback;
1106
134
  const langs = header.split(",").map((part) => {
1107
135
  const [lang, q] = part.trim().split(";q=");
1108
136
  return {
@@ -1112,12 +140,14 @@ function parseAcceptLanguage(header, supported = DEFAULT_SUPPORTED, fallback = D
1112
140
  }).sort((a, b) => b.q - a.q);
1113
141
  for (const { lang } of langs) {
1114
142
  const prefix = lang.split("-")[0];
1115
- if (supported.includes(prefix)) {
143
+ if (effectiveSupported.includes(prefix)) {
1116
144
  return prefix;
1117
145
  }
1118
146
  }
1119
- return fallback;
147
+ return effectiveFallback;
1120
148
  }
149
+
150
+ // ../server/src/app.ts
1121
151
  function createSSRApp(options) {
1122
152
  const {
1123
153
  root,
@@ -1131,8 +161,14 @@ function createSSRApp(options) {
1131
161
  const app = new Hono();
1132
162
  async function readTemplate(url) {
1133
163
  if (!isProduction && vite) {
1134
- const { readFileSync: readFileSync2 } = await import("fs");
1135
- const { resolve: resolve2 } = await import("path");
164
+ const { readFileSync: readFileSync2 } = await import(
165
+ /* @vite-ignore */
166
+ "fs"
167
+ );
168
+ const { resolve: resolve2 } = await import(
169
+ /* @vite-ignore */
170
+ "path"
171
+ );
1136
172
  const raw = readFileSync2(resolve2(root, "index.html"), "utf-8");
1137
173
  return vite.transformIndexHtml(url, raw);
1138
174
  }
@@ -1142,8 +178,14 @@ function createSSRApp(options) {
1142
178
  new URL("../dist/client/index.html", import.meta.url)
1143
179
  );
1144
180
  }
1145
- const { readFileSync } = await import("fs");
1146
- const { resolve } = await import("path");
181
+ const { readFileSync } = await import(
182
+ /* @vite-ignore */
183
+ "fs"
184
+ );
185
+ const { resolve } = await import(
186
+ /* @vite-ignore */
187
+ "path"
188
+ );
1147
189
  return readFileSync(resolve(root, "dist/client/index.html"), "utf-8");
1148
190
  }
1149
191
  async function loadSSRModule() {
@@ -1189,6 +231,11 @@ function createSSRApp(options) {
1189
231
  });
1190
232
  return app;
1191
233
  }
234
+
235
+ // ../server/src/create-server.ts
236
+ import { Hono as Hono3 } from "hono";
237
+
238
+ // ../server/src/runtime.ts
1192
239
  function detectRuntime() {
1193
240
  return {
1194
241
  isDeno: typeof globalThis.Deno !== "undefined",
@@ -1206,40 +253,89 @@ async function resolveRoot(importMetaUrl, levelsUp = 0) {
1206
253
  }
1207
254
  return url.pathname;
1208
255
  }
1209
- const { dirname, resolve, normalize } = await import("path");
1210
- const { fileURLToPath } = await import("url");
256
+ const { dirname, resolve, normalize } = await import(
257
+ /* @vite-ignore */
258
+ "path"
259
+ );
260
+ const { fileURLToPath } = await import(
261
+ /* @vite-ignore */
262
+ "url"
263
+ );
1211
264
  let dir = normalize(dirname(fileURLToPath(importMetaUrl)));
1212
265
  for (let i = 0; i < levelsUp; i++) {
1213
266
  dir = resolve(dir, "..");
1214
267
  }
1215
268
  return dir;
1216
269
  }
270
+
271
+ // ../server/src/start.ts
272
+ import { Hono as Hono2 } from "hono";
1217
273
  async function startServer(options) {
1218
- const { app, root, port = 3e3, isProduction, vite } = options;
274
+ const {
275
+ app,
276
+ root,
277
+ port = 3e3,
278
+ isProduction,
279
+ vite,
280
+ routes,
281
+ locales,
282
+ ssrEntryPath
283
+ } = options;
1219
284
  const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
285
+ function printStartupBanner() {
286
+ const lines = [
287
+ `
288
+ Server running at http://localhost:${port}
289
+ `
290
+ ];
291
+ if (routes && routes.length > 0) {
292
+ lines.push(" Routes:");
293
+ for (const r of routes) {
294
+ lines.push(` ${r}`);
295
+ }
296
+ lines.push("");
297
+ }
298
+ if (locales && locales.length > 0) {
299
+ lines.push(` Locales: ${locales.join(", ")}`);
300
+ }
301
+ if (ssrEntryPath) {
302
+ lines.push(` SSR Entry: ${ssrEntryPath}`);
303
+ }
304
+ if (locales?.length || ssrEntryPath) {
305
+ lines.push("");
306
+ }
307
+ console.log(lines.join("\n"));
308
+ }
1220
309
  if (isVercel) {
1221
310
  return { vite };
1222
311
  }
1223
312
  if (!isProduction) {
1224
313
  let devVite = vite;
1225
314
  if (!devVite) {
1226
- const { createServer: createViteServer } = await import("vite");
315
+ const { createServer: createViteServer } = await import(
316
+ /* @vite-ignore */
317
+ "vite"
318
+ );
1227
319
  devVite = await createViteServer({
1228
320
  root,
1229
321
  server: { middlewareMode: true },
1230
322
  appType: "custom"
1231
323
  });
1232
324
  }
1233
- const { getRequestListener } = await import("@hono/node-server");
1234
- const { createServer: createServer2 } = await import("http");
325
+ const { getRequestListener } = await import(
326
+ /* @vite-ignore */
327
+ "@hono/node-server"
328
+ );
329
+ const { createServer: createServer2 } = await import(
330
+ /* @vite-ignore */
331
+ "http"
332
+ );
1235
333
  const listener = getRequestListener(app.fetch);
1236
334
  const server = createServer2((req, res) => {
1237
335
  devVite.middlewares(req, res, () => listener(req, res));
1238
336
  });
1239
337
  server.listen(port, () => {
1240
- console.log(`
1241
- Server running at http://localhost:${port}
1242
- `);
338
+ printStartupBanner();
1243
339
  });
1244
340
  return { vite: devVite };
1245
341
  }
@@ -1247,20 +343,29 @@ async function startServer(options) {
1247
343
  globalThis.Deno.serve({ port }, app.fetch);
1248
344
  } else if (isBun) {
1249
345
  } else {
1250
- const { serveStatic } = await import("@hono/node-server/serve-static");
1251
- const { resolve } = await import("path");
346
+ const { serveStatic } = await import(
347
+ /* @vite-ignore */
348
+ "@hono/node-server/serve-static"
349
+ );
350
+ const { resolve } = await import(
351
+ /* @vite-ignore */
352
+ "path"
353
+ );
1252
354
  const prodApp = new Hono2();
1253
355
  prodApp.use("/*", serveStatic({ root: resolve(root, "dist/client") }));
1254
356
  prodApp.route("/", app);
1255
- const { serve } = await import("@hono/node-server");
357
+ const { serve } = await import(
358
+ /* @vite-ignore */
359
+ "@hono/node-server"
360
+ );
1256
361
  serve({ fetch: prodApp.fetch, port }, () => {
1257
- console.log(`
1258
- Server running at http://localhost:${port}
1259
- `);
362
+ printStartupBanner();
1260
363
  });
1261
364
  }
1262
365
  return { vite };
1263
366
  }
367
+
368
+ // ../server/src/create-server.ts
1264
369
  async function createServer(config = {}) {
1265
370
  const {
1266
371
  root: rootOverride,
@@ -1271,12 +376,21 @@ async function createServer(config = {}) {
1271
376
  ssr
1272
377
  } = config;
1273
378
  const root = rootOverride ?? process.cwd();
1274
- const { existsSync } = await import("fs");
1275
- const { resolve } = await import("path");
379
+ const { existsSync } = await import(
380
+ /* @vite-ignore */
381
+ "fs"
382
+ );
383
+ const { resolve } = await import(
384
+ /* @vite-ignore */
385
+ "path"
386
+ );
1276
387
  const envPath = resolve(root, ".env");
1277
388
  if (existsSync(envPath)) {
1278
389
  try {
1279
- const { config: dotenvConfig } = await import("dotenv");
390
+ const { config: dotenvConfig } = await import(
391
+ /* @vite-ignore */
392
+ "dotenv"
393
+ );
1280
394
  dotenvConfig({ path: envPath });
1281
395
  } catch {
1282
396
  }
@@ -1284,7 +398,10 @@ async function createServer(config = {}) {
1284
398
  const runtime = detectRuntime();
1285
399
  let vite;
1286
400
  if (!runtime.isProduction && !runtime.isVercel) {
1287
- const { createServer: createViteServer } = await import("vite");
401
+ const { createServer: createViteServer } = await import(
402
+ /* @vite-ignore */
403
+ "vite"
404
+ );
1288
405
  vite = await createViteServer({
1289
406
  root,
1290
407
  server: { middlewareMode: true },
@@ -1310,7 +427,9 @@ async function createServer(config = {}) {
1310
427
  port,
1311
428
  isProduction: runtime.isProduction,
1312
429
  vite,
1313
- runtime
430
+ runtime,
431
+ locales,
432
+ ssrEntryPath: ssr?.ssrEntryPath
1314
433
  });
1315
434
  return { app, vite, runtime };
1316
435
  }
@@ -1333,6 +452,7 @@ export {
1333
452
  LruMap,
1334
453
  PrefetchedIntents,
1335
454
  Router,
455
+ SSR_PLACEHOLDERS,
1336
456
  buildUrl,
1337
457
  createPrefetchedIntentsFromDom,
1338
458
  createSSRApp,