@finesoft/front 0.5.0 → 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.
- package/README.md +4 -4
- package/dist/Outlet.svelte +39 -0
- package/dist/Outlet.svelte.d.ts +7 -0
- package/dist/browser-DIU6Sxl3.mjs +1237 -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-Bpf9VayB.d.mts +779 -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 -698
- 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-1SphZ7x7.mjs +436 -0
- package/dist/proxy-2dSWO-Xw.d.mts +53 -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-Ftl_0rhu.mjs +28 -0
- package/dist/src-qwx7Vw8g.mjs +3807 -0
- package/dist/ssr-BEUNDvbj.d.mts +210 -0
- package/dist/ssr-C8xnYXoY.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-BuaZHRG7.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 +6 -333
- 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 +4 -160
- 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 +6 -333
- 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 +4 -160
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/package.json +118 -20
- package/dist/browser-BYZq9Jp7.mjs +0 -2
- package/dist/browser-JTs2jqVY.d.mts +0 -2811
|
@@ -1,314 +1,236 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Routes, controllers and typed pages
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All six templates load pages with `BaseController`, which remains a public API. `definePage` connects a controller factory or a function handler to a Web application. Both forms share the runtime, execution scopes and policies.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Responsibilities
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
▼
|
|
14
|
-
Controller.execute() → Page
|
|
15
|
-
```
|
|
7
|
+
| API | Responsibility |
|
|
8
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `BaseController<TInput, TResult>` | Implement `execute`; inherited `perform` handles parameters, cancellation and `fallback` |
|
|
10
|
+
| `definePage({ id, create })` | Declare the page-loading factory and provide reusable `route`, `leaf` and `bindView` references |
|
|
11
|
+
| `defineWebApp` | Assemble page declarations, routes, guards and optional navigation structure |
|
|
12
|
+
| Native page component | Receive `page` data, render UI and handle interactions |
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
Import controllers, pages, routes and public-data declarations from `@finesoft/front`.
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
- An **intent id** (logical name for the operation; one intent can have multiple routes)
|
|
21
|
-
- A **controller instance** (where the page data is produced)
|
|
22
|
-
- Optional **param codecs** (`params` / `query`) that validate and type the URL params
|
|
23
|
-
- An optional **render mode** (`ssr` / `csr` / `prerender`)
|
|
24
|
-
- Optional **guards** (`beforeLoad` / `afterLoad`)
|
|
16
|
+
## Load a page with BaseController
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
`src/lib/controllers/product.ts`:
|
|
27
19
|
|
|
28
20
|
```ts
|
|
29
|
-
|
|
30
|
-
import { type
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export function bootstrap(framework: Framework): void {
|
|
36
|
-
defineRoutes(framework, [
|
|
37
|
-
// Plain SSR route
|
|
38
|
-
{ path: "/", intentId: "home", controller: new HomeController() },
|
|
39
|
-
|
|
40
|
-
// Dynamic segment
|
|
41
|
-
{ path: "/products/:id", intentId: "product", controller: new ProductController() },
|
|
42
|
-
|
|
43
|
-
// CSR-only (server returns an empty shell)
|
|
44
|
-
{
|
|
45
|
-
path: "/dashboard",
|
|
46
|
-
intentId: "dashboard",
|
|
47
|
-
controller: new DashboardController(),
|
|
48
|
-
renderMode: "csr",
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// Statically prerendered at build time
|
|
52
|
-
{
|
|
53
|
-
path: "/about",
|
|
54
|
-
intentId: "about",
|
|
55
|
-
controller: new AboutController(),
|
|
56
|
-
renderMode: "prerender",
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
// Protected route — reuses the home intent but gates with a guard
|
|
60
|
-
{
|
|
61
|
-
path: "/admin",
|
|
62
|
-
intentId: "home",
|
|
63
|
-
controller: new HomeController(),
|
|
64
|
-
beforeLoad: [authGuard],
|
|
65
|
-
},
|
|
66
|
-
]);
|
|
21
|
+
import { BaseController, DEP_KEYS } from "@finesoft/front";
|
|
22
|
+
import { markPublic, type BasePage } from "@finesoft/front";
|
|
23
|
+
|
|
24
|
+
export interface ProductPage extends BasePage {
|
|
25
|
+
pageType: "product";
|
|
26
|
+
product: { id: number; name: string };
|
|
67
27
|
}
|
|
68
|
-
```
|
|
69
28
|
|
|
70
|
-
|
|
29
|
+
export class ProductController extends BaseController {
|
|
30
|
+
async execute({ params, context }): Promise<ProductPage> {
|
|
31
|
+
const logger = await context.get(DEP_KEYS.LOGGER_FACTORY);
|
|
32
|
+
logger.loggerFor("ProductController").info(`Loading product ${params.id}`);
|
|
33
|
+
context.record("product.load", { productId: params.id });
|
|
34
|
+
|
|
35
|
+
return markPublic(
|
|
36
|
+
{
|
|
37
|
+
id: String(params.id),
|
|
38
|
+
pageType: "product",
|
|
39
|
+
title: `Product ${params.id}`,
|
|
40
|
+
product: { id: params.id, name: `Product ${params.id}` },
|
|
41
|
+
},
|
|
42
|
+
{ product: { id: true, name: true } },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
46
|
+
override fallback({ params }): ProductPage {
|
|
47
|
+
return markPublic(
|
|
48
|
+
{
|
|
49
|
+
id: String(params.id),
|
|
50
|
+
pageType: "product",
|
|
51
|
+
title: "Product unavailable",
|
|
52
|
+
product: { id: params.id, name: "Unavailable" },
|
|
53
|
+
},
|
|
54
|
+
{ product: { id: true, name: true } },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The framework maintains type imports, method annotations and base-class generics from the route declaration below; these are omitted in the initial source shown here. Run `vp dev`, `vp check` or the build to generate them. Types live in `.finesoft/controller-types.d.ts`, with no declaration block appended to the controller. See [automatic controller types](./11-navigation.md) for setup.
|
|
82
61
|
|
|
83
|
-
|
|
62
|
+
Both methods receive one object: `execute({ params, query, context })` and `fallback({ params, query, context, error })`. Destructure only the fields you need. Query remains separate from path parameters and is inferred from the route's `query` schemas.
|
|
84
63
|
|
|
85
|
-
|
|
86
|
-
- Parameterized: `/products/:id`, `/users/:userId/posts/:postId`
|
|
87
|
-
- Optional param: `/blog/:slug?` matches both `/blog` and `/blog/hello`.
|
|
64
|
+
This example uses local data; application controllers can call services from `execute`. `context` provides its `signal`, `fetch`, `get(token)` and `execute(operation, input)`. Pass `context.signal` to asynchronous request APIs so cancellation reaches the actual work.
|
|
88
65
|
|
|
89
|
-
|
|
66
|
+
`fallback` runs when `execute` throws an ordinary error and still returns `ProductPage`; this example chooses to display an unavailable state. The default implementation rethrows so page loading can handle the error. Cancellation, `AbortError` and `ExecutionError("cancelled")` propagate without entering `fallback`. Runtime policies execute outside the controller, so policy rejection also bypasses controller recovery.
|
|
90
67
|
|
|
91
|
-
##
|
|
68
|
+
## Register a factory, route and navigation target
|
|
92
69
|
|
|
93
|
-
|
|
70
|
+
`src/app-definition.ts`:
|
|
94
71
|
|
|
95
72
|
```ts
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
73
|
+
import { int } from "@finesoft/front";
|
|
74
|
+
import { definePage, defineWebApp } from "@finesoft/front";
|
|
75
|
+
import { ProductController } from "./lib/controllers/product";
|
|
76
|
+
|
|
77
|
+
export const product = definePage({
|
|
78
|
+
id: "load-product",
|
|
79
|
+
create: () => new ProductController(),
|
|
80
|
+
routes: [{ path: "/products/:id", params: { id: int() } }],
|
|
81
|
+
});
|
|
82
|
+
export const target = product.leaf({ id: 42 });
|
|
83
|
+
|
|
84
|
+
export const app = defineWebApp({
|
|
85
|
+
id: "example",
|
|
86
|
+
pages: [product],
|
|
87
|
+
getErrorPage: (status, message) => ({ id: String(status), pageType: "error", title: message }),
|
|
88
|
+
});
|
|
111
89
|
```
|
|
112
90
|
|
|
113
|
-
|
|
91
|
+
The page declaration owns the operation id; controllers do not repeat it. The URL `/products/42` is decoded by `int()`, so `execute` receives `params: { id: 42 }`. In application code, `product.leaf({ id: 42 })` retains the same parameter type.
|
|
114
92
|
|
|
115
|
-
|
|
116
|
-
| ----------------------- | ------------- | ----------------------------------------------------------- |
|
|
117
|
-
| `str(opts?)` | `string` | `minLength` / `maxLength` / `pattern` (`RegExp`) |
|
|
118
|
-
| `int(opts?)` | `number` | integer + `min` / `max` |
|
|
119
|
-
| `num(opts?)` | `number` | finite number + `min` / `max` |
|
|
120
|
-
| `bool()` | `boolean` | `"true" \| "1" \| "false" \| "0"` |
|
|
121
|
-
| `oneOf([...] as const)` | literal union | membership |
|
|
122
|
-
| `uuid()` | `string` | UUID v1–v5 |
|
|
123
|
-
| `list(item, opts?)` | `T[]` | multi-value query; each item via `item` + `min`/`max` count |
|
|
93
|
+
`create` returns a new controller for each actual execution. Declaration, route discovery and reference helpers only read definitions; consuming a prefetched or retained page result also skips controller creation. Keep request identity in execution context or scoped providers, and page drafts in page instances.
|
|
124
94
|
|
|
125
|
-
|
|
95
|
+
`id` is operation identity, `pageType` selects a view, and each leaf receives a fresh EntryId. `product.bindView("product", ProductView)` checks literal result page types without inferring one from the operation id. Retain `ProductPage.pageType: "product"` to get this check; a widened `BasePage.pageType: string` cannot constrain view names.
|
|
126
96
|
|
|
127
|
-
|
|
128
|
-
- `withDefault(codec, fallback)` — missing input → `fallback`; key stays required.
|
|
97
|
+
Typed leaf parameters retain controller types. External URLs still require explicit codec decoding. Generics provide compile-time constraints, not runtime input validation, and cannot prove that an arbitrary codec matches business validation rules.
|
|
129
98
|
|
|
130
|
-
|
|
99
|
+
## Function handlers for simple pages
|
|
131
100
|
|
|
132
|
-
|
|
101
|
+
Use `handler` when a page only needs a direct data-loading function. It can also access dependencies, cancellation and nested operations through execution context. Handle any required recovery inside the function. Choose either `create` or `handler`.
|
|
133
102
|
|
|
134
103
|
```ts
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
104
|
+
import { definePage, markPublic } from "@finesoft/front";
|
|
105
|
+
|
|
106
|
+
export const home = definePage({
|
|
107
|
+
id: "load-home",
|
|
108
|
+
handler: () => markPublic({ id: "home", pageType: "home" as const, title: "Home" }, []),
|
|
109
|
+
});
|
|
141
110
|
```
|
|
142
111
|
|
|
143
|
-
|
|
112
|
+
Templates use controller classes for consistent organization; function handlers suit short loading logic. Existing function handlers receive `(params, context, query)`. Inheritance is optional: a `create` factory can also return an object implementing `perform(params, context, query)`.
|
|
113
|
+
|
|
114
|
+
## Controllers for independent business operations
|
|
144
115
|
|
|
145
|
-
`
|
|
116
|
+
The result generic of `BaseController` does not require a page. Bind a class to an operation with `implementController` to execute it in a standalone data runtime. HTTP and Worker hosts can also invoke that operation.
|
|
146
117
|
|
|
147
118
|
```ts
|
|
148
119
|
import {
|
|
149
120
|
BaseController,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
121
|
+
createRuntime,
|
|
122
|
+
defineApp,
|
|
123
|
+
defineOperation,
|
|
124
|
+
implementController,
|
|
125
|
+
type ControllerInput,
|
|
155
126
|
} from "@finesoft/front";
|
|
156
127
|
|
|
157
|
-
|
|
158
|
-
|
|
128
|
+
type TotalInput = { unitPrice: number; quantity: number };
|
|
129
|
+
interface TotalResult {
|
|
130
|
+
total: number;
|
|
131
|
+
}
|
|
159
132
|
|
|
160
|
-
class
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
> {
|
|
164
|
-
readonly intentId = "product";
|
|
165
|
-
execute(params) {
|
|
166
|
-
// params.id: number, params.sort: "asc" | "desc" | undefined
|
|
133
|
+
class TotalController extends BaseController<ControllerInput<TotalInput>, TotalResult> {
|
|
134
|
+
execute({ params }: ControllerInput<TotalInput>): TotalResult {
|
|
135
|
+
return { total: params.unitPrice * params.quantity };
|
|
167
136
|
}
|
|
168
137
|
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### `route()` — param-key safety
|
|
172
138
|
|
|
173
|
-
|
|
139
|
+
const calculateTotal = defineOperation<TotalInput, TotalResult>({
|
|
140
|
+
id: "calculate-total",
|
|
141
|
+
kind: "query",
|
|
142
|
+
});
|
|
143
|
+
const runtime = createRuntime({
|
|
144
|
+
app: defineApp({
|
|
145
|
+
id: "data-example",
|
|
146
|
+
operations: [calculateTotal],
|
|
147
|
+
implementations: [implementController(calculateTotal, () => new TotalController())],
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
174
150
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
151
|
+
try {
|
|
152
|
+
const result = await runtime.execute(calculateTotal, { unitPrice: 29, quantity: 2 });
|
|
153
|
+
console.log(result); // { total: 58 }
|
|
154
|
+
} finally {
|
|
155
|
+
await runtime.dispose();
|
|
156
|
+
}
|
|
178
157
|
```
|
|
179
158
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
`defineRoute(path, def)` takes a **handler** function instead of a controller class, and infers its params type from the codecs automatically — no `InferParams` needed. It mirrors `BaseController`'s `try/catch → fallback`:
|
|
159
|
+
The operation declaration defines the input/output contract; the controller supplies the implementation. Exposing the operation over HTTP still requires an explicit endpoint. Registering a controller does not publish an API.
|
|
183
160
|
|
|
184
|
-
|
|
185
|
-
defineRoute("/products/:id", {
|
|
186
|
-
intentId: "product",
|
|
187
|
-
params: { id: int() },
|
|
188
|
-
query: { page: withDefault(int(), 1) },
|
|
189
|
-
handler: (params, container) => {
|
|
190
|
-
// params: { id: number; page: number } — inferred from the codecs
|
|
191
|
-
return loadProduct(params.id, params.page);
|
|
192
|
-
},
|
|
193
|
-
fallback: (params, error) => errorPage(error), // optional
|
|
194
|
-
});
|
|
195
|
-
```
|
|
161
|
+
<Ch02RouteResolver />
|
|
196
162
|
|
|
197
|
-
|
|
163
|
+
## Server controllers and request context
|
|
198
164
|
|
|
199
|
-
|
|
165
|
+
Use `BaseServerController` from `@finesoft/front` for pages requiring secrets, HttpOnly cookies, or response mutation. Keep `execute({ params, query, context })`, `fallback({ params, query, context, error })`, and the same `definePage({ create, routes })` registration. The type generator maintains the existing compact `Input` / `Failure` references and selects the context from the base class.
|
|
200
166
|
|
|
201
167
|
```ts
|
|
202
|
-
|
|
203
|
-
import {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
readonly intentId = "product";
|
|
214
|
-
|
|
215
|
-
async execute(params: { id: string }, container: Container): Promise<ProductPage> {
|
|
216
|
-
const http = container.resolve<HttpClient>("http");
|
|
217
|
-
const product = await http.get<{ name: string; price: number }>(
|
|
218
|
-
`/api/products/${params.id}`,
|
|
168
|
+
import { BaseServerController } from "@finesoft/front";
|
|
169
|
+
import { markPublic } from "@finesoft/front";
|
|
170
|
+
import { loadAccount } from "./account-service";
|
|
171
|
+
|
|
172
|
+
export class AccountController extends BaseServerController {
|
|
173
|
+
async execute({ params, context }) {
|
|
174
|
+
const account = await loadAccount(params.id, context.getCookie("session"));
|
|
175
|
+
context.responseHeaders.set("Cache-Control", "private, no-store");
|
|
176
|
+
return markPublic(
|
|
177
|
+
{ id: String(params.id), pageType: "account", title: account.name, account },
|
|
178
|
+
{ account: { id: true, name: true } },
|
|
219
179
|
);
|
|
220
|
-
return {
|
|
221
|
-
kind: "product",
|
|
222
|
-
id: params.id,
|
|
223
|
-
name: product.name,
|
|
224
|
-
price: product.price,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
fallback(params: { id: string }, _error: unknown): ProductPage {
|
|
229
|
-
return { kind: "product", id: params.id, name: "Not available", price: 0 };
|
|
230
180
|
}
|
|
231
181
|
}
|
|
232
182
|
```
|
|
233
183
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
| Member | Required | Purpose |
|
|
237
|
-
| ---------- | -------- | ---------------------------------------------------------------------------------- |
|
|
238
|
-
| `intentId` | yes | Must match the route's `intentId` (or the `IntentDispatcher.register` call). |
|
|
239
|
-
| `execute` | yes | Produce the page. Receives parsed path params and the request-scoped DI container. |
|
|
240
|
-
| `fallback` | yes | Return a degraded page when `execute()` throws. Must be synchronous and total. |
|
|
241
|
-
|
|
242
|
-
`BaseController` wraps `execute()` in `try/catch` and routes any error through `fallback()`. The framework never throws out of `dispatch()` — your `fallback()` is the last line of defense.
|
|
184
|
+
Generated annotations are omitted above; `account-service` is application code. The controller may live in any directory. Vite replaces its entire module with browser references, while SSR executes the original implementation directly. Browser navigation uses same-origin `POST /__finesoft/controller`; the server revalidates routes and executes policies and guards. Hydration consumes existing SSR data without another call.
|
|
243
185
|
|
|
244
|
-
|
|
186
|
+
| Capability | Shared page ControllerContext | ServerControllerContext |
|
|
187
|
+
| ----------------------------------------------- | ------------------------------------------------ | --------------------------------------------------- |
|
|
188
|
+
| `params`, `query` | Separate, route-inferred inputs | Same |
|
|
189
|
+
| `url`, `path`, `intent`, `isServer` | Current page and execution environment | Same; `isServer: true` |
|
|
190
|
+
| `getCookie(name)` | Browser-readable cookies, or SSR request cookies | Original request cookies, including HttpOnly |
|
|
191
|
+
| `getHeader(name)` | SSR request headers; unavailable in the browser | Current request headers |
|
|
192
|
+
| DI, fetch, signal, operations, logging, tracing | Existing execution capabilities | Same |
|
|
193
|
+
| `request`, `responseHeaders` | No dedicated interface | Original Request and outgoing Headers |
|
|
194
|
+
| `setCookie`, `deleteCookie` | No dedicated interface | Write outgoing cookies; leave the request unchanged |
|
|
245
195
|
|
|
246
|
-
|
|
196
|
+
Generated page inputs use `ControllerContext` or `ServerControllerContext`. Portable data operations continue to use `ExecutionContext`. Direct page-operation execution without navigation has empty `url` / `path`. For remote loads, `request.url` names the transport endpoint; use `context.url` for the page address.
|
|
247
197
|
|
|
248
|
-
|
|
198
|
+
### Module isolation
|
|
249
199
|
|
|
250
|
-
|
|
251
|
-
| ------------- | -------------------------------------------- | -------------------------------------------------------- |
|
|
252
|
-
| `"ssr"` | Fully rendered HTML + serialized data | Default. Best for SEO and TTFB-sensitive pages. |
|
|
253
|
-
| `"csr"` | Empty shell HTML; controller runs in browser | Authenticated dashboards, heavy-personalization pages. |
|
|
254
|
-
| `"prerender"` | Static HTML built at deploy time | Marketing, docs, blog. Combine with ISR (see chapter 4). |
|
|
200
|
+
A server controller module may export controller classes and types only. Its local runtime imports are also private to the server; importing these dependencies from client code fails. Use `import type` for shared contracts and separate modules for client implementations. Keep secret-dependent construction inside the protected module or server configuration, rather than arguments to the shared `create` factory.
|
|
255
201
|
|
|
256
|
-
|
|
202
|
+
Client bundles and source maps omit the replaced implementation. The development plugin rejects raw controller assets and direct browser source requests for protected dependencies. It indexes modules once, caches ASTs, and reanalyzes changed files. Dependencies remain protected for the lifetime of a development server; restart when intentionally moving one back into shared code. Computed file paths, `public/` assets, and independently copied source are outside the static import boundary. Development servers still belong on trusted development networks.
|
|
257
203
|
|
|
258
|
-
|
|
204
|
+
Use the framework Vite plugin and a request host such as Node or Worker. Static-only hosting cannot run the remote endpoint. The endpoint loads registered server page controllers; it does not expose arbitrary methods or replace explicit login/registration command endpoints.
|
|
259
205
|
|
|
260
|
-
|
|
206
|
+
### Explicit request and response tools
|
|
261
207
|
|
|
262
|
-
|
|
263
|
-
framework.intentDispatcher.register("checkout", new CheckoutController());
|
|
264
|
-
|
|
265
|
-
// Elsewhere:
|
|
266
|
-
const page = await framework.intentDispatcher.dispatch({
|
|
267
|
-
intentId: "checkout",
|
|
268
|
-
params: { cartId },
|
|
269
|
-
});
|
|
270
|
-
```
|
|
208
|
+
The framework supplies `request`, `getHeader`, `getCookie`, `fetch`, `responseHeaders`, `setCookie` and `deleteCookie`. Applications own login state, authentication, token storage/refresh and the choice to forward credentials.
|
|
271
209
|
|
|
272
|
-
|
|
210
|
+
Server `context.fetch` uses only caller-supplied headers. It does not inherit Cookie/Authorization from the outer request or copy internal Set-Cookie values to the page response. The credentials option does not create a server-side cookie store. Requests made by the browser retain the browser's native same-origin cookie behavior.
|
|
273
211
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
Same intent can serve different URLs:
|
|
212
|
+
For example, application code can choose to forward Authorization to a particular internal endpoint and copy that endpoint's cookies to the outgoing response:
|
|
277
213
|
|
|
278
214
|
```ts
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{ path: "/landing/:slug", intentId: "home" }, // same intent, params differ
|
|
283
|
-
]);
|
|
284
|
-
```
|
|
215
|
+
const headers = new Headers();
|
|
216
|
+
const authorization = context.getHeader("authorization");
|
|
217
|
+
if (authorization) headers.set("authorization", authorization);
|
|
285
218
|
|
|
286
|
-
|
|
219
|
+
const response = await context.fetch("/api/account", { headers });
|
|
220
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
221
|
+
context.responseHeaders.append("set-cookie", cookie);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
287
224
|
|
|
288
|
-
|
|
225
|
+
The destination and header/cookie selection are application decisions. Reuse a business HttpClient interceptor or request-scoped provider through context.get when several controllers share the policy.
|
|
289
226
|
|
|
290
|
-
|
|
227
|
+
Applications can also construct outgoing cookies directly:
|
|
291
228
|
|
|
292
229
|
```ts
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// intent: { id: "product", params: { id: "42" } },
|
|
296
|
-
// action: { kind: "flow", url: "/products/42" },
|
|
297
|
-
// renderMode: "ssr",
|
|
298
|
-
// beforeGuards: [...],
|
|
299
|
-
// afterGuards: [...],
|
|
300
|
-
// }
|
|
230
|
+
context.setCookie("theme", "dark", { path: "/", sameSite: "Lax" });
|
|
231
|
+
context.deleteCookie("theme", { path: "/" });
|
|
301
232
|
```
|
|
302
233
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
## Try it
|
|
306
|
-
|
|
307
|
-
A live `Router` instance is registered with the sample routes below. Type a URL on the left and watch `Router.resolve()` produce a `RouteMatch` on the right — the same code path the framework uses at runtime.
|
|
308
|
-
|
|
309
|
-
<Ch02RouteResolver />
|
|
310
|
-
|
|
311
|
-
## Next
|
|
234
|
+
getCookie always reads the incoming request. Writing a response cookie does not change that request view or update identity. Later business logic uses its own result when it needs a new value. Explicit response headers are assembled into the final response, including error responses, with the framework retaining control of its error body content type and no-store policy.
|
|
312
235
|
|
|
313
|
-
-
|
|
314
|
-
- [Rendering & hydration](./04-rendering-and-hydration.md) — what happens after the controller produces a page
|
|
236
|
+
Client-supplied context or bindings are never accepted as identity. markPublic continues to control data sent to the browser; secrets must also stay out of rendered HTML.
|