@finesoft/front 0.1.19 → 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.
package/dist/index.cjs CHANGED
@@ -395,10 +395,10 @@ var init_router = __esm({
395
395
  Router = class {
396
396
  routes = [];
397
397
  /** 添加路由规则 */
398
- add(pattern, intentId) {
398
+ add(pattern, intentId, renderMode) {
399
399
  const paramNames = [];
400
400
  const regexStr = pattern.replace(
401
- /\/:(\w+)(\?)?/g,
401
+ /\/:([\w]+)(\?)?/g,
402
402
  (_, name, optional) => {
403
403
  paramNames.push(name);
404
404
  return optional ? "(?:/([^/]+))?" : "/([^/]+)";
@@ -408,7 +408,8 @@ var init_router = __esm({
408
408
  pattern,
409
409
  intentId,
410
410
  regex: new RegExp(`^${regexStr}/?$`),
411
- paramNames
411
+ paramNames,
412
+ renderMode
412
413
  });
413
414
  return this;
414
415
  }
@@ -429,7 +430,8 @@ var init_router = __esm({
429
430
  }
430
431
  return {
431
432
  intent: { id: route.intentId, params },
432
- action: makeFlowAction(urlOrPath)
433
+ action: makeFlowAction(urlOrPath),
434
+ renderMode: route.renderMode
433
435
  };
434
436
  }
435
437
  }
@@ -817,7 +819,7 @@ function defineRoutes(framework, definitions) {
817
819
  framework.registerIntent(def.controller);
818
820
  registeredIntents.add(def.intentId);
819
821
  }
820
- framework.router.add(def.path, def.intentId);
822
+ framework.router.add(def.path, def.intentId, def.renderMode);
821
823
  }
822
824
  }
823
825
  var init_define_routes = __esm({
@@ -973,6 +975,16 @@ async function ssrRender(options) {
973
975
  const parsed = new URL(url, "http://localhost");
974
976
  const fullPath = parsed.pathname + parsed.search;
975
977
  const match = framework.routeUrl(fullPath);
978
+ if (match?.renderMode === "csr") {
979
+ framework.dispose();
980
+ return {
981
+ html: "",
982
+ head: "",
983
+ css: "",
984
+ serverData: [],
985
+ renderMode: "csr"
986
+ };
987
+ }
976
988
  let page;
977
989
  let serverData = [];
978
990
  if (match) {
@@ -991,7 +1003,8 @@ async function ssrRender(options) {
991
1003
  html: result.html,
992
1004
  head: result.head,
993
1005
  css: result.css,
994
- serverData
1006
+ serverData,
1007
+ renderMode: match?.renderMode
995
1008
  };
996
1009
  }
997
1010
  var init_render = __esm({
@@ -1029,6 +1042,9 @@ ${cssTag}`).replace(SSR_PLACEHOLDERS.BODY, html).replace(
1029
1042
  `<script id="serialized-server-data" type="application/json">${serializedData}</script>`
1030
1043
  );
1031
1044
  }
1045
+ function injectCSRShell(template, locale) {
1046
+ return template.replace(SSR_PLACEHOLDERS.LANG, locale).replace(SSR_PLACEHOLDERS.HEAD, "").replace(SSR_PLACEHOLDERS.BODY, "").replace(SSR_PLACEHOLDERS.DATA, "");
1047
+ }
1032
1048
  var SSR_PLACEHOLDERS;
1033
1049
  var init_inject = __esm({
1034
1050
  "../ssr/src/inject.ts"() {
@@ -1071,6 +1087,7 @@ __export(src_exports, {
1071
1087
  Framework: () => Framework,
1072
1088
  SSR_PLACEHOLDERS: () => SSR_PLACEHOLDERS,
1073
1089
  createSSRRender: () => createSSRRender,
1090
+ injectCSRShell: () => injectCSRShell,
1074
1091
  injectSSRContent: () => injectSSRContent,
1075
1092
  serializeServerData: () => serializeServerData,
1076
1093
  ssrRender: () => ssrRender
@@ -1132,6 +1149,15 @@ function createSSRApp(options) {
1132
1149
  defaultLocale
1133
1150
  } = options;
1134
1151
  const app = new import_hono.Hono();
1152
+ const ISR_CACHE_MAX = 1e3;
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
+ }
1135
1161
  async function readTemplate(url) {
1136
1162
  if (!isProduction && vite) {
1137
1163
  const { readFileSync: readFileSync2 } = await import(
@@ -1182,6 +1208,9 @@ function createSSRApp(options) {
1182
1208
  app.get("*", async (c) => {
1183
1209
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1184
1210
  try {
1211
+ const cacheKey = url;
1212
+ const cached = isrCache.get(cacheKey);
1213
+ if (cached) return c.html(cached);
1185
1214
  const template = await readTemplate(url);
1186
1215
  const { render, serializeServerData: serializeServerData2 } = await loadSSRModule();
1187
1216
  const locale = parseAcceptLanguage(
@@ -1193,8 +1222,12 @@ function createSSRApp(options) {
1193
1222
  html: appHtml,
1194
1223
  head,
1195
1224
  css,
1196
- serverData
1225
+ serverData,
1226
+ renderMode
1197
1227
  } = await render(url, locale);
1228
+ if (renderMode === "csr") {
1229
+ return c.html(injectCSRShell(template, locale));
1230
+ }
1198
1231
  const serializedData = serializeServerData2(serverData);
1199
1232
  const finalHtml = injectSSRContent({
1200
1233
  template,
@@ -1204,6 +1237,9 @@ function createSSRApp(options) {
1204
1237
  html: appHtml,
1205
1238
  serializedData
1206
1239
  });
1240
+ if (renderMode === "prerender") {
1241
+ isrSet(cacheKey, finalHtml);
1242
+ }
1207
1243
  return c.html(finalHtml);
1208
1244
  } catch (e) {
1209
1245
  if (!isProduction && vite) {
@@ -1261,6 +1297,7 @@ __export(index_exports, {
1261
1297
  finesoftFrontViteConfig: () => finesoftFrontViteConfig,
1262
1298
  generateUuid: () => generateUuid,
1263
1299
  getBaseUrl: () => getBaseUrl,
1300
+ injectCSRShell: () => injectCSRShell,
1264
1301
  injectSSRContent: () => injectSSRContent,
1265
1302
  isCompoundAction: () => isCompoundAction,
1266
1303
  isExternalUrlAction: () => isExternalUrlAction,
@@ -1645,6 +1682,20 @@ function generateSSREntry(ctx, opts) {
1645
1682
  const setupCall = ctx.setupPath ? `if (typeof _setupDefault === "function") await _setupDefault(app);` : ``;
1646
1683
  const locales = JSON.stringify(ctx.locales);
1647
1684
  const defaultLocale = JSON.stringify(ctx.defaultLocale);
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
+ }`;
1648
1699
  return `
1649
1700
  import { Hono } from "hono";
1650
1701
  ${opts.platformImport}
@@ -1654,6 +1705,8 @@ ${setupImport}
1654
1705
  const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
1655
1706
  const LOCALES = ${locales};
1656
1707
  const DEFAULT_LOCALE = ${defaultLocale};
1708
+ const RENDER_MODES = ${renderModes};
1709
+ ${cacheImpl}
1657
1710
 
1658
1711
  function parseAcceptLanguage(header) {
1659
1712
  if (!header) return DEFAULT_LOCALE;
@@ -1676,16 +1729,62 @@ function injectSSR(t, locale, head, css, html, data) {
1676
1729
  .replace("<!--ssr-data-->", '<script id="serialized-server-data" type="application/json">' + data + "</script>");
1677
1730
  }
1678
1731
 
1732
+ function injectCSRShell(t, locale) {
1733
+ return t
1734
+ .replace("<!--ssr-lang-->", locale)
1735
+ .replace("<!--ssr-head-->", "")
1736
+ .replace("<!--ssr-body-->", "")
1737
+ .replace("<!--ssr-data-->", "");
1738
+ }
1739
+
1740
+ function matchRenderMode(url) {
1741
+ const path = url.split("?")[0];
1742
+ if (RENDER_MODES[path]) return RENDER_MODES[path];
1743
+ for (const [pattern, mode] of Object.entries(RENDER_MODES)) {
1744
+ if (pattern.includes("*")) {
1745
+ const re = new RegExp("^" + pattern.replace(/\\*/g, ".*") + "$");
1746
+ if (re.test(path)) return mode;
1747
+ }
1748
+ }
1749
+ return null;
1750
+ }
1751
+
1679
1752
  const app = new Hono();
1680
1753
  ${setupCall}
1754
+ ${opts.platformMiddleware ?? ""}
1681
1755
 
1682
1756
  app.get("*", async (c) => {
1683
1757
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
1684
1758
  try {
1685
1759
  const locale = parseAcceptLanguage(c.req.header("accept-language"));
1686
- const { html: appHtml, head, css, serverData } = await render(url, locale);
1760
+
1761
+ // Vite \u914D\u7F6E\u7EA7\u522B\u8986\u76D6: CSR \u76F4\u63A5\u8FD4\u56DE\u7A7A\u58F3
1762
+ const overrideMode = matchRenderMode(url);
1763
+ if (overrideMode === "csr") {
1764
+ return c.html(injectCSRShell(TEMPLATE, locale));
1765
+ }
1766
+
1767
+ // ISR \u7F13\u5B58\u547D\u4E2D
1768
+ const cached = await platformCacheGet(url);
1769
+ if (cached) return c.html(cached);
1770
+
1771
+ const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale);
1772
+
1773
+ // \u8DEF\u7531\u7EA7 CSR
1774
+ if (renderMode === "csr") {
1775
+ return c.html(injectCSRShell(TEMPLATE, locale));
1776
+ }
1777
+
1687
1778
  const serializedData = serializeServerData(serverData);
1688
- return c.html(injectSSR(TEMPLATE, locale, head, css, appHtml, serializedData));
1779
+ const finalHtml = injectSSR(TEMPLATE, locale, head, css, appHtml, serializedData);
1780
+
1781
+ // Prerender ISR \u7F13\u5B58\uFF08\u5305\u62EC Vite \u914D\u7F6E\u8986\u76D6\u548C\u8DEF\u7531\u7EA7\uFF09
1782
+ if (renderMode === "prerender" || overrideMode === "prerender") {
1783
+ await platformCacheSet(url, finalHtml);
1784
+ ${opts.platformPrerenderResponseHook ?? ""}
1785
+ }
1786
+
1787
+ return c.html(finalHtml);
1689
1788
  } catch (e) {
1690
1789
  console.error("[SSR Error]", e);
1691
1790
  return c.text("Internal Server Error", 500);
@@ -1724,6 +1823,89 @@ function copyStaticAssets(ctx, destDir, opts) {
1724
1823
  fs.rmSync(path.join(destDir, "index.html"), { force: true });
1725
1824
  }
1726
1825
  }
1826
+ async function prerenderRoutes(ctx, routesExport = "src/lib/bootstrap.ts") {
1827
+ const { fs, path, root, vite } = ctx;
1828
+ const { pathToFileURL } = await import(
1829
+ /* @vite-ignore */
1830
+ "url"
1831
+ );
1832
+ await vite.build({
1833
+ root,
1834
+ build: {
1835
+ ssr: routesExport,
1836
+ outDir: path.resolve(root, "dist/server"),
1837
+ emptyOutDir: false,
1838
+ rollupOptions: {
1839
+ output: { entryFileNames: "_routes_prerender.mjs" }
1840
+ }
1841
+ },
1842
+ resolve: ctx.resolvedResolve
1843
+ });
1844
+ const routesPath = pathToFileURL(
1845
+ path.resolve(root, "dist/server/_routes_prerender.mjs")
1846
+ ).href;
1847
+ const routesMod = await import(
1848
+ /* @vite-ignore */
1849
+ routesPath
1850
+ );
1851
+ const routes = routesMod.routes ?? routesMod.default ?? [];
1852
+ fs.rmSync(path.resolve(root, "dist/server/_routes_prerender.mjs"), {
1853
+ force: true
1854
+ });
1855
+ const prerenderPaths = /* @__PURE__ */ new Set();
1856
+ for (const r of routes) {
1857
+ if (r.renderMode === "prerender" && r.path && !r.path.includes(":")) {
1858
+ prerenderPaths.add(r.path);
1859
+ }
1860
+ }
1861
+ if (ctx.renderModes) {
1862
+ for (const [pattern, mode] of Object.entries(ctx.renderModes)) {
1863
+ if (mode === "prerender" && !pattern.includes("*") && !pattern.includes(":")) {
1864
+ prerenderPaths.add(pattern);
1865
+ }
1866
+ }
1867
+ }
1868
+ if (prerenderPaths.size === 0) return [];
1869
+ const ssrPath = pathToFileURL(
1870
+ path.resolve(root, "dist/server/ssr.js")
1871
+ ).href;
1872
+ const ssrModule = await import(
1873
+ /* @vite-ignore */
1874
+ ssrPath
1875
+ );
1876
+ const results = [];
1877
+ for (const routePath of prerenderPaths) {
1878
+ for (const locale of ctx.locales) {
1879
+ const url = locale === ctx.defaultLocale ? routePath : `/${locale}${routePath === "/" ? "" : routePath}`;
1880
+ try {
1881
+ const {
1882
+ html: appHtml,
1883
+ head,
1884
+ css,
1885
+ serverData
1886
+ } = await ssrModule.render(url, locale);
1887
+ const serializedData = ssrModule.serializeServerData(serverData);
1888
+ const finalHtml = ctx.templateHtml.replace("<!--ssr-lang-->", locale).replace(
1889
+ "<!--ssr-head-->",
1890
+ head + "\n<style>" + css + "</style>"
1891
+ ).replace("<!--ssr-body-->", appHtml).replace(
1892
+ "<!--ssr-data-->",
1893
+ '<script id="serialized-server-data" type="application/json">' + serializedData + "</script>"
1894
+ );
1895
+ results.push({ url, html: finalHtml });
1896
+ } catch (e) {
1897
+ console.warn(` [prerender] Failed to render ${url}:`, e);
1898
+ }
1899
+ }
1900
+ }
1901
+ if (results.length > 0) {
1902
+ console.log(
1903
+ ` Pre-rendered ${results.length} pages (${prerenderPaths.size} routes \xD7 ${ctx.locales.length} locales)
1904
+ `
1905
+ );
1906
+ }
1907
+ return results;
1908
+ }
1727
1909
 
1728
1910
  // ../server/src/adapters/cloudflare.ts
1729
1911
  function cloudflareAdapter() {
@@ -1735,7 +1917,29 @@ function cloudflareAdapter() {
1735
1917
  fs.rmSync(outputDir, { recursive: true, force: true });
1736
1918
  const entrySource = generateSSREntry(ctx, {
1737
1919
  platformImport: ``,
1738
- 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
+ }`
1739
1943
  });
1740
1944
  const tempEntry = path.resolve(root, ".cf-entry.tmp.mjs");
1741
1945
  fs.writeFileSync(tempEntry, entrySource);
@@ -1749,6 +1953,14 @@ function cloudflareAdapter() {
1749
1953
  // 这些构建工具运行时不需要,且 fsevents 是 macOS .node 原生二进制无法打包
1750
1954
  });
1751
1955
  copyStaticAssets(ctx, path.resolve(outputDir, "assets"));
1956
+ const prerendered = await prerenderRoutes(ctx);
1957
+ for (const { url, html } of prerendered) {
1958
+ const filePath = url === "/" ? path.join(outputDir, "assets", "index.html") : path.join(outputDir, "assets", url, "index.html");
1959
+ fs.mkdirSync(path.resolve(filePath, ".."), {
1960
+ recursive: true
1961
+ });
1962
+ fs.writeFileSync(filePath, html);
1963
+ }
1752
1964
  } finally {
1753
1965
  fs.rmSync(tempEntry, { force: true });
1754
1966
  }
@@ -1773,7 +1985,25 @@ function netlifyAdapter() {
1773
1985
  });
1774
1986
  const entrySource = generateSSREntry(ctx, {
1775
1987
  platformImport: `import { handle } from "hono/netlify";`,
1776
- 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");`
1777
2007
  });
1778
2008
  const tempEntry = path.resolve(root, ".netlify-entry.tmp.mjs");
1779
2009
  fs.writeFileSync(tempEntry, entrySource);
@@ -1792,6 +2022,13 @@ function netlifyAdapter() {
1792
2022
  path.resolve(root, "dist/client/_redirects"),
1793
2023
  redirects
1794
2024
  );
2025
+ const prerendered = await prerenderRoutes(ctx);
2026
+ const clientDir = path.resolve(root, "dist/client");
2027
+ for (const { url, html } of prerendered) {
2028
+ const filePath = url === "/" ? path.join(clientDir, "index.html") : path.join(clientDir, url, "index.html");
2029
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2030
+ fs.writeFileSync(filePath, html);
2031
+ }
1795
2032
  console.log(
1796
2033
  " Netlify output \u2192 .netlify/functions-internal/ssr/\n Publish dir: dist/client/\n"
1797
2034
  );
@@ -1806,7 +2043,31 @@ function nodeAdapter() {
1806
2043
  async build(ctx) {
1807
2044
  const { fs, path, root } = ctx;
1808
2045
  const entrySource = generateSSREntry(ctx, {
1809
- 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
+ `,
1810
2071
  platformExport: `
1811
2072
  const port = +(process.env.PORT || 3000);
1812
2073
  serve({ fetch: app.fetch, port }, (info) => {
@@ -1825,6 +2086,18 @@ serve({ fetch: app.fetch, port }, (info) => {
1825
2086
  } finally {
1826
2087
  fs.rmSync(tempEntry, { force: true });
1827
2088
  }
2089
+ const prerendered = await prerenderRoutes(ctx);
2090
+ if (prerendered.length > 0) {
2091
+ const prerenderDir = path.resolve(root, "dist/prerender");
2092
+ fs.mkdirSync(prerenderDir, { recursive: true });
2093
+ for (const { url, html } of prerendered) {
2094
+ const filePath = url === "/" ? path.join(prerenderDir, "index.html") : path.join(prerenderDir, url, "index.html");
2095
+ fs.mkdirSync(path.resolve(filePath, ".."), {
2096
+ recursive: true
2097
+ });
2098
+ fs.writeFileSync(filePath, html);
2099
+ }
2100
+ }
1828
2101
  console.log(
1829
2102
  " Node output \u2192 dist/server/index.mjs\n Run: node dist/server/index.mjs\n"
1830
2103
  );
@@ -1853,7 +2126,7 @@ function staticAdapter(opts = {}) {
1853
2126
  ssrPath
1854
2127
  );
1855
2128
  ctx.copyStaticAssets(outputDir, { excludeHtml: true });
1856
- const routePaths = await extractRoutes(ctx, opts);
2129
+ const { paths: routePaths, defs: routeDefs } = await extractRoutesWithModes(ctx, opts);
1857
2130
  const allUrls = [];
1858
2131
  for (const routePath of routePaths) {
1859
2132
  for (const locale of ctx.locales) {
@@ -1872,21 +2145,37 @@ function staticAdapter(opts = {}) {
1872
2145
  ctx.locales,
1873
2146
  ctx.defaultLocale
1874
2147
  );
1875
- const {
1876
- html: appHtml,
1877
- head,
1878
- css,
1879
- serverData
1880
- } = await ssrModule.render(url, locale);
1881
- const serializedData = ssrModule.serializeServerData(serverData);
1882
- const finalHtml = injectSSRForStatic(
1883
- ctx.templateHtml,
1884
- locale,
1885
- head,
1886
- css,
1887
- appHtml,
1888
- serializedData
2148
+ const routeDef = routeDefs.find(
2149
+ (r) => r.path === stripLocalePrefix(url, ctx.locales)
1889
2150
  );
2151
+ const mode = resolveRenderMode(
2152
+ stripLocalePrefix(url, ctx.locales),
2153
+ routeDef?.renderMode,
2154
+ ctx.renderModes
2155
+ );
2156
+ let finalHtml;
2157
+ if (mode === "csr") {
2158
+ finalHtml = injectCSRShellForStatic(
2159
+ ctx.templateHtml,
2160
+ locale
2161
+ );
2162
+ } else {
2163
+ const {
2164
+ html: appHtml,
2165
+ head,
2166
+ css,
2167
+ serverData
2168
+ } = await ssrModule.render(url, locale);
2169
+ const serializedData = ssrModule.serializeServerData(serverData);
2170
+ finalHtml = injectSSRForStatic(
2171
+ ctx.templateHtml,
2172
+ locale,
2173
+ head,
2174
+ css,
2175
+ appHtml,
2176
+ serializedData
2177
+ );
2178
+ }
1890
2179
  const filePath = url === "/" ? path.join(outputDir, "index.html") : path.join(outputDir, url, "index.html");
1891
2180
  fs.mkdirSync(path.resolve(filePath, ".."), {
1892
2181
  recursive: true
@@ -1901,9 +2190,10 @@ function staticAdapter(opts = {}) {
1901
2190
  }
1902
2191
  };
1903
2192
  }
1904
- async function extractRoutes(ctx, opts) {
2193
+ async function extractRoutesWithModes(ctx, opts) {
1905
2194
  const routesFile = opts.routesExport ?? "src/lib/bootstrap.ts";
1906
2195
  const paths = [];
2196
+ const defs = [];
1907
2197
  try {
1908
2198
  const { pathToFileURL } = await import(
1909
2199
  /* @vite-ignore */
@@ -1933,6 +2223,7 @@ async function extractRoutes(ctx, opts) {
1933
2223
  for (const r of routes) {
1934
2224
  if (r.path && !r.path.includes(":")) {
1935
2225
  paths.push(r.path);
2226
+ defs.push({ path: r.path, renderMode: r.renderMode });
1936
2227
  }
1937
2228
  }
1938
2229
  }
@@ -1952,7 +2243,7 @@ async function extractRoutes(ctx, opts) {
1952
2243
  }
1953
2244
  }
1954
2245
  if (paths.length === 0) paths.push("/");
1955
- return paths;
2246
+ return { paths, defs };
1956
2247
  }
1957
2248
  function inferLocale(url, locales, defaultLocale) {
1958
2249
  const segments = url.split("/").filter(Boolean);
@@ -1967,6 +2258,29 @@ function injectSSRForStatic(template, locale, head, css, html, serializedData) {
1967
2258
  '<script id="serialized-server-data" type="application/json">' + serializedData + "</script>"
1968
2259
  );
1969
2260
  }
2261
+ function injectCSRShellForStatic(template, locale) {
2262
+ return template.replace("<!--ssr-lang-->", locale).replace("<!--ssr-head-->", "").replace("<!--ssr-body-->", "").replace("<!--ssr-data-->", "");
2263
+ }
2264
+ function stripLocalePrefix(url, locales) {
2265
+ const segments = url.split("/").filter(Boolean);
2266
+ if (segments.length > 0 && locales.includes(segments[0])) {
2267
+ const rest = segments.slice(1).join("/");
2268
+ return rest ? `/${rest}` : "/";
2269
+ }
2270
+ return url;
2271
+ }
2272
+ function resolveRenderMode(routePath, routeRenderMode, renderModes) {
2273
+ if (renderModes) {
2274
+ if (renderModes[routePath]) return renderModes[routePath];
2275
+ for (const [pattern, mode] of Object.entries(renderModes)) {
2276
+ if (pattern.includes("*")) {
2277
+ const re = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
2278
+ if (re.test(routePath)) return mode;
2279
+ }
2280
+ }
2281
+ }
2282
+ return routeRenderMode ?? "ssr";
2283
+ }
1970
2284
 
1971
2285
  // ../server/src/adapters/vercel.ts
1972
2286
  function vercelAdapter() {
@@ -2015,7 +2329,7 @@ function vercelAdapter() {
2015
2329
  version: 3,
2016
2330
  routes: [
2017
2331
  { handle: "filesystem" },
2018
- { src: "/(.*)", dest: "/ssr" }
2332
+ { src: "/(.*)", dest: "/ssr/$1" }
2019
2333
  ]
2020
2334
  },
2021
2335
  null,
@@ -2025,6 +2339,39 @@ function vercelAdapter() {
2025
2339
  } finally {
2026
2340
  fs.rmSync(tempEntry, { force: true });
2027
2341
  }
2342
+ const prerendered = await prerenderRoutes(ctx);
2343
+ const staticDir = path.resolve(root, ".vercel/output/static");
2344
+ for (const { url, html } of prerendered) {
2345
+ const filePath = url === "/" ? path.join(staticDir, "index.html") : path.join(staticDir, url, "index.html");
2346
+ fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
2347
+ fs.writeFileSync(filePath, html);
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
+ }
2028
2375
  console.log(" Vercel output \u2192 .vercel/output/\n");
2029
2376
  }
2030
2377
  };
@@ -2297,6 +2644,18 @@ function resolveSetupFn(mod) {
2297
2644
  const first = Object.values(mod).find((v) => typeof v === "function");
2298
2645
  return first ?? null;
2299
2646
  }
2647
+ function matchRenderModeConfig(url, renderModes) {
2648
+ if (!renderModes) return null;
2649
+ const path = url.split("?")[0];
2650
+ if (renderModes[path]) return renderModes[path];
2651
+ for (const [pattern, mode] of Object.entries(renderModes)) {
2652
+ if (pattern.includes("*")) {
2653
+ const re = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
2654
+ if (re.test(path)) return mode;
2655
+ }
2656
+ }
2657
+ return null;
2658
+ }
2300
2659
  function finesoftFrontViteConfig(options = {}) {
2301
2660
  const ssrEntry = options.ssr?.entry ?? "src/ssr.ts";
2302
2661
  let root = process.cwd();
@@ -2376,13 +2735,14 @@ function finesoftFrontViteConfig(options = {}) {
2376
2735
  /* @vite-ignore */
2377
2736
  "hono"
2378
2737
  );
2379
- const { injectSSRContent: injectSSRContent2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
2738
+ const { injectSSRContent: injectSSRContent2, injectCSRShell: injectCSRShell2 } = await Promise.resolve().then(() => (init_src2(), src_exports));
2380
2739
  const { parseAcceptLanguage: parseAcceptLanguage2 } = await Promise.resolve().then(() => (init_locale(), locale_exports));
2381
2740
  const { getRequestListener } = await import(
2382
2741
  /* @vite-ignore */
2383
2742
  "@hono/node-server"
2384
2743
  );
2385
2744
  const app = new HonoClass();
2745
+ const isrCache = /* @__PURE__ */ new Map();
2386
2746
  if (typeof options.setup === "function") {
2387
2747
  await options.setup(app);
2388
2748
  } else if (typeof options.setup === "string") {
@@ -2421,12 +2781,25 @@ function finesoftFrontViteConfig(options = {}) {
2421
2781
  options.locales,
2422
2782
  options.defaultLocale
2423
2783
  );
2784
+ const overrideMode = matchRenderModeConfig(
2785
+ url,
2786
+ options.renderModes
2787
+ );
2788
+ if (overrideMode === "csr") {
2789
+ return c.html(injectCSRShell2(template, locale));
2790
+ }
2791
+ const cached = isrCache.get(url);
2792
+ if (cached) return c.html(cached);
2424
2793
  const {
2425
2794
  html: appHtml,
2426
2795
  head,
2427
2796
  css,
2428
- serverData
2797
+ serverData,
2798
+ renderMode
2429
2799
  } = await ssrModule.render(url, locale);
2800
+ if (renderMode === "csr") {
2801
+ return c.html(injectCSRShell2(template, locale));
2802
+ }
2430
2803
  const serializedData = ssrModule.serializeServerData(serverData);
2431
2804
  const finalHtml = injectSSRContent2({
2432
2805
  template,
@@ -2436,6 +2809,9 @@ function finesoftFrontViteConfig(options = {}) {
2436
2809
  html: appHtml,
2437
2810
  serializedData
2438
2811
  });
2812
+ if (renderMode === "prerender" || overrideMode === "prerender") {
2813
+ isrCache.set(url, finalHtml);
2814
+ }
2439
2815
  return c.html(finalHtml);
2440
2816
  } catch (e) {
2441
2817
  console.error("[SSR Preview Error]", e);
@@ -2506,6 +2882,7 @@ function finesoftFrontViteConfig(options = {}) {
2506
2882
  locales,
2507
2883
  defaultLocale,
2508
2884
  templateHtml,
2885
+ renderModes: options.renderModes,
2509
2886
  resolvedResolve,
2510
2887
  resolvedCss,
2511
2888
  vite,
@@ -2565,6 +2942,7 @@ function finesoftFrontViteConfig(options = {}) {
2565
2942
  finesoftFrontViteConfig,
2566
2943
  generateUuid,
2567
2944
  getBaseUrl,
2945
+ injectCSRShell,
2568
2946
  injectSSRContent,
2569
2947
  isCompoundAction,
2570
2948
  isExternalUrlAction,