@finesoft/front 0.5.1 → 0.5.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.
Files changed (128) hide show
  1. package/README.md +4 -4
  2. package/dist/Outlet.svelte +39 -0
  3. package/dist/Outlet.svelte.d.ts +7 -0
  4. package/dist/browser-DIU6Sxl3.mjs +1237 -0
  5. package/dist/browser-kFMjlLGT.d.mts +262 -0
  6. package/dist/browser.d.mts +7 -2
  7. package/dist/browser.mjs +9 -1
  8. package/dist/controller-types-CgmJ6-le.d.mts +16 -0
  9. package/dist/cookies-Bpf9VayB.d.mts +779 -0
  10. package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
  11. package/dist/host-guard-DDWxLpFL.mjs +222 -0
  12. package/dist/http-B6CJqDyf.d.mts +46 -0
  13. package/dist/http-CaxrMD1A.d.mts +1 -0
  14. package/dist/http-D70PL72H.mjs +257 -0
  15. package/dist/http.d.mts +3 -0
  16. package/dist/http.mjs +2 -0
  17. package/dist/index-node.d.mts +15 -0
  18. package/dist/index-node.mjs +15 -0
  19. package/dist/index.d.mts +48 -697
  20. package/dist/index.mjs +44 -261
  21. package/dist/load-node.d.mts +5 -0
  22. package/dist/load-node.mjs +10 -0
  23. package/dist/load-portable.d.mts +5 -0
  24. package/dist/load-portable.mjs +8 -0
  25. package/dist/lru-map-BKoUAySU.mjs +50 -0
  26. package/dist/messages-CAt2QdGr.mjs +140 -0
  27. package/dist/native-contract-DuR25hYB.d.mts +14 -0
  28. package/dist/native-contract.d.mts +2 -0
  29. package/dist/native-contract.mjs +1 -0
  30. package/dist/node-D9hB4dsz.d.mts +35 -0
  31. package/dist/node.d.mts +2 -0
  32. package/dist/node.mjs +59 -0
  33. package/dist/path-CGFl2w7D.mjs +113 -0
  34. package/dist/path-CXT6xGPO.d.mts +261 -0
  35. package/dist/portable-CaxrMD1A.d.mts +1 -0
  36. package/dist/portable.d.mts +11 -0
  37. package/dist/portable.mjs +12 -0
  38. package/dist/proxy-1SphZ7x7.mjs +436 -0
  39. package/dist/proxy-2dSWO-Xw.d.mts +53 -0
  40. package/dist/public-types-BcJM-AYc.mjs +835 -0
  41. package/dist/react-DhwBRw01.d.mts +16 -0
  42. package/dist/react.d.mts +3 -0
  43. package/dist/react.mjs +29 -0
  44. package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
  45. package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
  46. package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
  47. package/dist/session-DnB4ZC3x.d.mts +1279 -0
  48. package/dist/src-Ftl_0rhu.mjs +28 -0
  49. package/dist/src-qwx7Vw8g.mjs +3807 -0
  50. package/dist/ssr-BEUNDvbj.d.mts +210 -0
  51. package/dist/ssr-C8xnYXoY.mjs +357 -0
  52. package/dist/ssr.d.mts +3 -0
  53. package/dist/ssr.mjs +3 -0
  54. package/dist/svelte-Dr5to3SE.d.mts +16 -0
  55. package/dist/svelte.d.mts +3 -0
  56. package/dist/svelte.mjs +13 -0
  57. package/dist/typegen-C-WeJCtf.d.mts +12 -0
  58. package/dist/typegen-cli.d.mts +1 -0
  59. package/dist/typegen-cli.mjs +11 -0
  60. package/dist/typegen.d.mts +3 -0
  61. package/dist/typegen.mjs +2 -0
  62. package/dist/types-BuaZHRG7.mjs +402 -0
  63. package/dist/undici-CPfL25Hr.mjs +22262 -0
  64. package/dist/vite-Cj4SPA8D.d.mts +277 -0
  65. package/dist/vite.d.mts +4 -0
  66. package/dist/vite.mjs +2354 -0
  67. package/dist/vue-DGmzuKho.d.mts +32 -0
  68. package/dist/vue.d.mts +3 -0
  69. package/dist/vue.mjs +56 -0
  70. package/dist/web.d.mts +6 -0
  71. package/dist/web.mjs +8 -0
  72. package/dist/worker.d.mts +2 -0
  73. package/dist/worker.mjs +2 -0
  74. package/docs/01-getting-started.md +67 -199
  75. package/docs/02-routing-and-controllers.md +163 -241
  76. package/docs/03-middleware.md +10 -212
  77. package/docs/04-rendering-and-hydration.md +6 -333
  78. package/docs/05-i18n.md +7 -237
  79. package/docs/06-http-client.md +20 -263
  80. package/docs/07-di-container.md +23 -257
  81. package/docs/08-observability.md +6 -286
  82. package/docs/09-server-and-deployment.md +59 -219
  83. package/docs/10-features-platform-pwa.md +7 -231
  84. package/docs/11-navigation.md +143 -288
  85. package/docs/12-session-restoration.md +6 -214
  86. package/docs/README.md +7 -7
  87. package/docs/advanced/custom-action-handler.md +29 -229
  88. package/docs/advanced/custom-adapter.md +7 -259
  89. package/docs/advanced/custom-event-recorder.md +7 -312
  90. package/docs/advanced/inline-proxy-codegen.md +6 -185
  91. package/docs/advanced/multi-tenant-scopes.md +10 -323
  92. package/docs/engineering/ci-release-flow.md +35 -222
  93. package/docs/engineering/project-structure.md +34 -277
  94. package/docs/engineering/testing.md +8 -310
  95. package/docs/pitfalls/container-scope-leak.md +2 -214
  96. package/docs/pitfalls/i18n-bundle-size.md +6 -176
  97. package/docs/pitfalls/proxy-binary-payloads.md +8 -12
  98. package/docs/pitfalls/ssr-hydration-mismatch.md +4 -160
  99. package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
  100. package/docs/zh/01-getting-started.md +67 -199
  101. package/docs/zh/02-routing-and-controllers.md +166 -244
  102. package/docs/zh/03-middleware.md +10 -212
  103. package/docs/zh/04-rendering-and-hydration.md +6 -333
  104. package/docs/zh/05-i18n.md +7 -237
  105. package/docs/zh/06-http-client.md +20 -263
  106. package/docs/zh/07-di-container.md +23 -257
  107. package/docs/zh/08-observability.md +6 -283
  108. package/docs/zh/09-server-and-deployment.md +59 -219
  109. package/docs/zh/10-features-platform-pwa.md +7 -231
  110. package/docs/zh/11-navigation.md +130 -290
  111. package/docs/zh/12-session-restoration.md +6 -214
  112. package/docs/zh/README.md +4 -4
  113. package/docs/zh/advanced/custom-action-handler.md +29 -229
  114. package/docs/zh/advanced/custom-adapter.md +7 -259
  115. package/docs/zh/advanced/custom-event-recorder.md +7 -312
  116. package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
  117. package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
  118. package/docs/zh/engineering/ci-release-flow.md +35 -222
  119. package/docs/zh/engineering/project-structure.md +34 -277
  120. package/docs/zh/engineering/testing.md +8 -310
  121. package/docs/zh/pitfalls/container-scope-leak.md +2 -214
  122. package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
  123. package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
  124. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +4 -160
  125. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
  126. package/package.json +118 -20
  127. package/dist/browser-BHhVWXik.mjs +0 -2
  128. package/dist/browser-BV2BBXm7.d.mts +0 -2811
