@cmj/juice 0.3.1 → 0.5.0
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 +30 -1
- package/dist/client/client.d.ts +57 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +197 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/runtime/actions.d.ts +61 -8
- package/dist/runtime/actions.d.ts.map +1 -1
- package/dist/runtime/actions.js +109 -23
- package/dist/runtime/actions.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +3 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/matcher.d.ts.map +1 -1
- package/dist/runtime/matcher.js +39 -13
- package/dist/runtime/matcher.js.map +1 -1
- package/dist/runtime/router.d.ts.map +1 -1
- package/dist/runtime/router.js +10 -2
- package/dist/runtime/router.js.map +1 -1
- package/dist/runtime/rsc.d.ts +46 -0
- package/dist/runtime/rsc.d.ts.map +1 -0
- package/dist/runtime/rsc.js +149 -0
- package/dist/runtime/rsc.js.map +1 -0
- package/dist/runtime/types.d.ts +96 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -63,6 +63,34 @@ const handler = createRouter(manifest);
|
|
|
63
63
|
// handler: (Request) => Promise<Response>
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
### Server Actions (Two-Argument Pattern)
|
|
67
|
+
|
|
68
|
+
Simple form actions receive `FormData` as the first argument — React 19 native. When you need more power, use `ActionContext` as the opt-in second argument:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
'use server';
|
|
72
|
+
import type { ActionContext } from '@cmj/juice/runtime';
|
|
73
|
+
|
|
74
|
+
// Simple: FormData-only (React 19 native)
|
|
75
|
+
export async function addToCart(formData: FormData) {
|
|
76
|
+
const id = formData.get('productId');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Power: headers, cookies, params via second arg
|
|
80
|
+
export async function processWebhook(body: unknown, ctx: ActionContext) {
|
|
81
|
+
ctx.request.headers.get('x-signature');
|
|
82
|
+
ctx.cookies.get('session_id');
|
|
83
|
+
ctx.params.id;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Test your actions with the public `createActionContext` factory:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createActionContext } from '@cmj/juice/runtime';
|
|
91
|
+
const ctx = createActionContext(new Request('https://example.com'));
|
|
92
|
+
```
|
|
93
|
+
|
|
66
94
|
## How It Works
|
|
67
95
|
|
|
68
96
|
```
|
|
@@ -76,12 +104,13 @@ Source (.tsx) → Vite Plugin → flight-manifest.json → Runtime → Response
|
|
|
76
104
|
## Features
|
|
77
105
|
|
|
78
106
|
- **React 19 RSC** — Server Components, Suspense, streaming SSR
|
|
79
|
-
- **Server Actions** — `'use server'` with FormData
|
|
107
|
+
- **Server Actions** — `'use server'` with FormData + opt-in `ActionContext` for headers, cookies, params
|
|
80
108
|
- **Zero config** — One plugin call, no magic files
|
|
81
109
|
- **One dependency** — Only `urlpattern-polyfill` for cross-platform routing
|
|
82
110
|
- **Multi-platform** — Bun, Node.js, Cloudflare Workers, Deno
|
|
83
111
|
- **Empathic errors** — "What-Why-How" error messages for fast debugging
|
|
84
112
|
- **HMR** — Full hot module replacement in development
|
|
113
|
+
- **Testable actions** — Public `createActionContext` factory for unit testing
|
|
85
114
|
|
|
86
115
|
## License
|
|
87
116
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Callback to update the React root with a new RSC payload.
|
|
4
|
+
* Set by `initNavigation()`.
|
|
5
|
+
*/
|
|
6
|
+
type PageSetter = (node: ReactNode) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Options for `initNavigation`.
|
|
9
|
+
*/
|
|
10
|
+
export interface NavigationOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Whether to use View Transitions API for page transitions.
|
|
13
|
+
* Falls back to instant swap if the API is not available.
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
viewTransitions?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Enable prefetching of RSC payloads on hover/focus of `<a>` tags.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
prefetch?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Custom function to call server actions via RSC protocol.
|
|
24
|
+
* Used by `createFromFetch` for `'use server'` invocations.
|
|
25
|
+
*/
|
|
26
|
+
callServer?: (id: string, args: unknown[]) => Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Initialize client-side SPA navigation using the Navigation API.
|
|
30
|
+
*
|
|
31
|
+
* Call this once after hydrating your React root. Subsequent navigations
|
|
32
|
+
* (clicking `<a>` tags, browser back/forward) will fetch RSC payloads
|
|
33
|
+
* from the server and update the React tree without full page reloads.
|
|
34
|
+
*
|
|
35
|
+
* @param setPage - Callback to update the React root with new content.
|
|
36
|
+
* Typically wraps a `useState` setter.
|
|
37
|
+
* @param options - Configuration options.
|
|
38
|
+
*
|
|
39
|
+
* @returns A cleanup function that removes the navigation listener.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* import { hydrateRoot } from 'react-dom/client';
|
|
44
|
+
* import { initNavigation } from '@cmj/juice/client';
|
|
45
|
+
*
|
|
46
|
+
* function App() {
|
|
47
|
+
* const [page, setPage] = useState<ReactNode>(initialServerContent);
|
|
48
|
+
* useEffect(() => initNavigation(setPage), []);
|
|
49
|
+
* return <>{page}</>;
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* hydrateRoot(document, <App />);
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function initNavigation(setPage: PageSetter, options?: NavigationOptions): () => void;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"AASA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AASxD;;;GAGG;AACH,KAAK,UAAU,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAChE;AAoGD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,UAAU,EACnB,OAAO,GAAE,iBAAsB,GAC9B,MAAM,IAAI,CAmFZ"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// ── juice/client — Navigation API bridge ──────────────────────────
|
|
2
|
+
// Intercepts browser navigations via the Navigation API and fetches
|
|
3
|
+
// RSC payloads from the server for seamless SPA transitions.
|
|
4
|
+
//
|
|
5
|
+
// ~2KB minified. Zero dependencies beyond React 19.
|
|
6
|
+
// Progressive enhancement: falls back to MPA if Navigation API
|
|
7
|
+
// is not available or JavaScript is disabled.
|
|
8
|
+
// ────────────────────────────────────────────────────────────────────
|
|
9
|
+
import { startTransition } from 'react';
|
|
10
|
+
import { createFromFetch } from 'react-server-dom-webpack/client.browser';
|
|
11
|
+
/**
|
|
12
|
+
* The RSC content type used for payload negotiation.
|
|
13
|
+
* Must match the server's `RSC_CONTENT_TYPE` in `rsc.ts`.
|
|
14
|
+
*/
|
|
15
|
+
const RSC_CONTENT_TYPE = 'text/x-component';
|
|
16
|
+
// ── Prefetch cache ────────────────────────────────────────────────
|
|
17
|
+
const prefetchCache = new Map();
|
|
18
|
+
function prefetchRSC(url) {
|
|
19
|
+
if (prefetchCache.has(url))
|
|
20
|
+
return;
|
|
21
|
+
const promise = fetch(url, {
|
|
22
|
+
headers: { 'Accept': RSC_CONTENT_TYPE },
|
|
23
|
+
priority: 'low',
|
|
24
|
+
});
|
|
25
|
+
prefetchCache.set(url, promise);
|
|
26
|
+
// Expire after 30 seconds to avoid stale data
|
|
27
|
+
setTimeout(() => prefetchCache.delete(url), 30_000);
|
|
28
|
+
}
|
|
29
|
+
// ── Prefetch observer ─────────────────────────────────────────────
|
|
30
|
+
function setupPrefetching() {
|
|
31
|
+
// Prefetch on hover (desktop) and focus (keyboard nav)
|
|
32
|
+
document.addEventListener('pointerenter', (e) => {
|
|
33
|
+
const anchor = e.target.closest('a[href]');
|
|
34
|
+
if (!anchor)
|
|
35
|
+
return;
|
|
36
|
+
const href = anchor.getAttribute('href');
|
|
37
|
+
if (href && isSameOrigin(href)) {
|
|
38
|
+
prefetchRSC(new URL(href, location.href).href);
|
|
39
|
+
}
|
|
40
|
+
}, { capture: true, passive: true });
|
|
41
|
+
document.addEventListener('focusin', (e) => {
|
|
42
|
+
const anchor = e.target.closest('a[href]');
|
|
43
|
+
if (!anchor)
|
|
44
|
+
return;
|
|
45
|
+
const href = anchor.getAttribute('href');
|
|
46
|
+
if (href && isSameOrigin(href)) {
|
|
47
|
+
prefetchRSC(new URL(href, location.href).href);
|
|
48
|
+
}
|
|
49
|
+
}, { capture: true, passive: true });
|
|
50
|
+
// IntersectionObserver for data-prefetch links
|
|
51
|
+
if ('IntersectionObserver' in globalThis) {
|
|
52
|
+
const observer = new IntersectionObserver((entries) => {
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.isIntersecting) {
|
|
55
|
+
const href = entry.target.href;
|
|
56
|
+
if (href)
|
|
57
|
+
prefetchRSC(href);
|
|
58
|
+
observer.unobserve(entry.target);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, { rootMargin: '200px' });
|
|
62
|
+
// Observe current and future [data-prefetch] links
|
|
63
|
+
for (const el of document.querySelectorAll('a[data-prefetch]')) {
|
|
64
|
+
observer.observe(el);
|
|
65
|
+
}
|
|
66
|
+
// MutationObserver to catch dynamically added links
|
|
67
|
+
new MutationObserver((mutations) => {
|
|
68
|
+
for (const mutation of mutations) {
|
|
69
|
+
for (const node of mutation.addedNodes) {
|
|
70
|
+
if (node instanceof HTMLAnchorElement && node.hasAttribute('data-prefetch')) {
|
|
71
|
+
observer.observe(node);
|
|
72
|
+
}
|
|
73
|
+
if (node instanceof HTMLElement) {
|
|
74
|
+
for (const el of node.querySelectorAll('a[data-prefetch]')) {
|
|
75
|
+
observer.observe(el);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
84
|
+
function isSameOrigin(href) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(href, location.href);
|
|
87
|
+
return url.origin === location.origin;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── View Transitions wrapper ──────────────────────────────────────
|
|
94
|
+
function withViewTransition(enabled, fn) {
|
|
95
|
+
if (enabled && 'startViewTransition' in document) {
|
|
96
|
+
document.startViewTransition(fn);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
fn();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── Main: initNavigation ──────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Initialize client-side SPA navigation using the Navigation API.
|
|
105
|
+
*
|
|
106
|
+
* Call this once after hydrating your React root. Subsequent navigations
|
|
107
|
+
* (clicking `<a>` tags, browser back/forward) will fetch RSC payloads
|
|
108
|
+
* from the server and update the React tree without full page reloads.
|
|
109
|
+
*
|
|
110
|
+
* @param setPage - Callback to update the React root with new content.
|
|
111
|
+
* Typically wraps a `useState` setter.
|
|
112
|
+
* @param options - Configuration options.
|
|
113
|
+
*
|
|
114
|
+
* @returns A cleanup function that removes the navigation listener.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```tsx
|
|
118
|
+
* import { hydrateRoot } from 'react-dom/client';
|
|
119
|
+
* import { initNavigation } from '@cmj/juice/client';
|
|
120
|
+
*
|
|
121
|
+
* function App() {
|
|
122
|
+
* const [page, setPage] = useState<ReactNode>(initialServerContent);
|
|
123
|
+
* useEffect(() => initNavigation(setPage), []);
|
|
124
|
+
* return <>{page}</>;
|
|
125
|
+
* }
|
|
126
|
+
*
|
|
127
|
+
* hydrateRoot(document, <App />);
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function initNavigation(setPage, options = {}) {
|
|
131
|
+
const { viewTransitions = true, prefetch = false, callServer, } = options;
|
|
132
|
+
// Check for Navigation API support
|
|
133
|
+
if (!('navigation' in globalThis)) {
|
|
134
|
+
if (process.env.NODE_ENV === 'development') {
|
|
135
|
+
console.warn('[juice/client] Navigation API not available. ' +
|
|
136
|
+
'Client-side navigation disabled (MPA mode).');
|
|
137
|
+
}
|
|
138
|
+
return () => { };
|
|
139
|
+
}
|
|
140
|
+
const nav = globalThis.navigation;
|
|
141
|
+
// Set up prefetching if enabled
|
|
142
|
+
if (prefetch) {
|
|
143
|
+
setupPrefetching();
|
|
144
|
+
}
|
|
145
|
+
// ── Navigation interceptor ────────────────────────────────────
|
|
146
|
+
const handler = (event) => {
|
|
147
|
+
// Only intercept navigations we can handle
|
|
148
|
+
if (!event.canIntercept)
|
|
149
|
+
return;
|
|
150
|
+
// Skip hash-only navigations
|
|
151
|
+
if (event.hashChange)
|
|
152
|
+
return;
|
|
153
|
+
// Skip downloads
|
|
154
|
+
if (event.downloadRequest)
|
|
155
|
+
return;
|
|
156
|
+
// Skip form submissions (handled by server actions)
|
|
157
|
+
if (event.formData)
|
|
158
|
+
return;
|
|
159
|
+
// Only handle same-origin navigations
|
|
160
|
+
const destinationUrl = new URL(event.destination.url);
|
|
161
|
+
if (destinationUrl.origin !== location.origin)
|
|
162
|
+
return;
|
|
163
|
+
// Intercept the navigation
|
|
164
|
+
event.intercept({
|
|
165
|
+
// Enable automatic scroll restoration
|
|
166
|
+
scroll: 'after-transition',
|
|
167
|
+
async handler() {
|
|
168
|
+
const url = event.destination.url;
|
|
169
|
+
// Use prefetched response if available
|
|
170
|
+
const fetchPromise = prefetchCache.has(url)
|
|
171
|
+
? prefetchCache.get(url)
|
|
172
|
+
: fetch(url, {
|
|
173
|
+
headers: { 'Accept': RSC_CONTENT_TYPE },
|
|
174
|
+
signal: event.signal,
|
|
175
|
+
});
|
|
176
|
+
// Clear the prefetch cache entry
|
|
177
|
+
prefetchCache.delete(url);
|
|
178
|
+
// Decode the RSC payload
|
|
179
|
+
const rscPayload = createFromFetch(fetchPromise, {
|
|
180
|
+
callServer,
|
|
181
|
+
});
|
|
182
|
+
// Update the React tree with View Transitions
|
|
183
|
+
withViewTransition(viewTransitions, () => {
|
|
184
|
+
startTransition(() => {
|
|
185
|
+
setPage(rscPayload);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
nav.addEventListener('navigate', handler);
|
|
192
|
+
// Return cleanup function
|
|
193
|
+
return () => {
|
|
194
|
+
nav.removeEventListener('navigate', handler);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,OAAO,EAAE,cAAc,EAA0B,MAAM,aAAa,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlightManifest, RouterOptions } from './types.js';
|
|
1
|
+
import type { FlightManifest, RouterOptions, ParsedBody, ActionContext, _CompiledRoute } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Extracts the server action ID from the request.
|
|
4
4
|
*
|
|
@@ -12,20 +12,73 @@ import type { FlightManifest, RouterOptions } from './types.js';
|
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
14
|
export declare function _extractActionId(req: Request): Promise<string | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Parses the request body based on Content-Type and returns a
|
|
17
|
+
* discriminated `ParsedBody` union.
|
|
18
|
+
*
|
|
19
|
+
* - `multipart/form-data` → `{ kind: 'form', data: FormData }`
|
|
20
|
+
* - `application/x-www-form-urlencoded` → `{ kind: 'form', data: FormData }`
|
|
21
|
+
* - `application/json` → `{ kind: 'json', data: unknown }`
|
|
22
|
+
* - Everything else → `{ kind: 'text', data: string }`
|
|
23
|
+
*
|
|
24
|
+
* The `kind` field enables runtime narrowing without casts:
|
|
25
|
+
* ```ts
|
|
26
|
+
* if (parsed.kind === 'form') parsed.data.get('name'); // FormData ✅
|
|
27
|
+
* if (parsed.kind === 'json') parsed.data; // unknown ✅
|
|
28
|
+
* if (parsed.kind === 'text') parsed.data.length; // string ✅
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @param req - The incoming POST `Request`.
|
|
32
|
+
* @returns The parsed body with discriminator.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export declare function _parseBody(req: Request): Promise<ParsedBody>;
|
|
37
|
+
/**
|
|
38
|
+
* Parses cookies from the `Cookie` header into a `ReadonlyMap`.
|
|
39
|
+
*
|
|
40
|
+
* Zero dependencies. Handles edge cases:
|
|
41
|
+
* - Empty header → empty map
|
|
42
|
+
* - Malformed pairs (no `=`) → skipped
|
|
43
|
+
* - Values containing `=` → preserved
|
|
44
|
+
* - Leading/trailing whitespace → trimmed
|
|
45
|
+
*
|
|
46
|
+
* @param req - The incoming `Request`.
|
|
47
|
+
* @returns A readonly map of cookie name → value.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function _parseCookies(req: Request): ReadonlyMap<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Constructs an `ActionContext` from the raw request.
|
|
54
|
+
*
|
|
55
|
+
* This is a **public API** — use it in tests to create mock contexts:
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { createActionContext } from '@cmj/juice/runtime';
|
|
58
|
+
* const ctx = createActionContext(new Request('https://example.com'));
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* The `cookies` property is lazy — only parsed on first access via
|
|
62
|
+
* a getter. This keeps the fast path (no cookie access) allocation-free.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createActionContext(req: Request, params?: Readonly<Record<string, string>>): ActionContext;
|
|
15
65
|
/**
|
|
16
66
|
* The POST server action pipeline.
|
|
17
67
|
*
|
|
18
68
|
* 1. Extracts the action ID from the request.
|
|
19
69
|
* 2. Looks up the action in the manifest.
|
|
20
70
|
* 3. Dynamically imports the action module.
|
|
21
|
-
* 4. Parses the request body
|
|
22
|
-
* 5.
|
|
23
|
-
* 6.
|
|
71
|
+
* 4. Parses the request body into a `ParsedBody`.
|
|
72
|
+
* 5. Constructs an `ActionContext` (request, cookies, params).
|
|
73
|
+
* 6. Calls the action with **two arguments**: `(body, ctx)` —
|
|
74
|
+
* preserving React 19 `<form>` compatibility.
|
|
75
|
+
* 7. Returns the result as a JSON response, or passes through
|
|
24
76
|
* a thrown `Response` directly.
|
|
25
77
|
*
|
|
26
|
-
* @param req
|
|
27
|
-
* @param manifest
|
|
28
|
-
* @param options
|
|
78
|
+
* @param req - The incoming POST `Request`.
|
|
79
|
+
* @param manifest - The flight manifest for action resolution.
|
|
80
|
+
* @param options - Router options (hooks).
|
|
81
|
+
* @param compiledRoutes - Pre-compiled routes for URL→params extraction.
|
|
29
82
|
* @returns A `Response` with the action result or error.
|
|
30
83
|
*
|
|
31
84
|
* @internal
|
|
@@ -33,5 +86,5 @@ export declare function _extractActionId(req: Request): Promise<string | null>;
|
|
|
33
86
|
export declare function _serverActionPipeline(req: Request, manifest: FlightManifest, options: Required<Pick<RouterOptions, 'onError'>> & RouterOptions & {
|
|
34
87
|
isDev?: boolean;
|
|
35
88
|
hmrUrl?: string;
|
|
36
|
-
}): Promise<Response>;
|
|
89
|
+
}, compiledRoutes?: readonly _CompiledRoute[]): Promise<Response>;
|
|
37
90
|
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/runtime/actions.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/runtime/actions.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAK3G;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAelE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAavE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,EACZ,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM,GAC5C,aAAa,CAkBf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,GAAG,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACxG,cAAc,CAAC,EAAE,SAAS,cAAc,EAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,CAoInB"}
|
package/dist/runtime/actions.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { ActionNotFoundError, ModuleLoadError } from './errors.js';
|
|
6
6
|
import { _resolveModulePath } from './resolve.js';
|
|
7
7
|
import { _bustModuleCache } from './dev.js';
|
|
8
|
+
import { _matchRoute } from './matcher.js';
|
|
8
9
|
/**
|
|
9
10
|
* Extracts the server action ID from the request.
|
|
10
11
|
*
|
|
@@ -36,28 +37,96 @@ export async function _extractActionId(req) {
|
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* Parses the request body based on Content-Type
|
|
40
|
+
* Parses the request body based on Content-Type and returns a
|
|
41
|
+
* discriminated `ParsedBody` union.
|
|
40
42
|
*
|
|
41
|
-
* - `multipart/form-data` → `FormData`
|
|
42
|
-
* - `application/x-www-form-urlencoded` → `FormData`
|
|
43
|
-
* - `application/json` →
|
|
44
|
-
* - Everything else →
|
|
43
|
+
* - `multipart/form-data` → `{ kind: 'form', data: FormData }`
|
|
44
|
+
* - `application/x-www-form-urlencoded` → `{ kind: 'form', data: FormData }`
|
|
45
|
+
* - `application/json` → `{ kind: 'json', data: unknown }`
|
|
46
|
+
* - Everything else → `{ kind: 'text', data: string }`
|
|
47
|
+
*
|
|
48
|
+
* The `kind` field enables runtime narrowing without casts:
|
|
49
|
+
* ```ts
|
|
50
|
+
* if (parsed.kind === 'form') parsed.data.get('name'); // FormData ✅
|
|
51
|
+
* if (parsed.kind === 'json') parsed.data; // unknown ✅
|
|
52
|
+
* if (parsed.kind === 'text') parsed.data.length; // string ✅
|
|
53
|
+
* ```
|
|
45
54
|
*
|
|
46
55
|
* @param req - The incoming POST `Request`.
|
|
47
|
-
* @returns The parsed body.
|
|
56
|
+
* @returns The parsed body with discriminator.
|
|
48
57
|
*
|
|
49
58
|
* @internal
|
|
50
59
|
*/
|
|
51
|
-
async function _parseBody(req) {
|
|
60
|
+
export async function _parseBody(req) {
|
|
52
61
|
const contentType = req.headers.get('content-type') ?? '';
|
|
53
62
|
if (contentType.includes('multipart/form-data') ||
|
|
54
63
|
contentType.includes('application/x-www-form-urlencoded')) {
|
|
55
|
-
return req.formData();
|
|
64
|
+
return { kind: 'form', data: await req.formData() };
|
|
56
65
|
}
|
|
57
66
|
if (contentType.includes('application/json')) {
|
|
58
|
-
return req.json();
|
|
67
|
+
return { kind: 'json', data: await req.json() };
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'text', data: await req.text() };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parses cookies from the `Cookie` header into a `ReadonlyMap`.
|
|
73
|
+
*
|
|
74
|
+
* Zero dependencies. Handles edge cases:
|
|
75
|
+
* - Empty header → empty map
|
|
76
|
+
* - Malformed pairs (no `=`) → skipped
|
|
77
|
+
* - Values containing `=` → preserved
|
|
78
|
+
* - Leading/trailing whitespace → trimmed
|
|
79
|
+
*
|
|
80
|
+
* @param req - The incoming `Request`.
|
|
81
|
+
* @returns A readonly map of cookie name → value.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export function _parseCookies(req) {
|
|
86
|
+
const header = req.headers.get('cookie') ?? '';
|
|
87
|
+
if (!header)
|
|
88
|
+
return new Map();
|
|
89
|
+
const entries = [];
|
|
90
|
+
for (const pair of header.split(';')) {
|
|
91
|
+
const eqIndex = pair.indexOf('=');
|
|
92
|
+
if (eqIndex === -1)
|
|
93
|
+
continue;
|
|
94
|
+
const key = pair.slice(0, eqIndex).trim();
|
|
95
|
+
const value = pair.slice(eqIndex + 1).trim();
|
|
96
|
+
if (key)
|
|
97
|
+
entries.push([key, value]);
|
|
59
98
|
}
|
|
60
|
-
return
|
|
99
|
+
return new Map(entries);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Constructs an `ActionContext` from the raw request.
|
|
103
|
+
*
|
|
104
|
+
* This is a **public API** — use it in tests to create mock contexts:
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { createActionContext } from '@cmj/juice/runtime';
|
|
107
|
+
* const ctx = createActionContext(new Request('https://example.com'));
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* The `cookies` property is lazy — only parsed on first access via
|
|
111
|
+
* a getter. This keeps the fast path (no cookie access) allocation-free.
|
|
112
|
+
*/
|
|
113
|
+
export function createActionContext(req, params = {}) {
|
|
114
|
+
const url = new URL(req.url);
|
|
115
|
+
// Lazy cookie parsing via getter
|
|
116
|
+
let _cookies;
|
|
117
|
+
return Object.create(null, {
|
|
118
|
+
request: { value: req, enumerable: true },
|
|
119
|
+
url: { value: url, enumerable: true },
|
|
120
|
+
params: { value: params, enumerable: true },
|
|
121
|
+
cookies: {
|
|
122
|
+
get() {
|
|
123
|
+
if (!_cookies)
|
|
124
|
+
_cookies = _parseCookies(req);
|
|
125
|
+
return _cookies;
|
|
126
|
+
},
|
|
127
|
+
enumerable: true,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
61
130
|
}
|
|
62
131
|
/**
|
|
63
132
|
* The POST server action pipeline.
|
|
@@ -65,19 +134,22 @@ async function _parseBody(req) {
|
|
|
65
134
|
* 1. Extracts the action ID from the request.
|
|
66
135
|
* 2. Looks up the action in the manifest.
|
|
67
136
|
* 3. Dynamically imports the action module.
|
|
68
|
-
* 4. Parses the request body
|
|
69
|
-
* 5.
|
|
70
|
-
* 6.
|
|
137
|
+
* 4. Parses the request body into a `ParsedBody`.
|
|
138
|
+
* 5. Constructs an `ActionContext` (request, cookies, params).
|
|
139
|
+
* 6. Calls the action with **two arguments**: `(body, ctx)` —
|
|
140
|
+
* preserving React 19 `<form>` compatibility.
|
|
141
|
+
* 7. Returns the result as a JSON response, or passes through
|
|
71
142
|
* a thrown `Response` directly.
|
|
72
143
|
*
|
|
73
|
-
* @param req
|
|
74
|
-
* @param manifest
|
|
75
|
-
* @param options
|
|
144
|
+
* @param req - The incoming POST `Request`.
|
|
145
|
+
* @param manifest - The flight manifest for action resolution.
|
|
146
|
+
* @param options - Router options (hooks).
|
|
147
|
+
* @param compiledRoutes - Pre-compiled routes for URL→params extraction.
|
|
76
148
|
* @returns A `Response` with the action result or error.
|
|
77
149
|
*
|
|
78
150
|
* @internal
|
|
79
151
|
*/
|
|
80
|
-
export async function _serverActionPipeline(req, manifest, options) {
|
|
152
|
+
export async function _serverActionPipeline(req, manifest, options, compiledRoutes) {
|
|
81
153
|
// ── 1. Extract action ID ─────────────────────────────────────
|
|
82
154
|
const actionId = await _extractActionId(req);
|
|
83
155
|
if (!actionId) {
|
|
@@ -92,7 +164,12 @@ export async function _serverActionPipeline(req, manifest, options) {
|
|
|
92
164
|
});
|
|
93
165
|
}
|
|
94
166
|
// ── 2. Lookup in manifest ────────────────────────────────
|
|
95
|
-
|
|
167
|
+
// SECURITY: Use Object.hasOwn() to prevent prototype pollution.
|
|
168
|
+
// Without this, action IDs like "constructor", "__proto__", "toString"
|
|
169
|
+
// would resolve to Object.prototype methods instead of returning 404.
|
|
170
|
+
const actionRef = Object.hasOwn(manifest.serverActions, actionId)
|
|
171
|
+
? manifest.serverActions[actionId]
|
|
172
|
+
: undefined;
|
|
96
173
|
if (!actionRef) {
|
|
97
174
|
// Return a clean 404 JSON response instead of throwing.
|
|
98
175
|
// Throwing would leak stack traces in production.
|
|
@@ -127,11 +204,20 @@ export async function _serverActionPipeline(req, manifest, options) {
|
|
|
127
204
|
`"${actionRef.moduleId}" but export "${actionRef.exportName}" ` +
|
|
128
205
|
`is not a function. Check your 'use server' exports.`);
|
|
129
206
|
}
|
|
130
|
-
// ── 4. Parse body
|
|
131
|
-
const
|
|
132
|
-
// ── 5.
|
|
133
|
-
|
|
134
|
-
//
|
|
207
|
+
// ── 4. Parse body into discriminated union ─────────────────
|
|
208
|
+
const parsed = await _parseBody(req);
|
|
209
|
+
// ── 5. Construct ActionContext with route-matched params ───
|
|
210
|
+
// Match the POST URL against compiled routes to extract params
|
|
211
|
+
// (e.g., /product/42 → { id: '42' }). Falls back to {} if no match.
|
|
212
|
+
const routeMatch = compiledRoutes ? _matchRoute(req.url, compiledRoutes) : null;
|
|
213
|
+
const ctx = createActionContext(req, routeMatch?.params ?? {});
|
|
214
|
+
// ── 6. Execute with two arguments: (body, ctx) ────────────
|
|
215
|
+
// First arg: the parsed body (FormData, JSON, or text)
|
|
216
|
+
// → React 19 <form action={fn}> compatibility preserved
|
|
217
|
+
// Second arg: ActionContext (request, cookies, params)
|
|
218
|
+
// → opt-in power when the developer needs it
|
|
219
|
+
const result = await actionFn(parsed.data, ctx);
|
|
220
|
+
// ── 7. Return ──────────────────────────────────────────────
|
|
135
221
|
// If the action returns a Response directly, pass it through.
|
|
136
222
|
if (result instanceof Response) {
|
|
137
223
|
return result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/runtime/actions.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,sEAAsE;AACtE,uEAAuE;AACvE,uEAAuE;AAEvE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/runtime/actions.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createRouter } from './router.js';
|
|
2
|
-
export
|
|
2
|
+
export { createActionContext } from './actions.js';
|
|
3
|
+
export { _isRSCRequest, _buildModuleMap, RSC_CONTENT_TYPE } from './rsc.js';
|
|
4
|
+
export type { FlightManifest, RouterOptions, ActionContext, ParsedBody } from './types.js';
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|