@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.
Files changed (58) hide show
  1. package/README.md +2 -411
  2. package/dist/browser.d.mts +2 -0
  3. package/dist/browser.mjs +1 -0
  4. package/dist/index.d.mts +2 -1248
  5. package/dist/index.mjs +54 -3557
  6. package/dist/server-data-DGbiKzMS.d.mts +1249 -0
  7. package/dist/start-app-BdXBCcor.mjs +2 -0
  8. package/docs/01-getting-started.md +230 -0
  9. package/docs/02-routing-and-controllers.md +197 -0
  10. package/docs/03-middleware.md +214 -0
  11. package/docs/04-rendering-and-hydration.md +271 -0
  12. package/docs/05-i18n.md +243 -0
  13. package/docs/06-http-client.md +286 -0
  14. package/docs/07-di-container.md +264 -0
  15. package/docs/08-observability.md +290 -0
  16. package/docs/09-server-and-deployment.md +242 -0
  17. package/docs/10-features-platform-pwa.md +238 -0
  18. package/docs/README.md +72 -0
  19. package/docs/advanced/custom-action-handler.md +248 -0
  20. package/docs/advanced/custom-adapter.md +264 -0
  21. package/docs/advanced/custom-event-recorder.md +318 -0
  22. package/docs/advanced/inline-proxy-codegen.md +200 -0
  23. package/docs/advanced/multi-tenant-scopes.md +330 -0
  24. package/docs/engineering/ci-release-flow.md +244 -0
  25. package/docs/engineering/project-structure.md +296 -0
  26. package/docs/engineering/testing.md +317 -0
  27. package/docs/pitfalls/container-scope-leak.md +215 -0
  28. package/docs/pitfalls/i18n-bundle-size.md +182 -0
  29. package/docs/pitfalls/proxy-binary-payloads.md +133 -0
  30. package/docs/pitfalls/redirect-vs-rewrite.md +147 -0
  31. package/docs/pitfalls/ssr-hydration-mismatch.md +163 -0
  32. package/docs/pitfalls/ssr-vs-csr-globals.md +176 -0
  33. package/docs/zh/01-getting-started.md +230 -0
  34. package/docs/zh/02-routing-and-controllers.md +197 -0
  35. package/docs/zh/03-middleware.md +214 -0
  36. package/docs/zh/04-rendering-and-hydration.md +271 -0
  37. package/docs/zh/05-i18n.md +243 -0
  38. package/docs/zh/06-http-client.md +286 -0
  39. package/docs/zh/07-di-container.md +264 -0
  40. package/docs/zh/08-observability.md +287 -0
  41. package/docs/zh/09-server-and-deployment.md +242 -0
  42. package/docs/zh/10-features-platform-pwa.md +238 -0
  43. package/docs/zh/README.md +72 -0
  44. package/docs/zh/advanced/custom-action-handler.md +248 -0
  45. package/docs/zh/advanced/custom-adapter.md +264 -0
  46. package/docs/zh/advanced/custom-event-recorder.md +318 -0
  47. package/docs/zh/advanced/inline-proxy-codegen.md +200 -0
  48. package/docs/zh/advanced/multi-tenant-scopes.md +330 -0
  49. package/docs/zh/engineering/ci-release-flow.md +244 -0
  50. package/docs/zh/engineering/project-structure.md +296 -0
  51. package/docs/zh/engineering/testing.md +317 -0
  52. package/docs/zh/pitfalls/container-scope-leak.md +215 -0
  53. package/docs/zh/pitfalls/i18n-bundle-size.md +182 -0
  54. package/docs/zh/pitfalls/proxy-binary-payloads.md +133 -0
  55. package/docs/zh/pitfalls/redirect-vs-rewrite.md +147 -0
  56. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +163 -0
  57. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +176 -0
  58. 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.