@finesoft/front 0.5.1 → 0.5.3
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/README.md +4 -4
- package/dist/Outlet.svelte +39 -0
- package/dist/Outlet.svelte.d.ts +7 -0
- package/dist/browser-CR5vhgXg.mjs +1317 -0
- package/dist/browser-kFMjlLGT.d.mts +262 -0
- package/dist/browser.d.mts +7 -2
- package/dist/browser.mjs +9 -1
- package/dist/controller-types-CgmJ6-le.d.mts +16 -0
- package/dist/cookies-BiRlUanX.d.mts +786 -0
- package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
- package/dist/host-guard-DDWxLpFL.mjs +222 -0
- package/dist/http-B6CJqDyf.d.mts +46 -0
- package/dist/http-CaxrMD1A.d.mts +1 -0
- package/dist/http-D70PL72H.mjs +257 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +2 -0
- package/dist/index-node.d.mts +15 -0
- package/dist/index-node.mjs +15 -0
- package/dist/index.d.mts +48 -697
- package/dist/index.mjs +44 -261
- package/dist/load-node.d.mts +5 -0
- package/dist/load-node.mjs +10 -0
- package/dist/load-portable.d.mts +5 -0
- package/dist/load-portable.mjs +8 -0
- package/dist/lru-map-BKoUAySU.mjs +50 -0
- package/dist/messages-CAt2QdGr.mjs +140 -0
- package/dist/native-contract-DuR25hYB.d.mts +14 -0
- package/dist/native-contract.d.mts +2 -0
- package/dist/native-contract.mjs +1 -0
- package/dist/node-D9hB4dsz.d.mts +35 -0
- package/dist/node.d.mts +2 -0
- package/dist/node.mjs +59 -0
- package/dist/path-CGFl2w7D.mjs +113 -0
- package/dist/path-CXT6xGPO.d.mts +261 -0
- package/dist/portable-CaxrMD1A.d.mts +1 -0
- package/dist/portable.d.mts +11 -0
- package/dist/portable.mjs +12 -0
- package/dist/proxy-2dSWO-Xw.d.mts +53 -0
- package/dist/proxy-z02VvGIj.mjs +7520 -0
- package/dist/public-types-BcJM-AYc.mjs +835 -0
- package/dist/react-DhwBRw01.d.mts +16 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +29 -0
- package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
- package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
- package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
- package/dist/session-DnB4ZC3x.d.mts +1279 -0
- package/dist/src-BQBfaaPO.mjs +3825 -0
- package/dist/src-Ftl_0rhu.mjs +28 -0
- package/dist/ssr-BLzYP4wU.d.mts +207 -0
- package/dist/ssr-Tn4YkuxM.mjs +357 -0
- package/dist/ssr.d.mts +3 -0
- package/dist/ssr.mjs +3 -0
- package/dist/svelte-Dr5to3SE.d.mts +16 -0
- package/dist/svelte.d.mts +3 -0
- package/dist/svelte.mjs +13 -0
- package/dist/typegen-C-WeJCtf.d.mts +12 -0
- package/dist/typegen-cli.d.mts +1 -0
- package/dist/typegen-cli.mjs +11 -0
- package/dist/typegen.d.mts +3 -0
- package/dist/typegen.mjs +2 -0
- package/dist/types-B1BT0N3t.mjs +402 -0
- package/dist/undici-CPfL25Hr.mjs +22262 -0
- package/dist/vite-Cj4SPA8D.d.mts +277 -0
- package/dist/vite.d.mts +4 -0
- package/dist/vite.mjs +2354 -0
- package/dist/vue-DGmzuKho.d.mts +32 -0
- package/dist/vue.d.mts +3 -0
- package/dist/vue.mjs +56 -0
- package/dist/web.d.mts +6 -0
- package/dist/web.mjs +8 -0
- package/dist/worker.d.mts +2 -0
- package/dist/worker.mjs +2 -0
- package/docs/01-getting-started.md +67 -199
- package/docs/02-routing-and-controllers.md +163 -241
- package/docs/03-middleware.md +10 -212
- package/docs/04-rendering-and-hydration.md +7 -332
- package/docs/05-i18n.md +7 -237
- package/docs/06-http-client.md +20 -263
- package/docs/07-di-container.md +23 -257
- package/docs/08-observability.md +6 -286
- package/docs/09-server-and-deployment.md +59 -219
- package/docs/10-features-platform-pwa.md +7 -231
- package/docs/11-navigation.md +143 -288
- package/docs/12-session-restoration.md +6 -214
- package/docs/README.md +7 -7
- package/docs/advanced/custom-action-handler.md +29 -229
- package/docs/advanced/custom-adapter.md +7 -259
- package/docs/advanced/custom-event-recorder.md +7 -312
- package/docs/advanced/inline-proxy-codegen.md +6 -185
- package/docs/advanced/multi-tenant-scopes.md +10 -323
- package/docs/engineering/ci-release-flow.md +35 -222
- package/docs/engineering/project-structure.md +34 -277
- package/docs/engineering/testing.md +8 -310
- package/docs/pitfalls/container-scope-leak.md +2 -214
- package/docs/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/pitfalls/ssr-hydration-mismatch.md +9 -155
- package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/docs/zh/01-getting-started.md +67 -199
- package/docs/zh/02-routing-and-controllers.md +166 -244
- package/docs/zh/03-middleware.md +10 -212
- package/docs/zh/04-rendering-and-hydration.md +7 -332
- package/docs/zh/05-i18n.md +7 -237
- package/docs/zh/06-http-client.md +20 -263
- package/docs/zh/07-di-container.md +23 -257
- package/docs/zh/08-observability.md +6 -283
- package/docs/zh/09-server-and-deployment.md +59 -219
- package/docs/zh/10-features-platform-pwa.md +7 -231
- package/docs/zh/11-navigation.md +130 -290
- package/docs/zh/12-session-restoration.md +6 -214
- package/docs/zh/README.md +4 -4
- package/docs/zh/advanced/custom-action-handler.md +29 -229
- package/docs/zh/advanced/custom-adapter.md +7 -259
- package/docs/zh/advanced/custom-event-recorder.md +7 -312
- package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
- package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
- package/docs/zh/engineering/ci-release-flow.md +35 -222
- package/docs/zh/engineering/project-structure.md +34 -277
- package/docs/zh/engineering/testing.md +8 -310
- package/docs/zh/pitfalls/container-scope-leak.md +2 -214
- package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/zh/pitfalls/ssr-hydration-mismatch.md +9 -155
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/package.json +118 -20
- package/dist/browser-BHhVWXik.mjs +0 -2
- package/dist/browser-BV2BBXm7.d.mts +0 -2811
|
@@ -1,242 +1,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# HTTP and deployment hosts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Portable data handlers return standard Responses. Bind the same operation to Node or Worker without pages, DOM, Hono, or Vite in its business module.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- `createServer` — the standalone Hono server
|
|
7
|
-
- The proxy router — declarative API forwarding with SSRF / binary integrity guards
|
|
8
|
-
- Adapters — Node, Vercel, Cloudflare, Netlify, static
|
|
9
|
-
|
|
10
|
-
## The Vite plugin
|
|
5
|
+
## Data endpoint / 数据接口
|
|
11
6
|
|
|
12
7
|
```ts
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// ... view layer plugin (Vue/React/Svelte)
|
|
20
|
-
finesoftFrontViteConfig({
|
|
21
|
-
ssr: { entry: "src/ssr.ts" },
|
|
22
|
-
i18n: { messagesDir: "src/locales" },
|
|
23
|
-
proxies: [{ prefix: "/api", target: "https://upstream.example" }],
|
|
24
|
-
adapter: "auto",
|
|
25
|
-
renderModes: { "/blog/*": "prerender" },
|
|
26
|
-
}),
|
|
27
|
-
],
|
|
8
|
+
import { defineApp, defineOperation, createRuntime, ExecutionError } from "@finesoft/front";
|
|
9
|
+
import { defineEndpoint } from "@finesoft/front";
|
|
10
|
+
export const double = defineOperation({
|
|
11
|
+
id: "double",
|
|
12
|
+
kind: "query",
|
|
13
|
+
handler: (value: number) => value * 2,
|
|
28
14
|
});
|
|
15
|
+
export const endpoints = [
|
|
16
|
+
defineEndpoint({
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: "/double",
|
|
19
|
+
operation: double,
|
|
20
|
+
decode: async (request) => {
|
|
21
|
+
const body: unknown = await request.json();
|
|
22
|
+
if (typeof body !== "number" || !Number.isFinite(body))
|
|
23
|
+
throw new ExecutionError("validation");
|
|
24
|
+
return body;
|
|
25
|
+
},
|
|
26
|
+
encode: (value) => Response.json({ value }),
|
|
27
|
+
}),
|
|
28
|
+
];
|
|
29
|
+
export function createDataApp() {
|
|
30
|
+
const runtime = createRuntime({ app: defineApp({ id: "data-app", operations: [double] }) });
|
|
31
|
+
return { runtime, endpoints };
|
|
32
|
+
}
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
|
|
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
|
-
| `renderModes` | `Record<string, RenderMode>` | Per-route render-mode override (glob keys); `"prerender"` enables ISR caching. |
|
|
40
|
-
|
|
41
|
-
### What it does
|
|
42
|
-
|
|
43
|
-
In dev:
|
|
44
|
-
|
|
45
|
-
- Starts a Hono server that runs your SSR entry on every request
|
|
46
|
-
- Hot-reloads SSR code via Vite's module graph
|
|
47
|
-
- Serves the proxy routes locally so client-side `fetch("/api/...")` works
|
|
48
|
-
|
|
49
|
-
In build:
|
|
50
|
-
|
|
51
|
-
- Bundles the client bundle with Vite's standard pipeline
|
|
52
|
-
- Bundles the SSR entry as a separate module
|
|
53
|
-
- Generates an adapter-specific entry file (`vercel.func`, `_worker.js`, `node-server.js`, etc.)
|
|
54
|
-
- Prerenders any `renderMode: "prerender"` routes to static HTML
|
|
55
|
-
|
|
56
|
-
## `createServer` — the standalone Hono server
|
|
57
|
-
|
|
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 }`:
|
|
35
|
+
## Separate platform entries / 独立平台入口
|
|
59
36
|
|
|
60
37
|
```ts
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
38
|
+
// node.ts
|
|
39
|
+
import { startNodeHandler } from "@finesoft/front";
|
|
40
|
+
import { createHttpHandler } from "@finesoft/front";
|
|
41
|
+
import { createDataApp } from "./data-app";
|
|
42
|
+
const options = createDataApp();
|
|
43
|
+
const handler = createHttpHandler(options);
|
|
44
|
+
const server = await startNodeHandler({
|
|
45
|
+
handler,
|
|
66
46
|
port: 3000,
|
|
47
|
+
disposeApp: () => options.runtime.dispose(),
|
|
67
48
|
});
|
|
49
|
+
// await server.dispose();
|
|
68
50
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### What it includes
|
|
75
|
-
|
|
76
|
-
- Static file serving for the client bundle
|
|
77
|
-
- All your proxy routes (registered via `registerProxyRoutes`)
|
|
78
|
-
- SSR rendering with full middleware pipeline
|
|
79
|
-
- ISR cache for prerendered routes
|
|
80
|
-
- Locale resolution from `Accept-Language`
|
|
81
|
-
|
|
82
|
-
## Proxy routes
|
|
83
|
-
|
|
84
|
-
Declarative API forwarding with built-in SSRF protection, binary-safe forwarding, and configurable auth/cache.
|
|
85
|
-
|
|
86
|
-
### Basic config
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
proxies: [
|
|
90
|
-
{
|
|
91
|
-
prefix: "/api", // must start with /
|
|
92
|
-
target: "https://api.example.com", // must be https:// or http://
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Now `GET /api/users/42` → `GET https://api.example.com/users/42`. Query params and request headers are forwarded.
|
|
98
|
-
|
|
99
|
-
### Full options
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
{
|
|
103
|
-
prefix: "/api/apple",
|
|
104
|
-
target: "https://api.music.apple.com",
|
|
105
|
-
methods: ["get", "post"], // default ["all"]
|
|
106
|
-
headers: { "X-App": "finesoft" }, // injected per request
|
|
107
|
-
auth: { type: "bearer", envKey: "APPLE_TOKEN" }, // reads process.env.APPLE_TOKEN
|
|
108
|
-
cache: "public, max-age=60", // Cache-Control on response
|
|
109
|
-
followRedirects: false, // default false (redirect: "manual")
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`auth.type`: `"bearer"` → `Authorization: Bearer <token>`. `"basic"` → `Authorization: Basic <token>`. The `envKey` is read at request time, so changing it (or unsetting it) does not require a restart.
|
|
114
|
-
|
|
115
|
-
### What the framework enforces
|
|
116
|
-
|
|
117
|
-
- **SSRF protection**: path is rejected if URL-encoded (any `%`-encoded char), starts with `//`, or contains characters outside the allowed set (`[/\w.\-~%:@!$&'()*+,;=]`). Decoded ≠ raw also rejected (prevents `%2F` smuggling).
|
|
118
|
-
- **Open-redirect protection**: the constructed target URL must have the same `origin` as the configured `target`. Different origin → `400 Invalid proxy target`.
|
|
119
|
-
- **Binary integrity**: response body forwarded via `arrayBuffer()`, not `text()` — preserves bytes exactly. PDF, image, protobuf responses are byte-identical to the upstream response.
|
|
120
|
-
- **Size limit**: 10 MB. `Content-Length` header is checked first for fast rejection; actual body byte length is checked after fetch.
|
|
121
|
-
- **HTTP warning**: any `http://` target logs a warning at startup. Use HTTPS in production.
|
|
122
|
-
|
|
123
|
-
### Generated proxy code (serverless / edge)
|
|
124
|
-
|
|
125
|
-
For serverless functions, the proxy logic is inlined into the deployed function bundle instead of relying on `registerProxyRoutes` at runtime. See [advanced/inline-proxy-codegen](./advanced/inline-proxy-codegen.md).
|
|
126
|
-
|
|
127
|
-
## Adapters
|
|
128
|
-
|
|
129
|
-
| Adapter | Target | Build output |
|
|
130
|
-
| -------------- | -------------------------- | ------------------------------------------------------ |
|
|
131
|
-
| `"node"` | Standalone Node.js server | `dist/server/index.js` — `serve({ fetch: app.fetch })` |
|
|
132
|
-
| `"vercel"` | Vercel Build Output API v3 | `.vercel/output/` with `functions/` and `static/` |
|
|
133
|
-
| `"cloudflare"` | Cloudflare Workers | `dist/_worker.js` + `dist/_routes.json` |
|
|
134
|
-
| `"netlify"` | Netlify Functions v2 | `netlify/functions/` + `_redirects` |
|
|
135
|
-
| `"static"` | Pre-rendered static files | `dist/client/` only (no server) |
|
|
136
|
-
| `"auto"` | Auto-detect at build time | Picks one of the above by environment variable |
|
|
137
|
-
|
|
138
|
-
### Auto-detection
|
|
139
|
-
|
|
140
|
-
`adapter: "auto"` checks (in order):
|
|
141
|
-
|
|
142
|
-
1. `VERCEL=1` → vercel
|
|
143
|
-
2. `CF_PAGES=1` → cloudflare
|
|
144
|
-
3. `NETLIFY=1` → netlify
|
|
145
|
-
4. otherwise → node
|
|
146
|
-
|
|
147
|
-
This works for most CI environments — Vercel / Cloudflare / Netlify all set these automatically during their build.
|
|
148
|
-
|
|
149
|
-
## ISR (Incremental Static Regeneration)
|
|
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
|
-
|
|
153
|
-
```ts
|
|
154
|
-
finesoftFrontViteConfig({
|
|
155
|
-
ssr: { entry: "src/ssr.ts" },
|
|
156
|
-
renderModes: { "/blog/*": "prerender", "/products/*": "prerender" },
|
|
157
|
-
});
|
|
51
|
+
// worker.ts (a separate host entry)
|
|
52
|
+
import { createHttpHandler } from "@finesoft/front";
|
|
53
|
+
import { createDataApp } from "./data-app";
|
|
54
|
+
export default createHttpHandler(createDataApp);
|
|
158
55
|
```
|
|
159
56
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
### Stale-while-revalidate is delegated to the CDN
|
|
168
|
-
|
|
169
|
-
Platform adapters set cache headers on prerender responses so the edge does the real ISR:
|
|
170
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
There is no programmatic invalidation API. To force a refresh:
|
|
182
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
## Custom Hono middleware
|
|
57
|
+
The HTTP owner executes requests through `handler.fetch(request, bindings, host)`. Node and Worker use the same object; `startNodeHandler` takes the object rather than a detached function. A factory argument initializes once inside the first request, avoiding Runtime creation during workerd module evaluation; bindings, cancellation and task hosts remain per request. `createWorkerHandler` has been removed.
|
|
188
58
|
|
|
189
|
-
|
|
59
|
+
## Web build / 页面构建
|
|
190
60
|
|
|
191
61
|
```ts
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
62
|
+
import { defineConfig } from "vite-plus";
|
|
63
|
+
import react from "@vitejs/plugin-react";
|
|
64
|
+
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
plugins: [
|
|
67
|
+
react(),
|
|
68
|
+
finesoftFrontViteConfig({
|
|
69
|
+
adapter: "node",
|
|
70
|
+
ssr: { entry: "src/ssr.ts" },
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
202
73
|
});
|
|
203
74
|
```
|
|
204
75
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
The framework reads:
|
|
208
|
-
|
|
209
|
-
- `NODE_ENV` — `"production"` enables prod-only optimizations
|
|
210
|
-
- `PROXY_TOKEN` / `BASIC_TOKEN` / any `auth.envKey` — proxy auth secrets
|
|
211
|
-
- `VERCEL`, `CF_PAGES`, `NETLIFY` — adapter auto-detection
|
|
212
|
-
|
|
213
|
-
Anything else is yours. Access via `process.env` directly or by registering a config object in the DI container:
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
framework.container.register("config", () => ({
|
|
217
|
-
upstreamUrl: process.env.UPSTREAM_URL ?? "https://api.example.com",
|
|
218
|
-
sessionSecret: requireEnv("SESSION_SECRET"),
|
|
219
|
-
}));
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Health checks and graceful shutdown
|
|
223
|
-
|
|
224
|
-
For Node deployments behind a load balancer:
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
await createServer({
|
|
228
|
-
ssr: { ssrProductionModule: "./dist/server/ssr.js" },
|
|
229
|
-
setup: (app) => app.get("/health", (c) => c.json({ ok: true })),
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// createServer starts the listener itself — no manual serve() needed.
|
|
233
|
-
process.on("SIGTERM", () => process.exit(0));
|
|
234
|
-
```
|
|
76
|
+
Node’s host requires `@hono/node-server`. Worker fetch has no Node compatibility requirement for the portable graph. DNS enforcement belongs to the Node host; an unavailable required capability fails explicitly. Browser networking uses an explicit browser policy. Streaming resources live until consumption, cancellation or failure. Background work uses `runManagedTask` and host `waitUntil`; detached tasks must not retain response-owned resources. The Vite adapters emit thin host modules using the same SSR response assembler; building locally does not deploy or publish.
|
|
235
77
|
|
|
236
|
-
`
|
|
78
|
+
When `setup` is a module path, that module must `export default` its setup function. Development, preview and generated hosts use this explicit export; named functions are not discovered automatically.
|
|
237
79
|
|
|
238
|
-
##
|
|
80
|
+
## Static hosting boundary
|
|
239
81
|
|
|
240
|
-
|
|
241
|
-
- [Engineering: CI & release flow](./engineering/ci-release-flow.md) — automating releases
|
|
242
|
-
- [Pitfalls: proxy binary payloads](./pitfalls/proxy-binary-payloads.md) — why `arrayBuffer` matters
|
|
82
|
+
`staticAdapter` reads built `render.routes` by default, uses the same SSR host to generate HTML and awaits cleanup. `dynamicRoutes` supplies concrete dynamic paths; `routesExport` is an explicit extension. Discovery and rendering failures fail the build. Plain HTML cannot express redirects, error status, Set-Cookie or custom HTTP response headers, so the adapter rejects these responses; choose a request host when they are required.
|
|
@@ -1,238 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Features, platform and PWA
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Portable detection and feature contracts belong to the root entry. Browser-only PWA and DOM behavior belongs to the browser entry.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Platform detection** — user-agent parsing for OS / browser / engine
|
|
7
|
-
- **PWA mode** — detect whether the app is installed (standalone)
|
|
8
|
-
|
|
9
|
-
Each is replaceable, each composable with custom providers.
|
|
10
|
-
|
|
11
|
-
## Feature flags
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
const framework = Framework.create({
|
|
15
|
-
featureFlags: {
|
|
16
|
-
darkMode: true,
|
|
17
|
-
maxRetries: 3,
|
|
18
|
-
experimentalCheckout: false,
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const flags = framework.container.resolve(DEP_KEYS.FEATURE_FLAGS);
|
|
23
|
-
flags.get("darkMode"); // true
|
|
24
|
-
flags.get("maxRetries"); // 3
|
|
25
|
-
flags.get("missing"); // undefined
|
|
26
|
-
flags.get("missing", "fallback"); // "fallback"
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Flags can be any JSON-serializable value: booleans, strings, numbers, arrays, objects.
|
|
30
|
-
|
|
31
|
-
### Static config
|
|
32
|
-
|
|
33
|
-
The simplest case — flags shipped with the bundle:
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
Framework.create({
|
|
37
|
-
featureFlags: {
|
|
38
|
-
darkMode: process.env.NODE_ENV !== "production",
|
|
39
|
-
analytics: true,
|
|
40
|
-
cdnUrl: "https://cdn.example.com",
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Use this for flags driven by environment, not user attributes.
|
|
46
|
-
|
|
47
|
-
### Remote providers
|
|
48
|
-
|
|
49
|
-
Plug in a provider that fetches from a remote service (LaunchDarkly, GrowthBook, Unleash, your own config service):
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
import { type FeatureFlagsProvider } from "@finesoft/front";
|
|
53
|
-
|
|
54
|
-
const remoteConfigProvider: FeatureFlagsProvider = {
|
|
55
|
-
async load() {
|
|
56
|
-
const resp = await fetch("https://config.example.com/flags");
|
|
57
|
-
return resp.json(); // { ...flags }
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const framework = Framework.create({
|
|
62
|
-
featureFlags: {
|
|
63
|
-
darkMode: false,
|
|
64
|
-
maxRetries: 3,
|
|
65
|
-
},
|
|
66
|
-
featureFlagsProviders: [remoteConfigProvider],
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Providers run in registration order. Later providers override earlier values for the same key — "last registered wins."
|
|
71
|
-
|
|
72
|
-
### Cache lifecycle
|
|
73
|
-
|
|
74
|
-
The framework loads provider values once during `Framework.create()`. After that, flags are read synchronously from memory.
|
|
75
|
-
|
|
76
|
-
To refresh, call `flags.refresh()`:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const flags = framework.container.resolve(DEP_KEYS.FEATURE_FLAGS);
|
|
80
|
-
await flags.refresh(); // re-runs all providers
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
You'd typically call this on a timer or in response to a server-sent event.
|
|
84
|
-
|
|
85
|
-
### Targeting
|
|
86
|
-
|
|
87
|
-
Built-in flags are global (same value for every user). For per-user targeting, structure your provider to return a function or use a separate evaluation step:
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
class TargetingProvider implements FeatureFlagsProvider {
|
|
91
|
-
constructor(private userId: string) {}
|
|
92
|
-
async load() {
|
|
93
|
-
const resp = await fetch(`https://config.example.com/flags?userId=${this.userId}`);
|
|
94
|
-
return resp.json();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Register per-request in a beforeLoad guard:
|
|
99
|
-
async function flagsGuard(ctx) {
|
|
100
|
-
const userId = await getUserIdFromCookie(ctx);
|
|
101
|
-
const targeting = new TargetingProvider(userId);
|
|
102
|
-
const flags = await targeting.load();
|
|
103
|
-
ctx.container.register(DEP_KEYS.FEATURE_FLAGS, () => ({
|
|
104
|
-
get: (key, fallback) => flags[key] ?? fallback,
|
|
105
|
-
}));
|
|
106
|
-
return next();
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
For complex bucketing, hand the user id to a dedicated service (GrowthBook SDK, etc.) and store its evaluator in DI.
|
|
111
|
-
|
|
112
|
-
### SSR / CSR consistency
|
|
113
|
-
|
|
114
|
-
Flags evaluated on the server and not re-evaluated on the browser would cause hydration mismatch. The framework serializes flag values into `PrefetchedIntents` if a controller reads them. Browser-side reads return the same value the server saw.
|
|
115
|
-
|
|
116
|
-
For flags that _should_ differ (e.g., A/B variants), evaluate them in a `beforeLoad` guard and store the result in the request scope — both server and browser will use the value resolved by the server.
|
|
117
|
-
|
|
118
|
-
## Platform detection
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
import { detectPlatform } from "@finesoft/front";
|
|
122
|
-
|
|
123
|
-
const info = detectPlatform();
|
|
124
|
-
// {
|
|
125
|
-
// os: "ios" | "android" | "macos" | "windows" | "linux" | "other",
|
|
126
|
-
// browser: "safari" | "chrome" | "firefox" | "edge" | ...,
|
|
127
|
-
// engine: "webkit" | "blink" | "gecko" | "other",
|
|
128
|
-
// isMobile: boolean,
|
|
129
|
-
// isTouch: boolean,
|
|
130
|
-
// isServer: boolean,
|
|
131
|
-
// }
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
In the browser, `detectPlatform()` reads `navigator.userAgent`. On the server, parsing the request's `User-Agent` header is automatic via the framework:
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
const platform = framework.getPlatform();
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
For controllers and guards, resolve from DI:
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
const platform = ctx.container.resolve(DEP_KEYS.PLATFORM);
|
|
144
|
-
if (platform.isMobile) {
|
|
145
|
-
return rewrite("/m" + ctx.url.pathname);
|
|
146
|
-
}
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Reliability
|
|
150
|
-
|
|
151
|
-
User-Agent strings lie — every modern browser embeds substrings of every other browser for compatibility. The framework's detection prioritizes well-known patterns and falls back to `"other"` on ambiguity. Don't make critical decisions on `browser` alone:
|
|
152
|
-
|
|
153
|
-
- ✅ Adjust layout for `isMobile`
|
|
154
|
-
- ✅ Hide Safari-only features for non-WebKit
|
|
155
|
-
- ❌ Lock specific browsers out
|
|
156
|
-
- ❌ Choose code paths based on browser version
|
|
157
|
-
|
|
158
|
-
## PWA detection
|
|
5
|
+
## Imports / 入口
|
|
159
6
|
|
|
160
7
|
```ts
|
|
8
|
+
import { detectPlatform, type FeatureFlagsProvider } from "@finesoft/front";
|
|
161
9
|
import { getPWADisplayMode } from "@finesoft/front";
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// "standalone" | "twa" | "browser"
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
- `"standalone"` — running as an installed PWA (Safari add-to-home, Chrome install)
|
|
168
|
-
- `"twa"` — Trusted Web Activity (Android, wrapped as a native app)
|
|
169
|
-
- `"browser"` — regular browser tab
|
|
170
|
-
|
|
171
|
-
The function reads `window.matchMedia("(display-mode: standalone)")` and Android's TWA referrer. Server-side: returns `"browser"`.
|
|
172
|
-
|
|
173
|
-
### Common uses
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
const mode = getPWADisplayMode();
|
|
177
|
-
|
|
178
|
-
if (mode === "browser") {
|
|
179
|
-
showInstallBanner();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (mode === "standalone") {
|
|
183
|
-
// Customize navigation — installed app shouldn't show "Install" prompt
|
|
184
|
-
hideInstallButton();
|
|
185
|
-
enableNativeBackButtonHandling();
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Service worker registration
|
|
190
|
-
|
|
191
|
-
PWA install is independent of service workers — you can have one without the other. To register a service worker:
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
// src/main.ts
|
|
195
|
-
startBrowserApp({
|
|
196
|
-
bootstrap,
|
|
197
|
-
mount,
|
|
198
|
-
onAfterStart() {
|
|
199
|
-
if ("serviceWorker" in navigator) {
|
|
200
|
-
navigator.serviceWorker.register("/sw.js");
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
The framework does not ship a service worker generator. Use [Vite PWA](https://vite-pwa-org.netlify.app/) or hand-roll one.
|
|
207
|
-
|
|
208
|
-
## Composing them
|
|
209
|
-
|
|
210
|
-
A common navigation guard combining all three:
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
import { next, rewrite, DEP_KEYS } from "@finesoft/front";
|
|
214
|
-
|
|
215
|
-
function mobilePwaGuard(ctx) {
|
|
216
|
-
const platform = ctx.container.resolve(DEP_KEYS.PLATFORM);
|
|
217
|
-
const flags = ctx.container.resolve(DEP_KEYS.FEATURE_FLAGS);
|
|
218
|
-
|
|
219
|
-
if (flags.get("mobilePwaRedesign") && platform.isMobile && !ctx.isSsr) {
|
|
220
|
-
if (getPWADisplayMode() === "standalone") {
|
|
221
|
-
return rewrite(`/pwa${ctx.url.pathname}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return next();
|
|
225
|
-
}
|
|
10
|
+
// Configure configuration.featureFlags and configuration.platform in the Web declaration.
|
|
11
|
+
// Call getPWADisplayMode only in a browser-owned lifecycle.
|
|
226
12
|
```
|
|
227
13
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
## Caveats
|
|
231
|
-
|
|
232
|
-
- **Feature flags resolved server-side ship in HTML.** Don't store secrets in flags.
|
|
233
|
-
- **Platform detection on the server uses request headers.** A bot or curl might not send a useful User-Agent — handle `"other"` gracefully.
|
|
234
|
-
- **PWA detection on the server always returns `"browser"`.** Don't rely on it in SSR rendering paths; conditional UI based on PWA mode should be client-only or use `<noscript>` fallbacks.
|
|
235
|
-
|
|
236
|
-
## Next
|
|
237
|
-
|
|
238
|
-
- [Engineering: project structure](./engineering/project-structure.md) — where to put flag config, platform-aware code
|
|
14
|
+
Feature flags are application decisions, not authentication. Enforce protected operations with runtime policies. A service worker manifest/caching strategy remains application work; selecting the browser host does not install one.
|