@rangojs/router 0.0.0-experimental.8678bb02 → 0.0.0-experimental.87
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 +126 -38
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +847 -384
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +5 -5
- package/skills/breadcrumbs/SKILL.md +3 -1
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +28 -20
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +35 -2
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +59 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/rango/SKILL.md +24 -22
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +24 -0
- package/skills/router-setup/SKILL.md +35 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +3 -1
- package/src/__internal.ts +1 -1
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +87 -6
- package/src/browser/navigation-client.ts +128 -77
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/partial-update.ts +60 -7
- package/src/browser/prefetch/cache.ts +129 -21
- package/src/browser/prefetch/fetch.ts +156 -18
- package/src/browser/prefetch/queue.ts +36 -5
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +72 -8
- package/src/browser/react/NavigationProvider.tsx +57 -11
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +60 -9
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/server-action-bridge.ts +8 -18
- package/src/browser/types.ts +33 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +50 -24
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cf/cf-cache-store.ts +5 -7
- package/src/client.tsx +84 -230
- package/src/deps/browser.ts +0 -1
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +6 -1
- package/src/index.ts +49 -6
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +210 -35
- package/src/route-definition/helpers-types.ts +61 -14
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +70 -17
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/lazy-includes.ts +6 -6
- package/src/router/loader-resolution.ts +153 -21
- package/src/router/manifest.ts +22 -13
- package/src/router/match-api.ts +127 -192
- package/src/router/match-middleware/cache-lookup.ts +28 -8
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +82 -4
- package/src/router/middleware-types.ts +2 -28
- package/src/router/middleware.ts +32 -7
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +60 -9
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +70 -5
- package/src/router/segment-resolution/revalidation.ts +87 -9
- package/src/router/trie-matching.ts +10 -4
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +54 -7
- package/src/rsc/handler.ts +478 -399
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +14 -3
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +15 -2
- package/src/rsc/server-action.ts +10 -2
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +6 -4
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +11 -61
- package/src/server/context.ts +65 -5
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +142 -55
- package/src/ssr/index.tsx +3 -0
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +17 -43
- package/src/types/loader-types.ts +37 -11
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-entry.ts +12 -1
- package/src/types/segments.ts +1 -1
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -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-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +64 -206
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +40 -18
- package/src/vite/router-discovery.ts +237 -37
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/package-resolution.ts +1 -1
- package/src/vite/utils/prerender-utils.ts +37 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/src/browser/debug-channel.ts +0 -93
package/src/route-types.ts
CHANGED
|
@@ -176,6 +176,13 @@ export type IncludeItem = {
|
|
|
176
176
|
>;
|
|
177
177
|
/** Root scope flag for dot-local reverse resolution */
|
|
178
178
|
rootScoped?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Positional include scope token composed from the parent scope plus this
|
|
181
|
+
* include's sibling index (`${parentScope}I${idx}`). Applied to direct-
|
|
182
|
+
* descendant shortCodes during lazy evaluation so routes inside the
|
|
183
|
+
* include cannot collide with siblings declared outside it.
|
|
184
|
+
*/
|
|
185
|
+
includeScope?: string;
|
|
179
186
|
};
|
|
180
187
|
[IncludeBrand]: void;
|
|
181
188
|
};
|
|
@@ -257,3 +264,14 @@ export type LoaderUseItem = RevalidateItem | CacheItem;
|
|
|
257
264
|
* runtime via .flat(3).
|
|
258
265
|
*/
|
|
259
266
|
export type UseItems<T> = (T | readonly T[])[];
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Union of all items that handler.use() may return.
|
|
270
|
+
* A handler doesn't know its mount site at definition time, so the type
|
|
271
|
+
* is intentionally broad — validation happens per-mount-site at runtime.
|
|
272
|
+
*/
|
|
273
|
+
export type HandlerUseItem =
|
|
274
|
+
| RouteUseItem
|
|
275
|
+
| LayoutUseItem
|
|
276
|
+
| ParallelUseItem
|
|
277
|
+
| InterceptUseItem;
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
* Content Negotiation Utilities
|
|
3
3
|
*
|
|
4
4
|
* Pure functions for HTTP Accept header parsing and response type matching.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by previewMatch and classifyRequest for content negotiation between
|
|
6
6
|
* RSC routes and response routes (JSON, text, image, stream, etc.).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { EntryData } from "../server/context.js";
|
|
10
|
+
import type { CollectedMiddleware } from "./middleware-types.js";
|
|
11
|
+
import { collectRouteMiddleware } from "./middleware.js";
|
|
12
|
+
import { loadManifest } from "./manifest.js";
|
|
13
|
+
import { traverseBack } from "./pattern-matching.js";
|
|
14
|
+
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
15
|
+
import type { RouteSnapshot } from "./route-snapshot.js";
|
|
16
|
+
|
|
9
17
|
// Response type -> MIME type used for Accept header matching
|
|
10
18
|
export const RESPONSE_TYPE_MIME: Record<string, string> = {
|
|
11
19
|
json: "application/json",
|
|
@@ -114,3 +122,94 @@ export function pickNegotiateVariant(
|
|
|
114
122
|
// No match -- use first candidate as default
|
|
115
123
|
return candidates[0]!;
|
|
116
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Result of content negotiation for a route with negotiate variants.
|
|
128
|
+
*/
|
|
129
|
+
export interface NegotiationResult {
|
|
130
|
+
/** The winning response type */
|
|
131
|
+
responseType: string;
|
|
132
|
+
/** Handler function for the winning variant */
|
|
133
|
+
handler: Function;
|
|
134
|
+
/** Manifest entry for the winning variant (may differ from primary) */
|
|
135
|
+
manifestEntry: EntryData;
|
|
136
|
+
/** Route middleware for the winning variant */
|
|
137
|
+
routeMiddleware: CollectedMiddleware[];
|
|
138
|
+
/** Always true — negotiation occurred */
|
|
139
|
+
negotiated: true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Perform content negotiation for a route with negotiate variants.
|
|
144
|
+
*
|
|
145
|
+
* Returns a NegotiationResult when a response route wins negotiation.
|
|
146
|
+
* Returns null when RSC wins or no negotiation is needed.
|
|
147
|
+
*
|
|
148
|
+
* Shared by previewMatch and classifyRequest to avoid duplicating
|
|
149
|
+
* the candidate-building and variant-loading logic.
|
|
150
|
+
*/
|
|
151
|
+
export async function negotiateRoute(
|
|
152
|
+
request: Request,
|
|
153
|
+
pathname: string,
|
|
154
|
+
snapshot: RouteSnapshot,
|
|
155
|
+
): Promise<NegotiationResult | null> {
|
|
156
|
+
const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
|
|
157
|
+
if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
|
|
162
|
+
|
|
163
|
+
// Build candidate list preserving definition order.
|
|
164
|
+
const variants = matched.negotiateVariants;
|
|
165
|
+
let candidates: Array<{ routeKey: string; responseType: string }>;
|
|
166
|
+
if (responseType) {
|
|
167
|
+
candidates = [...variants, { routeKey: matched.routeKey, responseType }];
|
|
168
|
+
} else {
|
|
169
|
+
const rscCandidate = {
|
|
170
|
+
routeKey: matched.routeKey,
|
|
171
|
+
responseType: RSC_RESPONSE_TYPE,
|
|
172
|
+
};
|
|
173
|
+
candidates = matched.rscFirst
|
|
174
|
+
? [rscCandidate, ...variants]
|
|
175
|
+
: [...variants, rscCandidate];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const variant = pickNegotiateVariant(acceptEntries, candidates);
|
|
179
|
+
|
|
180
|
+
// RSC won negotiation
|
|
181
|
+
if (variant.responseType === RSC_RESPONSE_TYPE) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Primary response-type won — use existing manifest entry and middleware
|
|
186
|
+
if (responseType && variant.routeKey === matched.routeKey) {
|
|
187
|
+
return {
|
|
188
|
+
responseType,
|
|
189
|
+
handler: manifestEntry.handler as Function,
|
|
190
|
+
manifestEntry,
|
|
191
|
+
routeMiddleware,
|
|
192
|
+
negotiated: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Different variant won — load its manifest entry
|
|
197
|
+
const negotiateEntry = await loadManifest(
|
|
198
|
+
matched.entry,
|
|
199
|
+
variant.routeKey,
|
|
200
|
+
pathname,
|
|
201
|
+
undefined,
|
|
202
|
+
false,
|
|
203
|
+
);
|
|
204
|
+
const variantMiddleware = collectRouteMiddleware(
|
|
205
|
+
traverseBack(negotiateEntry),
|
|
206
|
+
matched.params,
|
|
207
|
+
);
|
|
208
|
+
return {
|
|
209
|
+
responseType: variant.responseType,
|
|
210
|
+
handler: negotiateEntry.handler as Function,
|
|
211
|
+
manifestEntry: negotiateEntry,
|
|
212
|
+
routeMiddleware: variantMiddleware,
|
|
213
|
+
negotiated: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -18,6 +18,8 @@ import { isInsideCacheScope } from "../server/context.js";
|
|
|
18
18
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
19
19
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
20
20
|
import { PRERENDER_PASSTHROUGH } from "../prerender.js";
|
|
21
|
+
import { encodePathSegment } from "./url-params.js";
|
|
22
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Strip internal _rsc* query params from a URL.
|
|
@@ -114,9 +116,9 @@ function createPrerenderPassthroughFn(
|
|
|
114
116
|
}
|
|
115
117
|
if (!isPassthroughRoute) {
|
|
116
118
|
throw new Error(
|
|
117
|
-
"ctx.passthrough() is only available on routes
|
|
118
|
-
"
|
|
119
|
-
"
|
|
119
|
+
"ctx.passthrough() is only available on routes wrapped with " +
|
|
120
|
+
"Passthrough(). Remove the passthrough() call or wrap the " +
|
|
121
|
+
"Prerender definition with Passthrough(prerenderDef, liveHandler).",
|
|
120
122
|
);
|
|
121
123
|
}
|
|
122
124
|
return PRERENDER_PASSTHROUGH;
|
|
@@ -166,17 +168,42 @@ export function createReverseFunction(
|
|
|
166
168
|
: hrefParams;
|
|
167
169
|
|
|
168
170
|
// Substitute params (strip constraint and optional syntax: :param(a|b)? -> value)
|
|
171
|
+
// Optional params (:param?) are omitted when not provided
|
|
169
172
|
if (effectiveParams) {
|
|
173
|
+
let hadOmittedOptional = false;
|
|
174
|
+
// First pass: optional params (trailing ?)
|
|
170
175
|
result = result.replace(
|
|
171
|
-
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))
|
|
176
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
177
|
+
(_, key) => {
|
|
178
|
+
const value = effectiveParams[key];
|
|
179
|
+
// Empty string is treated as omitted — the trie matcher fills
|
|
180
|
+
// unmatched optional params with "" (not undefined), so reverse
|
|
181
|
+
// must collapse those segments instead of leaving empty slots.
|
|
182
|
+
if (value === undefined || value === "") {
|
|
183
|
+
hadOmittedOptional = true;
|
|
184
|
+
return "";
|
|
185
|
+
}
|
|
186
|
+
return encodePathSegment(value);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
// Second pass: required params (no trailing ?)
|
|
190
|
+
result = result.replace(
|
|
191
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
172
192
|
(_, key) => {
|
|
173
193
|
const value = effectiveParams[key];
|
|
174
194
|
if (value === undefined) {
|
|
175
195
|
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
176
196
|
}
|
|
177
|
-
return
|
|
197
|
+
return encodePathSegment(value);
|
|
178
198
|
},
|
|
179
199
|
);
|
|
200
|
+
// Clean up slashes only when an optional param was actually omitted,
|
|
201
|
+
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
202
|
+
if (hadOmittedOptional) {
|
|
203
|
+
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
204
|
+
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
205
|
+
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
206
|
+
}
|
|
180
207
|
}
|
|
181
208
|
|
|
182
209
|
// Append search params as query string
|
|
@@ -207,7 +234,7 @@ export function createHandlerContext<TEnv>(
|
|
|
207
234
|
// Get variables from request context - this is the unified context
|
|
208
235
|
// shared between middleware and route handlers
|
|
209
236
|
const requestContext = _getRequestContext();
|
|
210
|
-
const variables: any = requestContext?.
|
|
237
|
+
const variables: any = requestContext?._variables ?? {};
|
|
211
238
|
|
|
212
239
|
// If route has a search schema, parse URLSearchParams into typed object
|
|
213
240
|
const searchSchema = routeName ? getSearchSchema(routeName) : undefined;
|
|
@@ -250,14 +277,19 @@ export function createHandlerContext<TEnv>(
|
|
|
250
277
|
ctx = {
|
|
251
278
|
params,
|
|
252
279
|
build: false,
|
|
280
|
+
dev: false,
|
|
253
281
|
request,
|
|
254
282
|
searchParams,
|
|
255
283
|
search: searchSchema ? resolvedSearchParams : {},
|
|
256
284
|
pathname,
|
|
257
285
|
url,
|
|
258
|
-
originalUrl: new URL(request.url),
|
|
286
|
+
originalUrl: requestContext?.originalUrl ?? new URL(request.url),
|
|
259
287
|
env: bindings,
|
|
260
|
-
|
|
288
|
+
waitUntil: requestContext
|
|
289
|
+
? requestContext.waitUntil.bind(requestContext)
|
|
290
|
+
: fireAndForgetWaitUntil,
|
|
291
|
+
executionContext: requestContext?.executionContext,
|
|
292
|
+
_variables: variables,
|
|
261
293
|
get: ((keyOrVar: any) => {
|
|
262
294
|
// Read-time guard: non-cacheable var inside cache() → throw.
|
|
263
295
|
// Works for both ContextVar tokens and string keys.
|
|
@@ -320,7 +352,7 @@ export function createHandlerContext<TEnv>(
|
|
|
320
352
|
*
|
|
321
353
|
* Returns an InternalHandlerContext where params, pathname, url, searchParams,
|
|
322
354
|
* search, reverse, and use(handle) work. Request-time properties
|
|
323
|
-
* (request, env, headers, cookies,
|
|
355
|
+
* (request, env, headers, cookies, get, set, res) throw with a clear error.
|
|
324
356
|
*/
|
|
325
357
|
export function createPrerenderContext<TEnv>(
|
|
326
358
|
params: Record<string, string>,
|
|
@@ -329,6 +361,8 @@ export function createPrerenderContext<TEnv>(
|
|
|
329
361
|
routeName?: string,
|
|
330
362
|
buildVars?: Record<string, any>,
|
|
331
363
|
isPassthroughRoute?: boolean,
|
|
364
|
+
buildEnv?: TEnv,
|
|
365
|
+
devMode?: boolean,
|
|
332
366
|
): InternalHandlerContext<any, TEnv> {
|
|
333
367
|
const syntheticUrl = new URL(`http://prerender${pathname}`);
|
|
334
368
|
const variables = buildVars ?? {};
|
|
@@ -343,6 +377,7 @@ export function createPrerenderContext<TEnv>(
|
|
|
343
377
|
return {
|
|
344
378
|
params,
|
|
345
379
|
build: true,
|
|
380
|
+
dev: devMode ?? false,
|
|
346
381
|
get request(): Request {
|
|
347
382
|
return throwUnavailable("request");
|
|
348
383
|
},
|
|
@@ -352,11 +387,19 @@ export function createPrerenderContext<TEnv>(
|
|
|
352
387
|
url: syntheticUrl,
|
|
353
388
|
originalUrl: syntheticUrl,
|
|
354
389
|
get env(): TEnv {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
390
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
391
|
+
throw new Error(
|
|
392
|
+
"ctx.env is not available during pre-rendering. " +
|
|
393
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
394
|
+
);
|
|
359
395
|
},
|
|
396
|
+
// Build-time prerender has no live request. waitUntil is a true no-op
|
|
397
|
+
// (running fn() here would fire side effects during build, which is
|
|
398
|
+
// incorrect — these are meant to outlive the live response).
|
|
399
|
+
// executionContext is absent for the same reason.
|
|
400
|
+
waitUntil: () => {},
|
|
401
|
+
executionContext: undefined,
|
|
402
|
+
_variables: variables,
|
|
360
403
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
361
404
|
set: ((keyOrVar: any, value: any) => {
|
|
362
405
|
contextSet(variables, keyOrVar, value);
|
|
@@ -402,6 +445,8 @@ export function createPrerenderContext<TEnv>(
|
|
|
402
445
|
export function createStaticContext<TEnv>(
|
|
403
446
|
routeMap: Record<string, string>,
|
|
404
447
|
routeName?: string,
|
|
448
|
+
buildEnv?: TEnv,
|
|
449
|
+
devMode?: boolean,
|
|
405
450
|
): InternalHandlerContext<any, TEnv> {
|
|
406
451
|
const variables: Record<string, any> = {};
|
|
407
452
|
|
|
@@ -417,6 +462,7 @@ export function createStaticContext<TEnv>(
|
|
|
417
462
|
return throwUnavailable("params");
|
|
418
463
|
},
|
|
419
464
|
build: true,
|
|
465
|
+
dev: devMode ?? false,
|
|
420
466
|
get request(): Request {
|
|
421
467
|
return throwUnavailable("request");
|
|
422
468
|
},
|
|
@@ -436,11 +482,18 @@ export function createStaticContext<TEnv>(
|
|
|
436
482
|
return throwUnavailable("originalUrl");
|
|
437
483
|
},
|
|
438
484
|
get env(): TEnv {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
485
|
+
if (buildEnv !== undefined) return buildEnv;
|
|
486
|
+
throw new Error(
|
|
487
|
+
"ctx.env is not available in Static() handlers. " +
|
|
488
|
+
"Configure buildEnv in your rango() plugin options to enable build-time env access.",
|
|
489
|
+
);
|
|
443
490
|
},
|
|
491
|
+
// Static() handlers have no live request. waitUntil is a true no-op
|
|
492
|
+
// (running fn() here would fire side effects during build, which is
|
|
493
|
+
// incorrect). executionContext is absent for the same reason.
|
|
494
|
+
waitUntil: () => {},
|
|
495
|
+
executionContext: undefined,
|
|
496
|
+
_variables: variables,
|
|
444
497
|
get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
|
|
445
498
|
set: ((keyOrVar: any, value: any) => {
|
|
446
499
|
contextSet(variables, keyOrVar, value);
|
|
@@ -11,7 +11,11 @@ import type {
|
|
|
11
11
|
InterceptEntry,
|
|
12
12
|
InterceptSelectorContext,
|
|
13
13
|
} from "../server/context";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
HandlerContext,
|
|
16
|
+
InternalHandlerContext,
|
|
17
|
+
ResolvedSegment,
|
|
18
|
+
} from "../types";
|
|
15
19
|
import { evaluateRevalidation } from "./revalidation.js";
|
|
16
20
|
import { getRequestContext } from "../server/request-context.js";
|
|
17
21
|
import { executeInterceptMiddleware } from "./middleware.js";
|
|
@@ -20,6 +24,7 @@ import { getGlobalRouteMap } from "../route-map-builder.js";
|
|
|
20
24
|
import { handleHandlerResult } from "./segment-resolution.js";
|
|
21
25
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
22
26
|
import { debugLog } from "./logging.js";
|
|
27
|
+
import { runInsideLoaderScope } from "../server/context.js";
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Check if an intercept's when conditions are satisfied.
|
|
@@ -133,7 +138,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
133
138
|
context.request,
|
|
134
139
|
context.env,
|
|
135
140
|
params,
|
|
136
|
-
context
|
|
141
|
+
(context as InternalHandlerContext<any, TEnv>)._variables,
|
|
137
142
|
requestCtx.res,
|
|
138
143
|
createReverseFunction(getGlobalRouteMap()),
|
|
139
144
|
);
|
|
@@ -207,7 +212,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
207
212
|
loaderIds.push(loader.$$id);
|
|
208
213
|
loaderPromises.push(
|
|
209
214
|
deps.wrapLoaderPromise(
|
|
210
|
-
context.use(loader),
|
|
215
|
+
runInsideLoaderScope(() => context.use(loader)),
|
|
211
216
|
parentEntry,
|
|
212
217
|
segmentId,
|
|
213
218
|
context.pathname,
|
|
@@ -374,7 +379,7 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
374
379
|
loaderIds.push(loader.$$id);
|
|
375
380
|
loaderPromises.push(
|
|
376
381
|
deps.wrapLoaderPromise(
|
|
377
|
-
context.use(loader),
|
|
382
|
+
runInsideLoaderScope(() => context.use(loader)),
|
|
378
383
|
parentEntry,
|
|
379
384
|
segmentId,
|
|
380
385
|
context.pathname,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerRouteMap } from "../route-map-builder.js";
|
|
2
2
|
import { extractStaticPrefix } from "./pattern-matching.js";
|
|
3
3
|
import {
|
|
4
|
-
EntryData,
|
|
4
|
+
type EntryData,
|
|
5
5
|
RSCRouterContext,
|
|
6
6
|
runWithPrefixes,
|
|
7
7
|
getIsolatedLazyParent,
|
|
@@ -125,9 +125,8 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
125
125
|
// Merge captured counters from include() to maintain consistent
|
|
126
126
|
// shortCode indices with sibling entries from pattern extraction
|
|
127
127
|
const lazyCounters: Record<string, number> = {};
|
|
128
|
-
if (lazyContext
|
|
129
|
-
const
|
|
130
|
-
for (const [key, value] of Object.entries(captured)) {
|
|
128
|
+
if (lazyContext?.counters) {
|
|
129
|
+
for (const [key, value] of Object.entries(lazyContext.counters)) {
|
|
131
130
|
lazyCounters[key] = value;
|
|
132
131
|
}
|
|
133
132
|
}
|
|
@@ -141,8 +140,9 @@ export function evaluateLazyEntry<TEnv = any>(
|
|
|
141
140
|
namespace: "lazy",
|
|
142
141
|
parent: getIsolatedLazyParent(lazyContext?.parent as EntryData | null),
|
|
143
142
|
counters: lazyCounters,
|
|
144
|
-
cacheProfiles:
|
|
145
|
-
rootScoped:
|
|
143
|
+
cacheProfiles: lazyContext?.cacheProfiles,
|
|
144
|
+
rootScoped: lazyContext?.rootScoped,
|
|
145
|
+
includeScope: lazyContext?.includeScope,
|
|
146
146
|
},
|
|
147
147
|
() => {
|
|
148
148
|
// Run the lazy patterns handler with the original context prefixes
|
|
@@ -20,10 +20,11 @@ import type {
|
|
|
20
20
|
ErrorInfo,
|
|
21
21
|
} from "../types";
|
|
22
22
|
import type { LoaderRevalidationResult, ActionContext } from "./types";
|
|
23
|
-
import { isHandle, type Handle } from "../handle.js";
|
|
24
|
-
import
|
|
23
|
+
import { isHandle, collectHandleData, type Handle } from "../handle.js";
|
|
24
|
+
import { buildHandleSnapshot } from "../server/handle-store.js";
|
|
25
25
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
26
26
|
import { _getRequestContext } from "../server/request-context.js";
|
|
27
|
+
import { isInsideLoaderScope } from "../server/context.js";
|
|
27
28
|
import { debugLog } from "./logging.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -242,6 +243,16 @@ function createLoaderExecutor<TEnv>(
|
|
|
242
243
|
pendingLoaders.add(loader.$$id);
|
|
243
244
|
|
|
244
245
|
const currentLoaderId = loader.$$id;
|
|
246
|
+
const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
|
|
247
|
+
|
|
248
|
+
// Capture whether this loader is being started from a DSL loader scope
|
|
249
|
+
// (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
|
|
250
|
+
// inside loader scope. This determines whether rendered() is allowed.
|
|
251
|
+
const isDslLoader = isInsideLoaderScope();
|
|
252
|
+
|
|
253
|
+
let renderedResolved = false;
|
|
254
|
+
let renderedPromise: Promise<void> | null = null;
|
|
255
|
+
|
|
245
256
|
// Loader functions are always fresh (never cached), so they get an
|
|
246
257
|
// unguarded get that bypasses non-cacheable read guards. This applies
|
|
247
258
|
// to ALL loaders — DSL and handler-called — because the loader
|
|
@@ -255,17 +266,90 @@ function createLoaderExecutor<TEnv>(
|
|
|
255
266
|
search: (ctx as any).search,
|
|
256
267
|
pathname: ctx.pathname,
|
|
257
268
|
url: ctx.url,
|
|
269
|
+
originalUrl: ctx.originalUrl,
|
|
258
270
|
env: ctx.env,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
271
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
272
|
+
executionContext: ctx.executionContext,
|
|
273
|
+
get: ((keyOrVar: any) =>
|
|
274
|
+
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
275
|
+
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
276
|
+
if (isHandle(item)) {
|
|
277
|
+
if (!renderedResolved) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
|
|
280
|
+
`Handle "${item.$$id}" cannot be read until the render tree has settled.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
284
|
+
if (!reqCtx) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`ctx.use(handle) failed: request context not available.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
|
|
290
|
+
const snapshot =
|
|
291
|
+
reqCtx._renderBarrierHandleSnapshot ??
|
|
292
|
+
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
293
|
+
return collectHandleData(item, snapshot, segmentOrder);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Loader case
|
|
297
|
+
return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
|
|
298
|
+
}) as LoaderContext["use"],
|
|
266
299
|
method: "GET",
|
|
267
300
|
body: undefined,
|
|
268
301
|
reverse: ctx.reverse as LoaderContext["reverse"],
|
|
302
|
+
rendered: (): Promise<void> => {
|
|
303
|
+
// Guard: only DSL loaders may use rendered()
|
|
304
|
+
if (!isDslLoader) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
307
|
+
`Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Guard: reject streaming trees
|
|
312
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
313
|
+
if (reqCtx?._treeHasStreaming) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`ctx.rendered() is not supported when the matched route tree uses loading(). ` +
|
|
316
|
+
`Streaming handlers may not have settled when rendered() resolves. ` +
|
|
317
|
+
`Remove loading() from the route tree or restructure to avoid rendered().`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (renderedPromise) return renderedPromise;
|
|
322
|
+
|
|
323
|
+
if (!reqCtx) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`ctx.rendered() failed: request context not available.`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Bidirectional deadlock check: if a handler already started
|
|
330
|
+
// awaiting this loader, calling rendered() would deadlock.
|
|
331
|
+
if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
|
|
334
|
+
`is already awaiting this loader via ctx.use(). The handler blocks ` +
|
|
335
|
+
`segment resolution, which blocks the barrier, which blocks this loader. ` +
|
|
336
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Register this loader as waiting for the barrier so that
|
|
341
|
+
// setupLoaderAccess can detect deadlocks when a handler
|
|
342
|
+
// tries to await the same loader via ctx.use().
|
|
343
|
+
if (!reqCtx._renderBarrierWaiters) {
|
|
344
|
+
reqCtx._renderBarrierWaiters = new Set();
|
|
345
|
+
}
|
|
346
|
+
reqCtx._renderBarrierWaiters.add(currentLoaderId);
|
|
347
|
+
|
|
348
|
+
renderedPromise = reqCtx._renderBarrier.then(() => {
|
|
349
|
+
renderedResolved = true;
|
|
350
|
+
});
|
|
351
|
+
return renderedPromise;
|
|
352
|
+
},
|
|
269
353
|
};
|
|
270
354
|
|
|
271
355
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
@@ -296,15 +380,22 @@ export function setupLoaderAccess<TEnv>(
|
|
|
296
380
|
ctx: HandlerContext<any, TEnv>,
|
|
297
381
|
loaderPromises: Map<string, Promise<any>>,
|
|
298
382
|
): void {
|
|
299
|
-
// Eagerly capture the HandleStore at setup time
|
|
300
|
-
// In workerd/Cloudflare, dynamic imports and
|
|
301
|
-
// can disrupt AsyncLocalStorage, causing
|
|
302
|
-
// undefined when handlers later call
|
|
303
|
-
//
|
|
304
|
-
const
|
|
383
|
+
// Eagerly capture the request context and HandleStore at setup time
|
|
384
|
+
// (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
|
|
385
|
+
// fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
|
|
386
|
+
// getRequestContext() to return undefined when handlers later call
|
|
387
|
+
// ctx.use(handle). Capturing early ensures references survive ALS disruption.
|
|
388
|
+
const reqCtxRef = _getRequestContext();
|
|
389
|
+
const handleStoreRef = reqCtxRef?._handleStore;
|
|
305
390
|
|
|
306
391
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
307
392
|
|
|
393
|
+
// Track whether we're inside a handle push callback. Loaders started
|
|
394
|
+
// from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
|
|
395
|
+
// block segment resolution, so they must not be registered as handler
|
|
396
|
+
// dependencies for deadlock detection.
|
|
397
|
+
let insideHandlePush = false;
|
|
398
|
+
|
|
308
399
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
309
400
|
if (isHandle(item)) {
|
|
310
401
|
const handle = item;
|
|
@@ -324,16 +415,57 @@ export function setupLoaderAccess<TEnv>(
|
|
|
324
415
|
) => {
|
|
325
416
|
if (!store) return;
|
|
326
417
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
418
|
+
if (typeof dataOrFn === "function") {
|
|
419
|
+
// Mark scope so ctx.use(loader) calls inside the callback
|
|
420
|
+
// are not registered as handler-to-loader deps.
|
|
421
|
+
insideHandlePush = true;
|
|
422
|
+
try {
|
|
423
|
+
const result = (dataOrFn as () => Promise<unknown>)();
|
|
424
|
+
store.push(handle.$$id, segmentId, result);
|
|
425
|
+
} finally {
|
|
426
|
+
insideHandlePush = false;
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
331
430
|
|
|
332
|
-
store.push(handle.$$id, segmentId,
|
|
431
|
+
store.push(handle.$$id, segmentId, dataOrFn);
|
|
333
432
|
};
|
|
334
433
|
}
|
|
335
434
|
|
|
336
|
-
|
|
435
|
+
// Deadlock guard and handler-to-loader dependency tracking.
|
|
436
|
+
// Skip when inside a DSL loader scope (resolveLoaderData also calls
|
|
437
|
+
// ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
|
|
438
|
+
// inside a handle push callback (push callbacks don't block segment
|
|
439
|
+
// resolution so they can't cause rendered() deadlocks).
|
|
440
|
+
const loader = item as LoaderDefinition<any, any>;
|
|
441
|
+
if (!isInsideLoaderScope() && !insideHandlePush) {
|
|
442
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
443
|
+
if (reqCtx) {
|
|
444
|
+
// Direction 1: handler awaits loader that already called rendered()
|
|
445
|
+
if (
|
|
446
|
+
loaderPromises.has(loader.$$id) &&
|
|
447
|
+
reqCtx._renderBarrierWaiters?.has(loader.$$id)
|
|
448
|
+
) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
|
|
451
|
+
`The loader is waiting for segment resolution, but the handler blocks resolution. ` +
|
|
452
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
// Direction 2: track dep so rendered() can detect the deadlock
|
|
456
|
+
// if the loader calls it later. Skip when the barrier has already
|
|
457
|
+
// resolved — no deadlock is possible (rendered() resolves immediately).
|
|
458
|
+
// _renderBarrierSegmentOrder is undefined before resolution, string[]
|
|
459
|
+
// after. This also prevents false positives from handle push callbacks
|
|
460
|
+
// that resume after their first await (post-barrier-resolution).
|
|
461
|
+
if (reqCtx._renderBarrierSegmentOrder === undefined) {
|
|
462
|
+
if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
|
|
463
|
+
reqCtx._handlerLoaderDeps.add(loader.$$id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return useLoader(loader, null);
|
|
337
469
|
}) as typeof ctx.use;
|
|
338
470
|
}
|
|
339
471
|
|