@finesoft/front 0.1.75 → 0.1.77
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 +2 -411
- package/dist/browser.d.mts +2 -0
- package/dist/browser.mjs +1 -0
- package/dist/index.d.mts +2 -1248
- package/dist/index.mjs +54 -3557
- package/dist/server-data-DGbiKzMS.d.mts +1249 -0
- package/dist/start-app-BdXBCcor.mjs +2 -0
- package/docs/01-getting-started.md +230 -0
- package/docs/02-routing-and-controllers.md +197 -0
- package/docs/03-middleware.md +214 -0
- package/docs/04-rendering-and-hydration.md +271 -0
- package/docs/05-i18n.md +243 -0
- package/docs/06-http-client.md +286 -0
- package/docs/07-di-container.md +264 -0
- package/docs/08-observability.md +290 -0
- package/docs/09-server-and-deployment.md +242 -0
- package/docs/10-features-platform-pwa.md +238 -0
- package/docs/README.md +72 -0
- package/docs/advanced/custom-action-handler.md +248 -0
- package/docs/advanced/custom-adapter.md +264 -0
- package/docs/advanced/custom-event-recorder.md +318 -0
- package/docs/advanced/inline-proxy-codegen.md +200 -0
- package/docs/advanced/multi-tenant-scopes.md +330 -0
- package/docs/engineering/ci-release-flow.md +244 -0
- package/docs/engineering/project-structure.md +296 -0
- package/docs/engineering/testing.md +317 -0
- package/docs/pitfalls/container-scope-leak.md +215 -0
- package/docs/pitfalls/i18n-bundle-size.md +182 -0
- package/docs/pitfalls/proxy-binary-payloads.md +133 -0
- package/docs/pitfalls/redirect-vs-rewrite.md +147 -0
- package/docs/pitfalls/ssr-hydration-mismatch.md +163 -0
- package/docs/pitfalls/ssr-vs-csr-globals.md +176 -0
- package/docs/zh/01-getting-started.md +230 -0
- package/docs/zh/02-routing-and-controllers.md +197 -0
- package/docs/zh/03-middleware.md +214 -0
- package/docs/zh/04-rendering-and-hydration.md +271 -0
- package/docs/zh/05-i18n.md +243 -0
- package/docs/zh/06-http-client.md +286 -0
- package/docs/zh/07-di-container.md +264 -0
- package/docs/zh/08-observability.md +287 -0
- package/docs/zh/09-server-and-deployment.md +242 -0
- package/docs/zh/10-features-platform-pwa.md +238 -0
- package/docs/zh/README.md +72 -0
- package/docs/zh/advanced/custom-action-handler.md +248 -0
- package/docs/zh/advanced/custom-adapter.md +264 -0
- package/docs/zh/advanced/custom-event-recorder.md +318 -0
- package/docs/zh/advanced/inline-proxy-codegen.md +200 -0
- package/docs/zh/advanced/multi-tenant-scopes.md +330 -0
- package/docs/zh/engineering/ci-release-flow.md +244 -0
- package/docs/zh/engineering/project-structure.md +296 -0
- package/docs/zh/engineering/testing.md +317 -0
- package/docs/zh/pitfalls/container-scope-leak.md +215 -0
- package/docs/zh/pitfalls/i18n-bundle-size.md +182 -0
- package/docs/zh/pitfalls/proxy-binary-payloads.md +133 -0
- package/docs/zh/pitfalls/redirect-vs-rewrite.md +147 -0
- package/docs/zh/pitfalls/ssr-hydration-mismatch.md +163 -0
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +176 -0
- package/package.json +12 -3
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# 9. Server & deployment
|
|
2
|
+
|
|
3
|
+
The server side of the framework. This chapter covers:
|
|
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
|
|
11
|
+
|
|
12
|
+
```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
|
+
isr: { routes: ["/blog/*"], ttl: 300 },
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
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
|
+
| `isr` | `{ routes, ttl }` | Incremental Static Regeneration for prerendered routes. |
|
|
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 a function that gives you a ready-to-run Hono app:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createServer } from "@finesoft/front";
|
|
62
|
+
|
|
63
|
+
const app = createServer({
|
|
64
|
+
ssrEntry: "./dist/server/ssr.js",
|
|
65
|
+
proxies: [{ prefix: "/api", target: "https://upstream.example" }],
|
|
66
|
+
staticDir: "./dist/client",
|
|
67
|
+
isr: { routes: ["/blog/*"], ttl: 300 },
|
|
68
|
+
});
|
|
69
|
+
|
|
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 });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### What it includes
|
|
76
|
+
|
|
77
|
+
- Static file serving for the client bundle
|
|
78
|
+
- All your proxy routes (registered via `registerProxyRoutes`)
|
|
79
|
+
- SSR rendering with full middleware pipeline
|
|
80
|
+
- ISR cache for prerendered routes
|
|
81
|
+
- Locale resolution from `Accept-Language`
|
|
82
|
+
|
|
83
|
+
## Proxy routes
|
|
84
|
+
|
|
85
|
+
Declarative API forwarding with built-in SSRF protection, binary-safe forwarding, and configurable auth/cache.
|
|
86
|
+
|
|
87
|
+
### Basic config
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
proxies: [
|
|
91
|
+
{
|
|
92
|
+
prefix: "/api", // must start with /
|
|
93
|
+
target: "https://api.example.com", // must be https:// or http://
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Now `GET /api/users/42` → `GET https://api.example.com/users/42`. Query params and request headers are forwarded.
|
|
99
|
+
|
|
100
|
+
### Full options
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
{
|
|
104
|
+
prefix: "/api/apple",
|
|
105
|
+
target: "https://api.music.apple.com",
|
|
106
|
+
methods: ["get", "post"], // default ["all"]
|
|
107
|
+
headers: { "X-App": "finesoft" }, // injected per request
|
|
108
|
+
auth: { type: "bearer", envKey: "APPLE_TOKEN" }, // reads process.env.APPLE_TOKEN
|
|
109
|
+
cache: "public, max-age=60", // Cache-Control on response
|
|
110
|
+
followRedirects: false, // default false (redirect: "manual")
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`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.
|
|
115
|
+
|
|
116
|
+
### What the framework enforces
|
|
117
|
+
|
|
118
|
+
- **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).
|
|
119
|
+
- **Open-redirect protection**: the constructed target URL must have the same `origin` as the configured `target`. Different origin → `400 Invalid proxy target`.
|
|
120
|
+
- **Binary integrity**: response body forwarded via `arrayBuffer()`, not `text()` — preserves bytes exactly. PDF, image, protobuf responses are byte-identical to the upstream response.
|
|
121
|
+
- **Size limit**: 10 MB. `Content-Length` header is checked first for fast rejection; actual body byte length is checked after fetch.
|
|
122
|
+
- **HTTP warning**: any `http://` target logs a warning at startup. Use HTTPS in production.
|
|
123
|
+
|
|
124
|
+
### Generated proxy code (serverless / edge)
|
|
125
|
+
|
|
126
|
+
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).
|
|
127
|
+
|
|
128
|
+
## Adapters
|
|
129
|
+
|
|
130
|
+
| Adapter | Target | Build output |
|
|
131
|
+
| -------------- | -------------------------- | ------------------------------------------------------ |
|
|
132
|
+
| `"node"` | Standalone Node.js server | `dist/server/index.js` — `serve({ fetch: app.fetch })` |
|
|
133
|
+
| `"vercel"` | Vercel Build Output API v3 | `.vercel/output/` with `functions/` and `static/` |
|
|
134
|
+
| `"cloudflare"` | Cloudflare Workers | `dist/_worker.js` + `dist/_routes.json` |
|
|
135
|
+
| `"netlify"` | Netlify Functions v2 | `netlify/functions/` + `_redirects` |
|
|
136
|
+
| `"static"` | Pre-rendered static files | `dist/client/` only (no server) |
|
|
137
|
+
| `"auto"` | Auto-detect at build time | Picks one of the above by environment variable |
|
|
138
|
+
|
|
139
|
+
### Auto-detection
|
|
140
|
+
|
|
141
|
+
`adapter: "auto"` checks (in order):
|
|
142
|
+
|
|
143
|
+
1. `VERCEL=1` → vercel
|
|
144
|
+
2. `CF_PAGES=1` → cloudflare
|
|
145
|
+
3. `NETLIFY=1` → netlify
|
|
146
|
+
4. otherwise → node
|
|
147
|
+
|
|
148
|
+
This works for most CI environments — Vercel / Cloudflare / Netlify all set these automatically during their build.
|
|
149
|
+
|
|
150
|
+
## ISR (Incremental Static Regeneration)
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
isr: {
|
|
154
|
+
routes: ["/blog/*", "/products/*"],
|
|
155
|
+
ttl: 300, // seconds
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
How it works:
|
|
160
|
+
|
|
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
|
|
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.
|
|
166
|
+
|
|
167
|
+
Routes not matched by `isr.routes` always render fresh.
|
|
168
|
+
|
|
169
|
+
### Cache invalidation
|
|
170
|
+
|
|
171
|
+
Programmatic invalidation is not exposed in the public API. To force a refresh:
|
|
172
|
+
|
|
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
|
|
176
|
+
|
|
177
|
+
For production, push invalidation up to CDN level — the framework's in-memory cache is for single-instance serving.
|
|
178
|
+
|
|
179
|
+
## Custom Hono middleware
|
|
180
|
+
|
|
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:
|
|
182
|
+
|
|
183
|
+
```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 });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// SSR catch-all is registered last by createServer — your routes win.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Environment variables
|
|
197
|
+
|
|
198
|
+
The framework reads:
|
|
199
|
+
|
|
200
|
+
- `NODE_ENV` — `"production"` enables prod-only optimizations
|
|
201
|
+
- `PROXY_TOKEN` / `BASIC_TOKEN` / any `auth.envKey` — proxy auth secrets
|
|
202
|
+
- `VERCEL`, `CF_PAGES`, `NETLIFY` — adapter auto-detection
|
|
203
|
+
|
|
204
|
+
Anything else is yours. Access via `process.env` directly or by registering a config object in the DI container:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
framework.container.register("config", () => ({
|
|
208
|
+
upstreamUrl: process.env.UPSTREAM_URL ?? "https://api.example.com",
|
|
209
|
+
sessionSecret: requireEnv("SESSION_SECRET"),
|
|
210
|
+
}));
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Health checks and graceful shutdown
|
|
214
|
+
|
|
215
|
+
For Node deployments behind a load balancer:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { serve } from "@hono/node-server";
|
|
219
|
+
|
|
220
|
+
const app = createServer({
|
|
221
|
+
/* ... */
|
|
222
|
+
});
|
|
223
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
224
|
+
|
|
225
|
+
const server = serve({ fetch: app.fetch, port: 3000 });
|
|
226
|
+
|
|
227
|
+
process.on("SIGTERM", () => {
|
|
228
|
+
server.close(() => {
|
|
229
|
+
// dispose Framework if you held a reference
|
|
230
|
+
framework.dispose();
|
|
231
|
+
process.exit(0);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`framework.dispose()` recursively disposes the container, calls `destroy()` on registered recorders/loggers, and unregisters all routes.
|
|
237
|
+
|
|
238
|
+
## Next
|
|
239
|
+
|
|
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
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# 10. Features, platform, PWA
|
|
2
|
+
|
|
3
|
+
Three small, independent runtime helpers:
|
|
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
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
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
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
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
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# `@finesoft/front` documentation
|
|
2
|
+
|
|
3
|
+
> **Language:** English (this page) · **[简体中文](./zh/README.md)**
|
|
4
|
+
|
|
5
|
+
Full-stack TypeScript framework — router, DI, actions, SSR, and server — in one package. Works with **Vue**, **React**, or **Svelte**. Deploys to Node.js, Vercel, Cloudflare Workers, Netlify, or static hosting.
|
|
6
|
+
|
|
7
|
+
## Three entry points
|
|
8
|
+
|
|
9
|
+
Pick where to start based on what you need.
|
|
10
|
+
|
|
11
|
+
### New to the framework — read top to bottom
|
|
12
|
+
|
|
13
|
+
A linear path. Each chapter assumes the previous one. By the end you can build, render, and deploy a real app.
|
|
14
|
+
|
|
15
|
+
1. [Getting started](./01-getting-started.md) — install, Vite config, first page
|
|
16
|
+
2. [Routing & controllers](./02-routing-and-controllers.md) — route definitions, intents, controllers, render modes
|
|
17
|
+
3. [Middleware](./03-middleware.md) — `beforeLoad` / `afterLoad`, redirect / rewrite / deny
|
|
18
|
+
4. [Rendering & hydration](./04-rendering-and-hydration.md) — SSR / CSR / Prerender, `PrefetchedIntents`
|
|
19
|
+
5. [Internationalization](./05-i18n.md) — `locale`, `Translator`, dictionary loading, RTL
|
|
20
|
+
6. [HTTP client](./06-http-client.md) — `HttpClient` subclassing, interceptors, `HttpError`
|
|
21
|
+
7. [DI container](./07-di-container.md) — registration, scopes, `DEP_KEYS`, dispose
|
|
22
|
+
8. [Observability](./08-observability.md) — `Logger`, `EventRecorder`, impression tracking, `ReportCallback`
|
|
23
|
+
9. [Server & deployment](./09-server-and-deployment.md) — `createServer`, proxy, adapters, Vite plugin
|
|
24
|
+
10. [Features, platform, PWA](./10-features-platform-pwa.md) — feature flags, platform detection, PWA mode
|
|
25
|
+
|
|
26
|
+
### Engineer working on an existing app — jump to practice
|
|
27
|
+
|
|
28
|
+
Cross-cutting concerns and conventions. Read after you understand the basics.
|
|
29
|
+
|
|
30
|
+
- [Project structure](./engineering/project-structure.md) — recommended layout, `bootstrap.ts` splitting, single source of truth
|
|
31
|
+
- [Testing](./engineering/testing.md) — controllers, middleware, scoped DI, mocking the framework
|
|
32
|
+
- [CI & release flow](./engineering/ci-release-flow.md) — changesets, the bundled release workflow, version reconciliation
|
|
33
|
+
|
|
34
|
+
### Hit a problem — go to pitfalls
|
|
35
|
+
|
|
36
|
+
Each entry is **symptom → root cause → fix**, kept short.
|
|
37
|
+
|
|
38
|
+
- [SSR hydration mismatch](./pitfalls/ssr-hydration-mismatch.md)
|
|
39
|
+
- [SSR vs CSR globals](./pitfalls/ssr-vs-csr-globals.md)
|
|
40
|
+
- [Redirect vs rewrite](./pitfalls/redirect-vs-rewrite.md)
|
|
41
|
+
- [Proxy binary payloads](./pitfalls/proxy-binary-payloads.md)
|
|
42
|
+
- [Container scope leak](./pitfalls/container-scope-leak.md)
|
|
43
|
+
- [i18n bundle size](./pitfalls/i18n-bundle-size.md)
|
|
44
|
+
|
|
45
|
+
### Extending the framework — advanced recipes
|
|
46
|
+
|
|
47
|
+
Each recipe is a complete, runnable extension example with explanation.
|
|
48
|
+
|
|
49
|
+
- [Custom action handler](./advanced/custom-action-handler.md) — beyond `FlowAction` / `ExternalUrlAction`
|
|
50
|
+
- [Custom event recorder](./advanced/custom-event-recorder.md) — wire Sentry / Datadog / your own pipeline
|
|
51
|
+
- [Custom adapter](./advanced/custom-adapter.md) — target a new platform
|
|
52
|
+
- [Inline proxy codegen](./advanced/inline-proxy-codegen.md) — generate self-contained proxy routes for serverless / edge
|
|
53
|
+
- [Multi-tenant scopes](./advanced/multi-tenant-scopes.md) — per-tenant DI containers
|
|
54
|
+
|
|
55
|
+
## At-a-glance
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
URL/Action → Router.resolve()
|
|
59
|
+
→ beforeLoad chain (NavigationContext: redirect/rewrite/deny/next)
|
|
60
|
+
→ IntentDispatcher (controller.execute() → Page; fallback() on error)
|
|
61
|
+
→ afterLoad chain (PostLoadContext)
|
|
62
|
+
→ render (SSR: HTML + serialized PrefetchedIntents; CSR: shell)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The same `bootstrap()` runs on the server and in the browser. SSR serializes prefetched intent results into HTML; the browser deserializes them into `PrefetchedIntents` so the first client navigation reuses server results without a refetch.
|
|
66
|
+
|
|
67
|
+
## Conventions used in these docs
|
|
68
|
+
|
|
69
|
+
- **Code blocks** are runnable as written unless a comment says otherwise.
|
|
70
|
+
- **File paths** are relative to the project root (the directory containing `vite.config.ts`).
|
|
71
|
+
- **`vp`** is the [Vite+](https://github.com/voidzero-dev/setup-vp) CLI. Use it instead of calling `pnpm` / `npm` / `vitest` / `tsdown` directly.
|
|
72
|
+
- **`@finesoft/front`** is the only import surface for application code. Internal packages (`core`, `browser`, `ssr`, `server`) are bundled in and not published.
|