@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.
package/dist/index.d.cts CHANGED
@@ -151,6 +151,25 @@ interface GenerateSSREntryOptions {
151
151
  platformImport: string;
152
152
  /** 平台特定的导出语句(如 `export default handle(app);`) */
153
153
  platformExport: string;
154
+ /**
155
+ * 平台特定的 ISR 缓存实现代码(可选)。
156
+ * 需提供 `async function platformCacheGet(url)` 返回 string|null,
157
+ * 和 `async function platformCacheSet(url, html)` 的函数定义。
158
+ * 不提供时使用内置的内存 Map 缓存。
159
+ */
160
+ platformCache?: string;
161
+ /**
162
+ * 平台特定的响应后处理代码(可选)。
163
+ * 在 prerender 路由返回前执行,可用于设置 CDN 缓存头等。
164
+ * 代码中可使用变量 `c`(Hono Context)。
165
+ */
166
+ platformPrerenderResponseHook?: string;
167
+ /**
168
+ * 平台特定的中间件代码(可选)。
169
+ * 插入在 catch-all GET 路由之前,可用于添加静态文件服务等。
170
+ * 代码中可使用变量 `app`(Hono 实例)。
171
+ */
172
+ platformMiddleware?: string;
154
173
  }
155
174
  interface BuildBundleOptions {
156
175
  /** 临时入口文件路径 */
package/dist/index.d.ts CHANGED
@@ -151,6 +151,25 @@ interface GenerateSSREntryOptions {
151
151
  platformImport: string;
152
152
  /** 平台特定的导出语句(如 `export default handle(app);`) */
153
153
  platformExport: string;
154
+ /**
155
+ * 平台特定的 ISR 缓存实现代码(可选)。
156
+ * 需提供 `async function platformCacheGet(url)` 返回 string|null,
157
+ * 和 `async function platformCacheSet(url, html)` 的函数定义。
158
+ * 不提供时使用内置的内存 Map 缓存。
159
+ */
160
+ platformCache?: string;
161
+ /**
162
+ * 平台特定的响应后处理代码(可选)。
163
+ * 在 prerender 路由返回前执行,可用于设置 CDN 缓存头等。
164
+ * 代码中可使用变量 `c`(Hono Context)。
165
+ */
166
+ platformPrerenderResponseHook?: string;
167
+ /**
168
+ * 平台特定的中间件代码(可选)。
169
+ * 插入在 catch-all GET 路由之前,可用于添加静态文件服务等。
170
+ * 代码中可使用变量 `app`(Hono 实例)。
171
+ */
172
+ platformMiddleware?: string;
154
173
  }
155
174
  interface BuildBundleOptions {
156
175
  /** 临时入口文件路径 */
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-H3RNYNSD.js";
11
11
  import {
12
12
  createSSRApp
13
- } from "./chunk-SN3OO3DU.js";
13
+ } from "./chunk-IITKGRCO.js";
14
14
  import {
15
15
  SSR_PLACEHOLDERS,
16
16
  createSSRRender,
@@ -77,6 +77,19 @@ function generateSSREntry(ctx, opts) {
77
77
  const locales = JSON.stringify(ctx.locales);
78
78
  const defaultLocale = JSON.stringify(ctx.defaultLocale);
79
79
  const renderModes = JSON.stringify(ctx.renderModes ?? {});
80
+ const cacheImpl = opts.platformCache ? opts.platformCache : `
81
+ const ISR_CACHE_MAX = 1000;
82
+ const _isrMap = new Map();
83
+ async function platformCacheGet(url) {
84
+ return _isrMap.get(url) ?? null;
85
+ }
86
+ async function platformCacheSet(url, html) {
87
+ if (_isrMap.size >= ISR_CACHE_MAX) {
88
+ const first = _isrMap.keys().next().value;
89
+ _isrMap.delete(first);
90
+ }
91
+ _isrMap.set(url, html);
92
+ }`;
80
93
  return `
81
94
  import { Hono } from "hono";
82
95
  ${opts.platformImport}
@@ -87,6 +100,7 @@ const TEMPLATE = ${JSON.stringify(ctx.templateHtml)};
87
100
  const LOCALES = ${locales};
88
101
  const DEFAULT_LOCALE = ${defaultLocale};
89
102
  const RENDER_MODES = ${renderModes};
103
+ ${cacheImpl}
90
104
 
91
105
  function parseAcceptLanguage(header) {
92
106
  if (!header) return DEFAULT_LOCALE;
@@ -129,10 +143,9 @@ function matchRenderMode(url) {
129
143
  return null;
130
144
  }
131
145
 
132
- const isrCache = new Map();
133
-
134
146
  const app = new Hono();
135
147
  ${setupCall}
148
+ ${opts.platformMiddleware ?? ""}
136
149
 
137
150
  app.get("*", async (c) => {
138
151
  const url = c.req.path + (c.req.url.includes("?") ? "?" + c.req.url.split("?")[1] : "");
@@ -146,7 +159,7 @@ app.get("*", async (c) => {
146
159
  }
147
160
 
148
161
  // ISR \u7F13\u5B58\u547D\u4E2D
149
- const cached = isrCache.get(url);
162
+ const cached = await platformCacheGet(url);
150
163
  if (cached) return c.html(cached);
151
164
 
152
165
  const { html: appHtml, head, css, serverData, renderMode } = await render(url, locale);
@@ -161,7 +174,8 @@ app.get("*", async (c) => {
161
174
 
162
175
  // Prerender ISR \u7F13\u5B58\uFF08\u5305\u62EC Vite \u914D\u7F6E\u8986\u76D6\u548C\u8DEF\u7531\u7EA7\uFF09
163
176
  if (renderMode === "prerender" || overrideMode === "prerender") {
164
- isrCache.set(url, finalHtml);
177
+ await platformCacheSet(url, finalHtml);
178
+ ${opts.platformPrerenderResponseHook ?? ""}
165
179
  }
166
180
 
167
181
  return c.html(finalHtml);
@@ -297,7 +311,29 @@ function cloudflareAdapter() {
297
311
  fs.rmSync(outputDir, { recursive: true, force: true });
298
312
  const entrySource = generateSSREntry(ctx, {
299
313
  platformImport: ``,
300
- platformExport: `export default app;`
314
+ platformExport: `export default app;`,
315
+ // Cloudflare Cache API — 持久化 ISR 缓存到 CDN 边缘节点
316
+ platformCache: `
317
+ const ISR_CACHE_TTL = 3600; // 1 hour
318
+ async function platformCacheGet(url) {
319
+ try {
320
+ const cache = caches.default;
321
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
322
+ const resp = await cache.match(cacheKey);
323
+ if (resp) return await resp.text();
324
+ } catch {}
325
+ return null;
326
+ }
327
+ async function platformCacheSet(url, html) {
328
+ try {
329
+ const cache = caches.default;
330
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
331
+ const resp = new Response(html, {
332
+ headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=" + ISR_CACHE_TTL },
333
+ });
334
+ await cache.put(cacheKey, resp);
335
+ } catch {}
336
+ }`
301
337
  });
302
338
  const tempEntry = path.resolve(root, ".cf-entry.tmp.mjs");
303
339
  fs.writeFileSync(tempEntry, entrySource);
@@ -343,7 +379,25 @@ function netlifyAdapter() {
343
379
  });
344
380
  const entrySource = generateSSREntry(ctx, {
345
381
  platformImport: `import { handle } from "hono/netlify";`,
346
- platformExport: `export default handle(app);`
382
+ platformExport: `export default handle(app);
383
+ export const config = { path: "/*", preferStatic: true };`,
384
+ // Netlify CDN 缓存 — 使用 Netlify-CDN-Cache-Control 头做 ISR
385
+ platformCache: `
386
+ const ISR_SWR_TTL = 3600;
387
+ const ISR_CACHE_MAX = 1000;
388
+ const _isrMap = new Map();
389
+ async function platformCacheGet(url) {
390
+ return _isrMap.get(url) ?? null;
391
+ }
392
+ async function platformCacheSet(url, html) {
393
+ if (_isrMap.size >= ISR_CACHE_MAX) {
394
+ const first = _isrMap.keys().next().value;
395
+ _isrMap.delete(first);
396
+ }
397
+ _isrMap.set(url, html);
398
+ }`,
399
+ platformPrerenderResponseHook: `c.header("Cache-Control", "public, max-age=0, must-revalidate");
400
+ c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`
347
401
  });
348
402
  const tempEntry = path.resolve(root, ".netlify-entry.tmp.mjs");
349
403
  fs.writeFileSync(tempEntry, entrySource);
@@ -383,7 +437,31 @@ function nodeAdapter() {
383
437
  async build(ctx) {
384
438
  const { fs, path, root } = ctx;
385
439
  const entrySource = generateSSREntry(ctx, {
386
- platformImport: `import { serve } from "@hono/node-server";`,
440
+ platformImport: `import { serve } from "@hono/node-server";
441
+ import { readFileSync, existsSync } from "node:fs";
442
+ import { resolve, dirname } from "node:path";
443
+ import { fileURLToPath } from "node:url";`,
444
+ platformMiddleware: `
445
+ // \u9884\u6E32\u67D3\u6587\u4EF6\u4E2D\u95F4\u4EF6\uFF1A\u68C0\u67E5 dist/prerender/ \u4E0B\u662F\u5426\u6709\u5BF9\u5E94\u7684\u9759\u6001 HTML
446
+ const __entry_dirname = dirname(fileURLToPath(import.meta.url));
447
+ const prerenderDir = resolve(__entry_dirname, "../prerender");
448
+
449
+ app.use("*", async (c, next) => {
450
+ const urlPath = c.req.path;
451
+ const candidates = [
452
+ resolve(prerenderDir, "." + urlPath, "index.html"),
453
+ resolve(prerenderDir, "." + urlPath + ".html"),
454
+ ];
455
+ if (urlPath === "/") candidates.unshift(resolve(prerenderDir, "index.html"));
456
+ for (const f of candidates) {
457
+ if (existsSync(f)) {
458
+ const html = readFileSync(f, "utf-8");
459
+ return c.html(html);
460
+ }
461
+ }
462
+ await next();
463
+ });
464
+ `,
387
465
  platformExport: `
388
466
  const port = +(process.env.PORT || 3000);
389
467
  serve({ fetch: app.fetch, port }, (info) => {
@@ -645,7 +723,7 @@ function vercelAdapter() {
645
723
  version: 3,
646
724
  routes: [
647
725
  { handle: "filesystem" },
648
- { src: "/(.*)", dest: "/ssr" }
726
+ { src: "/(.*)", dest: "/ssr/$1" }
649
727
  ]
650
728
  },
651
729
  null,
@@ -662,6 +740,32 @@ function vercelAdapter() {
662
740
  fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
663
741
  fs.writeFileSync(filePath, html);
664
742
  }
743
+ if (prerendered.length > 0) {
744
+ const configPath = path.resolve(
745
+ root,
746
+ ".vercel/output/config.json"
747
+ );
748
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
749
+ config.overrides = config.overrides ?? {};
750
+ for (const { url } of prerendered) {
751
+ const key = url === "/" ? "index.html" : `${url.replace(/^\//, "")}/index.html`;
752
+ config.overrides[key] = {
753
+ path: url === "/" ? "/" : url,
754
+ contentType: "text/html; charset=utf-8"
755
+ };
756
+ }
757
+ const isrRoutes = prerendered.map(({ url }) => ({
758
+ src: `^${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/?$`,
759
+ dest: url,
760
+ has: [{ type: "header", key: "x-vercel-isr" }]
761
+ }));
762
+ config.routes = [
763
+ ...isrRoutes,
764
+ { handle: "filesystem" },
765
+ { src: "/(.*)", dest: "/ssr/$1" }
766
+ ];
767
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
768
+ }
665
769
  console.log(" Vercel output \u2192 .vercel/output/\n");
666
770
  }
667
771
  };
@@ -971,7 +1075,7 @@ function finesoftFrontViteConfig(options = {}) {
971
1075
  /* @vite-ignore */
972
1076
  "hono"
973
1077
  );
974
- const { createSSRApp: createSSRApp2 } = await import("./app-Z3EFLAP2.js");
1078
+ const { createSSRApp: createSSRApp2 } = await import("./app-BPO26FAD.js");
975
1079
  const { getRequestListener } = await import(
976
1080
  /* @vite-ignore */
977
1081
  "@hono/node-server"