@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4
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 +120 -25
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +2151 -846
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +57 -11
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +229 -20
- package/skills/host-router/SKILL.md +45 -20
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +46 -4
- package/skills/layout/SKILL.md +28 -7
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +219 -9
- package/skills/middleware/SKILL.md +47 -12
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +71 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +66 -9
- package/skills/route/SKILL.md +57 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +751 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/testing/SKILL.md +647 -0
- package/skills/typesafety/SKILL.md +319 -27
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/event-controller.ts +86 -70
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +84 -11
- package/src/browser/navigation-client.ts +76 -28
- package/src/browser/navigation-store.ts +32 -9
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +64 -26
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +148 -16
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +30 -2
- package/src/browser/react/NavigationProvider.tsx +72 -31
- package/src/browser/react/filter-segment-order.ts +51 -7
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +20 -8
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +22 -2
- package/src/browser/react/use-segments.ts +11 -8
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +64 -22
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +21 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-trie.ts +52 -25
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +54 -13
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +92 -182
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +9 -4
- package/src/index.ts +53 -15
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -36
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +384 -257
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +100 -28
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +26 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +21 -38
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +8 -8
- package/src/router/loader-resolution.ts +19 -2
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +4 -3
- package/src/router/match-handlers.ts +63 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +53 -32
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +15 -26
- package/src/router/middleware.ts +99 -84
- package/src/router/pattern-matching.ts +101 -17
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +58 -2
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +27 -6
- package/src/router/segment-resolution/revalidation.ts +147 -106
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/trie-matching.ts +18 -13
- package/src/router/types.ts +8 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +38 -23
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +28 -69
- package/src/rsc/helpers.ts +91 -43
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/progressive-enhancement.ts +4 -0
- package/src/rsc/response-route-handler.ts +46 -53
- package/src/rsc/rsc-rendering.ts +35 -51
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +17 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +8 -2
- package/src/search-params.ts +4 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +132 -116
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +143 -53
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +20 -42
- package/src/ssr/index.tsx +5 -1
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +68 -50
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +5 -6
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +11 -0
- package/src/types/segments.ts +35 -2
- package/src/urls/include-helper.ts +34 -67
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +41 -7
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +101 -51
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +67 -26
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +8 -7
- package/src/vite/plugins/client-ref-dedup.ts +16 -0
- package/src/vite/plugins/client-ref-hashing.ts +28 -5
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +54 -30
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
- package/src/vite/plugins/expose-internal-ids.ts +496 -486
- package/src/vite/plugins/performance-tracks.ts +29 -25
- package/src/vite/plugins/use-cache-transform.ts +65 -50
- package/src/vite/plugins/version-injector.ts +39 -23
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +116 -29
- package/src/vite/router-discovery.ts +750 -100
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/package-resolution.ts +41 -1
- package/src/vite/utils/prerender-utils.ts +21 -6
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -138,34 +138,38 @@ export async function collectSegments(
|
|
|
138
138
|
function deduplicateLoaderSegments(
|
|
139
139
|
segments: ResolvedSegment[],
|
|
140
140
|
logPrefix: string,
|
|
141
|
-
): ResolvedSegment[] {
|
|
142
|
-
//
|
|
143
|
-
// and
|
|
141
|
+
): { segments: ResolvedSegment[]; removedIds: Set<string> } {
|
|
142
|
+
// Single pass: original (non-inherited) loaderIds, all loaderIds grouped by
|
|
143
|
+
// namespace, and namespaces of segments that declare loading().
|
|
144
144
|
const originalLoaders = new Set<string>();
|
|
145
|
-
const
|
|
145
|
+
const loaderIdsByNamespace = new Map<string, string[]>();
|
|
146
|
+
const namespacesWithLoading = new Set<string>();
|
|
146
147
|
for (const s of segments) {
|
|
147
|
-
if (s.type === "loader" && s.loaderId
|
|
148
|
-
originalLoaders.add(s.loaderId);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
if (s.type === "loader" && s.loaderId) {
|
|
149
|
+
if (!s._inherited) originalLoaders.add(s.loaderId);
|
|
150
|
+
const ids = loaderIdsByNamespace.get(s.namespace);
|
|
151
|
+
if (ids) ids.push(s.loaderId);
|
|
152
|
+
else loaderIdsByNamespace.set(s.namespace, [s.loaderId]);
|
|
153
|
+
} else if (
|
|
154
|
+
s.type !== "loader" &&
|
|
155
|
+
s.loading !== undefined &&
|
|
156
|
+
s.loading !== false
|
|
157
|
+
) {
|
|
158
|
+
namespacesWithLoading.add(s.namespace);
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
loadersWithLoading.add(l.loaderId);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
161
|
+
|
|
162
|
+
// An inherited loader is needed when it shares a namespace with a
|
|
163
|
+
// loading-bearing segment (its data sits behind that LoaderBoundary).
|
|
164
|
+
const loadersWithLoading = new Set<string>();
|
|
165
|
+
for (const ns of namespacesWithLoading) {
|
|
166
|
+
for (const id of loaderIdsByNamespace.get(ns) ?? []) {
|
|
167
|
+
loadersWithLoading.add(id);
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
const result: ResolvedSegment[] = [];
|
|
168
|
-
|
|
172
|
+
const removedIds = new Set<string>();
|
|
169
173
|
|
|
170
174
|
for (const s of segments) {
|
|
171
175
|
if (
|
|
@@ -175,17 +179,20 @@ function deduplicateLoaderSegments(
|
|
|
175
179
|
originalLoaders.has(s.loaderId) &&
|
|
176
180
|
!loadersWithLoading.has(s.loaderId)
|
|
177
181
|
) {
|
|
178
|
-
|
|
182
|
+
removedIds.add(s.id);
|
|
179
183
|
continue;
|
|
180
184
|
}
|
|
181
185
|
result.push(s);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
|
-
if (
|
|
185
|
-
debugLog(
|
|
188
|
+
if (removedIds.size > 0) {
|
|
189
|
+
debugLog(
|
|
190
|
+
logPrefix,
|
|
191
|
+
`deduped ${removedIds.size} inherited loader segment(s)`,
|
|
192
|
+
);
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
return result;
|
|
195
|
+
return { segments: result, removedIds };
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
/**
|
|
@@ -244,7 +251,7 @@ export function buildMatchResult<TEnv>(
|
|
|
244
251
|
);
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
const dedupedSegments = deduplicateLoaderSegments(
|
|
254
|
+
const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
|
|
248
255
|
segmentsToRender,
|
|
249
256
|
logPrefix,
|
|
250
257
|
);
|
|
@@ -262,18 +269,32 @@ export function buildMatchResult<TEnv>(
|
|
|
262
269
|
|
|
263
270
|
// Remove deduped loader IDs from matched so the client doesn't treat
|
|
264
271
|
// them as missing segments and trigger a fallback refetch.
|
|
265
|
-
const removedIds = new Set(
|
|
266
|
-
segmentsToRender
|
|
267
|
-
.filter((s) => !dedupedSegments.includes(s))
|
|
268
|
-
.map((s) => s.id),
|
|
269
|
-
);
|
|
270
272
|
const matchedIds =
|
|
271
273
|
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
272
274
|
|
|
275
|
+
// resolvedIds: every segment whose handler actually ran this request.
|
|
276
|
+
// For full-match every segment is fresh; for partial-match we filter by
|
|
277
|
+
// the internal `_handlerRan` flag set in revalidation.ts. Drives the
|
|
278
|
+
// client's handle-bucket cleanup — a slot that re-resolved and pushed
|
|
279
|
+
// nothing must have its previous handle data cleared, but `diff` won't
|
|
280
|
+
// carry it because the segment payload skips null-component cached
|
|
281
|
+
// segments to save bytes.
|
|
282
|
+
const resolvedIds = ctx.isFullMatch
|
|
283
|
+
? allSegments.map((s) => s.id)
|
|
284
|
+
: allSegments.filter((s) => s._handlerRan).map((s) => s.id);
|
|
285
|
+
|
|
286
|
+
// Strip internal-only fields from the segments going on the wire.
|
|
287
|
+
const cleanedSegments = dedupedSegments.map((s) => {
|
|
288
|
+
if (s._handlerRan === undefined) return s;
|
|
289
|
+
const { _handlerRan: _drop, ...rest } = s;
|
|
290
|
+
return rest as ResolvedSegment;
|
|
291
|
+
});
|
|
292
|
+
|
|
273
293
|
return {
|
|
274
|
-
segments:
|
|
294
|
+
segments: cleanedSegments,
|
|
275
295
|
matched: matchedIds,
|
|
276
|
-
diff:
|
|
296
|
+
diff: cleanedSegments.map((s) => s.id),
|
|
297
|
+
resolvedIds,
|
|
277
298
|
params: ctx.matched.params,
|
|
278
299
|
routeName: ctx.routeKey,
|
|
279
300
|
slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
|
package/src/router/metrics.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import type { ScopedReverseFunction } from "../reverse.js";
|
|
15
15
|
import type { Theme } from "../theme/types.js";
|
|
16
16
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
17
|
+
import type { RequestScope } from "../types/request-scope.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Get variable function type
|
|
@@ -52,33 +53,15 @@ export interface CookieOptions {
|
|
|
52
53
|
* Context passed to middleware
|
|
53
54
|
*
|
|
54
55
|
* @template TEnv - Environment type (bindings, variables) - defaults to any for internal flexibility
|
|
55
|
-
* @template TParams - URL params type (typed for route middleware,
|
|
56
|
+
* @template TParams - URL params type (typed for route middleware,
|
|
57
|
+
* `Record<string, string | undefined>` for global middleware — absent
|
|
58
|
+
* optional segments are omitted from the params record at runtime, so
|
|
59
|
+
* the index signature must include `undefined`)
|
|
56
60
|
*/
|
|
57
61
|
export interface MiddlewareContext<
|
|
58
62
|
TEnv = any,
|
|
59
|
-
TParams = Record<string, string>,
|
|
60
|
-
> {
|
|
61
|
-
/** Original request */
|
|
62
|
-
request: Request;
|
|
63
|
-
|
|
64
|
-
/** Parsed URL (with internal `_rsc*` params stripped) */
|
|
65
|
-
url: URL;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The original request URL with all parameters intact, including
|
|
69
|
-
* internal `_rsc*` transport params.
|
|
70
|
-
*/
|
|
71
|
-
originalUrl: URL;
|
|
72
|
-
|
|
73
|
-
/** URL pathname */
|
|
74
|
-
pathname: string;
|
|
75
|
-
|
|
76
|
-
/** URL search params */
|
|
77
|
-
searchParams: URLSearchParams;
|
|
78
|
-
|
|
79
|
-
/** Platform bindings (Cloudflare, etc.) */
|
|
80
|
-
env: TEnv;
|
|
81
|
-
|
|
63
|
+
TParams = Record<string, string | undefined>,
|
|
64
|
+
> extends RequestScope<TEnv> {
|
|
82
65
|
/** URL params extracted from route/middleware pattern */
|
|
83
66
|
params: TParams;
|
|
84
67
|
|
|
@@ -157,7 +140,7 @@ export interface MiddlewareContext<
|
|
|
157
140
|
* @template TEnv - Environment type - defaults to any for internal flexibility
|
|
158
141
|
* @template TParams - URL params type (typed for route middleware)
|
|
159
142
|
*
|
|
160
|
-
* When using middleware with global augmentation (
|
|
143
|
+
* When using middleware with global augmentation (Rango.Env), explicitly
|
|
161
144
|
* annotate your middleware functions, or the types will be inferred from context:
|
|
162
145
|
*
|
|
163
146
|
* @example
|
|
@@ -169,7 +152,10 @@ export interface MiddlewareContext<
|
|
|
169
152
|
* router.use((ctx, next) => {...}) // ctx is typed from router's TEnv
|
|
170
153
|
* ```
|
|
171
154
|
*/
|
|
172
|
-
export type MiddlewareFn<
|
|
155
|
+
export type MiddlewareFn<
|
|
156
|
+
TEnv = any,
|
|
157
|
+
TParams = Record<string, string | undefined>,
|
|
158
|
+
> = (
|
|
173
159
|
ctx: MiddlewareContext<TEnv, TParams>,
|
|
174
160
|
next: () => Promise<Response>,
|
|
175
161
|
) => Response | void | Promise<Response | void>;
|
|
@@ -216,5 +202,8 @@ export interface MiddlewareCollectableEntry {
|
|
|
216
202
|
*/
|
|
217
203
|
export interface CollectedMiddleware {
|
|
218
204
|
handler: MiddlewareFn<any, any>;
|
|
205
|
+
// Internal shape only. The user-facing `MiddlewareContext.params` is
|
|
206
|
+
// typed `Record<string, string | undefined>` to reflect that absent
|
|
207
|
+
// optional segments are omitted from the params record at runtime.
|
|
219
208
|
params: Record<string, string>;
|
|
220
209
|
}
|
package/src/router/middleware.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { contextGet, contextSet } from "../context-var.js";
|
|
13
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
14
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
15
|
import type {
|
|
14
16
|
CollectedMiddleware,
|
|
15
17
|
MiddlewareCollectableEntry,
|
|
@@ -22,6 +24,7 @@ import { _getRequestContext } from "../server/request-context.js";
|
|
|
22
24
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
25
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
24
26
|
import { stripInternalParams } from "./handler-context.js";
|
|
27
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
25
28
|
|
|
26
29
|
// Re-export types and cookie utilities for backward compatibility
|
|
27
30
|
export type {
|
|
@@ -112,7 +115,12 @@ function escapeRegex(str: string): string {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/**
|
|
115
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
118
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
119
|
+
*
|
|
120
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
121
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
122
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
123
|
+
* Express/React Router/Fastify/Koa.
|
|
116
124
|
*/
|
|
117
125
|
export function extractParams(
|
|
118
126
|
pathname: string,
|
|
@@ -124,7 +132,7 @@ export function extractParams(
|
|
|
124
132
|
|
|
125
133
|
const params: Record<string, string> = {};
|
|
126
134
|
for (let i = 0; i < paramNames.length; i++) {
|
|
127
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
135
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
128
136
|
}
|
|
129
137
|
return params;
|
|
130
138
|
}
|
|
@@ -179,14 +187,22 @@ export function createMiddlewareContext<TEnv>(
|
|
|
179
187
|
return responseHolder.response;
|
|
180
188
|
};
|
|
181
189
|
|
|
190
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
191
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
192
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
193
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
194
|
+
// during `await next()`.
|
|
195
|
+
const reqCtx = _getRequestContext();
|
|
182
196
|
return {
|
|
183
197
|
request,
|
|
184
198
|
url,
|
|
185
|
-
originalUrl: new URL(request.url),
|
|
199
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
186
200
|
pathname: url.pathname,
|
|
187
201
|
searchParams: url.searchParams,
|
|
188
202
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
189
203
|
params,
|
|
204
|
+
executionContext: reqCtx?.executionContext,
|
|
205
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
190
206
|
// Getter: re-derives from request context on each access so that global
|
|
191
207
|
// middleware sees the matched route name after await next().
|
|
192
208
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -291,6 +307,46 @@ export function matchMiddleware<TEnv>(
|
|
|
291
307
|
return matches;
|
|
292
308
|
}
|
|
293
309
|
|
|
310
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
311
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
312
|
+
// explicit short-circuit Response's own headers win).
|
|
313
|
+
function mergeStubHeaders(
|
|
314
|
+
target: Headers,
|
|
315
|
+
stub: Headers,
|
|
316
|
+
stubOverridesNonCookie: boolean,
|
|
317
|
+
): void {
|
|
318
|
+
stub.forEach((value, name) => {
|
|
319
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
320
|
+
target.append(name, value);
|
|
321
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
322
|
+
target.set(name, value);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
328
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
329
|
+
function mergeReqCtxStub(
|
|
330
|
+
target: Headers,
|
|
331
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
332
|
+
): void {
|
|
333
|
+
if (!reqCtx) return;
|
|
334
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
335
|
+
if (stubCookies.length > 0) {
|
|
336
|
+
const existing = new Set(target.getSetCookie());
|
|
337
|
+
for (const cookie of stubCookies) {
|
|
338
|
+
if (!existing.has(cookie)) {
|
|
339
|
+
target.append("set-cookie", cookie);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
344
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
345
|
+
target.set(name, value);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
294
350
|
/**
|
|
295
351
|
* Execute middleware chain
|
|
296
352
|
*
|
|
@@ -329,35 +385,13 @@ export async function executeMiddleware<TEnv>(
|
|
|
329
385
|
// End of chain - call actual RSC handler
|
|
330
386
|
const response = await finalHandler();
|
|
331
387
|
|
|
332
|
-
// Merge headers set on stub into the real response.
|
|
333
|
-
// Use append for Set-Cookie to preserve multiple cookies.
|
|
334
388
|
const mergedHeaders = new Headers(response.headers);
|
|
335
|
-
stubResponse.headers
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
342
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
343
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
344
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
345
|
-
const reqCtx = _getRequestContext();
|
|
346
|
-
if (reqCtx) {
|
|
347
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
348
|
-
if (stubCookies.length > 0) {
|
|
349
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
350
|
-
for (const cookie of stubCookies) {
|
|
351
|
-
if (!existing.has(cookie)) {
|
|
352
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
357
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
358
|
-
mergedHeaders.set(name, value);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
389
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, true);
|
|
390
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
391
|
+
|
|
392
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
393
|
+
responseHolder.response = response;
|
|
394
|
+
return response;
|
|
361
395
|
}
|
|
362
396
|
|
|
363
397
|
// Clone response with merged headers (mutable for post-next() modifications)
|
|
@@ -426,8 +460,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
426
460
|
try {
|
|
427
461
|
result = await entry.handler(ctx, wrappedNext);
|
|
428
462
|
} catch (error) {
|
|
429
|
-
|
|
430
|
-
|
|
463
|
+
// Thrown Response is short-circuit control flow, not an error.
|
|
464
|
+
// Fall through to the `if (result instanceof Response)` branch below
|
|
465
|
+
// so stub headers and request-context cookies merge as they do for
|
|
466
|
+
// an explicit `return new Response(...)`. Real errors propagate.
|
|
467
|
+
if (error instanceof Response) {
|
|
468
|
+
result = error;
|
|
469
|
+
} else {
|
|
470
|
+
finishMiddleware();
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
431
473
|
}
|
|
432
474
|
finishMiddleware();
|
|
433
475
|
|
|
@@ -451,34 +493,13 @@ export async function executeMiddleware<TEnv>(
|
|
|
451
493
|
// RequestContext stub headers (from ctx.setCookie) into the
|
|
452
494
|
// returned Response so they are not lost.
|
|
453
495
|
if (result instanceof Response) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
mergedHeaders.append(name, value);
|
|
458
|
-
} else if (!mergedHeaders.has(name)) {
|
|
459
|
-
mergedHeaders.set(name, value);
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
463
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
464
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
465
|
-
const reqCtx = _getRequestContext();
|
|
466
|
-
if (reqCtx) {
|
|
467
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
468
|
-
if (stubCookies.length > 0) {
|
|
469
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
470
|
-
for (const cookie of stubCookies) {
|
|
471
|
-
if (!existing.has(cookie)) {
|
|
472
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
477
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
478
|
-
mergedHeaders.set(name, value);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
496
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
497
|
+
responseHolder.response = result;
|
|
498
|
+
return result;
|
|
481
499
|
}
|
|
500
|
+
const mergedHeaders = new Headers(result.headers);
|
|
501
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
|
|
502
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
482
503
|
const merged = new Response(result.body, {
|
|
483
504
|
status: result.status,
|
|
484
505
|
statusText: result.statusText,
|
|
@@ -527,23 +548,12 @@ export async function executeMiddleware<TEnv>(
|
|
|
527
548
|
// last merge point (e.g. cookies().set() called after await next()).
|
|
528
549
|
// The reqCtx stub may have already been partially merged during finalHandler
|
|
529
550
|
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
551
|
+
//
|
|
552
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
553
|
+
// set-cookie on an upgrade is not meaningful.
|
|
530
554
|
const reqCtx = _getRequestContext();
|
|
531
|
-
if (reqCtx) {
|
|
532
|
-
|
|
533
|
-
if (stubCookies.length > 0) {
|
|
534
|
-
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
535
|
-
for (const cookie of stubCookies) {
|
|
536
|
-
if (!existingCookies.has(cookie)) {
|
|
537
|
-
finalResponse.headers.append("set-cookie", cookie);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
// Fill in non-cookie headers that aren't already on the response
|
|
542
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
543
|
-
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
544
|
-
finalResponse.headers.set(name, value);
|
|
545
|
-
}
|
|
546
|
-
});
|
|
555
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
556
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
547
557
|
}
|
|
548
558
|
|
|
549
559
|
return finalResponse;
|
|
@@ -613,7 +623,18 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
613
623
|
return next();
|
|
614
624
|
};
|
|
615
625
|
|
|
616
|
-
|
|
626
|
+
let result: Response | void;
|
|
627
|
+
try {
|
|
628
|
+
result = await middleware(ctx, guardedNext);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
631
|
+
// explicit-return path below. Real errors propagate.
|
|
632
|
+
if (error instanceof Response) {
|
|
633
|
+
result = error;
|
|
634
|
+
} else {
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
617
638
|
|
|
618
639
|
if (result instanceof Response) {
|
|
619
640
|
earlyResponse = result;
|
|
@@ -641,13 +662,7 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
641
662
|
// Only fill in missing headers — the returned Response's explicit
|
|
642
663
|
// headers take precedence, matching executeMiddleware behavior.
|
|
643
664
|
const mergedHeaders = new Headers(response.headers);
|
|
644
|
-
stubResponse.headers
|
|
645
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
646
|
-
mergedHeaders.append(name, value);
|
|
647
|
-
} else if (!mergedHeaders.has(name)) {
|
|
648
|
-
mergedHeaders.set(name, value);
|
|
649
|
-
}
|
|
650
|
-
});
|
|
665
|
+
mergeStubHeaders(mergedHeaders, stubResponse.headers, false);
|
|
651
666
|
return new Response(response.body, {
|
|
652
667
|
status: response.status,
|
|
653
668
|
statusText: response.statusText,
|