@finesoft/front 0.1.20 → 0.1.22

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
@@ -118,6 +118,8 @@ interface AdapterContext {
118
118
  ssrEntry: string;
119
119
  /** setup 文件相对路径(如 "src/proxies.ts"),仅当 options.setup 为 string 时有值 */
120
120
  setupPath?: string;
121
+ /** 路由定义入口文件(用于预渲染时加载路由),如 "src/lib/bootstrap.ts" */
122
+ bootstrapEntry?: string;
121
123
  /** 支持的语言列表 */
122
124
  locales: string[];
123
125
  /** 默认语言 */
@@ -151,6 +153,25 @@ interface GenerateSSREntryOptions {
151
153
  platformImport: string;
152
154
  /** 平台特定的导出语句(如 `export default handle(app);`) */
153
155
  platformExport: string;
156
+ /**
157
+ * 平台特定的 ISR 缓存实现代码(可选)。
158
+ * 需提供 `async function platformCacheGet(url)` 返回 string|null,
159
+ * 和 `async function platformCacheSet(url, html)` 的函数定义。
160
+ * 不提供时使用内置的内存 Map 缓存。
161
+ */
162
+ platformCache?: string;
163
+ /**
164
+ * 平台特定的响应后处理代码(可选)。
165
+ * 在 prerender 路由返回前执行,可用于设置 CDN 缓存头等。
166
+ * 代码中可使用变量 `c`(Hono Context)。
167
+ */
168
+ platformPrerenderResponseHook?: string;
169
+ /**
170
+ * 平台特定的中间件代码(可选)。
171
+ * 插入在 catch-all GET 路由之前,可用于添加静态文件服务等。
172
+ * 代码中可使用变量 `app`(Hono 实例)。
173
+ */
174
+ platformMiddleware?: string;
154
175
  }
155
176
  interface BuildBundleOptions {
156
177
  /** 临时入口文件路径 */
@@ -440,6 +461,11 @@ interface FinesoftFrontViteOptions {
440
461
  * ```
441
462
  */
442
463
  renderModes?: Record<string, "ssr" | "csr" | "prerender">;
464
+ /**
465
+ * 路由定义入口文件(用于预渲染时加载路由),默认 "src/lib/bootstrap.ts"。
466
+ * 如果项目不使用声明式路由定义,可以不设置。
467
+ */
468
+ bootstrapEntry?: string;
443
469
  }
444
470
  declare function finesoftFrontViteConfig(options?: FinesoftFrontViteOptions): {
445
471
  name: string;
package/dist/index.d.ts CHANGED
@@ -118,6 +118,8 @@ interface AdapterContext {
118
118
  ssrEntry: string;
119
119
  /** setup 文件相对路径(如 "src/proxies.ts"),仅当 options.setup 为 string 时有值 */
120
120
  setupPath?: string;
121
+ /** 路由定义入口文件(用于预渲染时加载路由),如 "src/lib/bootstrap.ts" */
122
+ bootstrapEntry?: string;
121
123
  /** 支持的语言列表 */
122
124
  locales: string[];
123
125
  /** 默认语言 */
@@ -151,6 +153,25 @@ interface GenerateSSREntryOptions {
151
153
  platformImport: string;
152
154
  /** 平台特定的导出语句(如 `export default handle(app);`) */
153
155
  platformExport: string;
156
+ /**
157
+ * 平台特定的 ISR 缓存实现代码(可选)。
158
+ * 需提供 `async function platformCacheGet(url)` 返回 string|null,
159
+ * 和 `async function platformCacheSet(url, html)` 的函数定义。
160
+ * 不提供时使用内置的内存 Map 缓存。
161
+ */
162
+ platformCache?: string;
163
+ /**
164
+ * 平台特定的响应后处理代码(可选)。
165
+ * 在 prerender 路由返回前执行,可用于设置 CDN 缓存头等。
166
+ * 代码中可使用变量 `c`(Hono Context)。
167
+ */
168
+ platformPrerenderResponseHook?: string;
169
+ /**
170
+ * 平台特定的中间件代码(可选)。
171
+ * 插入在 catch-all GET 路由之前,可用于添加静态文件服务等。
172
+ * 代码中可使用变量 `app`(Hono 实例)。
173
+ */
174
+ platformMiddleware?: string;
154
175
  }
155
176
  interface BuildBundleOptions {
156
177
  /** 临时入口文件路径 */
@@ -440,6 +461,11 @@ interface FinesoftFrontViteOptions {
440
461
  * ```
441
462
  */
442
463
  renderModes?: Record<string, "ssr" | "csr" | "prerender">;
464
+ /**
465
+ * 路由定义入口文件(用于预渲染时加载路由),默认 "src/lib/bootstrap.ts"。
466
+ * 如果项目不使用声明式路由定义,可以不设置。
467
+ */
468
+ bootstrapEntry?: string;
443
469
  }
444
470
  declare function finesoftFrontViteConfig(options?: FinesoftFrontViteOptions): {
445
471
  name: string;
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);
@@ -203,35 +217,40 @@ function copyStaticAssets(ctx, destDir, opts) {
203
217
  fs.rmSync(path.join(destDir, "index.html"), { force: true });
204
218
  }
205
219
  }
206
- async function prerenderRoutes(ctx, routesExport = "src/lib/bootstrap.ts") {
220
+ async function prerenderRoutes(ctx) {
207
221
  const { fs, path, root, vite } = ctx;
208
222
  const { pathToFileURL } = await import(
209
223
  /* @vite-ignore */
210
224
  "url"
211
225
  );
212
- await vite.build({
213
- root,
214
- build: {
215
- ssr: routesExport,
216
- outDir: path.resolve(root, "dist/server"),
217
- emptyOutDir: false,
218
- rollupOptions: {
219
- output: { entryFileNames: "_routes_prerender.mjs" }
220
- }
221
- },
222
- resolve: ctx.resolvedResolve
223
- });
224
- const routesPath = pathToFileURL(
225
- path.resolve(root, "dist/server/_routes_prerender.mjs")
226
- ).href;
227
- const routesMod = await import(
228
- /* @vite-ignore */
229
- routesPath
230
- );
231
- const routes = routesMod.routes ?? routesMod.default ?? [];
232
- fs.rmSync(path.resolve(root, "dist/server/_routes_prerender.mjs"), {
233
- force: true
234
- });
226
+ const routesExport = ctx.bootstrapEntry ?? "src/lib/bootstrap.ts";
227
+ let routes = [];
228
+ const routesFileExists = fs.existsSync(path.resolve(root, routesExport));
229
+ if (routesFileExists) {
230
+ await vite.build({
231
+ root,
232
+ build: {
233
+ ssr: routesExport,
234
+ outDir: path.resolve(root, "dist/server"),
235
+ emptyOutDir: false,
236
+ rollupOptions: {
237
+ output: { entryFileNames: "_routes_prerender.mjs" }
238
+ }
239
+ },
240
+ resolve: ctx.resolvedResolve
241
+ });
242
+ const routesPath = pathToFileURL(
243
+ path.resolve(root, "dist/server/_routes_prerender.mjs")
244
+ ).href;
245
+ const routesMod = await import(
246
+ /* @vite-ignore */
247
+ routesPath
248
+ );
249
+ routes = routesMod.routes ?? routesMod.default ?? [];
250
+ fs.rmSync(path.resolve(root, "dist/server/_routes_prerender.mjs"), {
251
+ force: true
252
+ });
253
+ }
235
254
  const prerenderPaths = /* @__PURE__ */ new Set();
236
255
  for (const r of routes) {
237
256
  if (r.renderMode === "prerender" && r.path && !r.path.includes(":")) {
@@ -297,7 +316,29 @@ function cloudflareAdapter() {
297
316
  fs.rmSync(outputDir, { recursive: true, force: true });
298
317
  const entrySource = generateSSREntry(ctx, {
299
318
  platformImport: ``,
300
- platformExport: `export default app;`
319
+ platformExport: `export default app;`,
320
+ // Cloudflare Cache API — 持久化 ISR 缓存到 CDN 边缘节点
321
+ platformCache: `
322
+ const ISR_CACHE_TTL = 3600; // 1 hour
323
+ async function platformCacheGet(url) {
324
+ try {
325
+ const cache = caches.default;
326
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
327
+ const resp = await cache.match(cacheKey);
328
+ if (resp) return await resp.text();
329
+ } catch {}
330
+ return null;
331
+ }
332
+ async function platformCacheSet(url, html) {
333
+ try {
334
+ const cache = caches.default;
335
+ const cacheKey = new Request("https://isr-cache/" + encodeURIComponent(url));
336
+ const resp = new Response(html, {
337
+ headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=" + ISR_CACHE_TTL },
338
+ });
339
+ await cache.put(cacheKey, resp);
340
+ } catch {}
341
+ }`
301
342
  });
302
343
  const tempEntry = path.resolve(root, ".cf-entry.tmp.mjs");
303
344
  fs.writeFileSync(tempEntry, entrySource);
@@ -343,7 +384,25 @@ function netlifyAdapter() {
343
384
  });
344
385
  const entrySource = generateSSREntry(ctx, {
345
386
  platformImport: `import { handle } from "hono/netlify";`,
346
- platformExport: `export default handle(app);`
387
+ platformExport: `export default handle(app);
388
+ export const config = { path: "/*", preferStatic: true };`,
389
+ // Netlify CDN 缓存 — 使用 Netlify-CDN-Cache-Control 头做 ISR
390
+ platformCache: `
391
+ const ISR_SWR_TTL = 3600;
392
+ const ISR_CACHE_MAX = 1000;
393
+ const _isrMap = new Map();
394
+ async function platformCacheGet(url) {
395
+ return _isrMap.get(url) ?? null;
396
+ }
397
+ async function platformCacheSet(url, html) {
398
+ if (_isrMap.size >= ISR_CACHE_MAX) {
399
+ const first = _isrMap.keys().next().value;
400
+ _isrMap.delete(first);
401
+ }
402
+ _isrMap.set(url, html);
403
+ }`,
404
+ platformPrerenderResponseHook: `c.header("Cache-Control", "public, max-age=0, must-revalidate");
405
+ c.header("Netlify-CDN-Cache-Control", "public, max-age=" + ISR_SWR_TTL + ", stale-while-revalidate=" + ISR_SWR_TTL + ", durable");`
347
406
  });
348
407
  const tempEntry = path.resolve(root, ".netlify-entry.tmp.mjs");
349
408
  fs.writeFileSync(tempEntry, entrySource);
@@ -383,7 +442,31 @@ function nodeAdapter() {
383
442
  async build(ctx) {
384
443
  const { fs, path, root } = ctx;
385
444
  const entrySource = generateSSREntry(ctx, {
386
- platformImport: `import { serve } from "@hono/node-server";`,
445
+ platformImport: `import { serve } from "@hono/node-server";
446
+ import { readFileSync, existsSync } from "node:fs";
447
+ import { resolve, dirname } from "node:path";
448
+ import { fileURLToPath } from "node:url";`,
449
+ platformMiddleware: `
450
+ // \u9884\u6E32\u67D3\u6587\u4EF6\u4E2D\u95F4\u4EF6\uFF1A\u68C0\u67E5 dist/prerender/ \u4E0B\u662F\u5426\u6709\u5BF9\u5E94\u7684\u9759\u6001 HTML
451
+ const __entry_dirname = dirname(fileURLToPath(import.meta.url));
452
+ const prerenderDir = resolve(__entry_dirname, "../prerender");
453
+
454
+ app.use("*", async (c, next) => {
455
+ const urlPath = c.req.path;
456
+ const candidates = [
457
+ resolve(prerenderDir, "." + urlPath, "index.html"),
458
+ resolve(prerenderDir, "." + urlPath + ".html"),
459
+ ];
460
+ if (urlPath === "/") candidates.unshift(resolve(prerenderDir, "index.html"));
461
+ for (const f of candidates) {
462
+ if (existsSync(f)) {
463
+ const html = readFileSync(f, "utf-8");
464
+ return c.html(html);
465
+ }
466
+ }
467
+ await next();
468
+ });
469
+ `,
387
470
  platformExport: `
388
471
  const port = +(process.env.PORT || 3000);
389
472
  serve({ fetch: app.fetch, port }, (info) => {
@@ -645,7 +728,7 @@ function vercelAdapter() {
645
728
  version: 3,
646
729
  routes: [
647
730
  { handle: "filesystem" },
648
- { src: "/(.*)", dest: "/ssr" }
731
+ { src: "/(.*)", dest: "/ssr/$1" }
649
732
  ]
650
733
  },
651
734
  null,
@@ -662,6 +745,32 @@ function vercelAdapter() {
662
745
  fs.mkdirSync(path.resolve(filePath, ".."), { recursive: true });
663
746
  fs.writeFileSync(filePath, html);
664
747
  }
748
+ if (prerendered.length > 0) {
749
+ const configPath = path.resolve(
750
+ root,
751
+ ".vercel/output/config.json"
752
+ );
753
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
754
+ config.overrides = config.overrides ?? {};
755
+ for (const { url } of prerendered) {
756
+ const key = url === "/" ? "index.html" : `${url.replace(/^\//, "")}/index.html`;
757
+ config.overrides[key] = {
758
+ path: url === "/" ? "/" : url,
759
+ contentType: "text/html; charset=utf-8"
760
+ };
761
+ }
762
+ const isrRoutes = prerendered.map(({ url }) => ({
763
+ src: `^${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/?$`,
764
+ dest: url,
765
+ has: [{ type: "header", key: "x-vercel-isr" }]
766
+ }));
767
+ config.routes = [
768
+ ...isrRoutes,
769
+ { handle: "filesystem" },
770
+ { src: "/(.*)", dest: "/ssr/$1" }
771
+ ];
772
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
773
+ }
665
774
  console.log(" Vercel output \u2192 .vercel/output/\n");
666
775
  }
667
776
  };
@@ -971,7 +1080,7 @@ function finesoftFrontViteConfig(options = {}) {
971
1080
  /* @vite-ignore */
972
1081
  "hono"
973
1082
  );
974
- const { createSSRApp: createSSRApp2 } = await import("./app-Z3EFLAP2.js");
1083
+ const { createSSRApp: createSSRApp2 } = await import("./app-BPO26FAD.js");
975
1084
  const { getRequestListener } = await import(
976
1085
  /* @vite-ignore */
977
1086
  "@hono/node-server"
@@ -1165,6 +1274,7 @@ function finesoftFrontViteConfig(options = {}) {
1165
1274
  root,
1166
1275
  ssrEntry,
1167
1276
  setupPath: typeof options.setup === "string" ? options.setup : void 0,
1277
+ bootstrapEntry: options.bootstrapEntry,
1168
1278
  locales,
1169
1279
  defaultLocale,
1170
1280
  templateHtml,