@@ -1,242 +1,82 @@
1
- # 9. Server & deployment
1
+ # HTTP and deployment hosts
2
2
 
3
- The server side of the framework. This chapter covers:
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
- - The Vite plugin (`finesoftFrontViteConfig`) — dev server, build config, code generation
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
- // vite.config.ts
14
- import { finesoftFrontViteConfig } from "@finesoft/front";
15
- import { defineConfig } from "vite";
16
-
17
- export default defineConfig({
18
- plugins: [
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
- ### Options
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
- import { createServer } from "@finesoft/front";
62
-
63
- const { app } = await createServer({
64
- ssr: { ssrProductionModule: "./dist/server/ssr.js" }, // or ssrEntryPath in dev
65
- proxies: [{ prefix: "/api", target: "https://upstream.example" }],
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
- // `app` is the started Hono instance — export it for serverless runtimes whose
70
- // adapter imports the fetch handler (Vercel / Cloudflare / Netlify).
71
- export { app };
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
- A `prerender` route is served two ways:
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
- 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:
59
+ ## Web build / 页面构建
190
60
 
191
61
  ```ts
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
- },
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
- ## Environment variables
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
- `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.
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
- ## Next
80
+ ## Static hosting boundary
239
81
 
240
- - [Features, platform, PWA](./10-features-platform-pwa.md) feature flags, platform detection
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
- # 10. Features, platform, PWA
1
+ # Features, platform and PWA
2
2
 
3
- Three small, independent runtime helpers:
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
- - **Feature flags** — config that can change without redeploy
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
- const mode = getPWADisplayMode();
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
- This routes installed mobile PWA users to a different page tree without affecting other users.
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.