@finesoft/front 0.1.47 → 0.1.48
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 +22 -48
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +226 -185
- 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
|
/**
|
|
@@ -1545,18 +1585,24 @@ function registerProxyRoutes(app, configs) {
|
|
|
1545
1585
|
const subPath = sanitizeProxyPath(c.req.path.replace(config.prefix, ""));
|
|
1546
1586
|
if (!subPath) return c.text("Invalid path", 400);
|
|
1547
1587
|
const targetUrl = new URL(subPath, config.target);
|
|
1588
|
+
const expectedOrigin = new URL(config.target).origin;
|
|
1589
|
+
if (targetUrl.origin !== expectedOrigin) return c.text("Invalid proxy target", 400);
|
|
1548
1590
|
new URL(c.req.url).searchParams.forEach((v, k) => targetUrl.searchParams.set(k, v));
|
|
1549
1591
|
const headers = { ...config.headers };
|
|
1550
1592
|
if (config.auth) {
|
|
1551
|
-
const token = process.env[config.auth.envKey]
|
|
1552
|
-
if (token)
|
|
1593
|
+
const token = process.env[config.auth.envKey];
|
|
1594
|
+
if (!token) console.warn(`[Proxy ${config.prefix}] Auth env var "${config.auth.envKey}" is not set`);
|
|
1595
|
+
else headers.Authorization = config.auth.type === "bearer" ? `Bearer ${token}` : `Basic ${token}`;
|
|
1553
1596
|
}
|
|
1554
1597
|
try {
|
|
1555
1598
|
const resp = await fetch(targetUrl.toString(), {
|
|
1556
1599
|
headers,
|
|
1557
1600
|
redirect: config.followRedirects ? "follow" : "manual"
|
|
1558
1601
|
});
|
|
1602
|
+
const contentLength = resp.headers.get("Content-Length");
|
|
1603
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
|
|
1559
1604
|
const body = await resp.text();
|
|
1605
|
+
if (body.length > MAX_RESPONSE_SIZE) return c.text("Proxy response too large", 502);
|
|
1560
1606
|
const respHeaders = { "Content-Type": resp.headers.get("Content-Type") ?? "application/json" };
|
|
1561
1607
|
if (config.cache) respHeaders["Cache-Control"] = config.cache;
|
|
1562
1608
|
return c.newResponse(body, resp.status, respHeaders);
|
|
@@ -1578,7 +1624,10 @@ function generateProxyCode(configs) {
|
|
|
1578
1624
|
blocks.push(`
|
|
1579
1625
|
// ─── 框架声明式代理路由 ───
|
|
1580
1626
|
function _sanitizeProxyPath(raw) {
|
|
1627
|
+
if (raw.length > 2048) return null;
|
|
1628
|
+
try { if (decodeURIComponent(raw) !== raw) return null; } catch { return null; }
|
|
1581
1629
|
if (raw.startsWith("//")) return null;
|
|
1630
|
+
if (!/^[/\\w.\\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
|
|
1582
1631
|
return raw.startsWith("/") ? raw : "/" + raw;
|
|
1583
1632
|
}
|
|
1584
1633
|
`);
|
|
@@ -1656,14 +1705,12 @@ const NODE_BUILTINS = [
|
|
|
1656
1705
|
/**
|
|
1657
1706
|
* 生成 SSR serverless/edge 入口源码
|
|
1658
1707
|
*
|
|
1659
|
-
* 内联
|
|
1708
|
+
* 内联 injectSSR 以避免
|
|
1660
1709
|
* @finesoft/front → @finesoft/server → vite-plugin → import("vite") 依赖链。
|
|
1661
1710
|
*/
|
|
1662
1711
|
function generateSSREntry(ctx, opts) {
|
|
1663
1712
|
const setupImport = ctx.setupPath ? `import _setupDefault from "./${ctx.setupPath}";` : ``;
|
|
1664
1713
|
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
1714
|
const renderModes = JSON.stringify(ctx.renderModes ?? {});
|
|
1668
1715
|
const cacheImpl = opts.platformCache ? opts.platformCache : `
|
|
1669
1716
|
const ISR_CACHE_MAX = 1000;
|
|
@@ -1685,38 +1732,23 @@ import { render, serializeServerData } from "./${ctx.ssrEntry}";
|
|
|
1685
1732
|
${setupImport}
|
|
1686
1733
|
|
|
1687
1734
|
const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
|
|
1688
|
-
const LOCALES = ${locales};
|
|
1689
|
-
const DEFAULT_LOCALE = ${defaultLocale};
|
|
1690
1735
|
const RENDER_MODES = ${renderModes};
|
|
1691
1736
|
${cacheImpl}
|
|
1692
1737
|
|
|
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) {
|
|
1738
|
+
function injectSSR(t, head, css, html, data) {
|
|
1707
1739
|
return t
|
|
1708
|
-
.replace(
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1740
|
+
.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_, name) => {
|
|
1741
|
+
const replacements = {
|
|
1742
|
+
head: head + "\\n<style>" + css + "</style>",
|
|
1743
|
+
body: html,
|
|
1744
|
+
data: '<script id="serialized-server-data" type="application/json">' + data + "<\/script>",
|
|
1745
|
+
};
|
|
1746
|
+
return replacements[name] ?? "";
|
|
1747
|
+
});
|
|
1712
1748
|
}
|
|
1713
1749
|
|
|
1714
|
-
function injectCSRShell(t
|
|
1715
|
-
return t
|
|
1716
|
-
.replace("<!--ssr-lang-->", locale)
|
|
1717
|
-
.replace("<!--ssr-head-->", "")
|
|
1718
|
-
.replace("<!--ssr-body-->", "")
|
|
1719
|
-
.replace("<!--ssr-data-->", "");
|
|
1750
|
+
function injectCSRShell(t) {
|
|
1751
|
+
return t.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
|
|
1720
1752
|
}
|
|
1721
1753
|
|
|
1722
1754
|
function matchRenderMode(url) {
|
|
@@ -1762,32 +1794,29 @@ app.get("*", async (c) => {
|
|
|
1762
1794
|
|
|
1763
1795
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
1764
1796
|
try {
|
|
1765
|
-
const locale = parseAcceptLanguage(c.req.header("accept-language"));
|
|
1766
|
-
|
|
1767
1797
|
// Vite 配置级别覆盖: CSR 直接返回空壳
|
|
1768
1798
|
const overrideMode = matchRenderMode(url);
|
|
1769
1799
|
if (overrideMode === "csr") {
|
|
1770
|
-
return c.html(injectCSRShell(TEMPLATE
|
|
1800
|
+
return c.html(injectCSRShell(TEMPLATE));
|
|
1771
1801
|
}
|
|
1772
1802
|
|
|
1773
|
-
// ISR
|
|
1774
|
-
const
|
|
1775
|
-
const cached = await platformCacheGet(_cacheKey);
|
|
1803
|
+
// ISR 缓存命中
|
|
1804
|
+
const cached = await platformCacheGet(url);
|
|
1776
1805
|
if (cached) return c.html(cached);
|
|
1777
1806
|
|
|
1778
|
-
const { html: appHtml, head, css, serverData, renderMode } = await render(url,
|
|
1807
|
+
const { html: appHtml, head, css, serverData, renderMode } = await render(url, { fetch: _createInternalFetch(_ssrDepth + 1) });
|
|
1779
1808
|
|
|
1780
1809
|
// 路由级 CSR
|
|
1781
1810
|
if (renderMode === "csr") {
|
|
1782
|
-
return c.html(injectCSRShell(TEMPLATE
|
|
1811
|
+
return c.html(injectCSRShell(TEMPLATE));
|
|
1783
1812
|
}
|
|
1784
1813
|
|
|
1785
1814
|
const serializedData = serializeServerData(serverData);
|
|
1786
|
-
const finalHtml = injectSSR(TEMPLATE,
|
|
1815
|
+
const finalHtml = injectSSR(TEMPLATE, head, css, appHtml, serializedData);
|
|
1787
1816
|
|
|
1788
1817
|
// Prerender ISR 缓存(包括 Vite 配置覆盖和路由级)
|
|
1789
1818
|
if (renderMode === "prerender" || overrideMode === "prerender") {
|
|
1790
|
-
await platformCacheSet(
|
|
1819
|
+
await platformCacheSet(url, finalHtml);
|
|
1791
1820
|
${opts.platformPrerenderResponseHook ?? ""}
|
|
1792
1821
|
}
|
|
1793
1822
|
|
|
@@ -1863,21 +1892,24 @@ async function prerenderRoutes(ctx) {
|
|
|
1863
1892
|
const ssrPath = pathToFileURL(path.resolve(root, "dist/server/ssr.js")).href;
|
|
1864
1893
|
const ssrModule = await dynamicImport(ssrPath);
|
|
1865
1894
|
const results = [];
|
|
1866
|
-
for (const
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1895
|
+
for (const url of prerenderPaths) try {
|
|
1896
|
+
const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
|
|
1897
|
+
const serializedData = ssrModule.serializeServerData(serverData);
|
|
1898
|
+
const finalHtml = ctx.templateHtml.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, (_match, name) => {
|
|
1899
|
+
return {
|
|
1900
|
+
head: head + "\n<style>" + css + "</style>",
|
|
1901
|
+
body: appHtml,
|
|
1902
|
+
data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
|
|
1903
|
+
}[name] ?? "";
|
|
1904
|
+
});
|
|
1905
|
+
results.push({
|
|
1906
|
+
url,
|
|
1907
|
+
html: finalHtml
|
|
1908
|
+
});
|
|
1909
|
+
} catch (e) {
|
|
1910
|
+
console.warn(` [prerender] Failed to render ${url}:`, e);
|
|
1879
1911
|
}
|
|
1880
|
-
if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes
|
|
1912
|
+
if (results.length > 0) console.log(` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes)\n`);
|
|
1881
1913
|
return results;
|
|
1882
1914
|
}
|
|
1883
1915
|
//#endregion
|
|
@@ -2111,22 +2143,15 @@ function staticAdapter(opts = {}) {
|
|
|
2111
2143
|
const ssrModule = await dynamicImport(ssrPath);
|
|
2112
2144
|
ctx.copyStaticAssets(outputDir, { excludeHtml: true });
|
|
2113
2145
|
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);
|
|
2146
|
+
console.log(` Pre-rendering ${routePaths.length} pages...\n`);
|
|
2147
|
+
for (const url of routePaths) try {
|
|
2148
|
+
const mode = resolveRenderMode(url, routeDefs.find((r) => r.path === url)?.renderMode, ctx.renderModes);
|
|
2124
2149
|
let finalHtml;
|
|
2125
|
-
if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml
|
|
2150
|
+
if (mode === "csr") finalHtml = injectCSRShellForStatic(ctx.templateHtml);
|
|
2126
2151
|
else {
|
|
2127
|
-
const { html: appHtml, head, css, serverData } = await ssrModule.render(url
|
|
2152
|
+
const { html: appHtml, head, css, serverData } = await ssrModule.render(url);
|
|
2128
2153
|
const serializedData = ssrModule.serializeServerData(serverData);
|
|
2129
|
-
finalHtml = injectSSRForStatic(ctx.templateHtml,
|
|
2154
|
+
finalHtml = injectSSRForStatic(ctx.templateHtml, head, css, appHtml, serializedData);
|
|
2130
2155
|
}
|
|
2131
2156
|
const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
|
|
2132
2157
|
fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
|
|
@@ -2181,28 +2206,19 @@ async function extractRoutesWithModes(ctx, opts) {
|
|
|
2181
2206
|
defs
|
|
2182
2207
|
};
|
|
2183
2208
|
}
|
|
2184
|
-
/**
|
|
2185
|
-
function
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
return template.replace(
|
|
2209
|
+
/** 内联 SSR 注入 */
|
|
2210
|
+
function injectSSRForStatic(template, head, css, html, serializedData) {
|
|
2211
|
+
const PLACEHOLDER_REGEX = /<!--ssr-([a-z][a-z0-9-]*)-->/g;
|
|
2212
|
+
const replacements = {
|
|
2213
|
+
head: head + "\n<style>" + css + "</style>",
|
|
2214
|
+
body: html,
|
|
2215
|
+
data: "<script id=\"serialized-server-data\" type=\"application/json\">" + serializedData + "<\/script>"
|
|
2216
|
+
};
|
|
2217
|
+
return template.replace(PLACEHOLDER_REGEX, (_, name) => replacements[name] ?? "");
|
|
2193
2218
|
}
|
|
2194
2219
|
/** 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;
|
|
2220
|
+
function injectCSRShellForStatic(template) {
|
|
2221
|
+
return template.replace(/<!--ssr-([a-z][a-z0-9-]*)-->/g, () => "");
|
|
2206
2222
|
}
|
|
2207
2223
|
/** 解析最终渲染模式:Vite 配置覆盖 > 路由级 > 默认 "ssr" */
|
|
2208
2224
|
function resolveRenderMode(routePath, routeRenderMode, renderModes) {
|
|
@@ -2402,7 +2418,7 @@ function matchRenderModeOverride(url, renderModes) {
|
|
|
2402
2418
|
return null;
|
|
2403
2419
|
}
|
|
2404
2420
|
function createSSRApp(options) {
|
|
2405
|
-
const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule,
|
|
2421
|
+
const { root, vite, isProduction, ssrEntryPath = "/src/ssr.ts", ssrProductionModule, parentFetch, renderModes } = options;
|
|
2406
2422
|
const app = new Hono();
|
|
2407
2423
|
/** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */
|
|
2408
2424
|
const ISR_CACHE_MAX = 1e3;
|
|
@@ -2446,28 +2462,28 @@ function createSSRApp(options) {
|
|
|
2446
2462
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
2447
2463
|
try {
|
|
2448
2464
|
const template = await readTemplate(url);
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2465
|
+
const ssrMod = await loadSSRModule();
|
|
2466
|
+
if (typeof ssrMod.render !== "function" || typeof ssrMod.serializeServerData !== "function") throw new Error("[SSR] Module missing required exports: render, serializeServerData");
|
|
2467
|
+
const { render, serializeServerData } = ssrMod;
|
|
2451
2468
|
const overrideMode = matchRenderModeOverride(url, renderModes);
|
|
2452
|
-
if (overrideMode === "csr") return c.html(injectCSRShell(template
|
|
2453
|
-
const
|
|
2454
|
-
const cached = isrCache.get(cacheKey);
|
|
2469
|
+
if (overrideMode === "csr") return c.html(injectCSRShell(template));
|
|
2470
|
+
const cached = isrCache.get(url);
|
|
2455
2471
|
if (cached) return c.html(cached);
|
|
2456
2472
|
const requestFetch = parentFetch ? createInternalFetch(parentFetch, ssrDepth + 1) : void 0;
|
|
2457
2473
|
const ssrContext = { request: c.req.raw };
|
|
2458
2474
|
if (requestFetch) ssrContext.fetch = requestFetch;
|
|
2459
|
-
const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect } = await render(url,
|
|
2475
|
+
const { html: appHtml, head, css, serverData, renderMode, redirect: middlewareRedirect, slots } = await render(url, ssrContext);
|
|
2460
2476
|
if (middlewareRedirect) return c.redirect(middlewareRedirect.url, middlewareRedirect.status);
|
|
2461
|
-
if (renderMode === "csr") return c.html(injectCSRShell(template
|
|
2477
|
+
if (renderMode === "csr") return c.html(injectCSRShell(template));
|
|
2462
2478
|
const finalHtml = injectSSRContent({
|
|
2463
2479
|
template,
|
|
2464
|
-
locale,
|
|
2465
2480
|
head,
|
|
2466
2481
|
css,
|
|
2467
2482
|
html: appHtml,
|
|
2468
|
-
serializedData: serializeServerData(serverData)
|
|
2483
|
+
serializedData: serializeServerData(serverData),
|
|
2484
|
+
slots
|
|
2469
2485
|
});
|
|
2470
|
-
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(
|
|
2486
|
+
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
|
|
2471
2487
|
return c.html(finalHtml);
|
|
2472
2488
|
} catch (e) {
|
|
2473
2489
|
if (!isProduction && vite) vite.ssrFixStacktrace(e);
|
|
@@ -2517,7 +2533,7 @@ async function resolveRoot(importMetaUrl, levelsUp = 0) {
|
|
|
2517
2533
|
* 支持 Node.js (dev HMR + prod)、Deno、Bun、Vercel。
|
|
2518
2534
|
*/
|
|
2519
2535
|
async function startServer(options) {
|
|
2520
|
-
const { app, root, port = 3e3, isProduction, vite, routes,
|
|
2536
|
+
const { app, root, port = 3e3, isProduction, vite, routes, ssrEntryPath } = options;
|
|
2521
2537
|
const { isDeno, isBun, isVercel } = options.runtime ?? detectRuntime();
|
|
2522
2538
|
function printStartupBanner() {
|
|
2523
2539
|
const lines = [`\n Server running at http://localhost:${port}\n`];
|
|
@@ -2526,9 +2542,8 @@ async function startServer(options) {
|
|
|
2526
2542
|
for (const r of routes) lines.push(` ${r}`);
|
|
2527
2543
|
lines.push("");
|
|
2528
2544
|
}
|
|
2529
|
-
if (locales && locales.length > 0) lines.push(` Locales: ${locales.join(", ")}`);
|
|
2530
2545
|
if (ssrEntryPath) lines.push(` SSR Entry: ${ssrEntryPath}`);
|
|
2531
|
-
if (
|
|
2546
|
+
if (ssrEntryPath) lines.push("");
|
|
2532
2547
|
console.log(lines.join("\n"));
|
|
2533
2548
|
}
|
|
2534
2549
|
if (isVercel) return { vite };
|
|
@@ -2587,21 +2602,22 @@ async function startServer(options) {
|
|
|
2587
2602
|
* @example
|
|
2588
2603
|
* ```ts
|
|
2589
2604
|
* const { app } = await createServer({
|
|
2590
|
-
* locales: ["zh", "en"],
|
|
2591
2605
|
* setup: (app) => registerProxies(app),
|
|
2592
2606
|
* });
|
|
2593
2607
|
* export { app };
|
|
2594
2608
|
* ```
|
|
2595
2609
|
*/
|
|
2596
2610
|
async function createServer(config = {}) {
|
|
2597
|
-
const { root: rootOverride,
|
|
2611
|
+
const { root: rootOverride, port = Number(process.env.PORT) || 3e3, setup, proxies, ssr } = config;
|
|
2598
2612
|
const root = rootOverride ?? process.cwd();
|
|
2599
2613
|
const { existsSync } = await dynamicImport("node:fs");
|
|
2600
2614
|
const envPath = (await dynamicImport("node:path")).resolve(root, ".env");
|
|
2601
2615
|
if (existsSync(envPath)) try {
|
|
2602
2616
|
const { config: dotenvConfig } = await dynamicImport("dotenv");
|
|
2603
2617
|
dotenvConfig({ path: envPath });
|
|
2604
|
-
} catch {
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
console.warn(`[Server] Failed to load .env: ${e.message}`);
|
|
2620
|
+
}
|
|
2605
2621
|
const runtime = detectRuntime();
|
|
2606
2622
|
let vite;
|
|
2607
2623
|
if (!runtime.isProduction && !runtime.isVercel) {
|
|
@@ -2619,8 +2635,6 @@ async function createServer(config = {}) {
|
|
|
2619
2635
|
root,
|
|
2620
2636
|
vite,
|
|
2621
2637
|
isProduction: runtime.isProduction,
|
|
2622
|
-
supportedLocales: locales,
|
|
2623
|
-
defaultLocale,
|
|
2624
2638
|
parentFetch: app.fetch.bind(app),
|
|
2625
2639
|
...ssr
|
|
2626
2640
|
});
|
|
@@ -2632,7 +2646,6 @@ async function createServer(config = {}) {
|
|
|
2632
2646
|
isProduction: runtime.isProduction,
|
|
2633
2647
|
vite,
|
|
2634
2648
|
runtime,
|
|
2635
|
-
locales,
|
|
2636
2649
|
ssrEntryPath: ssr?.ssrEntryPath
|
|
2637
2650
|
});
|
|
2638
2651
|
return {
|
|
@@ -2642,6 +2655,35 @@ async function createServer(config = {}) {
|
|
|
2642
2655
|
};
|
|
2643
2656
|
}
|
|
2644
2657
|
//#endregion
|
|
2658
|
+
//#region ../server/src/locale.ts
|
|
2659
|
+
/**
|
|
2660
|
+
* Accept-Language 解析
|
|
2661
|
+
*/
|
|
2662
|
+
/** Accept-Language 头最大长度 */
|
|
2663
|
+
const MAX_HEADER_LENGTH = 1024;
|
|
2664
|
+
/** 最大解析语言条目数 */
|
|
2665
|
+
const MAX_LANG_ENTRIES = 50;
|
|
2666
|
+
function parseAcceptLanguage(header, supported, fallback) {
|
|
2667
|
+
const effectiveSupported = supported ?? ["zh", "en"];
|
|
2668
|
+
const effectiveFallback = fallback ?? effectiveSupported[0] ?? "en";
|
|
2669
|
+
if (!header || header.length > MAX_HEADER_LENGTH) return effectiveFallback;
|
|
2670
|
+
const parts = header.split(",");
|
|
2671
|
+
if (parts.length > MAX_LANG_ENTRIES) return effectiveFallback;
|
|
2672
|
+
const langs = parts.map((part) => {
|
|
2673
|
+
const [lang, q] = part.trim().split(";q=");
|
|
2674
|
+
const qVal = q ? parseFloat(q) : 1;
|
|
2675
|
+
return {
|
|
2676
|
+
lang: lang.trim().toLowerCase(),
|
|
2677
|
+
q: Number.isFinite(qVal) && qVal >= 0 && qVal <= 1 ? qVal : 0
|
|
2678
|
+
};
|
|
2679
|
+
}).sort((a, b) => b.q - a.q);
|
|
2680
|
+
for (const { lang } of langs) {
|
|
2681
|
+
const prefix = lang.split("-")[0];
|
|
2682
|
+
if (effectiveSupported.includes(prefix)) return prefix;
|
|
2683
|
+
}
|
|
2684
|
+
return effectiveFallback;
|
|
2685
|
+
}
|
|
2686
|
+
//#endregion
|
|
2645
2687
|
//#region ../server/src/vite-plugin.ts
|
|
2646
2688
|
/**
|
|
2647
2689
|
* finesoftFrontViteConfig — Vite 插件
|
|
@@ -2711,11 +2753,12 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2711
2753
|
}
|
|
2712
2754
|
const cssUrls = [];
|
|
2713
2755
|
const visited = /* @__PURE__ */ new Set();
|
|
2714
|
-
function walk(mod) {
|
|
2756
|
+
function walk(mod, depth = 0) {
|
|
2757
|
+
if (depth > 100) return;
|
|
2715
2758
|
if (!mod?.url || visited.has(mod.url)) return;
|
|
2716
2759
|
visited.add(mod.url);
|
|
2717
2760
|
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);
|
|
2761
|
+
if (mod.importedModules) for (const imported of mod.importedModules) walk(imported, depth + 1);
|
|
2719
2762
|
}
|
|
2720
2763
|
const browserMod = await server.moduleGraph.getModuleByUrl(browserEntry);
|
|
2721
2764
|
if (browserMod) walk(browserMod);
|
|
@@ -2749,8 +2792,6 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2749
2792
|
vite: server,
|
|
2750
2793
|
isProduction: false,
|
|
2751
2794
|
ssrEntryPath: "/" + ssrEntry,
|
|
2752
|
-
supportedLocales: options.locales,
|
|
2753
|
-
defaultLocale: options.defaultLocale,
|
|
2754
2795
|
parentFetch: app.fetch.bind(app),
|
|
2755
2796
|
renderModes: options.renderModes
|
|
2756
2797
|
});
|
|
@@ -2767,10 +2808,17 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2767
2808
|
const path = await dynamicImport("node:path");
|
|
2768
2809
|
const { pathToFileURL } = await dynamicImport("node:url");
|
|
2769
2810
|
const { Hono: HonoClass } = await dynamicImport("hono");
|
|
2770
|
-
const { parseAcceptLanguage } = await import("./locale-CvU-U6aP.mjs").then((n) => n.t);
|
|
2771
2811
|
const { getRequestListener } = await dynamicImport("@hono/node-server");
|
|
2772
2812
|
const app = new HonoClass();
|
|
2813
|
+
const ISR_CACHE_MAX = 1e3;
|
|
2773
2814
|
const isrCache = /* @__PURE__ */ new Map();
|
|
2815
|
+
function isrSet(key, val) {
|
|
2816
|
+
if (isrCache.size >= ISR_CACHE_MAX) {
|
|
2817
|
+
const first = isrCache.keys().next().value;
|
|
2818
|
+
if (first !== void 0) isrCache.delete(first);
|
|
2819
|
+
}
|
|
2820
|
+
isrCache.set(key, val);
|
|
2821
|
+
}
|
|
2774
2822
|
if (options.proxies?.length) registerProxyRoutes(app, options.proxies);
|
|
2775
2823
|
if (typeof options.setup === "function") await options.setup(app);
|
|
2776
2824
|
else if (typeof options.setup === "string") try {
|
|
@@ -2788,23 +2836,20 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2788
2836
|
if (ssrDepth >= 5) return c.text("SSR recursion loop detected", 508);
|
|
2789
2837
|
const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
|
|
2790
2838
|
try {
|
|
2791
|
-
const locale = parseAcceptLanguage(c.req.header("accept-language"), options.locales, options.defaultLocale);
|
|
2792
2839
|
const overrideMode = matchRenderModeConfig(url, options.renderModes);
|
|
2793
|
-
if (overrideMode === "csr") return c.html(injectCSRShell(template
|
|
2794
|
-
const
|
|
2795
|
-
const cached = isrCache.get(cacheKey);
|
|
2840
|
+
if (overrideMode === "csr") return c.html(injectCSRShell(template));
|
|
2841
|
+
const cached = isrCache.get(url);
|
|
2796
2842
|
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
|
|
2843
|
+
const { html: appHtml, head, css, serverData, renderMode } = await ssrModule.render(url, { fetch: createInternalFetch(app.fetch.bind(app), ssrDepth + 1) });
|
|
2844
|
+
if (renderMode === "csr") return c.html(injectCSRShell(template));
|
|
2799
2845
|
const finalHtml = injectSSRContent({
|
|
2800
2846
|
template,
|
|
2801
|
-
locale,
|
|
2802
2847
|
head,
|
|
2803
2848
|
css,
|
|
2804
2849
|
html: appHtml,
|
|
2805
2850
|
serializedData: ssrModule.serializeServerData(serverData)
|
|
2806
2851
|
});
|
|
2807
|
-
if (renderMode === "prerender" || overrideMode === "prerender")
|
|
2852
|
+
if (renderMode === "prerender" || overrideMode === "prerender") isrSet(url, finalHtml);
|
|
2808
2853
|
return c.html(finalHtml);
|
|
2809
2854
|
} catch (e) {
|
|
2810
2855
|
console.error("[SSR Preview Error]", e);
|
|
@@ -2851,16 +2896,12 @@ function finesoftFrontViteConfig(options = {}) {
|
|
|
2851
2896
|
}
|
|
2852
2897
|
if (options.adapter) {
|
|
2853
2898
|
const adapter = resolveAdapter(options.adapter);
|
|
2854
|
-
const locales = options.locales ?? ["zh", "en"];
|
|
2855
|
-
const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
|
|
2856
2899
|
const templateHtml = fs.readFileSync(path.resolve(root, "dist/client/index.html"), "utf-8");
|
|
2857
2900
|
const ctx = {
|
|
2858
2901
|
root,
|
|
2859
2902
|
ssrEntry,
|
|
2860
2903
|
setupPath: typeof options.setup === "string" ? options.setup : void 0,
|
|
2861
2904
|
bootstrapEntry: options.bootstrapEntry,
|
|
2862
|
-
locales,
|
|
2863
|
-
defaultLocale,
|
|
2864
2905
|
templateHtml,
|
|
2865
2906
|
renderModes: options.renderModes,
|
|
2866
2907
|
proxies: options.proxies,
|