@finesoft/front 0.4.0 → 0.4.2

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.
@@ -193,21 +193,18 @@ The adapter serves these static files directly. No controller runs at request ti
193
193
 
194
194
  ### Incremental Static Regeneration (ISR)
195
195
 
196
- The bundled server (`createServer`) and the preview server (`vp preview`) support cached on-demand regeneration. Configure via `finesoftFrontViteConfig`:
196
+ The bundled server (`createServer`) and the preview server (`vp preview`) also cache `prerender` routes at runtime: a route is rendered on its **first** request and the HTML kept in an in-memory LRU (`ISR_CACHE_MAX = 1000` entries, evicted least-recently-used). Subsequent requests serve the cached HTML without re-running the controller.
197
+
198
+ Mark routes `prerender` per route (`renderMode: "prerender"`) or per glob via the Vite plugin (config-level wins over route-level):
197
199
 
198
200
  ```ts
199
201
  finesoftFrontViteConfig({
200
202
  ssr: { entry: "src/ssr.ts" },
201
- isr: {
202
- // routes that should regenerate on demand
203
- routes: ["/blog/*"],
204
- // cache TTL in seconds
205
- ttl: 300,
206
- },
203
+ renderModes: { "/blog/*": "prerender" },
207
204
  });
208
205
  ```
209
206
 
210
- The first request after expiry triggers a fresh render; concurrent requests get the stale version until the regeneration completes. See [server & deployment](./09-server-and-deployment.md#isr) for details.
207
+ The runtime cache has **no TTL and no background regeneration** entries live until LRU-evicted or the process restarts. Time-based stale-while-revalidate is delegated to the CDN by the platform adapters (Netlify emits a real `stale-while-revalidate` header; Cloudflare a plain `max-age`; node/Vercel none). See [server & deployment](./09-server-and-deployment.md#isr-incremental-static-regeneration) for the full picture.
211
208
 
212
209
  ## `PrefetchedIntents` — the SSR → CSR bridge
213
210
 
@@ -22,7 +22,7 @@ export default defineConfig({
22
22
  i18n: { messagesDir: "src/locales" },
23
23
  proxies: [{ prefix: "/api", target: "https://upstream.example" }],
24
24
  adapter: "auto",
25
- isr: { routes: ["/blog/*"], ttl: 300 },
25
+ renderModes: { "/blog/*": "prerender" },
26
26
  }),
27
27
  ],
28
28
  });
