@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
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/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -101,7 +101,10 @@
|
|
|
101
101
|
* - Non-GET request (only GET requests are cacheable)
|
|
102
102
|
*/
|
|
103
103
|
import type { ResolvedSegment } from "../../types.js";
|
|
104
|
-
import {
|
|
104
|
+
import {
|
|
105
|
+
getRequestContext,
|
|
106
|
+
runWithRequestContext,
|
|
107
|
+
} from "../../server/request-context.js";
|
|
105
108
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
109
|
import { getRouterContext } from "../router-context.js";
|
|
107
110
|
import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
@@ -231,34 +234,46 @@ export function withCacheStore<TEnv>(
|
|
|
231
234
|
setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
|
|
232
235
|
|
|
233
236
|
const Store = ctx.Store;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
// Re-establish the request-context ALS around the re-render. Store
|
|
238
|
+
// is a different ALS (DSL build context); on workerd a waitUntil
|
|
239
|
+
// task runs detached from the request's I/O context, so a handler/
|
|
240
|
+
// component that reads the ambient getRequestContext() during this
|
|
241
|
+
// background re-render would otherwise throw "called outside of a
|
|
242
|
+
// request context".
|
|
243
|
+
const freshSegments = await runWithRequestContext(requestCtx, () =>
|
|
244
|
+
Store.run(() =>
|
|
245
|
+
resolveAllSegments(
|
|
246
|
+
ctx.entries,
|
|
247
|
+
ctx.routeKey,
|
|
248
|
+
ctx.matched.params,
|
|
249
|
+
proactiveHandlerContext,
|
|
250
|
+
proactiveLoaderPromises,
|
|
251
|
+
{ skipLoaders: true },
|
|
252
|
+
),
|
|
242
253
|
),
|
|
243
254
|
);
|
|
244
255
|
|
|
245
256
|
let freshInterceptSegments: ResolvedSegment[] = [];
|
|
246
257
|
if (ctx.interceptResult) {
|
|
247
|
-
freshInterceptSegments = await
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
258
|
+
freshInterceptSegments = await runWithRequestContext(
|
|
259
|
+
requestCtx,
|
|
260
|
+
() =>
|
|
261
|
+
Store.run(() =>
|
|
262
|
+
resolveInterceptEntry(
|
|
263
|
+
ctx.interceptResult!.intercept,
|
|
264
|
+
ctx.interceptResult!.entry,
|
|
265
|
+
ctx.matched.params,
|
|
266
|
+
proactiveHandlerContext,
|
|
267
|
+
true, // belongsToRoute
|
|
268
|
+
// No revalidationContext = render fresh
|
|
269
|
+
undefined,
|
|
270
|
+
// Skip intercept middleware: the foreground already ran it
|
|
271
|
+
// before the response was sent. Re-running here (post-
|
|
272
|
+
// response, background) would fire side effects twice and a
|
|
273
|
+
// short-circuit Response would silently abort this write.
|
|
274
|
+
{ skipMiddleware: true },
|
|
275
|
+
),
|
|
276
|
+
),
|
|
262
277
|
);
|
|
263
278
|
}
|
|
264
279
|
|
|
@@ -201,17 +201,23 @@ export function buildMatchResult<TEnv>(
|
|
|
201
201
|
|
|
202
202
|
let allIds: string[];
|
|
203
203
|
let segmentsToRender: ResolvedSegment[];
|
|
204
|
+
let resolvedIds: string[];
|
|
204
205
|
|
|
205
206
|
if (ctx.isFullMatch) {
|
|
207
|
+
// One pass over allSegments: dedup by id (segmentsToRender + allIds) while
|
|
208
|
+
// collecting every id for resolvedIds, which keeps duplicates unlike allIds.
|
|
206
209
|
const seen = new Set<string>();
|
|
207
210
|
segmentsToRender = [];
|
|
211
|
+
allIds = [];
|
|
212
|
+
resolvedIds = [];
|
|
208
213
|
for (const s of allSegments) {
|
|
214
|
+
resolvedIds.push(s.id);
|
|
209
215
|
if (!seen.has(s.id)) {
|
|
210
216
|
seen.add(s.id);
|
|
211
217
|
segmentsToRender.push(s);
|
|
218
|
+
allIds.push(s.id);
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
|
-
allIds = segmentsToRender.map((s) => s.id);
|
|
215
221
|
} else {
|
|
216
222
|
allIds = ctx.interceptResult
|
|
217
223
|
? ctx.clientSegmentIds.length > 0
|
|
@@ -221,11 +227,21 @@ export function buildMatchResult<TEnv>(
|
|
|
221
227
|
|
|
222
228
|
allIds = [...new Set(allIds)];
|
|
223
229
|
|
|
230
|
+
// One pass over allSegments: keep renderable segments and collect the
|
|
231
|
+
// handler-ran ids (resolvedIds) together.
|
|
224
232
|
const clientIdSet = new Set(ctx.clientSegmentIds);
|
|
225
|
-
segmentsToRender =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
233
|
+
segmentsToRender = [];
|
|
234
|
+
resolvedIds = [];
|
|
235
|
+
for (const s of allSegments) {
|
|
236
|
+
if (s._handlerRan) resolvedIds.push(s.id);
|
|
237
|
+
if (
|
|
238
|
+
s.component !== null ||
|
|
239
|
+
s.type === "loader" ||
|
|
240
|
+
!clientIdSet.has(s.id)
|
|
241
|
+
) {
|
|
242
|
+
segmentsToRender.push(s);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
const { segments: dedupedSegments, removedIds } = deduplicateLoaderSegments(
|
|
@@ -236,20 +252,24 @@ export function buildMatchResult<TEnv>(
|
|
|
236
252
|
const matchedIds =
|
|
237
253
|
removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
|
|
238
254
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
if (s._handlerRan === undefined)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
255
|
+
// One pass over dedupedSegments: strip the internal _handlerRan marker and
|
|
256
|
+
// collect the diff ids (id is unchanged by the strip) together.
|
|
257
|
+
const cleanedSegments: ResolvedSegment[] = [];
|
|
258
|
+
const diff: string[] = [];
|
|
259
|
+
for (const s of dedupedSegments) {
|
|
260
|
+
if (s._handlerRan === undefined) {
|
|
261
|
+
cleanedSegments.push(s);
|
|
262
|
+
} else {
|
|
263
|
+
const { _handlerRan: _drop, ...rest } = s;
|
|
264
|
+
cleanedSegments.push(rest as ResolvedSegment);
|
|
265
|
+
}
|
|
266
|
+
diff.push(s.id);
|
|
267
|
+
}
|
|
248
268
|
|
|
249
269
|
return {
|
|
250
270
|
segments: cleanedSegments,
|
|
251
271
|
matched: matchedIds,
|
|
252
|
-
diff
|
|
272
|
+
diff,
|
|
253
273
|
resolvedIds,
|
|
254
274
|
params: ctx.matched.params,
|
|
255
275
|
routeName: ctx.routeKey,
|
package/src/router/middleware.ts
CHANGED
|
@@ -104,11 +104,20 @@ export function compileMiddlewarePattern(pattern: string): {
|
|
|
104
104
|
const segment = segments[i];
|
|
105
105
|
|
|
106
106
|
if (segment.type === "wildcard") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
if (segment.value === "*") {
|
|
108
|
+
// Bare `*`: optional subtree match, no capture (parity with the original
|
|
109
|
+
// middleware parser). A trailing `*` matches the subtree; a non-trailing
|
|
110
|
+
// `*` matches zero-or-more intermediate segments, so `/a/<star>/b` still
|
|
111
|
+
// matches `/a/b`.
|
|
112
|
+
regexStr += "(?:/.*)?";
|
|
113
|
+
} else {
|
|
114
|
+
// Named catch-all `:name+` / `:name*`: capture the remainder under the
|
|
115
|
+
// name so a scoping middleware sees `ctx.params.<name>`, and respect the
|
|
116
|
+
// one-or-more arity (`+` must not match the bare prefix), mirroring the
|
|
117
|
+
// route matcher instead of collapsing to the bare-`*` subtree.
|
|
118
|
+
paramNames.push(segment.value);
|
|
119
|
+
regexStr += segment.oneOrMore ? "/(.+)" : "(?:/(.*))?";
|
|
120
|
+
}
|
|
112
121
|
if (i === segments.length - 1) {
|
|
113
122
|
hasTrailingWildcard = true;
|
|
114
123
|
}
|
|
@@ -177,7 +186,15 @@ export function createMiddlewareContext<TEnv>(
|
|
|
177
186
|
search?: Record<string, unknown>,
|
|
178
187
|
) => string,
|
|
179
188
|
): MiddlewareContext<TEnv> {
|
|
180
|
-
|
|
189
|
+
// Reuse the request context's clean URL when available. Each middleware still
|
|
190
|
+
// gets its OWN URL clone (a middleware may mutate ctx.url / ctx.searchParams),
|
|
191
|
+
// but cloning the already-clean URL is cheaper than re-parsing + re-stripping
|
|
192
|
+
// request.url per middleware. Fall back to parsing when no ALS context.
|
|
193
|
+
const reqCtx = _getRequestContext();
|
|
194
|
+
const ctxUrl = reqCtx?.url;
|
|
195
|
+
const url = ctxUrl
|
|
196
|
+
? new URL(ctxUrl)
|
|
197
|
+
: stripInternalParams(new URL(request.url));
|
|
181
198
|
|
|
182
199
|
// Track the initial response to detect pre/post-next() phase.
|
|
183
200
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -208,12 +225,11 @@ export function createMiddlewareContext<TEnv>(
|
|
|
208
225
|
return responseHolder.response;
|
|
209
226
|
};
|
|
210
227
|
|
|
211
|
-
//
|
|
228
|
+
// reqCtx captured once above: the request-scoped platform fields
|
|
212
229
|
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
213
230
|
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
214
231
|
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
215
232
|
// during `await next()`.
|
|
216
|
-
const reqCtx = _getRequestContext();
|
|
217
233
|
return {
|
|
218
234
|
request,
|
|
219
235
|
url,
|
|
@@ -296,7 +312,10 @@ export function createMiddlewareContext<TEnv>(
|
|
|
296
312
|
const reqCtx = _getRequestContext();
|
|
297
313
|
if (reqCtx) {
|
|
298
314
|
reqCtx._debugPerformance = true;
|
|
299
|
-
reqCtx.
|
|
315
|
+
// Anchor to the true request entry (reqCtx._handlerStart) so phases that
|
|
316
|
+
// began before this opt-in report non-negative offsets; undefined falls
|
|
317
|
+
// back to performance.now() in createMetricsStore.
|
|
318
|
+
reqCtx._metricsStore ??= createMetricsStore(true, reqCtx._handlerStart);
|
|
300
319
|
}
|
|
301
320
|
},
|
|
302
321
|
};
|
|
@@ -534,22 +553,28 @@ export async function executeMiddleware<TEnv>(
|
|
|
534
553
|
// when neither surface is active.
|
|
535
554
|
let result: Response | void;
|
|
536
555
|
try {
|
|
537
|
-
result = await observePhase(PHASES.middleware(metricLabel), () =>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
556
|
+
result = await observePhase(PHASES.middleware(metricLabel), async () => {
|
|
557
|
+
try {
|
|
558
|
+
return await entry.handler(ctx, wrappedNext);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Thrown Response is short-circuit control flow, not an error —
|
|
561
|
+
// absorb it INSIDE the span so the tracing runner settles the
|
|
562
|
+
// rango.middleware span as success, not STATUS_ERROR (every auth
|
|
563
|
+
// redirect would otherwise inflate trace error rates). Returning it
|
|
564
|
+
// routes through the `if (result instanceof Response)` branch below,
|
|
565
|
+
// so stub headers and request-context cookies merge identically to an
|
|
566
|
+
// explicit `return new Response(...)`. Segment handlers already follow
|
|
567
|
+
// this convention (segment-resolution/helpers.ts keeps result handling
|
|
568
|
+
// outside the span). Real errors propagate past the span.
|
|
569
|
+
if (error instanceof Response) return error;
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
// Settle the middleware own-time metric once on both the success and
|
|
575
|
+
// error paths (idempotent guard in finishMiddleware).
|
|
576
|
+
finishMiddleware();
|
|
551
577
|
}
|
|
552
|
-
finishMiddleware();
|
|
553
578
|
|
|
554
579
|
// Record post-next() processing time when middleware did work after
|
|
555
580
|
// the downstream chain resolved (e.g. adding headers, logging).
|
|
@@ -709,20 +734,21 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
709
734
|
ordinal,
|
|
710
735
|
);
|
|
711
736
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
737
|
+
const result: Response | void = await observePhase(
|
|
738
|
+
PHASES.middleware(label),
|
|
739
|
+
async () => {
|
|
740
|
+
try {
|
|
741
|
+
return await middleware(ctx, guardedNext);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
744
|
+
// explicit-return path below. Absorb it INSIDE the span so the tracing
|
|
745
|
+
// runner settles rango.middleware as success, not STATUS_ERROR (same
|
|
746
|
+
// reasoning as executeMiddleware's main chain). Real errors propagate.
|
|
747
|
+
if (error instanceof Response) return error;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
);
|
|
726
752
|
|
|
727
753
|
if (result instanceof Response) {
|
|
728
754
|
earlyResponse = result;
|
|
@@ -25,15 +25,17 @@ export interface NavigationSnapshot {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface ResolveNavigationDeps {
|
|
28
|
-
findMatch: (
|
|
28
|
+
findMatch: (
|
|
29
|
+
pathname: string,
|
|
30
|
+
) => RouteMatchResult | null | Promise<RouteMatchResult | null>;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export function resolveNavigation(
|
|
33
|
+
export async function resolveNavigation(
|
|
32
34
|
request: Request,
|
|
33
35
|
url: URL,
|
|
34
36
|
currentRouteKey: string,
|
|
35
37
|
deps: ResolveNavigationDeps,
|
|
36
|
-
): NavigationSnapshot | null {
|
|
38
|
+
): Promise<NavigationSnapshot | null> {
|
|
37
39
|
const clientSegmentIds =
|
|
38
40
|
url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
|
|
39
41
|
const stale = url.searchParams.get("_rsc_stale") === "true";
|
|
@@ -65,10 +67,10 @@ export function resolveNavigation(
|
|
|
65
67
|
interceptContextUrl = prevUrl;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
const prevMatch = deps.findMatch(prevUrl.pathname);
|
|
70
|
+
const prevMatch = await deps.findMatch(prevUrl.pathname);
|
|
69
71
|
const prevParams = prevMatch?.params || {};
|
|
70
72
|
const interceptContextMatch = interceptSourceUrl
|
|
71
|
-
? deps.findMatch(interceptContextUrl.pathname)
|
|
73
|
+
? await deps.findMatch(interceptContextUrl.pathname)
|
|
72
74
|
: prevMatch;
|
|
73
75
|
|
|
74
76
|
const isSameRouteNavigation = !!(
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route pattern parsing (grammar only).
|
|
3
|
+
*
|
|
4
|
+
* Deliberately dependency-free so it is safe to bundle into the CLIENT — the
|
|
5
|
+
* reverse helper (`substitute-pattern-params.ts` -> `use-reverse`) needs it, and
|
|
6
|
+
* pulling it from `pattern-matching.ts` would drag that module's server-only
|
|
7
|
+
* transitive imports (`node:async_hooks` via `logging.ts`) into the browser.
|
|
8
|
+
* `pattern-matching.ts` re-exports these so existing importers are unaffected.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parsed segment info
|
|
13
|
+
*/
|
|
14
|
+
export interface ParsedSegment {
|
|
15
|
+
type: "static" | "param" | "wildcard";
|
|
16
|
+
value: string; // static text, param name, or "*"
|
|
17
|
+
optional: boolean;
|
|
18
|
+
constraint?: string[]; // enum values like ["en", "gb"]
|
|
19
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
20
|
+
/**
|
|
21
|
+
* Named catch-all repeat modifier. On a `wildcard` segment whose `value` is a
|
|
22
|
+
* param name (`:name+` / `:name*`), `true` marks one-or-more (`+`, rejects the
|
|
23
|
+
* zero-segment case); absent/false marks zero-or-more (`*`, and the bare `/*`).
|
|
24
|
+
*/
|
|
25
|
+
oneOrMore?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a route pattern into segments
|
|
30
|
+
*
|
|
31
|
+
* Supports:
|
|
32
|
+
* - Static: /blog, /about
|
|
33
|
+
* - Params: /:slug, /:id
|
|
34
|
+
* - Optional: /:locale?, /:page?
|
|
35
|
+
* - Constrained: /:locale(en|gb), /:type(post|page)
|
|
36
|
+
* - Optional + Constrained: /:locale(en|gb)?
|
|
37
|
+
* - Wildcard: /*
|
|
38
|
+
* - Named catch-all: /:slug* (zero-or-more), /:path+ (one-or-more)
|
|
39
|
+
*/
|
|
40
|
+
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
41
|
+
const segments: ParsedSegment[] = [];
|
|
42
|
+
// The `([+*])?` group peels a trailing `+`/`*` off a `:name` BEFORE the
|
|
43
|
+
// literal-suffix group `([^/]*)` so it can be inspected. Whether it is a
|
|
44
|
+
// catch-all MODIFIER or a literal suffix character is decided below — a bare
|
|
45
|
+
// trailing `+`/`*` is the named catch-all of issue #634; any other combination
|
|
46
|
+
// is folded back into the literal suffix so previously-valid patterns are
|
|
47
|
+
// unaffected. It sits after `(\?)?` so `:name?*` is seen as `?` + suffix `*`.
|
|
48
|
+
const segmentRegex =
|
|
49
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([+*])?([^/]*)|(\*)|([^/]+))/g;
|
|
50
|
+
|
|
51
|
+
let match;
|
|
52
|
+
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
53
|
+
const [
|
|
54
|
+
,
|
|
55
|
+
,
|
|
56
|
+
paramName,
|
|
57
|
+
,
|
|
58
|
+
constraint,
|
|
59
|
+
optional,
|
|
60
|
+
repeat,
|
|
61
|
+
suffix,
|
|
62
|
+
wildcard,
|
|
63
|
+
staticText,
|
|
64
|
+
] = match;
|
|
65
|
+
|
|
66
|
+
if (wildcard) {
|
|
67
|
+
// Bare `/*`: zero-or-more, captured under "*".
|
|
68
|
+
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
69
|
+
} else if (paramName) {
|
|
70
|
+
// A trailing `+`/`*` is a named catch-all ONLY when it stands alone on the
|
|
71
|
+
// param — no `?`, no constraint, no literal suffix after it. In any other
|
|
72
|
+
// combination it is the start of a literal suffix, exactly as before this
|
|
73
|
+
// feature existed, so `:version+build` still matches `/…/v1+build` and
|
|
74
|
+
// never throws at registration.
|
|
75
|
+
if (repeat && !suffix && optional !== "?" && !constraint) {
|
|
76
|
+
segments.push({
|
|
77
|
+
type: "wildcard",
|
|
78
|
+
value: paramName,
|
|
79
|
+
optional: false,
|
|
80
|
+
oneOrMore: repeat === "+",
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
segments.push({
|
|
84
|
+
type: "param",
|
|
85
|
+
value: paramName,
|
|
86
|
+
optional: optional === "?",
|
|
87
|
+
constraint: constraint ? constraint.split("|") : undefined,
|
|
88
|
+
// Fold a non-modifier `+`/`*` back into the literal suffix.
|
|
89
|
+
suffix: (repeat ?? "") + (suffix ?? "") || undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} else if (staticText) {
|
|
93
|
+
segments.push({ type: "static", value: staticText, optional: false });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A named catch-all consumes the remainder, so it only makes sense as the final
|
|
98
|
+
// segment. If it isn't last, it isn't really a catch-all: restore the literal
|
|
99
|
+
// parse (`:name` + literal `+`/`*` suffix) rather than error, so a pattern like
|
|
100
|
+
// `/docs/:slug+/edit` keeps its pre-feature behavior (matches `/docs/x+/edit`).
|
|
101
|
+
// Bare `/*` keeps its historical mid-pattern leniency and is left untouched.
|
|
102
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
103
|
+
const s = segments[i];
|
|
104
|
+
if (s.type === "wildcard" && s.value !== "*") {
|
|
105
|
+
segments[i] = {
|
|
106
|
+
type: "param",
|
|
107
|
+
value: s.value,
|
|
108
|
+
optional: false,
|
|
109
|
+
suffix: s.oneOrMore ? "+" : "*",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return segments;
|
|
115
|
+
}
|
|
@@ -9,65 +9,13 @@ import type { EntryData } from "../server/context";
|
|
|
9
9
|
import { debugLog, isRouterDebugEnabled } from "./logging.js";
|
|
10
10
|
import { escapeRegExp } from "../regex-escape.js";
|
|
11
11
|
import { safeDecodeURIComponent } from "./url-params.js";
|
|
12
|
+
import { parsePattern, type ParsedSegment } from "./parse-pattern.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
value: string; // static text, param name, or "*"
|
|
19
|
-
optional: boolean;
|
|
20
|
-
constraint?: string[]; // enum values like ["en", "gb"]
|
|
21
|
-
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Parse a route pattern into segments
|
|
26
|
-
*
|
|
27
|
-
* Supports:
|
|
28
|
-
* - Static: /blog, /about
|
|
29
|
-
* - Params: /:slug, /:id
|
|
30
|
-
* - Optional: /:locale?, /:page?
|
|
31
|
-
* - Constrained: /:locale(en|gb), /:type(post|page)
|
|
32
|
-
* - Optional + Constrained: /:locale(en|gb)?
|
|
33
|
-
* - Wildcard: /*
|
|
34
|
-
*/
|
|
35
|
-
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
36
|
-
const segments: ParsedSegment[] = [];
|
|
37
|
-
const segmentRegex =
|
|
38
|
-
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
39
|
-
|
|
40
|
-
let match;
|
|
41
|
-
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
42
|
-
const [
|
|
43
|
-
,
|
|
44
|
-
,
|
|
45
|
-
paramName,
|
|
46
|
-
,
|
|
47
|
-
constraint,
|
|
48
|
-
optional,
|
|
49
|
-
suffix,
|
|
50
|
-
wildcard,
|
|
51
|
-
staticText,
|
|
52
|
-
] = match;
|
|
53
|
-
|
|
54
|
-
if (wildcard) {
|
|
55
|
-
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
56
|
-
} else if (paramName) {
|
|
57
|
-
segments.push({
|
|
58
|
-
type: "param",
|
|
59
|
-
value: paramName,
|
|
60
|
-
optional: optional === "?",
|
|
61
|
-
constraint: constraint ? constraint.split("|") : undefined,
|
|
62
|
-
suffix: suffix || undefined,
|
|
63
|
-
});
|
|
64
|
-
} else if (staticText) {
|
|
65
|
-
segments.push({ type: "static", value: staticText, optional: false });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return segments;
|
|
70
|
-
}
|
|
14
|
+
// `parsePattern`/`ParsedSegment` live in the dependency-free `parse-pattern.ts`
|
|
15
|
+
// so the client reverse helper can import them without dragging this module's
|
|
16
|
+
// server-only deps into the browser bundle. Re-exported here for existing
|
|
17
|
+
// importers (build/route-trie, middleware, tests).
|
|
18
|
+
export { parsePattern, type ParsedSegment };
|
|
71
19
|
|
|
72
20
|
/**
|
|
73
21
|
* Compiled pattern result containing regex, param metadata, and trailing slash info.
|
|
@@ -83,6 +31,14 @@ export interface CompiledPattern {
|
|
|
83
31
|
* path's behavior (trie-matching.ts:validateAndBuild).
|
|
84
32
|
*/
|
|
85
33
|
constraints?: Record<string, string[]>;
|
|
34
|
+
/**
|
|
35
|
+
* The pattern's catch-all param, if any (`*` for bare `/*`, the name for a
|
|
36
|
+
* named `:name+`/`:name*`). A zero-or-more catch-all (`oneOrMore: false`)
|
|
37
|
+
* whose optional group is absent binds "" rather than being omitted — so
|
|
38
|
+
* `/docs` matches `/docs/:slug*` with `slug === ""`. `oneOrMore` keeps the
|
|
39
|
+
* same polarity as `ParsedSegment.oneOrMore` and the trie's `w1`.
|
|
40
|
+
*/
|
|
41
|
+
catchAll?: { name: string; oneOrMore: boolean };
|
|
86
42
|
}
|
|
87
43
|
|
|
88
44
|
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
@@ -143,13 +99,32 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
143
99
|
const segments = parsePattern(normalizedPattern);
|
|
144
100
|
const paramNames: string[] = [];
|
|
145
101
|
let constraints: Record<string, string[]> | undefined;
|
|
102
|
+
let catchAll: { name: string; oneOrMore: boolean } | undefined;
|
|
146
103
|
|
|
147
104
|
let regexPattern = "";
|
|
148
105
|
|
|
149
106
|
for (const segment of segments) {
|
|
150
107
|
if (segment.type === "wildcard") {
|
|
151
|
-
|
|
152
|
-
|
|
108
|
+
// Wildcards capture the remainder under `segment.value` ("*" for the bare
|
|
109
|
+
// form, the param name for a named catch-all).
|
|
110
|
+
paramNames.push(segment.value);
|
|
111
|
+
catchAll = { name: segment.value, oneOrMore: Boolean(segment.oneOrMore) };
|
|
112
|
+
if (segment.oneOrMore) {
|
|
113
|
+
// `:name+` — one-or-more, rejects the zero-segment (bare-prefix) case.
|
|
114
|
+
regexPattern += "/(.+)";
|
|
115
|
+
} else {
|
|
116
|
+
// Zero-or-more catch-all: named `:name*` OR the bare `/*` (both parse to
|
|
117
|
+
// `oneOrMore: false`). The whole `/segment` is optional so the bare
|
|
118
|
+
// prefix matches directly, aligning the regex fallback with the trie
|
|
119
|
+
// (which already matches the bare prefix binding "" — trie-matching.ts);
|
|
120
|
+
// buildParamsFromMatch binds "" when the optional group is absent.
|
|
121
|
+
//
|
|
122
|
+
// The bare `/*` previously used a required `/(.*)`, so `/files/*` failed
|
|
123
|
+
// to match `/files` and fell through to trailing-slash normalization,
|
|
124
|
+
// emitting a corrupt `/file` redirect instead of a match (issue #636,
|
|
125
|
+
// parity row C1). It is the same alignment #635 made for named `:name*`.
|
|
126
|
+
regexPattern += "(?:/(.*))?";
|
|
127
|
+
}
|
|
153
128
|
} else if (segment.type === "param") {
|
|
154
129
|
paramNames.push(segment.value);
|
|
155
130
|
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
@@ -202,6 +177,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
202
177
|
paramNames,
|
|
203
178
|
hasTrailingSlash,
|
|
204
179
|
...(constraints ? { constraints } : {}),
|
|
180
|
+
...(catchAll ? { catchAll } : {}),
|
|
205
181
|
};
|
|
206
182
|
}
|
|
207
183
|
|
|
@@ -236,16 +212,29 @@ function satisfiesConstraints(
|
|
|
236
212
|
* keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
|
|
237
213
|
* keeps the runtime aligned with the `ExtractParams` type and matches the
|
|
238
214
|
* trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
|
|
215
|
+
*
|
|
216
|
+
* A zero-or-more catch-all (`compiled.catchAll`, `oneOrMore: false`) whose
|
|
217
|
+
* optional group didn't capture binds "" instead of being omitted, so `/docs`
|
|
218
|
+
* matches `/docs/:slug*` with `slug === ""`. Exported so the `renderRoute`
|
|
219
|
+
* testing harness (`matchLeaf`) shares this exact logic instead of forking it.
|
|
239
220
|
*/
|
|
240
|
-
function buildParamsFromMatch(
|
|
221
|
+
export function buildParamsFromMatch(
|
|
241
222
|
match: RegExpExecArray,
|
|
242
223
|
paramNames: string[],
|
|
224
|
+
catchAll?: { name: string; oneOrMore: boolean },
|
|
243
225
|
): Record<string, string> {
|
|
244
226
|
const params: Record<string, string> = {};
|
|
245
227
|
paramNames.forEach((name, index) => {
|
|
246
228
|
const captured = match[index + 1];
|
|
247
229
|
if (captured !== undefined) {
|
|
230
|
+
// A catch-all remainder decodes identically whether split-per-segment or
|
|
231
|
+
// whole-string (a literal `/` never lives inside a `%XX` escape), so a
|
|
232
|
+
// single decode is correct and cheapest.
|
|
248
233
|
params[name] = safeDecodeURIComponent(captured);
|
|
234
|
+
} else if (catchAll && name === catchAll.name && !catchAll.oneOrMore) {
|
|
235
|
+
// A zero-or-more catch-all (`:name*` or the bare `/*`) whose optional
|
|
236
|
+
// group was absent binds "" rather than being omitted.
|
|
237
|
+
params[name] = "";
|
|
249
238
|
}
|
|
250
239
|
});
|
|
251
240
|
return params;
|
|
@@ -454,7 +443,7 @@ export function findMatch<TEnv>(
|
|
|
454
443
|
fullPattern = entry.prefix + pattern;
|
|
455
444
|
}
|
|
456
445
|
|
|
457
|
-
const { regex, paramNames, hasTrailingSlash, constraints } =
|
|
446
|
+
const { regex, paramNames, hasTrailingSlash, constraints, catchAll } =
|
|
458
447
|
getCompiledPattern(fullPattern);
|
|
459
448
|
|
|
460
449
|
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
@@ -469,7 +458,7 @@ export function findMatch<TEnv>(
|
|
|
469
458
|
|
|
470
459
|
const match = regex.exec(pathname);
|
|
471
460
|
if (match) {
|
|
472
|
-
const params = buildParamsFromMatch(match, paramNames);
|
|
461
|
+
const params = buildParamsFromMatch(match, paramNames, catchAll);
|
|
473
462
|
|
|
474
463
|
if (!satisfiesConstraints(params, constraints)) {
|
|
475
464
|
continue;
|
|
@@ -518,7 +507,7 @@ export function findMatch<TEnv>(
|
|
|
518
507
|
|
|
519
508
|
const altMatch = regex.exec(alternatePathname);
|
|
520
509
|
if (altMatch) {
|
|
521
|
-
const params = buildParamsFromMatch(altMatch, paramNames);
|
|
510
|
+
const params = buildParamsFromMatch(altMatch, paramNames, catchAll);
|
|
522
511
|
|
|
523
512
|
if (!satisfiesConstraints(params, constraints)) {
|
|
524
513
|
continue;
|