@rangojs/router 0.24.4 → 0.26.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 +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/handle-snapshot.d.ts +2 -2
- package/dist/types/cache/types.d.ts +16 -6
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/handles/deferred-resolution.d.ts +3 -2
- package/dist/types/handles/meta.d.ts +5 -0
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/prerender/build-shell-capture.d.ts +1 -2
- package/dist/types/prerender/writable-store.d.ts +2 -3
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -11
- package/dist/types/router/router-options.d.ts +4 -27
- package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/router-versions.d.ts +8 -6
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
- package/dist/types/rsc/shell-capture.d.ts +23 -2
- package/dist/types/rsc/types.d.ts +5 -8
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/build-version-table.d.ts +10 -13
- package/dist/types/server/context.d.ts +90 -6
- package/dist/types/server/handle-store.d.ts +17 -6
- package/dist/types/server/request-context.d.ts +45 -7
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/build-versions.d.ts +0 -3
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/internal/seed-vars.d.ts +4 -4
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +42 -5
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +10 -4
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6819 -6555
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/cache-guide/SKILL.md +4 -8
- package/skills/caching/SKILL.md +6 -6
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/host-router/SKILL.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +42 -19
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +103 -52
- package/skills/prerender/SKILL.md +4 -5
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/router-setup/SKILL.md +0 -2
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/render-handler.md +17 -17
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +86 -22
- package/skills/testing/server-actions.md +16 -16
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +6 -7
- package/src/cache/cf/cf-cache-types.ts +0 -17
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/handle-snapshot.ts +2 -2
- package/src/cache/types.ts +16 -6
- package/src/cache/vercel/vercel-cache-store.ts +8 -19
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/handles/deferred-resolution.ts +3 -2
- package/src/handles/meta.ts +5 -0
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/prerender/build-shell-capture.ts +1 -2
- package/src/prerender/writable-store.ts +2 -3
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +41 -21
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +15 -10
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -13
- package/src/router/router-options.ts +4 -28
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/loader-cache.ts +4 -3
- package/src/router/segment-resolution/mask-nested.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router-versions.ts +7 -7
- package/src/router.ts +5 -17
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +86 -48
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +26 -3
- package/src/rsc/shell-capture.ts +137 -51
- package/src/rsc/types.ts +5 -9
- package/src/segment-system.tsx +125 -119
- package/src/server/build-version-table.ts +12 -24
- package/src/server/context.ts +160 -7
- package/src/server/handle-store.ts +80 -12
- package/src/server/request-context.ts +46 -6
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/build-versions.ts +0 -3
- package/src/testing/dispatch.ts +5 -8
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/internal/seed-vars.ts +4 -4
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +79 -9
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +10 -4
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/shell-prerender-phase.ts +1 -5
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* reference. We detect these and return HTML instead of RSC stream.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { requestHeaders } from "../server/request-headers.js";
|
|
9
|
+
import { bodylessHeaders, requestHeaders } from "../server/request-headers.js";
|
|
10
10
|
import {
|
|
11
11
|
getRequestContext,
|
|
12
12
|
setRequestContextParams,
|
|
@@ -16,10 +16,12 @@ import type { MiddlewareFn } from "../router/middleware.js";
|
|
|
16
16
|
import { executeMiddleware } from "../router/middleware.js";
|
|
17
17
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
18
18
|
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
19
|
-
import { payloadInitialTheme } from "./full-payload.js";
|
|
19
|
+
import { payloadInitialTheme, payloadLocation } from "./full-payload.js";
|
|
20
20
|
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
21
21
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
22
|
+
import type { MatchResult } from "../types.js";
|
|
22
23
|
import {
|
|
24
|
+
actionRenderRequest,
|
|
23
25
|
createResponseWithMergedHeaders,
|
|
24
26
|
finalizeResponse,
|
|
25
27
|
buildRouteMiddlewareEntries,
|
|
@@ -78,17 +80,30 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
78
80
|
// renders and the re-render), so a PE request prints ONE tree. A non-PE form
|
|
79
81
|
// POST (no $ACTION fields) drives no plan and stays silent.
|
|
80
82
|
const trace = INTERNAL_RANGO_DEBUG ? createRoutineTrace("pe") : undefined;
|
|
83
|
+
const input: PeRequestInput<TEnv> = {
|
|
84
|
+
ctx,
|
|
85
|
+
request,
|
|
86
|
+
env,
|
|
87
|
+
url,
|
|
88
|
+
handleStore,
|
|
89
|
+
nonce,
|
|
90
|
+
routeMwInfo,
|
|
91
|
+
trace,
|
|
92
|
+
};
|
|
81
93
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
94
|
+
const reqCtx = getRequestContext();
|
|
95
|
+
// A rewrite pass after the form's action ran: re-render, never run it again.
|
|
96
|
+
const ran = reqCtx._actionRan;
|
|
97
|
+
if (ran?.kind === "form") {
|
|
98
|
+
return await rerenderAfterFormAction(
|
|
99
|
+
input,
|
|
100
|
+
await outcomeForRewritePass(input, ran.outcome),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const outcome = await runFormAction(input);
|
|
104
|
+
if (!outcome || outcome instanceof Response) return outcome;
|
|
105
|
+
reqCtx._actionRan = { kind: "form", outcome };
|
|
106
|
+
return await rerenderAfterFormAction(input, outcome);
|
|
92
107
|
} finally {
|
|
93
108
|
if (trace && trace.entries.length > 0) {
|
|
94
109
|
console.log(
|
|
@@ -98,16 +113,27 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
/** One PE request's inputs, shared by the action run and the re-render. */
|
|
117
|
+
interface PeRequestInput<TEnv> {
|
|
118
|
+
ctx: HandlerContext<TEnv>;
|
|
119
|
+
request: Request;
|
|
120
|
+
env: TEnv;
|
|
121
|
+
url: URL;
|
|
122
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"];
|
|
123
|
+
nonce: string | undefined;
|
|
124
|
+
routeMwInfo: PeRouteMiddlewareInfo | undefined;
|
|
125
|
+
trace: RoutineTrace | undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run the no-JS form's action, outside route middleware. Returns the outcome
|
|
130
|
+
* the re-render renders from, a Response that ends the request (a redirect, a
|
|
131
|
+
* 400 for an unparseable body), or null for a POST that is not a form action.
|
|
132
|
+
*/
|
|
133
|
+
async function runFormAction<TEnv>(
|
|
134
|
+
input: PeRequestInput<TEnv>,
|
|
135
|
+
): Promise<FormActionOutcome | Response | null> {
|
|
136
|
+
const { ctx, request, env, url } = input;
|
|
111
137
|
// Clone the request to read FormData without consuming it.
|
|
112
138
|
// Wrap in try-catch so malformed POST bodies are reported as action
|
|
113
139
|
// errors, not routing errors from the outer catch in handler.ts.
|
|
@@ -116,32 +142,10 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
116
142
|
formData = await request.clone().formData();
|
|
117
143
|
} catch (error) {
|
|
118
144
|
// Attempt error boundary rendering so the user sees a meaningful page.
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
env,
|
|
123
|
-
url,
|
|
124
|
-
error,
|
|
125
|
-
handleStore,
|
|
126
|
-
nonce,
|
|
127
|
-
trace,
|
|
128
|
-
);
|
|
129
|
-
if (errorHtml) {
|
|
130
|
-
ctx.callOnError(error, "action", {
|
|
131
|
-
request,
|
|
132
|
-
url,
|
|
133
|
-
env,
|
|
134
|
-
handledByBoundary: true,
|
|
135
|
-
});
|
|
136
|
-
return errorHtml;
|
|
145
|
+
const failure = await matchPeFailure(input, error, undefined, false);
|
|
146
|
+
if (failure.boundary) {
|
|
147
|
+
return { reactFormState: null, directActionId: null, failure };
|
|
137
148
|
}
|
|
138
|
-
|
|
139
|
-
ctx.callOnError(error, "action", {
|
|
140
|
-
request,
|
|
141
|
-
url,
|
|
142
|
-
env,
|
|
143
|
-
handledByBoundary: false,
|
|
144
|
-
});
|
|
145
149
|
console.error("[RSC] Progressive enhancement form parse error:", error);
|
|
146
150
|
return createResponseWithMergedHeaders(null, { status: 400 });
|
|
147
151
|
}
|
|
@@ -167,13 +171,7 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
167
171
|
// Execute action and return HTML
|
|
168
172
|
let actionResult: unknown = undefined;
|
|
169
173
|
let reactFormState: ReactFormState | null = null;
|
|
170
|
-
|
|
171
|
-
// When an action throws and NO error boundary matches, the PE path re-renders
|
|
172
|
-
// the page (below) the same way it would after a successful action. Without
|
|
173
|
-
// this the re-render serves HTTP 200, diverging from the JS path which serves
|
|
174
|
-
// 500 (server-action.ts sets actionStatus=500 for the same boundaryless error).
|
|
175
|
-
// 500 is carried only into the final HTML response, never the redirect branch.
|
|
176
|
-
let boundarylessErrorStatus: number | undefined;
|
|
174
|
+
let failure: PeFailure | undefined;
|
|
177
175
|
|
|
178
176
|
if (isUseActionState) {
|
|
179
177
|
// Decode and extract action identity before execution so error
|
|
@@ -192,33 +190,7 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
192
190
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
193
191
|
const redirectResponse = extractRedirectResponse(error);
|
|
194
192
|
if (redirectResponse) return redirectResponse;
|
|
195
|
-
|
|
196
|
-
// Attempt error boundary rendering for the PE path
|
|
197
|
-
const errorHtml = await renderPeErrorBoundary(
|
|
198
|
-
ctx,
|
|
199
|
-
request,
|
|
200
|
-
env,
|
|
201
|
-
url,
|
|
202
|
-
error,
|
|
203
|
-
handleStore,
|
|
204
|
-
nonce,
|
|
205
|
-
trace,
|
|
206
|
-
useActionStateId,
|
|
207
|
-
true, // an action ran and threw
|
|
208
|
-
);
|
|
209
|
-
if (errorHtml) return errorHtml;
|
|
210
|
-
|
|
211
|
-
ctx.callOnError(error, "action", {
|
|
212
|
-
request,
|
|
213
|
-
url,
|
|
214
|
-
env,
|
|
215
|
-
actionId: useActionStateId,
|
|
216
|
-
handledByBoundary: false,
|
|
217
|
-
});
|
|
218
|
-
console.error("[RSC] Progressive enhancement action error:", error);
|
|
219
|
-
// No boundary matched — the fall-through re-render must carry 500 to match
|
|
220
|
-
// the JS path's boundaryless-error status (server-action.ts).
|
|
221
|
-
boundarylessErrorStatus = 500;
|
|
193
|
+
failure = await matchPeFailure(input, error, useActionStateId, true);
|
|
222
194
|
}
|
|
223
195
|
} else if (isDirectAction && directActionId) {
|
|
224
196
|
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
@@ -247,36 +219,17 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
247
219
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
248
220
|
const redirectResponse = extractRedirectResponse(error);
|
|
249
221
|
if (redirectResponse) return redirectResponse;
|
|
250
|
-
|
|
251
|
-
// Attempt error boundary rendering for the PE path
|
|
252
|
-
const errorHtml = await renderPeErrorBoundary(
|
|
253
|
-
ctx,
|
|
254
|
-
request,
|
|
255
|
-
env,
|
|
256
|
-
url,
|
|
257
|
-
error,
|
|
258
|
-
handleStore,
|
|
259
|
-
nonce,
|
|
260
|
-
trace,
|
|
261
|
-
directActionId,
|
|
262
|
-
true, // an action ran and threw
|
|
263
|
-
);
|
|
264
|
-
if (errorHtml) return errorHtml;
|
|
265
|
-
|
|
266
|
-
ctx.callOnError(error, "action", {
|
|
267
|
-
request,
|
|
268
|
-
url,
|
|
269
|
-
env,
|
|
270
|
-
actionId: directActionId,
|
|
271
|
-
handledByBoundary: false,
|
|
272
|
-
});
|
|
273
|
-
console.error("[RSC] Progressive enhancement action error:", error);
|
|
274
|
-
// No boundary matched — the fall-through re-render must carry 500 to match
|
|
275
|
-
// the JS path's boundaryless-error status (server-action.ts).
|
|
276
|
-
boundarylessErrorStatus = 500;
|
|
222
|
+
failure = await matchPeFailure(input, error, directActionId, true);
|
|
277
223
|
}
|
|
278
224
|
}
|
|
279
225
|
|
|
226
|
+
if (failure?.boundary) {
|
|
227
|
+
return { reactFormState: null, directActionId, failure };
|
|
228
|
+
}
|
|
229
|
+
if (failure) {
|
|
230
|
+
console.error("[RSC] Progressive enhancement action error:", failure.error);
|
|
231
|
+
}
|
|
232
|
+
|
|
280
233
|
// Handle Response returned from action during PE.
|
|
281
234
|
// In the JS path, executeServerAction intercepts redirect Responses and
|
|
282
235
|
// short-circuits. The PE path must handle them too.
|
|
@@ -304,6 +257,58 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
304
257
|
console.error("[RSC] Failed to decode form state:", error);
|
|
305
258
|
}
|
|
306
259
|
|
|
260
|
+
return { reactFormState, directActionId, failure };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** What a no-JS form post's action left for the re-render (`_actionRan`). */
|
|
264
|
+
export interface FormActionOutcome {
|
|
265
|
+
reactFormState: ReactFormState | null;
|
|
266
|
+
directActionId: string | null;
|
|
267
|
+
/** The action threw, or the form body did not parse. */
|
|
268
|
+
failure?: PeFailure;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A failed no-JS form post and the error boundary matched for it. The boundary
|
|
273
|
+
* renders in place of the page, inside route middleware as the JS path renders
|
|
274
|
+
* its own (server-action.ts `errorBoundary`). With none, the page re-renders
|
|
275
|
+
* with 500, the JS path's status for the same boundaryless error.
|
|
276
|
+
*/
|
|
277
|
+
export interface PeFailure {
|
|
278
|
+
error: unknown;
|
|
279
|
+
actionId: string | null | undefined;
|
|
280
|
+
boundary: MatchResult | null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The outcome for a rewrite pass: a failure's boundary is re-matched on the
|
|
285
|
+
* target, since pass 1 matched (or missed) the rewriting route's
|
|
286
|
+
* (`continuationForRewritePass` in server-action.ts).
|
|
287
|
+
*/
|
|
288
|
+
async function outcomeForRewritePass<TEnv>(
|
|
289
|
+
input: PeRequestInput<TEnv>,
|
|
290
|
+
outcome: FormActionOutcome,
|
|
291
|
+
): Promise<FormActionOutcome> {
|
|
292
|
+
const { failure } = outcome;
|
|
293
|
+
if (!failure) return outcome;
|
|
294
|
+
const boundary = await input.ctx.router.matchError(
|
|
295
|
+
input.request,
|
|
296
|
+
{ env: input.env },
|
|
297
|
+
failure.error,
|
|
298
|
+
"route",
|
|
299
|
+
);
|
|
300
|
+
return { ...outcome, failure: { ...failure, boundary } };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The re-render after a no-JS form post's action, wrapped in route middleware. */
|
|
304
|
+
async function rerenderAfterFormAction<TEnv>(
|
|
305
|
+
input: PeRequestInput<TEnv>,
|
|
306
|
+
outcome: FormActionOutcome,
|
|
307
|
+
): Promise<Response> {
|
|
308
|
+
const { ctx, request, env, url, handleStore, nonce, routeMwInfo, trace } =
|
|
309
|
+
input;
|
|
310
|
+
const { failure } = outcome;
|
|
311
|
+
|
|
307
312
|
// Re-render the page and return HTML.
|
|
308
313
|
// Route middleware wraps the render so context variables, headers, and
|
|
309
314
|
// cookies set by route middleware are available during re-render — matching
|
|
@@ -313,11 +318,7 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
313
318
|
// custom headers) so loaders that read request headers/cookies behave
|
|
314
319
|
// identically under PE and the JS action path. Drop body-framing headers
|
|
315
320
|
// from the bodyless GET and force the HTML accept.
|
|
316
|
-
const headers =
|
|
317
|
-
headers.delete("content-type");
|
|
318
|
-
headers.delete("content-length");
|
|
319
|
-
headers.delete("content-encoding");
|
|
320
|
-
headers.delete("transfer-encoding");
|
|
321
|
+
const headers = bodylessHeaders(request);
|
|
321
322
|
headers.set("accept", "text/html");
|
|
322
323
|
const renderRequest = new Request(url.toString(), {
|
|
323
324
|
method: "GET",
|
|
@@ -340,14 +341,17 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
340
341
|
renderRequest,
|
|
341
342
|
handleStore,
|
|
342
343
|
nonce,
|
|
343
|
-
|
|
344
|
-
boundarylessErrorStatus,
|
|
345
|
-
directActionId,
|
|
344
|
+
...outcome,
|
|
346
345
|
}),
|
|
347
346
|
{ trace, owner: getRequestContext() },
|
|
348
347
|
);
|
|
349
348
|
};
|
|
350
349
|
|
|
350
|
+
const boundary = failure?.boundary;
|
|
351
|
+
const render = boundary
|
|
352
|
+
? () => renderPeErrorBoundary(input, failure?.actionId, boundary)
|
|
353
|
+
: renderPage;
|
|
354
|
+
|
|
351
355
|
// Execute route middleware wrapping the render, if any.
|
|
352
356
|
// finalizeResponse drains onResponse callbacks that middleware short-circuits
|
|
353
357
|
// may leave behind (executeMiddleware does not finalize them itself).
|
|
@@ -355,19 +359,19 @@ async function handleProgressiveEnhancementInner<TEnv>(
|
|
|
355
359
|
return finalizeResponse(
|
|
356
360
|
await executeMiddleware(
|
|
357
361
|
buildRouteMiddlewareEntries(routeMwInfo.routeMiddleware),
|
|
358
|
-
request,
|
|
362
|
+
actionRenderRequest(request),
|
|
359
363
|
env,
|
|
360
364
|
routeMwInfo.variables,
|
|
361
|
-
|
|
365
|
+
render,
|
|
362
366
|
routeMwInfo.routeReverse,
|
|
363
367
|
),
|
|
364
368
|
);
|
|
365
369
|
}
|
|
366
370
|
|
|
367
|
-
return
|
|
371
|
+
return render();
|
|
368
372
|
}
|
|
369
373
|
|
|
370
|
-
interface PeRenderInput<TEnv> {
|
|
374
|
+
interface PeRenderInput<TEnv> extends FormActionOutcome {
|
|
371
375
|
ctx: HandlerContext<TEnv>;
|
|
372
376
|
request: Request;
|
|
373
377
|
env: TEnv;
|
|
@@ -375,9 +379,6 @@ interface PeRenderInput<TEnv> {
|
|
|
375
379
|
renderRequest: Request;
|
|
376
380
|
handleStore: ReturnType<typeof getRequestContext>["_handleStore"];
|
|
377
381
|
nonce: string | undefined;
|
|
378
|
-
reactFormState: ReactFormState | null;
|
|
379
|
-
boundarylessErrorStatus: number | undefined;
|
|
380
|
-
directActionId: string | null;
|
|
381
382
|
}
|
|
382
383
|
|
|
383
384
|
/** PE re-render: match the bodyless GET mirror, then render full HTML. */
|
|
@@ -413,13 +414,13 @@ function renderPeResponse<TEnv>(
|
|
|
413
414
|
handleStore,
|
|
414
415
|
nonce,
|
|
415
416
|
reactFormState,
|
|
416
|
-
|
|
417
|
+
failure,
|
|
417
418
|
directActionId,
|
|
418
419
|
} = input;
|
|
419
420
|
|
|
420
421
|
const payload: RscPayload = {
|
|
421
422
|
metadata: {
|
|
422
|
-
|
|
423
|
+
...payloadLocation(getRequestContext(), url),
|
|
423
424
|
routerId: ctx.router.id,
|
|
424
425
|
basename: ctx.router.basename,
|
|
425
426
|
segments: attachTransitionWhen(match.segments, getRequestContext()),
|
|
@@ -461,13 +462,10 @@ function renderPeResponse<TEnv>(
|
|
|
461
462
|
url,
|
|
462
463
|
payload,
|
|
463
464
|
init: {
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
...(boundarylessErrorStatus !== undefined
|
|
469
|
-
? { status: boundarylessErrorStatus }
|
|
470
|
-
: {}),
|
|
465
|
+
// A failure reaching the page render matched no boundary: 500, like
|
|
466
|
+
// the JS path. The redirect branch in peRenderPlan returns before
|
|
467
|
+
// this, so a redirect re-render keeps its 308.
|
|
468
|
+
...(failure ? { status: 500 } : {}),
|
|
471
469
|
headers: { "content-type": "text/html;charset=utf-8" },
|
|
472
470
|
},
|
|
473
471
|
tracking: stageTracking,
|
|
@@ -487,80 +485,22 @@ function renderPeResponse<TEnv>(
|
|
|
487
485
|
);
|
|
488
486
|
}
|
|
489
487
|
|
|
490
|
-
interface PeErrorBoundaryInput<TEnv> {
|
|
491
|
-
ctx: HandlerContext<TEnv>;
|
|
492
|
-
request: Request;
|
|
493
|
-
env: TEnv;
|
|
494
|
-
url: URL;
|
|
495
|
-
error: unknown;
|
|
496
|
-
handleStore: ReturnType<typeof getRequestContext>["_handleStore"];
|
|
497
|
-
nonce: string | undefined;
|
|
498
|
-
actionId: string | null | undefined;
|
|
499
|
-
actionRan: boolean;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
type PeErrorMatch<TEnv> = NonNullable<
|
|
503
|
-
Awaited<ReturnType<HandlerContext<TEnv>["router"]["matchError"]>>
|
|
504
|
-
>;
|
|
505
|
-
|
|
506
488
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
489
|
+
* Match an error boundary for a failed PE post and report the error, handled
|
|
490
|
+
* or not. A matchError failure reports the ORIGINAL error as unhandled and
|
|
491
|
+
* rethrows the match failure.
|
|
510
492
|
*/
|
|
511
|
-
async function
|
|
512
|
-
|
|
513
|
-
request: Request,
|
|
514
|
-
env: TEnv,
|
|
515
|
-
url: URL,
|
|
493
|
+
async function matchPeFailure<TEnv>(
|
|
494
|
+
input: PeRequestInput<TEnv>,
|
|
516
495
|
error: unknown,
|
|
517
|
-
|
|
518
|
-
nonce: string | undefined,
|
|
519
|
-
trace: RoutineTrace | undefined,
|
|
520
|
-
actionId?: string | null,
|
|
496
|
+
actionId: string | null | undefined,
|
|
521
497
|
// True when an action actually ran and threw (vs a malformed form body, where
|
|
522
498
|
// no action executed). Drives _inActionRevalidation for JS/PE parity — it must
|
|
523
499
|
// NOT be inferred from actionId, since a useActionState bound action can run
|
|
524
500
|
// and throw with no $$id (actionId === undefined) yet still be an action error.
|
|
525
|
-
actionRan
|
|
526
|
-
): Promise<
|
|
527
|
-
|
|
528
|
-
peErrorBoundaryPlan({
|
|
529
|
-
ctx,
|
|
530
|
-
request,
|
|
531
|
-
env,
|
|
532
|
-
url,
|
|
533
|
-
error,
|
|
534
|
-
handleStore,
|
|
535
|
-
nonce,
|
|
536
|
-
actionId,
|
|
537
|
-
actionRan,
|
|
538
|
-
}),
|
|
539
|
-
{ trace, owner: getRequestContext() },
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/** PE error boundary: match a boundary for the thrown error, then render it. */
|
|
544
|
-
function* peErrorBoundaryPlan<TEnv>(
|
|
545
|
-
input: PeErrorBoundaryInput<TEnv>,
|
|
546
|
-
): RoutinePlan<Response | null> {
|
|
547
|
-
const boundary = yield* step("match:error", () =>
|
|
548
|
-
matchPeErrorBoundary(input),
|
|
549
|
-
);
|
|
550
|
-
if (!boundary) return null;
|
|
551
|
-
return yield* step("render", () => renderPeErrorResponse(input, boundary));
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Match an error boundary for the PE path, owning the reporting protocol:
|
|
556
|
-
* a matchError failure reports the ORIGINAL error as unhandled and rethrows
|
|
557
|
-
* the match failure; a miss returns null; a hit reports handled and stamps
|
|
558
|
-
* params + action gate context before the render.
|
|
559
|
-
*/
|
|
560
|
-
async function matchPeErrorBoundary<TEnv>(
|
|
561
|
-
input: PeErrorBoundaryInput<TEnv>,
|
|
562
|
-
): Promise<PeErrorMatch<TEnv> | null> {
|
|
563
|
-
const { ctx, request, env, url, error, actionId, actionRan } = input;
|
|
501
|
+
actionRan: boolean,
|
|
502
|
+
): Promise<PeFailure> {
|
|
503
|
+
const { ctx, request, env, url } = input;
|
|
564
504
|
|
|
565
505
|
// JS/PE parity for an action-triggered error re-render: a stale
|
|
566
506
|
// `foregroundOnAction` cache entry inside the error boundary must foreground
|
|
@@ -572,9 +512,9 @@ async function matchPeErrorBoundary<TEnv>(
|
|
|
572
512
|
getRequestContext()._inActionRevalidation = true;
|
|
573
513
|
}
|
|
574
514
|
|
|
575
|
-
let
|
|
515
|
+
let boundary: MatchResult | null;
|
|
576
516
|
try {
|
|
577
|
-
|
|
517
|
+
boundary = await ctx.router.matchError(request, { env }, error, "route");
|
|
578
518
|
} catch (matchErr) {
|
|
579
519
|
ctx.callOnError(error, "action", {
|
|
580
520
|
request,
|
|
@@ -586,31 +526,41 @@ async function matchPeErrorBoundary<TEnv>(
|
|
|
586
526
|
throw matchErr;
|
|
587
527
|
}
|
|
588
528
|
|
|
589
|
-
if (!errorResult) return null;
|
|
590
|
-
|
|
591
529
|
ctx.callOnError(error, "action", {
|
|
592
530
|
request,
|
|
593
531
|
url,
|
|
594
532
|
env,
|
|
595
533
|
actionId: actionId ?? undefined,
|
|
596
|
-
handledByBoundary:
|
|
534
|
+
handledByBoundary: !!boundary,
|
|
597
535
|
});
|
|
536
|
+
return { error, actionId, boundary };
|
|
537
|
+
}
|
|
598
538
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
539
|
+
/** Render a failed PE post's boundary, inside route middleware. */
|
|
540
|
+
function renderPeErrorBoundary<TEnv>(
|
|
541
|
+
input: PeRequestInput<TEnv>,
|
|
542
|
+
actionId: string | null | undefined,
|
|
543
|
+
boundary: MatchResult,
|
|
544
|
+
): Promise<Response> {
|
|
545
|
+
return runRoutine(
|
|
546
|
+
step("render", () => renderPeErrorResponse(input, actionId, boundary)),
|
|
547
|
+
{ trace: input.trace, owner: getRequestContext() },
|
|
548
|
+
);
|
|
602
549
|
}
|
|
603
550
|
|
|
604
551
|
/** Render the matched PE error boundary as a full HTML document. */
|
|
605
552
|
function renderPeErrorResponse<TEnv>(
|
|
606
|
-
input:
|
|
607
|
-
|
|
553
|
+
input: PeRequestInput<TEnv>,
|
|
554
|
+
actionId: string | null | undefined,
|
|
555
|
+
errorResult: MatchResult,
|
|
608
556
|
): Promise<Response> {
|
|
609
|
-
const { ctx, request, env, url, handleStore, nonce
|
|
557
|
+
const { ctx, request, env, url, handleStore, nonce } = input;
|
|
558
|
+
|
|
559
|
+
setRequestContextParams(errorResult.params, errorResult.routeName);
|
|
610
560
|
|
|
611
561
|
const payload: RscPayload = {
|
|
612
562
|
metadata: {
|
|
613
|
-
|
|
563
|
+
...payloadLocation(getRequestContext(), url),
|
|
614
564
|
routerId: ctx.router.id,
|
|
615
565
|
basename: ctx.router.basename,
|
|
616
566
|
segments: attachTransitionWhen(errorResult.segments, getRequestContext()),
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
rebaseRequestContext,
|
|
22
22
|
type RequestContext,
|
|
23
23
|
} from "../server/request-context.js";
|
|
24
|
-
import {
|
|
24
|
+
import { bodylessRequest } from "../server/request-headers.js";
|
|
25
|
+
import {
|
|
26
|
+
recordRewriteHop,
|
|
27
|
+
recordRewriteShell,
|
|
28
|
+
} from "../server/rewrite-trail.js";
|
|
25
29
|
|
|
26
30
|
/** Response header naming the route path a rewritten request rendered. */
|
|
27
31
|
export const REWRITE_HEADER = "x-rango-rewrite";
|
|
@@ -45,11 +49,9 @@ export function resolveRewriteTarget(
|
|
|
45
49
|
const path =
|
|
46
50
|
"path" in target
|
|
47
51
|
? prefixBasename(target.path, basename)
|
|
48
|
-
:
|
|
49
|
-
// missing ones instead of resolving to its raw pattern.
|
|
50
|
-
createReverseFunction(routeMap)(
|
|
52
|
+
: createReverseFunction(routeMap)(
|
|
51
53
|
target.name,
|
|
52
|
-
target.params
|
|
54
|
+
target.params,
|
|
53
55
|
target.search,
|
|
54
56
|
);
|
|
55
57
|
const resolved = stripInternalParams(new URL(path, base.origin));
|
|
@@ -94,11 +96,38 @@ export interface RewritePass {
|
|
|
94
96
|
hops: number;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
let lateLockReported = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `.disallowMutations()` thrown after the action ran cannot refuse it. Dev
|
|
103
|
+
* warns on every throw; production logs one error per isolate, so a lock
|
|
104
|
+
* that never refused anything is not silent there.
|
|
105
|
+
*/
|
|
106
|
+
function reportLateLock(rewrite: Rewrite): void {
|
|
107
|
+
const message = `[rango] ${describeRewriteTarget(rewrite.target)}.disallowMutations() came after the server action or form post ran, so it cannot refuse it: route middleware, and global middleware on a later rewritten pass, run only after the action. The render moves to the target. To refuse mutations, throw it from global middleware (router.use()) matched on the URL the form posts to.`;
|
|
108
|
+
if (process.env.NODE_ENV !== "production") {
|
|
109
|
+
console.warn(message);
|
|
110
|
+
} else if (!lateLockReported) {
|
|
111
|
+
lateLockReported = true;
|
|
112
|
+
console.error(message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
/**
|
|
98
117
|
* Move the request context to `rewrite`'s target for the next pass: check
|
|
99
118
|
* that this request can be rewritten, resolve the target, count the hop,
|
|
100
|
-
* then rebase the context. Throws the site error for a
|
|
101
|
-
*
|
|
119
|
+
* then rebase the context. Throws the site error for a loader fetch, a
|
|
120
|
+
* refused mutation or one whose body middleware read before its action ran,
|
|
121
|
+
* a resolution error, and RewriteLoopError.
|
|
122
|
+
*
|
|
123
|
+
* A mutation moves too (#1145). Before its action ran (a rewrite from global
|
|
124
|
+
* middleware) it moves with its body, and the target's pass runs the action
|
|
125
|
+
* after the target's global middleware, as a direct post there would. After
|
|
126
|
+
* it ran (route middleware wraps only the re-render), the target's pass
|
|
127
|
+
* re-renders from `reqCtx._actionRan`. `disallowMutations()` refuses the
|
|
128
|
+
* first, so a lock never runs a mutation; the second is already committed,
|
|
129
|
+
* and refusing it reports an error for a write that happened, so the caller
|
|
130
|
+
* retries it.
|
|
102
131
|
*/
|
|
103
132
|
export function applyRewrite(
|
|
104
133
|
rewrite: Rewrite,
|
|
@@ -113,12 +142,30 @@ export function applyRewrite(
|
|
|
113
142
|
throw rewriteNotSupported(rewrite, "loader-fetch");
|
|
114
143
|
}
|
|
115
144
|
if (passRequest.method !== "GET" && passRequest.method !== "HEAD") {
|
|
116
|
-
|
|
145
|
+
if (!reqCtx._actionRan) {
|
|
146
|
+
if (rewrite.mutationsDisallowed) {
|
|
147
|
+
throw rewriteNotSupported(rewrite, "mutation");
|
|
148
|
+
}
|
|
149
|
+
// The target's pass hands the body to the action: one middleware read
|
|
150
|
+
// would run it with no arguments.
|
|
151
|
+
if (passRequest.bodyUsed) throw rewriteNotSupported(rewrite, "body-read");
|
|
152
|
+
} else if (rewrite.mutationsDisallowed) {
|
|
153
|
+
reportLateLock(rewrite);
|
|
154
|
+
}
|
|
117
155
|
}
|
|
118
156
|
const target = resolveRewriteTarget(rewrite, routeMap, reqCtx.url, basename);
|
|
119
157
|
const hops = recordRewriteHop(reqCtx, reqCtx.url, target);
|
|
158
|
+
recordRewriteShell(reqCtx, rewrite.shellAudience);
|
|
120
159
|
const url = rewriteTransportUrl(target, reqCtx.originalUrl);
|
|
121
|
-
|
|
160
|
+
// A mutation keeps its body: one whose action has not run yet, and a no-JS
|
|
161
|
+
// form post, since the PE handler reads a clone, so a form-field check
|
|
162
|
+
// reads it on every pass. A JS action that ran consumed it
|
|
163
|
+
// (`new Request(url, used)` throws): the same request without it or its
|
|
164
|
+
// content-type, so a body check gated on the content-type passes over this
|
|
165
|
+
// pass, which never runs the action.
|
|
166
|
+
const request = passRequest.bodyUsed
|
|
167
|
+
? bodylessRequest(url, passRequest, passRequest.method)
|
|
168
|
+
: new Request(url, passRequest);
|
|
122
169
|
rebaseRequestContext(reqCtx, request, url);
|
|
123
170
|
return { request, url, target, hops };
|
|
124
171
|
}
|