@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { UrlPatterns } from "./pattern-types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What an async `include()` provider may resolve to: a `urls()` value directly,
|
|
5
|
+
* or a module namespace whose `default` export is a `urls()` value (the shape
|
|
6
|
+
* produced by `() => import("./routes")` when the route module does
|
|
7
|
+
* `export default urls(...)`).
|
|
8
|
+
*/
|
|
9
|
+
export type IncludeModule<TEnv = any> =
|
|
10
|
+
| UrlPatterns<TEnv>
|
|
11
|
+
| { default: UrlPatterns<TEnv> };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An async/lazy include provider: a thunk returning a `urls()` value (or a
|
|
15
|
+
* Promise of one). The thunk is stored unevaluated by `include()` and called
|
|
16
|
+
* once, on the first request that matches the prefix — so the route module and
|
|
17
|
+
* its (code-split) subtree are not evaluated at startup.
|
|
18
|
+
*
|
|
19
|
+
* Forward-compatible: `() => import("./routes")` today (async, separate chunk);
|
|
20
|
+
* `() => m.routes` with native `import defer` later (sync, deferred eval).
|
|
21
|
+
*/
|
|
22
|
+
export type IncludeProvider<TEnv = any> = () =>
|
|
23
|
+
| IncludeModule<TEnv>
|
|
24
|
+
| Promise<IncludeModule<TEnv>>;
|
|
25
|
+
|
|
26
|
+
/** True when the include() argument is a provider thunk rather than a value. */
|
|
27
|
+
export function isIncludeProvider(value: unknown): value is IncludeProvider {
|
|
28
|
+
// A `urls()` value is a (branded) object; a provider is a function.
|
|
29
|
+
return typeof value === "function";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A `urls()` value is an object exposing a synchronous `handler()`. */
|
|
33
|
+
function isUrlPatterns(value: unknown): value is UrlPatterns {
|
|
34
|
+
return (
|
|
35
|
+
!!value && typeof (value as { handler?: unknown }).handler === "function"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize an async provider's resolved value to a `UrlPatterns`. Accepts a
|
|
41
|
+
* `urls()` value directly or a module whose `default` export is one.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveIncludeModule<TEnv = any>(
|
|
44
|
+
mod: IncludeModule<TEnv>,
|
|
45
|
+
id?: string,
|
|
46
|
+
): UrlPatterns<TEnv> {
|
|
47
|
+
// Prefer an explicit `default` export (the `export default urls(...)`
|
|
48
|
+
// convention) BEFORE duck-typing the namespace. isUrlPatterns() keys on a
|
|
49
|
+
// `.handler` function, but a routes module can legitimately carry a NAMED
|
|
50
|
+
// `export function handler(...)` alongside its `export default urls(...)`;
|
|
51
|
+
// checking the namespace first would then misidentify the whole module as the
|
|
52
|
+
// urls() value and invoke the user's helper as the DSL handler (the group
|
|
53
|
+
// 404s with a misleading error). A bare `() => urls(...)` provider (no
|
|
54
|
+
// module) has no `default`, so it still resolves via the mod-as-value branch.
|
|
55
|
+
const def = (mod as { default?: unknown })?.default;
|
|
56
|
+
if (isUrlPatterns(def)) return def as UrlPatterns<TEnv>;
|
|
57
|
+
if (isUrlPatterns(mod)) return mod as UrlPatterns<TEnv>;
|
|
58
|
+
// The common failure is a module namespace whose `default` is missing or not a
|
|
59
|
+
// urls() value (e.g. only named exports); `typeof` alone says "object" and
|
|
60
|
+
// hides that, so name the keys present. "provider" (not "async provider") —
|
|
61
|
+
// synchronous providers are supported (see IncludeProvider).
|
|
62
|
+
const got =
|
|
63
|
+
mod && typeof mod === "object"
|
|
64
|
+
? `a module with keys [${Object.keys(mod).join(", ") || "none"}] but no valid \`default\``
|
|
65
|
+
: typeof mod;
|
|
66
|
+
throw new Error(
|
|
67
|
+
`[@rangojs/router] include() provider${id ? ` for "${id}"` : ""} must ` +
|
|
68
|
+
`resolve to a urls() value — either returned directly or as the module's ` +
|
|
69
|
+
`\`default\` export (e.g. \`export default urls(...)\`). Got ${got}.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/urls/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import type {
|
|
|
28
28
|
ParallelUseItem,
|
|
29
29
|
InterceptUseItem,
|
|
30
30
|
LoaderUseItem,
|
|
31
|
-
WhenItem,
|
|
32
31
|
TypedCacheItem,
|
|
33
32
|
TransitionItem,
|
|
34
33
|
TypedTransitionItem,
|
|
@@ -42,7 +41,6 @@ import type {
|
|
|
42
41
|
PassthroughHandlerDefinition,
|
|
43
42
|
} from "../prerender.js";
|
|
44
43
|
import type { StaticHandlerDefinition } from "../static-handler.js";
|
|
45
|
-
import type { InterceptWhenFn } from "../server/context";
|
|
46
44
|
import type {
|
|
47
45
|
ResponseHandler,
|
|
48
46
|
ResponseHandlerContext,
|
|
@@ -154,6 +152,30 @@ export type TextResponsePathFn<TEnv> = <
|
|
|
154
152
|
use?: () => UseItems<ResponseRouteUseItem>,
|
|
155
153
|
) => TypedRouteItem<TName, TPattern, string, TSearch>;
|
|
156
154
|
|
|
155
|
+
/**
|
|
156
|
+
* What an async include() provider resolves to. Route types (`TRoutes`) are
|
|
157
|
+
* inferred from the resolved `urls()` value so `href()` and named routes stay
|
|
158
|
+
* type-safe through a code-split module (`() => import("./routes")`).
|
|
159
|
+
*/
|
|
160
|
+
type IncludeResolved<
|
|
161
|
+
TEnv,
|
|
162
|
+
TRoutes extends Record<string, any>,
|
|
163
|
+
TResponses extends Record<string, unknown>,
|
|
164
|
+
> =
|
|
165
|
+
| UrlPatterns<TEnv, TRoutes, TResponses>
|
|
166
|
+
| { default: UrlPatterns<TEnv, TRoutes, TResponses> };
|
|
167
|
+
|
|
168
|
+
/** include() argument: an eager `urls()` value or an async provider thunk. */
|
|
169
|
+
export type IncludeArg<
|
|
170
|
+
TEnv,
|
|
171
|
+
TRoutes extends Record<string, any>,
|
|
172
|
+
TResponses extends Record<string, unknown>,
|
|
173
|
+
> =
|
|
174
|
+
| UrlPatterns<TEnv, TRoutes, TResponses>
|
|
175
|
+
| (() =>
|
|
176
|
+
| IncludeResolved<TEnv, TRoutes, TResponses>
|
|
177
|
+
| Promise<IncludeResolved<TEnv, TRoutes, TResponses>>);
|
|
178
|
+
|
|
157
179
|
/**
|
|
158
180
|
* Base include function signature.
|
|
159
181
|
*/
|
|
@@ -164,7 +186,7 @@ export type IncludeFn<TEnv> = <
|
|
|
164
186
|
TResponses extends Record<string, unknown> = Record<string, unknown>,
|
|
165
187
|
>(
|
|
166
188
|
prefix: TUrlPrefix,
|
|
167
|
-
patterns:
|
|
189
|
+
patterns: IncludeArg<TEnv, TRoutes, TResponses>,
|
|
168
190
|
options?: IncludeOptions<TNamePrefix>,
|
|
169
191
|
) => TypedIncludeItem<TRoutes, TNamePrefix, TUrlPrefix, TResponses>;
|
|
170
192
|
|
|
@@ -246,9 +268,16 @@ export type PathHelpers<TEnv> = {
|
|
|
246
268
|
* `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
|
|
247
269
|
* merge order is `handler.use` → shared `use` → slot-local `use`, with
|
|
248
270
|
* narrowest scope winning for last-write-wins items like `loading()`.
|
|
271
|
+
*
|
|
272
|
+
* Not generic over the slots record: an inferred type parameter makes the
|
|
273
|
+
* object literal an inference site, which suppresses contextual typing of
|
|
274
|
+
* arrow slot handlers (`(ctx) => ...` was implicit any). Bare handlers infer
|
|
275
|
+
* now; a descriptor's `handler:` arrow still needs an explicit ctx annotation
|
|
276
|
+
* because StaticHandlerDefinition's own `.handler` joins the contextual union
|
|
277
|
+
* (two callables — see parallel-slot-handler-types.test.ts).
|
|
249
278
|
*/
|
|
250
|
-
parallel:
|
|
251
|
-
|
|
279
|
+
parallel: (
|
|
280
|
+
slots: Record<
|
|
252
281
|
`@${string}`,
|
|
253
282
|
| Handler<any, any, TEnv>
|
|
254
283
|
| ReactNode
|
|
@@ -261,8 +290,6 @@ export type PathHelpers<TEnv> = {
|
|
|
261
290
|
use?: () => ParallelUseItem[];
|
|
262
291
|
}
|
|
263
292
|
>,
|
|
264
|
-
>(
|
|
265
|
-
slots: TSlots,
|
|
266
293
|
use?: () => ParallelUseItem[],
|
|
267
294
|
) => ParallelItem;
|
|
268
295
|
|
|
@@ -275,12 +302,18 @@ export type PathHelpers<TEnv> = {
|
|
|
275
302
|
slotName: `@${string}`,
|
|
276
303
|
routeName: string,
|
|
277
304
|
handler: ReactNode | Handler<any, any, TEnv>,
|
|
305
|
+
config?:
|
|
306
|
+
| import("../server/context.js").InterceptConfig<TEnv>
|
|
307
|
+
| (() => InterceptUseItem[]),
|
|
278
308
|
use?: () => InterceptUseItem[],
|
|
279
309
|
) => InterceptItem
|
|
280
310
|
: (
|
|
281
311
|
slotName: `@${string}`,
|
|
282
312
|
routeName: (keyof Rango.GeneratedRouteMap & string) | `.${string}`,
|
|
283
313
|
handler: ReactNode | Handler<any, any, TEnv>,
|
|
314
|
+
config?:
|
|
315
|
+
| import("../server/context.js").InterceptConfig<TEnv>
|
|
316
|
+
| (() => InterceptUseItem[]),
|
|
284
317
|
use?: () => InterceptUseItem[],
|
|
285
318
|
) => InterceptItem;
|
|
286
319
|
|
|
@@ -335,11 +368,6 @@ export type PathHelpers<TEnv> = {
|
|
|
335
368
|
fallback: ReactNode | NotFoundBoundaryHandler,
|
|
336
369
|
) => NotFoundBoundaryItem;
|
|
337
370
|
|
|
338
|
-
/**
|
|
339
|
-
* Define a condition for when an intercept should activate
|
|
340
|
-
*/
|
|
341
|
-
when: (fn: InterceptWhenFn) => WhenItem;
|
|
342
|
-
|
|
343
371
|
/**
|
|
344
372
|
* Define cache configuration for segments
|
|
345
373
|
*/
|
|
@@ -365,6 +393,10 @@ export type PathHelpers<TEnv> = {
|
|
|
365
393
|
* `{ viewTransition: false }` to keep #1 without the router boundary. A view
|
|
366
394
|
* transition cannot fire without a startTransition. See
|
|
367
395
|
* skills/view-transitions for the startTransition x ViewTransition matrix.
|
|
396
|
+
*
|
|
397
|
+
* Pass `when: (ctx) => boolean` to gate the transition per request: it runs
|
|
398
|
+
* server-side after the route handler (can read `ctx.get(...)`), and returning
|
|
399
|
+
* false drops the transition so the navigation streams its loading() skeleton.
|
|
368
400
|
*/
|
|
369
401
|
transition: {
|
|
370
402
|
(): TransitionItem;
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -175,6 +175,11 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
175
175
|
...(resolveResponseType(options)
|
|
176
176
|
? { responseType: resolveResponseType(options) }
|
|
177
177
|
: {}),
|
|
178
|
+
// PPR shell-caching opt-in (document-level). Stored raw; the integrated
|
|
179
|
+
// serve path normalizes it via resolvePprConfig (rsc/shell-serve.ts).
|
|
180
|
+
...(options?.ppr !== undefined && options.ppr !== false
|
|
181
|
+
? { ppr: options.ppr }
|
|
182
|
+
: {}),
|
|
178
183
|
};
|
|
179
184
|
|
|
180
185
|
if (isStaticHandler(handler) && handler.$$id && ctx.namePrefix) {
|
|
@@ -35,12 +35,48 @@ export type LocalOnlyInclude = string & { [LOCAL_ONLY_BRAND]: void };
|
|
|
35
35
|
/**
|
|
36
36
|
* Options for path() function
|
|
37
37
|
*/
|
|
38
|
+
/**
|
|
39
|
+
* Options for the `ppr` path option (PPR shell caching — Axis 2, see
|
|
40
|
+
* docs/design/ppr-shell-resume.md and the /ppr skill). Declaring
|
|
41
|
+
* `ppr: true | PartialPrerenderProps` on a page route opts that DOCUMENT into
|
|
42
|
+
* shell capture: the rendered HTML shell (everything that is not a live hole) is
|
|
43
|
+
* cached and, on a later GET, flushed immediately while fizz resumes only the
|
|
44
|
+
* holes. Serving is integral to the router — there is no middleware to mount;
|
|
45
|
+
* the shell store is the app-level `createRouter({ cache })` store (which must
|
|
46
|
+
* implement the `getShell`/`putShell` family).
|
|
47
|
+
*/
|
|
48
|
+
export interface PartialPrerenderProps {
|
|
49
|
+
/**
|
|
50
|
+
* Shell time-to-live in seconds. Defaults to 300 (`ppr: true` uses the same
|
|
51
|
+
* default).
|
|
52
|
+
*/
|
|
53
|
+
ttl?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Stale-while-revalidate window in seconds: a stale shell is still served
|
|
56
|
+
* while a background recapture refreshes it.
|
|
57
|
+
*/
|
|
58
|
+
swr?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Operational tags attached to the captured shell entry for
|
|
61
|
+
* `updateTag()`/`revalidateTag()`-driven eviction. UNIONED with the tags the
|
|
62
|
+
* capture render auto-collects (the shell's own non-loader request tags).
|
|
63
|
+
*/
|
|
64
|
+
tags?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
export interface PathOptions<
|
|
39
68
|
TName extends string = string,
|
|
40
69
|
TSearch extends SearchSchema = {},
|
|
41
70
|
> {
|
|
42
71
|
/** Route name for href() lookups */
|
|
43
72
|
name?: TName;
|
|
73
|
+
/**
|
|
74
|
+
* PPR shell caching opt-in for this page route (document-level). `true` uses
|
|
75
|
+
* the default policy (ttl 300); an object sets ttl/swr/tags. See
|
|
76
|
+
* {@link PartialPrerenderProps}. Routes without this option are pure axis 1 —
|
|
77
|
+
* no capture, no store reads, no logs.
|
|
78
|
+
*/
|
|
79
|
+
ppr?: boolean | PartialPrerenderProps;
|
|
44
80
|
/** Search param schema for typed query parameters */
|
|
45
81
|
search?: TSearch;
|
|
46
82
|
/** Trailing slash behavior: "never" (redirect /path/ to /path), "always" (redirect /path to /path/), "ignore" (match both) */
|
|
@@ -81,16 +81,26 @@ type ExtractRoutesFromItem<T> =
|
|
|
81
81
|
// When search schema is non-empty, value becomes { path, search } object
|
|
82
82
|
T extends TypedRouteItem<infer TName, infer TPattern, any, infer TSearch>
|
|
83
83
|
? TName extends string
|
|
84
|
-
?
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
? // Widened-name guard (#642): some name-less call forms — notably the
|
|
85
|
+
// 3-arg children-fn overload path(pattern, component, () => [...]) —
|
|
86
|
+
// let TName infer to the bare `string` constraint instead of the
|
|
87
|
+
// UnnamedRoute sentinel, because the children-fn argument structurally
|
|
88
|
+
// satisfies the all-optional PathOptions<TName> union member. Mapping
|
|
89
|
+
// over a bare `string` key would emit `{ [K in string]: TPattern }`, an
|
|
90
|
+
// index signature that poisons the whole sibling map (Rango.Path
|
|
91
|
+
// collapses to never). Treat an unresolved name as unnamed.
|
|
92
|
+
string extends TName
|
|
93
|
+
? {}
|
|
94
|
+
: TName extends UnnamedRoute
|
|
95
|
+
? {} // Exclude unnamed routes from type map
|
|
96
|
+
: {} extends TSearch
|
|
97
|
+
? { [K in TName]: TPattern }
|
|
98
|
+
: {
|
|
99
|
+
[K in TName]: {
|
|
100
|
+
readonly path: TPattern;
|
|
101
|
+
readonly search: TSearch;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
94
104
|
: {}
|
|
95
105
|
: // TypedIncludeItem: extract prefixed routes (both name and URL prefix)
|
|
96
106
|
T extends TypedIncludeItem<
|
|
@@ -128,9 +138,15 @@ type ExtractRoutesFromItems<T extends readonly any[]> = T extends readonly any[]
|
|
|
128
138
|
? UnionToIntersection<
|
|
129
139
|
{ [K in keyof T]: ExtractRoutesFromItem<T[K]> }[number]
|
|
130
140
|
> extends infer R
|
|
131
|
-
?
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
? // Blast-radius guard: never let a single malformed item collapse the
|
|
142
|
+
// whole map. A `never` intersection satisfies `extends Record<string,any>`
|
|
143
|
+
// (never extends everything), so check it explicitly first and fall back
|
|
144
|
+
// to `{}` rather than propagating `never`. See #642.
|
|
145
|
+
[R] extends [never]
|
|
146
|
+
? {}
|
|
147
|
+
: R extends Record<string, any>
|
|
148
|
+
? R
|
|
149
|
+
: {}
|
|
134
150
|
: {}
|
|
135
151
|
: {};
|
|
136
152
|
|
|
@@ -169,9 +185,15 @@ type PrefixKeys<
|
|
|
169
185
|
type ExtractResponsesFromItem<T> =
|
|
170
186
|
T extends TypedRouteItem<infer TName, any, infer TData>
|
|
171
187
|
? TName extends string
|
|
172
|
-
?
|
|
188
|
+
? // Widened-name guard (#642), parallels ExtractRoutesFromItem. A name-less
|
|
189
|
+
// children-fn path.json(pattern, handler, () => [...]) infers TName as
|
|
190
|
+
// bare `string`; without this the response map picks up an index
|
|
191
|
+
// signature { [K in string]: TData } that wipes named siblings.
|
|
192
|
+
string extends TName
|
|
173
193
|
? {}
|
|
174
|
-
:
|
|
194
|
+
: TName extends UnnamedRoute
|
|
195
|
+
? {}
|
|
196
|
+
: { [K in TName]: TData }
|
|
175
197
|
: {}
|
|
176
198
|
: T extends TypedIncludeItem<any, infer TNamePrefix, any, infer TResponses>
|
|
177
199
|
? TNamePrefix extends LocalOnlyInclude
|
|
@@ -206,9 +228,12 @@ type ExtractResponsesFromItems<T extends readonly any[]> =
|
|
|
206
228
|
? UnionToIntersection<
|
|
207
229
|
{ [K in keyof T]: ExtractResponsesFromItem<T[K]> }[number]
|
|
208
230
|
> extends infer R
|
|
209
|
-
?
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
? // Blast-radius guard (parallels ExtractRoutesFromItems). See #642.
|
|
232
|
+
[R] extends [never]
|
|
233
|
+
? {}
|
|
234
|
+
: R extends Record<string, unknown>
|
|
235
|
+
? R
|
|
236
|
+
: {}
|
|
212
237
|
: {}
|
|
213
238
|
: {};
|
|
214
239
|
|
|
@@ -58,7 +58,6 @@ export function urls<
|
|
|
58
58
|
loading: baseHelpers.loading,
|
|
59
59
|
errorBoundary: baseHelpers.errorBoundary,
|
|
60
60
|
notFoundBoundary: baseHelpers.notFoundBoundary,
|
|
61
|
-
when: baseHelpers.when,
|
|
62
61
|
cache: baseHelpers.cache as PathHelpers<TEnv>["cache"],
|
|
63
62
|
transition: baseHelpers.transition as PathHelpers<TEnv>["transition"],
|
|
64
63
|
};
|
package/src/vercel/tracing.ts
CHANGED
|
@@ -44,15 +44,15 @@ import {
|
|
|
44
44
|
} from "../router/telemetry-otel.js";
|
|
45
45
|
import type {
|
|
46
46
|
RouterTracingConfig,
|
|
47
|
-
|
|
47
|
+
TracingToggleOptions,
|
|
48
48
|
} from "../router/tracing.js";
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Options for createVercelTracing. Extends the shared
|
|
52
|
+
* {@link TracingToggleOptions} (`enabled` + per-phase `spans`) with the
|
|
53
|
+
* Vercel-specific tracer selectors.
|
|
54
|
+
*/
|
|
55
|
+
export interface VercelTracingOptions extends TracingToggleOptions {
|
|
56
56
|
/**
|
|
57
57
|
* OTel instrumentation-scope name passed to `trace.getTracer()`. Defaults to
|
|
58
58
|
* `"rango"`. Ignored when `tracer` is provided.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev prerender result cache — memoizes /__rsc_prerender responses between
|
|
3
|
+
* HMR edits (issue #654).
|
|
4
|
+
*
|
|
5
|
+
* Freshness invariant (why identity keying is correct): the endpoint
|
|
6
|
+
* re-imports the user's entry module through a Vite module runner on every
|
|
7
|
+
* request. When ANY module in the entry → router → urls → handler chain was
|
|
8
|
+
* invalidated (file edit propagates up the importer graph), that import
|
|
9
|
+
* re-executes the entry, `createRouter()` re-runs, and `RouterRegistry.set`
|
|
10
|
+
* (router.ts) replaces the router instance with a NEW object. When nothing
|
|
11
|
+
* was invalidated, the import is a module-cache hit and the instance is the
|
|
12
|
+
* SAME object. Router-instance identity therefore IS the HMR generation:
|
|
13
|
+
* keying the cache on it gives "serve cached until an edit invalidates the
|
|
14
|
+
* chain" without any watcher bookkeeping, and the WeakMap lets every stale
|
|
15
|
+
* generation's bucket be garbage-collected the moment the registry drops
|
|
16
|
+
* the old instance.
|
|
17
|
+
*
|
|
18
|
+
* Staleness envelope: a cached body can only go stale through NON-module
|
|
19
|
+
* inputs (a getParams() reading a database, fs content outside the module
|
|
20
|
+
* graph). Production freezes those at build time, so serving them frozen
|
|
21
|
+
* between edits makes dev MORE faithful to production, not less — the
|
|
22
|
+
* pre-#654 behavior of re-rendering per request was the anomaly.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { SerializedSegmentData } from "../../cache/types.js";
|
|
26
|
+
|
|
27
|
+
/** Structural slice of `matchForPrerender`'s result the payload derivation needs. */
|
|
28
|
+
export interface DevPrerenderMatchPayloadSource {
|
|
29
|
+
segments: SerializedSegmentData[];
|
|
30
|
+
handles: string;
|
|
31
|
+
interceptSegments?: SerializedSegmentData[];
|
|
32
|
+
interceptHandles?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Serialized response bodies for the two endpoint variants of one render. */
|
|
36
|
+
export interface DevPrerenderPayloadBodies {
|
|
37
|
+
/** `intercept != 1` response: the route's own segments + handles. */
|
|
38
|
+
main: string;
|
|
39
|
+
/**
|
|
40
|
+
* `intercept=1` response: main + intercept segments with the MERGED handle
|
|
41
|
+
* string. Falls back to `main` byte-for-byte when the route has no
|
|
42
|
+
* intercept segments — mirroring the endpoint's historical behavior.
|
|
43
|
+
*/
|
|
44
|
+
intercept: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DevPrerenderCache {
|
|
48
|
+
get(router: object, key: string): string | undefined;
|
|
49
|
+
set(router: object, key: string, body: string): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Derive both variant bodies from one matchForPrerender result, so a single
|
|
54
|
+
* render warms the main AND intercept cache keys (matchForPrerender computes
|
|
55
|
+
* intercept segments unconditionally either way — see prerender-match.ts
|
|
56
|
+
* step 14).
|
|
57
|
+
*/
|
|
58
|
+
export function payloadBodiesFromResult(
|
|
59
|
+
result: DevPrerenderMatchPayloadSource,
|
|
60
|
+
): DevPrerenderPayloadBodies {
|
|
61
|
+
const main = JSON.stringify({
|
|
62
|
+
segments: result.segments,
|
|
63
|
+
handles: result.handles,
|
|
64
|
+
});
|
|
65
|
+
const intercept = result.interceptSegments?.length
|
|
66
|
+
? JSON.stringify({
|
|
67
|
+
segments: [...result.segments, ...result.interceptSegments],
|
|
68
|
+
handles: result.interceptHandles ?? "",
|
|
69
|
+
})
|
|
70
|
+
: main;
|
|
71
|
+
return { main, intercept };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cache key for one endpoint request shape. `pathname` and `routeName` come
|
|
76
|
+
* straight from the query; `passthrough` selects the getParams-checked match
|
|
77
|
+
* path; `intercept` selects which variant body is served. All four dimensions
|
|
78
|
+
* change the response, so all four are in the key.
|
|
79
|
+
*/
|
|
80
|
+
export function devPrerenderCacheKey(
|
|
81
|
+
pathname: string,
|
|
82
|
+
opts: { intercept: boolean; passthrough: boolean; routeName: string | null },
|
|
83
|
+
): string {
|
|
84
|
+
return `${pathname}|i=${opts.intercept ? 1 : 0}|p=${opts.passthrough ? 1 : 0}|r=${opts.routeName ?? ""}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per-router FIFO cap: a dev session that visits an unbounded set of
|
|
89
|
+
* prerendered pathnames (e.g. crawling param routes) must not grow one
|
|
90
|
+
* bucket without limit. 500 rendered payloads is far beyond any interactive
|
|
91
|
+
* session; eviction order does not matter for correctness (a miss just
|
|
92
|
+
* re-renders).
|
|
93
|
+
*/
|
|
94
|
+
const MAX_ENTRIES_PER_ROUTER = 500;
|
|
95
|
+
|
|
96
|
+
export function createDevPrerenderCache(
|
|
97
|
+
maxEntriesPerRouter: number = MAX_ENTRIES_PER_ROUTER,
|
|
98
|
+
): DevPrerenderCache {
|
|
99
|
+
const buckets = new WeakMap<object, Map<string, string>>();
|
|
100
|
+
return {
|
|
101
|
+
get(router: object, key: string): string | undefined {
|
|
102
|
+
return buckets.get(router)?.get(key);
|
|
103
|
+
},
|
|
104
|
+
set(router: object, key: string, body: string): void {
|
|
105
|
+
let bucket = buckets.get(router);
|
|
106
|
+
if (!bucket) {
|
|
107
|
+
bucket = new Map();
|
|
108
|
+
buckets.set(router, bucket);
|
|
109
|
+
}
|
|
110
|
+
if (!bucket.has(key) && bucket.size >= maxEntriesPerRouter) {
|
|
111
|
+
const oldest = bucket.keys().next().value;
|
|
112
|
+
if (oldest !== undefined) bucket.delete(oldest);
|
|
113
|
+
}
|
|
114
|
+
bucket.set(key, body);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -192,3 +192,64 @@ export class DiscoveryError extends Error {
|
|
|
192
192
|
Object.setPrototypeOf(this, DiscoveryError.prototype);
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
/** How the dev caller should surface a discovery failure. */
|
|
197
|
+
export interface DiscoveryFailureReport {
|
|
198
|
+
level: "error" | "warn";
|
|
199
|
+
message: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Decide how to surface a dev-boot discovery failure in the terminal.
|
|
204
|
+
*
|
|
205
|
+
* The bare "no routers found" case (a DiscoveryError with no caught host-handler
|
|
206
|
+
* failures) is ambiguous. It is either:
|
|
207
|
+
* - a genuine misconfiguration — the entry never calls createRouter(), or the
|
|
208
|
+
* configured entry path is wrong; or
|
|
209
|
+
* - a transient artifact of a Vite dependency re-optimization racing with boot
|
|
210
|
+
* discovery (a module read before the entry import resolves to the
|
|
211
|
+
* pre-optimize copy of the runner graph while createRouter() populated the
|
|
212
|
+
* post-optimize copy — see router-discovery.ts).
|
|
213
|
+
*
|
|
214
|
+
* We can only tell them apart when the caller observed the dep optimizer's
|
|
215
|
+
* `browserHash` change across the discovery attempt (`reoptimizeObserved`): a
|
|
216
|
+
* reload-causing re-optimization landed mid-flight, so the empty read was
|
|
217
|
+
* transient and the app self-heals per-request (handler.ts builds the trie from
|
|
218
|
+
* the router's live urlpatterns) with discovery re-running on the next boot.
|
|
219
|
+
*
|
|
220
|
+
* A DiscoveryError that DOES carry caught host-handler failures already embeds
|
|
221
|
+
* the real cause in its message, and any non-DiscoveryError is a hard failure;
|
|
222
|
+
* both stay loud with full detail.
|
|
223
|
+
*/
|
|
224
|
+
export function describeDiscoveryFailure(
|
|
225
|
+
err: unknown,
|
|
226
|
+
opts: { reoptimizeObserved?: boolean } = {},
|
|
227
|
+
): DiscoveryFailureReport {
|
|
228
|
+
if (err instanceof DiscoveryError && err.caught.length === 0) {
|
|
229
|
+
const entry = err.entryPath ?? "the router entry";
|
|
230
|
+
if (opts.reoptimizeObserved) {
|
|
231
|
+
return {
|
|
232
|
+
level: "warn",
|
|
233
|
+
message:
|
|
234
|
+
`[rango] No routers found while Vite was re-optimizing dependencies on ` +
|
|
235
|
+
`dev boot. This is transient: routes are served per-request and ` +
|
|
236
|
+
`discovery re-runs automatically, so it clears on the next boot. If ` +
|
|
237
|
+
`routes still 404, confirm ${entry} calls createRouter().`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
level: "error",
|
|
242
|
+
message:
|
|
243
|
+
`${err.message}\n` +
|
|
244
|
+
` Ensure ${entry} calls createRouter() at module top level and that the ` +
|
|
245
|
+
`configured router entry path is correct.`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const e = err as { stack?: string; message?: string };
|
|
250
|
+
const detail = e?.stack ?? e?.message ?? String(err);
|
|
251
|
+
return {
|
|
252
|
+
level: "error",
|
|
253
|
+
message: `[rango] Router discovery failed: ${detail}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
package/src/vite/index.ts
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
export { rango } from "./rango.js";
|
|
10
10
|
export { poke } from "./plugins/refresh-cmd.js";
|
|
11
|
+
// The built-in clientChunks strategy, exported so a custom `clientChunks`
|
|
12
|
+
// function can OVERLAY it (route a few modules to a dedicated chunk, delegate
|
|
13
|
+
// the rest) instead of replacing the whole route/marker grouping. Without this
|
|
14
|
+
// a consumer override silently loses app-fallback/route splitting for the
|
|
15
|
+
// entire app. Note: called without a ClientChunkContext the fallbackRefs-based
|
|
16
|
+
// `app-fallback` split is inactive — discovery wires it only for the built-in.
|
|
17
|
+
export { directoryClientChunks } from "./utils/client-chunks.js";
|
|
11
18
|
|
|
12
19
|
export type {
|
|
13
20
|
RangoNodeOptions,
|