@chidchanun/bcp 0.1.8 → 0.1.9
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/CHANGELOG.md +41 -0
- package/README.md +82 -4
- package/docs/application-modules.md +63 -3
- package/docs/server-data-loaders.md +240 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production.ts +25 -0
- package/packages/client/src/index.tsx +4 -0
- package/packages/client/src/loader-data.tsx +99 -0
- package/packages/client/src/router-v2.tsx +254 -29
- package/packages/server/src/dev-navigation-target.ts +188 -0
- package/packages/server/src/index.ts +209 -119
- package/packages/server/src/navigation-payload.ts +24 -1
- package/packages/server/src/navigation-response.ts +284 -0
- package/packages/server/src/page-loader.ts +346 -0
- package/packages/server/src/standalone-production-runtime-v2-navigation.ts +785 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +131 -19
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
- package/packages/server/src/standalone-production-runtime-v4.ts +42 -2
- package/packages/server/src/static-dev-server.ts +21 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
All notable framework changes are tracked here before release.
|
|
4
4
|
|
|
5
|
+
## 0.1.9 - Server data loaders
|
|
6
|
+
|
|
7
|
+
### Server rendering and data
|
|
8
|
+
|
|
9
|
+
- Added route-level `loader.ts` / `loader.tsx` files next to `page.tsx` for server-only page data loading before SSR.
|
|
10
|
+
- Added public `useLoaderData<T>()` through `bcp` so the page consumes the same loader value during SSR, browser hydration and later client navigations.
|
|
11
|
+
- Loader context includes matched route `params` and a fresh `URLSearchParams` instance for the target request query string.
|
|
12
|
+
- Loaders run inside the normal request context, so `cookies()`, `headers()`, `requestUrl()`, request/auth helpers and JWT session APIs can be used directly.
|
|
13
|
+
- Loader modules may import application server-only/database modules without placing those dependencies in the browser bundle.
|
|
14
|
+
- Loaders may return either JSON-safe data or a Web `Response`, including `redirect()` responses.
|
|
15
|
+
- Response cookies created while a loader runs are merged into the final page, navigation or redirect response.
|
|
16
|
+
- Loader results are serialized into framework data with the existing script-safe serialization rules and are provided to SSR through an internal loader-data provider.
|
|
17
|
+
- Loader data rejects unsupported values such as functions, symbols, BigInt, non-finite numbers, class instances, Date/Map/Set objects and circular graphs.
|
|
18
|
+
- Having both `loader.ts` and `loader.tsx` next to one page is rejected as an ambiguous route configuration.
|
|
19
|
+
|
|
20
|
+
### SPA navigation and cache safety
|
|
21
|
+
|
|
22
|
+
- Loader-backed routes now participate in BCP SPA navigation instead of requiring a full document reload.
|
|
23
|
+
- `/_bcp/navigation` executes the target route loader on the server and returns fresh loader data together with route metadata and client asset information.
|
|
24
|
+
- Navigation loaders receive a request context for the target page URL rather than the internal navigation transport URL, preserving target query parameters, cookies, sessions and request helpers.
|
|
25
|
+
- Loader redirects are converted to a navigation redirect payload while preserving all response cookies; same-origin redirects continue through the SPA router and cross-origin redirects use normal browser navigation.
|
|
26
|
+
- Non-navigation Web `Response` values continue to fall back to a document request so their original HTTP semantics remain authoritative.
|
|
27
|
+
- Client navigation explicitly sends same-origin credentials, aborts superseded requests and uses a monotonically increasing navigation sequence so stale responses cannot update router state after a newer navigation has started.
|
|
28
|
+
- Navigation redirect chains are capped at 10 redirects to prevent loops.
|
|
29
|
+
- `useNavigation()` becomes active before the server request starts and route loading markup can be displayed while the target client runtime/assets finish after the payload arrives.
|
|
30
|
+
- Standalone loader navigation is placed inside the existing middleware/security chain rather than bypassing application middleware or framework security gateways.
|
|
31
|
+
- Production response-cache manifests continue to exclude loader-backed pages automatically because loader output may be user/request specific.
|
|
32
|
+
- Navigation payload responses use `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`.
|
|
33
|
+
|
|
34
|
+
### Production and reliability
|
|
35
|
+
|
|
36
|
+
- Standalone production entries bundle each loader into the server artifact only and attach it to the corresponding page route definition.
|
|
37
|
+
- Added a standalone loader-navigation gateway only when an application actually has loader-backed routes; applications without loaders keep the existing production runtime path.
|
|
38
|
+
- Development loader imports use a server-side cache-busting version so edits are reflected without creating a client dependency edge.
|
|
39
|
+
- Added a `/loader-demo/[id]` fixture covering route params, query params, cookies, request method, request URL, SSR serialization and redirect response cookies.
|
|
40
|
+
- Added unit coverage for loader discovery, execution, response returns, serialization validation, duplicate-loader detection and SPA redirect/cookie transport.
|
|
41
|
+
- Added development integration coverage for SSR loader data plus SPA navigation data, target request context, sessions and redirect cookies.
|
|
42
|
+
- Added standalone production E2E coverage to verify the same SPA loader protocol after production bundling.
|
|
43
|
+
- Expanded package artifact smoke checks to require the client router, dev navigation target runtime, navigation response transport and standalone loader navigation runtime.
|
|
44
|
+
- Added dedicated server data loader documentation and updated the main README/application boundary guidance.
|
|
45
|
+
|
|
5
46
|
## 0.1.8 - JWT cookie sessions and auth preset
|
|
6
47
|
|
|
7
48
|
### Authentication
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
BCP Framework is a React full-stack framework with file-based routing, SSR, client navigation, API routes, middleware, metadata, client islands, cache/revalidation, security defaults and standalone production builds.
|
|
4
4
|
|
|
5
|
-
> Current development version: `0.1.
|
|
5
|
+
> Current development version: `0.1.9`. BCP is still pre-1.0 and validates each release candidate before the manual npm publish step.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -35,6 +35,7 @@ app/
|
|
|
35
35
|
├─ not-found.tsx
|
|
36
36
|
├─ users/
|
|
37
37
|
│ └─ [id]/
|
|
38
|
+
│ ├─ loader.ts
|
|
38
39
|
│ └─ page.tsx
|
|
39
40
|
└─ api/
|
|
40
41
|
└─ hello/
|
|
@@ -73,7 +74,7 @@ Server-only modules can declare:
|
|
|
73
74
|
import "bcp/server-only";
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
Database helpers generated by `create-bcp-app` include the server-only marker automatically. A server-only helper must not be imported from a page/client graph
|
|
77
|
+
Database helpers generated by `create-bcp-app` include the server-only marker automatically. A server-only helper must not be imported from a hydrated page/client graph. Use an API route or, for page data in BCP 0.1.9+, a route-level `loader.ts` that runs only on the server.
|
|
77
78
|
|
|
78
79
|
See [Application Modules](docs/application-modules.md) for the complete boundary model and examples.
|
|
79
80
|
|
|
@@ -123,7 +124,7 @@ Supported methods include GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS. HEAD
|
|
|
123
124
|
|
|
124
125
|
## Server request APIs
|
|
125
126
|
|
|
126
|
-
BCP 0.1.7 adds request-scoped server helpers through `bcp/server`.
|
|
127
|
+
BCP 0.1.7 adds request-scoped server helpers through `bcp/server`. These helpers are server-only and are available from API handlers and server data loaders.
|
|
127
128
|
|
|
128
129
|
```ts
|
|
129
130
|
import {
|
|
@@ -196,6 +197,82 @@ export async function POST() {
|
|
|
196
197
|
|
|
197
198
|
`redirect()` defaults to status `307`; supported statuses are `301`, `302`, `303`, `307` and `308`. Use `cookieStore.delete("session")` to expire a cookie. See [Server Request APIs](docs/server-request-apis.md) for proxy trust, URL handling, response helpers, request isolation, cookie options and boundary details.
|
|
198
199
|
|
|
200
|
+
## Server data loaders
|
|
201
|
+
|
|
202
|
+
BCP 0.1.9 adds a server-only `loader.ts` convention next to `page.tsx`. The loader runs before SSR, receives route `params` plus `searchParams`, and can use `bcp/server` APIs, sessions and server-only database modules.
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
app/users/[id]/
|
|
206
|
+
├─ loader.ts
|
|
207
|
+
└─ page.tsx
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// app/users/[id]/loader.ts
|
|
212
|
+
import {
|
|
213
|
+
getSession,
|
|
214
|
+
redirect,
|
|
215
|
+
} from "bcp/server";
|
|
216
|
+
|
|
217
|
+
export async function loader({
|
|
218
|
+
params,
|
|
219
|
+
searchParams,
|
|
220
|
+
}: {
|
|
221
|
+
params: {
|
|
222
|
+
id: string;
|
|
223
|
+
};
|
|
224
|
+
searchParams: URLSearchParams;
|
|
225
|
+
}) {
|
|
226
|
+
const session =
|
|
227
|
+
await getSession();
|
|
228
|
+
|
|
229
|
+
if (!session) {
|
|
230
|
+
return redirect(
|
|
231
|
+
"/login",
|
|
232
|
+
303
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
id:
|
|
238
|
+
params.id,
|
|
239
|
+
query:
|
|
240
|
+
searchParams.get("q"),
|
|
241
|
+
session,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Pages consume the serializable result with `useLoaderData<T>()`:
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
"use client";
|
|
250
|
+
|
|
251
|
+
import {
|
|
252
|
+
useLoaderData,
|
|
253
|
+
} from "bcp";
|
|
254
|
+
|
|
255
|
+
export default function UserPage() {
|
|
256
|
+
const data =
|
|
257
|
+
useLoaderData<{
|
|
258
|
+
id: string;
|
|
259
|
+
query: string | null;
|
|
260
|
+
}>();
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<main>
|
|
264
|
+
User {data.id}
|
|
265
|
+
</main>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Loader data is validated as JSON-safe before SSR and is serialized into framework data so hydration reads the same value without importing `loader.ts` into the browser graph. A loader may also return a Web `Response`; response cookies are preserved on redirects.
|
|
271
|
+
|
|
272
|
+
Loader-backed pages now use the same SPA navigation model as normal BCP routes. `<Link>`, `navigate()`, `router.push()`, `router.replace()`, history navigation and `router.refresh()` request `/_bcp/navigation`; the server executes the target loader inside a fresh target-page request context and returns current loader data with the route payload. Same-origin loader redirects continue through the router while preserving `Set-Cookie` headers. Superseded navigations are aborted and sequence-checked so stale responses cannot update the active route. Loader pages remain excluded from the production response-cache manifest by default because their output may depend on session, cookie, request identity or user-specific database state.
|
|
273
|
+
|
|
274
|
+
See [Server Data Loaders](docs/server-data-loaders.md) for serialization rules, authentication patterns, redirect behavior, SPA navigation, cache safety and request-context semantics.
|
|
275
|
+
|
|
199
276
|
## JWT cookie sessions
|
|
200
277
|
|
|
201
278
|
BCP 0.1.8 adds HS256 JWT cookie sessions directly to `bcp/server`. Configure a server-only secret of at least 32 bytes:
|
|
@@ -335,7 +412,7 @@ import {
|
|
|
335
412
|
} from "bcp/cache";
|
|
336
413
|
```
|
|
337
414
|
|
|
338
|
-
The current cache implementation is process-local and intentionally does not provide distributed invalidation across multiple Node.js instances.
|
|
415
|
+
The current cache implementation is process-local and intentionally does not provide distributed invalidation across multiple Node.js instances. Loader-backed pages are excluded from the automatic production response-cache manifest in 0.1.9; cache loader data explicitly only when the application has a safe user-aware cache key and invalidation strategy.
|
|
339
416
|
|
|
340
417
|
## Middleware
|
|
341
418
|
|
|
@@ -445,6 +522,7 @@ No real npm publish command is run automatically by the repository.
|
|
|
445
522
|
- [Getting Started](docs/getting-started.md)
|
|
446
523
|
- [Application Modules](docs/application-modules.md)
|
|
447
524
|
- [Server Request APIs](docs/server-request-apis.md)
|
|
525
|
+
- [Server Data Loaders](docs/server-data-loaders.md)
|
|
448
526
|
- [JWT Cookie Sessions](docs/session-auth.md)
|
|
449
527
|
- [Routing](docs/routing.md)
|
|
450
528
|
- [Configuration](docs/configuration.md)
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "@/lib/database";
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The generated `tsconfig.json` maps `@/*` to the project root. BCP also installs a Node.js resolver during development so the same alias works in SSR and API route modules. Production builds resolve the alias through the application bundler.
|
|
11
|
+
The generated `tsconfig.json` maps `@/*` to the project root. BCP also installs a Node.js resolver during development so the same alias works in SSR, loader and API route modules. Production builds resolve the alias through the application bundler.
|
|
12
12
|
|
|
13
13
|
## Client modules
|
|
14
14
|
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
} from "@/lib/database";
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
For browser-driven data operations, put the database operation behind an API route:
|
|
83
83
|
|
|
84
84
|
```ts
|
|
85
85
|
// app/api/users/route.ts
|
|
@@ -111,6 +111,66 @@ const result =
|
|
|
111
111
|
await response.json();
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
## Server data loaders
|
|
115
|
+
|
|
116
|
+
BCP 0.1.9 adds a server-only page data path. A `loader.ts` next to `page.tsx` is not part of the hydrated page graph and may import server-only database helpers directly.
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
app/users/
|
|
120
|
+
├─ loader.ts
|
|
121
|
+
└─ page.tsx
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// app/users/loader.ts
|
|
126
|
+
import {
|
|
127
|
+
db,
|
|
128
|
+
} from "@/lib/database";
|
|
129
|
+
|
|
130
|
+
export async function loader() {
|
|
131
|
+
const [rows] =
|
|
132
|
+
await db.query(
|
|
133
|
+
"SELECT id, name FROM users"
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
users: rows,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The page consumes only the serializable result:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
"use client";
|
|
146
|
+
|
|
147
|
+
import {
|
|
148
|
+
useLoaderData,
|
|
149
|
+
} from "bcp";
|
|
150
|
+
|
|
151
|
+
export default function UsersPage() {
|
|
152
|
+
const data =
|
|
153
|
+
useLoaderData<{
|
|
154
|
+
users: Array<{
|
|
155
|
+
id: number;
|
|
156
|
+
name: string;
|
|
157
|
+
}>;
|
|
158
|
+
}>();
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<pre>
|
|
162
|
+
{JSON.stringify(
|
|
163
|
+
data.users,
|
|
164
|
+
null,
|
|
165
|
+
2
|
|
166
|
+
)}
|
|
167
|
+
</pre>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The loader itself stays server-only; only its validated JSON-safe result is serialized for hydration. See [Server Data Loaders](server-data-loaders.md) for request context, authentication, redirects and cache behavior.
|
|
173
|
+
|
|
114
174
|
## Current boundary model
|
|
115
175
|
|
|
116
|
-
BCP 0.1.x is not a React Server Components implementation. Pages and layouts can participate in browser hydration, so server-only dependencies must remain behind API routes or other server
|
|
176
|
+
BCP 0.1.x is not a React Server Components implementation. Pages and layouts can participate in browser hydration, so server-only dependencies must remain behind explicit server execution paths: API routes, route-level server data loaders, middleware, or other framework server runtimes. The boundary checks are designed to prevent accidental database or secret-bearing code from entering client bundles.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Server data loaders
|
|
2
|
+
|
|
3
|
+
BCP 0.1.9 adds route-level server data loading for pages. A loader lives next to a page and runs on the server before that page is rendered. Loader-backed routes also participate in BCP client navigation: a navigation request executes the target loader on the server, returns fresh loader data, and renders the next route without requiring a full document reload.
|
|
4
|
+
|
|
5
|
+
## File convention
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
app/
|
|
9
|
+
└─ users/
|
|
10
|
+
└─ [id]/
|
|
11
|
+
├─ loader.ts
|
|
12
|
+
└─ page.tsx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`loader.ts` must export a named `loader()` function:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
getSession,
|
|
20
|
+
redirect,
|
|
21
|
+
} from "bcp/server";
|
|
22
|
+
|
|
23
|
+
export async function loader({
|
|
24
|
+
params,
|
|
25
|
+
searchParams,
|
|
26
|
+
}: {
|
|
27
|
+
params: {
|
|
28
|
+
id: string;
|
|
29
|
+
};
|
|
30
|
+
searchParams: URLSearchParams;
|
|
31
|
+
}) {
|
|
32
|
+
const session =
|
|
33
|
+
await getSession();
|
|
34
|
+
|
|
35
|
+
if (!session) {
|
|
36
|
+
return redirect(
|
|
37
|
+
"/login",
|
|
38
|
+
303
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id:
|
|
44
|
+
params.id,
|
|
45
|
+
filter:
|
|
46
|
+
searchParams.get(
|
|
47
|
+
"filter"
|
|
48
|
+
),
|
|
49
|
+
user:
|
|
50
|
+
session,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The matching page reads the loader result with `useLoaderData<T>()`:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
"use client";
|
|
59
|
+
|
|
60
|
+
import {
|
|
61
|
+
useLoaderData,
|
|
62
|
+
} from "bcp";
|
|
63
|
+
|
|
64
|
+
interface UserLoaderData {
|
|
65
|
+
id: string;
|
|
66
|
+
filter: string | null;
|
|
67
|
+
user: {
|
|
68
|
+
sub: string;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default function UserPage() {
|
|
73
|
+
const data =
|
|
74
|
+
useLoaderData<
|
|
75
|
+
UserLoaderData
|
|
76
|
+
>();
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<main>
|
|
80
|
+
<h1>
|
|
81
|
+
User {data.id}
|
|
82
|
+
</h1>
|
|
83
|
+
</main>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Loader context
|
|
89
|
+
|
|
90
|
+
A loader receives:
|
|
91
|
+
|
|
92
|
+
- `params` — the matched file-route parameters, including dynamic segments.
|
|
93
|
+
- `searchParams` — a fresh `URLSearchParams` instance for the target request URL.
|
|
94
|
+
|
|
95
|
+
Loaders run inside the normal BCP request context. Server-only helpers from `bcp/server`, including `headers()`, `cookies()`, `requestUrl()`, `requestMethod()`, `requestId()`, `clientIp()`, `bearerToken()`, `getSession()` and `redirect()`, are available while the loader is running.
|
|
96
|
+
|
|
97
|
+
For both initial document requests and SPA navigations, the loader request context is created for the target page URL rather than the internal `/_bcp/navigation` transport URL. For example, navigating to `/users/42?filter=active` makes `requestUrl()` observe `/users/42?filter=active` and gives the loader the matching query string.
|
|
98
|
+
|
|
99
|
+
This makes the recommended authenticated page flow:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
Initial document request
|
|
103
|
+
↓
|
|
104
|
+
loader.ts
|
|
105
|
+
↓
|
|
106
|
+
Session / database / authorization
|
|
107
|
+
↓
|
|
108
|
+
serializable loader data
|
|
109
|
+
↓
|
|
110
|
+
SSR page
|
|
111
|
+
↓
|
|
112
|
+
useLoaderData() during hydration
|
|
113
|
+
|
|
114
|
+
Later <Link> / router navigation
|
|
115
|
+
↓
|
|
116
|
+
/_bcp/navigation
|
|
117
|
+
↓
|
|
118
|
+
loader.ts runs for the target URL
|
|
119
|
+
↓
|
|
120
|
+
fresh session / cookie / database state
|
|
121
|
+
↓
|
|
122
|
+
navigation payload with loader data
|
|
123
|
+
↓
|
|
124
|
+
client renders the target route
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Database modules should remain server-only and can be imported directly by `loader.ts`.
|
|
128
|
+
|
|
129
|
+
## Return values
|
|
130
|
+
|
|
131
|
+
A loader can return either serializable data or a Web `Response`.
|
|
132
|
+
|
|
133
|
+
### Data
|
|
134
|
+
|
|
135
|
+
Loader data must contain only JSON-compatible values:
|
|
136
|
+
|
|
137
|
+
- `null`
|
|
138
|
+
- strings
|
|
139
|
+
- booleans
|
|
140
|
+
- finite numbers
|
|
141
|
+
- arrays of supported values
|
|
142
|
+
- plain objects containing supported values
|
|
143
|
+
|
|
144
|
+
Values such as `Date`, `Map`, `Set`, functions, symbols, `BigInt`, class instances, `NaN`, `Infinity` and circular object graphs are rejected. Convert database-specific values to JSON-safe values before returning them.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
export async function loader() {
|
|
148
|
+
const user =
|
|
149
|
+
await findUser();
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
id:
|
|
153
|
+
user.id,
|
|
154
|
+
createdAt:
|
|
155
|
+
user.createdAt.toISOString(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The result is rendered through a server-side loader-data provider and serialized into BCP framework data. During SPA navigation the same data is carried in the navigation payload and written into the framework data before the target client bundle renders. `loader.ts` itself is never imported by the browser entry.
|
|
161
|
+
|
|
162
|
+
### Redirects and other responses
|
|
163
|
+
|
|
164
|
+
A loader can return a standard `Response`, including the BCP `redirect()` helper:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import {
|
|
168
|
+
cookies,
|
|
169
|
+
redirect,
|
|
170
|
+
} from "bcp/server";
|
|
171
|
+
|
|
172
|
+
export async function loader() {
|
|
173
|
+
const cookieStore =
|
|
174
|
+
await cookies();
|
|
175
|
+
|
|
176
|
+
cookieStore.set(
|
|
177
|
+
"last_guard",
|
|
178
|
+
"private-page",
|
|
179
|
+
{
|
|
180
|
+
httpOnly: true,
|
|
181
|
+
sameSite: "lax",
|
|
182
|
+
path: "/",
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return redirect(
|
|
187
|
+
"/login",
|
|
188
|
+
303
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Response cookies created during the loader are merged into the returned response, including redirects.
|
|
194
|
+
|
|
195
|
+
For an SPA navigation, BCP converts an HTTP redirect into a navigation redirect payload while preserving all `Set-Cookie` values. Same-origin redirects continue through the BCP router without a full page reload. Cross-origin redirects are handed to normal browser navigation. The client router limits a single navigation chain to 10 redirects to prevent redirect loops.
|
|
196
|
+
|
|
197
|
+
A loader may also return another Web `Response`. If that response is not a navigation payload or supported redirect response, the client router falls back to a normal document request so the original HTTP semantics are preserved.
|
|
198
|
+
|
|
199
|
+
## Client navigation behavior in 0.1.9
|
|
200
|
+
|
|
201
|
+
Routes with and without loaders both support BCP SPA navigation.
|
|
202
|
+
|
|
203
|
+
When `<Link>`, `navigate()`, `router.push()`, `router.replace()`, browser history navigation, or `router.refresh()` targets a loader-backed page, BCP performs the following work:
|
|
204
|
+
|
|
205
|
+
1. Starts navigation state immediately and aborts the previous in-flight navigation request.
|
|
206
|
+
2. Requests `/_bcp/navigation` with the target pathname and query string.
|
|
207
|
+
3. Runs the target route loader on the server inside a fresh request context.
|
|
208
|
+
4. Returns route metadata, client asset information and the fresh loader data in one navigation payload.
|
|
209
|
+
5. Writes the payload into `__BCP_DATA__` before importing/rendering the target client bundle.
|
|
210
|
+
6. Reuses the existing React root so persistent layouts can keep client state where React reconciliation allows it.
|
|
211
|
+
|
|
212
|
+
`AbortController` cancels superseded network requests. BCP also assigns each navigation a monotonically increasing sequence so a stale response that completes after a newer navigation is not allowed to update router state. The route bundle import receives the navigation identifier as its cache-busting query parameter as well.
|
|
213
|
+
|
|
214
|
+
`loading.tsx` remains part of the navigation payload. `useNavigation()` is set to navigating before the server request starts; once the payload arrives, the matching loading markup can be shown while target assets/runtime work completes.
|
|
215
|
+
|
|
216
|
+
## Middleware and security
|
|
217
|
+
|
|
218
|
+
SPA loader navigation still travels through the normal BCP middleware and security gateway chain before reaching the loader navigation runtime. Applications should still enforce page authorization inside the loader itself because the loader owns the target page data and can make the authorization decision using `getSession()`, cookies, headers or database state.
|
|
219
|
+
|
|
220
|
+
The internal `/_bcp/navigation` endpoint is transport infrastructure and is not a substitute for loader-level authorization checks.
|
|
221
|
+
|
|
222
|
+
## Response cache behavior
|
|
223
|
+
|
|
224
|
+
Loader-backed pages are excluded from the production response-cache manifest by default. A loader can depend on cookies, sessions, authorization headers, request identity or user-specific database state, so automatically placing the resulting HTML in a public route cache would be unsafe.
|
|
225
|
+
|
|
226
|
+
Caching inside a loader can still be implemented explicitly with application-aware data caching where the cache key and invalidation model are known.
|
|
227
|
+
|
|
228
|
+
Navigation payloads are sent with `Cache-Control: no-store` and `X-Content-Type-Options: nosniff`.
|
|
229
|
+
|
|
230
|
+
## Error and not-found behavior
|
|
231
|
+
|
|
232
|
+
Errors thrown by a loader enter the same page error handling path as SSR errors. For navigation requests, server errors or unsupported response shapes cause the router to fall back to a normal document request so the page-level error/not-found rendering path remains authoritative.
|
|
233
|
+
|
|
234
|
+
A loader can also call existing server/runtime primitives that throw the framework not-found signal; the matching `not-found.tsx` boundary remains responsible for rendering the 404 page.
|
|
235
|
+
|
|
236
|
+
## Development reloads
|
|
237
|
+
|
|
238
|
+
`loader.ts` and `loader.tsx` are watched as server files during development. Loader modules use a server-side cache-busting version during document rendering and navigation, so edits are reflected without putting loader code in the browser dependency graph.
|
|
239
|
+
|
|
240
|
+
Only one loader file may exist next to a page. Having both `loader.ts` and `loader.tsx` is treated as a framework configuration error.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,10 @@ import type {
|
|
|
22
22
|
Route,
|
|
23
23
|
} from "../../router/src/index.js";
|
|
24
24
|
|
|
25
|
+
import {
|
|
26
|
+
findPageLoaderFile,
|
|
27
|
+
} from "../../server/src/page-loader.js";
|
|
28
|
+
|
|
25
29
|
import type {
|
|
26
30
|
PartialHydrationBuildManifest,
|
|
27
31
|
} from "./partial-hydration.js";
|
|
@@ -416,6 +420,26 @@ function createServerEntry(
|
|
|
416
420
|
`const ${pageName} = ${pageModuleName}.default;`
|
|
417
421
|
);
|
|
418
422
|
|
|
423
|
+
const loaderFile =
|
|
424
|
+
findPageLoaderFile(
|
|
425
|
+
route.filePath
|
|
426
|
+
);
|
|
427
|
+
let loaderName =
|
|
428
|
+
"null";
|
|
429
|
+
|
|
430
|
+
if (loaderFile) {
|
|
431
|
+
loaderName =
|
|
432
|
+
`Loader${routeIndex}`;
|
|
433
|
+
imports.push(
|
|
434
|
+
`import { loader as ${loaderName} } from ${JSON.stringify(
|
|
435
|
+
toImportSpecifier(
|
|
436
|
+
entryDirectory,
|
|
437
|
+
loaderFile
|
|
438
|
+
)
|
|
439
|
+
)};`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
419
443
|
const layoutModules:
|
|
420
444
|
Array<{
|
|
421
445
|
name: string;
|
|
@@ -630,6 +654,7 @@ function createServerEntry(
|
|
|
630
654
|
error: ${errorName},
|
|
631
655
|
notFound: ${notFoundName},
|
|
632
656
|
metadataSources: [${metadataSources.join(", ")}],
|
|
657
|
+
loader: ${loaderName},
|
|
633
658
|
hydration: ${JSON.stringify(hydration)},
|
|
634
659
|
client: ${JSON.stringify(client)}
|
|
635
660
|
}`);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
createElement,
|
|
4
|
+
useContext,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from "react";
|
|
7
|
+
|
|
8
|
+
const LOADER_DATA_PARAM =
|
|
9
|
+
"__bcp_loader_data";
|
|
10
|
+
const MISSING_LOADER_DATA =
|
|
11
|
+
Symbol(
|
|
12
|
+
"bcp-loader-data"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const LoaderDataContext =
|
|
16
|
+
createContext<unknown>(
|
|
17
|
+
MISSING_LOADER_DATA
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export interface BcpLoaderDataProviderProps {
|
|
21
|
+
data: unknown;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function BcpLoaderDataProvider({
|
|
26
|
+
data,
|
|
27
|
+
children,
|
|
28
|
+
}: BcpLoaderDataProviderProps) {
|
|
29
|
+
return createElement(
|
|
30
|
+
LoaderDataContext.Provider,
|
|
31
|
+
{
|
|
32
|
+
value:
|
|
33
|
+
data,
|
|
34
|
+
},
|
|
35
|
+
children
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useLoaderData<
|
|
40
|
+
T = unknown
|
|
41
|
+
>(): T {
|
|
42
|
+
const contextValue =
|
|
43
|
+
useContext(
|
|
44
|
+
LoaderDataContext
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
contextValue !==
|
|
49
|
+
MISSING_LOADER_DATA
|
|
50
|
+
) {
|
|
51
|
+
return contextValue as T;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
typeof document !==
|
|
56
|
+
"undefined"
|
|
57
|
+
) {
|
|
58
|
+
const element =
|
|
59
|
+
document.getElementById(
|
|
60
|
+
"__BCP_DATA__"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (element) {
|
|
64
|
+
try {
|
|
65
|
+
const frameworkData =
|
|
66
|
+
JSON.parse(
|
|
67
|
+
element.textContent ||
|
|
68
|
+
"{}"
|
|
69
|
+
) as {
|
|
70
|
+
params?: Record<
|
|
71
|
+
string,
|
|
72
|
+
unknown
|
|
73
|
+
>;
|
|
74
|
+
};
|
|
75
|
+
const params =
|
|
76
|
+
frameworkData.params;
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
params &&
|
|
80
|
+
Object.prototype
|
|
81
|
+
.hasOwnProperty.call(
|
|
82
|
+
params,
|
|
83
|
+
LOADER_DATA_PARAM
|
|
84
|
+
)
|
|
85
|
+
) {
|
|
86
|
+
return params[
|
|
87
|
+
LOADER_DATA_PARAM
|
|
88
|
+
] as T;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Fall through to the framework error below.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new Error(
|
|
97
|
+
"BCP Framework: useLoaderData() was used on a route without loader data. Add loader.ts next to page.tsx."
|
|
98
|
+
);
|
|
99
|
+
}
|