@finesoft/front 0.1.47 → 0.1.49
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 +0 -1
- package/dist/index.d.mts +23 -49
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +231 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/locale-CvU-U6aP.mjs +0 -38
- package/dist/locale-CvU-U6aP.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { n as parseAcceptLanguage } from "./locale-CvU-U6aP.mjs";
|
|
2
1
|
import { Hono } from "hono";
|
|
3
2
|
//#region ../core/src/actions/types.ts
|
|
4
3
|
/**
|
|
@@ -38,22 +37,23 @@ function makeExternalUrlAction(url) {
|
|
|
38
37
|
}
|
|
39
38
|
//#endregion
|
|
40
39
|
//#region ../core/src/actions/dispatcher.ts
|
|
40
|
+
/** CompoundAction 最大递归展开深度 */
|
|
41
|
+
const MAX_COMPOUND_DEPTH = 32;
|
|
41
42
|
var ActionDispatcher = class {
|
|
42
43
|
handlers = /* @__PURE__ */ new Map();
|
|
43
|
-
wiredActions = /* @__PURE__ */ new Set();
|
|
44
44
|
/** 注册指定 kind 的 handler(防止重复注册) */
|
|
45
45
|
onAction(kind, handler) {
|
|
46
|
-
if (this.
|
|
46
|
+
if (this.handlers.has(kind)) {
|
|
47
47
|
console.warn(`[ActionDispatcher] kind="${kind}" already registered, skipping`);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
this.wiredActions.add(kind);
|
|
51
50
|
this.handlers.set(kind, handler);
|
|
52
51
|
}
|
|
53
|
-
/** 执行一个 Action(CompoundAction
|
|
54
|
-
async perform(action) {
|
|
52
|
+
/** 执行一个 Action(CompoundAction 递归展开,有深度限制) */
|
|
53
|
+
async perform(action, _depth = 0) {
|
|
55
54
|
if (isCompoundAction(action)) {
|
|
56
|
-
|
|
55
|
+
if (_depth >= MAX_COMPOUND_DEPTH) throw new Error(`[ActionDispatcher] CompoundAction recursion depth exceeded (max ${MAX_COMPOUND_DEPTH})`);
|
|
56
|
+
for (const subAction of action.actions) await this.perform(subAction, _depth + 1);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
const handler = this.handlers.get(action.kind);
|
|
@@ -87,6 +87,7 @@ var IntentDispatcher = class {
|
|
|
87
87
|
//#region ../core/src/dependencies/container.ts
|
|
88
88
|
var Container = class {
|
|
89
89
|
registrations = /* @__PURE__ */ new Map();
|
|
90
|
+
resolutionStack = /* @__PURE__ */ new Set();
|
|
90
91
|
/** 注册依赖(默认单例) */
|
|
91
92
|
register(key, factory, singleton = true) {
|
|
92
93
|
this.registrations.set(key, {
|
|
@@ -100,7 +101,15 @@ var Container = class {
|
|
|
100
101
|
const reg = this.registrations.get(key);
|
|
101
102
|
if (!reg) throw new Error(`[Container] No registration for key: "${key}"`);
|
|
102
103
|
if (reg.singleton) {
|
|
103
|
-
if (reg.instance === void 0)
|
|
104
|
+
if (reg.instance === void 0) {
|
|
105
|
+
if (this.resolutionStack.has(key)) throw new Error(`[Container] Circular dependency detected: ${[...this.resolutionStack, key].join(" → ")}`);
|
|
106
|
+
this.resolutionStack.add(key);
|
|
107
|
+
try {
|
|
108
|
+
reg.instance = reg.factory();
|
|
109
|
+
} finally {
|
|
110
|
+
this.resolutionStack.delete(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
104
113
|
return reg.instance;
|
|
105
114
|
}
|
|
106
115
|
return reg.factory();
|
|
@@ -212,20 +221,11 @@ const DEP_KEYS = {
|
|
|
212
221
|
LOGGER: "logger",
|
|
213
222
|
LOGGER_FACTORY: "loggerFactory",
|
|
214
223
|
NET: "net",
|
|
215
|
-
LOCALE: "locale",
|
|
216
224
|
STORAGE: "storage",
|
|
217
225
|
FEATURE_FLAGS: "featureFlags",
|
|
218
226
|
METRICS: "metrics",
|
|
219
227
|
FETCH: "fetch"
|
|
220
228
|
};
|
|
221
|
-
var DefaultLocale = class {
|
|
222
|
-
language = "en";
|
|
223
|
-
storefront = "us";
|
|
224
|
-
setActiveLocale(language, storefront) {
|
|
225
|
-
this.language = language;
|
|
226
|
-
this.storefront = storefront;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
229
|
var MemoryStorage = class {
|
|
230
230
|
store = /* @__PURE__ */ new Map();
|
|
231
231
|
get(key) {
|
|
@@ -264,16 +264,11 @@ var ConsoleMetrics = class {
|
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
266
|
function makeDependencies(container, options = {}) {
|
|
267
|
-
const { fetch: fetchFn = globalThis.fetch?.bind(globalThis),
|
|
267
|
+
const { fetch: fetchFn = globalThis.fetch?.bind(globalThis), featureFlags = {} } = options;
|
|
268
268
|
const loggerFactory = new ConsoleLoggerFactory();
|
|
269
269
|
container.register(DEP_KEYS.LOGGER_FACTORY, () => loggerFactory);
|
|
270
270
|
container.register(DEP_KEYS.LOGGER, () => loggerFactory.loggerFor("framework"));
|
|
271
271
|
container.register(DEP_KEYS.NET, () => ({ fetch: (url, opts) => fetchFn(url, opts) }));
|
|
272
|
-
container.register(DEP_KEYS.LOCALE, () => {
|
|
273
|
-
const locale = new DefaultLocale();
|
|
274
|
-
locale.setActiveLocale(language, storefront);
|
|
275
|
-
return locale;
|
|
276
|
-
});
|
|
277
272
|
container.register(DEP_KEYS.STORAGE, () => new MemoryStorage());
|
|
278
273
|
container.register(DEP_KEYS.FEATURE_FLAGS, () => new DefaultFeatureFlags(featureFlags));
|
|
279
274
|
container.register(DEP_KEYS.METRICS, () => new ConsoleMetrics());
|
|
@@ -293,6 +288,7 @@ var Router = class {
|
|
|
293
288
|
const regexStr = pattern.split(/(\/:[\w]+\??)/).map((segment) => {
|
|
294
289
|
const paramMatch = segment.match(/^\/:(\w+)(\?)?$/);
|
|
295
290
|
if (paramMatch) {
|
|
291
|
+
if (paramNames.includes(paramMatch[1])) throw new Error(`[Router] Duplicate parameter name ":${paramMatch[1]}" in pattern "${pattern}"`);
|
|
296
292
|
paramNames.push(paramMatch[1]);
|
|
297
293
|
return paramMatch[2] ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
298
294
|
}
|
|
@@ -311,8 +307,7 @@ var Router = class {
|
|
|
311
307
|
}
|
|
312
308
|
/** 解析 URL → RouteMatch */
|
|
313
309
|
resolve(urlOrPath) {
|
|
314
|
-
const path = this.
|
|
315
|
-
const queryParams = this.extractQueryParams(urlOrPath);
|
|
310
|
+
const { path, queryParams } = this.parseUrl(urlOrPath);
|
|
316
311
|
for (const route of this.routes) {
|
|
317
312
|
const match = path.match(route.regex);
|
|
318
313
|
if (match) {
|
|
@@ -340,23 +335,22 @@ var Router = class {
|
|
|
340
335
|
getRoutes() {
|
|
341
336
|
return this.routes.map((r) => `${r.pattern} → ${r.intentId}`);
|
|
342
337
|
}
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
return new URL(url, "http://localhost").pathname;
|
|
346
|
-
} catch {
|
|
347
|
-
return url.split("?")[0].split("#")[0];
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
extractQueryParams(url) {
|
|
338
|
+
parseUrl(url) {
|
|
351
339
|
try {
|
|
352
340
|
const parsed = new URL(url, "http://localhost");
|
|
353
341
|
const params = {};
|
|
354
342
|
parsed.searchParams.forEach((v, k) => {
|
|
355
343
|
params[k] = v;
|
|
356
344
|
});
|
|
357
|
-
return
|
|
345
|
+
return {
|
|
346
|
+
path: parsed.pathname,
|
|
347
|
+
queryParams: params
|
|
348
|
+
};
|
|
358
349
|
} catch {
|
|
359
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
path: url.split("?")[0].split("#")[0],
|
|
352
|
+
queryParams: {}
|
|
353
|
+
};
|
|
360
354
|
}
|
|
361
355
|
}
|
|
362
356
|
};
|
|
@@ -416,14 +410,33 @@ async function runAfterLoadGuards(guards, ctx) {
|
|
|
416
410
|
*
|
|
417
411
|
* 用作缓存 key:相同内容的对象始终产生相同字符串。
|
|
418
412
|
*/
|
|
419
|
-
|
|
413
|
+
/** WeakMap 缓存已序列化的对象,避免重复计算 */
|
|
414
|
+
const stringifyCache = /* @__PURE__ */ new WeakMap();
|
|
415
|
+
/** 最大递归深度 */
|
|
416
|
+
const MAX_DEPTH = 50;
|
|
417
|
+
function stableStringify(obj, _seen, _depth) {
|
|
420
418
|
if (obj === null || obj === void 0) return String(obj);
|
|
421
419
|
if (typeof obj !== "object") return JSON.stringify(obj);
|
|
420
|
+
const cached = stringifyCache.get(obj);
|
|
421
|
+
if (cached !== void 0) return cached;
|
|
422
|
+
const depth = _depth ?? 0;
|
|
423
|
+
if (depth > MAX_DEPTH) return "\"[Max Depth]\"";
|
|
422
424
|
const seen = _seen ?? /* @__PURE__ */ new Set();
|
|
423
425
|
if (seen.has(obj)) return "\"[Circular]\"";
|
|
424
426
|
seen.add(obj);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
+
let result;
|
|
428
|
+
if (Array.isArray(obj)) result = "[" + obj.map((v) => stableStringify(v, seen, depth + 1)).join(",") + "]";
|
|
429
|
+
else {
|
|
430
|
+
const keys = Object.keys(obj).sort();
|
|
431
|
+
const parts = [];
|
|
432
|
+
for (const k of keys) {
|
|
433
|
+
const v = obj[k];
|
|
434
|
+
if (v !== void 0) parts.push(JSON.stringify(k) + ":" + stableStringify(v, seen, depth + 1));
|
|
435
|
+
}
|
|
436
|
+
result = "{" + parts.join(",") + "}";
|
|
437
|
+
}
|
|
438
|
+
stringifyCache.set(obj, result);
|
|
439
|
+
return result;
|
|
427
440
|
}
|
|
428
441
|
//#endregion
|
|
429
442
|
//#region ../core/src/prefetched-intents/prefetched-intents.ts
|
|
@@ -482,6 +495,7 @@ var Framework = class Framework {
|
|
|
482
495
|
prefetchedIntents;
|
|
483
496
|
beforeGuards = [];
|
|
484
497
|
afterGuards = [];
|
|
498
|
+
_logger;
|
|
485
499
|
constructor(container, prefetchedIntents) {
|
|
486
500
|
this.container = container;
|
|
487
501
|
this.intentDispatcher = new IntentDispatcher();
|
|
@@ -497,9 +511,12 @@ var Framework = class Framework {
|
|
|
497
511
|
config.setupRoutes?.(fw.router);
|
|
498
512
|
return fw;
|
|
499
513
|
}
|
|
514
|
+
getLogger() {
|
|
515
|
+
return this._logger ??= this.container.resolve(DEP_KEYS.LOGGER);
|
|
516
|
+
}
|
|
500
517
|
/** 分发 Intent — 获取页面数据 */
|
|
501
518
|
async dispatch(intent) {
|
|
502
|
-
const logger = this.
|
|
519
|
+
const logger = this.getLogger();
|
|
503
520
|
const cached = this.prefetchedIntents.get(intent);
|
|
504
521
|
if (cached !== void 0) {
|
|
505
522
|
logger.debug(`[Framework] re-using prefetched intent response for: ${intent.id}`, intent.params);
|
|
@@ -510,7 +527,7 @@ var Framework = class Framework {
|
|
|
510
527
|
}
|
|
511
528
|
/** 执行 Action — 处理用户交互 */
|
|
512
529
|
async perform(action) {
|
|
513
|
-
this.
|
|
530
|
+
this.getLogger().debug(`[Framework] perform action: ${action.kind}`);
|
|
514
531
|
return this.actionDispatcher.perform(action);
|
|
515
532
|
}
|
|
516
533
|
/** 路由 URL — 将 URL 解析为 Intent + Action */
|
|
@@ -645,7 +662,12 @@ var HttpClient = class {
|
|
|
645
662
|
const body = await response.text().catch(() => void 0);
|
|
646
663
|
throw new HttpError(response.status, response.statusText, body);
|
|
647
664
|
}
|
|
648
|
-
|
|
665
|
+
try {
|
|
666
|
+
return await response.json();
|
|
667
|
+
} catch (e) {
|
|
668
|
+
if (e instanceof SyntaxError) throw new HttpError(response.status, "Invalid JSON response", await response.text().catch(() => void 0));
|
|
669
|
+
throw e;
|
|
670
|
+
}
|
|
649
671
|
}
|
|
650
672
|
/** 构建完整 URL — 子类可覆写以自定义 URL 拼接逻辑 */
|
|
651
673
|
buildUrl(path, params) {
|
|
@@ -771,6 +793,7 @@ var LruMap = class {
|
|
|
771
793
|
map = /* @__PURE__ */ new Map();
|
|
772
794
|
capacity;
|
|
773
795
|
constructor(capacity) {
|
|
796
|
+
if (capacity < 1) throw new Error(`[LruMap] capacity must be >= 1, got ${capacity}`);
|
|
774
797
|
this.capacity = capacity;
|
|
775
798
|
}
|
|
776
799
|
get(key) {
|
|
@@ -1300,17 +1323,15 @@ function createPrefetchedIntentsFromDom() {
|
|
|
1300
1323
|
* 自动执行 hydration 全流程。
|
|
1301
1324
|
*/
|
|
1302
1325
|
async function startBrowserApp(config) {
|
|
1303
|
-
const { bootstrap,
|
|
1326
|
+
const { bootstrap, mountId = "app", mount, callbacks } = config;
|
|
1304
1327
|
const prefetchedIntents = createPrefetchedIntentsFromDom();
|
|
1305
1328
|
const framework = Framework.create({ prefetchedIntents });
|
|
1306
1329
|
bootstrap(framework);
|
|
1307
1330
|
const log = framework.container.resolve(DEP_KEYS.LOGGER_FACTORY).loggerFor("browser");
|
|
1308
1331
|
const initialAction = framework.routeUrl(window.location.pathname + window.location.search);
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
locale
|
|
1313
|
-
});
|
|
1332
|
+
const target = document.getElementById(mountId);
|
|
1333
|
+
if (!target) throw new Error(`[startBrowserApp] Mount target not found: #${mountId}. Ensure your HTML has <div id="${mountId}"></div>.`);
|
|
1334
|
+
const updateApp = mount(target, { framework });
|
|
1314
1335
|
registerActionHandlers({
|
|
1315
1336
|
framework,
|
|
1316
1337
|
log,
|
|
@@ -1393,7 +1414,8 @@ async function ssrRender(options) {
|
|
|
1393
1414
|
head: result.head,
|
|
1394
1415
|
css: result.css,
|
|
1395
1416
|
serverData,
|
|
1396
|
-
renderMode: match?.renderMode
|
|
1417
|
+
renderMode: match?.renderMode,
|
|
1418
|
+
slots: result.slots
|
|
1397
1419
|
};
|
|
1398
1420
|
} finally {
|
|
1399
1421
|
framework.dispose();
|
|
@@ -1432,7 +1454,8 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
|
|
|
1432
1454
|
html: rendered.html,
|
|
1433
1455
|
head: rendered.head,
|
|
1434
1456
|
css: rendered.css,
|
|
1435
|
-
serverData: []
|
|
1457
|
+
serverData: [],
|
|
1458
|
+
slots: rendered.slots
|
|
1436
1459
|
};
|
|
1437
1460
|
}
|
|
1438
1461
|
}
|
|
@@ -1442,16 +1465,16 @@ async function handleMiddlewareResult(result, getErrorPage, renderApp, framework
|
|
|
1442
1465
|
/**
|
|
1443
1466
|
* 创建 render 函数
|
|
1444
1467
|
*
|
|
1445
|
-
* @returns `render(url,
|
|
1468
|
+
* @returns `render(url, ssrContext?)` — 供 @finesoft/server SSRModule 使用
|
|
1446
1469
|
*/
|
|
1447
1470
|
function createSSRRender(config) {
|
|
1448
1471
|
const { bootstrap, getErrorPage, renderApp, frameworkConfig } = config;
|
|
1449
|
-
return (url,
|
|
1472
|
+
return (url, ssrContext) => ssrRender({
|
|
1450
1473
|
url,
|
|
1451
1474
|
frameworkConfig: frameworkConfig ?? {},
|
|
1452
1475
|
bootstrap,
|
|
1453
1476
|
getErrorPage,
|
|
1454
|
-
renderApp: (page) => renderApp(page
|
|
1477
|
+
renderApp: (page) => renderApp(page),
|
|
1455
1478
|
ssrContext
|
|
1456
1479
|
});
|
|
1457
1480
|
}
|
|
@@ -1462,22 +1485,28 @@ function createSSRRender(config) {
|
|
|
1462
1485
|
*/
|
|
1463
1486
|
/** SSR HTML 模板占位符常量 */
|
|
1464
1487
|
const SSR_PLACEHOLDERS = {
|
|
1465
|
-
LANG: "<!--ssr-lang-->",
|
|
1466
1488
|
HEAD: "<!--ssr-head-->",
|
|
1467
1489
|
BODY: "<!--ssr-body-->",
|
|
1468
1490
|
DATA: "<!--ssr-data-->"
|
|
1469
1491
|
};
|
|
1492
|
+
/** 匹配所有 <!--ssr-xxx--> 占位符(含内置与自定义) */
|
|
1493
|
+
const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
|
|
1470
1494
|
function injectSSRContent(options) {
|
|
1471
|
-
const { template,
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1495
|
+
const { template, head, css, html, serializedData, slots } = options;
|
|
1496
|
+
const replacements = {
|
|
1497
|
+
head: `${head}\n${css ? `<style>${css}</style>` : ""}`,
|
|
1498
|
+
body: html,
|
|
1499
|
+
data: `<script id="serialized-server-data" type="application/json">${serializedData}<\/script>`,
|
|
1500
|
+
...slots
|
|
1501
|
+
};
|
|
1502
|
+
return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
|
|
1474
1503
|
}
|
|
1475
1504
|
/**
|
|
1476
|
-
* CSR 空壳注入 —
|
|
1505
|
+
* CSR 空壳注入 — 清空所有占位符
|
|
1477
1506
|
* 用于 renderMode === "csr" 的路由
|
|
1478
1507
|
*/
|
|
1479
|
-
function injectCSRShell(template
|
|
1480
|
-
return template.replace(
|
|
1508
|
+
function injectCSRShell(template) {
|
|
1509
|
+
return template.replace(PLACEHOLDER_REGEX, () => "");
|
|
1481
1510
|
}
|
|
1482
1511
|
//#endregion
|
|
1483
1512
|
//#region ../ssr/src/server-data.ts
|
|
@@ -1517,12 +1546,23 @@ async function dynamicImport(specifier) {
|
|
|
1517
1546
|
}
|
|
1518
1547
|
//#endregion
|
|
1519
1548
|
//#region ../server/src/proxy.ts
|
|
1549
|
+
/** 代理路径最大长度 */
|
|
1550
|
+
const MAX_PROXY_PATH_LENGTH = 2048;
|
|
1551
|
+
/** 代理响应最大体积(10 MB) */
|
|
1552
|
+
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
|
1520
1553
|
/**
|
|
1521
|
-
* 校验代理路径,防止 SSRF(协议相对 URL
|
|
1554
|
+
* 校验代理路径,防止 SSRF(协议相对 URL 绕过、编码绕过)。
|
|
1522
1555
|
* 返回规范化的路径,或 null 表示非法。
|
|
1523
1556
|
*/
|
|
1524
1557
|
function sanitizeProxyPath(raw) {
|
|
1558
|
+
if (raw.length > MAX_PROXY_PATH_LENGTH) return null;
|
|
1559
|
+
try {
|
|
1560
|
+
if (decodeURIComponent(raw) !== raw) return null;
|
|
1561
|
+
} catch {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1525
1564
|
if (raw.startsWith("//")) return null;
|
|
1565
|
+
if (!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
|
|
1526
1566
|
return raw.startsWith("/") ? raw : `/${raw}`;
|
|
1527
1567
|
}
|
|
1528
1568
|
/**
|
|
@@ -1531,7 +1571,10 @@ function sanitizeProxyPath(raw) {
|
|
|
1531
1571
|
*/
|
|
1532
1572
|
function validateConfig(config) {
|
|
1533
1573
|
if (!config.prefix.startsWith("/")) throw new Error(`[proxy] prefix must start with "/": "${config.prefix}"`);
|
|
1534
|
-
|
|
1574
|
+
const isHttps = config.target.startsWith("https://");
|
|
1575
|
+
const isHttp = config.target.startsWith("http://");
|
|
1576
|
+
if (!isHttps && !isHttp) throw new Error(`[proxy] target must start with "https://" or "http://": "${config.target}"`);
|
|
1577
|
+
if (isHttp) console.warn(`[proxy] ⚠ target "${config.target}" uses plain HTTP — traffic will not be encrypted. Use HTTPS in production to prevent data interception.`);
|
|
1535
1578
|
}
|
|
1536
1579
|
/**
|
|
1537
1580
|
* 注册声明式代理路由到 Hono app(运行时使用:dev / preview / createServer)
|
|
@@ -1545,18 +1588,24 @@ function registerProxyRoutes(app, configs) {
|
|
|
1545
1588
|
const subPath = sanitizeProxyPath(c.req.path.replace(config.prefix, ""));
|
|
1546
1589
|
if (!subPath) return c.text("Invalid path", 400);
|
|
1547
1590
|
const targetUrl = new URL(subPath, config.target);
|
|
1591
|
+
const expectedOrigin = new URL(config.target).origin;
|
|
1592
|
+
if (targetUrl.origin !== expectedOrigin) return c.text("Invalid proxy target", 400);
|
|
1548
1593
|
new URL(c.req.url).searchParams.forEach((v, k) => targetUrl.searchParams.set(k, v));
|
|
1549
1594
|
const headers = { ...config.headers };
|
|
1550
1595
|
if (config.auth) {
|
|
1551
|
-
const token = process.env[config.auth.envKey]
|
|
1552
|
-
if (token)
|
|
1596
|
+
const token = process.env[config.auth.envKey];
|
|
1597
|
+
if (!token) console.warn(`[Proxy ${config.prefix}] Auth env var "${config.auth.envKey}" is not set`);
|
|
1598
|
+
else headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
|
|
1553
1599
|
}
|
|
1554
1600
|
try {
|
|
1555
1601
|
const resp = await fetch(targetUrl.toString(), {
|
|
1556
1602
|
headers,
|
|
1557
1603
|
redirect: config.followRedirects ? "follow" : "manual"
|
|
1558
1604
|
});
|
|
1605
|
+
const contentLength = resp.headers.get("Content-Length");
|
|
1606
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
|
|
1559
1607
|
const body = await resp.text();
|
|
1608
|
+
if (body.length > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
|
|
1560
1609
|
const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
|
|
1561
1610
|
if (config.cache) respHeaders["Cache-Control"] = config.cache;
|
|
1562
1611
|
return c.newResponse(body, resp.status, respHeaders);
|
|
@@ -1578,7 +1627,10 @@ function generateProxyCode(configs) {
|
|
|
1578
1627
|
blocks.push(`
|
|
1579
1628
|
// ─── 框架声明式代理路由 ───
|
|
1580
1629
|
function _sanitizeProxyPath(raw) {
|
|
1630
|
+
if (raw.length > 2048) return null;
|
|
1631
|
+
try { if (decodeURIComponent(raw) !== raw) return null; } catch { return null; }
|
|
1581
1632
|
if (raw.startsWith("//")) return null;
|
|
1633
|
+
if (!/^[/\\w.\\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
|
|
1582
1634
|
return raw.startsWith("/") ? raw : "/" + raw;
|
|
1583
1635
|
}
|
|
1584
1636
|
`);
|
|
@@ -1596,6 +1648,7 @@ function _sanitizeProxyPath(raw) {
|
|
|
1596
1648
|
const _sub = _sanitizeProxyPath(c.req.path.replace(${JSON.stringify(config.prefix)}, ""));
|
|
1597
1649
|
if (!_sub) return c.text("Invalid path", 400);
|
|
1598
1650
|
const _target = new URL(_sub, ${JSON.stringify(config.target)});
|
|
1651
|
+
if (_target.origin !== ${JSON.stringify(new URL(config.target).origin)}) return c.text("Invalid proxy target", 400);
|
|
1599
1652
|
const _reqUrl = new URL(c.req.url);
|
|
1600
1653
|
_reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
|
|
1601
1654
|
const _headers = ${headersJson};${authCode}
|
|
@@ -1656,14 +1709,12 @@ const NODE_BUILTINS = [
|
|
|
1656
1709
|
/**
|
|
1657
1710
|
* 生成 SSR serverless/edge 入口源码
|
|
1658
1711
|
*
|
|
1659
|
-
* 内联
|
|
1712
|
+
* 内联 injectSSR 以避免
|
|
1660
1713
|
* @finesoft/front → @finesoft/server → vite-plugin → import("vite") 依赖链。
|
|
1661
1714
|
*/
|
|
1662
1715
|
function generateSSREntry(ctx, opts) {
|
|
1663
1716
|
const setupImport = ctx.setupPath ? `import _setupDefault from "./${ctx.setupPath}";` : ``;
|
|
1664
1717
|
const setupCall = ctx.setupPath ? `if (typeof _setupDefault === "function") await _setupDefault(app);` : ``;
|
|
1665
|
-
const locales = JSON.stringify(ctx.locales);
|
|
1666
|
-
const defaultLocale = JSON.stringify(ctx.defaultLocale);
|
|
1667
1718
|
const renderModes = JSON.stringify(ctx.renderModes ?? {});
|
|
1668
1719
|
const cacheImpl = opts.platformCache ? opts.platformCache : `
|
|
1669
1720
|
const ISR_CACHE_MAX = 1000;
|
|
@@ -1685,38 +1736,23 @@ import { render, serializeServerData } from "./${ctx.ssrEntry}";
|
|
|
1685
1736
|
${setupImport}
|
|
1686
1737
|
|
|
1687
1738
|
const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
|
|
1688
|
-
const LOCALES = ${locales};
|
|
1689
|
-
const DEFAULT_LOCALE = ${defaultLocale};
|
|
1690
1739
|
const RENDER_MODES = ${renderModes};
|
|
1691
1740
|
${cacheImpl}
|
|
1692
1741
|
|
|
1693
|
-
function
|
|
1694
|
-
if (!header) return DEFAULT_LOCALE;
|
|
1695
|
-
const langs = header.split(",").map(p => {
|
|
1696
|
-
const [l, q] = p.trim().split(";q=");
|
|
1697
|
-
return { l: l.trim().toLowerCase(), q: q ? (+q || 0) : 1 };
|
|
1698
|
-
}).sort((a, b) => b.q - a.q);
|
|
1699
|
-
for (const { l } of langs) {
|
|
1700
|
-
const prefix = l.split("-")[0];
|
|
1701
|
-
if (LOCALES.includes(prefix)) return prefix;
|
|
1702
|
-
}
|
|
1703
|
-
return DEFAULT_LOCALE;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
function injectSSR(t, locale, head, css, html, data) {
|
|
1742
|
+
function injectSSR(t, head, css, html, data) {
|
|
1707
1743
|
return t
|
|
1708
|
-
.replace(
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1744
|
+
.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_, name) => {
|
|
1745
|
+
const replacements = {
|
|
1746
|
+
head: head + "\\n<style>" + css + "</style>",
|
|
1747
|
+
body: html,
|
|
1748
|
+
data: '<script id="serialized-server-data" type="application/json">' + data + "<\/script>",
|
|
1749
|
+
};
|
|
1750
|
+
return replacements[name] ?? "";
|
|
1751
|
+
});
|
|
1712
1752
|
}
|
|
1713
1753
|
|
|
1714
|
-
function injectCSRShell(t
|
|
1715
|
-
return t
|
|
1716
|
-
.replace("<!--ssr-lang-->", locale)
|
|
1717
|
-
.replace("<!--ssr-head-->", "")
|
|
1718
|
-
.replace("<!--ssr-body-->", "")
|
|
1719
|
-
.replace("<!--ssr-data-->", "");
|
|
1754
|
+
function injectCSRShell(t) {
|
|
1755
|
+
return t.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
|
|
1720
1756
|
}
|
|
1721
1757
|
|
|
1722
1758
|
function matchRenderMode(url) {
|
|
@@ -1762,32 +1798,29 @@ app.get("*", async (c) => {
|
|
|
1762
1798
|
|
|
1763
1799
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
1764
1800
|
try {
|
|
1765
|
-
const locale = parseAcceptLanguage(c.req.header("accept-language"));
|
|
1766
|
-
|
|
1767
1801
|
// Vite 配置级别覆盖: CSR 直接返回空壳
|
|
1768
1802
|
const overrideMode = matchRenderMode(url);
|
|
1769
1803
|
if (overrideMode === "csr") {
|
|
1770
|
-
return c.html(injectCSRShell(TEMPLATE
|
|
1804
|
+
return c.html(injectCSRShell(TEMPLATE));
|
|
1771
1805
|
}
|
|
1772
1806
|
|
|
1773
|
-
// ISR
|
|
1774
|
-
const
|
|
1775
|
-
const cached = await platformCacheGet(_cacheKey);
|
|
1807
|
+
// ISR 缓存命中
|
|
1808
|
+
const cached = await platformCacheGet(url);
|
|
1776
1809
|
if (cached) return c.html(cached);
|
|
1777
1810
|
|
|
1778
|
-
const { html: appHtml, head, css, serverData, renderMode } = await render(url,
|
|
1811
|
+
const { html: appHtml, head, css, serverData, renderMode } = await render(url, { fetch: _createInternalFetch(_ssrDepth + 1) });
|
|
1779
1812
|
|
|
1780
1813
|
// 路由级 CSR
|
|
1781
1814
|
if (renderMode === "csr") {
|
|
1782
|
-
return c.html(injectCSRShell(TEMPLATE
|
|
1815
|
+
return c.html(injectCSRShell(TEMPLATE));
|
|
1783
1816
|
}
|
|
1784
1817
|
|
|
1785
1818
|
const serializedData = serializeServerData(serverData);
|
|
1786
|
-
const finalHtml = injectSSR(TEMPLATE,
|
|
1819
|
+
const finalHtml = injectSSR(TEMPLATE, head, css, appHtml, serializedData);
|
|
1787
1820
|
|
|
1788
1821
|
// Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
|
|
1789
1822
|
if (renderMode === "prerender" || overrideMode === "prerender") {
|
|
1790
|
-
await platformCacheSet(
|
|
1823
|
+
await platformCacheSet(url, finalHtml);
|
|
1791
1824
|
${opts.platformPrerenderResponseHook ?? ""}
|
|
1792
1825
|
}
|
|
1793
1826
|
|
|
@@ -1863,21 +1896,24 @@ async function prerenderRoutes(ctx) {
|
|
|
1863
1896
|
const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
|
|
1864
1897
|
const ssrModule = await dynamicImport(ssrPath);
|
|
1865
1898
|
const results = [];
|
|
1866
|
-
for (const
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1899
|
+
for (const url of prerenderPaths) try {
|
|
1900
|
+
const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
|
|
1901
|
+
const serializedData = ssrModule.serializeServerData(serverData);
|
|
1902
|
+
const finalHtml = ctx.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_match, name) => {
|
|
1903
|
+
return {
|
|
1904
|
+
head: head + "\n<style>" + css + "</style>",
|
|
1905
|
+
body: appHtml,
|
|
1906
|
+
data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
|
|
1907
|
+
}[name] ?? "";
|
|
1908
|
+
});
|
|
1909
|
+
results.push({
|
|
1910
|
+
url,
|
|
1911
|
+
html: finalHtml
|
|
1912
|
+
});
|
|
1913
|
+
} catch (e) {
|
|
1914
|
+
console.warn(` [prerender] Failed to render ${url}:`, e);
|
|
1879
1915
|
}
|
|
1880
|
-
if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes
|
|
1916
|
+
if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes)\n`);
|
|
1881
1917
|
return results;
|
|
1882
1918
|
}
|
|
1883
1919
|
//#endregion
|
|
@@ -2111,22 +2147,15 @@ function staticAdapter(opts = {}) {
|
|
|
2111
2147
|
const ssrModule = await dynamicImport(ssrPath);
|
|
2112
2148
|
ctx.copyStaticAssets(outputDir, { excludeHtml: true });
|
|
2113
2149
|
const { paths: routePaths, defs: routeDefs } = await extractRoutesWithModes(ctx, opts);
|
|
2114
|
-
|
|
2115
|
-
for (const
|
|
2116
|
-
const
|
|
2117
|
-
allUrls.push(url);
|
|
2118
|
-
}
|
|
2119
|
-
console.log(` Pre-rendering ${allUrls.length} pages (${routePaths.length} routes × ${ctx.locales.length} locales)...\n`);
|
|
2120
|
-
for (const url of allUrls) try {
|
|
2121
|
-
const locale = inferLocale(url, ctx.locales, ctx.defaultLocale);
|
|
2122
|
-
const routeDef = routeDefs.find((r) => r.path === stripLocalePrefix(url, ctx.locales));
|
|
2123
|
-
const mode = resolveRenderMode(stripLocalePrefix(url, ctx.locales), routeDef?.renderMode, ctx.renderModes);
|
|
2150
|
+
console.log(` Pre-rendering ${routePaths.length} pages...\n`);
|
|
2151
|
+
for (const url of routePaths) try {
|
|
2152
|
+
const mode = resolveRenderMode(url, routeDefs.find((r) => r.path === url)?.renderMode, ctx.renderModes);
|
|
2124
2153
|
let finalHtml;
|
|
2125
|
-
if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml
|
|
2154
|
+
if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml);
|
|
2126
2155
|
else {
|
|
2127
|
-
const { html: appHtml, head, css, serverData } = await ssrModule.render(url
|
|
2156
|
+
const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
|
|
2128
2157
|
const serializedData = ssrModule.serializeServerData(serverData);
|
|
2129
|
-
finalHtml = injectSSRForStatic(ctx.templateHtml,
|
|
2158
|
+
finalHtml = injectSSRForStatic(ctx.templateHtml, head, css, appHtml, serializedData);
|
|
2130
2159
|
}
|
|
2131
2160
|
const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
|
|
2132
2161
|
fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
|
|
@@ -2181,28 +2210,19 @@ async function extractRoutesWithModes(ctx, opts) {
|
|
|
2181
2210
|
defs
|
|
2182
2211
|
};
|
|
2183
2212
|
}
|
|
2184
|
-
/**
|
|
2185
|
-
function
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
return template.replace(
|
|
2213
|
+
/** 内联 SSR 注入 */
|
|
2214
|
+
function injectSSRForStatic(template, head, css, html, serializedData) {
|
|
2215
|
+
const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
|
|
2216
|
+
const replacements = {
|
|
2217
|
+
head: head + "\n<style>" + css + "</style>",
|
|
2218
|
+
body: html,
|
|
2219
|
+
data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
|
|
2220
|
+
};
|
|
2221
|
+
return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
|
|
2193
2222
|
}
|
|
2194
2223
|
/** CSR 空壳注入 */
|
|
2195
|
-
function injectCSRShellForStatic(template
|
|
2196
|
-
return template.replace(
|
|
2197
|
-
}
|
|
2198
|
-
/** 从 URL 去除 locale 前缀,还原路由路径 */
|
|
2199
|
-
function stripLocalePrefix(url, locales) {
|
|
2200
|
-
const segments = url.split("/").filter(Boolean);
|
|
2201
|
-
if (segments.length > 0 && locales.includes(segments[0])) {
|
|
2202
|
-
const rest = segments.slice(1).join("/");
|
|
2203
|
-
return rest ? `/${rest}` : "/";
|
|
2204
|
-
}
|
|
2205
|
-
return url;
|
|
2224
|
+
function injectCSRShellForStatic(template) {
|
|
2225
|
+
return template.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
|
|
2206
2226
|
}
|
|
2207
2227
|
/** 解析最终渲染模式:Vite 配置覆盖 > 路由级 > 默认 "ssr" */
|
|
2208
2228
|
function resolveRenderMode(routePath, routeRenderMode, renderModes) {
|
|
@@ -2402,7 +2422,7 @@ function matchRenderModeOverride(url, renderModes) {
|
|
|
2402
2422
|
return null;
|
|
2403
2423
|
}
|
|
2404
2424
|
function createSSRApp(options) {
|
|
2405
|
-
const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule,
|
|
2425
|
+
const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes } = options;
|
|
2406
2426
|
const app = new Hono();
|
|
2407
2427
|
/** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
|
|
2408
2428
|
const ISR_CACHE_MAX = 1e3;
|
|
@@ -2446,28 +2466,28 @@ function createSSRApp(options) {
|
|
|
2446
2466
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
2447
2467
|
try {
|
|
2448
2468
|
const template = await readTemplate(url);
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2469
|
+
const ssrMod = await loadSSRModule();
|
|
2470
|
+
if (typeof ssrMod.render !== "function" || typeof ssrMod.serializeServerData !== "function") throw new Error("[SSR] Module missing required exports: render, serializeServerData");
|
|
2471
|
+
const { render, serializeServerData } = ssrMod;
|
|
2451
2472
|
const overrideMode = matchRenderModeOverride(url, renderModes);
|
|
2452
|
-
if (overrideMode === "csr") return c.html(injectCSRShell(template
|
|
2453
|
-
const
|
|
2454
|
-
const cached = isrCache.get(cacheKey);
|
|
2473
|
+
if (overrideMode === "csr") return c.html(injectCSRShell(template));
|
|
2474
|
+
const cached = isrCache.get(url);
|
|
2455
2475
|
if (cached) return c.html(cached);
|
|
2456
2476
|
const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
|
|
2457
2477
|
const ssrContext = { request: c.req.raw };
|
|
2458
2478
|
if (requestFetch) ssrContext.fetch = requestFetch;
|
|
2459
|
-
const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect } = await render(url,
|
|
2479
|
+
const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots } = await render(url, ssrContext);
|
|
2460
2480
|
if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
|
|
2461
|
-
if (renderMode === "csr") return c.html(injectCSRShell(template
|
|
2481
|
+
if (renderMode === "csr") return c.html(injectCSRShell(template));
|
|
2462
2482
|
const finalHtml = injectSSRContent({
|
|
2463
2483
|
template,
|
|
2464
|
-
locale,
|
|
2465
2484
|
head,
|
|
2466
2485
|
css,
|
|
2467
2486
|
html: appHtml,
|
|
2468
|
-
serializedData: serializeServerData(serverData)
|
|
2487
|
+
serializedData: serializeServerData(serverData),
|
|
2488
|
+
slots
|
|
2469
2489
|
});
|
|
2470
|
-
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(
|
|
2490
|
+
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
|
|
2471
2491
|
return c.html(finalHtml);
|
|
2472
2492
|
} catch (e) {
|
|
2473
2493
|
if (!isProduction && vite) vite.ssrFixStacktrace(e);
|
|
@@ -2517,7 +2537,7 @@ async function resolveRoot(importMetaUrl, levelsUp = 0) {
|
|
|
2517
2537
|
* 支持 Node.js (dev HMR + prod)、Deno、Bun、Vercel。
|
|
2518
2538
|
*/
|
|
2519
2539
|
async function startServer(options) {
|
|
2520
|
-
const { app, root, port = 3e3, isProduction, vite, routes,
|
|
2540
|
+
const { app, root, port = 3e3, isProduction, vite, routes, ssrEntryPath } = options;
|
|
2521
2541
|
const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
|
|
2522
2542
|
function printStartupBanner() {
|
|
2523
2543
|
const lines = [`\n Server running at http://localhost:${port}\n`];
|
|
@@ -2526,9 +2546,8 @@ async function startServer(options) {
|
|
|
2526
2546
|
for (const r of routes) lines.push(` ${r}`);
|
|
2527
2547
|
lines.push("");
|
|
2528
2548
|
}
|
|
2529
|
-
if (locales && locales.length > 0) lines.push(` Locales: ${locales.join(", ")}`);
|
|
2530
2549
|
if (ssrEntryPath) lines.push(` SSR Entry: ${ssrEntryPath}`);
|
|
2531
|
-
if (
|
|
2550
|
+
if (ssrEntryPath) lines.push("");
|
|
2532
2551
|
console.log(lines.join("\n"));
|
|
2533
2552
|
}
|
|
2534
2553
|
if (isVercel) return { vite };
|
|
@@ -2587,21 +2606,22 @@ async function startServer(options) {
|
|
|
2587
2606
|
* @example
|
|
2588
2607
|
* ```ts
|
|
2589
2608
|
* const { app } = await createServer({
|
|
2590
|
-
* locales: ["zh", "en"],
|
|
2591
2609
|
* setup: (app) => registerProxies(app),
|
|
2592
2610
|
* });
|
|
2593
2611
|
* export { app };
|
|
2594
2612
|
* ```
|
|
2595
2613
|
*/
|
|
2596
2614
|
async function createServer(config = {}) {
|
|
2597
|
-
const { root: rootOverride,
|
|
2615
|
+
const { root: rootOverride, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
|
|
2598
2616
|
const root = rootOverride ?? process.cwd();
|
|
2599
2617
|
const { existsSync } = await dynamicImport("node:fs");
|
|
2600
2618
|
const envPath = (await dynamicImport("node:path")).resolve(root, ".env");
|
|
2601
2619
|
if (existsSync(envPath)) try {
|
|
2602
2620
|
const { config: dotenvConfig } = await dynamicImport("dotenv");
|
|
2603
2621
|
dotenvConfig({ path: envPath });
|
|
2604
|
-
} catch {
|
|
2622
|
+
} catch (e) {
|
|
2623
|
+
console.warn(`[Server] Failed to load .env: ${e.message}`);
|
|
2624
|
+
}
|
|
2605
2625
|
const runtime = detectRuntime();
|
|
2606
2626
|
let vite;
|
|
2607
2627
|
if (!runtime.isProduction && !runtime.isVercel) {
|
|
@@ -2619,8 +2639,6 @@ async function createServer(config = {}) {
|
|
|
2619
2639
|
root,
|
|
2620
2640
|
vite,
|
|
2621
2641
|
isProduction: runtime.isProduction,
|
|
2622
|
-
supportedLocales: locales,
|
|
2623
|
-
defaultLocale,
|
|
2624
2642
|
parentFetch: app.fetch.bind(app),
|
|
2625
2643
|
...ssr
|
|
2626
2644
|
});
|
|
@@ -2632,7 +2650,6 @@ async function createServer(config = {}) {
|
|
|
2632
2650
|
isProduction: runtime.isProduction,
|
|
2633
2651
|
vite,
|
|
2634
2652
|
runtime,
|
|
2635
|
-
locales,
|
|
2636
2653
|
ssrEntryPath: ssr?.ssrEntryPath
|
|
2637
2654
|
});
|
|
2638
2655
|
return {
|
|
@@ -2642,6 +2659,35 @@ async function createServer(config = {}) {
|
|
|
2642
2659
|
};
|
|
2643
2660
|
}
|
|
2644
2661
|
//#endregion
|
|
2662
|
+
//#region ../server/src/locale.ts
|
|
2663
|
+
/**
|
|
2664
|
+
* Accept-Language 解析
|
|
2665
|
+
*/
|
|
2666
|
+
/** Accept-Language 头最大长度 */
|
|
2667
|
+
const MAX_HEADER_LENGTH = 1024;
|
|
2668
|
+
/** 最大解析语言条目数 */
|
|
2669
|
+
const MAX_LANG_ENTRIES = 50;
|
|
2670
|
+
function parseAcceptLanguage(header, supported, fallback) {
|
|
2671
|
+
const effectiveSupported = supported ?? ["zh", "en"];
|
|
2672
|
+
const effectiveFallback = fallback ?? effectiveSupported[0] ?? "en";
|
|
2673
|
+
if (!header || header.length > MAX_HEADER_LENGTH) return effectiveFallback;
|
|
2674
|
+
const parts = header.split(",");
|
|
2675
|
+
if (parts.length > MAX_LANG_ENTRIES) return effectiveFallback;
|
|
2676
|
+
const langs = parts.map((part) => {
|
|
2677
|
+
const [lang, q] = part.trim().split(";q=");
|
|
2678
|
+
const qVal = q ? parseFloat(q) : 1;
|
|
2679
|
+
return {
|
|
2680
|
+
lang: lang.trim().toLowerCase(),
|
|
2681
|
+
q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
|
|
2682
|
+
};
|
|
2683
|
+
}).sort((a, b) => b.q - a.q);
|
|
2684
|
+
for (const { lang } of langs) {
|
|
2685
|
+
const prefix = lang.split("-")[0];
|
|
2686
|
+
if (effectiveSupported.includes(prefix)) return prefix;
|
|
2687
|
+
}
|
|
2688
|
+
return effectiveFallback;
|
|
2689
|
+
}
|
|
2690
|
+
//#endregion
|
|
2645
2691
|
//#region ../server/src/vite-plugin.ts
|
|
2646
2692
|
/**
|
|
2647
2693
|
* finesoftFrontViteConfig — Vite 插件
|
|
@@ -2711,11 +2757,12 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2711
2757
|
}
|
|
2712
2758
|
const cssUrls = [];
|
|
2713
2759
|
const visited = /* @__PURE__ */ new Set();
|
|
2714
|
-
function walk(mod) {
|
|
2760
|
+
function walk(mod, depth = 0) {
|
|
2761
|
+
if (depth > 100) return;
|
|
2715
2762
|
if (!mod?.url || visited.has(mod.url)) return;
|
|
2716
2763
|
visited.add(mod.url);
|
|
2717
2764
|
if (CSS_EXTENSIONS.test(mod.url) && !mod.url.includes(".svelte")) cssUrls.push(mod.url);
|
|
2718
|
-
if (mod.importedModules) for (const imported of mod.importedModules) walk(imported);
|
|
2765
|
+
if (mod.importedModules) for (const imported of mod.importedModules) walk(imported, depth + 1);
|
|
2719
2766
|
}
|
|
2720
2767
|
const browserMod = await server.moduleGraph.getModuleByUrl(browserEntry);
|
|
2721
2768
|
if (browserMod) walk(browserMod);
|
|
@@ -2749,8 +2796,6 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2749
2796
|
vite: server,
|
|
2750
2797
|
isProduction: false,
|
|
2751
2798
|
ssrEntryPath: "/" + ssrEntry,
|
|
2752
|
-
supportedLocales: options.locales,
|
|
2753
|
-
defaultLocale: options.defaultLocale,
|
|
2754
2799
|
parentFetch: app.fetch.bind(app),
|
|
2755
2800
|
renderModes: options.renderModes
|
|
2756
2801
|
});
|
|
@@ -2767,10 +2812,17 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2767
2812
|
const path = await dynamicImport("node:path");
|
|
2768
2813
|
const { pathToFileURL } = await dynamicImport("node:url");
|
|
2769
2814
|
const { Hono: HonoClass } = await dynamicImport("hono");
|
|
2770
|
-
const { parseAcceptLanguage } = await import("./locale-CvU-U6aP.mjs").then((n) => n.t);
|
|
2771
2815
|
const { getRequestListener } = await dynamicImport("@hono/node-server");
|
|
2772
2816
|
const app = new HonoClass();
|
|
2817
|
+
const ISR_CACHE_MAX = 1e3;
|
|
2773
2818
|
const isrCache = /* @__PURE__ */ new Map();
|
|
2819
|
+
function isrSet(key, val) {
|
|
2820
|
+
if (isrCache.size >= ISR_CACHE_MAX) {
|
|
2821
|
+
const first = isrCache.keys().next().value;
|
|
2822
|
+
if (first !== void 0) isrCache.delete(first);
|
|
2823
|
+
}
|
|
2824
|
+
isrCache.set(key, val);
|
|
2825
|
+
}
|
|
2774
2826
|
if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
|
|
2775
2827
|
if (typeof options.setup === "function") await options.setup(app);
|
|
2776
2828
|
else if (typeof options.setup === "string") try {
|
|
@@ -2788,23 +2840,20 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2788
2840
|
if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
|
|
2789
2841
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
2790
2842
|
try {
|
|
2791
|
-
const locale = parseAcceptLanguage(c.req.header("accept-language"), options.locales, options.defaultLocale);
|
|
2792
2843
|
const overrideMode = matchRenderModeConfig(url, options.renderModes);
|
|
2793
|
-
if (overrideMode === "csr") return c.html(injectCSRShell(template
|
|
2794
|
-
const
|
|
2795
|
-
const cached = isrCache.get(cacheKey);
|
|
2844
|
+
if (overrideMode === "csr") return c.html(injectCSRShell(template));
|
|
2845
|
+
const cached = isrCache.get(url);
|
|
2796
2846
|
if (cached) return c.html(cached);
|
|
2797
|
-
const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url,
|
|
2798
|
-
if (renderMode === "csr") return c.html(injectCSRShell(template
|
|
2847
|
+
const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
|
|
2848
|
+
if (renderMode === "csr") return c.html(injectCSRShell(template));
|
|
2799
2849
|
const finalHtml = injectSSRContent({
|
|
2800
2850
|
template,
|
|
2801
|
-
locale,
|
|
2802
2851
|
head,
|
|
2803
2852
|
css,
|
|
2804
2853
|
html: appHtml,
|
|
2805
2854
|
serializedData: ssrModule.serializeServerData(serverData)
|
|
2806
2855
|
});
|
|
2807
|
-
if (renderMode === "prerender" || overrideMode === "prerender")
|
|
2856
|
+
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
|
|
2808
2857
|
return c.html(finalHtml);
|
|
2809
2858
|
} catch (e) {
|
|
2810
2859
|
console.error("[SSR Preview Error]", e);
|
|
@@ -2851,16 +2900,12 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2851
2900
|
}
|
|
2852
2901
|
if (options.adapter) {
|
|
2853
2902
|
const adapter = resolveAdapter(options.adapter);
|
|
2854
|
-
const locales = options.locales ?? ["zh", "en"];
|
|
2855
|
-
const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
|
|
2856
2903
|
const templateHtml = fs.readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
|
|
2857
2904
|
const ctx = {
|
|
2858
2905
|
root,
|
|
2859
2906
|
ssrEntry,
|
|
2860
2907
|
setupPath: typeof options.setup === "string" ? options.setup : void 0,
|
|
2861
2908
|
bootstrapEntry: options.bootstrapEntry,
|
|
2862
|
-
locales,
|
|
2863
|
-
defaultLocale,
|
|
2864
2909
|
templateHtml,
|
|
2865
2910
|
renderModes: options.renderModes,
|
|
2866
2911
|
proxies: options.proxies,
|