@finesoft/front 0.1.53 → 0.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -0
- package/dist/index.d.mts +392 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +489 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -85,9 +85,10 @@ var IntentDispatcher = class {
|
|
|
85
85
|
};
|
|
86
86
|
//#endregion
|
|
87
87
|
//#region ../core/src/dependencies/container.ts
|
|
88
|
-
var Container = class {
|
|
88
|
+
var Container = class Container {
|
|
89
89
|
registrations = /* @__PURE__ */ new Map();
|
|
90
90
|
resolutionStack = /* @__PURE__ */ new Set();
|
|
91
|
+
parent;
|
|
91
92
|
/** 注册依赖(默认单例) */
|
|
92
93
|
register(key, factory, singleton = true) {
|
|
93
94
|
this.registrations.set(key, {
|
|
@@ -96,10 +97,13 @@ var Container = class {
|
|
|
96
97
|
});
|
|
97
98
|
return this;
|
|
98
99
|
}
|
|
99
|
-
/** 解析依赖 */
|
|
100
|
+
/** 解析依赖 — 当前容器未注册时回退到 parent */
|
|
100
101
|
resolve(key) {
|
|
101
102
|
const reg = this.registrations.get(key);
|
|
102
|
-
if (!reg)
|
|
103
|
+
if (!reg) {
|
|
104
|
+
if (this.parent) return this.parent.resolve(key);
|
|
105
|
+
throw new Error(`[Container] No registration for key: "${key}"`);
|
|
106
|
+
}
|
|
103
107
|
if (reg.singleton) {
|
|
104
108
|
if (reg.instance === void 0) {
|
|
105
109
|
if (this.resolutionStack.has(key)) throw new Error(`[Container] Circular dependency detected: ${[...this.resolutionStack, key].join(" → ")}`);
|
|
@@ -114,9 +118,20 @@ var Container = class {
|
|
|
114
118
|
}
|
|
115
119
|
return reg.factory();
|
|
116
120
|
}
|
|
117
|
-
/**
|
|
121
|
+
/** 检查是否已注册(含 parent) */
|
|
118
122
|
has(key) {
|
|
119
|
-
return this.registrations.has(key);
|
|
123
|
+
return this.registrations.has(key) || (this.parent?.has(key) ?? false);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 创建子容器(请求级 scope)
|
|
127
|
+
*
|
|
128
|
+
* 子容器可覆写父容器的依赖(如每请求的 locale、user),
|
|
129
|
+
* 未覆写的 key 自动回退到父容器解析。
|
|
130
|
+
*/
|
|
131
|
+
createScope() {
|
|
132
|
+
const child = new Container();
|
|
133
|
+
child.parent = this;
|
|
134
|
+
return child;
|
|
120
135
|
}
|
|
121
136
|
/** 销毁容器,清除所有缓存 */
|
|
122
137
|
dispose() {
|
|
@@ -125,6 +140,101 @@ var Container = class {
|
|
|
125
140
|
}
|
|
126
141
|
};
|
|
127
142
|
//#endregion
|
|
143
|
+
//#region ../core/src/i18n/locale.ts
|
|
144
|
+
/** RTL 语言列表 */
|
|
145
|
+
const RTL_LANGUAGES = new Set([
|
|
146
|
+
"ar",
|
|
147
|
+
"arc",
|
|
148
|
+
"dv",
|
|
149
|
+
"fa",
|
|
150
|
+
"ha",
|
|
151
|
+
"he",
|
|
152
|
+
"khw",
|
|
153
|
+
"ks",
|
|
154
|
+
"ku",
|
|
155
|
+
"ps",
|
|
156
|
+
"ur",
|
|
157
|
+
"yi"
|
|
158
|
+
]);
|
|
159
|
+
/** 检测语言是否为 RTL */
|
|
160
|
+
function isRtl(language) {
|
|
161
|
+
const primary = language.split("-")[0].toLowerCase();
|
|
162
|
+
return RTL_LANGUAGES.has(primary);
|
|
163
|
+
}
|
|
164
|
+
/** 获取文本方向 */
|
|
165
|
+
function getTextDirection(language) {
|
|
166
|
+
return isRtl(language) ? "rtl" : "ltr";
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 从语言代码生成 HTML lang/dir 属性
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* getLocaleAttributes("ar-SA") // { lang: "ar-SA", dir: "rtl" }
|
|
174
|
+
* getLocaleAttributes("en-US") // { lang: "en-US", dir: "ltr" }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
function getLocaleAttributes(language) {
|
|
178
|
+
return {
|
|
179
|
+
lang: language,
|
|
180
|
+
dir: getTextDirection(language)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 构建 LocaleInfo
|
|
185
|
+
*
|
|
186
|
+
* @param language - 语言代码(如 "zh-Hans")
|
|
187
|
+
* @param region - 地区代码(如 "CN"),可选
|
|
188
|
+
*/
|
|
189
|
+
function makeLocaleInfo(language, region) {
|
|
190
|
+
return {
|
|
191
|
+
language,
|
|
192
|
+
region,
|
|
193
|
+
bcp47: region ? `${language}-${region}` : language,
|
|
194
|
+
dir: getTextDirection(language)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 将 locale 属性应用到 `<html>` 元素
|
|
199
|
+
*
|
|
200
|
+
* 服务端渲染时可用于字符串拼接,浏览器端直接操作 DOM。
|
|
201
|
+
*/
|
|
202
|
+
function setHtmlLocaleAttributes(attrs) {
|
|
203
|
+
document.documentElement.lang = attrs.lang;
|
|
204
|
+
document.documentElement.dir = attrs.dir;
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region ../core/src/logger/composite.ts
|
|
208
|
+
var CompositeLoggerFactory = class {
|
|
209
|
+
constructor(factories) {
|
|
210
|
+
this.factories = factories;
|
|
211
|
+
}
|
|
212
|
+
loggerFor(name) {
|
|
213
|
+
return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var CompositeLogger = class {
|
|
217
|
+
constructor(loggers) {
|
|
218
|
+
this.loggers = loggers;
|
|
219
|
+
}
|
|
220
|
+
debug(...args) {
|
|
221
|
+
return this.callAll("debug", args);
|
|
222
|
+
}
|
|
223
|
+
info(...args) {
|
|
224
|
+
return this.callAll("info", args);
|
|
225
|
+
}
|
|
226
|
+
warn(...args) {
|
|
227
|
+
return this.callAll("warn", args);
|
|
228
|
+
}
|
|
229
|
+
error(...args) {
|
|
230
|
+
return this.callAll("error", args);
|
|
231
|
+
}
|
|
232
|
+
callAll(method, args) {
|
|
233
|
+
for (const logger of this.loggers) logger[method](...args);
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
//#endregion
|
|
128
238
|
//#region ../core/src/logger/base.ts
|
|
129
239
|
var BaseLogger = class {
|
|
130
240
|
category;
|
|
@@ -213,6 +323,111 @@ var ConsoleLoggerFactory = class {
|
|
|
213
323
|
}
|
|
214
324
|
};
|
|
215
325
|
//#endregion
|
|
326
|
+
//#region ../core/src/logger/reporting.ts
|
|
327
|
+
/**
|
|
328
|
+
* ReportingLogger — 上报型日志实现
|
|
329
|
+
*
|
|
330
|
+
* 将 warn/error 级别日志转发到外部监控服务(如 Sentry、Datadog)。
|
|
331
|
+
* 用户通过 ReportCallback 注入上报逻辑,框架不直接依赖任何第三方 SDK。
|
|
332
|
+
*/
|
|
333
|
+
const LEVEL_PRIORITY = {
|
|
334
|
+
debug: 0,
|
|
335
|
+
info: 1,
|
|
336
|
+
warn: 2,
|
|
337
|
+
error: 3
|
|
338
|
+
};
|
|
339
|
+
var ReportingLogger = class extends BaseLogger {
|
|
340
|
+
minPriority;
|
|
341
|
+
report;
|
|
342
|
+
constructor(category, options) {
|
|
343
|
+
super(category);
|
|
344
|
+
this.minPriority = LEVEL_PRIORITY[options.minLevel ?? "warn"];
|
|
345
|
+
this.report = options.report;
|
|
346
|
+
}
|
|
347
|
+
debug(...args) {
|
|
348
|
+
this.maybeReport("debug", args);
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
info(...args) {
|
|
352
|
+
this.maybeReport("info", args);
|
|
353
|
+
return "";
|
|
354
|
+
}
|
|
355
|
+
warn(...args) {
|
|
356
|
+
this.maybeReport("warn", args);
|
|
357
|
+
return "";
|
|
358
|
+
}
|
|
359
|
+
error(...args) {
|
|
360
|
+
this.maybeReport("error", args);
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
maybeReport(level, args) {
|
|
364
|
+
if (LEVEL_PRIORITY[level] >= this.minPriority) this.report(level, this.category, args);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var ReportingLoggerFactory = class {
|
|
368
|
+
options;
|
|
369
|
+
constructor(options) {
|
|
370
|
+
this.options = options;
|
|
371
|
+
}
|
|
372
|
+
loggerFor(category) {
|
|
373
|
+
return new ReportingLogger(category, this.options);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region ../core/src/metrics/console-recorder.ts
|
|
378
|
+
var ConsoleEventRecorder = class {
|
|
379
|
+
prefix;
|
|
380
|
+
constructor(prefix = "Metrics") {
|
|
381
|
+
this.prefix = prefix;
|
|
382
|
+
}
|
|
383
|
+
record(type, fields) {
|
|
384
|
+
console.info(`[${this.prefix}:${type}]`, fields ?? "");
|
|
385
|
+
}
|
|
386
|
+
async flush() {}
|
|
387
|
+
destroy() {}
|
|
388
|
+
};
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region ../core/src/utils/platform.ts
|
|
391
|
+
/**
|
|
392
|
+
* 从 User-Agent 字符串解析平台信息
|
|
393
|
+
*
|
|
394
|
+
* @param ua - User-Agent 字符串(默认取 navigator.userAgent)
|
|
395
|
+
*/
|
|
396
|
+
function detectPlatform(ua) {
|
|
397
|
+
const agent = ua ?? (typeof navigator !== "undefined" ? navigator.userAgent : "");
|
|
398
|
+
const lower = agent.toLowerCase();
|
|
399
|
+
return {
|
|
400
|
+
os: detectOS(lower),
|
|
401
|
+
browser: detectBrowser(lower),
|
|
402
|
+
engine: detectEngine(lower),
|
|
403
|
+
isMobile: /mobile|android|iphone|ipad|ipod/i.test(agent),
|
|
404
|
+
isTouch: typeof navigator !== "undefined" && "maxTouchPoints" in navigator ? navigator.maxTouchPoints > 0 : false
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function detectOS(ua) {
|
|
408
|
+
if (/iphone|ipad|ipod/.test(ua)) return "ios";
|
|
409
|
+
if (/android/.test(ua)) return "android";
|
|
410
|
+
if (/macintosh|mac os x/.test(ua)) return "macos";
|
|
411
|
+
if (/windows/.test(ua)) return "windows";
|
|
412
|
+
if (/linux/.test(ua)) return "linux";
|
|
413
|
+
return "unknown";
|
|
414
|
+
}
|
|
415
|
+
function detectBrowser(ua) {
|
|
416
|
+
if (/edg\//.test(ua)) return "edge";
|
|
417
|
+
if (/opr\/|opera/.test(ua)) return "opera";
|
|
418
|
+
if (/samsungbrowser/.test(ua)) return "samsung";
|
|
419
|
+
if (/chrome|crios/.test(ua) && !/edg\//.test(ua)) return "chrome";
|
|
420
|
+
if (/firefox|fxios/.test(ua)) return "firefox";
|
|
421
|
+
if (/safari/.test(ua) && !/chrome/.test(ua)) return "safari";
|
|
422
|
+
return "unknown";
|
|
423
|
+
}
|
|
424
|
+
function detectEngine(ua) {
|
|
425
|
+
if (/applewebkit/.test(ua) && !/chrome/.test(ua)) return "webkit";
|
|
426
|
+
if (/applewebkit/.test(ua) && /chrome/.test(ua)) return "blink";
|
|
427
|
+
if (/gecko\//.test(ua)) return "gecko";
|
|
428
|
+
return "unknown";
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
216
431
|
//#region ../core/src/dependencies/make-dependencies.ts
|
|
217
432
|
/**
|
|
218
433
|
* 依赖工厂 — 创建所有基础依赖
|
|
@@ -224,7 +439,10 @@ const DEP_KEYS = {
|
|
|
224
439
|
STORAGE: "storage",
|
|
225
440
|
FEATURE_FLAGS: "featureFlags",
|
|
226
441
|
METRICS: "metrics",
|
|
227
|
-
FETCH: "fetch"
|
|
442
|
+
FETCH: "fetch",
|
|
443
|
+
EVENT_RECORDER: "eventRecorder",
|
|
444
|
+
LOCALE: "locale",
|
|
445
|
+
PLATFORM: "platform"
|
|
228
446
|
};
|
|
229
447
|
var MemoryStorage = class {
|
|
230
448
|
store = /* @__PURE__ */ new Map();
|
|
@@ -240,38 +458,67 @@ var MemoryStorage = class {
|
|
|
240
458
|
};
|
|
241
459
|
var DefaultFeatureFlags = class {
|
|
242
460
|
flags;
|
|
461
|
+
providers = [];
|
|
243
462
|
constructor(flags = {}) {
|
|
244
463
|
this.flags = flags;
|
|
245
464
|
}
|
|
465
|
+
/** 注册外部 provider(如远程配置、A/B 测试 SDK) */
|
|
466
|
+
addProvider(provider) {
|
|
467
|
+
this.providers.push(provider);
|
|
468
|
+
}
|
|
246
469
|
isEnabled(key) {
|
|
470
|
+
for (let i = this.providers.length - 1; i >= 0; i--) if (this.providers[i].isEnabled(key)) return true;
|
|
247
471
|
return this.flags[key] === true;
|
|
248
472
|
}
|
|
249
473
|
getString(key) {
|
|
474
|
+
for (let i = this.providers.length - 1; i >= 0; i--) {
|
|
475
|
+
const result = this.providers[i].getString?.(key);
|
|
476
|
+
if (result !== void 0) return result;
|
|
477
|
+
}
|
|
250
478
|
const v = this.flags[key];
|
|
251
479
|
return typeof v === "string" ? v : void 0;
|
|
252
480
|
}
|
|
253
481
|
getNumber(key) {
|
|
482
|
+
for (let i = this.providers.length - 1; i >= 0; i--) {
|
|
483
|
+
const result = this.providers[i].getNumber?.(key);
|
|
484
|
+
if (result !== void 0) return result;
|
|
485
|
+
}
|
|
254
486
|
const v = this.flags[key];
|
|
255
487
|
return typeof v === "number" ? v : void 0;
|
|
256
488
|
}
|
|
257
489
|
};
|
|
258
490
|
var ConsoleMetrics = class {
|
|
491
|
+
record(type, fields) {
|
|
492
|
+
console.info(`[Metrics:${type}]`, fields ?? "");
|
|
493
|
+
}
|
|
259
494
|
recordPageView(page, fields) {
|
|
260
|
-
|
|
495
|
+
this.record("PageView", {
|
|
496
|
+
page,
|
|
497
|
+
...fields
|
|
498
|
+
});
|
|
261
499
|
}
|
|
262
500
|
recordEvent(name, fields) {
|
|
263
|
-
|
|
501
|
+
this.record("Event", {
|
|
502
|
+
name,
|
|
503
|
+
...fields
|
|
504
|
+
});
|
|
264
505
|
}
|
|
265
506
|
};
|
|
266
507
|
function makeDependencies(container, options = {}) {
|
|
267
|
-
const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {} } = options;
|
|
268
|
-
const
|
|
508
|
+
const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {}, featureFlagsProviders = [], reportCallback, eventRecorder, locale, platform } = options;
|
|
509
|
+
const consoleFactory = new ConsoleLoggerFactory();
|
|
510
|
+
const loggerFactory = reportCallback ? new CompositeLoggerFactory([consoleFactory, new ReportingLoggerFactory({ report: reportCallback })]) : consoleFactory;
|
|
269
511
|
container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
|
|
270
512
|
container.register(DEP_KEYS.LOGGER, () => loggerFactory.loggerFor("framework"));
|
|
271
513
|
container.register(DEP_KEYS.NET, () => ({ fetch: (url, opts) => fetchFn(url, opts) }));
|
|
272
514
|
container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
|
|
273
|
-
|
|
515
|
+
const flags = new DefaultFeatureFlags(featureFlags);
|
|
516
|
+
for (const provider of featureFlagsProviders) flags.addProvider(provider);
|
|
517
|
+
container.register(DEP_KEYS.FEATURE_FLAGS, () => flags);
|
|
274
518
|
container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
|
|
519
|
+
container.register(DEP_KEYS.EVENT_RECORDER, () => eventRecorder ?? new ConsoleEventRecorder());
|
|
520
|
+
if (locale) container.register(DEP_KEYS.LOCALE, () => getLocaleAttributes(locale));
|
|
521
|
+
container.register(DEP_KEYS.PLATFORM, () => platform ?? detectPlatform(typeof navigator !== "undefined" ? navigator.userAgent : void 0));
|
|
275
522
|
container.register(DEP_KEYS.FETCH, () => fetchFn);
|
|
276
523
|
}
|
|
277
524
|
//#endregion
|
|
@@ -355,37 +602,6 @@ var Router = class {
|
|
|
355
602
|
}
|
|
356
603
|
};
|
|
357
604
|
//#endregion
|
|
358
|
-
//#region ../core/src/logger/composite.ts
|
|
359
|
-
var CompositeLoggerFactory = class {
|
|
360
|
-
constructor(factories) {
|
|
361
|
-
this.factories = factories;
|
|
362
|
-
}
|
|
363
|
-
loggerFor(name) {
|
|
364
|
-
return new CompositeLogger(this.factories.map((f) => f.loggerFor(name)));
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
var CompositeLogger = class {
|
|
368
|
-
constructor(loggers) {
|
|
369
|
-
this.loggers = loggers;
|
|
370
|
-
}
|
|
371
|
-
debug(...args) {
|
|
372
|
-
return this.callAll("debug", args);
|
|
373
|
-
}
|
|
374
|
-
info(...args) {
|
|
375
|
-
return this.callAll("info", args);
|
|
376
|
-
}
|
|
377
|
-
warn(...args) {
|
|
378
|
-
return this.callAll("warn", args);
|
|
379
|
-
}
|
|
380
|
-
error(...args) {
|
|
381
|
-
return this.callAll("error", args);
|
|
382
|
-
}
|
|
383
|
-
callAll(method, args) {
|
|
384
|
-
for (const logger of this.loggers) logger[method](...args);
|
|
385
|
-
return "";
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
//#endregion
|
|
389
605
|
//#region ../core/src/middleware/pipeline.ts
|
|
390
606
|
/** 执行 beforeLoad 守卫链 */
|
|
391
607
|
async function runBeforeLoadGuards(guards, ctx) {
|
|
@@ -541,6 +757,14 @@ var Framework = class Framework {
|
|
|
541
757
|
title: page.title
|
|
542
758
|
});
|
|
543
759
|
}
|
|
760
|
+
/** 获取 locale 信息(如果已配置) */
|
|
761
|
+
getLocale() {
|
|
762
|
+
return this.container.has(DEP_KEYS.LOCALE) ? this.container.resolve(DEP_KEYS.LOCALE) : void 0;
|
|
763
|
+
}
|
|
764
|
+
/** 获取平台信息 */
|
|
765
|
+
getPlatform() {
|
|
766
|
+
return this.container.resolve(DEP_KEYS.PLATFORM);
|
|
767
|
+
}
|
|
544
768
|
/** 注册 Action 处理器 */
|
|
545
769
|
onAction(kind, handler) {
|
|
546
770
|
this.actionDispatcher.onAction(kind, handler);
|
|
@@ -606,10 +830,24 @@ var HttpClient = class {
|
|
|
606
830
|
baseUrl;
|
|
607
831
|
defaultHeaders;
|
|
608
832
|
fetchFn;
|
|
833
|
+
requestInterceptors;
|
|
834
|
+
responseInterceptors;
|
|
609
835
|
constructor(config) {
|
|
610
836
|
this.baseUrl = config.baseUrl;
|
|
611
837
|
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
612
838
|
this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
839
|
+
this.requestInterceptors = [...config.requestInterceptors ?? []];
|
|
840
|
+
this.responseInterceptors = [...config.responseInterceptors ?? []];
|
|
841
|
+
}
|
|
842
|
+
/** 动态添加请求拦截器 */
|
|
843
|
+
useRequestInterceptor(interceptor) {
|
|
844
|
+
this.requestInterceptors.push(interceptor);
|
|
845
|
+
return this;
|
|
846
|
+
}
|
|
847
|
+
/** 动态添加响应拦截器 */
|
|
848
|
+
useResponseInterceptor(interceptor) {
|
|
849
|
+
this.responseInterceptors.push(interceptor);
|
|
850
|
+
return this;
|
|
613
851
|
}
|
|
614
852
|
/** GET 请求,返回解析后的 JSON */
|
|
615
853
|
async get(path, params) {
|
|
@@ -649,7 +887,7 @@ var HttpClient = class {
|
|
|
649
887
|
...this.defaultHeaders,
|
|
650
888
|
...options?.headers
|
|
651
889
|
};
|
|
652
|
-
|
|
890
|
+
let init = {
|
|
653
891
|
method,
|
|
654
892
|
headers
|
|
655
893
|
};
|
|
@@ -657,7 +895,9 @@ var HttpClient = class {
|
|
|
657
895
|
headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
|
|
658
896
|
init.body = JSON.stringify(options.body);
|
|
659
897
|
}
|
|
660
|
-
const
|
|
898
|
+
for (const interceptor of this.requestInterceptors) init = await interceptor(url, init);
|
|
899
|
+
let response = await this.fetchFn(url, init);
|
|
900
|
+
for (const interceptor of this.responseInterceptors) response = await interceptor(response, url);
|
|
661
901
|
if (!response.ok) {
|
|
662
902
|
const body = await response.text().catch(() => void 0);
|
|
663
903
|
throw new HttpError(response.status, response.statusText, body);
|
|
@@ -834,6 +1074,21 @@ function isNone(value) {
|
|
|
834
1074
|
return value === null || value === void 0;
|
|
835
1075
|
}
|
|
836
1076
|
//#endregion
|
|
1077
|
+
//#region ../core/src/utils/pwa.ts
|
|
1078
|
+
/**
|
|
1079
|
+
* 检测 PWA display mode
|
|
1080
|
+
*
|
|
1081
|
+
* - `standalone`: 已安装的 PWA(通过 Add to Home Screen)
|
|
1082
|
+
* - `twa`: Trusted Web Activity(Android 原生壳)
|
|
1083
|
+
* - `browser`: 普通浏览器标签页
|
|
1084
|
+
*/
|
|
1085
|
+
function getPWADisplayMode() {
|
|
1086
|
+
if (typeof window === "undefined") return "browser";
|
|
1087
|
+
if (document.referrer.startsWith("android-app://")) return "twa";
|
|
1088
|
+
if (window.matchMedia("(display-mode: standalone)").matches || "standalone" in window.navigator && window.navigator.standalone === true) return "standalone";
|
|
1089
|
+
return "browser";
|
|
1090
|
+
}
|
|
1091
|
+
//#endregion
|
|
837
1092
|
//#region ../core/src/utils/url.ts
|
|
838
1093
|
/**
|
|
839
1094
|
* URL 工具函数
|
|
@@ -953,6 +1208,171 @@ function deny(status = 403, message = "Forbidden") {
|
|
|
953
1208
|
};
|
|
954
1209
|
}
|
|
955
1210
|
//#endregion
|
|
1211
|
+
//#region ../core/src/metrics/composite-recorder.ts
|
|
1212
|
+
var CompositeEventRecorder = class {
|
|
1213
|
+
recorders;
|
|
1214
|
+
constructor(recorders) {
|
|
1215
|
+
this.recorders = recorders;
|
|
1216
|
+
}
|
|
1217
|
+
record(type, fields) {
|
|
1218
|
+
for (const recorder of this.recorders) recorder.record(type, fields);
|
|
1219
|
+
}
|
|
1220
|
+
async flush() {
|
|
1221
|
+
await Promise.all(this.recorders.map((r) => r.flush?.()));
|
|
1222
|
+
}
|
|
1223
|
+
destroy() {
|
|
1224
|
+
for (const recorder of this.recorders) recorder.destroy?.();
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
//#endregion
|
|
1228
|
+
//#region ../core/src/metrics/impression-observer.ts
|
|
1229
|
+
var IntersectionImpressionObserver = class {
|
|
1230
|
+
observer;
|
|
1231
|
+
tracked = /* @__PURE__ */ new Map();
|
|
1232
|
+
captured = [];
|
|
1233
|
+
minDuration;
|
|
1234
|
+
constructor(options = {}) {
|
|
1235
|
+
this.minDuration = options.minVisibleDuration ?? 1e3;
|
|
1236
|
+
this.observer = new IntersectionObserver((entries) => {
|
|
1237
|
+
const now = Date.now();
|
|
1238
|
+
for (const entry of entries) {
|
|
1239
|
+
const tracked = this.tracked.get(entry.target);
|
|
1240
|
+
if (!tracked) continue;
|
|
1241
|
+
if (entry.isIntersecting) {
|
|
1242
|
+
if (tracked.visibleSince === null) tracked.visibleSince = now;
|
|
1243
|
+
} else if (tracked.visibleSince !== null) {
|
|
1244
|
+
if (now - tracked.visibleSince >= this.minDuration) this.captured.push({
|
|
1245
|
+
id: tracked.id,
|
|
1246
|
+
timestamp: tracked.visibleSince,
|
|
1247
|
+
metadata: tracked.metadata
|
|
1248
|
+
});
|
|
1249
|
+
tracked.visibleSince = null;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}, { threshold: options.threshold ?? .5 });
|
|
1253
|
+
}
|
|
1254
|
+
observe(element, id, metadata) {
|
|
1255
|
+
this.tracked.set(element, {
|
|
1256
|
+
id,
|
|
1257
|
+
metadata,
|
|
1258
|
+
visibleSince: null
|
|
1259
|
+
});
|
|
1260
|
+
this.observer.observe(element);
|
|
1261
|
+
}
|
|
1262
|
+
unobserve(element) {
|
|
1263
|
+
this.observer.unobserve(element);
|
|
1264
|
+
this.tracked.delete(element);
|
|
1265
|
+
}
|
|
1266
|
+
consume() {
|
|
1267
|
+
const now = Date.now();
|
|
1268
|
+
for (const [, tracked] of this.tracked) if (tracked.visibleSince !== null) {
|
|
1269
|
+
if (now - tracked.visibleSince >= this.minDuration) {
|
|
1270
|
+
this.captured.push({
|
|
1271
|
+
id: tracked.id,
|
|
1272
|
+
timestamp: tracked.visibleSince,
|
|
1273
|
+
metadata: tracked.metadata
|
|
1274
|
+
});
|
|
1275
|
+
tracked.visibleSince = now;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return this.captured.splice(0);
|
|
1279
|
+
}
|
|
1280
|
+
destroy() {
|
|
1281
|
+
this.observer.disconnect();
|
|
1282
|
+
this.tracked.clear();
|
|
1283
|
+
this.captured.length = 0;
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region ../core/src/metrics/void-recorder.ts
|
|
1288
|
+
var VoidEventRecorder = class {
|
|
1289
|
+
record() {}
|
|
1290
|
+
async flush() {}
|
|
1291
|
+
destroy() {}
|
|
1292
|
+
};
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region ../core/src/metrics/with-fields-recorder.ts
|
|
1295
|
+
var WithFieldsRecorder = class {
|
|
1296
|
+
constructor(inner, providers) {
|
|
1297
|
+
this.inner = inner;
|
|
1298
|
+
this.providers = providers;
|
|
1299
|
+
}
|
|
1300
|
+
record(type, fields) {
|
|
1301
|
+
let merged = {};
|
|
1302
|
+
for (const provider of this.providers) Object.assign(merged, provider.getFields());
|
|
1303
|
+
if (fields) Object.assign(merged, fields);
|
|
1304
|
+
this.inner.record(type, merged);
|
|
1305
|
+
}
|
|
1306
|
+
async flush() {
|
|
1307
|
+
return this.inner.flush?.();
|
|
1308
|
+
}
|
|
1309
|
+
destroy() {
|
|
1310
|
+
this.inner.destroy?.();
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
//#endregion
|
|
1314
|
+
//#region ../core/src/i18n/interpolate.ts
|
|
1315
|
+
/**
|
|
1316
|
+
* ICU 消息格式插值
|
|
1317
|
+
*
|
|
1318
|
+
* 支持 `{name}` 占位符替换和基础复数规则。
|
|
1319
|
+
*/
|
|
1320
|
+
/** 将 `{key}` 占位符替换为 values 中的对应值 */
|
|
1321
|
+
function interpolate(template, values) {
|
|
1322
|
+
if (!values) return template;
|
|
1323
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
1324
|
+
const val = values[key];
|
|
1325
|
+
return val !== void 0 ? String(val) : `{${key}}`;
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* 英语复数规则(默认)
|
|
1330
|
+
* 0 → other, 1 → one, 2+ → other
|
|
1331
|
+
*/
|
|
1332
|
+
function englishPlural(count) {
|
|
1333
|
+
return count === 1 ? "one" : "other";
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* 解析带复数后缀的翻译 key
|
|
1337
|
+
*
|
|
1338
|
+
* 约定: `key.one`, `key.other`, `key.zero`, etc.
|
|
1339
|
+
*/
|
|
1340
|
+
function resolvePluralKey(key, category) {
|
|
1341
|
+
return `${key}.${category}`;
|
|
1342
|
+
}
|
|
1343
|
+
//#endregion
|
|
1344
|
+
//#region ../core/src/i18n/translator.ts
|
|
1345
|
+
/**
|
|
1346
|
+
* SimpleTranslator — 默认翻译器实现
|
|
1347
|
+
*
|
|
1348
|
+
* 从扁平的 key→string 映射提供翻译,支持 ICU 插值和复数规则。
|
|
1349
|
+
*/
|
|
1350
|
+
var SimpleTranslator = class {
|
|
1351
|
+
locale;
|
|
1352
|
+
messages;
|
|
1353
|
+
pluralRule;
|
|
1354
|
+
fallback;
|
|
1355
|
+
constructor(options) {
|
|
1356
|
+
this.locale = options.locale;
|
|
1357
|
+
this.messages = options.messages;
|
|
1358
|
+
this.pluralRule = options.pluralRule ?? englishPlural;
|
|
1359
|
+
this.fallback = options.fallback ?? ((key) => key);
|
|
1360
|
+
}
|
|
1361
|
+
t(key, values) {
|
|
1362
|
+
const template = this.messages[key];
|
|
1363
|
+
if (template === void 0) return this.fallback(key);
|
|
1364
|
+
return interpolate(template, values);
|
|
1365
|
+
}
|
|
1366
|
+
plural(key, count, values) {
|
|
1367
|
+
const pluralKey = resolvePluralKey(key, this.pluralRule(count));
|
|
1368
|
+
const mergedValues = {
|
|
1369
|
+
count,
|
|
1370
|
+
...values
|
|
1371
|
+
};
|
|
1372
|
+
return this.t(pluralKey, mergedValues);
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
//#endregion
|
|
956
1376
|
//#region ../browser/src/action-handlers/external-url-action.ts
|
|
957
1377
|
function registerExternalUrlHandler(deps) {
|
|
958
1378
|
const { framework, log } = deps;
|
|
@@ -1395,11 +1815,20 @@ function createPrefetchedIntentsFromDom() {
|
|
|
1395
1815
|
* 自动执行 hydration 全流程。
|
|
1396
1816
|
*/
|
|
1397
1817
|
async function startBrowserApp(config) {
|
|
1398
|
-
const { bootstrap, mountId = "app", mount, callbacks } = config;
|
|
1818
|
+
const { bootstrap, mountId = "app", mount, callbacks, onBeforeStart, onAfterStart, frameworkConfig } = config;
|
|
1399
1819
|
const prefetchedIntents = createPrefetchedIntentsFromDom();
|
|
1400
|
-
const framework = Framework.create({
|
|
1820
|
+
const framework = Framework.create({
|
|
1821
|
+
...frameworkConfig,
|
|
1822
|
+
prefetchedIntents
|
|
1823
|
+
});
|
|
1401
1824
|
bootstrap(framework);
|
|
1402
1825
|
const log = framework.container.resolve(DEP_KEYS.LOGGER_FACTORY).loggerFor("browser");
|
|
1826
|
+
const locale = framework.getLocale();
|
|
1827
|
+
if (locale) {
|
|
1828
|
+
setHtmlLocaleAttributes(locale);
|
|
1829
|
+
log.debug("[startBrowserApp] Applied locale attributes:", locale);
|
|
1830
|
+
}
|
|
1831
|
+
await onBeforeStart?.(framework);
|
|
1403
1832
|
const initialAction = framework.routeUrl(window.location.pathname + window.location.search);
|
|
1404
1833
|
const target = document.getElementById(mountId);
|
|
1405
1834
|
if (!target) throw new Error(`[startBrowserApp] Mount target not found: #${mountId}. Ensure your HTML has <div id="${mountId}"></div>.`);
|
|
@@ -1416,6 +1845,7 @@ async function startBrowserApp(config) {
|
|
|
1416
1845
|
page: Promise.reject(/* @__PURE__ */ new Error("404")),
|
|
1417
1846
|
isFirstPage: true
|
|
1418
1847
|
});
|
|
1848
|
+
await onAfterStart?.(framework);
|
|
1419
1849
|
}
|
|
1420
1850
|
//#endregion
|
|
1421
1851
|
//#region ../ssr/src/render.ts
|
|
@@ -1428,7 +1858,7 @@ async function startBrowserApp(config) {
|
|
|
1428
1858
|
* 4. 调用应用层提供的渲染函数
|
|
1429
1859
|
*/
|
|
1430
1860
|
async function ssrRender(options) {
|
|
1431
|
-
const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext } = options;
|
|
1861
|
+
const { url, frameworkConfig, bootstrap, getErrorPage, renderApp, ssrContext, resolveLocale } = options;
|
|
1432
1862
|
const mergedConfig = ssrContext?.fetch ? {
|
|
1433
1863
|
...frameworkConfig,
|
|
1434
1864
|
fetch: ssrContext.fetch
|
|
@@ -1481,13 +1911,15 @@ async function ssrRender(options) {
|
|
|
1481
1911
|
}
|
|
1482
1912
|
} else page = getErrorPage(404, "Page not found");
|
|
1483
1913
|
const result = await renderApp(page, framework);
|
|
1914
|
+
const locale = resolveLocale?.(url, ssrContext?.request) ?? framework.getLocale();
|
|
1484
1915
|
return {
|
|
1485
1916
|
html: result.html,
|
|
1486
1917
|
head: result.head,
|
|
1487
1918
|
css: result.css,
|
|
1488
1919
|
serverData,
|
|
1489
1920
|
renderMode: match?.renderMode,
|
|
1490
|
-
slots: result.slots
|
|
1921
|
+
slots: result.slots,
|
|
1922
|
+
locale
|
|
1491
1923
|
};
|
|
1492
1924
|
} finally {
|
|
1493
1925
|
framework.dispose();
|
|
@@ -1564,14 +1996,18 @@ const SSR_PLACEHOLDERS = {
|
|
|
1564
1996
|
/** 匹配所有 <!--ssr-xxx--> 占位符(含内置与自定义) */
|
|
1565
1997
|
const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
|
|
1566
1998
|
function injectSSRContent(options) {
|
|
1567
|
-
const { template, head, css, html, serializedData, slots } = options;
|
|
1999
|
+
const { template, head, css, html, serializedData, slots, locale } = options;
|
|
1568
2000
|
const replacements = {
|
|
1569
2001
|
head: `${head}\n${css ? `<style>${css}</style>` : ""}`,
|
|
1570
2002
|
body: html,
|
|
1571
2003
|
data: `<script id="serialized-server-data" type="application/json">${serializedData}<\/script>`,
|
|
1572
2004
|
...slots
|
|
1573
2005
|
};
|
|
1574
|
-
|
|
2006
|
+
let result = template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
|
|
2007
|
+
if (locale) result = result.replace(/(<html)([^>]*)(>)/i, (match, open, attrs, close) => {
|
|
2008
|
+
return `${open}${attrs.replace(/\s+lang="[^"]*"/gi, "").replace(/\s+dir="[^"]*"/gi, "")} lang="${locale.lang}" dir="${locale.dir}"${close}`;
|
|
2009
|
+
});
|
|
2010
|
+
return result;
|
|
1575
2011
|
}
|
|
1576
2012
|
/**
|
|
1577
2013
|
* CSR 空壳注入 — 清空所有占位符
|
|
@@ -2443,13 +2879,13 @@ function autoAdapter() {
|
|
|
2443
2879
|
return {
|
|
2444
2880
|
name: "auto",
|
|
2445
2881
|
async build(ctx) {
|
|
2446
|
-
const detected = detectPlatform();
|
|
2882
|
+
const detected = detectPlatform$1();
|
|
2447
2883
|
console.log(` [auto] Detected platform: ${detected}\n`);
|
|
2448
2884
|
return resolveAdapter(detected).build(ctx);
|
|
2449
2885
|
}
|
|
2450
2886
|
};
|
|
2451
2887
|
}
|
|
2452
|
-
function detectPlatform() {
|
|
2888
|
+
function detectPlatform$1() {
|
|
2453
2889
|
if (process.env.VERCEL) return "vercel";
|
|
2454
2890
|
if (process.env.CF_PAGES) return "cloudflare";
|
|
2455
2891
|
if (process.env.NETLIFY) return "netlify";
|
|
@@ -3020,6 +3456,6 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
3020
3456
|
};
|
|
3021
3457
|
}
|
|
3022
3458
|
//#endregion
|
|
3023
|
-
export { ACTION_KINDS, ActionDispatcher, BaseController, BaseLogger, CompositeLogger, CompositeLoggerFactory, ConsoleLogger, ConsoleLoggerFactory, Container, DEP_KEYS, Framework, History, HttpClient, HttpError, IntentDispatcher, LruMap, PrefetchedIntents, Router, SSR_PLACEHOLDERS, autoAdapter, buildUrl, cloudflareAdapter, createBrowserContext, createPrefetchedIntentsFromDom, createSSRApp, createSSRRender, createServer, createServerContext, defineRoutes, deny, deserializeServerData, detectRuntime, finesoftFrontViteConfig, generateProxyCode, generateUuid, getBaseUrl, injectCSRShell, injectSSRContent, isCompoundAction, isExternalUrlAction, isFlowAction, isNone, isSome, makeDependencies, makeExternalUrlAction, makeFlowAction, mapEach, netlifyAdapter, next, nodeAdapter, parseAcceptLanguage, pipe, pipeAsync, redirect, registerActionHandlers, registerExternalUrlHandler, registerFlowActionHandler, registerProxyRoutes, removeHost, removeQueryParams, removeScheme, resetFilterCache, resolveAdapter, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
|
|
3459
|
+
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, resolvePluralKey, resolveRoot, rewrite, runAfterLoadGuards, runBeforeLoadGuards, serializeServerData, setHtmlLocaleAttributes, shouldLog, ssrRender, stableStringify, startBrowserApp, startServer, staticAdapter, tryScroll, vercelAdapter };
|
|
3024
3460
|
|
|
3025
3461
|
//# sourceMappingURL=index.mjs.map
|