@reckona/mreact-router 0.0.202 → 0.0.203
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 +5 -2
- package/dist/actions.d.ts +2 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +61 -41
- package/dist/actions.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +3 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +26 -10
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/build.d.ts +3 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +98 -12
- package/dist/build.js.map +1 -1
- package/dist/cache.d.ts +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +5 -4
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +10 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/navigation-marker.d.ts +2 -0
- package/dist/navigation-marker.d.ts.map +1 -0
- package/dist/navigation-marker.js +294 -0
- package/dist/navigation-marker.js.map +1 -0
- package/dist/navigation-runtime.d.ts +1 -1
- package/dist/navigation-runtime.d.ts.map +1 -1
- package/dist/navigation-runtime.js +1 -1
- package/dist/navigation-runtime.js.map +1 -1
- package/dist/prerender-entry.d.ts +3 -1
- package/dist/prerender-entry.d.ts.map +1 -1
- package/dist/prerender-entry.js +61 -3
- package/dist/prerender-entry.js.map +1 -1
- package/dist/render.d.ts +7 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +51 -23
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +3 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +54 -30
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +72 -56
- package/src/adapters/cloudflare.ts +40 -10
- package/src/build.ts +107 -10
- package/src/cache.ts +10 -4
- package/src/cli-options.ts +15 -2
- package/src/cli.ts +2 -1
- package/src/client.ts +15 -2
- package/src/navigation-marker.ts +353 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/prerender-entry.ts +78 -5
- package/src/render.ts +80 -24
- package/src/serve.ts +71 -34
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ local import:
|
|
|
81
81
|
|
|
82
82
|
Internal anchors are intercepted by the app-router client runtime and update the changed route payload instead of forcing a full document reload. The runtime keeps head metadata and route-data scripts synchronized, restores scroll on back/forward navigation, and prefetches client route scripts for likely navigations when the browser is not in reduced-data mode. Client route assets share app-local module instances across development and production route chunks, so app-local modules imported by multiple client routes use the browser's normal single ESM instance across SPA navigation.
|
|
83
83
|
|
|
84
|
+
Server-only document responses preserve the page's original DOM shape, while client-navigation responses use the `data-mreact-route-id` wrapper required for swapping route content. Route caches, prerender storage, and downstream caches keep those response variants separate. Serialized hydration payloads carry only the request pathname, so shared HTML does not capture visitor-specific origin or query data; routes that read request input restore `props.request.url` from the current document URL immediately before hydration. Read the full URL from the server-side `request` when server route logic needs it, which also makes request dependence visible to the cache tracker.
|
|
85
|
+
|
|
84
86
|
Use `Link` or `linkProps()` when a route needs explicit navigation behavior:
|
|
85
87
|
|
|
86
88
|
```tsx
|
|
@@ -120,6 +122,7 @@ client-only code. Navigation observers are available from
|
|
|
120
122
|
- Imported functions passed to `<form action={...}>` are inferred as server actions, including supported typed registry references such as `actions.save`. Inferred action implementations stay in the server graph and are stripped from production client route bundles. A top-level `"use server"` directive is still supported and marks every exported function in that module as a server action. Production dispatch is fail-closed without a generated or explicit action manifest; direct integrations that intentionally expose every registered action must pass `serverActions: { allowedActions: "any" }`.
|
|
121
123
|
- Browser-enhanced form actions that mutate data, call `revalidatePath()` for the current route, and return normally can use a single-flight mutation response: the action POST response carries fresh navigation HTML marked with `x-mreact-action-single-flight`, so the browser updates the visible route without a second GET. Unsupported action flows still fall back to the existing `x-mreact-revalidate` invalidation and redirect/navigation behavior. This is unrelated to duplicate-submission coalescing or application idempotency keys.
|
|
122
124
|
- Server actions reject `Content-Length` values over `10 MiB` by default. Pass `serverActions: { maxBodyBytes }` to configure the limit.
|
|
125
|
+
- Server action nonces are atomically claimed immediately before application action code runs. Custom `serverActions.replayStore` implementations must provide an atomic `claim()` operation; replay returns `409`, while capacity or store failures fail closed with `503`. An invoked action consumes its nonce even if it later throws because its side effects may already have committed.
|
|
123
126
|
- Route handlers may return or throw standard `Response` objects from method exports such as `GET`, `POST`, or `ALL`. Dynamic route handlers receive decoded params as the second argument: `GET(request, { params })`; catch-all `$...slug` params are arrays of decoded segments. The context also includes `request`, `route`, and adapter `env`; Cloudflare route handlers additionally receive the Worker execution `context`.
|
|
124
127
|
- `redirect(location)` throws router control flow that defaults to HTTP 303 so auth and form flows continue as GET requests after the redirect. Use `redirect(location, { status: 307 })` when preserving the original method and body is required.
|
|
125
128
|
|
|
@@ -360,7 +363,7 @@ The build manifest records this separately from `client: true`, emits a shared n
|
|
|
360
363
|
|
|
361
364
|
For Cloudflare Workers, `mreact-router build --target=cloudflare` emits `.mreact/cloudflare/worker.mjs`, `.mreact/cloudflare/route-modules.mjs`, and per-route module chunks for non-prerendered and dynamic App Router pages plus `route.ts` server routes and metadata conventions, so deployments do not need a hand-written Worker entrypoint or Vite-only `import.meta.glob()` transforms. Deploy that output with a Worker config whose `main` is `.mreact/cloudflare/worker.mjs` and whose `ASSETS` binding points at `.mreact/client`.
|
|
362
365
|
|
|
363
|
-
For Cloudflare Pages advanced mode, run `mreact-router package cloudflare-pages --from .mreact --out .mreact/pages` after `mreact-router build --target=cloudflare`, then deploy with `wrangler pages deploy .mreact/pages`. The package command creates a Pages output directory with bundled `_worker.js`, `_mreact/client/*` static route assets, root public assets, and `mreact-cloudflare-pages-artifact.json`. Pages provides the `ASSETS` binding to `_worker.js`; mreact's generated worker reads that binding and serves only generated client manifest assets, route-linked CSS, and public files. The package step bundles the generated Worker entry, route registry, route chunks, and router adapter code, so application deploy scripts do not need to copy `worker.mjs` manually or decide whether Wrangler should bundle package imports.
|
|
366
|
+
For Cloudflare Pages advanced mode, run `mreact-router package cloudflare-pages --from .mreact --out .mreact/pages` after `mreact-router build --target=cloudflare`, then deploy with `wrangler pages deploy .mreact/pages`. The package command creates a Pages output directory with bundled `_worker.js`, `_mreact/client/*` static route assets, root public assets, and `mreact-cloudflare-pages-artifact.json`. Pages provides the `ASSETS` binding to `_worker.js`; mreact's generated worker reads that binding and serves only generated client manifest assets, route-linked CSS, and public files. The package step bundles the generated Worker entry, route registry, route chunks, and router adapter code, so application deploy scripts do not need to copy `worker.mjs` manually or decide whether Wrangler should bundle package imports. Configure the `nodejs_compat` compatibility flag so the adapter can use native request-local storage and dependencies can use Cloudflare's Node.js compatibility runtime. Without it, mreact preserves query-client isolation by serializing renders and emits a one-time warning.
|
|
364
367
|
|
|
365
368
|
User Vite plugins from the project config are forwarded into router bundle builds after the `mreact-router` plugin itself is removed, so MDX-style syntax transforms and custom content loaders can participate in server, client, Cloudflare, and prerender bundles. Vite `define` values are also forwarded into generated Cloudflare server bundles, including `import.meta.env.*` aliases and plain define identifiers used by loaders and route handlers. Client assets, route stylesheet assets imported by pages/layouts/templates or special `error.tsx`/`not-found.tsx` boundaries, and copied public assets are served only when they appear in the generated manifest allow-list. Dynamic routes resolve modules through a build-time registry keyed by `route.file`, not by constructing module ids from request input. Generated Cloudflare route modules preserve app-router layout/template shells and named slots for both string and `stream = true` pages, including route-local `<Await>` boundaries and local server-component imports. Generated Cloudflare server route modules dispatch method exports such as `GET`, `POST`, and `ALL` with decoded dynamic params plus `context.env`, `context.context`, `context.request`, and `context.route`, so handlers can use Worker bindings directly. Cloudflare page rendering installs the per-request query client scope before loader, page, metadata, and document rendering, so server-rendered helpers can call `getQueryClient()` even in runtimes without `AsyncLocalStorage`. The Cloudflare adapter marks streamed HTML with `Cache-Control: no-transform` and `Content-Encoding: identity` so Workers compression does not gzip-buffer the first shell before placeholders can paint. Pass `onResponse` to add cross-cutting response headers to rendered, static, asset, reload, not-found, and error responses. If a generated Cloudflare route module cannot produce the `data-mreact-route-id` marker contract required by client navigation, the adapter returns a reload signal for `x-mreact-navigation: 1` requests so the browser performs a normal document navigation without first buffering the full HTML response.
|
|
366
369
|
|
|
@@ -392,7 +395,7 @@ await warmAwsLambdaRuntime(options);
|
|
|
392
395
|
export const handler = await createPreloadedAwsLambdaRequestHandler(options);
|
|
393
396
|
```
|
|
394
397
|
|
|
395
|
-
Production adapters enforce the app-router import policy when bundling loaders, middleware, route handlers, metadata, and server actions. `mreact-router build` writes `.mreact/server/import-policy.json` from server-side static imports. The built
|
|
398
|
+
Production adapters enforce the app-router import policy when bundling loaders, middleware, route handlers, metadata, and server actions. `mreact-router build` writes `.mreact/server/import-policy.json` from server-side static imports. The public built-output entry points, including `mreact-router start .mreact` and `renderBuiltAppRequest()`, read that generated policy automatically and merge its runtime packages with any explicit policy options. Custom adapters should compose the public render and preload APIs rather than depend on internal runtime constructors. Lambda handlers can use `importPolicy: "generated"`. You can still pass an explicit `importPolicy.allowedPackages` list when you need a hand-audited policy.
|
|
396
399
|
|
|
397
400
|
For Lambda deployments, build with `mreact-router build --target=aws-lambda` or `buildApp({ targets: ["aws-lambda"] })`. The generated buffered and streaming handlers warm the same middleware policy during module initialization by default and then use request handlers with duplicate preload disabled. The artifact manifest records `mreact-handler.handler` and `mreact-streaming-handler.handler` separately. Select a broader or disabled initialization policy with `--aws-lambda-preload=hot-route-requests|all|none` or `buildApp({ awsLambdaPreload: "all", targets: ["aws-lambda"] })` only after measuring the packaged handler; pair `hot-route-requests` with `--aws-lambda-preload-routes=/,/login` or `awsLambdaPreloadRoutes`. `none` disables generated module preload but still materializes the writable runtime directory during generated handler import. The package command preserves the build-generated policy unless package options explicitly override it. The target writes Node-compatible server/client output, `.mreact/server/import-policy.json`, and generated Lambda entries while skipping `.mreact/cloudflare` route modules, so loaders and server helpers may import Node-only dependencies such as database drivers without being bundled for the Workers runtime.
|
|
398
401
|
|
package/dist/actions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ServerActionHandlerOptions, type ServerActionReplayStore, type ServerActionRequestReference } from "@reckona/mreact-server";
|
|
1
|
+
import { type ServerActionHandlerOptions, type ServerActionReplayClaim, type ServerActionReplayStore, type ServerActionRequestReference } from "@reckona/mreact-server";
|
|
2
2
|
import { type AppRouterCache } from "./cache.js";
|
|
3
3
|
import { type AppRouterImportPolicy } from "./import-policy.js";
|
|
4
4
|
import { type RequestCookies } from "./navigation.js";
|
|
@@ -20,8 +20,7 @@ declare class BoundedReplayStore {
|
|
|
20
20
|
private readonly maxEntries;
|
|
21
21
|
private readonly entries;
|
|
22
22
|
constructor(ttlMs: number, maxEntries: number);
|
|
23
|
-
|
|
24
|
-
add(value: string): void;
|
|
23
|
+
claim(value: string): ServerActionReplayClaim;
|
|
25
24
|
size(): number;
|
|
26
25
|
}
|
|
27
26
|
export declare function __clearDefaultReplayStore(): void;
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,0BAA0B,
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
package/dist/actions.js
CHANGED
|
@@ -71,35 +71,36 @@ class BoundedReplayStore {
|
|
|
71
71
|
this.ttlMs = ttlMs;
|
|
72
72
|
this.maxEntries = maxEntries;
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
return false;
|
|
78
|
-
if (expiresAt < Date.now()) {
|
|
74
|
+
claim(value) {
|
|
75
|
+
const existing = this.entries.get(value);
|
|
76
|
+
if (existing?.state === "completed" && existing.expiresAt < Date.now()) {
|
|
79
77
|
this.entries.delete(value);
|
|
80
|
-
return false;
|
|
81
78
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
add(value) {
|
|
85
|
-
const now = Date.now();
|
|
79
|
+
if (this.entries.has(value))
|
|
80
|
+
return { status: "replay" };
|
|
86
81
|
if (this.entries.size >= this.maxEntries) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (expiresAt < now) {
|
|
92
|
-
while (this.entries.size >= this.maxEntries) {
|
|
93
|
-
const nextOldest = this.entries.entries().next().value;
|
|
94
|
-
if (nextOldest === undefined || nextOldest[1] >= now) {
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
this.entries.delete(nextOldest[0]);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
for (const [key, entry] of this.entries) {
|
|
84
|
+
if (entry.state === "completed" && entry.expiresAt < now)
|
|
85
|
+
this.entries.delete(key);
|
|
100
86
|
}
|
|
87
|
+
if (this.entries.size >= this.maxEntries)
|
|
88
|
+
return { status: "capacity-exceeded" };
|
|
101
89
|
}
|
|
102
|
-
|
|
90
|
+
const entry = { state: "in-flight" };
|
|
91
|
+
this.entries.set(value, entry);
|
|
92
|
+
let finalized = false;
|
|
93
|
+
return {
|
|
94
|
+
status: "claimed",
|
|
95
|
+
finalize: () => {
|
|
96
|
+
if (finalized)
|
|
97
|
+
return;
|
|
98
|
+
finalized = true;
|
|
99
|
+
if (this.entries.get(value) === entry) {
|
|
100
|
+
this.entries.set(value, { state: "completed", expiresAt: Date.now() + this.ttlMs });
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
103
104
|
}
|
|
104
105
|
// Exposed for tests; not part of the ServerActionReplayStore interface.
|
|
105
106
|
size() {
|
|
@@ -272,7 +273,7 @@ async function dispatchServerActionRequestWithoutCacheContext(options) {
|
|
|
272
273
|
: { allowedActions: jsonAllowedServerActions(options.serverActions.allowedActions) }),
|
|
273
274
|
csrf: { cookieName: serverActionCookieName() },
|
|
274
275
|
maxBodyBytes: options.serverActions?.maxBodyBytes ?? DEFAULT_ACTION_BODY_MAX_BYTES,
|
|
275
|
-
replayProtection: {
|
|
276
|
+
replayProtection: { store: replayStore },
|
|
276
277
|
});
|
|
277
278
|
return handle(options.request);
|
|
278
279
|
}
|
|
@@ -321,10 +322,6 @@ async function dispatchServerActionRequestWithoutCacheContext(options) {
|
|
|
321
322
|
})) {
|
|
322
323
|
return jsonResponse({ ok: false, error: "Unknown server action." }, 404);
|
|
323
324
|
}
|
|
324
|
-
const nonceResponse = validateFormNonce(formData, options.serverActions?.replayStore ?? usedFormActionNonces);
|
|
325
|
-
if (nonceResponse !== undefined) {
|
|
326
|
-
return nonceResponse;
|
|
327
|
-
}
|
|
328
325
|
let registry;
|
|
329
326
|
try {
|
|
330
327
|
registry = await loadServerActionRegistry({
|
|
@@ -351,19 +348,35 @@ async function dispatchServerActionRequestWithoutCacheContext(options) {
|
|
|
351
348
|
if (authorizationResponse !== undefined) {
|
|
352
349
|
return authorizationResponse;
|
|
353
350
|
}
|
|
351
|
+
const replayClaim = await claimFormNonce(nonce, options.serverActions?.replayStore ?? usedFormActionNonces);
|
|
352
|
+
if (replayClaim instanceof Response)
|
|
353
|
+
return replayClaim;
|
|
354
|
+
let actionResponse;
|
|
355
|
+
let actionError;
|
|
354
356
|
try {
|
|
355
357
|
const value = await action(actionFormData, createServerActionContext(options.request));
|
|
356
358
|
if (value instanceof Response) {
|
|
357
|
-
|
|
359
|
+
actionResponse = value;
|
|
358
360
|
}
|
|
359
|
-
if (value === undefined || value === null) {
|
|
360
|
-
|
|
361
|
+
else if (value === undefined || value === null) {
|
|
362
|
+
actionResponse = redirectToFormReferer(options.request);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
actionResponse = jsonResponse({ ok: true, value }, 200);
|
|
361
366
|
}
|
|
362
|
-
return jsonResponse({ ok: true, value }, 200);
|
|
363
367
|
}
|
|
364
368
|
catch (error) {
|
|
365
|
-
|
|
369
|
+
actionError = error;
|
|
366
370
|
}
|
|
371
|
+
try {
|
|
372
|
+
await replayClaim.finalize();
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return replayStoreUnavailableResponse();
|
|
376
|
+
}
|
|
377
|
+
return actionError === undefined
|
|
378
|
+
? actionResponse
|
|
379
|
+
: jsonResponse({ ok: false, error: actionError instanceof Error ? actionError.message : String(actionError) }, 500);
|
|
367
380
|
}
|
|
368
381
|
function createServerActionContext(request) {
|
|
369
382
|
const clientIp = clientIpFromRequest(request);
|
|
@@ -986,16 +999,23 @@ function validateServerActionRequestOrigin(request) {
|
|
|
986
999
|
return jsonResponse({ ok: false, error: "Origin not allowed." }, 403);
|
|
987
1000
|
}
|
|
988
1001
|
}
|
|
989
|
-
function
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1002
|
+
async function claimFormNonce(nonce, replayStore) {
|
|
1003
|
+
let claim;
|
|
1004
|
+
try {
|
|
1005
|
+
claim = await replayStore.claim(nonce);
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
return replayStoreUnavailableResponse();
|
|
993
1009
|
}
|
|
994
|
-
if (
|
|
1010
|
+
if (claim.status === "replay") {
|
|
995
1011
|
return jsonResponse({ ok: false, error: "Server action nonce was already used." }, 409);
|
|
996
1012
|
}
|
|
997
|
-
|
|
998
|
-
|
|
1013
|
+
if (claim.status === "capacity-exceeded")
|
|
1014
|
+
return replayStoreUnavailableResponse();
|
|
1015
|
+
return claim;
|
|
1016
|
+
}
|
|
1017
|
+
function replayStoreUnavailableResponse() {
|
|
1018
|
+
return new Response(JSON.stringify({ ok: false, error: "Server action replay protection is unavailable." }), { status: 503, headers: { "content-type": "application/json", "retry-after": "1" } });
|
|
999
1019
|
}
|
|
1000
1020
|
function cleanActionFormData(formData) {
|
|
1001
1021
|
const cleaned = new FormData();
|