@@ -30,13 +30,13 @@ export default defineConfig({
30
30
 
31
31
  ### Options
32
32
 
33
- | Option | Type | Notes |
34
- | ------------------ | ------------------------- | ------------------------------------------------------- |
35
- | `ssr.entry` | `string` | Path to your SSR entry (default `src/ssr.ts`). |
36
- | `i18n.messagesDir` | `string` | Folder with `{locale}.json` files (default off). |
37
- | `proxies` | `ProxyRouteConfig[]` | Declarative API forwarding. See below. |
38
- | `adapter` | `"auto" \| "node" \| ...` | Target platform. `"auto"` detects from env vars. |
39
- | `isr` | `{ routes, ttl }` | Incremental Static Regeneration for prerendered routes. |
33
+ | Option | Type | Notes |
34
+ | ------------------ | ---------------------------- | ------------------------------------------------------------------------------ |
35
+ | `ssr.entry` | `string` | Path to your SSR entry (default `src/ssr.ts`). |
36
+ | `i18n.messagesDir` | `string` | Folder with `{locale}.json` files (default off). |
37
+ | `proxies` | `ProxyRouteConfig[]` | Declarative API forwarding. See below. |
38
+ | `adapter` | `"auto" \| "node" \| ...` | Target platform. `"auto"` detects from env vars. |
39
+ | `renderModes` | `Record<string, RenderMode>` | Per-route render-mode override (glob keys); `"prerender"` enables ISR caching. |
40
40
 
41
41
  ### What it does
42
42
 
@@ -55,21 +55,20 @@ In build:
55
55
 
56
56
  ## `createServer` — the standalone Hono server
57
57
 
58
- For Node deployments and tests, the framework exports a function that gives you a ready-to-run Hono app:
58
+ For Node deployments and tests, the framework exports an async factory. It loads `.env`, detects the runtime, builds the Hono app, registers proxies + your `setup` routes, mounts the SSR catch-all, and **starts listening** (port from config or `PORT`, default `3000`) — then returns `{ app, vite, runtime }`:
59
59
 
60
60
  ```ts
61
61
  import { createServer } from "@finesoft/front";
62
62
 
63
- const app = createServer({
64
- ssrEntry: "./dist/server/ssr.js",
63
+ const { app } = await createServer({
64
+ ssr: { ssrProductionModule: "./dist/server/ssr.js" }, // or ssrEntryPath in dev
65
65
  proxies: [{ prefix: "/api", target: "https://upstream.example" }],
66
- staticDir: "./dist/client",
67
- isr: { routes: ["/blog/*"], ttl: 300 },
66
+ port: 3000,
68
67
  });
69
68
 
70
- // app is a Hono instance — mount it however your runtime expects
71
- import { serve } from "@hono/node-server";
72
- serve({ fetch: app.fetch, port: 3000 });
69
+ // `app` is the started Hono instance — export it for serverless runtimes whose
70
+ // adapter imports the fetch handler (Vercel / Cloudflare / Netlify).
71
+ export { app };
73
72
  ```
74
73
 
75
74
  ### What it includes
@@ -149,48 +148,58 @@ This works for most CI environments — Vercel / Cloudflare / Netlify all set th
149
148
 
150
149
  ## ISR (Incremental Static Regeneration)
151
150
 
151
+ Mark routes `prerender` — per route (`renderMode: "prerender"`) or per glob via the Vite plugin's `renderModes` (config-level wins over route-level):
152
+
152
153
  ```ts
153
- isr: {
154
- routes: ["/blog/*", "/products/*"],
155
- ttl: 300, // seconds
156
- }
154
+ finesoftFrontViteConfig({
155
+ ssr: { entry: "src/ssr.ts" },
156
+ renderModes: { "/blog/*": "prerender", "/products/*": "prerender" },
157
+ });
157
158
  ```
158
159
 
159
- How it works:
160
+ A `prerender` route is served two ways:
160
161
 
161
- 1. First request to `/blog/hello-world`: render fully, cache the HTML, set expiry to now + 300s
162
- 2. Subsequent requests within TTL: serve cached HTML directly
163
- 3. After expiry: next request triggers re-render; concurrent requests get stale HTML until re-render finishes
162
+ 1. **Build-time static** the static adapter renders each prerender route at build and writes `dist/<route>.html` (one per locale when i18n is on). Served as plain static files; no controller runs at request time.
163
+ 2. **Runtime cache** — the bundled server (`createServer`) and `vp preview` render a prerender route on its **first** request and store the HTML in an in-memory LRU (`ISR_CACHE_MAX = 1000` entries, evicted least-recently-used). Subsequent requests serve the cached HTML without re-running the controller.
164
164
 
165
- The cache is in-memory per server instance. For multi-instance deployments where consistency matters, put a CDN in front and use HTTP `Cache-Control` headers instead.
165
+ > **No TTL, no background regeneration.** The runtime cache has no time-based expiry and no stale-while-revalidate — an entry lives until it is LRU-evicted or the process restarts. The "regenerate after N seconds" semantics live at the **CDN**, not in the framework (below). There is no `isr` config option and no programmatic invalidation API.
166
166
 
167
- Routes not matched by `isr.routes` always render fresh.
167
+ ### Stale-while-revalidate is delegated to the CDN
168
168
 
169
- ### Cache invalidation
169
+ Platform adapters set cache headers on prerender responses so the edge does the real ISR:
170
170
 
171
- Programmatic invalidation is not exposed in the public API. To force a refresh:
171
+ | Adapter | Header on prerender responses |
172
+ | ------------------------- | ------------------------------------------------------------------------------------------ |
173
+ | Netlify | `Netlify-CDN-Cache-Control: max-age=3600, stale-while-revalidate=3600, durable` (true SWR) |
174
+ | Cloudflare | `Cache-Control: public, max-age=3600` |
175
+ | Node (self-host) / Vercel | none — relies on the in-memory LRU |
176
+
177
+ The `3600`s window is a hard-coded per-adapter constant, not user-configurable. For multi-instance / multi-region deployments the CDN headers are what give you consistent caching; the in-memory LRU is per-instance single-server serving.
178
+
179
+ ### Cache invalidation
172
180
 
173
- - Restart the server (loses entire cache)
174
- - Wait for TTL
175
- - Add a cache-busting query param the controller can ignore but that bypasses the cache key
181
+ There is no programmatic invalidation API. To force a refresh:
176
182
 
177
- For production, push invalidation up to CDN level — the framework's in-memory cache is for single-instance serving.
183
+ - Restart the server (clears the entire in-memory LRU)
184
+ - Redeploy (rebuilds build-time static and resets caches)
185
+ - On Netlify / Cloudflare, purge the CDN cache for the path
178
186
 
179
187
  ## Custom Hono middleware
180
188
 
181
- If you need server logic outside the proxy and SSR (e.g., a webhook endpoint, a health check), mount it on the same Hono app:
189
+ If you need server logic outside the proxy and SSR (e.g., a webhook endpoint, a health check), register it via the `setup` hook — it runs after proxies but **before** the SSR catch-all, so your routes win:
182
190
 
183
191
  ```ts
184
- const app = createServer({ ssrEntry: "./dist/server/ssr.js" });
185
-
186
- app.get("/health", (c) => c.json({ status: "ok" }));
187
- app.post("/webhook", async (c) => {
188
- const body = await c.req.json();
189
- await handleWebhook(body);
190
- return c.json({ ok: true });
192
+ await createServer({
193
+ ssr: { ssrProductionModule: "./dist/server/ssr.js" },
194
+ setup: (app) => {
195
+ app.get("/health", (c) => c.json({ status: "ok" }));
196
+ app.post("/webhook", async (c) => {
197
+ const body = await c.req.json();
198
+ await handleWebhook(body);
199
+ return c.json({ ok: true });
200
+ });
201
+ },
191
202
  });
192
-
193
- // SSR catch-all is registered last by createServer — your routes win.
194
203
  ```
195
204
 
196
205
  ## Environment variables
@@ -215,25 +224,16 @@ framework.container.register("config", () => ({
215
224
  For Node deployments behind a load balancer:
216
225
 
217
226
  ```ts
218
- import { serve } from "@hono/node-server";
219
-
220
- const app = createServer({
221
- /* ... */
227
+ await createServer({
228
+ ssr: { ssrProductionModule: "./dist/server/ssr.js" },
229
+ setup: (app) => app.get("/health", (c) => c.json({ ok: true })),
222
230
  });
223
- app.get("/health", (c) => c.json({ ok: true }));
224
-
225
- const server = serve({ fetch: app.fetch, port: 3000 });
226
231
 
227
- process.on("SIGTERM", () => {
228
- server.close(() => {
229
- // dispose Framework if you held a reference
230
- framework.dispose();
231
- process.exit(0);
232
- });
233
- });
232
+ // createServer starts the listener itself — no manual serve() needed.
233
+ process.on("SIGTERM", () => process.exit(0));
234
234
  ```
235
235
 
236
- `framework.dispose()` recursively disposes the container, calls `destroy()` on registered recorders/loggers, and unregisters all routes.
236
+ `createServer` does not return the underlying `http.Server`, so there's no built-in `server.close()` connection-drain. If you need graceful draining — or a handle to call `framework.dispose()` (recursively disposes the container, calls `destroy()` on recorders/loggers, unregisters routes) on shutdown — compose the lower level instead: build the Hono app and own framework yourself and `serve()` it so you keep both handles.
237
237
 
238
238
  ## Next
239
239
 
@@ -193,21 +193,18 @@ adapter 直接服务这些静态文件。请求时不跑 Controller。
193
193
 
194
194
  ### 增量静态再生成(ISR)
195
195
 
196
- 打包的服务器(`createServer`)和预览服务器(`vp preview`)支持按需缓存的再生成。通过 `finesoftFrontViteConfig` 配置:
196
+ 打包的服务器(`createServer`)和预览服务器(`vp preview`)会在运行时缓存 `prerender` 路由:路由在**首次**请求时渲染,HTML 存入内存 LRU(`ISR_CACHE_MAX = 1000` 条,按最近最少使用驱逐)。后续请求直接吐缓存、不再跑 controller。
197
+
198
+ 把路由标 `prerender`:路由级(`renderMode: "prerender"`)或经 Vite 插件按 glob 配置(配置级优先于路由级):
197
199
 
198
200
  ```ts
199
201
  finesoftFrontViteConfig({
200
202
  ssr: { entry: "src/ssr.ts" },
201
- isr: {
202
- // 哪些路由按需再生成
203
- routes: ["/blog/*"],
204
- // 缓存 TTL(秒)
205
- ttl: 300,
206
- },
203
+ renderModes: { "/blog/*": "prerender" },
207
204
  });
208
205
  ```
209
206
 
210
- 过期后的首个请求触发新一轮渲染;并发请求拿到陈旧版本直到新版生成完成。详见 [服务器与部署](./09-server-and-deployment.md#isr)。
207
+ 运行时缓存**无 TTL、无后台再生成** —— 条目存活到被 LRU 驱逐或进程重启为止。基于时间的 stale-while-revalidate 由平台 adapter 委托给 CDN(Netlify 发真正的 `stale-while-revalidate` 头;Cloudflare 发普通 `max-age`;node/Vercel 不发)。完整说明见 [服务器与部署](./09-server-and-deployment.md#isr增量静态再生成)。
211
208
 
212
209
  ## `PrefetchedIntents` —— SSR → CSR 的桥梁
213
210
 
@@ -22,7 +22,7 @@ export default defineConfig({
22
22
  i18n: { messagesDir: "src/locales" },
23
23
  proxies: [{ prefix: "/api", target: "https://upstream.example" }],
24
24
  adapter: "auto",
25
- isr: { routes: ["/blog/*"], ttl: 300 },
25
+ renderModes: { "/blog/*": "prerender" },
26
26
  }),
27
27
  ],
28
28
  });
@@ -30,13 +30,13 @@ export default defineConfig({
30
30
 
31
31
  ### 选项
32
32
 
33
- | 选项 | 类型 | 说明 |
34
- | ------------------ | ------------------------- | ------------------------------------------- |
35
- | `ssr.entry` | `string` | SSR 入口路径(默认 `src/ssr.ts`)。 |
36
- | `i18n.messagesDir` | `string` | 含 `{locale}.json` 文件的目录(默认关闭)。 |
37
- | `proxies` | `ProxyRouteConfig[]` | 声明式 API 转发。详见下文。 |
38
- | `adapter` | `"auto" \| "node" \| ...` | 目标平台。`"auto"` 从环境变量检测。 |
39
- | `isr` | `{ routes, ttl }` | Prerendered 路由的 ISR|
33
+ | 选项 | 类型 | 说明 |
34
+ | ------------------ | ---------------------------- | ------------------------------------------------------------ |
35
+ | `ssr.entry` | `string` | SSR 入口路径(默认 `src/ssr.ts`)。 |
36
+ | `i18n.messagesDir` | `string` | 含 `{locale}.json` 文件的目录(默认关闭)。 |
37
+ | `proxies` | `ProxyRouteConfig[]` | 声明式 API 转发。详见下文。 |
38
+ | `adapter` | `"auto" \| "node" \| ...` | 目标平台。`"auto"` 从环境变量检测。 |
39
+ | `renderModes` | `Record<string, RenderMode>` | 按路由覆盖渲染模式(glob 键);`"prerender"` 启用 ISR 缓存。 |
40
40
 
41
41
  ### 它做什么
42
42
 
@@ -55,21 +55,20 @@ Build:
55
55
 
56
56
  ## `createServer` —— 独立 Hono 服务器
57
57
 
58
- Node 部署和测试时,框架导出一个函数给你一个开箱即用的 Hono app
58
+ Node 部署和测试时,框架导出一个 async 工厂。它加载 `.env`、检测运行时、构建 Hono app、注册 proxies + 你的 `setup` 路由、挂 SSR catch-all,并**启动监听**(端口取自配置或 `PORT`,默认 `3000`)—— 然后返回 `{ app, vite, runtime }`:
59
59
 
60
60
  ```ts
61
61
  import { createServer } from "@finesoft/front";
62
62
 
63
- const app = createServer({
64
- ssrEntry: "./dist/server/ssr.js",
63
+ const { app } = await createServer({
64
+ ssr: { ssrProductionModule: "./dist/server/ssr.js" }, // dev 用 ssrEntryPath
65
65
  proxies: [{ prefix: "/api", target: "https://upstream.example" }],
66
- staticDir: "./dist/client",
67
- isr: { routes: ["/blog/*"], ttl: 300 },
66
+ port: 3000,
68
67
  });
69
68
 
70
- // app Hono 实例 —— 按你的运行时方式挂载
71
- import { serve } from "@hono/node-server";
72
- serve({ fetch: app.fetch, port: 3000 });
69
+ // `app` 是已启动的 Hono 实例 —— 导出给 adapter 导入 fetch handler 的 serverless 运行时
70
+ // (Vercel / Cloudflare / Netlify)。
71
+ export { app };
73
72
  ```
74
73
 
75
74
  ### 包含什么
@@ -149,48 +148,58 @@ serverless 函数下,proxy 逻辑可以内联到部署的函数 bundle 而不
149
148
 
150
149
  ## ISR(增量静态再生成)
151
150
 
151
+ 把路由标 `prerender`:路由级(`renderMode: "prerender"`)或经 Vite 插件按 glob 配置(配置级优先于路由级):
152
+
152
153
  ```ts
153
- isr: {
154
- routes: ["/blog/*", "/products/*"],
155
- ttl: 300, //
156
- }
154
+ finesoftFrontViteConfig({
155
+ ssr: { entry: "src/ssr.ts" },
156
+ renderModes: { "/blog/*": "prerender", "/products/*": "prerender" },
157
+ });
157
158
  ```
158
159
 
159
- 工作原理:
160
+ `prerender` 路由有两种服务方式:
160
161
 
161
- 1. `/blog/hello-world` 的首次请求:完整渲染、缓存 HTML、过期时间设为 now + 300s
162
- 2. TTL 内的后续请求:直接服务缓存的 HTML
163
- 3. 过期后:下个请求触发重渲染;并发请求拿到陈旧 HTML 直到重渲染完成
162
+ 1. **构建期静态化** —— static adapter 在 build 时渲染每个 prerender 路由、写 `dist/<route>.html`(开 i18n 时按 locale 各一份)。当作纯静态文件服务,请求时不跑 controller。
163
+ 2. **运行时缓存** —— 打包服务器(`createServer`)和 `vp preview` 在路由**首次**请求时渲染、把 HTML 存入内存 LRU(`ISR_CACHE_MAX = 1000` 条,按最近最少使用驱逐)。后续请求直接吐缓存、不再跑 controller。
164
164
 
165
- 缓存按服务器实例存内存。多实例部署且一致性重要时,前置 CDN HTTP `Cache-Control` 头。
165
+ > **无 TTL、无后台再生成。** 运行时缓存没有基于时间的过期、也没有 stale-while-revalidate —— 条目存活到被 LRU 驱逐或进程重启。「N 秒后再生成」的语义在 **CDN**、不在框架(见下)。没有 `isr` 配置项,也没有程序化失效 API。
166
166
 
167
- `isr.routes` 不匹配的路由总是新渲染。
167
+ ### stale-while-revalidate 委托给 CDN
168
168
 
169
- ### 缓存失效
169
+ 平台 adapter 在 prerender 响应上设缓存头,让边缘做真正的 ISR:
170
170
 
171
- 公共 API 不暴露程序化失效。要强制刷新:
171
+ | Adapter | prerender 响应的缓存头 |
172
+ | ---------------------- | ----------------------------------------------------------------------------------------- |
173
+ | Netlify | `Netlify-CDN-Cache-Control: max-age=3600, stale-while-revalidate=3600, durable`(真 SWR) |
174
+ | Cloudflare | `Cache-Control: public, max-age=3600` |
175
+ | Node(自托管)/ Vercel | 无 —— 依赖内存 LRU |
176
+
177
+ `3600` 秒窗口是各 adapter 里硬编码的常量,不可配。多实例 / 多区域部署靠 CDN 头保证一致缓存;内存 LRU 是单实例单服务器的服务。
178
+
179
+ ### 缓存失效
172
180
 
173
- - 重启服务器(丢整个缓存)
174
- - 等 TTL
175
- - 加 Controller 能忽略但能绕过缓存 key 的 cache-bust query 参数
181
+ 没有程序化失效 API。要强制刷新:
176
182
 
177
- 生产把失效推到 CDN 层 —— 框架的内存缓存为单实例服务而设。
183
+ - 重启服务器(清空整个内存 LRU)
184
+ - 重新部署(重建构建期静态 + 重置缓存)
185
+ - 在 Netlify / Cloudflare 上 purge 该路径的 CDN 缓存
178
186
 
179
187
  ## 自定义 Hono 中间件
180
188
 
181
- 如果你需要 proxy 和 SSR 之外的服务端逻辑(如 webhook、健康检查),挂到同一个 Hono app 上:
189
+ 如果你需要 proxy 和 SSR 之外的服务端逻辑(如 webhook、健康检查),通过 `setup` 钩子注册 —— 它在 proxies 之后、SSR catch-all **之前**跑,所以你的路由优先:
182
190
 
183
191
  ```ts
184
- const app = createServer({ ssrEntry: "./dist/server/ssr.js" });
185
-
186
- app.get("/health", (c) => c.json({ status: "ok" }));
187
- app.post("/webhook", async (c) => {
188
- const body = await c.req.json();
189
- await handleWebhook(body);
190
- return c.json({ ok: true });
192
+ await createServer({
193
+ ssr: { ssrProductionModule: "./dist/server/ssr.js" },
194
+ setup: (app) => {
195
+ app.get("/health", (c) => c.json({ status: "ok" }));
196
+ app.post("/webhook", async (c) => {
197
+ const body = await c.req.json();
198
+ await handleWebhook(body);
199
+ return c.json({ ok: true });
200
+ });
201
+ },
191
202
  });
192
-
193
- // SSR catch-all 由 createServer 最后注册 —— 你的路由优先。
194
203
  ```
195
204
 
196
205
  ## 环境变量
@@ -215,25 +224,16 @@ framework.container.register("config", () => ({
215
224
  负载均衡器后的 Node 部署:
216
225
 
217
226
  ```ts
218
- import { serve } from "@hono/node-server";
219
-
220
- const app = createServer({
221
- /* ... */
227
+ await createServer({
228
+ ssr: { ssrProductionModule: "./dist/server/ssr.js" },
229
+ setup: (app) => app.get("/health", (c) => c.json({ ok: true })),
222
230
  });
223
- app.get("/health", (c) => c.json({ ok: true }));
224
-
225
- const server = serve({ fetch: app.fetch, port: 3000 });
226
231
 
227
- process.on("SIGTERM", () => {
228
- server.close(() => {
229
- // 如果你拿着 Framework 引用,dispose 它
230
- framework.dispose();
231
- process.exit(0);
232
- });
233
- });
232
+ // createServer 自身启动监听 —— 不需要再手动 serve()
233
+ process.on("SIGTERM", () => process.exit(0));
234
234
  ```
235
235
 
236
- `framework.dispose()` 递归 dispose 容器、对注册的 recorder/logger 调 `destroy()`、注销所有路由。
236
+ `createServer` 不返回底层 `http.Server`,因此没有内建的 `server.close()` 连接 drain。若你需要优雅 drain —— 或需要句柄在关闭时调 `framework.dispose()`(递归 dispose 容器、对 recorder/logger 调 `destroy()`、注销路由)—— 改用更底层的搭建:自己建 Hono app 和 framework 并 `serve()`,从而同时握住两个句柄。
237
237
 
238
238
  ## 下一步
239
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finesoft/front",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Full-stack framework: router, DI, actions, SSR, and server — all in one package",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -32,7 +32,7 @@
32
32
  "@hono/node-server": "^1.19.13",
33
33
  "@types/node": "^22",
34
34
  "dotenv": "^17.3.1",
35
- "hono": "^4.12.12",
35
+ "hono": "^4.12.21",
36
36
  "vite": "npm:@voidzero-dev/vite-plus-core@0.1.16",
37
37
  "vite-plus": "0.1.17"
38
38
  },