@chidchanun/bcp 0.1.8 → 0.1.10
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 +82 -0
- package/README.md +218 -7
- package/docs/application-modules.md +63 -3
- package/docs/releasing.md +34 -19
- package/docs/route-guards.md +240 -0
- package/docs/server-data-loaders.md +240 -0
- package/docs/updating.md +130 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-guards.ts +497 -0
- package/packages/bundler/src/server-production-middleware.ts +33 -8
- package/packages/bundler/src/server-production.ts +25 -0
- package/packages/cli/src/args.ts +58 -0
- package/packages/cli/src/index.ts +41 -13
- package/packages/cli/src/update.ts +860 -0
- package/packages/client/src/index.tsx +5 -0
- package/packages/client/src/loader-data.tsx +229 -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-guard.ts +529 -0
- package/packages/server/src/page-loader.ts +643 -0
- package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Protected Route Guards
|
|
2
|
+
|
|
3
|
+
BCP route guards provide server-side protection for pages and page subtrees without repeating authentication checks in every `loader.ts`.
|
|
4
|
+
|
|
5
|
+
A guard lives in `guard.ts` or `guard.tsx` inside the `app/` tree. It protects pages in that directory and descendant route directories.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
app/
|
|
9
|
+
└─ dashboard/
|
|
10
|
+
├─ guard.ts
|
|
11
|
+
├─ page.tsx
|
|
12
|
+
└─ users/
|
|
13
|
+
└─ [id]/
|
|
14
|
+
├─ loader.ts
|
|
15
|
+
└─ page.tsx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Authentication guard
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// app/dashboard/guard.ts
|
|
22
|
+
import {
|
|
23
|
+
getSession,
|
|
24
|
+
redirect,
|
|
25
|
+
} from "bcp/server";
|
|
26
|
+
|
|
27
|
+
export async function guard() {
|
|
28
|
+
const session =
|
|
29
|
+
await getSession<{
|
|
30
|
+
userId: number;
|
|
31
|
+
email: string;
|
|
32
|
+
role: string;
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
if (!session) {
|
|
36
|
+
return redirect(
|
|
37
|
+
"/login",
|
|
38
|
+
303
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
session,
|
|
44
|
+
role:
|
|
45
|
+
session.role,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The guard runs before the route loader and page render. Returning a Web `Response`, including `redirect()`, stops the pipeline immediately.
|
|
51
|
+
|
|
52
|
+
## Guard context
|
|
53
|
+
|
|
54
|
+
A guard receives:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface GuardContext {
|
|
58
|
+
params: Record<string, string | string[] | undefined>;
|
|
59
|
+
searchParams: URLSearchParams;
|
|
60
|
+
parentData: Readonly<Record<string, unknown>>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`params` contains the matched route params and `searchParams` represents the target page URL. `parentData` contains the JSON-safe values returned by ancestor guards.
|
|
65
|
+
|
|
66
|
+
Example nested role guard:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// app/dashboard/admin/guard.ts
|
|
70
|
+
import {
|
|
71
|
+
redirect,
|
|
72
|
+
} from "bcp/server";
|
|
73
|
+
|
|
74
|
+
export async function guard({
|
|
75
|
+
parentData,
|
|
76
|
+
}) {
|
|
77
|
+
if (
|
|
78
|
+
parentData.role !==
|
|
79
|
+
"admin"
|
|
80
|
+
) {
|
|
81
|
+
return redirect(
|
|
82
|
+
"/dashboard",
|
|
83
|
+
303
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
section:
|
|
89
|
+
"admin",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Guards execute from the app root toward the page directory. Later guards receive the merged output of earlier guards.
|
|
95
|
+
|
|
96
|
+
## Using guard data in a loader
|
|
97
|
+
|
|
98
|
+
Loaders receive the final merged guard data as `guardData`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// app/dashboard/users/[id]/loader.ts
|
|
102
|
+
export async function loader({
|
|
103
|
+
params,
|
|
104
|
+
guardData,
|
|
105
|
+
}) {
|
|
106
|
+
return {
|
|
107
|
+
id:
|
|
108
|
+
params.id,
|
|
109
|
+
currentUser:
|
|
110
|
+
guardData.session,
|
|
111
|
+
role:
|
|
112
|
+
guardData.role,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This avoids calling `getSession()` again after the guard has already validated the request.
|
|
118
|
+
|
|
119
|
+
## Using guard data in a page
|
|
120
|
+
|
|
121
|
+
Pages can read the same serialized guard result with `useGuardData<T>()`:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
"use client";
|
|
125
|
+
|
|
126
|
+
import {
|
|
127
|
+
useGuardData,
|
|
128
|
+
} from "bcp";
|
|
129
|
+
|
|
130
|
+
export default function DashboardPage() {
|
|
131
|
+
const guard =
|
|
132
|
+
useGuardData<{
|
|
133
|
+
session: {
|
|
134
|
+
userId: number;
|
|
135
|
+
email: string;
|
|
136
|
+
};
|
|
137
|
+
role: string;
|
|
138
|
+
}>();
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<main>
|
|
142
|
+
Signed in as {
|
|
143
|
+
guard.session.email
|
|
144
|
+
}
|
|
145
|
+
</main>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Guard data is available during SSR, hydration and SPA navigation. A guarded page does not need a `loader.ts` just to expose guard data.
|
|
151
|
+
|
|
152
|
+
## Serialization rules
|
|
153
|
+
|
|
154
|
+
Guard return values must be JSON-safe because they are passed to loaders and, when the page reads them, serialized into framework navigation/SSR data.
|
|
155
|
+
|
|
156
|
+
Supported values include:
|
|
157
|
+
|
|
158
|
+
- `null`
|
|
159
|
+
- strings
|
|
160
|
+
- booleans
|
|
161
|
+
- finite numbers
|
|
162
|
+
- arrays containing supported values
|
|
163
|
+
- plain objects containing supported values
|
|
164
|
+
|
|
165
|
+
Unsupported values include functions, symbols, BigInt, non-finite numbers, class instances, Date/Map/Set objects and circular references.
|
|
166
|
+
|
|
167
|
+
Do not place passwords, raw authentication secrets, private keys or other sensitive server-only secrets in guard data. Guard data may be serialized to the browser when the page uses the route framework data pipeline.
|
|
168
|
+
|
|
169
|
+
## Cookies and sessions
|
|
170
|
+
|
|
171
|
+
Guards run inside the normal BCP request context, so they can use:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import {
|
|
175
|
+
cookies,
|
|
176
|
+
getSession,
|
|
177
|
+
headers,
|
|
178
|
+
requestId,
|
|
179
|
+
requestMethod,
|
|
180
|
+
requestUrl,
|
|
181
|
+
} from "bcp/server";
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Cookies set while an allowed guard runs are preserved on the final document or SPA navigation response. Cookies set before a guard redirect are preserved on that redirect as well.
|
|
185
|
+
|
|
186
|
+
## Direct requests and SPA navigation
|
|
187
|
+
|
|
188
|
+
The same guard rules apply to:
|
|
189
|
+
|
|
190
|
+
- direct browser requests
|
|
191
|
+
- `<Link>` navigation
|
|
192
|
+
- `navigate()`
|
|
193
|
+
- `router.push()`
|
|
194
|
+
- `router.replace()`
|
|
195
|
+
- browser history navigation
|
|
196
|
+
- loader-backed SPA navigation
|
|
197
|
+
|
|
198
|
+
For SPA navigation, the guard evaluates the target page URL rather than the internal `/_bcp/navigation` transport URL.
|
|
199
|
+
|
|
200
|
+
A guard redirect is converted to a navigation redirect payload when appropriate so same-origin navigation can continue through the BCP router while preserving response cookies.
|
|
201
|
+
|
|
202
|
+
## Middleware and guard order
|
|
203
|
+
|
|
204
|
+
Production request order is intentionally layered:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
Security gateway
|
|
208
|
+
↓
|
|
209
|
+
Response cache gateway
|
|
210
|
+
↓
|
|
211
|
+
Project middleware
|
|
212
|
+
↓
|
|
213
|
+
Route guard
|
|
214
|
+
↓
|
|
215
|
+
Page loader
|
|
216
|
+
↓
|
|
217
|
+
SSR / navigation payload
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Project middleware therefore remains the outer application interception layer. Route guards are intended for page authorization and route-specific access policy.
|
|
221
|
+
|
|
222
|
+
## Cache safety
|
|
223
|
+
|
|
224
|
+
Pages protected by route guards are excluded from the automatic production page response cache. Guard results may depend on sessions, cookies, roles or user-specific state, so caching a guarded document without a user-aware key would be unsafe.
|
|
225
|
+
|
|
226
|
+
Applications can still use explicit server data caching where the cache key and invalidation strategy are safe for the data being cached.
|
|
227
|
+
|
|
228
|
+
## Internal transport hardening
|
|
229
|
+
|
|
230
|
+
Standalone production transports evaluated guard data to the inner page runtime through a private framework header. Incoming user-supplied values for that header are removed before guard evaluation and before proxying so a browser cannot forge a successful guard result.
|
|
231
|
+
|
|
232
|
+
The serialized internal guard payload is size-limited. Applications should return only the small identity/authorization data required by descendant guards, loaders and pages.
|
|
233
|
+
|
|
234
|
+
## Guard versus middleware
|
|
235
|
+
|
|
236
|
+
Use a route guard when access policy belongs to a page subtree and should integrate directly with loaders and page data.
|
|
237
|
+
|
|
238
|
+
Use middleware when a request-wide concern needs to run before route execution, such as rewrites, global redirects, shared request policy or non-page request interception.
|
|
239
|
+
|
|
240
|
+
Both can be used together; middleware runs before route guards.
|
|
@@ -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/docs/updating.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Updating BCP Framework
|
|
2
|
+
|
|
3
|
+
BCP includes a project updater that can move an existing application to a newer published framework version and refresh the active package-manager lockfile.
|
|
4
|
+
|
|
5
|
+
## First update from 0.1.9 or older
|
|
6
|
+
|
|
7
|
+
BCP versions published before the updater do not know the `update` command. Run the newest CLI explicitly once:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @chidchanun/bcp@latest update
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The updater adds this project script when the application does not already define its own `update` script:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"scripts": {
|
|
18
|
+
"update": "bcp update"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Normal future updates can therefore use either:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bcp update
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
or, without a global BCP installation:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm run update
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Update to latest
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bcp update
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This resolves `@chidchanun/bcp@latest` from npm, pins that exact resolved release in `package.json`, detects the project's package manager and runs its install command.
|
|
42
|
+
|
|
43
|
+
For example, if `latest` resolves to `0.1.10`, a generated application becomes:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"bcp": "npm:@chidchanun/bcp@0.1.10"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The exact pin is intentional. A later plain `npm install` should not silently move the framework to a release that the BCP updater did not explicitly resolve.
|
|
54
|
+
|
|
55
|
+
Generated applications use the dependency key `bcp` with an npm alias to `@chidchanun/bcp`. The updater also recognizes a direct `@chidchanun/bcp` dependency.
|
|
56
|
+
|
|
57
|
+
## Update to a specific version or dist-tag
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bcp update 0.1.10
|
|
61
|
+
bcp update next
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The target may be an exact published version or an npm dist-tag. The updater always records the exact version that the registry resolves.
|
|
65
|
+
|
|
66
|
+
## Check without changing files
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bcp update --check
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The command resolves the current `latest` target and prints the installed/declaration state without modifying `package.json` or the lockfile.
|
|
73
|
+
|
|
74
|
+
## Dry run
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bcp update 0.1.10 --dry-run
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A dry run resolves the target and shows the dependency/script changes without writing files or installing packages.
|
|
81
|
+
|
|
82
|
+
## Project root
|
|
83
|
+
|
|
84
|
+
Use `--root` when updating a project from another directory:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bcp update --root ./apps/admin
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Package-manager detection
|
|
91
|
+
|
|
92
|
+
BCP detects the package manager from its lockfile:
|
|
93
|
+
|
|
94
|
+
- `package-lock.json` -> npm
|
|
95
|
+
- `pnpm-lock.yaml` -> pnpm
|
|
96
|
+
- `yarn.lock` -> Yarn
|
|
97
|
+
- `bun.lock` / `bun.lockb` -> Bun
|
|
98
|
+
|
|
99
|
+
If no lockfile exists, BCP uses the invoking package-manager user agent when available and otherwise falls back to npm. If lockfiles from multiple package managers are present, the updater stops instead of guessing.
|
|
100
|
+
|
|
101
|
+
Registry version discovery currently uses npm's registry CLI (`npm view`) even when the project itself installs dependencies with pnpm, Yarn or Bun.
|
|
102
|
+
|
|
103
|
+
## Failure safety
|
|
104
|
+
|
|
105
|
+
Before installation BCP keeps the original `package.json` and detected lockfile in memory. If the package-manager install fails, those files are restored before the command exits with an error.
|
|
106
|
+
|
|
107
|
+
A package manager may still have touched `node_modules` before failing, so run the normal install command again after resolving the underlying package-manager or network problem.
|
|
108
|
+
|
|
109
|
+
## What the updater changes
|
|
110
|
+
|
|
111
|
+
The updater changes the framework dependency, adds `scripts.update = "bcp update"` when no update script exists, and refreshes the package-manager lockfile. An existing custom `update` script is never overwritten.
|
|
112
|
+
|
|
113
|
+
It does not overwrite application source code, environment files, authentication implementations, database schemas or custom configuration.
|
|
114
|
+
|
|
115
|
+
Features that were originally generator presets, such as JWT auth scaffolding, are not automatically injected into an existing application merely by updating the framework package.
|
|
116
|
+
|
|
117
|
+
## Recommended verification
|
|
118
|
+
|
|
119
|
+
After an update, run the checks used by your project, for example:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm run typecheck
|
|
123
|
+
npm run build
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Commit `package.json` and the lockfile together after verification.
|
|
127
|
+
|
|
128
|
+
## Release channels
|
|
129
|
+
|
|
130
|
+
Stable BCP releases use the npm `latest` dist-tag. A release maintainer can still publish an explicit pre-release channel with `BCP_DIST_TAG`, for example `BCP_DIST_TAG=next`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|