@chidchanun/bcp 0.1.10 → 0.1.12
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 +43 -2
- package/README.md +131 -9
- package/docs/form-actions.md +314 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-actions.ts +523 -0
- package/packages/bundler/src/server-production-middleware.ts +17 -0
- package/packages/client/src/form.tsx +548 -0
- package/packages/client/src/index.tsx +10 -0
- package/packages/client/src/server.ts +6 -0
- package/packages/server/src/form-action-dev-proxy.ts +601 -0
- package/packages/server/src/form-action-transport.ts +271 -0
- package/packages/server/src/form-action.ts +493 -0
- package/packages/server/src/middleware-dev-server.ts +56 -2
- package/packages/server/src/standalone-production-runtime-v2-action.ts +765 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
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.11 - Form actions and server mutations
|
|
6
|
+
|
|
7
|
+
### Route mutations
|
|
8
|
+
|
|
9
|
+
- Added route-level `actions.ts` / `actions.tsx` files next to `page.tsx` for server-only create, update and delete operations without requiring an API route for every page-owned mutation.
|
|
10
|
+
- Actions are named exports and receive browser `FormData` plus route `params`, target `searchParams`, merged `guardData` and the semantic mutation `method`.
|
|
11
|
+
- Supported action methods are `POST`, `PUT`, `PATCH` and `DELETE`; GET remains a loader/API read concern.
|
|
12
|
+
- Added `PageAction`, `PageActionContext` and `PageActionMethod` types through the server-only `bcp/server` entrypoint.
|
|
13
|
+
- Action modules may use normal server-only application/database imports plus request/session APIs such as `cookies()`, `getSession()`, `requestUrl()` and `requestMethod()`.
|
|
14
|
+
- `requestMethod()` reflects the semantic action method even though progressive HTML forms use POST as the browser transport.
|
|
15
|
+
- Actions may return JSON-safe data, `undefined` (normalized to `null`) or a Web `Response` such as `redirect()`.
|
|
16
|
+
- Action data uses JSON-safe validation and rejects unsupported/circular values before browser transport.
|
|
17
|
+
|
|
18
|
+
### Form client API and progressive enhancement
|
|
19
|
+
|
|
20
|
+
- Added public `<Form action="name">` with `post`, `put`, `patch` and `delete` semantic methods.
|
|
21
|
+
- Added `useFormStatus()` for pending/action/method/error state, `useActionData<T>()` for the latest serializable mutation result and `useActionError()` for transport/runtime failures.
|
|
22
|
+
- Enhanced submissions send the original `FormData` through the internal `/_bcp/action` transport without a document reload.
|
|
23
|
+
- Named actions use strings instead of importing server functions into the client graph, preserving server-only boundaries until the later compiler-backed Server Actions milestone.
|
|
24
|
+
- Same-origin redirects from enhanced actions continue through BCP client navigation while response cookies remain intact.
|
|
25
|
+
- Forms without JavaScript use normal HTML POST and POST/Redirect/GET `303` behavior for data-returning actions, preventing duplicate resubmission on browser refresh.
|
|
26
|
+
- Added optional `<Form refresh>` support to rerun the current BCP navigation/loader after a successful data-returning mutation.
|
|
27
|
+
|
|
28
|
+
### Authorization, caching and production
|
|
29
|
+
|
|
30
|
+
- Route guards execute before matching actions and can stop a mutation with a redirect/Response; merged authorization data is passed to the action through `context.guardData`.
|
|
31
|
+
- Project middleware and framework security remain outside the action gateway, so mutation requests keep the existing request interception and body-limit policy.
|
|
32
|
+
- Cookies created by actions or guards are preserved for data and redirect responses in both enhanced and progressive paths.
|
|
33
|
+
- `revalidatePath()` and `revalidateTag()` can be called directly inside actions using the shared process-local BCP cache runtime.
|
|
34
|
+
- Pages with sibling form actions are excluded from the automatic production response-cache manifest to avoid stale POST/Redirect/GET pages after mutations.
|
|
35
|
+
- Standalone builds generate `server/actions.mjs` only when action routes exist and execute bundled action modules entirely on the server.
|
|
36
|
+
- Added development and standalone action gateways without exposing action/database modules to browser bundles.
|
|
37
|
+
|
|
38
|
+
### Reliability and documentation
|
|
39
|
+
|
|
40
|
+
- Added `/action-demo/[id]` plus guarded mutation fixtures for FormData, semantic methods, cookies, redirects, cache revalidation hooks, progressive enhancement and guard-data handoff.
|
|
41
|
+
- Added unit coverage for action naming, method normalization, route context, Response returns and serialization rejection.
|
|
42
|
+
- Added development integration and standalone E2E coverage for enhanced mutations, no-JavaScript PRG behavior, response cookies and guards-before-actions ordering.
|
|
43
|
+
- Added publish-artifact smoke checks for the public Form API, server mutation runtime, dev gateway, standalone action gateway and production action bundle.
|
|
44
|
+
- Added dedicated Form Actions and Server Mutations documentation.
|
|
45
|
+
|
|
5
46
|
## 0.1.10 - Framework updater and protected route guards
|
|
6
47
|
|
|
7
48
|
### Framework updater
|
|
@@ -20,7 +61,7 @@ All notable framework changes are tracked here before release.
|
|
|
20
61
|
### Protected routes and auth guards
|
|
21
62
|
|
|
22
63
|
- Added scoped `guard.ts` / `guard.tsx` files that protect pages in their route directory and descendant page directories.
|
|
23
|
-
- Guards execute from app root toward the matched page and receive `params`, target `searchParams` and merged ancestor `
|
|
64
|
+
- Guards execute from app root toward the matched page and receive `params`, target `searchParams` and merged ancestor `guardData`.
|
|
24
65
|
- A guard may return JSON-safe authorization data or a Web `Response`; redirects stop the pipeline before loader/page execution.
|
|
25
66
|
- Guards run inside the normal BCP request context, so sessions, cookies, headers, request URLs, request IDs and other `bcp/server` helpers are available directly.
|
|
26
67
|
- Loader context now receives merged `guardData`, avoiding repeated session/role lookups after a guard has already validated the request.
|
|
@@ -65,7 +106,7 @@ All notable framework changes are tracked here before release.
|
|
|
65
106
|
- 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.
|
|
66
107
|
- 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.
|
|
67
108
|
- Non-navigation Web `Response` values continue to fall back to a document request so their original HTTP semantics remain authoritative.
|
|
68
|
-
- Client navigation explicitly sends same-origin credentials, aborts superseded requests and uses a monotonically increasing navigation sequence so stale responses cannot update
|
|
109
|
+
- Client navigation explicitly sends same-origin credentials, aborts superseded requests and uses a monotonically increasing navigation sequence so stale responses cannot update the active route after a newer navigation has started.
|
|
69
110
|
- Navigation redirect chains are capped at 10 redirects to prevent loops.
|
|
70
111
|
- `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.
|
|
71
112
|
- Standalone loader navigation is placed inside the existing middleware/security chain rather than bypassing application middleware or framework security gateways.
|
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 release target: `0.1.
|
|
5
|
+
> Current release target: `0.1.11`. 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
|
|
|
@@ -39,6 +39,7 @@ app/
|
|
|
39
39
|
│ └─ users/
|
|
40
40
|
│ └─ [id]/
|
|
41
41
|
│ ├─ loader.ts
|
|
42
|
+
│ ├─ actions.ts
|
|
42
43
|
│ └─ page.tsx
|
|
43
44
|
└─ api/
|
|
44
45
|
└─ hello/
|
|
@@ -77,7 +78,7 @@ Server-only modules can declare:
|
|
|
77
78
|
import "bcp/server-only";
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
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 a server-only route primitive such as `loader.ts` or `
|
|
81
|
+
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 a server-only route primitive such as `loader.ts`, `guard.ts` or `actions.ts`.
|
|
81
82
|
|
|
82
83
|
See [Application Modules](docs/application-modules.md) for the complete boundary model and examples.
|
|
83
84
|
|
|
@@ -116,7 +117,7 @@ bcp update --dry-run
|
|
|
116
117
|
Or select a published version/dist-tag explicitly:
|
|
117
118
|
|
|
118
119
|
```bash
|
|
119
|
-
bcp update 0.1.
|
|
120
|
+
bcp update 0.1.11
|
|
120
121
|
bcp update next
|
|
121
122
|
```
|
|
122
123
|
|
|
@@ -158,7 +159,7 @@ Supported methods include GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS. HEAD
|
|
|
158
159
|
|
|
159
160
|
## Server request APIs
|
|
160
161
|
|
|
161
|
-
BCP 0.1.7 adds request-scoped server helpers through `bcp/server`. These helpers are server-only and are available from API handlers, server data loaders and
|
|
162
|
+
BCP 0.1.7 adds request-scoped server helpers through `bcp/server`. These helpers are server-only and are available from API handlers, server data loaders, route guards and form actions.
|
|
162
163
|
|
|
163
164
|
```ts
|
|
164
165
|
import {
|
|
@@ -309,7 +310,7 @@ See [Server Data Loaders](docs/server-data-loaders.md) for serialization rules,
|
|
|
309
310
|
|
|
310
311
|
## Protected routes and auth guards
|
|
311
312
|
|
|
312
|
-
|
|
313
|
+
BCP 0.1.10 adds scoped `guard.ts` / `guard.tsx` files for page authorization. A guard protects the pages in its directory and descendant route directories, runs before the page loader, and can use the same request/session APIs as a loader.
|
|
313
314
|
|
|
314
315
|
```text
|
|
315
316
|
app/dashboard/
|
|
@@ -395,10 +396,129 @@ export default function DashboardPage() {
|
|
|
395
396
|
}
|
|
396
397
|
```
|
|
397
398
|
|
|
398
|
-
Nested guards execute from root to child and receive ancestor output as `
|
|
399
|
+
Nested guards execute from root to child and receive merged ancestor output as `guardData`. Guard redirects and response cookies work for both direct document requests and SPA navigation. Guarded pages are excluded from the automatic production response cache, and standalone production strips user-supplied internal guard transport headers before evaluating authorization.
|
|
399
400
|
|
|
400
401
|
See [Protected Route Guards](docs/route-guards.md) for nested role policies, serialization rules, middleware ordering, cookie behavior and production hardening.
|
|
401
402
|
|
|
403
|
+
## Form actions and server mutations
|
|
404
|
+
|
|
405
|
+
BCP 0.1.11 adds route-owned server mutations with `actions.ts` / `actions.tsx`. Actions stay on the server and let page forms perform create, update and delete operations without creating a dedicated API route for every mutation.
|
|
406
|
+
|
|
407
|
+
```text
|
|
408
|
+
app/users/[id]/
|
|
409
|
+
├─ guard.ts
|
|
410
|
+
├─ loader.ts
|
|
411
|
+
├─ actions.ts
|
|
412
|
+
└─ page.tsx
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
// app/users/[id]/actions.ts
|
|
417
|
+
import "bcp/server-only";
|
|
418
|
+
|
|
419
|
+
import {
|
|
420
|
+
redirect,
|
|
421
|
+
type PageActionContext,
|
|
422
|
+
} from "bcp/server";
|
|
423
|
+
|
|
424
|
+
import {
|
|
425
|
+
revalidatePath,
|
|
426
|
+
} from "bcp/cache";
|
|
427
|
+
|
|
428
|
+
export async function saveUser(
|
|
429
|
+
formData: FormData,
|
|
430
|
+
context: PageActionContext
|
|
431
|
+
) {
|
|
432
|
+
const name =
|
|
433
|
+
String(
|
|
434
|
+
formData.get("name") ?? ""
|
|
435
|
+
).trim();
|
|
436
|
+
|
|
437
|
+
// await db.execute(...)
|
|
438
|
+
|
|
439
|
+
revalidatePath("/users");
|
|
440
|
+
|
|
441
|
+
if (!name) {
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
message: "Name is required",
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (
|
|
449
|
+
context.searchParams.get("done") === "1"
|
|
450
|
+
) {
|
|
451
|
+
return redirect(
|
|
452
|
+
"/users",
|
|
453
|
+
303
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
ok: true,
|
|
459
|
+
id: context.params.id,
|
|
460
|
+
name,
|
|
461
|
+
method: context.method,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Client forms reference the named server action without importing it into the browser graph:
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
"use client";
|
|
470
|
+
|
|
471
|
+
import {
|
|
472
|
+
Form,
|
|
473
|
+
useActionData,
|
|
474
|
+
useActionError,
|
|
475
|
+
useFormStatus,
|
|
476
|
+
} from "bcp";
|
|
477
|
+
|
|
478
|
+
function SubmitButton() {
|
|
479
|
+
const status =
|
|
480
|
+
useFormStatus();
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<button
|
|
484
|
+
type="submit"
|
|
485
|
+
disabled={status.pending}
|
|
486
|
+
>
|
|
487
|
+
{status.pending
|
|
488
|
+
? "Saving..."
|
|
489
|
+
: "Save"}
|
|
490
|
+
</button>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export default function UserForm() {
|
|
495
|
+
const result =
|
|
496
|
+
useActionData<{
|
|
497
|
+
ok: boolean;
|
|
498
|
+
message?: string;
|
|
499
|
+
}>();
|
|
500
|
+
const error =
|
|
501
|
+
useActionError();
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<Form
|
|
505
|
+
action="saveUser"
|
|
506
|
+
method="patch"
|
|
507
|
+
refresh
|
|
508
|
+
>
|
|
509
|
+
<input name="name" />
|
|
510
|
+
<SubmitButton />
|
|
511
|
+
{result?.message}
|
|
512
|
+
{error?.message}
|
|
513
|
+
</Form>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Supported semantic methods are POST, PUT, PATCH and DELETE. Guards execute before actions and pass merged authorization state through `context.guardData`. Actions can use cookies, JWT sessions, `requestMethod()`, redirects and cache revalidation. Enhanced forms submit through the BCP SPA transport; forms without JavaScript fall back to standard HTML POST with POST/Redirect/GET semantics. Action-backed pages are excluded from automatic production response caching.
|
|
519
|
+
|
|
520
|
+
See [Form Actions and Server Mutations](docs/form-actions.md) for action return values, pending/error state, progressive enhancement, guards, redirects, cookies, cache invalidation and standalone behavior.
|
|
521
|
+
|
|
402
522
|
## JWT cookie sessions
|
|
403
523
|
|
|
404
524
|
BCP 0.1.8 adds HS256 JWT cookie sessions directly to `bcp/server`. Configure a server-only secret of at least 32 bytes:
|
|
@@ -538,7 +658,7 @@ import {
|
|
|
538
658
|
} from "bcp/cache";
|
|
539
659
|
```
|
|
540
660
|
|
|
541
|
-
The current cache implementation is process-local and intentionally does not provide distributed invalidation across multiple Node.js instances. Loader-backed
|
|
661
|
+
The current cache implementation is process-local and intentionally does not provide distributed invalidation across multiple Node.js instances. Loader-backed, route-guarded and action-backed pages are excluded from the automatic production response-cache manifest when their output or mutation flow may depend on request/session state. Cache server data explicitly only when the application has a safe user-aware cache key and invalidation strategy.
|
|
542
662
|
|
|
543
663
|
## Middleware
|
|
544
664
|
|
|
@@ -563,7 +683,7 @@ export function middleware(
|
|
|
563
683
|
}
|
|
564
684
|
```
|
|
565
685
|
|
|
566
|
-
Use middleware for request-wide interception and `guard.ts` for page-subtree authorization that needs to feed identity/role data into loaders and pages. In standalone production, project middleware runs before route guards.
|
|
686
|
+
Use middleware for request-wide interception and `guard.ts` for page-subtree authorization that needs to feed identity/role data into loaders, actions and pages. In standalone production, project middleware runs before route guards and form actions.
|
|
567
687
|
|
|
568
688
|
## Configuration
|
|
569
689
|
|
|
@@ -617,11 +737,12 @@ The standalone output is generated under:
|
|
|
617
737
|
├─ server.mjs
|
|
618
738
|
├─ middleware.mjs
|
|
619
739
|
├─ guards.mjs
|
|
740
|
+
├─ actions.mjs
|
|
620
741
|
├─ cache-manifest.json
|
|
621
742
|
└─ config.json
|
|
622
743
|
```
|
|
623
744
|
|
|
624
|
-
`guards.mjs` is generated only when the application contains protected route guards.
|
|
745
|
+
`guards.mjs` is generated only when the application contains protected route guards. `actions.mjs` is generated only when the application contains route form actions.
|
|
625
746
|
|
|
626
747
|
## Package preparation
|
|
627
748
|
|
|
@@ -655,6 +776,7 @@ No real npm publish command is run automatically by the repository.
|
|
|
655
776
|
- [Server Request APIs](docs/server-request-apis.md)
|
|
656
777
|
- [Server Data Loaders](docs/server-data-loaders.md)
|
|
657
778
|
- [Protected Route Guards](docs/route-guards.md)
|
|
779
|
+
- [Form Actions and Server Mutations](docs/form-actions.md)
|
|
658
780
|
- [JWT Cookie Sessions](docs/session-auth.md)
|
|
659
781
|
- [Updating BCP Framework](docs/updating.md)
|
|
660
782
|
- [Routing](docs/routing.md)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Form Actions and Server Mutations
|
|
2
|
+
|
|
3
|
+
BCP form actions provide a route-scoped server mutation primitive for application forms without requiring an API route for every create, update or delete operation.
|
|
4
|
+
|
|
5
|
+
This feature is intentionally separate from future BCP Server Actions. In this release, client pages do **not** import a server function directly. A page submits a named action such as `"saveUser"`, and BCP resolves that name from the sibling `actions.ts` / `actions.tsx` file on the server.
|
|
6
|
+
|
|
7
|
+
## Route convention
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
app/users/[id]/
|
|
11
|
+
├─ guard.ts # optional
|
|
12
|
+
├─ loader.ts # optional read path
|
|
13
|
+
├─ actions.ts # server-only mutation path
|
|
14
|
+
└─ page.tsx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Only one of `actions.ts` or `actions.tsx` may exist next to a page.
|
|
18
|
+
|
|
19
|
+
## Defining an action
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// app/users/[id]/actions.ts
|
|
23
|
+
import "bcp/server-only";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
cookies,
|
|
27
|
+
redirect,
|
|
28
|
+
type PageActionContext,
|
|
29
|
+
} from "bcp/server";
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
revalidatePath,
|
|
33
|
+
revalidateTag,
|
|
34
|
+
} from "bcp/cache";
|
|
35
|
+
|
|
36
|
+
export async function saveUser(
|
|
37
|
+
formData: FormData,
|
|
38
|
+
context: PageActionContext
|
|
39
|
+
) {
|
|
40
|
+
const name =
|
|
41
|
+
String(
|
|
42
|
+
formData.get("name") ?? ""
|
|
43
|
+
).trim();
|
|
44
|
+
|
|
45
|
+
// Perform the database mutation here.
|
|
46
|
+
|
|
47
|
+
const cookieStore =
|
|
48
|
+
await cookies();
|
|
49
|
+
|
|
50
|
+
cookieStore.set(
|
|
51
|
+
"last-user",
|
|
52
|
+
context.params.id,
|
|
53
|
+
{
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
sameSite: "lax",
|
|
56
|
+
path: "/",
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
revalidatePath("/users");
|
|
61
|
+
revalidateTag("users");
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
id: context.params.id,
|
|
66
|
+
name,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function removeUser(
|
|
71
|
+
_formData: FormData,
|
|
72
|
+
context: PageActionContext
|
|
73
|
+
) {
|
|
74
|
+
// Delete from the database.
|
|
75
|
+
|
|
76
|
+
revalidatePath("/users");
|
|
77
|
+
|
|
78
|
+
return redirect(
|
|
79
|
+
"/users",
|
|
80
|
+
303
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
An action receives:
|
|
86
|
+
|
|
87
|
+
- `formData` — the submitted browser `FormData`.
|
|
88
|
+
- `context.params` — matched dynamic route parameters.
|
|
89
|
+
- `context.searchParams` — a fresh `URLSearchParams` for the target page URL.
|
|
90
|
+
- `context.guardData` — merged data from route guards that ran before the action.
|
|
91
|
+
- `context.method` — the semantic mutation method: `POST`, `PUT`, `PATCH`, or `DELETE`.
|
|
92
|
+
|
|
93
|
+
The normal `bcp/server` request context is active while the action runs, so `cookies()`, `headers()`, `requestUrl()`, `requestMethod()`, `requestId()`, `getSession()` and other server helpers work directly. `requestMethod()` reflects the semantic action method rather than the internal POST transport.
|
|
94
|
+
|
|
95
|
+
## Rendering a form
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
"use client";
|
|
99
|
+
|
|
100
|
+
import {
|
|
101
|
+
Form,
|
|
102
|
+
useActionData,
|
|
103
|
+
useActionError,
|
|
104
|
+
useFormStatus,
|
|
105
|
+
} from "bcp";
|
|
106
|
+
|
|
107
|
+
interface SaveResult {
|
|
108
|
+
ok: boolean;
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default function UserEditor() {
|
|
114
|
+
return (
|
|
115
|
+
<Form
|
|
116
|
+
action="saveUser"
|
|
117
|
+
method="patch"
|
|
118
|
+
>
|
|
119
|
+
<input
|
|
120
|
+
name="name"
|
|
121
|
+
required
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<SubmitButton />
|
|
125
|
+
<Result />
|
|
126
|
+
</Form>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SubmitButton() {
|
|
131
|
+
const status =
|
|
132
|
+
useFormStatus();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
type="submit"
|
|
137
|
+
disabled={status.pending}
|
|
138
|
+
>
|
|
139
|
+
{status.pending
|
|
140
|
+
? "Saving..."
|
|
141
|
+
: "Save"}
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Result() {
|
|
147
|
+
const data =
|
|
148
|
+
useActionData<SaveResult>();
|
|
149
|
+
const error =
|
|
150
|
+
useActionError();
|
|
151
|
+
|
|
152
|
+
if (error) {
|
|
153
|
+
return (
|
|
154
|
+
<p role="alert">
|
|
155
|
+
{error.message}
|
|
156
|
+
</p>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!data) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<p>
|
|
166
|
+
Saved {data.name}
|
|
167
|
+
</p>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Supported mutation methods
|
|
173
|
+
|
|
174
|
+
`<Form>` supports:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<Form action="createUser" method="post" />
|
|
178
|
+
<Form action="replaceUser" method="put" />
|
|
179
|
+
<Form action="updateUser" method="patch" />
|
|
180
|
+
<Form action="deleteUser" method="delete" />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Browsers only support GET and POST as native HTML form methods. BCP therefore uses POST as the wire transport for progressive enhancement and preserves the intended method separately. The action context and `requestMethod()` still report `PUT`, `PATCH`, or `DELETE` as requested.
|
|
184
|
+
|
|
185
|
+
GET is deliberately not an action method. Reads belong in page loaders or API GET handlers.
|
|
186
|
+
|
|
187
|
+
## Action return values
|
|
188
|
+
|
|
189
|
+
A mutation may return either:
|
|
190
|
+
|
|
191
|
+
1. JSON-safe action data.
|
|
192
|
+
2. `undefined`, which BCP normalizes to `null`.
|
|
193
|
+
3. A Web `Response`, commonly `redirect()`.
|
|
194
|
+
|
|
195
|
+
Serializable action data follows the same safety model as loader data: primitives, arrays and plain objects are allowed. Functions, symbols, BigInt, non-finite numbers, Date/Map/Set/class instances and circular references are rejected before transport.
|
|
196
|
+
|
|
197
|
+
For validation-style UI in this release, return a serializable object such as:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
fieldErrors: {
|
|
203
|
+
email: "Email is required",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Typed validation helpers and CSRF-specific APIs are planned as a separate security/validation milestone.
|
|
209
|
+
|
|
210
|
+
## Pending, data, and transport errors
|
|
211
|
+
|
|
212
|
+
Inside a `<Form>` subtree:
|
|
213
|
+
|
|
214
|
+
- `useFormStatus()` exposes `pending`, `action`, `method`, and the current transport/server error.
|
|
215
|
+
- `useActionData<T>()` exposes the most recent serializable action result.
|
|
216
|
+
- `useActionError()` exposes the most recent thrown transport/runtime error.
|
|
217
|
+
|
|
218
|
+
Submitting the form again clears the previous action data before the new request begins.
|
|
219
|
+
|
|
220
|
+
## Redirects
|
|
221
|
+
|
|
222
|
+
Actions can return a normal BCP redirect:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
return redirect(
|
|
226
|
+
"/users",
|
|
227
|
+
303
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
With JavaScript enabled, BCP converts the redirect into an action transport payload and continues with client navigation. Without JavaScript, the browser receives the normal HTTP redirect.
|
|
232
|
+
|
|
233
|
+
Cookies set before the redirect are preserved in both paths.
|
|
234
|
+
|
|
235
|
+
## Revalidation and refreshing loaders
|
|
236
|
+
|
|
237
|
+
`revalidatePath()` and `revalidateTag()` can be called directly inside an action to invalidate BCP data cache entries:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
revalidatePath("/users");
|
|
241
|
+
revalidateTag("users");
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If the current page should immediately rerun its loader after a successful data-returning action, enable the form's `refresh` option:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<Form
|
|
248
|
+
action="saveUser"
|
|
249
|
+
method="patch"
|
|
250
|
+
refresh
|
|
251
|
+
>
|
|
252
|
+
...
|
|
253
|
+
</Form>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`refresh` uses the existing BCP router refresh path after the action result is received. Redirecting actions do not need `refresh`.
|
|
257
|
+
|
|
258
|
+
## Progressive enhancement
|
|
259
|
+
|
|
260
|
+
The rendered HTML form posts to BCP's internal action endpoint using a normal browser POST. The target page is recovered from the same-origin referrer when JavaScript is unavailable.
|
|
261
|
+
|
|
262
|
+
When an action returns serializable data and no explicit `Response`, the no-JavaScript path uses POST/Redirect/GET with HTTP `303` back to the original target page. This prevents duplicate browser resubmission on refresh.
|
|
263
|
+
|
|
264
|
+
With JavaScript enabled, `<Form>` intercepts the submit and sends the same `FormData` through the action transport without reloading the document.
|
|
265
|
+
|
|
266
|
+
## Middleware, guards, and request order
|
|
267
|
+
|
|
268
|
+
For a mutation, the effective server order is:
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
Security gateway
|
|
272
|
+
↓
|
|
273
|
+
Project middleware
|
|
274
|
+
↓
|
|
275
|
+
Resolve target page
|
|
276
|
+
↓
|
|
277
|
+
Route guards (root → child)
|
|
278
|
+
↓
|
|
279
|
+
Named action
|
|
280
|
+
↓
|
|
281
|
+
Response cookies / redirect / data
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A guard redirect prevents the action from running. Guard data is passed into `context.guardData`, so an authorization lookup does not need to be repeated in the action.
|
|
285
|
+
|
|
286
|
+
## Why action names are strings
|
|
287
|
+
|
|
288
|
+
This release intentionally uses:
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<Form action="saveUser">
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
and not:
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
<Form action={saveUser}>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Passing a server function through a client bundle requires a compiler-generated server reference, serialization protocol and action identifier system. That belongs to the later BCP Server Actions milestone. Keeping named route actions separate now prevents server/database modules from being pulled into hydrated client graphs.
|
|
301
|
+
|
|
302
|
+
## When to use an API route instead
|
|
303
|
+
|
|
304
|
+
Use a form action when the mutation belongs to a BCP page UI.
|
|
305
|
+
|
|
306
|
+
Keep an API route when the caller is an external client such as:
|
|
307
|
+
|
|
308
|
+
- a mobile application,
|
|
309
|
+
- another backend service,
|
|
310
|
+
- a webhook provider,
|
|
311
|
+
- Postman/API consumers,
|
|
312
|
+
- a public or versioned HTTP API.
|
|
313
|
+
|
|
314
|
+
A project can use both patterns: loaders for page reads, guards for authorization, form actions for page-owned mutations, and API routes for external HTTP contracts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|