@finesoft/front 0.1.74 → 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/README.md +2 -411
- package/dist/browser.d.mts +2 -0
- package/dist/browser.mjs +1 -0
- package/dist/index.d.mts +2 -1248
- package/dist/index.mjs +54 -3557
- package/dist/server-data-DGbiKzMS.d.mts +1249 -0
- package/dist/start-app-BdXBCcor.mjs +2 -0
- package/package.json +11 -3
package/dist/index.mjs
CHANGED
|
@@ -1,2383 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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(
|
|
2404
|
-
if (_target.origin !== ${JSON.stringify(new URL(
|
|
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 = ${
|
|
19
|
+
const _headers = ${i};${s}
|
|
2408
20
|
try {
|
|
2409
|
-
const _resp = await fetch(_target.toString(), { headers: _headers, 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) > ${
|
|
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 > ${
|
|
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 (${
|
|
33
|
+
if (${a}) _rh["Cache-Control"] = ${a};
|
|
2422
34
|
return c.newResponse(_body, _resp.status, _rh);
|
|
2423
35
|
} catch (_e) {
|
|
2424
|
-
console.error("[Proxy ${
|
|
36
|
+
console.error("[Proxy ${n.prefix}]", _e);
|
|
2425
37
|
return c.json({ error: "Proxy request failed" }, 502);
|
|
2426
38
|
}
|
|
2427
|
-
}`;
|
|
2428
|
-
|
|
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
|
-
${
|
|
2497
|
-
import { render, serializeServerData } from "./${
|
|
2498
|
-
${
|
|
54
|
+
${t.platformImport}
|
|
55
|
+
import { render, serializeServerData } from "./${e.ssrEntry}";
|
|
56
|
+
${n}
|
|
2499
57
|
|
|
2500
|
-
const TEMPLATE = ${JSON.stringify(
|
|
2501
|
-
const RENDER_MODES = ${
|
|
2502
|
-
const DEFAULT_LOCALE = ${JSON.stringify(
|
|
2503
|
-
${
|
|
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
|
-
${
|
|
2555
|
-
${
|
|
2556
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
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(
|
|
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
|
-
|
|
3572
|
-
|
|
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
|