@finesoft/front 0.1.20 → 0.1.21

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.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createSSRApp
3
- } from "./chunk-SN3OO3DU.js";
3
+ } from "./chunk-IITKGRCO.js";
4
4
  import "./chunk-5ZECBECP.js";
5
5
  import "./chunk-BUYWNNNQ.js";
6
6
  import "./chunk-FYP2ZYYV.js";
7
7
  export {
8
8
  createSSRApp
9
9
  };
10
- //# sourceMappingURL=app-Z3EFLAP2.js.map
10
+ //# sourceMappingURL=app-BPO26FAD.js.map
@@ -19,7 +19,15 @@ function createSSRApp(options) {
19
19
  defaultLocale
20
20
  } = options;
21
21
  const app = new Hono();
22
+ const ISR_CACHE_MAX = 1e3;
22
23
  const isrCache = /* @__PURE__ */ new Map();
24
+ function isrSet(key, val) {
25
+ if (isrCache.size >= ISR_CACHE_MAX) {
26
+ const first = isrCache.keys().next().value;
27
+ if (first !== void 0) isrCache.delete(first);
28
+ }
29
+ isrCache.set(key, val);
30
+ }
23
31
  async function readTemplate(url) {
24
32
  if (!isProduction && vite) {
25
33
  const { readFileSync: readFileSync2 } = await import(
@@ -100,7 +108,7 @@ function createSSRApp(options) {
100
108
  serializedData
101
109
  });
102
110
  if (renderMode === "prerender") {
103
- isrCache.set(cacheKey, finalHtml);
111
+ isrSet(cacheKey, finalHtml);
104
112
  }
105
113
  return c.html(finalHtml);
106
114
  } catch (e) {
@@ -117,4 +125,4 @@ function createSSRApp(options) {
117
125
  export {
118
126
  createSSRApp
119
127
  };
120
- //# sourceMappingURL=chunk-SN3OO3DU.js.map
128
+ //# sourceMappingURL=chunk-IITKGRCO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../server/src/app.ts"],"sourcesContent":["/**\n * createSSRApp — 创建 Hono SSR 应用\n *\n * 提供 SSR 通配路由,读取模板、加载 SSR 模块、渲染。\n * 应用层可在此之上追加自定义路由(API 代理等)。\n */\n\nimport { injectCSRShell, injectSSRContent } from \"@finesoft/ssr\";\nimport { Hono } from \"hono\";\nimport type { ViteDevServer } from \"vite\";\nimport { parseAcceptLanguage } from \"./locale\";\n\nexport interface SSRModule {\n\trender: (\n\t\turl: string,\n\t\tlocale: string,\n\t) => Promise<{\n\t\thtml: string;\n\t\thead: string;\n\t\tcss: string;\n\t\tserverData: unknown;\n\t\trenderMode?: string;\n\t}>;\n\tserializeServerData: (data: unknown) => string;\n}\n\nexport interface SSRAppOptions {\n\t/** 项目根路径 */\n\troot: string;\n\t/** Vite dev server(仅开发模式) */\n\tvite?: ViteDevServer;\n\t/** 是否生产环境 */\n\tisProduction: boolean;\n\t/** SSR 入口文件路径(开发用,如 \"/src/ssr.ts\") */\n\tssrEntryPath?: string;\n\t/** 生产环境 SSR 模块路径(如 \"../dist/server/ssr.js\") */\n\tssrProductionModule?: string;\n\t/** 支持的语言列表 */\n\tsupportedLocales?: string[];\n\t/** 默认语言 */\n\tdefaultLocale?: string;\n}\n\nexport function createSSRApp(options: SSRAppOptions): Hono {\n\tconst {\n\t\troot,\n\t\tvite,\n\t\tisProduction,\n\t\tssrEntryPath = \"/src/ssr.ts\",\n\t\tssrProductionModule,\n\t\tsupportedLocales,\n\t\tdefaultLocale,\n\t} = options;\n\n\tconst app = new Hono();\n\n\t/** ISR 内存缓存(prerender 路由首次请求后缓存,LRU 驱逐) */\n\tconst ISR_CACHE_MAX = 1000;\n\tconst isrCache = new Map<string, string>();\n\tfunction isrSet(key: string, val: string) {\n\t\tif (isrCache.size >= ISR_CACHE_MAX) {\n\t\t\tconst first = isrCache.keys().next().value;\n\t\t\tif (first !== undefined) isrCache.delete(first);\n\t\t}\n\t\tisrCache.set(key, val);\n\t}\n\n\tasync function readTemplate(url: string): Promise<string> {\n\t\tif (!isProduction && vite) {\n\t\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\t\tconst raw = readFileSync(resolve(root, \"index.html\"), \"utf-8\");\n\t\t\treturn vite.transformIndexHtml(url, raw);\n\t\t}\n\n\t\tconst isDeno = typeof (globalThis as any).Deno !== \"undefined\";\n\t\tif (isDeno) {\n\t\t\treturn (globalThis as any).Deno.readTextFileSync(\n\t\t\t\tnew URL(\"../dist/client/index.html\", import.meta.url),\n\t\t\t);\n\t\t}\n\n\t\tconst { readFileSync } = await import(/* @vite-ignore */ \"node:fs\");\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\treturn readFileSync(resolve(root, \"dist/client/index.html\"), \"utf-8\");\n\t}\n\n\tasync function loadSSRModule(): Promise<SSRModule> {\n\t\tif (!isProduction && vite) {\n\t\t\treturn (await vite.ssrLoadModule(ssrEntryPath)) as SSRModule;\n\t\t}\n\t\tif (ssrProductionModule) {\n\t\t\treturn import(ssrProductionModule) as Promise<SSRModule>;\n\t\t}\n\t\tconst { resolve } = await import(/* @vite-ignore */ \"node:path\");\n\t\tconst { pathToFileURL } = await import(/* @vite-ignore */ \"node:url\");\n\t\tconst absPath = pathToFileURL(resolve(root, \"dist/server/ssr.js\")).href;\n\t\treturn import(absPath) as Promise<SSRModule>;\n\t}\n\n\tapp.get(\"*\", async (c) => {\n\t\tconst url =\n\t\t\tc.req.path +\n\t\t\t(c.req.url.includes(\"?\") ? \"?\" + c.req.url.split(\"?\")[1] : \"\");\n\n\t\ttry {\n\t\t\t// ISR 缓存命中\n\t\t\tconst cacheKey = url;\n\t\t\tconst cached = isrCache.get(cacheKey);\n\t\t\tif (cached) return c.html(cached);\n\n\t\t\tconst template = await readTemplate(url);\n\t\t\tconst { render, serializeServerData } = await loadSSRModule();\n\n\t\t\tconst locale = parseAcceptLanguage(\n\t\t\t\tc.req.header(\"accept-language\"),\n\t\t\t\tsupportedLocales,\n\t\t\t\tdefaultLocale,\n\t\t\t);\n\n\t\t\tconst {\n\t\t\t\thtml: appHtml,\n\t\t\t\thead,\n\t\t\t\tcss,\n\t\t\t\tserverData,\n\t\t\t\trenderMode,\n\t\t\t} = await render(url, locale);\n\n\t\t\t// CSR 模式:返回空壳 HTML\n\t\t\tif (renderMode === \"csr\") {\n\t\t\t\treturn c.html(injectCSRShell(template, locale));\n\t\t\t}\n\n\t\t\tconst serializedData = serializeServerData(serverData);\n\n\t\t\tconst finalHtml = injectSSRContent({\n\t\t\t\ttemplate,\n\t\t\t\tlocale,\n\t\t\t\thead,\n\t\t\t\tcss,\n\t\t\t\thtml: appHtml,\n\t\t\t\tserializedData,\n\t\t\t});\n\n\t\t\t// Prerender ISR 缓存\n\t\t\tif (renderMode === \"prerender\") {\n\t\t\t\tisrSet(cacheKey, finalHtml);\n\t\t\t}\n\n\t\t\treturn c.html(finalHtml);\n\t\t} catch (e) {\n\t\t\tif (!isProduction && vite) {\n\t\t\t\tvite.ssrFixStacktrace(e as Error);\n\t\t\t}\n\t\t\tconsole.error(\"[SSR Error]\", e);\n\t\t\treturn c.text(\"Internal Server Error\", 500);\n\t\t}\n\t});\n\n\treturn app;\n}\n"],"mappings":"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","names":["readFileSync","resolve"]}
package/dist/index.cjs CHANGED
@@ -1149,7 +1149,15 @@ function createSSRApp(options) {
1149
1149
  defaultLocale
1150
1150
  } = options;
1151
1151
  const app = new import_hono.Hono();
1152
+ const ISR_CACHE_MAX = 1e3;
1152
1153
  const isrCache = /* @__PURE__ */ new Map();
1154
+ function isrSet(key, val) {
1155
+ if (isrCache.size >= ISR_CACHE_MAX) {
1156
+ const first = isrCache.keys().next().value;
1157
+ if (first !== void 0) isrCache.delete(first);
1158
+ }
1159
+ isrCache.set(key, val);
1160
+ }
1153
1161
  async function readTemplate(url) {
1154
1162
  if (!isProduction && vite) {
1155
1163
  const { readFileSync: readFileSync2 } = await import(
@@ -1230,7 +1238,7 @@ function createSSRApp(options) {
1230
1238
  serializedData
1231
1239
  });
1232
1240
  if (renderMode === "prerender") {
1233
- isrCache.set(cacheKey, finalHtml);
1241
+ isrSet(cacheKey, finalHtml);
1234
1242
  }
1235
1243
  return c.html(finalHtml);
1236
1244
  } catch (e) {
@@ -1675,6 +1683,19 @@ function generateSSREntry(ctx, opts) {
1675
1683
  const locales = JSON.stringify(ctx.locales);
1676
1684
  const defaultLocale = JSON.stringify(ctx.defaultLocale);
1677
1685
  const renderModes = JSON.stringify(ctx.renderModes ?? {});
1686
+ const cacheImpl = opts.platformCache ? opts.platformCache : `
1687
+ const ISR_CACHE_MAX = 1000;
1688
+ const _isrMap = new Map();
1689
+ async function platformCacheGet(url) {
1690
+ return _isrMap.get(url) ?? null;
1691
+ }
1692
+ async function platformCacheSet(url, html) {
1693
+ if (_isrMap.size >= ISR_CACHE_MAX) {
1694
+ const first = _isrMap.keys().next().value;
1695
+ _isrMap.delete(first);
1696
+ }
1697
+ _isrMap.set(url, html);
1698
+ }`;
1678
1699
  return `
1679
1700
  import { Hono } from "hono";
1680
1701
  ${opts.platformImport}
@@ -1685,6 +1706,7 @@ const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
1685
1706
  const LOCALES = ${locales};
1686
1707
  const DEFAULT_LOCALE = ${defaultLocale};
1687
1708
  const RENDER_MODES = ${renderModes};
1709
+ ${cacheImpl}
1688
1710
 
1689
1711
  function parseAcceptLanguage(header) {
1690
1712
  if (!header) return DEFAULT_LOCALE;
@@ -1727,10 +1749,9 @@ function matchRenderMode(url) {
1727
1749
  return null;
1728
1750
  }
1729
1751
 
1730
- const isrCache = new Map();
1731
-
1732
1752
  const app = new Hono();
1733
1753
  ${setupCall}
1754
+ ${opts.platformMiddleware ?? ""}
1734
1755
 
1735
1756
  app.get("*", async (c) => {
1736
1757
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
@@ -1744,7 +1765,7 @@ app.get("*", async (c) => {
1744
1765
  }
1745
1766
 
1746
1767
  // ISR \u7F13\u5B58\u547D\u4E2D
1747
- const cached = isrCache.get(url);
1768
+ const cached = await platformCacheGet(url);
1748
1769
  if (cached) return c.html(cached);
1749
1770
 
1750
1771
  const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale);
@@ -1759,7 +1780,8 @@ app.get("*", async (c) => {
1759
1780
 
1760
1781
  // Prerender ISR \u7F13\u5B58\uFF08\u5305\u62EC Vite \u914D\u7F6E\u8986\u76D6\u548C\u8DEF\u7531\u7EA7\uFF09
1761
1782
  if (renderMode === "prerender" || overrideMode === "prerender") {
1762
- isrCache.set(url, finalHtml);
1783
+ await platformCacheSet(url, finalHtml);
1784
+ ${opts.platformPrerenderResponseHook ?? ""}
1763
1785
  }
1764
1786
 
1765
1787
  return c.html(finalHtml);
@@ -1895,7 +1917,29 @@ function cloudflareAdapter() {
1895
1917
  fs.rmSync(outputDir, { recursive: true, force: true });
1896
1918
  const entrySource = generateSSREntry(ctx, {
1897
1919
  platformImport: ``,
1898
- platformExport: `export default app;`
1920
+ platformExport: `export default app;`,
1921
+ // Cloudflare Cache API — 持久化 ISR 缓存到 CDN 边缘节点
1922
+ platformCache: `
1923
+ const ISR_CACHE_TTL = 3600; // 1 hour
1924
+ async function platformCacheGet(url) {
1925
+ try {
1926
+ const cache = caches.default;
1927
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
1928
+ const resp = await cache.match(cacheKey);
1929
+ if (resp) return await resp.text();
1930
+ } catch {}
1931
+ return null;
1932
+ }
1933
+ async function platformCacheSet(url, html) {
1934
+ try {
1935
+ const cache = caches.default;
1936
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
1937
+ const resp = new Response(html, {
1938
+ headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=" + ISR_CACHE_TTL },
1939
+ });
1940
+ await cache.put(cacheKey, resp);
1941
+ } catch {}
1942
+ }`
1899
1943
  });
1900
1944
  const tempEntry = path.resolve(root, ".cf-entry.tmp.mjs");
1901
1945
  fs.writeFileSync(tempEntry, entrySource);
@@ -1941,7 +1985,25 @@ function netlifyAdapter() {
1941
1985
  });
1942
1986
  const entrySource = generateSSREntry(ctx, {
1943
1987
  platformImport: `import { handle } from "hono/netlify";`,
1944
- platformExport: `export default handle(app);`
1988
+ platformExport: `export default handle(app);
1989
+ export const config = { path: "/*", preferStatic: true };`,
1990
+ // Netlify CDN 缓存 — 使用 Netlify-CDN-Cache-Control 头做 ISR
1991
+ platformCache: `
1992
+ const ISR_SWR_TTL = 3600;
1993
+ const ISR_CACHE_MAX = 1000;
1994
+ const _isrMap = new Map();
1995
+ async function platformCacheGet(url) {
1996
+ return _isrMap.get(url) ?? null;
1997
+ }
1998
+ async function platformCacheSet(url, html) {
1999
+ if (_isrMap.size >= ISR_CACHE_MAX) {
2000
+ const first = _isrMap.keys().next().value;
2001
+ _isrMap.delete(first);
2002
+ }
2003
+ _isrMap.set(url, html);
2004
+ }`,
2005
+ platformPrerenderResponseHook: `c.header("Cache-Control", "public, max-age=0, must-revalidate");
2006
+ c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`
1945
2007
  });
1946
2008
  const tempEntry = path.resolve(root, ".netlify-entry.tmp.mjs");
1947
2009
  fs.writeFileSync(tempEntry, entrySource);
@@ -1981,7 +2043,31 @@ function nodeAdapter() {
1981
2043
  async build(ctx) {
1982
2044
  const { fs, path, root } = ctx;
1983
2045
  const entrySource = generateSSREntry(ctx, {
1984
- platformImport: `import { serve } from "@hono/node-server";`,
2046
+ platformImport: `import { serve } from "@hono/node-server";
2047
+ import { readFileSync, existsSync } from "node:fs";
2048
+ import { resolve, dirname } from "node:path";
2049
+ import { fileURLToPath } from "node:url";`,
2050
+ platformMiddleware: `
2051
+ // \u9884\u6E32\u67D3\u6587\u4EF6\u4E2D\u95F4\u4EF6\uFF1A\u68C0\u67E5 dist/prerender/ \u4E0B\u662F\u5426\u6709\u5BF9\u5E94\u7684\u9759\u6001 HTML
2052
+ const __entry_dirname = dirname(fileURLToPath(import.meta.url));
2053
+ const prerenderDir = resolve(__entry_dirname, "../prerender");
2054
+
2055
+ app.use("*", async (c, next) => {
2056
+ const urlPath = c.req.path;
2057
+ const candidates = [
2058
+ resolve(prerenderDir, "." + urlPath, "index.html"),
2059
+ resolve(prerenderDir, "." + urlPath + ".html"),
2060
+ ];
2061
+ if (urlPath === "/") candidates.unshift(resolve(prerenderDir, "index.html"));
2062
+ for (const f of candidates) {
2063
+ if (existsSync(f)) {
2064
+ const html = readFileSync(f, "utf-8");
2065
+ return c.html(html);
2066
+ }
2067
+ }
2068
+ await next();
2069
+ });
2070
+ `,
1985
2071
  platformExport: `
1986
2072
  const port = +(process.env.PORT || 3000);
1987
2073
  serve({ fetch: app.fetch, port }, (info) => {
@@ -2243,7 +2329,7 @@ function vercelAdapter() {
2243
2329
  version: 3,
2244
2330
  routes: [
2245
2331
  { handle: "filesystem" },
2246
- { src: "/(.*)", dest: "/ssr" }
2332
+ { src: "/(.*)", dest: "/ssr/$1" }
2247
2333
  ]
2248
2334
  },
2249
2335
  null,
@@ -2260,6 +2346,32 @@ function vercelAdapter() {
2260
2346
  fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2261
2347
  fs.writeFileSync(filePath, html);
2262
2348
  }
2349
+ if (prerendered.length > 0) {
2350
+ const configPath = path.resolve(
2351
+ root,
2352
+ ".vercel/output/config.json"
2353
+ );
2354
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2355
+ config.overrides = config.overrides ?? {};
2356
+ for (const { url } of prerendered) {
2357
+ const key = url === "/" ? "index.html" : `${url.replace(/^\//, "")}/index.html`;
2358
+ config.overrides[key] = {
2359
+ path: url === "/" ? "/" : url,
2360
+ contentType: "text/html; charset=utf-8"
2361
+ };
2362
+ }
2363
+ const isrRoutes = prerendered.map(({ url }) => ({
2364
+ src: `^${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/?$`,
2365
+ dest: url,
2366
+ has: [{ type: "header", key: "x-vercel-isr" }]
2367
+ }));
2368
+ config.routes = [
2369
+ ...isrRoutes,
2370
+ { handle: "filesystem" },
2371
+ { src: "/(.*)", dest: "/ssr/$1" }
2372
+ ];
2373
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
2374
+ }
2263
2375
  console.log(" Vercel output \u2192 .vercel/output/\n");
2264
2376
  }
2265
2377
  };