@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,248 @@
|
|
|
1
|
+
# Advanced: custom action handler
|
|
2
|
+
|
|
3
|
+
The framework ships three action kinds: `flow` (in-app navigation), `external-url` (full browser navigation), and `compound` (a tuple of actions executed in order). For most apps these are enough.
|
|
4
|
+
|
|
5
|
+
This recipe shows how to add your own — useful when you have a class of operations that need cross-cutting handling (analytics, confirmations, telemetry) without polluting every callsite.
|
|
6
|
+
|
|
7
|
+
## Use case: confirmation-gated action
|
|
8
|
+
|
|
9
|
+
We'll add a `"confirm"` action kind: dispatch it with `{ kind: "confirm", message, then }`, and the framework shows a confirmation dialog before dispatching `then` (which is itself an action).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
dispatch({
|
|
13
|
+
kind: "confirm",
|
|
14
|
+
message: "Delete this item permanently?",
|
|
15
|
+
then: { kind: "flow", url: "/items/42/deleted" },
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The user clicks Cancel → no navigation. Clicks OK → the inner flow action fires.
|
|
20
|
+
|
|
21
|
+
## Step 1: define the action type
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/lib/actions/confirm.ts
|
|
25
|
+
import { type Action } from "@finesoft/front";
|
|
26
|
+
|
|
27
|
+
export interface ConfirmAction {
|
|
28
|
+
kind: "confirm";
|
|
29
|
+
message: string;
|
|
30
|
+
then: Action;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function makeConfirmAction(message: string, then: Action): ConfirmAction {
|
|
34
|
+
return { kind: "confirm", message, then };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isConfirmAction(action: Action): action is ConfirmAction {
|
|
38
|
+
return (action as any).kind === "confirm";
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The shape is yours — `kind` just has to be unique among registered handlers.
|
|
43
|
+
|
|
44
|
+
## Step 2: extend the `Action` type union
|
|
45
|
+
|
|
46
|
+
TypeScript doesn't auto-expand the framework's `Action` type. Declare a module augmentation:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// src/lib/actions/confirm.ts
|
|
50
|
+
declare module "@finesoft/front" {
|
|
51
|
+
interface ActionRegistry {
|
|
52
|
+
confirm: ConfirmAction;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the framework exposes `ActionRegistry` (most pluggable frameworks do), this lets TypeScript know about your new kind. If it doesn't, cast at registration time:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
framework.actionDispatcher.register("confirm" as any, handleConfirm as any);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The runtime doesn't care — `kind` is a plain string at dispatch time.
|
|
64
|
+
|
|
65
|
+
## Step 3: write the handler
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// src/lib/actions/confirm.ts
|
|
69
|
+
import type { Framework } from "@finesoft/front";
|
|
70
|
+
|
|
71
|
+
export function registerConfirmHandler(framework: Framework): void {
|
|
72
|
+
framework.actionDispatcher.register("confirm", async (action: ConfirmAction) => {
|
|
73
|
+
if (typeof window === "undefined") {
|
|
74
|
+
// SSR: confirmation isn't possible — fall through to the inner action
|
|
75
|
+
await framework.actionDispatcher.dispatch(action.then);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const confirmed = window.confirm(action.message);
|
|
80
|
+
if (!confirmed) return;
|
|
81
|
+
|
|
82
|
+
await framework.actionDispatcher.dispatch(action.then);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Key points:
|
|
88
|
+
|
|
89
|
+
- The handler runs on both server and client. On the server `window` doesn't exist — decide what "no UI" means for your action.
|
|
90
|
+
- Recursive dispatch (`actionDispatcher.dispatch(action.then)`) goes through the regular pipeline, including any other custom handlers.
|
|
91
|
+
- The framework already protects compound actions with a recursion-depth limit (default 4). Your handler is reached via dispatch, so it inherits that limit.
|
|
92
|
+
|
|
93
|
+
## Step 4: register at app startup
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/main.ts
|
|
97
|
+
import { startBrowserApp } from "@finesoft/front/browser";
|
|
98
|
+
import { bootstrap } from "./bootstrap";
|
|
99
|
+
import { registerConfirmHandler } from "./lib/actions/confirm";
|
|
100
|
+
|
|
101
|
+
startBrowserApp({
|
|
102
|
+
bootstrap,
|
|
103
|
+
onBeforeStart(framework) {
|
|
104
|
+
registerConfirmHandler(framework);
|
|
105
|
+
},
|
|
106
|
+
mount: /* ... */,
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Mirror on the SSR side:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// src/ssr.ts
|
|
114
|
+
export const render = createSSRRender({
|
|
115
|
+
bootstrap,
|
|
116
|
+
onBeforeStart(framework) {
|
|
117
|
+
registerConfirmHandler(framework);
|
|
118
|
+
},
|
|
119
|
+
async renderApp(page) {
|
|
120
|
+
/* ... */
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or, simpler: register inside `bootstrap()` so both sides get it automatically.
|
|
126
|
+
|
|
127
|
+
## Step 5: use it
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// In a view component
|
|
131
|
+
import { makeConfirmAction, makeFlowAction } from "@finesoft/front";
|
|
132
|
+
|
|
133
|
+
function onDelete(id: string) {
|
|
134
|
+
framework.actionDispatcher.dispatch(
|
|
135
|
+
makeConfirmAction(`Delete item ${id}?`, makeFlowAction(`/items/${id}/deleted`)),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Replacing an existing handler
|
|
141
|
+
|
|
142
|
+
Each `kind` can be registered exactly once. The dispatcher warns on duplicate registrations and skips:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
framework.actionDispatcher.register("flow", myFlowHandler);
|
|
146
|
+
// [ActionDispatcher] kind="flow" already registered, skipping
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
To replace, unregister first:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
framework.actionDispatcher.removeAction("flow");
|
|
153
|
+
framework.actionDispatcher.register("flow", myFlowHandler);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Useful when you want to wrap the default flow handler with logging or analytics:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { registerFlowActionHandler, type FlowActionDependencies } from "@finesoft/front";
|
|
160
|
+
|
|
161
|
+
const baseHandler = framework.actionDispatcher.getHandler("flow"); // hypothetical
|
|
162
|
+
framework.actionDispatcher.removeAction("flow");
|
|
163
|
+
framework.actionDispatcher.register("flow", async (action) => {
|
|
164
|
+
console.log("[nav]", action.url);
|
|
165
|
+
await baseHandler(action);
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
In practice, prefer middleware (`beforeLoad`) for cross-cutting concerns on navigation — replacing the flow handler is invasive.
|
|
170
|
+
|
|
171
|
+
## Compound actions with custom kinds
|
|
172
|
+
|
|
173
|
+
`CompoundAction` works with any registered kind:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
framework.actionDispatcher.dispatch({
|
|
177
|
+
kind: "compound",
|
|
178
|
+
actions: [
|
|
179
|
+
makeFlowAction("/checkout/complete"),
|
|
180
|
+
makeConfirmAction("Add to email list?", { kind: "subscribe", email: user.email }),
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Each inner action runs sequentially. A handler that throws short-circuits the remaining actions in the compound — wrap with `try/catch` if you want best-effort semantics.
|
|
186
|
+
|
|
187
|
+
## Server-side considerations
|
|
188
|
+
|
|
189
|
+
Action handlers run on the server during SSR if the controller dispatches them. Common patterns:
|
|
190
|
+
|
|
191
|
+
- **External URLs**: the server can't navigate the user — most apps return early. The framework's built-in `external-url` handler does exactly that on SSR.
|
|
192
|
+
- **Confirm-style**: no user to ask. Either auto-accept (use the inner action) or auto-reject (drop it).
|
|
193
|
+
- **Telemetry-only**: works the same on both sides. Just record.
|
|
194
|
+
|
|
195
|
+
If your handler depends on browser APIs that don't exist on the server, gate with `typeof window === "undefined"`.
|
|
196
|
+
|
|
197
|
+
## Testing
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { afterEach, describe, expect, test, vi } from "vite-plus/test";
|
|
201
|
+
import { Framework } from "@finesoft/front";
|
|
202
|
+
import { registerConfirmHandler, makeConfirmAction } from "./confirm";
|
|
203
|
+
|
|
204
|
+
describe("confirm action", () => {
|
|
205
|
+
afterEach(() => vi.restoreAllMocks());
|
|
206
|
+
|
|
207
|
+
test("dispatches inner action when confirmed", async () => {
|
|
208
|
+
const framework = Framework.create({});
|
|
209
|
+
registerConfirmHandler(framework);
|
|
210
|
+
vi.stubGlobal("window", { confirm: () => true });
|
|
211
|
+
|
|
212
|
+
const innerHandler = vi.fn();
|
|
213
|
+
framework.actionDispatcher.register("test", innerHandler);
|
|
214
|
+
|
|
215
|
+
await framework.actionDispatcher.dispatch(
|
|
216
|
+
makeConfirmAction("ok?", { kind: "test" } as any),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(innerHandler).toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("skips inner action when cancelled", async () => {
|
|
223
|
+
const framework = Framework.create({});
|
|
224
|
+
registerConfirmHandler(framework);
|
|
225
|
+
vi.stubGlobal("window", { confirm: () => false });
|
|
226
|
+
|
|
227
|
+
const innerHandler = vi.fn();
|
|
228
|
+
framework.actionDispatcher.register("test", innerHandler);
|
|
229
|
+
|
|
230
|
+
await framework.actionDispatcher.dispatch(
|
|
231
|
+
makeConfirmAction("ok?", { kind: "test" } as any),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(innerHandler).not.toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## When to use a custom action vs middleware
|
|
240
|
+
|
|
241
|
+
| Concern | Custom action | Middleware (`beforeLoad`) |
|
|
242
|
+
| ----------------------------------------------- | ------------- | ---------------------------- |
|
|
243
|
+
| Confirmation before navigating to specific URLs | ✅ | ❌ (would run for every nav) |
|
|
244
|
+
| Audit log on every navigation | ❌ | ✅ |
|
|
245
|
+
| New mechanism for performing an operation | ✅ | ❌ |
|
|
246
|
+
| Gate-keeping all navigation to admin routes | ❌ | ✅ |
|
|
247
|
+
|
|
248
|
+
Custom actions are for **new kinds of operations.** Middleware is for **cross-cutting concerns on existing operations.**
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Advanced: custom adapter
|
|
2
|
+
|
|
3
|
+
Target a platform the framework doesn't ship with. The bundled adapters are Node, Vercel, Cloudflare, Netlify, and Static. Anything else — Deno Deploy, Bun, AWS Lambda, custom on-prem — is a custom adapter.
|
|
4
|
+
|
|
5
|
+
This recipe walks through writing one end-to-end. The pattern: emit a platform-specific entry file at build time, then point that entry at the framework's SSR + proxy pipeline.
|
|
6
|
+
|
|
7
|
+
## What an adapter does
|
|
8
|
+
|
|
9
|
+
At build time:
|
|
10
|
+
|
|
11
|
+
1. Bundle the SSR entry (`src/ssr.ts`) into a single JS file with all dependencies inlined.
|
|
12
|
+
2. Bundle the client entry into the platform's expected shape (`dist/client/` for most).
|
|
13
|
+
3. Emit a **platform-specific entry** that:
|
|
14
|
+
- Imports the SSR bundle
|
|
15
|
+
- Receives requests in the platform's native shape (Request, Lambda event, etc.)
|
|
16
|
+
- Calls `createServer({ ssrEntry, proxies })` and serves the response
|
|
17
|
+
|
|
18
|
+
The framework provides `buildBundle`, `generateSSREntry`, `copyStaticAssets`, and `prerenderRoutes` helpers in `packages/server/src/adapters/shared.ts`. Use them — they handle the heavy lifting consistently across all adapters.
|
|
19
|
+
|
|
20
|
+
## Example: Deno Deploy adapter
|
|
21
|
+
|
|
22
|
+
Deno Deploy runs ES modules with web-standard Request/Response. Workflow is similar to Cloudflare Workers but with native Deno APIs available.
|
|
23
|
+
|
|
24
|
+
### Adapter interface
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// src/lib/adapters/deno-deploy.ts
|
|
28
|
+
import type { AdapterDefinition, AdapterContext } from "@finesoft/front";
|
|
29
|
+
import { buildBundle, copyStaticAssets, generateSSREntry, prerenderRoutes } from "@finesoft/front";
|
|
30
|
+
|
|
31
|
+
export const denoDeployAdapter: AdapterDefinition = {
|
|
32
|
+
name: "deno-deploy",
|
|
33
|
+
|
|
34
|
+
async build(ctx: AdapterContext): Promise<void> {
|
|
35
|
+
// 1. Bundle SSR
|
|
36
|
+
const ssrEntry = generateSSREntry(ctx, {
|
|
37
|
+
// Deno supports native fetch / URL / Response, so no shims needed
|
|
38
|
+
external: [],
|
|
39
|
+
});
|
|
40
|
+
await buildBundle(ctx, {
|
|
41
|
+
entry: ssrEntry,
|
|
42
|
+
outFile: "dist/server.js",
|
|
43
|
+
format: "esm",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 2. Copy static assets
|
|
47
|
+
copyStaticAssets(ctx, "dist/client", "dist/static");
|
|
48
|
+
|
|
49
|
+
// 3. Prerender any prerender routes
|
|
50
|
+
await prerenderRoutes(ctx);
|
|
51
|
+
|
|
52
|
+
// 4. Emit the Deno entry
|
|
53
|
+
writeEntryFile(
|
|
54
|
+
ctx,
|
|
55
|
+
"dist/main.ts",
|
|
56
|
+
`
|
|
57
|
+
import { createServer } from "./server.js";
|
|
58
|
+
const app = createServer({
|
|
59
|
+
ssrEntry: "./server.js",
|
|
60
|
+
staticDir: "./static",
|
|
61
|
+
});
|
|
62
|
+
Deno.serve(app.fetch);
|
|
63
|
+
`,
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Registering
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// vite.config.ts
|
|
73
|
+
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
74
|
+
import { denoDeployAdapter } from "./src/lib/adapters/deno-deploy";
|
|
75
|
+
|
|
76
|
+
export default {
|
|
77
|
+
plugins: [
|
|
78
|
+
finesoftFrontViteConfig({
|
|
79
|
+
ssr: { entry: "src/ssr.ts" },
|
|
80
|
+
adapter: denoDeployAdapter,
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `adapter` option accepts either a string (built-in) or an `AdapterDefinition` (custom).
|
|
87
|
+
|
|
88
|
+
## Adapter context
|
|
89
|
+
|
|
90
|
+
The `AdapterContext` passed to `build()` exposes:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
interface AdapterContext {
|
|
94
|
+
root: string; // absolute path to project root
|
|
95
|
+
outDir: string; // absolute path to dist directory
|
|
96
|
+
ssrEntryPath: string; // resolved path to src/ssr.ts
|
|
97
|
+
routes: RouteDefinition[]; // routes from bootstrap (for prerendering)
|
|
98
|
+
proxies: ProxyRouteConfig[]; // proxy config from finesoftFrontViteConfig
|
|
99
|
+
isr: IsrConfig | null; // ISR config if enabled
|
|
100
|
+
env: Record<string, string>; // build-time env vars
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
You don't typically read all of these — `buildBundle` and `generateSSREntry` take what they need.
|
|
105
|
+
|
|
106
|
+
## Common patterns
|
|
107
|
+
|
|
108
|
+
### Edge runtime (Workers / Deno / Bun)
|
|
109
|
+
|
|
110
|
+
Standard Web APIs (Request, Response, fetch). Bundle as ESM, target `webworker`. Most edge runtimes accept a default-exported handler:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
export default {
|
|
114
|
+
async fetch(request, env) {
|
|
115
|
+
return app.fetch(request, env);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The Cloudflare adapter in `packages/server/src/adapters/cloudflare.ts` is the canonical reference.
|
|
121
|
+
|
|
122
|
+
### Lambda-style (AWS Lambda, GCF, Azure Functions)
|
|
123
|
+
|
|
124
|
+
Platform-specific event shapes. Convert to/from `Request`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { app } from "./server.js";
|
|
128
|
+
|
|
129
|
+
export const handler = async (event: APIGatewayProxyEventV2) => {
|
|
130
|
+
const request = lambdaEventToRequest(event);
|
|
131
|
+
const response = await app.fetch(request);
|
|
132
|
+
return responseToLambdaResult(response);
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Each cloud's SDK ships type definitions and helpers for the event-to-request conversion. Lift-and-shift them; don't reinvent.
|
|
137
|
+
|
|
138
|
+
### Multi-process server (Bun cluster, PM2)
|
|
139
|
+
|
|
140
|
+
Bun and modern Node support `cluster`-style multi-process serving for CPU parallelism:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { app } from "./server.js";
|
|
144
|
+
import { serve } from "@hono/node-server";
|
|
145
|
+
|
|
146
|
+
const port = parseInt(process.env.PORT ?? "3000", 10);
|
|
147
|
+
serve({ fetch: app.fetch, port });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Each process is independent. The ISR cache is per-process — for a true shared cache, put a CDN in front.
|
|
151
|
+
|
|
152
|
+
## Static (no server)
|
|
153
|
+
|
|
154
|
+
`adapter: "static"` is the simplest target — everything is prerendered, nothing runs at request time.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
export const staticAdapter: AdapterDefinition = {
|
|
158
|
+
name: "static",
|
|
159
|
+
async build(ctx) {
|
|
160
|
+
// Skip SSR bundle entirely
|
|
161
|
+
await prerenderRoutes(ctx); // every route must have renderMode: "prerender"
|
|
162
|
+
copyStaticAssets(ctx, "dist/client", "dist/static");
|
|
163
|
+
// No server entry — just the static files
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Verify every route is prerenderable:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
if (!ctx.routes.every((r) => r.renderMode === "prerender")) {
|
|
172
|
+
throw new Error("Static adapter requires every route to be renderMode: 'prerender'");
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Auto-detection extension
|
|
177
|
+
|
|
178
|
+
The built-in `"auto"` adapter checks env vars in order:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
function detectAdapter(env: Record<string, string>): string {
|
|
182
|
+
if (env.VERCEL === "1") return "vercel";
|
|
183
|
+
if (env.CF_PAGES === "1") return "cloudflare";
|
|
184
|
+
if (env.NETLIFY === "true") return "netlify";
|
|
185
|
+
return "node";
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If your custom adapter has a known env signature, you can wrap auto-detection yourself in your project's `vite.config.ts`:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
function pickAdapter() {
|
|
193
|
+
if (process.env.DENO_DEPLOYMENT_ID) return denoDeployAdapter;
|
|
194
|
+
return "node";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
finesoftFrontViteConfig({
|
|
198
|
+
adapter: pickAdapter(),
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Testing the adapter
|
|
203
|
+
|
|
204
|
+
Integration test: run the build, then exercise the emitted entry:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { describe, test, expect } from "vite-plus/test";
|
|
208
|
+
import { build } from "vite";
|
|
209
|
+
import { denoDeployAdapter } from "./deno-deploy";
|
|
210
|
+
|
|
211
|
+
describe("denoDeployAdapter", () => {
|
|
212
|
+
test("emits a Deno-compatible entry", async () => {
|
|
213
|
+
await build({
|
|
214
|
+
root: "test/fixtures/basic",
|
|
215
|
+
plugins: [
|
|
216
|
+
finesoftFrontViteConfig({
|
|
217
|
+
ssr: { entry: "src/ssr.ts" },
|
|
218
|
+
adapter: denoDeployAdapter,
|
|
219
|
+
}),
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const entry = await readFile("test/fixtures/basic/dist/main.ts", "utf-8");
|
|
224
|
+
expect(entry).toContain("Deno.serve");
|
|
225
|
+
expect(entry).toContain("./server.js");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Smoke test the runtime: spin up the actual platform locally and hit `/`. This catches platform-specific quirks (CORS, header normalization, body decoding) that unit tests don't.
|
|
231
|
+
|
|
232
|
+
## Gotchas
|
|
233
|
+
|
|
234
|
+
### Don't bundle Node built-ins on edge runtimes
|
|
235
|
+
|
|
236
|
+
`fs`, `path`, `http`, etc. don't exist on Workers / Deno. `generateSSREntry` accepts an `external` list — set it to the platform-incompatible modules so the bundler errors out at build time rather than the deploy crashing at request time.
|
|
237
|
+
|
|
238
|
+
### `process.env` works differently per platform
|
|
239
|
+
|
|
240
|
+
- Node, Vercel: `process.env.FOO`
|
|
241
|
+
- Cloudflare Workers: secrets via `env` arg to `fetch()`
|
|
242
|
+
- Deno: `Deno.env.get("FOO")`
|
|
243
|
+
|
|
244
|
+
The framework handles `process.env` for declared proxy auth keys, but for your own runtime env reads, wrap them in a platform-aware helper.
|
|
245
|
+
|
|
246
|
+
### File system access for assets
|
|
247
|
+
|
|
248
|
+
If you rely on reading files at request time (rare; most serve through `staticDir`), only Node-like adapters have native fs access. For edge runtimes, embed assets into the bundle or proxy through KV stores.
|
|
249
|
+
|
|
250
|
+
## Submitting upstream
|
|
251
|
+
|
|
252
|
+
If your adapter targets a popular platform that doesn't ship with the framework, consider opening a PR. Adapters live in `packages/server/src/adapters/` and follow a consistent structure — `cloudflare.ts` is the cleanest reference.
|
|
253
|
+
|
|
254
|
+
The framework's adapter API is intentionally small. Keep your contribution minimal:
|
|
255
|
+
|
|
256
|
+
- One file in `adapters/`
|
|
257
|
+
- One entry in `auto.ts` for auto-detection (if applicable)
|
|
258
|
+
- One section in this doc
|
|
259
|
+
|
|
260
|
+
## Related
|
|
261
|
+
|
|
262
|
+
- The bundled adapters: `packages/server/src/adapters/`
|
|
263
|
+
- The shared helpers you'll use: `packages/server/src/adapters/shared.ts`
|
|
264
|
+
- [Chapter 9: Server & deployment](../09-server-and-deployment.md) — what the adapters wrap
|